Third consumer swap of v8 Phase 5 step 4, after codepoints and OWASP_MAP, and the last one with a behavioural gate behind it. The 83 regex literals leave injection-patterns.mjs; the four arrays are now built in scanners/lib/injection-lexicon.mjs from the vendored lexicon/injection-lexicon.json and re-exported unchanged, so every consumer sees the same published surface. Behaviour-preserving by measurement, not by intent. The proven recipe ran in order: a differential over all 83 positions (regex source, flags, label, aliases.llm_security) found 0 divergences BEFORE anything changed; the golden dump was then diffed post-for-post rather than read as a 9000- character assertion, and the ONLY changed record was the sha256 of injection-patterns.mjs itself -- 83 regex posts, 7 table records and all counts identical. That single file digest is the diff a swap MUST produce, so the baseline was re-blessed rather than silenced. Two deliberate departures from the two earlier swaps: FAILURE IS LOUD. codepoints and owasp-map fail silently on purpose: an empty codepoint table weakens normalization, an empty OWASP map mislabels a report. An empty injection table is different in kind -- scanForInjection returns found:false for every input, and the UserPromptSubmit scan, the MCP output scan and the pre-compact scan all go blind while reporting success. That is precisely the v7.8.2 defect class, which bit this plugin four times in one release. An unresolvable commons therefore writes one line to stderr naming the disabled capability. It still does not throw: hooks run per-tool-call, and a module-load throw breaks the tool call instead of degrading the scan. The warning is suppressed for an explicit commonsRoot, so tests and dev checkouts stay quiet and the line keeps meaning something. ENTRIES COMPILE DEFENSIVELY. commons is vendored data, not code. An uncompilable pattern or unknown flag would throw inside new RegExp at module load -- in a hook. Malformed entries are dropped instead, the same call owasp-map.mjs makes for a non-array value. Gates proven by mutating the vendored JSON in BOTH directions, five ways, all firing: re-adding the script-tag tail commons dropped (golden 1, lexicon 2, corpus 1), dropping a critical pattern (2/1/3), stripping the `m` flag off a spoofed-header anchor (2/1), adding a pattern commons never published (2/2/85), and removing commons outright -- which produced the stderr line, four empty tables and 5 red rather than a green suite over zero patterns. Lexicon restored byte-identical after each. Full suite 2191 pass / 0 fail / 6 skipped (2184 -> 2197). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017XDdiKC9ZXmcSUQ2m84s6y |
||
|---|---|---|
| .. | ||
| patterns.json | ||
| README.md | ||
| reference-run.json | ||
| suite-counts.json | ||
Golden baseline — v8 Phase 5
Reference artifacts recorded before any commons extraction, so that each table-by-table swap in Phase 5 can be proven behaviour-preserving (or rolled back). Generated and checked by one code path:
node scripts/golden-baseline.mjs # check only, exits 1 on drift
node scripts/golden-baseline.mjs --write # (re-)bless
node scripts/golden-baseline.mjs --write --suite # also refresh suite counts (slow)
The gate is tests/lib/golden-baseline.test.mjs.
The artifacts
| File | What it pins | Why that layer exists |
|---|---|---|
patterns.json |
.source + .flags of every RegExp reachable from the walked modules' exports |
After a swap a pattern is new RegExp(jsonString, flags). The plan's named hazard — JSON backslash-doubling — is visible only on the compiled object. |
| ↳ table records | key/value digest of HOMOGLYPH_MAP, TYPOSQUAT_SUSPICIOUS_TOKENS, SEVERITY, the four OWASP maps |
Most of what Phase 4 moves is not a regex. A regex-only dump is blind to a broken homoglyph or OWASP-map swap, i.e. blind to the bulk of the payload. |
| ↳ file records | sha256 of the five source files in the moving set | The completeness layer, complete by construction: it covers regexes inlined in function bodies (NAMED at string-utils.mjs:291, the BIDI/tag/PUA ranges at 357–404) that no export walk can reach. |
reference-run.json |
the 61 showcase payloads through the real hook entry points, plus a static reachability probe | Byte identity over a corpus that trips a handful of patterns would prove almost nothing, so reachability is recorded and unreachable patterns are listed by name. |
suite-counts.json |
per-file pass/fail, each file run alone | A total is unattributable, and npm test runs files concurrently where three timing-sensitive files flake. Running each alone is the only reproducible recording. |
Why there is no regex enumerator
The obvious design — parse the sources and enumerate every regex literal — was
rejected. It needs a JS parser this zero-dependency repo does not have, and a
lexical approximation is contaminated by comments and division (severity.mjs
scores 4 "regexes" that way and exports none). The two-layer split — export
walk for what the scanners actually use, file digest for everything else —
answers the same question without a parser.
During a swap
A diff here means the swap changed observable behaviour. Roll the swap back.
--write is for deliberately re-blessing a change you have already decided is
correct, not for making the gate quiet.
Known coverage gaps — read before quoting a number
coverage is static reachability, NOT observed coverage. It probes the 61
payload strings against the 83 injection patterns in-process: "if you threw
every payload string at every regex, how many would match?" It does not
measure what the 61 hook invocations evaluated — the pre-bash-destructive
payloads never reach injection-patterns at all, yet their strings are in the
probe set and can mark a pattern reachable. 47/83 is therefore an upper
bound on what the corpus could protect, not a measurement of what it did.
The block is named kind: "static-reachability" and carries that caveat in a
note field; the gate asserts both, so the honest label cannot be dropped
quietly.
Two gaps follow:
- 36 unreachable patterns, listed by key under
coverage.unreachablePatterns. A swap that breaks one of those is caught by the pattern dump only. Closing it means growing the conformance corpus (Phase 5 step 5). - The four OWASP maps have no behavioural coverage at all. They are scanner-side and no hook in this corpus reaches them. They were the reason the dump was widened to table records, and the digest is their only protection — do not read the reference run as backing them.
corpusContains says a payload contains a homoglyph or is altered by
normalizeForScan. It does not say the run folded one.