fini-proof 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1066 @@
1
+ // PROVENANCE — ported, not rewritten.
2
+ // source : finipe/scripts/ci/check-vacuous-spec.mjs (EOS F4 vacuous-spec detector)
3
+ // commit : finipe 4ac53b367b348d23d000629cb14e19633beb9613 (branch claude/aeos-factory-batch-2026-09-26;
4
+ // file last changed in 04152854b; rule families from 4de27d974)
5
+ // kept : lines 159-1210 verbatim (masking, block finder, all 11 rule families, helper resolution).
6
+ // changed: Finipe range resolution (resolveBase/changedSpecFiles) removed — the fini-proof runner owns scope;
7
+ // ROOT is settable (setRoot) instead of the Finipe repo root; CLI/self-test/guard dropped
8
+ // (self-test cases live on as negative controls in src/engines/hollow-tests.mjs + test/).
9
+ import { existsSync, readFileSync } from 'node:fs'
10
+ import path from 'node:path'
11
+
12
+ let ROOT = process.cwd()
13
+ export function setRoot(r) { ROOT = r }
14
+
15
+ // `}` `(` `)` that only exists inside text. Same technique this repo already
16
+ // uses in backend/scripts/check-super-admin-root-only.ts's literals() /
17
+ // backend/scripts/lib/strip-ts-comments.ts, reimplemented in plain .mjs here
18
+ // because this gate must run without ts-node (host law §2 — no toolchain, no
19
+ // jest, on the Mac).
20
+ // ─────────────────────────────────────────────────────────────────────────
21
+ function regexMayStart(source, i) {
22
+ let k = i - 1
23
+ while (k >= 0 && (source[k] === ' ' || source[k] === '\t')) k--
24
+ if (k < 0) return true
25
+ const p = source[k]
26
+ if ('(,=:[!&|?{};\n+-*%<>~^'.includes(p)) return true
27
+ const word = /([A-Za-z_$][\w$]*)$/.exec(source.slice(Math.max(0, k - 12), k + 1))
28
+ return !!word && ['return', 'typeof', 'case', 'in', 'of', 'delete', 'void', 'throw', 'new', 'else', 'do', 'await', 'yield'].includes(word[1])
29
+ }
30
+
31
+ export function maskStringsAndComments(source) {
32
+ const out = source.split('')
33
+ const n = out.length
34
+ let i = 0
35
+ while (i < n) {
36
+ const c = source[i]
37
+ if (c === '/' && source[i + 1] === '/') {
38
+ let j = i
39
+ while (j < n && source[j] !== '\n') { out[j] = ' '; j++ }
40
+ i = j
41
+ continue
42
+ }
43
+ if (c === '/' && source[i + 1] === '*') {
44
+ out[i] = ' '; out[i + 1] = ' '
45
+ let j = i + 2
46
+ while (j < n && !(source[j] === '*' && source[j + 1] === '/')) {
47
+ if (source[j] !== '\n') out[j] = ' '
48
+ j++
49
+ }
50
+ if (j < n) { out[j] = ' '; out[j + 1] = ' '; j += 2 }
51
+ i = j
52
+ continue
53
+ }
54
+ // REGEX LITERAL (factory batch 2026-09-26): `/\{([^}]*)\}/` holds braces the
55
+ // bracket matcher counted, so a test whose body carried such a literal was cut
56
+ // short at the first unbalanced `}` and read as NO_ASSERTION. A `/` starts a
57
+ // regex (not a division) when the previous significant character cannot end an
58
+ // operand, or the previous word is `return`/`typeof`/…; its contents — including
59
+ // a character class, where `/` does not end the literal — are blanked like a
60
+ // string's.
61
+ if (c === '/' && source[i + 1] !== '/' && source[i + 1] !== '*' && regexMayStart(source, i)) {
62
+ let j = i + 1
63
+ let inClass = false
64
+ let closed = false
65
+ while (j < n && source[j] !== '\n') {
66
+ const d = source[j]
67
+ if (d === '\\') { out[j] = ' '; if (j + 1 < n && source[j + 1] !== '\n') out[j + 1] = ' '; j += 2; continue }
68
+ if (d === '[') inClass = true
69
+ else if (d === ']') inClass = false
70
+ else if (d === '/' && !inClass) { closed = true; break }
71
+ out[j] = ' '
72
+ j++
73
+ }
74
+ if (closed) { i = j + 1; continue }
75
+ // not a regex after all (no closing `/` on the line): undo, treat as an operator
76
+ for (let k = i + 1; k < j; k++) out[k] = source[k]
77
+ i++
78
+ continue
79
+ }
80
+ if (c === '"' || c === "'" || c === '`') {
81
+ const quote = c
82
+ out[i] = ' '
83
+ let j = i + 1
84
+ while (j < n && source[j] !== quote) {
85
+ if (source[j] === '\\') {
86
+ if (source[j] !== '\n') out[j] = ' '
87
+ j++
88
+ if (j < n && source[j] !== '\n') out[j] = ' '
89
+ j++
90
+ continue
91
+ }
92
+ if (source[j] !== '\n') out[j] = ' '
93
+ j++
94
+ }
95
+ if (j < n) { out[j] = ' '; j++ }
96
+ i = j
97
+ continue
98
+ }
99
+ i++
100
+ }
101
+ return out.join('')
102
+ }
103
+
104
+ function lineAt(source, idx) {
105
+ let line = 1
106
+ for (let i = 0; i < idx && i < source.length; i++) if (source[i] === '\n') line++
107
+ return line
108
+ }
109
+
110
+ /** Find the index of the `{`/`(` matching the one at `openIdx`, scanning the
111
+ * MASKED text so nothing inside a string/comment can desync the count. */
112
+ function matchBracket(masked, openIdx, openCh, closeCh) {
113
+ let depth = 0
114
+ for (let i = openIdx; i < masked.length; i++) {
115
+ if (masked[i] === openCh) depth++
116
+ else if (masked[i] === closeCh) {
117
+ depth--
118
+ if (depth === 0) return i
119
+ }
120
+ }
121
+ return -1
122
+ }
123
+
124
+ // ─────────────────────────────────────────────────────────────────────────
125
+ // Block extraction
126
+ // ─────────────────────────────────────────────────────────────────────────
127
+ const ACTIVE_RE = /\b(?:it|test|fit|ftest)\s*\(/g
128
+
129
+ /**
130
+ * The test callback's BODY inside the call `(`…`)` at [callOpen, callClose] of the
131
+ * masked source → { start, end } (exclusive end), or null when no function literal.
132
+ *
133
+ * FALSE-POSITIVE CLASS (factory batch 2026-09-26, measured): this used to take the
134
+ * FIRST `{` among the call's arguments as the body. For an expression-bodied arrow —
135
+ * `it('x', async () => expect(await f({ a: 1 })).toBe(2))` — that `{` is an object
136
+ * literal INSIDE the assertion, so the "body" was `a: 1` and a real assertion read as
137
+ * NO_ASSERTION. The body is now the callback's own: after `=>` either its `{…}` block
138
+ * or, for an expression body, the expression itself; for a `function (…) {…}` the
139
+ * block after its parameter list.
140
+ */
141
+ function functionBody(masked, callOpen, callClose) {
142
+ const args = masked.slice(callOpen + 1, callClose)
143
+ // top-level `=>` of the callback (depth 0 relative to the call's own parens)
144
+ let depth = 0
145
+ for (let i = 0; i < args.length - 1; i++) {
146
+ const ch = args[i]
147
+ if (ch === '(' || ch === '[' || ch === '{') depth++
148
+ else if (ch === ')' || ch === ']' || ch === '}') depth--
149
+ else if (depth === 0 && ch === '=' && args[i + 1] === '>') {
150
+ let p = i - 1
151
+ while (p >= 0 && /\s/.test(args[p])) p--
152
+ let params = ''
153
+ if (args[p] === ')') { const o = matchBracketBack(args, p, '(', ')'); if (o !== -1) params = args.slice(o + 1, p) }
154
+ else { let q = p; while (q >= 0 && /[\w$]/.test(args[q])) q--; params = args.slice(q + 1, p + 1) }
155
+ let j = i + 2
156
+ while (j < args.length && /\s/.test(args[j])) j++
157
+ if (args[j] === '{') {
158
+ const open = callOpen + 1 + j
159
+ const close = matchBracket(masked, open, '{', '}')
160
+ return close === -1 ? null : { start: open + 1, end: close, params }
161
+ }
162
+ // expression body: up to the next top-level `,` (a timeout argument) or the call's end
163
+ let d = 0
164
+ let k = j
165
+ for (; k < args.length; k++) {
166
+ const c = args[k]
167
+ if (c === '(' || c === '[' || c === '{') d++
168
+ else if (c === ')' || c === ']' || c === '}') d--
169
+ else if (d === 0 && c === ',') break
170
+ }
171
+ return { start: callOpen + 1 + j, end: callOpen + 1 + k, params }
172
+ }
173
+ }
174
+ const fn = /\bfunction\b[^(]*\(/.exec(args)
175
+ if (fn) {
176
+ const pOpen = callOpen + 1 + fn.index + fn[0].length - 1
177
+ const pClose = matchBracket(masked, pOpen, '(', ')')
178
+ if (pClose === -1) return null
179
+ const open = masked.indexOf('{', pClose)
180
+ if (open === -1 || open > callClose) return null
181
+ const close = matchBracket(masked, open, '{', '}')
182
+ return close === -1 ? null : { start: open + 1, end: close, params: masked.slice(pOpen + 1, pClose) }
183
+ }
184
+ return null
185
+ }
186
+ const SKIP_RE = /\b(?:it\.skip|test\.skip|xit|xtest|describe\.skip|xdescribe)\s*\(/g
187
+
188
+ /**
189
+ * @returns {Array<{kind:'active', line:number, bodyText:string, bodyMasked:string}
190
+ * | {kind:'skip', line:number, matchStart:number}>}
191
+ */
192
+ export function findTestBlocks(source) {
193
+ const masked = maskStringsAndComments(source)
194
+ const blocks = []
195
+
196
+ SKIP_RE.lastIndex = 0
197
+ let m
198
+ while ((m = SKIP_RE.exec(masked)) !== null) {
199
+ blocks.push({ kind: 'skip', line: lineAt(source, m.index), matchStart: m.index })
200
+ }
201
+
202
+ ACTIVE_RE.lastIndex = 0
203
+ while ((m = ACTIVE_RE.exec(masked)) !== null) {
204
+ const callOpen = m.index + m[0].length - 1 // index of the call's own '('
205
+ const callClose = matchBracket(masked, callOpen, '(', ')')
206
+ if (callClose === -1) continue
207
+ const body = functionBody(masked, callOpen, callClose)
208
+ if (!body) continue // no function-literal argument (e.g. it.todo, a bare reference) — nothing to assert
209
+ blocks.push({
210
+ kind: 'active',
211
+ line: lineAt(source, m.index),
212
+ bodyText: source.slice(body.start, body.end),
213
+ bodyMasked: masked.slice(body.start, body.end),
214
+ bodyStart: body.start,
215
+ params: body.params || '',
216
+ })
217
+ }
218
+
219
+ return blocks.sort((a, b) => a.line - b.line)
220
+ }
221
+
222
+ // ─────────────────────────────────────────────────────────────────────────
223
+ // Criterion 1 + 2 — assertion presence / tautology
224
+ // ─────────────────────────────────────────────────────────────────────────
225
+ const EXPECT_CALL_RE = /\bexpect\s*\(/g
226
+ // NOT global. A /g regex keeps `lastIndex` between `.test()` calls, so after one block
227
+ // matched, the NEXT block was searched from the previous block's match offset — a
228
+ // shorter block then "had no assert" (factory batch 2026-09-26: 130 of the 243 findings
229
+ // on the release line were this one defect, in node:test `assert.*` specs).
230
+ const ASSERT_CALL_RE = /\bassert(?:\.\w+)?\s*\(/
231
+
232
+ function findExpectCalls(bodyText, bodyMasked) {
233
+ const calls = []
234
+ EXPECT_CALL_RE.lastIndex = 0
235
+ let m
236
+ while ((m = EXPECT_CALL_RE.exec(bodyMasked)) !== null) {
237
+ const argOpen = m.index + m[0].length - 1
238
+ const argClose = matchBracket(bodyMasked, argOpen, '(', ')')
239
+ if (argClose === -1) continue
240
+ const arg = bodyText.slice(argOpen + 1, argClose).trim()
241
+ // matcher: `.toBeX(...)` immediately following the expect(...) call.
242
+ const rest = bodyMasked.slice(argClose + 1)
243
+ const matcherM = /^\s*\.\s*(\w+)\s*\(/.exec(rest)
244
+ let matcherName = null
245
+ let matcherArg = null
246
+ if (matcherM) {
247
+ const matcherOpen = argClose + 1 + matcherM.index + matcherM[0].length - 1
248
+ const matcherClose = matchBracket(bodyMasked, matcherOpen, '(', ')')
249
+ matcherName = matcherM[1]
250
+ if (matcherClose !== -1) matcherArg = bodyText.slice(matcherOpen + 1, matcherClose).trim()
251
+ }
252
+ // F4 idioms (2026-09-26): the FULL matcher chain (`not.toBeUndefined`,
253
+ // `resolves.toBe`) and the MASKED argument texts, so a name that occurs only inside a
254
+ // quoted string (`toHaveProperty('status')`) is never read as the identifier itself.
255
+ const chainM = /^(?:\s*\.\s*\w+)+\s*\(/.exec(rest)
256
+ let chain = null
257
+ let matcherArgMasked = null
258
+ if (chainM) {
259
+ chain = chainM[0].replace(/[\s(]/g, '').replace(/^\./, '')
260
+ const cOpen = argClose + 1 + chainM[0].length - 1
261
+ const cClose = matchBracket(bodyMasked, cOpen, '(', ')')
262
+ if (cClose !== -1) matcherArgMasked = bodyMasked.slice(cOpen + 1, cClose).trim()
263
+ }
264
+ const argMasked = bodyMasked.slice(argOpen + 1, argClose).trim()
265
+ calls.push({ arg, matcherName, matcherArg, chain, argMasked, matcherArgMasked, index: m.index, argOpen, argClose })
266
+ }
267
+ return calls
268
+ }
269
+
270
+ function isBooleanTautology(call) {
271
+ if (call.arg === 'true' && (call.matcherName === 'toBe' && call.matcherArg === 'true')) return true
272
+ if (call.arg === 'true' && call.matcherName === 'toBeTruthy') return true
273
+ if (call.arg === 'false' && (call.matcherName === 'toBe' && call.matcherArg === 'false')) return true
274
+ if (call.arg === 'false' && call.matcherName === 'toBeFalsy') return true
275
+ return false
276
+ }
277
+
278
+ /** Is `ident`'s nearest same-block binding before `beforeIdx` a "just
279
+ * constructed" literal (object/array/string/number/template literal or a
280
+ * bare `new X(...)`), as opposed to a call to the unit under test? */
281
+ function isJustConstructed(bodyText, bodyMasked, ident, beforeIdx) {
282
+ const bindRe = new RegExp(`\\b(?:const|let|var)\\s+${ident}\\s*=\\s*`, 'g')
283
+ let lastBind = null
284
+ let m
285
+ while ((m = bindRe.exec(bodyMasked)) !== null) {
286
+ if (m.index < beforeIdx) lastBind = m
287
+ else break
288
+ }
289
+ if (!lastBind) return false
290
+ const rhsStart = lastBind.index + lastBind[0].length
291
+ const rhsMasked = bodyMasked.slice(rhsStart).trimStart()
292
+ const rhsText = bodyText.slice(rhsStart).trimStart()
293
+ if (/^[{[]/.test(rhsMasked)) return true // object / array literal
294
+ if (/^["'`]/.test(rhsText) && !/^["'`]\s*[)+]/.test(rhsMasked)) return true // quote char survives only in bodyText (masked blanks it) — detect via original
295
+ if (/^-?\d/.test(rhsMasked)) return true // number literal
296
+ if (/^new\s+[A-Za-z_$][\w$.]*\s*\(/.test(rhsMasked)) return true // bare constructor
297
+ return false
298
+ }
299
+
300
+ /**
301
+ * `assertingHelpers` (optional): names of functions, defined in this spec file or
302
+ * imported from a RELATIVE module, whose own body asserts (expect/assert, or a call
303
+ * to another asserting helper). A test whose only assertion is `await
304
+ * expectRefusedAndNothingPersisted(...)` asserts; a test that calls a helper that
305
+ * asserts nothing is still NO_ASSERTION. (Factory batch 2026-09-26: 42 of the
306
+ * release line's NO_ASSERTION findings were assertions the detector could not see
307
+ * because they lived in a named helper — `qFail`, `expectRejectedBeforeSideEffects`,
308
+ * `assertNoNegativeBalances`, … — not missing assertions.)
309
+ */
310
+ export function isNoAssertion(block, assertingHelpers = new Set(), ruleTesters = new Set()) {
311
+ if (findExpectCalls(block.bodyText, block.bodyMasked).length > 0) return false
312
+ if (ASSERT_CALL_RE.test(block.bodyMasked)) return false
313
+ for (const name of calledNames(block.bodyMasked)) if (assertingHelpers.has(name)) return false
314
+ // F4 idioms 2026-09-26 — [FAIL_CAPABLE_THROW] a hand-rolled `if (bad) throw …` at test
315
+ // level, or inside a SYNCHRONOUS array callback / an IIFE (the throw propagates), fails
316
+ // the test: it is an assertion. A throw inside `.then(…)`/`setTimeout(…)` does not.
317
+ if (hasPropagatingThrow(block.bodyMasked)) return false
318
+ // [RULE_TESTER] `<receiver>.run(name, rule, {valid, invalid})` asserts every case.
319
+ if (callsRuleTesterRun(block.bodyMasked, ruleTesters)) return false
320
+ return true
321
+ }
322
+
323
+ // ─────────────────────────────────────────────────────────────────────────
324
+ // Structural helpers for the idiom rules (F4 idioms, 2026-09-26).
325
+ // ─────────────────────────────────────────────────────────────────────────
326
+ function matchBracketBack(masked, closeIdx, openCh, closeCh) {
327
+ let depth = 0
328
+ for (let i = closeIdx; i >= 0; i--) {
329
+ if (masked[i] === closeCh) depth++
330
+ else if (masked[i] === openCh) {
331
+ depth--
332
+ if (depth === 0) return i
333
+ }
334
+ }
335
+ return -1
336
+ }
337
+
338
+ /** Index of the innermost UNMATCHED `(`/`[`/`{` before `idx`, or -1. */
339
+ function enclosingOpen(masked, idx) {
340
+ let depth = 0
341
+ for (let i = idx - 1; i >= 0; i--) {
342
+ const c = masked[i]
343
+ if (c === ')' || c === ']' || c === '}') depth++
344
+ else if (c === '(' || c === '[' || c === '{') {
345
+ if (depth === 0) return i
346
+ depth--
347
+ }
348
+ }
349
+ return -1
350
+ }
351
+
352
+ const CONTROL_KW = new Set(['if', 'for', 'while', 'switch', 'catch', 'with', 'return', 'typeof', 'await', 'new', 'else', 'do', 'try', 'finally'])
353
+
354
+ /**
355
+ * Every BLOCK-bodied function literal in `masked`: { start, open, close, callee, iife }.
356
+ * `callee` = name of the call the literal is a direct argument of (`forEach`, `then`,
357
+ * `setTimeout`…), `iife` = the literal is invoked where it is written.
358
+ */
359
+ export function functionLiterals(masked) {
360
+ const out = []
361
+ for (let i = 0; i < masked.length; i++) {
362
+ if (masked[i] !== '{') continue
363
+ let k = i - 1
364
+ while (k >= 0 && /\s/.test(masked[k])) k--
365
+ let start = -1
366
+ if (k >= 1 && masked[k] === '>' && masked[k - 1] === '=') {
367
+ let p = k - 2
368
+ while (p >= 0 && /\s/.test(masked[p])) p--
369
+ if (masked[p] === ')') { start = matchBracketBack(masked, p, '(', ')'); if (start === -1) continue }
370
+ else { let q = p; while (q >= 0 && /[\w$]/.test(masked[q])) q--; start = q + 1 }
371
+ } else if (masked[k] === ')') {
372
+ const o = matchBracketBack(masked, k, '(', ')')
373
+ if (o === -1) continue
374
+ const before = masked.slice(0, o)
375
+ const w = /([A-Za-z_$][\w$]*)\s*(?:<[^<>()]*>)?\s*$/.exec(before)
376
+ if (!w || CONTROL_KW.has(w[1])) continue
377
+ const fnKw = /(?:\basync\s+)?\bfunction\s*\*?\s*(?:[A-Za-z_$][\w$]*)?\s*$/.exec(before)
378
+ start = fnKw ? fnKw.index : w.index
379
+ } else continue
380
+ const am = /\basync\s*$/.exec(masked.slice(0, start))
381
+ if (am) start = am.index
382
+ const close = matchBracket(masked, i, '{', '}')
383
+ if (close === -1) continue
384
+ let callee = null
385
+ let iife = false
386
+ let b = start - 1
387
+ while (b >= 0 && /\s/.test(masked[b])) b--
388
+ const encl = enclosingOpen(masked, start)
389
+ if (encl !== -1 && masked[encl] === '(' && (masked[b] === '(' || masked[b] === ',')) {
390
+ const nm = /([A-Za-z_$][\w$]*)\s*$/.exec(masked.slice(0, encl))
391
+ if (masked[b] === '(' && b === encl) {
392
+ const pc = matchBracket(masked, encl, '(', ')')
393
+ let a = close + 1
394
+ while (a < masked.length && /\s/.test(masked[a])) a++
395
+ if (pc !== -1 && pc === a && /^\)\s*\(/.test(masked.slice(pc))) iife = true
396
+ }
397
+ if (nm && !CONTROL_KW.has(nm[1])) callee = nm[1]
398
+ }
399
+ out.push({ start, open: i, close, callee, iife })
400
+ }
401
+ return out
402
+ }
403
+
404
+ const SYNC_CALLBACK = new Set(['forEach', 'map', 'flatMap', 'filter', 'some', 'every', 'find', 'findIndex', 'findLast', 'findLastIndex', 'reduce', 'reduceRight', 'sort', 'toSorted'])
405
+ const DEFERRED_CALLBACK = new Set(['then', 'catch', 'finally', 'setTimeout', 'setImmediate', 'setInterval', 'nextTick', 'queueMicrotask'])
406
+
407
+ /** [{tryOpen, tryClose, catchOpen, catchClose, rethrows}] for every try/catch in `masked`. */
408
+ function tryCatches(masked) {
409
+ const out = []
410
+ const re = /\btry\s*\{/g
411
+ let m
412
+ while ((m = re.exec(masked)) !== null) {
413
+ const tryOpen = m.index + m[0].length - 1
414
+ const tryClose = matchBracket(masked, tryOpen, '{', '}')
415
+ if (tryClose === -1) continue
416
+ const cm = /^\s*catch\s*(?:\([^)]*\))?\s*\{/.exec(masked.slice(tryClose + 1))
417
+ if (!cm) continue
418
+ const catchOpen = tryClose + 1 + cm.index + cm[0].length - 1
419
+ const catchClose = matchBracket(masked, catchOpen, '{', '}')
420
+ if (catchClose === -1) continue
421
+ out.push({ tryOpen, tryClose, catchOpen, catchClose, rethrows: ASSERT_IN_CATCH_RE.test(masked.slice(catchOpen + 1, catchClose)) })
422
+ }
423
+ return out
424
+ }
425
+
426
+ export function hasPropagatingThrow(masked) {
427
+ const fns = functionLiterals(masked)
428
+ const tcs = tryCatches(masked)
429
+ const re = /\bthrow\b/g
430
+ let m
431
+ while ((m = re.exec(masked)) !== null) {
432
+ const at = m.index
433
+ const enclosing = fns.filter((f) => f.open < at && at < f.close)
434
+ if (!enclosing.every((f) => f.iife || (f.callee && SYNC_CALLBACK.has(f.callee)))) continue
435
+ if (tcs.some((t) => t.tryOpen < at && at < t.tryClose && !t.rethrows)) continue
436
+ return true
437
+ }
438
+ return false
439
+ }
440
+
441
+ /** Local names bound to a RuleTester: `= new RuleTester(`, anything imported/required
442
+ * from a `*rule-tester*` module (any case, any alias), or a name containing "ruletester". */
443
+ export function ruleTesterNames(source) {
444
+ const masked = maskStringsAndComments(source)
445
+ const names = new Set()
446
+ for (const m of masked.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*new\s+(?:[\w$]+\.)?RuleTester\b/g)) names.add(m[1])
447
+ for (const m of source.matchAll(/import\s+(?:type\s+)?([\s\S]*?)\s+from\s*['"]([^'"]+)['"]/g)) {
448
+ if (!/rule[-_]?tester/i.test(m[2])) continue
449
+ const clause = m[1]
450
+ const def = /^([A-Za-z_$][\w$]*)/.exec(clause)
451
+ if (def) names.add(def[1])
452
+ const named = /\{([^}]*)\}/.exec(clause)
453
+ if (named) for (const part of named[1].split(',')) { const [o, a] = part.trim().split(/\s+as\s+/); if (o) names.add((a || o).trim()) }
454
+ }
455
+ for (const m of source.matchAll(/\b(?:const|let|var)\s+(\{[^}]*\}|[A-Za-z_$][\w$]*)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) {
456
+ if (!/rule[-_]?tester/i.test(m[2])) continue
457
+ if (m[1].startsWith('{')) for (const part of m[1].slice(1, -1).split(',')) { const [o, a] = part.trim().split(/\s*:\s*/); if (o) names.add((a || o).trim()) }
458
+ else names.add(m[1])
459
+ }
460
+ names.delete('')
461
+ return names
462
+ }
463
+
464
+ function callsRuleTesterRun(masked, ruleTesters) {
465
+ if (/(?<![.\w$])[\w$]*rule_?tester[\w$]*\s*\.\s*run\s*\(/i.test(masked)) return true
466
+ for (const n of ruleTesters) {
467
+ if (new RegExp(`(?<![.\\w$])${n.replace(/\$/g, '\\$')}\\s*\\.\\s*run\\s*\\(`).test(masked)) return true
468
+ }
469
+ return false
470
+ }
471
+
472
+ /** Strip TS-only syntax from a MASKED argument: `x as { status: T }`, `x as Foo<Bar>`,
473
+ * `x satisfies T`, `x!` — a name that occurs only in a type is not a value read. */
474
+ function stripTypeSyntax(maskedArg) {
475
+ let s = maskedArg
476
+ for (let guard = 0; guard < 20; guard++) {
477
+ const m = /\b(?:as|satisfies)\s+/.exec(s)
478
+ if (!m) break
479
+ let j = m.index + m[0].length
480
+ let d = 0
481
+ for (; j < s.length; j++) {
482
+ const c = s[j]
483
+ if (c === '{' || c === '<' || c === '(' || c === '[') d++
484
+ else if (c === '}' || c === '>' || c === ')' || c === ']') { if (d === 0) break; d-- }
485
+ else if (d === 0 && (c === ',' || c === ';' || c === '&' || c === '|' || c === '?' || c === '=' || c === '\n')) break
486
+ }
487
+ s = s.slice(0, m.index) + ' '.repeat(j - m.index) + s.slice(j)
488
+ }
489
+ return s
490
+ }
491
+
492
+ const CALL_NAME_RE = /(?<![.\w$])([A-Za-z_$][\w$]*)\s*\(/g
493
+ const NOT_A_CALL = new Set(['if', 'for', 'while', 'switch', 'catch', 'function', 'return', 'typeof', 'await', 'new', 'expect', 'assert'])
494
+ function calledNames(masked) {
495
+ const out = new Set()
496
+ CALL_NAME_RE.lastIndex = 0
497
+ let m
498
+ while ((m = CALL_NAME_RE.exec(masked)) !== null) if (!NOT_A_CALL.has(m[1])) out.add(m[1])
499
+ return out
500
+ }
501
+
502
+ /** name → masked body of every function this source DEFINES:
503
+ * `function NAME(…) {…}` and `const|let NAME = [async] (…|x) => {…}|expr` / `= [async] function (…) {…}`. */
504
+ export function functionDefinitions(source) {
505
+ const masked = maskStringsAndComments(source)
506
+ const defs = new Map()
507
+ const fnRe = /\bfunction\s*\*?\s*([A-Za-z_$][\w$]*)\s*(?:<[^>(]*>)?\s*\(/g
508
+ let m
509
+ while ((m = fnRe.exec(masked)) !== null) {
510
+ const pOpen = m.index + m[0].length - 1
511
+ const pClose = matchBracket(masked, pOpen, '(', ')')
512
+ if (pClose === -1) continue
513
+ const open = masked.indexOf('{', pClose)
514
+ if (open === -1) continue
515
+ const close = matchBracket(masked, open, '{', '}')
516
+ if (close !== -1) defs.set(m[1], masked.slice(open + 1, close))
517
+ }
518
+ const constRe = /\b(?:const|let)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:async\s*)?/g
519
+ while ((m = constRe.exec(masked)) !== null) {
520
+ const at = m.index + m[0].length
521
+ const rest = masked.slice(at)
522
+ let open = -1
523
+ if (/^function\b/.test(rest) || /^\(/.test(rest) || /^[A-Za-z_$][\w$]*\s*=>/.test(rest)) {
524
+ const arrow = masked.indexOf('=>', at)
525
+ const fnKw = /^function\b/.test(rest)
526
+ if (fnKw) {
527
+ const pOpen = masked.indexOf('(', at)
528
+ const pClose = pOpen === -1 ? -1 : matchBracket(masked, pOpen, '(', ')')
529
+ open = pClose === -1 ? -1 : masked.indexOf('{', pClose)
530
+ } else if (arrow !== -1) {
531
+ let j = arrow + 2
532
+ while (j < masked.length && /\s/.test(masked[j])) j++
533
+ if (masked[j] === '{') open = j
534
+ else {
535
+ // expression body: to the end of the statement (newline at depth 0 or `;`)
536
+ let d = 0
537
+ let k = j
538
+ for (; k < masked.length; k++) {
539
+ const c = masked[k]
540
+ if (c === '(' || c === '[' || c === '{') d++
541
+ else if (c === ')' || c === ']' || c === '}') { if (d === 0) break; d-- }
542
+ else if (d === 0 && (c === ';' || c === '\n')) break
543
+ }
544
+ defs.set(m[1], masked.slice(j, k))
545
+ continue
546
+ }
547
+ }
548
+ }
549
+ if (open === -1) continue
550
+ const close = matchBracket(masked, open, '{', '}')
551
+ if (close !== -1) defs.set(m[1], masked.slice(open + 1, close))
552
+ }
553
+ return defs
554
+ }
555
+
556
+ /** Relative imports of a spec: local name → absolute module path (resolved on disk). */
557
+ function relativeImports(relPath, source) {
558
+ const out = new Map()
559
+ const dir = path.dirname(path.join(ROOT, relPath))
560
+ const re = /import\s*(?:type\s+)?\{([^}]*)\}\s*from\s*['"](\.{1,2}\/[^'"]+)['"]/g
561
+ let m
562
+ while ((m = re.exec(source)) !== null) {
563
+ const base = path.resolve(dir, m[2])
564
+ const file = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '/index.ts', '/index.js'].map((e) => base + e).find((f) => existsSync(f) && !f.endsWith('/'))
565
+ if (!file) continue
566
+ for (const part of m[1].split(',')) {
567
+ const [orig, alias] = part.trim().split(/\s+as\s+/)
568
+ if (orig) out.set((alias || orig).trim(), { file, name: orig.trim() })
569
+ }
570
+ }
571
+ return out
572
+ }
573
+
574
+ /** The set of helper names, callable from this spec, whose body asserts (fixpoint, so a
575
+ * helper that only calls an asserting helper asserts too). */
576
+ export function assertingHelpersOf(relPath, source, readFile = (f) => readFileSync(f, 'utf8')) {
577
+ const bodies = new Map(functionDefinitions(source))
578
+ for (const [local, { file, name }] of relativeImports(relPath, source)) {
579
+ try {
580
+ const b = functionDefinitions(readFile(file)).get(name)
581
+ if (b !== undefined && !bodies.has(local)) bodies.set(local, b)
582
+ } catch { /* unreadable import: its helpers simply do not count */ }
583
+ }
584
+ const asserting = new Set()
585
+ let grew = true
586
+ while (grew) {
587
+ grew = false
588
+ for (const [name, body] of bodies) {
589
+ if (asserting.has(name)) continue
590
+ EXPECT_CALL_RE.lastIndex = 0
591
+ const direct = EXPECT_CALL_RE.test(body) || ASSERT_CALL_RE.test(body)
592
+ EXPECT_CALL_RE.lastIndex = 0
593
+ if (direct || [...calledNames(body)].some((n) => n !== name && asserting.has(n))) { asserting.add(name); grew = true }
594
+ }
595
+ }
596
+ return asserting
597
+ }
598
+
599
+ export function isAllTautological(block) {
600
+ const calls = findExpectCalls(block.bodyText, block.bodyMasked)
601
+ if (calls.length === 0) return false // that's criterion 1, not 2
602
+ return calls.every((call) => {
603
+ if (isBooleanTautology(call)) return true
604
+ if (call.matcherName === 'toBeDefined' && /^[A-Za-z_$][\w$]*$/.test(call.arg)) {
605
+ // Need the call's position to bound "nearest binding before this call" —
606
+ // recompute it via indexOf since findExpectCalls doesn't carry position.
607
+ const callIdx = block.bodyMasked.indexOf(`${call.arg}`)
608
+ return isJustConstructed(block.bodyText, block.bodyMasked, call.arg, callIdx >= 0 ? callIdx : block.bodyMasked.length)
609
+ }
610
+ return false
611
+ })
612
+ }
613
+
614
+ // ─────────────────────────────────────────────────────────────────────────
615
+ // Criterion 3 — unlinked skip
616
+ // ─────────────────────────────────────────────────────────────────────────
617
+ const SKIP_LEDGER_RE = /SKIP-LEDGER:\s*\S+/
618
+
619
+ export function skipLacksLedgerEntry(source, skipLine) {
620
+ const lines = source.split('\n')
621
+ const from = Math.max(0, skipLine - 1 - 3)
622
+ const window = lines.slice(from, skipLine).join('\n')
623
+ return !SKIP_LEDGER_RE.test(window)
624
+ }
625
+
626
+ // ─────────────────────────────────────────────────────────────────────────
627
+ // Criterion 4 — swallowed catch around the unit under test
628
+ // ─────────────────────────────────────────────────────────────────────────
629
+ const TRY_RE = /\btry\s*\{/g
630
+ const ASSERT_IN_CATCH_RE = /\b(expect|assert|fail)\s*\(|\bthrow\b/
631
+
632
+ /** Is any of `names` referenced inside an expect(…)/assert…(…) call (its arguments, or
633
+ * the matcher's) in `seg`? */
634
+ const EXISTENCE_ONLY = new Set(['toBeDefined', 'not.toBeUndefined', 'not.toBeNull'])
635
+ function assertedAfter(seg, names, { excludeExistence = false } = {}) {
636
+ // V1 (Opus cert f66867f32, 2026-09-26): the binding must appear as a NAME, not as a
637
+ // property of something else — `res.status` / `row.error` must not count as asserting
638
+ // a captured `status` / `error`. Same lookbehind the capture side already uses.
639
+ // F4 idioms (cert A10 baecbbf7f gap): matched on the MASKED, type-stripped text — a
640
+ // name that occurs only in a quoted matcher arg (`toHaveProperty('status')`) or only in
641
+ // a TS type (`x as { status: T }`) is not an assertion on that binding.
642
+ const re = new RegExp(`(?<![.\\w$])(?:${names.map((n) => n.replace(/[$]/g, '\\$')).join('|')})\\b`)
643
+ for (const c of findExpectCalls(seg.text, seg.masked)) {
644
+ if (excludeExistence && EXISTENCE_ONLY.has(c.chain)) continue
645
+ if (re.test(stripTypeSyntax(c.argMasked)) || (c.matcherArgMasked && re.test(stripTypeSyntax(c.matcherArgMasked)))) return true
646
+ }
647
+ const aRe = /\bassert(?:\.\w+)?\s*\(/g
648
+ let m
649
+ while ((m = aRe.exec(seg.masked)) !== null) {
650
+ const open = m.index + m[0].length - 1
651
+ const close = matchBracket(seg.masked, open, '(', ')')
652
+ if (close !== -1 && re.test(stripTypeSyntax(seg.masked.slice(open + 1, close)))) return true
653
+ }
654
+ return false
655
+ }
656
+
657
+ export function hasSwallowedCatch(block) {
658
+ const masked = block.bodyMasked
659
+ TRY_RE.lastIndex = 0
660
+ let m
661
+ while ((m = TRY_RE.exec(masked)) !== null) {
662
+ const tryBraceOpen = m.index + m[0].length - 1
663
+ const tryBraceClose = matchBracket(masked, tryBraceOpen, '{', '}')
664
+ if (tryBraceClose === -1) continue
665
+ const tryBody = masked.slice(tryBraceOpen + 1, tryBraceClose)
666
+ if (!/\(/.test(tryBody)) continue // try body calls nothing — not "around the unit under test"
667
+
668
+ const afterTry = masked.slice(tryBraceClose + 1)
669
+ const catchM = /^\s*catch\s*(?:\([^)]*\))?\s*\{/.exec(afterTry)
670
+ if (!catchM) continue
671
+ const catchBraceOpen = tryBraceClose + 1 + catchM.index + catchM[0].length - 1
672
+ const catchBraceClose = matchBracket(masked, catchBraceOpen, '{', '}')
673
+ if (catchBraceClose === -1) continue
674
+ const catchBody = masked.slice(catchBraceOpen + 1, catchBraceClose)
675
+ if (ASSERT_IN_CATCH_RE.test(catchBody)) continue
676
+ // CAPTURE-THEN-ASSERT is not a swallow: `catch (e) { caught = e }` followed by
677
+ // `expect(caught).toBeInstanceOf(X)` asserts on exactly what the catch took. It
678
+ // counts ONLY when a binding the catch assigns is referenced inside an assertion
679
+ // AFTER the try/catch; a captured error that nothing asserts is still RED.
680
+ // (Factory batch 2026-09-26: 27 of the 41 SWALLOWED_CATCH findings were this.)
681
+ const assigned = [
682
+ ...[...catchBody.matchAll(/(?<![.\w$])([A-Za-z_$][\w$]*)\s*=(?![=>])/g)].map((x) => x[1]),
683
+ // …or collected into an outer list/set/map: `unparsed.push(err)`
684
+ ...[...catchBody.matchAll(/(?<![.\w$])([A-Za-z_$][\w$]*)\s*\.\s*(?:push|add|set)\s*\(/g)].map((x) => x[1]),
685
+ ]
686
+ const after = { text: block.bodyText.slice(catchBraceClose + 1), masked: masked.slice(catchBraceClose + 1) }
687
+ if (assigned.length && assertedAfter(after, assigned)) continue
688
+ // [SENTINEL] F4 idioms 2026-09-26: `try { unit(); return true } catch { return false }`
689
+ // inside an IIFE (or a named arrow) whose RESULT is asserted — `assert.ok((() => …)())`,
690
+ // `const ok = (() => …)(); expect(ok).toBe(true)`, `expect(probe()).toBeTruthy()` — is
691
+ // fail-capable: the catch turns the error into a value the assertion reads. It stays RED
692
+ // when the sentinel is discarded, or only asserted to exist (`toBeDefined`).
693
+ if (sentinelAsserted(block, m.index, catchBody)) continue
694
+ return true
695
+ }
696
+ return false
697
+ }
698
+
699
+ function sentinelAsserted(block, tryIdx, catchBody) {
700
+ if (!/^\s*return\b[^;{}]*;?\s*$/.test(catchBody)) return false
701
+ const masked = block.bodyMasked
702
+ const fns = functionLiterals(masked).filter((f) => f.open < tryIdx && tryIdx < f.close)
703
+ if (!fns.length) return false
704
+ const fn = fns.reduce((a, b) => (b.open > a.open ? b : a))
705
+ let exprStart = fn.start
706
+ let exprEnd = fn.close + 1
707
+ if (fn.iife) {
708
+ exprStart = enclosingOpen(masked, fn.start)
709
+ const pc = matchBracket(masked, exprStart, '(', ')')
710
+ const ao = masked.indexOf('(', pc + 1)
711
+ exprEnd = matchBracket(masked, ao, '(', ')') + 1
712
+ }
713
+ // (a) the IIFE is itself an assertion's argument
714
+ if (fn.iife) {
715
+ for (const c of findExpectCalls(block.bodyText, masked)) {
716
+ if (c.argOpen < exprStart && exprEnd <= c.argClose + 1 && !EXISTENCE_ONLY.has(c.chain)) return true
717
+ }
718
+ const aRe = /\bassert(?:\.\w+)?\s*\(/g
719
+ let m
720
+ while ((m = aRe.exec(masked)) !== null) {
721
+ const open = m.index + m[0].length - 1
722
+ const close = matchBracket(masked, open, '(', ')')
723
+ if (open < exprStart && exprEnd <= close + 1) return true
724
+ }
725
+ }
726
+ // (b) bound to a name that a later assertion reads (IIFE result, or the named function called)
727
+ const bind = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*$/.exec(masked.slice(0, exprStart))
728
+ if (!bind) return false
729
+ const after = { text: block.bodyText.slice(exprEnd), masked: masked.slice(exprEnd) }
730
+ return assertedAfter(after, [bind[1]], { excludeExistence: true })
731
+ }
732
+
733
+ // ─────────────────────────────────────────────────────────────────────────
734
+ // Idiom rules (F4 idioms, 2026-09-26). Evidence: aeos-live-status/reports/
735
+ // aeos-a10-skill.md §10 — on 7 of 16 sealed-holdout cases F4 never fired, so no
736
+ // repair skill (whose trigger IS F4) could do better. One rule per missed FAMILY,
737
+ // written from the family, not from any holdout snippet. Each is proven RED, GREEN
738
+ // (a control of the same shape that is fail-capable) and by a negative control
739
+ // (rule neutered in a scratch copy ⇒ its RED fixture passes) — see the report
740
+ // aeos-live-status/reports/aeos-f4-idioms.md.
741
+ // ─────────────────────────────────────────────────────────────────────────
742
+
743
+ /** Every assertion call in a block: expect(...) calls (with chain) and assert*(...) calls. */
744
+ function assertionCalls(block) {
745
+ const out = findExpectCalls(block.bodyText, block.bodyMasked).map((c) => ({ kind: 'expect', ...c }))
746
+ const re = /\bassert(?:\.(\w+))?\s*\(/g
747
+ let m
748
+ while ((m = re.exec(block.bodyMasked)) !== null) {
749
+ const open = m.index + m[0].length - 1
750
+ const close = matchBracket(block.bodyMasked, open, '(', ')')
751
+ if (close === -1) continue
752
+ out.push({ kind: 'assert', method: m[1] || '', index: m.index, argOpen: open, argClose: close, argMasked: block.bodyMasked.slice(open + 1, close).trim(), arg: block.bodyText.slice(open + 1, close).trim() })
753
+ }
754
+ return out
755
+ }
756
+
757
+ function insideFunctionLiteral(fns, idx) {
758
+ return fns.some((f) => f.open < idx && idx < f.close)
759
+ }
760
+
761
+ const DONE_NAMES = new Set(['done', 'cb', 'callback', 'finish', 'next', 'end'])
762
+ function doneParam(block) {
763
+ const ps = block.params.split(',').map((x) => x.trim().replace(/[:=][\s\S]*$/, '').trim()).filter(Boolean)
764
+ const last = ps[ps.length - 1]
765
+ return last && DONE_NAMES.has(last) ? last : null
766
+ }
767
+
768
+ /** Asserting deferred callbacks in a masked body: [{ callee, callOpen, callClose, nameIdx }]. */
769
+ function assertingDeferredCalls(masked) {
770
+ const out = []
771
+ const re = /(?:\.\s*(then|catch|finally)|(?<![.\w$])(setTimeout|setImmediate|setInterval|queueMicrotask|(?:process\s*\.\s*)?nextTick))\s*\(/g
772
+ let m
773
+ while ((m = re.exec(masked)) !== null) {
774
+ const callOpen = m.index + m[0].length - 1
775
+ const callClose = matchBracket(masked, callOpen, '(', ')')
776
+ if (callClose === -1) continue
777
+ const inner = masked.slice(callOpen + 1, callClose)
778
+ EXPECT_CALL_RE.lastIndex = 0
779
+ const asserts = EXPECT_CALL_RE.test(inner) || ASSERT_CALL_RE.test(inner)
780
+ EXPECT_CALL_RE.lastIndex = 0
781
+ if (asserts) out.push({ callee: m[1] || m[2], callOpen, callClose, nameIdx: m.index })
782
+ }
783
+ return out
784
+ }
785
+
786
+ /** [PROMISE_NOT_AWAITED] an assertion lives in a `.then/.catch/.finally` (or timer)
787
+ * callback whose chain the test neither awaits, returns, nor hands to anything — the
788
+ * test ends before the callback runs, so the assertion can never fail it. */
789
+ export function hasFloatingAsyncAssertion(block) {
790
+ if (doneParam(block)) return false // done-style: DONE_EARLY's territory
791
+ const masked = block.bodyMasked
792
+ const fns = functionLiterals(masked)
793
+ for (const d of assertingDeferredCalls(masked)) {
794
+ if (insideFunctionLiteral(fns, d.nameIdx)) continue // nested inside another callback: not judged here
795
+ // walk back over the chain `a.b(x).c[0]\n .then` to the token before its root
796
+ let i = d.nameIdx - 1
797
+ let rootStart = d.nameIdx
798
+ for (;;) {
799
+ while (i >= 0 && /\s/.test(masked[i])) i--
800
+ if (i < 0) break
801
+ const c = masked[i]
802
+ if (c === ')' || c === ']') { const o = matchBracketBack(masked, i, c === ')' ? '(' : '[', c); if (o === -1) break; i = o - 1; continue }
803
+ if (c === '.') { i--; if (masked[i] === '?') i--; continue }
804
+ if (/[\w$]/.test(c)) {
805
+ let q = i
806
+ while (q >= 0 && /[\w$]/.test(masked[q])) q--
807
+ let r = q
808
+ while (r >= 0 && /\s/.test(masked[r])) r--
809
+ if (r >= 0 && masked[r] === '.') { i = r; continue } // `x\n .y` — still the chain
810
+ rootStart = q + 1
811
+ i = r
812
+ // `new X(…).then`: step over `new`
813
+ const nw = /\bnew\s*$/.exec(masked.slice(0, i + 1))
814
+ if (nw) { i = nw.index - 1; while (i >= 0 && /\s/.test(masked[i])) i-- }
815
+ break
816
+ }
817
+ break
818
+ }
819
+ const prev = i < 0 ? '' : masked[i]
820
+ // a string/comment (blanked in the mask) ending the previous line is an ASI boundary
821
+ const gap = block.bodyText.slice(i + 1, rootStart)
822
+ if (/\S/.test(gap) && /\n/.test(gap.slice(gap.search(/\S/)))) return true
823
+ if (/[\w$]/.test(prev)) {
824
+ const w = /([\w$]+)$/.exec(masked.slice(0, i + 1))
825
+ if (w && ['await', 'return', 'yield'].includes(w[1])) continue // awaited / returned
826
+ return true // ASI statement boundary after a word
827
+ }
828
+ if (prev === '' || prev === ';' || prev === '{' || prev === '}' || prev === ')') return true
829
+ // `=`, `(`, `,`, `[`, `=>`, `:` … — the chain is assigned, passed, or returned: not judged
830
+ }
831
+ return false
832
+ }
833
+
834
+ /** [DONE_EARLY] a done-callback test calls `done()` synchronously at test level while an
835
+ * assertion sits in a promise/timer callback — jest ends the test at `done()`, so that
836
+ * assertion can never fail it. (A done that is NEVER called is not vacuous: jest times
837
+ * the test out RED.) */
838
+ export function hasDoneBeforeAsyncAssertion(block) {
839
+ const done = doneParam(block)
840
+ if (!done) return false
841
+ const masked = block.bodyMasked
842
+ const fns = functionLiterals(masked)
843
+ const re = new RegExp(`(?<![.\\w$])${done.replace(/\$/g, '\\$')}\\s*\\(`, 'g')
844
+ let early = false
845
+ let m
846
+ while ((m = re.exec(masked)) !== null) if (!insideFunctionLiteral(fns, m.index) && !/=>\s*$/.test(masked.slice(0, m.index))) early = true
847
+ if (!early) return false
848
+ return assertingDeferredCalls(masked).length > 0
849
+ }
850
+
851
+ /** [EARLY_RETURN] a bare `return` at test level (not inside a nested callback) that comes
852
+ * BEFORE an assertion: whenever that path is taken the test passes having asserted
853
+ * nothing — a skip in disguise. Linkable like a skip: `// SKIP-LEDGER: <ref>` on the
854
+ * line or within 3 lines above exempts it. Returns the body offset of the return, or -1. */
855
+ export function earlyReturnIndex(block, assertingHelpers = new Set()) {
856
+ const masked = block.bodyMasked
857
+ const fns = functionLiterals(masked)
858
+ const re = /\breturn\b/g
859
+ let m
860
+ while ((m = re.exec(masked)) !== null) {
861
+ if (insideFunctionLiteral(fns, m.index)) continue
862
+ const tail = masked.slice(m.index + 6).split('\n')[0].trim()
863
+ if (!(tail === '' || tail === ';' || tail.startsWith('}') || /^(?:undefined|void\s+0)\s*;?\s*(?:}|$)/.test(tail))) continue
864
+ // Only a return reached BEFORE anything was asserted is a vacuous path. A type-narrowing
865
+ // `expect(r.ok).toBe(true); if (!r.ok) return` or an `if (x) { expect(…); return }`
866
+ // branch has already asserted — measured on the release range, 2026-09-26. A
867
+ // tautological `expect(true).toBe(true); return` has not.
868
+ const before = { text: block.bodyText.slice(0, m.index), masked: masked.slice(0, m.index) }
869
+ const deferred = assertingDeferredCalls(before.masked)
870
+ const assertedBefore =
871
+ findExpectCalls(before.text, before.masked).some((c) => !isBooleanTautology(c) && !deferred.some((d) => d.callOpen < c.index && c.index < d.callClose)) ||
872
+ assertionCalls(before).some((c) => c.kind === 'assert' && !deferred.some((d) => d.callOpen < c.index && c.index < d.callClose)) ||
873
+ [...calledNames(before.masked)].some((n) => assertingHelpers.has(n))
874
+ if (assertedBefore) continue
875
+ // `test.skip(); return` / `t.skip(…); return` / `this.skip(); return`: a runtime skip the
876
+ // runner REPORTS as skipped — not a silent pass (UNLINKED_SKIP governs the skip call).
877
+ if (/(?:\b(?:test|it|t|ctx|context|this)\s*\.\s*skip\s*\([^;{}]*\)\s*;?\s*$)/.test(before.masked)) continue
878
+ const after = masked.slice(m.index)
879
+ EXPECT_CALL_RE.lastIndex = 0
880
+ let asserts = EXPECT_CALL_RE.test(after) || ASSERT_CALL_RE.test(after)
881
+ EXPECT_CALL_RE.lastIndex = 0
882
+ if (!asserts) for (const n of calledNames(after)) if (assertingHelpers.has(n)) { asserts = true; break }
883
+ if (asserts) return m.index
884
+ }
885
+ return -1
886
+ }
887
+
888
+ /** [MOCK_ONLY_ASSERTION] every assertion reads only what the TEST ITSELF got back from a
889
+ * mock it defined (`const f = jest.fn().mockReturnValue(x); expect(f(…)).toBe(x)`) — the
890
+ * code under test is never exercised, so the assertion is the mock echoing its setup. */
891
+ export function isMockOnlyAssertion(block) {
892
+ const masked = block.bodyMasked
893
+ const mocks = new Set()
894
+ for (const m of masked.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:jest|vi|vitest)\s*\.\s*fn\b|\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*sinon\s*\.\s*(?:stub|fake|spy)\b/g)) mocks.add(m[1] || m[2])
895
+ if (!mocks.size) return false
896
+ const alt = [...mocks].map((n) => n.replace(/\$/g, '\\$')).join('|')
897
+ const results = new Set()
898
+ for (const m of masked.matchAll(new RegExp(`\\b(?:const|let|var)\\s+(\\{[^}]*\\}|[A-Za-z_$][\\w$]*)\\s*(?::[^=]+)?=\\s*(?:await\\s+)?(?:${alt})\\s*\\(`, 'g'))) {
899
+ if (m[1].startsWith('{')) for (const part of m[1].slice(1, -1).split(',')) { const n = part.split(':').pop().trim(); if (n) results.add(n) }
900
+ else results.add(m[1])
901
+ }
902
+ const calls = assertionCalls(block)
903
+ if (!calls.length) return false
904
+ const direct = new RegExp(`^(?:await\\s+)?(?:${alt})\\s*\\(`)
905
+ return calls.every((c) => {
906
+ const a = stripTypeSyntax(c.argMasked)
907
+ if (direct.test(a)) return true
908
+ const root = /^(?:await\s+)?([A-Za-z_$][\w$]*)/.exec(a)
909
+ return !!root && results.has(root[1])
910
+ })
911
+ }
912
+
913
+ /** [TYPE_ONLY_ASSERTION] a binding annotated with an inline object type
914
+ * (`const r: { status?: 'ok' } = await f()`) whose only assertions are existence checks
915
+ * on the bare binding: the field the test is about appears only in the TYPE, which is
916
+ * erased at runtime — nothing reads it. */
917
+ export function isTypeOnlyAssertion(block) {
918
+ const masked = block.bodyMasked
919
+ const typed = new Set()
920
+ for (const m of masked.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*:\s*\{/g)) typed.add(m[1])
921
+ if (!typed.size) return false
922
+ const calls = assertionCalls(block)
923
+ if (!calls.length) return false
924
+ const EXIST = new Set(['toBeDefined', 'not.toBeUndefined', 'not.toBeNull', 'toBeTruthy'])
925
+ return calls.every((c) => {
926
+ const a = stripTypeSyntax(c.argMasked).trim()
927
+ if (!typed.has(a)) return false
928
+ if (c.kind === 'expect') return EXIST.has(c.chain)
929
+ return c.method === '' || c.method === 'ok'
930
+ })
931
+ }
932
+
933
+ /** [KEY_PRESENCE_ONLY] every assertion checks only that a QUOTED key exists
934
+ * (`toHaveProperty('k')` with no value, `expect(Object.keys(x)).toContain('k')`,
935
+ * `'k' in x`) — the value under that key is never read, so any value (null, the wrong
936
+ * status, an error shape) passes. */
937
+ export function isKeyPresenceOnly(block) {
938
+ const calls = assertionCalls(block)
939
+ if (!calls.length) return false
940
+ const STR = /^(['"`])[^'"`]*\1$/
941
+ return calls.every((c) => {
942
+ if (c.kind === 'assert') return (c.method === '' || c.method === 'ok') && /^(['"])[^'"]*\1\s+in\s+/.test(c.arg) && !/,/.test(c.argMasked)
943
+ if (c.chain === 'toHaveProperty' && c.matcherArg && STR.test(c.matcherArg)) return true
944
+ if (/^Object\s*\.\s*(?:keys|getOwnPropertyNames)\s*\(/.test(c.argMasked) && ['toContain', 'toContainEqual'].includes(c.chain) && c.matcherArg && STR.test(c.matcherArg)) return true
945
+ if (/^(['"])[^'"]*\1\s+in\s+/.test(c.arg) && (c.chain === 'toBeTruthy' || (c.chain === 'toBe' && c.matcherArg === 'true'))) return true
946
+ return false
947
+ })
948
+ }
949
+
950
+ /** [CONDITIONAL_SKIP] a skip used as a VALUE — `(cond ? describe : describe.skip)(…)`,
951
+ * `const d = ok ? it : xit`, `describe.skipIf(c)`, `it.runIf(c)`, `test.skip.each` —
952
+ * the suite silently vanishes whenever the condition flips (e.g. in CI). Same
953
+ * SKIP-LEDGER linkage as a plain skip. Returns masked-source offsets. */
954
+ const CONDITIONAL_SKIP_RE = /\b(?:describe|it|test)\s*\.\s*skip\b(?!\s*\()|(?<![.\w$])(?:xdescribe|xit|xtest)\b(?!\s*\()|\b(?:describe|it|test)\s*\.\s*(?:skipIf|runIf)\s*\(/g
955
+ export function conditionalSkipOffsets(masked) {
956
+ const out = []
957
+ CONDITIONAL_SKIP_RE.lastIndex = 0
958
+ let m
959
+ while ((m = CONDITIONAL_SKIP_RE.exec(masked)) !== null) {
960
+ out.push(m.index)
961
+ }
962
+ return out
963
+ }
964
+
965
+ // ─────────────────────────────────────────────────────────────────────────
966
+ // Orchestration
967
+ // ─────────────────────────────────────────────────────────────────────────
968
+ export function findVacuousSpecs(relPath, source, assertingHelpers = null) {
969
+ const violations = []
970
+ const helpers = assertingHelpers ?? (() => { try { return assertingHelpersOf(relPath, source) } catch { return new Set() } })()
971
+ const ruleTesters = ruleTesterNames(source)
972
+ const srcMasked = maskStringsAndComments(source)
973
+ for (const at of conditionalSkipOffsets(srcMasked)) {
974
+ const line = lineAt(source, at)
975
+ if (!skipLacksLedgerEntry(source, line)) continue
976
+ violations.push({
977
+ file: relPath,
978
+ line,
979
+ rule: 'CONDITIONAL_SKIP',
980
+ message:
981
+ 'a skip used as a value (`cond ? describe : describe.skip`, `skipIf`, `runIf`, `xit` as a ' +
982
+ 'variable) — whenever the condition flips (e.g. in CI) the suite silently disappears and ' +
983
+ 'the run stays green. FIX: run it unconditionally, or add `// SKIP-LEDGER: <ref>` naming ' +
984
+ 'why and where that decision is recorded.',
985
+ })
986
+ }
987
+ for (const block of findTestBlocks(source)) {
988
+ if (block.kind === 'skip') {
989
+ if (skipLacksLedgerEntry(source, block.line)) {
990
+ violations.push({
991
+ file: relPath,
992
+ line: block.line,
993
+ rule: 'UNLINKED_SKIP',
994
+ message:
995
+ `skipped test with no SKIP-LEDGER: tag. FIX: add ` +
996
+ `\`// SKIP-LEDGER: <doc path, work-order id, or ledger id>\` on this line ` +
997
+ `or within the 3 lines above it, naming why this is skipped and where that ` +
998
+ `decision is recorded.`,
999
+ })
1000
+ }
1001
+ continue
1002
+ }
1003
+ if (isNoAssertion(block, helpers, ruleTesters)) {
1004
+ violations.push({
1005
+ file: relPath,
1006
+ line: block.line,
1007
+ rule: 'NO_ASSERTION',
1008
+ message: 'test body calls no expect(...)/assert(...) — it can never fail. FIX: assert the behaviour this test claims to cover, or remove it.',
1009
+ })
1010
+ continue
1011
+ }
1012
+ if (isAllTautological(block)) {
1013
+ violations.push({
1014
+ file: relPath,
1015
+ line: block.line,
1016
+ rule: 'TAUTOLOGICAL',
1017
+ message:
1018
+ 'every assertion in this test is tautological (a literal compared to itself, or ' +
1019
+ 'toBeDefined() on a value the test just constructed) — it cannot fail regardless of ' +
1020
+ 'the code under test. FIX: assert against the real output of the unit under test.',
1021
+ })
1022
+ continue
1023
+ }
1024
+ if (hasSwallowedCatch(block)) {
1025
+ violations.push({
1026
+ file: relPath,
1027
+ line: block.line,
1028
+ rule: 'SWALLOWED_CATCH',
1029
+ message:
1030
+ 'a try/catch around a call swallows the error with no assertion in the catch body — ' +
1031
+ 'if the unit under test throws, this test still passes. FIX: assert on the caught ' +
1032
+ 'error (expect(...).rejects/toThrow, or assert its shape), or remove the catch and let ' +
1033
+ 'a real failure fail the test.',
1034
+ })
1035
+ continue
1036
+ }
1037
+ const idiom = idiomViolation(source, block, helpers)
1038
+ if (idiom) violations.push({ file: relPath, line: block.line, ...idiom })
1039
+ }
1040
+ return violations
1041
+ }
1042
+
1043
+ /** The idiom rules, in order; the first that fires is reported (one rule per block). */
1044
+ function idiomViolation(source, block, helpers) {
1045
+ const er = earlyReturnIndex(block, helpers)
1046
+ if (er !== -1 && skipLacksLedgerEntry(source, lineAt(source, block.bodyStart + er))) {
1047
+ return { rule: 'EARLY_RETURN', message: `a bare \`return\` at test level (line ${lineAt(source, block.bodyStart + er)}) comes before the assertions — on that path the test passes having asserted nothing. FIX: assert on both paths, make the precondition itself an assertion, or add \`// SKIP-LEDGER: <ref>\` if it is a recorded skip.` }
1048
+ }
1049
+ if (hasDoneBeforeAsyncAssertion(block)) {
1050
+ return { rule: 'DONE_EARLY', message: 'done() is called synchronously while an assertion sits in a promise/timer callback — jest ends the test at done(), so that assertion can never fail it. FIX: make the test async and await the promise, or call done() inside the callback after the assertion.' }
1051
+ }
1052
+ if (hasFloatingAsyncAssertion(block)) {
1053
+ return { rule: 'PROMISE_NOT_AWAITED', message: 'an assertion lives in a .then/.catch/.finally (or timer) callback whose promise the test never awaits or returns — the test finishes before the assertion runs. FIX: `await` (or `return`) the chain, or assert on `await`ed values.' }
1054
+ }
1055
+ if (isMockOnlyAssertion(block)) {
1056
+ return { rule: 'MOCK_ONLY_ASSERTION', message: 'every assertion reads only what a mock this test defined returned when the test itself called it — the unit under test is never exercised. FIX: call the real unit (inject the mock as its dependency) and assert on its output.' }
1057
+ }
1058
+ if (isTypeOnlyAssertion(block)) {
1059
+ return { rule: 'TYPE_ONLY_ASSERTION', message: 'the fields this test is about appear only in a TypeScript type annotation (erased at runtime); the only assertion is an existence check on the whole value. FIX: assert the field values, e.g. expect(r.status).toBe(...).' }
1060
+ }
1061
+ if (isKeyPresenceOnly(block)) {
1062
+ return { rule: 'KEY_PRESENCE_ONLY', message: 'every assertion checks only that a quoted key EXISTS (toHaveProperty(\'k\') / Object.keys(x) toContain(\'k\') / \'k\' in x) — the value is never read, so any value passes. FIX: assert the value, e.g. toHaveProperty(\'k\', expected) or expect(x.k).toBe(...).' }
1063
+ }
1064
+ return null
1065
+ }
1066
+