mjolnir-qa 1.0.1 → 1.0.2

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/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 compareRecords(a, b) {
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(compareRecords);
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 rLine = r.line;
183
- if (rLine <= line && (match === void 0 || match.line <= rLine)) match = r;
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) {
@@ -12753,7 +13003,7 @@ function renderSarif(result, repoRootUri) {
12753
13003
  tool: { driver: {
12754
13004
  name: "Mjölnir",
12755
13005
  informationUri: "https://github.com/Sergey-Bar/Mjolnir",
12756
- version: "1.0.1",
13006
+ version: "1.0.2",
12757
13007
  rules: [...rules.values()].map((r) => {
12758
13008
  const meta = RULES.find((x) => x.id === r.id);
12759
13009
  return {
@@ -18417,6 +18667,19 @@ function loadManifest(path) {
18417
18667
  *
18418
18668
  * Exit codes reuse the frozen set: 0 healthy · 1 violations · 20 crash.
18419
18669
  */
18670
+ /** The closed reason-code set (master plan P8) — mirrored from
18671
+ * src/rules/rule.ts's StrategyReasonCode union via a runtime set so the
18672
+ * doctor check validates membership without an extra export cycle. */
18673
+ const VALID_REASON_CODES = /* @__PURE__ */ new Set([
18674
+ "shell-string-in-config",
18675
+ "exact-key-match",
18676
+ "lexical-artifact",
18677
+ "runner-semantic",
18678
+ "string-content-defect",
18679
+ "absence-aggregate",
18680
+ "family-fallback-lockstep",
18681
+ "migration-deferred-next-measurement"
18682
+ ]);
18420
18683
  /** Uniform error rendering for doctor details (Error or thrown-as-string). */
18421
18684
  function errorText(e) {
18422
18685
  return e instanceof Error ? e.message : String(e);
@@ -18455,7 +18718,8 @@ function checkFixtureFirewall(fixturesRoot) {
18455
18718
  }
18456
18719
  return check("fixture-firewall", ok ? "pass" : "fail", details);
18457
18720
  }
18458
- /** Check 2: registry sanity — IDs unique, well-formed, titles distinct. */
18721
+ /** Check 2: registry sanity — IDs unique, well-formed, titles distinct,
18722
+ * and (P8) every LEXICAL rule carries a valid depth-adjudication record. */
18459
18723
  function checkRegistry(rules = RULES) {
18460
18724
  const details = [];
18461
18725
  const ids = /* @__PURE__ */ new Set();
@@ -18470,6 +18734,19 @@ function checkRegistry(rules = RULES) {
18470
18734
  details.push(`${r.id}: duplicate registration`);
18471
18735
  }
18472
18736
  ids.add(r.id);
18737
+ if (r.detectionStrategy === "LEXICAL") {
18738
+ const j = r.strategyJustification;
18739
+ if (!j) {
18740
+ ok = false;
18741
+ details.push(`${r.id}: LEXICAL strategy without strategyJustification — declare the reason the lexical form ships (docs/DEPTH-ADJUDICATION.md)`);
18742
+ } else if (!VALID_REASON_CODES.has(j.reasonCode)) {
18743
+ ok = false;
18744
+ details.push(`${r.id}: strategyJustification.reasonCode "${String(j.reasonCode)}" is outside the closed set (${[...VALID_REASON_CODES].join(", ")})`);
18745
+ } else if (j.detail.trim().length < 20) {
18746
+ ok = false;
18747
+ details.push(`${r.id}: strategyJustification.detail is boilerplate — cite the detector's actual shape`);
18748
+ }
18749
+ }
18473
18750
  const family = r.id.split("-")[1];
18474
18751
  for (const other of rules) if (other.id !== r.id && other.title === r.title && other.id.split("-")[1] === family) {
18475
18752
  ok = false;
@@ -19157,7 +19434,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
19157
19434
  * `scripts/sync-sarif-version.cjs` on release and guarded by
19158
19435
  * `tests/version-consistency.spec.ts` locally.
19159
19436
  */
19160
- const CLI_VERSION = "1.0.1";
19437
+ const CLI_VERSION = "1.0.2";
19161
19438
  function parseArgs(argv, onError) {
19162
19439
  const args = {
19163
19440
  target: ".",
@@ -19493,8 +19770,9 @@ async function runExplainCommand(argv, io = {
19493
19770
  return 10;
19494
19771
  }
19495
19772
  try {
19496
- io.out(renderVerdictExplain(explainVerdict(resolve(jsonPath))));
19497
- return explainVerdict(resolve(jsonPath)).ok ? 0 : 10;
19773
+ const r = explainVerdict(resolve(jsonPath));
19774
+ io.out(renderVerdictExplain(r));
19775
+ return r.ok ? 0 : 10;
19498
19776
  } catch (err) {
19499
19777
  internalErrorMessage(err, io.err, argv.includes("--debug"));
19500
19778
  return 20;