Files
configkit/docs/roadmap/resolution-sequences.md

255 lines
13 KiB
Markdown
Raw Normal View History

# Field-level resolution and identity-aware sequence history
Status: agreed direction; not implemented. This roadmap describes field-level
resolution auditing and identity-aware inspection of registered collections.
A staged implementation plan will follow review of the integrated roadmap.
## Purpose
Allow applications to normalize or derive individual fields inside lists and
record those fields directly. Preserve the requirement that every net change
made during resolution has an exact or ancestor record, together with coherent
final values, provenance, history, and sensitivity metadata.
For a collection with an applicable application registration, the declared key
defines a record across positions. Use that correspondence in auditing and
inspection. For ordinary lists, position is the only available correspondence;
compare positions as addresses without inferring which application object an
element represents.
## Current 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.
For example, changing `/resources/0/path` and recording only that path currently
produces an unrecorded-change diagnostic at `/resources`. This is 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.
Application registrations already define identities for collections and constrain
their final values. YAML-only declarations have a shorter lifetime. The
[identity contract](../config.md#application-registrations-and-identity-details)
and [ADR 0017](../adr/0017-collection-sequence-policy.md) own those distinctions.
## Target behavior
### Mappings and ordinary lists
Mapping comparison remains recursive. For lists without applicable application
identity registrations:
- If the lists have equal lengths, compare corresponding positions recursively.
A position is an address, not an assertion of element identity.
- If the length changes, report a structural change at the list path, requiring
a record for that list or an ancestor.
- For equal-length lists, a reorder manifests as changes at the affected
positions. Every detected change needs coverage; a whole-list record remains
a convenient way to cover the operation.
Apply these rules recursively to nested values. Preserve existing treatment of
additions, removals, null, scalar differences, and changes of value kind except
where finer sequence comparison deliberately changes the reported path.
Ordinary-list inspection remains positional; similar field values do not justify
moving provenance or history between positions.
### Lists with registered identity
Use applicable `Definition.SequenceIdentities` registrations to match records
between the entry and final snapshots. Reuse existing identity-field selection,
compound-identity, scalar-equality, and duplicate-detection semantics. Do not infer
an identity from a field merely named `name` or `id`.
| Observed change | Proposed auditing behavior |
| --- | --- |
| An ordinary field changes on a matched record | Compare recursively and allow a record covering that field. |
| Identity order changes | Report a structural change at the list path; require a list or ancestor record. |
| An identity is added or removed | Report a structural change at the list path; require a list or ancestor record. |
| An identity changes | Treat it as removal and addition, without inferring a rename. |
Compare matched records by identity even when their numeric positions differ.
Structural changes still require whole-list coverage. A field record by itself
does not cover an accompanying reorder or membership change.
Identity matching serves both audit coverage and inspection. Although a
whole-list record covers every edit in a moved collection, matching still
identifies which record's earlier values and source evidence belong to each
final record. Stopping at a whole-list delta is insufficient for that purpose.
Match only the resolution-entry and final snapshots. Identity changes are
removal and addition; temporary identities, intermediate moves, and the order of
callback mutations are not reconstructed. Matching does not introduce a new
collection merge operation or change whole-record `upsert`.
Use the existing registration selectors and scope. They traverse mapping keys,
not sequence indexes; recursive comparison of nested lists does not expand the
registration API. YAML-only declarations do not acquire a new final-resolution
guarantee or become identity selectors for this work.
### Matching eligibility
Use identity matching when both snapshots provide valid, unique identities under
the applicable registration. Otherwise use conservative whole-list auditing for
a changed collection, without inventing correspondence. An unchanged collection
does not require a resolution record merely because matching is unavailable.
Preserve existing final identity validation. Resolution may repair invalid entry
values; this feature does not reject them earlier or require valid identities
throughout the callback. When matching is unavailable, the recorded container
operation describes the change without claiming that individual records survived.
### Example
Assume the application registers `name` as the identity for `resources`:
```yaml
resources:
- name: reports
path: ./reports
- name: invoices
path: ./invoices
```
Changing the first path to `/srv/reports` can be covered by a normalization record
at `/resources/0/path`. The unchanged `invoices` record requires no record.
If resolution also swaps the records, the final collection is:
```yaml
resources:
- name: invoices
path: ./invoices
- name: reports
path: /srv/reports
```
Recording only `/resources/1/path` does not cover the reorder. Recording
`/resources` covers that structural operation and any field edits within the
list. A field record can additionally explain the path normalization.
Inspection at the final `reports` position includes that record's earlier
evidence from position 0, with its original source coordinates, and the recorded
collection operation explaining the move. Its previous path is `./reports`,
not the `./invoices` value that previously occupied position 1. The `invoices`
record moved without a field change. An ordinary unregistered list instead
describes changes at positions 0 and 1, without identifying records across them.
### Recording, inspection, and ownership
Resolution records and helper targets for surviving values describe final
positions. Preserve the existing ability to record removed values.
`Resolution.EntryInfo` and `Resolution.IsExplicit` retain their existing fixed
entry-snapshot meaning. Numeric paths and helper targets are not automatically
rebound to an identity when the application later moves an element. A helper
assignment must still agree with the final value at its declared target.
Identity establishes correspondence internally; this feature adds no public
identity-addressed path or mutation-tracking API. An application that restructures
a list can record its container rather than maintain individual helper targets
through moves.
Preserve helper-value consistency checks, reason and dependency recording,
presence distinctions, detached query results, and independent-load ownership.
Fine-grained accepted changes must appear consistently in the returned values,
metadata, history, explanation, and sensitivity propagation. For eligible
registered collections, use the same entry-to-final correspondence for before
values, source attribution, input explicitness, and sensitivity. Do not attach
the previous occupant's evidence to a different record merely because it now
occupies that index. Identity matching alone does not make a newly added record
explicit input or give it an invented source.
A history query at a final record's position includes the matched record's
earlier evidence from its entry position. Preserve original source coordinates
and include the recorded collection operation explaining the move. Recorded
normalization and derivation retain their resolution origins; following prior
evidence does not erase the application operation. Removed evidence remains
available as described below.
Explicit structural records remain the explanation of application-owned list
restructuring. Dependency paths retain their existing recording semantics;
identity matching does not turn supplied paths into selectors that follow later
mutations. Check sensitivity propagation through both the matched evidence and
recorded dependencies, including moves and removed values. The proposal does not
reconstruct the callback's intermediate mutations.
Public effective-configuration diff output remains atomic for lists. Resolution
comparison may need a dedicated internal operation or comparison mode so that
its new behavior does not alter unrelated callers.
### Removed records and reused positions
For eligible registered collections, inspection distinguishes a surviving
record's lineage from evidence of records removed during resolution:
- Queries at surviving positions describe the surviving record's lineage,
including its matched entry evidence and the recorded collection operation.
A removed record that previously occupied that position does not become part
of the survivor's individual history.
- Collection-level history and explanation retain the removal and the removed
record's evidence, including available source attribution and before values.
- Historical positions that no longer exist remain inspectable under the
existing historical-path rules. Their availability does not give a reused
position a second, identity-addressed meaning.
For example, if `[reports, invoices]` becomes `[invoices]`, a query at
`/resources/0` follows `invoices` back to its entry position 1. The removal of
`reports` and its earlier evidence remain inspectable at `/resources`, rather
than being mixed into the individual history of `invoices`. The now-absent
position `/resources/1` remains available under the historical-path contract.
Original source coordinates are preserved, and existing sensitivity rules apply
to all retained evidence.
## Work needed from the baseline
- Trace resolution comparison, record coverage, helper verification, metadata
updates, history replay, and sensitivity handling to identify where each needs
finer sequence changes or explicit before/after correspondence.
- Add positional recursive comparison for ordinary equal-length lists and
identity-aware comparison for eligible registered lists, while retaining
structural coverage for the cases above.
- Reuse collection identity machinery and validate matching eligibility without
strengthening the existing final-only registration guarantee.
- Carry matched entry and final positions into resolution snapshots, metadata,
history, and sensitivity handling through the existing shared history model.
A coverage-only implementation does not complete this feature. Preserve
source coordinates and distinguish a record's lineage from the prior occupant
of its final position.
- Verify field edits, nested lists, zero/false/null values, multiple simultaneous
changes, length changes, reorders, identity changes, helper targets, and
metadata behavior across the public loading boundary. Include cases in which
a field record must not conceal another unrecorded change.
- Exercise moved records with distinct source evidence, explicitness, and
sensitivity; combined reorders and field edits; removed values; repaired entry
identities; and dynamic registration selectors. Check agreement between final
metadata and explanation while retaining entry-query behavior and atomic public
diff output. Verify the removal-and-position-reuse example above: surviving
record queries follow their own lineage, collection queries retain removed
evidence, and absent historical positions remain inspectable.
- Update the resolution contract and relevant API comments when implemented.
Review ADR and policy follow-up for registered identity as the correspondence
used during resolution inspection, as well as the distinction between
resolution comparison and public diff presentation. Preserve accepted ADR
decision bodies.
Use identity lookup tables for expected linear matching work in collection size,
plus recursive value-comparison work. Preserve deterministic output and existing
equality semantics; avoid pairwise searches for matching records. This is a
design objective, not a new public performance guarantee.
No configuration syntax, dependency, release gate, or new mutation-history system
is proposed. Existing callbacks that record a whole list should continue to work.
The matching-eligibility, final-position-addressing, and removed-record inspection
decisions are settled and integrated above.