From 341c7e04f4934271722bbd0413424a63bd4b4158 Mon Sep 17 00:00:00 2001 From: Conduction Release Bot Date: Mon, 10 Aug 2026 22:18:28 +0200 Subject: [PATCH] fix(spec): four @spec tags named a Scenario as if it were a Requirement, and one named a change dir that never existed gate-46 (spec-anchor-existence) reported 5 unresolved findings from 4 distinct targets on the full-scope dispatch. They are two different mistakes, not one. FOUR of them, all in SbomRegisterShapeTest, are the same typo shape: #requirement-a-successful-import-records-provenance-on-the-version #requirement-existing-versions-are-unaffected-by-the-schema-addition #requirement-a-parsed-component-persists-with-its-moduleversie-relation Each of those strings is a real heading in openspec/specs/sbom-import/spec.md -- but it is a `#### Scenario:` heading, kebab-cased and then prefixed with `requirement-`. The requirement of that name does not exist and never did, so the tag resolved to nothing. The scenario is the more precise target anyway: each of these four test methods verifies exactly one scenario, so the tags now say `#scenario-...` and point at the heading they were always describing. The FIFTH is different in kind. RegisterFragmentMergeTest pointed at openspec/changes/modular-register-manifest-fragments/specs/modular-config/spec.md which is not merely archived -- it is absent from openspec/changes/, from openspec/changes/archive/, and from openspec/specs/ under any capability name, and `modular-config` is not a capability this repo has ever had. The gate resolves change-dir targets through the archive index and the capability index before reporting, so this is a target with no home rather than a stale path. Repointing a tag at a nearby requirement would have made the gate green while leaving the behaviour the test asserts unspecified, so instead the behaviour is now written down where it belongs: REQ-007 in openspec/specs/settings-service, the spec that owns SettingsService, covering the ADR-037 fragment deep-merge contract (disjoint fragments union; lists concatenate; scalars overwrite) with one scenario per test method. It explicitly defers to catalog-ratings for the `authorization` replace-on-merge carve-out rather than restating or overriding it. Both directions, measured with the gate's own checker: before 1 target-file-not-found + 4 anchor-not-found after exit 0, empty findings log gate [gate-46] spec-anchor-existence: FAIL -- 5 ... -> PASS Adding two scenarios did NOT add gate-19 debt: settings-service carries a file-level `@e2e exclude` (PHP backend, no UI surface), and the gate-19 finding list is byte-identical before and after (291 both times, diff empty). No other gate moved: 13/19/25/26 unchanged. phpcs scans lib/ only, so the tests/ docblocks are out of its scope; the spec change is markdown. PHPUnit on the two affected classes: 6/6 pass, 37 assertions. --- openspec/specs/settings-service/spec.md | 22 +++++++++++++++++++ .../Service/RegisterFragmentMergeTest.php | 4 +++- tests/Unit/Service/SbomRegisterShapeTest.php | 8 +++---- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/openspec/specs/settings-service/spec.md b/openspec/specs/settings-service/spec.md index d410e889..f3332084 100644 --- a/openspec/specs/settings-service/spec.md +++ b/openspec/specs/settings-service/spec.md @@ -142,3 +142,25 @@ The service MUST call `importFromApp` from a single call site with the same, sta - AND WHEN the true fix is determined to belong in OpenRegister rather than this app - THEN an issue MUST be filed against the owning repository documenting the mechanism, and referenced from this app's code and docs +### Requirement: The system SHALL merge ADR-037 register fragments so disjoint fragments never collide (REQ-007) + +`SettingsService::deepMergeConfig()` is the single merge primitive that folds every `register.d/*.json` fragment onto the monolith register. Its contract MUST be: nested maps merge key-by-key so that fragments touching *disjoint* keys all survive the merge; list values concatenate; scalar values from the overlay overwrite the base. This is what allows concurrent OpenSpec changes to each ship their own fragment without either one's schemas or paths being lost — the property ADR-037 exists to provide. The one deliberate exception is the `authorization` subtree, which replaces rather than concatenates; that carve-out and its rationale are specified under `catalog-ratings`, and this requirement MUST NOT be read as overriding it. + +#### Scenario: Disjoint fragments union their schemas and paths + +- GIVEN a base register declaring schema `Existing` and path `/existing` +- AND a fragment adding schema `AlphaComponent` and path `/alpha` +- AND a second fragment adding schema `BetaService` and path `/beta` +- WHEN the fragments are merged onto the base in sequence +- THEN all three schemas MUST be present under `components.schemas` +- AND all three paths MUST be present under `paths` +- AND no earlier fragment's contribution may be dropped by a later one + +#### Scenario: Lists concatenate and scalars overwrite + +- GIVEN a base with `required: ["a", "b"]` and `info.version: "0.1.0"` +- AND an overlay with `required: ["c"]` and `info.version: "0.2.0"` +- WHEN the overlay is merged onto the base +- THEN `required` MUST be `["a", "b", "c"]` +- AND `info.version` MUST be `"0.2.0"` + diff --git a/tests/Unit/Service/RegisterFragmentMergeTest.php b/tests/Unit/Service/RegisterFragmentMergeTest.php index 5f9b31bf..c47b9667 100644 --- a/tests/Unit/Service/RegisterFragmentMergeTest.php +++ b/tests/Unit/Service/RegisterFragmentMergeTest.php @@ -12,7 +12,7 @@ * * @link https://conduction.nl * - * @spec openspec/changes/modular-register-manifest-fragments/specs/modular-config/spec.md + * @spec openspec/specs/settings-service/spec.md#requirement-the-system-shall-merge-adr-037-register-fragments-so-disjoint-fragments-never-collide-req-007 */ declare(strict_types=1); @@ -48,6 +48,7 @@ private function merge(array $base, array $overlay): array * Two fragments adding disjoint OpenAPI schemas/paths union by key. * * @return void + * @spec openspec/specs/settings-service/spec.md#scenario-disjoint-fragments-union-their-schemas-and-paths */ public function testDisjointFragmentsUnionSchemasAndPaths(): void { @@ -84,6 +85,7 @@ public function testDisjointFragmentsUnionSchemasAndPaths(): void * List arrays are concatenated; scalars overwrite. * * @return void + * @spec openspec/specs/settings-service/spec.md#scenario-lists-concatenate-and-scalars-overwrite */ public function testListsConcatenateAndScalarsOverwrite(): void { diff --git a/tests/Unit/Service/SbomRegisterShapeTest.php b/tests/Unit/Service/SbomRegisterShapeTest.php index 7d3d6689..d8cffded 100644 --- a/tests/Unit/Service/SbomRegisterShapeTest.php +++ b/tests/Unit/Service/SbomRegisterShapeTest.php @@ -87,7 +87,7 @@ private function schema(string $slug): array * actually writes them to and the status endpoint reads them from. * * @return void - * @spec openspec/specs/sbom-import/spec.md#requirement-a-successful-import-records-provenance-on-the-version + * @spec openspec/specs/sbom-import/spec.md#scenario-a-successful-import-records-provenance-on-the-version */ public function testModuleVersieCarriesSbomProvenance(): void { @@ -117,7 +117,7 @@ public function testModuleVersieCarriesSbomProvenance(): void * that shipped the silent-drop defect. * * @return void - * @spec openspec/specs/sbom-import/spec.md#requirement-a-successful-import-records-provenance-on-the-version + * @spec openspec/specs/sbom-import/spec.md#scenario-a-successful-import-records-provenance-on-the-version */ public function testOrganisatieDoesNotCarrySbomProvenance(): void { @@ -136,7 +136,7 @@ public function testOrganisatieDoesNotCarrySbomProvenance(): void * rows without these fields stays non-destructive. * * @return void - * @spec openspec/specs/sbom-import/spec.md#requirement-existing-versions-are-unaffected-by-the-schema-addition + * @spec openspec/specs/sbom-import/spec.md#scenario-existing-versions-are-unaffected-by-the-schema-addition */ public function testSbomProvenanceIsOptional(): void { @@ -155,7 +155,7 @@ public function testSbomProvenanceIsOptional(): void * voorzieningen register, so imported components have a home table. * * @return void - * @spec openspec/specs/sbom-import/spec.md#requirement-a-parsed-component-persists-with-its-moduleversie-relation + * @spec openspec/specs/sbom-import/spec.md#scenario-a-parsed-component-persists-with-its-moduleversie-relation */ public function testSbomComponentIsRegisteredInVoorzieningen(): void {