diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 11489b6..2b1f2a7 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -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 -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 [history_model.go](../../internal/engine/history_model.go), lines 116–139; diff --git a/internal/engine/history_model.go b/internal/engine/history_model.go index 5a44b81..97540b8 100644 --- a/internal/engine/history_model.go +++ b/internal/engine/history_model.go @@ -69,7 +69,7 @@ type historyModel struct { shapesByPointer map[string][]Path shapeOrder []Path observationsByPointer map[string][]int - reportedPathIndex pathPrefixIndex + affectedPathIndex pathPrefixIndex documentLookup *documentLookup frozen bool @@ -127,14 +127,14 @@ func (builder *historyBuilder) addObservation(observation historyObservation) in builder.model.observations = append(builder.model.observations, observation) builder.addShape(observation.path) switch observation.role { - case historyRoleDefinition, historyRoleContribution, historyRoleCompositeDefinition, historyRoleCheckpoint: + case historyRoleDefinition, historyRoleContribution, historyRoleCompositeDefinition, historyRoleCheckpoint, historyRoleOperation, historyRoleControl: pointer := observation.path.String() builder.model.observationsByPointer[pointer] = append(builder.model.observationsByPointer[pointer], observationID) - if observation.role == historyRoleContribution { - builder.model.reportedPathIndex.insert(pointer, observationID) + if observation.role == historyRoleContribution || observation.role == historyRoleOperation || observation.role == historyRoleControl { + // 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 } diff --git a/internal/engine/history_select.go b/internal/engine/history_select.go index 83646ba..7affcff 100644 --- a/internal/engine/history_select.go +++ b/internal/engine/history_select.go @@ -36,10 +36,10 @@ func (model *historyModel) selectHistory(path Path) []historySelection { for _, observationID := range model.observationsByPointer[path.String()] { candidateSet[observationID] = struct{}{} } - model.reportedPathIndex.visitAncestors(path.String(), func(observationID int) { - candidateSet[observationID] = struct{}{} - }) - model.reportedPathIndex.visitDescendants(path.String(), func(observationID int) { + // Producers register all affected before/after paths. Exact facts cover + // ancestor replacements; descendants cover operations within an enclosing + // query. Logical operations are still deduplicated by payload below. + model.affectedPathIndex.visitDescendants(path.String(), func(observationID int) { candidateSet[observationID] = struct{}{} }) candidates := make([]int, 0, len(candidateSet)) diff --git a/internal/engine/history_source.go b/internal/engine/history_source.go index b0f7870..30f7c34 100644 --- a/internal/engine/history_source.go +++ b/internal/engine/history_source.go @@ -192,26 +192,25 @@ func sourceApplicationOrigin(application historyApplication, node *document.Node } func sourceOriginRepresentedByOperation(model *historyModel, path Path, origin Origin) bool { - represented := false - model.reportedPathIndex.visitAncestors(path.String(), func(observationID int) { + for _, observationID := range model.observationsByPointer[path.String()] { observation := model.observations[observationID] if observation.phase != historyPhaseSources || observation.role != historyRoleOperation { - return + continue } if !observation.afterPresent || !sameConcreteHistoryPath(path, observation.path) { - return + continue } operationOrigin, ok := model.resolveAttribution(observation.attributionRef) if !ok { - return + continue } if originRepresentedByChange(path, origin, Change{ Path: observation.reportedPath, Origin: operationOrigin.origin, }) { - represented = true + return true } - }) - return represented + } + return false } func originRepresentedByChange(path Path, origin Origin, change Change) bool {