Localize history selection to affected paths

This commit is contained in:
2026-10-27 03:10:07 +00:00
parent 9ff873f54c
commit 731149fc6d
4 changed files with 26 additions and 19 deletions

View File

@@ -266,7 +266,15 @@ coverage; do not add timing thresholds or implementation-specific scan counts.
## 5. Prefix Indexing Multiplies One Operation By All Its Descendants ## 5. Prefix Indexing Multiplies One Operation By All Its Descendants
Priority: P2. Status: open. Priority: P2. Status: complete.
Completion: indexes locate concrete affected paths rather than placing every
descendant at the reported ancestor. Exact path buckets drive source-origin
containment (with early success); enclosing queries visit only their affected
subtree. Logical operations remain payload references, deduplicated once by the
common selector. Thus a leaf no longer scans sibling facts of an ancestor
replacement. The full ordinary suite and `git diff --check` passed, including
parsed key/index sides, removed descendants, inheritance, and enclosing queries.
Relevant code: `addObservation` in Relevant code: `addObservation` in
[history_model.go](../../internal/engine/history_model.go), lines 116–139; [history_model.go](../../internal/engine/history_model.go), lines 116–139;

View File

@@ -69,7 +69,7 @@ type historyModel struct {
shapesByPointer map[string][]Path shapesByPointer map[string][]Path
shapeOrder []Path shapeOrder []Path
observationsByPointer map[string][]int observationsByPointer map[string][]int
reportedPathIndex pathPrefixIndex affectedPathIndex pathPrefixIndex
documentLookup *documentLookup documentLookup *documentLookup
frozen bool frozen bool
@@ -127,14 +127,14 @@ func (builder *historyBuilder) addObservation(observation historyObservation) in
builder.model.observations = append(builder.model.observations, observation) builder.model.observations = append(builder.model.observations, observation)
builder.addShape(observation.path) builder.addShape(observation.path)
switch observation.role { switch observation.role {
case historyRoleDefinition, historyRoleContribution, historyRoleCompositeDefinition, historyRoleCheckpoint: case historyRoleDefinition, historyRoleContribution, historyRoleCompositeDefinition, historyRoleCheckpoint, historyRoleOperation, historyRoleControl:
pointer := observation.path.String() pointer := observation.path.String()
builder.model.observationsByPointer[pointer] = append(builder.model.observationsByPointer[pointer], observationID) builder.model.observationsByPointer[pointer] = append(builder.model.observationsByPointer[pointer], observationID)
if observation.role == historyRoleContribution { if observation.role == historyRoleContribution || observation.role == historyRoleOperation || observation.role == historyRoleControl {
builder.model.reportedPathIndex.insert(pointer, observationID) // Index the concrete affected path, not the common reported ancestor.
// A leaf query must not enumerate every sibling fact of one operation.
builder.model.affectedPathIndex.insert(pointer, observationID)
} }
case historyRoleOperation, historyRoleControl:
builder.model.reportedPathIndex.insert(observation.reportedPath.String(), observationID)
} }
return observationID return observationID
} }

View File

@@ -36,10 +36,10 @@ func (model *historyModel) selectHistory(path Path) []historySelection {
for _, observationID := range model.observationsByPointer[path.String()] { for _, observationID := range model.observationsByPointer[path.String()] {
candidateSet[observationID] = struct{}{} candidateSet[observationID] = struct{}{}
} }
model.reportedPathIndex.visitAncestors(path.String(), func(observationID int) { // Producers register all affected before/after paths. Exact facts cover
candidateSet[observationID] = struct{}{} // ancestor replacements; descendants cover operations within an enclosing
}) // query. Logical operations are still deduplicated by payload below.
model.reportedPathIndex.visitDescendants(path.String(), func(observationID int) { model.affectedPathIndex.visitDescendants(path.String(), func(observationID int) {
candidateSet[observationID] = struct{}{} candidateSet[observationID] = struct{}{}
}) })
candidates := make([]int, 0, len(candidateSet)) candidates := make([]int, 0, len(candidateSet))

View File

@@ -192,26 +192,25 @@ func sourceApplicationOrigin(application historyApplication, node *document.Node
} }
func sourceOriginRepresentedByOperation(model *historyModel, path Path, origin Origin) bool { func sourceOriginRepresentedByOperation(model *historyModel, path Path, origin Origin) bool {
represented := false for _, observationID := range model.observationsByPointer[path.String()] {
model.reportedPathIndex.visitAncestors(path.String(), func(observationID int) {
observation := model.observations[observationID] observation := model.observations[observationID]
if observation.phase != historyPhaseSources || observation.role != historyRoleOperation { if observation.phase != historyPhaseSources || observation.role != historyRoleOperation {
return continue
} }
if !observation.afterPresent || !sameConcreteHistoryPath(path, observation.path) { if !observation.afterPresent || !sameConcreteHistoryPath(path, observation.path) {
return continue
} }
operationOrigin, ok := model.resolveAttribution(observation.attributionRef) operationOrigin, ok := model.resolveAttribution(observation.attributionRef)
if !ok { if !ok {
return continue
} }
if originRepresentedByChange(path, origin, Change{ if originRepresentedByChange(path, origin, Change{
Path: observation.reportedPath, Origin: operationOrigin.origin, Path: observation.reportedPath, Origin: operationOrigin.origin,
}) { }) {
represented = true return true
} }
}) }
return represented return false
} }
func originRepresentedByChange(path Path, origin Origin, change Change) bool { func originRepresentedByChange(path Path, origin Origin, change Change) bool {