Retain matched resolution evidence in history
This commit is contained in:
@@ -190,6 +190,8 @@ moved afterward and no longer matches its declared final address.
|
||||
|
||||
## Stage 5 — Retain correspondence in the shared history owner
|
||||
|
||||
**Status:** Complete. Focused history, correspondence, and retained-budget checks passed.
|
||||
|
||||
**Scope:** evidence transport, ownership, and retained-node accounting.
|
||||
|
||||
1. Extend `historyResolutionPayload` to own the compact correspondence and paired
|
||||
|
||||
@@ -12,15 +12,16 @@ type resolutionReplayGroup struct {
|
||||
}
|
||||
|
||||
type historyResolutionPayload struct {
|
||||
snapshots []resolutionSnapshot
|
||||
replayGroups []resolutionReplayGroup
|
||||
snapshots []resolutionSnapshot
|
||||
replayGroups []resolutionReplayGroup
|
||||
correspondence resolutionCorrespondence
|
||||
}
|
||||
|
||||
// observeResolutionEvidence retains audited records in producer order and
|
||||
// indexes the concrete shapes in every original before/after subtree. Snapshot
|
||||
// roots are already detached by runResolution and are shared with the replay
|
||||
// group that refers to their covering record.
|
||||
func (builder *historyBuilder) observeResolutionEvidence(snapshots []resolutionSnapshot) {
|
||||
func (builder *historyBuilder) observeResolutionEvidence(snapshots []resolutionSnapshot, correspondence ...resolutionCorrespondence) {
|
||||
builder.requireOpen()
|
||||
if len(snapshots) == 0 {
|
||||
return
|
||||
@@ -36,10 +37,14 @@ func (builder *historyBuilder) observeResolutionEvidence(snapshots []resolutionS
|
||||
snapshots: make([]resolutionSnapshot, len(snapshots)),
|
||||
replayGroups: coalescedResolutionSnapshots(snapshots),
|
||||
}
|
||||
if len(correspondence) != 0 {
|
||||
payload.correspondence = correspondence[0].clone()
|
||||
}
|
||||
attributions := make([]historyAttributionRef, len(snapshots))
|
||||
lookup := buildDocumentLookup(builder.scratch)
|
||||
for recordID, snapshot := range snapshots {
|
||||
snapshot.change = cloneChange(snapshot.change)
|
||||
snapshot.entryPath = snapshot.entryPath.clone()
|
||||
payload.snapshots[recordID] = snapshot
|
||||
|
||||
ref := builder.nextPayload(historyPhaseResolution)
|
||||
@@ -48,8 +53,12 @@ func (builder *historyBuilder) observeResolutionEvidence(snapshots []resolutionS
|
||||
}
|
||||
attribution := builder.addAttribution(snapshot.change.Origin, snapshot.change.Reasons, snapshot.change.Dependencies)
|
||||
attributions[recordID] = attribution
|
||||
beforePath, beforeFound := historyConcretePathAt(builder.scratch, snapshot.change.Path, lookup)
|
||||
if beforeFound {
|
||||
entryPath, hasEntry := snapshot.change.Path, true
|
||||
if len(correspondence) != 0 {
|
||||
entryPath, hasEntry = snapshot.entryPath, snapshot.hasEntry
|
||||
}
|
||||
beforePath, beforeFound := historyConcretePathAt(builder.scratch, entryPath, lookup)
|
||||
if hasEntry && beforeFound {
|
||||
observeResolutionSubtree(builder, snapshot.before, beforePath, snapshot.change.Path, ref, attribution, true, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func resolveLoadInput[T any](schema *schemaDescriptor, definition Definition[T],
|
||||
final = resolved.final
|
||||
resolutionSnapshots = resolved.snapshots
|
||||
if builder != nil && len(resolutionSnapshots) != 0 {
|
||||
builder.observeResolutionEvidence(resolutionSnapshots)
|
||||
builder.observeResolutionEvidence(resolutionSnapshots, resolved.correspondence)
|
||||
}
|
||||
applyResolutionMetadata(&metadata, resolved, prepared.resolution)
|
||||
finalizeEffectiveMetadataPaths(&metadata, final)
|
||||
|
||||
@@ -117,10 +117,26 @@ func TestResolutionAuditsRegisteredCollectionChanges(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if test.move {
|
||||
correspondence := result.history.resolutionPayload.correspondence
|
||||
entry, ok := correspondence.entryPath(Keys("items").Index(1).Key("value"))
|
||||
if !ok || entry.String() != "/items/0/value" {
|
||||
t.Fatalf("retained entry = %s, %t", entry.String(), ok)
|
||||
}
|
||||
for _, snapshot := range result.history.resolutionPayload.snapshots {
|
||||
if snapshot.change.Path.String() == "/items/1/value" && (snapshot.before == nil || snapshot.before.Text != "old") {
|
||||
if snapshot.change.Path.String() == "/items/1/value" && (snapshot.before == nil || snapshot.before.Text != "old" || !snapshot.hasEntry || snapshot.entryPath.String() != "/items/0/value") {
|
||||
t.Fatalf("paired before = %#v", snapshot.before)
|
||||
}
|
||||
if snapshot.before != nil {
|
||||
found := false
|
||||
for _, root := range result.history.documentRoots() {
|
||||
if root == snapshot.before {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("paired snapshot omitted from retained roots")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
@@ -16,6 +16,17 @@ type resolutionCorrespondence struct {
|
||||
collections map[string]collectionCorrespondence
|
||||
}
|
||||
|
||||
func (c resolutionCorrespondence) clone() resolutionCorrespondence {
|
||||
result := resolutionCorrespondence{collections: make(map[string]collectionCorrespondence, len(c.collections))}
|
||||
for key, collection := range c.collections {
|
||||
collection.path = collection.path.clone()
|
||||
collection.entryToFinal = append([]int(nil), collection.entryToFinal...)
|
||||
collection.finalToEntry = append([]int(nil), collection.finalToEntry...)
|
||||
result.collections[key] = collection
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func buildResolutionCorrespondence(entry, final *document.Node, policy sequencePolicy) resolutionCorrespondence {
|
||||
result := resolutionCorrespondence{collections: make(map[string]collectionCorrespondence)}
|
||||
var indexes mappingIndex
|
||||
|
||||
@@ -51,6 +51,14 @@ func TestResolutionCorrespondenceDynamicCompoundIdentities(t *testing.T) {
|
||||
before := parseCompositionDocument(t, "before", "groups: {alpha: {items: [{id: 1, kind: x}, {id: '1', kind: x}]}, empty: {items: []}}")
|
||||
after := parseCompositionDocument(t, "after", "groups: {alpha: {items: [{id: '1', kind: x}, {id: 1.0, kind: x}]}, empty: {items: []}}")
|
||||
matching := buildResolutionCorrespondence(before, after, policy)
|
||||
retained := matching.clone()
|
||||
changed := matching.collections["/groups/alpha/items"]
|
||||
changed.finalToEntry[0] = 0
|
||||
matching.collections["/groups/alpha/items"] = changed
|
||||
if entry, ok := retained.entryPath(Keys("groups", "alpha", "items").Index(0)); !ok || entry.String() != "/groups/alpha/items/1" {
|
||||
t.Fatalf("retained correspondence shares mutable indexes: %s, %t", entry.String(), ok)
|
||||
}
|
||||
matching = retained
|
||||
if empty := matching.collections["/groups/empty/items"]; !empty.eligible || empty.structural {
|
||||
t.Fatalf("empty = %#v", empty)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user