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 {