mjolnir-qa 0.5.6 → 0.5.7
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 +8 -0
- package/dist/cli.d.mts +10 -1
- package/dist/cli.mjs +229 -13
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -74,6 +74,14 @@ scan`. Unknown categories are a usage error (exit 10).
|
|
|
74
74
|
- help registry gained `why`, `handoff`, `install` and the new flags;
|
|
75
75
|
site/reference/cli.md documents the handoff trust model.
|
|
76
76
|
|
|
77
|
+
## [0.5.7] — 2026-09-06
|
|
78
|
+
|
|
79
|
+
### Changes since 0.5.6
|
|
80
|
+
|
|
81
|
+
- Merge pull request #40 from Sergey-Bar/eng/machine-contract-2.1
|
|
82
|
+
- test: plural + unknown-cause arms for inconclusive resolution rendering
|
|
83
|
+
- feat: machine verification contract + finding detectorRevision + lifecycle resolution (blueprint §12-§15, §17, §25)
|
|
84
|
+
|
|
77
85
|
## [0.5.6] — 2026-09-06
|
|
78
86
|
|
|
79
87
|
### Changes since 0.5.5
|
package/dist/cli.d.mts
CHANGED
|
@@ -103,6 +103,15 @@ interface Finding {
|
|
|
103
103
|
measuredFpRate?: number;
|
|
104
104
|
/** Classified (TP+FP) verdicts behind `measuredFpRate`. */
|
|
105
105
|
measuredFpN?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Detector revision of the rule that produced this finding, stamped
|
|
108
|
+
* from the registry at scan time (blueprint §13, G-16). Identity
|
|
109
|
+
* participates: "same ruleId, different detectorRevision" is formally
|
|
110
|
+
* a different detector for comparison purposes. Additive within
|
|
111
|
+
* schemaVersion 1; absent means the producer predates the field
|
|
112
|
+
* (revision-unknown).
|
|
113
|
+
*/
|
|
114
|
+
detectorRevision?: number;
|
|
106
115
|
/**
|
|
107
116
|
* Runtime corroboration from a real run report (plan §16), stamped
|
|
108
117
|
* when a report was available and matched this finding's file/test.
|
|
@@ -690,7 +699,7 @@ declare const runScan: typeof runScan$1, buildUniversalRules: typeof buildUniver
|
|
|
690
699
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
691
700
|
* `tests/version-consistency.spec.ts` locally.
|
|
692
701
|
*/
|
|
693
|
-
declare const CLI_VERSION = "0.5.
|
|
702
|
+
declare const CLI_VERSION = "0.5.7";
|
|
694
703
|
/** A usage-error detail: the offending token, when one exists. */
|
|
695
704
|
interface UsageErrorDetail {
|
|
696
705
|
/** The unknown flag or rejected value (e.g. `--nope`, `loud`). */
|
package/dist/cli.mjs
CHANGED
|
@@ -11407,6 +11407,7 @@ async function runScan$1(args, hooks = {}) {
|
|
|
11407
11407
|
});
|
|
11408
11408
|
const tierByRuleId = tiers;
|
|
11409
11409
|
const pluginsLoaded = pluginMeta;
|
|
11410
|
+
const REVISION_BY_RULE_ID = new Map(activeRules.map((r) => [r.id, r.detectorRevision ?? MEASURED_FP[r.id]?.detectorRevision ?? 1]));
|
|
11410
11411
|
const cache = args.cache ? createScanCache(workspace.root) : disabledScanCache;
|
|
11411
11412
|
const rulesDigest = computeRulesDigest(activeRules);
|
|
11412
11413
|
for (const perr of pluginErrors) findings.push({
|
|
@@ -11619,6 +11620,10 @@ async function runScan$1(args, hooks = {}) {
|
|
|
11619
11620
|
f.measuredFpN = m.n;
|
|
11620
11621
|
}
|
|
11621
11622
|
}
|
|
11623
|
+
for (const f of findings) {
|
|
11624
|
+
const rev = REVISION_BY_RULE_ID.get(f.ruleId);
|
|
11625
|
+
if (rev !== void 0) f.detectorRevision = rev;
|
|
11626
|
+
}
|
|
11622
11627
|
enforceTierPolicy(findings, tierByRuleId);
|
|
11623
11628
|
for (const f of findings) f.fixGroupId = f.ruleId;
|
|
11624
11629
|
const runtimeReportPath = discoverRuntimeReport$1(scanRoot.root);
|
|
@@ -11676,6 +11681,103 @@ async function runScan$1(args, hooks = {}) {
|
|
|
11676
11681
|
await releaseTreeSitterResources();
|
|
11677
11682
|
return result;
|
|
11678
11683
|
}
|
|
11684
|
+
/**
|
|
11685
|
+
* Fields that participate in the verification digest: identity (ruleId,
|
|
11686
|
+
* detectorRevision, file, line, column) + evidence (severity,
|
|
11687
|
+
* evidenceLevel, trustLevel, confidence, findingType) + lifecycle-adjacent
|
|
11688
|
+
* metadata (fixGroupId). PRESENTATION (message/why/fix) is excluded by
|
|
11689
|
+
* design: rewording a detector's prose must not change the digest of a
|
|
11690
|
+
* scan whose semantics are identical.
|
|
11691
|
+
*/
|
|
11692
|
+
function digestView(f) {
|
|
11693
|
+
return {
|
|
11694
|
+
ruleId: f.ruleId,
|
|
11695
|
+
detectorRevision: f.detectorRevision ?? null,
|
|
11696
|
+
file: f.file,
|
|
11697
|
+
line: f.line,
|
|
11698
|
+
column: f.column,
|
|
11699
|
+
severity: f.severity,
|
|
11700
|
+
evidenceLevel: f.evidenceLevel ?? null,
|
|
11701
|
+
trustLevel: f.trustLevel ?? null,
|
|
11702
|
+
confidence: f.confidence,
|
|
11703
|
+
findingType: f.findingType,
|
|
11704
|
+
fixGroupId: f.fixGroupId ?? null
|
|
11705
|
+
};
|
|
11706
|
+
}
|
|
11707
|
+
/**
|
|
11708
|
+
* Deterministic canonical serialization: findings in ScanResult order
|
|
11709
|
+
* (already compareFindings-sorted), stable key insertion order, no
|
|
11710
|
+
* indentation. JSON.stringify of plain objects with fixed key order is
|
|
11711
|
+
* deterministic across runs and platforms. `durationMs` is EXCLUDED —
|
|
11712
|
+
* it is wall-clock, not semantics; including it would make two runs of
|
|
11713
|
+
* the same repo produce different digests.
|
|
11714
|
+
*/
|
|
11715
|
+
function canonicalScanJson(result) {
|
|
11716
|
+
return JSON.stringify({
|
|
11717
|
+
score: result.score,
|
|
11718
|
+
partial: result.partial,
|
|
11719
|
+
analysisStatus: {
|
|
11720
|
+
discovery: result.analysisStatus.discovery,
|
|
11721
|
+
rules: result.analysisStatus.rules,
|
|
11722
|
+
skippedFiles: result.analysisStatus.skippedFiles,
|
|
11723
|
+
rulesCrashed: result.analysisStatus.rulesCrashed ?? 0,
|
|
11724
|
+
truncationReasons: result.analysisStatus.truncationReasons ?? []
|
|
11725
|
+
},
|
|
11726
|
+
findings: result.findings.map(digestView)
|
|
11727
|
+
});
|
|
11728
|
+
}
|
|
11729
|
+
/** Advisory when the DERIVED evidence level is E0 (same rule as scoring). */
|
|
11730
|
+
function isAdvisory(f) {
|
|
11731
|
+
return (f.evidenceLevel ?? deriveEvidenceLevel(f.findingType, f.confidence)) === "E0";
|
|
11732
|
+
}
|
|
11733
|
+
function levelFor(f) {
|
|
11734
|
+
if (f.severity === "error") return "failure";
|
|
11735
|
+
if (f.severity === "warning") return "warning";
|
|
11736
|
+
return "notice";
|
|
11737
|
+
}
|
|
11738
|
+
function countBy(findings, predicate) {
|
|
11739
|
+
return findings.filter(predicate).length;
|
|
11740
|
+
}
|
|
11741
|
+
/**
|
|
11742
|
+
* Project a canonical ScanResult into the machine contract. Pure:
|
|
11743
|
+
* same input → same output, always (§16 semantic integrity, §25.1).
|
|
11744
|
+
*/
|
|
11745
|
+
function buildMachineContract(result) {
|
|
11746
|
+
const digest = createHash("sha256").update(canonicalScanJson(result)).digest("hex");
|
|
11747
|
+
const annotations = result.findings.map((f) => ({
|
|
11748
|
+
path: f.file,
|
|
11749
|
+
start_line: f.line,
|
|
11750
|
+
annotation_level: levelFor(f),
|
|
11751
|
+
message: `${f.ruleId}: ${f.message}`,
|
|
11752
|
+
ruleId: f.ruleId,
|
|
11753
|
+
...f.detectorRevision !== void 0 ? { detectorRevision: f.detectorRevision } : {},
|
|
11754
|
+
advisory: isAdvisory(f)
|
|
11755
|
+
}));
|
|
11756
|
+
return {
|
|
11757
|
+
contractVersion: 1,
|
|
11758
|
+
summary: {
|
|
11759
|
+
digest: `sha256:${digest}`,
|
|
11760
|
+
findings: result.findings.length,
|
|
11761
|
+
score: result.score,
|
|
11762
|
+
errors: countBy(result.findings, (f) => f.severity === "error"),
|
|
11763
|
+
warnings: countBy(result.findings, (f) => f.severity === "warning"),
|
|
11764
|
+
infos: countBy(result.findings, (f) => f.severity === "info"),
|
|
11765
|
+
advisory: countBy(result.findings, isAdvisory)
|
|
11766
|
+
},
|
|
11767
|
+
annotations: annotations.slice(0, 50),
|
|
11768
|
+
annotationsTruncated: result.findings.length > 50,
|
|
11769
|
+
completeness: {
|
|
11770
|
+
partial: result.partial,
|
|
11771
|
+
discovery: result.analysisStatus.discovery,
|
|
11772
|
+
rules: result.analysisStatus.rules,
|
|
11773
|
+
skippedFiles: result.analysisStatus.skippedFiles,
|
|
11774
|
+
rulesCrashed: result.analysisStatus.rulesCrashed ?? 0,
|
|
11775
|
+
truncationReasons: result.analysisStatus.truncationReasons ?? [],
|
|
11776
|
+
frameworkDetectionUnknown: result.frameworkDetectionUnknown,
|
|
11777
|
+
durationMs: result.analysisStatus.durationMs
|
|
11778
|
+
}
|
|
11779
|
+
};
|
|
11780
|
+
}
|
|
11679
11781
|
//#endregion
|
|
11680
11782
|
//#region src/scorer/prioritize.ts
|
|
11681
11783
|
/**
|
|
@@ -12473,7 +12575,7 @@ function renderSarif(result, repoRootUri) {
|
|
|
12473
12575
|
tool: { driver: {
|
|
12474
12576
|
name: "Mjölnir",
|
|
12475
12577
|
informationUri: "https://github.com/Sergey-Bar/Mjolnir",
|
|
12476
|
-
version: "0.5.
|
|
12578
|
+
version: "0.5.7",
|
|
12477
12579
|
rules: [...rules.values()].map((r) => {
|
|
12478
12580
|
const meta = RULES.find((x) => x.id === r.id);
|
|
12479
12581
|
return {
|
|
@@ -12888,8 +12990,17 @@ function renderPrComment(result, options = {}) {
|
|
|
12888
12990
|
lines.push("");
|
|
12889
12991
|
lines.push(`_Advisory only — this comment never blocks merging. Generated by [Mjölnir](${options.repoUrl ?? "https://github.com/Sergey-Bar/Mjolnir"})._`);
|
|
12890
12992
|
if (usingDiff && diff.resolvedFindings.length > 0) {
|
|
12891
|
-
|
|
12892
|
-
|
|
12993
|
+
const verified = diff.resolvedFindings.filter((f) => f.resolution.status === "VERIFIED-RESOLVED");
|
|
12994
|
+
const inconclusive = diff.resolvedFindings.filter((f) => f.resolution.status === "INCONCLUSIVE");
|
|
12995
|
+
if (verified.length > 0) {
|
|
12996
|
+
lines.push("");
|
|
12997
|
+
lines.push(`✨ ${verified.length} pre-existing finding${verified.length === 1 ? "" : "s"} verified as fixed in this PR.`);
|
|
12998
|
+
}
|
|
12999
|
+
if (inconclusive.length > 0) {
|
|
13000
|
+
const causes = [...new Set(inconclusive.map((f) => f.resolution.cause ?? "unknown"))].join(", ");
|
|
13001
|
+
lines.push("");
|
|
13002
|
+
lines.push(`ℹ ${inconclusive.length} pre-existing finding${inconclusive.length === 1 ? "" : "s"} disappeared, but this scan can't confirm a fix (${causes}).`);
|
|
13003
|
+
}
|
|
12893
13004
|
}
|
|
12894
13005
|
return lines.join("\n");
|
|
12895
13006
|
}
|
|
@@ -15185,7 +15296,7 @@ function gitBuffer(root, args) {
|
|
|
15185
15296
|
}
|
|
15186
15297
|
}
|
|
15187
15298
|
/** Fingerprint a finding for cross-commit matching (line numbers shift). */
|
|
15188
|
-
function fingerprint$
|
|
15299
|
+
function fingerprint$2(f) {
|
|
15189
15300
|
return `${f.ruleId}\u0000${f.file}\u0000${f.message}`;
|
|
15190
15301
|
}
|
|
15191
15302
|
async function computeImpact(root, options) {
|
|
@@ -15292,9 +15403,9 @@ async function computeImpact(root, options) {
|
|
|
15292
15403
|
}
|
|
15293
15404
|
const headResult = await options.runScan(root);
|
|
15294
15405
|
const baseSet = /* @__PURE__ */ new Map();
|
|
15295
|
-
for (const f of baseResult.findings) baseSet.set(fingerprint$
|
|
15406
|
+
for (const f of baseResult.findings) baseSet.set(fingerprint$2(f), f);
|
|
15296
15407
|
const headSet = /* @__PURE__ */ new Map();
|
|
15297
|
-
for (const f of headResult.findings) headSet.set(fingerprint$
|
|
15408
|
+
for (const f of headResult.findings) headSet.set(fingerprint$2(f), f);
|
|
15298
15409
|
const resolved = [];
|
|
15299
15410
|
for (const [key, f] of baseSet) if (!headSet.has(key)) resolved.push({
|
|
15300
15411
|
ruleId: f.ruleId,
|
|
@@ -15352,6 +15463,79 @@ function renderImpact(report) {
|
|
|
15352
15463
|
return lines.join("\n");
|
|
15353
15464
|
}
|
|
15354
15465
|
//#endregion
|
|
15466
|
+
//#region src/engine/resolution.ts
|
|
15467
|
+
/** Correlation identity (v1-compatible): ruleId\0file\0message. */
|
|
15468
|
+
function fingerprint$1(entry) {
|
|
15469
|
+
return `${entry.ruleId}\u0000${entry.file}\u0000${entry.message}`;
|
|
15470
|
+
}
|
|
15471
|
+
/**
|
|
15472
|
+
* The §15 ordered algorithm. Pure; first match wins.
|
|
15473
|
+
*/
|
|
15474
|
+
function resolve$1(input) {
|
|
15475
|
+
const { entry, baseline, current } = input;
|
|
15476
|
+
const fp = fingerprint$1(entry);
|
|
15477
|
+
const comparedAgainst = input.baselineCommit ?? baseline.commit ?? "unknown baseline";
|
|
15478
|
+
if (current.partial || current.analysisStatus.rules !== "complete") return {
|
|
15479
|
+
status: "INCONCLUSIVE",
|
|
15480
|
+
cause: "partial",
|
|
15481
|
+
comparedAgainst
|
|
15482
|
+
};
|
|
15483
|
+
if (input.crashedRuleIds?.has(entry.ruleId)) return {
|
|
15484
|
+
status: "INCONCLUSIVE",
|
|
15485
|
+
cause: "crash",
|
|
15486
|
+
comparedAgainst
|
|
15487
|
+
};
|
|
15488
|
+
if (input.skippedFiles?.has(entry.file)) return {
|
|
15489
|
+
status: "INCONCLUSIVE",
|
|
15490
|
+
cause: "skipped",
|
|
15491
|
+
comparedAgainst
|
|
15492
|
+
};
|
|
15493
|
+
if (input.suppressed?.has(`${entry.ruleId}\u0000${entry.file}`)) return {
|
|
15494
|
+
status: "SUPPRESSED",
|
|
15495
|
+
comparedAgainst
|
|
15496
|
+
};
|
|
15497
|
+
if (input.excludedFiles?.has(entry.file)) return {
|
|
15498
|
+
status: "DISAPPEARED-NON-FIX",
|
|
15499
|
+
cause: "excluded",
|
|
15500
|
+
comparedAgainst
|
|
15501
|
+
};
|
|
15502
|
+
const entryRev = entry.detectorRevision;
|
|
15503
|
+
if (entryRev === void 0) return {
|
|
15504
|
+
status: "INCONCLUSIVE",
|
|
15505
|
+
cause: "legacy-baseline",
|
|
15506
|
+
comparedAgainst
|
|
15507
|
+
};
|
|
15508
|
+
const registryRev = input.registryRevisions.get(entry.ruleId);
|
|
15509
|
+
if (registryRev === void 0) return {
|
|
15510
|
+
status: "DISAPPEARED-NON-FIX",
|
|
15511
|
+
cause: "retired",
|
|
15512
|
+
comparedAgainst
|
|
15513
|
+
};
|
|
15514
|
+
if (registryRev !== entryRev) return {
|
|
15515
|
+
status: "INCONCLUSIVE",
|
|
15516
|
+
cause: "revision-changed",
|
|
15517
|
+
comparedAgainst
|
|
15518
|
+
};
|
|
15519
|
+
if (current.findings.some((f) => fingerprint$1(f) === fp)) return {
|
|
15520
|
+
status: "STILL-PRESENT",
|
|
15521
|
+
comparedAgainst
|
|
15522
|
+
};
|
|
15523
|
+
return {
|
|
15524
|
+
status: "VERIFIED-RESOLVED",
|
|
15525
|
+
comparedAgainst
|
|
15526
|
+
};
|
|
15527
|
+
}
|
|
15528
|
+
/** The §15 rendering law: "FIXED" only for VERIFIED-RESOLVED. */
|
|
15529
|
+
function renderResolution(r) {
|
|
15530
|
+
switch (r.status) {
|
|
15531
|
+
case "VERIFIED-RESOLVED": return "FIXED SINCE BASELINE (verified by a complete same-revision scan)";
|
|
15532
|
+
case "STILL-PRESENT": return "STILL PRESENT";
|
|
15533
|
+
case "SUPPRESSED": return "SUPPRESSED (active ignore entry)";
|
|
15534
|
+
case "INCONCLUSIVE": return `INCONCLUSIVE (${r.cause})`;
|
|
15535
|
+
case "DISAPPEARED-NON-FIX": return `DISAPPEARED — NOT A FIX (${r.cause})`;
|
|
15536
|
+
}
|
|
15537
|
+
}
|
|
15538
|
+
//#endregion
|
|
15355
15539
|
//#region src/commands/baseline.ts
|
|
15356
15540
|
/**
|
|
15357
15541
|
* `mjolnir baseline` / `mjolnir diff` — Sprint 6 Task 24
|
|
@@ -15376,6 +15560,14 @@ function renderImpact(report) {
|
|
|
15376
15560
|
const ui$7 = plainContext();
|
|
15377
15561
|
const DEFAULT_BASELINE_PATH = join(".mjolnir", "baseline.json");
|
|
15378
15562
|
/**
|
|
15563
|
+
* Registry-declared detector revisions (§17): a baseline entry whose
|
|
15564
|
+
* revision differs from today's registry is INCONCLUSIVE(revision-
|
|
15565
|
+
* changed), never resolved. Omitted declarations mean revision 1 (the
|
|
15566
|
+
* documented RuleMeta default for first-generation detectors); a
|
|
15567
|
+
* ruleId ABSENT from this map means the rule is retired.
|
|
15568
|
+
*/
|
|
15569
|
+
const REGISTRY_REVISIONS = new Map(RULES.map((r) => [r.id, r.detectorRevision ?? 1]));
|
|
15570
|
+
/**
|
|
15379
15571
|
* Correlation identity for before/after comparison (agent-handoff plan
|
|
15380
15572
|
* §5.2): ruleId + file + message, deliberately EXCLUDING `line` — a
|
|
15381
15573
|
* source edit that shifts a finding still correlates. file:line is an
|
|
@@ -15397,7 +15589,8 @@ function buildBaseline(result, commit) {
|
|
|
15397
15589
|
ruleId: f.ruleId,
|
|
15398
15590
|
file: f.file,
|
|
15399
15591
|
message: f.message,
|
|
15400
|
-
severity: f.severity
|
|
15592
|
+
severity: f.severity,
|
|
15593
|
+
...f.detectorRevision !== void 0 ? { detectorRevision: f.detectorRevision } : {}
|
|
15401
15594
|
}))
|
|
15402
15595
|
};
|
|
15403
15596
|
}
|
|
@@ -15467,7 +15660,18 @@ function diffAgainstBaseline(result, baseline) {
|
|
|
15467
15660
|
else newFindings.push(f);
|
|
15468
15661
|
}
|
|
15469
15662
|
const resolvedFindings = [];
|
|
15470
|
-
for (const [key, f] of baseSet) if (!headKeys.has(key))
|
|
15663
|
+
for (const [key, f] of baseSet) if (!headKeys.has(key)) {
|
|
15664
|
+
const resolution = resolve$1({
|
|
15665
|
+
entry: f,
|
|
15666
|
+
baseline,
|
|
15667
|
+
current: result,
|
|
15668
|
+
registryRevisions: REGISTRY_REVISIONS
|
|
15669
|
+
});
|
|
15670
|
+
resolvedFindings.push({
|
|
15671
|
+
...f,
|
|
15672
|
+
resolution
|
|
15673
|
+
});
|
|
15674
|
+
}
|
|
15471
15675
|
return {
|
|
15472
15676
|
hasBaseline: true,
|
|
15473
15677
|
baselineCapturedAt: baseline.capturedAt,
|
|
@@ -15507,8 +15711,17 @@ function renderBaselineDiff(diff) {
|
|
|
15507
15711
|
}
|
|
15508
15712
|
lines.push("");
|
|
15509
15713
|
if (diff.resolvedFindings.length > 0) {
|
|
15510
|
-
|
|
15511
|
-
|
|
15714
|
+
const verified = diff.resolvedFindings.filter((f) => f.resolution.status === "VERIFIED-RESOLVED");
|
|
15715
|
+
const unresolved = diff.resolvedFindings.filter((f) => f.resolution.status !== "VERIFIED-RESOLVED");
|
|
15716
|
+
if (verified.length > 0) {
|
|
15717
|
+
lines.push(`FIXED SINCE BASELINE (${verified.length}):`);
|
|
15718
|
+
for (const f of verified) lines.push(` ✓ ${f.ruleId} (${f.severity}) · ${f.file} — ${f.message}`);
|
|
15719
|
+
lines.push("");
|
|
15720
|
+
}
|
|
15721
|
+
if (unresolved.length > 0) {
|
|
15722
|
+
lines.push(`DISAPPEARED — NOT CLASSIFIED AS FIXED (${unresolved.length}):`);
|
|
15723
|
+
for (const f of unresolved) lines.push(` ${renderResolution(f.resolution)} · ${f.ruleId} (${f.severity}) · ${f.file} — ${f.message}`);
|
|
15724
|
+
}
|
|
15512
15725
|
}
|
|
15513
15726
|
return lines.join("\n");
|
|
15514
15727
|
}
|
|
@@ -16787,7 +17000,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
|
|
|
16787
17000
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
16788
17001
|
* `tests/version-consistency.spec.ts` locally.
|
|
16789
17002
|
*/
|
|
16790
|
-
const CLI_VERSION = "0.5.
|
|
17003
|
+
const CLI_VERSION = "0.5.7";
|
|
16791
17004
|
function parseArgs(argv, onError) {
|
|
16792
17005
|
const args = {
|
|
16793
17006
|
target: ".",
|
|
@@ -17214,7 +17427,10 @@ async function runScanCommand(argv, io = {
|
|
|
17214
17427
|
}
|
|
17215
17428
|
if (args.format === "sarif") io.out(renderSarif(result));
|
|
17216
17429
|
else if (args.format === "mermaid") io.out(renderMermaid(result));
|
|
17217
|
-
else if (args.json) io.out(JSON.stringify(
|
|
17430
|
+
else if (args.json) io.out(JSON.stringify({
|
|
17431
|
+
...result,
|
|
17432
|
+
contract: buildMachineContract(result)
|
|
17433
|
+
}, null, 2));
|
|
17218
17434
|
else {
|
|
17219
17435
|
const categories = args.categories;
|
|
17220
17436
|
const visible = categories && categories.length > 0 ? result.findings.filter((f) => categories.includes(f.category)) : result.findings;
|
|
@@ -17490,7 +17706,7 @@ async function runDiffCommand(argv, io = {
|
|
|
17490
17706
|
const statsPath = join(target, DEFAULT_STATS_PATH);
|
|
17491
17707
|
const stats = recordResolved(loadStats(statsPath), diff);
|
|
17492
17708
|
if (!saveStats(stats, statsPath)) io.err(" (warning: stats could not be written — read-only filesystem? counters not recorded)");
|
|
17493
|
-
if (diff.resolvedFindings.
|
|
17709
|
+
if (diff.resolvedFindings.some((f) => f.resolution.status === "VERIFIED-RESOLVED")) {
|
|
17494
17710
|
const milestone = recordMilestones(stats, ["first-debt-reduction"]);
|
|
17495
17711
|
if (milestone.newlyAnnounced.length > 0) {
|
|
17496
17712
|
if (saveStats(milestone.stats, statsPath)) for (const id of milestone.newlyAnnounced) io.out(MILESTONE_MESSAGES[id]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mjolnir-qa",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "Mjölnir — the Verification Trust Engine for QA. Audits test suites and CI pipelines, reports a worthiness score and prioritized findings.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"fp-audit:generate": "tsx scripts/generate-fp-audit-table.ts",
|
|
36
36
|
"docs:rules": "tsx scripts/generate-rule-docs.ts",
|
|
37
37
|
"docs:capability": "tsx scripts/generate-capability-matrix.ts",
|
|
38
|
+
"docs:machine-contract": "tsx scripts/generate-machine-contract-doc.ts",
|
|
38
39
|
"docs:hero": "tsx scripts/generate-readme-hero.ts",
|
|
39
40
|
"docs:formats": "tsx scripts/generate-site-formats.ts",
|
|
40
41
|
"docs:forensics-samples": "tsx scripts/generate-forensics-samples.ts",
|