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
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://frontend-verify.dev/schemas/verify-read-only.schema.json",
4
+ "title": "verify.read-only.json",
5
+ "description": "The repository's declaration that specific POST paths read rather than write, so the forced-state pass may substitute a response for them. Committed, explicit, and exhaustive: every entry is one literal path, never a pattern -- a wildcard could sweep a real write into a lane that replays it. A declared path is added to a cell's apiTargets for the forced-state pass ONLY; nothing else in qa replays it, and no other method is declarable. See lib/qa/forced-states.mjs loadReadOnlyPosts() and AGENTS.md, section Run inputs, writes, and coverage.",
6
+ "type": "object",
7
+ "required": ["schema_version", "read_only_posts"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "schema_version": { "const": 1 },
11
+ "read_only_posts": {
12
+ "type": "array",
13
+ "minItems": 1,
14
+ "maxItems": 128,
15
+ "items": {
16
+ "type": "object",
17
+ "required": ["path", "why"],
18
+ "additionalProperties": false,
19
+ "properties": {
20
+ "path": {
21
+ "description": "One literal, absolute request path. No wildcard, query string, or fragment: a pattern is not a declaration.",
22
+ "type": "string",
23
+ "minLength": 2,
24
+ "maxLength": 512,
25
+ "pattern": "^/[^*?#]{1,511}$"
26
+ },
27
+ "why": {
28
+ "description": "Why this POST changes nothing. Written for the reviewer who has to believe it, not for the parser.",
29
+ "type": "string",
30
+ "minLength": 1,
31
+ "maxLength": 512
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,80 @@
1
+ name: frontend-verify
2
+
3
+ on:
4
+ pull_request:
5
+
6
+ permissions:
7
+ contents: read
8
+
9
+ env:
10
+ AUTOMATED_QA_REPO: https://github.com/ArkashJ/Automated_QA
11
+ AUTOMATED_QA_REF: main
12
+
13
+ jobs:
14
+ static:
15
+ name: frontend-verify / static gate
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Set up Node
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: 20
25
+
26
+ - name: Clone frontend-verify
27
+ run: git clone --depth 1 --branch "$AUTOMATED_QA_REF" "$AUTOMATED_QA_REPO" "$RUNNER_TEMP/automated-qa"
28
+
29
+ - name: Static gate
30
+ run: node "$RUNNER_TEMP/automated-qa/bin/qa.mjs" verify "$GITHUB_WORKSPACE"
31
+
32
+ # This job always starts (GitHub Actions cannot read `secrets` directly in a
33
+ # job-level `if:`), but every step below is gated on FRONTEND_VERIFY_BASE
34
+ # being set, so it is a no-op until that secret exists.
35
+ runtime:
36
+ name: frontend-verify / runtime gate
37
+ runs-on: ubuntu-latest
38
+ env:
39
+ FRONTEND_VERIFY_BASE: ${{ secrets.FRONTEND_VERIFY_BASE }}
40
+ permissions:
41
+ contents: read
42
+ security-events: write
43
+ steps:
44
+ - name: Checkout
45
+ if: ${{ env.FRONTEND_VERIFY_BASE != '' }}
46
+ uses: actions/checkout@v4
47
+
48
+ - name: Set up Node
49
+ if: ${{ env.FRONTEND_VERIFY_BASE != '' }}
50
+ uses: actions/setup-node@v4
51
+ with:
52
+ node-version: 20
53
+
54
+ - name: Clone frontend-verify
55
+ if: ${{ env.FRONTEND_VERIFY_BASE != '' }}
56
+ run: git clone --depth 1 --branch "$AUTOMATED_QA_REF" "$AUTOMATED_QA_REPO" "$RUNNER_TEMP/automated-qa"
57
+
58
+ - name: Install Chromium for Playwright
59
+ if: ${{ env.FRONTEND_VERIFY_BASE != '' }}
60
+ run: npx playwright install chromium
61
+
62
+ - name: Runtime gate
63
+ id: gate
64
+ if: ${{ env.FRONTEND_VERIFY_BASE != '' }}
65
+ continue-on-error: true
66
+ run: node "$RUNNER_TEMP/automated-qa/bin/qa.mjs" verify "$GITHUB_WORKSPACE" --base "$FRONTEND_VERIFY_BASE" --prod
67
+
68
+ - name: Generate SARIF
69
+ if: ${{ env.FRONTEND_VERIFY_BASE != '' }}
70
+ run: node "$RUNNER_TEMP/automated-qa/bin/qa.mjs" show ci "$GITHUB_WORKSPACE" --format sarif --out "$RUNNER_TEMP/frontend-verify.sarif"
71
+
72
+ - name: Upload SARIF
73
+ if: ${{ env.FRONTEND_VERIFY_BASE != '' }}
74
+ uses: github/codeql-action/upload-sarif@v3
75
+ with:
76
+ sarif_file: ${{ runner.temp }}/frontend-verify.sarif
77
+
78
+ - name: Fail if the runtime gate found findings
79
+ if: ${{ env.FRONTEND_VERIFY_BASE != '' && steps.gate.outcome == 'failure' }}
80
+ run: exit 1
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ # frontend-verify pre-commit gate.
3
+ #
4
+ # Copy this to .git/hooks/pre-commit (or wherever your hook manager reads
5
+ # samples from, e.g. .husky/pre-commit) and make it executable:
6
+ # cp .verify-hooks/pre-commit.sample .git/hooks/pre-commit
7
+ # chmod +x .git/hooks/pre-commit
8
+ #
9
+ # Static only (fast; runs on every commit). Run it bare — its own exit status
10
+ # is the gate: 0 clean, 1 findings, 2 could not run. Do not append anything
11
+ # after `||` that does not end in `exit 1`, or a failure reads as green.
12
+ set -euo pipefail
13
+
14
+ AUTOMATED_QA="${FRONTEND_VERIFY_SKILL_ROOT:-$HOME/.claude/skills/frontend-verify}"
15
+ REPO="$(git rev-parse --show-toplevel)"
16
+
17
+ if [ ! -d "$AUTOMATED_QA" ]; then
18
+ echo "frontend-verify: $AUTOMATED_QA not found (set FRONTEND_VERIFY_SKILL_ROOT); skipping" >&2
19
+ exit 0
20
+ fi
21
+
22
+ bash "$AUTOMATED_QA/verify.sh" "$REPO"
package/verify.sh ADDED
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env bash
2
+ # One command. Inventory -> classify -> sweep, in that order, cheapest first.
3
+ #
4
+ # verify.sh <repoRoot> [--base URL] [--lane LANE] [--width N] [--quiet]
5
+ # [--mutate] [--ratchet] [--resume] [--prod] [--no-warm]
6
+ # [--widths 390,1440] [--states [empty,error,...]]
7
+ # [--exercise [budget]] [--no-journeys]
8
+ # [--capture [--capture-full] [--capture-dir DIR]]
9
+ #
10
+ # --lane one of source/public-runtime/mock-contract/real-integration/
11
+ # mfa-integration; defaults to public-runtime when --base is
12
+ # given. The two real lanes provision and verify every declared
13
+ # persona through the host-harness launcher (verify.auth-
14
+ # launcher.mjs) and its identity oracle BEFORE measuring anything
15
+ # -- see ARCHITECTURE.md. --auth/--login/--login-user/--login-pass/--login-path
16
+ # are removed; there is no credential flag any more.
17
+ #
18
+ # verify.sh /path/to/repo static only, seconds, no install
19
+ # verify.sh /path/to/repo --base http://localhost:3000 + the runtime sweep
20
+ #
21
+ # --mutate DESTRUCTIVE, opt-in: allows an app-owned journey declaring
22
+ # mutates:true to write. Generic guessed replay is disabled and
23
+ # fails closed. Dev database only.
24
+ # --ratchet fix-loop guard: after the measured source changes, total findings
25
+ # may never exceed the best run seen (.verify/ratchet.json). Repeated
26
+ # runtime measurements of identical source may vary without becoming
27
+ # a fabricated source regression; the best still tightens.
28
+ # --resume continue an aborted sweep instead of restarting it; already
29
+ # measured routes are kept, unmeasured ones are re-visited.
30
+ # --prod the base URL is a PRODUCTION build, so timing findings grade
31
+ # normally. Without it they are P3: against a dev server the number
32
+ # measured is the compiler's, not the app's.
33
+ # --no-warm skip the precompile pass. The warm pass exists because a dev
34
+ # server compiles a route on first request (measured: 30.5s) and a
35
+ # mock service worker does not intercept until it controls a page.
36
+ # --capture photograph every measured cell and index the images beside what
37
+ # was measured for that page (.verify/capture/index.html). OFF by
38
+ # default -- a screenshot of an authenticated app is user data.
39
+ # --capture-full takes the whole page; --capture-dir relocates it.
40
+ # --delay MS pause this long before each cell's navigation. Against a shared,
41
+ # rate-limited target the sweep's OWN request volume can trip the
42
+ # target's rate limiter -- --parallel already defaults to 1
43
+ # (serial), so this is the remaining lever to slow the sweep down.
44
+ #
45
+ # Multiple principals: put <repo>/verify.roles.json beside the repo and each
46
+ # role sweeps only the routes it OWNS -- ownership and expected identity
47
+ # only now, never a credential store. See "Roles" in SKILL.md.
48
+ #
49
+ # Nothing else may build while the sweep runs -- a concurrent web build starves
50
+ # the dev server and the whole run aborts as "server unreachable".
51
+ #
52
+ # Exit: 0 clean · 1 P0/P1 findings (or ratchet regression) · 2 could not run.
53
+ # That exit code is the definition of done -- it is what a Stop hook, a
54
+ # pre-commit hook or CI reads. Everything else here is reporting.
55
+ set -uo pipefail
56
+
57
+ SKILL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
58
+ REPO=""; BASE=""; WIDTH=""; QUIET=0; MUTATE=0; RATCHET=0; PARALLEL=""; PASSTHRU=()
59
+ while [ $# -gt 0 ]; do
60
+ case "$1" in
61
+ --base) BASE="${2:-}"; shift 2 ;;
62
+ # Removed: a credential value must never be an argv value. Fail loudly and
63
+ # name the replacement, not "unknown flag" -- an operator who used this
64
+ # yesterday needs to hear where it went.
65
+ --auth|--login|--login-user|--login-pass|--login-path)
66
+ echo "verify: --${1#--} was removed -- use the host-harness launcher, invoked by persona name (verify.auth-launcher.mjs)" >&2
67
+ exit 2 ;;
68
+ --width) WIDTH="${2:-}"; shift 2 ;;
69
+ --parallel) PARALLEL="${2:-}"; shift 2 ;;
70
+ --quiet) QUIET=1; shift ;;
71
+ --mutate) MUTATE=1; shift ;;
72
+ --ratchet) RATCHET=1; shift ;;
73
+ # Straight through to sweep.mjs. --resume continues an aborted sweep instead
74
+ # of restarting it, --prod says the base URL is a production build so timing
75
+ # findings grade normally, --no-warm skips the precompile pass.
76
+ --resume|--prod|--no-warm|--no-journeys|--leak-check|--dark) PASSTHRU+=( "$1" ); shift ;;
77
+ # --capture/--capture-full photograph every measured cell. Opt-in, and they
78
+ # must reach sweep.mjs: without these arms the flag fell through to the
79
+ # positional branch below and was dropped in silence, so `qa verify --capture`
80
+ # measured normally and wrote no pixels while claiming the flag was accepted.
81
+ --capture|--capture-full) PASSTHRU+=( "$1" ); shift ;;
82
+ --lane|--launcher|--launcher-timeout|--oracle-path|--capture-dir|--theme-storage-key) PASSTHRU+=( "$1" "${2:-}" ); shift 2 ;;
83
+ --auth-lease-fd)
84
+ [ -n "${2:-}" ] || { echo 'verify: --auth-lease-fd needs a value' >&2; exit 2; }
85
+ PASSTHRU+=( "$1" "$2" ); shift 2 ;;
86
+ # --exercise [budget] click every visible, enabled control on a measured
87
+ # cell (default budget 40) and grade whether it does
88
+ # anything. Bare --exercise uses the default budget.
89
+ --exercise) PASSTHRU+=( "$1" ); case "${2:-}" in ''|--*) ;; *) PASSTHRU+=( "$2" ); shift ;; esac; shift ;;
90
+ --nav-timeout|--warm-timeout|--settle|--delay) PASSTHRU+=( "$1" "${2:-}" ); shift 2 ;;
91
+ # --widths 390,1440 the cell matrix: every route at every width. Half the
92
+ # invariants here are width-dependent, so a one-width run
93
+ # grades an app nobody uses at one width.
94
+ # --states [kinds] force empty/error/forbidden/malformed/slow on the data
95
+ # each route actually fetches, and grade what it renders.
96
+ # Bare --states runs all five.
97
+ --widths|--text-floor|--integrity-share) PASSTHRU+=( "$1" "${2:-}" ); shift 2 ;;
98
+ --states) PASSTHRU+=( "$1" ); case "${2:-}" in ''|--*) ;; *) PASSTHRU+=( "$2" ); shift ;; esac; shift ;;
99
+ -h|--help) sed -n '2,40p' "$0"; exit 0 ;;
100
+ *) [ -z "$REPO" ] && REPO="$1" || true; shift ;;
101
+ esac
102
+ done
103
+
104
+ [ -z "$REPO" ] && REPO="$PWD"
105
+ REPO="$(cd "$REPO" 2>/dev/null && pwd)" || { echo "verify: no such directory" >&2; exit 2; }
106
+ command -v node >/dev/null || { echo "verify: node not found" >&2; exit 2; }
107
+
108
+ OUT="$REPO/.verify"
109
+ mkdir -p "$OUT"
110
+ say() { [ "$QUIET" -eq 1 ] || printf '%s\n' "$*"; }
111
+ rc=0
112
+
113
+ # The JSON declarations are trust-boundary inputs. Use the same loaders and
114
+ # schema validator as `qa verify --preflight` before inventory starts, so a bad
115
+ # roles/CRUD file cannot spend a scan or reach the runtime sweep. This checks
116
+ # no journey module: app-owned JavaScript is untrusted and only opens when a
117
+ # runtime journey phase actually needs it.
118
+ if ! node --input-type=module -e '
119
+ import { pathToFileURL } from "node:url";
120
+ const { checkInputDeclarations } = await import(pathToFileURL(process.argv[2] + "/lib/qa/doctor.mjs"));
121
+ const failed = checkInputDeclarations(process.argv[1], process.argv[2]).filter((check) => check.level === "fail");
122
+ for (const check of failed) {
123
+ console.error(`verify: preflight ${check.id}: ${check.detail}`);
124
+ if (check.fix) console.error(`verify: preflight fix: ${check.fix}`);
125
+ }
126
+ process.exit(failed.length ? 2 : 0);
127
+ ' "$REPO" "$SKILL" >"$OUT/preflight.log" 2>&1; then
128
+ cat "$OUT/preflight.log" >&2
129
+ echo "verify: preflight failed before inventory" >&2
130
+ exit 2
131
+ fi
132
+
133
+ say ""
134
+ say " frontend-verify $REPO"
135
+
136
+ # --- 1. inventory (always; also the route list the sweep uses) --------------
137
+ if ! node "$SKILL/bin/inventory.mjs" "$REPO" >"$OUT/inventory.log" 2>&1; then
138
+ cat "$OUT/inventory.log" >&2; echo "verify: inventory failed" >&2; exit 2
139
+ fi
140
+ [ "$QUIET" -eq 1 ] || sed -n '2,5p' "$OUT/inventory.log"
141
+ if ! node --input-type=module -e '
142
+ import fs from "node:fs"; import { pathToFileURL } from "node:url";
143
+ const { shapes } = await import(pathToFileURL(process.argv[1]));
144
+ let value; try { value = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); } catch { process.exit(1); }
145
+ const problem = shapes.inventory(value);
146
+ if (problem) { console.error("verify: inventory report is invalid: " + problem); process.exit(1); }
147
+ ' "$SKILL/lib/qa/reports.mjs" "$OUT/inventory.json"; then
148
+ echo "verify: inventory did not produce valid evidence" >&2
149
+ exit 2
150
+ fi
151
+
152
+ # A sync risk that names the routes it breaks is a real defect, and the only one
153
+ # of the three phases that does not already exit non-zero on its own.
154
+ if [ "$QUIET" -eq 1 ]; then
155
+ node -e 'try { process.exit(require(process.argv[1]).syncRisks.some((r) => r.severity === "P1") ? 1 : 0) } catch { process.exit(0) }' "$OUT/inventory.json" || rc=1
156
+ else
157
+ node -e '
158
+ let p1 = [];
159
+ try { p1 = require(process.argv[1]).syncRisks.filter((r) => r.severity === "P1") } catch { process.exit(0) }
160
+ if (!p1.length) process.exit(0);
161
+ console.log("");
162
+ console.log(" " + p1.length + " P1 sync risk(s) -- a write with no invalidation, on routes that render it:");
163
+ for (const r of p1.slice(0, 12)) console.log(" " + r.detail);
164
+ if (p1.length > 12) console.log(" ... and " + (p1.length - 12) + " more in .verify/inventory.json");
165
+ process.exit(1);
166
+ ' "$OUT/inventory.json" || rc=1
167
+ fi
168
+
169
+ # --- 2. classify (always) ---------------------------------------------------
170
+ # Delete before running, exactly as autonomous-verify.sh quarantines a stale
171
+ # sweep.json. classify.mjs exits 1 for "found findings" AND 1 for an uncaught
172
+ # exception, so a crash was indistinguishable from a clean detection -- and the
173
+ # PREVIOUS run's classify.json stayed on disk, got archived as `produced:
174
+ # classify true`, and its findings were reported as this run's. Stale evidence
175
+ # presented as current is worse than no evidence: it is the one failure mode
176
+ # that survives review, because the report looks entirely normal.
177
+ rm -f "$OUT/classify.json"
178
+ node "$SKILL/bin/classify.mjs" "$REPO" >"$OUT/classify.log" 2>&1
179
+ cls=$?
180
+ [ "$QUIET" -eq 1 ] || sed -n '2,40p' "$OUT/classify.log"
181
+ # No report on disk means the classifier did not complete, whatever it exited
182
+ # with. That is INVALID -- could not run -- never "clean" and never "findings".
183
+ if [ ! -f "$OUT/classify.json" ]; then
184
+ echo "" >&2
185
+ echo " INVALID classify produced no report (exit $cls)" >&2
186
+ echo " The static classifier could not complete, so its defect classes are" >&2
187
+ echo " UNMEASURED, not clean. See $OUT/classify.log" >&2
188
+ exit 2
189
+ fi
190
+ [ "$cls" -ne 0 ] && rc=1
191
+
192
+ # Optional repo-owned rule packs. The baseline remains dependency-free and
193
+ # unchanged; packs are explicit, versioned modules under .verify/rules/.
194
+ if [ -d "$REPO/.verify/rules" ]; then
195
+ node "$SKILL/bin/rules.mjs" "$REPO" >"$OUT/rules.log" 2>&1
196
+ packs=$?
197
+ [ "$QUIET" -eq 1 ] || cat "$OUT/rules.log"
198
+ case "$packs" in
199
+ 0) ;;
200
+ 1) rc=1 ;;
201
+ 2) exit 2 ;;
202
+ *) exit 2 ;;
203
+ esac
204
+ fi
205
+
206
+ # --- 3. sweep (only with --base; needs the app running) ---------------------
207
+ if [ -n "$BASE" ]; then
208
+ args=( --repo "$REPO" --base "$BASE" )
209
+ [ -n "$WIDTH" ] && args+=( --width "$WIDTH" )
210
+ [ -n "$PARALLEL" ] && args+=( --parallel "$PARALLEL" )
211
+ [ "$MUTATE" -eq 1 ] && args+=( --mutate )
212
+ [ ${#PASSTHRU[@]} -gt 0 ] && args+=( "${PASSTHRU[@]}" )
213
+ # Keep the previous report available to --resume, but never accept it as
214
+ # this invocation's evidence if the child exits before publishing a report.
215
+ sweep_started="$(mktemp "$OUT/.sweep-start.XXXXXX")" || exit 2
216
+ sweep_before=""
217
+ if [ -f "$OUT/sweep.json" ]; then
218
+ sweep_before="$(node -e 'const fs=require("fs"),crypto=require("crypto"); process.stdout.write(crypto.createHash("sha256").update(fs.readFileSync(process.argv[1])).digest("hex"))' "$OUT/sweep.json")"
219
+ fi
220
+ printf 'QA_SWEEP_STARTED\n' >"$OUT/sweep.log"
221
+ node "$SKILL/bin/sweep.mjs" "${args[@]}" >>"$OUT/sweep.log" 2>&1
222
+ sw=$?
223
+ printf '\nQA_SWEEP_RC=%s\n' "$sw" >>"$OUT/sweep.log"
224
+ fresh=0
225
+ if [ -f "$OUT/sweep.json" ]; then
226
+ sweep_after="$(node -e 'const fs=require("fs"),crypto=require("crypto"); process.stdout.write(crypto.createHash("sha256").update(fs.readFileSync(process.argv[1])).digest("hex"))' "$OUT/sweep.json")"
227
+ { [ -z "$sweep_before" ] || [ "$sweep_after" != "$sweep_before" ]; } && fresh=1
228
+ fi
229
+ [ "$QUIET" -eq 1 ] || sed -n '2,40p' "$OUT/sweep.log"
230
+ # A crash can exit 1 just like a finding; a wrapper can even exit 0 with
231
+ # nothing written. Require completed evidence as well as a recognized exit.
232
+ if { [ "$sw" -ne 0 ] && [ "$sw" -ne 1 ]; } || [ "$fresh" -ne 1 ] || ! node -e '
233
+ try {
234
+ const s = require(process.argv[1]), since = Math.floor(require("fs").statSync(process.argv[2]).mtimeMs);
235
+ const started = Date.parse(s.started), finished = Date.parse(s.finished);
236
+ const failures = [
237
+ [s.status === "invalid", "status-invalid"],
238
+ [s.integrity?.ok !== true, "integrity"],
239
+ [!Number.isFinite(started) || !Number.isFinite(finished), "timestamps"],
240
+ [finished + 1000 < since, "stale-finished"],
241
+ [finished < started, "time-order"],
242
+ [!Array.isArray(s.routes) || !Array.isArray(s.unreached), "route-arrays"],
243
+ [!Number.isInteger(s.summary?.routesReached) || !Number.isInteger(s.summary?.routesRequested), "route-counts"],
244
+ [s.summary?.routesReached <= 0 || s.summary?.routesReached > s.summary?.routesRequested, "route-denominator"],
245
+ ].filter(([failed]) => failed).map(([, code]) => code);
246
+ if (failures.length) {
247
+ console.error("sweep evidence invalid: " + failures.join(","));
248
+ process.exit(1);
249
+ }
250
+ } catch { console.error("sweep evidence invalid: unreadable"); process.exit(1); }
251
+ ' "$OUT/sweep.json" "$sweep_started"; then
252
+ rm -f "$sweep_started"
253
+ echo "verify: INVALID sweep did not produce fresh completed evidence (exit $sw) -- see $OUT/sweep.log" >&2
254
+ exit 2
255
+ fi
256
+ rm -f "$sweep_started"
257
+ runtime_counts=$(node --input-type=module -e '
258
+ import fs from "node:fs"; import { pathToFileURL } from "node:url";
259
+ const { runtimeGateSummary } = await import(pathToFileURL(process.argv[1]));
260
+ const sweep = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
261
+ const c = runtimeGateSummary(sweep, { repo: process.argv[3] });
262
+ process.stdout.write([c.raw, c.accepted, c.live, c.gating].join(" "));
263
+ ' "$SKILL/lib/qa/reports.mjs" "$OUT/sweep.json" "$REPO") || {
264
+ echo "verify: runtime adjudication failed; findings remain unmeasured" >&2
265
+ exit 2
266
+ }
267
+ read -r runtime_raw runtime_accepted runtime_live runtime_gating <<<"$runtime_counts"
268
+ if [ "${runtime_accepted:-0}" -gt 0 ]; then
269
+ say " runtime findings $runtime_raw raw · $runtime_accepted adjudicated · $runtime_live active"
270
+ fi
271
+ if [ "${runtime_gating:-0}" -gt 0 ] || { [ "$sw" -ne 0 ] && [ "${runtime_accepted:-0}" -eq 0 ]; }; then rc=1; fi
272
+ else
273
+ say ""
274
+ say " runtime sweep skipped (no --base). Static analysis cannot see whether the"
275
+ say " code runs -- pass --base http://localhost:3000 with the app up to cover that."
276
+ fi
277
+
278
+ # PASS must mean "measured, and clean" -- never "measured nothing". A repo with no
279
+ # routes is one the tool did not understand, and reporting it green is the exact
280
+ # failure this whole skill exists to prevent.
281
+ ROUTES=$(node -e 'try { console.log(require(process.argv[1]).counts.routes) } catch { console.log(0) }' "$OUT/inventory.json")
282
+ if [ "${ROUTES:-0}" -eq 0 ]; then
283
+ echo "" >&2
284
+ echo " INCONCLUSIVE 0 routes found in $REPO" >&2
285
+ echo " Nothing was measured, so this is not a pass. Either this is not a frontend" >&2
286
+ echo " repo, or its router is not one inventory.mjs recognises (Next app/pages," >&2
287
+ echo " react-router config, or a src/routes file convention)." >&2
288
+ exit 2
289
+ fi
290
+
291
+ # --- ratchet (opt-in): a fix loop that trades one finding for two is going
292
+ # backwards, and an agent mid-loop will not notice on its own. After source changes,
293
+ # total findings (every severity, all phases) may never exceed the best run seen;
294
+ # repeated runtime measurements of the exact same source may vary without being
295
+ # mislabeled as a source regression. The baseline still tightens on every
296
+ # improvement. Static-only and runtime runs measure different things, so a mode
297
+ # switch resets the baseline instead of comparing them.
298
+ if [ "$RATCHET" -eq 1 ]; then
299
+ ROUT=/dev/stdout; [ "$QUIET" -eq 1 ] && ROUT=/dev/null
300
+ node --input-type=module -e '
301
+ import fs from "node:fs"; import { pathToFileURL } from "node:url";
302
+ const { ratchetUpdate } = await import(pathToFileURL(process.argv[1]));
303
+ const out = process.argv[2], mode = process.argv[3], repo = process.argv[4];
304
+ const j = (f) => { try { return JSON.parse(fs.readFileSync(out + "/" + f, "utf8")); } catch { return null; } };
305
+ const inv = j("inventory.json"), cls = j("classify.json"), sw = mode === "runtime" ? j("sweep.json") : null;
306
+ // Exit 2 is never a pass, so it can never be a baseline either (#80): the
307
+ // sweep step above already exits 2 -- before this block runs at all -- on
308
+ // integrity failure, so this is a second, explicit guard on the same
309
+ // invariant rather than one this block relies on the shell script above
310
+ // to enforce silently. A run that did not measure the app must never
311
+ // tighten (or loosen) what "best" means.
312
+ if (sw && sw.integrity && sw.integrity.ok === false) {
313
+ console.error(" ratchet skipped -- this run failed its own integrity gate and can never become the baseline");
314
+ process.exit(1);
315
+ }
316
+ const file = out + "/ratchet.json";
317
+ const prev = j("ratchet.json");
318
+ const next = ratchetUpdate({ inventory: inv, classify: cls, sweep: sw, mode, previous: prev, repo });
319
+ if (next.regression) {
320
+ console.error(" RATCHET " + next.total + " active findings, best was " + next.best + " in the same measured scope -- this change went backwards; fix or revert it");
321
+ process.exit(1);
322
+ }
323
+ fs.writeFileSync(file, JSON.stringify(next.state, null, 2));
324
+ console.log(" ratchet " + next.total + " active findings (best " + next.best + ", " + next.accepted + " adjudicated, scope " + next.scope.slice(-12) + ")");
325
+ ' "$SKILL/lib/qa/reports.mjs" "$OUT" "$([ -n "$BASE" ] && echo runtime || echo static)" "$REPO" >"$ROUT" || rc=1
326
+ fi
327
+
328
+ # PASS must carry the DENOMINATOR. "No P0/P1 findings" over 41 of 51 routes and
329
+ # over 51 of 51 are different claims, and printing them identically is how a
330
+ # partial run gets read as a clean one -- the exact failure this skill exists for.
331
+ # The verdict LEADS with its denominator, and the unmeasured routes are broken
332
+ # out by reason directly beneath it.
333
+ #
334
+ # The line this replaces read "PASS 51 routes analysed, no P0/P1 findings" with
335
+ # the coverage appended after a middot. Two things were wrong with that, and
336
+ # both are the failure this whole tool exists to prevent:
337
+ # - "51 routes analysed" counted the INVENTORY while the findings covered only
338
+ # the 41 the sweep attempted, so the headline number and the claim beneath it
339
+ # had different denominators;
340
+ # - the coverage rode at the END of the line, after the verdict a reader has
341
+ # already acted on.
342
+ # SKILL.md states the contract as "no P0/P1 over 41 of 51 routes" -- so that is
343
+ # the literal shape printed here, and every unmeasured route says WHY it was not
344
+ # measured and what would measure it. All numbers come from sweep.json's
345
+ # `summary`, the same single source lib/qa/depth.mjs reads; nothing is
346
+ # recomputed here, so the two can never disagree.
347
+ COVER=""
348
+ UNMEASURED=""
349
+ if [ -n "$BASE" ]; then
350
+ COVER=$(node -e '
351
+ try {
352
+ const s = require(process.argv[1]).summary ?? {};
353
+ if (s.routesRequested == null) process.exit(0);
354
+ // The denominator is what the INVENTORY found, not what the sweep chose
355
+ // to attempt. Printing "41 of 41" for a 51-route app because ten routes
356
+ // carry a dynamic segment is that same failure, committed by the line
357
+ // written to prevent it.
358
+ const denom = s.routesInInventory ?? s.routesRequested;
359
+ process.stdout.write(" over " + s.routesReached + " of " + denom + " routes");
360
+ } catch {}
361
+ ' "$OUT/sweep.json" 2>/dev/null)
362
+ UNMEASURED=$(node -e '
363
+ try {
364
+ const s = require(process.argv[1]).summary ?? {};
365
+ if (s.routesRequested == null) process.exit(0);
366
+ const rows = [
367
+ [s.routesDynamicSkipped ?? 0, "dynamic segment -- promote .verify/journeys.draft.mjs"],
368
+ [s.routesUnreached ?? 0, "redirected -- landed somewhere else, so this route is a coverage gap"],
369
+ [s.routesUnowned ?? 0, "unowned -- no role in verify.roles.json claims it"],
370
+ ].filter(([n]) => n > 0);
371
+ for (const [n, why] of rows) {
372
+ process.stdout.write("\n " + String(n).padStart(3) + " NOT MEASURED " + why);
373
+ }
374
+ } catch {}
375
+ ' "$OUT/sweep.json" 2>/dev/null)
376
+ else
377
+ COVER=" over $ROUTES routes, STATICALLY ONLY"
378
+ UNMEASURED="
379
+ no runtime invariant was measured -- pass --base URL with the app running"
380
+ fi
381
+
382
+ say ""
383
+ if [ "$rc" -eq 0 ]; then say " PASS no P0/P1 findings$COVER$UNMEASURED"
384
+ else say " FAIL P0/P1 findings above$COVER · reports in $OUT/$UNMEASURED"; fi
385
+ exit "$rc"