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
|
@@ -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"
|