refactor(routes): one supersession seam for REST and MCP; PUT/PATCH notes share a handler; shared mask/not-found/caller helpers (#2829, milestone 296 area 5)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Failing after 37s
CI & Build / Build & push image (push) Skipped

Reading the 28 route modules against each other and against the MCP tools:
- routes/notes.py carried a PUT and a PATCH handler that were the same
  function minus the supersedes contract on one of them — one handler now
  serves both verbs, so both carry it.
- The two _attach_supersession copies (REST + MCP) become
  supersession_svc.attach_relations(uid, note_id, data, hint=) — the seam
  the two surfaces must agree through; only the agent surface adds the
  one-sentence reading hint.
- Three local _uid() wrappers over g.user.id → scribe.auth.get_current_user_id
  like every other module; design_systems' private _not_found → routes.utils.
  not_found; the four "********" literals → settings_svc.SECRET_MASK with the
  read/write contract written once.
- routes/plugin.py: the project_id/repo resolution block and the
  comma-separated id parse were copied into three endpoints — _project_scope()
  and _int_list() now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 11:23:47 -04:00
co-authored by Claude Fable 5
parent c211e12b61
commit 64c641ce80
11 changed files with 155 additions and 212 deletions
+3 -27
View File
@@ -62,30 +62,6 @@ async def list_notes(
return {"notes": [n.to_dict() for n in rows], "total": total}
async def _attach_supersession(uid: int, note_id: int, data: dict) -> None:
"""Add both directions of the supersession relation to a note payload.
Both, because they answer different questions and only one of them is
obvious. `supersedes` is what the author claimed. `superseded_by` is what a
READER needs and what the note itself cannot know — a stale record handed
over without that marker gets acted on confidently, which is worse than
never surfacing it.
Omitted entirely when empty, so an ordinary note's payload doesn't grow two
permanently-empty lists. A field that always says nothing trains readers to
skip fields, which is the lesson `consolidated_at` cost us (#2483).
"""
rel = await supersession_svc.get_relations(uid, note_id)
if rel["supersedes"]:
data["supersedes"] = rel["supersedes"]
if rel["superseded_by"]:
data["superseded_by"] = rel["superseded_by"]
data["superseded_note"] = (
"A later note claims to bring this up to date — see superseded_by. "
"Read this as what was true when written, and check the newer one "
"before acting on it."
)
async def get_note(note_id: int) -> dict:
"""Fetch the full content of a single Scribe note by its ID.
@@ -113,7 +89,7 @@ async def get_note(note_id: int) -> dict:
# snippets would leave those permanently at zero pulls and make them look
# like dead weight next to snippets that merely had a counter (#2085).
record_pulled(user_id=uid, note_id=int(note.id), source="mcp_get_note")
await _attach_supersession(uid, note_id, out)
await supersession_svc.attach_relations(uid, note_id, out, hint=True)
await systems_tools.attach_systems(
uid, getattr(note, "user_id", uid) or uid, out, note.id, note.project_id
)
@@ -186,7 +162,7 @@ async def create_note(
raise ValueError(str(exc)) from exc
data = note.to_dict()
await systems_tools.attach_systems(uid, uid, data, note.id, project_id or None)
await _attach_supersession(uid, note.id, data)
await supersession_svc.attach_relations(uid, note.id, data, hint=True)
return data
@@ -237,7 +213,7 @@ async def update_note(
await systems_tools.attach_systems(
uid, getattr(note, "user_id", uid) or uid, data, note_id, note.project_id
)
await _attach_supersession(uid, note_id, data)
await supersession_svc.attach_relations(uid, note_id, data, hint=True)
return data