Record interface remediation review findings
This commit is contained in:
277
docs/roadmap/remediation.md
Normal file
277
docs/roadmap/remediation.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# Interface Remediation Review Findings
|
||||
|
||||
Reviewed commit: `98c8b7c8423a816acdfceace36165366b13719ee`.
|
||||
Comparison baseline: `0dd16aa8a935645ae2847d32edd86461dc4618f0`
|
||||
(the planning commit, before the implementation sprint).
|
||||
|
||||
Scope: the completed [implementation plan](implementation.md) and
|
||||
[interface remediation roadmap](interface-remediation.md). This report records
|
||||
remaining defects and newly introduced scalability problems; it does not change
|
||||
the selected architecture or public contracts. All six findings below are open.
|
||||
|
||||
## Architecture And Review Evidence
|
||||
|
||||
The public root-owned facade still delegates to the synchronous engine. A
|
||||
per-load history builder now normalizes baseline, source, inheritance, typed
|
||||
checkpoint, CLI, and resolution evidence. Metadata and inspection share its
|
||||
frozen model, with separate public projections and detached query results. The
|
||||
resolution callback retains a separate fixed entry-state view. Retained-root
|
||||
admission remains before effective secret lookup and application validation.
|
||||
The old independent metadata history and published inheritance copies are gone.
|
||||
|
||||
The original presence/explicitness corrections and composition claimant-order
|
||||
correction are present. The shared-history migration is substantial, but the
|
||||
findings below prevent treating its behavioral preservation and indexed-query
|
||||
targets as fully satisfied. No additional public restriction, guarantee, or
|
||||
prohibition requiring reversal was identified in the reviewed changes. In
|
||||
particular, private builder lifecycle guards are not new consumer obligations.
|
||||
|
||||
Review combined code/plan inspection, graph-based producer/consumer tracing,
|
||||
existing tests, and disposable public-API probes run against both revisions.
|
||||
The first three findings were reproduced; the remaining three follow from the
|
||||
new loops and index contents, without performance measurements. Probe programs
|
||||
and the baseline checkout were kept outside the repository. Production code and
|
||||
repository tests were not changed during the review.
|
||||
|
||||
The following checks passed with Go 1.26.10 on Linux, using the installed toolchain
|
||||
and cached dependencies:
|
||||
|
||||
```sh
|
||||
GOTOOLCHAIN=local GOTELEMETRY=off GOPROXY=off go test ./... -count=1
|
||||
GOTOOLCHAIN=local GOTELEMETRY=off GOPROXY=off go test -race ./...
|
||||
GOTOOLCHAIN=local GOTELEMETRY=off GOPROXY=off ./scripts/check.sh
|
||||
git diff --check
|
||||
```
|
||||
|
||||
The passing suite does not detect the reproduced regressions. Suggested coverage
|
||||
below targets those gaps rather than adding a general test matrix or new gates.
|
||||
|
||||
## 1. Final Dynamic Field Tags Can Be Lost From Redaction
|
||||
|
||||
Priority: P1. Status: open.
|
||||
|
||||
Relevant code: `resolveLoadInput` in
|
||||
[load_result.go](../../internal/engine/load_result.go), lines 56–73;
|
||||
`freezeMetadataHistory` in [metadata.go](../../internal/engine/metadata.go),
|
||||
lines 146–155; and `sensitivityCandidates` in
|
||||
[history_select.go](../../internal/engine/history_select.go), lines 201–253.
|
||||
|
||||
Resolution evidence is observed only when resolution produces snapshots. The
|
||||
final projection is assigned to `model.final` but never unconditionally observed
|
||||
for path/sensitivity facts. Classification now consumes model observations,
|
||||
instead of also traversing the final projection as the baseline did.
|
||||
|
||||
Reproduction through the public API:
|
||||
|
||||
```go
|
||||
type classified struct {
|
||||
Token string `yaml:"token" configkit:"secret"`
|
||||
}
|
||||
type configuration struct {
|
||||
Value any `yaml:"value"`
|
||||
}
|
||||
|
||||
result, err := configkit.Load[configuration](
|
||||
configkit.Bytes("input", []byte("value: {token: AUDIT-CANARY}\n")),
|
||||
configkit.Options[configuration]{
|
||||
Definition: configkit.Definition[configuration]{
|
||||
Resolve: func(c *configuration, _ *configkit.Resolution) error {
|
||||
c.Value = classified{Token: "AUDIT-CANARY"}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
The map-to-struct assignment leaves canonical values unchanged, so the existing
|
||||
net-change audit accepts the load without a recorded normalization. The final
|
||||
projection carries the concrete struct's secret tag, but no snapshot adds that
|
||||
classification to the model. `WriteEffective` and `Explain(Keys("value",
|
||||
"token"))` expose the canary, including historical explanation values. At the
|
||||
baseline they redact it and report `Redacted: true`.
|
||||
|
||||
Expected behavior follows the existing
|
||||
[secret-classification contract](../config.md#secret-classification-and-effective-rendering):
|
||||
concrete dynamic tags and final/historical classification remain effective.
|
||||
The plan also expressly requires observing the final projection. This concerns
|
||||
ordinary tagged configuration, not evidence of resolved credential bytes being
|
||||
stored in history.
|
||||
|
||||
Remediation: register final concrete path and sensitivity facts before freezing
|
||||
and classification, independently of whether resolution recorded a value
|
||||
change. Preserve pointer-conservative historical masking and dependency closure.
|
||||
Do not require extra resolver records or reject equal-valued dynamic type
|
||||
changes as a workaround. Add a public lifecycle regression for this case that
|
||||
checks effective and historical redaction.
|
||||
|
||||
## 2. Intermediate Source Definitions Are Duplicated In Metadata History
|
||||
|
||||
Priority: P2. Status: open.
|
||||
|
||||
Relevant code: `observeSourceEvidence` in
|
||||
[history_source.go](../../internal/engine/history_source.go), lines 82–117;
|
||||
and previous-origin construction in `selectHistory` in
|
||||
[history_select.go](../../internal/engine/history_select.go), lines 108–117.
|
||||
|
||||
Each overlay record unconditionally registers its displaced definitions. A
|
||||
definition already represented by the preceding replacement operation is
|
||||
therefore emitted again when the next tier displaces it. Selection chains
|
||||
`Previous` through both records. The baseline's `appendInputOriginEvent`
|
||||
suppressed an already represented origin; the new normalization does not apply
|
||||
that rule to displaced definitions.
|
||||
|
||||
Reproduction: load `map[string]any` with two ordered underlays containing
|
||||
`x: low` and `x: mid`, and main input containing `x: high`. Query
|
||||
`Metadata().History(Keys("x"))`.
|
||||
|
||||
- Expected and baseline: three contributions, low → mid → high.
|
||||
- Actual: four contributions, low → mid → mid → high. The second mid record
|
||||
names the same source location as its own previous origin.
|
||||
|
||||
This is not a genuine additional equal-valued write. It is one source
|
||||
contribution registered twice, contrary to the plan's explicit preservation of
|
||||
origin containment and duplicate suppression.
|
||||
|
||||
Remediation: normalize displaced definitions against already represented
|
||||
semantic operations in the shared model. Preserve genuine independent writes,
|
||||
including equal values, and keep key/index shapes distinct. Add a three-tier
|
||||
history regression checking source chronology and previous-origin links.
|
||||
|
||||
## 3. Container Explanations Take The Last Descendant Replacement's Origin
|
||||
|
||||
Priority: P2. Status: open.
|
||||
|
||||
Relevant code: `historySelectionForApplication` in
|
||||
[explain.go](../../internal/engine/explain.go), lines 183–207; and descendant
|
||||
selection in [history_select.go](../../internal/engine/history_select.go).
|
||||
|
||||
The explanation helper obtains a direct application selection, then overwrites
|
||||
it with every selected replacement linked to that application. For a container
|
||||
query, multiple child replacements can be relevant; the last one supplies the
|
||||
origin for the entire application event, regardless of the query's scope.
|
||||
|
||||
Reproduction: main YAML is:
|
||||
|
||||
```yaml
|
||||
a: base
|
||||
group: {x: X, y: Y}
|
||||
```
|
||||
|
||||
Select an overlay containing:
|
||||
|
||||
```yaml
|
||||
a: overlay
|
||||
group: {x: O}
|
||||
```
|
||||
|
||||
The overlay event in `Explain(Keys("group"))` is attributed to the `O` scalar
|
||||
(line 2, column 12), instead of the contributing mapping (baseline: line 2,
|
||||
column 8). The root explanation also attributes the whole overlay event to
|
||||
that scalar, despite the independent change to `a` (baseline: document root,
|
||||
line 1, column 1). Values are correct; the contribution location is not.
|
||||
|
||||
The roadmap requires retaining source attribution and using common semantic
|
||||
selection, while allowing different record grouping between public projections.
|
||||
It does not authorize replacing enclosing attribution with an arbitrary leaf.
|
||||
|
||||
Remediation: resolve application attribution at the requested semantic scope in
|
||||
the common selection/attribution path. Prefer a valid direct contribution for
|
||||
an enclosing query; retain replacement fallback where it is needed for removed
|
||||
or replaced descendants. Do not restore a second independent explanation
|
||||
attribution system. Cover enclosing root/mapping and precise leaf attribution
|
||||
in one focused overlay regression.
|
||||
|
||||
## 4. History Construction Repeatedly Performs Unindexed Mapping Searches
|
||||
|
||||
Priority: P2. Status: open.
|
||||
|
||||
Relevant code: `observeTypedCheckpoint` in
|
||||
[history_cli.go](../../internal/engine/history_cli.go), lines 38–49;
|
||||
`sourceContributions` in [history_source.go](../../internal/engine/history_source.go),
|
||||
lines 185–218; and the lookup implementation in
|
||||
[document_walk.go](../../internal/engine/document_walk.go).
|
||||
|
||||
For every checkpoint path, construction calls `documentAtPath` on both trees.
|
||||
That helper supplies no lookup index, so each mapping step linearly searches
|
||||
its entries. For a flat mapping with n fields, locating all n children requires
|
||||
Theta(n²) entry comparisons. Both checkpoints run even on ordinary loads with
|
||||
no CLI overrides. Source-summary construction introduces the same pattern:
|
||||
it calls `sourceContributions` for surviving paths, and that helper searches
|
||||
each raw application again without an index.
|
||||
|
||||
These are new repeated sibling scans, not the deliberately retained whole-tree
|
||||
explanation replay. Existing document lookup machinery or paired traversal can
|
||||
avoid them. Existing sorting and genuinely repeated producer evidence still
|
||||
have their own costs; no universal linear-time guarantee is implied.
|
||||
|
||||
Remediation: build/reuse phase-local lookups or collect corresponding nodes
|
||||
during shared traversal instead of independently searching a broad mapping
|
||||
for every child. Review the summary membership work as well: the new
|
||||
`historySourcePayload.contributions` list is populated and tested but currently
|
||||
has no production reader. Preserve any membership the selected design needs,
|
||||
but avoid computing unused data through repeated searches. Retain behavior
|
||||
coverage; do not add timing thresholds or implementation-specific scan counts.
|
||||
|
||||
## 5. Prefix Indexing Multiplies One Operation By All Its Descendants
|
||||
|
||||
Priority: P2. Status: open.
|
||||
|
||||
Relevant code: `addObservation` in
|
||||
[history_model.go](../../internal/engine/history_model.go), lines 116–139;
|
||||
replacement observation construction in
|
||||
[history_source.go](../../internal/engine/history_source.go), lines 101–116;
|
||||
`sourceOriginRepresentedByOperation` in that file, lines 245–263; and candidate
|
||||
collection in [history_select.go](../../internal/engine/history_select.go),
|
||||
lines 35–53. CLI and resolution effects use the same operation index.
|
||||
|
||||
An ancestor replacement registers before/after facts for every affected
|
||||
descendant, indexing every distinct observation ID at the same reported
|
||||
operation pointer. The prefix index consequently holds O(n) entries for one
|
||||
logical replacement of an n-node subtree, not one indexed operation plus
|
||||
shape-specific affected-path lookup.
|
||||
|
||||
During source normalization, each surviving descendant calls
|
||||
`sourceOriginRepresentedByOperation`. It visits all those ancestor entries and
|
||||
re-resolves the same operation attribution, even after finding a match. One
|
||||
large subtree replacement therefore creates Theta(n²) visits. Later, a single
|
||||
leaf `History` query gathers and sorts all n entries at its enclosing operation
|
||||
before rejecting unrelated affected paths. Querying n such leaves performs
|
||||
O(n² log n) candidate work despite each leaf's short returned history.
|
||||
|
||||
Remediation: distinguish logical operation indexing from exact affected-path
|
||||
and before/after-shape indexing. Visit operation attribution once, then locate
|
||||
the query's relevant shape facts rather than expanding every sibling fact.
|
||||
Preserve parsed-query deduplication, removed descendants, ancestor replacement
|
||||
effects, and enclosing queries. This should implement the selected localized
|
||||
candidate-query design, not replace the existing prefix mechanism wholesale or
|
||||
coarsen semantic identity.
|
||||
|
||||
## 6. Sensitivity Candidate Deduplication Cannot Match Parsed Paths
|
||||
|
||||
Priority: P3. Status: open.
|
||||
|
||||
Relevant code: `sensitivityCandidates` and `appendDistinctHistoryPath` in
|
||||
[history_select.go](../../internal/engine/history_select.go), especially
|
||||
lines 207–234; and `sameConcreteHistoryPath` in
|
||||
[history_model.go](../../internal/engine/history_model.go), lines 271–294.
|
||||
|
||||
The candidate collector adds a parsed version of every observation's pointer,
|
||||
then uses a concrete-only comparator to deduplicate candidates. That comparator
|
||||
returns false for a parsed segment, including comparison of a non-root parsed
|
||||
path with itself. Repeated observations of one path therefore append repeated
|
||||
parsed candidates instead of merging them. Parsed resolution dependencies have
|
||||
the same problem in `appendDistinctHistoryPath`.
|
||||
|
||||
For k observations of one non-root pointer, the per-pointer candidate list grows
|
||||
with k and insertion repeatedly scans it: Theta(k²) comparisons and unnecessary
|
||||
retained temporary paths/dependency entries. The outer classifier later uses
|
||||
structural equality to suppress duplicate predicate candidates; this finding
|
||||
does not establish repeated public predicate calls or another redaction leak.
|
||||
|
||||
Remediation: use structural path equality that handles parsed segments for
|
||||
candidate/dependency deduplication, while retaining concrete-only comparison
|
||||
where concrete inventory requires it. Preserve distinct parsed, explicit-key,
|
||||
and explicit-index candidates until classification is unioned by pointer.
|
||||
Extend focused candidate/dependency coverage for repeated parsed paths without
|
||||
asserting timing or promising a new public call-count contract.
|
||||
Reference in New Issue
Block a user