mjolnir-qa 1.0.1 → 1.0.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 +42 -0
- package/README.ar.md +4 -4
- package/README.bn.md +4 -4
- package/README.br.md +4 -4
- package/README.bs.md +4 -4
- package/README.da.md +4 -4
- package/README.de.md +4 -4
- package/README.es.md +4 -4
- package/README.fr.md +4 -4
- package/README.gr.md +4 -4
- package/README.he.md +4 -4
- package/README.it.md +4 -4
- package/README.ja.md +4 -4
- package/README.ko.md +4 -4
- package/README.md +5 -5
- package/README.no.md +4 -4
- package/README.pl.md +4 -4
- package/README.ru.md +4 -4
- package/README.th.md +4 -4
- package/README.tr.md +4 -4
- package/README.uk.md +4 -4
- package/README.vi.md +4 -4
- package/README.zh.md +4 -4
- package/README.zht.md +4 -4
- package/dist/cli.d.mts +55 -1
- package/dist/cli.mjs +638 -83
- package/dist/mcp/stdio.mjs +638 -83
- package/package.json +9 -2
package/dist/cli.mjs
CHANGED
|
@@ -119,12 +119,13 @@ const DEDUCTIONS = {
|
|
|
119
119
|
*/
|
|
120
120
|
const EVIDENCE_CORE_VERSION = "evidence-core@1";
|
|
121
121
|
/** Deterministic canonical order: file → line (absent last) → title → source. */
|
|
122
|
-
function
|
|
122
|
+
function compareEvidenceRecords(a, b) {
|
|
123
123
|
if (a.file !== b.file) return a.file < b.file ? -1 : 1;
|
|
124
124
|
const la = a.line ?? Number.POSITIVE_INFINITY;
|
|
125
125
|
const lb = b.line ?? Number.POSITIVE_INFINITY;
|
|
126
126
|
if (la !== lb) return la - lb;
|
|
127
127
|
if (a.title !== b.title) return a.title < b.title ? -1 : 1;
|
|
128
|
+
if (a.source !== b.source) return a.source < b.source ? -1 : 1;
|
|
128
129
|
return 0;
|
|
129
130
|
}
|
|
130
131
|
function normalizeOne(report, artifact, v) {
|
|
@@ -159,7 +160,7 @@ function normalizeOne(report, artifact, v) {
|
|
|
159
160
|
* record list — report verdict order never leaks into the output.
|
|
160
161
|
*/
|
|
161
162
|
function buildEvidenceRecords(report, artifact) {
|
|
162
|
-
return report.verdicts.map((v) => normalizeOne(report, artifact, v)).sort(
|
|
163
|
+
return report.verdicts.map((v) => normalizeOne(report, artifact, v)).sort(compareEvidenceRecords);
|
|
163
164
|
}
|
|
164
165
|
/**
|
|
165
166
|
* The test whose declaration span contains `line` — the exact matching
|
|
@@ -179,8 +180,8 @@ function findTestAt(records, file, line) {
|
|
|
179
180
|
for (const r of inFile) if (r.line === void 0) return void 0;
|
|
180
181
|
let match;
|
|
181
182
|
for (const r of inFile) {
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
183
|
+
const l = r.line;
|
|
184
|
+
if (l <= line && (match === void 0 || match.line <= l)) match = r;
|
|
184
185
|
}
|
|
185
186
|
return match;
|
|
186
187
|
}
|
|
@@ -3321,6 +3322,10 @@ const focusedTestCommitted = defineRule({
|
|
|
3321
3322
|
falsePositiveRisk: "low",
|
|
3322
3323
|
autofix: true,
|
|
3323
3324
|
detectionStrategy: "LEXICAL",
|
|
3325
|
+
strategyJustification: {
|
|
3326
|
+
reasonCode: "runner-semantic",
|
|
3327
|
+
detail: ".only/focus is runner skip-scheduling state; the detector matches the runner's own member-call tokens (.only/.fit/fdescribe) on the code-only text — lexical precision equals the AST call-shape here, and the §13.2 fallback keeps parity"
|
|
3328
|
+
},
|
|
3324
3329
|
introduced: "0.1.0",
|
|
3325
3330
|
tier: "quarantine",
|
|
3326
3331
|
suiteInvalidating: true,
|
|
@@ -3381,6 +3386,10 @@ const skippedTest = defineRule({
|
|
|
3381
3386
|
falsePositiveRisk: "medium",
|
|
3382
3387
|
autofix: false,
|
|
3383
3388
|
detectionStrategy: "LEXICAL",
|
|
3389
|
+
strategyJustification: {
|
|
3390
|
+
reasonCode: "runner-semantic",
|
|
3391
|
+
detail: "skip/xfail/ignore are runner skip-state annotations whose forms are runner API tokens (it.skip, xit, t.skip, @unittest.skip); the detector matches those exact tokens on the code-only text"
|
|
3392
|
+
},
|
|
3384
3393
|
introduced: "0.1.0",
|
|
3385
3394
|
tier: "quarantine",
|
|
3386
3395
|
run(ctx) {
|
|
@@ -3498,6 +3507,10 @@ const noAssertions = defineRule({
|
|
|
3498
3507
|
falsePositiveRisk: "medium",
|
|
3499
3508
|
autofix: false,
|
|
3500
3509
|
detectionStrategy: "LEXICAL",
|
|
3510
|
+
strategyJustification: {
|
|
3511
|
+
reasonCode: "runner-semantic",
|
|
3512
|
+
detail: "assertion-less test bodies are runner-outcome semantics (the runner reports a pass that proves nothing); the detector matches the test-def plus body shapes on the code-only text"
|
|
3513
|
+
},
|
|
3501
3514
|
introduced: "0.1.0",
|
|
3502
3515
|
tier: "quarantine",
|
|
3503
3516
|
run(ctx) {
|
|
@@ -3572,6 +3585,10 @@ const hardSleep = defineRule({
|
|
|
3572
3585
|
falsePositiveRisk: "low",
|
|
3573
3586
|
autofix: false,
|
|
3574
3587
|
detectionStrategy: "LEXICAL",
|
|
3588
|
+
strategyJustification: {
|
|
3589
|
+
reasonCode: "runner-semantic",
|
|
3590
|
+
detail: "hard-sleep is behavioral wait-shape matching (the wait call plus its interaction context), not a single node; the detector's pattern+wait-shape oracle is the recorded design (§12.1), and the hard-sleep family's structural path carries the depth where available"
|
|
3591
|
+
},
|
|
3575
3592
|
detectionNotes: "regex pattern + behavioral wait-shape matching",
|
|
3576
3593
|
introduced: "0.1.0",
|
|
3577
3594
|
tier: "extended",
|
|
@@ -3637,6 +3654,10 @@ const retryAbuse = defineRule({
|
|
|
3637
3654
|
falsePositiveRisk: "low",
|
|
3638
3655
|
autofix: false,
|
|
3639
3656
|
detectionStrategy: "LEXICAL",
|
|
3657
|
+
strategyJustification: {
|
|
3658
|
+
reasonCode: "runner-semantic",
|
|
3659
|
+
detail: "retry abuse is the runner's retry contract (jest.retries, vitest retry, playwright retries); the detector matches the runner's retry API tokens across runners — each an exact key"
|
|
3660
|
+
},
|
|
3640
3661
|
introduced: "0.1.0",
|
|
3641
3662
|
tier: "quarantine",
|
|
3642
3663
|
run(ctx) {
|
|
@@ -3698,6 +3719,10 @@ const emptyTestBody = defineRule({
|
|
|
3698
3719
|
falsePositiveRisk: "low",
|
|
3699
3720
|
autofix: false,
|
|
3700
3721
|
detectionStrategy: "LEXICAL",
|
|
3722
|
+
strategyJustification: {
|
|
3723
|
+
reasonCode: "runner-semantic",
|
|
3724
|
+
detail: "empty test bodies are runner-outcome semantics; the detector matches the test-def plus empty-body shapes on the code-only text — the AST body-shape is the same predicate"
|
|
3725
|
+
},
|
|
3701
3726
|
introduced: "0.1.0",
|
|
3702
3727
|
tier: "quarantine",
|
|
3703
3728
|
overlapWith: ["QA-TEST-003"],
|
|
@@ -3749,6 +3774,10 @@ const tautologicalAssertion = defineRule({
|
|
|
3749
3774
|
falsePositiveRisk: "low",
|
|
3750
3775
|
autofix: false,
|
|
3751
3776
|
detectionStrategy: "LEXICAL",
|
|
3777
|
+
strategyJustification: {
|
|
3778
|
+
reasonCode: "runner-semantic",
|
|
3779
|
+
detail: "tautological assertions (x === x, expect(true)) are assertion-semantics on the code-only text; the detector matches the tautology shapes after comment stripping — the AST call-shape is the same predicate"
|
|
3780
|
+
},
|
|
3752
3781
|
detectionNotes: "AST-stripped text pattern",
|
|
3753
3782
|
introduced: "0.1.0",
|
|
3754
3783
|
tier: "quarantine",
|
|
@@ -3828,6 +3857,10 @@ const unawaitedPromiseAssertion = defineRule({
|
|
|
3828
3857
|
falsePositiveRisk: "low",
|
|
3829
3858
|
autofix: false,
|
|
3830
3859
|
detectionStrategy: "LEXICAL",
|
|
3860
|
+
strategyJustification: {
|
|
3861
|
+
reasonCode: "runner-semantic",
|
|
3862
|
+
detail: "un-awaited promise assertions are runner async semantics; the detector matches the assertion-call shapes inside promise chains on the code-only text — the async contract is runner behavior"
|
|
3863
|
+
},
|
|
3831
3864
|
introduced: "0.2.0",
|
|
3832
3865
|
tier: "quarantine",
|
|
3833
3866
|
run(ctx) {
|
|
@@ -3927,6 +3960,10 @@ const commentedOutTest = defineRule({
|
|
|
3927
3960
|
falsePositiveRisk: "medium",
|
|
3928
3961
|
autofix: false,
|
|
3929
3962
|
detectionStrategy: "LEXICAL",
|
|
3963
|
+
strategyJustification: {
|
|
3964
|
+
reasonCode: "lexical-artifact",
|
|
3965
|
+
detail: "commented-out assertions are lexical artifacts — the comment-wrapped assertion text is the finding itself; the detector matches the shapes on the raw text"
|
|
3966
|
+
},
|
|
3930
3967
|
detectionNotes: "regex heuristic",
|
|
3931
3968
|
introduced: "0.2.0",
|
|
3932
3969
|
tier: "extended",
|
|
@@ -4172,6 +4209,10 @@ const committedDebugArtifacts = defineRule({
|
|
|
4172
4209
|
falsePositiveRisk: "low",
|
|
4173
4210
|
autofix: true,
|
|
4174
4211
|
detectionStrategy: "LEXICAL",
|
|
4212
|
+
strategyJustification: {
|
|
4213
|
+
reasonCode: "exact-key-match",
|
|
4214
|
+
detail: "page.pause() and test.only() are exact Playwright runner tokens; the detector matches the member-call identifiers on the code-only text — closed token set, unique to the defect"
|
|
4215
|
+
},
|
|
4175
4216
|
introduced: "0.1.0",
|
|
4176
4217
|
tier: "core",
|
|
4177
4218
|
run(ctx) {
|
|
@@ -4224,6 +4265,10 @@ const brittleSelectors = defineRule({
|
|
|
4224
4265
|
falsePositiveRisk: "medium",
|
|
4225
4266
|
autofix: false,
|
|
4226
4267
|
detectionStrategy: "LEXICAL",
|
|
4268
|
+
strategyJustification: {
|
|
4269
|
+
reasonCode: "string-content-defect",
|
|
4270
|
+
detail: "brittle selectors ARE string arguments (css=/xpath=/nth-child shapes) — the code-text masking that protects other rules deliberately excludes string content here; the detector reads the string shapes directly (inside-string oracle)"
|
|
4271
|
+
},
|
|
4227
4272
|
detectionNotes: "regex pattern + inside-string oracle",
|
|
4228
4273
|
introduced: "0.1.0",
|
|
4229
4274
|
tier: "quarantine",
|
|
@@ -4521,6 +4566,10 @@ const swallowedExitCode = defineRule({
|
|
|
4521
4566
|
falsePositiveRisk: "low",
|
|
4522
4567
|
autofix: false,
|
|
4523
4568
|
detectionStrategy: "LEXICAL",
|
|
4569
|
+
strategyJustification: {
|
|
4570
|
+
reasonCode: "shell-string-in-config",
|
|
4571
|
+
detail: "exit-code swallowing lives inside workflow run: strings (shell scripts embedded in YAML); the YAML statement IS a string literal — a shell syntax tree of a YAML value adds parsing without adding classification power"
|
|
4572
|
+
},
|
|
4524
4573
|
introduced: "0.1.0",
|
|
4525
4574
|
tier: "extended",
|
|
4526
4575
|
detectorRevision: 2,
|
|
@@ -4573,6 +4622,10 @@ const retryMasking = defineRule({
|
|
|
4573
4622
|
falsePositiveRisk: "low",
|
|
4574
4623
|
autofix: false,
|
|
4575
4624
|
detectionStrategy: "LEXICAL",
|
|
4625
|
+
strategyJustification: {
|
|
4626
|
+
reasonCode: "runner-semantic",
|
|
4627
|
+
detail: "retry masking is defined by the runner's retry semantics, which no language syntax tree represents; the detector matches the runner's own retry keys in workflow YAML where statements are shell strings"
|
|
4628
|
+
},
|
|
4576
4629
|
introduced: "0.1.0",
|
|
4577
4630
|
tier: "extended",
|
|
4578
4631
|
detectorRevision: 2,
|
|
@@ -4756,6 +4809,10 @@ const reportNeverGenerated = defineRule({
|
|
|
4756
4809
|
falsePositiveRisk: "low",
|
|
4757
4810
|
autofix: false,
|
|
4758
4811
|
detectionStrategy: "LEXICAL",
|
|
4812
|
+
strategyJustification: {
|
|
4813
|
+
reasonCode: "runner-semantic",
|
|
4814
|
+
detail: "report generation is a runner side effect of the workflow step sequence, not a syntax tree property; the detector reads the workflow step graph, whose statements are already literal text"
|
|
4815
|
+
},
|
|
4759
4816
|
introduced: "0.1.0",
|
|
4760
4817
|
tier: "quarantine",
|
|
4761
4818
|
detectorRevision: 2,
|
|
@@ -4856,6 +4913,10 @@ const alwaysSuccessStep = defineRule({
|
|
|
4856
4913
|
falsePositiveRisk: "low",
|
|
4857
4914
|
autofix: false,
|
|
4858
4915
|
detectionStrategy: "LEXICAL",
|
|
4916
|
+
strategyJustification: {
|
|
4917
|
+
reasonCode: "runner-semantic",
|
|
4918
|
+
detail: "always()-success is a workflow-step outcome contract, not a code construct; the detector matches the step's run/if keys, which are string fields of the YAML config surface"
|
|
4919
|
+
},
|
|
4859
4920
|
introduced: "0.1.0",
|
|
4860
4921
|
tier: "quarantine",
|
|
4861
4922
|
detectorRevision: 2,
|
|
@@ -5102,6 +5163,10 @@ const pyNoAssertions = defineRule({
|
|
|
5102
5163
|
falsePositiveRisk: "low",
|
|
5103
5164
|
autofix: false,
|
|
5104
5165
|
detectionStrategy: "LEXICAL",
|
|
5166
|
+
strategyJustification: {
|
|
5167
|
+
reasonCode: "runner-semantic",
|
|
5168
|
+
detail: "assertion-less pytest bodies are runner-outcome semantics (the runner reports a pass that proves nothing); the detector matches the test-def plus body shapes on the code-only text — pytest's pass contract is runner behavior"
|
|
5169
|
+
},
|
|
5105
5170
|
introduced: "0.3.0",
|
|
5106
5171
|
tier: "quarantine",
|
|
5107
5172
|
detectorRevision: 3,
|
|
@@ -5185,6 +5250,10 @@ const pyHardSleep = defineRule({
|
|
|
5185
5250
|
falsePositiveRisk: "low",
|
|
5186
5251
|
autofix: false,
|
|
5187
5252
|
detectionStrategy: "LEXICAL",
|
|
5253
|
+
strategyJustification: {
|
|
5254
|
+
reasonCode: "exact-key-match",
|
|
5255
|
+
detail: "time.sleep(n) is an exact stdlib token with a numeric argument; the detector matches the call identifier on the code-only text — closed token, unique to the defect"
|
|
5256
|
+
},
|
|
5188
5257
|
introduced: "0.3.0",
|
|
5189
5258
|
tier: "extended",
|
|
5190
5259
|
overlapWith: ["QA-PY-102"],
|
|
@@ -5230,6 +5299,10 @@ const pySkippedTest = defineRule({
|
|
|
5230
5299
|
falsePositiveRisk: "low",
|
|
5231
5300
|
autofix: false,
|
|
5232
5301
|
detectionStrategy: "LEXICAL",
|
|
5302
|
+
strategyJustification: {
|
|
5303
|
+
reasonCode: "runner-semantic",
|
|
5304
|
+
detail: "pytest.mark.skip/xfail are runner marker decorators — exact runner tokens; a syntax tree re-derives the same call shape with no added classification power"
|
|
5305
|
+
},
|
|
5233
5306
|
introduced: "0.3.0",
|
|
5234
5307
|
run(ctx) {
|
|
5235
5308
|
const text = ctx.codeText ?? ctx.text;
|
|
@@ -5286,6 +5359,10 @@ const pyTautological = defineRule({
|
|
|
5286
5359
|
falsePositiveRisk: "low",
|
|
5287
5360
|
autofix: false,
|
|
5288
5361
|
detectionStrategy: "LEXICAL",
|
|
5362
|
+
strategyJustification: {
|
|
5363
|
+
reasonCode: "runner-semantic",
|
|
5364
|
+
detail: "tautological assertions in Python (assert x == x) are assertion-semantics on the code-only text; the detector matches the tautology shapes — the AST re-derives the same comparison"
|
|
5365
|
+
},
|
|
5289
5366
|
introduced: "0.3.0",
|
|
5290
5367
|
tier: "quarantine",
|
|
5291
5368
|
run(ctx) {
|
|
@@ -5340,6 +5417,10 @@ const pyFocusedTest = defineRule({
|
|
|
5340
5417
|
falsePositiveRisk: "low",
|
|
5341
5418
|
autofix: false,
|
|
5342
5419
|
detectionStrategy: "LEXICAL",
|
|
5420
|
+
strategyJustification: {
|
|
5421
|
+
reasonCode: "runner-semantic",
|
|
5422
|
+
detail: "pytest.skip/xfail/parametrize marks are runner decorators and module-level calls; the detector matches those exact tokens on the code-only text — the semantics are runner skip state"
|
|
5423
|
+
},
|
|
5343
5424
|
introduced: "0.3.0",
|
|
5344
5425
|
tier: "core",
|
|
5345
5426
|
suiteInvalidating: true,
|
|
@@ -5392,6 +5473,10 @@ const pyRaisesWithoutMatch = defineRule({
|
|
|
5392
5473
|
falsePositiveRisk: "medium",
|
|
5393
5474
|
autofix: false,
|
|
5394
5475
|
detectionStrategy: "LEXICAL",
|
|
5476
|
+
strategyJustification: {
|
|
5477
|
+
reasonCode: "runner-semantic",
|
|
5478
|
+
detail: "pytest.raises without match is a runner exception-contract semantic; the detector matches the raises-call plus its argumentless form — the runner's exception contract, not a syntax property"
|
|
5479
|
+
},
|
|
5395
5480
|
introduced: "0.3.0",
|
|
5396
5481
|
tier: "quarantine",
|
|
5397
5482
|
detectorRevision: 3,
|
|
@@ -5471,6 +5556,10 @@ const pyCommentedOutTest = defineRule({
|
|
|
5471
5556
|
falsePositiveRisk: "medium",
|
|
5472
5557
|
autofix: false,
|
|
5473
5558
|
detectionStrategy: "LEXICAL",
|
|
5559
|
+
strategyJustification: {
|
|
5560
|
+
reasonCode: "lexical-artifact",
|
|
5561
|
+
detail: "commented-out test code is a lexical artifact by definition — the text IS the finding (comment-wrapped test bodies); the detector matches the commented shapes on the raw text, which is where the artifact lives"
|
|
5562
|
+
},
|
|
5474
5563
|
detectionNotes: "regex heuristic",
|
|
5475
5564
|
introduced: "0.3.0",
|
|
5476
5565
|
tier: "core",
|
|
@@ -5516,6 +5605,10 @@ const pyBareTruthinessAssert = defineRule({
|
|
|
5516
5605
|
falsePositiveRisk: "medium",
|
|
5517
5606
|
autofix: false,
|
|
5518
5607
|
detectionStrategy: "LEXICAL",
|
|
5608
|
+
strategyJustification: {
|
|
5609
|
+
reasonCode: "runner-semantic",
|
|
5610
|
+
detail: "bare truthiness asserts (assert obj) are assertion-semantics on the code-only text; the detector matches the bare-assert shapes — the AST re-derives the same call"
|
|
5611
|
+
},
|
|
5519
5612
|
introduced: "0.3.0",
|
|
5520
5613
|
tier: "quarantine",
|
|
5521
5614
|
detectorRevision: 3,
|
|
@@ -5584,6 +5677,10 @@ const pyMutableFixture = defineRule({
|
|
|
5584
5677
|
falsePositiveRisk: "medium",
|
|
5585
5678
|
autofix: false,
|
|
5586
5679
|
detectionStrategy: "LEXICAL",
|
|
5680
|
+
strategyJustification: {
|
|
5681
|
+
reasonCode: "runner-semantic",
|
|
5682
|
+
detail: "pytest fixture mutation is fixture-lifecycle semantics (autouse/scope keys plus mutation calls); the detector matches the runner's fixture decorator tokens plus the mutation shapes"
|
|
5683
|
+
},
|
|
5587
5684
|
introduced: "0.3.0",
|
|
5588
5685
|
tier: "core",
|
|
5589
5686
|
run(ctx) {
|
|
@@ -5632,6 +5729,10 @@ const pwWaitForTimeout = defineRule({
|
|
|
5632
5729
|
falsePositiveRisk: "low",
|
|
5633
5730
|
autofix: false,
|
|
5634
5731
|
detectionStrategy: "LEXICAL",
|
|
5732
|
+
strategyJustification: {
|
|
5733
|
+
reasonCode: "family-fallback-lockstep",
|
|
5734
|
+
detail: "the hard-sleep family's §13.2 structural path (AST hook) carries the depth where a tree is available; this lexical path is the mandatory deterministic fallback kept in lockstep — the family's depth is real, the regex is its degraded mode"
|
|
5735
|
+
},
|
|
5635
5736
|
introduced: "0.3.0",
|
|
5636
5737
|
overlapWith: ["QA-TEST-004"],
|
|
5637
5738
|
run(ctx) {
|
|
@@ -5675,6 +5776,10 @@ const pwWaitForLoadEvent = defineRule({
|
|
|
5675
5776
|
falsePositiveRisk: "medium",
|
|
5676
5777
|
autofix: false,
|
|
5677
5778
|
detectionStrategy: "LEXICAL",
|
|
5779
|
+
strategyJustification: {
|
|
5780
|
+
reasonCode: "exact-key-match",
|
|
5781
|
+
detail: "waitForLoadState('load') is an exact Playwright token plus a closed argument enum; the detector matches the call plus its argument — the AST re-derives the same call shape"
|
|
5782
|
+
},
|
|
5678
5783
|
introduced: "0.3.0",
|
|
5679
5784
|
tier: "quarantine",
|
|
5680
5785
|
detectorRevision: 2,
|
|
@@ -5727,6 +5832,10 @@ const pwDeepFrameLocator = defineRule({
|
|
|
5727
5832
|
falsePositiveRisk: "low",
|
|
5728
5833
|
autofix: false,
|
|
5729
5834
|
detectionStrategy: "LEXICAL",
|
|
5835
|
+
strategyJustification: {
|
|
5836
|
+
reasonCode: "exact-key-match",
|
|
5837
|
+
detail: "frameLocator chaining depth is an exact Playwright token sequence; the detector matches the frameLocator call chains — the token sequence is closed and unique to the defect"
|
|
5838
|
+
},
|
|
5730
5839
|
introduced: "0.3.0",
|
|
5731
5840
|
tier: "core",
|
|
5732
5841
|
run(ctx) {
|
|
@@ -5774,6 +5883,10 @@ const pwSerialNoJustification = defineRule({
|
|
|
5774
5883
|
falsePositiveRisk: "low",
|
|
5775
5884
|
autofix: false,
|
|
5776
5885
|
detectionStrategy: "LEXICAL",
|
|
5886
|
+
strategyJustification: {
|
|
5887
|
+
reasonCode: "runner-semantic",
|
|
5888
|
+
detail: "fullyParallel/serial are runner scheduling keys on the config and describe blocks; the detector matches the runner's exact API tokens — the semantics are scheduling, not syntax"
|
|
5889
|
+
},
|
|
5777
5890
|
introduced: "0.3.0",
|
|
5778
5891
|
tier: "core",
|
|
5779
5892
|
run(ctx) {
|
|
@@ -5827,6 +5940,10 @@ const pwConfigRetryAbuse = defineRule({
|
|
|
5827
5940
|
falsePositiveRisk: "low",
|
|
5828
5941
|
autofix: false,
|
|
5829
5942
|
detectionStrategy: "LEXICAL",
|
|
5943
|
+
strategyJustification: {
|
|
5944
|
+
reasonCode: "runner-semantic",
|
|
5945
|
+
detail: "retries in playwright.config.* is a runner top-level option, not a syntax node; the detector reads the config surface whose statements are object-literal keys — exact-key precision"
|
|
5946
|
+
},
|
|
5830
5947
|
detectionNotes: "regex heuristic",
|
|
5831
5948
|
introduced: "0.3.0",
|
|
5832
5949
|
tier: "core",
|
|
@@ -5888,6 +6005,10 @@ const pwNoTraceOnRetry = defineRule({
|
|
|
5888
6005
|
falsePositiveRisk: "low",
|
|
5889
6006
|
autofix: false,
|
|
5890
6007
|
detectionStrategy: "LEXICAL",
|
|
6008
|
+
strategyJustification: {
|
|
6009
|
+
reasonCode: "runner-semantic",
|
|
6010
|
+
detail: "trace/reporter capture is runner lifecycle state set in the config file; the detector reads the config surface's keys and enum values, which are exact matches — a syntax tree adds no semantic the config text lacks"
|
|
6011
|
+
},
|
|
5891
6012
|
detectionNotes: "regex heuristic",
|
|
5892
6013
|
introduced: "0.3.0",
|
|
5893
6014
|
tier: "extended",
|
|
@@ -5941,6 +6062,10 @@ const pwNoProjectSplit = defineRule({
|
|
|
5941
6062
|
falsePositiveRisk: "low",
|
|
5942
6063
|
autofix: false,
|
|
5943
6064
|
detectionStrategy: "LEXICAL",
|
|
6065
|
+
strategyJustification: {
|
|
6066
|
+
reasonCode: "runner-semantic",
|
|
6067
|
+
detail: "project split is a runner config concept (projects array arrangement); the detector reads playwright.config.* keys (adapter-gated), whose object-literal shape is exact-match text"
|
|
6068
|
+
},
|
|
5944
6069
|
detectionNotes: "regex heuristic over playwright.config.* (adapter-gated)",
|
|
5945
6070
|
introduced: "0.3.0",
|
|
5946
6071
|
detectorRevision: 2,
|
|
@@ -5991,6 +6116,10 @@ const pwTrialMisuse = defineRule({
|
|
|
5991
6116
|
falsePositiveRisk: "medium",
|
|
5992
6117
|
autofix: false,
|
|
5993
6118
|
detectionStrategy: "LEXICAL",
|
|
6119
|
+
strategyJustification: {
|
|
6120
|
+
reasonCode: "exact-key-match",
|
|
6121
|
+
detail: "the trial-click shape is an exact Playwright API token pair; the detector matches the call identifier on the code-only text — the token is closed and unique to the defect"
|
|
6122
|
+
},
|
|
5994
6123
|
introduced: "0.3.0",
|
|
5995
6124
|
tier: "core",
|
|
5996
6125
|
run(ctx) {
|
|
@@ -6035,6 +6164,10 @@ const pwStorageStateNoExpiry = defineRule({
|
|
|
6035
6164
|
falsePositiveRisk: "medium",
|
|
6036
6165
|
autofix: false,
|
|
6037
6166
|
detectionStrategy: "LEXICAL",
|
|
6167
|
+
strategyJustification: {
|
|
6168
|
+
reasonCode: "exact-key-match",
|
|
6169
|
+
detail: "storageState is an exact Playwright config/use option token; the detector matches the option key and its value shapes — closed config surface"
|
|
6170
|
+
},
|
|
6038
6171
|
introduced: "0.3.0",
|
|
6039
6172
|
run(ctx) {
|
|
6040
6173
|
const text = ctx.text;
|
|
@@ -6079,6 +6212,10 @@ const pwGlobalSetupSharedState = defineRule({
|
|
|
6079
6212
|
falsePositiveRisk: "medium",
|
|
6080
6213
|
autofix: false,
|
|
6081
6214
|
detectionStrategy: "LEXICAL",
|
|
6215
|
+
strategyJustification: {
|
|
6216
|
+
reasonCode: "runner-semantic",
|
|
6217
|
+
detail: "globalSetup/globalTeardown are runner lifecycle hooks declared in the config file; the detector matches those exact keys — the defect is the runner's execution order, not a code shape"
|
|
6218
|
+
},
|
|
6082
6219
|
detectionNotes: "regex heuristic",
|
|
6083
6220
|
introduced: "0.3.0",
|
|
6084
6221
|
run(ctx) {
|
|
@@ -6131,6 +6268,10 @@ const pwSharedPage = defineRule({
|
|
|
6131
6268
|
falsePositiveRisk: "medium",
|
|
6132
6269
|
autofix: false,
|
|
6133
6270
|
detectionStrategy: "LEXICAL",
|
|
6271
|
+
strategyJustification: {
|
|
6272
|
+
reasonCode: "runner-semantic",
|
|
6273
|
+
detail: "page reuse across tests is runner fixture-lifecycle semantics; the detector matches the page-consumption shapes against the test boundaries — the lifecycle is runner behavior"
|
|
6274
|
+
},
|
|
6134
6275
|
introduced: "0.3.0",
|
|
6135
6276
|
tier: "quarantine",
|
|
6136
6277
|
run(ctx) {
|
|
@@ -6178,6 +6319,10 @@ const qaPw140 = defineRule({
|
|
|
6178
6319
|
falsePositiveRisk: "medium",
|
|
6179
6320
|
autofix: false,
|
|
6180
6321
|
detectionStrategy: "LEXICAL",
|
|
6322
|
+
strategyJustification: {
|
|
6323
|
+
reasonCode: "exact-key-match",
|
|
6324
|
+
detail: "the detector matches a closed, exact runner API token on the code-only text; the token identifies the defect uniquely"
|
|
6325
|
+
},
|
|
6181
6326
|
introduced: "0.3.0",
|
|
6182
6327
|
tier: "core",
|
|
6183
6328
|
run(ctx) {
|
|
@@ -6245,6 +6390,10 @@ const hardcodedBaseUrl = defineRule({
|
|
|
6245
6390
|
falsePositiveRisk: "low",
|
|
6246
6391
|
autofix: false,
|
|
6247
6392
|
detectionStrategy: "LEXICAL",
|
|
6393
|
+
strategyJustification: {
|
|
6394
|
+
reasonCode: "string-content-defect",
|
|
6395
|
+
detail: "hardcoded environment URLs are string literals (http(s):// shapes); the detector reads the string-content shapes the code-text mask preserves for exactly this defect class"
|
|
6396
|
+
},
|
|
6248
6397
|
introduced: "0.3.0",
|
|
6249
6398
|
tier: "quarantine",
|
|
6250
6399
|
run(ctx) {
|
|
@@ -6295,6 +6444,10 @@ const envCoupling = defineRule({
|
|
|
6295
6444
|
falsePositiveRisk: "medium",
|
|
6296
6445
|
autofix: false,
|
|
6297
6446
|
detectionStrategy: "LEXICAL",
|
|
6447
|
+
strategyJustification: {
|
|
6448
|
+
reasonCode: "absence-aggregate",
|
|
6449
|
+
detail: "environment-guard absence over the suite is an aggregate property; the detector aggregates the suite's guard shapes — the defect is what the suite lacks, not a node it has"
|
|
6450
|
+
},
|
|
6298
6451
|
detectionNotes: "regex heuristic",
|
|
6299
6452
|
introduced: "0.2.0",
|
|
6300
6453
|
tier: "quarantine",
|
|
@@ -6369,6 +6522,10 @@ const pwRetryMaskingNoForensics = defineRule({
|
|
|
6369
6522
|
falsePositiveRisk: "low",
|
|
6370
6523
|
autofix: false,
|
|
6371
6524
|
detectionStrategy: "LEXICAL",
|
|
6525
|
+
strategyJustification: {
|
|
6526
|
+
reasonCode: "runner-semantic",
|
|
6527
|
+
detail: "retry triage is the runner's retry loop interacting with the reporter config; the detector reads both config keys and the triage call shape — the semantics live in runner behavior"
|
|
6528
|
+
},
|
|
6372
6529
|
detectionNotes: "regex heuristic",
|
|
6373
6530
|
introduced: "0.3.8",
|
|
6374
6531
|
tier: "extended",
|
|
@@ -6421,6 +6578,10 @@ const pwBlanketRouteMock = defineRule({
|
|
|
6421
6578
|
falsePositiveRisk: "medium",
|
|
6422
6579
|
autofix: false,
|
|
6423
6580
|
detectionStrategy: "LEXICAL",
|
|
6581
|
+
strategyJustification: {
|
|
6582
|
+
reasonCode: "absence-aggregate",
|
|
6583
|
+
detail: "blanket route interception is an aggregate of route calls across the suite; the detector aggregates the route-call shapes — the finding is the pattern's breadth, not one call"
|
|
6584
|
+
},
|
|
6424
6585
|
detectionNotes: "regex heuristic",
|
|
6425
6586
|
introduced: "0.3.8",
|
|
6426
6587
|
tier: "extended",
|
|
@@ -6474,6 +6635,10 @@ const pwNoFailureArtifacts = defineRule({
|
|
|
6474
6635
|
falsePositiveRisk: "low",
|
|
6475
6636
|
autofix: false,
|
|
6476
6637
|
detectionStrategy: "LEXICAL",
|
|
6638
|
+
strategyJustification: {
|
|
6639
|
+
reasonCode: "absence-aggregate",
|
|
6640
|
+
detail: "artifact-capture absence (no trace/video/screenshot anywhere in the suite) is a directory-level aggregate; the detector aggregates over the suite's shapes — absence, not presence"
|
|
6641
|
+
},
|
|
6477
6642
|
detectionNotes: "regex heuristic",
|
|
6478
6643
|
introduced: "0.3.8",
|
|
6479
6644
|
tier: "extended",
|
|
@@ -6524,6 +6689,10 @@ const pwSingleBrowserMatrix = defineRule({
|
|
|
6524
6689
|
falsePositiveRisk: "low",
|
|
6525
6690
|
autofix: false,
|
|
6526
6691
|
detectionStrategy: "LEXICAL",
|
|
6692
|
+
strategyJustification: {
|
|
6693
|
+
reasonCode: "absence-aggregate",
|
|
6694
|
+
detail: "single-browser coverage absence is a config/projects aggregate property; the detector reads the projects arrangement across the config — no single node constitutes the finding"
|
|
6695
|
+
},
|
|
6527
6696
|
detectionNotes: "regex heuristic",
|
|
6528
6697
|
introduced: "0.3.8",
|
|
6529
6698
|
tier: "extended",
|
|
@@ -6607,6 +6776,10 @@ const pwLocatorNormalize = defineRule({
|
|
|
6607
6776
|
falsePositiveRisk: "medium",
|
|
6608
6777
|
autofix: false,
|
|
6609
6778
|
detectionStrategy: "LEXICAL",
|
|
6779
|
+
strategyJustification: {
|
|
6780
|
+
reasonCode: "string-content-defect",
|
|
6781
|
+
detail: "CSS/XPath string selectors are string-argument shapes (css=/xpath= engines, bare id/class/attr CSS, nth-child); the detector classifies the string shapes directly"
|
|
6782
|
+
},
|
|
6610
6783
|
detectionNotes: "string-selector shapes (css=/xpath= engines, bare id/class/attr CSS, nth-child) inside .locator()/waitForSelector()/page.$ APIs, on the RAW text view (the selector text lives inside string literals, which the code-only view blanks)",
|
|
6611
6784
|
introduced: "0.6.0",
|
|
6612
6785
|
tier: "quarantine",
|
|
@@ -6666,6 +6839,10 @@ const pwCodegenArtifact = defineRule({
|
|
|
6666
6839
|
falsePositiveRisk: "medium",
|
|
6667
6840
|
autofix: false,
|
|
6668
6841
|
detectionStrategy: "LEXICAL",
|
|
6842
|
+
strategyJustification: {
|
|
6843
|
+
reasonCode: "lexical-artifact",
|
|
6844
|
+
detail: "the codegen recorder's default title ('test', 'test 1', …) committed is a recording artifact — the default-title string is the finding; the detector matches the recorder's exact title shapes"
|
|
6845
|
+
},
|
|
6669
6846
|
detectionNotes: "the codegen recorder's default test title ('test', 'test 1', 'test 2', …) committed verbatim, on the RAW text view (the title is a string literal)",
|
|
6670
6847
|
introduced: "0.6.0",
|
|
6671
6848
|
tier: "quarantine",
|
|
@@ -6712,6 +6889,10 @@ const pyPwSyncAsyncMix = defineRule({
|
|
|
6712
6889
|
falsePositiveRisk: "low",
|
|
6713
6890
|
autofix: false,
|
|
6714
6891
|
detectionStrategy: "LEXICAL",
|
|
6892
|
+
strategyJustification: {
|
|
6893
|
+
reasonCode: "family-fallback-lockstep",
|
|
6894
|
+
detail: "the hard-sleep family's Python sync/async variant: the family's structural path carries the depth; this variant's lexical path is the lockstep fallback (async-mix shapes across the sync/async boundary)"
|
|
6895
|
+
},
|
|
6715
6896
|
detectionNotes: "regex heuristic",
|
|
6716
6897
|
introduced: "0.3.8",
|
|
6717
6898
|
run(ctx) {
|
|
@@ -6757,6 +6938,10 @@ const pyPwWaitForTimeout = defineRule({
|
|
|
6757
6938
|
falsePositiveRisk: "low",
|
|
6758
6939
|
autofix: false,
|
|
6759
6940
|
detectionStrategy: "LEXICAL",
|
|
6941
|
+
strategyJustification: {
|
|
6942
|
+
reasonCode: "exact-key-match",
|
|
6943
|
+
detail: "page.waitForTimeout is an exact Playwright token in Python tests; the detector matches the call identifier — closed token, same predicate the AST would encode"
|
|
6944
|
+
},
|
|
6760
6945
|
introduced: "0.3.8",
|
|
6761
6946
|
tier: "core",
|
|
6762
6947
|
run(ctx) {
|
|
@@ -6802,6 +6987,10 @@ const pyPwNoAssertions = defineRule({
|
|
|
6802
6987
|
falsePositiveRisk: "low",
|
|
6803
6988
|
autofix: false,
|
|
6804
6989
|
detectionStrategy: "LEXICAL",
|
|
6990
|
+
strategyJustification: {
|
|
6991
|
+
reasonCode: "runner-semantic",
|
|
6992
|
+
detail: "Playwright test bodies without assertions are runner-outcome semantics; the detector matches the test-def plus body shapes on the code-only text"
|
|
6993
|
+
},
|
|
6805
6994
|
detectionNotes: "regex heuristic",
|
|
6806
6995
|
introduced: "0.3.8",
|
|
6807
6996
|
tier: "quarantine",
|
|
@@ -6878,6 +7067,10 @@ const jvDisabledTest = defineRule({
|
|
|
6878
7067
|
falsePositiveRisk: "low",
|
|
6879
7068
|
autofix: false,
|
|
6880
7069
|
detectionStrategy: "LEXICAL",
|
|
7070
|
+
strategyJustification: {
|
|
7071
|
+
reasonCode: "exact-key-match",
|
|
7072
|
+
detail: "@Disabled/@Ignore are exact JUnit/TestNG annotation tokens; the detector matches the annotation identifier — annotation shapes are closed token sets where lexical and structural match coincide"
|
|
7073
|
+
},
|
|
6881
7074
|
introduced: "0.3.8",
|
|
6882
7075
|
tier: "core",
|
|
6883
7076
|
run(ctx) {
|
|
@@ -7937,6 +8130,10 @@ const csSkippedTest = defineRule({
|
|
|
7937
8130
|
falsePositiveRisk: "low",
|
|
7938
8131
|
autofix: false,
|
|
7939
8132
|
detectionStrategy: "LEXICAL",
|
|
8133
|
+
strategyJustification: {
|
|
8134
|
+
reasonCode: "exact-key-match",
|
|
8135
|
+
detail: "[Ignore]/[Fact(Skip=…)] are exact xUnit/NUnit/MSTest attribute tokens; the detector matches the attribute identifiers — closed token sets where lexical precision equals structural"
|
|
8136
|
+
},
|
|
7940
8137
|
introduced: "0.3.8",
|
|
7941
8138
|
run(ctx) {
|
|
7942
8139
|
const text = ctx.codeText ?? ctx.text;
|
|
@@ -8290,6 +8487,10 @@ const cypCyWait = defineRule({
|
|
|
8290
8487
|
falsePositiveRisk: "medium",
|
|
8291
8488
|
autofix: false,
|
|
8292
8489
|
detectionStrategy: "LEXICAL",
|
|
8490
|
+
strategyJustification: {
|
|
8491
|
+
reasonCode: "runner-semantic",
|
|
8492
|
+
detail: "cy.wait(numeric) is a Cypress runner wait contract; the detector matches the member-call token with a numeric-literal argument on the code-only text — alias waits (cy.wait('@…')) are structurally distinct and excluded by the argument shape"
|
|
8493
|
+
},
|
|
8293
8494
|
detectionNotes: "cy.wait with a numeric-literal argument only — alias waits (cy.wait('@…')) are the legitimate routed-request form and never fire",
|
|
8294
8495
|
introduced: "0.6.0",
|
|
8295
8496
|
tier: "extended",
|
|
@@ -8362,6 +8563,10 @@ const cypFocusedTest = defineRule({
|
|
|
8362
8563
|
falsePositiveRisk: "low",
|
|
8363
8564
|
autofix: false,
|
|
8364
8565
|
detectionStrategy: "LEXICAL",
|
|
8566
|
+
strategyJustification: {
|
|
8567
|
+
reasonCode: "runner-semantic",
|
|
8568
|
+
detail: "Cypress .only is the runner's focus token; the detector matches the it/describe/context .only member-call shape on the code-only text — exact-key precision"
|
|
8569
|
+
},
|
|
8365
8570
|
detectionNotes: "it/describe/context .only member-call shape on the code-only text view",
|
|
8366
8571
|
introduced: "0.6.0",
|
|
8367
8572
|
tier: "quarantine",
|
|
@@ -8428,6 +8633,10 @@ const cypConfigSecurity = defineRule({
|
|
|
8428
8633
|
falsePositiveRisk: "low",
|
|
8429
8634
|
autofix: false,
|
|
8430
8635
|
detectionStrategy: "LEXICAL",
|
|
8636
|
+
strategyJustification: {
|
|
8637
|
+
reasonCode: "exact-key-match",
|
|
8638
|
+
detail: "chromeWebSecurity:false is an exact config key/value pair inside cypress.config.*; the detector matches the key and value literally — the config surface's statements are the finding"
|
|
8639
|
+
},
|
|
8431
8640
|
detectionNotes: "positive match on chromeWebSecurity:false inside cypress.config.* (the config is the artifact — no heuristic)",
|
|
8432
8641
|
introduced: "0.6.0",
|
|
8433
8642
|
tier: "quarantine",
|
|
@@ -8535,6 +8744,10 @@ const seJavaSleepLookup = defineRule({
|
|
|
8535
8744
|
falsePositiveRisk: "medium",
|
|
8536
8745
|
autofix: false,
|
|
8537
8746
|
detectionStrategy: "LEXICAL",
|
|
8747
|
+
strategyJustification: {
|
|
8748
|
+
reasonCode: "runner-semantic",
|
|
8749
|
+
detail: "the Selenium family's variants (QA-SE-001/002/003): the defect is the SEQUENCE sleep-then-interact — runner timing semantics, not a single node; the detector matches the sleep token followed by a lookup within the recorded window"
|
|
8750
|
+
},
|
|
8538
8751
|
detectionNotes: "sequence shape: Thread.sleep followed by a findElement/interaction call within 3 lines (code-only view)",
|
|
8539
8752
|
introduced: "0.6.0",
|
|
8540
8753
|
tier: "quarantine",
|
|
@@ -8560,6 +8773,10 @@ const seCSharpSleepLookup = defineRule({
|
|
|
8560
8773
|
falsePositiveRisk: "medium",
|
|
8561
8774
|
autofix: false,
|
|
8562
8775
|
detectionStrategy: "LEXICAL",
|
|
8776
|
+
strategyJustification: {
|
|
8777
|
+
reasonCode: "runner-semantic",
|
|
8778
|
+
detail: "the Selenium family's variants (QA-SE-001/002/003): the defect is the SEQUENCE sleep-then-interact — runner timing semantics, not a single node; the detector matches the sleep token followed by a lookup within the recorded window"
|
|
8779
|
+
},
|
|
8563
8780
|
detectionNotes: "sequence shape: Thread.Sleep/Task.Delay followed by a FindElement/interaction call within 3 lines (code-only view)",
|
|
8564
8781
|
introduced: "0.6.0",
|
|
8565
8782
|
tier: "quarantine",
|
|
@@ -8585,6 +8802,10 @@ const sePythonSleepLookup = defineRule({
|
|
|
8585
8802
|
falsePositiveRisk: "medium",
|
|
8586
8803
|
autofix: false,
|
|
8587
8804
|
detectionStrategy: "LEXICAL",
|
|
8805
|
+
strategyJustification: {
|
|
8806
|
+
reasonCode: "runner-semantic",
|
|
8807
|
+
detail: "the Selenium family's variants (QA-SE-001/002/003): the defect is the SEQUENCE sleep-then-interact — runner timing semantics, not a single node; the detector matches the sleep token followed by a lookup within the recorded window"
|
|
8808
|
+
},
|
|
8588
8809
|
detectionNotes: "sequence shape: time.sleep followed by a find_element/interaction call within 3 lines (code-only view)",
|
|
8589
8810
|
introduced: "0.6.0",
|
|
8590
8811
|
tier: "quarantine",
|
|
@@ -8623,6 +8844,7 @@ function definePatternFamily(opts) {
|
|
|
8623
8844
|
falsePositiveRisk: v.falsePositiveRisk ?? opts.falsePositiveRisk,
|
|
8624
8845
|
autofix: opts.autofix ?? false,
|
|
8625
8846
|
detectionStrategy: v.detectionStrategy ?? opts.detectionStrategy ?? "LEXICAL",
|
|
8847
|
+
...v.strategyJustification ?? opts.strategyJustification ? { strategyJustification: v.strategyJustification ?? opts.strategyJustification } : {},
|
|
8626
8848
|
...v.detectionNotes !== void 0 ? { detectionNotes: v.detectionNotes } : { ...opts.detectionNotes !== void 0 ? { detectionNotes: opts.detectionNotes } : {} },
|
|
8627
8849
|
...opts.introduced ? { introduced: opts.introduced } : {},
|
|
8628
8850
|
...v.tier !== void 0 ? { tier: v.tier } : { ...opts.tier !== void 0 ? { tier: opts.tier } : {} },
|
|
@@ -8775,6 +8997,10 @@ const hardSleepFamily = definePatternFamily({
|
|
|
8775
8997
|
why: WHY,
|
|
8776
8998
|
falsePositiveRisk: "low",
|
|
8777
8999
|
detectionStrategy: "LEXICAL",
|
|
9000
|
+
strategyJustification: {
|
|
9001
|
+
reasonCode: "family-fallback-lockstep",
|
|
9002
|
+
detail: "the hard-sleep family's Java variant (QA-JV-102): the family's structural path carries the depth where a tree is available; this lexical path is the mandatory deterministic fallback kept in lockstep (§13.2)"
|
|
9003
|
+
},
|
|
8778
9004
|
introduced: "0.3.8",
|
|
8779
9005
|
tier: "extended",
|
|
8780
9006
|
useCodeText: true,
|
|
@@ -8824,6 +9050,10 @@ const networkIdleFamily = definePatternFamily({
|
|
|
8824
9050
|
why: "Analytics, websockets, and polling make network idle never fire or fire randomly — a documented source of Playwright flakes.",
|
|
8825
9051
|
falsePositiveRisk: "low",
|
|
8826
9052
|
detectionStrategy: "LEXICAL",
|
|
9053
|
+
strategyJustification: {
|
|
9054
|
+
reasonCode: "runner-semantic",
|
|
9055
|
+
detail: "the network-idle family's variants (QA-JV-107/QA-CS-107/QA-PY-107): networkidle waits are runner timing semantics; the detector matches the runner's wait tokens — exact keys"
|
|
9056
|
+
},
|
|
8827
9057
|
introduced: "0.4.0",
|
|
8828
9058
|
useCodeText: false,
|
|
8829
9059
|
variants: [
|
|
@@ -8899,6 +9129,10 @@ const hardcodedUrlFamily = definePatternFamily({
|
|
|
8899
9129
|
why: "Absolute URLs break when environments change and can hit production by accident from a CI runner.",
|
|
8900
9130
|
falsePositiveRisk: "low",
|
|
8901
9131
|
detectionStrategy: "LEXICAL",
|
|
9132
|
+
strategyJustification: {
|
|
9133
|
+
reasonCode: "string-content-defect",
|
|
9134
|
+
detail: "the hardcoded-url family's variants (QA-JV-108/QA-CS-108/QA-PY-108): hardcoded URLs are string literals; the detector matches the http(s):// string shapes — the URL lives in the string, not the syntax tree"
|
|
9135
|
+
},
|
|
8902
9136
|
introduced: "0.4.0",
|
|
8903
9137
|
useCodeText: false,
|
|
8904
9138
|
variants: [{
|
|
@@ -8929,6 +9163,10 @@ const sharedPageFamily = definePatternFamily({
|
|
|
8929
9163
|
why: "A shared Page/Browser leaks cookies, localStorage, and navigation state between tests — failures become order-dependent and impossible to reproduce in isolation.",
|
|
8930
9164
|
falsePositiveRisk: "medium",
|
|
8931
9165
|
detectionStrategy: "LEXICAL",
|
|
9166
|
+
strategyJustification: {
|
|
9167
|
+
reasonCode: "runner-semantic",
|
|
9168
|
+
detail: "the shared-page family's variants (QA-JV-104/QA-CS-104/QA-PY-106): page/fixture reuse across tests is runner fixture-lifecycle semantics; the detector matches the consumption shapes against test boundaries"
|
|
9169
|
+
},
|
|
8932
9170
|
introduced: "0.4.0",
|
|
8933
9171
|
useCodeText: true,
|
|
8934
9172
|
variants: [
|
|
@@ -9004,6 +9242,10 @@ function makeBrittleSelectors(id, appliesTo, ext, languages, frameworks, pattern
|
|
|
9004
9242
|
falsePositiveRisk: "medium",
|
|
9005
9243
|
autofix: false,
|
|
9006
9244
|
detectionStrategy: "LEXICAL",
|
|
9245
|
+
strategyJustification: {
|
|
9246
|
+
reasonCode: "string-content-defect",
|
|
9247
|
+
detail: "the brittle-selector family's variants (QA-JV-106/QA-CS-106/QA-PY-104): selector defects are string-argument shapes (xpath=/nth-child/absolute-path); the detector classifies the string shapes the code-text mask preserves for this class"
|
|
9248
|
+
},
|
|
9007
9249
|
introduced: "0.4.0",
|
|
9008
9250
|
tier: "quarantine",
|
|
9009
9251
|
detectorRevision: 2,
|
|
@@ -9100,6 +9342,10 @@ const retryMaskingFamily = [defineRule({
|
|
|
9100
9342
|
falsePositiveRisk: "medium",
|
|
9101
9343
|
autofix: false,
|
|
9102
9344
|
detectionStrategy: "LEXICAL",
|
|
9345
|
+
strategyJustification: {
|
|
9346
|
+
reasonCode: "runner-semantic",
|
|
9347
|
+
detail: "the retry-masking family's variants (QA-JV-109/QA-CS-109): retry masking is the runner's retry contract; the detector matches the runner's retry tokens on the code-only text"
|
|
9348
|
+
},
|
|
9103
9349
|
introduced: "0.4.0",
|
|
9104
9350
|
tier: "core",
|
|
9105
9351
|
run(ctx) {
|
|
@@ -9153,6 +9399,10 @@ const retryMaskingFamily = [defineRule({
|
|
|
9153
9399
|
falsePositiveRisk: "medium",
|
|
9154
9400
|
autofix: false,
|
|
9155
9401
|
detectionStrategy: "LEXICAL",
|
|
9402
|
+
strategyJustification: {
|
|
9403
|
+
reasonCode: "runner-semantic",
|
|
9404
|
+
detail: "the retry-masking family's variants (QA-JV-109/QA-CS-109): retry masking is the runner's retry contract; the detector matches the runner's retry tokens on the code-only text"
|
|
9405
|
+
},
|
|
9156
9406
|
introduced: "0.4.0",
|
|
9157
9407
|
tier: "extended",
|
|
9158
9408
|
run(ctx) {
|
|
@@ -9827,6 +10077,237 @@ function createScanCache(root) {
|
|
|
9827
10077
|
};
|
|
9828
10078
|
}
|
|
9829
10079
|
//#endregion
|
|
10080
|
+
//#region src/brand/tokens.ts
|
|
10081
|
+
/**
|
|
10082
|
+
* The single source of brand truth.
|
|
10083
|
+
*
|
|
10084
|
+
* Every colour, typeface and motion constant Mjölnir shows a human —
|
|
10085
|
+
* terminal, README SVGs, demo video, website, docs, badges — resolves to
|
|
10086
|
+
* a value in this file. Nothing else may define one.
|
|
10087
|
+
*
|
|
10088
|
+
* WHY THIS EXISTS. Before it, the palette existed in six independent
|
|
10089
|
+
* copies: `site/.vitepress/theme/styles/vars.css`, `NORSE` in
|
|
10090
|
+
* `src/reporter/theme.ts`, `scripts/readme-svg.ts`,
|
|
10091
|
+
* `scripts/video/terminal-page.ts`, `scripts/generate-readme-architecture.ts`
|
|
10092
|
+
* and the table in `assets/brand/README.md`. Exactly one pair of those
|
|
10093
|
+
* was guarded (site-doctor Check 8, doc ↔ vars.css). The unguarded edges
|
|
10094
|
+
* are where the shipped surfaces drifted apart: the terminal and the site
|
|
10095
|
+
* disagreed on six semantic roles, the architecture diagram invented its
|
|
10096
|
+
* own neutral ramp, and the README badges still carried a palette retired
|
|
10097
|
+
* two releases earlier. `scripts/brand-doctor.mjs` now checks every edge
|
|
10098
|
+
* against this file.
|
|
10099
|
+
*
|
|
10100
|
+
* PURITY. Pure data. No I/O, no rendering, no environment access, no
|
|
10101
|
+
* imports, no logic. Consumers convert (hex → ANSI triplet, hex → CSS)
|
|
10102
|
+
* themselves. Same reason `score-state.ts` is pure: it makes the whole
|
|
10103
|
+
* thing golden-testable and safe to ship inside the npm package, where
|
|
10104
|
+
* it costs a few hundred bytes and replaces values the package already
|
|
10105
|
+
* carried anyway.
|
|
10106
|
+
*
|
|
10107
|
+
* DERIVATION. The palette is the one derived from the logo in PR #20
|
|
10108
|
+
* (brushed steel and forge gold under an aurora, over midnight iron).
|
|
10109
|
+
* Where the terminal disagreed with it, the terminal converges — see
|
|
10110
|
+
* `PENDING_TERMINAL` below. Full rationale: `assets/brand/README.md`.
|
|
10111
|
+
*
|
|
10112
|
+
* ACCESSIBILITY. Every foreground token in `BRAND` meets WCAG AA
|
|
10113
|
+
* (≥ 4.5:1) against every surface token it is allowed to sit on. That is
|
|
10114
|
+
* not a claim, it is `brand-doctor` rule 8, which computes the ratios.
|
|
10115
|
+
* The weakest legal pairing is `steelDim` on `ink800` at 5.00:1.
|
|
10116
|
+
*/
|
|
10117
|
+
/**
|
|
10118
|
+
* The two brand hues plus the neutral they sit on.
|
|
10119
|
+
*
|
|
10120
|
+
* GOLD IS SCARCE. It means forged / certified / earned / decisive — the
|
|
10121
|
+
* primary mark, the FORGED state, one call to action. It is not a paint
|
|
10122
|
+
* bucket: gold as default text, default border or default heading is a
|
|
10123
|
+
* brand-doctor finding, not a style choice.
|
|
10124
|
+
*
|
|
10125
|
+
* AURORA is verification energy — the secondary, and the hue that marks
|
|
10126
|
+
* the runtime half of the trust ladder.
|
|
10127
|
+
*/
|
|
10128
|
+
const BRAND = {
|
|
10129
|
+
gold: "#C19A34",
|
|
10130
|
+
goldBright: "#E6BD57",
|
|
10131
|
+
goldHot: "#F4DC9C",
|
|
10132
|
+
/** Pressed / deepest gold — the only step dark enough to carry white. */
|
|
10133
|
+
goldDeep: "#A5811C",
|
|
10134
|
+
aurora: "#37ABBD",
|
|
10135
|
+
auroraBright: "#45C1D4",
|
|
10136
|
+
auroraCyan: "#5CBDE0",
|
|
10137
|
+
steel: "#C8CBCF",
|
|
10138
|
+
steelDim: "#8B939D"
|
|
10139
|
+
};
|
|
10140
|
+
/**
|
|
10141
|
+
* Midnight iron. One ramp, four steps, darkest first.
|
|
10142
|
+
*
|
|
10143
|
+
* `terminal` and `terminalBar` share one tone deliberately: the window's
|
|
10144
|
+
* only seam is a hairline ring and an inset shadow, never a second fill.
|
|
10145
|
+
* `chromeDot` is the three window dots — see the note on
|
|
10146
|
+
* `PENDING_TERMINAL.chromeDots` for why they are no longer red/amber/green.
|
|
10147
|
+
*/
|
|
10148
|
+
const SURFACE = {
|
|
10149
|
+
ink950: "#0A1119",
|
|
10150
|
+
ink900: "#0C1420",
|
|
10151
|
+
ink850: "#111A29",
|
|
10152
|
+
ink800: "#18243A",
|
|
10153
|
+
/** Raised panel (cards, elevated surfaces). */
|
|
10154
|
+
panel: "#141F33",
|
|
10155
|
+
/** Soft fill (inline code, quiet chips). */
|
|
10156
|
+
soft: "#1A2740",
|
|
10157
|
+
/** Terminal body — the deepest tone, so a terminal reads as recessed. */
|
|
10158
|
+
terminal: "#0A1119",
|
|
10159
|
+
/** Terminal title bar — the same tone; the seam is shadow, not colour. */
|
|
10160
|
+
terminalBar: "#0A1119",
|
|
10161
|
+
/** The three window dots. One neutral, not a traffic light. */
|
|
10162
|
+
chromeDot: "#18243A"
|
|
10163
|
+
};
|
|
10164
|
+
const TEXT = {
|
|
10165
|
+
primary: "#EAEEF5",
|
|
10166
|
+
secondary: "#ABB6C6",
|
|
10167
|
+
muted: "#8B939D",
|
|
10168
|
+
/** Ink for text set ON gold (buttons, the FORGED chip). 7.17:1 on `gold`. */
|
|
10169
|
+
onGold: "#0A1119"
|
|
10170
|
+
};
|
|
10171
|
+
/**
|
|
10172
|
+
* Non-score status. `ok` is the one green in the system and it is NOT a
|
|
10173
|
+
* score colour — it survives only for contexts with no worthiness
|
|
10174
|
+
* meaning ("autofix applied", "analysis complete"). A green score would
|
|
10175
|
+
* say "your software is fine", which is the exact claim this product
|
|
10176
|
+
* refuses to make.
|
|
10177
|
+
*/
|
|
10178
|
+
const STATUS = {
|
|
10179
|
+
ok: "#4FB477",
|
|
10180
|
+
info: "#5CC4E0",
|
|
10181
|
+
warning: "#E6BD57",
|
|
10182
|
+
error: "#EC6B66"
|
|
10183
|
+
};
|
|
10184
|
+
/**
|
|
10185
|
+
* The four ScoreState bands plus the unmeasured state. Band thresholds
|
|
10186
|
+
* and runes live in `src/reporter/score-state.ts`, which stays free of
|
|
10187
|
+
* colour — it emits a palette KEY and each surface resolves it here.
|
|
10188
|
+
*
|
|
10189
|
+
* `unmeasured` is steel-dim on purpose. UNKNOWN is a legitimate answer,
|
|
10190
|
+
* not a failure: colouring it red would make "we did not measure this"
|
|
10191
|
+
* look like "this is broken", which is precisely the dishonesty the
|
|
10192
|
+
* north-star law exists to prevent.
|
|
10193
|
+
*/
|
|
10194
|
+
const SCORE = {
|
|
10195
|
+
critical: "#EC6B66",
|
|
10196
|
+
warning: "#E6BD57",
|
|
10197
|
+
trusted: "#5CC4E0",
|
|
10198
|
+
forged: "#F4DC9C",
|
|
10199
|
+
unmeasured: "#8B939D"
|
|
10200
|
+
};
|
|
10201
|
+
/**
|
|
10202
|
+
* E0 → E1 → E2 is a certainty ramp, and it is deliberately HUE-FREE.
|
|
10203
|
+
*
|
|
10204
|
+
* Evidence level says how sure we are, not whether the news is good. A
|
|
10205
|
+
* deterministic proof (E2) is a defect we are certain about — painting
|
|
10206
|
+
* it gold or green would read as an achievement. So certainty is carried
|
|
10207
|
+
* by brightness alone, and the *shape* does the real work:
|
|
10208
|
+
*
|
|
10209
|
+
* E0 open ring observation, no weight
|
|
10210
|
+
* E1 half-filled pattern evidence, half weight
|
|
10211
|
+
* E2 sealed deterministic proof, full weight
|
|
10212
|
+
*
|
|
10213
|
+
* Colour never carries this alone (R11): the geometry is the signal and
|
|
10214
|
+
* survives `--ascii`, `NO_COLOR` and monochrome print.
|
|
10215
|
+
*/
|
|
10216
|
+
const EVIDENCE = {
|
|
10217
|
+
e0: "#8B939D",
|
|
10218
|
+
e1: "#ABB6C6",
|
|
10219
|
+
e2: "#EAEEF5"
|
|
10220
|
+
};
|
|
10221
|
+
/**
|
|
10222
|
+
* L0–L5, and the most important boundary in the product.
|
|
10223
|
+
*
|
|
10224
|
+
* L0–L2 are STATIC: the neutral steel ramp, brightening to the static
|
|
10225
|
+
* ceiling at L2. L3–L5 require a real run, and the hue changes to aurora
|
|
10226
|
+
* exactly there. The boundary is a hue break, not a gradient step,
|
|
10227
|
+
* because it is a change of kind and not of degree — a static-only
|
|
10228
|
+
* finding can never climb past L2, however confident it is.
|
|
10229
|
+
*
|
|
10230
|
+
* Every surface that draws the ladder must draw that break.
|
|
10231
|
+
*/
|
|
10232
|
+
const TRUST = {
|
|
10233
|
+
l0: "#8B939D",
|
|
10234
|
+
l1: "#ABB6C6",
|
|
10235
|
+
l2: "#C8CBCF",
|
|
10236
|
+
l3: "#37ABBD",
|
|
10237
|
+
l4: "#45C1D4",
|
|
10238
|
+
l5: "#5CC4E0"
|
|
10239
|
+
};
|
|
10240
|
+
const TINT = {
|
|
10241
|
+
gold: {
|
|
10242
|
+
fill: "#F6EBCC",
|
|
10243
|
+
stroke: "#7A5F16",
|
|
10244
|
+
text: "#4A3A0E"
|
|
10245
|
+
},
|
|
10246
|
+
aurora: {
|
|
10247
|
+
fill: "#D9F0F4",
|
|
10248
|
+
stroke: "#1F6F7C",
|
|
10249
|
+
text: "#10353C"
|
|
10250
|
+
},
|
|
10251
|
+
error: {
|
|
10252
|
+
fill: "#FADEDD",
|
|
10253
|
+
stroke: "#A83A35",
|
|
10254
|
+
text: "#4E1B19"
|
|
10255
|
+
},
|
|
10256
|
+
/** The unmeasured / unknown state. Neutral, never the error tint. */
|
|
10257
|
+
neutral: {
|
|
10258
|
+
fill: "#E4E7EB",
|
|
10259
|
+
stroke: "#5C646E",
|
|
10260
|
+
text: "#262B31"
|
|
10261
|
+
},
|
|
10262
|
+
ok: {
|
|
10263
|
+
fill: "#DCF0E4",
|
|
10264
|
+
stroke: "#276B45",
|
|
10265
|
+
text: "#163A26"
|
|
10266
|
+
}
|
|
10267
|
+
};
|
|
10268
|
+
/**
|
|
10269
|
+
* The score bands as the badge Mjölnir itself generates renders them.
|
|
10270
|
+
*
|
|
10271
|
+
* These are DEEPER than the score tokens on purpose, and it is not a
|
|
10272
|
+
* style preference: shields.io sets the message text in white and gives
|
|
10273
|
+
* you no say in it. `score.forged` (#F4DC9C) under white text measures
|
|
10274
|
+
* 1.35:1 — an unreadable badge, shipped to look on-brand. The brand's
|
|
10275
|
+
* own deep steps put every band between 4.9 and 6.3:1.
|
|
10276
|
+
*
|
|
10277
|
+
* What they replace was worse than off-brand, it was wrong:
|
|
10278
|
+
*
|
|
10279
|
+
* band was rendered as white-on now white-on
|
|
10280
|
+
* ────────────────────────────────────────────────────────────────────────────
|
|
10281
|
+
* 0-49 `red` #dd4343 4.24 #A83A35 6.32
|
|
10282
|
+
* 50-79 `yellow` #d8b800 1.95 #7A5F16 6.04
|
|
10283
|
+
* 80-99 `important` #ea7233 ORANGE 3.02 #1F6F7C 5.80
|
|
10284
|
+
* 100 `success` #44bb00 GREEN 2.51 #8A6D1E 4.90
|
|
10285
|
+
* unmeasured `lightgrey` #939393 3.07 #5C646E 5.99
|
|
10286
|
+
*
|
|
10287
|
+
* Two of those were defects, not preferences. `success` is green, and
|
|
10288
|
+
* green is not a score colour here — a 100 badge said "your software is
|
|
10289
|
+
* fine", which is the one claim this product refuses to make. And
|
|
10290
|
+
* `important` is ORANGE, not the blue-family colour the code's own
|
|
10291
|
+
* comment claimed for eight releases: every WORTHY badge ever rendered
|
|
10292
|
+
* showed the trusted band in a warning colour. Nobody had resolved a
|
|
10293
|
+
* shields name to a value and looked.
|
|
10294
|
+
*
|
|
10295
|
+
* Values are hex without `#`, the form shields.io's endpoint takes.
|
|
10296
|
+
* `A83A35`, `7A5F16`, `1F6F7C` and `5C646E` are the `TINT` strokes —
|
|
10297
|
+
* the same deep steps the mermaid diagrams use, for the same reason.
|
|
10298
|
+
* `8A6D1E` is the gold the brand document already named as the light
|
|
10299
|
+
* FORGED gradient's start.
|
|
10300
|
+
*/
|
|
10301
|
+
const BADGE_BAND = {
|
|
10302
|
+
critical: "A83A35",
|
|
10303
|
+
warning: "7A5F16",
|
|
10304
|
+
trusted: "1F6F7C",
|
|
10305
|
+
forged: "8A6D1E",
|
|
10306
|
+
unmeasured: "5C646E"
|
|
10307
|
+
};
|
|
10308
|
+
SURFACE.ink950, SURFACE.ink900, SURFACE.ink850, SURFACE.ink800, BRAND.steel, BRAND.steelDim, BRAND.gold, BRAND.goldBright, BRAND.goldHot, BRAND.aurora, BRAND.auroraBright, BRAND.auroraCyan;
|
|
10309
|
+
SCORE.trusted, SCORE.forged, SCORE.warning, SCORE.critical, STATUS.info, TEXT.onGold, STATUS.ok, EVIDENCE.e0, EVIDENCE.e1, EVIDENCE.e2, TRUST.l0, TRUST.l1, TRUST.l2, TRUST.l3, TRUST.l4, TRUST.l5;
|
|
10310
|
+
//#endregion
|
|
9830
10311
|
//#region src/reporter/score-state.ts
|
|
9831
10312
|
const HEADLINES = {
|
|
9832
10313
|
critical: "The hammer is cracked — {n} findings break its edge.",
|
|
@@ -9903,11 +10384,11 @@ function headlineFor(state, findings) {
|
|
|
9903
10384
|
* Respects NO_COLOR and non-TTY via `palette(isTTY)` — every renderer
|
|
9904
10385
|
* receives a palette and never touches process.env directly.
|
|
9905
10386
|
*
|
|
9906
|
-
* Palette:
|
|
9907
|
-
*
|
|
9908
|
-
*
|
|
9909
|
-
*
|
|
9910
|
-
*
|
|
10387
|
+
* Palette: resolved from `src/brand/tokens.ts`, the single source of
|
|
10388
|
+
* brand truth — this file defines no colour of its own. Emitted as
|
|
10389
|
+
* 24-bit truecolor SGR (`38;2;r;g;b`), which every modern terminal
|
|
10390
|
+
* renders and which `shouldColorize` already gates behind
|
|
10391
|
+
* TTY + !NO_COLOR.
|
|
9911
10392
|
*
|
|
9912
10393
|
* Symbols always accompany color (color-blind safe, R11).
|
|
9913
10394
|
*
|
|
@@ -9920,53 +10401,42 @@ function headlineFor(state, findings) {
|
|
|
9920
10401
|
* flag so output degrades to plain characters on cmd.exe/legacy
|
|
9921
10402
|
* consoles that mangle box-drawing glyphs and emoji.
|
|
9922
10403
|
*/
|
|
9923
|
-
/**
|
|
10404
|
+
/**
|
|
10405
|
+
* `"#RRGGBB"` → the `[r, g, b]` triplet the SGR truecolor emitter needs.
|
|
10406
|
+
* Lives here rather than in `src/brand/tokens.ts`, which is pure data:
|
|
10407
|
+
* each surface converts the canonical hex into its own colour space.
|
|
10408
|
+
*/
|
|
10409
|
+
function fromHex(hex) {
|
|
10410
|
+
const n = Number.parseInt(hex.slice(1), 16);
|
|
10411
|
+
return [
|
|
10412
|
+
n >> 16 & 255,
|
|
10413
|
+
n >> 8 & 255,
|
|
10414
|
+
n & 255
|
|
10415
|
+
];
|
|
10416
|
+
}
|
|
10417
|
+
/**
|
|
10418
|
+
* The terminal palette, 24-bit truecolor, resolved from
|
|
10419
|
+
* `src/brand/tokens.ts` — the single source of brand truth. Nothing in
|
|
10420
|
+
* this file may name a hex value of its own, and `brand-doctor` rule 2
|
|
10421
|
+
* fails if it tries.
|
|
10422
|
+
*
|
|
10423
|
+
* Every role is now the canonical token. Six of them used to be the
|
|
10424
|
+
* terminal's own: a frost-steel blue for headers, a teal for info, an
|
|
10425
|
+
* amber for warnings, a rune-red for errors, a bone white for bold and a
|
|
10426
|
+
* weathered stone for dim — a second palette for one product. The
|
|
10427
|
+
* rune-red also failed WCAG AA at 4.36:1 on this terminal's own
|
|
10428
|
+
* background; `STATUS.error` on the canonical ground is 6.20:1.
|
|
10429
|
+
*/
|
|
9924
10430
|
const NORSE = {
|
|
9925
|
-
ok:
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
],
|
|
9935
|
-
accent: [
|
|
9936
|
-
138,
|
|
9937
|
-
180,
|
|
9938
|
-
216
|
|
9939
|
-
],
|
|
9940
|
-
warning: [
|
|
9941
|
-
224,
|
|
9942
|
-
165,
|
|
9943
|
-
38
|
|
9944
|
-
],
|
|
9945
|
-
error: [
|
|
9946
|
-
208,
|
|
9947
|
-
69,
|
|
9948
|
-
59
|
|
9949
|
-
],
|
|
9950
|
-
trusted: [
|
|
9951
|
-
92,
|
|
9952
|
-
196,
|
|
9953
|
-
224
|
|
9954
|
-
],
|
|
9955
|
-
forged: [
|
|
9956
|
-
244,
|
|
9957
|
-
220,
|
|
9958
|
-
156
|
|
9959
|
-
],
|
|
9960
|
-
bold: [
|
|
9961
|
-
237,
|
|
9962
|
-
230,
|
|
9963
|
-
214
|
|
9964
|
-
],
|
|
9965
|
-
dim: [
|
|
9966
|
-
124,
|
|
9967
|
-
133,
|
|
9968
|
-
144
|
|
9969
|
-
]
|
|
10431
|
+
ok: fromHex(STATUS.ok),
|
|
10432
|
+
info: fromHex(BRAND.aurora),
|
|
10433
|
+
accent: fromHex(BRAND.steel),
|
|
10434
|
+
warning: fromHex(STATUS.warning),
|
|
10435
|
+
error: fromHex(STATUS.error),
|
|
10436
|
+
trusted: fromHex(SCORE.trusted),
|
|
10437
|
+
forged: fromHex(SCORE.forged),
|
|
10438
|
+
bold: fromHex(TEXT.primary),
|
|
10439
|
+
dim: fromHex(TEXT.muted)
|
|
9970
10440
|
};
|
|
9971
10441
|
const on = {
|
|
9972
10442
|
ok: rgb(NORSE.ok),
|
|
@@ -12608,16 +13078,47 @@ function countBySeverity(result) {
|
|
|
12608
13078
|
}
|
|
12609
13079
|
return counts;
|
|
12610
13080
|
}
|
|
13081
|
+
EVIDENCE.e0, EVIDENCE.e1, EVIDENCE.e2;
|
|
13082
|
+
const RUNG_MEANINGS = [
|
|
13083
|
+
"observation only",
|
|
13084
|
+
"heuristic static",
|
|
13085
|
+
"deterministic static",
|
|
13086
|
+
"the finding's file executed",
|
|
13087
|
+
"the finding's test executed",
|
|
13088
|
+
"the run verdict corroborates"
|
|
13089
|
+
];
|
|
13090
|
+
const RUNG_COLORS = [
|
|
13091
|
+
TRUST.l0,
|
|
13092
|
+
TRUST.l1,
|
|
13093
|
+
TRUST.l2,
|
|
13094
|
+
TRUST.l3,
|
|
13095
|
+
TRUST.l4,
|
|
13096
|
+
TRUST.l5
|
|
13097
|
+
];
|
|
13098
|
+
const TRUST_RUNGS = RUNG_MEANINGS.map((meaning, i) => ({
|
|
13099
|
+
level: `L${i}`,
|
|
13100
|
+
meaning,
|
|
13101
|
+
runtime: i >= 3,
|
|
13102
|
+
color: RUNG_COLORS[i]
|
|
13103
|
+
}));
|
|
12611
13104
|
//#endregion
|
|
12612
13105
|
//#region src/reporter/trust-report.ts
|
|
12613
|
-
|
|
12614
|
-
|
|
12615
|
-
|
|
12616
|
-
|
|
12617
|
-
|
|
12618
|
-
|
|
12619
|
-
|
|
12620
|
-
|
|
13106
|
+
/**
|
|
13107
|
+
* The rung labels, built from `src/brand/symbols.ts` rather than typed
|
|
13108
|
+
* again here.
|
|
13109
|
+
*
|
|
13110
|
+
* They had drifted the moment there were two copies: this file said
|
|
13111
|
+
* "file executed" and "run corroborates defect" where the symbol module,
|
|
13112
|
+
* the architecture diagram and the website's ladder all said "the
|
|
13113
|
+
* finding's file executed" and "the run verdict corroborates". Small
|
|
13114
|
+
* enough that nobody would notice, and exactly the kind of divergence
|
|
13115
|
+
* that makes a reader wonder whether two surfaces mean the same thing.
|
|
13116
|
+
*
|
|
13117
|
+
* The runtime marker is not decoration either: L3 and above cannot be
|
|
13118
|
+
* reached without a real run report, and the label says so wherever the
|
|
13119
|
+
* ladder is not drawn to show it.
|
|
13120
|
+
*/
|
|
13121
|
+
const TRUST_LABELS = Object.fromEntries(TRUST_RUNGS.map((r) => [r.level, `${r.level} · ${r.meaning}${r.runtime ? " · runtime" : ""}`]));
|
|
12621
13122
|
function pct$1(v) {
|
|
12622
13123
|
return `${Math.round(v * 100)}%`;
|
|
12623
13124
|
}
|
|
@@ -12753,7 +13254,7 @@ function renderSarif(result, repoRootUri) {
|
|
|
12753
13254
|
tool: { driver: {
|
|
12754
13255
|
name: "Mjölnir",
|
|
12755
13256
|
informationUri: "https://github.com/Sergey-Bar/Mjolnir",
|
|
12756
|
-
version: "1.0.
|
|
13257
|
+
version: "1.0.3",
|
|
12757
13258
|
rules: [...rules.values()].map((r) => {
|
|
12758
13259
|
const meta = RULES.find((x) => x.id === r.id);
|
|
12759
13260
|
return {
|
|
@@ -12882,6 +13383,22 @@ function renderCodeQuality(result) {
|
|
|
12882
13383
|
}
|
|
12883
13384
|
//#endregion
|
|
12884
13385
|
//#region src/reporter/mermaid.ts
|
|
13386
|
+
/**
|
|
13387
|
+
* `--format mermaid` — test-architecture diagram (Sprint 9 Task 38,
|
|
13388
|
+
* Master-Stabilization-Plan.md). Ranked first among the delight
|
|
13389
|
+
* features because it is genuinely useful to QA leads presenting scan
|
|
13390
|
+
* results to stakeholders, not merely decorative — a flowchart of
|
|
13391
|
+
* detected frameworks → rule categories → severity buckets, so gaps
|
|
13392
|
+
* are visible at a glance in a format that pastes directly into a
|
|
13393
|
+
* GitHub/GitLab markdown comment or a slide (Mermaid renders natively
|
|
13394
|
+
* in both).
|
|
13395
|
+
*
|
|
13396
|
+
* Score-neutral (Sprint 9's own DoD line): this is a pure alternate
|
|
13397
|
+
* rendering of the exact same ScanResult every other format uses — it
|
|
13398
|
+
* changes no scoring, no exit code, no JSON contract field. Output is
|
|
13399
|
+
* fully deterministic: every collection is sorted before rendering, so
|
|
13400
|
+
* the same ScanResult always produces byte-identical Mermaid source.
|
|
13401
|
+
*/
|
|
12885
13402
|
function sanitizeId(raw) {
|
|
12886
13403
|
return raw.replace(/[^a-z0-9]/gi, "_");
|
|
12887
13404
|
}
|
|
@@ -12889,6 +13406,14 @@ function sanitizeId(raw) {
|
|
|
12889
13406
|
function escapeLabel(text) {
|
|
12890
13407
|
return text.replaceAll("\"", """).replaceAll("\n", " ");
|
|
12891
13408
|
}
|
|
13409
|
+
/**
|
|
13410
|
+
* One Mermaid `classDef` line from one brand tint. Every colour this
|
|
13411
|
+
* renderer emits comes from `src/brand/tokens.ts`; it names none of its
|
|
13412
|
+
* own, and `brand-doctor` rule 6 fails if it starts to.
|
|
13413
|
+
*/
|
|
13414
|
+
function classDef(name, tint) {
|
|
13415
|
+
return ` classDef ${name} fill:${tint.fill},stroke:${tint.stroke},color:${tint.text};`;
|
|
13416
|
+
}
|
|
12892
13417
|
function dimensionStyleClass(dim) {
|
|
12893
13418
|
if (dim.score >= 80) return "healthy";
|
|
12894
13419
|
if (dim.score >= 50) return "warn";
|
|
@@ -12907,8 +13432,8 @@ function renderMermaid(result) {
|
|
|
12907
13432
|
if (result.score === null) {
|
|
12908
13433
|
lines.push(` ${rootId} --> NOTESTS["No test files detected"]`);
|
|
12909
13434
|
lines.push("");
|
|
12910
|
-
lines.push("
|
|
12911
|
-
lines.push(" class NOTESTS
|
|
13435
|
+
lines.push(classDef("unknown", TINT.neutral));
|
|
13436
|
+
lines.push(" class NOTESTS unknown;");
|
|
12912
13437
|
return lines.join("\n");
|
|
12913
13438
|
}
|
|
12914
13439
|
const frameworks = [...result.frameworks].sort((a, b) => a.localeCompare(b));
|
|
@@ -12944,10 +13469,11 @@ function renderMermaid(result) {
|
|
|
12944
13469
|
}
|
|
12945
13470
|
}
|
|
12946
13471
|
lines.push("");
|
|
12947
|
-
lines.push("
|
|
12948
|
-
lines.push("
|
|
12949
|
-
lines.push("
|
|
12950
|
-
lines.push("
|
|
13472
|
+
lines.push(classDef("healthy", TINT.ok));
|
|
13473
|
+
lines.push(classDef("warn", TINT.gold));
|
|
13474
|
+
lines.push(classDef("critical", TINT.error));
|
|
13475
|
+
lines.push(classDef("info", TINT.aurora));
|
|
13476
|
+
lines.push(classDef("unknown", TINT.neutral));
|
|
12951
13477
|
for (const { id, cls } of styleAssignments) lines.push(` class ${id} ${cls};`);
|
|
12952
13478
|
return lines.join("\n");
|
|
12953
13479
|
}
|
|
@@ -16219,17 +16745,19 @@ function renderMutationSummary(report) {
|
|
|
16219
16745
|
* retarget fixes the historical threshold drift (the badge used
|
|
16220
16746
|
* ≥90/≥75/≥50 with four bands while the reporter used ≥80/≥50).
|
|
16221
16747
|
*
|
|
16222
|
-
*
|
|
16223
|
-
*
|
|
16224
|
-
*
|
|
16225
|
-
*
|
|
16748
|
+
* They are brand values now, from `BADGE_BAND`, not shields.io's named
|
|
16749
|
+
* colors. That mapping was documented as "trusted → `important`
|
|
16750
|
+
* (blue-family, closest to aurora-cyan)" and was simply untrue:
|
|
16751
|
+
* `important` resolves to #ea7233, which is orange. Every WORTHY badge
|
|
16752
|
+
* rendered the trusted band in a warning colour, and `success` — the
|
|
16753
|
+
* 100 state — rendered green, which is not a score colour here.
|
|
16754
|
+
*
|
|
16755
|
+
* ScoreState remains the truth; the badge is still a peripheral
|
|
16756
|
+
* surface. But peripheral is not the same as unchecked.
|
|
16226
16757
|
*/
|
|
16227
16758
|
function colorFor(score) {
|
|
16228
16759
|
const band = deriveScoreState(score).band;
|
|
16229
|
-
|
|
16230
|
-
if (band === "forged") return "success";
|
|
16231
|
-
if (band === "trusted") return "important";
|
|
16232
|
-
return band === "warning" ? "yellow" : "red";
|
|
16760
|
+
return BADGE_BAND[band];
|
|
16233
16761
|
}
|
|
16234
16762
|
/** Build the shields.io endpoint payload from a scan result. */
|
|
16235
16763
|
function buildBadge(result, commit) {
|
|
@@ -16240,7 +16768,6 @@ function buildBadge(result, commit) {
|
|
|
16240
16768
|
label: "MJÖLNIR",
|
|
16241
16769
|
message: score === null ? "no tests found" : score === 100 && errors === 0 ? "100/100 · forged" : `${score}/100 · ${errors} error${errors === 1 ? "" : "s"}`,
|
|
16242
16770
|
color: colorFor(score),
|
|
16243
|
-
namedLogo: "vitest",
|
|
16244
16771
|
...commit !== void 0 ? { commit } : {}
|
|
16245
16772
|
};
|
|
16246
16773
|
}
|
|
@@ -18417,6 +18944,19 @@ function loadManifest(path) {
|
|
|
18417
18944
|
*
|
|
18418
18945
|
* Exit codes reuse the frozen set: 0 healthy · 1 violations · 20 crash.
|
|
18419
18946
|
*/
|
|
18947
|
+
/** The closed reason-code set (master plan P8) — mirrored from
|
|
18948
|
+
* src/rules/rule.ts's StrategyReasonCode union via a runtime set so the
|
|
18949
|
+
* doctor check validates membership without an extra export cycle. */
|
|
18950
|
+
const VALID_REASON_CODES = /* @__PURE__ */ new Set([
|
|
18951
|
+
"shell-string-in-config",
|
|
18952
|
+
"exact-key-match",
|
|
18953
|
+
"lexical-artifact",
|
|
18954
|
+
"runner-semantic",
|
|
18955
|
+
"string-content-defect",
|
|
18956
|
+
"absence-aggregate",
|
|
18957
|
+
"family-fallback-lockstep",
|
|
18958
|
+
"migration-deferred-next-measurement"
|
|
18959
|
+
]);
|
|
18420
18960
|
/** Uniform error rendering for doctor details (Error or thrown-as-string). */
|
|
18421
18961
|
function errorText(e) {
|
|
18422
18962
|
return e instanceof Error ? e.message : String(e);
|
|
@@ -18455,7 +18995,8 @@ function checkFixtureFirewall(fixturesRoot) {
|
|
|
18455
18995
|
}
|
|
18456
18996
|
return check("fixture-firewall", ok ? "pass" : "fail", details);
|
|
18457
18997
|
}
|
|
18458
|
-
/** Check 2: registry sanity — IDs unique, well-formed, titles distinct
|
|
18998
|
+
/** Check 2: registry sanity — IDs unique, well-formed, titles distinct,
|
|
18999
|
+
* and (P8) every LEXICAL rule carries a valid depth-adjudication record. */
|
|
18459
19000
|
function checkRegistry(rules = RULES) {
|
|
18460
19001
|
const details = [];
|
|
18461
19002
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -18470,6 +19011,19 @@ function checkRegistry(rules = RULES) {
|
|
|
18470
19011
|
details.push(`${r.id}: duplicate registration`);
|
|
18471
19012
|
}
|
|
18472
19013
|
ids.add(r.id);
|
|
19014
|
+
if (r.detectionStrategy === "LEXICAL") {
|
|
19015
|
+
const j = r.strategyJustification;
|
|
19016
|
+
if (!j) {
|
|
19017
|
+
ok = false;
|
|
19018
|
+
details.push(`${r.id}: LEXICAL strategy without strategyJustification — declare the reason the lexical form ships (docs/DEPTH-ADJUDICATION.md)`);
|
|
19019
|
+
} else if (!VALID_REASON_CODES.has(j.reasonCode)) {
|
|
19020
|
+
ok = false;
|
|
19021
|
+
details.push(`${r.id}: strategyJustification.reasonCode "${String(j.reasonCode)}" is outside the closed set (${[...VALID_REASON_CODES].join(", ")})`);
|
|
19022
|
+
} else if (j.detail.trim().length < 20) {
|
|
19023
|
+
ok = false;
|
|
19024
|
+
details.push(`${r.id}: strategyJustification.detail is boilerplate — cite the detector's actual shape`);
|
|
19025
|
+
}
|
|
19026
|
+
}
|
|
18473
19027
|
const family = r.id.split("-")[1];
|
|
18474
19028
|
for (const other of rules) if (other.id !== r.id && other.title === r.title && other.id.split("-")[1] === family) {
|
|
18475
19029
|
ok = false;
|
|
@@ -19157,7 +19711,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
|
|
|
19157
19711
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
19158
19712
|
* `tests/version-consistency.spec.ts` locally.
|
|
19159
19713
|
*/
|
|
19160
|
-
const CLI_VERSION = "1.0.
|
|
19714
|
+
const CLI_VERSION = "1.0.3";
|
|
19161
19715
|
function parseArgs(argv, onError) {
|
|
19162
19716
|
const args = {
|
|
19163
19717
|
target: ".",
|
|
@@ -19493,8 +20047,9 @@ async function runExplainCommand(argv, io = {
|
|
|
19493
20047
|
return 10;
|
|
19494
20048
|
}
|
|
19495
20049
|
try {
|
|
19496
|
-
|
|
19497
|
-
|
|
20050
|
+
const r = explainVerdict(resolve(jsonPath));
|
|
20051
|
+
io.out(renderVerdictExplain(r));
|
|
20052
|
+
return r.ok ? 0 : 10;
|
|
19498
20053
|
} catch (err) {
|
|
19499
20054
|
internalErrorMessage(err, io.err, argv.includes("--debug"));
|
|
19500
20055
|
return 20;
|