mjolnir-qa 3.0.0 → 5.0.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.
- package/CHANGELOG.md +628 -6
- package/README.ar.md +58 -29
- package/README.bn.md +58 -29
- package/README.br.md +58 -29
- package/README.bs.md +58 -29
- package/README.da.md +59 -30
- package/README.de.md +59 -30
- package/README.es.md +58 -29
- package/README.fr.md +59 -30
- package/README.gr.md +58 -29
- package/README.he.md +58 -29
- package/README.it.md +58 -29
- package/README.ja.md +58 -29
- package/README.ko.md +58 -29
- package/README.md +95 -56
- package/README.no.md +58 -29
- package/README.pl.md +58 -29
- package/README.ru.md +58 -29
- package/README.th.md +58 -29
- package/README.tr.md +58 -29
- package/README.uk.md +58 -29
- package/README.vi.md +58 -29
- package/README.zh.md +58 -29
- package/README.zht.md +58 -29
- package/dist/cli.d.mts +398 -17
- package/dist/cli.mjs +2893 -1005
- package/dist/mcp/stdio.mjs +1576 -715
- package/dist/scan-pipeline-DNYbnWjE.mjs +2 -0
- package/dist/{scan-pipeline-CAH9_Qgh.mjs → scan-pipeline-DT8DWAMl.mjs} +1777 -978
- package/package.json +70 -9
- package/dist/scan-pipeline-CRe7-MQn.mjs +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -11,12 +11,598 @@ once shipped, so this file is the record of what changed between versions.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [5.0.0] — 2026-09-27
|
|
15
|
+
|
|
16
|
+
### Breaking changes
|
|
17
|
+
|
|
18
|
+
- **`business-case` no longer prints a dollar figure you did not supply.** The
|
|
19
|
+
`--industry` flag selected from a table of invented incident costs (fintech
|
|
20
|
+
$50 000, healthcare $100 000, …) with no source, and the product of that table
|
|
21
|
+
and a measured false-positive rate was printed as "Expected Savings" and
|
|
22
|
+
"Total potential savings". Pass `--incident-cost <n>` to see the arithmetic;
|
|
23
|
+
the measured FP rates, their sample sizes and the evidence weight behind them
|
|
24
|
+
are always shown. `--industry`, `--history` and `--projected` now exit with a
|
|
25
|
+
reason: `--history` read no history despite promising estimates from scan
|
|
26
|
+
improvements, and `--projected` divided the total by six and called the
|
|
27
|
+
quotient a monthly rate.
|
|
28
|
+
- **`enterprise sso` and `enterprise compliance` no longer write files.** They
|
|
29
|
+
wrote an SSO guide instructing readers to add an `sso` block to
|
|
30
|
+
`mjolnir.config.json` — a key nothing reads — and three auditor-facing SOC 2 /
|
|
31
|
+
HIPAA / PCI-DSS templates mapping controls to capabilities this product does
|
|
32
|
+
not have (SSO/SAML integration, a scan "audit trail", a "privacy scan"). Both
|
|
33
|
+
now refuse with a reason. `enterprise config` emits
|
|
34
|
+
`capability-manifest.json`, whose `notProvided` list records the absences
|
|
35
|
+
instead of inventing a deployment.
|
|
36
|
+
- **The evidence descriptor is now the same on every surface.** The HTML and
|
|
37
|
+
Markdown trust reports showed four bare words (`E2 · deterministic`) where the
|
|
38
|
+
terminal showed the full descriptor. They now show
|
|
39
|
+
`[E2 · deterministic · measured FP 8% · n=14 · trust L3 · runtime: file executed]`.
|
|
40
|
+
A finding with no `evidenceLevel` of its own now derives one from its type and
|
|
41
|
+
confidence instead of defaulting to `E2` — the strongest claim the product can
|
|
42
|
+
make, previously asserted for findings nobody had measured.
|
|
43
|
+
- **Unmeasured counts read "unknown — not measured", not `0`.** The trust
|
|
44
|
+
report, the PR comment and the step summary all rendered
|
|
45
|
+
`testDeclarationCount ?? 0` as "0 tests analyzed in 0 files" for a producer
|
|
46
|
+
that had never measured a declaration. The trust-report JSON twin now emits
|
|
47
|
+
`null` for those fields. A zero is a claim; an absent measurement is not.
|
|
48
|
+
- **The dashboard no longer varies between runs of an unchanged repository.**
|
|
49
|
+
The generation timestamp moved out of the visible body into
|
|
50
|
+
`<meta name="mjolnir-generated-at">`, and `--deterministic` omits it entirely
|
|
51
|
+
so the artifact is byte-identical and can be diffed in review.
|
|
52
|
+
- **`mjolnir report` no longer publishes a test-run status.** The top-level
|
|
53
|
+
`status` was computed from the findings
|
|
54
|
+
(`partial ? "interrupted" : hasError ? "failed" : "passed"`), so a clean scan
|
|
55
|
+
wrote `{status: "passed", totalTests: 0}` — the first field a Playwright
|
|
56
|
+
consumer reads, asserting a green run over zero executed tests, with the
|
|
57
|
+
contradicting `execution: "STATIC_ANALYSIS"` marker buried in the extension
|
|
58
|
+
block. `failed` was equally dishonest: it implies tests ran and lost. The
|
|
59
|
+
field is now the literal `interrupted` on every path, and what the scan
|
|
60
|
+
actually found is reported separately as
|
|
61
|
+
`mjolnir.scanOutcome` (`clean` / `blocked` / `partial`), which the previous
|
|
62
|
+
`mjolnir.status` field carried. Consumers reading the root `status` will see
|
|
63
|
+
`passed` → `interrupted`; that is the correction, not a regression.
|
|
64
|
+
- **Score bands are decided in one place.** The dashboard split at 60 while the
|
|
65
|
+
terminal split at 80, so a score the terminal called UNWORTHY rendered amber
|
|
66
|
+
in HTML; the contributor handover used 90; `mermaid.ts` and
|
|
67
|
+
`monorepo-analysis.ts` each carried their own copy. All of them now read
|
|
68
|
+
`src/reporter/presentation.ts`.
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- **`npm run ci:standard` now runs in CI.** `scripts/check-ci-standard.mjs`
|
|
73
|
+
was unit-tested by `tests/integrations/ci-standard.spec.ts` but referenced
|
|
74
|
+
by no workflow, so the invariants it checks — the PR scan's partial-analysis
|
|
75
|
+
handling, the coverage and ratchet gates, and the read-only scan job's
|
|
76
|
+
permissions — were enforced nowhere. It runs as a step in `workflow-lint`,
|
|
77
|
+
next to actionlint, since both are static checks of the repo's own CI
|
|
78
|
+
config.
|
|
79
|
+
|
|
80
|
+
- **Opt-in Sentry crash reporting.** Set `SENTRY_DSN` and the CLI and the MCP
|
|
81
|
+
stdio server report their own fatal errors — release-tagged as
|
|
82
|
+
`mjolnir-qa@<version>`, tagged by surface, and nothing else: no user data
|
|
83
|
+
(`sendDefaultPii: false`), no traced spans (`tracesSampleRate: 0`), no
|
|
84
|
+
findings or file contents. With no DSN the SDK is never even imported, so the
|
|
85
|
+
default install gains no dependency, no startup cost, and no network call. A
|
|
86
|
+
tool that reads private repositories must not phone home on its own, so the
|
|
87
|
+
DSN is the only switch. `@sentry/node` is an OPTIONAL peer dependency for the
|
|
88
|
+
same reason: ~1.5 MB that nothing in a code scanner needs. Capture points are
|
|
89
|
+
the two existing top-level catch blocks rather than `uncaughtException` /
|
|
90
|
+
`unhandledRejection` handlers, which would have changed the frozen exit-code
|
|
91
|
+
contract. `npm run sentry:release` creates the release and uploads source maps
|
|
92
|
+
(the build now emits them; the published tarball still excludes them). Both
|
|
93
|
+
release pipelines now run it after `npm run build` and before `npm pack`,
|
|
94
|
+
because the debug ids it stamps are written into `dist/` in place: pack first
|
|
95
|
+
and the shipped tarball carries no ids, so the maps uploaded for the release
|
|
96
|
+
match nothing and every stack trace degrades to a bundle frame.
|
|
97
|
+
- `npm run report:honesty` — no surface may print a zero for a measurement that
|
|
98
|
+
may be absent. Four reviewed exceptions are recorded for computation inputs,
|
|
99
|
+
each with a reason and a follow-up.
|
|
100
|
+
- `npm run thresholds:parity` — no score-band literal may exist outside the
|
|
101
|
+
threshold registry. It immediately caught a selector-health threshold that
|
|
102
|
+
shared a number with a trust band but not a meaning.
|
|
103
|
+
- `npm run claims:revalidate` — every claim asserted `fixed` must rest on a
|
|
104
|
+
candidate-bound, independent revalidation. It rejects evidence that cites the
|
|
105
|
+
artifact it is meant to prove, and downgrades rather than deleting a claim
|
|
106
|
+
that loses its evidence.
|
|
107
|
+
- `npm run verbs:budget` — the 5.x law, ratcheted at 50 verbs with a 44 target
|
|
108
|
+
for 5.0.
|
|
109
|
+
- `npm run script:paths` — no `package.json` script may name a `tests/`,
|
|
110
|
+
`src/`, `scripts/`, `docs/`, `site/` or `enterprise/` path that does not
|
|
111
|
+
exist. 107 path references are checked today.
|
|
112
|
+
- `npm run m26:gaps:revalidate` — re-runs every gap row's own
|
|
113
|
+
`revalidation_command` against the working tree and records the exit code and
|
|
114
|
+
the commit it ran at.
|
|
115
|
+
- `tests/contract/artifact-determinism.spec.ts` — two runs of the HTML
|
|
116
|
+
artifact over an unchanged repository must be byte-identical.
|
|
117
|
+
- `src/reporter/presentation.ts` — the one module that makes a presentation
|
|
118
|
+
decision. It reads a finding, a score or a palette and returns the word, band
|
|
119
|
+
or descriptor to print. It opens no file, touches no clock and formats no
|
|
120
|
+
document, and a test enforces that.
|
|
121
|
+
|
|
14
122
|
### Changed
|
|
15
123
|
|
|
16
|
-
- **
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
124
|
+
- **Ruleset `01` now enforces 11 required checks instead of 4.** It required
|
|
125
|
+
only the four `build-test` matrix contexts, which meant the repo's entire
|
|
126
|
+
review surface — self-scan, certification, generated-docs-drift, site-build,
|
|
127
|
+
workflow-lint, property, fuzz — could be red while a PR still merged. That
|
|
128
|
+
made every one of those checks advice rather than a gate, and it meant the
|
|
129
|
+
deleted `merge-verify.yml` header was describing a ruleset that never
|
|
130
|
+
existed. The enforced set is now the four matrix entries plus
|
|
131
|
+
`property-tests`, `fuzz`, `site-build`, `workflow-lint`, `self-scan`,
|
|
132
|
+
`certification` and `generated-docs-drift` — every check that can actually
|
|
133
|
+
block. Deliberately still _not_ required: `detector-revision-diff` (advisory
|
|
134
|
+
by design — a hard fail would only manufacture routine revision bumps),
|
|
135
|
+
`scan` / `publish` (the Mjölnir PR report, which reviewers read but which
|
|
136
|
+
duplicates `self-scan` on the `src` tree), and the CodeQL/OSV workflows,
|
|
137
|
+
which report but do not gate. `strict_required_status_checks_policy` and
|
|
138
|
+
`do_not_enforce_on_create` are preserved, so a PR must still be current with
|
|
139
|
+
`main` to merge.
|
|
140
|
+
- **`docs/BRANCH-PROTECTION.md` now states the enforced set instead of an
|
|
141
|
+
aspiration.** It listed eight required checks; three of them were enforced.
|
|
142
|
+
Its verification command also read `branches/main/protection`, which does not
|
|
143
|
+
carry required checks at all on this repo — they live in ruleset `01` — so it
|
|
144
|
+
could not have worked. Both are corrected.
|
|
145
|
+
|
|
146
|
+
### Removed
|
|
147
|
+
|
|
148
|
+
- **The Snyk webhook is gone.** Snyk posted a `security/snyk (sergey-bar)`
|
|
149
|
+
commit status on every push, duplicating the first-party `osv-scanner.yml`
|
|
150
|
+
(`scan-pr` / `scan-scheduled`) and the Dependabot security updates that are
|
|
151
|
+
already configured. Repo webhooks are now empty. Restore with Snyk's own
|
|
152
|
+
integration settings if it is ever wanted back — the deleted hook pointed at
|
|
153
|
+
`https://api.snyk.io/webhook/github/4ac3f376-3f24-42f7-a2c4-8830700f671d`.
|
|
154
|
+
- **The `merge-verify` workflow is gone.** It re-ran the entire PR gate
|
|
155
|
+
(build, typecheck, lint, coverage, property, fuzz, site, self-scan, docs
|
|
156
|
+
regen) on every push to `main` — roughly doubling CI cost. Its stated
|
|
157
|
+
justification was that "a green PR that merges into a red main is not a
|
|
158
|
+
green PR", but that failure class is already closed by ruleset `01` running
|
|
159
|
+
`strict_required_status_checks_policy`, which refuses to merge a PR that is
|
|
160
|
+
not current with `main`. The file also asserted that ruleset `01` "requires
|
|
161
|
+
the complete PR matrix, property/fuzz, site, workflow-lint, self-scan,
|
|
162
|
+
certification, detector-revision, and generated-docs checks before merge" —
|
|
163
|
+
it did not, and never has. The ruleset requires only the four `build-test`
|
|
164
|
+
matrix contexts. Both the workflow and the claim are removed rather than left
|
|
165
|
+
to mislead a reviewer about what blocks a merge.
|
|
166
|
+
- **The `bench-advisory` CI job is gone.** It spent a full `npm ci` plus a
|
|
167
|
+
3 000-file benchmark run on every PR to produce a comparison that
|
|
168
|
+
`continue-on-error: true` then discarded, and whose only output was a
|
|
169
|
+
warning nobody could gate on. `npm run bench:scan`, the pinned
|
|
170
|
+
`.mjolnir/bench-baseline.json`, and their schema/comparison tests in
|
|
171
|
+
`tests/adversarial/bench-gate.spec.ts` are untouched — the benchmark is
|
|
172
|
+
still runnable and still tested, it just no longer costs every PR a runner.
|
|
173
|
+
- **Four copies of the soft job time-budget report.** Each of `build-test`,
|
|
174
|
+
`self-scan`, `generated-docs-drift` and `bench-advisory` re-derived its own
|
|
175
|
+
wall-clock minutes into `$GITHUB_STEP_SUMMARY` and printed a warning that,
|
|
176
|
+
by its own comment, "never gates". The jobs already declare
|
|
177
|
+
`timeout-minutes`, which is the hard bound, and GitHub reports job duration
|
|
178
|
+
natively.
|
|
179
|
+
|
|
180
|
+
### Fixed
|
|
181
|
+
|
|
182
|
+
- **Order in the report no longer depends on the machine that produced it.** 23
|
|
183
|
+
sites in `src/` sorted with a bare `localeCompare`, which resolves against the
|
|
184
|
+
process's default locale. `scan-cache.ts` was the worst: it sorted INSIDE
|
|
185
|
+
`hashDir`, so the order was fed to `hash.update(entry.name)` and became a
|
|
186
|
+
semantic input to the detector fingerprint that `isIncrementalSafe` keys off
|
|
187
|
+
— one repository, two fingerprints, one per locale. All 23 now use
|
|
188
|
+
`compareCodePoints` from `src/lib/compare.ts`, except two whose key is a
|
|
189
|
+
human-readable label, which use a comparator pinned to `en` rather than
|
|
190
|
+
inheriting the environment. `tests/contract/deterministic-ordering.spec.ts`
|
|
191
|
+
fails the build on any unpinned `localeCompare` in `src/`.
|
|
192
|
+
- **`mjolnir bind` can no longer be hijacked by a repository's own `git`.**
|
|
193
|
+
`src/engine/candidate-binding.ts` — called on every scan — ran
|
|
194
|
+
`execFileSync("git", …)` with a bare name. On Windows `CreateProcess` searches
|
|
195
|
+
the current directory before `PATH`, so a committed `git.exe` in an untrusted
|
|
196
|
+
repo chose the `commit` and `tree` that land in `runIdentity` and the evidence
|
|
197
|
+
graph. It is the last bare-name process launch left in `src/`, and
|
|
198
|
+
`tests/contract/no-bare-process-exec.spec.ts` now audits the whole tree.
|
|
199
|
+
- **The coverage exemption ledger can no longer drift silently.** Each
|
|
200
|
+
non-structural entry in `docs/COVERAGE-EXEMPTIONS.json` now carries
|
|
201
|
+
`defectSignatures` — regexes that must still match its file — or an explicit
|
|
202
|
+
`closureState` when the defect it described is gone. A signature that stops
|
|
203
|
+
matching fails the gate, so a code fix forces a ledger edit in the same
|
|
204
|
+
commit. Six entries were already describing defects the current code had
|
|
205
|
+
fixed (`maturity`, `business-case`, `dashboard`, `enterprise`, `exec-report`
|
|
206
|
+
and the `report-playwright` row), and are reclassified accordingly.
|
|
207
|
+
- **Skipped and expected-fail tests now have a price.** Nothing in the
|
|
208
|
+
repository counted them, so the count could grow from zero to hundreds with
|
|
209
|
+
every other signal still green. `docs/SKIP-BUDGET.json` records the current
|
|
210
|
+
counts — 8 skipped tests, 1 skipped file, 3 expected failures — measured by
|
|
211
|
+
`scripts/vitest-skip-budget-reporter.mjs` during the run. They may fall;
|
|
212
|
+
raising them fails `npm run skip:budget` and names the tests that spent the
|
|
213
|
+
budget. A missing or incomplete measurement fails too, so a reporter that
|
|
214
|
+
stops being wired in cannot read as a clean run.
|
|
215
|
+
- **The M26 release gate and the version-drift checks are covered where it
|
|
216
|
+
matters.** Both run inside `certify` and both were excluded from the coverage
|
|
217
|
+
ratchet, so their reject arms — the paths that only execute when something is
|
|
218
|
+
wrong — had no reviewer pressure on them. `tests/ledger/m26-validator-reject-arms.spec.ts`
|
|
219
|
+
is table-driven over every required field, so a field that is added without a
|
|
220
|
+
test fails; `tests/release/version-surface-drift-arms.spec.ts` covers the
|
|
221
|
+
fail-fast arms of the version synchronizer.
|
|
222
|
+
- **One error derivation, one `isRecord`, one crash-isolation path.** The tree
|
|
223
|
+
carried `isRecord` eight times (seven byte-identical), error rendering four
|
|
224
|
+
times under two names, and crash isolation seven times — and the copies
|
|
225
|
+
disagreed, which is the only reason any of this mattered. `String(obj)`
|
|
226
|
+
renders `[object Object]`, so the two _thinner_ error helpers turned a real
|
|
227
|
+
Node failure shape like `throw { code: "EISDIR" }` into a message naming
|
|
228
|
+
nothing — in a doctor detail, an MCP tool result and a `summary` line
|
|
229
|
+
respectively. The richer body survived the collapse, and
|
|
230
|
+
`isRecord` — the predicate every JSON reader uses to decide "this is an
|
|
231
|
+
object I may index into", which is a question a hostile saved report gets
|
|
232
|
+
to ask — is now the exported one in `src/lib/safe-json.ts`. The four
|
|
233
|
+
adapters' duplicated `try { rule.run } catch { onCrash }` is now
|
|
234
|
+
`runRuleIsolated`, because that block is what the `rulesCrashed` count is
|
|
235
|
+
built on and seven copies mean seven chances to lose it.
|
|
236
|
+
The per-adapter _filter chain_ and budget check were deliberately **not**
|
|
237
|
+
folded into the existing `runRulesShared`: the TypeScript adapter injects
|
|
238
|
+
an AST, framework tags, a lazy `codeText` getter, config-only gating and
|
|
239
|
+
`configGateMatches`, and it _aborts the whole file_ on budget exhaustion
|
|
240
|
+
where `runRulesShared` only skips one rule. Routing every adapter through
|
|
241
|
+
it as written would have silently changed which rules run — the exact
|
|
242
|
+
unmeasured claim this program exists to delete.
|
|
243
|
+
`tests/contract/single-error-helper.spec.ts` holds the counts at one, and
|
|
244
|
+
pins the object arm so a future "simplification" cannot drop it.
|
|
245
|
+
|
|
246
|
+
- **A crashed rule is no longer certified as a rule that stayed silent.**
|
|
247
|
+
The doc generator caught a rule THROW while running it against its own
|
|
248
|
+
must-not-fire fixture and returned `null`; the caller read that as
|
|
249
|
+
`fired: false` and committed, into a generated doc page and the
|
|
250
|
+
certification surface, the sentence _"Verified against … — a legitimate,
|
|
251
|
+
similar-looking pattern this rule correctly leaves alone."_ A detector that
|
|
252
|
+
crashes produces exactly the same absence of findings as one that correctly
|
|
253
|
+
abstains, so that claim had no evidence behind it — and it contradicted the
|
|
254
|
+
same function's own docstring, which says the field degrades honestly.
|
|
255
|
+
The outcome is now the tri-state the doctor's own model already defines:
|
|
256
|
+
`FIRED` (a real firewall violation), `DID_NOT_FIRE` (certified), and
|
|
257
|
+
`INCONCLUSIVE` (the rule did not run). An `INCONCLUSIVE` page says so and
|
|
258
|
+
says why, and it never renders the certification sentence. A missing
|
|
259
|
+
fixture is a fourth, separate thing — nothing was attempted — and says
|
|
260
|
+
that instead. The field is typed as a `FixtureOutcome` union rather than a
|
|
261
|
+
boolean, so reintroducing a derived `fired` flag does not typecheck.
|
|
262
|
+
|
|
263
|
+
- **A human-facing table stopped sorting by code unit.** Pinning every
|
|
264
|
+
`localeCompare` to code-unit order (the fix for the ambient-locale drift)
|
|
265
|
+
made the per-rule corpus-occurrence table read
|
|
266
|
+
`SeleniumHQ-selenium` before `microsoft-playwright-dotnet`, because `S`
|
|
267
|
+
sorts before `m`. Stable, and worse to read than the alphabetical order the
|
|
268
|
+
table implies it has. That surface is a table a person chooses repos from,
|
|
269
|
+
so it now uses an **explicitly pinned** locale — readable order, identical
|
|
270
|
+
on every machine, which is the actual defect being avoided.
|
|
271
|
+
|
|
272
|
+
- **Fifteen silent capability losses are now counted, and the count reaches the
|
|
273
|
+
report.** A `catch` in a detection path that returned a clean default made
|
|
274
|
+
the scan exit 0, report `analysisComplete`, and quietly lose the thing that
|
|
275
|
+
made its verdict trustworthy — the reader had no way to tell a whole scan
|
|
276
|
+
from a narrowed one. The worst case was `getCodeOnlyText` returning raw text
|
|
277
|
+
after a throw, which switches OFF the comment/string false-positive firewall
|
|
278
|
+
for that file rather than merely slowing it down. Every such site now records
|
|
279
|
+
a reason in one append-only ledger (`src/engine/degradation-ledger.ts`),
|
|
280
|
+
following the precedent already set by `parserRetryDegradationCount`, and the
|
|
281
|
+
scan result carries `analysisStatus.degradations` — reason-coded counts,
|
|
282
|
+
present only when something was actually lost, alongside a
|
|
283
|
+
`degraded:<reason>:<count>` entry in `reasons`. A non-empty set makes the
|
|
284
|
+
scan `partial`. `truncationReasons` is untouched: a degradation is not
|
|
285
|
+
truncation, and a reader must be able to tell "the scan stopped" from "the
|
|
286
|
+
scan lost a layer".
|
|
287
|
+
- Two distinctions the ledger refuses to blur. **Absent is not degraded:** a
|
|
288
|
+
Python, Java, Go or Rust repository has no `package.json`, and recording
|
|
289
|
+
that would mark every non-Node project partial while training readers to
|
|
290
|
+
ignore the field — so only a manifest that exists and will not parse
|
|
291
|
+
counts. **A no-AST adapter is not a downgrade either:** the same line
|
|
292
|
+
W1.4 draws for the deadline fallback.
|
|
293
|
+
- Two sites outside the original list were found by the new contract spec
|
|
294
|
+
and fixed in the same commit: the tree-sitter Java/C#/Python parses (left
|
|
295
|
+
alone, because the pipeline already counts every `parseAst → undefined` as
|
|
296
|
+
one parse fallback, and counting again would tally one file twice in two
|
|
297
|
+
different fields) and `hashDir`'s `readdirSync`, whose early `return`
|
|
298
|
+
omitted a whole subtree from the detector fingerprint — two rule trees
|
|
299
|
+
differing only inside an unreadable directory then hash identically, which
|
|
300
|
+
is a stale-cache hit wearing a fresh one's clothes.
|
|
301
|
+
- `mjolnir trend show` no longer prints "No trend data yet." and exits clean
|
|
302
|
+
for a history file whose every line failed to parse. The count of
|
|
303
|
+
discarded lines is now reported on stderr, and a history that exists but
|
|
304
|
+
cannot be read is `EXIT_INTERNAL` — the difference between no history and
|
|
305
|
+
unreadable history, which both used to arrive as the same empty array.
|
|
306
|
+
- `mjolnir explain` distinguishes _the rule found nothing_ from _the rule
|
|
307
|
+
never ran_. A rule that THREW on its own fixture produced the same output
|
|
308
|
+
as a correctly silent one, in the one surface whose purpose is to show
|
|
309
|
+
that a rule works. It now says so, with the reason.
|
|
310
|
+
- `tests/contract/no-uncounted-degradation.spec.ts` is the durable half: a
|
|
311
|
+
`catch` in the detection path that RETURNS A VALUE must record a reason,
|
|
312
|
+
rethrow, route to a counted sink, or be exempted with a stated failure
|
|
313
|
+
direction. The rule is about the _return_, not the `catch` — 37 of the 70
|
|
314
|
+
detection-path catches return nothing and cannot lie to a caller, and a
|
|
315
|
+
spec that listed all 70 as individual exemptions would be the same as no
|
|
316
|
+
exemption list at all.
|
|
317
|
+
|
|
318
|
+
- **Scanning a subdirectory no longer reports a framework it read from its
|
|
319
|
+
parent.** `runScan` walks up past the target to find the project root, and
|
|
320
|
+
re-anchors the scan root to the target when the target is a strict
|
|
321
|
+
descendant — but it kept the discovered root's parsed `package.json`. So
|
|
322
|
+
`mjolnir scan monorepo/packages/foo` reported `jest` because
|
|
323
|
+
`monorepo/package.json` said so: a read outside the explicit scan root, and
|
|
324
|
+
a framework verdict about a tree nobody pointed at. Framework detection now
|
|
325
|
+
works from in-root evidence only (config files and the anchor's own
|
|
326
|
+
manifest), and reports `frameworkDetectionUnknown` when that evidence is
|
|
327
|
+
absent — an absent verdict you can see, rather than a parent's you cannot.
|
|
328
|
+
Scanning the project root itself is unchanged. This is the CLI counterpart
|
|
329
|
+
to the containment property the MCP workspace-boundary contract already
|
|
330
|
+
enforced.
|
|
331
|
+
- **A scan that ran out of time no longer loses its AST stage silently.** When a
|
|
332
|
+
scan passes `--max-duration`, the remaining files were analyzed by regex
|
|
333
|
+
instead of AST — a different detection capability, not a slower version of the
|
|
334
|
+
same one, so the same tree produced different findings and a different score.
|
|
335
|
+
The mode was faithfully recorded in the scan cache and reported nowhere; the
|
|
336
|
+
only nearby disclosure, `file-budget`, fired incidentally because the
|
|
337
|
+
per-file rule-loop deadline was usually already spent too. Such a scan now
|
|
338
|
+
carries the truncation reason `ast-budget-fallback` and an
|
|
339
|
+
`analysisStatus.reasons` entry `ast-budget-fallback-files:<n>` saying how many
|
|
340
|
+
files were analyzed by regex, and is honestly `partial`. An adapter that
|
|
341
|
+
declares no AST stage at all (a YAML workflow, for instance) is a declared
|
|
342
|
+
capability envelope and is NOT reported as truncation. `truncationReasons` is
|
|
343
|
+
an open `string[]` by design, so consumers should test for members rather than
|
|
344
|
+
assert the exact array.
|
|
345
|
+
- **`frontier:contracts` was reporting 19 suites it never ran.** It named 24
|
|
346
|
+
test files; nineteen had been deleted by the `cc5fcb88` cleanup and its
|
|
347
|
+
follow-up. Vitest treats a missing path as "no tests here" rather than an
|
|
348
|
+
error, so the script exited 0 and printed "57 passed" while nineteen contract
|
|
349
|
+
suites — including every `m3x`/`m4x`/`m5x` frontier contract — silently did
|
|
350
|
+
not execute. A missing test file and a green test file are indistinguishable
|
|
351
|
+
from the outside, so a gate that passes because its subject is absent has
|
|
352
|
+
certified nothing. The list is rebuilt from the 23 contract suites that
|
|
353
|
+
exist (1 110 tests, not 57), and `npm run script:paths` now fails the build if
|
|
354
|
+
any `package.json` script names a path that is gone.
|
|
355
|
+
- The certification ladder's language vocabulary could not name the answer its
|
|
356
|
+
own admission gate produces. `admit()` returns `UNMEASURED` as the supported
|
|
357
|
+
state of any claim not backed by evidence, but `LANGUAGE_STATE_RANK` had no
|
|
358
|
+
word for it — so a language claiming `CERTIFIED` with an empty cohort was
|
|
359
|
+
handed a result its own vocabulary could not record. `UNMEASURED` is now
|
|
360
|
+
expressible in both vocabularies, and the four rule-level states
|
|
361
|
+
(`MEASURED-CORE` … `PROVISIONAL`) are declared in `RULE_ONLY_STATES` as
|
|
362
|
+
explicitly _not_ language outcomes rather than being filtered out by a
|
|
363
|
+
hand-maintained list.
|
|
364
|
+
- The contributor handover reported "Welcome aboard — the suite is in good
|
|
365
|
+
shape" whenever no issues were found, including when no run report had been
|
|
366
|
+
ingested and flakiness was therefore unmeasured. It now says what it did not
|
|
367
|
+
check.
|
|
368
|
+
- The confidence table's row labels were encoded as `"k|v"` strings and split
|
|
369
|
+
back apart, so any value containing a pipe was silently truncated. The rows
|
|
370
|
+
are typed pairs. The table also gained a `<caption>`, a `<thead>` and
|
|
371
|
+
`scope` attributes; it was previously a bare `<tbody>` a screen reader
|
|
372
|
+
announced as six unlabelled rows.
|
|
373
|
+
- `handoff` and `summary` each carried a private colourless score bar that
|
|
374
|
+
rendered 99 and 100 identically. Both now use the canonical `scoreGauge`, and
|
|
375
|
+
the redundant `theme.meter` alias is gone.
|
|
376
|
+
- The gap ledger gained three revalidation statuses
|
|
377
|
+
(`ALREADY_FIXED` / `CONFIRMED_STILL_OPEN` / `STALE_UNVERIFIABLE`). A row may
|
|
378
|
+
no longer be cleared without a revalidation that was actually run at a named
|
|
379
|
+
commit, and `STALE_UNVERIFIABLE` deliberately does not clear a release gate —
|
|
380
|
+
an unverifiable claim must never be able to unblock a release.
|
|
381
|
+
- `m26:github:sync` derived every one of 429 issue dispositions from GitHub's
|
|
382
|
+
own `state` field — a pure function of its input, containing no engineering
|
|
383
|
+
judgement — and `GAP-M26-002` then marked that script `fixed`, citing the
|
|
384
|
+
artifacts it produces. Dispositions now come from
|
|
385
|
+
`docs/issue-dispositions.json`, where each entry must carry a non-empty
|
|
386
|
+
reason and a verification naming a command or a source path, and may not be
|
|
387
|
+
verified by the script that wrote it. An untriaged issue stays open and is
|
|
388
|
+
flagged, never guessed and never silently closed. 215 of 429 are now visibly
|
|
389
|
+
awaiting a human decision, which is the honest count.
|
|
390
|
+
|
|
391
|
+
### Documentation
|
|
392
|
+
|
|
393
|
+
- `PRODUCT-ENHANCEMENT-ANALYSIS.md` is marked `SUPERSEDED-BY: BITTERSWEET` and
|
|
394
|
+
gained a section XII auditing its "all 16 features shipped" claims: six do not
|
|
395
|
+
hold (two shipped fabricated values, two shipped a different capability than
|
|
396
|
+
described, two never shipped). The document is kept as the record of what was
|
|
397
|
+
believed on 2026-09-20, because deleting it would destroy the only evidence
|
|
398
|
+
that the belief was wrong.
|
|
399
|
+
- `docs/RELEASE-TRAINS.md` now records why each of the six scheduled verb
|
|
400
|
+
removals is justified differently, so the deprecation notices do not all send
|
|
401
|
+
the same message to users who acted on different output.
|
|
402
|
+
|
|
403
|
+
## [4.0.0] — 2026-09-26
|
|
404
|
+
|
|
405
|
+
The first release whose reporting surface is checked against the working tree
|
|
406
|
+
rather than against a plan. Every box below is enforced by a gate in
|
|
407
|
+
`npm run certify`.
|
|
408
|
+
|
|
409
|
+
### What you get, relative to 3.0.0
|
|
410
|
+
|
|
411
|
+
**The product no longer reports numbers it did not measure.**
|
|
412
|
+
|
|
413
|
+
- [x] A scan that finished now says it finished. A minified bundle or a lockfile
|
|
414
|
+
in the repository used to downgrade a complete scan to `PARTIAL`, and the
|
|
415
|
+
terminal told you "some files were not analyzed, so the surface is
|
|
416
|
+
unverified" when every discovered test file _had_ been analyzed. Across
|
|
417
|
+
the 37-repository corpus, complete scans went from 13/37 to 29/37.
|
|
418
|
+
- [x] Unmeasured counts read `unknown — not measured`, not `0`. The trust report,
|
|
419
|
+
the PR comment and the step summary all printed "0 tests analyzed in 0
|
|
420
|
+
files" for a producer that had never measured a test declaration. A zero
|
|
421
|
+
is a claim; an absent measurement is not.
|
|
422
|
+
- [x] `handover` no longer tells you "the suite is in good shape" when no run
|
|
423
|
+
report was ingested and flakiness was therefore never checked. It now
|
|
424
|
+
names what it did not check.
|
|
425
|
+
- [x] The HTML dashboard produces the same bytes for the same repository. It
|
|
426
|
+
embedded `new Date()` in its visible body, so it could never be diffed in
|
|
427
|
+
review. `--deterministic` omits the timestamp entirely.
|
|
428
|
+
- [x] The trust report's evidence descriptor is the terminal's, on every surface.
|
|
429
|
+
HTML and Markdown showed four bare words (`E2 · deterministic`) where the
|
|
430
|
+
terminal showed the measured FP rate, the sample size, the trust rung and
|
|
431
|
+
what the runtime actually corroborated.
|
|
432
|
+
- [x] A finding with no evidence level derives one from its own type and
|
|
433
|
+
confidence. It used to default to `E2` — the strongest claim the product
|
|
434
|
+
can make — for findings nobody had measured.
|
|
435
|
+
- [x] The contributor handover no longer derives a dollar figure from a table of
|
|
436
|
+
invented incident costs. `business-case` multiplied a measured
|
|
437
|
+
false-positive rate by unsourced per-industry costs and printed the product
|
|
438
|
+
as "Expected Savings". A cost figure now requires `--incident-cost`, so the
|
|
439
|
+
number is yours. `--history` (which read no history) and `--projected`
|
|
440
|
+
(which divided by six) are gone, with reasons.
|
|
441
|
+
- [x] `mjolnir enterprise` no longer writes a deployment config claiming
|
|
442
|
+
`sso-saml`, an SSO guide telling you to add a config key nothing reads, or
|
|
443
|
+
auditor-facing SOC 2 / HIPAA / PCI-DSS templates mapping controls to
|
|
444
|
+
capabilities this product does not have. `sso` and `compliance` refuse;
|
|
445
|
+
`config` emits a capability manifest whose `notProvided` list records the
|
|
446
|
+
absences.
|
|
447
|
+
- [x] The dependency graph no longer prints a count derived from nothing.
|
|
448
|
+
"Reachable files: N" was always the number of files that went in.
|
|
449
|
+
|
|
450
|
+
**The product now sees things it was blind to.**
|
|
451
|
+
|
|
452
|
+
- [x] Node-native TypeScript tests (`.test.mts`, `.spec.mts`, `.test.cts`,
|
|
453
|
+
`.spec.cts`) are scanned. They were not mis-scored — they were never
|
|
454
|
+
scanned, so every rule was silent on them with no finding and no note.
|
|
455
|
+
Verified with two byte-identical tests: the `.ts` copy produced the
|
|
456
|
+
finding, the `.mts` copy produced nothing at all.
|
|
457
|
+
- [x] The CI matrix installs cleanly on all three platforms. `npm ci` failed on
|
|
458
|
+
every job before a test ran, and then failed again with a missing
|
|
459
|
+
`lightningcss` binary for Linux, because a lockfile regenerated on Windows
|
|
460
|
+
cannot contain the other platforms' optional dependencies.
|
|
461
|
+
|
|
462
|
+
**The gates now catch these instead of the next person.**
|
|
463
|
+
|
|
464
|
+
- [x] `report:honesty` — no surface may print a zero for an absent measurement.
|
|
465
|
+
Four reviewed exceptions, each with a reason and a follow-up.
|
|
466
|
+
- [x] `thresholds:parity` — no score-band literal outside the registry. It
|
|
467
|
+
immediately caught a selector-health threshold that shared a number with a
|
|
468
|
+
trust band but not a meaning.
|
|
469
|
+
- [x] `script:paths` — no `package.json` script may name a path that is missing
|
|
470
|
+
or uncommitted. It found `frontier:contracts` naming 19 test files deleted
|
|
471
|
+
by an earlier cleanup: Vitest treats a missing path as "no tests here", so
|
|
472
|
+
the script exited 0 reporting "57 passed" while nineteen contract suites
|
|
473
|
+
never ran. Rebuilt from the 23 that exist — 1 110 tests, not 57.
|
|
474
|
+
- [x] `lockfile:platforms` — the lockfile must carry the optional binaries for
|
|
475
|
+
every platform CI builds on. The failure is invisible locally by
|
|
476
|
+
construction, so it needed a gate that runs everywhere.
|
|
477
|
+
- [x] `claims:revalidate` — a claim asserted `fixed` must rest on a
|
|
478
|
+
candidate-bound, independent revalidation. It rejects evidence citing the
|
|
479
|
+
artifact it is meant to prove, and downgrades rather than deletes a claim
|
|
480
|
+
that loses its evidence.
|
|
481
|
+
- [x] `verbs:budget` — the 5.x law: one new verb requires one removal or a merge.
|
|
482
|
+
- [x] `m26:gaps:revalidate` — re-runs each gap row's own command and records the
|
|
483
|
+
exit code and the commit it ran at.
|
|
484
|
+
- [x] `release:verify` reports every blocker in one run. It was a `&&` chain, so
|
|
485
|
+
it stopped at the first of four and you met them one per run.
|
|
486
|
+
- [x] The coverage gate is a floor and a ratchet, not one number pretending to
|
|
487
|
+
be both.
|
|
488
|
+
|
|
489
|
+
**Truth in the project's own records.**
|
|
490
|
+
|
|
491
|
+
- [x] The gap ledger was revalidated: all 17 rows had their own
|
|
492
|
+
`revalidation_command` run against this tree. `GAP-M26-005` read
|
|
493
|
+
`fixed` / PASS while its command _fails_. A cleared row can no longer
|
|
494
|
+
exist without a revalidation bound to a named commit, and
|
|
495
|
+
`STALE_UNVERIFIABLE` deliberately cannot unblock a release.
|
|
496
|
+
- [x] **A gap row could be marked fixed while its own revalidation failed.**
|
|
497
|
+
The `exit_code === 0` check was reachable through only one of the four
|
|
498
|
+
combinations the schema allows, so a `fixed` row — or an
|
|
499
|
+
`ALREADY_FIXED` row carrying closure evidence — was accepted with
|
|
500
|
+
`exit_code: 1` recorded in its own revalidation. Three of those four
|
|
501
|
+
routes cleared the row. The test that covered it hid the hole, because
|
|
502
|
+
the fixture only set `closure_evidence` for `fixed`.
|
|
503
|
+
- [x] The 429 GitHub dispositions no longer derive from GitHub's own `state`
|
|
504
|
+
field. A snapshot cannot prove itself; each disposition now needs a reason
|
|
505
|
+
and a verification. 215 issues are visibly awaiting a human decision,
|
|
506
|
+
which is the honest count.
|
|
507
|
+
- [x] `PRODUCT-ENHANCEMENT-ANALYSIS.md` claimed 16 shipped features; six do not
|
|
508
|
+
hold. It is marked `SUPERSEDED-BY` and carries a per-feature audit, kept
|
|
509
|
+
rather than deleted because deleting it would destroy the only evidence
|
|
510
|
+
that the belief was wrong.
|
|
511
|
+
|
|
512
|
+
**Found by the pre-release bug sweep, after the fix list was written.** Both
|
|
513
|
+
were live in 3.0.0 and neither was in the plan.
|
|
514
|
+
|
|
515
|
+
- [x] `mjolnir business-case <path>` answered a path that does not exist with
|
|
516
|
+
exit 0 and a clean report: "Measured: 0 of 0 findings carry a
|
|
517
|
+
corpus-measured FP rate". It called `runScan` directly and so never
|
|
518
|
+
passed through `validateScanTarget` — the check whose own comment says a
|
|
519
|
+
typo'd CI path must be a loud red, never a silent green. Thirteen of the
|
|
520
|
+
fifteen scanning verbs were already loud; this one was not, and a green
|
|
521
|
+
table over a path that was never scanned is the most dangerous output
|
|
522
|
+
this product can produce. Now exit 10.
|
|
523
|
+
- [x] The extended TypeScript test-file regex was probed against 27 shapes:
|
|
524
|
+
all 12 that must match do, all 15 that must not are rejected —
|
|
525
|
+
including `a.min.mts`, `a.spec.cts.bak` and `notatest.mts`.
|
|
526
|
+
- [x] `ships()` in `script:paths` was probed for the failure modes a new gate
|
|
527
|
+
usually has: an empty directory, a directory holding only untracked
|
|
528
|
+
files, and a symlink loop. All resolve correctly and none hangs. A
|
|
529
|
+
first version treated a directory as "tracked" only if `git ls-files`
|
|
530
|
+
listed the directory itself, which it never does — that produced two
|
|
531
|
+
false positives and is fixed.
|
|
532
|
+
|
|
533
|
+
### Known open, deliberately
|
|
534
|
+
|
|
535
|
+
- [ ] **Trust certification is not claimed, and this release was authorized
|
|
536
|
+
without it.** `candidate:readiness` reports `BLOCKED` /
|
|
537
|
+
`INCONCLUSIVE` on all ten of its checks — candidate identity is not a
|
|
538
|
+
release candidate, the candidate SHA is not authorized, engineering
|
|
539
|
+
certification is not complete, and external validation, protected holdout
|
|
540
|
+
proof, real-world repository proof, the platform matrix, consumer install
|
|
541
|
+
proof and remote workflow proof are all absent.
|
|
542
|
+
`m26:audit` is likewise unsatisfied. Both ran, both printed, and neither
|
|
543
|
+
blocked the publish, because `vars.STABLE_RELEASE_AUTHORIZED` is `true` —
|
|
544
|
+
the same explicit override 4.0.0 shipped under.
|
|
545
|
+
The distinction is the point: **publication of a package is not the same
|
|
546
|
+
claim as a certification of it.** 5.0.0 asserts that the reporting
|
|
547
|
+
surface is honest, that a silent capability loss is disclosed, and that no
|
|
548
|
+
verdict is published without a measurement behind it. It does not assert
|
|
549
|
+
that a holdout corpus, a platform matrix and an external validation run
|
|
550
|
+
have been completed, because none of them have.
|
|
551
|
+
Consumers are not asked to take that on trust: the npm package publishes
|
|
552
|
+
the candidate manifest, which reads `NOT_CERTIFIED` and
|
|
553
|
+
`NOT_AUTHORIZED`, and every one of those ten blockers is enumerated above
|
|
554
|
+
rather than summarised. Closing them is the work that would make a
|
|
555
|
+
certification claim possible; it is not work this release did.
|
|
556
|
+
- [ ] `CERTIFICATION_STATES` is not in ladder order, so `rankOf` — the only
|
|
557
|
+
ordering function — ranks `KNOWN` above `TRUST-COMPLETE`, and
|
|
558
|
+
`requiresEvidence` asks `BLOCKED` and `DEGRADED` for a corpus their own
|
|
559
|
+
doc comment says they need none. Two `it.fails` tests record it, so
|
|
560
|
+
reordering the array fails the build. Fixing it moves `rankOf` for every
|
|
561
|
+
state and changes admission outcomes, so it is a contract change, not a
|
|
562
|
+
patch.
|
|
563
|
+
- [ ] 23 corpus count-drifts are unreviewed, the largest being `QA-PY-007`
|
|
564
|
+
firing 1332 times in apache-airflow and 92 now. Not accepted: running
|
|
565
|
+
`--update` without reading them would delete the only evidence that
|
|
566
|
+
detection changed.
|
|
567
|
+
- [ ] 8 of 37 corpus repositories report `PARTIAL` because test-support files
|
|
568
|
+
under `__tests__/` are not scanned. The verdict is honest — an incomplete
|
|
569
|
+
scan is not a pass — and the fix is a design decision about what "the
|
|
570
|
+
surface" means, not a patch.
|
|
571
|
+
- [ ] 215 of 229 GitHub issues are untriaged. Prepared dispositions exist in
|
|
572
|
+
`docs/issue-dispositions.json`; none has been applied, because closing a
|
|
573
|
+
public issue is destructive and is a person's decision.
|
|
574
|
+
- [ ] `BW-022` (one workspace model, real graph edges) is open. The plan claimed
|
|
575
|
+
it is "what makes `impact` honest"; it is not — `impact` never used the
|
|
576
|
+
graph and is honest on its own path. The graph is a no-op on an unshipped
|
|
577
|
+
code path, and the function now reports that instead of implying otherwise.
|
|
578
|
+
|
|
579
|
+
### Upgrade notes
|
|
580
|
+
|
|
581
|
+
- Node-native TypeScript test files are now scanned, so a project using
|
|
582
|
+
`.mts`/`.cts` tests **will see new findings**. That is the fix working, not a
|
|
583
|
+
regression.
|
|
584
|
+
- `business-case` requires `--incident-cost` for any dollar figure, and now
|
|
585
|
+
exits 10 on a target that does not exist instead of printing an empty report.
|
|
586
|
+
- `enterprise sso` and `enterprise compliance` now exit non-zero with a reason.
|
|
587
|
+
- The trust report JSON's `tests.files` and `tests.declarations` are `null`
|
|
588
|
+
rather than `0` when unmeasured.
|
|
589
|
+
|
|
590
|
+
## [4.0.0-rc.1] — 2026-09-25
|
|
591
|
+
|
|
592
|
+
### Breaking changes
|
|
593
|
+
|
|
594
|
+
- The next published version must be a new major line. Generated CI is now
|
|
595
|
+
advisory-first while the direct composite Action remains blocking by default;
|
|
596
|
+
suppression metrics changed from configured to matched findings, and
|
|
597
|
+
zero-finding output no longer implies a clean or green suite. Publishing
|
|
598
|
+
these changes as another `3.0.0` is prohibited.
|
|
599
|
+
|
|
600
|
+
### Changed
|
|
601
|
+
|
|
602
|
+
- **Advisory-first generated CI adoption** — `mjolnir ci install` now
|
|
603
|
+
defaults to non-blocking findings and non-blocking partial scans. The direct
|
|
604
|
+
composite Action remains blocking by default. Blocking behavior is explicit
|
|
605
|
+
through `--gate error`, `--gate warning`, or `fail-on`.
|
|
20
606
|
- Suppression counts now report matched findings, not configured entries;
|
|
21
607
|
`suppression-gate` evaluates all-tier pre-suppression findings and enforces
|
|
22
608
|
total-count limits.
|
|
@@ -34,6 +620,44 @@ once shipped, so this file is the record of what changed between versions.
|
|
|
34
620
|
before it ships; and `scripts/check-ci-local-parity.mjs`, which keeps
|
|
35
621
|
`ci.yml`, `merge-verify.yml` and the local gate exercising the same
|
|
36
622
|
command list. All additive — no frozen surface changed.
|
|
623
|
+
- **M26–M50 execution ledger** — added `docs/ROADMAP.yaml`, dated GitHub
|
|
624
|
+
issue/milestone/PR reconciliation, explicit issue dispositions, an
|
|
625
|
+
append-only gap ledger, a finite support matrix, and an external-validation
|
|
626
|
+
record. Approval authority and staged dependency resolution are recorded;
|
|
627
|
+
external evidence remains `BLOCKED`/`NOT_RUN` and cannot be synthesized.
|
|
628
|
+
- **Provisional capability contracts** — added bounded, non-promoting
|
|
629
|
+
contracts for runtime evidence, provider capabilities, QA domain records,
|
|
630
|
+
agent decision receipts, plugin packs, change intelligence, mutation
|
|
631
|
+
sensitivity, adversarial challenges, simulation, language/framework packs,
|
|
632
|
+
cross-repository systems, history, detector lifecycle, research, benchmark,
|
|
633
|
+
scale, UX parity, and Trust OS release proofs. Each has focused hostile,
|
|
634
|
+
stale, foreign, malformed, and recovery coverage.
|
|
635
|
+
- **Release control surfaces** — added exact version-surface synchronization,
|
|
636
|
+
RC-aware changelog validation, candidate manifest refresh/readiness,
|
|
637
|
+
reporter ramp policy, fail-closed M26 audit gates, and current-major-only
|
|
638
|
+
Action tag maintenance.
|
|
639
|
+
- **README release preparation** — canonical and translated README install
|
|
640
|
+
references, release-status blocks, and new command rows are synchronized to
|
|
641
|
+
`3.0.0`; English fallback blocks are explicitly marked for human translation
|
|
642
|
+
review, and the canonical README states the certification boundary.
|
|
643
|
+
- **Release proof hardening** — stable publication now generates and attaches
|
|
644
|
+
an SPDX SBOM, rejects same-version registry artifacts with a different
|
|
645
|
+
integrity digest, updates only the current `v3` Action major, and never
|
|
646
|
+
clobbers release assets.
|
|
647
|
+
- **Support matrix reconciliation** — 136 cells are classified as 90 tested,
|
|
648
|
+
44 explicitly blocked, and 2 not applicable, with evidence paths and revisit
|
|
649
|
+
triggers checked locally.
|
|
650
|
+
|
|
651
|
+
### Fixed
|
|
652
|
+
|
|
653
|
+
- Corpus runs now record complete rule-crash provenance, reject parser
|
|
654
|
+
fallbacks, and support a provenance-only refresh that cannot update counts.
|
|
655
|
+
- QA-PY-004 sanitizes assertion roots before constructing a pattern, removing
|
|
656
|
+
corpus crashes caused by nested call-like targets.
|
|
657
|
+
- Pathological stress runs no longer mask crashes with `|| true`; soak evidence
|
|
658
|
+
no longer claims parent-process RSS as child memory.
|
|
659
|
+
- Exact registry-install smoke can install and verify a published version
|
|
660
|
+
instead of substituting the checkout tarball.
|
|
37
661
|
|
|
38
662
|
## [3.0.0] — 2026-09-24
|
|
39
663
|
|
|
@@ -286,8 +910,6 @@ Complete implementation of the Mjolnir Master Engineering Roadmap & Product Spec
|
|
|
286
910
|
- `src/commands/doctor.ts` gains check 11 (rule metadata contract validation).
|
|
287
911
|
- 4 fingerprint implementations consolidated into `src/engine/finding-identity.ts`.
|
|
288
912
|
|
|
289
|
-
## [Unreleased] — Nordic brand pass
|
|
290
|
-
|
|
291
913
|
### Changed
|
|
292
914
|
|
|
293
915
|
- Terminal report: section headers are marked `▍ TITLE` instead of
|