Reconcile retained history accounting
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Interface Contract Remediation Implementation Plan
|
# Interface Contract Remediation Implementation Plan
|
||||||
|
|
||||||
Status: in progress. Stages 1–12 are complete; Stages 13–18 remain pending.
|
Status: in progress. Stages 1–15 are complete; Stages 16–18 remain pending.
|
||||||
Baseline: `f6d54a094a2b5ada5748686620659399dce7ebf0`.
|
Baseline: `f6d54a094a2b5ada5748686620659399dce7ebf0`.
|
||||||
Audience: a Luna coding agent executing the stages sequentially.
|
Audience: a Luna coding agent executing the stages sequentially.
|
||||||
|
|
||||||
@@ -663,7 +663,11 @@ mutates it. Focused check:
|
|||||||
## Stage 15 — Reconcile Complete Retained-History Accounting
|
## Stage 15 — Reconcile Complete Retained-History Accounting
|
||||||
|
|
||||||
Owners: `executeLoadRequest`, `retainedNodeCounter`, model root enumeration,
|
Owners: `executeLoadRequest`, `retainedNodeCounter`, model root enumeration,
|
||||||
`history_budget_test.go`, inheritance budget tests. Status: pending.
|
`history_budget_test.go`, inheritance budget tests. Status: complete. Validation:
|
||||||
|
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'History.*Budget|HistoryNode|Retained|ProjectionBudget' -count=1`,
|
||||||
|
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./...`,
|
||||||
|
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test -race ./internal/engine -run 'History.*Budget|HistoryNode|Retained|ProjectionBudget|CLIHistoryEvidenceKeepsCheckpoints|Detached|ConcurrentReadOnly' -count=1`,
|
||||||
|
and `git diff --check`.
|
||||||
|
|
||||||
1. Replace `addInspectionHistory`'s old field/copy enumeration with the actual
|
1. Replace `addInspectionHistory`'s old field/copy enumeration with the actual
|
||||||
model and final-inspection root graph. Include all checkpoint, application,
|
model and final-inspection root graph. Include all checkpoint, application,
|
||||||
|
|||||||
@@ -40,50 +40,6 @@ func (c *retainedNodeCounter) add(root *document.Node) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *retainedNodeCounter) canAddClones(roots []*document.Node, copies int) bool {
|
|
||||||
if c == nil || c.limit < 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
remaining := c.limit - c.count
|
|
||||||
if remaining < 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for copyIndex := 0; copyIndex < copies; copyIndex++ {
|
|
||||||
for _, root := range roots {
|
|
||||||
count, within := documentCloneNodeCount(root, remaining)
|
|
||||||
if !within {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
remaining -= count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func documentCloneNodeCount(root *document.Node, limit int) (int, bool) {
|
|
||||||
if root == nil {
|
|
||||||
return 0, true
|
|
||||||
}
|
|
||||||
count := 0
|
|
||||||
stack := []*document.Node{root}
|
|
||||||
for len(stack) != 0 {
|
|
||||||
node := stack[len(stack)-1]
|
|
||||||
stack = stack[:len(stack)-1]
|
|
||||||
if node == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
if count > limit {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
for _, entry := range node.Mapping {
|
|
||||||
stack = append(stack, entry.Value)
|
|
||||||
}
|
|
||||||
stack = append(stack, node.Sequence...)
|
|
||||||
}
|
|
||||||
return count, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func addInspectionHistory(counter *retainedNodeCounter, inspection *Inspection) bool {
|
func addInspectionHistory(counter *retainedNodeCounter, inspection *Inspection) bool {
|
||||||
if inspection == nil || inspection.history == nil {
|
if inspection == nil || inspection.history == nil {
|
||||||
return false
|
return false
|
||||||
@@ -93,7 +49,7 @@ func addInspectionHistory(counter *retainedNodeCounter, inspection *Inspection)
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return counter.add(inspection.final)
|
||||||
}
|
}
|
||||||
|
|
||||||
// History applications borrow immutable policy-validated input trees. Metadata
|
// History applications borrow immutable policy-validated input trees. Metadata
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type historyBudgetLayeredConfig struct {
|
|||||||
func retainedInspectionNodeCountForTest(inspection *Inspection) int {
|
func retainedInspectionNodeCountForTest(inspection *Inspection) int {
|
||||||
seen := make(map[*document.Node]struct{})
|
seen := make(map[*document.Node]struct{})
|
||||||
model := inspection.history
|
model := inspection.history
|
||||||
stack := []*document.Node{model.baseline, model.final}
|
stack := []*document.Node{model.baseline, model.final, inspection.final}
|
||||||
for _, payload := range model.sourcePayloads {
|
for _, payload := range model.sourcePayloads {
|
||||||
if payload.kind == historySourceApplicationPayload {
|
if payload.kind == historySourceApplicationPayload {
|
||||||
stack = append(stack, payload.application.value)
|
stack = append(stack, payload.application.value)
|
||||||
@@ -115,6 +115,25 @@ func TestLoadHistoryBudgetUsesExactRetainedNodeCount(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHistoryNodeBudgetIncludesFinalInspectionProjection(t *testing.T) {
|
||||||
|
final := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{
|
||||||
|
Key: "value", Value: &document.Node{Kind: document.String, Text: "final"},
|
||||||
|
}}}
|
||||||
|
builder := newHistoryBuilder()
|
||||||
|
model := builder.freeze()
|
||||||
|
inspection := &Inspection{history: model, metadata: Metadata{historyModel: model}, final: final}
|
||||||
|
|
||||||
|
counter := newRetainedNodeCounter(2)
|
||||||
|
admitted := addInspectionHistory(counter, inspection)
|
||||||
|
if !admitted || counter.count != 2 {
|
||||||
|
t.Fatalf("final inspection projection was not counted exactly: admitted=%t count=%d", admitted, counter.count)
|
||||||
|
}
|
||||||
|
counter = newRetainedNodeCounter(1)
|
||||||
|
if addInspectionHistory(counter, inspection) {
|
||||||
|
t.Fatal("final inspection projection did not consume the node budget")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLayeredHistoryBudgetRejectsBeforeSecretsAndValidation(t *testing.T) {
|
func TestLayeredHistoryBudgetRejectsBeforeSecretsAndValidation(t *testing.T) {
|
||||||
type callbackCounts struct {
|
type callbackCounts struct {
|
||||||
lookups int
|
lookups int
|
||||||
@@ -131,6 +150,7 @@ func TestLayeredHistoryBudgetRejectsBeforeSecretsAndValidation(t *testing.T) {
|
|||||||
Resolve: func(configuration *historyBudgetLayeredConfig, resolution *Resolution) error {
|
Resolve: func(configuration *historyBudgetLayeredConfig, resolution *Resolution) error {
|
||||||
configuration.Child.Value += "-resolved"
|
configuration.Child.Value += "-resolved"
|
||||||
resolution.Record(Keys("child", "value"), ChangeNormalization, "normalize child")
|
resolution.Record(Keys("child", "value"), ChangeNormalization, "normalize child")
|
||||||
|
resolution.Record(Keys("child"), ChangeDerivedDefault, "record enclosing child state")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Validate: func(*historyBudgetLayeredConfig, *Validation) { counts.validations++ },
|
Validate: func(*historyBudgetLayeredConfig, *Validation) { counts.validations++ },
|
||||||
@@ -159,6 +179,25 @@ func TestLayeredHistoryBudgetRejectsBeforeSecretsAndValidation(t *testing.T) {
|
|||||||
if baseline.Inspection.history == nil || baseline.Inspection.metadata.historyModel != baseline.Inspection.history || len(baseline.Inspection.history.inheritancePayloads) == 0 {
|
if baseline.Inspection.history == nil || baseline.Inspection.metadata.historyModel != baseline.Inspection.history || len(baseline.Inspection.history.inheritancePayloads) == 0 {
|
||||||
t.Fatal("layered load did not publish its shared inheritance evidence owner")
|
t.Fatal("layered load did not publish its shared inheritance evidence owner")
|
||||||
}
|
}
|
||||||
|
resolution := baseline.Inspection.history.resolutionPayload
|
||||||
|
if resolution == nil || len(resolution.snapshots) != 2 || len(resolution.replayGroups) == 0 {
|
||||||
|
t.Fatalf("layered load did not retain overlapping original resolution evidence: %#v", resolution)
|
||||||
|
}
|
||||||
|
resolutionPaths := make(map[string]bool, len(resolution.snapshots))
|
||||||
|
for _, snapshot := range resolution.snapshots {
|
||||||
|
resolutionPaths[snapshot.change.Path.String()] = true
|
||||||
|
}
|
||||||
|
if !resolutionPaths["/child"] || !resolutionPaths["/child/value"] {
|
||||||
|
t.Fatalf("layered load resolution snapshots have paths %v, want /child and /child/value", resolutionPaths)
|
||||||
|
}
|
||||||
|
group := resolution.replayGroups[0]
|
||||||
|
groupRecords := make(map[int]bool, len(group.originalRecordIDs))
|
||||||
|
for _, id := range group.originalRecordIDs {
|
||||||
|
groupRecords[id] = true
|
||||||
|
}
|
||||||
|
if len(groupRecords) != 2 || !groupRecords[0] || !groupRecords[1] || group.change.Path.String() != "/child" {
|
||||||
|
t.Fatalf("layered load did not group both overlapping original records: %#v", group)
|
||||||
|
}
|
||||||
exactCounts := &callbackCounts{}
|
exactCounts := &callbackCounts{}
|
||||||
loaded, err := loadGroupedWithSecretLookupLimit(makeRequest(exactCounts), testSecretBinding(), lookup(exactCounts), want)
|
loaded, err := loadGroupedWithSecretLookupLimit(makeRequest(exactCounts), testSecretBinding(), lookup(exactCounts), want)
|
||||||
if err != nil || loaded == nil {
|
if err != nil || loaded == nil {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func (builder *historyBuilder) observeTypedCheckpoint(phase historyPhase, root *
|
|||||||
panic("configkit: typed checkpoint has an invalid history phase")
|
panic("configkit: typed checkpoint has an invalid history phase")
|
||||||
}
|
}
|
||||||
payload := builder.nextPayload(phase)
|
payload := builder.nextPayload(phase)
|
||||||
checkpoint := historyCheckpointPayload{root: document.Clone(root), origins: cloneOriginIndex(origins)}
|
checkpoint := historyCheckpointPayload{root: root, origins: cloneOriginIndex(origins)}
|
||||||
switch phase {
|
switch phase {
|
||||||
case historyPhasePreCLI:
|
case historyPhasePreCLI:
|
||||||
if builder.model.preCLICheckpoint != nil || payload.id != 0 {
|
if builder.model.preCLICheckpoint != nil || payload.id != 0 {
|
||||||
|
|||||||
@@ -119,14 +119,9 @@ func TestCLIHistoryEvidenceKeepsCheckpointsAndConditionalReconciliation(t *testi
|
|||||||
builder.observePreCLICheckpoint(preCLI, preOrigins)
|
builder.observePreCLICheckpoint(preCLI, preOrigins)
|
||||||
builder.observeCLIEvidence(applications, records)
|
builder.observeCLIEvidence(applications, records)
|
||||||
builder.observePostCLICheckpoint(postCLI, postOrigins)
|
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()
|
model := builder.freeze()
|
||||||
|
|
||||||
if model.preCLICheckpoint == nil || model.postCLICheckpoint == nil ||
|
if model.preCLICheckpoint == nil || model.postCLICheckpoint == nil ||
|
||||||
model.preCLICheckpoint.root == preCLI || model.postCLICheckpoint.root == postCLI ||
|
|
||||||
len(model.cliPayloads) != 6 {
|
len(model.cliPayloads) != 6 {
|
||||||
t.Fatalf("checkpoint/CLI payload ownership = pre:%#v post:%#v CLI:%d", model.preCLICheckpoint, model.postCLICheckpoint, len(model.cliPayloads))
|
t.Fatalf("checkpoint/CLI payload ownership = pre:%#v post:%#v CLI:%d", model.preCLICheckpoint, model.postCLICheckpoint, len(model.cliPayloads))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (builder *historyBuilder) observeInheritanceEvidence(operations []inheritan
|
|||||||
if payload.id != len(builder.model.inheritancePayloads) {
|
if payload.id != len(builder.model.inheritancePayloads) {
|
||||||
panic("configkit: inheritance payload identifiers are out of order")
|
panic("configkit: inheritance payload identifiers are out of order")
|
||||||
}
|
}
|
||||||
owned := cloneInheritanceOperations(operations)
|
owned := copyInheritanceOperations(operations, false)
|
||||||
builder.model.inheritancePayloads = append(builder.model.inheritancePayloads, historyInheritancePayload{operations: owned})
|
builder.model.inheritancePayloads = append(builder.model.inheritancePayloads, historyInheritancePayload{operations: owned})
|
||||||
|
|
||||||
for operationIndex, operation := range owned {
|
for operationIndex, operation := range owned {
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ func cloneInheritanceTransfer(transfer inheritanceTransfer) inheritanceTransfer
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cloneInheritanceOperations(operations []inheritanceOperation) []inheritanceOperation {
|
func cloneInheritanceOperations(operations []inheritanceOperation) []inheritanceOperation {
|
||||||
|
return copyInheritanceOperations(operations, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyInheritanceOperations detaches mutable operation metadata. History keeps
|
||||||
|
// the already immutable engine-owned document roots rather than copying them.
|
||||||
|
func copyInheritanceOperations(operations []inheritanceOperation, cloneDocuments bool) []inheritanceOperation {
|
||||||
if operations == nil {
|
if operations == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -101,8 +107,12 @@ func cloneInheritanceOperations(operations []inheritanceOperation) []inheritance
|
|||||||
transfer := cloneInheritanceTransfer(*operation.transfer)
|
transfer := cloneInheritanceTransfer(*operation.transfer)
|
||||||
cloned[index].transfer = &transfer
|
cloned[index].transfer = &transfer
|
||||||
}
|
}
|
||||||
cloned[index].parent = document.Clone(operation.parent)
|
cloned[index].parent = operation.parent
|
||||||
cloned[index].effective = document.Clone(operation.effective)
|
cloned[index].effective = operation.effective
|
||||||
|
if cloneDocuments {
|
||||||
|
cloned[index].parent = document.Clone(operation.parent)
|
||||||
|
cloned[index].effective = document.Clone(operation.effective)
|
||||||
|
}
|
||||||
cloned[index].childOrigins = cloneOriginIndex(operation.childOrigins)
|
cloned[index].childOrigins = cloneOriginIndex(operation.childOrigins)
|
||||||
cloned[index].parentOrigins = cloneOriginIndex(operation.parentOrigins)
|
cloned[index].parentOrigins = cloneOriginIndex(operation.parentOrigins)
|
||||||
cloned[index].effectiveOrigins = cloneOriginIndex(operation.effectiveOrigins)
|
cloned[index].effectiveOrigins = cloneOriginIndex(operation.effectiveOrigins)
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ func TestHistoryNodeBudgetCountsSharedInheritanceRootsOnce(t *testing.T) {
|
|||||||
model := builder.freeze()
|
model := builder.freeze()
|
||||||
inspection := &Inspection{history: model, metadata: Metadata{historyModel: model}}
|
inspection := &Inspection{history: model, metadata: Metadata{historyModel: model}}
|
||||||
counter := newRetainedNodeCounter(2)
|
counter := newRetainedNodeCounter(2)
|
||||||
if !addInspectionHistory(counter, inspection) || counter.count != 2 {
|
if !addInspectionHistory(counter, inspection) || counter.count != 1 {
|
||||||
t.Fatal("shared evidence node accounting is incorrect")
|
t.Fatal("shared evidence node accounting is incorrect")
|
||||||
}
|
}
|
||||||
operation.effective = extra
|
operation.effective = extra
|
||||||
|
|||||||
Reference in New Issue
Block a user