Commit Graph
3 Commits
Author SHA1 Message Date
bvandeusen 24d071619b fix(dedup): compare the artefact, not the prose describing it
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 48s
CI & Build / Build & push image (push) Successful in 25s
The snippet gate was reading the wrong field, and #2464's UI recipes made it
measurable in both directions at once:

  .btn-danger vs .btn-danger-outline   0.92   siblings, BLOCKED
  .btn-primary re-recorded verbatim
  under a different name              <0.90   a literal copy, ALLOWED

The second is what settles it. Identical code at an identical repo·path·symbol
sailed through because the description differed, while two deliberately
parallel variants were refused because theirs did not. A snippet's embedded
document is mostly prose ABOUT the code, so no threshold fixes this: lowering
it blocks more siblings, raising it admits more copies.

So structure decides. Two exact signals, both index-served off the notes.data
mirror that already exists, no migration and no backfill:

  location  the same named thing in the same file. Requires BOTH path and
            symbol — a path alone is a directory of artefacts, and matching on
            it would refuse every second recipe from one stylesheet.
  code      byte-identical code anywhere, via the same fingerprint the drift
            check uses.

The semantic arm survives as a backstop for a genuine reword that shares
neither, raised to 0.96 so it sits above the 0.92 band where real variants
live. Structural hits say what they matched instead of hedging with "similar",
and point at merge rather than update — two records of one artefact is what
merge exists to fold back together.

find_duplicate_snippets gets the same correction: pairs where both snippets
name a symbol, name DIFFERENT symbols, and hold different code are variants,
not copies. Without it a design system's button family reports as one merge
set — eight recipes, every direct pair over the floor, top score 0.92, one
click from collapsing a component family. The cost is real and stated in the
code: a helper recorded twice under two names no longer reports. That trade
favours the report being usable, and same-symbol and unnamed duplicates — how
re-recording usually looks — still surface. The filter fails open, so a lookup
failure degrades to the old unfiltered report rather than to a reassuring
empty one.

resolve_locations extracted: compose_body, create_snippet and now the gate each
had their own copy of the repo/path/symbol shorthand fallback, and the gate is
the one where a disagreement would mean matching a location the record won't be
stored with. Applied to both create surfaces (#33) — the web UI must not be the
way to record what the agent was stopped from writing.

Refs #2518, #2464
2026-08-06 11:31:02 -04:00
bvandeusenandClaude Opus 4.8 dd1fc2d506 feat(mcp): extend dedup gate to create_rule / create_project_rule
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 53s
CI & Build / Build & push image (push) Successful in 52s
Completes the Phase 5 follow-up: rules now get the same update-over-create
gate. Title-based only (rules aren't a semantic-retrieval/RAG surface), scoped
to the same topic (rulebook rule) or same project (project rule). force=true
overrides; fail-open like the note/task gate.

Deferred-item decisions (operator): REST/web gating SKIPPED (kept MCP-only —
humans rarely double-create and a hard block needs UI affordance); orphan scope
kept orphan↔orphan (no change). So this rule gate is the only remaining build.

- services/dedup.py: find_duplicate_rule(title, topic_id|project_id).
- create_rule + create_project_rule: force param + gate.
- tests: rule title match, scope-required guard, tool gate (block + force).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:43:17 -04:00
bvandeusenandClaude Opus 4.8 322cbc3b5e feat(mcp): Phase 5 — write-time near-duplicate gate (update-over-create)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Failing after 42s
CI & Build / Build & push image (push) Has been skipped
#755 Phase 5. create_note / create_task now BLOCK a near-duplicate instead of
silently inserting: they return {"duplicate": true, "existing_id", message}
pointing at the record to UPDATE. Fights store bloat and stale competing copies
that semantic search (RAG) would otherwise resurface for reconciliation. A
force=true override creates anyway for genuinely-distinct records.

- services/dedup.py: find_duplicate_note — two signals, scoped to owner + same
  project + same kind: (1) normalized-title exact match (cheap, always); (2)
  semantic cosine ≥ 0.90 but ONLY when body ≥ 200 chars (short/title-only
  embeddings false-positive — the pre-pivot lesson). Project-less (orphan)
  records compare only to other orphans on BOTH signals (orphan_only on the
  semantic call) — they're not matched across every project.
- Gate wired into the MCP create_note/create_task tools (the LLM write path)
  with force override; _INSTRUCTIONS documents the duplicate response + force.
- Opt-in by design: the service helper is only called from the interactive
  create tools. Internal/programmatic creates (recurrence spawn, imports) go
  straight through services.create_note and are NOT gated — a recurring task
  spawning its next same-titled instance must not be blocked.
- Scope v1: MCP tools only. REST/web (human CRUD, needs a UI affordance) and
  create_rule (not a RAG surface; _INSTRUCTIONS already steer it) are follow-ups.
- tests: dedup service (title/semantic/body-gate/type-filter) + tool gate
  (blocks, force bypasses) for notes and tasks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:21:35 -04:00