mjolnir-qa 0.5.5 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -74,6 +74,17 @@ scan`. Unknown categories are a usage error (exit 10).
74
74
  - help registry gained `why`, `handoff`, `install` and the new flags;
75
75
  site/reference/cli.md documents the handoff trust model.
76
76
 
77
+ ## [0.5.6] — 2026-09-06
78
+
79
+ ### Changes since 0.5.5
80
+
81
+ - Merge pull request #39 from Sergey-Bar/eng/verification-trust-2.0
82
+ - docs: regenerate readme SVGs under the revision-2 CI-rule measurements
83
+ - docs: regenerate rule-doc occurrence tables (yarnpkg-berry corpus lane)
84
+ - fix: eslint pragma for the file-wide consumer fallback regex
85
+ - fix: rev-2 enforcement arm in QA-CI-008 was dead code + coverage arms for CI rules
86
+ - ci-family trust repair: M2 detector re-adjudication, revision 2 re-measurement, workflow corpus lane
87
+
77
88
  ## [0.5.5] — 2026-09-06
78
89
 
79
90
  ### Audit Remediation 1.0 (engine correctness + trust boundary)
package/README.md CHANGED
@@ -284,13 +284,13 @@ firewall.
284
284
 
285
285
  ### How much of this is measured
286
286
 
287
- **74 of 99 rules carry a false-positive rate measured against real OSS code** (≥ 10 hand-classified findings each; see
288
- [docs/FP-AUDIT.md](docs/FP-AUDIT.md)). The other 19 ship on the author's
287
+ **76 of 99 rules carry a false-positive rate measured against real OSS code** (≥ 10 hand-classified findings each; see
288
+ [docs/FP-AUDIT.md](docs/FP-AUDIT.md)). The other 23 ship on the author's
289
289
  estimate. Every scan footer tells you how many of the rules that _fired_
290
290
  are measured; `mjolnir rules --unmeasured` lists the ones that aren't;
291
291
  every rule's `mjolnir explain` page states its status. We publish the rate
292
292
  even when it's ugly — QA-CS-103 audits at 95% and is quarantined for it.
293
- Growing that 78 is the project's continuing work.
293
+ Growing that number is the project's continuing work.
294
294
 
295
295
  ### Rule tiers and language maturity
296
296
 
package/dist/cli.d.mts CHANGED
@@ -690,7 +690,7 @@ declare const runScan: typeof runScan$1, buildUniversalRules: typeof buildUniver
690
690
  * `scripts/sync-sarif-version.cjs` on release and guarded by
691
691
  * `tests/version-consistency.spec.ts` locally.
692
692
  */
693
- declare const CLI_VERSION = "0.5.5";
693
+ declare const CLI_VERSION = "0.5.6";
694
694
  /** A usage-error detail: the offending token, when one exists. */
695
695
  interface UsageErrorDetail {
696
696
  /** The unknown flag or rejected value (e.g. `--nope`, `loud`). */
package/dist/cli.mjs CHANGED
@@ -1766,8 +1766,10 @@ function parseWorkflow(text) {
1766
1766
  const step = s;
1767
1767
  return {
1768
1768
  ...typeof step["name"] === "string" ? { name: step["name"] } : {},
1769
+ ...typeof step["id"] === "string" ? { id: step["id"] } : {},
1769
1770
  ...typeof step["run"] === "string" ? { run: step["run"] } : {},
1770
1771
  ...typeof step["uses"] === "string" ? { uses: step["uses"] } : {},
1772
+ ...typeof step["if"] === "string" ? { if: step["if"] } : {},
1771
1773
  ...step["with"] && typeof step["with"] === "object" && !Array.isArray(step["with"]) ? { with: { ...step["with"] } } : {},
1772
1774
  ...typeof step["continue-on-error"] === "boolean" || typeof step["continue-on-error"] === "string" ? { "continue-on-error": step["continue-on-error"] } : {}
1773
1775
  };
@@ -3546,7 +3548,7 @@ const VERIFICATION_GATE_RE = new RegExp([
3546
3548
  String.raw`\bplaywright\s+test\b`,
3547
3549
  String.raw`\b(?:pytest|tox|nox)\b`,
3548
3550
  String.raw`\bpython\s+-m\s+(?:pytest|unittest)\b`,
3549
- String.raw`\bmvn\b[^\n]+\b(?:test|verify)\b`,
3551
+ String.raw`\bmvn[wd]?\b[^\n]+\b(?:test|verify)\b`,
3550
3552
  String.raw`\b(?:\./)?gradlew?\b[^\n]+\btest\b`,
3551
3553
  String.raw`\bdotnet\s+test\b`,
3552
3554
  String.raw`\bgo\s+test\b`,
@@ -3578,6 +3580,45 @@ function stepIsVerificationGate(step) {
3578
3580
  }
3579
3581
  return false;
3580
3582
  }
3583
+ /**
3584
+ * detectorRevision 2 (M2, 2026-09-04): legitimate continue-on-error shapes
3585
+ * the rev-1 detector flagged. Each class was proven by adjudication
3586
+ * (docs/FP-AUDIT.md notes, 2026-09-02):
3587
+ * 1. Re-run idiom (appsmith ci-test-playwright.yml): the gate runs under
3588
+ * continue-on-error ONLY to convert attempt 1 into a non-failing
3589
+ * outcome so a follow-up step can re-run the SAME gate; the follow-up
3590
+ * runs unconditionally and its failure fails the job — the gate still
3591
+ * blocks.
3592
+ * 2. Non-gate shapes that matched the rev-1 run-text regex: report
3593
+ * aggregation (`test --merge-reports`), test-ID collection, shard
3594
+ * rebalancing helpers — they execute no verification.
3595
+ * 3. Steps whose `if:`/name explicitly mark them advisory/non-blocking
3596
+ * inside jobs that run the real gates (vault custom-linter,
3597
+ * github-docs sync-sdk-docs).
3598
+ */
3599
+ const RERUN_OUTCOME_RE = /steps\.[\w-]+\.outcome\s*==\s*['"]?failure['"]?|steps\.[\w-]+\.conclusion\s*==\s*['"]?failure['"]?/;
3600
+ function jobReRunsTheMaskedGate(steps, maskedIndex) {
3601
+ return steps.some((s, i) => {
3602
+ if (i === maskedIndex) return false;
3603
+ const cond = typeof s?.if === "string" ? s.if : "";
3604
+ if (!RERUN_OUTCOME_RE.test(cond)) return false;
3605
+ return stepIsVerificationGate(s);
3606
+ });
3607
+ }
3608
+ /** Explicit advisory/non-verification shapes in run text. */
3609
+ const NON_GATE_RUN_RE = /--merge-reports\b|\bmerge-reports\b|\bcollect[- ]only\b|list[- ]tests\b|\brebalance\b/i;
3610
+ /**
3611
+ * Steps whose own name declares the non-blocking intent (adjudicated FP:
3612
+ * vault code-checker.yml "Check custom linters (non-blocking)" — the
3613
+ * workflow's own vocabulary marks the advisory contract, so the green
3614
+ * check hides nothing the author claimed would gate).
3615
+ */
3616
+ const NON_BLOCKING_NAME_RE = /\(\s*non-?blocking\s*\)|\[.*non-?blocking.*\]/i;
3617
+ function stepIsGateExcludingNonVerification(step) {
3618
+ if (step.name && NON_BLOCKING_NAME_RE.test(step.name)) return false;
3619
+ if (step.run && NON_GATE_RUN_RE.test(step.run)) return false;
3620
+ return stepIsVerificationGate(step);
3621
+ }
3581
3622
  const continueOnError = defineRule({
3582
3623
  id: "QA-CI-001",
3583
3624
  category: "QA-CI",
@@ -3604,13 +3645,13 @@ const continueOnError = defineRule({
3604
3645
  for (const [jobName, job] of Object.entries(jobs)) {
3605
3646
  const steps = job?.steps ?? [];
3606
3647
  if (job && job["continue-on-error"] === true) {
3607
- if (steps.some(stepIsVerificationGate)) findings.push({
3648
+ if (steps.some(stepIsGateExcludingNonVerification)) findings.push({
3608
3649
  severity: "error",
3609
3650
  confidence: "high",
3610
3651
  findingType: "deterministic-defect",
3611
3652
  qaImpact: "FALSE-GREEN",
3612
3653
  file: ctx.path,
3613
- line: findLine$4(ctx.text, new RegExp(`^\\s{2,6}${escapeRe$2(jobName)}:`, "m")),
3654
+ line: findLine$2(ctx.text, new RegExp(`^\\s{2,6}${escapeRe$1(jobName)}:`, "m")),
3614
3655
  column: 1,
3615
3656
  message: `Job \`${jobName}\` runs a verification gate under \`continue-on-error: true\`.`,
3616
3657
  why: "This job can fail every day and CI will still show green. The checkmark on this workflow cannot be trusted.",
@@ -3619,7 +3660,8 @@ const continueOnError = defineRule({
3619
3660
  }
3620
3661
  for (const [i, step] of steps.entries()) {
3621
3662
  if (!step || step["continue-on-error"] !== true) continue;
3622
- if (!stepIsVerificationGate(step)) continue;
3663
+ if (!stepIsGateExcludingNonVerification(step)) continue;
3664
+ if (jobReRunsTheMaskedGate(steps, i)) continue;
3623
3665
  findings.push({
3624
3666
  severity: "error",
3625
3667
  confidence: "high",
@@ -3681,7 +3723,7 @@ function locateStepContinueOnError(text, step) {
3681
3723
  if (anchorAt !== -1) return lineOf(text, anchorAt);
3682
3724
  return 1;
3683
3725
  }
3684
- function findLine$4(text, re) {
3726
+ function findLine$2(text, re) {
3685
3727
  const m = re.exec(text);
3686
3728
  if (!m) return 1;
3687
3729
  return lineOf(text, m.index);
@@ -3691,7 +3733,7 @@ function lineOf(text, index) {
3691
3733
  for (let i = 0; i < index; i++) if (text[i] === "\n") line++;
3692
3734
  return line;
3693
3735
  }
3694
- function escapeRe$2(s) {
3736
+ function escapeRe$1(s) {
3695
3737
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3696
3738
  }
3697
3739
  //#endregion
@@ -3716,6 +3758,7 @@ const swallowedExitCode = defineRule({
3716
3758
  detectionStrategy: "LEXICAL",
3717
3759
  introduced: "0.1.0",
3718
3760
  tier: "extended",
3761
+ detectorRevision: 2,
3719
3762
  run(ctx) {
3720
3763
  const findings = [];
3721
3764
  const re = /\|\|\s*true\b|:\s*(?:npm|yarn|pnpm|make|pytest|go)\b[^`\n]*\|\|\s*echo/g;
@@ -3767,6 +3810,7 @@ const retryMasking = defineRule({
3767
3810
  detectionStrategy: "LEXICAL",
3768
3811
  introduced: "0.1.0",
3769
3812
  tier: "extended",
3813
+ detectorRevision: 2,
3770
3814
  run(ctx) {
3771
3815
  const findings = [];
3772
3816
  const doc = ctx.ast;
@@ -3782,38 +3826,74 @@ const retryMasking = defineRule({
3782
3826
  findingType: "deterministic-defect",
3783
3827
  qaImpact: "FLAKY-RISK",
3784
3828
  file: ctx.path,
3785
- line: findLine$3(ctx.text, new RegExp(escapeRe$1(step.uses))),
3829
+ line: findStepUsesLine(ctx.text, step.uses, command),
3786
3830
  column: 1,
3787
3831
  message: `Job \`${jobName}\` wraps a test command in an automatic retry action.`,
3788
3832
  why: "Retrying tests until they pass hides flaky and intermittent failures — the green check no longer means the suite passed.",
3789
3833
  fix: "Remove the retry wrapper; investigate the underlying flakiness instead."
3790
3834
  });
3791
3835
  }
3792
- if (step?.run && /\bfor\b[\s\S]+retry|max_attempts|until.*succeed/i.test(step.run) && /test/i.test(step.run)) findings.push({
3793
- severity: "warning",
3794
- confidence: "medium",
3795
- findingType: "heuristic-risk",
3796
- qaImpact: "FLAKY-RISK",
3797
- file: ctx.path,
3798
- line: findLine$3(ctx.text, /max_attempts|until.*succeed/i),
3799
- column: 1,
3800
- message: `Job \`${jobName}\` contains a shell retry loop around tests.`,
3801
- why: "Retry-until-pass loops mask intermittent failures instead of surfacing them.",
3802
- fix: "Run tests once; track and fix flakes explicitly."
3803
- });
3836
+ if (step?.run) {
3837
+ const LOOP_RE = /\bfor\b[^\n]*\$\(\s*(?:seq|range)\b|\bfor\b[^\n]*\{\d+\.\.\d+\}|\bfor\s+\w+\s+in\s+\d+(?:[,\t ]+\d+)*[;\s]*do\b|\bwhile\b[^\n]*\btrue\b|\bwhile\s+:;|\bmax_attempts\b|\buntil\b[^\n]*\bsucceed\b/i;
3838
+ const TEST_GATE_RE = /\b(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?(?:test|t)\b|\bnpx\s+(?:vitest|jest|mocha|ava|playwright\s+test)\b|\b(?:vitest|jest|mocha|ava|tap)\b|\bplaywright\s+test\b|\b(?:pytest|tox|nox)\b|\bpython\s+-m\s+(?:pytest|unittest)\b|\bmvn[wd]?\b[^\n]+\b(?:test|verify)\b|\b(?:[.]\/)?gradlew?\b[^\n]+\btest\b|\bdotnet\s+test\b|\bgo\s+test\b|\bcargo\s+test\b|\bmake\s+[\w./\\-]*test\b/i;
3839
+ const isCurlProbe = /\b(?:curl|wget)\b/.test(step.run) && !TEST_GATE_RE.test(step.run);
3840
+ if (LOOP_RE.test(step.run) && TEST_GATE_RE.test(step.run) && !isCurlProbe) findings.push({
3841
+ severity: "warning",
3842
+ confidence: "medium",
3843
+ findingType: "heuristic-risk",
3844
+ qaImpact: "FLAKY-RISK",
3845
+ file: ctx.path,
3846
+ line: findStepLoopLine(ctx.text, step.run, LOOP_RE),
3847
+ column: 1,
3848
+ message: `Job \`${jobName}\` contains a shell retry loop around tests.`,
3849
+ why: "Retry-until-pass loops mask intermittent failures instead of surfacing them.",
3850
+ fix: "Run tests once; track and fix flakes explicitly."
3851
+ });
3852
+ }
3804
3853
  }
3805
3854
  return findings;
3806
3855
  }
3807
3856
  });
3808
- function findLine$3(text, re) {
3857
+ /**
3858
+ * Line of the loop construct belonging to THIS step.
3859
+ *
3860
+ * detectorRevision 2: anchors at the step's first run line, then finds the
3861
+ * loop construct at or after it — a file-wide LOOP_RE search reported every
3862
+ * matching step on the first loop occurrence in the file.
3863
+ */
3864
+ function findStepLoopLine(text, run, loopRe) {
3865
+ const firstLine = run.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
3866
+ const anchorAt = text.indexOf(firstLine);
3867
+ const re = new RegExp(loopRe.source, "gi");
3868
+ re.lastIndex = Math.max(0, anchorAt);
3809
3869
  const m = re.exec(text);
3810
3870
  if (!m) return 1;
3811
3871
  let line = 1;
3812
3872
  for (let i = 0; i < m.index; i++) if (text[i] === "\n") line++;
3813
3873
  return line;
3814
3874
  }
3815
- function escapeRe$1(s) {
3816
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3875
+ /**
3876
+ * Line of the retry-action `uses:` belonging to THIS step.
3877
+ *
3878
+ * detectorRevision 2: anchors at the step's own `with.command` text and
3879
+ * takes the nearest `uses:` occurrence at or before it — a file-wide search
3880
+ * reported every wrapping step on the first occurrence when several jobs
3881
+ * use the same retry action at the same version.
3882
+ */
3883
+ function findStepUsesLine(text, uses, command) {
3884
+ const needle = uses.trim();
3885
+ const firstCmdLine = command.trim().split("\n")[0]?.trim();
3886
+ const at = nearestBefore(text, Math.max(0, text.indexOf(firstCmdLine)), needle);
3887
+ if (at === -1) return 1;
3888
+ let line = 1;
3889
+ for (let i = 0; i < at; i++) if (text[i] === "\n") line++;
3890
+ return line;
3891
+ }
3892
+ /** The nearest occurrence of `needle` ending at or before `at`. */
3893
+ function nearestBefore(text, at, needle) {
3894
+ const windowStart = Math.max(0, at - 2e3);
3895
+ const rel = text.slice(windowStart, at + 1).lastIndexOf(needle);
3896
+ return rel !== -1 ? windowStart + rel : -1;
3817
3897
  }
3818
3898
  //#endregion
3819
3899
  //#region src/rules/ci/qa-ci-005-report-never-generated.ts
@@ -3825,15 +3905,76 @@ function escapeRe$1(s) {
3825
3905
  * that no previous step produces — the gate reads an empty or stale file
3826
3906
  * and passes vacuously. Product-MVP §35: "Required report not produced".
3827
3907
  */
3908
+ /**
3909
+ * Producer vocabulary for coverage reports, one regex over run text.
3910
+ *
3911
+ * detectorRevision 2 (M2, 2026-09-04): the rev-1 regex knew only
3912
+ * `vitest|jest|nyc --coverage`. Adjudication against the real corpus
3913
+ * (docs/FP-AUDIT.md rows) proved four blindness classes, each a real
3914
+ * producer the regex could not see:
3915
+ * 1. Java/JaCoCo — `mvn verify` with the jacoco-maven-plugin, Gradle
3916
+ * `jacocoTestReport`
3917
+ * 2. Python — `pytest --cov` (pytest-cov), `coverage run/combine/json/xml`
3918
+ * (coverage.py), `COVERAGE_FILE` env wiring
3919
+ * 3. Go / .NET / Rust — `go test -coverprofile`, `dotnet test --collect`,
3920
+ * `cargo llvm-cov` / `cargo tarpaulin`
3921
+ * 4. Script-name coverage — `npm run test:coverage` and friends, where the
3922
+ * coverage flag lives inside package.json, invisible from the workflow
3923
+ *
3924
+ * Known residual limitation (documented, not fixable single-file): a
3925
+ * producer in a DIFFERENT workflow (reusable workflows, artifact producers
3926
+ * from `workflow_run` jobs) or inside tox.ini / vitest.config is invisible;
3927
+ * cross-workflow consumers remain flaggable — measured rates carry those
3928
+ * verdicts (streamlit ai-test-coverage.yml class).
3929
+ */
3930
+ /**
3931
+ * Producer vocabulary for coverage reports, tested one literal at a time
3932
+ * over run/`with` text.
3933
+ *
3934
+ * detectorRevision 2 (M2, 2026-09-04): the rev-1 regex knew only
3935
+ * `vitest|jest|nyc --coverage`. Adjudication against the real corpus
3936
+ * (docs/FP-AUDIT.md rows) proved four blindness classes, each a real
3937
+ * producer the regex could not see:
3938
+ * 1. Java/JaCoCo — `mvn verify` with the jacoco-maven-plugin, Gradle
3939
+ * `jacocoTestReport`
3940
+ * 2. Python — `pytest --cov` (pytest-cov), `coverage run/combine/json/xml`
3941
+ * (coverage.py), `COVERAGE_FILE` env wiring
3942
+ * 3. Go / .NET / Rust — `go test -coverprofile`, `dotnet test --collect`,
3943
+ * `cargo llvm-cov` / `cargo tarpaulin`
3944
+ * 4. Script-name coverage — `npm run test:coverage` and friends, where the
3945
+ * coverage flag lives inside package.json, invisible from the workflow
3946
+ *
3947
+ * Known residual limitation (documented, not fixable single-file): a
3948
+ * producer in a DIFFERENT workflow (reusable workflows, artifact producers
3949
+ * from `workflow_run` jobs) or inside tox.ini / vitest.config is invisible;
3950
+ * cross-workflow consumers remain flaggable — measured rates carry those
3951
+ * verdicts (streamlit ai-test-coverage.yml class).
3952
+ */
3953
+ const COVERAGE_PRODUCERS = [
3954
+ /\b(?:npx\s+)?(?:vitest|jest|nyc)\b[^\n]*--coverage\b/i,
3955
+ /--coverage\b/i,
3956
+ /\b(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?[\w:@/.-]*coverage[\w:@/.-]*/i,
3957
+ /\bmvn[wd]?\b[^\n]*\b(?:jacoco|verify)\b/i,
3958
+ /\b(?:[.]\/)?gradlew?\b[^\n]*\bjacoco\w*\b/i,
3959
+ /\bjacoco\w*Report\b/i,
3960
+ /\bpytest\b[^\n]*--cov\b/i,
3961
+ /\bcoverage\s+(?:run|combine|json|xml|report)\b/i,
3962
+ /\bCOVERAGE_FILE\b/i,
3963
+ /\bpytest-cov\b/i,
3964
+ /\bgo\s+test\b[^\n]*-cover(?:profile|mode|age)\b/i,
3965
+ /\bdotnet\s+test\b[^\n]*(?:--collect|--coverage|collect coverage)/i,
3966
+ /\bcargo\s+(?:llvm-cov|tarpaulin)\b/i,
3967
+ /\b(?:coverage|lcov|cobertura)[\w./-]*\.(?:info|xml|json|lcov)\b/i
3968
+ ];
3828
3969
  /** Known report-consumption patterns and the commands that produce them. */
3829
3970
  const CONSUMERS = [{
3830
3971
  re: /codecov|coveralls/i,
3831
3972
  stepRe: /upload-artifact/i,
3832
- producer: /\b(?:npx\s+)?(?:vitest|jest|nyc)\b[\s\S]*--coverage|--coverage\b/i,
3973
+ producers: COVERAGE_PRODUCERS,
3833
3974
  label: "coverage artifact"
3834
3975
  }, {
3835
3976
  re: /codecov|coveralls/i,
3836
- producer: /--coverage\b|(?:vitest|jest|nyc)\b[\s\S]+coverage/i,
3977
+ producers: COVERAGE_PRODUCERS,
3837
3978
  label: "coverage upload"
3838
3979
  }];
3839
3980
  const reportNeverGenerated = defineRule({
@@ -3852,27 +3993,34 @@ const reportNeverGenerated = defineRule({
3852
3993
  detectionStrategy: "LEXICAL",
3853
3994
  introduced: "0.1.0",
3854
3995
  tier: "quarantine",
3996
+ detectorRevision: 2,
3855
3997
  run(ctx) {
3856
3998
  const findings = [];
3857
3999
  const doc = ctx.ast;
3858
4000
  if (!doc?.jobs) return findings;
3859
4001
  const jobEntries = Object.entries(doc.jobs);
3860
- const workflowRunText = jobEntries.map(([, j]) => (j?.steps ?? []).map((s) => s?.run ?? "").join("\n")).join("\n");
4002
+ const stepText = (s) => [s.run ?? "", ...s.uses?.includes("upload-artifact") ? [] : Object.values(s.with ?? {}).filter((v) => typeof v === "string")].join("\n");
4003
+ const workflowRunText = jobEntries.map(([, j]) => (j?.steps ?? []).map(stepText).join("\n")).join("\n");
3861
4004
  for (const [jobName, job] of jobEntries) {
3862
4005
  const steps = job?.steps ?? [];
3863
- const allRunText = steps.map((s) => s?.run ?? "").join("\n");
4006
+ const allRunText = steps.map(stepText).join("\n");
3864
4007
  for (const consumer of CONSUMERS) {
3865
4008
  if (!(steps.some((s) => {
3866
4009
  if (s.uses && consumer.re.test(s.uses)) return true;
3867
4010
  if (consumer.stepRe && s.uses && consumer.stepRe.test(s.uses) && s.with && typeof s.with["path"] === "string" && /coverage|lcov/i.test(s.with["path"])) return true;
3868
4011
  return false;
3869
4012
  }) || consumer.re.test(allRunText))) continue;
3870
- if (!(consumer.producer.test(allRunText) || consumer.producer.test(workflowRunText))) findings.push({
4013
+ const coverageDataDownload = steps.some((s) => s.uses?.includes("download-artifact") === true && s.with !== void 0 && [
4014
+ "pattern",
4015
+ "path",
4016
+ "name"
4017
+ ].some((k) => typeof s.with?.[k] === "string" && /coverage/i.test(s.with[k])));
4018
+ if (!(consumer.producers.some((p) => p.test(allRunText) || p.test(workflowRunText)) || coverageDataDownload)) findings.push({
3871
4019
  severity: "error",
3872
4020
  confidence: "high",
3873
4021
  findingType: "deterministic-defect",
3874
4022
  file: ctx.path,
3875
- line: findLine$2(ctx.text, consumer.re),
4023
+ line: findJobConsumerLine(ctx.text, jobName, consumer.re),
3876
4024
  column: 1,
3877
4025
  message: `Job \`${jobName}\` consumes a ${consumer.label} that no step generates.`,
3878
4026
  why: "The gate reads a report that is never produced — it passes on empty/stale data while appearing to verify something.",
@@ -3884,11 +4032,27 @@ const reportNeverGenerated = defineRule({
3884
4032
  return findings;
3885
4033
  }
3886
4034
  });
3887
- function findLine$2(text, re) {
3888
- const m = re.exec(text);
3889
- if (!m) return 1;
4035
+ /**
4036
+ * Line of the consumer signal belonging to THIS job.
4037
+ *
4038
+ * detectorRevision 2: `findLine` searched the whole file and always
4039
+ * returned the FIRST consumer occurrence — every consuming job in a
4040
+ * multi-job workflow reported the same line, so distinct findings
4041
+ * collapsed into one verdict position. Anchoring at the job's own
4042
+ * declaration keeps each job's finding on its own line.
4043
+ */
4044
+ function findJobConsumerLine(text, jobName, consumerRe) {
4045
+ const declMatch = new RegExp(`^\\s{2,6}${jobName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:`, "m").exec(text);
4046
+ const re = new RegExp(consumerRe.source, "gi");
4047
+ re.lastIndex = declMatch ? declMatch.index : 0;
4048
+ const anchored = re.exec(text);
4049
+ if (anchored) return lineOfIndex(text, anchored.index);
4050
+ const anywhere = new RegExp(consumerRe.source, "i").exec(text);
4051
+ return anywhere ? lineOfIndex(text, anywhere.index) : 1;
4052
+ }
4053
+ function lineOfIndex(text, index) {
3890
4054
  let line = 1;
3891
- for (let i = 0; i < m.index; i++) if (text[i] === "\n") line++;
4055
+ for (let i = 0; i < index; i++) if (text[i] === "\n") line++;
3892
4056
  return line;
3893
4057
  }
3894
4058
  //#endregion
@@ -3901,6 +4065,18 @@ function findLine$2(text, re) {
3901
4065
  * failure-tolerant pattern can flip a red job green — the classic
3902
4066
  * false-green trick.
3903
4067
  */
4068
+ /**
4069
+ * Failure-enforcement shapes that legitimately follow tolerated steps: a
4070
+ * later `if: always()` step that checks the recorded status and exits
4071
+ * non-zero on failure. Adjudication (hashicorp/vault build.yml:889,
4072
+ * ci.yml:492, test-go.yml:992 — 2026-09-02) proved the rev-1 detector
4073
+ * blind to this canonical honest pattern: the tolerated steps' outcomes
4074
+ * ARE enforced after them, so nothing is masked.
4075
+ */
4076
+ const ENFORCED_FAILURE_LATER = /(?:result|status)[^\n]*!=\s*['"]?success['"]?|exit\s+1\b|FAILURE|has\s*failed/i;
4077
+ function laterStepEnforcesFailure(steps, fromIndex) {
4078
+ return steps.slice(fromIndex + 1).some((s) => typeof s?.if === "string" && /always\s*\(\)/.test(s.if) && /!=\s*['"]?success['"]?|==\s*['"]?failure['"]?|result/i.test(s.if) && typeof s?.run === "string" && ENFORCED_FAILURE_LATER.test(s.run));
4079
+ }
3904
4080
  const alwaysSuccessStep = defineRule({
3905
4081
  id: "QA-CI-008",
3906
4082
  category: "QA-CI",
@@ -3917,6 +4093,7 @@ const alwaysSuccessStep = defineRule({
3917
4093
  detectionStrategy: "LEXICAL",
3918
4094
  introduced: "0.1.0",
3919
4095
  tier: "quarantine",
4096
+ detectorRevision: 2,
3920
4097
  run(ctx) {
3921
4098
  const findings = [];
3922
4099
  const doc = ctx.ast;
@@ -3926,9 +4103,11 @@ const alwaysSuccessStep = defineRule({
3926
4103
  if (steps.length < 2) continue;
3927
4104
  const last = steps[steps.length - 1];
3928
4105
  if (!last?.run) continue;
3929
- const suspicious = /^\s*exit\s+0\s*$/m.test(last.run) || /^\s*(?:echo|printf)\b[^&|;]*$/.test(last.run.trim()) || /^\s*true\s*$/.test(last.run.trim());
3930
- const earlierTolerant = steps.slice(0, -1).some((s) => s?.["continue-on-error"] === true || /\|\|\s*true\b/.test(s?.run ?? ""));
3931
- if (suspicious && earlierTolerant) findings.push({
4106
+ const suspicious = !/exit\s+1\b|!=\s*['"]?success['"]?|==\s*['"]?failure['"]?/.test(last.run) && (/^\s*exit\s+0\s*$/m.test(last.run) || /^\s*(?:echo|printf)\b[^&|;]*$/.test(last.run.trim()) || /^\s*true\s*$/.test(last.run.trim()));
4107
+ const earlierTolerantGate = steps.slice(0, -1).some((s) => (s?.["continue-on-error"] === true || /\|\|\s*true\b/.test(s?.run ?? "")) && (looksLikeVerificationGate(s?.run ?? "") || s?.uses !== void 0 && /playwright|cypress|codecov\/codecov-action/i.test(s.uses)));
4108
+ const toleratedIdx = steps.findIndex((s) => (s?.["continue-on-error"] === true || /\|\|\s*true\b/.test(s?.run ?? "")) && (looksLikeVerificationGate(s?.run ?? "") || s?.uses !== void 0 && /playwright|cypress|codecov\/codecov-action/i.test(s.uses)));
4109
+ if (toleratedIdx !== -1 && laterStepEnforcesFailure(steps, toleratedIdx)) continue;
4110
+ if (suspicious && earlierTolerantGate) findings.push({
3932
4111
  severity: "error",
3933
4112
  confidence: "high",
3934
4113
  findingType: "deterministic-defect",
@@ -3981,6 +4160,8 @@ const exitCodeNotPropagated = defineRule({
3981
4160
  detectionStrategy: "FRAMEWORK",
3982
4161
  detectionNotes: "regex pattern on parsed workflow AST",
3983
4162
  introduced: "0.4.0",
4163
+ detectorRevision: 2,
4164
+ tier: "extended",
3984
4165
  run(ctx) {
3985
4166
  const findings = [];
3986
4167
  const doc = ctx.ast;
@@ -4008,11 +4189,13 @@ const exitCodeNotPropagated = defineRule({
4008
4189
  });
4009
4190
  }
4010
4191
  if (/set\s+(?:-[A-Za-df-z]*e[A-Za-z]*|-o\s+errexit)\b/.test(run)) continue;
4192
+ const strippedRun = stripQuoted(run);
4011
4193
  const seqRe = new RegExp(`(?:${TEST_CMD$1.source})[^\\n;]*;\\s*[^\\n]+`, "g");
4012
4194
  let sm;
4013
- while ((sm = seqRe.exec(run)) !== null) {
4195
+ while ((sm = seqRe.exec(strippedRun)) !== null) {
4014
4196
  const seg = sm[0];
4015
4197
  if (/&&|\|\|/.test(seg)) continue;
4198
+ if (/^\s*(?:do|then|else|fi|done|elif|esac)\b|^\s*\}/.test(seg.slice(seg.indexOf(";") + 1))) continue;
4016
4199
  const afterSemi = seg.slice(seg.indexOf(";") + 1);
4017
4200
  if (TEST_CMD$1.test(afterSemi)) continue;
4018
4201
  findings.push({
@@ -4032,6 +4215,17 @@ const exitCodeNotPropagated = defineRule({
4032
4215
  return findings;
4033
4216
  }
4034
4217
  });
4218
+ /**
4219
+ * Replaces the CONTENT of double- and single-quoted segments with spaces
4220
+ * (length-preserving) so shell separators inside strings — a JS test file
4221
+ * echoed through `tee`, e.g. `echo "it('x'); expect(y)" | tee t.js` — can
4222
+ * never anchor a `;`-sequence finding. Quotes are shell-sensitive; this is
4223
+ * deliberately conservative: only quote-delimited, same-line segments are
4224
+ * stripped.
4225
+ */
4226
+ function stripQuoted(text) {
4227
+ return text.replace(/"[^"\n]*"|'[^'\n]*'/g, (m) => " ".repeat(m.length));
4228
+ }
4035
4229
  function findLine$1(text, needle) {
4036
4230
  const idx = text.indexOf(needle);
4037
4231
  if (idx === -1) return 1;
@@ -4057,7 +4251,19 @@ const TEST_CMD = /\b(?:npm|yarn|pnpm)\s+(?:run\s+)?test\b|\b(?:jest|vitest|pytes
4057
4251
  * `if:` conditions that skip the job on pull requests.
4058
4252
  * NOTE: `!=` only — `github.event_name == 'pull_request'` is the OPPOSITE
4059
4253
  * (run ONLY on PRs) and must never match here.
4060
- */
4254
+ *
4255
+ * detectorRevision 2 (M2, 2026-09-04): a condition that CONTAINS a
4256
+ * push-match alternative can still RUN on PRs via `||` branches —
4257
+ * `github.event_name == 'push' || ... || (pull_request && !draft)` runs on
4258
+ * non-draft PRs (adjudicated FPs: nocodb jest-unit-test.yml, grafana
4259
+ * pr-frontend-unit-tests.yml, streamlit publish jobs). The condition is a
4260
+ * skip only when EVERY PR-shaped alternative excludes PRs: i.e. the
4261
+ * condition contains a PR-inclusive branch (== pull_request, !draft,
4262
+ * labeled trigger) or consists solely of push/schedule/dispatch
4263
+ * alternatives — matched below only when no PR-inclusive alternative
4264
+ * exists anywhere in the condition.
4265
+ */
4266
+ const PR_INCLUSIVE_RE = /github\.event_name\s*==\s*['"]?pull_request\b|pull_request[^\n]*!\s*=\s*['"]?draft|!\s*github\.event\.pull_request|github\.event\.label\.name|!=\s*['"]?draft['"]?|head\.repo\.fork\s*==\s*['"]?false['"]?/;
4061
4267
  const SKIP_ON_PR = /github\.event_name\s*!=\s*['"]?pull_request|github\.event_name\s*==\s*['"]?(?:push|schedule|workflow_dispatch)\b|!\s*github\.event\b|github\.ref\s*==\s*['"]?refs\/heads\/(?:main|master)\b/;
4062
4268
  const nonBlockingTestJob = defineRule({
4063
4269
  id: "QA-CI-010",
@@ -4076,6 +4282,7 @@ const nonBlockingTestJob = defineRule({
4076
4282
  detectionNotes: "regex heuristic on parsed workflow AST",
4077
4283
  introduced: "0.4.0",
4078
4284
  tier: "quarantine",
4285
+ detectorRevision: 2,
4079
4286
  run(ctx) {
4080
4287
  const findings = [];
4081
4288
  const doc = ctx.ast;
@@ -4083,6 +4290,7 @@ const nonBlockingTestJob = defineRule({
4083
4290
  for (const [jobName, job] of Object.entries(doc.jobs)) {
4084
4291
  if (!(job?.steps ?? []).some((s) => TEST_CMD.test(s?.run ?? ""))) continue;
4085
4292
  const cond = typeof job?.if === "string" ? job.if : "";
4293
+ if (cond && PR_INCLUSIVE_RE.test(cond)) continue;
4086
4294
  if (cond && SKIP_ON_PR.test(cond)) findings.push({
4087
4295
  severity: "error",
4088
4296
  confidence: "medium",
@@ -8589,46 +8797,53 @@ function getRule(id) {
8589
8797
  //#region src/rules/measured-fp.generated.ts
8590
8798
  const MEASURED_FP = {
8591
8799
  "QA-CI-001": {
8592
- fpRate: .316,
8800
+ fpRate: .105,
8593
8801
  n: 19,
8594
8802
  detectorRevision: 2,
8595
- ciLow: .1536,
8596
- ciHigh: .5399
8803
+ ciLow: .0294,
8804
+ ciHigh: .3139
8597
8805
  },
8598
8806
  "QA-CI-002": {
8599
- fpRate: .188,
8600
- n: 16,
8601
- detectorRevision: 1,
8602
- ciLow: .0659,
8603
- ciHigh: .4301
8807
+ fpRate: .111,
8808
+ n: 18,
8809
+ detectorRevision: 2,
8810
+ ciLow: .031,
8811
+ ciHigh: .328
8604
8812
  },
8605
8813
  "QA-CI-005": {
8606
- fpRate: .824,
8607
- n: 17,
8608
- detectorRevision: 1,
8609
- ciLow: .5897,
8610
- ciHigh: .9381
8814
+ fpRate: .077,
8815
+ n: 13,
8816
+ detectorRevision: 2,
8817
+ ciLow: .0137,
8818
+ ciHigh: .3331
8611
8819
  },
8612
8820
  "QA-CI-007": {
8613
- fpRate: .273,
8821
+ fpRate: 0,
8614
8822
  n: 11,
8615
- detectorRevision: 1,
8616
- ciLow: .0975,
8617
- ciHigh: .5656
8823
+ detectorRevision: 2,
8824
+ ciLow: 0,
8825
+ ciHigh: .2588
8618
8826
  },
8619
8827
  "QA-CI-008": {
8620
- fpRate: .75,
8621
- n: 16,
8622
- detectorRevision: 1,
8623
- ciLow: .505,
8624
- ciHigh: .8982
8828
+ fpRate: .1,
8829
+ n: 10,
8830
+ detectorRevision: 2,
8831
+ ciLow: .0179,
8832
+ ciHigh: .4042
8833
+ },
8834
+ "QA-CI-009": {
8835
+ fpRate: 0,
8836
+ n: 10,
8837
+ detectorRevision: 2,
8838
+ ciLow: 0,
8839
+ ciHigh: .2775
8625
8840
  },
8626
8841
  "QA-CI-010": {
8627
- fpRate: .4,
8842
+ fpRate: .1,
8628
8843
  n: 10,
8629
- detectorRevision: 1,
8630
- ciLow: .1682,
8631
- ciHigh: .6873
8844
+ detectorRevision: 2,
8845
+ ciLow: .0179,
8846
+ ciHigh: .4042
8632
8847
  },
8633
8848
  "QA-CS-101": {
8634
8849
  fpRate: 0,
@@ -8659,18 +8874,18 @@ const MEASURED_FP = {
8659
8874
  ciHigh: .495
8660
8875
  },
8661
8876
  "QA-CS-108": {
8662
- fpRate: 1,
8663
- n: 20,
8877
+ fpRate: .87,
8878
+ n: 23,
8664
8879
  detectorRevision: 1,
8665
- ciLow: .8389,
8666
- ciHigh: 1
8880
+ ciLow: .6787,
8881
+ ciHigh: .9546
8667
8882
  },
8668
8883
  "QA-CS-110": {
8669
- fpRate: 1,
8670
- n: 20,
8884
+ fpRate: .769,
8885
+ n: 26,
8671
8886
  detectorRevision: 1,
8672
- ciLow: .8389,
8673
- ciHigh: 1
8887
+ ciLow: .5795,
8888
+ ciHigh: .8897
8674
8889
  },
8675
8890
  "QA-CS-111": {
8676
8891
  fpRate: 1,
@@ -8688,17 +8903,17 @@ const MEASURED_FP = {
8688
8903
  },
8689
8904
  "QA-JV-101": {
8690
8905
  fpRate: 0,
8691
- n: 20,
8906
+ n: 23,
8692
8907
  detectorRevision: 1,
8693
8908
  ciLow: 0,
8694
- ciHigh: .1611
8909
+ ciHigh: .1431
8695
8910
  },
8696
8911
  "QA-JV-102": {
8697
- fpRate: .3,
8698
- n: 20,
8912
+ fpRate: .261,
8913
+ n: 23,
8699
8914
  detectorRevision: 1,
8700
- ciLow: .1455,
8701
- ciHigh: .519
8915
+ ciLow: .1255,
8916
+ ciHigh: .4647
8702
8917
  },
8703
8918
  "QA-JV-103": {
8704
8919
  fpRate: .259,
@@ -8722,25 +8937,25 @@ const MEASURED_FP = {
8722
8937
  ciHigh: .301
8723
8938
  },
8724
8939
  "QA-JV-108": {
8725
- fpRate: 1,
8726
- n: 20,
8940
+ fpRate: .87,
8941
+ n: 23,
8727
8942
  detectorRevision: 1,
8728
- ciLow: .8389,
8729
- ciHigh: 1
8943
+ ciLow: .6787,
8944
+ ciHigh: .9546
8730
8945
  },
8731
8946
  "QA-JV-109": {
8732
8947
  fpRate: 0,
8733
- n: 10,
8948
+ n: 18,
8734
8949
  detectorRevision: 1,
8735
8950
  ciLow: 0,
8736
- ciHigh: .2775
8951
+ ciHigh: .1759
8737
8952
  },
8738
8953
  "QA-JV-110": {
8739
- fpRate: 1,
8740
- n: 20,
8954
+ fpRate: .833,
8955
+ n: 24,
8741
8956
  detectorRevision: 1,
8742
- ciLow: .8389,
8743
- ciHigh: 1
8957
+ ciLow: .6415,
8958
+ ciHigh: .9332
8744
8959
  },
8745
8960
  "QA-JV-111": {
8746
8961
  fpRate: 1,
@@ -8764,11 +8979,11 @@ const MEASURED_FP = {
8764
8979
  ciHigh: .4042
8765
8980
  },
8766
8981
  "QA-PW-004": {
8767
- fpRate: .375,
8768
- n: 16,
8982
+ fpRate: .429,
8983
+ n: 14,
8769
8984
  detectorRevision: 1,
8770
- ciLow: .1848,
8771
- ciHigh: .6136
8985
+ ciLow: .2138,
8986
+ ciHigh: .6741
8772
8987
  },
8773
8988
  "QA-PW-005": {
8774
8989
  fpRate: 1,
@@ -8785,11 +9000,11 @@ const MEASURED_FP = {
8785
9000
  ciHigh: .1611
8786
9001
  },
8787
9002
  "QA-PW-103": {
8788
- fpRate: 1,
8789
- n: 20,
9003
+ fpRate: .29,
9004
+ n: 69,
8790
9005
  detectorRevision: 1,
8791
- ciLow: .8389,
8792
- ciHigh: 1
9006
+ ciLow: .1962,
9007
+ ciHigh: .4057
8793
9008
  },
8794
9009
  "QA-PW-104": {
8795
9010
  fpRate: 0,
@@ -8806,11 +9021,11 @@ const MEASURED_FP = {
8806
9021
  ciHigh: 1
8807
9022
  },
8808
9023
  "QA-PW-107": {
8809
- fpRate: 1,
8810
- n: 20,
9024
+ fpRate: .952,
9025
+ n: 21,
8811
9026
  detectorRevision: 1,
8812
- ciLow: .8389,
8813
- ciHigh: 1
9027
+ ciLow: .7733,
9028
+ ciHigh: .9915
8814
9029
  },
8815
9030
  "QA-PW-108": {
8816
9031
  fpRate: 1,
@@ -8849,10 +9064,10 @@ const MEASURED_FP = {
8849
9064
  },
8850
9065
  "QA-PW-117": {
8851
9066
  fpRate: 0,
8852
- n: 20,
9067
+ n: 24,
8853
9068
  detectorRevision: 1,
8854
9069
  ciLow: 0,
8855
- ciHigh: .1611
9070
+ ciHigh: .138
8856
9071
  },
8857
9072
  "QA-PW-118": {
8858
9073
  fpRate: 1,
@@ -8883,11 +9098,11 @@ const MEASURED_FP = {
8883
9098
  ciHigh: .2425
8884
9099
  },
8885
9100
  "QA-PW-122": {
8886
- fpRate: .25,
8887
- n: 20,
9101
+ fpRate: .063,
9102
+ n: 80,
8888
9103
  detectorRevision: 1,
8889
- ciLow: .1119,
8890
- ciHigh: .4687
9104
+ ciLow: .027,
9105
+ ciHigh: .1381
8891
9106
  },
8892
9107
  "QA-PW-123": {
8893
9108
  fpRate: .455,
@@ -8904,11 +9119,11 @@ const MEASURED_FP = {
8904
9119
  ciHigh: .2775
8905
9120
  },
8906
9121
  "QA-PW-141": {
8907
- fpRate: .15,
8908
- n: 20,
9122
+ fpRate: .091,
9123
+ n: 33,
8909
9124
  detectorRevision: 1,
8910
- ciLow: .0524,
8911
- ciHigh: .3604
9125
+ ciLow: .0314,
9126
+ ciHigh: .2357
8912
9127
  },
8913
9128
  "QA-PW-142": {
8914
9129
  fpRate: .182,
@@ -8918,25 +9133,25 @@ const MEASURED_FP = {
8918
9133
  ciHigh: .477
8919
9134
  },
8920
9135
  "QA-PW-143": {
8921
- fpRate: .25,
8922
- n: 20,
9136
+ fpRate: .063,
9137
+ n: 80,
8923
9138
  detectorRevision: 1,
8924
- ciLow: .1119,
8925
- ciHigh: .4687
9139
+ ciLow: .027,
9140
+ ciHigh: .1381
8926
9141
  },
8927
9142
  "QA-PW-144": {
8928
- fpRate: .15,
8929
- n: 20,
9143
+ fpRate: .143,
9144
+ n: 21,
8930
9145
  detectorRevision: 1,
8931
- ciLow: .0524,
8932
- ciHigh: .3604
9146
+ ciLow: .0498,
9147
+ ciHigh: .3464
8933
9148
  },
8934
9149
  "QA-PW-145": {
8935
- fpRate: 1,
8936
- n: 20,
9150
+ fpRate: .5,
9151
+ n: 40,
8937
9152
  detectorRevision: 1,
8938
- ciLow: .8389,
8939
- ciHigh: 1
9153
+ ciLow: .352,
9154
+ ciHigh: .648
8940
9155
  },
8941
9156
  "QA-PW-146": {
8942
9157
  fpRate: .118,
@@ -8960,25 +9175,25 @@ const MEASURED_FP = {
8960
9175
  ciHigh: .2099
8961
9176
  },
8962
9177
  "QA-PY-003": {
8963
- fpRate: .824,
8964
- n: 17,
9178
+ fpRate: .467,
9179
+ n: 30,
8965
9180
  detectorRevision: 3,
8966
- ciLow: .5897,
8967
- ciHigh: .9381
9181
+ ciLow: .3023,
9182
+ ciHigh: .6386
8968
9183
  },
8969
9184
  "QA-PY-004": {
8970
- fpRate: .762,
8971
- n: 21,
9185
+ fpRate: .533,
9186
+ n: 30,
8972
9187
  detectorRevision: 3,
8973
- ciLow: .5491,
8974
- ciHigh: .8937
9188
+ ciLow: .3614,
9189
+ ciHigh: .6977
8975
9190
  },
8976
9191
  "QA-PY-005": {
8977
- fpRate: .158,
8978
- n: 19,
9192
+ fpRate: .13,
9193
+ n: 23,
8979
9194
  detectorRevision: 1,
8980
- ciLow: .0552,
8981
- ciHigh: .3757
9195
+ ciLow: .0454,
9196
+ ciHigh: .3213
8982
9197
  },
8983
9198
  "QA-PY-006": {
8984
9199
  fpRate: 1,
@@ -9023,39 +9238,46 @@ const MEASURED_FP = {
9023
9238
  ciHigh: .4042
9024
9239
  },
9025
9240
  "QA-PY-012": {
9026
- fpRate: .6,
9027
- n: 20,
9241
+ fpRate: .4,
9242
+ n: 30,
9028
9243
  detectorRevision: 1,
9029
- ciLow: .3866,
9030
- ciHigh: .7812
9244
+ ciLow: .2459,
9245
+ ciHigh: .5768
9031
9246
  },
9032
9247
  "QA-PY-103": {
9033
- fpRate: .1,
9034
- n: 20,
9248
+ fpRate: .08,
9249
+ n: 25,
9035
9250
  detectorRevision: 1,
9036
- ciLow: .0279,
9037
- ciHigh: .301
9251
+ ciLow: .0222,
9252
+ ciHigh: .2497
9253
+ },
9254
+ "QA-PY-105": {
9255
+ fpRate: 0,
9256
+ n: 12,
9257
+ detectorRevision: 2,
9258
+ ciLow: 0,
9259
+ ciHigh: .2425
9038
9260
  },
9039
9261
  "QA-TEST-001": {
9040
- fpRate: .632,
9041
- n: 19,
9262
+ fpRate: .6,
9263
+ n: 20,
9042
9264
  detectorRevision: 1,
9043
- ciLow: .4104,
9044
- ciHigh: .8085
9265
+ ciLow: .3866,
9266
+ ciHigh: .7812
9045
9267
  },
9046
9268
  "QA-TEST-002": {
9047
- fpRate: .65,
9048
- n: 20,
9269
+ fpRate: .619,
9270
+ n: 21,
9049
9271
  detectorRevision: 1,
9050
- ciLow: .4329,
9051
- ciHigh: .8188
9272
+ ciLow: .4088,
9273
+ ciHigh: .7925
9052
9274
  },
9053
9275
  "QA-TEST-003": {
9054
- fpRate: .85,
9055
- n: 20,
9276
+ fpRate: .218,
9277
+ n: 78,
9056
9278
  detectorRevision: 1,
9057
- ciLow: .6396,
9058
- ciHigh: .9476
9279
+ ciLow: .1408,
9280
+ ciHigh: .3216
9059
9281
  },
9060
9282
  "QA-TEST-004": {
9061
9283
  fpRate: .3,
@@ -9072,11 +9294,11 @@ const MEASURED_FP = {
9072
9294
  ciHigh: .6462
9073
9295
  },
9074
9296
  "QA-TEST-010": {
9075
- fpRate: .9,
9076
- n: 20,
9297
+ fpRate: .581,
9298
+ n: 31,
9077
9299
  detectorRevision: 1,
9078
- ciLow: .699,
9079
- ciHigh: .9721
9300
+ ciLow: .4077,
9301
+ ciHigh: .7358
9080
9302
  },
9081
9303
  "QA-TQUAL-001": {
9082
9304
  fpRate: 1,
@@ -9086,11 +9308,11 @@ const MEASURED_FP = {
9086
9308
  ciHigh: 1
9087
9309
  },
9088
9310
  "QA-TQUAL-002": {
9089
- fpRate: .85,
9090
- n: 20,
9311
+ fpRate: .531,
9312
+ n: 32,
9091
9313
  detectorRevision: 1,
9092
- ciLow: .6396,
9093
- ciHigh: .9476
9314
+ ciLow: .3645,
9315
+ ciHigh: .6913
9094
9316
  },
9095
9317
  "QA-TQUAL-009": {
9096
9318
  fpRate: .786,
@@ -9100,11 +9322,11 @@ const MEASURED_FP = {
9100
9322
  ciHigh: .9243
9101
9323
  },
9102
9324
  "QA-TQUAL-011": {
9103
- fpRate: .3,
9104
- n: 20,
9325
+ fpRate: .24,
9326
+ n: 25,
9105
9327
  detectorRevision: 1,
9106
- ciLow: .1455,
9107
- ciHigh: .519
9328
+ ciLow: .115,
9329
+ ciHigh: .4343
9108
9330
  }
9109
9331
  };
9110
9332
  //#endregion
@@ -12251,7 +12473,7 @@ function renderSarif(result, repoRootUri) {
12251
12473
  tool: { driver: {
12252
12474
  name: "Mjölnir",
12253
12475
  informationUri: "https://github.com/Sergey-Bar/Mjolnir",
12254
- version: "0.5.5",
12476
+ version: "0.5.6",
12255
12477
  rules: [...rules.values()].map((r) => {
12256
12478
  const meta = RULES.find((x) => x.id === r.id);
12257
12479
  return {
@@ -16565,7 +16787,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
16565
16787
  * `scripts/sync-sarif-version.cjs` on release and guarded by
16566
16788
  * `tests/version-consistency.spec.ts` locally.
16567
16789
  */
16568
- const CLI_VERSION = "0.5.5";
16790
+ const CLI_VERSION = "0.5.6";
16569
16791
  function parseArgs(argv, onError) {
16570
16792
  const args = {
16571
16793
  target: ".",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mjolnir-qa",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "Mjölnir — the Verification Trust Engine for QA. Audits test suites and CI pipelines, reports a worthiness score and prioritized findings.",
5
5
  "type": "module",
6
6
  "engines": {