feat(snippets): body provenance — the cache-with-provenance half of the pointer model (#2688)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Failing after 21s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 53s
CI & Build / Build & push image (push) Successful in 28s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Failing after 21s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 53s
CI & Build / Build & push image (push) Successful in 28s
Decision #2686: the recorded location is the source of truth for a snippet's code; the stored body is a cache of it. data.provenance now records what the cache is a cache OF — commit_sha + fetched_at — as a carried JSONB field following the verification precedent, so no migration is needed and absence keeps today's exact semantics. The rules: provenance follows the code (fresh SHA restamps it, a code edit without one drops it, a metadata edit carries it); writes ABOUT the code carry it — record_verification rebuilds data from scratch and would otherwise erase it silently; an ok verdict at a known commit restamps it, since the checker just proved the cache matches the source there. verify_snippet verdicts also record the commit they ran at, making "the repo moved on since the check" computable once the forge integration lands. create/update/verify MCP tools take commit_sha (git rev-parse HEAD — free for any session with a checkout). Unit tests pin the compose/carry logic; real-Postgres integration tests run create→verify→update end-to-end (#2663: DB paths get no mocked-only coverage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -342,7 +342,7 @@ def parse_snippet_fields(
|
||||
# and copying a blob into the column we index *around* would be pure weight.
|
||||
_DATA_FIELDS = (
|
||||
"name", "when_to_use", "signature", "language", "locations", "merged_from",
|
||||
"verification",
|
||||
"verification", "provenance",
|
||||
)
|
||||
|
||||
# --- drift check (#2086) -----------------------------------------------------
|
||||
@@ -398,6 +398,7 @@ def compose_verification(
|
||||
detail: str = "",
|
||||
path: str = "",
|
||||
checked_at: str = "",
|
||||
commit_sha: str = "",
|
||||
) -> dict:
|
||||
"""Build the `data.verification` record. Unknown statuses are rejected here
|
||||
rather than stored, so the filter never has to cope with a typo'd status."""
|
||||
@@ -414,9 +415,36 @@ def compose_verification(
|
||||
out["detail"] = detail.strip()
|
||||
if (path or "").strip():
|
||||
out["path"] = path.strip()
|
||||
# The repo commit the working tree was at when the check ran (#2688). The
|
||||
# code_sha above expires a verdict when the RECORD is edited; this makes
|
||||
# "the REPO moved on since the check" computable too, once the forge
|
||||
# integration can compare it against the current head.
|
||||
if (commit_sha or "").strip():
|
||||
out["commit_sha"] = commit_sha.strip()
|
||||
return out
|
||||
|
||||
|
||||
def compose_provenance(*, commit_sha: str, fetched_at: str = "") -> dict | None:
|
||||
"""Build the `data.provenance` record: which commit the cached body was
|
||||
read at, and when.
|
||||
|
||||
This is the pointer-model half of decision #2686 — the recorded location
|
||||
is the source of truth for the code and the stored body is a CACHE of it.
|
||||
Provenance says what that cache is a cache OF, so a reader (and later the
|
||||
forge fetch, step 5 of milestone 288) can judge staleness instead of
|
||||
guessing. Absent provenance is valid and means exactly what every snippet
|
||||
meant before this existed: a body captured by hand at an unknown point.
|
||||
"""
|
||||
sha = (commit_sha or "").strip()
|
||||
if not sha:
|
||||
return None
|
||||
return {
|
||||
"commit_sha": sha,
|
||||
"fetched_at": (fetched_at or "").strip()
|
||||
or datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def verification_view(note, fields: dict) -> dict:
|
||||
"""The verification readout for one snippet, including whether it's expired.
|
||||
|
||||
@@ -434,6 +462,7 @@ def verification_view(note, fields: dict) -> dict:
|
||||
"checked_at": stored.get("checked_at"),
|
||||
"detail": stored.get("detail"),
|
||||
"path": stored.get("path"),
|
||||
"commit_sha": stored.get("commit_sha"),
|
||||
# What the operator actually wants to know: is there something to fix?
|
||||
# An expired verdict counts as "needs looking at" even if it said ok,
|
||||
# since the code it blessed is not the code that's there now.
|
||||
@@ -451,6 +480,7 @@ def compose_data(
|
||||
locations: list[dict] | None = None,
|
||||
merged_from: list[int] | None = None,
|
||||
verification: dict | None = None,
|
||||
provenance: dict | None = None,
|
||||
) -> dict:
|
||||
"""Build the `notes.data` mirror of a snippet's structured fields.
|
||||
|
||||
@@ -479,6 +509,12 @@ def compose_data(
|
||||
# either: the verdict's code_sha expires it on read if the code moved on.
|
||||
if verification:
|
||||
out["verification"] = verification
|
||||
# Also carried: what commit the cached body was read at (#2688). The caller
|
||||
# owns the live-or-die rule — update_snippet drops it when the code changes
|
||||
# without a fresh SHA, because keeping it would claim the new body came
|
||||
# from the old commit.
|
||||
if provenance:
|
||||
out["provenance"] = provenance
|
||||
# The current code's fingerprint — NOT the code, which stays in the body
|
||||
# (see _DATA_FIELDS). Its only job is to make "this verdict has expired"
|
||||
# expressible in SQL: a jsonpath can compare `@.verification.code_sha` to
|
||||
@@ -616,10 +652,15 @@ async def create_snippet(
|
||||
locations: list[dict] | None = None,
|
||||
tags: list[str] | None = None,
|
||||
project_id: int | None = None,
|
||||
commit_sha: str = "",
|
||||
):
|
||||
"""Create a snippet note (embedded on create for immediate recall). Returns
|
||||
the created Note. Pass ``locations`` for the multi-location case; the single
|
||||
``repo``/``path``/``symbol`` are the one-location shorthand."""
|
||||
``repo``/``path``/``symbol`` are the one-location shorthand.
|
||||
|
||||
``commit_sha`` stamps the body's provenance — the commit the recording
|
||||
session read the code at (#2688). Optional: absent means what it always
|
||||
meant, a body captured at an unknown point."""
|
||||
locations = resolve_locations(repo, path, symbol, locations)
|
||||
note = await notes_svc.create_note(
|
||||
user_id,
|
||||
@@ -636,6 +677,7 @@ async def create_snippet(
|
||||
data=compose_data(
|
||||
name=name, when_to_use=when_to_use, signature=signature,
|
||||
language=language, code=code, locations=locations,
|
||||
provenance=compose_provenance(commit_sha=commit_sha),
|
||||
),
|
||||
)
|
||||
return note
|
||||
@@ -714,11 +756,17 @@ async def update_snippet(
|
||||
locations: list[dict] | None = None,
|
||||
tags: list[str] | None = None,
|
||||
project_id: int | None | object = UNSET,
|
||||
commit_sha: str | None = None,
|
||||
):
|
||||
"""Partial update: only fields passed (not None) change. Re-serializes the
|
||||
merged field set back into title/body/tags. Returns the Note, or None if the
|
||||
id isn't a snippet the caller can see.
|
||||
|
||||
``commit_sha`` restamps the body's provenance (#2688). It lives or dies
|
||||
with the code: passed → restamped at that commit; code changed without it →
|
||||
dropped, because keeping it would claim the new body came from the old
|
||||
commit; code untouched → carried.
|
||||
|
||||
Share-aware (rule #47/#78): resolves the read scope, then requires WRITE —
|
||||
so an editor/admin grant lets the holder edit, and a viewer grant does not.
|
||||
Raises PermissionError when the caller can read but not write, because "not
|
||||
@@ -761,6 +809,17 @@ async def update_snippet(
|
||||
else:
|
||||
merged_locations = cur["locations"]
|
||||
|
||||
# Provenance follows the code (#2688): a fresh SHA restamps it; a code
|
||||
# change without one drops it; an edit that leaves the code alone carries
|
||||
# it. Order matters — the explicit SHA wins even when the code changed,
|
||||
# because that is precisely the caller saying where the new body came from.
|
||||
if commit_sha is not None and commit_sha.strip():
|
||||
provenance = compose_provenance(commit_sha=commit_sha)
|
||||
elif code is not None and code != (cur.get("code") or ""):
|
||||
provenance = None
|
||||
else:
|
||||
provenance = cur.get("provenance")
|
||||
|
||||
fields: dict = {
|
||||
"title": compose_title(merged["name"], merged["when_to_use"]),
|
||||
"body": compose_body(
|
||||
@@ -782,6 +841,7 @@ async def update_snippet(
|
||||
# the code, the verdict's code_sha stops matching and it reads as
|
||||
# unverified from here on — no invalidation branch to get wrong.
|
||||
verification=merged.get("verification"),
|
||||
provenance=provenance,
|
||||
),
|
||||
}
|
||||
# Recompute tags: keep any non-language, non-marker tags the note already had
|
||||
@@ -808,6 +868,7 @@ async def record_verification(
|
||||
status: str,
|
||||
detail: str = "",
|
||||
path: str = "",
|
||||
commit_sha: str = "",
|
||||
):
|
||||
"""Record the result of a drift check against the snippet's source.
|
||||
|
||||
@@ -836,6 +897,7 @@ async def record_verification(
|
||||
checked_code_sha=code_sha(fields.get("code") or ""),
|
||||
detail=detail,
|
||||
path=path or fields.get("path") or "",
|
||||
commit_sha=commit_sha,
|
||||
)
|
||||
# Rebuilt from the CURRENT stored fields plus the new verdict, so recording a
|
||||
# check can't quietly rewrite anything else about the record. Note the body
|
||||
@@ -850,6 +912,15 @@ async def record_verification(
|
||||
locations=fields.get("locations") or [],
|
||||
merged_from=fields.get("merged_from") or [],
|
||||
verification=verification,
|
||||
# An "ok" verdict at a known commit IS a provenance claim — the checker
|
||||
# just established that the cached body matches the source there — so
|
||||
# it restamps. Any other verdict carries what was known: a verdict is
|
||||
# about the code, not a change to it, and must not erase it (#2688).
|
||||
provenance=(
|
||||
compose_provenance(commit_sha=commit_sha)
|
||||
if status == VERIFY_OK and (commit_sha or "").strip()
|
||||
else fields.get("provenance")
|
||||
),
|
||||
)
|
||||
return await notes_svc.update_note(note.user_id, snippet_id, data=data)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user