Drafter reuse-recall (both halves) + the multi-user ACL work it exposed #78
Reference in New Issue
Block a user
Delete Branch "dev"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
14 commits, CI green on head
4b5d900(run 2901). Two bodies of work: the Drafter's reuse-recall layer, and the sharing/ACL corrections that reviewing it turned up.Drafter — reuse-recall
Milestones #227 (recall backend) and #231 (snippet merge), both complete.
A snippet is a note with
note_type='snippet'— no new table, no migration. It inherits embeddings, ACL, project/System association and the dedup gate, and joins the existing title-first auto-inject pool, so prior art can surface before a one-off gets written. Structured fields live in a body-convention (title =name — when to reach for it, which is exactly what a recall menu shows), swappable to a structured column later without touching the MCP/REST/UI contract.Ships with MCP tools, REST routes, a Snippets list/detail/editor UI, a
reusing-codeplugin skill, and merge — unifying scattered one-offs into one canonical record carrying every call site as a location.What reviewing it found
A review of both halves (2026-07-25) turned up four defects, all fixed here:
update_snippetmapped""to "unchanged", so no field could ever be cleared and no snippet detached from its projectlocationsfrom MCP, nosystem_idsfrom REST/UI, no dedup gate on REST createThe ACL work #2079 opened up
Fixing that last one exposed a real gap: list queries filtered on ownership alone while
get_note_permissionresolved shares properly, so a record shared with you could be opened by id but never found. Now there are two scopes, deliberately different:The split is a trust boundary. Anything appearing unasked reads as material you endorsed, so a one-off direct share waits until you go looking for it — which also means a shared Process is never installed as an auto-surfacing skill on your machine.
Everything handed to an agent or a UI now says whose it is (
shared/owner/permission),get_processexplicitly countermands its own follow-verbatim contract for a shared record, and both halves of a hybrid search see equally — previously a shared record was findable by wording but invisible by meaning (#2092).Writes follow the same model (#2112): editor/admin grants can edit, viewer grants cannot, and a read-only refusal says so rather than claiming not-found. Merge is stricter than update since it trashes sources. Two regressions found and fixed along the way: #2093 (lists returning records
get_*couldn't open) and the write/delete asymmetry.Scope is explicit per retrieval — the near-duplicate gate stays owner-only, because a verdict that blocks a create must not hinge on someone else's notes.
Verification
Per rule #11:
devgreen on4b5d900— analyzers, TypeScript typecheck, 403 unit tests, integration onpgvector/pg17(full0001→headmigration chain), image build. No migration in this range.Plugin
0.1.11 → 0.1.16; the marketplace serves frommain, so merging is what publishes thereusing-codeskill and the provenance guidance.Deliberately left open
Milestone #232 (recall hardening — write-path trigger, drift checks, duplicate finder, the deferred
dataJSONB column), #2080 (ampersands HTML-escaped on store, unrelated to this work), and Drafter half 2 — the design/component explorer.Option B, per the operator. Closes the last inconsistency from the ACL work. The agent path had drifted into an indefensible position: delete_snippet honoured editor shares (I made it share-aware so widening the read wouldn't let a VIEWER trash things) while update_snippet still resolved through the owner-only notes.get_note. So through an agent you could destroy a colleague's snippet but not improve it — and the refusal claimed "not found" for a record you could plainly open. Now update_snippet, merge_snippets and update_process all resolve the read scope and then require can_write_note, matching the REST routes and the sharing UI's own promise that viewer / editor / admin are distinct grants. A viewer grant is refused with the actual reason ("shared with you read-only — ask its owner for edit access, or record your own version"), because not-found would send an agent hunting for a missing id instead of recording its own copy. Authorised writes are performed as the OWNER, since the underlying note update is owner-scoped and a shared editor's own id would match nothing. Merge additionally requires each source to share the TARGET'S owner and to be writable by the caller — merging trashes the source, so read access isn't enough, and cross-owner merge stays out of scope (#231). Sources failing either test are skipped rather than half-merged. A record the caller cannot read at all still returns not-found rather than forbidden, so the error can't be used to confirm that an id exists. Also fixed _fake_snippet's missing user_id proactively — the same auto-MagicMock-reads-as-foreign trap that broke CI twice (see note 2109). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLwAaV4DQEmVyn496HnEvt