Avoid redundant decoded graph clones

This commit is contained in:
2026-08-09 01:34:26 +00:00
parent 1456aa51cc
commit d36d4e7689
5 changed files with 29 additions and 17 deletions

View File

@@ -102,11 +102,11 @@ func Build(request BuildRequest) (ChunkMap, error) {
Annotations: source.CloneChunkAnnotations(chunk.Annotations), Annotations: source.CloneChunkAnnotations(chunk.Annotations),
} }
} }
canonical, err := canonicalize(value) canonical, err := canonicalizeOwned(value)
if err != nil { if err != nil {
return ChunkMap{}, fmt.Errorf("validate chunk map: %w", err) return ChunkMap{}, fmt.Errorf("validate chunk map: %w", err)
} }
return clone(canonical), nil return canonical, nil
} }
// Serialize builds and encodes the framework-owned serialized artifact. // Serialize builds and encodes the framework-owned serialized artifact.
@@ -132,7 +132,7 @@ func (c *Codec) Encode(value ChunkMap) ([]byte, error) {
if _, err := c.schemaBytes(); err != nil { if _, err := c.schemaBytes(); err != nil {
return nil, err return nil, err
} }
canonical, err := canonicalize(clone(value)) canonical, err := canonicalize(value)
if err != nil { if err != nil {
return nil, fmt.Errorf("encode source chunk map: %w", err) return nil, fmt.Errorf("encode source chunk map: %w", err)
} }
@@ -163,11 +163,11 @@ func (c *Codec) Decode(content []byte) (ChunkMap, error) {
if err := decoder.Decode(&trailing); err != io.EOF { if err := decoder.Decode(&trailing); err != io.EOF {
return ChunkMap{}, fmt.Errorf("decode source chunk map: multiple JSON values") return ChunkMap{}, fmt.Errorf("decode source chunk map: multiple JSON values")
} }
canonical, err := canonicalize(value) canonical, err := canonicalizeOwned(value)
if err != nil { if err != nil {
return ChunkMap{}, fmt.Errorf("decode source chunk map: %w", err) return ChunkMap{}, fmt.Errorf("decode source chunk map: %w", err)
} }
return clone(canonical), nil return canonical, nil
} }
func (c *Codec) schemaBytes() ([]byte, error) { func (c *Codec) schemaBytes() ([]byte, error) {
@@ -241,6 +241,10 @@ func hasRequiredFields(required []string) bool {
} }
func canonicalize(value ChunkMap) (ChunkMap, error) { func canonicalize(value ChunkMap) (ChunkMap, error) {
return canonicalizeOwned(clone(value))
}
func canonicalizeOwned(value ChunkMap) (ChunkMap, error) {
if err := requireIdentity("source_id", value.SourceID); err != nil { if err := requireIdentity("source_id", value.SourceID); err != nil {
return ChunkMap{}, err return ChunkMap{}, err
} }

View File

@@ -56,6 +56,15 @@ func TestCodecRoundTripsValidFixture(t *testing.T) {
if !bytes.Equal(encoded, bytes.TrimSpace(fixture)) { if !bytes.Equal(encoded, bytes.TrimSpace(fixture)) {
t.Fatalf("fixture does not use canonical encoding\nwant: %s\n got: %s", fixture, encoded) t.Fatalf("fixture does not use canonical encoding\nwant: %s\n got: %s", fixture, encoded)
} }
value.PlanAnnotations["test/chunker"][0] = '['
value.Chunks[0].Annotations["test/chunker"][0] = '['
decoded, err := codec.Decode(encoded)
if err != nil {
t.Fatalf("Decode(encoded) after mutation error = %v", err)
}
if string(decoded.PlanAnnotations["test/chunker"]) != `{"label":"fixture"}` || string(decoded.Chunks[0].Annotations["test/chunker"]) != `{"category":"sample"}` {
t.Fatalf("Decode() reused mutable chunk-map storage: %#v", decoded)
}
} }
func TestBuildCanonicalizesAnnotationFormatting(t *testing.T) { func TestBuildCanonicalizesAnnotationFormatting(t *testing.T) {

View File

@@ -84,11 +84,11 @@ func Build(request BuildRequest) (Document, error) {
} }
document.Contexts = append(document.Contexts, context) document.Contexts = append(document.Contexts, context)
} }
canonical, err := canonicalize(document) canonical, err := canonicalizeOwned(document)
if err != nil { if err != nil {
return Document{}, fmt.Errorf("validate evidence context: %w", err) return Document{}, fmt.Errorf("validate evidence context: %w", err)
} }
return clone(canonical) return canonical, nil
} }
type evidenceKey struct { type evidenceKey struct {

View File

@@ -93,11 +93,11 @@ func (c *Codec) Decode(content []byte) (Document, error) {
if err := decoder.Decode(&trailing); err != io.EOF { if err := decoder.Decode(&trailing); err != io.EOF {
return Document{}, fmt.Errorf("decode evidence context: multiple JSON values") return Document{}, fmt.Errorf("decode evidence context: multiple JSON values")
} }
canonical, err := canonicalize(value) canonical, err := canonicalizeOwned(value)
if err != nil { if err != nil {
return Document{}, fmt.Errorf("decode evidence context: %w", err) return Document{}, fmt.Errorf("decode evidence context: %w", err)
} }
return clone(canonical) return canonical, nil
} }
func (c *Codec) schemaBytes() ([]byte, error) { func (c *Codec) schemaBytes() ([]byte, error) {
@@ -171,7 +171,10 @@ func canonicalize(value Document) (Document, error) {
if err != nil { if err != nil {
return Document{}, err return Document{}, err
} }
value = owned return canonicalizeOwned(owned)
}
func canonicalizeOwned(value Document) (Document, error) {
if err := requireIdentity("source_id", value.SourceID); err != nil { if err := requireIdentity("source_id", value.SourceID); err != nil {
return Document{}, err return Document{}, err
} }
@@ -230,10 +233,7 @@ func canonicalizeContext(sourceID string, selected map[string]struct{}, seenUnit
} }
positions := make(map[int]int, len(value.Units)) positions := make(map[int]int, len(value.Units))
for unitIndex := range value.Units { for unitIndex := range value.Units {
unit, err := cloneSourceUnit(value.Units[unitIndex]) unit := value.Units[unitIndex]
if err != nil {
return Context{}, fmt.Errorf("%s.units[%d]: %w", prefix, unitIndex, err)
}
if unit.ID <= 0 || strings.TrimSpace(unit.Kind) == "" || strings.TrimSpace(unit.Text) == "" { if unit.ID <= 0 || strings.TrimSpace(unit.Kind) == "" || strings.TrimSpace(unit.Text) == "" {
return Context{}, fmt.Errorf("%s.units[%d] has invalid required fields", prefix, unitIndex) return Context{}, fmt.Errorf("%s.units[%d] has invalid required fields", prefix, unitIndex)
} }
@@ -251,7 +251,6 @@ func canonicalizeContext(sourceID string, selected map[string]struct{}, seenUnit
} }
positions[unit.ID] = unitIndex positions[unit.ID] = unitIndex
seenUnits[unit.ID] = struct{}{} seenUnits[unit.ID] = struct{}{}
value.Units[unitIndex] = unit
} }
if value.ContextRef.StartUnitID != value.Units[0].ID || value.ContextRef.EndUnitID != value.Units[len(value.Units)-1].ID { if value.ContextRef.StartUnitID != value.Units[0].ID || value.ContextRef.EndUnitID != value.Units[len(value.Units)-1].ID {
return Context{}, fmt.Errorf("%s.context_ref must identify the first and last units", prefix) return Context{}, fmt.Errorf("%s.context_ref must identify the first and last units", prefix)

View File

@@ -180,12 +180,12 @@ func TestCodecRoundTripsCompactFixtureAndOwnsDecodedValues(t *testing.T) {
t.Fatalf("fixture does not use canonical encoding\nwant: %s\n got: %s", fixture, encoded) t.Fatalf("fixture does not use canonical encoding\nwant: %s\n got: %s", fixture, encoded)
} }
value.Contexts[0].Units[0].Text = "changed" value.Contexts[0].Units[0].Text = "changed"
decoded, err := codec.Decode(fixture) decoded, err := codec.Decode(encoded)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if decoded.Contexts[0].Units[0].Text != "The party meets Rowan." { if decoded.Contexts[0].Units[0].Text != "The party meets Rowan." {
t.Fatal("Decode() reused mutable document storage") t.Fatal("Encode() retained mutable document storage")
} }
built, err := Build(BuildRequest{Source: testDocument(t), WindowUnits: 1, SelectedLanes: []string{"npcs"}, LaneEvidence: []LaneEvidence{{LaneID: "npcs", SourceRefs: []source.SourceRef{ref(3, 3)}}}}) built, err := Build(BuildRequest{Source: testDocument(t), WindowUnits: 1, SelectedLanes: []string{"npcs"}, LaneEvidence: []LaneEvidence{{LaneID: "npcs", SourceRefs: []source.SourceRef{ref(3, 3)}}}})
if err != nil { if err != nil {