mjolnir-qa 0.5.0 → 0.5.3

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.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,1057 @@ Rule behavior changes (new rules, FP-rate changes against the corpus,
9
9
  severity changes) are first-class entries here — rule IDs are immutable
10
10
  once shipped, so this file is the record of what changed between versions.
11
11
 
12
+ ## [0.5.3] — 2026-09-05
13
+
14
+ ### Terminal + CI UX Overhaul (plan 1788579907109)
15
+
16
+ ### Added
17
+
18
+ - **Design-system core** (`src/reporter/ui.ts`): one canonical visual
19
+ language — `▚ TITLE` section headers (ASCII fallback `= TITLE`),
20
+ `✗/⚠/ℹ` severity icons (ASCII `X/!/i`), rounded panels, 58-glyph
21
+ dividers, and a dim `$ command` next-step affordance. All
22
+ subcommand renderers (`baseline`, `debt`, `init`, `doctor`, `fix`,
23
+ `impact`, `stats`, `handover`, `triage`, `pw-report`, `explain`,
24
+ `rules-catalog`, `create-rule`, `suppressions`, forensics,
25
+ selector-health) now render through it; per-renderer `▚▞`/`🔨`/`╔══╗`
26
+ headers and `╞══╡` tables are gone. `FORCE_COLOR` is honored
27
+ (chalk convention: `0`/`false`/empty = plain, other values force
28
+ color even piped, winning over `NO_COLOR`).
29
+ - **`mjolnir help` + per-command help** (`src/commands/help.ts`): the
30
+ grouped overview (Scan · CI & PRs · Forensics · Maintenance · Meta,
31
+ copy-pasteable starts, exit-code table, docs link) and
32
+ `mjolnir help <verb>` / `mjolnir <verb> --help` pages for every
33
+ registered verb. **Behavior call-out:** `help` now dispatches as a
34
+ verb BEFORE the scan fall-through — bare `mjolnir help` no longer
35
+ scans the CWD (it never was a documented behavior); a folder named
36
+ `help/` is still scanned via `mjolnir ./help`. `--help`/`-h` on the
37
+ root scan still print usage and exit 10 (frozen contract).
38
+ - **Friendly usage errors** (exit 10 preserved): unknown flags name
39
+ themselves on stderr, suggest up to three nearest real flags
40
+ (hand-rolled Levenshtein ≤ 2 — no new dependencies), and point at
41
+ `mjolnir --help`. The exit-20 crash path says "this is a bug in
42
+ Mjölnir, not your repo", carries the message, and prints the stack
43
+ trace only under `--debug`.
44
+ - **Live scan progress** (`src/reporter/progress.ts`): an event-driven
45
+ stderr line (`Discovering files… → Parsing frameworks… → Running
46
+ rules… → Scoring…`) fed by the new additive `ScanHooks.onProgress`.
47
+ Render-on-event only — no timers, deterministic under a fake stream.
48
+ Auto-off when stderr is not a TTY, in machine formats, under
49
+ `GITHUB_ACTIONS=true`/`CI=true`, or with the new additive
50
+ `--no-progress` flag. stdout purity and `--json` byte-identity are
51
+ unchanged.
52
+ - **`mjolnir summary [mjolnir.json]`** (`src/commands/summary.ts` +
53
+ `src/reporter/github.ts`): reads a saved `--json` report and emits
54
+ GitHub annotations (only when `GITHUB_ACTIONS=true`, per-finding
55
+ `::error|warning|notice` with spec-exact `%25/%0D/%0A/%3A/%2C`
56
+ escaping, messages truncated at ~250 chars) and a step-summary
57
+ markdown document (score + band, text score bar, dimensions table,
58
+ collapsible per-severity `<details>` with `Fix:` lines, honesty
59
+ notice for `partial`/`score:null` reports). `--stdout` forces
60
+ stdout; `--path-prefix <dir>` re-scopes paths for subdirectory
61
+ scans. Exit `0` on success — the gate step decides; `10` missing
62
+ file; `2` invalid JSON.
63
+ - **CI template v2** (`ci install`): the inline `SUMMARY_SCRIPT` step
64
+ is replaced by `mjolnir summary mjolnir.json`; the gate script is
65
+ unchanged. v1-generated workflows are still recognized on
66
+ overwrite-refusal, so `ci install` upgrades stay frictionless. The
67
+ dogfooded `.github/workflows/mjolnir.yml` and `ci.yml` self-scan use
68
+ the same command.
69
+ - **PR comment redesign** (`pr-comment`): header
70
+ `### 🔨 Mjölnir — Verification Trust` with score + band + verdict
71
+ headline, dimensions mini-table, findings grouped in collapsible
72
+ `<details>` (errors open, warnings/infos collapsed) with explicit
73
+ `Fix:` lines and evidence tags, a "what to run next" footer with the
74
+ pinned `npx mjolnir-qa@<ver>` commands, and the
75
+ `✨ N pre-existing findings fixed in this PR` callout. Same
76
+ idempotency marker; same markdown escaping.
77
+ - **Site**: new `site/reference/cli.md` (help, usage errors, summary,
78
+ progress, `FORCE_COLOR`) in the Reference sidebar.
79
+
80
+ ### Changed
81
+
82
+ - README output examples and all 22 translations: the `▚▞` header
83
+ glyph in rendered-output samples is now `▚` (the design-system
84
+ token). English README is canonical; translation sync dates unchanged
85
+ (glyph-only diff, advisory parity script).
86
+ - Regenerated committed assets: `assets/readme/terminal-hero.svg`,
87
+ `demo.svg` (`docs:hero`, `docs:demo`), and the forensics/selector
88
+ samples (`docs:forensics-samples`).
89
+
90
+ ### Removed
91
+
92
+ - The hand-rolled `╔══╗`/`▚▞`/`🔨` per-command header styles and the
93
+ `╞══╡` ASCII tables they wrapped (replaced by the shared `ui.ts`
94
+ primitives; no CLI surface change).
95
+
96
+ ### Fixed — review hardening (post-implementation audit)
97
+
98
+ - **`ci install` v1 recognition actually works now:** the v1 inline
99
+ summary script is matched in its INDENTED form (`indentBlock(…, 10)`)
100
+ — the raw unindented needle never appeared in a real v1 workflow, so
101
+ the first cut of the recognition would have refused every genuine v1
102
+ file despite the "frictionless upgrade" promise. Spec reconstructs
103
+ the embedded form from the real v1 output and pins that
104
+ hand-customized files are still refused.
105
+ - **Advisory template stays green on a crashed scan:** the generated
106
+ "Annotations + Job Summary" step is now `continue-on-error: true` —
107
+ a crashed scan leaves `mjolnir.json` empty and `summary` exits 2,
108
+ which must not turn the advisory job red (v1's inline script never
109
+ did). The gate step still owns the verdict.
110
+ - **Step summary escapes hostile finding metadata:** `ruleId`, `file`,
111
+ `message` and `fix` are markdown-escaped before `$GITHUB_STEP_SUMMARY`
112
+ (GitHub renders HTML there) — a hostile report can no longer break
113
+ out of the `<details>` structure. Annotations additionally sanitize
114
+ `file`/`ruleId`/`message` through the same `sanitizeData` layer the
115
+ terminal uses (OSC/C0 bytes), closing the gap its own docs assumed.
116
+ - **Progress line sanitizes the detail path** through `sanitizeData` —
117
+ a filename with ANSI/OSC bytes can no longer hijack the terminal.
118
+ - **PR comment overflow counts are honest:** the "...and N more
119
+ overall" line now subtracts the actually rendered count
120
+ (Σ min(group, 25)) instead of a flat 25 — no more phantom hidden
121
+ findings, and per-group overflow lines name their group
122
+ ("...and 5 more errors").
123
+ - **Usage-error contract completed:** the 8 scan-backed subcommands
124
+ (badge, debt, fix, impact, baseline, diff, pr-comment, handover) no
125
+ longer print the full usage wall after the friendly stderr error;
126
+ `mjolnir summary` rejects unknown flags with the shared
127
+ did-you-mean machinery (exit 10) instead of silently swallowing a
128
+ typo'd `--stdout`; `mjolnir ci --help` / `mjolnir help ci install` /
129
+ `mjolnir ci install --help` now reach the `ci install` help page
130
+ (two-word verb lookup).
131
+ - **Dead surface removed:** `theme.severityTag` (byte-identical twin of
132
+ `ui.severityIcon`, test-only) deleted with the two plugin specs
133
+ re-pointed; the new-module exports nothing without a caller
134
+ (`severityGlyph`, `wrapFor`, `centerIn` dropped; `keyValue` and
135
+ `bullet` remain — the plan's primitive list mandates them).
136
+
137
+ ## [0.5.2] — 2026-09-05
138
+
139
+ ### npm 12 pack-shape repair of the release pipeline
140
+
141
+ ### Fixed
142
+
143
+ - The fresh-install gate (`tests/integrations/registry-install.spec.ts`),
144
+ `tests/integrations/package-smoke.spec.ts`, and
145
+ `tests/e2e/journey-1-first-run.spec.ts` parse `npm pack --json` through
146
+ a shared shape-tolerant helper
147
+ (`tests/helpers/npm-pack-json.ts`): npm 12 changed the output from an
148
+ array to an object keyed by package name, which stopped the v0.5.1
149
+ publish at the gate — tag cut, nothing shipped, by design.
150
+ - `release.yml` upgrades to `npm@11` (the proven line) instead of
151
+ `npm@latest`: toolchain majors must be deliberate, verified changes,
152
+ never implicit drift on the publish path.
153
+ - `registry-install.spec.ts` cleanup no longer cascades a second error
154
+ when `beforeAll` fails early (the cascade buried the real diagnosis).
155
+
156
+ ## [0.5.1] — 2026-09-05
157
+
158
+ ### Verification Trust Evolution, Phase 8 — Local Extensibility (plan §18)
159
+
160
+ ### Added — folder-based external rules, zero network
161
+
162
+ - **`mjolnir-rules/` contract** (`src/plugins/local-rules.ts`): a
163
+ workspace directory loaded from the scan target root alongside npm
164
+ plugins. Two file kinds: **JSON rule manifests** (declarative regex
165
+ patterns — NO code executed; id/title/severity/category/appliesTo/
166
+ patterns/message/why/fix/languages/frameworks) and **JS modules**
167
+ (`rules: QADoctorRule[]`, full-Node trust, same posture as npm
168
+ plugins). Missing directory → no-op.
169
+ - **Same trust contract as core/npm plugins:** reserved core prefixes
170
+ rejected (case-insensitive spoofing guard); bad metadata/regexes
171
+ degrade to warning entries (QA-PLUGIN-000), never a crash; external
172
+ rules carry the full trust metadata shape and are born
173
+ quarantine/unmeasured. **Core-tier clamp**: an external rule
174
+ declaring `tier: "core"` is clamped to `extended` with a load
175
+ warning — core requires a measured FP rate from the committed corpus
176
+ sidecar, which external rules cannot have.
177
+ - **Tier caps obeyed, filter unified:** the quarantine exclusion filter
178
+ in `buildUniversalRules` now consults the tier map that includes
179
+ plugin/external tiers — plugin-declared quarantine rules were
180
+ previously excluded only when the core registry knew the ID; the
181
+ unified filter excludes them from non-strict scans exactly like core
182
+ (post-scan cap still observable under `--strict`; the cli-scan-arms
183
+ test updated to cover both sides).
184
+ - **Drift-checked:** `mjolnir rules --md --external` renders the
185
+ catalog from the LOADED external rules with a provenance column
186
+ (`core`/`external`) — an on-disk edit changes the next render; the
187
+ catalog can never drift from what actually ships.
188
+ `scripts/generate-capability-matrix.ts --external <root>` writes a
189
+ workspace-local `MJOLNIR-RULES-MATRIX.md` with provenance "external"
190
+ (unmeasured by definition — outside the corpus sidecar). The
191
+ committed matrix stays core-registry-only and byte-stable.
192
+ - **S-8 disclosure:** external rule surfaces appear in the scan's
193
+ plugin disclosure block.
194
+
195
+ Registry/marketplace explicitly deferred (plan §18).
196
+
197
+ ### Verification Trust Evolution, Phase 7 — Agentic QA Trust (plan §17)
198
+
199
+ ### Added — Agentic Trust Profile (plan §17.2, §17.4)
200
+
201
+ - **Provenance detection** (`src/engine/provenance.ts`): per-file static
202
+ markers, EXTRACTED not invented — the industry-wide generated-file
203
+ header convention ("auto-generated"/"generated by"/"do not edit") and
204
+ the Playwright codegen recorder fingerprint (its default test title
205
+ is exactly `'test'`). Honest boundary, documented in the module:
206
+ healed and MCP-edited files WITHOUT markers are statically
207
+ indistinguishable from hand-written ones — the classifier returns
208
+ `unmarked`, never a guess.
209
+ - **`ScanResult.agenticProfile`** (additive): share of test files with
210
+ detected generative markers + the findings split across those
211
+ surfaces, with the boundary in the profile's own `note`. Surfaced in
212
+ the terminal report only when something was actually detected
213
+ (silence over noise); JSON carries it on every scan.
214
+ - **§17.4 — same evidence standard regardless of author:** the profile
215
+ is metadata only. It never changes scoring, evidence levels, tier
216
+ caps, or rule behavior. AI tests earn trust through exactly the same
217
+ evidence as human ones.
218
+
219
+ ### Added — locator.normalize() framework-standards rules (plan §17.3)
220
+
221
+ Two new frozen `QA-PW-*` rules, BORN QUARANTINE (§17 exit gate:
222
+ measured before leaving provisional), fixtures both directions:
223
+
224
+ - **QA-PW-146 — CSS/XPath string selector instead of a normalized
225
+ locator** (warning, heuristic): `css=`/`xpath=` engine prefixes, bare
226
+ id/class/attr CSS, and `nth-child` chains inside
227
+ `.locator()`/`waitForSelector()`/`page.$` — aligned with Playwright's
228
+ locator standard. Fills the TS gap (the brittle-selectors family
229
+ covers Java/C#/Python; the retired QA-PW-112 was style police, this
230
+ is markup-coupling detection). Not auto-fixable: the normalized
231
+ getter depends on app semantics only a human knows — the fix field
232
+ carries the concrete `getByRole`/`getByTestId`/`getByText`
233
+ suggestion. Both fixtures + all three cached OSS corpora (0 fires —
234
+ real suites use normalized locators) recorded.
235
+ - **QA-PW-147 — codegen default test title** (info, observation): a
236
+ committed spec still titled `'test'`/`'test N'` is an unreviewed
237
+ recording artifact — the §17.1 provenance marker as a finding.
238
+
239
+ Registry: 99 rules (73 measured). Docs pages + capability matrix
240
+ regenerated.
241
+
242
+ ### Verification Trust Evolution, Phase 6 — Runtime Evidence (plan §16)
243
+
244
+ ### Added — runtime corroboration + the honest L0–L5 trust ladder
245
+
246
+ Built on the existing forensics ingestion (no greenfield):
247
+ `packages/playwright-reporter` → `mjolnir.report.json` → the
248
+ `ForensicsReport` pipeline that `forensics`/`triage`/`pw-report`
249
+ already consume. Findings gain two additive, optional fields
250
+ (schemaVersion 1 unchanged):
251
+
252
+ - **`runtimeCorroboration`** — what a real run report vouches for:
253
+ `level: "file" | "test" | "defect"`, the report source, executed-test
254
+ count, and (test/defect level) the containing test's full verdict
255
+ (attempts, final status, passed-on-retry, ever-failed, skipped).
256
+ - **`trustLevel`** — the L0–L5 ladder, derived deterministically:
257
+ L0 (E0 observation) / L1 (E1 heuristic) / L2 (E2 deterministic) are
258
+ the static-only ceiling; L3 (file executed) / L4 (the containing
259
+ test executed — matched via the report's spec declaration lines) /
260
+ L5 (the run verdict directly corroborates the defect class — a
261
+ FLAKY-RISK finding whose test actually flaked, retried, or timed
262
+ out) exist ONLY when runtime corroboration is present. The
263
+ no-static-only-L4/L5 invariant is structurally enforced in
264
+ `deriveTrustLevel` and locked by tests across the full
265
+ findingType×confidence×evidenceLevel matrix.
266
+
267
+ - **Matching is honest by construction:** Playwright JSON reports now
268
+ carry the spec declaration line (additive `line` on
269
+ `TestRecord`/`TestVerdict`); a finding is tied to a specific test
270
+ only when the report's declaration spans place it there — otherwise
271
+ corroboration stays at file level, and files the report never ran
272
+ get NOTHING (no fabricated evidence). JUnit XML has no locations:
273
+ file-level only.
274
+ - **Scan wiring:** `runScan` auto-discovers a run report next to the
275
+ scan target (`mjolnir.report.json` — the reporter package's default
276
+ output — or a `test-results/` directory), runs the existing
277
+ forensics ingestion, and stamps findings. No report → findings
278
+ unchanged (honest "runtime evidence: not available"). A hostile
279
+ report degrades the scan, never fails it.
280
+ - **Reporters split verified vs assumed (plan §16):** the terminal
281
+ footer reports `Runtime evidence: N/M findings corroborated by a
282
+ real run report (trust L3–L5)` or the explicit not-available line;
283
+ finding cards show `trust L4 · runtime: test executed`. SARIF
284
+ results carry `trustLevel` + `runtimeCorroboration` in properties.
285
+
286
+ ### Verification Trust Evolution, Phase 5 — Framework Expansion (plan §15, D7 closed)
287
+
288
+ ### Added — FrameworkDimension enforced (plan §15.1, defect D7 closed)
289
+
290
+ - **Real dependency parsing per build system:** `package.json` (JSON),
291
+ `pom.xml` (Maven `<dependency>` blocks), `build.gradle(.kts)`
292
+ (dependency-statement coordinates), and EVERY `.csproj` at the root
293
+ (`<PackageReference Include="…">` attributes — the old "first
294
+ `.csproj` only" defect is closed), plus requirements*.txt for Python.
295
+ - **Per-file framework tags** (`ParsedFile.frameworkTags`): derived from
296
+ the file's OWN imports/usings/import-lines — `@playwright/test` →
297
+ "playwright", `cypress` → "cypress", `org.junit.*` → "junit",
298
+ `NUnit` → "nunit", `import selenium` → "selenium", etc. AST-truth for
299
+ Java/C#/TS; import-line scan for Python (no AST seam by design).
300
+ - **`rule.frameworks ∩ file.frameworkTags` filtering** with
301
+ open-when-unknown: a rule that declares `frameworks` runs on a file
302
+ only when the file's tags intersect it; files without tags and rules
303
+ without `frameworks` are always analyzed — the dimension narrows, it
304
+ never silently drops evidence. Shared in
305
+ `src/engine/adapter.ts` (`frameworkFilterApplies`), enforced in the
306
+ TS/Java/C#/Python adapters' `runRules`.
307
+ - **Generalized config-gating (§15.2):** `configRule: true` +
308
+ `configFiles: string[]` (regex sources) replaces the hard-coded
309
+ `playwright.config.*` regex that lived in the TS adapter and
310
+ duplicated inside all five config rules (QA-PW-121/122/141/143/144
311
+ migrated). Discovery knows the config filename conventions
312
+ (`cypress.config.*` added alongside `playwright.config.*`).
313
+
314
+ ### Added — Cypress integration (first framework per the §15 order)
315
+
316
+ Three rules in the new frozen `QA-CYP-*` namespace, BORN QUARANTINE
317
+ (§15.5) with fixtures both directions (`tests/fixtures/QA-CYP-*/`):
318
+
319
+ - **QA-CYP-001 — fixed `cy.wait(n)`** (warning): numeric-literal
320
+ `cy.wait(3000)` — the Cypress hard-sleep idiom. Alias waits
321
+ (`cy.wait('@route')`) are the legitimate form and never fire.
322
+ File gate: framework tag, `.cy.*` extension, or `cy.*` API usage in
323
+ the file (real Cypress suites rarely import cypress — surfaced by the
324
+ first measurement against cypress-example-kitchensink). MEASURED on
325
+ the kitchensink corpus: 12 TP (viewport-switch fixed waits) / 3 FP
326
+ (doc-example artifacts, the suite intentionally demonstrates the API
327
+ — QA-PY-003 precedent) → 20% FP at n=15, 95% Wilson
328
+ [7.1%, 45.2%]; tier extended (band-consistent), corpus
329
+ cypress-io-kitchensink added with baselines.
330
+ cypress-realworld-app scanned as precision evidence: 0 fires (the
331
+ suite uses alias waits exclusively), baseline recorded.
332
+ - **QA-CYP-002 — focused test (`.only`)** (error): committed
333
+ `it.only`/`describe.only`/`context.only` de-schedules the rest of the
334
+ suite. Quarantine, unmeasured (fixtures only) pending a measured
335
+ Cypress corpus with the pattern.
336
+ - **QA-CYP-003 — `chromeWebSecurity: false`** (error): deterministic
337
+ config defect via the generalized `configFiles` gate. Quarantine,
338
+ unmeasured pending corpus.
339
+
340
+ ### Added — Selenium cross-language reach (plan §15.3, JV/CS/Py reuse)
341
+
342
+ Three rules in the new frozen `QA-SE-*` namespace sharing one sequence
343
+ detector (hard sleep followed by an element lookup within 3 lines —
344
+ the sleep standing in for an explicit `WebDriverWait`), all BORN
345
+ QUARANTINE:
346
+
347
+ - **QA-SE-001** (Java): `Thread.sleep` → `findElement`/`click`/
348
+ `sendKeys` within 3 lines.
349
+ - **QA-SE-002** (C#): `Thread.Sleep`/`Task.Delay` →
350
+ `FindElement`/`Click`/`SendKeys` within 3 lines. MEASURED on the
351
+ SeleniumHQ/selenium .NET webdriver suite: 3 TP + 1 FP (a
352
+ sleep-inside-polling-loop cadence — loop-body containment is the
353
+ documented residue) → 25% FP at n=4; below the n≥10 measurement bar,
354
+ stays quarantine/unmeasured with verdicts recorded
355
+ (tests/corpus/verdicts/SeleniumHQ-selenium.jsonl, baseline added).
356
+ - **QA-SE-003** (Python): `time.sleep` → `find_element`/`click`/
357
+ `send_keys` within 3 lines.
358
+
359
+ ### Registry
360
+
361
+ - 97 rules (was 91); registry/doctor/scaffolder ID validators widened
362
+ for the new frozen namespaces (QA-CYP-_, QA-SE-_, QA-WDIO-_,
363
+ QA-PPTR-_, QA-APM-* reserved). Measured coverage 73/97 (75%); the
364
+ §20.1(a) unmeasured-count ratchet now tracks the PRE-EXISTING set
365
+ mechanically (`introduced` ≤ 0.5.0) — new waves onboard under §20.1(b)
366
+ - the per-framework exit gate, not the global count freeze.
367
+ - Defect ledger: **D7 closed** (target phase 5). Capability matrix
368
+ regenerated (97 rules); docs pages regenerated; corpus baselines
369
+ added for the three Phase 5 measurement repos.
370
+
371
+ ### Verification Trust Evolution, Phase 4 — Common QA Semantic Model (plan §14, behavior-neutral)
372
+
373
+ ### Added — `src/engine/qa-model.ts`: the normalized QA concept IR (extract-only, no scan wiring)
374
+
375
+ The plan-§14 vocabulary — Test, TestBoundary, Setup/Teardown, Fixture,
376
+ Action, Locator, Wait, Assertion, Mock, NetworkInteraction, Retry,
377
+ Navigation, Interaction, Lifecycle — now exists as a typed model
378
+ (`QaNode`/`QaSemanticModel`, all 14 concepts in `EXTRACTOR_COVERAGE`)
379
+ with per-language extractors over the ALREADY-existing parse stage:
380
+ ts-morph for TS/JS (tests/hooks via the scorer's it/test vocabulary,
381
+ calls via the measured rule vocabularies of qa-pw-002/004/005/101–145,
382
+ qa-test-001/003/004/006, jest/vi mock+retry), tree-sitter for Java/C#
383
+ (test boundaries REUSE `javaTestMethods`/`csharpTestMethods` verbatim —
384
+ the model cannot drift from the rules' scoping; call/hook/retry
385
+ vocabularies copied from the QA-JV-_/QA-CS-_ rules), regex boundaries
386
+ for Python (def test_, @pytest.fixture, time.sleep, assert).
387
+
388
+ - **Extracted, not invented:** every classification table cites the
389
+ rule whose measured vocabulary it copies (headers in qa-model.ts);
390
+ the shared `isHelperIdiom` helper moved to `jv-cs-ast.ts` so the
391
+ QA-CS-103 rule and the model share ONE implementation.
392
+ - **Adoption is additive:** NOTHING in the scan pipeline imports the
393
+ module — BEHAVIOR-NEUTRAL by construction, golden/corpus locks
394
+ untouched (proven: locks byte-identical with the model present).
395
+ - **Coverage is honest:** `EXTRACTOR_COVERAGE` documents per-language
396
+ gaps (e.g. Java has no fixture/interaction/lifecycle extractor;
397
+ Python extracts only test/fixture/assertion/wait) — visible gaps,
398
+ never silent claims (No False Proof).
399
+ - **Equivalence proven:** `tests/qa-model.spec.ts` re-expresses the
400
+ QA-JV-103 / QA-CS-103 / QA-CS-102 oracles over the model
401
+ (`testVerifies`, ancestor-chain containment via the
402
+ `firstAncestorCallNamed` generalization now carried as
403
+ `node.ancestors`) and asserts FINDING-IDENTICAL results against the
404
+ rules on the committed fixture corpora + synthetic edge shapes —
405
+ the model can carry these rules without changing any output.
406
+ Awaitedness (qa-pw-002's consumption oracle) rides on TS nodes as
407
+ `node.awaited`.
408
+
409
+ ### Verification Trust Evolution, Phase 3 — Java/C# semantic upgrade (plan §13)
410
+
411
+ ### Changed — three JV/CS rules migrated to L2 tree-sitter analysis (EVIDENCE-BACKED, detectorRevision 2)
412
+
413
+ The Phase 0.5 parse stage's tree-sitter trees (`parseJavaAst`/`parseCSharpAst`,
414
+ delivered via `ParsedFile.ast`) are now consumed by rules: a new L2
415
+ structural-analysis layer (`src/engine/jv-cs-ast.ts`) provides test-method
416
+ scoping by annotation/attribute, invocation structure, and call/argument
417
+ containment; the rule contract gains an optional `astQuery` hook whose
418
+ regex path is a MANDATORY fallback (no AST ⇒ regex, never a second
419
+ detector, plan §13.2). No type/symbol semantics are promised for JV/CS
420
+ (no Roslyn, no classpath) — semantic depth is L2 per plan §13.4.
421
+
422
+ - **QA-JV-103 (test without assertions), 50% FP (n=20) → 25.9% FP
423
+ (n=58), quarantine → extended.** Test boundaries now come from real
424
+ `method_declaration` nodes (any `@Test`/`@org.junit.Test`, argumented
425
+ TestNG forms included). The assertion oracle adds the two measured
426
+ rev-1 FP classes: Playwright's THROWING waits (`waitFor*` except
427
+ `waitForTimeout` — they throw on timeout, so the wait IS the
428
+ verification; 6 rev-1 FPs) and `verify*/check*/assert*` helper
429
+ calls (4 rev-1 FPs). Remaining FP class, documented as the L2
430
+ boundary: assertions behind arbitrarily-named helpers (keycloak's
431
+ `testValidationValid`, playwright-java's `testEnterKey`,
432
+ appsmith's `check`).
433
+ - **QA-CS-103 (test without assertions), 95% FP (n=20) → 0% FP (n=9),
434
+ quarantine → core.** The rev-1 oracle missed the whole Shouldly
435
+ extension family — 17 of the 19 FPs were `ShouldBeOfType<...>(
436
+ ...).Message.ShouldBe(...)`-style assertion-rich tests (spectre-console,
437
+ 310 count-lock fires → 0). The rev-2 oracle counts Shouldly chains,
438
+ `Assert`/`Should` receivers, the Verify snapshot framework,
439
+ PascalCase `verify*/check*/assert*` helpers (Humanizer's
440
+ `VerifyAnalyzerAsync` — the lowercase-only form was a Java camelCase
441
+ inheritance bug), C# throwing waits (`WaitFor*Async` except
442
+ `WaitForTimeoutAsync`), and conditional `throw new
443
+ *Assertion*Exception` (playwright-dotnet's ConventionTests). Grammar
444
+ error-node guard: a truncated parse never produces a finding.
445
+ - **QA-CS-102 (`Thread.Sleep`/`Task.Delay` hard sleep), 65% FP (n=20) →
446
+ 8.3% FP (n=24), quarantine → core.** Tree-sitter invocation scoping
447
+ excludes the measured environment-simulation classes: delays inside
448
+ route/expose/server delegates (`Route*Async`, `SetRoute`,
449
+ `ExposeFunction*`, 10 rev-1 FPs + delta), deliberate infinite/negative
450
+ blocks (`Task.Delay(-1)`, `int.MaxValue`, `Timeout.Infinite*`),
451
+ `Task.WhenAny` timeout races, and runner payload fixtures
452
+ (`RunAndWaitFor{Request,RequestFinished,Response}Async(() => Task.Delay…)`,
453
+ `UnrouteAllAsync` timing-window sleeps stay flagged — their delay
454
+ creates the timing window the assertion measures). Documented
455
+ trade-off: one rev-1 TP (a sub-second artificial-timing delay inside a
456
+ route delegate) is no longer flagged; the structural boundary cannot
457
+ read that intent.
458
+
459
+ All three: verdict corpus reconciled per the §07 loop (41 superseded rows
460
+ removed, 49 fresh rows adjudicated from source with per-row notes, class-B
461
+ positive fixtures for QA-CS-102/QA-JV-103 and class-C negative fixtures
462
+ for all three), count-lock baselines refreshed (microsoft-playwright-java
463
+ QA-JV-103 97→43, microsoft-playwright-dotnet QA-CS-102 52→21,
464
+ spectre-console QA-CS-103 310→0, Humanizer 34→2; keycloak +602 is a
465
+ recall gain — the rev-1 regex missed `void x() throws Exception`
466
+ signatures, tree-sitter scoping does not), and
467
+ `detector-revisions.json` bumped (sidecar entries stay; measurements
468
+ stamped rev 2). QA-JV-102 stays LEXICAL rev 1 — no migration for
469
+ symmetry (plan §12.4).
470
+
471
+ ### Verification Trust Evolution, Phase 2 — quarantine-cluster triage (plan §12.2)
472
+
473
+ ### Deprecated — 21 rules retired per docs/RULE-LIFECYCLE.md (measured 100% FP, premise wrong)
474
+
475
+ Every rule below measured 100% FP (zero TPs at n ≥ 10, `docs/FP-AUDIT.md`)
476
+ on real-world code. Per the lifecycle policy the severity is downgraded to
477
+ `info` (non-blocking everywhere) and `falsePositiveRisk` is set to `high`;
478
+ the code and fixtures stay in the repo, the frozen ID is never reused, and
479
+ any salvageable detection idea ships under a NEW rule ID. If you gated CI
480
+ on these findings, they no longer block at `info` severity; add an explicit
481
+ `severityOverrides` entry in `mjolnir.config.json` to restore blocking.
482
+
483
+ - **QA-PW-005** (business logic in `page.evaluate()`): in test files,
484
+ branching inside evaluate is the only way to reach browser state — every
485
+ measured use was browser-only test instrumentation or the API under test,
486
+ never leaked app logic. No successor.
487
+ - **QA-PW-103** (missing timeout on navigation): bare `goto()` to the app
488
+ under test is the universal navigation idiom; per-call budgets are
489
+ config territory. No successor.
490
+ - **QA-PW-105** (`expect.poll` without timeout): the default poll timeout
491
+ is a hard bound that raises — the claimed masking harm cannot occur.
492
+ No successor.
493
+ - **QA-PW-107** (`toBeVisible` on toast/banner/modal): presence +
494
+ auto-retry semantics is what suites assert; viewport visibility is a
495
+ different question the suites are not asking. No successor.
496
+ - **QA-PW-108** (`toHaveText` coupling): asserting self-owned markup's
497
+ exact text is a legitimate strong assertion; "whose markup is this" is
498
+ not statically decidable. No successor.
499
+ - **QA-PW-112** (testid naming convention): hardcoding kebab-case as _the_
500
+ convention is the defect — conventions are repo-local. Successor idea
501
+ (mixed-conventions-within-one-repo check) requires a NEW ID.
502
+ - **QA-PW-114** (legacy element handles): the auto-wait harm needs a
503
+ timing window; every measured use was immediate reads or deliberate
504
+ existence checks. No successor.
505
+ - **QA-PW-118** (`networkidle` waits): the flake source is environmental
506
+ background traffic, absent by construction where the rule fired; harm is
507
+ not code-detectable. No successor.
508
+ - **QA-PW-119** (module-level state order dependence, was `error`): FPs
509
+ scatter across ≥5 legitimate infrastructure idioms (per-test teardown
510
+ harnesses, counters, vi.hoisted fixtures, memoized shared infra) — an
511
+ error-severity rule at 0 TP / 24 is actively misleading. Successor idea
512
+ (cross-test write→read dataflow analysis) requires a NEW ID.
513
+ - **QA-PW-120** (missing environment guard): file-level keyword
514
+ co-occurrence does not imply engine dependence; even the corpus's e2e
515
+ specs are engine-agnostic. No successor.
516
+ - **QA-PW-145** (no a11y assertions): absence of optional coverage is not
517
+ a defect finding; the heuristic fires on every UI spec by construction.
518
+ Successor idea (a11y-coverage reporting) requires a NEW ID.
519
+ - **QA-TQUAL-001** (mock-only verification, 0 TP / 26): spies observe the
520
+ real unit's output — the mock call IS the observable contract;
521
+ stand-in vs observer is not statically decidable. No successor.
522
+ - **QA-PY-006** (empty test body `pass`): 18/20 FPs were pytester
523
+ test-data scripts; genuinely collected empty tests don't occur in real
524
+ code. No successor.
525
+ - **QA-PY-008** (mock-only verification): boundary mocking and real-output
526
+ spies are contract testing, not mock theater. No successor.
527
+ - **QA-PY-010** (random/time without freeze): wall-clock reads are the
528
+ measured subject in timing/throttle tests; freezing would defeat them.
529
+ No successor.
530
+ - **QA-JV-108 / QA-CS-108** (hardcoded environment URL): FPs split across
531
+ HAR-replay fixtures, route-mocked origins, and proxy-failure tests — no
532
+ mechanically discriminable shape (the M-06 header concedes a fake-TLD
533
+ lookahead fixes zero measured FPs). No successor.
534
+ - **QA-JV-110 / QA-CS-110** (no a11y assertions): same absence-heuristic
535
+ premise failure as QA-PW-145. Successor idea requires a NEW ID.
536
+ - **QA-JV-111 / QA-CS-111** (blanket route mock): route-API self-tests and
537
+ fixture setup; no provable exclusion, and framework gating cannot help.
538
+ Revival on an application-repo corpus would need a NEW ID.
539
+
540
+ ### Changed — Phase 2 retunes (EVIDENCE-BACKED, detectorRevision 2 per plan §07)
541
+
542
+ Each retune targets the single fixable root cause its measured FP cohort
543
+ shares (full evidence table: docs/RULE-LIFECYCLE.md "Phase 2
544
+ quarantine-cluster triage"). Detection-logic changes bump the rules'
545
+ `detectorRevision` to 2, invalidating the revision-1 measurements (stale →
546
+ provisional → re-measure, plan §07); must-not-fire fixtures now encode the
547
+ measured FP shapes so the retunes are regression-locked in both directions.
548
+ Tier stays quarantine until re-measurement says otherwise.
549
+
550
+ - **QA-PW-102** (load-wait instead of assertion): the wait now fires only
551
+ when it is the TERMINAL wait (no `expect`/assert/`expect.poll` follows)
552
+ and is skipped when consumed by `expect(...).rejects` — the "instead of
553
+ an assertion" premise, now actually checked. Clears all 20 measured FPs
554
+ (vite HMR synchronization waits); keeps the must-fire no-assertion shape.
555
+ - **QA-ENV-001** (environment coupling): the fixed-port sub-pattern no
556
+ longer matches loopback endpoints (`localhost`/`127.x`) — they are the
557
+ suite's own fixture containers (Azurite / DynamoDB Local / Mongo), the
558
+ entire measured FP cohort. Dotted hostnames and non-loopback IPv4
559
+ literals with ports still fire; OS-path, locale, and local-time-getter
560
+ sub-patterns are unchanged.
561
+ - **QA-PY-003** (no-assertion test): the verification vocabulary gains
562
+ `pytest.warns` / `pytest.deprecated_call` / `pytest.fail`, and a
563
+ `test_*` function referenced by name elsewhere in its file (pytester-
564
+ style test data) is skipped — the collected assertion lives in the
565
+ parent test.
566
+ - **QA-PY-004** (bare truthiness assert): boolean-predicate calls are
567
+ skipped — `assert isinstance(x, T)` type guards and
568
+ `assert s.startswith(...)`-style content predicates are real checks
569
+ (the measured FP clusters). Bare identifier/attribute asserts still
570
+ fire. This rule was measured at 45% FP (n=20), not 100%; with the
571
+ clusters removed the re-measurement is expected to approach the
572
+ extended band (≤30%), pending delta classification.
573
+ - **QA-PY-007** (raises without match): `pytest.raises(X) as exc_info`
574
+ followed by an assert/expect on `exc_info.value` is skipped — the
575
+ message IS verified without `match=` (8 of 13 measured FPs). The
576
+ genuinely vague raises blocks still fire; remaining FP residue
577
+ (single-possible-exception blocks) is not statically decidable.
578
+ - **QA-PY-105** (Playwright-Python test without assertions): a called
579
+ helper whose name asserts (`assert_*`/`expect_*`/`verify_*`/`check_*`)
580
+ or waits (`wait_for_*`) counts as verification — assertions delegated
581
+ to imported helpers, the entire measured FP cohort (streamlit e2e).
582
+ - **QA-JV-106 / QA-CS-106 / QA-PY-104** (brittle selectors): no pattern
583
+ change — the measured 100% FP rows predate Bug Map M-06's removal of
584
+ the querySelector/QuerySelectorAsync/query_selector patterns, so the
585
+ measurement described a detector that no longer ships. The §07 fix is
586
+ the detectorRevision bump to 2 (stale → provisional → re-measure);
587
+ the surviving xpath=/nth-child/absolute-path patterns stay
588
+ quarantine-tier until re-measured.
589
+
590
+ ### Changed — §07 loop closed: rev-3 delta re-measurement (corpus rescans + verdict reconciliation)
591
+
592
+ The revision-2/3 detectors were re-run over the corpus repos that
593
+ produced the retuned rules' original verdicts, and the committed verdict
594
+ corpus was reconciled to the current detectors (the §07 loop the
595
+ capability matrix calls "re-measure"):
596
+
597
+ - **Delta method:** every committed row whose finding no longer fires
598
+ was removed (superseded — it described a pre-retune detector); every
599
+ NEW finding was adjudicated from source context per
600
+ `tests/corpus/verdicts/README.md` criteria, with a quota sample of 20
601
+ per rule on the largest surfaces (QA-PY-004/007) and full coverage on
602
+ the small ones. Net: 157 rows retired, 71 adjudicated rows appended,
603
+ 1423 → 1337 classified verdicts, measured coverage 78 → **72/91**
604
+ (the six rules whose remaining classified counts fell below n=10 —
605
+ QA-PW-102, QA-PY-104, QA-PY-105, QA-ENV-001, QA-JV-106, QA-CS-106 —
606
+ are now unmeasured at their current revisions; their sidecar entries
607
+ were removed accordingly and they stay quarantine/extended-tier
608
+ pending re-measurement).
609
+ - **Re-measured envelopes (rev 3):** QA-PY-003 82% FP (n=17; remaining
610
+ FPs are pytest doc examples and pytester collection fixtures —
611
+ accepted residue), QA-PY-004 76% (n=21; remaining FPs are
612
+ pytest's own predicate-call idiom families: fnmatch/samefile/
613
+ isimportable membership checks and deliberate `assert False`
614
+ fail-marker DATA), QA-PY-007 79% (n=34; remaining FPs are
615
+ single-possible-exception API-error contracts where the raised type
616
+ IS the assertion — not statically decidable). All three stay
617
+ quarantine-tier with their detectorRevision 3 stamped in the sidecar
618
+ and FP-AUDIT; their accepted-residue FP causes are documented in
619
+ docs/RULE-LIFECYCLE.md's triage table.
620
+ - **QA-PW-102 / QA-PY-105: fully cleared** — 0 findings on the
621
+ previously-20/20-FP corpus slices at revision 2.
622
+ - **QA-ENV-001 (rev 3):** the wave-2 delta (20/20 FP on the rev-2
623
+ detector) showed no host shape is statically decidable — the
624
+ fixed-port sub-pattern is dropped entirely; OS-path, locale, and
625
+ local-time-getter sub-patterns stay (their remaining 6 findings on
626
+ vite are path-literal test DATA, adjudicated FP and recorded).
627
+ - **§07 comparability guard:** the FP regression governor
628
+ (`checkFpRegression`) no longer compares measurements across
629
+ detectorRevision boundaries — a revision mismatch means the old rate
630
+ does not describe the current detector, so it is neither flagged nor
631
+ blessed.
632
+
633
+ ### Verification Trust Evolution, Phase 2 — detectionStrategy enum (D6 closed, scan-behavior-neutral)
634
+
635
+ ### Changed — D6 enum migration (plan §12.1; metadata-only)
636
+
637
+ - **`detectionStrategy` is now the enforced §09.6 enum** (`LEXICAL | AST |
638
+ SEMANTIC | FRAMEWORK | RUNTIME`) instead of free text: `src/rules/rule.ts`
639
+ types `RuleMeta.detectionStrategy` as the union, and a registry ratchet
640
+ (tests/rules.registry.spec.ts) fails CI when any rule omits it or carries
641
+ a non-enum value — the "free text" drift class cannot reintroduce itself.
642
+ All 91 registry rules were migrated in place. This is a metadata migration,
643
+ not a detection change: no `run()` body, pattern, or scoping was touched,
644
+ so scan findings, golden fixtures, and corpus baselines are byte-identical
645
+ (the Phase 0 classification of this work as BEHAVIOR-NEUTRAL for scan
646
+ findings, plan §06). Per §11.3 the enum conversion is metadata naming only
647
+ and does NOT bump any `detectorRevision` — every measurement stays valid.
648
+ - **Legacy nuance preserved, not deleted:** the richer free-text
649
+ declarations ("regex pattern + inside-string oracle", "parsed YAML +
650
+ test-command gate", …) moved verbatim into a new optional
651
+ `detectionNotes` field rendered alongside the enum in the rule docs
652
+ pages. The capability matrix now renders the declared enum directly in a
653
+ single "Detection strategy (enum)" column (the provisional
654
+ "Enum (proposed)" guess column is gone — the declared value IS the
655
+ enum); `UNCLASSIFIED` renders only for an undeclared value.
656
+ - Mapping applied: regex-over-text/absence-sweep detectors → `LEXICAL`
657
+ (incl. QA-TQUAL-002's AST-stripped text pattern, which is a text pass
658
+ over code-only text); ts-morph node-walk detectors (QA-PW-002,
659
+ QA-PW-005) → `AST`; GitHub-Actions workflow-structure detectors
660
+ (QA-CI-001/009/010) → `FRAMEWORK`. `SEMANTIC` and `RUNTIME` remain
661
+ reserved (no rule ships either yet).
662
+
663
+ ### Verification Trust Evolution, Phase 1 exit — dedicated corpora + wave-5 measurement (plan §11.5/§08)
664
+
665
+ ### Added — dedicated corpora (§11.5) and the wave-5 measurement
666
+
667
+ - **CORPUS 19 → 34 repos** (`tests/corpus/audit.ts`): 14 new real-world
668
+ repos chosen by evaluating each candidate's unmeasured-rule fire count
669
+ at HEAD before committing (`vitest-dev-vitest`, `streamlit-streamlit`,
670
+ `apache-airflow`, `iluwatar-java-design-patterns`,
671
+ `spectreconsole-spectre-console`, `Humanizr-Humanizer`,
672
+ `cypress-realworld-app`, `keycloak-keycloak`, `appsmithorg-appsmith`,
673
+ `getsentry-sentry`, `github-docs`, `vercel-next-js`, `hashicorp-vault`,
674
+ `nocodb-nocodb`). This is the plan's dedicated-Corpora requirement for
675
+ §11.5: CI-workflow density for the starved QA-CI-* rules and JV/CS
676
+ application repos so the QA-JV/QA-CS rules measure on consumer code,
677
+ not just the Playwright bindings themselves (D5). Candidates whose scan
678
+ truncated against the budget (`n8n`, `posthog`, `vscode`) were rejected —
679
+ a partial scan can never be count-locked.
680
+ - **Committed class-B/C fixture corpora (§08):**
681
+ `tests/corpus/positive-fixtures/` (realistic anti-pattern exhibits that
682
+ MUST fire — class-B recall evidence, every fire classifies TP) and
683
+ `tests/corpus/negative-fixtures/` (realistic legitimate code that must
684
+ NOT fire — class-C precision evidence, any fire is a recorded FP).
685
+ These give rules whose patterns are rare in the wild a measurement-grade
686
+ verdict surface, versioned with the verdicts that classify them via
687
+ `local:` corpus URLs. Excluded from self-scan, vitest, eslint, prettier
688
+ and the test tsconfig — they are DATA.
689
+ - **`corpus-sample.ts --unmeasured-only`:** the verdict-harvesting loop
690
+ can sample only rules without a valid measurement, so classification
691
+ effort goes to the exit gate instead of re-sampling measured rules.
692
+ - **`audit.ts --only=<name>,<name>`:** re-check or re-record a corpus
693
+ subset (used to re-verify a repo after a transient truncation without
694
+ rescanning everything); the completeness threshold now applies to the
695
+ filtered set. Clones use `git -c core.longpaths=true` (scoped, not a
696
+ global config change) and the per-repo scan budget is 60s → 120s; the
697
+ audit job gets `NODE_OPTIONS=--max-old-space-size=8192` (sentry's
698
+ repo-scale parse OOMs the default heap) and a 60-minute timeout.
699
+ - **Measured coverage 43 → 78 of 91** (1423 classified verdicts, 0 blank,
700
+ 0 UNSURE): 35 rules newly measured at n ≥ 10 — QA-TEST-001, QA-TEST-006,
701
+ QA-TEST-010, QA-TQUAL-002, QA-TQUAL-009, QA-TQUAL-011, QA-CI-001,
702
+ QA-CI-002, QA-CI-005, QA-CI-008, QA-PW-003, QA-PW-004, QA-PW-104,
703
+ QA-PW-113, QA-PW-115, QA-PW-117, QA-PW-121, QA-PW-123, QA-PW-140,
704
+ QA-PW-141, QA-PW-142, QA-PW-144, QA-PY-001, QA-PY-009, QA-PY-011,
705
+ QA-PY-012, QA-PY-103, QA-PY-105, QA-JV-101, QA-JV-102, QA-JV-109,
706
+ QA-CS-103, QA-CS-107 (plus the four previously counted). **Phase 1
707
+ exit gate MET: unmeasured 48 → 13 (≤ 20).**
708
+ - **Explicit tier declarations for all 35 newly measured rules, set from
709
+ the measured FP band (§11.2: ≤10% core, ≤30% extended, >30%
710
+ quarantine):** core — QA-PW-003, QA-PW-104, QA-PW-113, QA-PW-117,
711
+ QA-PW-121, QA-PW-140, QA-PY-001, QA-PY-009, QA-PY-011, QA-PY-103,
712
+ QA-JV-101, QA-JV-109; extended — QA-TQUAL-011, QA-CI-002, QA-CI-007,
713
+ QA-PW-141, QA-PW-142, QA-PW-144; quarantine — QA-TEST-001, QA-TEST-006,
714
+ QA-TEST-010, QA-TQUAL-002, QA-TQUAL-009, QA-PW-004, QA-PW-115,
715
+ QA-PW-123, QA-PY-012, QA-PY-105, QA-CI-001, QA-CI-005, QA-CI-008,
716
+ QA-CI-010, QA-CS-103. Every measured entry carries `detectorRevision`
717
+ in the sidecar. **Evidence-backed, not silent:** the quarantine
718
+ demotions remove error-severity deductions from default scans, which is
719
+ the tier policy working as designed — the failing specs were updated
720
+ with their reasoning inline (gate tests re-anchored on QA-CI-009, the
721
+ demo repo's CI grew real CI-009 exhibits, the demo/hero assets and
722
+ `fix` command scan with `--strict`).
723
+ - **Recall-floor + corpus baselines:** 34 count-locked baselines recorded
724
+ (non-partial scans only); the §20.6 recall floor extends to the
725
+ expanded registry.
726
+
727
+ ### Changed — scan-behavior fallout of the measured demotions (explained)
728
+
729
+ - `mjolnir fix` now scans with `--strict`: an auto-fixable rule that is
730
+ measured into quarantine (QA-TEST-001's `.only` fix) must still be
731
+ fixable — hiding it would make `fix` a no-op on its own target debt.
732
+ - The demo repo's CI workflow gained genuine CI-009 exhibits (piped and
733
+ `;`-sequenced test commands) so the demo keeps demonstrating the
734
+ NEEDS-WORK band now that its QA-TEST-001/QA-CI-001/PW-004 debt is
735
+ quarantine-capped to info (non-deducting); `docs:demo`/`docs:hero` scan
736
+ with `--strict` so the committed assets and their drift locks stay in
737
+ sync with the precision-contract spec.
738
+
739
+ ### Verification Trust Evolution, Phase 1 — measurement infrastructure
740
+
741
+ ### Added — UNSURE adjudication gate + QA-PW-101 measured (plan §11.5)
742
+
743
+ - **UNSURE ceiling ratchet:** `npm run fp-audit:generate` now fails when the
744
+ UNSURE backlog grows beyond the committed
745
+ `tests/corpus/verdicts/unsure-ceiling.json` (the §11.5 mechanism: UNSURE
746
+ never counts into `n` but always triggers review). The ceiling only moves
747
+ DOWN via documented adjudication; upward movement requires an explicit
748
+ `--update` whose diff names every rule that grew. Criteria live in
749
+ `tests/corpus/verdicts/README.md`.
750
+ - **QA-PW-101 is measured (the D5 "parked on 20 UNSURE" defect resolved):**
751
+ all 20 UNSURE verdicts were adjudicated by reading the cited sources at
752
+ repo HEAD — **20 TP, 0 FP, n=20** (next-auth's session-sync sleeps before
753
+ reading session state; sveltejs/kit's 100 ms request-observation windows
754
+ that false-pass when a stray refresh starts after the window). The rule
755
+ now declares `tier: "core"` (0% FP ≤ 10%, n ≥ 10, revision 1, recall
756
+ floor satisfied: fires in 4 corpus baselines) — the measurement-dependent
757
+ default would resolve it to core anyway, but measured rules declare
758
+ their tier explicitly (the D3 Step 2 invariant).
759
+ - **QA-TQUAL-009 UNSURE row resolved → FP** (tanstack-query angular
760
+ inject-query:529 — a deliberate `void` fire-and-forget whose assertions
761
+ still fail the run via vitest's unhandled-rejection handling; the
762
+ FALSE-GREEN diagnosis does not hold). n=2 — below the measurement bar,
763
+ still unmeasured/PROVISIONAL.
764
+ - **Coverage:** 42/91 → **43/91** measured rules; UNSURE backlog 21 → **0**.
765
+
766
+ ### Changed — D3 two-step tier fix (plan §11.2; scan-behavior-neutral)
767
+
768
+ - **Step 1 (explicit tiers):** every rule now declares its `tier`
769
+ explicitly — the 42 formerly implicit-core rules received an explicit
770
+ `tier: "core"` matching their effective tier. Byte-identical scan
771
+ behavior (the declared value equaled the omitted-tier default);
772
+ generated docs updated with an explained diff (the matrix's
773
+ "explicit tier declarations" line moved 49/91 → 91/91).
774
+ - **Step 2 (measurement-dependent default):** the omitted-tier default is
775
+ no longer unconditionally core. A rule that omits `tier` resolves via
776
+ `effectiveTier` (new `src/rules/measurement.ts`): **core** only with a
777
+ valid corpus measurement (n ≥ 10 at a matching `detectorRevision`),
778
+ otherwise **extended**, displayed with the new **PROVISIONAL** status.
779
+ PROVISIONAL is a display status (`tier extended/unmeasured`), not a
780
+ tier value — no schema churn. Scan findings are byte-identical: the
781
+ pipeline enforces only `quarantine` (severity/info + E0 caps, --strict
782
+ filter), and every quarantine rule declares its tier explicitly;
783
+ overlap-dedup consumes declared tiers only, and no demoted rule
784
+ participates in an `overlapWith` tier comparison that could change a
785
+ survivor set (all overlap groups are single-declarer/single-target).
786
+ - **The D3 demotion (38 rules drop from effective core to
787
+ extended/PROVISIONAL):** QA-TEST-001, QA-TEST-006, QA-TEST-010,
788
+ QA-TQUAL-002, QA-TQUAL-009, QA-TQUAL-011, QA-PW-003, QA-PW-004,
789
+ QA-PW-101, QA-PW-104, QA-PW-113, QA-PW-115, QA-PW-116, QA-PW-117,
790
+ QA-PW-121, QA-PW-123, QA-PW-124, QA-PW-125, QA-PW-140, QA-PW-141,
791
+ QA-PW-142, QA-PW-144, QA-CI-001, QA-CI-002, QA-CI-005, QA-CI-007,
792
+ QA-CI-008, QA-CI-009, QA-CI-010, QA-PY-001, QA-PY-009, QA-PY-011,
793
+ QA-PY-012, QA-PY-101, QA-PY-103, QA-PY-105, QA-PY-106, QA-PY-107.
794
+ The 4 measured implicit-core rules (QA-PW-002, QA-PY-002, QA-JV-105,
795
+ QA-CS-101) keep core. Unmeasured-effective-core count: 38 → **0**.
796
+ **Suite-invalidating callout (plan §11.2):** QA-TEST-001 (focused test
797
+ committed, `suiteInvalidating: true`) is among the demoted rules — its
798
+ findings still void the suite's pass claim and still gate CI at
799
+ severity=error; only its tier/statistics status changed. QA-PY-001 is
800
+ the suite-invalidating Python sibling (same class).
801
+ - **Registry ratchet (plan §20.3, enforced in code):** new
802
+ `tests/registry-ratchet.spec.ts` fails CI on ANY unmeasured rule in
803
+ effective core (`tier core ⇒ valid MEASURED_FP with matching
804
+ detectorRevision, FP ≤ 10%, n ≥ 10`), on any detectorRevision mismatch
805
+ (§20.5: stale ⇒ provisional), and — recall floor (§20.6) — on a core
806
+ rule that fires nowhere in the corpus baselines. §20.1 evidence-state
807
+ monotonicity: the measured ratio may only improve without a
808
+ machine-detectable `MEASUREMENT-EXCEPTION` marker in this CHANGELOG.
809
+ - **`mjolnir doctor`**: `MAX_UNMEASURED_CORE` lowered 40 → **0** (Phase 1
810
+ exit gate: 0 unmeasured in effective core, now enforced); tier checks
811
+ consume `effectiveTier` + stale-measurement logic. Display surfaces
812
+ (`mjolnir explain`, `mjolnir rules`, generated rule docs, capability
813
+ matrix) render the PROVISIONAL status honestly.
814
+
815
+ ### Verification Trust Evolution, Phase 0 + Phase 1 prep
816
+
817
+ ### Added — Rule Capability Matrix (Phase 0)
818
+
819
+ - **Capability Matrix v0** (`docs/RULE-CAPABILITY-MATRIX.md` +
820
+ `docs/RULE-CAPABILITY-MATRIX.json`): generated per-rule inventory of all
821
+ 91 rules from the registry + `MEASURED_FP` + the verdict corpus —
822
+ category, languages, frameworks, declared detection strategy with a
823
+ provisional enum mapping (Phase 0 contract proposal), semantic depth,
824
+ measured flag, FP rate with sample size, corpus size and diversity.
825
+ Unknown fields render as `UNCLASSIFIED` — visible gaps are the
826
+ deliverable. Regenerated with `npm run docs:capability`; drift-locked by
827
+ `tests/capability-matrix.spec.ts` and the generated-docs-drift CI job.
828
+ - **Declared-vs-measured cross-check report** (ledger class D9): the
829
+ matrix names every measured rule whose declared tier violates the FP
830
+ ceilings (core > 10%, extended > 30%) and produces the D3 demotion list
831
+ (38 unmeasured rules currently in effective core — Phase 1 input).
832
+ Current run: **0 D9 mismatches** (all measured > 30% FP rules already
833
+ declare `quarantine`).
834
+ - **Defect ledger recorded** into the matrix metadata (plan §02, D1–D8
835
+ with owning phase per defect).
836
+
837
+ ### Added — detectorRevision scaffold (Phase 1 prep)
838
+
839
+ - **`tests/corpus/detector-revisions.json` sidecar**: hand-maintained,
840
+ one entry per measured rule (all at revision 1 today), diffable.
841
+ `MEASURED_FP` entries now carry `detectorRevision` stamped from the
842
+ sidecar by `fp-audit:generate`; `docs/FP-AUDIT.md` gains a
843
+ `detectorRev` column. Measurement inheritance law (§07): a measurement
844
+ belongs to a specific detector implementation, not merely to a rule
845
+ ID. Drift lock extended in `tests/measured-fp-generated.spec.ts`
846
+ (sidecar covers exactly the measured set; revisions are positive
847
+ integers matching the sidecar).
848
+
849
+ ### Fixed — packaging: offline grammar loading (Phase 0.5 spike, D2)
850
+
851
+ - **`tree-sitter-wasms` and `web-tree-sitter` moved to `dependencies`**
852
+ (web-tree-sitter keeps its exact `0.25.6` pin — 0.26.x cannot load the
853
+ prebuilt grammar files). The published CLI's dependency tree now
854
+ carries the tree-sitter Java/C# grammars, so `npm install mjolnir-qa`
855
+ can load them offline once the Phase 0.5 parse-stage wiring consumes
856
+ them. Removed the misleading `!dist/**/*.wasm` files exclusion (the
857
+ grammars ship via the dependency, not the bundle).
858
+ - **Pack-smoke regression test**: `tests/package-smoke.spec.ts` asserts
859
+ the packed package declares both as runtime dependencies and that the
860
+ java/c_sharp grammars resolve inside the installed dependency tree.
861
+
862
+ ### Fixed — adapter header claims (D4)
863
+
864
+ - `src/adapters/java.ts` no longer claims to be a "Second tree-sitter
865
+ consumer" — it is a regex-layer adapter; the tree-sitter-java grammar
866
+ and the async `parseJavaAst` seam exist but are not wired into the
867
+ synchronous scan (D1). Same honest correction for
868
+ `src/adapters/csharp.ts` and `src/engine/adapter.ts` (whose header
869
+ still claimed tree-sitter "arrives in R2 with Python").
870
+
871
+ ### Verification Trust Evolution, Phase 0.5 — async parse stage (D1)
872
+
873
+ ### Changed — parse stage wired into the scan pipeline (D1 closed, BEHAVIOR-NEUTRAL)
874
+
875
+ - **Async parse stage between discovery and rule execution** (plan §10.1):
876
+ `runScan`'s per-file loop now awaits an optional `LanguageAdapter.parseAst`
877
+ hook before running rules. `runRules` and every rule stay synchronous and
878
+ consume the tree via `ParsedFile.ast` — the engine is NOT async end-to-end,
879
+ only the one inherently-async seam (WASM grammar load) is. `main()` and the
880
+ script entry points await the returned promise.
881
+ - **Java and C# adapters implement `parseAst`** backed by the previously-dead
882
+ `parseJavaAst`/`parseCSharpAst` (defect D1: tree-sitter AST was built and
883
+ tested in Sprint 8 but never consumed). Parse failure or a missing grammar
884
+ resolves `undefined` and rules fall back to the regex path — never fatal.
885
+ No rule consumes the AST yet (Phase 3 wires specific JV/CS rules), so scan
886
+ findings are byte-identical: golden lock, corpus baselines, and generated
887
+ assets all unchanged (BEHAVIOR-NEUTRAL mode, plan §06).
888
+ - **Parser lifecycle management** (plan §10.3): one memoized `Parser` per
889
+ grammar, bounded by a fixed-size parse-slot semaphore
890
+ (`MAX_CONCURRENT_PARSES`); every per-file tree is released via
891
+ `ParsedAst.dispose()` (`tree.delete()`) in a `finally`-equivalent position
892
+ that runs on normal completion, rule crash, per-file budget expiry, and
893
+ adapter throw; `releaseTreeSitterResources()` tears down the memoized
894
+ parsers after each scan (library-consumer hygiene). No leak path depends
895
+ on rules completing successfully.
896
+ - **Call-graph consequences**: `computeImpact` (and therefore
897
+ `runImpactCommand`) is async; `scripts/corpus-sample.ts`,
898
+ `scripts/generate-readme-demo.ts`, and `scripts/generate-readme-hero.ts`
899
+ await `runScan`; the full test suite (~40 call sites across 25 spec files)
900
+ awaits the now-async commands. No behavior change anywhere.
901
+ - **Verification evidence** (exit gate §10): golden lock byte-identical
902
+ (3/3), corpus count-lock unaffected (no JV/CS rule reads the AST),
903
+ capability-matrix + FP-AUDIT generated docs byte-identical, pack smoke
904
+ re-proven manually: `npm pack` → clean `npm install` → offline grammar
905
+ load + parse of real Java and C# source → offline `mjolnir` scan of a
906
+ Java fixture detecting QA-JV-102. `web-tree-sitter` stays pinned to
907
+ exactly `0.25.6` (§10.5, documented 0.26.x breakage).
908
+
909
+ ### Added — score instrument redesign (hammer states)
910
+
911
+ - **ScoreState model** (`src/reporter/score-state.ts`): one pure source of
912
+ truth for band / verdict / color / headline per score — critical 0–49,
913
+ warning 50–79, trusted 80–99, forged 100. `verdictFor`, the terminal
914
+ gauge and the badge all delegate to it.
915
+ - **The hammer is now the score instrument** (terminal): a state-colored
916
+ hammer block renders above the WORTHINESS line — cracked (0–49),
917
+ strained with partial runes (50–79), charged with energy arcs (80–99),
918
+ halo + lightning at 100. A plain-text caption (`[CRACKED]` /
919
+ `[STRAINED]` / `[CHARGED]` / `[FORGED]`) carries the state without
920
+ color; ASCII fallback included.
921
+ - **Trusted is aurora-cyan, forged is white-gold** on every surface
922
+ (terminal palette, web tokens, brand README). Green is no longer a
923
+ score color — it survives for non-score success contexts only.
924
+ - **Findings render as cards** (terminal): Problem → Impact → Fix →
925
+ Verify with the evidence tag and measured FP rate beside the title;
926
+ rules with >3 findings collapse under one "same fix applies" header;
927
+ non-verbose shows 10 cards with an overflow line, `--verbose` shows
928
+ everything.
929
+ - **FORGED block at 100** replaces the bare FLAWLESS VICTORY line in
930
+ unicode mode (trophy retained inside; the `*** FLAWLESS VICTORY ***`
931
+ ASCII contract string is preserved).
932
+ - **PR comments show score drift** (`Score: 72/100 (+5 since baseline
933
+ <sha>)`) using the new additive `score` field in the baseline JSON, and
934
+ carry per-finding evidence tags.
935
+ - **Badge thresholds aligned** with the reporter: ≥80 / ≥50 / 100
936
+ (was ≥90 / ≥75 / ≥50), colors `red` / `yellow` / `important` /
937
+ `success`; the message at 100 reads `100/100 · forged`.
938
+
939
+ ### Fixed — security & detection-regression audit (`.planning/AUDIT-2026-08-30-QA.md`)
940
+
941
+ - **QA-1 (P0, detection regression):** QA-TEST-003's M0-#4 header rewrite
942
+ required a space after `async` — `async()=>{` bodies silently stopped
943
+ matching. `\s*` restores them without reopening the bare-`return` exemption.
944
+ - **QA-2 (P0, detection regression):** single-walk discovery applied the
945
+ UNION of every adapter's `dirSkips`, so Python's `env` (= virtualenv)
946
+ and Java's `build` (= Gradle output) hid directories from every other
947
+ language — real TS test dirs (e.g. withastro/astro's `test/units/env/`)
948
+ silently vanished from scans. `dirSkips` are now applied per owning
949
+ language at file level.
950
+ - **QA-3 (P1):** `isInsideEmbeddedCode` truncated the masked run at the
951
+ first space inside a literal, so embedded test-data
952
+ (`'test(" foo", function () {})'`) was classified as live code and
953
+ fired 6 FP findings on eslint-plugin-playwright's ruleTester tables.
954
+ - **QA-4 (P1):** config `exclude` was never validated —
955
+ `exclude: [1, {}, null]` crashed the scan (exit 20). Non-string entries
956
+ are now a fixable usage error (exit 10), with a defense-in-depth filter
957
+ in pattern compilation.
958
+ - **QA-5 (P1):** an unparseable suppression `expires` value silently
959
+ degraded to "expired" via NaN comparisons; it is now a fixable
960
+ validation error at load time.
961
+ - **QA-6 (P1, documented-behavior fix):** the README's 90-day suppression
962
+ policy was only applied at write time by the `ignore` command —
963
+ hand-written entries without `expires` stayed active forever. The
964
+ default is now enforced at enforcement time, anchored at the config
965
+ file's mtime; `mjolnir suppressions` labels the default explicitly.
966
+ - **QA-7 (P1):** plugin reserved-prefix spoof rejection was
967
+ case-sensitive — `"qa-test-001"` walked straight past it. Matching is
968
+ now case-insensitive.
969
+ - **QA-8 (P2):** suppression `files` globs written with Windows
970
+ backslashes could never match the normalized finding paths; both sides
971
+ are normalized before matching.
972
+ - **QA-9 (P2, defense in depth):** option-shaped `--base` values are
973
+ refused at the git layer even when a programmatic caller bypasses
974
+ `parseArgs` (option-injection → `--upload-pack=` command execution).
975
+ - **QA-10 (P2):** finding metadata (hostile filenames, plugin messages)
976
+ reached the terminal and markdown PR comments unsanitized — ANSI
977
+ escapes and control characters are now stripped, and the PR comment
978
+ escapes markdown-significant characters.
979
+ - **QA-11 (P2):** the JUnit XML scan was quadratic on unclosed
980
+ `<testcase` floods (minutes of CPU for a 20 MB hostile report); the
981
+ scanner is now linear with a bounded-time regression test.
982
+ - **QA-12 (P2):** `loadBaseline`/`loadStats` are now total over arbitrary
983
+ JSON — `null` finding entries used to crash `diff`, and hostile stats
984
+ shapes leaked string junk into totals.
985
+ - **QA-13 (P3, defense in depth):** SARIF artifact URIs normalize
986
+ backslashes — `encodeURI` leaves `\` literal, which is not a valid
987
+ RFC 3986 uri-reference character.
988
+ - **QA-15 (P1):** the code-text mask iterated code points instead of
989
+ UTF-16 code units — one emoji made the mask shorter than the text and
990
+ silently disabled masking for the whole file.
991
+ - **QA-16 (P2):** files larger than the 1 MiB discovery cap were dropped
992
+ silently; the skip is now counted and flagged (`file-size`), keeping
993
+ the scan honest about what it did not read.
994
+
995
+ ### Changed — corpus baseline
996
+
997
+ - Regenerated after review: M0-#2's QA-TEST-004 duplicate-count fix
998
+ (−5 on withastro-astro — every removed finding was a same-position
999
+ duplicate), M0-#4's function-body/return-narrowing detections (net
1000
+ +1/+1/+8/+6 across vite/svelte-kit/tanstack-query/eslint-plugin-playwright,
1001
+ spot-reviewed on real code), QA-3/QA-6 masking suppressions
1002
+ (embedded-code test data no longer fires), and QA-15's mask-alignment
1003
+ fix (QA-PW-105/108 match against `codeText`, whose offsets were
1004
+ misaligned after any astral character — +27/+22/+1 restored matches on
1005
+ vite/astro/tanstack-query).
1006
+
1007
+ ### Added — Open-Beta E2E test plan (Tier 1–5, ~1,700 new test assertions)
1008
+
1009
+ - **Coverage ratchet:** statements/branches/functions/lines at literal
1010
+ 100% per file (`perFile: true`). Every branch arm, catch path, and
1011
+ fallback in `src/**` is exercised or provably dead — ~30 dead guards
1012
+ (unreachable `?? ""` fallbacks, unreachable-`if` arms behind
1013
+ `noUncheckedIndexedAccess`, a dead proof-abort block in `fix`) were
1014
+ removed or exported as testable pure helpers with the golden lock,
1015
+ self-scan and determinism tripwires green throughout.
1016
+ - **E2E journeys** (`tests/e2e/`): nine spec files run the built
1017
+ `dist/cli.mjs` as a real child process — tarball first-run (the path
1018
+ that historically caught macOS-only CLI breakage), CI PR flow with
1019
+ changed-scope attribution against real git fixtures, the baseline →
1020
+ diff → stats loop, the fix flow, the forensics flow, explain/rules,
1021
+ create-rule onboarding, the config journey (gate/severityOverrides/
1022
+ ignore/expiry honored end-to-end), and a full exit-code contract sweep
1023
+ across every documented command.
1024
+ - **Precision & accuracy:** every scorer number verified against the
1025
+ documented benchmark (deductions 8/3/1, E2/E1/E0, honesty cap 99,
1026
+ error ceiling 95, suite-invalidating ceiling 49, smoothing) with
1027
+ fast-check property invariants (≥1,000 iterations: score bounds,
1028
+ monotonicity, order-symmetry), Selector Health exact score vectors,
1029
+ hand-computed forensics math, terminal-footer/JSON deduction
1030
+ consistency, Mermaid well-formedness, and a three-verdict-band proof
1031
+ (WORTHY / NEEDS WORK / UNWORTHY each reached for its stated reason).
1032
+ - **Regression & integration:** adapter→reporter matrix (one finding
1033
+ asserted on terminal, JSON, SARIF, and Mermaid), plugin flow
1034
+ integration (valid plugin + reserved-prefix rejection), cross-file
1035
+ analysis, monorepo containment (a workspace scan never reports
1036
+ siblings), a mutation guard (each sampled rule's finding drops to zero
1037
+ when the offending line is removed), and baseline forward-compat.
1038
+ - **Nightly stress workflow** (`.github/workflows/stress.yml`, never
1039
+ PR-blocking): 10k-file mixed-language synthetic repo under a 120 s
1040
+ budget with no partial degradation, pathological trees (200-deep
1041
+ nesting, 10k-char lines, unicode filenames, LF/CRLF/BOM mixes,
1042
+ junctions, malformed specs), a 20-run byte-identical + flat-RSS soak,
1043
+ the networked registry-install smoke (previously skipped locally), and
1044
+ 4 concurrent scans of one read-only target. Fixture generators live in
1045
+ `tests/stress/`.
1046
+ - **Stability:** the 1 MB masking budget now takes the median of 3 runs
1047
+ with a 15 s ceiling (coverage instrumentation slows string-heavy loops
1048
+ ~3x and there is no in-worker marker to detect it; the ratio-based
1049
+ linearity test remains the true non-quadratic guard), and the
1050
+ scale-benchmark's 3k-file budget moved 20 s → 25 s with the same
1051
+ rationale. The vitest global-setup builds `dist/` once before any
1052
+ worker starts — parallel-file builds used to wipe dist mid-suite
1053
+ (tsdown cleans `outDir`) and fail unrelated E2E spawns with
1054
+ module-not-found.
1055
+ - **Fixed (flagged by the new E2E sweep):** `doctor --bogus` ignored the
1056
+ unknown flag and scanned the CWD as a surprise full run; it now prints
1057
+ usage and exits 10, matching the flag-error parity of every other
1058
+ subcommand (unit + E2E regression tests added).
1059
+ - **Soak drift artifacts** moved from `coverage/` (a CI-generated dir the
1060
+ tool's own QA-CI-005 rule correctly flags as "consumed but never
1061
+ generated" in stress.yml) to a dedicated `soak-drift/` dir.
1062
+
12
1063
  ## [0.5.0] — 2026-08-29
13
1064
 
14
1065
  ### Added — measurement is now visible at the point of use
@@ -318,6 +1369,20 @@ by a `must-not-fire` fixture so the class cannot return silently.
318
1369
  shared position helpers, ignore-pattern resolution, and the per-arm
319
1370
  behavior of ten Playwright rules plus QA-PY-010.
320
1371
 
1372
+ ### Infrastructure — automated npm publishing is live
1373
+
1374
+ - `0.5.0` is the first version published to npm by CI. `release.yml` now
1375
+ publishes via **OIDC trusted publishing** (no `NODE_AUTH_TOKEN`
1376
+ anywhere) with `--provenance`; the published tarball carries a SLSA
1377
+ provenance attestation (`npm audit signatures`). Every subsequent
1378
+ release is `git push --follow-tags` and nothing else.
1379
+ - The blocker was a mismatch in the npmjs.com Trusted Publisher config
1380
+ (`Sergey-bar` vs the real `Sergey-Bar` — npm matches the OIDC
1381
+ `repository` claim case-sensitively), which surfaced as
1382
+ `OIDC token exchange error - package not found` / `ENEEDAUTH`. Fixed on
1383
+ npmjs.com; re-run against the existing `v0.5.0` tag via
1384
+ `workflow_dispatch`.
1385
+
321
1386
  ### Known gaps
322
1387
 
323
1388
  - **19 of 91 rules carry a measured FP rate** (n ≥ 10, from 381 hand-classified