Find the enclosing watcher by brace depth, not by looking back

gospel reviewed the shapes I taught this gate an hour ago and caught a real
bug in it: deciding whether a call site sits inside a reactive wrapper by
scanning the preceding 260 characters. Their GiDrawer.vue opens its
computed( at line 35 with the call sites at 95-100, and a lookback window
reported six false positives on working code — they nearly "fixed" code that
was right.

Measured on the same shape here: 2,600 characters between the watch( and the
call, ten times the window. A window cannot answer a containment question.
The check now walks backwards counting bracket depth, so it finds the wrapper
that actually encloses the offset however far above it sits, and cannot be
fooled by a closed wrapper that merely precedes it.

Two cases added, and the first is proven non-vacuous: reverting to the
lookback fails it (21/22), restoring brace depth passes (22/22). The second
is gospel's model.js:46 — a doc comment quoting a translator call, which must
not read as code.

This is the second correction to these rules in an hour. The first draft
flagged five production files by asking whether a function body merely
mentions t(); this one flagged correct code by asking what sits nearby
instead of what encloses. Both were found by someone else reading the rule
against their own plugin, which is the only reason either was caught before
landing.
This commit is contained in:
Doxa Maps 2026-09-29 08:56:17 +00:00
parent fc369cbd9d
commit fddae84821

View file

@ -150,6 +150,42 @@ const DEAD_LOCALE_GETTER = /\b(getRuntimeLocale|detectLocale|localeFromUrl)\s*\(
* source under a local name. A signal that looked for the literal `locale` would have flagged the
* FIX as the defect, which is the worst thing a gate can do to someone who just did the work.
*/
/**
* Is offset `at` inside the parentheses of a computed()/watch()/watchEffect() that tracks the locale?
*
* ⚠ BRACE DEPTH, NOT A LOOKBACK WINDOW (gospel, GOS-FROZEN-SR). The first version of this asked
* whether a wrapper appeared in the preceding 260 characters. gospel hit exactly this in GiDrawer.vue
* — the enclosing `computed(` opens at line 35 and the call sites are at 95-100 — and reported SIX
* false positives on working code, nearly "fixing" it. Measured on the same shape here: 2,600
* characters between the `watch(` and the call, ten times the window. A window cannot answer a
* containment question; only matching brackets can.
*
* Walks backwards counting bracket depth, so it finds the wrapper that actually ENCLOSES this
* offset however far above it sits, and cannot be fooled by a closed wrapper that merely precedes it.
*/
function enclosingReactiveWrapper(code, at) {
let depth = 0
for (let i = at; i >= 0; i--) {
const c = code[i]
if (c === ')' || c === '}' || c === ']') depth++
else if (c === '(' || c === '{' || c === '[') {
if (depth > 0) { depth--; continue }
// an unclosed opener: is it the paren of a reactive wrapper call?
if (c === '(') {
const head = code.slice(Math.max(0, i - 40), i)
const m = /\b(computed|watch|watchEffect)\s*$/.exec(head)
if (m) {
// watchEffect always tracks what it reads; watch/computed must name a locale source.
if (m[1] === 'watchEffect') return true
const scope = code.slice(i, Math.min(code.length, at))
if (REACTIVE_LOCALE.test(scope)) return true
}
}
}
}
return false
}
function findStoredTreeFreeze(code) {
const out = []
// functions whose body calls a translator
@ -180,10 +216,9 @@ function findStoredTreeFreeze(code) {
// is ANY call site assigned to a ref/let, and is ANY inside a reactive wrapper or locale watcher?
let stored = false, live = false
for (const i of sites) {
const before = code.slice(Math.max(0, i - 260), i)
const line = code.slice(code.lastIndexOf('\n', i) + 1, code.indexOf('\n', i))
if (/\.value\s*=|^\s*(?:let|const)\s+\w+\s*=/.test(line)) stored = true
if (/\b(computed|watch|watchEffect)\s*\(/.test(before) && (REACTIVE_LOCALE.test(before) || /watchEffect/.test(before))) live = true
if (enclosingReactiveWrapper(code, i)) live = true
}
if (stored && !live) {
const line = code.slice(0, m.index).split('\n').length
@ -265,6 +300,14 @@ const CASES = [
// calls a translator once for an error string and returns something unrelated.
['GOOD fetcher that merely mentions t()',
() => scanSource("function fetchPhotos(g){ const e = t('no_photo'); return new Map() }\nconst p = fetchPhotos(x)").filter((f) => f.call === 'stored-tree').length, 0],
// ── gospel (GOS-FROZEN-SR): the wrapper can open FAR above the call site ──────────────────────
// Their GiDrawer.vue has computed( at line 35 and the call sites at 95-100. A lookback window
// reported SIX false positives on working code; this fixture is ~2,600 chars between the two.
['GOOD watcher opens 40 lines above the call site',
() => scanSource("function opts(){ return { title: t('poster.zone_deaf') } }\nwatch(() => packetLang(), () => {\n // padding 0\n // padding 1\n // padding 2\n // padding 3\n // padding 4\n // padding 5\n // padding 6\n // padding 7\n // padding 8\n // padding 9\n // padding 10\n // padding 11\n // padding 12\n // padding 13\n // padding 14\n // padding 15\n // padding 16\n // padding 17\n // padding 18\n // padding 19\n // padding 20\n // padding 21\n // padding 22\n // padding 23\n // padding 24\n // padding 25\n // padding 26\n // padding 27\n // padding 28\n // padding 29\n // padding 30\n // padding 31\n // padding 32\n // padding 33\n // padding 34\n // padding 35\n // padding 36\n // padding 37\n // padding 38\n // padding 39\n nodes.value = build(opts())\n})").filter((f) => f.call === 'stored-tree').length, 0],
// ⚠ gospel's model.js:46 matched only because a DOC COMMENT quotes a translator call.
['GOOD a doc comment that quotes t()',
() => scanSource("// returns { title: t('a.b') } for each zone\nfunction opts(){ return { id: 1 } }\nnodes.value = build(opts())").filter((f) => f.call === 'stored-tree').length, 0],
['BAD computed reads only a dead locale getter',
() => scanSource("const x = computed(() => zones.map(z => title(z, getRuntimeLocale())))").filter((f) => f.call === 'dead-locale-getter').length, 1],
['GOOD computed reads locale.value first',