Files
ConfigKit/docs/roadmap/interfaces.md

203 lines
9.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# API and Interface Contract Audit
Reviewed baseline: `01b08e76a3fecc22c4414437bfeaa07b4b7a85aa`.
Status: four open findings. This is an audit record, not an implementation plan.
No production code or repository tests were changed.
The [interface remediation roadmap](interface-remediation.md) describes the
settled target, required changes, and dependencies, including a shared internal
history model for metadata history and explanation. Implementation remains pending.
This audit retains the findings and reproduction evidence.
## Scope and evidence
Reviewed the root-owned public facade and its conversions, engine loading and
callback boundaries, source/discovery and filesystem ownership, schema and typed
value handling, metadata/history, safe rendering and comparison, secrets, and
the document/YAML boundary against declaration comments, the
[configuration contract](../config.md), and applicable architectural decisions.
The root delegates to the synchronous engine lifecycle; parser-independent
documents support source assembly and inheritance, and completed results retain
detached configuration and frozen inspection state.
The findings below were reproduced through the public API with a disposable Go
program outside the repository, using labeled byte sources and `fstest.MapFS`.
The full existing suite passed with Go 1.26.10 on Linux:
```sh
GOTOOLCHAIN=local GOTELEMETRY=off GOPROXY=off go test ./... -count=1
```
The existing tests cover many ownership, lifetime, error, and zero-value cases,
but do not detect these cross-interface discrepancies. No benchmarks, external
service calls, or new test files were needed. The recommendations restore
existing contracts rather than introduce new API restrictions or guarantees.
## 1. Resolution presence queries confuse explicit keys with sequence indexes
Priority: P2. Status: open.
Relevant code:
- [`Resolution.IsExplicit`](../../internal/engine/resolution.go), lines 75–82.
- `Resolution.InputInfo` and `resolutionInput.lookup` in
[resolution.go](../../internal/engine/resolution.go) and
[load_metadata.go](../../internal/engine/load_metadata.go).
- `resolveSchemaPath` in [schema.go](../../internal/engine/schema.go).
The public [path contract](../../path.go) distinguishes explicit mapping keys
from sequence indexes. The [resolution contract](../config.md#recorded-application-resolution)
defines surviving-input presence and a fixed entry-state metadata view;
[metadata queries](../config.md#layered-metadata) preserve container shapes.
`IsExplicit` validates against the schema but then looks up presence using only
`path.String()`. In dynamic `any` state, schema validation cannot establish the
actual container shape. Consequently these distinct queries collide:
```go
key := configkit.Keys("shape", "0")
index := configkit.Keys("shape").Index(0)
```
Load `map[string]any` from `shape: [input]` with a resolver that queries both
paths, without changing the configuration. Observed results:
| Query | `Resolution.IsExplicit` | `Resolution.InputInfo` found | Final `Metadata.IsExplicit` |
| --- | --- | --- | --- |
| Explicit mapping key | true | false | false |
| Explicit sequence index | true | true | true |
The mapping-key path never existed and should not be reported as explicit.
Applications branching on presence receive inconsistent answers depending on
which public query they use, even during an otherwise unchanged load.
Potential remediation: use the retained entry-state semantic shape when checking
resolution presence, consistently with `InputInfo` and `Metadata.IsExplicit`.
Review the same string-only presence lookup in `Default` while preserving its
existing assignment and callback-lifetime rules. Add focused public coverage for
both mapping-to-index and index-to-mapping mismatches under dynamic values,
including contextually parsed pointers. This does not require changing the
documented treatment of invalid schema paths.
## 2. Resolution can transfer explicitness to a different semantic path
Priority: P2. Status: open.
Relevant code: `applyResolutionMetadata`, `applyResolutionFieldOrigins`, and
`refreshResolutionFields` in
[load_metadata.go](../../internal/engine/load_metadata.go), especially lines
371–387; final shape checking in
[`Metadata.effectiveField`](../../internal/engine/metadata.go).
The [metadata contract](../config.md#layered-metadata) distinguishes key/index
shapes and keeps derived values for absent input non-explicit. Recorded
resolution changes origins without creating input explicitness.
Reproduction:
1. Load `map[string]any` from `shape: {'0': old}`.
2. In resolution, replace `configuration["shape"]` with `[]any{"new"}` and
record normalization at `Keys("shape")`.
3. Query final metadata using the two paths from finding 1.
The old explicit mapping-key path correctly stops being effective, but the newly
created sequence index reports `Explicit: true`. That sequence index was never
supplied by input. `refreshResolutionFields` reuses the old `FieldInfo` whenever
its textual pointer survives; it updates null state without checking whether
the old and new containers give that pointer different semantic shapes. Final
lookup validates only the new shape, so it cannot recover the lost distinction.
This can mislead consumers inspecting whether a final value was explicitly
configured rather than introduced by application resolution. The converse
sequence-to-numeric-key transition has the same underlying collision.
Potential remediation: retain entry/effective shape evidence across resolution
and carry input explicitness forward only for the matching semantic path.
Preserve existing explicitness for same-shape paths and the recorded resolution
origin; do not clear every descendant's presence merely because an ancestor was
normalized. Cover both shape transitions and agreement among `Lookup`,
`IsExplicit`, and explanation. Fixing finding 1's callback query alone does not
repair this publication path.
## 3. Metadata history mixes numeric-key and sequence-index definitions
Priority: P2. Status: open.
Relevant code:
- [`Metadata.History`](../../internal/engine/metadata.go), lines 56–65.
- `metadataFromDecoded`, `applyOverlayMetadata`, and history event construction
in [load_metadata.go](../../internal/engine/load_metadata.go).
- Shape-aware replay in [`Inspection.Explain`](../../internal/engine/explain.go).
The [metadata contract](../config.md#layered-metadata) promises semantic
key/index distinctions for historical as well as effective queries. The
historical-path index retains both shapes, but `History` uses that index only to
check that the query was ever known. It then retrieves the entire history bucket
by textual pointer, without selecting the query's shape.
Reproduction: load `shape: {'0': old}` into `map[string]any`, then apply a CLI
override at `Keys("shape")` with `[]any{"new"}`. Both explicit shapes were
observed and share `/shape/0`.
`History(Keys("shape").Index(0))` incorrectly includes the original base
definition of the mapping key, and identifies that base origin as the previous
origin of the CLI operation. The old input defined a key, not an index. In the
same successful result, `Explain` for the explicit index correctly shows only
its CLI introduction. `History` for the key and index returns identical records
despite their different lifetimes.
Consequences include false source attribution and disagreement between the two
public history interfaces when consumers inspect removed or introduced paths.
Potential remediation: retain enough container-shape evidence with stored
history events, or select relevant events from existing shape-aware replay
evidence. Filter exact definitions and previous origins by the queried semantic
shape while retaining genuine ancestor replacement/removal events. Filtering
only on the current `Change.Path` is insufficient where construction already
replaced concrete segment kinds with parsed tokens. Preserve the intentional
one-pointer-per-entry `HistoricalPaths` enumeration and contextual behavior of
`ParsePath`; neither public contract needs redesign. Cover both direction changes
and compare metadata history with explanation.
## 4. Diagnostic canonicalization breaks composition claimant ordering
Priority: P3. Status: open.
Relevant code: `compositionConflictDiagnostic` in
[compose.go](../../internal/engine/compose.go) and `canonicalDiagnostics` in
[diagnostic.go](../../internal/engine/diagnostic.go), particularly lines 88–92.
The latter runs both while constructing `LoadError` and in `WriteDiagnostics`.
The [strict-composition contract](../config.md#strict-document-composition)
specifies the first claim in scoped assembly order as primary and every remaining
claim in the same order as related locations.
A native-filesystem fixture demonstrates the discrepancy:
```text
main.yml imports a.yml, b.yml, c.yml
a.yml imports z.yml
b.yml, c.yml, and z.yml each define x
```
Acquisition assigns main=source-1, a=source-2, z=source-3, b=source-4,
c=source-5. Dependency-ready assembly considers the three conflicting claims
in b, c, z order. The composer correctly chooses b as primary and constructs
related locations c, z. Generic canonicalization sorts those related locations
by acquisition ID, publishing z, c instead. The reproduced public diagnostic
has primary source-4 and related source-3, source-5.
All claimants remain present and the conflict still fails loading, so this is
lower impact than the presence/history defects. It nevertheless violates the
documented reporting order and gives consumers two different ordering rules
within one conflict report.
Potential remediation: preserve the selected semantic ordering for composition
claimant lists through accumulation and rendering, while retaining canonical
ordering of diagnostic records and the established sorting of related locations
for diagnostics without a semantic-order contract. Add one public failure-path
case whose acquisition and claimant orders differ and verify both structured
diagnostics and rendered output. No scheduler or precedence change is needed.