automated-qa 0.11.0

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.
Files changed (187) hide show
  1. package/LICENSE +22 -0
  2. package/NOTICE +9 -0
  3. package/README.md +100 -0
  4. package/SKILL.md +65 -0
  5. package/agent/prebuilt/darwin-universal/qa-agent +0 -0
  6. package/agent/prebuilt/linux-arm64/qa-agent +0 -0
  7. package/agent/prebuilt/linux-x64/qa-agent +0 -0
  8. package/agent/release-manifest.json +31 -0
  9. package/bin/autonomous-cache.mjs +2 -0
  10. package/bin/bench-generate.mjs +232 -0
  11. package/bin/bench-graph-query.mjs +2 -0
  12. package/bin/bench-score.mjs +10 -0
  13. package/bin/classify.mjs +15 -0
  14. package/bin/coherence-playwright.mjs +1 -0
  15. package/bin/coherence-runner.mjs +6 -0
  16. package/bin/crud-runner.mjs +12 -0
  17. package/bin/experiment-executor.mjs +10 -0
  18. package/bin/experiment-runner.mjs +3 -0
  19. package/bin/inventory.mjs +17 -0
  20. package/bin/probe.js +879 -0
  21. package/bin/qa.mjs +16 -0
  22. package/bin/rules.mjs +5 -0
  23. package/bin/run-benchmarks.mjs +2 -0
  24. package/bin/runtime-packs.mjs +3 -0
  25. package/bin/semantic-explorer.mjs +3 -0
  26. package/bin/shadow-collector.mjs +3 -0
  27. package/bin/shadow-replay.mjs +6 -0
  28. package/bin/soak-marathon.mjs +15 -0
  29. package/bin/sweep.mjs +43 -0
  30. package/bin/validate-priors.mjs +3 -0
  31. package/bin/verify-artifacts.mjs +3 -0
  32. package/bin/verify-manifest.mjs +2 -0
  33. package/install-skill.sh +45 -0
  34. package/lib/qa/agent-availability.mjs +1 -0
  35. package/lib/qa/agent-result.mjs +1 -0
  36. package/lib/qa/analysis.mjs +5 -0
  37. package/lib/qa/atlas-coverage.mjs +11 -0
  38. package/lib/qa/atlas-v1.mjs +1 -0
  39. package/lib/qa/atlas.mjs +2 -0
  40. package/lib/qa/auth/coordinator.mjs +1 -0
  41. package/lib/qa/auth/launcher.mjs +3 -0
  42. package/lib/qa/auth/lease.mjs +1 -0
  43. package/lib/qa/auth/oracle.mjs +1 -0
  44. package/lib/qa/auth/personas.mjs +3 -0
  45. package/lib/qa/autonomous-cache.mjs +3 -0
  46. package/lib/qa/block-ledger.mjs +1 -0
  47. package/lib/qa/capture-report.mjs +63 -0
  48. package/lib/qa/ci.mjs +11 -0
  49. package/lib/qa/cli.mjs +538 -0
  50. package/lib/qa/coherence/analyze.mjs +1 -0
  51. package/lib/qa/coherence/cache-key.mjs +6 -0
  52. package/lib/qa/coherence/causes.mjs +1 -0
  53. package/lib/qa/coherence/concurrency.mjs +1 -0
  54. package/lib/qa/coherence/contracts.mjs +1 -0
  55. package/lib/qa/coherence/entity-aliases.mjs +1 -0
  56. package/lib/qa/coherence/enums.mjs +1 -0
  57. package/lib/qa/coherence/family-report.mjs +1 -0
  58. package/lib/qa/coherence/flow-pool.mjs +1 -0
  59. package/lib/qa/coherence/graph.mjs +1 -0
  60. package/lib/qa/coherence/identity.mjs +1 -0
  61. package/lib/qa/coherence/ir.mjs +4 -0
  62. package/lib/qa/coherence/navigation.mjs +1 -0
  63. package/lib/qa/coherence/observe.mjs +2 -0
  64. package/lib/qa/coherence/offline.mjs +1 -0
  65. package/lib/qa/coherence/plan-join.mjs +1 -0
  66. package/lib/qa/coherence/provenance.mjs +2 -0
  67. package/lib/qa/coherence/report.mjs +1 -0
  68. package/lib/qa/coherence/representations.mjs +1 -0
  69. package/lib/qa/coherence/risk.mjs +2 -0
  70. package/lib/qa/coherence/structural-js.mjs +11 -0
  71. package/lib/qa/coherence/symbols.mjs +6 -0
  72. package/lib/qa/coherence/tanstack.mjs +4 -0
  73. package/lib/qa/coherence/views.mjs +1 -0
  74. package/lib/qa/coherence-planner.mjs +1 -0
  75. package/lib/qa/command-catalog.mjs +2 -0
  76. package/lib/qa/commands/dev.mjs +1 -0
  77. package/lib/qa/commands/fix.mjs +1 -0
  78. package/lib/qa/commands/impl/analysis.mjs +1 -0
  79. package/lib/qa/commands/impl/atlas.mjs +1 -0
  80. package/lib/qa/commands/impl/coherence.mjs +1 -0
  81. package/lib/qa/commands/impl/crud.mjs +2 -0
  82. package/lib/qa/commands/impl/doctor.mjs +1 -0
  83. package/lib/qa/commands/impl/experiments.mjs +1 -0
  84. package/lib/qa/commands/impl/fp.mjs +1 -0
  85. package/lib/qa/commands/impl/graph.mjs +1 -0
  86. package/lib/qa/commands/impl/invariants.mjs +1 -0
  87. package/lib/qa/commands/impl/lanes.mjs +7 -0
  88. package/lib/qa/commands/impl/ledger.mjs +11 -0
  89. package/lib/qa/commands/impl/loop.mjs +1 -0
  90. package/lib/qa/commands/impl/marathon.mjs +1 -0
  91. package/lib/qa/commands/impl/next.mjs +38 -0
  92. package/lib/qa/commands/impl/report.mjs +4 -0
  93. package/lib/qa/commands/impl/research.mjs +4 -0
  94. package/lib/qa/commands/impl/selftest.mjs +1 -0
  95. package/lib/qa/commands/impl/serve.mjs +1 -0
  96. package/lib/qa/commands/impl/shadow.mjs +1 -0
  97. package/lib/qa/commands/impl/stability.mjs +1 -0
  98. package/lib/qa/commands/impl/triage.mjs +1 -0
  99. package/lib/qa/commands/impl/validate.mjs +4 -0
  100. package/lib/qa/commands/impl/version.mjs +1 -0
  101. package/lib/qa/commands/impl/watch.mjs +1 -0
  102. package/lib/qa/commands/init.mjs +6 -0
  103. package/lib/qa/commands/measure.mjs +1 -0
  104. package/lib/qa/commands/run.mjs +1 -0
  105. package/lib/qa/commands/show.mjs +1 -0
  106. package/lib/qa/commands/verify.mjs +6 -0
  107. package/lib/qa/deepening.mjs +1 -0
  108. package/lib/qa/denominators.mjs +1 -0
  109. package/lib/qa/depth.mjs +4 -0
  110. package/lib/qa/doctor.mjs +2 -0
  111. package/lib/qa/experiments-default.mjs +1 -0
  112. package/lib/qa/fix-lanes.mjs +3 -0
  113. package/lib/qa/forced-states.mjs +1 -0
  114. package/lib/qa/fp.mjs +2 -0
  115. package/lib/qa/fs.mjs +6 -0
  116. package/lib/qa/graph-schema.mjs +2 -0
  117. package/lib/qa/graph.mjs +3 -0
  118. package/lib/qa/harness-plan.mjs +3 -0
  119. package/lib/qa/init.mjs +5 -0
  120. package/lib/qa/integrity.mjs +1 -0
  121. package/lib/qa/invariants.mjs +84 -0
  122. package/lib/qa/jcs.mjs +1 -0
  123. package/lib/qa/lanes.mjs +1 -0
  124. package/lib/qa/loop.mjs +7 -0
  125. package/lib/qa/marathon-coherence.mjs +1 -0
  126. package/lib/qa/marathon-phases.mjs +1 -0
  127. package/lib/qa/marathon-wait.mjs +3 -0
  128. package/lib/qa/marathon.mjs +5 -0
  129. package/lib/qa/mode-coverage.mjs +1 -0
  130. package/lib/qa/mutation-policy.mjs +1 -0
  131. package/lib/qa/pattern-ledger.mjs +3 -0
  132. package/lib/qa/pending.mjs +1 -0
  133. package/lib/qa/proc.mjs +4 -0
  134. package/lib/qa/recommend.mjs +1 -0
  135. package/lib/qa/release-manifest.mjs +1 -0
  136. package/lib/qa/report-html.mjs +178 -0
  137. package/lib/qa/reports.mjs +2 -0
  138. package/lib/qa/request-evidence.mjs +1 -0
  139. package/lib/qa/research.mjs +20 -0
  140. package/lib/qa/routing.mjs +1 -0
  141. package/lib/qa/runs.mjs +1 -0
  142. package/lib/qa/schema.mjs +1 -0
  143. package/lib/qa/serve.mjs +4 -0
  144. package/lib/qa/server.mjs +1 -0
  145. package/lib/qa/source-snapshot.mjs +3 -0
  146. package/lib/qa/stability.mjs +3 -0
  147. package/lib/qa/state-evidence.mjs +1 -0
  148. package/lib/qa/sweep-incremental.mjs +1 -0
  149. package/lib/qa/triage.mjs +2 -0
  150. package/lib/qa/verdict.mjs +1 -0
  151. package/lib/qa/watch.mjs +2 -0
  152. package/package.json +62 -0
  153. package/references/ATLAS-COVERAGE.md +231 -0
  154. package/references/atlas-v1.md +318 -0
  155. package/references/atlas.md +338 -0
  156. package/references/verify.crud.example.json +1 -0
  157. package/references/verify.impact.example.json +1 -0
  158. package/references/verify.shadow.example.json +1 -0
  159. package/schemas/agent-result.schema.json +135 -0
  160. package/schemas/analysis.schema.json +15 -0
  161. package/schemas/classify.schema.json +61 -0
  162. package/schemas/coherence-ir.schema.json +1585 -0
  163. package/schemas/coherence-observed.schema.json +1171 -0
  164. package/schemas/coherence-plan.schema.json +77 -0
  165. package/schemas/coherence-risk.schema.json +46 -0
  166. package/schemas/crud-observed.schema.json +118 -0
  167. package/schemas/experiment-execution.schema.json +82 -0
  168. package/schemas/graph-edge.schema.json +22 -0
  169. package/schemas/graph-node.schema.json +81 -0
  170. package/schemas/graph.schema.json +2493 -0
  171. package/schemas/identity-oracle.schema.json +19 -0
  172. package/schemas/inventory.schema.json +2167 -0
  173. package/schemas/lanes-plan.schema.json +52 -0
  174. package/schemas/next.schema.json +54 -0
  175. package/schemas/personas.schema.json +52 -0
  176. package/schemas/research.schema.json +192 -0
  177. package/schemas/run.schema.json +160 -0
  178. package/schemas/shadow-replay.schema.json +68 -0
  179. package/schemas/stability.schema.json +92 -0
  180. package/schemas/sweep.schema.json +239 -0
  181. package/schemas/v1/agent-result.schema.json +106 -0
  182. package/schemas/v1/run.schema.json +96 -0
  183. package/schemas/verify-crud.schema.json +37 -0
  184. package/schemas/verify-read-only.schema.json +37 -0
  185. package/templates/github-workflow.yml +80 -0
  186. package/templates/pre-commit.sample +22 -0
  187. package/verify.sh +385 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Automated QA Binary Distribution License
2
+ Copyright 2026 Benmore Studio. All rights reserved.
3
+
4
+ Permission is granted to download, install, and use unmodified official
5
+ Automated QA release binaries and their accompanying runtime files for internal
6
+ software quality assurance.
7
+
8
+ You may not redistribute, sublicense, sell, publish, expose, or create derivative
9
+ works from the release binaries or runtime files. You may not reverse engineer,
10
+ decompile, or attempt to recover non-public source code, mathematical models,
11
+ algorithms, benchmark oracles, or other proprietary implementation material,
12
+ except to the limited extent such a restriction is prohibited by applicable law.
13
+
14
+ No permission is granted to use the Automated QA name or Benmore Studio marks to
15
+ endorse another product or service. No patent, source-code, trade-secret, or
16
+ mathematical-model rights are granted by implication or otherwise.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR
20
+ A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT
21
+ HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE
22
+ SOFTWARE OR ITS USE.
package/NOTICE ADDED
@@ -0,0 +1,9 @@
1
+ Automated QA binary distribution
2
+ Copyright 2026 Benmore Studio
3
+
4
+ This distribution includes a Node.js runtime application and a statically linked
5
+ Rust executable. Third-party Rust dependencies are distributed under their
6
+ respective permissive licenses.
7
+
8
+ No mathematical specifications, derivations, notebooks, benchmark oracles, or
9
+ private development source are included in this distribution.
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # Automated QA
2
+
3
+ [![distribution](https://github.com/Benmore-Studio/automated-qa/actions/workflows/distribution.yml/badge.svg)](https://github.com/Benmore-Studio/automated-qa/actions/workflows/distribution.yml)
4
+
5
+ Automated QA is an evidence-first frontend verification CLI. It inventories an
6
+ application, finds source-decidable defects, exercises real browser behavior,
7
+ and reports both findings and what was not measured.
8
+
9
+ This is the public **distribution repository**. It contains installation and
10
+ support material only. The private development repository, mathematical model,
11
+ derivations, notebooks, audits, benchmarks, and kernel source are not published.
12
+
13
+ ## Install
14
+
15
+ ### Homebrew
16
+
17
+ ```bash
18
+ brew install benmore-studio/benmore/qa
19
+ ```
20
+
21
+ ### npm
22
+
23
+ ```bash
24
+ npm install --global automated-qa
25
+ ```
26
+
27
+ ### Direct installer
28
+
29
+ ```bash
30
+ git clone --depth 1 https://github.com/Benmore-Studio/automated-qa.git
31
+ bash automated-qa/install.sh
32
+ ```
33
+
34
+ Requirements: macOS or Linux on x64 or arm64, Node.js 18 or newer, and Bash.
35
+ Playwright and Chromium are fetched on first runtime use. Static verification
36
+ does not need a browser.
37
+
38
+ All three methods install `qa`, `automated-qa`, and `qa-install-skill`.
39
+ The direct installer registers the skill automatically; after Homebrew or npm,
40
+ run `qa-install-skill` once.
41
+
42
+ ## Quick start
43
+
44
+ ```bash
45
+ # Static inventory and source checks
46
+ qa verify /absolute/path/to/frontend
47
+
48
+ # Add runtime browser evidence
49
+ qa verify /absolute/path/to/frontend --base http://127.0.0.1:3000
50
+
51
+ # Inspect findings and coverage gaps
52
+ qa show /absolute/path/to/frontend
53
+ qa show next /absolute/path/to/frontend
54
+ ```
55
+
56
+ Exit codes are part of the public contract:
57
+
58
+ - `0`: clean within the scope actually measured;
59
+ - `1`: measured gating findings;
60
+ - `2`: invalid, inconclusive, or required evidence could not run.
61
+
62
+ Exit `2` is never a pass. Every clean result carries its denominator.
63
+
64
+ ## What is public and private
65
+
66
+ | Public distribution | Kept private |
67
+ | --- | --- |
68
+ | Installer, sanitized skill, usage documentation, checksums | Development Git history and internal issue tracker |
69
+ | Minified operational runtime required for local execution | Readable implementation and Rust kernel source |
70
+ | Compiled macOS/Linux kernels and artifact schemas | Mathematical specifications and derivations |
71
+ | Public CLI and evidence contracts | Notebooks, figures, formal audits, tests, and benchmark oracles |
72
+
73
+ Compiled and minified software can be reverse engineered. This owner-approved
74
+ distribution boundary prevents ordinary source disclosure; it is not a claim
75
+ of cryptographic secrecy.
76
+
77
+ ## Data and safety
78
+
79
+ - The CLI runs locally; there is no hosted service receiving your repository.
80
+ - It never authorizes production mutations.
81
+ - Runtime writes require explicit opt-in, a committed non-production flow,
82
+ loopback or an explicit override, a mutation budget, fixture isolation, and
83
+ cleanup capability.
84
+ - Do not publish `.verify/capture/` screenshots without reviewing them; they may
85
+ contain application data.
86
+
87
+ ## Support
88
+
89
+ Use [GitHub Issues](https://github.com/Benmore-Studio/automated-qa/issues) for
90
+ bugs and feature requests. Do not attach credentials, cookies, tokens, request
91
+ payloads, private source, or unreviewed screenshots.
92
+
93
+ Report vulnerabilities through a
94
+ [private security advisory](https://github.com/Benmore-Studio/automated-qa/security/advisories/new).
95
+
96
+ ## License
97
+
98
+ The installer and official runtime distribution are covered by
99
+ [LICENSE](LICENSE). No readable source-code or mathematical-model license is
100
+ granted.
package/SKILL.md ADDED
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: frontend-verify
3
+ description: Verify a frontend with evidence-backed static analysis, browser measurements, coverage gaps, and focused retesting. Use for broken or stale data, cross-route consistency, pre-release QA, and sustained QA runs.
4
+ ---
5
+
6
+ # Frontend Verify
7
+
8
+ Use the installed `qa` CLI as an instrument against an application repository.
9
+ Do not run the application under test against this distribution repository.
10
+
11
+ ## Start with a bounded measurement
12
+
13
+ ```bash
14
+ qa run /absolute/path/to/app --base http://127.0.0.1:3000 --once
15
+ ```
16
+
17
+ Use `qa verify /path/to/app` for static-only evidence or add `--base URL` for
18
+ browser evidence. Run `qa help --phases` or `qa help <command>` instead of
19
+ guessing flags.
20
+
21
+ ## Full analysis and graph workflow
22
+
23
+ The public CLI runs the same analysis and compiled planning engine as the
24
+ private build. Use the generated artifacts rather than recreating its logic:
25
+
26
+ ```bash
27
+ qa init --analysis /absolute/path/to/app
28
+ qa show analysis /absolute/path/to/app
29
+ qa measure coherence /absolute/path/to/app --views
30
+ qa show graph /absolute/path/to/app --format mermaid
31
+ qa show graph /absolute/path/to/app --kind transition --format json
32
+ qa dev validate /absolute/path/to/app
33
+ ```
34
+
35
+ For a complete autonomous pass, use `qa run ... --once` or a bounded `--hours`
36
+ run. It performs preflight, source study, baseline verification, compiled
37
+ planning and scoring, coherence and invariant measurements, graph generation,
38
+ prioritized handoff, retesting, and HTML reporting. Features that require
39
+ application-owned roles, journeys, mutation flows, or authoritative readback
40
+ remain explicitly unmeasured until the application declares them.
41
+
42
+ ## Evidence contract
43
+
44
+ - Exit `0` means clean only within the measured scope.
45
+ - Exit `1` means measured gating findings.
46
+ - Exit `2` means invalid, inconclusive, or unable to run; never report it as a pass.
47
+ - Preserve every denominator and named unmeasured gap.
48
+ - Distinguish source evidence, mocks, local runtime, staging, and production.
49
+
50
+ Read `.verify/next.md` and the referenced archived run before editing. Trace the
51
+ finding to its shared root cause, inspect callers, make the smallest complete
52
+ fix, run the application's own focused checks, and rerun the affected `qa`
53
+ measurement. The host agent owns application edits and commits; `qa` measures
54
+ and hands off evidence.
55
+
56
+ ## Safety
57
+
58
+ Never mutate production. Runtime writes require the user's explicit authority,
59
+ `--mutate`, a committed flow declaring `production: false`, an authorized lane,
60
+ a positive budget, isolated fixtures, and cleanup capability.
61
+
62
+ Treat `.verify/capture/` screenshots as user data. Do not publish screenshots,
63
+ credentials, cookies, tokens, request payloads, or private source. Report the
64
+ exact command, exit code, measured denominator, remaining gaps, and evidence
65
+ location.
@@ -0,0 +1,31 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "protocolVersion": 1,
4
+ "version": "0.11.0",
5
+ "binaries": [
6
+ {
7
+ "platform": "darwin",
8
+ "arch": "arm64",
9
+ "path": "agent/prebuilt/darwin-universal/qa-agent",
10
+ "sha256": "a74c569ec262ba599d3b4ab844256a56f5ee742efc9f31167b60b40ce02de921"
11
+ },
12
+ {
13
+ "platform": "darwin",
14
+ "arch": "x64",
15
+ "path": "agent/prebuilt/darwin-universal/qa-agent",
16
+ "sha256": "a74c569ec262ba599d3b4ab844256a56f5ee742efc9f31167b60b40ce02de921"
17
+ },
18
+ {
19
+ "platform": "linux",
20
+ "arch": "x64",
21
+ "path": "agent/prebuilt/linux-x64/qa-agent",
22
+ "sha256": "b7d722022308d8eaaf446e95219f3d553bedd3e44fc4f6dc6743abdb52824a97"
23
+ },
24
+ {
25
+ "platform": "linux",
26
+ "arch": "arm64",
27
+ "path": "agent/prebuilt/linux-arm64/qa-agent",
28
+ "sha256": "03ffe42a24d484ec6036334723d31a9d988397fd5910348f2f62cd1bcde3e79e"
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import n from"node:path";import{fileURLToPath as l}from"node:url";import{tryReuseGraph as a}from"../lib/qa/autonomous-cache.mjs";const c=n.resolve(n.dirname(l(import.meta.url)),".."),p=new Set(["--base","--label","--widths"]),u=new Set(["--states","--mutate"]);function g(e){const t={repo:null,skillRoot:c,agentFlags:[]};for(let s=0;s<e.length;s+=1){const o=e[s];if(o==="--repo"){t.repo=e[++s];continue}if(o==="--skill-root"){t.skillRoot=e[++s];continue}if(p.has(o)){t.agentFlags.push(o,e[++s]);continue}if(u.has(o)){t.agentFlags.push(o);continue}throw new Error(`unknown arg: ${o}`)}if(!t.repo)throw new Error("--repo is required");return t}let r;try{r=g(process.argv.slice(2))}catch(e){console.error(`autonomous-cache: ${e.message}`),process.exit(2)}const h=n.resolve(r.repo),i=await a({skillRoot:r.skillRoot,repo:h,agentFlags:r.agentFlags});i.hit?(console.log(`hit ${i.inputFingerprint}`),process.exit(0)):(console.log(`miss ${i.reason}`),process.exit(1));
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+ import p from"node:fs";import c from"node:path";function C(e){const t={};for(let r=0;r<e.length;r++){const n=e[r];if(!n.startsWith("--"))continue;const i=n.slice(2),a=e[r+1];a!==void 0&&!a.startsWith("--")?(t[i]=a,r++):t[i]=!0}return t}const l=C(process.argv.slice(2)),f=e=>{console.error(`INVALID: ${e}`),process.exit(2)};l.seed===void 0&&f("--seed is required"),l.out===void 0&&f("--out is required");const m=Number(l.seed);Number.isInteger(m)||f("--seed must be an integer");const g=c.resolve(String(l.out)),y=l.count===void 0?5:Number(l.count);(!Number.isInteger(y)||y<1)&&f("--count must be a positive integer");const h=l["oracle-out"]!==void 0?c.resolve(String(l["oracle-out"])):c.join(c.dirname(g),"oracle");function w(e){let t=e>>>0;return function(){t+=1831565813;let n=Math.imul(t^t>>>15,1|t);return n=n+Math.imul(n^n>>>7,61|n)^n,((n^n>>>14)>>>0)/4294967296}}function E(e,t){return Math.imul(e>>>0,1000003)+t>>>0}function q(e,t){const r=e.slice();for(let n=r.length-1;n>0;n--){const i=Math.floor(t()*(n+1));[r[n],r[i]]=[r[i],r[n]]}return r}function o(e,t,r=1){let n=-1;for(let i=0;i<r;i++)if(n=e.indexOf(t,n+1),n===-1)throw new Error(`bench-generate: locate() could not find occurrence ${r} of ${JSON.stringify(t)}`);return e.slice(0,n).split(`
3
+ `).length}function L(){const e="src/plants/missing-empty-state.tsx",t=`export function UnguardedListWidget() {
4
+ const { data } = useWidgetSource()
5
+ return <ul>{(data ?? []).map((w: any, i: number) => <li key={w.id}>{w.name}</li>)}</ul>
6
+ }
7
+ function useWidgetSource() { return { data: [] as any[] } }
8
+ `;return{files:{[e]:t},expected:[{rule:"missing-empty-state",file:e,line:o(t,".map(")}]}}function F(){const e="src/plants/index-as-list-key.tsx",t=`export function IndexKeyList({ rows }: { rows: string[] }) {
9
+ return <ul>{rows.map((r, index) => <li key={index}>{r}</li>)}</ul>
10
+ }
11
+ `;return{files:{[e]:t},expected:[{rule:"index-as-list-key",file:e,line:o(t,"key={index}")}]}}function O(){const e="src/plants/runtime-env.tsx",t=`export function RuntimeEnvBadge() {
12
+ const secret = process.env.SUPER_SECRET_TOKEN
13
+ return <span>{secret}</span>
14
+ }
15
+ `;return{files:{[e]:t},expected:[{rule:"runtime-env-in-client-code",file:e,line:o(t,"process.env.SUPER_SECRET_TOKEN")}]}}function P(){const e="src/plants/server-store.ts",t=`import { create } from 'zustand'
16
+ export const useServerStore = create((set) => ({
17
+ items: [] as any[],
18
+ load: async () => { const res = await fetch('/api/server-items'); if (!res.ok) throw new Error(String(res.status)); set({ items: await res.json() }) },
19
+ }))
20
+ `;return{files:{[e]:t},expected:[{rule:"server-state-in-client-store",file:e,line:o(t,"create((set)")}]}}function j(){const e="src/plants/submit-guard.tsx",t=`export function SubmitGuardForm() {
21
+ const create = useCreateSomething()
22
+ return <form onSubmit={() => create.mutate({})}><button type="submit">Save</button></form>
23
+ }
24
+ function useCreateSomething() { return { mutate: (_: any) => {} } }
25
+ `;return{files:{[e]:t},expected:[{rule:"submit-without-pending-guard",file:e,line:o(t,"onSubmit")}]}}function N(){const e="src/plants/unread-query-error.tsx",t=`import { useQuery } from '@tanstack/react-query'
26
+ export function UnreadErrorPanel() {
27
+ const { data, isLoading } = useQuery({ queryKey: ['panelA'], queryFn: () => fetch('/api/panelA') })
28
+ if (isLoading) return <p>Loading</p>
29
+ return <p>{JSON.stringify(data)}</p>
30
+ }
31
+ `;return{files:{[e]:t},expected:[{rule:"unread-query-error",file:e,line:o(t,"useQuery(")}]}}function Q(){const e="src/plants/unread-query-loading.tsx",t=`import { useQuery } from '@tanstack/react-query'
32
+ export function UnreadLoadingPanel() {
33
+ const { data, isError } = useQuery({ queryKey: ['panelB'], queryFn: () => fetch('/api/panelB') })
34
+ if (isError) return <p>failed</p>
35
+ return <p>{JSON.stringify(data)}</p>
36
+ }
37
+ `;return{files:{[e]:t},expected:[{rule:"unread-query-loading",file:e,line:o(t,"useQuery(")}]}}function M(){const e="src/plants/derived-state.tsx",t=`import { useState } from 'react'
38
+ export function DerivedNameField(props: { initialName: string }) {
39
+ const [initialName] = useState(props.initialName)
40
+ return <span>{initialName}</span>
41
+ }
42
+ `;return{files:{[e]:t},expected:[{rule:"derived-state-in-useState",file:e,line:o(t,"useState(props.initialName")}]}}function U(){const e="src/plants/effect-fetch-no-abort.tsx",t=`import { useEffect } from 'react'
43
+ export function FetchNoAbort() {
44
+ useEffect(() => {
45
+ fetch('/api/pingback').then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
46
+ }, [])
47
+ return <span>ok</span>
48
+ }
49
+ `;return{files:{[e]:t},expected:[{rule:"effect-fetch-without-abort",file:e,line:o(t,"useEffect(")}]}}function K(){const e="src/plants/stale-closure.tsx",t=`import { useEffect, useState } from 'react'
50
+ export function StaleCounter() {
51
+ const [count, setCount] = useState(0)
52
+ useEffect(() => {
53
+ const t = setInterval(() => console.log(count), 1000)
54
+ return () => clearInterval(t)
55
+ }, [])
56
+ return <span>{count}</span>
57
+ }
58
+ `;return{files:{[e]:t},expected:[{rule:"stale-closure",file:e,line:o(t,"useEffect(")}]}}function W(){const e="src/plants/unstable-context.tsx",t=`import { createContext } from 'react'
59
+ const ThemeCtx = createContext({})
60
+ export function ThemeWrap({ children }: { children: any }) {
61
+ return <ThemeCtx.Provider value={{ mode: 'dark' }}>{children}</ThemeCtx.Provider>
62
+ }
63
+ `;return{files:{[e]:t},expected:[{rule:"unstable-context-value",file:e,line:o(t,".Provider value=")}]}}function T(){const e="src/plants/external-store.ts",t=`export function bindExternalStore(store: { subscribe: (f: () => void) => void; getState: () => unknown }, render: (s: unknown) => void) {
64
+ store.subscribe(() => render(store.getState()))
65
+ }
66
+ `;return{files:{[e]:t},expected:[{rule:"external-store-without-sync",file:e,line:o(t,"subscribe(")}]}}function B(){const e="src/plants/unsanitized-html.tsx",t=`export function RawHtmlBlock({ html }: { html: string }) {
67
+ return <div dangerouslySetInnerHTML={{ __html: html }} />
68
+ }
69
+ `;return{files:{[e]:t},expected:[{rule:"unsanitized-html",file:e,line:o(t,"dangerouslySetInnerHTML")}]}}function I(){const e="src/plants/proto-pollution.ts",t=`export function applyRemoteConfig(config: Record<string, unknown>, raw: string) {
70
+ Object.assign(config, JSON.parse(raw))
71
+ }
72
+ `;return{files:{[e]:t},expected:[{rule:"prototype-pollution-shape",file:e,line:o(t,"Object.assign(")}]}}function A(){const e="app/nb-error/page.tsx",t=`export default async function NbErrorPage() {
73
+ const res = await fetch('https://example.test/api/data')
74
+ const data = await res.json()
75
+ return <p>{JSON.stringify(data)}</p>
76
+ }
77
+ `,r="app/nb-error/loading.tsx",n=`export default function Loading() { return <p>Loading...</p> }
78
+ `;return{files:{[e]:t,[r]:n},expected:[{rule:"no-error-boundary",file:e,line:1}]}}function _(){const e="app/nb-loading/page.tsx",t=`export default async function NbLoadingPage() {
79
+ const res = await fetch('https://example.test/api/data2')
80
+ const data = await res.json()
81
+ return <p>{JSON.stringify(data)}</p>
82
+ }
83
+ `,r="app/nb-loading/error.tsx",n=`'use client'
84
+ export default function ErrorBoundary() { return <p>Something broke</p> }
85
+ `;return{files:{[e]:t,[r]:n},expected:[{rule:"no-loading-boundary",file:e,line:1}]}}function R(){const e="src/plants/sync-no-invalidation/api.ts",t=`import { useMutation, useQuery } from '@tanstack/react-query'
86
+ import axios from 'axios'
87
+ export function useGadgets() {
88
+ return useQuery({ queryKey: ['gadgets'], queryFn: () => axios.get('/api/gadgets') })
89
+ }
90
+ export function useCreateGadget() {
91
+ return useMutation({ mutationFn: (body: unknown) => axios.post('/api/gadgets', body) })
92
+ }
93
+ `,r="app/sync-no-invalidation/page.tsx",n=`'use client'
94
+ import { useGadgets, useCreateGadget } from '@/src/plants/sync-no-invalidation/api'
95
+ export default function GadgetsPage() {
96
+ const { data, isError, isLoading } = useGadgets()
97
+ const create = useCreateGadget()
98
+ if (isLoading) return <p>Loading</p>
99
+ if (isError) return <p>failed</p>
100
+ return <button disabled={create.isPending} onClick={() => create.mutate({})}>Add {String(!!data)}</button>
101
+ }
102
+ `;return{files:{[e]:t,[r]:n},expected:[{kind:"no-invalidation",file:e,line:o(t,"useMutation(")},{coherence:"no-invalidation-candidate",file:e,line:o(t,"useMutation(")}]}}function D(){const e="src/plants/sync-partial-invalidation/api.ts",t=`import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
103
+ import axios from 'axios'
104
+ export function useBudgets() {
105
+ return useQuery({ queryKey: ['budgets'], queryFn: () => axios.get('/api/budgets') })
106
+ }
107
+ export function useUpdateBudget() {
108
+ const qc = useQueryClient()
109
+ return useMutation({
110
+ mutationFn: (body: unknown) => axios.put('/api/budgets', body),
111
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['unrelated-thing'] }),
112
+ })
113
+ }
114
+ `,r="app/sync-partial-invalidation/page.tsx",n=`'use client'
115
+ import { useBudgets, useUpdateBudget } from '@/src/plants/sync-partial-invalidation/api'
116
+ export default function BudgetsPage() {
117
+ const { data, isError, isLoading } = useBudgets()
118
+ const update = useUpdateBudget()
119
+ if (isLoading) return <p>Loading</p>
120
+ if (isError) return <p>failed</p>
121
+ return <button disabled={update.isPending} onClick={() => update.mutate({})}>Update {String(!!data)}</button>
122
+ }
123
+ `;return{files:{[e]:t,[r]:n},expected:[{kind:"partial-invalidation",file:e,line:o(t,"useMutation(")},{coherence:"partial-invalidation-candidate",file:e,line:o(t,"useMutation(")}]}}function G(){const e="src/plants/coherence-literal-key-covers/api.ts",t=`import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
124
+ import axios from 'axios'
125
+ export function useWidgets() {
126
+ return useQuery({ queryKey: ['widgets'], queryFn: () => axios.get('/api/widgets') })
127
+ }
128
+ export function useUpdateWidget() {
129
+ const qc = useQueryClient()
130
+ return useMutation({
131
+ mutationFn: (body: unknown) => axios.put('/api/widgets', body),
132
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['widgets'] }),
133
+ })
134
+ }
135
+ `,r="app/coherence-literal-key-covers/page.tsx",n=`'use client'
136
+ import { useWidgets, useUpdateWidget } from '@/src/plants/coherence-literal-key-covers/api'
137
+ export default function WidgetsPage() {
138
+ const { data, isError, isLoading } = useWidgets()
139
+ const update = useUpdateWidget()
140
+ if (isLoading) return <p>Loading</p>
141
+ if (isError) return <p>failed</p>
142
+ return <button disabled={update.isPending} onClick={() => update.mutate({})}>Update {String(!!data)}</button>
143
+ }
144
+ `;return{files:{[e]:t,[r]:n},expected:[]}}function J(){const e="src/plants/coherence-key-factory-unresolved/api.ts",t=`import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
145
+ import axios from 'axios'
146
+ import { projectKeys } from 'shared-query-keys'
147
+ export function useProject(id: string) {
148
+ return useQuery({ queryKey: projectKeys.detail(id), queryFn: () => axios.get('/api/projects') })
149
+ }
150
+ export function useUpdateProject() {
151
+ const qc = useQueryClient()
152
+ return useMutation({
153
+ mutationFn: (body: unknown) => axios.put('/api/projects', body),
154
+ onSuccess: () => qc.invalidateQueries({ queryKey: projectKeys.detail(body.id) }),
155
+ })
156
+ }
157
+ `,r="app/coherence-key-factory-unresolved/page.tsx",n=`'use client'
158
+ import { useProject, useUpdateProject } from '@/src/plants/coherence-key-factory-unresolved/api'
159
+ export default function ProjectPage() {
160
+ const { data, isError, isLoading } = useProject('p1')
161
+ const update = useUpdateProject()
162
+ if (isLoading) return <p>Loading</p>
163
+ if (isError) return <p>failed</p>
164
+ return <button disabled={update.isPending} onClick={() => update.mutate({})}>Update {String(!!data)}</button>
165
+ }
166
+ `;return{files:{[e]:t,[r]:n},expected:[]}}function H(){const e="src/plants/coherence-exact-prefix-miss/api.ts",t=`import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
167
+ import axios from 'axios'
168
+ export function useTask(id: string) {
169
+ return useQuery({ queryKey: ['tasks', id], queryFn: () => axios.get('/api/tasks') })
170
+ }
171
+ export function useUpdateTask() {
172
+ const qc = useQueryClient()
173
+ return useMutation({
174
+ mutationFn: (body: unknown) => axios.put('/api/tasks', body),
175
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['tasks'], exact: true }),
176
+ })
177
+ }
178
+ `,r="app/coherence-exact-prefix-miss/page.tsx",n=`'use client'
179
+ import { useTask, useUpdateTask } from '@/src/plants/coherence-exact-prefix-miss/api'
180
+ export default function TaskPage() {
181
+ const { data, isError, isLoading } = useTask('t1')
182
+ const update = useUpdateTask()
183
+ if (isLoading) return <p>Loading</p>
184
+ if (isError) return <p>failed</p>
185
+ return <button disabled={update.isPending} onClick={() => update.mutate({})}>Update {String(!!data)}</button>
186
+ }
187
+ `;return{files:{[e]:t,[r]:n},expected:[{coherence:"partial-invalidation-candidate",file:e,line:o(t,"useMutation(")}]}}function $(){const e="src/plants/coherence-shared-component/api.ts",t=`import { useMutation, useQuery } from '@tanstack/react-query'
188
+ import axios from 'axios'
189
+ export function useOrders() {
190
+ return useQuery({ queryKey: ['orders'], queryFn: () => axios.get('/api/orders') })
191
+ }
192
+ export function useCreateOrder() {
193
+ return useMutation({ mutationFn: (body: unknown) => axios.post('/api/orders', body) })
194
+ }
195
+ `,r="src/plants/coherence-shared-component/OrdersWidget.tsx",n=`'use client'
196
+ import { useOrders, useCreateOrder } from './api'
197
+ export function OrdersWidget() {
198
+ const { data, isError, isLoading } = useOrders()
199
+ const create = useCreateOrder()
200
+ if (isLoading) return <p>Loading</p>
201
+ if (isError) return <p>failed</p>
202
+ return <button disabled={create.isPending} onClick={() => create.mutate({})}>Add {String(!!data)}</button>
203
+ }
204
+ `,i="app/coherence-shared-a/page.tsx",a=`import { OrdersWidget } from '@/src/plants/coherence-shared-component/OrdersWidget'
205
+ export default function SharedA() { return <OrdersWidget /> }
206
+ `,d="app/coherence-shared-b/page.tsx",s=`import { OrdersWidget } from '@/src/plants/coherence-shared-component/OrdersWidget'
207
+ export default function SharedB() { return <OrdersWidget /> }
208
+ `;return{files:{[e]:t,[r]:n,[i]:a,[d]:s},expected:[{kind:"no-invalidation",file:e,line:o(t,"useMutation(")},{coherence:"no-invalidation-candidate",file:e,line:o(t,"useMutation(")}]}}const z=[G,J,H,$];function V(){return{"src/plants/decoy-chart-style.tsx":`export function ChartStyleDecoy({ css }: { css: string }) {
209
+ return <style dangerouslySetInnerHTML={{ __html: css }} />
210
+ }
211
+ `}}function X(){return{"src/_proto/decoy-lab.jsx":`export function DecoyLab({ rows }) {
212
+ return <ul>{rows.map((r, index) => <li key={index}>{r}</li>)}</ul>
213
+ }
214
+ `}}function Y(){return{"src/plants/decoy-online-badge.tsx":`import { useEffect, useState } from 'react'
215
+ export function OnlineBadgeDecoy() {
216
+ const [online, setOnline] = useState(true)
217
+ useEffect(() => {
218
+ const update = () => setOnline(navigator.onLine)
219
+ window.addEventListener('online', update)
220
+ window.addEventListener('offline', update)
221
+ return () => {
222
+ window.removeEventListener('online', update)
223
+ window.removeEventListener('offline', update)
224
+ }
225
+ }, [])
226
+ return <span>{online ? 'on' : 'off'}</span>
227
+ }
228
+ `}}const b=[["missing-empty-state",L],["index-as-list-key",F],["runtime-env-in-client-code",O],["server-state-in-client-store",P],["submit-without-pending-guard",j],["unread-query-error",N],["unread-query-loading",Q],["derived-state-in-useState",M],["effect-fetch-without-abort",U],["stale-closure",K],["unstable-context-value",W],["external-store-without-sync",T],["unsanitized-html",B],["prototype-pollution-shape",I],["no-error-boundary",A],["no-loading-boundary",_],["sync-risk:no-invalidation",R],["sync-risk:partial-invalidation",D]],S=4;function Z(){return{"package.json":JSON.stringify({name:"bench-fixture",private:!0,dependencies:{next:"14.0.0",react:"18.0.0"},scripts:{build:"next build",start:"next start"}},null,2)+`
229
+ `,"tsconfig.json":JSON.stringify({compilerOptions:{baseUrl:".",paths:{"@/*":["./*"]}}},null,2)+`
230
+ `,"app/page.tsx":`export default function Home() { return <main><h1>Home</h1></main> }
231
+ `}}function ee(e,t){const r=q(b.map(([s])=>s),t),n=S+Math.floor(t()*(b.length-S+1)),i=new Set(r.slice(0,n)),a={...Z(),...V(),...X(),...Y()},d=[];for(const[s,u]of b){if(!i.has(s))continue;const{files:x,expected:k}=u();Object.assign(a,x),d.push(...k)}for(const s of z){const{files:u,expected:x}=s();Object.assign(a,u),d.push(...x)}return d.sort((s,u)=>(s.rule??s.kind??s.coherence).localeCompare(u.rule??u.kind??u.coherence)||s.file.localeCompare(u.file)||s.line-u.line),{name:e,files:a,expected:d,plantedKinds:[...i].sort()}}function te(e,t){for(const[r,n]of Object.entries(t)){const i=c.join(e,r);p.mkdirSync(c.dirname(i),{recursive:!0}),p.writeFileSync(i,n)}}p.mkdirSync(g,{recursive:!0}),p.mkdirSync(h,{recursive:!0});const v={};for(let e=0;e<y;e++){const t=`fixture-${String(e).padStart(2,"0")}`,r=w(E(m,e)),n=ee(t,r),i=c.join(g,t);p.rmSync(i,{recursive:!0,force:!0}),te(i,n.files),v[t]={expected:n.expected,plantedKinds:n.plantedKinds}}const ne={judge_only:!0,seed:m,generated:null,fixtures:v};p.writeFileSync(c.join(h,"oracle.json"),JSON.stringify(ne,null,2)+`
232
+ `),console.log(`bench-generate: wrote ${y} fixture(s) to ${g}`),console.log(`bench-generate: oracle -> ${c.join(h,"oracle.json")} (judge_only, keep out of proposer reach)`),process.exit(0);
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{performance as u}from"node:perf_hooks";import{neighborhood as b}from"../lib/qa/graph-schema.mjs";const g=process.argv.indexOf("--samples"),l=g===-1?7:Number(process.argv[g+1]);(!Number.isSafeInteger(l)||l<3)&&(console.error("--samples must be an integer >= 3"),process.exit(2));const d=o=>o.toString(16).padStart(64,"0");function _(o){const t=Array.from({length:o},(n,s)=>({recordId:d(s+1),kind:"route",attrs:{template:`/${s}`}})),r=Array.from({length:o-1},(n,s)=>({edgeId:d(o+s+1),from:d(s+1),to:d(s+2),relation:"transitions-to"}));return{nodes:t,edges:r}}function k(o,t){const r=[{recordId:d(1),kind:"route",attrs:{template:"/root"}}],n=[];let s=[1],a=2,p=1e6;for(let m=0;m<t;m++){const f=[];for(const h of s)for(let e=0;e<o;e++){const c=a++;f.push(c),r.push({recordId:d(c),kind:"route",attrs:{template:`/${c}`}}),n.push({edgeId:d(p++),from:d(h),to:d(c),relation:"transitions-to"})}s=f}return{nodes:r,edges:n}}function w({nodes:o,edges:t},{around:r,depth:n,limit:s=1e7}){const a=new Set([r]),p=new Map(o.map(e=>[e.recordId,e]));for(let e=0;e<n;e++){const c=new Set;for(const i of t)a.has(i.from)&&!a.has(i.to)&&c.add(i.to),a.has(i.to)&&!a.has(i.from)&&c.add(i.from);if(c.size===0)break;for(const i of c)p.has(i)&&a.add(i)}const m=o.filter(e=>a.has(e.recordId)).slice(0,s).map(e=>({recordId:e.recordId,kind:e.kind,attrs:{...e.attrs}})),f=new Set(m.map(e=>e.recordId)),h=t.filter(e=>f.has(e.from)&&f.has(e.to)).slice(0,s).map(e=>({edgeId:e.edgeId,from:e.from,to:e.to,relation:e.relation}));return{nodes:m,edges:h}}function y(o){return[...o].sort((t,r)=>t-r)[Math.floor(o.length/2)]}function I(o){const t=[];for(let r=0;r<l;r++){const n=u.now();o(),t.push(u.now()-n)}return{samples_ms:t.map(r=>Number(r.toFixed(2))),median_ms:Number(y(t).toFixed(2))}}const N=[{name:"chain-100k-depth-600",graph:_(1e5),depth:600},{name:"branch-10x5",graph:k(10,5),depth:5}],S=N.map(({name:o,graph:t,depth:r})=>{const n={around:d(1),depth:r,limit:1e7},s=I(()=>w(t,n)),a=I(()=>b(t,n));return{name:o,nodes:t.nodes.length,edges:t.edges.length,depth:r,legacy:s,adjacency:a,median_speedup:Number((s.median_ms/a.median_ms).toFixed(2))}});console.log(JSON.stringify({samples:l,node:process.version,workloads:S},null,2));
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import S from"node:fs";import f from"node:path";import{spawnSync as K}from"node:child_process";import{fileURLToPath as W}from"node:url";function D(e){const n={};for(let s=0;s<e.length;s++){const r=e[s];if(!r.startsWith("--"))continue;const o=r.slice(2),t=e[s+1];t!==void 0&&!t.startsWith("--")?(n[o]=t,s++):n[o]=!0}return n}const c=D(process.argv.slice(2)),l=e=>{console.error(`INVALID: ${e}`),process.exit(2)};c.corpus||l("--corpus is required"),c.oracle||l("--oracle is required");const I=f.resolve(String(c.corpus)),O=f.resolve(String(c.oracle)),C=c.skill?f.resolve(String(c.skill)):f.dirname(f.dirname(W(import.meta.url))),y=c["min-precision"]!==void 0?Number(c["min-precision"]):.95,b=c["min-recall"]!==void 0?Number(c["min-recall"]):.9,U=!!c.json;(!Number.isFinite(y)||!Number.isFinite(b))&&l("--min-precision / --min-recall must be numbers");const P=f.join(C,"bin","inventory.mjs"),E=f.join(C,"bin","classify.mjs");for(const[e,n]of[["inventory.mjs",P],["classify.mjs",E]])S.existsSync(n)||l(`--skill does not resolve to a repo with bin/${e} (looked at ${n})`);let u;try{u=JSON.parse(S.readFileSync(O,"utf8"))}catch(e){l(`cannot read/parse --oracle ${O}: ${e.message}`)}u?.judge_only!==!0&&l("oracle must declare judge_only: true"),(!u.fixtures||typeof u.fixtures!="object")&&l("oracle.fixtures must be an object");const m=Object.keys(u.fixtures);m.length||l("oracle has no fixtures");for(const e of m){const n=u.fixtures[e];Array.isArray(n?.expected)||l(`oracle fixture "${e}" has no expected[] array`);for(const s of n.expected)(typeof(s.rule??s.kind??s.coherence)!="string"||typeof s.file!="string"||!Number.isInteger(s.line))&&l(`oracle fixture "${e}" has a malformed expected entry: ${JSON.stringify(s)}`)}const $=(e,n,s,r)=>`${e}|${n}|${s}|${r}`;function J(e,n){const s=K(process.execPath,[e,...n],{encoding:"utf8",maxBuffer:67108864});if(s.error)return{ok:!1,reason:`spawn failed: ${s.error.message}`};if(s.status===null)return{ok:!1,reason:`killed by signal ${s.signal}`};if(s.status!==0&&s.status!==1)return{ok:!1,reason:`exited ${s.status}: ${s.stderr.slice(0,500)}`};let r;try{r=JSON.parse(s.stdout)}catch(o){return{ok:!1,reason:`stdout was not JSON: ${o.message}`}}return{ok:!0,json:r}}const B={},g=[];for(const e of m){const n=f.join(I,e);if(!S.existsSync(n)){g.push(`fixture "${e}" is in the oracle but missing from --corpus (${n})`);continue}const s=J(P,[n,"--stdout"]);if(!s.ok){g.push(`fixture "${e}": inventory.mjs failed to run -- ${s.reason}`);continue}const r=J(E,[n,"--json"]);if(!r.ok){g.push(`fixture "${e}": classify.mjs failed to run -- ${r.reason}`);continue}const o=new Map;for(const t of r.json.findings??[])o.set($("rule",t.rule,t.file,t.line),t.detail??"");for(const t of s.json.syncRisks??[]){const i=String(t.mutation??""),h=i.lastIndexOf(":");if(h<0)continue;const v=i.slice(0,h),x=Number(i.slice(h+1));Number.isInteger(x)&&o.set($("kind",t.kind,v,x),t.detail??"")}for(const t of s.json.coherence?.candidates??[]){const i=String(t.kind??"").replace(/^coherence\./,"");i!=="no-invalidation-candidate"&&i!=="partial-invalidation-candidate"||typeof t.source?.file!="string"||!Number.isInteger(t.source?.line)||o.set($("coherence",i,t.source.file,t.source.line),`${t.family??""} routes=${(t.routes??[]).join(",")}`)}B[e]=o}if(g.length){console.error("INVALID BENCHMARK RUN:");for(const e of g)console.error(` ${e}`);process.exit(2)}const k=new Map,F=(e,n)=>{const s=k.get(e)??{tp:0,fp:0,fn:0};s[n]++,k.set(e,s)},N=[],j=[],M=e=>e.rule?"rule":e.kind?"kind":"coherence",V=e=>e.rule?e.rule:e.kind?`sync-risk:${e.kind}`:`coherence:${e.coherence}`,q=e=>{const[n,s]=e.split("|");return n==="rule"?s:n==="kind"?`sync-risk:${s}`:`coherence:${s}`};for(const e of m){const n=B[e],s=u.fixtures[e].expected,r=new Set(s.map(o=>$(M(o),o.rule??o.kind??o.coherence,o.file,o.line)));for(const o of s){const t=$(M(o),o.rule??o.kind??o.coherence,o.file,o.line),i=V(o);n.has(t)?F(i,"tp"):(F(i,"fn"),j.push({fixture:e,id:i,file:o.file,line:o.line}))}for(const[o,t]of n){if(r.has(o))continue;const i=q(o);F(i,"fp");const[,h,v,x]=o.split("|");N.push({fixture:e,id:i,file:v,line:Number(x),detail:t})}}const _=({tp:e,fp:n,fn:s})=>{const r=e+n?e/(e+n):1,o=e+s?e/(e+s):1,t=r+o?2*r*o/(r+o):0;return{precision:r,recall:o,f1:t}},R={};let A=0,L=0,w=0;for(const[e,n]of[...k.entries()].sort((s,r)=>s[0].localeCompare(r[0])))e.startsWith("coherence:")||(R[e]={...n,..._(n)},A+=n.tp,L+=n.fp,w+=n.fn);const a={tp:A,fp:L,fn:w,..._({tp:A,fp:L,fn:w})},T=[...k.entries()].filter(([e])=>e.startsWith("coherence:")).sort((e,n)=>e[0].localeCompare(n[0])),p=(()=>{if(!T.length)return{status:"not_applicable",evaluated:0,tp:0,fp:0,fn:0,precision:null,recall:null,f1:null,per_candidate:{}};const e={};let n=0,s=0,r=0;for(const[o,t]of T)e[o]={...t,..._(t)},n+=t.tp,s+=t.fp,r+=t.fn;return{status:"measured",evaluated:n+r,tp:n,fp:s,fn:r,..._({tp:n,fp:s,fn:r}),per_candidate:e}})(),d={min_precision:y,min_recall:b,precision_pass:a.precision>=y,recall_pass:a.recall>=b};d.pass=d.precision_pass&&d.recall_pass;const H={schema_version:1,corpus:I,oracle:O,fixtures:m.length,seed:u.seed??null,overall:a,per_rule:R,false_positives:N,misses:j,coherence:p,gates:d};if(U)console.log(JSON.stringify(H,null,2));else{console.log(`
3
+ bench-score: ${m.length} fixture(s), corpus ${I}
4
+ `),console.log(` overall precision ${a.precision.toFixed(3)} recall ${a.recall.toFixed(3)} f1 ${a.f1.toFixed(3)} (tp=${a.tp} fp=${a.fp} fn=${a.fn})
5
+ `),console.log(" per-rule:");for(const[e,n]of Object.entries(R))console.log(` ${e.padEnd(34)} P ${n.precision.toFixed(3)} R ${n.recall.toFixed(3)} F1 ${n.f1.toFixed(3)} (tp=${n.tp} fp=${n.fp} fn=${n.fn})`);if(console.log(`
6
+ coherence: ${p.status}`+(p.status==="measured"?` precision ${p.precision.toFixed(3)} recall ${p.recall.toFixed(3)} (tp=${p.tp} fp=${p.fp} fn=${p.fn}, not gated -- P3 by policy)`:" (no judge-only coherence entries in this oracle)")),N.length){console.log(`
7
+ false positives:`);for(const e of N)console.log(` ${e.fixture} ${e.id} ${e.file}:${e.line} ${e.detail.slice(0,100)}`)}if(j.length){console.log(`
8
+ misses:`);for(const e of j)console.log(` ${e.fixture} ${e.id} ${e.file}:${e.line}`)}console.log(`
9
+ gates: precision >= ${y} [${d.precision_pass?"pass":"FAIL"}] recall >= ${b} [${d.recall_pass?"pass":"FAIL"}]
10
+ `)}process.exit(d.pass?0:1);
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import M from"node:fs";import b from"node:path";import{fileURLToPath as xe}from"node:url";import{loadSourceSnapshot as be,computeCacheIdentity as we,readCacheableReport as ye}from"../lib/qa/source-snapshot.mjs";import{maskNonCode as Se}from"../lib/qa/coherence/structural-js.mjs";const H=1,$e=b.resolve(b.dirname(xe(import.meta.url)),".."),E=b.resolve(process.argv[2]?.startsWith("--")?".":process.argv[2]??"."),Q=process.argv.includes("--json"),ve=process.argv.includes("--no-cache"),Ae=/(^|\/)(node_modules|\.next|\.git|dist|build|out|coverage|\.turbo|\.vercel|venv|site-packages|vendor|storybook-static|__tests__|__mocks__)(\/|$)|(^|\/)\.[^/]+\/|worktrees\/|\.(test|spec|stories|d)\.[tj]sx?$/,_=be(E,{skipRules:Ae}),R=t=>b.relative(E,t),k=_.files.filter(t=>/\.[jt]sx?$/.test(t)).map(t=>b.join(E,t)),N=Q?null:we(E,_,{skillRoot:$e,toolFiles:["bin/classify.mjs","lib/qa/source-snapshot.mjs"],schemaVersion:H,flags:{}}).identity;if(N&&!ve){const t=b.join(E,".verify","classify.json"),n=ye(t,{identity:N,schemaVersion:H});if(n){const e=new Date().toISOString();n.cache={hit:!0,identity:N,schemaVersion:H,sourceDigest:_.identity.sourceDigest,computedAt:n.cache.computedAt,reusedAt:e},M.writeFileSync(t,JSON.stringify(n,null,2)+`
3
+ `),process.stderr.write(`classify: cache hit (identity ${N.slice(7,19)})
4
+ `),console.log(`
5
+ ${n.files} source files scanned in ${E} (cached)`),console.log(` ${n.counts.total} findings ${Object.entries(n.counts).filter(([i])=>["P0","P1","P2","P3"].includes(i)).map(([i,o])=>`${i}:${o}`).join(" ")}
6
+ `),console.log(` -> ${t}
7
+ `),process.exit(n.counts.P0||n.counts.P1?1:0)}}const p=(t,n)=>t.slice(0,n).split(`
8
+ `).length,Y=t=>t.replace(/[^\n]/g," "),I=t=>t.replace(/\/\*[\s\S]*?\*\//g,Y).replace(/(^|[^:])\/\/[^\n]*/g,Y);function S(t,n,e=8e3){let i=0;for(let o=n;o<Math.min(t.length,n+e);o++){const a=t[o];if("([{".includes(a))i++;else if(")]}".includes(a)&&(i--,i===0))return t.slice(n,o+1)}return t.slice(n,n+e)}function K(t,n,e=300){const i=t.slice(n,n+e),o=i.indexOf("{"),a=i.indexOf(";");return o===-1||a!==-1&&a<o?-1:n+o}const Ee=["isError","error","isLoadingError","failureReason","status"],Re=["isLoading","isPending","isFetching","status"];function ke(t,n){const e=t.slice(Math.max(0,n-240),n),i=/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::\s*[^=;{}]+)?=\s*$/.exec(e);if(i)return{name:i[1]};const o=/(?:const|let|var)\s*\{([^}]*)\}\s*(?::\s*[^=;{}]+)?=\s*$/.exec(e);if(!o)return null;const a=o[1].split(",").map(g=>g.trim()).filter(Boolean),u=a.find(g=>g.startsWith("..."));return{fields:a.filter(g=>!g.startsWith("...")).map(g=>g.split(":")[0].trim()),rest:u?u.slice(3).trim():null}}function Pe(t,n,e,i){if(e.fields&&e.fields.some(g=>i.includes(g)))return!0;const o=e.name??e.rest;if(!o)return!1;const a=o.replace(/\$/g,"\\$"),u=t.slice(n);return new RegExp(`(?<![\\w$.])${a}\\s*\\??\\.\\s*(?:${i.join("|")})\\b`).test(u)||new RegExp(`\\.\\.\\.\\s*${a}(?![\\w$])`).test(u)?!0:new RegExp(`(?<![\\w$.])${a}(?![\\w$])(?!\\s*\\??\\.)(?!\\s*\\[)(?!\\s*:)(?!\\s*=[^=])`).test(u)}function X(t){const n=[];for(const e of t.matchAll(/\bfunction\s+([A-Z]\w*|use[A-Z]\w*)\s*\(/g)){const i=e.index+e[0].length-1,o=S(t,i),a=K(t,i+o.length);a!==-1&&n.push({name:e[1],braceOpen:a})}for(const e of t.matchAll(/\b(?:const|let)\s+([A-Z]\w*|use[A-Z]\w*)\s*=\s*/g)){const i=e.index+e[0].length;let o;if(t[i]==="(")o=i+S(t,i).length;else{const g=t.slice(i,i+80).match(/^[A-Za-z_$][\w$]*/);if(!g)continue;o=i+g[0].length}const a=t.slice(o,o+200).indexOf("=>");if(a===-1)continue;const u=K(t,o+a+2);u!==-1&&n.push({name:e[1],braceOpen:u})}return n}const J=t=>/^\[\s*\S/.test(t)&&!t.includes("...");function Oe(t,n){const e=t.slice(0,n).trimEnd();if(e.endsWith("]")){let a=0;for(let u=e.length-1;u>=0&&e.length-u<4e3;u--)if(")]}".includes(e[u]))a++;else if("([{".includes(e[u])){if(a--,a!==0)continue;return!/[\w$)\]]/.test(e.slice(0,u).trimEnd().slice(-1))&&J(e.slice(u))}return!1}const i=/(^|[^.\w$])([A-Za-z_$][\w$]*)$/.exec(e);if(!i)return!1;const o=new RegExp(`\\bconst\\s+${i[2].replace(/\$/g,"\\$")}\\s*(?::[^=;]*)?=\\s*\\[`).exec(t);return o?J(S(t,t.indexOf("[",o.index+o[0].length-1))):!1}const G=/DOMPurify|sanitize-html|sanitizeHtml|\bsanitize\b|\bxss\s*\(|escapeHtml|escapeHTML|\bhe\.encode\b|striptags/i,_e=/\.replace\(\s*\/[^/\n]*[<>&][^/\n]*\/[a-z]*\s*,[\s\S]{0,400}?&(?:amp|lt|gt|quot|#39);/;function Le(t){const n=new Set,e=/\b(?:function\s+(\w+)\s*\(|(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:\(|[\w$]+\s*=>))/g;for(const i of t.matchAll(e)){const o=i[1]??i[2];if(!o)continue;const a=t.slice(i.index,i.index+500);(G.test(a)||_e.test(a))&&n.add(o)}return n}const v=[],m=(t,n,e,i,o,a)=>v.push({rule:t,severity:n,file:R(e),line:i,detail:o,atlas:a}),ee=new Set,te=new Map,je=/\bAuthorization\b|["'`]Bearer |\bgetToken\s*\(|\bgetAccessToken\w*\s*\(|\bauthHeaders\s*\(/,Te=/\bfetch\s*\(|\baxios\b/,Z=new Set;function Me(t){const n=new Set,e=/\b(?:function\s+(\w+)\s*\(|(?:export\s+)?(?:const|let)\s+(\w+)\s*[:=][^=\n]{0,80}?(?:\(|[\w$]+\s*=>))/g;for(const i of t.matchAll(e)){const o=i[1]??i[2];if(!o)continue;const a=t.slice(i.index,i.index+600);je.test(a)&&Te.test(a)&&n.add(o)}return n}for(const t of k){const n=_.read(R(t));if(!n)continue;const e=I(n),i=Le(e);te.set(t,i);for(const o of i)ee.add(o);for(const o of Me(e))Z.add(o)}const Ne=(t,n)=>new RegExp("import\\s*(?:[\\w$]+\\s*,\\s*)?\\{[^}]*\\b"+n+"\\b[^}]*\\}\\s*from|import\\s+"+n+"\\s*(?:,|\\bfrom\\b)").test(t),Ie=/(?:from\s+["']|require\(\s*["'])(node:[\w-]+|pg|@prisma\/client|prisma|drizzle-orm|mongoose|stripe|ioredis|redis|@aws-sdk\/[\w-]+|nodemailer|bcrypt|jsonwebtoken|express|fastify|koa|fs|path|crypto|child_process)(?=["'/])/,se=/(^|\/)(instrumentation|middleware)\.[jt]s|\.config\.[jtm]s|(^|\/)(app|pages)\/api\/|\.server\.[jt]s|(^|\/)sentry\.(server|edge)\.|(^|\/)(e2e|scripts|tools|tests?|cypress|playwright)\//,ne=t=>/from\s+["'](next\/server|server-only)["']/.test(t)||Ie.test(t),oe=/(^|\/)(server|backend)\//,De=["",".ts",".tsx",".js",".jsx",".mjs",".cjs","/index.ts","/index.tsx","/index.js","/index.jsx"],Ce=new Set(k.map(t=>R(t)));function ze(t,n){const e=[];for(const i of n.matchAll(/(?:from\s*|import\s*|require\(\s*)["']([^"']+)["']/g)){const o=i[1];let a;if(o.startsWith("."))a=[b.posix.normalize(b.posix.join(b.posix.dirname(t),o))];else if(/^[@~#]\//.test(o))a=[o.slice(2),"src/"+o.slice(2)];else continue;for(const u of a)for(const g of De)if(Ce.has(u+g)){e.push(u+g);break}}return e}const Be=(()=>{if(!k.some(a=>oe.test(R(a))))return new Set;const t=new Set,n=new Map,e=[];for(const a of k){const u=R(a),g=I(_.read(u));n.set(u,ze(u,g));const C=se.test(u)||ne(g);oe.test(u)&&!C?t.add(u):C||e.push(u)}if(!t.size)return new Set;const i=new Set(e),o=[...e];for(;o.length;)for(const a of n.get(o.pop())??[])i.has(a)||(i.add(a),o.push(a));return new Set([...t].filter(a=>!i.has(a)))})(),He=_.files.some(t=>/(^|\/)(package|app)\.json$/.test(t)&&/"expo"\s*:/.test(_.read(t))),Ve=new RegExp(`^(NEXT_PUBLIC_|NODE_ENV$|STORYBOOK_${He?"|EXPO_PUBLIC_":""})`);for(const t of k){const n=_.read(R(t)),e=I(n),i=se.test(R(t))||ne(e)||Be.has(R(t)),o=!i&&(/^\s*["']use client["']/m.test(e)||!/(^|\/)app\//.test(t)),a=/\.[jt]sx$/.test(t),u=[...e.matchAll(/const\s*\{([^}]*)\}\s*=\s*use[A-Z]\w*\s*\(/g)].flatMap(s=>s[1].split(",").map(r=>r.split(":").pop().trim()).filter(Boolean)),g=u.map(s=>e.match(new RegExp("\\b"+s+"\\b[^\\n]{0,40}\\.map\\s*\\("))).filter(Boolean).sort((s,r)=>s.index-r.index)[0]??null,C=X(e),U=[...e.matchAll(/\.map\s*\(/g)].map(s=>{if(Oe(e,s.index))return null;const r=C.find(({braceOpen:d})=>{const f=d+S(e,d).length;return s.index>=d&&s.index<f}),c=r?S(e,r.braceOpen):e;return/use(?:Suspense)?(?:Infinite)?Query|useSWR|await\s+fetch/.test(c)?s:null}).filter(Boolean).sort((s,r)=>s.index-r.index)[0]??null,de=!!g||!!U;if(a&&/\.map\s*\(/.test(e)&&de){const s=/\.length\s*===?\s*0|\.length\s*[?>]|!\w+\.length|\.length\s*<\s*1|\bisEmpty\b|<[\w.]*(?:EmptyState|NoResults|NoData|Empty)\b|\bempty(?:State|Message|Text|Content|Component|Render|Placeholder)\s*[=:]|\bno-results\b|\bnoResults\b/i.test(e),r=u.some(c=>/^toasts?$/i.test(c))&&/<[\w.]*Viewport\b/.test(e);if(!s&&!r){const c=g??U??e.match(/\.map\s*\(/);m("missing-empty-state","P1",t,p(e,c.index),"renders a list with no zero-length branch \u2014 an empty list and a failed fetch look identical","L5.36")}}for(const s of e.matchAll(/\buse(Suspense)?(?:Infinite)?Query\s*\(/g)){if(s[1])continue;const r=S(e,s.index+s[0].length-1),c=s.index+s[0].length-1+r.length,d=e.slice(Math.max(0,s.index-300),c+1200);if(/\breturn\s*$/.test(e.slice(Math.max(0,s.index-20),s.index).trim()?e.slice(Math.max(0,s.index-20),s.index):"")||/(?:return|=>)\s*$/.test(e.slice(Math.max(0,s.index-12),s.index)))continue;const x=ke(e,s.index),w=l=>x!==null&&Pe(e,c,x,l);!w(Ee)&&!/\b(isError|error|isLoadingError|failureReason)\b/.test(d)&&m("unread-query-error","P1",t,p(e,s.index),"useQuery result never reads isError/error \u2014 a failed fetch renders as empty, silently","L5.35"),!w(Re)&&!/\b(isLoading|isPending|isFetching|Suspense|skeleton|Skeleton)\b/.test(d)&&m("unread-query-loading","P2",t,p(e,s.index),"useQuery result never reads isLoading/isPending \u2014 no loading state, so empty renders first","L5.34")}const q=[];for(const s of e.matchAll(/\buse(?:Suspense)?(?:Infinite)?Query\s*\(/g)){const r=S(e,s.index+s[0].length-1),c=r.match(/queryKey:\s*(\[[^\]]*\])/),d=r.match(/queryFn:\s*([A-Za-z_$][\w$.]*)\b(?!\s*[:(])/);!c||!d||/\$\{/.test(c[1])||/^\[\s*(['"][^'"]*['"]\s*,?\s*)+\]$/.test(c[1].replace(/\s+/g," ").trim())&&q.push({key:c[1].replace(/\s+/g,""),fn:d[1],index:s.index})}{const s=new Map;for(const r of q)(s.get(r.key)??s.set(r.key,[]).get(r.key)).push(r);for(const[r,c]of s){const d=c[0];for(const f of c.slice(1))f.fn!==d.fn&&m("cache-key-collision","P3",t,p(e,f.index),`queryKey ${r} is also used by ${d.fn} at ${R(t)}:${p(e,d.index)} \u2014 two different queries share one cache slot and can overwrite each other`,"L3.17")}}if(/zustand/.test(n))for(const s of e.matchAll(/\bcreate\s*(?:<[^>]*>)?\s*\(/g)){const r=S(e,s.index+s[0].length-1);if(!/^\(\s*(?:\(set|\(\)|set|persist|immer|devtools|subscribeWithSelector)/.test(r))continue;/\b(data|items|list|results|rows|records|entities|byId)\b\s*:/.test(r)&&/\b(fetch|axios|api|await\s+\w+\()/.test(r)&&m("server-state-in-client-store","P0",t,p(e,s.index),"client store fetches and holds server data \u2014 the query cache can never invalidate it; page B goes stale","L4.22")}for(const s of e.matchAll(/useState\s*(?:<[^>]*>)?\s*\(\s*props\.(\w+)/g))m("derived-state-in-useState","P2",t,p(e,s.index),`useState seeded from props.${s[1]} \u2014 will not update when that prop changes`,"L4.28");for(const s of e.matchAll(/useEffect\s*\(\s*(?:async\s*)?\(\s*\)\s*=>\s*\{/g)){const r=S(e,e.indexOf("{",s.index+s[0].length-1)),c=e.indexOf("}",s.index+r.length-1),d=(e.slice(c,c+200).match(/\}\s*,\s*(\[[^\]]*\])/)??[])[1];if(/\b(fetch|axios)\s*[.(]/.test(r)&&!/AbortController|signal|cancelled|ignore|isMounted/.test(r)&&m("effect-fetch-without-abort","P1",t,p(e,s.index),"fetch in an effect with no AbortController \u2014 a late response lands on an unmounted or navigated view","L3.16"),d==="[]"){const x=[...r.replace(/\/\*[\s\S]*?\*\//g," ").replace(/\/\/[^\n]*/g," ").replace(/"(?:[^"\\]|\\.)*"/g,'""').replace(/'(?:[^'\\]|\\.)*'/g,"''").replace(/`(?:[^`\\]|\\.)*`/g,"``").matchAll(/(?<![.\w])\b([a-z][\w]*)\b(?!\s*[:(])/g)].map(h=>h[1]),w=[...e.matchAll(/const\s*\[\s*([a-z][\w]*)\s*,\s*set[A-Z]/g)].map(h=>h[1]),l=[...new Set(x.filter(h=>w.includes(h)))];l.length&&m("stale-closure","P1",t,p(e,s.index),`effect has [] deps but reads state (${l.slice(0,3).join(", ")}) \u2014 sees the first-render value forever`,"L4.28")}}if(o&&!i)for(const s of e.matchAll(/(^|[^.\w$])fetch\s*\(/g)){const r=s.index+s[1].length,c=e.indexOf("(",r),d=S(e,c),f=c+d.length,x=e.slice(f,f+320),w=p(e,r);let l=0,h=!1;for(let A=0;A<d.length&&!h;A++){const j=d[A];"([{".includes(j)?l++:")]}".includes(j)?l--:j===","&&l===1&&(h=!0)}const $=/^\(\s*[`'"](\/[^`'"?#]*)/.exec(d),P=!!$&&!/\.(json|txt|csv|xml|html?|svg|png|jpe?g|gif|webp|avif|js|mjs|css|map|wasm|pdf)$/i.test($[1]),T=P&&$[1].startsWith("/api/");if(Z.size&&T&&!h){const A=[...Z].slice(0,2).join(", ");m("unauthenticated-api-fetch","P3",t,w,`bare fetch() to a same-origin /api path with no options, in a repo whose requests otherwise go through ${A}() \u2014 whatever that helper attaches (session token, error contract) is absent here. Source cannot see the endpoint's authentication contract, so this is a candidate for the real-persona lane to confirm with a live 401, not a confirmed defect`,"L2.3")}const L=/^\s*\.\s*then\s*\(/.exec(x);if(L&&P){const A=S(e,f+L[0].length-1);/\.\s*json\s*\(\s*\)/.test(A)&&!/\b(ok|status|statusText)\b/.test(A)&&m("unchecked-fetch-response","P3",t,w,"fetch(...).then(... .json()) with no ok/status check \u2014 a 4xx/5xx body parses just as well as a success body, and reaches the caller as data","L2.1")}const O=/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::\s*[^=;]{0,80})?=\s*await\s+$/.exec(e.slice(Math.max(0,r-120),r));if(O&&!L&&P){const A=O[1].replace(/\$/g,"\\$"),j=e.slice(f,f+800),B=j.search(new RegExp("\\b"+A+"\\s*\\??\\.\\s*json\\s*\\(\\s*\\)")),ge=new RegExp("\\b"+A+"\\s*\\??\\.\\s*(ok|status|statusText)\\b");B>-1&&!ge.test(j.slice(0,B))&&m("unchecked-fetch-response","P3",t,w,`\`${O[1]}.json()\` is read with no \`${O[1]}.ok\`/\`.status\` check before it \u2014 a 4xx/5xx body parses just as well as a success body, and reaches the caller as data`,"L2.1")}}const fe=/(?:Array\s*\.\s*from\s*\(\s*\{[^{}]*\blength\b[^{}]*\}\s*\)|new\s+Array\s*\([^)]*\)|\[[^[\]]*\])\s*$/;for(const s of e.matchAll(/key\s*=\s*\{\s*(?:index|i|idx)\s*\}/g)){const c=e.slice(0,s.index).lastIndexOf(".map(");if(c!==-1&&s.index-c<600){const d=e.slice(Math.max(0,c-200),c),f=e.slice(c+5,c+40);if(fe.test(d)||/^\s*\(?\s*_\s*,/.test(f))continue;const x=(d.match(/([A-Za-z_$][\w$]*)(?:\.[A-Za-z_$][\w$]*)*\s*$/)??[])[1];if(x){const w=new RegExp("^(?:export\\s+)?const\\s+"+x+"\\b","m").test(e),l=(e.match(new RegExp("\\b"+x+"\\s*=(?!=)","g"))??[]).length;if(w&&l<=1)continue}}m("index-as-list-key","P3",t,p(e,s.index),"key={index} \u2014 if this list reorders, a row with local state or focus may keep the wrong state; static analysis does not prove that impact","L5.40")}for(const s of e.matchAll(/<(\w+)\.Provider\s+value=\{\s*[{[]/g)){if(/useMemo/.test(e.slice(Math.max(0,s.index-500),s.index)))continue;const r=s[1];new RegExp("\\buseContext\\s*\\(\\s*"+r+"\\b").test(e)||m("unstable-context-value","P2",t,p(e,s.index),"Provider value is a fresh literal each render \u2014 every consumer re-renders, memoization downstream is dead","L10.98")}if(/subscribe\s*\(/.test(e)&&/getSnapshot|getState/.test(e)&&!/useSyncExternalStore/.test(e)){const s=e.search(/subscribe\s*\(/);m("external-store-without-sync","P2",t,p(e,s),"external store read without useSyncExternalStore \u2014 tearing risk under concurrent rendering","L4.29")}const y=Se(n),W=new Set,F=(s,r)=>{const c=[],d=f=>{if(f<s||f>=r)return;const x=S(y,f);c.push({start:f,end:Math.min(r,f+x.length)})};for(const f of y.slice(s,r).matchAll(/=>\s*\{/g))d(s+f.index+f[0].lastIndexOf("{"));for(const f of y.slice(s,r).matchAll(/\bfunction\b[^;{}]*\{/g))d(s+f.index+f[0].lastIndexOf("{"));return c},ue=(s,r)=>{const c=[],d=F(s,r),f=l=>d.some(({start:h,end:$})=>l>h&&l<$),x=(l,h)=>{for(;l<r&&/\s/.test(y[l]);)l++;if(l>=r)return;if(y[l]==="{"){const L=S(y,l);c.push({start:l,end:Math.min(r,l+L.length),keyword:h});return}const $=y.indexOf(";",l),P=y.indexOf(`
9
+ `,l),T=[$===-1||$>=r?r:$+1,P===-1||P>=r?r:P+1];c.push({start:l,end:Math.min(...T),keyword:h})},w=y.slice(s,r).matchAll(/\b(if|for|while|switch|catch)\s*\(/g);for(const l of w){const h=s+l.index;if(f(h)||z(s,h).parens>0)continue;const $=h+l[0].length-1,P=S(y,$);x($+P.length,l[1])}for(const l of y.slice(s,r).matchAll(/\belse\b/g)){const h=s+l.index;f(h)||z(s,h).parens>0||x(h+l[0].length,"else")}return c},z=(s,r)=>{let c=0;for(let d=s;d<r;d++)y[d]==="("?c++:y[d]===")"&&(c=Math.max(0,c-1));return{parens:c}};for(const{braceOpen:s}of X(e)){const r=S(y,s),c=s+r.length-1,d=F(s+1,c),f=w=>d.some(({start:l,end:h})=>w>l&&w<h),x=ue(s+1,c);for(const w of y.slice(s+1,c).matchAll(/(?<![.\w])use[A-Z]\w*\s*\(/g)){const l=s+1+w.index;if(f(l)||z(s+1,l).parens>0)continue;const h=x.find(({start:L,end:O})=>l>=L&&l<O),$=[...y.slice(s+1,l).matchAll(/\breturn\b/g)].some(L=>{const O=s+1+L.index;if(f(O)||z(s+1,O).parens>0)return!1;const A=y.slice(O+L[0].length,l).trim();return!A||/^[([{]*$/.test(A)?!1:x.some(({start:j,end:B})=>O>=j&&O<B)});if(!h&&!$)continue;const P=`${t}:${p(e,l)}`;if(W.has(P))continue;W.add(P);const T=$&&!h?"after an early return":`inside a ${h.keyword} block`;m("conditional-hook-call","P3",t,p(e,l),`hook called ${T} \u2014 runs a different number of times across renders and shifts every hook after it`,"L5.42")}}if(o)for(const s of e.matchAll(/process\.env\.([A-Z][A-Z0-9_]*)/g))Ve.test(s[1])||m("runtime-env-in-client-code","P0",t,p(e,s.index),`process.env.${s[1]} in client code \u2014 inlined at build time, so it is undefined in the browser however the runtime is configured`,"L1.1");for(const s of e.matchAll(/import\.meta\.env\.([A-Z][A-Z0-9_]*)/g))/^(VITE_|MODE$|DEV$|PROD$|SSR$|BASE_URL$)/.test(s[1])||m("runtime-env-in-client-code","P0",t,p(e,s.index),`import.meta.env.${s[1]} is not VITE_-prefixed \u2014 Vite strips it from the client bundle`,"L1.1");for(const s of e.matchAll(/dangerouslySetInnerHTML|\.innerHTML\s*=/g)){const r=s[0]==="dangerouslySetInnerHTML"?e.indexOf("{",s.index+s[0].length):-1,d=r!==-1&&r-(s.index+s[0].length)<=3?S(e,r):e.slice(s.index,s.index+800),f=te.get(t)??new Set,x=[...d.matchAll(/\b(\w+)\s*\(/g)].some(h=>f.has(h[1])||ee.has(h[1])&&Ne(e,h[1]));if(G.test(d)||x)continue;const w=e.slice(Math.max(0,s.index-200),s.index+200);/application\/ld\+json|JSON\.stringify/.test(w)||[...e.slice(Math.max(0,s.index-300),s.index).matchAll(/<([a-zA-Z][\w.-]*)/g)].pop()?.[1]==="style"||m("unsanitized-html","P0",t,p(e,s.index),"raw HTML injection with no sanitizer in the file \u2014 XSS sink","L7.74")}if(/rehype-raw|rehypeRaw/.test(n)&&/react-markdown|ReactMarkdown/.test(n)&&!/rehype-sanitize|rehypeSanitize/.test(n)){const s=e.match(/rehype-raw|rehypeRaw/);m("unsanitized-markdown-raw-html","P3",t,p(e,s.index),"rehypeRaw lets raw HTML through react-markdown with no rehype-sanitize \u2014 markdown content becomes an XSS sink","L5.55")}for(const s of e.matchAll(/Object\.assign\s*\(\s*(?!\{)\w+\s*,\s*(?:JSON\.parse|await|res|data|body|payload|params)/g))m("prototype-pollution-shape","P1",t,p(e,s.index),"Object.assign into an existing object from untrusted data \u2014 __proto__ in the payload rewrites Object.prototype","L7.75");if(a&&/onSubmit|handleSubmit/.test(e)&&/useMutation|mutate\s*\(/.test(e)&&!/isPending|isLoading|isSubmitting|disabled=/.test(e)){const s=e.match(/onSubmit|handleSubmit/);m("submit-without-pending-guard","P1",t,p(e,s.index),"submit path with no pending/disabled guard \u2014 a double click writes the record twice","L6.59")}for(const s of e.matchAll(/<button\b((?:=>|[^>])*)>/g)){const r=s[1];if(!/onClick\s*=\s*\{/.test(r)||!/\.mutate(?:Async)?\s*\(/.test(r)||/\bdisabled\b/.test(r))continue;const c=e.indexOf("</button>",s.index+s[0].length),d=c===-1?r:e.slice(s.index,c);/\b(isPending|isLoading|isSubmitting)\b/.test(d)||m("async-click-without-feedback","P3",t,p(e,s.index),"onClick fires a mutation with no disabled attribute and no pending/loading state anywhere in the element \u2014 a slow click fires it twice","L6.68")}const he=/formState\s*:\s*\{[^}]*\bisDirty\b/.test(e)||/\b(isDirty|hasUnsavedChanges|unsavedChanges)\b\s*[,}]/.test(e),me=/next\/navigation|next\/link|react-router|useNavigate|useRouter\b/.test(n)||/(^|\/)(app|pages)\/(.*\/)?page\.[jt]sx?$/.test(R(t)),pe=/beforeunload|useBeforeUnload|usePrompt\b|useBlocker\b|unstable_useBlocker|routeChangeStart/.test(e);if(a&&he&&me&&!pe){const s=e.match(/formState\s*:\s*\{[^}]*\bisDirty\b/)||e.match(/\b(isDirty|hasUnsavedChanges|unsavedChanges)\b/);m("unsaved-changes-guard-missing","P3",t,p(e,s.index),"form tracks dirty/unsaved state and this file can navigate away, but no beforeunload/route-change guard exists \u2014 edits are lost silently","L6.63")}}const Ze=k.filter(t=>/(^|\/)app\/(.*\/)?page\.[jt]sx?$/.test(t)),ie=new Set;for(const t of Ze){const n=b.dirname(t),e=_.read(R(t));if(!/useQuery|useSWR|fetch\s*\(|await\s/.test(e))continue;const i=o=>{for(let a=n;a.startsWith(E);a=b.dirname(a))if(["tsx","ts","jsx","js"].some(u=>M.existsSync(b.join(a,`${o}.${u}`))))return!0;return!1};i("error")||(m("no-error-boundary","P1",t,1,"data route with no error.tsx in its segment chain \u2014 a render throw blanks the subtree","L5.38"),ie.add(t)),i("loading")||m("no-loading-boundary","P2",t,1,"data route with no loading.tsx \u2014 the empty shell paints before data arrives","L5.34")}const Ue=/<[\w.]*(?:ErrorBoundary|ErrorResetBoundary)\b|\berror(?:Component|Element)\s*[:=]|from\s*['"]react-error-boundary['"]/,re=new Set;for(const t of k){const n=_.read(R(t));n&&(/^error\.[jt]sx?$/.test(b.basename(t))||Ue.test(I(n)))&&re.add(b.dirname(t))}const qe=t=>{for(let n=b.dirname(t);n.startsWith(E);n=b.dirname(n))if(re.has(n))return!0;return!1};for(const t of k){if(ie.has(t))continue;const n=I(_.read(R(t))??"");let e=null;for(const i of n.matchAll(/\buse(Suspense)?(?:Infinite)?Query\s*\(/g)){if(!i[1]){const o=S(n,i.index+i[0].length-1);if(!(/\bthrowOnError\s*:/.test(o)&&!/\bthrowOnError\s*:\s*false\b/.test(o))&&!/\bsuspense\s*:\s*true\b/.test(o))continue}e=i.index;break}e===null||qe(t)||m("no-error-boundary","P3",t,p(n,e),"a query that THROWS its failure (Suspense, or throwOnError/suspense) with no error boundary above it \u2014 no errorComponent, no errorElement, no <ErrorBoundary>, no error.tsx anywhere in its directory chain, so one failed fetch unmounts the subtree","L5.38")}const We=6,D=[];{const t=new Map;for(const n of v){const e=b.join(E,n.file);if(!t.has(e))try{t.set(e,M.readFileSync(e,"utf8").split(`
10
+ `))}catch{t.set(e,null)}const i=t.get(e);if(!i)continue;const o=i.slice(Math.max(0,n.line-1-We),n.line).join(`
11
+ `);new RegExp(`verify-ignore:\\s*${n.rule}\\b`).test(o)&&(n.waived=!0)}for(let n=v.length-1;n>=0;n--)v[n].waived&&D.push(...v.splice(n,1))}const Fe=.3,Qe=10,Ye={P0:"P2",P1:"P3",P2:"P3",P3:"P3"};{const t=new Map;for(const n of v)t.has(n.rule)||t.set(n.rule,new Set),t.get(n.rule).add(n.file);for(const[n,e]of t){if(e.size<Qe||e.size/k.length<Fe)continue;const i=Math.round(e.size/k.length*100);for(const o of v)o.rule===n&&(o.severity=Ye[o.severity],o.unresolved=!0,o.detail+=` [demoted: ${n} fired in ${e.size}/${k.length} files (${i}%) -- one shared cause, not ${e.size} independent defects, until reviewed]`)}}const ae={P0:0,P1:1,P2:2,P3:3};v.sort((t,n)=>ae[t.severity]-ae[n.severity]||t.rule.localeCompare(n.rule)||t.file.localeCompare(n.file));const ce=v.reduce((t,n)=>(t[n.rule]=(t[n.rule]??0)+1,t),{}),V=v.reduce((t,n)=>(t[n.severity]=(t[n.severity]??0)+1,t),{}),le=JSON.stringify({root:E,atlasVersion:2,files:k.length,counts:{total:v.length,waived:D.length,...V},byRule:ce,findings:v,waived:D,cache:{hit:!1,identity:N,schemaVersion:H,sourceDigest:_.identity.sourceDigest,computedAt:new Date().toISOString()}},null,2)+`
12
+ `;if(Q)process.stdout.write(le);else{const t=b.join(E,".verify");M.mkdirSync(t,{recursive:!0}),M.writeFileSync(b.join(t,"classify.json"),le),console.log(`
13
+ ${k.length} source files scanned in ${E}`),console.log(` ${v.length} findings ${Object.entries(V).map(([n,e])=>`${n}:${e}`).join(" ")}`+(D.length?` (${D.length} waived by verify-ignore comments)`:"")+`
14
+ `);for(const[n,e]of Object.entries(ce).sort((i,o)=>o[1]-i[1])){const i=v.find(o=>o.rule===n);console.log(` ${String(e).padStart(4)} ${i.severity} ${n} [${i.atlas}]`),console.log(` ${i.detail}`);for(const o of v.filter(a=>a.rule===n).slice(0,3))console.log(` ${o.file}:${o.line}`);e>3&&console.log(` ... and ${e-3} more`),console.log("")}console.log(` -> ${b.join(E,".verify","classify.json")}
15
+ `)}process.exit(V.P0||V.P1?1:0);
@@ -0,0 +1 @@
1
+ import{randomUUID as U}from"node:crypto";const R=new Set(["document","image","font","stylesheet","script"]);function y(s,o){return String(s??"/").replace(/\{fixtureId\}/g,encodeURIComponent(o??"fixtureId"))}function g(s,o,l){return new URL(y(o,l),s).toString()}function T(s){try{return new URL(s).pathname}catch{return null}}async function E(s){const o=`__qa_coherence_${U().replaceAll("-","")}`,l=await s.evaluate(({key:i})=>(Object.defineProperty(window,i,{value:!0,configurable:!0}),{marked:window[i]===!0,timeOrigin:performance.timeOrigin}),{key:o});return{key:o,...l}}async function O(s,o){return s.evaluate(({key:l,timeOrigin:i})=>({marked:window[l]===!0,sameTimeOrigin:performance.timeOrigin===i}),o)}function D({browser:s,base:o,personaStates:l,journey:i,captureRepresentation:h}){if(!s||!o||!l||!i||typeof h!="function")throw new Error("incomplete coherence Playwright driver configuration");function p(e){return e?.id??"__default__"}let v=0,w=null;async function f(e,t){let a=e.pages.get(t);if(!a){const n=await e.raw.newPage(),u=[];n.on("framenavigated",r=>{try{e.navigation.push(new URL(r.url()).pathname)}catch{}}),n.on("response",r=>{try{const c=r.request(),m=typeof c.resourceType=="function"?c.resourceType():null;if(m&&R.has(m)||typeof c.method=="function"&&c.method()!=="GET")return;u.push(++v)}catch{}}),a={page:n,url:null,responseTimestamps:u},e.pages.set(t,a)}return a}async function k(e,t,a){let n;try{n=await E(e.page)}catch{return{status:"unmeasured",reason:"document-continuity-unmeasured",continuity:{status:"unmeasured",sameDocument:null,reason:"document-continuity-unmeasured"}}}const u=await e.page.$(`a[href="${t}"], a[href="${t}/"]`);if(!u)return{status:"unmeasured",reason:"no-client-link",continuity:{status:"unmeasured",sameDocument:null,reason:"no-client-link"}};await u.click(),await e.page.waitForLoadState("domcontentloaded").catch(()=>{}),typeof e.page.waitForTimeout=="function"&&await e.page.waitForTimeout(100).catch(()=>{});let r;try{r=await O(e.page,n)}catch{return{status:"unmeasured",reason:"document-continuity-unmeasured",continuity:{status:"unmeasured",sameDocument:null,reason:"document-continuity-unmeasured"}}}return!n.marked||!r.marked||!r.sameTimeOrigin?{status:"unmeasured",reason:"full-document-navigation",continuity:{status:"measured",sameDocument:!1,reason:"full-document-navigation"}}:(e.url=a,{status:"measured",reason:null,continuity:{status:"measured",sameDocument:!0,reason:null}})}return{async createContext({persona:e,purpose:t}){const a=l.get(e);if(!a?.statePath)throw new Error(`unprovisioned coherence persona: ${e}`);return{raw:await s.newContext({storageState:a.statePath,...a.headers&&Object.keys(a.headers).length?{extraHTTPHeaders:a.headers}:{}}),persona:e,purpose:t,pages:new Map,navigation:[]}},async prime({context:e,target:t,spec:a}){const n=await f(e,p(t)),u=t?.transition==="client-navigation"&&a?.route?a.route:t.route,r=g(o,u,t.fixtureId);return await n.page.goto(r,{waitUntil:"domcontentloaded"}),n.url=r,{navigation:[...e.navigation]}},async mutate({context:e,spec:t}){if(typeof i.mutate!="function")throw new Error("journey exports no mutate()");const a=await f(e,"__writer__"),n=await i.mutate(a.page,{base:o,spec:t});return w=++v,n},async navigate({context:e,step:t,target:a}){const n=await f(e,p(a));return k(n,y(t.toRoute,a?.fixtureId),g(o,t.toRoute,a?.fixtureId))},async observe({context:e,target:t,transition:a}){const n=await f(e,p(t)),u=g(o,t.route,t.fixtureId),r=a??"direct-url";let c=null;if(r!=="same-surface")if(r==="client-navigation"){const d=y(t.route,t.fixtureId),_=await k(n,d,u);c=_.status==="measured"?null:_.reason}else if(r==="reload")await n.page.reload({waitUntil:"domcontentloaded"}),n.url=u;else if(r==="history-back"||r==="history-forward"){r==="history-back"?await n.page.goBack({waitUntil:"domcontentloaded"}).catch(()=>{}):await n.page.goForward({waitUntil:"domcontentloaded"}).catch(()=>{});const d=typeof n.page.url=="function"?n.page.url():null;T(d)!==T(u)?c="history-target-mismatch":n.url=d??u}else r==="offline-replay"?c="offline-replay-not-driven-here":(await n.page.goto(u,{waitUntil:"domcontentloaded"}),n.url=u);if(c)return{representation:{state:"failed"},unmeasured:c,navigation:[...e.navigation],refetched:!1};const m=w!=null&&n.responseTimestamps.some(d=>d>w);return{representation:await h(n.page,t.representation,{target:t,context:e}),navigation:[...e.navigation],refetched:m}},async authoritativeReadback({spec:e,mutation:t}){if(typeof i.authoritativeReadback!="function")throw new Error("journey exports no authoritativeReadback()");return i.authoritativeReadback({base:o,spec:e,mutation:t})},async cleanup({spec:e,mutation:t}){if(typeof i.cleanup!="function")throw new Error("journey exports no cleanup()");return i.cleanup({base:o,spec:e,mutation:t})},async verifyCleanup({spec:e,mutation:t}){if(typeof i.verifyCleanup!="function")throw new Error("journey exports no verifyCleanup()");return i.verifyCleanup({base:o,spec:e,mutation:t})},async closeContext(e){for(const t of e.pages?.values()??[])await t.page?.close?.().catch(()=>{});await e.raw.close()}}}export{D as createPlaywrightObservationDriver};