feat(snippets): record merge provenance on the survivor
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Successful in 38s
CI & Build / TypeScript typecheck (push) Successful in 40s
CI & Build / Python tests (push) Successful in 59s
CI & Build / Build & push image (push) Successful in 1m7s

Merge kept the target's fields, unioned locations and tags, and trashed
the sources — recording nothing about what it absorbed. If a variant
handled an edge case the survivor doesn't, that difference left the
visible record entirely; recovering it meant knowing to go digging in
the trash.

The survivor now carries `merged_from`: a "**Merged from:** #2, #3" line
in the body for humans, and the same list in the `data` mirror for
queries, written from one value like every other field (#2087).

It accumulates rather than replaces — a target merged twice keeps both
histories — and skipped sources (cross-owner, per #231) are excluded, so
the record never claims to contain something it never absorbed.

Ordinary edits carry it forward. update_snippet recomposes body and
mirror from scratch, so an omission there would silently erase the
history on the next unrelated edit; that path is pinned by its own test,
including the pre-0070 case where the body line is the only copy.

Surfaced in the snippet detail view as a "Merged from" row — the view
renders parsed fields, not the raw body, so the body line alone would
have been invisible to the operator (rule #27).

Un-merge, the other half of #2087, stays open: restoring a source from
trash still doesn't strip its locations off the survivor, and what
partial un-merge should mean is a design question, not a coding one.
`merged_from` is the record that makes it tractable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLwAaV4DQEmVyn496HnEvt
This commit is contained in:
2026-07-27 15:23:41 -04:00
parent 9fa474b3c4
commit 0d396de215
6 changed files with 249 additions and 7 deletions
+40 -3
View File
@@ -235,20 +235,57 @@ def test_data_and_body_round_trip_to_the_same_fields():
# serializers get their own argument lists rather than a shared spread.
title = s.compose_title(name, when)
body = s.compose_body(code="const x = 1", language=lang, signature=sig,
when_to_use=when, locations=locs)
when_to_use=when, locations=locs, merged_from=[41, 42])
tags = s.compose_tags(lang)
from_body = s.snippet_fields(_Note(title=title, body=body, tags=tags))
from_data = s.snippet_fields(_Note(
title=title, body=body, tags=tags,
data=s.compose_data(name=name, when_to_use=when, signature=sig,
language=lang, locations=locs),
language=lang, locations=locs, merged_from=[41, 42]),
))
for key in ("name", "when_to_use", "signature", "language", "locations",
"repo", "path", "symbol", "code"):
"merged_from", "repo", "path", "symbol", "code"):
assert from_body[key] == from_data[key], key
# --- merge provenance (#2087) ------------------------------------------------
def test_normalize_merged_from_keeps_history_order_and_drops_junk():
"""Order is history, not sorting — earlier merges stay first."""
assert s._normalize_merged_from([9, 3, 9, "4", None, 0, -2, "x"]) == [9, 3, 4]
assert s._normalize_merged_from(None) == []
def test_compose_body_renders_merged_from_and_parse_reads_it_back():
body = s.compose_body(code="x = 1", merged_from=[12, 13])
assert "**Merged from:** #12, #13" in body
got = s.parse_snippet_fields("n — u", body, None)
assert got["merged_from"] == [12, 13]
# The code fence still comes last — provenance is header metadata.
assert body.rstrip().endswith("```")
def test_compose_body_omits_merged_from_when_there_is_none():
"""A snippet that was never merged says nothing about merging."""
assert "Merged from" not in s.compose_body(code="x = 1")
assert s.parse_snippet_fields("n — u", "```\nx = 1\n```", None)["merged_from"] == []
def test_compose_data_mirrors_merged_from():
assert s.compose_data(name="f", merged_from=[3, 3, 2])["merged_from"] == [3, 2]
assert "merged_from" not in s.compose_data(name="f")
def test_snippet_fields_prefers_the_data_columns_merged_from():
"""Same rule as every other mirrored field: `data` wins when it's populated,
so a hand-edited body can't silently rewrite the merge history."""
body = s.compose_body(code="x = 1", merged_from=[12])
note = _Note(title="n — u", body=body, tags=["snippet"],
data=s.compose_data(name="n", merged_from=[12, 13]))
assert s.snippet_fields(note)["merged_from"] == [12, 13]
def test_snippet_to_dict_includes_parsed_fields():
class FakeNote:
title = "debounce — rate-limit"