diff --git a/internal/framework/chunkmap/codec.go b/internal/framework/chunkmap/codec.go index 721a4e1..aaee537 100644 --- a/internal/framework/chunkmap/codec.go +++ b/internal/framework/chunkmap/codec.go @@ -102,11 +102,11 @@ func Build(request BuildRequest) (ChunkMap, error) { Annotations: source.CloneChunkAnnotations(chunk.Annotations), } } - canonical, err := canonicalize(value) + canonical, err := canonicalizeOwned(value) if err != nil { 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. @@ -132,7 +132,7 @@ func (c *Codec) Encode(value ChunkMap) ([]byte, error) { if _, err := c.schemaBytes(); err != nil { return nil, err } - canonical, err := canonicalize(clone(value)) + canonical, err := canonicalize(value) if err != nil { 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 { return ChunkMap{}, fmt.Errorf("decode source chunk map: multiple JSON values") } - canonical, err := canonicalize(value) + canonical, err := canonicalizeOwned(value) if err != nil { return ChunkMap{}, fmt.Errorf("decode source chunk map: %w", err) } - return clone(canonical), nil + return canonical, nil } func (c *Codec) schemaBytes() ([]byte, error) { @@ -241,6 +241,10 @@ func hasRequiredFields(required []string) bool { } 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 { return ChunkMap{}, err } diff --git a/internal/framework/chunkmap/codec_test.go b/internal/framework/chunkmap/codec_test.go index 4b36546..d00c125 100644 --- a/internal/framework/chunkmap/codec_test.go +++ b/internal/framework/chunkmap/codec_test.go @@ -56,6 +56,15 @@ func TestCodecRoundTripsValidFixture(t *testing.T) { if !bytes.Equal(encoded, bytes.TrimSpace(fixture)) { 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) { diff --git a/internal/framework/evidencecontext/build.go b/internal/framework/evidencecontext/build.go index c029a0a..d862a2d 100644 --- a/internal/framework/evidencecontext/build.go +++ b/internal/framework/evidencecontext/build.go @@ -84,11 +84,11 @@ func Build(request BuildRequest) (Document, error) { } document.Contexts = append(document.Contexts, context) } - canonical, err := canonicalize(document) + canonical, err := canonicalizeOwned(document) if err != nil { return Document{}, fmt.Errorf("validate evidence context: %w", err) } - return clone(canonical) + return canonical, nil } type evidenceKey struct { diff --git a/internal/framework/evidencecontext/codec.go b/internal/framework/evidencecontext/codec.go index 6782d2c..a4091e1 100644 --- a/internal/framework/evidencecontext/codec.go +++ b/internal/framework/evidencecontext/codec.go @@ -93,11 +93,11 @@ func (c *Codec) Decode(content []byte) (Document, error) { if err := decoder.Decode(&trailing); err != io.EOF { return Document{}, fmt.Errorf("decode evidence context: multiple JSON values") } - canonical, err := canonicalize(value) + canonical, err := canonicalizeOwned(value) if err != nil { return Document{}, fmt.Errorf("decode evidence context: %w", err) } - return clone(canonical) + return canonical, nil } func (c *Codec) schemaBytes() ([]byte, error) { @@ -171,7 +171,10 @@ func canonicalize(value Document) (Document, error) { if err != nil { return Document{}, err } - value = owned + return canonicalizeOwned(owned) +} + +func canonicalizeOwned(value Document) (Document, error) { if err := requireIdentity("source_id", value.SourceID); err != nil { return Document{}, err } @@ -230,10 +233,7 @@ func canonicalizeContext(sourceID string, selected map[string]struct{}, seenUnit } positions := make(map[int]int, len(value.Units)) for unitIndex := range value.Units { - unit, err := cloneSourceUnit(value.Units[unitIndex]) - if err != nil { - return Context{}, fmt.Errorf("%s.units[%d]: %w", prefix, unitIndex, err) - } + unit := value.Units[unitIndex] 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) } @@ -251,7 +251,6 @@ func canonicalizeContext(sourceID string, selected map[string]struct{}, seenUnit } positions[unit.ID] = unitIndex 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 { return Context{}, fmt.Errorf("%s.context_ref must identify the first and last units", prefix) diff --git a/internal/framework/evidencecontext/evidencecontext_test.go b/internal/framework/evidencecontext/evidencecontext_test.go index 4f9da76..5f6854a 100644 --- a/internal/framework/evidencecontext/evidencecontext_test.go +++ b/internal/framework/evidencecontext/evidencecontext_test.go @@ -180,12 +180,12 @@ func TestCodecRoundTripsCompactFixtureAndOwnsDecodedValues(t *testing.T) { t.Fatalf("fixture does not use canonical encoding\nwant: %s\n got: %s", fixture, encoded) } value.Contexts[0].Units[0].Text = "changed" - decoded, err := codec.Decode(fixture) + decoded, err := codec.Decode(encoded) if err != nil { t.Fatal(err) } 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)}}}}) if err != nil {