Index stable history trees during normalization

This commit is contained in:
2026-10-27 03:08:34 +00:00
parent 3f389662e9
commit 9ff873f54c
6 changed files with 30 additions and 89 deletions

View File

@@ -212,7 +212,15 @@ in one focused overlay regression.
## 4. History Construction Repeatedly Performs Unindexed Mapping Searches ## 4. History Construction Repeatedly Performs Unindexed Mapping Searches
Priority: P2. Status: open. Priority: P2. Status: complete.
Completion: checkpoints, inheritance sides, and resolution before/final states
use one immutable document lookup per stable state, rather than scanning broad
mappings for every path. Lookups are rebuilt after scratch replay changes.
Removed the unused summary contribution list and its searches; the existing
test still verifies composite metadata and individual explanation attribution.
The full ordinary suite and `git diff --check` passed, including collection,
inheritance, resolution shape, and typed-checkpoint cases.
Relevant code: `observeTypedCheckpoint` in Relevant code: `observeTypedCheckpoint` in
[history_cli.go](../../internal/engine/history_cli.go), lines 38–49; [history_cli.go](../../internal/engine/history_cli.go), lines 38–49;

View File

@@ -37,9 +37,10 @@ func (builder *historyBuilder) observeTypedCheckpoint(phase historyPhase, root *
beforeRoot := builder.scratch beforeRoot := builder.scratch
paths := unionCLIHistoryPaths(beforeRoot, root) paths := unionCLIHistoryPaths(beforeRoot, root)
lookup := buildDocumentLookup(beforeRoot, root)
for _, path := range paths { for _, path := range paths {
beforeNode, before := documentAtPath(beforeRoot, path) beforeNode, before := documentAtPathWithLookup(beforeRoot, path, lookup)
afterNode, after := documentAtPath(root, path) afterNode, after := documentAtPathWithLookup(root, path, lookup)
sensitive := beforeNode != nil && beforeNode.Sensitive || afterNode != nil && afterNode.Sensitive sensitive := beforeNode != nil && beforeNode.Sensitive || afterNode != nil && afterNode.Sensitive
builder.addObservation(historyObservation{ builder.addObservation(historyObservation{
path: path, phase: phase, payload: payload, path: path, phase: phase, payload: payload,

View File

@@ -95,12 +95,13 @@ func (builder *historyBuilder) observeInheritanceTransfer(payload historyPayload
parentSide := unionInheritancePaths(beforePaths, parentPaths) parentSide := unionInheritancePaths(beforePaths, parentPaths)
parentRef := payload parentRef := payload
parentRef.substep = operationIndex * 2 parentRef.substep = operationIndex * 2
lookup := buildDocumentLookup(beforeChild, operation.parent)
for _, path := range parentSide { for _, path := range parentSide {
if inheritanceOperationIdentity(operation, path) { if inheritanceOperationIdentity(operation, path) {
continue continue
} }
beforeNode, before := documentAtInheritancePath(beforeChild, transfer.to, path) beforeNode, before := documentAtInheritancePath(beforeChild, transfer.to, path, lookup)
parentNode, after := documentAtInheritancePath(operation.parent, transfer.to, path) parentNode, after := documentAtInheritancePath(operation.parent, transfer.to, path, lookup)
selection := selectInheritanceEvidence(resolveInheritanceAttribution(operation, path, inheritanceShapeFacts{ selection := selectInheritanceEvidence(resolveInheritanceAttribution(operation, path, inheritanceShapeFacts{
child: before, parent: after, child: before, parent: after,
}), inheritanceEvidenceParent, inheritanceEvidenceDisplaced) }), inheritanceEvidenceParent, inheritanceEvidenceDisplaced)
@@ -123,12 +124,13 @@ func (builder *historyBuilder) observeInheritanceTransfer(payload historyPayload
childSide := unionInheritancePaths(parentStatePaths, effectivePaths) childSide := unionInheritancePaths(parentStatePaths, effectivePaths)
childRef := payload childRef := payload
childRef.substep = operationIndex*2 + 1 childRef.substep = operationIndex*2 + 1
lookup = buildDocumentLookup(parentState, operation.effective)
for _, path := range childSide { for _, path := range childSide {
if inheritanceOperationIdentity(operation, path) { if inheritanceOperationIdentity(operation, path) {
continue continue
} }
parentNode, before := documentAtInheritancePath(parentState, transfer.to, path) parentNode, before := documentAtInheritancePath(parentState, transfer.to, path, lookup)
effectiveNode, after := documentAtInheritancePath(operation.effective, transfer.to, path) effectiveNode, after := documentAtInheritancePath(operation.effective, transfer.to, path, lookup)
selection := selectInheritanceEvidence(resolveInheritanceAttribution(operation, path, inheritanceShapeFacts{ selection := selectInheritanceEvidence(resolveInheritanceAttribution(operation, path, inheritanceShapeFacts{
parent: before, effective: after, parent: before, effective: after,
}), inheritanceEvidenceChild, inheritanceEvidenceRemoval, inheritanceEvidenceDisplaced) }), inheritanceEvidenceChild, inheritanceEvidenceRemoval, inheritanceEvidenceDisplaced)
@@ -203,12 +205,12 @@ func unionInheritancePaths(groups ...[]Path) []Path {
return paths return paths
} }
func documentAtInheritancePath(root *document.Node, rootPath, path Path) (*document.Node, bool) { func documentAtInheritancePath(root *document.Node, rootPath, path Path, lookup *documentLookup) (*document.Node, bool) {
relative, within := relativeHistoryPath(rootPath, path) relative, within := relativeHistoryPath(rootPath, path)
if !within { if !within {
return nil, false return nil, false
} }
return documentAtPath(root, relative) return documentAtPathWithLookup(root, relative, lookup)
} }
func resolveInheritanceAttribution(operation inheritanceOperation, destination Path, shapes ...inheritanceShapeFacts) []inheritanceAttributionSelection { func resolveInheritanceAttribution(operation inheritanceOperation, destination Path, shapes ...inheritanceShapeFacts) []inheritanceAttributionSelection {

View File

@@ -37,6 +37,7 @@ func (builder *historyBuilder) observeResolutionEvidence(snapshots []resolutionS
replayGroups: coalescedResolutionSnapshots(snapshots), replayGroups: coalescedResolutionSnapshots(snapshots),
} }
attributions := make([]historyAttributionRef, len(snapshots)) attributions := make([]historyAttributionRef, len(snapshots))
lookup := buildDocumentLookup(builder.scratch)
for recordID, snapshot := range snapshots { for recordID, snapshot := range snapshots {
snapshot.change = cloneChange(snapshot.change) snapshot.change = cloneChange(snapshot.change)
payload.snapshots[recordID] = snapshot payload.snapshots[recordID] = snapshot
@@ -47,7 +48,7 @@ func (builder *historyBuilder) observeResolutionEvidence(snapshots []resolutionS
} }
attribution := builder.addAttribution(snapshot.change.Origin, snapshot.change.Reasons, snapshot.change.Dependencies) attribution := builder.addAttribution(snapshot.change.Origin, snapshot.change.Reasons, snapshot.change.Dependencies)
attributions[recordID] = attribution attributions[recordID] = attribution
beforePath, beforeFound := historyConcretePathAt(builder.scratch, snapshot.change.Path) beforePath, beforeFound := historyConcretePathAt(builder.scratch, snapshot.change.Path, lookup)
if beforeFound { if beforeFound {
observeResolutionSubtree(builder, snapshot.before, beforePath, snapshot.change.Path, ref, attribution, true, false) observeResolutionSubtree(builder, snapshot.before, beforePath, snapshot.change.Path, ref, attribution, true, false)
} }
@@ -59,9 +60,10 @@ func (builder *historyBuilder) observeResolutionEvidence(snapshots []resolutionS
covering := payload.snapshots[group.coveringRecordID] covering := payload.snapshots[group.coveringRecordID]
replaceHistoryPath(&builder.scratch, group.change.Path, covering.after) replaceHistoryPath(&builder.scratch, group.change.Path, covering.after)
} }
lookup = buildDocumentLookup(builder.scratch)
for recordID, snapshot := range payload.snapshots { for recordID, snapshot := range payload.snapshots {
ref := historyPayloadRef{phase: historyPhaseResolution, id: recordID} ref := historyPayloadRef{phase: historyPhaseResolution, id: recordID}
afterPath, afterFound := historyConcretePathAt(builder.scratch, snapshot.change.Path) afterPath, afterFound := historyConcretePathAt(builder.scratch, snapshot.change.Path, lookup)
if afterFound { if afterFound {
observeResolutionSubtree(builder, snapshot.after, afterPath, snapshot.change.Path, ref, attributions[recordID], false, true) observeResolutionSubtree(builder, snapshot.after, afterPath, snapshot.change.Path, ref, attributions[recordID], false, true)
} }
@@ -85,7 +87,7 @@ func observeResolutionSubtree(builder *historyBuilder, root *document.Node, path
}) })
} }
func historyConcretePathAt(root *document.Node, path Path) (Path, bool) { func historyConcretePathAt(root *document.Node, path Path, lookup *documentLookup) (Path, bool) {
if root == nil || !path.valid() { if root == nil || !path.valid() {
return Path{}, false return Path{}, false
} }
@@ -97,17 +99,11 @@ func historyConcretePathAt(root *document.Node, path Path) (Path, bool) {
if segment.kind == pathSegmentIndex { if segment.kind == pathSegmentIndex {
return Path{}, false return Path{}, false
} }
found := false position := lookup.find(current, segment.key)
for _, entry := range current.Mapping { if position < 0 {
if entry.Key == segment.key {
current = entry.Value
found = true
break
}
}
if !found {
return Path{}, false return Path{}, false
} }
current = current.Mapping[position].Value
concrete = concrete.Key(segment.key) concrete = concrete.Key(segment.key)
case document.Sequence: case document.Sequence:
index, ok := schemaIndex(segment) index, ok := schemaIndex(segment)

View File

@@ -26,7 +26,6 @@ type historySourcePayload struct {
hasApplicationRef bool hasApplicationRef bool
path Path path Path
origin Origin origin Origin
contributions []historyPayloadRef
} }
func (builder *historyBuilder) observeBaseline(root *document.Node) { func (builder *historyBuilder) observeBaseline(root *document.Node) {
@@ -128,8 +127,7 @@ func (builder *historyBuilder) observeSourceEvidence(root *document.Node, origin
if !found || origin.Layer == "" || sourceOriginRepresentedByOperation(builder.model, path, origin) { if !found || origin.Layer == "" || sourceOriginRepresentedByOperation(builder.model, path, origin) {
return return
} }
contributions := builder.sourceContributions(path, origin) payload := builder.addSourceSummary(path, origin)
payload := builder.addSourceSummary(path, origin, contributions)
attribution := builder.addAttribution(origin, nil, nil) attribution := builder.addAttribution(origin, nil, nil)
builder.addObservation(historyObservation{ builder.addObservation(historyObservation{
path: path, phase: historyPhaseSources, payload: payload, path: path, phase: historyPhaseSources, payload: payload,
@@ -173,69 +171,17 @@ func (builder *historyBuilder) addSourcePayload(application *historyApplication,
return payload return payload
} }
func (builder *historyBuilder) addSourceSummary(path Path, origin Origin, contributions []historyPayloadRef) historyPayloadRef { func (builder *historyBuilder) addSourceSummary(path Path, origin Origin) historyPayloadRef {
payload := builder.nextPayload(historyPhaseSources) payload := builder.nextPayload(historyPhaseSources)
if payload.id != len(builder.model.sourcePayloads) { if payload.id != len(builder.model.sourcePayloads) {
panic("configkit: source payload identifiers are out of order") panic("configkit: source payload identifiers are out of order")
} }
builder.model.sourcePayloads = append(builder.model.sourcePayloads, historySourcePayload{ builder.model.sourcePayloads = append(builder.model.sourcePayloads, historySourcePayload{
kind: historySourceSummaryPayload, path: path.clone(), origin: cloneOrigin(origin), kind: historySourceSummaryPayload, path: path.clone(), origin: cloneOrigin(origin),
contributions: append([]historyPayloadRef(nil), contributions...),
}) })
return payload return payload
} }
func (builder *historyBuilder) sourceContributions(path Path, summary Origin) []historyPayloadRef {
var contributions []historyPayloadRef
seen := make(map[historyPayloadRef]struct{})
for _, observationID := range builder.model.observationsByPointer[path.String()] {
observation := builder.model.observations[observationID]
if observation.phase != historyPhaseSources || observation.role != historyRoleContribution || !sameConcreteHistoryPath(path, observation.path) {
continue
}
attribution, ok := builder.model.resolveAttribution(observation.attributionRef)
if !ok || !sourceLocationsContainedBy(attribution.origin, summary) {
continue
}
if observation.payload.id < 0 || observation.payload.id >= len(builder.model.sourcePayloads) {
continue
}
payload := builder.model.sourcePayloads[observation.payload.id]
if payload.kind != historySourceApplicationPayload {
continue
}
local, ok := historyApplicationLocalPath(payload.application, path)
if !ok {
continue
}
if _, exists := documentAtPath(payload.application.value, local); !exists {
continue
}
if _, exists := seen[observation.payload]; exists {
continue
}
seen[observation.payload] = struct{}{}
contributions = append(contributions, observation.payload)
}
return contributions
}
func sourceLocationsContainedBy(candidate, summary Origin) bool {
if len(candidate.Locations) == 0 {
return false
}
available := make(map[Location]struct{}, len(summary.Locations))
for _, location := range summary.Locations {
available[location] = struct{}{}
}
for _, location := range candidate.Locations {
if _, found := available[location]; !found {
return false
}
}
return true
}
func sourceApplicationOrigin(application historyApplication, node *document.Node) Origin { func sourceApplicationOrigin(application historyApplication, node *document.Node) Origin {
origin := cloneOrigin(application.origin) origin := cloneOrigin(application.origin)
origin.Layer = application.layer origin.Layer = application.layer

View File

@@ -77,18 +77,6 @@ func TestHistorySourceObservationsPreserveRawAndCompositeDefinitions(t *testing.
if composite == nil || !reflect.DeepEqual(locationSourceIDs(composite.origin.Locations), []string{"base", "overlay"}) { if composite == nil || !reflect.DeepEqual(locationSourceIDs(composite.origin.Locations), []string{"base", "overlay"}) {
t.Fatalf("empty mapping composite attribution = %#v; selections %#v", composite, emptySelections) t.Fatalf("empty mapping composite attribution = %#v; selections %#v", composite, emptySelections)
} }
var emptySummary *historySourcePayload
for index := range model.sourcePayloads {
payload := &model.sourcePayloads[index]
if payload.kind == historySourceSummaryPayload && sameConcreteHistoryPath(payload.path, Keys("section", "empty")) {
emptySummary = payload
break
}
}
if emptySummary == nil || len(emptySummary.contributions) != 2 || emptySummary.contributions[0].id != 0 || emptySummary.contributions[1].id != 1 {
t.Fatalf("empty mapping summary contribution references = %#v", emptySummary)
}
metadata := Metadata{historyModel: model} metadata := Metadata{historyModel: model}
metadataHistory := metadata.History(Keys("section", "empty")) metadataHistory := metadata.History(Keys("section", "empty"))
var compositeSources []string var compositeSources []string