Normalize typed CLI history evidence

This commit is contained in:
2026-10-26 15:36:42 +00:00
parent da74d4190d
commit 231ae66857
5 changed files with 366 additions and 3 deletions

View File

@@ -1,6 +1,6 @@
# Interface Contract Remediation Implementation Plan
Status: in progress. Stages 1–6 are complete; Stages 7–18 remain pending.
Status: in progress. Stages 1–7 are complete; Stages 8–18 remain pending.
Baseline: `f6d54a094a2b5ada5748686620659399dce7ebf0`.
Audience: a Luna coding agent executing the stages sequentially.
@@ -449,7 +449,10 @@ by a particular public projection. Focused check:
## Stage 7 — Normalize Typed Checkpoints And CLI Evidence
Owners: new `history_cli.go`, `override_context.go`, override metadata/replay
helpers. Status: pending.
helpers. Status: complete. Validation:
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'CLI.*History|Explain|Decoding|IntegerVersionInspection|Override' -count=1`,
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -count=1`,
and `git diff --check`.
1. Register pre-CLI and post-CLI canonical checkpoints with their origin indexes
on the correct sides of the CLI contribution phase. Preserve raw-versus-typed

View File

@@ -0,0 +1,156 @@
package engine
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
type historyCheckpointPayload struct {
root *document.Node
origins map[string]Origin
}
func (builder *historyBuilder) observePreCLICheckpoint(root *document.Node, origins map[string]Origin) {
builder.observeTypedCheckpoint(historyPhasePreCLI, root, origins, "applied retained defaults and canonical typed decoding")
}
func (builder *historyBuilder) observePostCLICheckpoint(root *document.Node, origins map[string]Origin) {
builder.observeTypedCheckpoint(historyPhasePostCLI, root, origins, "applied the typed CLI assignment")
}
func (builder *historyBuilder) observeTypedCheckpoint(phase historyPhase, root *document.Node, origins map[string]Origin, reason string) {
builder.requireOpen()
if phase != historyPhasePreCLI && phase != historyPhasePostCLI {
panic("configkit: typed checkpoint has an invalid history phase")
}
payload := builder.nextPayload(phase)
checkpoint := historyCheckpointPayload{root: document.Clone(root), origins: cloneOriginIndex(origins)}
switch phase {
case historyPhasePreCLI:
if builder.model.preCLICheckpoint != nil || payload.id != 0 {
panic("configkit: pre-CLI checkpoint was already registered")
}
builder.model.preCLICheckpoint = &checkpoint
case historyPhasePostCLI:
if builder.model.postCLICheckpoint != nil || payload.id != 0 {
panic("configkit: post-CLI checkpoint was already registered")
}
builder.model.postCLICheckpoint = &checkpoint
}
beforeRoot := builder.scratch
paths := unionCLIHistoryPaths(beforeRoot, root)
for _, path := range paths {
beforeNode, before := documentAtPath(beforeRoot, path)
afterNode, after := documentAtPath(root, path)
sensitive := beforeNode != nil && beforeNode.Sensitive || afterNode != nil && afterNode.Sensitive
builder.addObservation(historyObservation{
path: path, phase: phase, payload: payload,
reportedPath: path, role: historyRoleInventory,
sensitive: sensitive, attributionRef: noHistoryAttribution,
})
if configurationValuesEqual(beforeNode, afterNode) {
continue
}
attributionRef := noHistoryAttribution
if after {
origin, found := origins[path.String()]
if !found {
origin = Origin{Layer: LayerDefault}
}
origin.Operation = ChangeDecoding
attributionRef = builder.addAttribution(origin, []string{reason}, nil)
}
builder.addObservation(historyObservation{
path: path, phase: phase, payload: payload,
reportedPath: path, role: historyRoleCheckpoint,
beforePresent: before, afterPresent: after, sensitive: sensitive,
attributionRef: attributionRef,
})
}
builder.advanceScratch(document.Clone(root))
}
func (builder *historyBuilder) observeCLIEvidence(applications []historyApplication, records []overlayRecord) {
builder.requireOpen()
count := max(len(applications), len(records))
if count == 0 {
return
}
index := new(mappingIndex)
for item := 0; item < count; item++ {
if item < len(applications) {
application := applications[item]
payload := builder.addCLIPayload(&application, nil)
walkHistoryApplication(application, func(_, effective Path, node *document.Node) {
origin := sourceApplicationOrigin(application, node)
attribution := builder.addAttribution(origin, nil, nil)
builder.addObservation(historyObservation{
path: effective, phase: historyPhaseCLI, payload: payload,
reportedPath: effective, role: historyRoleContribution,
afterPresent: true, sensitive: node.Sensitive,
attributionRef: attribution,
})
})
applyHistoryApplication(&builder.scratch, application, index)
}
if item < len(records) {
record := records[item]
payload := builder.addCLIPayload(nil, &record)
attribution := builder.addAttribution(record.change.Origin, nil, nil)
for _, displaced := range record.displaced {
builder.addObservation(historyObservation{
path: displaced.path, phase: historyPhaseCLI, payload: payload,
reportedPath: record.change.Path, role: historyRoleOperation,
beforePresent: true, attributionRef: attribution,
})
}
for _, introduced := range record.introduced {
builder.addObservation(historyObservation{
path: introduced.path, phase: historyPhaseCLI, payload: payload,
reportedPath: record.change.Path, role: historyRoleOperation,
afterPresent: true, sensitive: introduced.sensitive,
attributionRef: attribution,
})
}
}
}
}
func (builder *historyBuilder) addCLIPayload(application *historyApplication, record *overlayRecord) historyPayloadRef {
payload := builder.nextPayload(historyPhaseCLI)
if payload.id != len(builder.model.cliPayloads) {
panic("configkit: CLI payload identifiers are out of order")
}
owned := historySourcePayload{}
switch {
case application != nil:
owned.kind = historySourceApplicationPayload
owned.application = cloneHistoryApplications([]historyApplication{*application})[0]
case record != nil:
owned.kind = historySourceOverlayPayload
owned.overlay = cloneOverlayRecords([]overlayRecord{*record})[0]
default:
panic("configkit: CLI payload requires an application or operation")
}
builder.model.cliPayloads = append(builder.model.cliPayloads, owned)
return payload
}
func unionCLIHistoryPaths(roots ...*document.Node) []Path {
var paths []Path
seen := make(map[string][]Path)
for _, root := range roots {
if root == nil {
continue
}
walkDocument(root, Path{}, func(path Path, _ *document.Node) {
for _, existing := range seen[path.String()] {
if sameConcreteHistoryPath(existing, path) {
return
}
}
owned := path.clone()
paths = append(paths, owned)
seen[path.String()] = append(seen[path.String()], owned)
})
}
return paths
}

View File

@@ -0,0 +1,191 @@
package engine
import (
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
func TestCLIOverrideRecordsTrackConcreteShapeChanges(t *testing.T) {
t.Parallel()
cases := []struct {
name string
beforeText string
incomingText string
oldPath Path
introducedPath Path
}{
{
name: "mapping to sequence",
beforeText: "target: {'0': {old: true}}\n",
incomingText: "target: [{new: true}]\n",
oldPath: Keys("target", "0", "old"),
introducedPath: Keys("target").Index(0).Key("new"),
},
{
name: "sequence to mapping",
beforeText: "target: [{old: true}]\n",
incomingText: "target: {'0': {new: true}}\n",
oldPath: Keys("target").Index(0).Key("old"),
introducedPath: Keys("target", "0", "new"),
},
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
before := parseDocumentForDecode(t, test.beforeText)
incomingRoot := parseDocumentForDecode(t, test.incomingText)
setOverrideTestDocumentSource(incomingRoot, "cli-source")
incoming, _ := documentAtPath(incomingRoot, Keys("target"))
origins := make(map[string]Origin)
collectOverlayOrigins(origins, before, Path{}, LayerBase)
records := overrideMetadataRecords(decodedConfiguration{final: before, origins: origins}, []preparedOverride{{
path: Keys("target"), value: incoming, source: SourceInfo{ID: "cli-source"},
}})
if len(records) != 1 {
t.Fatalf("override records = %#v", records)
}
record := records[0]
if !containsConcreteDisplacement(record.displaced, test.oldPath) {
t.Fatalf("displaced shape %s missing from %#v", test.oldPath, record.displaced)
}
introduced := findHistoryPathOrigin(record.introduced, test.introducedPath)
if introduced == nil || introduced.origin.Layer != LayerCLI || len(introduced.origin.Locations) != 1 || introduced.origin.Locations[0].SourceID != "cli-source" {
t.Fatalf("introduced shape %s has no CLI attribution: %#v", test.introducedPath, record.introduced)
}
application := historyApplication{
layer: LayerCLI, path: Keys("target"), value: incoming,
origin: overlayOrigin(LayerCLI, document.Location{SourceID: "cli-source"}),
}
builder := newHistoryBuilder()
builder.setScratch(before)
builder.observeCLIEvidence([]historyApplication{application}, records)
model := builder.freeze()
oldSelections := model.selectHistory(test.oldPath)
if len(oldSelections) != 1 || oldSelections[0].role != historyRoleOperation || !oldSelections[0].before || oldSelections[0].after {
t.Fatalf("displaced CLI shape %s = %#v", test.oldPath, oldSelections)
}
introducedSelections := model.selectHistory(test.introducedPath)
introducedOperation := false
for _, selection := range introducedSelections {
introducedOperation = introducedOperation || selection.role == historyRoleOperation && !selection.before && selection.after
}
if !introducedOperation {
t.Fatalf("introduced CLI shape %s selections = %#v", test.introducedPath, introducedSelections)
}
})
}
}
func TestCLIHistoryEvidenceKeepsCheckpointsAndConditionalReconciliation(t *testing.T) {
t.Parallel()
structural := parseDocumentForDecode(t, "count: 7\nsettings: {a: 1, b: 2}\nitems: {old: value}\n")
preCLI := parseDocumentForDecode(t, "count: '7'\nsettings: {a: 1, b: 2}\nitems: {old: value}\ndefaulted: 9\n")
preOrigins := map[string]Origin{
Keys("count").String(): {Layer: LayerBase, Locations: []Location{{SourceID: "input"}}},
Keys("defaulted").String(): {Layer: LayerDefault},
}
countRoot := parseDocumentForDecode(t, "count: 7\n")
settingsRoot := parseDocumentForDecode(t, "settings: {a: 1}\n")
itemsRoot := parseDocumentForDecode(t, "items: []\n")
setOverrideTestDocumentSource(countRoot, "cli-count")
setOverrideTestDocumentSource(settingsRoot, "cli-settings")
setOverrideTestDocumentSource(itemsRoot, "cli-items")
countValue, _ := documentAtPath(countRoot, Keys("count"))
settingsValue, _ := documentAtPath(settingsRoot, Keys("settings"))
itemsValue, _ := documentAtPath(itemsRoot, Keys("items"))
overrides := []preparedOverride{
{path: Keys("count"), value: countValue, source: SourceInfo{ID: "cli-count"}},
{path: Keys("settings"), value: settingsValue, source: SourceInfo{ID: "cli-settings"}},
{path: Keys("items"), value: itemsValue, source: SourceInfo{ID: "cli-items"}},
}
records := overrideMetadataRecords(decodedConfiguration{final: preCLI, origins: preOrigins}, overrides)
applications := make([]historyApplication, len(overrides))
for index, override := range overrides {
applications[index] = historyApplication{
layer: LayerCLI, path: override.path, value: override.value,
origin: overlayOrigin(LayerCLI, document.Location{SourceID: override.source.ID}),
}
}
postCLI := parseDocumentForDecode(t, "count: '7'\nsettings: {a: 1, b: 2}\nitems: []\ndefaulted: 9\n")
postOrigins := map[string]Origin{
Keys("count").String(): {Layer: LayerCLI, Locations: []Location{{SourceID: "cli-count"}}},
Keys("defaulted").String(): {Layer: LayerDefault},
}
builder := newHistoryBuilder()
builder.setScratch(structural)
builder.observePreCLICheckpoint(preCLI, preOrigins)
builder.observeCLIEvidence(applications, records)
builder.observePostCLICheckpoint(postCLI, postOrigins)
preCount, _ := documentAtPath(preCLI, Keys("count"))
postCount, _ := documentAtPath(postCLI, Keys("count"))
preCount.Text = "producer mutation"
postCount.Text = "producer mutation"
model := builder.freeze()
if model.preCLICheckpoint == nil || model.postCLICheckpoint == nil ||
model.preCLICheckpoint.root == preCLI || model.postCLICheckpoint.root == postCLI ||
len(model.cliPayloads) != 6 {
t.Fatalf("checkpoint/CLI payload ownership = pre:%#v post:%#v CLI:%d", model.preCLICheckpoint, model.postCLICheckpoint, len(model.cliPayloads))
}
preValue, _ := documentAtPath(model.preCLICheckpoint.root, Keys("count"))
postValue, _ := documentAtPath(model.postCLICheckpoint.root, Keys("count"))
if preValue.Kind != document.String || preValue.Text != "7" || postValue.Kind != document.String || postValue.Text != "7" {
t.Fatalf("typed checkpoint values = %#v / %#v", preValue, postValue)
}
count := model.selectHistory(Keys("count"))
var sawPreDecoding, sawEqualCLI, sawPostDecoding bool
for _, selection := range count {
switch selection.phase {
case historyPhasePreCLI:
sawPreDecoding = selection.role == historyRoleCheckpoint && selection.before && selection.after &&
selection.attribution.origin.Operation == ChangeDecoding && len(selection.attribution.reasons) != 0
case historyPhaseCLI:
sawEqualCLI = sawEqualCLI || selection.role == historyRoleOperation && selection.after
case historyPhasePostCLI:
sawPostDecoding = selection.role == historyRoleCheckpoint && selection.before && selection.after &&
selection.attribution.origin.Layer == LayerCLI && selection.attribution.origin.Operation == ChangeDecoding
}
}
if !sawPreDecoding || !sawEqualCLI || !sawPostDecoding {
t.Fatalf("checkpoint and equal CLI selections = %#v", count)
}
if metadataHistoryRole(historyRoleCheckpoint) || !metadataHistoryRole(historyRoleOperation) {
t.Fatal("typed reconciliation checkpoint leaked into metadata history roles")
}
defaulted := model.selectHistory(Keys("defaulted"))
if len(defaulted) != 1 || defaulted[0].phase != historyPhasePreCLI || defaulted[0].before || !defaulted[0].after ||
defaulted[0].attribution.origin.Layer != LayerDefault || len(defaulted[0].attribution.reasons) == 0 {
t.Fatalf("retained default checkpoint = %#v", defaulted)
}
removed := model.selectHistory(Keys("items", "old"))
if len(removed) != 1 || removed[0].phase != historyPhaseCLI || removed[0].role != historyRoleOperation || !removed[0].before || removed[0].after {
t.Fatalf("whole-sequence CLI removal = %#v", removed)
}
untouched := model.selectHistory(Keys("settings", "b"))
if len(untouched) != 0 {
t.Fatalf("partial mapping CLI override attributed an untouched key: %#v", untouched)
}
}
func containsConcreteDisplacement(displaced []displacedDefinition, path Path) bool {
for _, candidate := range displaced {
if sameConcreteHistoryPath(candidate.path, path) {
return true
}
}
return false
}
func findHistoryPathOrigin(introduced []historyPathOrigin, path Path) *historyPathOrigin {
for index := range introduced {
if sameConcreteHistoryPath(introduced[index].path, path) {
return &introduced[index]
}
}
return nil
}

View File

@@ -60,6 +60,9 @@ type historyModel struct {
baseline *document.Node
sourcePayloads []historySourcePayload
inheritancePayloads []historyInheritancePayload
cliPayloads []historySourcePayload
preCLICheckpoint *historyCheckpointPayload
postCLICheckpoint *historyCheckpointPayload
shapesByPointer map[string][]Path
shapeOrder []Path

View File

@@ -106,12 +106,22 @@ func overrideMetadataRecords(before decodedConfiguration, overrides []preparedOv
location := document.Location{SourceID: override.source.ID}
previous := cloneOrigin(before.origins[override.path.String()])
var displaced []displacedDefinition
var introduced []historyPathOrigin
if existing, ok := documentAtPathWithLookup(before.final, override.path, lookup); ok {
displaced = affectedOverrideOrigins(existing, override.value, before.origins, override.path, new(mappingIndex))
}
application := historyApplication{
layer: LayerCLI, path: override.path.clone(), value: override.value,
origin: overlayOrigin(LayerCLI, location),
}
walkHistoryApplication(application, func(_, effective Path, node *document.Node) {
introduced = append(introduced, historyPathOrigin{
path: effective, origin: sourceApplicationOrigin(application, node), sensitive: node.Sensitive,
})
})
records = append(records, overlayRecord{change: Change{
Path: override.path.clone(), Origin: overlayOrigin(LayerCLI, location), Previous: previousOrigins(previous),
}, displaced: displaced})
}, displaced: displaced, introduced: introduced})
}
return records
}