#4222 fixed one end of this defect — the extractors that decide what a payload
DEFINES now blank comment and string spans before any line matcher runs. This
is the other end: `scribe_local_dups`, which decides which OTHER files already
define that name, and which was still a plain `git grep`.
A grep sees lines, not spans, so the sentence
class with only modifier rules is a deletion that went half-way.
— real prose from a module docstring in this repo — matched the arm's pattern
for `name=with`. Writing a genuine `change`, `beside` or `wrapped` would be
told it already existed, and pointed at a docstring.
EVERY HIT IS NOW CONFIRMED by running `scribe_defs` over the candidate file and
keeping only names it actually reports. That is the only check that cannot
disagree with the other end of the pipe, which is the whole point.
MEASURED, NOT ASSUMED — both numbers the task reasoned from turned out wrong.
- WHAT IT REMOVES, across 141 payload files of this repo: 15 of 210 report
lines. Every one a string literal, a comment, a TypeScript `import { type
Foo }`, or Vue's `const emit = defineEmits()` boilerplate. No real
definition was lost. Where a name had both — `create_note` — the phantom
in a test's `shape_form("async def create_note(...)")` argument dropped out
and the definition in services/notes.py stayed. `with` went from four files
to none, which is the correct answer: nothing here defines it, and it is a
keyword in several of these languages.
- WHAT IT COSTS: mean 193ms -> 222ms, worst 569ms -> 572ms. The task feared
"over a second added to a PreToolUse hook" from 48 confirmations. It is
about 15%, because the arm was already dominated by its twelve `git grep`
calls, and because confirmation runs once per DISTINCT candidate file
rather than once per (name, file) pair. A deliberately pathological payload
— nine names that are ordinary English words — reaches 28 distinct files
and 947KB; `scribe_defs` runs at ~33ms per 250KB.
THE CANDIDATE CAP IS RAISED FROM FOUR TO TWELVE, and that is load-bearing.
Confirmation REMOVES hits, so capping before it runs lets phantom matches crowd
a real definition out of the window — hits dropped before anyone looked at
them, which is #4042's bug in a new place. The display cap stays at four and
now applies to CONFIRMED hits, which is where a cap belongs. #4042's own `||
true` inside the substitution is untouched, and its regression case still
passes.
The task's own advice not to fix this by tightening the grep pattern is
followed and written down: requiring `(` or `{` or `:` after the name rejects
`class with only…` and also `class Foo extends Bar {`, `class Foo : Base()` and
`type Foo struct {`. This arm exists because it works with no server, no index
and no binding (#2280, #2682), which makes a miss here invisible — a visible
false positive is the better failure.
Guards: tests/test_hook_duplicate_confirmation.py, against real git repos
because what is pinned is the interaction between `git grep`, `head` and the
extractor. Seven of the eight fail against the previous implementation; the
eighth is #4042's regression case, whose job is to keep passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy