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.
@@ -118,12 +118,13 @@ const DEDUCTIONS = {
118
118
  */
119
119
  const EVIDENCE_CORE_VERSION = "evidence-core@1";
120
120
  /** Deterministic canonical order: file → line (absent last) → title → source. */
121
- function compareRecords(a, b) {
121
+ function compareEvidenceRecords(a, b) {
122
122
  if (a.file !== b.file) return a.file < b.file ? -1 : 1;
123
123
  const la = a.line ?? Number.POSITIVE_INFINITY;
124
124
  const lb = b.line ?? Number.POSITIVE_INFINITY;
125
125
  if (la !== lb) return la - lb;
126
126
  if (a.title !== b.title) return a.title < b.title ? -1 : 1;
127
+ if (a.source !== b.source) return a.source < b.source ? -1 : 1;
127
128
  return 0;
128
129
  }
129
130
  function normalizeOne(report, artifact, v) {
@@ -158,7 +159,7 @@ function normalizeOne(report, artifact, v) {
158
159
  * record list — report verdict order never leaks into the output.
159
160
  */
160
161
  function buildEvidenceRecords(report, artifact) {
161
- return report.verdicts.map((v) => normalizeOne(report, artifact, v)).sort(compareRecords);
162
+ return report.verdicts.map((v) => normalizeOne(report, artifact, v)).sort(compareEvidenceRecords);
162
163
  }
163
164
  /**
164
165
  * The test whose declaration span contains `line` — the exact matching
@@ -178,8 +179,8 @@ function findTestAt(records, file, line) {
178
179
  for (const r of inFile) if (r.line === void 0) return void 0;
179
180
  let match;
180
181
  for (const r of inFile) {
181
- const rLine = r.line;
182
- if (rLine <= line && (match === void 0 || match.line <= rLine)) match = r;
182
+ const l = r.line;
183
+ if (l <= line && (match === void 0 || match.line <= l)) match = r;
183
184
  }
184
185
  return match;
185
186
  }
@@ -3320,6 +3321,10 @@ const focusedTestCommitted = defineRule({
3320
3321
  falsePositiveRisk: "low",
3321
3322
  autofix: true,
3322
3323
  detectionStrategy: "LEXICAL",
3324
+ strategyJustification: {
3325
+ reasonCode: "runner-semantic",
3326
+ 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"
3327
+ },
3323
3328
  introduced: "0.1.0",
3324
3329
  tier: "quarantine",
3325
3330
  suiteInvalidating: true,
@@ -3380,6 +3385,10 @@ const skippedTest = defineRule({
3380
3385
  falsePositiveRisk: "medium",
3381
3386
  autofix: false,
3382
3387
  detectionStrategy: "LEXICAL",
3388
+ strategyJustification: {
3389
+ reasonCode: "runner-semantic",
3390
+ 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"
3391
+ },
3383
3392
  introduced: "0.1.0",
3384
3393
  tier: "quarantine",
3385
3394
  run(ctx) {
@@ -3497,6 +3506,10 @@ const noAssertions = defineRule({
3497
3506
  falsePositiveRisk: "medium",
3498
3507
  autofix: false,
3499
3508
  detectionStrategy: "LEXICAL",
3509
+ strategyJustification: {
3510
+ reasonCode: "runner-semantic",
3511
+ 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"
3512
+ },
3500
3513
  introduced: "0.1.0",
3501
3514
  tier: "quarantine",
3502
3515
  run(ctx) {
@@ -3571,6 +3584,10 @@ const hardSleep = defineRule({
3571
3584
  falsePositiveRisk: "low",
3572
3585
  autofix: false,
3573
3586
  detectionStrategy: "LEXICAL",
3587
+ strategyJustification: {
3588
+ reasonCode: "runner-semantic",
3589
+ 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"
3590
+ },
3574
3591
  detectionNotes: "regex pattern + behavioral wait-shape matching",
3575
3592
  introduced: "0.1.0",
3576
3593
  tier: "extended",
@@ -3636,6 +3653,10 @@ const retryAbuse = defineRule({
3636
3653
  falsePositiveRisk: "low",
3637
3654
  autofix: false,
3638
3655
  detectionStrategy: "LEXICAL",
3656
+ strategyJustification: {
3657
+ reasonCode: "runner-semantic",
3658
+ 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"
3659
+ },
3639
3660
  introduced: "0.1.0",
3640
3661
  tier: "quarantine",
3641
3662
  run(ctx) {
@@ -3697,6 +3718,10 @@ const emptyTestBody = defineRule({
3697
3718
  falsePositiveRisk: "low",
3698
3719
  autofix: false,
3699
3720
  detectionStrategy: "LEXICAL",
3721
+ strategyJustification: {
3722
+ reasonCode: "runner-semantic",
3723
+ 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"
3724
+ },
3700
3725
  introduced: "0.1.0",
3701
3726
  tier: "quarantine",
3702
3727
  overlapWith: ["QA-TEST-003"],
@@ -3748,6 +3773,10 @@ const tautologicalAssertion = defineRule({
3748
3773
  falsePositiveRisk: "low",
3749
3774
  autofix: false,
3750
3775
  detectionStrategy: "LEXICAL",
3776
+ strategyJustification: {
3777
+ reasonCode: "runner-semantic",
3778
+ 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"
3779
+ },
3751
3780
  detectionNotes: "AST-stripped text pattern",
3752
3781
  introduced: "0.1.0",
3753
3782
  tier: "quarantine",
@@ -3827,6 +3856,10 @@ const unawaitedPromiseAssertion = defineRule({
3827
3856
  falsePositiveRisk: "low",
3828
3857
  autofix: false,
3829
3858
  detectionStrategy: "LEXICAL",
3859
+ strategyJustification: {
3860
+ reasonCode: "runner-semantic",
3861
+ 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"
3862
+ },
3830
3863
  introduced: "0.2.0",
3831
3864
  tier: "quarantine",
3832
3865
  run(ctx) {
@@ -3926,6 +3959,10 @@ const commentedOutTest = defineRule({
3926
3959
  falsePositiveRisk: "medium",
3927
3960
  autofix: false,
3928
3961
  detectionStrategy: "LEXICAL",
3962
+ strategyJustification: {
3963
+ reasonCode: "lexical-artifact",
3964
+ 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"
3965
+ },
3929
3966
  detectionNotes: "regex heuristic",
3930
3967
  introduced: "0.2.0",
3931
3968
  tier: "extended",
@@ -4171,6 +4208,10 @@ const committedDebugArtifacts = defineRule({
4171
4208
  falsePositiveRisk: "low",
4172
4209
  autofix: true,
4173
4210
  detectionStrategy: "LEXICAL",
4211
+ strategyJustification: {
4212
+ reasonCode: "exact-key-match",
4213
+ 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"
4214
+ },
4174
4215
  introduced: "0.1.0",
4175
4216
  tier: "core",
4176
4217
  run(ctx) {
@@ -4223,6 +4264,10 @@ const brittleSelectors = defineRule({
4223
4264
  falsePositiveRisk: "medium",
4224
4265
  autofix: false,
4225
4266
  detectionStrategy: "LEXICAL",
4267
+ strategyJustification: {
4268
+ reasonCode: "string-content-defect",
4269
+ 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)"
4270
+ },
4226
4271
  detectionNotes: "regex pattern + inside-string oracle",
4227
4272
  introduced: "0.1.0",
4228
4273
  tier: "quarantine",
@@ -4520,6 +4565,10 @@ const swallowedExitCode = defineRule({
4520
4565
  falsePositiveRisk: "low",
4521
4566
  autofix: false,
4522
4567
  detectionStrategy: "LEXICAL",
4568
+ strategyJustification: {
4569
+ reasonCode: "shell-string-in-config",
4570
+ 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"
4571
+ },
4523
4572
  introduced: "0.1.0",
4524
4573
  tier: "extended",
4525
4574
  detectorRevision: 2,
@@ -4572,6 +4621,10 @@ const retryMasking = defineRule({
4572
4621
  falsePositiveRisk: "low",
4573
4622
  autofix: false,
4574
4623
  detectionStrategy: "LEXICAL",
4624
+ strategyJustification: {
4625
+ reasonCode: "runner-semantic",
4626
+ 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"
4627
+ },
4575
4628
  introduced: "0.1.0",
4576
4629
  tier: "extended",
4577
4630
  detectorRevision: 2,
@@ -4755,6 +4808,10 @@ const reportNeverGenerated = defineRule({
4755
4808
  falsePositiveRisk: "low",
4756
4809
  autofix: false,
4757
4810
  detectionStrategy: "LEXICAL",
4811
+ strategyJustification: {
4812
+ reasonCode: "runner-semantic",
4813
+ 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"
4814
+ },
4758
4815
  introduced: "0.1.0",
4759
4816
  tier: "quarantine",
4760
4817
  detectorRevision: 2,
@@ -4855,6 +4912,10 @@ const alwaysSuccessStep = defineRule({
4855
4912
  falsePositiveRisk: "low",
4856
4913
  autofix: false,
4857
4914
  detectionStrategy: "LEXICAL",
4915
+ strategyJustification: {
4916
+ reasonCode: "runner-semantic",
4917
+ 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"
4918
+ },
4858
4919
  introduced: "0.1.0",
4859
4920
  tier: "quarantine",
4860
4921
  detectorRevision: 2,
@@ -5101,6 +5162,10 @@ const pyNoAssertions = defineRule({
5101
5162
  falsePositiveRisk: "low",
5102
5163
  autofix: false,
5103
5164
  detectionStrategy: "LEXICAL",
5165
+ strategyJustification: {
5166
+ reasonCode: "runner-semantic",
5167
+ 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"
5168
+ },
5104
5169
  introduced: "0.3.0",
5105
5170
  tier: "quarantine",
5106
5171
  detectorRevision: 3,
@@ -5184,6 +5249,10 @@ const pyHardSleep = defineRule({
5184
5249
  falsePositiveRisk: "low",
5185
5250
  autofix: false,
5186
5251
  detectionStrategy: "LEXICAL",
5252
+ strategyJustification: {
5253
+ reasonCode: "exact-key-match",
5254
+ 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"
5255
+ },
5187
5256
  introduced: "0.3.0",
5188
5257
  tier: "extended",
5189
5258
  overlapWith: ["QA-PY-102"],
@@ -5229,6 +5298,10 @@ const pySkippedTest = defineRule({
5229
5298
  falsePositiveRisk: "low",
5230
5299
  autofix: false,
5231
5300
  detectionStrategy: "LEXICAL",
5301
+ strategyJustification: {
5302
+ reasonCode: "runner-semantic",
5303
+ 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"
5304
+ },
5232
5305
  introduced: "0.3.0",
5233
5306
  run(ctx) {
5234
5307
  const text = ctx.codeText ?? ctx.text;
@@ -5285,6 +5358,10 @@ const pyTautological = defineRule({
5285
5358
  falsePositiveRisk: "low",
5286
5359
  autofix: false,
5287
5360
  detectionStrategy: "LEXICAL",
5361
+ strategyJustification: {
5362
+ reasonCode: "runner-semantic",
5363
+ 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"
5364
+ },
5288
5365
  introduced: "0.3.0",
5289
5366
  tier: "quarantine",
5290
5367
  run(ctx) {
@@ -5339,6 +5416,10 @@ const pyFocusedTest = defineRule({
5339
5416
  falsePositiveRisk: "low",
5340
5417
  autofix: false,
5341
5418
  detectionStrategy: "LEXICAL",
5419
+ strategyJustification: {
5420
+ reasonCode: "runner-semantic",
5421
+ 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"
5422
+ },
5342
5423
  introduced: "0.3.0",
5343
5424
  tier: "core",
5344
5425
  suiteInvalidating: true,
@@ -5391,6 +5472,10 @@ const pyRaisesWithoutMatch = defineRule({
5391
5472
  falsePositiveRisk: "medium",
5392
5473
  autofix: false,
5393
5474
  detectionStrategy: "LEXICAL",
5475
+ strategyJustification: {
5476
+ reasonCode: "runner-semantic",
5477
+ 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"
5478
+ },
5394
5479
  introduced: "0.3.0",
5395
5480
  tier: "quarantine",
5396
5481
  detectorRevision: 3,
@@ -5470,6 +5555,10 @@ const pyCommentedOutTest = defineRule({
5470
5555
  falsePositiveRisk: "medium",
5471
5556
  autofix: false,
5472
5557
  detectionStrategy: "LEXICAL",
5558
+ strategyJustification: {
5559
+ reasonCode: "lexical-artifact",
5560
+ 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"
5561
+ },
5473
5562
  detectionNotes: "regex heuristic",
5474
5563
  introduced: "0.3.0",
5475
5564
  tier: "core",
@@ -5515,6 +5604,10 @@ const pyBareTruthinessAssert = defineRule({
5515
5604
  falsePositiveRisk: "medium",
5516
5605
  autofix: false,
5517
5606
  detectionStrategy: "LEXICAL",
5607
+ strategyJustification: {
5608
+ reasonCode: "runner-semantic",
5609
+ 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"
5610
+ },
5518
5611
  introduced: "0.3.0",
5519
5612
  tier: "quarantine",
5520
5613
  detectorRevision: 3,
@@ -5583,6 +5676,10 @@ const pyMutableFixture = defineRule({
5583
5676
  falsePositiveRisk: "medium",
5584
5677
  autofix: false,
5585
5678
  detectionStrategy: "LEXICAL",
5679
+ strategyJustification: {
5680
+ reasonCode: "runner-semantic",
5681
+ 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"
5682
+ },
5586
5683
  introduced: "0.3.0",
5587
5684
  tier: "core",
5588
5685
  run(ctx) {
@@ -5631,6 +5728,10 @@ const pwWaitForTimeout = defineRule({
5631
5728
  falsePositiveRisk: "low",
5632
5729
  autofix: false,
5633
5730
  detectionStrategy: "LEXICAL",
5731
+ strategyJustification: {
5732
+ reasonCode: "family-fallback-lockstep",
5733
+ 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"
5734
+ },
5634
5735
  introduced: "0.3.0",
5635
5736
  overlapWith: ["QA-TEST-004"],
5636
5737
  run(ctx) {
@@ -5674,6 +5775,10 @@ const pwWaitForLoadEvent = defineRule({
5674
5775
  falsePositiveRisk: "medium",
5675
5776
  autofix: false,
5676
5777
  detectionStrategy: "LEXICAL",
5778
+ strategyJustification: {
5779
+ reasonCode: "exact-key-match",
5780
+ 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"
5781
+ },
5677
5782
  introduced: "0.3.0",
5678
5783
  tier: "quarantine",
5679
5784
  detectorRevision: 2,
@@ -5726,6 +5831,10 @@ const pwDeepFrameLocator = defineRule({
5726
5831
  falsePositiveRisk: "low",
5727
5832
  autofix: false,
5728
5833
  detectionStrategy: "LEXICAL",
5834
+ strategyJustification: {
5835
+ reasonCode: "exact-key-match",
5836
+ 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"
5837
+ },
5729
5838
  introduced: "0.3.0",
5730
5839
  tier: "core",
5731
5840
  run(ctx) {
@@ -5773,6 +5882,10 @@ const pwSerialNoJustification = defineRule({
5773
5882
  falsePositiveRisk: "low",
5774
5883
  autofix: false,
5775
5884
  detectionStrategy: "LEXICAL",
5885
+ strategyJustification: {
5886
+ reasonCode: "runner-semantic",
5887
+ 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"
5888
+ },
5776
5889
  introduced: "0.3.0",
5777
5890
  tier: "core",
5778
5891
  run(ctx) {
@@ -5826,6 +5939,10 @@ const pwConfigRetryAbuse = defineRule({
5826
5939
  falsePositiveRisk: "low",
5827
5940
  autofix: false,
5828
5941
  detectionStrategy: "LEXICAL",
5942
+ strategyJustification: {
5943
+ reasonCode: "runner-semantic",
5944
+ 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"
5945
+ },
5829
5946
  detectionNotes: "regex heuristic",
5830
5947
  introduced: "0.3.0",
5831
5948
  tier: "core",
@@ -5887,6 +6004,10 @@ const pwNoTraceOnRetry = defineRule({
5887
6004
  falsePositiveRisk: "low",
5888
6005
  autofix: false,
5889
6006
  detectionStrategy: "LEXICAL",
6007
+ strategyJustification: {
6008
+ reasonCode: "runner-semantic",
6009
+ 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"
6010
+ },
5890
6011
  detectionNotes: "regex heuristic",
5891
6012
  introduced: "0.3.0",
5892
6013
  tier: "extended",
@@ -5940,6 +6061,10 @@ const pwNoProjectSplit = defineRule({
5940
6061
  falsePositiveRisk: "low",
5941
6062
  autofix: false,
5942
6063
  detectionStrategy: "LEXICAL",
6064
+ strategyJustification: {
6065
+ reasonCode: "runner-semantic",
6066
+ 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"
6067
+ },
5943
6068
  detectionNotes: "regex heuristic over playwright.config.* (adapter-gated)",
5944
6069
  introduced: "0.3.0",
5945
6070
  detectorRevision: 2,
@@ -5990,6 +6115,10 @@ const pwTrialMisuse = defineRule({
5990
6115
  falsePositiveRisk: "medium",
5991
6116
  autofix: false,
5992
6117
  detectionStrategy: "LEXICAL",
6118
+ strategyJustification: {
6119
+ reasonCode: "exact-key-match",
6120
+ 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"
6121
+ },
5993
6122
  introduced: "0.3.0",
5994
6123
  tier: "core",
5995
6124
  run(ctx) {
@@ -6034,6 +6163,10 @@ const pwStorageStateNoExpiry = defineRule({
6034
6163
  falsePositiveRisk: "medium",
6035
6164
  autofix: false,
6036
6165
  detectionStrategy: "LEXICAL",
6166
+ strategyJustification: {
6167
+ reasonCode: "exact-key-match",
6168
+ detail: "storageState is an exact Playwright config/use option token; the detector matches the option key and its value shapes — closed config surface"
6169
+ },
6037
6170
  introduced: "0.3.0",
6038
6171
  run(ctx) {
6039
6172
  const text = ctx.text;
@@ -6078,6 +6211,10 @@ const pwGlobalSetupSharedState = defineRule({
6078
6211
  falsePositiveRisk: "medium",
6079
6212
  autofix: false,
6080
6213
  detectionStrategy: "LEXICAL",
6214
+ strategyJustification: {
6215
+ reasonCode: "runner-semantic",
6216
+ 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"
6217
+ },
6081
6218
  detectionNotes: "regex heuristic",
6082
6219
  introduced: "0.3.0",
6083
6220
  run(ctx) {
@@ -6130,6 +6267,10 @@ const pwSharedPage = defineRule({
6130
6267
  falsePositiveRisk: "medium",
6131
6268
  autofix: false,
6132
6269
  detectionStrategy: "LEXICAL",
6270
+ strategyJustification: {
6271
+ reasonCode: "runner-semantic",
6272
+ 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"
6273
+ },
6133
6274
  introduced: "0.3.0",
6134
6275
  tier: "quarantine",
6135
6276
  run(ctx) {
@@ -6177,6 +6318,10 @@ const qaPw140 = defineRule({
6177
6318
  falsePositiveRisk: "medium",
6178
6319
  autofix: false,
6179
6320
  detectionStrategy: "LEXICAL",
6321
+ strategyJustification: {
6322
+ reasonCode: "exact-key-match",
6323
+ detail: "the detector matches a closed, exact runner API token on the code-only text; the token identifies the defect uniquely"
6324
+ },
6180
6325
  introduced: "0.3.0",
6181
6326
  tier: "core",
6182
6327
  run(ctx) {
@@ -6244,6 +6389,10 @@ const hardcodedBaseUrl = defineRule({
6244
6389
  falsePositiveRisk: "low",
6245
6390
  autofix: false,
6246
6391
  detectionStrategy: "LEXICAL",
6392
+ strategyJustification: {
6393
+ reasonCode: "string-content-defect",
6394
+ 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"
6395
+ },
6247
6396
  introduced: "0.3.0",
6248
6397
  tier: "quarantine",
6249
6398
  run(ctx) {
@@ -6294,6 +6443,10 @@ const envCoupling = defineRule({
6294
6443
  falsePositiveRisk: "medium",
6295
6444
  autofix: false,
6296
6445
  detectionStrategy: "LEXICAL",
6446
+ strategyJustification: {
6447
+ reasonCode: "absence-aggregate",
6448
+ 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"
6449
+ },
6297
6450
  detectionNotes: "regex heuristic",
6298
6451
  introduced: "0.2.0",
6299
6452
  tier: "quarantine",
@@ -6368,6 +6521,10 @@ const pwRetryMaskingNoForensics = defineRule({
6368
6521
  falsePositiveRisk: "low",
6369
6522
  autofix: false,
6370
6523
  detectionStrategy: "LEXICAL",
6524
+ strategyJustification: {
6525
+ reasonCode: "runner-semantic",
6526
+ 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"
6527
+ },
6371
6528
  detectionNotes: "regex heuristic",
6372
6529
  introduced: "0.3.8",
6373
6530
  tier: "extended",
@@ -6420,6 +6577,10 @@ const pwBlanketRouteMock = defineRule({
6420
6577
  falsePositiveRisk: "medium",
6421
6578
  autofix: false,
6422
6579
  detectionStrategy: "LEXICAL",
6580
+ strategyJustification: {
6581
+ reasonCode: "absence-aggregate",
6582
+ 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"
6583
+ },
6423
6584
  detectionNotes: "regex heuristic",
6424
6585
  introduced: "0.3.8",
6425
6586
  tier: "extended",
@@ -6473,6 +6634,10 @@ const pwNoFailureArtifacts = defineRule({
6473
6634
  falsePositiveRisk: "low",
6474
6635
  autofix: false,
6475
6636
  detectionStrategy: "LEXICAL",
6637
+ strategyJustification: {
6638
+ reasonCode: "absence-aggregate",
6639
+ 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"
6640
+ },
6476
6641
  detectionNotes: "regex heuristic",
6477
6642
  introduced: "0.3.8",
6478
6643
  tier: "extended",
@@ -6523,6 +6688,10 @@ const pwSingleBrowserMatrix = defineRule({
6523
6688
  falsePositiveRisk: "low",
6524
6689
  autofix: false,
6525
6690
  detectionStrategy: "LEXICAL",
6691
+ strategyJustification: {
6692
+ reasonCode: "absence-aggregate",
6693
+ 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"
6694
+ },
6526
6695
  detectionNotes: "regex heuristic",
6527
6696
  introduced: "0.3.8",
6528
6697
  tier: "extended",
@@ -6606,6 +6775,10 @@ const pwLocatorNormalize = defineRule({
6606
6775
  falsePositiveRisk: "medium",
6607
6776
  autofix: false,
6608
6777
  detectionStrategy: "LEXICAL",
6778
+ strategyJustification: {
6779
+ reasonCode: "string-content-defect",
6780
+ 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"
6781
+ },
6609
6782
  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)",
6610
6783
  introduced: "0.6.0",
6611
6784
  tier: "quarantine",
@@ -6665,6 +6838,10 @@ const pwCodegenArtifact = defineRule({
6665
6838
  falsePositiveRisk: "medium",
6666
6839
  autofix: false,
6667
6840
  detectionStrategy: "LEXICAL",
6841
+ strategyJustification: {
6842
+ reasonCode: "lexical-artifact",
6843
+ 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"
6844
+ },
6668
6845
  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)",
6669
6846
  introduced: "0.6.0",
6670
6847
  tier: "quarantine",
@@ -6711,6 +6888,10 @@ const pyPwSyncAsyncMix = defineRule({
6711
6888
  falsePositiveRisk: "low",
6712
6889
  autofix: false,
6713
6890
  detectionStrategy: "LEXICAL",
6891
+ strategyJustification: {
6892
+ reasonCode: "family-fallback-lockstep",
6893
+ 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)"
6894
+ },
6714
6895
  detectionNotes: "regex heuristic",
6715
6896
  introduced: "0.3.8",
6716
6897
  run(ctx) {
@@ -6756,6 +6937,10 @@ const pyPwWaitForTimeout = defineRule({
6756
6937
  falsePositiveRisk: "low",
6757
6938
  autofix: false,
6758
6939
  detectionStrategy: "LEXICAL",
6940
+ strategyJustification: {
6941
+ reasonCode: "exact-key-match",
6942
+ 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"
6943
+ },
6759
6944
  introduced: "0.3.8",
6760
6945
  tier: "core",
6761
6946
  run(ctx) {
@@ -6801,6 +6986,10 @@ const pyPwNoAssertions = defineRule({
6801
6986
  falsePositiveRisk: "low",
6802
6987
  autofix: false,
6803
6988
  detectionStrategy: "LEXICAL",
6989
+ strategyJustification: {
6990
+ reasonCode: "runner-semantic",
6991
+ detail: "Playwright test bodies without assertions are runner-outcome semantics; the detector matches the test-def plus body shapes on the code-only text"
6992
+ },
6804
6993
  detectionNotes: "regex heuristic",
6805
6994
  introduced: "0.3.8",
6806
6995
  tier: "quarantine",
@@ -6877,6 +7066,10 @@ const jvDisabledTest = defineRule({
6877
7066
  falsePositiveRisk: "low",
6878
7067
  autofix: false,
6879
7068
  detectionStrategy: "LEXICAL",
7069
+ strategyJustification: {
7070
+ reasonCode: "exact-key-match",
7071
+ 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"
7072
+ },
6880
7073
  introduced: "0.3.8",
6881
7074
  tier: "core",
6882
7075
  run(ctx) {
@@ -7936,6 +8129,10 @@ const csSkippedTest = defineRule({
7936
8129
  falsePositiveRisk: "low",
7937
8130
  autofix: false,
7938
8131
  detectionStrategy: "LEXICAL",
8132
+ strategyJustification: {
8133
+ reasonCode: "exact-key-match",
8134
+ 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"
8135
+ },
7939
8136
  introduced: "0.3.8",
7940
8137
  run(ctx) {
7941
8138
  const text = ctx.codeText ?? ctx.text;
@@ -8289,6 +8486,10 @@ const cypCyWait = defineRule({
8289
8486
  falsePositiveRisk: "medium",
8290
8487
  autofix: false,
8291
8488
  detectionStrategy: "LEXICAL",
8489
+ strategyJustification: {
8490
+ reasonCode: "runner-semantic",
8491
+ 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"
8492
+ },
8292
8493
  detectionNotes: "cy.wait with a numeric-literal argument only — alias waits (cy.wait('@…')) are the legitimate routed-request form and never fire",
8293
8494
  introduced: "0.6.0",
8294
8495
  tier: "extended",
@@ -8361,6 +8562,10 @@ const cypFocusedTest = defineRule({
8361
8562
  falsePositiveRisk: "low",
8362
8563
  autofix: false,
8363
8564
  detectionStrategy: "LEXICAL",
8565
+ strategyJustification: {
8566
+ reasonCode: "runner-semantic",
8567
+ 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"
8568
+ },
8364
8569
  detectionNotes: "it/describe/context .only member-call shape on the code-only text view",
8365
8570
  introduced: "0.6.0",
8366
8571
  tier: "quarantine",
@@ -8427,6 +8632,10 @@ const cypConfigSecurity = defineRule({
8427
8632
  falsePositiveRisk: "low",
8428
8633
  autofix: false,
8429
8634
  detectionStrategy: "LEXICAL",
8635
+ strategyJustification: {
8636
+ reasonCode: "exact-key-match",
8637
+ 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"
8638
+ },
8430
8639
  detectionNotes: "positive match on chromeWebSecurity:false inside cypress.config.* (the config is the artifact — no heuristic)",
8431
8640
  introduced: "0.6.0",
8432
8641
  tier: "quarantine",
@@ -8534,6 +8743,10 @@ const seJavaSleepLookup = defineRule({
8534
8743
  falsePositiveRisk: "medium",
8535
8744
  autofix: false,
8536
8745
  detectionStrategy: "LEXICAL",
8746
+ strategyJustification: {
8747
+ reasonCode: "runner-semantic",
8748
+ 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"
8749
+ },
8537
8750
  detectionNotes: "sequence shape: Thread.sleep followed by a findElement/interaction call within 3 lines (code-only view)",
8538
8751
  introduced: "0.6.0",
8539
8752
  tier: "quarantine",
@@ -8559,6 +8772,10 @@ const seCSharpSleepLookup = defineRule({
8559
8772
  falsePositiveRisk: "medium",
8560
8773
  autofix: false,
8561
8774
  detectionStrategy: "LEXICAL",
8775
+ strategyJustification: {
8776
+ reasonCode: "runner-semantic",
8777
+ 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"
8778
+ },
8562
8779
  detectionNotes: "sequence shape: Thread.Sleep/Task.Delay followed by a FindElement/interaction call within 3 lines (code-only view)",
8563
8780
  introduced: "0.6.0",
8564
8781
  tier: "quarantine",
@@ -8584,6 +8801,10 @@ const sePythonSleepLookup = defineRule({
8584
8801
  falsePositiveRisk: "medium",
8585
8802
  autofix: false,
8586
8803
  detectionStrategy: "LEXICAL",
8804
+ strategyJustification: {
8805
+ reasonCode: "runner-semantic",
8806
+ 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"
8807
+ },
8587
8808
  detectionNotes: "sequence shape: time.sleep followed by a find_element/interaction call within 3 lines (code-only view)",
8588
8809
  introduced: "0.6.0",
8589
8810
  tier: "quarantine",
@@ -8622,6 +8843,7 @@ function definePatternFamily(opts) {
8622
8843
  falsePositiveRisk: v.falsePositiveRisk ?? opts.falsePositiveRisk,
8623
8844
  autofix: opts.autofix ?? false,
8624
8845
  detectionStrategy: v.detectionStrategy ?? opts.detectionStrategy ?? "LEXICAL",
8846
+ ...v.strategyJustification ?? opts.strategyJustification ? { strategyJustification: v.strategyJustification ?? opts.strategyJustification } : {},
8625
8847
  ...v.detectionNotes !== void 0 ? { detectionNotes: v.detectionNotes } : { ...opts.detectionNotes !== void 0 ? { detectionNotes: opts.detectionNotes } : {} },
8626
8848
  ...opts.introduced ? { introduced: opts.introduced } : {},
8627
8849
  ...v.tier !== void 0 ? { tier: v.tier } : { ...opts.tier !== void 0 ? { tier: opts.tier } : {} },
@@ -8774,6 +8996,10 @@ const hardSleepFamily = definePatternFamily({
8774
8996
  why: WHY,
8775
8997
  falsePositiveRisk: "low",
8776
8998
  detectionStrategy: "LEXICAL",
8999
+ strategyJustification: {
9000
+ reasonCode: "family-fallback-lockstep",
9001
+ 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)"
9002
+ },
8777
9003
  introduced: "0.3.8",
8778
9004
  tier: "extended",
8779
9005
  useCodeText: true,
@@ -8823,6 +9049,10 @@ const networkIdleFamily = definePatternFamily({
8823
9049
  why: "Analytics, websockets, and polling make network idle never fire or fire randomly — a documented source of Playwright flakes.",
8824
9050
  falsePositiveRisk: "low",
8825
9051
  detectionStrategy: "LEXICAL",
9052
+ strategyJustification: {
9053
+ reasonCode: "runner-semantic",
9054
+ 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"
9055
+ },
8826
9056
  introduced: "0.4.0",
8827
9057
  useCodeText: false,
8828
9058
  variants: [
@@ -8898,6 +9128,10 @@ const hardcodedUrlFamily = definePatternFamily({
8898
9128
  why: "Absolute URLs break when environments change and can hit production by accident from a CI runner.",
8899
9129
  falsePositiveRisk: "low",
8900
9130
  detectionStrategy: "LEXICAL",
9131
+ strategyJustification: {
9132
+ reasonCode: "string-content-defect",
9133
+ 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"
9134
+ },
8901
9135
  introduced: "0.4.0",
8902
9136
  useCodeText: false,
8903
9137
  variants: [{
@@ -8928,6 +9162,10 @@ const sharedPageFamily = definePatternFamily({
8928
9162
  why: "A shared Page/Browser leaks cookies, localStorage, and navigation state between tests — failures become order-dependent and impossible to reproduce in isolation.",
8929
9163
  falsePositiveRisk: "medium",
8930
9164
  detectionStrategy: "LEXICAL",
9165
+ strategyJustification: {
9166
+ reasonCode: "runner-semantic",
9167
+ 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"
9168
+ },
8931
9169
  introduced: "0.4.0",
8932
9170
  useCodeText: true,
8933
9171
  variants: [
@@ -9003,6 +9241,10 @@ function makeBrittleSelectors(id, appliesTo, ext, languages, frameworks, pattern
9003
9241
  falsePositiveRisk: "medium",
9004
9242
  autofix: false,
9005
9243
  detectionStrategy: "LEXICAL",
9244
+ strategyJustification: {
9245
+ reasonCode: "string-content-defect",
9246
+ 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"
9247
+ },
9006
9248
  introduced: "0.4.0",
9007
9249
  tier: "quarantine",
9008
9250
  detectorRevision: 2,
@@ -9099,6 +9341,10 @@ const retryMaskingFamily = [defineRule({
9099
9341
  falsePositiveRisk: "medium",
9100
9342
  autofix: false,
9101
9343
  detectionStrategy: "LEXICAL",
9344
+ strategyJustification: {
9345
+ reasonCode: "runner-semantic",
9346
+ 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"
9347
+ },
9102
9348
  introduced: "0.4.0",
9103
9349
  tier: "core",
9104
9350
  run(ctx) {
@@ -9152,6 +9398,10 @@ const retryMaskingFamily = [defineRule({
9152
9398
  falsePositiveRisk: "medium",
9153
9399
  autofix: false,
9154
9400
  detectionStrategy: "LEXICAL",
9401
+ strategyJustification: {
9402
+ reasonCode: "runner-semantic",
9403
+ 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"
9404
+ },
9155
9405
  introduced: "0.4.0",
9156
9406
  tier: "extended",
9157
9407
  run(ctx) {
@@ -9826,6 +10076,237 @@ function createScanCache(root) {
9826
10076
  };
9827
10077
  }
9828
10078
  //#endregion
10079
+ //#region src/brand/tokens.ts
10080
+ /**
10081
+ * The single source of brand truth.
10082
+ *
10083
+ * Every colour, typeface and motion constant Mjölnir shows a human —
10084
+ * terminal, README SVGs, demo video, website, docs, badges — resolves to
10085
+ * a value in this file. Nothing else may define one.
10086
+ *
10087
+ * WHY THIS EXISTS. Before it, the palette existed in six independent
10088
+ * copies: `site/.vitepress/theme/styles/vars.css`, `NORSE` in
10089
+ * `src/reporter/theme.ts`, `scripts/readme-svg.ts`,
10090
+ * `scripts/video/terminal-page.ts`, `scripts/generate-readme-architecture.ts`
10091
+ * and the table in `assets/brand/README.md`. Exactly one pair of those
10092
+ * was guarded (site-doctor Check 8, doc ↔ vars.css). The unguarded edges
10093
+ * are where the shipped surfaces drifted apart: the terminal and the site
10094
+ * disagreed on six semantic roles, the architecture diagram invented its
10095
+ * own neutral ramp, and the README badges still carried a palette retired
10096
+ * two releases earlier. `scripts/brand-doctor.mjs` now checks every edge
10097
+ * against this file.
10098
+ *
10099
+ * PURITY. Pure data. No I/O, no rendering, no environment access, no
10100
+ * imports, no logic. Consumers convert (hex → ANSI triplet, hex → CSS)
10101
+ * themselves. Same reason `score-state.ts` is pure: it makes the whole
10102
+ * thing golden-testable and safe to ship inside the npm package, where
10103
+ * it costs a few hundred bytes and replaces values the package already
10104
+ * carried anyway.
10105
+ *
10106
+ * DERIVATION. The palette is the one derived from the logo in PR #20
10107
+ * (brushed steel and forge gold under an aurora, over midnight iron).
10108
+ * Where the terminal disagreed with it, the terminal converges — see
10109
+ * `PENDING_TERMINAL` below. Full rationale: `assets/brand/README.md`.
10110
+ *
10111
+ * ACCESSIBILITY. Every foreground token in `BRAND` meets WCAG AA
10112
+ * (≥ 4.5:1) against every surface token it is allowed to sit on. That is
10113
+ * not a claim, it is `brand-doctor` rule 8, which computes the ratios.
10114
+ * The weakest legal pairing is `steelDim` on `ink800` at 5.00:1.
10115
+ */
10116
+ /**
10117
+ * The two brand hues plus the neutral they sit on.
10118
+ *
10119
+ * GOLD IS SCARCE. It means forged / certified / earned / decisive — the
10120
+ * primary mark, the FORGED state, one call to action. It is not a paint
10121
+ * bucket: gold as default text, default border or default heading is a
10122
+ * brand-doctor finding, not a style choice.
10123
+ *
10124
+ * AURORA is verification energy — the secondary, and the hue that marks
10125
+ * the runtime half of the trust ladder.
10126
+ */
10127
+ const BRAND = {
10128
+ gold: "#C19A34",
10129
+ goldBright: "#E6BD57",
10130
+ goldHot: "#F4DC9C",
10131
+ /** Pressed / deepest gold — the only step dark enough to carry white. */
10132
+ goldDeep: "#A5811C",
10133
+ aurora: "#37ABBD",
10134
+ auroraBright: "#45C1D4",
10135
+ auroraCyan: "#5CBDE0",
10136
+ steel: "#C8CBCF",
10137
+ steelDim: "#8B939D"
10138
+ };
10139
+ /**
10140
+ * Midnight iron. One ramp, four steps, darkest first.
10141
+ *
10142
+ * `terminal` and `terminalBar` share one tone deliberately: the window's
10143
+ * only seam is a hairline ring and an inset shadow, never a second fill.
10144
+ * `chromeDot` is the three window dots — see the note on
10145
+ * `PENDING_TERMINAL.chromeDots` for why they are no longer red/amber/green.
10146
+ */
10147
+ const SURFACE = {
10148
+ ink950: "#0A1119",
10149
+ ink900: "#0C1420",
10150
+ ink850: "#111A29",
10151
+ ink800: "#18243A",
10152
+ /** Raised panel (cards, elevated surfaces). */
10153
+ panel: "#141F33",
10154
+ /** Soft fill (inline code, quiet chips). */
10155
+ soft: "#1A2740",
10156
+ /** Terminal body — the deepest tone, so a terminal reads as recessed. */
10157
+ terminal: "#0A1119",
10158
+ /** Terminal title bar — the same tone; the seam is shadow, not colour. */
10159
+ terminalBar: "#0A1119",
10160
+ /** The three window dots. One neutral, not a traffic light. */
10161
+ chromeDot: "#18243A"
10162
+ };
10163
+ const TEXT = {
10164
+ primary: "#EAEEF5",
10165
+ secondary: "#ABB6C6",
10166
+ muted: "#8B939D",
10167
+ /** Ink for text set ON gold (buttons, the FORGED chip). 7.17:1 on `gold`. */
10168
+ onGold: "#0A1119"
10169
+ };
10170
+ /**
10171
+ * Non-score status. `ok` is the one green in the system and it is NOT a
10172
+ * score colour — it survives only for contexts with no worthiness
10173
+ * meaning ("autofix applied", "analysis complete"). A green score would
10174
+ * say "your software is fine", which is the exact claim this product
10175
+ * refuses to make.
10176
+ */
10177
+ const STATUS = {
10178
+ ok: "#4FB477",
10179
+ info: "#5CC4E0",
10180
+ warning: "#E6BD57",
10181
+ error: "#EC6B66"
10182
+ };
10183
+ /**
10184
+ * The four ScoreState bands plus the unmeasured state. Band thresholds
10185
+ * and runes live in `src/reporter/score-state.ts`, which stays free of
10186
+ * colour — it emits a palette KEY and each surface resolves it here.
10187
+ *
10188
+ * `unmeasured` is steel-dim on purpose. UNKNOWN is a legitimate answer,
10189
+ * not a failure: colouring it red would make "we did not measure this"
10190
+ * look like "this is broken", which is precisely the dishonesty the
10191
+ * north-star law exists to prevent.
10192
+ */
10193
+ const SCORE = {
10194
+ critical: "#EC6B66",
10195
+ warning: "#E6BD57",
10196
+ trusted: "#5CC4E0",
10197
+ forged: "#F4DC9C",
10198
+ unmeasured: "#8B939D"
10199
+ };
10200
+ /**
10201
+ * E0 → E1 → E2 is a certainty ramp, and it is deliberately HUE-FREE.
10202
+ *
10203
+ * Evidence level says how sure we are, not whether the news is good. A
10204
+ * deterministic proof (E2) is a defect we are certain about — painting
10205
+ * it gold or green would read as an achievement. So certainty is carried
10206
+ * by brightness alone, and the *shape* does the real work:
10207
+ *
10208
+ * E0 open ring observation, no weight
10209
+ * E1 half-filled pattern evidence, half weight
10210
+ * E2 sealed deterministic proof, full weight
10211
+ *
10212
+ * Colour never carries this alone (R11): the geometry is the signal and
10213
+ * survives `--ascii`, `NO_COLOR` and monochrome print.
10214
+ */
10215
+ const EVIDENCE = {
10216
+ e0: "#8B939D",
10217
+ e1: "#ABB6C6",
10218
+ e2: "#EAEEF5"
10219
+ };
10220
+ /**
10221
+ * L0–L5, and the most important boundary in the product.
10222
+ *
10223
+ * L0–L2 are STATIC: the neutral steel ramp, brightening to the static
10224
+ * ceiling at L2. L3–L5 require a real run, and the hue changes to aurora
10225
+ * exactly there. The boundary is a hue break, not a gradient step,
10226
+ * because it is a change of kind and not of degree — a static-only
10227
+ * finding can never climb past L2, however confident it is.
10228
+ *
10229
+ * Every surface that draws the ladder must draw that break.
10230
+ */
10231
+ const TRUST = {
10232
+ l0: "#8B939D",
10233
+ l1: "#ABB6C6",
10234
+ l2: "#C8CBCF",
10235
+ l3: "#37ABBD",
10236
+ l4: "#45C1D4",
10237
+ l5: "#5CC4E0"
10238
+ };
10239
+ const TINT = {
10240
+ gold: {
10241
+ fill: "#F6EBCC",
10242
+ stroke: "#7A5F16",
10243
+ text: "#4A3A0E"
10244
+ },
10245
+ aurora: {
10246
+ fill: "#D9F0F4",
10247
+ stroke: "#1F6F7C",
10248
+ text: "#10353C"
10249
+ },
10250
+ error: {
10251
+ fill: "#FADEDD",
10252
+ stroke: "#A83A35",
10253
+ text: "#4E1B19"
10254
+ },
10255
+ /** The unmeasured / unknown state. Neutral, never the error tint. */
10256
+ neutral: {
10257
+ fill: "#E4E7EB",
10258
+ stroke: "#5C646E",
10259
+ text: "#262B31"
10260
+ },
10261
+ ok: {
10262
+ fill: "#DCF0E4",
10263
+ stroke: "#276B45",
10264
+ text: "#163A26"
10265
+ }
10266
+ };
10267
+ /**
10268
+ * The score bands as the badge Mjölnir itself generates renders them.
10269
+ *
10270
+ * These are DEEPER than the score tokens on purpose, and it is not a
10271
+ * style preference: shields.io sets the message text in white and gives
10272
+ * you no say in it. `score.forged` (#F4DC9C) under white text measures
10273
+ * 1.35:1 — an unreadable badge, shipped to look on-brand. The brand's
10274
+ * own deep steps put every band between 4.9 and 6.3:1.
10275
+ *
10276
+ * What they replace was worse than off-brand, it was wrong:
10277
+ *
10278
+ * band was rendered as white-on now white-on
10279
+ * ────────────────────────────────────────────────────────────────────────────
10280
+ * 0-49 `red` #dd4343 4.24 #A83A35 6.32
10281
+ * 50-79 `yellow` #d8b800 1.95 #7A5F16 6.04
10282
+ * 80-99 `important` #ea7233 ORANGE 3.02 #1F6F7C 5.80
10283
+ * 100 `success` #44bb00 GREEN 2.51 #8A6D1E 4.90
10284
+ * unmeasured `lightgrey` #939393 3.07 #5C646E 5.99
10285
+ *
10286
+ * Two of those were defects, not preferences. `success` is green, and
10287
+ * green is not a score colour here — a 100 badge said "your software is
10288
+ * fine", which is the one claim this product refuses to make. And
10289
+ * `important` is ORANGE, not the blue-family colour the code's own
10290
+ * comment claimed for eight releases: every WORTHY badge ever rendered
10291
+ * showed the trusted band in a warning colour. Nobody had resolved a
10292
+ * shields name to a value and looked.
10293
+ *
10294
+ * Values are hex without `#`, the form shields.io's endpoint takes.
10295
+ * `A83A35`, `7A5F16`, `1F6F7C` and `5C646E` are the `TINT` strokes —
10296
+ * the same deep steps the mermaid diagrams use, for the same reason.
10297
+ * `8A6D1E` is the gold the brand document already named as the light
10298
+ * FORGED gradient's start.
10299
+ */
10300
+ const BADGE_BAND = {
10301
+ critical: "A83A35",
10302
+ warning: "7A5F16",
10303
+ trusted: "1F6F7C",
10304
+ forged: "8A6D1E",
10305
+ unmeasured: "5C646E"
10306
+ };
10307
+ SURFACE.ink950, SURFACE.ink900, SURFACE.ink850, SURFACE.ink800, BRAND.steel, BRAND.steelDim, BRAND.gold, BRAND.goldBright, BRAND.goldHot, BRAND.aurora, BRAND.auroraBright, BRAND.auroraCyan;
10308
+ 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;
10309
+ //#endregion
9829
10310
  //#region src/reporter/score-state.ts
9830
10311
  const HEADLINES = {
9831
10312
  critical: "The hammer is cracked — {n} findings break its edge.",
@@ -9902,11 +10383,11 @@ function headlineFor(state, findings) {
9902
10383
  * Respects NO_COLOR and non-TTY via `palette(isTTY)` — every renderer
9903
10384
  * receives a palette and never touches process.env directly.
9904
10385
  *
9905
- * Palette: a cold northern set — frost-steel, aurora teal, Yggdrasil
9906
- * green — with amber for warnings (Mjölnir's lightning) and a
9907
- * rune-red for errors. No magenta/pink. Emitted as 24-bit truecolor
9908
- * SGR (`38;2;r;g;b`), which every modern terminal renders and which
9909
- * `shouldColorize` already gates behind TTY + !NO_COLOR.
10386
+ * Palette: resolved from `src/brand/tokens.ts`, the single source of
10387
+ * brand truth — this file defines no colour of its own. Emitted as
10388
+ * 24-bit truecolor SGR (`38;2;r;g;b`), which every modern terminal
10389
+ * renders and which `shouldColorize` already gates behind
10390
+ * TTY + !NO_COLOR.
9910
10391
  *
9911
10392
  * Symbols always accompany color (color-blind safe, R11).
9912
10393
  *
@@ -9919,53 +10400,42 @@ function headlineFor(state, findings) {
9919
10400
  * flag so output degrades to plain characters on cmd.exe/legacy
9920
10401
  * consoles that mangle box-drawing glyphs and emoji.
9921
10402
  */
9922
- /** Norse-forge palette, 24-bit truecolor. */
10403
+ /**
10404
+ * `"#RRGGBB"` → the `[r, g, b]` triplet the SGR truecolor emitter needs.
10405
+ * Lives here rather than in `src/brand/tokens.ts`, which is pure data:
10406
+ * each surface converts the canonical hex into its own colour space.
10407
+ */
10408
+ function fromHex(hex) {
10409
+ const n = Number.parseInt(hex.slice(1), 16);
10410
+ return [
10411
+ n >> 16 & 255,
10412
+ n >> 8 & 255,
10413
+ n & 255
10414
+ ];
10415
+ }
10416
+ /**
10417
+ * The terminal palette, 24-bit truecolor, resolved from
10418
+ * `src/brand/tokens.ts` — the single source of brand truth. Nothing in
10419
+ * this file may name a hex value of its own, and `brand-doctor` rule 2
10420
+ * fails if it tries.
10421
+ *
10422
+ * Every role is now the canonical token. Six of them used to be the
10423
+ * terminal's own: a frost-steel blue for headers, a teal for info, an
10424
+ * amber for warnings, a rune-red for errors, a bone white for bold and a
10425
+ * weathered stone for dim — a second palette for one product. The
10426
+ * rune-red also failed WCAG AA at 4.36:1 on this terminal's own
10427
+ * background; `STATUS.error` on the canonical ground is 6.20:1.
10428
+ */
9923
10429
  const NORSE = {
9924
- ok: [
9925
- 79,
9926
- 180,
9927
- 119
9928
- ],
9929
- info: [
9930
- 63,
9931
- 176,
9932
- 160
9933
- ],
9934
- accent: [
9935
- 138,
9936
- 180,
9937
- 216
9938
- ],
9939
- warning: [
9940
- 224,
9941
- 165,
9942
- 38
9943
- ],
9944
- error: [
9945
- 208,
9946
- 69,
9947
- 59
9948
- ],
9949
- trusted: [
9950
- 92,
9951
- 196,
9952
- 224
9953
- ],
9954
- forged: [
9955
- 244,
9956
- 220,
9957
- 156
9958
- ],
9959
- bold: [
9960
- 237,
9961
- 230,
9962
- 214
9963
- ],
9964
- dim: [
9965
- 124,
9966
- 133,
9967
- 144
9968
- ]
10430
+ ok: fromHex(STATUS.ok),
10431
+ info: fromHex(BRAND.aurora),
10432
+ accent: fromHex(BRAND.steel),
10433
+ warning: fromHex(STATUS.warning),
10434
+ error: fromHex(STATUS.error),
10435
+ trusted: fromHex(SCORE.trusted),
10436
+ forged: fromHex(SCORE.forged),
10437
+ bold: fromHex(TEXT.primary),
10438
+ dim: fromHex(TEXT.muted)
9969
10439
  };
9970
10440
  const on = {
9971
10441
  ok: rgb(NORSE.ok),
@@ -12607,16 +13077,47 @@ function countBySeverity(result) {
12607
13077
  }
12608
13078
  return counts;
12609
13079
  }
13080
+ EVIDENCE.e0, EVIDENCE.e1, EVIDENCE.e2;
13081
+ const RUNG_MEANINGS = [
13082
+ "observation only",
13083
+ "heuristic static",
13084
+ "deterministic static",
13085
+ "the finding's file executed",
13086
+ "the finding's test executed",
13087
+ "the run verdict corroborates"
13088
+ ];
13089
+ const RUNG_COLORS = [
13090
+ TRUST.l0,
13091
+ TRUST.l1,
13092
+ TRUST.l2,
13093
+ TRUST.l3,
13094
+ TRUST.l4,
13095
+ TRUST.l5
13096
+ ];
13097
+ const TRUST_RUNGS = RUNG_MEANINGS.map((meaning, i) => ({
13098
+ level: `L${i}`,
13099
+ meaning,
13100
+ runtime: i >= 3,
13101
+ color: RUNG_COLORS[i]
13102
+ }));
12610
13103
  //#endregion
12611
13104
  //#region src/reporter/trust-report.ts
12612
- const TRUST_LABELS = {
12613
- L0: "L0 · observation only",
12614
- L1: "L1 · heuristic static",
12615
- L2: "L2 · deterministic static",
12616
- L3: "L3 · file executed",
12617
- L4: "L4 · test executed",
12618
- L5: "L5 · run corroborates defect"
12619
- };
13105
+ /**
13106
+ * The rung labels, built from `src/brand/symbols.ts` rather than typed
13107
+ * again here.
13108
+ *
13109
+ * They had drifted the moment there were two copies: this file said
13110
+ * "file executed" and "run corroborates defect" where the symbol module,
13111
+ * the architecture diagram and the website's ladder all said "the
13112
+ * finding's file executed" and "the run verdict corroborates". Small
13113
+ * enough that nobody would notice, and exactly the kind of divergence
13114
+ * that makes a reader wonder whether two surfaces mean the same thing.
13115
+ *
13116
+ * The runtime marker is not decoration either: L3 and above cannot be
13117
+ * reached without a real run report, and the label says so wherever the
13118
+ * ladder is not drawn to show it.
13119
+ */
13120
+ const TRUST_LABELS = Object.fromEntries(TRUST_RUNGS.map((r) => [r.level, `${r.level} · ${r.meaning}${r.runtime ? " · runtime" : ""}`]));
12620
13121
  function pct$1(v) {
12621
13122
  return `${Math.round(v * 100)}%`;
12622
13123
  }
@@ -12752,7 +13253,7 @@ function renderSarif(result, repoRootUri) {
12752
13253
  tool: { driver: {
12753
13254
  name: "Mjölnir",
12754
13255
  informationUri: "https://github.com/Sergey-Bar/Mjolnir",
12755
- version: "1.0.1",
13256
+ version: "1.0.3",
12756
13257
  rules: [...rules.values()].map((r) => {
12757
13258
  const meta = RULES.find((x) => x.id === r.id);
12758
13259
  return {
@@ -12881,6 +13382,22 @@ function renderCodeQuality(result) {
12881
13382
  }
12882
13383
  //#endregion
12883
13384
  //#region src/reporter/mermaid.ts
13385
+ /**
13386
+ * `--format mermaid` — test-architecture diagram (Sprint 9 Task 38,
13387
+ * Master-Stabilization-Plan.md). Ranked first among the delight
13388
+ * features because it is genuinely useful to QA leads presenting scan
13389
+ * results to stakeholders, not merely decorative — a flowchart of
13390
+ * detected frameworks → rule categories → severity buckets, so gaps
13391
+ * are visible at a glance in a format that pastes directly into a
13392
+ * GitHub/GitLab markdown comment or a slide (Mermaid renders natively
13393
+ * in both).
13394
+ *
13395
+ * Score-neutral (Sprint 9's own DoD line): this is a pure alternate
13396
+ * rendering of the exact same ScanResult every other format uses — it
13397
+ * changes no scoring, no exit code, no JSON contract field. Output is
13398
+ * fully deterministic: every collection is sorted before rendering, so
13399
+ * the same ScanResult always produces byte-identical Mermaid source.
13400
+ */
12884
13401
  function sanitizeId(raw) {
12885
13402
  return raw.replace(/[^a-z0-9]/gi, "_");
12886
13403
  }
@@ -12888,6 +13405,14 @@ function sanitizeId(raw) {
12888
13405
  function escapeLabel(text) {
12889
13406
  return text.replaceAll("\"", "&quot;").replaceAll("\n", " ");
12890
13407
  }
13408
+ /**
13409
+ * One Mermaid `classDef` line from one brand tint. Every colour this
13410
+ * renderer emits comes from `src/brand/tokens.ts`; it names none of its
13411
+ * own, and `brand-doctor` rule 6 fails if it starts to.
13412
+ */
13413
+ function classDef(name, tint) {
13414
+ return ` classDef ${name} fill:${tint.fill},stroke:${tint.stroke},color:${tint.text};`;
13415
+ }
12891
13416
  function dimensionStyleClass(dim) {
12892
13417
  if (dim.score >= 80) return "healthy";
12893
13418
  if (dim.score >= 50) return "warn";
@@ -12906,8 +13431,8 @@ function renderMermaid(result) {
12906
13431
  if (result.score === null) {
12907
13432
  lines.push(` ${rootId} --> NOTESTS["No test files detected"]`);
12908
13433
  lines.push("");
12909
- lines.push(" classDef critical fill:#fee2e2,stroke:#b91c1c,color:#7f1d1d;");
12910
- lines.push(" class NOTESTS critical;");
13434
+ lines.push(classDef("unknown", TINT.neutral));
13435
+ lines.push(" class NOTESTS unknown;");
12911
13436
  return lines.join("\n");
12912
13437
  }
12913
13438
  const frameworks = [...result.frameworks].sort((a, b) => a.localeCompare(b));
@@ -12943,10 +13468,11 @@ function renderMermaid(result) {
12943
13468
  }
12944
13469
  }
12945
13470
  lines.push("");
12946
- lines.push(" classDef healthy fill:#dcfce7,stroke:#15803d,color:#14532d;");
12947
- lines.push(" classDef warn fill:#fef9c3,stroke:#a16207,color:#713f12;");
12948
- lines.push(" classDef critical fill:#fee2e2,stroke:#b91c1c,color:#7f1d1d;");
12949
- lines.push(" classDef info fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a;");
13471
+ lines.push(classDef("healthy", TINT.ok));
13472
+ lines.push(classDef("warn", TINT.gold));
13473
+ lines.push(classDef("critical", TINT.error));
13474
+ lines.push(classDef("info", TINT.aurora));
13475
+ lines.push(classDef("unknown", TINT.neutral));
12950
13476
  for (const { id, cls } of styleAssignments) lines.push(` class ${id} ${cls};`);
12951
13477
  return lines.join("\n");
12952
13478
  }
@@ -15424,17 +15950,19 @@ function sweepStaleTempFiles(dir) {
15424
15950
  * retarget fixes the historical threshold drift (the badge used
15425
15951
  * ≥90/≥75/≥50 with four bands while the reporter used ≥80/≥50).
15426
15952
  *
15427
- * Shields.io has no cyan or white-gold, so the mapping is documented
15428
- * here: trusted → `important` (blue-family, closest to aurora-cyan),
15429
- * forged → `success` (the strongest positive signal shields offers).
15430
- * The badge is a peripheral surface; ScoreState remains the truth.
15953
+ * They are brand values now, from `BADGE_BAND`, not shields.io's named
15954
+ * colors. That mapping was documented as "trusted → `important`
15955
+ * (blue-family, closest to aurora-cyan)" and was simply untrue:
15956
+ * `important` resolves to #ea7233, which is orange. Every WORTHY badge
15957
+ * rendered the trusted band in a warning colour, and `success` — the
15958
+ * 100 state — rendered green, which is not a score colour here.
15959
+ *
15960
+ * ScoreState remains the truth; the badge is still a peripheral
15961
+ * surface. But peripheral is not the same as unchecked.
15431
15962
  */
15432
15963
  function colorFor(score) {
15433
15964
  const band = deriveScoreState(score).band;
15434
- if (band === "unmeasured") return "lightgrey";
15435
- if (band === "forged") return "success";
15436
- if (band === "trusted") return "important";
15437
- return band === "warning" ? "yellow" : "red";
15965
+ return BADGE_BAND[band];
15438
15966
  }
15439
15967
  /** Build the shields.io endpoint payload from a scan result. */
15440
15968
  function buildBadge(result, commit) {
@@ -15445,7 +15973,6 @@ function buildBadge(result, commit) {
15445
15973
  label: "MJÖLNIR",
15446
15974
  message: score === null ? "no tests found" : score === 100 && errors === 0 ? "100/100 · forged" : `${score}/100 · ${errors} error${errors === 1 ? "" : "s"}`,
15447
15975
  color: colorFor(score),
15448
- namedLogo: "vitest",
15449
15976
  ...commit !== void 0 ? { commit } : {}
15450
15977
  };
15451
15978
  }
@@ -17992,6 +18519,19 @@ function loadManifest(path) {
17992
18519
  *
17993
18520
  * Exit codes reuse the frozen set: 0 healthy · 1 violations · 20 crash.
17994
18521
  */
18522
+ /** The closed reason-code set (master plan P8) — mirrored from
18523
+ * src/rules/rule.ts's StrategyReasonCode union via a runtime set so the
18524
+ * doctor check validates membership without an extra export cycle. */
18525
+ const VALID_REASON_CODES = /* @__PURE__ */ new Set([
18526
+ "shell-string-in-config",
18527
+ "exact-key-match",
18528
+ "lexical-artifact",
18529
+ "runner-semantic",
18530
+ "string-content-defect",
18531
+ "absence-aggregate",
18532
+ "family-fallback-lockstep",
18533
+ "migration-deferred-next-measurement"
18534
+ ]);
17995
18535
  /** Uniform error rendering for doctor details (Error or thrown-as-string). */
17996
18536
  function errorText(e) {
17997
18537
  return e instanceof Error ? e.message : String(e);
@@ -18030,7 +18570,8 @@ function checkFixtureFirewall(fixturesRoot) {
18030
18570
  }
18031
18571
  return check("fixture-firewall", ok ? "pass" : "fail", details);
18032
18572
  }
18033
- /** Check 2: registry sanity — IDs unique, well-formed, titles distinct. */
18573
+ /** Check 2: registry sanity — IDs unique, well-formed, titles distinct,
18574
+ * and (P8) every LEXICAL rule carries a valid depth-adjudication record. */
18034
18575
  function checkRegistry(rules = RULES) {
18035
18576
  const details = [];
18036
18577
  const ids = /* @__PURE__ */ new Set();
@@ -18045,6 +18586,19 @@ function checkRegistry(rules = RULES) {
18045
18586
  details.push(`${r.id}: duplicate registration`);
18046
18587
  }
18047
18588
  ids.add(r.id);
18589
+ if (r.detectionStrategy === "LEXICAL") {
18590
+ const j = r.strategyJustification;
18591
+ if (!j) {
18592
+ ok = false;
18593
+ details.push(`${r.id}: LEXICAL strategy without strategyJustification — declare the reason the lexical form ships (docs/DEPTH-ADJUDICATION.md)`);
18594
+ } else if (!VALID_REASON_CODES.has(j.reasonCode)) {
18595
+ ok = false;
18596
+ details.push(`${r.id}: strategyJustification.reasonCode "${String(j.reasonCode)}" is outside the closed set (${[...VALID_REASON_CODES].join(", ")})`);
18597
+ } else if (j.detail.trim().length < 20) {
18598
+ ok = false;
18599
+ details.push(`${r.id}: strategyJustification.detail is boilerplate — cite the detector's actual shape`);
18600
+ }
18601
+ }
18048
18602
  const family = r.id.split("-")[1];
18049
18603
  for (const other of rules) if (other.id !== r.id && other.title === r.title && other.id.split("-")[1] === family) {
18050
18604
  ok = false;
@@ -18732,7 +19286,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
18732
19286
  * `scripts/sync-sarif-version.cjs` on release and guarded by
18733
19287
  * `tests/version-consistency.spec.ts` locally.
18734
19288
  */
18735
- const CLI_VERSION = "1.0.1";
19289
+ const CLI_VERSION = "1.0.3";
18736
19290
  function parseArgs(argv, onError) {
18737
19291
  const args = {
18738
19292
  target: ".",
@@ -19068,8 +19622,9 @@ async function runExplainCommand(argv, io = {
19068
19622
  return 10;
19069
19623
  }
19070
19624
  try {
19071
- io.out(renderVerdictExplain(explainVerdict(resolve(jsonPath))));
19072
- return explainVerdict(resolve(jsonPath)).ok ? 0 : 10;
19625
+ const r = explainVerdict(resolve(jsonPath));
19626
+ io.out(renderVerdictExplain(r));
19627
+ return r.ok ? 0 : 10;
19073
19628
  } catch (err) {
19074
19629
  internalErrorMessage(err, io.err, argv.includes("--debug"));
19075
19630
  return 20;