6db791965fa94bea430468459642cced855a2331
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6db791965f |
feat(snippets): near-duplicate finder — surface the sets worth merging
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 14s
CI & Build / integration (push) Successful in 36s
CI & Build / Python tests (push) Successful in 55s
CI & Build / Build & push image (push) Successful in 44s
#231's premise was unifying reusable things already scattered as one-offs. The create gate PREVENTS a new duplicate and merge_snippets CURES one you point it at, but nothing FOUND the duplicates already in the record — someone had to notice them by hand, which is the exact failure the Drafter exists to remove. One indexed self-join over note_embeddings, not an N² Python scan: pgvector's cosine distance is the same operator semantic search uses, so a similarity floor is a distance ceiling and the work stays in Postgres. `left.note_id < right.note_id` yields each unordered pair once and drops the self-pair that would otherwise dominate the ranking. Pairs are collapsed into merge SETS by connected components. Transitive on purpose: A~B plus B~C puts all three together even when A and C don't directly clear the bar, which is what merge actually does (it folds every source into one survivor). The cost is that a chain of mild resemblances can rope in a member that isn't really alike — so the UI presents a set as a proposal, shows the members, and never merges without a confirm. Two scope decisions worth naming: - OWN snippets only. merge_snippets requires one owner across the set, so surfacing someone else's would propose a merge that cannot be performed. The report is bounded by what the operator can act on, not what they can see. - Threshold defaults to 0.82, LOOSER than the write gate's 0.90, and is a setting rather than a constant (rule #25). The gate blocks a create and has to be unforgiving of noise; this only suggests a merge under review, so it must reach further or it would never surface the pairs the gate already let through — which are precisely the ones that accumulated. Fixes a real bug in the merge flow while wiring the UI: selectedList filtered the selection against the CURRENT PAGE, and doMerge derives its source ids from that list. A corpus-wide suggested group with off-page members would have rendered incomplete and silently merged only the visible subset. A group under review is now the authority for that list. Refs #2088 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs |
||
|
|
84c5c0dc81 |
fix(snippets): satisfy the parity guards the drift check tripped
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 52s
CI & Build / Build & push image (push) Successful in 45s
Two CI failures, both the guards working as intended rather than defects in them: - The MCP tool-name manifest and the REST route/service parity lists are explicit, so a new capability has to be added to both surfaces or the test fails. verify_snippet / verify_snippet_route / record_verification added, plus an assertion that the `verification` filter reaches both callers — a verdict an agent records must be visible to the human looking at the same corpus, or the two surfaces disagree about what's rotten (rule #33). - vue-tsc rejected two object-literal lookups keyed by the status union: `status` includes "ok" and "unverified", and an object literal has to enumerate every member even just to say "nothing to show for these". Typed as Record<string, string>, which is what the ?? "" fallback already assumed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs |
||
|
|
cca40affe4 |
feat(snippets): add notes.data JSONB — the indexed mirror of snippet fields
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 12s
CI & Build / integration (push) Successful in 25s
CI & Build / Python tests (push) Failing after 29s
CI & Build / Build & push image (push) Has been skipped
Milestone #232 step 1 (task #2081). Takes the enabler first rather than the write-path trigger: reverse lookup, drift checks and the duplicate finder all need to QUERY structured fields, and building them on body-regex first means writing them twice. #227 deferred this bag "unless body-convention ergonomics prove insufficient" — answering "which snippets live in this file?" by scanning every snippet and regexing its body is that condition being met. Migration 0070 adds `notes.data` (nullable JSONB) + a GIN index. The body is UNCHANGED and still what gets embedded and read by humans; `data` mirrors the same facts in a shape Postgres can index. Code is deliberately not copied into it — the body holds it, and duplicating a blob into the column we index around would be waste. - compose_data() builds the mirror, omitting empties so the column stays sparse - snippet_fields() prefers `data`, falling back to parsing the body. Rows written before 0070 have no `data` and are never backfilled, so a hand-edited body stays authoritative for them with no conversion deadline - create / update / merge all write body and mirror from the same merged field set, so the two can't drift; merge in particular has to grow the mirror with the survivor's location set or a merged snippet would be unfindable at the very call sites the merge just recorded Named `data`, not `metadata`, because that collides with SQLAlchemy's declarative Base.metadata — which is why the pre-0069 model had to map an awkward `entity_metadata` attribute. Not a revival of the column 0069 dropped: different name, different purpose, nothing reads the old shape. Two test fakes needed an explicit `data = None`: snippet_fields prefers `data` when truthy and an auto-MagicMock attribute is truthy, so every parsed field would have come back a MagicMock. Checked every fake reaching snippet code this time rather than waiting for CI (note 2109). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLwAaV4DQEmVyn496HnEvt |
||
|
|
3ffdbbc521 |
fix(acl): align MCP writes with the share model — editor edits, viewer doesn't
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Successful in 33s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Failing after 34s
CI & Build / Build & push image (push) Has been skipped
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
|
||
|
|
b33e2a79c6 |
fix(snippets): close the recall-surface gaps found reviewing the Drafter
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 11s
CI & Build / integration (push) Successful in 19s
CI & Build / Python tests (push) Successful in 43s
CI & Build / Build & push image (push) Successful in 1m7s
Four defects from the 2026-07-25 review of the recall (#227) and merge (#231) milestones. The theme: a snippet could be recorded but not fully corrected, and the agent and web surfaces had drifted apart. - #2076 language was mis-derived from the first caller tag, so a snippet created with tags and no language read that tag back as its language — corrupting the tag set and the code fence on the next update. Only the FIRST tag can carry the language, since compose_tags emits [language, "snippet", *caller]. - #2077 MCP update_snippet mapped "" to "unchanged", so no field could ever be cleared and no snippet detached from its project. Now an omitted field is left alone, an empty string clears, and project_id follows the -1 = detach convention. A service-level UNSET sentinel keeps None available as the clear. - #2078 surface parity: adds delete_snippet (MCP had none, so a wrong snippet could not be retired by the agent that recorded it), locations on MCP create and update, system_ids through the REST routes and the editor, and the near-duplicate gate on REST create with a "record it anyway" escape. - #2079 project scoping: list_snippets takes project_id through the service, the MCP tool and the REST route, defaulting to every project — reaching across projects is the point when the helper you need was written elsewhere. Sharing the list across owners is deliberately NOT in here: query_knowledge is shared with the Knowledge browse surface, so widening it changes behaviour well beyond snippets. Left open on #2079 for a scope decision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLwAaV4DQEmVyn496HnEvt |
||
|
|
85625de394 |
feat(scribe): merge_snippets — unify found one-off snippets into one canonical
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / integration (push) Successful in 38s
CI & Build / Python tests (push) Successful in 59s
CI & Build / Build & push image (push) Successful in 1m10s
Step 2 of the snippet-merge milestone (#231). The dedup gate only PREVENTS new near-duplicates; merge is the CURE for the ones already scattered. - services/snippets.py: merge_snippets(user_id, target_id, source_ids) — keep the target's scalar fields (name/when_to_use/signature/language/ code), union the sources' locations + extra tags onto it (so the survivor carries every call site as a location), trash the sources (recoverable), re-embed the survivor. Pure merge_snippet_fields() factored out for unit testing. Returns (survivor_note, merged_ids). - mcp/tools/snippets.py: merge_snippets(target_id, source_ids) tool (5th), and a create_snippet dedup-path nudge toward merge over a forced copy. - routes/snippets.py: POST /api/snippets/<id>/merge {source_ids} — share- aware (can_write target + every source, rule #78) with a same-owner guard (cross-owner merge is out of scope). - plugin reusing-code skill + MCP _INSTRUCTIONS: point found-duplicates at merge as the cure (rule #119 surfaces, not a Scribe rule). plugin.json 0.1.13 -> 0.1.14 in the same change (the #1040 marketplace-ship lesson). - Tests: pure merge-helper union/dedup; MCP tool (requires a source, survivor+merged_ids, not-found); route handler + 5-tool registration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pa2EsuB54BuWQ8GfJq9c7t |
||
|
|
1942913366 |
feat(scribe): add snippet recall — note_type='snippet' service + MCP tools
CI & Build / Python lint (push) Successful in 2s
CI & Build / integration (push) Successful in 20s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 50s
CI & Build / Build & push image (push) Successful in 1m7s
Record reusable functions/components once so they surface via the existing semantic search + title-first auto-inject, instead of re-solving as one-offs. A snippet is a Note with note_type='snippet' (no schema change): note_type is free-text, and semantic_search_notes never filters by type, so snippets join the recall/auto-inject pool the moment they're embedded. Structured fields (name/language/signature/location/when_to_use/code) are stored via a body convention — title = "name — when to use" (what auto-inject surfaces), language + "snippet" as tags, templated markdown body — keeping storage swappable later without changing the tool/UI contract. - services/snippets.py: compose/parse helpers + create/get/list/update wrappers over notes_svc (dedup + System association reused). - mcp/tools/snippets.py: list_snippets / create_snippet / get_snippet / update_snippet, registered in tools/__init__.py. - unit tests for the serialize/parse round-trip and the MCP tool surface. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pa2EsuB54BuWQ8GfJq9c7t |