From c2b3cb32daf2c82a0cb60857441988468fef397a Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 3 Nov 2026 01:01:25 +0000 Subject: [PATCH] Document field-level resolution and matched sequence history --- docs/config.md | 31 +++++++++++++++++++++++----- docs/development.md | 2 ++ docs/internal/overview.md | 3 +++ docs/policy/architecture.md | 5 +++++ docs/roadmap/implementation.md | 4 ++++ docs/roadmap/resolution-sequences.md | 26 +++++++++++------------ inspection.go | 1 + lifecycle.go | 3 ++- metadata.go | 2 ++ sequence_identity.go | 3 ++- 10 files changed, 59 insertions(+), 21 deletions(-) diff --git a/docs/config.md b/docs/config.md index 07b8866..30e055a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -772,6 +772,15 @@ in history. Across tiers, history follows the selected whole-record upsert. Incoming contributions use their assigned positions, and replaced payload remains available as historical evidence. +During resolution, eligible application-registered records are matched between +entry and final snapshots by identity at the same collection destination. +Queries at a surviving final position follow that record's entry history, while +collection queries retain physical ordering and removed evidence. For example, +if `[reports, invoices]` becomes `[invoices]`, item 0 follows invoices' earlier +position, the collection retains reports' removal, and the absent old item 1 +remains available through historical-position queries. An added identity does +not inherit the old occupant's history. Source locations stay source-local. + A mapping assembled from the selected file and imports has one composite base origin whose locations follow [scoped assembly order](#explicit-imports), not source-record acquisition order. A mapping also visited by an overlay has an overlay-layer @@ -1198,6 +1207,18 @@ records, helper targets, and unrecorded mutations fail the load. Equal-valued derived assignments may still record application intent even when there is no structural change. +Resolution compares equal-length ordinary lists recursively by position, so an +edit to `/resources/0/path` can be recorded at that field. A length change needs +a list or ancestor record. For application-registered collections, valid unique +entry and final identities match records at the same destination; changing order +or membership still requires list or ancestor coverage. Invalid or duplicate +endpoint identities make a changed list atomic for auditing, without an early +identity error; final registration validation remains at its normal boundary. +YAML-only declarations do not provide this correspondence. Records and helper +targets use final positions, and helpers never follow a later move automatically. +Fixed entry queries continue to use entry positions. See +[ADR 0021](adr/0021-sequence-resolution-correspondence.md) for the decision. + `Resolution.Error` and `Resolution.Warning` attach exact or nearest available input locations. Reported errors fail the load; warnings survive on a successful result. A returned resolver error adds one generic `resolve.hook_failed` error only when the @@ -1356,7 +1377,7 @@ Incompatible overlaps and invalid registrations produce `options.invalid` before source acquisition. Unused and root registrations are allowed and have no effect. Registrations provide identity for strict composition, sequence operations, -inheritance lookup, and final validation. Dynamic selectors apply at concrete +inheritance lookup, resolution correspondence, and final validation. Dynamic selectors apply at concrete strict-composition, inheritance, and final-result destinations without requiring the application to enumerate mapping keys. Each strict tier has independent duplicate-identity state among its peers; layered precedence and inheritance @@ -2056,10 +2077,10 @@ rebuilt by unmarshaling their canonical text into fresh values, detaching privat backing storage. Ignored and unexported struct fields are zeroed rather than copied. Private structural comparison operates on canonical trees. Mapping additions, -removals, and descendant replacements retain semantic paths. A sequence is atomic: -any sequence change reports the sequence path and never invents correspondence -between old and new indices. Cross-kind and null/container changes replace the -affected path. +removals, and descendant replacements retain semantic paths. The shared +comparison used by effective diff treats sequences atomically. Resolution uses +its separate positional and registered-identity comparison described above. +Cross-kind and null/container changes replace the affected path. These mechanisms support decoding, resolution auditing, metadata, and rendering. They are not a public generic conversion or cloning API. diff --git a/docs/development.md b/docs/development.md index 1132fb5..9eb3e5e 100644 --- a/docs/development.md +++ b/docs/development.md @@ -65,6 +65,7 @@ Accepted decisions: - [Apply CLI overrides after inheritance and decoding](adr/0018-cli-after-structural-resolution.md) - [Keep main-file discovery explicit and composable](adr/0019-explicit-configuration-discovery.md) - [Scope configuration controls to document roots](adr/0020-document-root-controls.md) +- [Match registered records during resolution inspection](adr/0021-sequence-resolution-correspondence.md) For cross-cutting changes, follow every applicable row. Completed implementation and remediation plans are preserved in Git history at `cb5b435`; use current @@ -285,6 +286,7 @@ code changes. Use the component overview to find the corresponding implementatio | Shared source-version modes and evidence | [Engine source-version tests](../internal/engine/source_version_test.go), [public facade tests](../facade_source_version_mode_test.go), and [consumer example tests](../examples/consumers/layered_test.go) | `go test ./internal/engine -run 'SourceVersion'`, `go test . -run 'SourceVersion'`, and `go test ./examples/consumers` | | Schema, defaults, projection, cloning | [Implementation flow](internal/overview.md#implementation-flow), [schema](../internal/engine/schema_test.go), [decoding](../internal/engine/decode_test.go), [projection and typed cloning](../internal/engine/projection_test.go) | `go test ./internal/engine -run 'Schema\|Decode\|Projection\|Clone\|Inline\|CompareDocument'` | | Lifecycle, resolution, validation, callback ownership | [Loading](../internal/engine/load_test.go), [resolution](../internal/engine/resolution_test.go), [validation](../internal/engine/validation_test.go), [ownership](../internal/engine/callback_ownership_test.go), [independence](../internal/engine/independence_test.go) | `go test ./internal/engine -run 'Load\|Resolution\|Validation\|Validator\|Hook\|Callback\|Parallel'` | +| Resolution sequence matching and inspection | [Correspondence](../internal/engine/resolution_correspondence_test.go), [auditing and history](../internal/engine/resolution_compare_test.go), [public integration](../facade_resolution_sequences_test.go) | `go test ./internal/engine -run 'Resolution\|History\|Explain\|Sensitivity'` and `go test . -run 'Resolution\|SequenceIdentity\|Diff'` | | Paths, metadata, provenance | [Paths](../internal/engine/path_test.go), [metadata](../internal/engine/metadata_test.go), [provenance](../internal/engine/provenance_test.go) | `go test ./internal/engine -run 'Path\|Metadata\|Provenance\|Queries'` | | Passive resource contexts | [Engine contexts](../internal/engine/resource_context_test.go), [public contexts](../facade_resource_context_test.go) | `go test ./internal/engine -run 'Resource'` and `go test . -run 'SourceContext'` | | Secrets, bootstrap, redaction, explanation | [References](../internal/engine/secret_reference_test.go), [bootstrap](../internal/engine/secrets_test.go), [rendering](../internal/engine/render_test.go), [explanation](../internal/engine/explain_test.go) | `go test ./internal/engine -run 'Secret\|Bootstrap\|Effective\|Explain\|Explanation\|History\|Sensitivity'` | diff --git a/docs/internal/overview.md b/docs/internal/overview.md index 4d35b9a..9f036a8 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -161,6 +161,9 @@ before application resolution begins. The callback retains its separate fixed entry view; it does not read a builder that continues to change. After successful resolution auditing, [`resolveLoadInput`](../../internal/engine/load_result.go) adds the original resolution records and their net replay groups. At the existing +resolution boundary, [`resolution_correspondence.go`](../../internal/engine/resolution_correspondence.go) +matches eligible registered records and the shared history retains their paired +coordinates for selection and safe replay. At the existing pre-secret admission boundary, [`prepareInspectionHistory`](../../internal/engine/load_result.go) freezes that builder and counts the retained document roots before secret lookup and validation. diff --git a/docs/policy/architecture.md b/docs/policy/architecture.md index 5acc23c..0bca337 100644 --- a/docs/policy/architecture.md +++ b/docs/policy/architecture.md @@ -376,6 +376,11 @@ it does not cover every intermediate callback state or later caller mutations. YAML-only declarations carry no such final guarantee through defaults, CLI assignments, or application resolution. +For resolution auditing and inspection, eligible registered collections match +entry and final records by identity within each destination. Structural changes +still require container coverage, and this correspondence does not change source +composition or move final validation earlier. Public diff remains sequence-atomic. + Preserve source attribution, presence, history, sensitivity, and resource context as records survive, move, or are replaced. Identity diagnostics use paths and available origin information without exposing identity values. The guarantee diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 4828ea5..35cf9d8 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,5 +1,7 @@ # Sequence resolution and inspection implementation plan +Status: complete. + ## Scope and execution Implement the agreed [feature roadmap](resolution-sequences.md) from baseline @@ -423,6 +425,8 @@ no exported type, selector, or callback lifecycle change is needed. ## Stage 13 — Document the implemented contract and validate the sprint +**Status:** Complete. Focused engine, facade, and consumer checks, `go test -race ./...`, and `./scripts/check.sh` passed with Go 1.27.0 on Linux. + **Scope:** canonical documentation and final integration checks. 1. Update `docs/config.md` resolution, layered metadata, historical explanation, diff --git a/docs/roadmap/resolution-sequences.md b/docs/roadmap/resolution-sequences.md index 6e4055e..b698052 100644 --- a/docs/roadmap/resolution-sequences.md +++ b/docs/roadmap/resolution-sequences.md @@ -1,6 +1,6 @@ # Field-level resolution and identity-aware sequence history -Status: agreed direction; not implemented. This roadmap describes field-level +Status: implemented. This roadmap records field-level resolution auditing and identity-aware inspection of registered collections. The [implementation plan](implementation.md) owns execution order and verification. @@ -17,27 +17,25 @@ inspection. For ordinary lists, position is the only available correspondence; compare positions as addresses without inferring which application object an element represents. -## Current baseline +## Prior baseline -The [resolution contract](../config.md#recorded-application-resolution) audits -the typed configuration before and after `Definition.Resolve`. Mappings are -compared recursively, while the private structural comparator reports any list -change at the whole-list path. A field-level record inside a changed list -therefore fails to cover that list-level delta. Recording the whole list works. +At the planning baseline, the resolution contract audited the typed configuration +before and after `Definition.Resolve`. Mappings were compared recursively, while +the private structural comparator reported any list change at the whole-list +path. A field-level record inside a changed list therefore failed to cover that +list-level delta. Recording the whole list worked. -For example, changing `/resources/0/path` and recording only that path currently -produces an unrecorded-change diagnostic at `/resources`. This is established +For example, changing `/resources/0/path` and recording only that path previously +produced an unrecorded-change diagnostic at `/resources`. That was established behavior rather than a new regression associated with v1.0.0. The comparator also serves other inspection and metadata operations. Making resolution auditing more precise must account for those callers rather than silently changing the public diff contract. -Resolution currently captures before and after values at the same numeric path. -Its metadata refresh also consults entry metadata at that path. Following a -registered record through a move therefore requires more than changing which -deltas pass the coverage audit: the retained evidence must identify the matching -entry and final record. +The prior resolution implementation captured before and after values at the +same numeric path and refreshed metadata at that path. Following a registered +record through a move requires the matching entry and final record evidence. Application registrations already define identities for collections and constrain their final values. YAML-only declarations have a shorter lifetime. The diff --git a/inspection.go b/inspection.go index 7eaa36d..d3c57cd 100644 --- a/inspection.go +++ b/inspection.go @@ -46,6 +46,7 @@ type Explanation struct { // or historical path, hiding sensitive information. History follows processing // order: inheritance appears after source combination and before the transition // to typed values. Unknown and malformed paths return false. +// Surviving registered records use their matched resolution-entry history. func (r *Result[T]) Explain(path Path) (Explanation, bool) { if r == nil { return Explanation{}, false diff --git a/lifecycle.go b/lifecycle.go index 9e83f24..8aea164 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -140,7 +140,8 @@ type Override struct { // Resolution lets a resolver inspect supplied values and record derived defaults // or normalization. Its input queries describe the state at callback entry, -// even after the resolver changes the configuration. Use the context only during +// even after the resolver changes the configuration. Records and helpers use +// final paths; helpers do not follow moved records. Use the context only during // the callback; do not retain it or use it concurrently. type Resolution struct{ value *engine.Resolution } diff --git a/metadata.go b/metadata.go index df70281..fb28158 100644 --- a/metadata.go +++ b/metadata.go @@ -22,6 +22,8 @@ func (m Metadata) IsExplicit(path Path) bool { // History returns detached definition, inheritance, and resolution records for path. A record's // Path can be an ancestor when that container operation replaced or removed path. // Removed historical paths can have history even though Lookup returns false. +// Surviving registered records follow their matched resolution-entry history; +// collection queries retain removed evidence at physical positions. // Unknown and invalid paths return nil. func (m Metadata) History(path Path) []Change { values := m.value.History(toEnginePath(path)) diff --git a/sequence_identity.go b/sequence_identity.go index eafd2c9..eb838a7 100644 --- a/sequence_identity.go +++ b/sequence_identity.go @@ -26,7 +26,8 @@ func SequenceAnyMappingKey() SequenceSelectorSegment { // SequenceIdentity records the identity fields used for strict sequence // composition, identified-record parent lookup, and final typed-result -// validation during loading. +// validation during loading. Eligible entry and final records also correspond +// during resolution auditing and inspection. type SequenceIdentity struct { exactPath bool path Path