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
|
## 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.
|
**Scope:** evidence transport, ownership, and retained-node accounting.
|
||||||
|
|
||||||
1. Extend `historyResolutionPayload` to own the compact correspondence and paired
|
1. Extend `historyResolutionPayload` to own the compact correspondence and paired
|
||||||
|
|||||||
@@ -12,15 +12,16 @@ type resolutionReplayGroup struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type historyResolutionPayload struct {
|
type historyResolutionPayload struct {
|
||||||
snapshots []resolutionSnapshot
|
snapshots []resolutionSnapshot
|
||||||
replayGroups []resolutionReplayGroup
|
replayGroups []resolutionReplayGroup
|
||||||
|
correspondence resolutionCorrespondence
|
||||||
}
|
}
|
||||||
|
|
||||||
// observeResolutionEvidence retains audited records in producer order and
|
// observeResolutionEvidence retains audited records in producer order and
|
||||||
// indexes the concrete shapes in every original before/after subtree. Snapshot
|
// indexes the concrete shapes in every original before/after subtree. Snapshot
|
||||||
// roots are already detached by runResolution and are shared with the replay
|
// roots are already detached by runResolution and are shared with the replay
|
||||||
// group that refers to their covering record.
|
// group that refers to their covering record.
|
||||||
func (builder *historyBuilder) observeResolutionEvidence(snapshots []resolutionSnapshot) {
|
func (builder *historyBuilder) observeResolutionEvidence(snapshots []resolutionSnapshot, correspondence ...resolutionCorrespondence) {
|
||||||
builder.requireOpen()
|
builder.requireOpen()
|
||||||
if len(snapshots) == 0 {
|
if len(snapshots) == 0 {
|
||||||
return
|
return
|
||||||
@@ -36,10 +37,14 @@ func (builder *historyBuilder) observeResolutionEvidence(snapshots []resolutionS
|
|||||||
snapshots: make([]resolutionSnapshot, len(snapshots)),
|
snapshots: make([]resolutionSnapshot, len(snapshots)),
|
||||||
replayGroups: coalescedResolutionSnapshots(snapshots),
|
replayGroups: coalescedResolutionSnapshots(snapshots),
|
||||||
}
|
}
|
||||||
|
if len(correspondence) != 0 {
|
||||||
|
payload.correspondence = correspondence[0].clone()
|
||||||
|
}
|
||||||
attributions := make([]historyAttributionRef, len(snapshots))
|
attributions := make([]historyAttributionRef, len(snapshots))
|
||||||
lookup := buildDocumentLookup(builder.scratch)
|
lookup := buildDocumentLookup(builder.scratch)
|
||||||
for recordID, snapshot := range snapshots {
|
for recordID, snapshot := range snapshots {
|
||||||
snapshot.change = cloneChange(snapshot.change)
|
snapshot.change = cloneChange(snapshot.change)
|
||||||
|
snapshot.entryPath = snapshot.entryPath.clone()
|
||||||
payload.snapshots[recordID] = snapshot
|
payload.snapshots[recordID] = snapshot
|
||||||
|
|
||||||
ref := builder.nextPayload(historyPhaseResolution)
|
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)
|
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, lookup)
|
entryPath, hasEntry := snapshot.change.Path, true
|
||||||
if beforeFound {
|
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)
|
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
|
final = resolved.final
|
||||||
resolutionSnapshots = resolved.snapshots
|
resolutionSnapshots = resolved.snapshots
|
||||||
if builder != nil && len(resolutionSnapshots) != 0 {
|
if builder != nil && len(resolutionSnapshots) != 0 {
|
||||||
builder.observeResolutionEvidence(resolutionSnapshots)
|
builder.observeResolutionEvidence(resolutionSnapshots, resolved.correspondence)
|
||||||
}
|
}
|
||||||
applyResolutionMetadata(&metadata, resolved, prepared.resolution)
|
applyResolutionMetadata(&metadata, resolved, prepared.resolution)
|
||||||
finalizeEffectiveMetadataPaths(&metadata, final)
|
finalizeEffectiveMetadataPaths(&metadata, final)
|
||||||
|
|||||||
@@ -117,10 +117,26 @@ func TestResolutionAuditsRegisteredCollectionChanges(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if test.move {
|
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 {
|
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)
|
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
|
return
|
||||||
|
|||||||
@@ -16,6 +16,17 @@ type resolutionCorrespondence struct {
|
|||||||
collections map[string]collectionCorrespondence
|
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 {
|
func buildResolutionCorrespondence(entry, final *document.Node, policy sequencePolicy) resolutionCorrespondence {
|
||||||
result := resolutionCorrespondence{collections: make(map[string]collectionCorrespondence)}
|
result := resolutionCorrespondence{collections: make(map[string]collectionCorrespondence)}
|
||||||
var indexes mappingIndex
|
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: []}}")
|
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: []}}")
|
after := parseCompositionDocument(t, "after", "groups: {alpha: {items: [{id: '1', kind: x}, {id: 1.0, kind: x}]}, empty: {items: []}}")
|
||||||
matching := buildResolutionCorrespondence(before, after, policy)
|
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 {
|
if empty := matching.collections["/groups/empty/items"]; !empty.eligible || empty.structural {
|
||||||
t.Fatalf("empty = %#v", empty)
|
t.Fatalf("empty = %#v", empty)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user