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.
- package/LICENSE +22 -0
- package/NOTICE +9 -0
- package/README.md +100 -0
- package/SKILL.md +65 -0
- package/agent/prebuilt/darwin-universal/qa-agent +0 -0
- package/agent/prebuilt/linux-arm64/qa-agent +0 -0
- package/agent/prebuilt/linux-x64/qa-agent +0 -0
- package/agent/release-manifest.json +31 -0
- package/bin/autonomous-cache.mjs +2 -0
- package/bin/bench-generate.mjs +232 -0
- package/bin/bench-graph-query.mjs +2 -0
- package/bin/bench-score.mjs +10 -0
- package/bin/classify.mjs +15 -0
- package/bin/coherence-playwright.mjs +1 -0
- package/bin/coherence-runner.mjs +6 -0
- package/bin/crud-runner.mjs +12 -0
- package/bin/experiment-executor.mjs +10 -0
- package/bin/experiment-runner.mjs +3 -0
- package/bin/inventory.mjs +17 -0
- package/bin/probe.js +879 -0
- package/bin/qa.mjs +16 -0
- package/bin/rules.mjs +5 -0
- package/bin/run-benchmarks.mjs +2 -0
- package/bin/runtime-packs.mjs +3 -0
- package/bin/semantic-explorer.mjs +3 -0
- package/bin/shadow-collector.mjs +3 -0
- package/bin/shadow-replay.mjs +6 -0
- package/bin/soak-marathon.mjs +15 -0
- package/bin/sweep.mjs +43 -0
- package/bin/validate-priors.mjs +3 -0
- package/bin/verify-artifacts.mjs +3 -0
- package/bin/verify-manifest.mjs +2 -0
- package/install-skill.sh +45 -0
- package/lib/qa/agent-availability.mjs +1 -0
- package/lib/qa/agent-result.mjs +1 -0
- package/lib/qa/analysis.mjs +5 -0
- package/lib/qa/atlas-coverage.mjs +11 -0
- package/lib/qa/atlas-v1.mjs +1 -0
- package/lib/qa/atlas.mjs +2 -0
- package/lib/qa/auth/coordinator.mjs +1 -0
- package/lib/qa/auth/launcher.mjs +3 -0
- package/lib/qa/auth/lease.mjs +1 -0
- package/lib/qa/auth/oracle.mjs +1 -0
- package/lib/qa/auth/personas.mjs +3 -0
- package/lib/qa/autonomous-cache.mjs +3 -0
- package/lib/qa/block-ledger.mjs +1 -0
- package/lib/qa/capture-report.mjs +63 -0
- package/lib/qa/ci.mjs +11 -0
- package/lib/qa/cli.mjs +538 -0
- package/lib/qa/coherence/analyze.mjs +1 -0
- package/lib/qa/coherence/cache-key.mjs +6 -0
- package/lib/qa/coherence/causes.mjs +1 -0
- package/lib/qa/coherence/concurrency.mjs +1 -0
- package/lib/qa/coherence/contracts.mjs +1 -0
- package/lib/qa/coherence/entity-aliases.mjs +1 -0
- package/lib/qa/coherence/enums.mjs +1 -0
- package/lib/qa/coherence/family-report.mjs +1 -0
- package/lib/qa/coherence/flow-pool.mjs +1 -0
- package/lib/qa/coherence/graph.mjs +1 -0
- package/lib/qa/coherence/identity.mjs +1 -0
- package/lib/qa/coherence/ir.mjs +4 -0
- package/lib/qa/coherence/navigation.mjs +1 -0
- package/lib/qa/coherence/observe.mjs +2 -0
- package/lib/qa/coherence/offline.mjs +1 -0
- package/lib/qa/coherence/plan-join.mjs +1 -0
- package/lib/qa/coherence/provenance.mjs +2 -0
- package/lib/qa/coherence/report.mjs +1 -0
- package/lib/qa/coherence/representations.mjs +1 -0
- package/lib/qa/coherence/risk.mjs +2 -0
- package/lib/qa/coherence/structural-js.mjs +11 -0
- package/lib/qa/coherence/symbols.mjs +6 -0
- package/lib/qa/coherence/tanstack.mjs +4 -0
- package/lib/qa/coherence/views.mjs +1 -0
- package/lib/qa/coherence-planner.mjs +1 -0
- package/lib/qa/command-catalog.mjs +2 -0
- package/lib/qa/commands/dev.mjs +1 -0
- package/lib/qa/commands/fix.mjs +1 -0
- package/lib/qa/commands/impl/analysis.mjs +1 -0
- package/lib/qa/commands/impl/atlas.mjs +1 -0
- package/lib/qa/commands/impl/coherence.mjs +1 -0
- package/lib/qa/commands/impl/crud.mjs +2 -0
- package/lib/qa/commands/impl/doctor.mjs +1 -0
- package/lib/qa/commands/impl/experiments.mjs +1 -0
- package/lib/qa/commands/impl/fp.mjs +1 -0
- package/lib/qa/commands/impl/graph.mjs +1 -0
- package/lib/qa/commands/impl/invariants.mjs +1 -0
- package/lib/qa/commands/impl/lanes.mjs +7 -0
- package/lib/qa/commands/impl/ledger.mjs +11 -0
- package/lib/qa/commands/impl/loop.mjs +1 -0
- package/lib/qa/commands/impl/marathon.mjs +1 -0
- package/lib/qa/commands/impl/next.mjs +38 -0
- package/lib/qa/commands/impl/report.mjs +4 -0
- package/lib/qa/commands/impl/research.mjs +4 -0
- package/lib/qa/commands/impl/selftest.mjs +1 -0
- package/lib/qa/commands/impl/serve.mjs +1 -0
- package/lib/qa/commands/impl/shadow.mjs +1 -0
- package/lib/qa/commands/impl/stability.mjs +1 -0
- package/lib/qa/commands/impl/triage.mjs +1 -0
- package/lib/qa/commands/impl/validate.mjs +4 -0
- package/lib/qa/commands/impl/version.mjs +1 -0
- package/lib/qa/commands/impl/watch.mjs +1 -0
- package/lib/qa/commands/init.mjs +6 -0
- package/lib/qa/commands/measure.mjs +1 -0
- package/lib/qa/commands/run.mjs +1 -0
- package/lib/qa/commands/show.mjs +1 -0
- package/lib/qa/commands/verify.mjs +6 -0
- package/lib/qa/deepening.mjs +1 -0
- package/lib/qa/denominators.mjs +1 -0
- package/lib/qa/depth.mjs +4 -0
- package/lib/qa/doctor.mjs +2 -0
- package/lib/qa/experiments-default.mjs +1 -0
- package/lib/qa/fix-lanes.mjs +3 -0
- package/lib/qa/forced-states.mjs +1 -0
- package/lib/qa/fp.mjs +2 -0
- package/lib/qa/fs.mjs +6 -0
- package/lib/qa/graph-schema.mjs +2 -0
- package/lib/qa/graph.mjs +3 -0
- package/lib/qa/harness-plan.mjs +3 -0
- package/lib/qa/init.mjs +5 -0
- package/lib/qa/integrity.mjs +1 -0
- package/lib/qa/invariants.mjs +84 -0
- package/lib/qa/jcs.mjs +1 -0
- package/lib/qa/lanes.mjs +1 -0
- package/lib/qa/loop.mjs +7 -0
- package/lib/qa/marathon-coherence.mjs +1 -0
- package/lib/qa/marathon-phases.mjs +1 -0
- package/lib/qa/marathon-wait.mjs +3 -0
- package/lib/qa/marathon.mjs +5 -0
- package/lib/qa/mode-coverage.mjs +1 -0
- package/lib/qa/mutation-policy.mjs +1 -0
- package/lib/qa/pattern-ledger.mjs +3 -0
- package/lib/qa/pending.mjs +1 -0
- package/lib/qa/proc.mjs +4 -0
- package/lib/qa/recommend.mjs +1 -0
- package/lib/qa/release-manifest.mjs +1 -0
- package/lib/qa/report-html.mjs +178 -0
- package/lib/qa/reports.mjs +2 -0
- package/lib/qa/request-evidence.mjs +1 -0
- package/lib/qa/research.mjs +20 -0
- package/lib/qa/routing.mjs +1 -0
- package/lib/qa/runs.mjs +1 -0
- package/lib/qa/schema.mjs +1 -0
- package/lib/qa/serve.mjs +4 -0
- package/lib/qa/server.mjs +1 -0
- package/lib/qa/source-snapshot.mjs +3 -0
- package/lib/qa/stability.mjs +3 -0
- package/lib/qa/state-evidence.mjs +1 -0
- package/lib/qa/sweep-incremental.mjs +1 -0
- package/lib/qa/triage.mjs +2 -0
- package/lib/qa/verdict.mjs +1 -0
- package/lib/qa/watch.mjs +2 -0
- package/package.json +62 -0
- package/references/ATLAS-COVERAGE.md +231 -0
- package/references/atlas-v1.md +318 -0
- package/references/atlas.md +338 -0
- package/references/verify.crud.example.json +1 -0
- package/references/verify.impact.example.json +1 -0
- package/references/verify.shadow.example.json +1 -0
- package/schemas/agent-result.schema.json +135 -0
- package/schemas/analysis.schema.json +15 -0
- package/schemas/classify.schema.json +61 -0
- package/schemas/coherence-ir.schema.json +1585 -0
- package/schemas/coherence-observed.schema.json +1171 -0
- package/schemas/coherence-plan.schema.json +77 -0
- package/schemas/coherence-risk.schema.json +46 -0
- package/schemas/crud-observed.schema.json +118 -0
- package/schemas/experiment-execution.schema.json +82 -0
- package/schemas/graph-edge.schema.json +22 -0
- package/schemas/graph-node.schema.json +81 -0
- package/schemas/graph.schema.json +2493 -0
- package/schemas/identity-oracle.schema.json +19 -0
- package/schemas/inventory.schema.json +2167 -0
- package/schemas/lanes-plan.schema.json +52 -0
- package/schemas/next.schema.json +54 -0
- package/schemas/personas.schema.json +52 -0
- package/schemas/research.schema.json +192 -0
- package/schemas/run.schema.json +160 -0
- package/schemas/shadow-replay.schema.json +68 -0
- package/schemas/stability.schema.json +92 -0
- package/schemas/sweep.schema.json +239 -0
- package/schemas/v1/agent-result.schema.json +106 -0
- package/schemas/v1/run.schema.json +96 -0
- package/schemas/verify-crud.schema.json +37 -0
- package/schemas/verify-read-only.schema.json +37 -0
- package/templates/github-workflow.yml +80 -0
- package/templates/pre-commit.sample +22 -0
- 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
|
+
[](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.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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);
|
package/bin/classify.mjs
ADDED
|
@@ -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};
|