Implement strict document composition

This commit is contained in:
2026-09-06 20:26:57 +00:00
parent a0cd0977ee
commit 3257b2124c
8 changed files with 438 additions and 21 deletions

View File

@@ -150,6 +150,36 @@ zero in returned detached values. Applications retain responsibility for domain
rules and runtime initialization; validators should not start clients or depend on
live services as a substitute for configuration validation.
## Strict document composition
ConfigKit's private document layer can strictly compose parsed base documents. The
operation is available for file-graph integration but is not yet reachable through
the public single-source `Load` lifecycle. Application defaults do not participate
in this operation.
| Existing node | Incoming node | Result |
| --- | --- | --- |
| Absent | Absent | Absent |
| Absent | Present | Retain the incoming node |
| Present | Absent | Retain the existing node |
| Mapping | Mapping | Recurse by mapping key |
| Any other pair of present nodes | Any other pair of present nodes | `compose.conflict` |
Every non-mapping overlap is a conflict, even when values are equal. Nulls and
sequences are present atomic values, so they also conflict when repeated; sequences
are never joined by index. Disjoint entries under shared mappings compose, making
stable keyed mappings the natural representation for independently contributed
entries.
An empty mapping contributes no child definitions. Two empty mappings compose, and
an empty mapping neither clears nor conflicts with another mapping's children.
Shared mapping containers retain all contributing value locations as their composite
base origin. A conflict reports the first claim as its primary location and every
remaining claim as related locations, in deterministic source and declaration
traversal order. Traversal order never authorizes replacement. Composition clones
the resulting tree and its ordered claim records rather than mutating or aliasing an
input document.
## YAML documents
The private YAML boundary currently accepts exactly one nonempty document whose root

View File

@@ -79,6 +79,15 @@ Assigned to document composition:
ConfigKit semantic path model; both precise locations and composite origins are
retained. Default values never participate in strict composition.
Implementation record (2026-09-06): `composeDocuments`, its pre-order claim
collection, conflict construction, and recursive mapping join in `compose.go` adapt
the assigned production symbols. `TestComposeDocumentsJoinsDisjointMappingsAndTracksClaims`,
`TestComposeDocumentsRejectsEveryPresentNonMappingOverlap`, and
`TestComposeDocumentsReportsAllClaimantsAtExactLocations` adapt the assigned cases.
The planned empty-mapping difference is covered separately: empty mappings retain
container provenance but contribute no child claims. No unplanned semantic deviation
from the extracted additive composer was required.
### Imports
Assigned to explicit import traversal and glob handling:

View File

@@ -10,6 +10,9 @@ module and package documentation boundary. Its implemented public primitives are
- a parser-independent ordered tree in `internal/document`;
- the sole upstream YAML integration in `internal/yaml`, which translates validated
input into that owned tree and exposes no parser types outside its boundary;
- a private strict composer in `compose.go` that joins only disjoint document-tree
definitions, retains ordered claims and composite mapping origins, reports every
conflicting location, and never aliases an input tree;
- a per-operation private reflection descriptor graph in `schema.go` for supported
types, YAML field binding, inlining, secret tags, recursion, and semantic paths;
- descriptor-driven canonical projection, detached typed cloning, cycle/limit

View File

@@ -802,7 +802,27 @@ Acceptance:
## Stage 10 — Implement strict document composition
Status: pending.
Status: complete.
Completion evidence (2026-09-06):
- Adapted Narratio's additive claim validation and recursive mapping join into the
private `composeDocuments` boundary over ConfigKit-owned document trees. Present
equal or unequal scalars, nulls, sequences, mapping/value kind changes, and nested
duplicate entries return `compose.conflict`; mapping pairs alone recurse.
- Changed the extracted empty-map behavior to ConfigKit's operation table: empty
mappings add no children, compose with other mappings, and retain their container
location in composite base origins. Application defaults never enter the composer.
- Preserved source/declaration traversal in detached ordered claim records and used
it only for assembled order and exact primary/related diagnostics. Conflicts across
three or more inputs report every claimant, while shared containers aggregate all
contributing locations instead of selecting an arbitrary owner.
- Cloned every retained branch and verified mutations of a composed result cannot
affect any input. Focused tests cover disjoint keyed joins, operation categories,
ancestor and nested conflicts, equal values, nulls, sequences, empty mappings,
exact locations, all claimants, invalid boundaries, and nonmutation. The
[current composition contract](../config.md#strict-document-composition) owns the
implemented semantics; public import wiring remains deferred to subsequent work.
Read: feature [composition table](initial.md#composition-and-overlay).

View File

@@ -450,28 +450,24 @@ origin is a resolution operation, with input origins retained separately.
## Composition and overlay
Composition combines the main file and explicitly imported fragments as peers.
Repeated semantic definitions are errors, even when their values are equal.
Both conflicting locations must be reported. Prefer mappings keyed by stable
identifiers when entries need to be contributed independently.
Strict base composition is implemented and now owned by the
[current configuration contract](../config.md#strict-document-composition).
Intentional profile overlay remains target behavior. Its exhaustive operation table
is below; “value” includes scalar, sequence, mapping, and null, while absent means no
node rather than null.
The following table is exhaustive by node category. “Value” includes scalar,
sequence, mapping, and null. Absent means no node, not null.
| Existing/lower node | Incoming/higher node | Overlay |
| --- | --- | --- |
| Absent | Absent | Absent |
| Absent | Value | Incoming |
| Value | Absent | Existing |
| Mapping | Mapping | Recurse by key |
| Any other pair of present nodes | Any other pair of present nodes | Replace entire existing node |
| Existing/lower node | Incoming/higher node | Strict composition | Overlay |
| --- | --- | --- | --- |
| Absent | Absent | Absent | Absent |
| Absent | Value | Incoming | Incoming |
| Value | Absent | Existing | Existing |
| Mapping | Mapping | Recurse by key | Recurse by key |
| Any other pair of present nodes | Any other pair of present nodes | Collision | Replace entire existing node |
Two empty mappings compose successfully. An empty mapping contributes no child
changes to an existing mapping; it does not clear that mapping. A mapping can
replace a lower scalar, sequence, or null during overlay, subject to the final
schema. Sequences are atomic: two definitions collide in composition and the
higher sequence replaces the entire lower sequence in an overlay. `[]` clears a
sequence. There is no append, union, index merge, or mapping-clear operator.
A mapping can replace a lower scalar, sequence, or null during overlay, subject to
the final schema. Sequences are atomic and the higher sequence replaces the entire
lower sequence in an overlay. `[]` clears a sequence. There is no append, union,
index merge, or mapping-clear operator.
Replacing a parent must not leave effective metadata for removed children.
Sequence histories describe whole-sequence replacement; index origins refer to