harnery 0.36.0 → 0.37.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/dist/commander.js +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +87 -46
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +26 -1
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +11 -0
- package/dist/commands/diagnostics.d.ts +4 -0
- package/dist/commands/diagnostics.d.ts.map +1 -0
- package/dist/commands/diagnostics.js +229 -0
- package/dist/commands/files.d.ts +12 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -1
- package/dist/commands/governor.d.ts.map +1 -1
- package/dist/commands/governor.js +4 -0
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +71 -2
- package/dist/commands/qa-run.d.ts +4 -0
- package/dist/commands/qa-run.d.ts.map +1 -0
- package/dist/commands/qa-run.js +141 -0
- package/dist/commands/resources.d.ts +4 -0
- package/dist/commands/resources.d.ts.map +1 -0
- package/dist/commands/resources.js +29 -0
- package/dist/commands/supervisor.d.ts +4 -0
- package/dist/commands/supervisor.d.ts.map +1 -0
- package/dist/commands/supervisor.js +101 -0
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +17 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +4 -0
- package/dist/core/agents/cli.js +7 -3
- package/dist/core/agents/health.d.ts +17 -0
- package/dist/core/agents/health.d.ts.map +1 -0
- package/dist/core/agents/health.js +109 -0
- package/dist/core/agents/mailbox.d.ts +92 -0
- package/dist/core/agents/mailbox.d.ts.map +1 -0
- package/dist/core/agents/mailbox.js +325 -0
- package/dist/core/agents/reconcile-coordination-v3.d.ts +44 -0
- package/dist/core/agents/reconcile-coordination-v3.d.ts.map +1 -0
- package/dist/core/agents/reconcile-coordination-v3.js +58 -0
- package/dist/core/agents/render/prompt-context.d.ts +5 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +34 -13
- package/dist/core/agents/render/session-context.d.ts +8 -0
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +34 -3
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +50 -1
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +6 -10
- package/dist/core/agents/session-finalizer-v3.js +4 -1
- package/dist/core/artifacts/index.d.ts +26 -1
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +146 -6
- package/dist/core/config.d.ts +12 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +43 -0
- package/dist/core/diagnostics/advice.d.ts +9 -0
- package/dist/core/diagnostics/advice.d.ts.map +1 -0
- package/dist/core/diagnostics/advice.js +112 -0
- package/dist/core/diagnostics/bundle.d.ts +35 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -0
- package/dist/core/diagnostics/bundle.js +874 -0
- package/dist/core/diagnostics/comparison.d.ts +5 -0
- package/dist/core/diagnostics/comparison.d.ts.map +1 -0
- package/dist/core/diagnostics/comparison.js +444 -0
- package/dist/core/diagnostics/contract.d.ts +245 -0
- package/dist/core/diagnostics/contract.d.ts.map +1 -0
- package/dist/core/diagnostics/contract.js +18 -0
- package/dist/core/diagnostics/identity.d.ts +3 -0
- package/dist/core/diagnostics/identity.d.ts.map +1 -0
- package/dist/core/diagnostics/identity.js +10 -0
- package/dist/core/diagnostics/index.d.ts +8 -0
- package/dist/core/diagnostics/index.d.ts.map +1 -0
- package/dist/core/diagnostics/index.js +7 -0
- package/dist/core/diagnostics/replay.d.ts +8 -0
- package/dist/core/diagnostics/replay.d.ts.map +1 -0
- package/dist/core/diagnostics/replay.js +213 -0
- package/dist/core/diagnostics/sanitize.d.ts +9 -0
- package/dist/core/diagnostics/sanitize.d.ts.map +1 -0
- package/dist/core/diagnostics/sanitize.js +84 -0
- package/dist/core/events/legacy-storage/compression.d.ts +13 -0
- package/dist/core/events/legacy-storage/compression.d.ts.map +1 -0
- package/dist/core/events/legacy-storage/compression.js +107 -0
- package/dist/core/events/legacy-storage/index.d.ts +1 -0
- package/dist/core/events/legacy-storage/index.d.ts.map +1 -1
- package/dist/core/events/legacy-storage/index.js +1 -0
- package/dist/core/events/v3/archive-retention.d.ts +33 -0
- package/dist/core/events/v3/archive-retention.d.ts.map +1 -0
- package/dist/core/events/v3/archive-retention.js +195 -0
- package/dist/core/events/v3/capabilities.d.ts +1 -1
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +1 -0
- package/dist/core/events/v3/index.d.ts +1 -0
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +1 -0
- package/dist/core/events/v3/live-routing.d.ts.map +1 -1
- package/dist/core/events/v3/live-routing.js +1 -0
- package/dist/core/events/v3/producers/hook-base.d.ts +1 -1
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +13 -2
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +1 -0
- package/dist/core/events/v3/producers/recorder.d.ts +16 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +88 -1
- package/dist/core/hooks/adapter/output.d.ts +8 -0
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +11 -1
- package/dist/core/hooks/cli.js +183 -192
- package/dist/core/hooks/health.d.ts +59 -0
- package/dist/core/hooks/health.d.ts.map +1 -0
- package/dist/core/hooks/health.js +84 -0
- package/dist/core/resources/contract.d.ts +90 -0
- package/dist/core/resources/contract.d.ts.map +1 -0
- package/dist/core/resources/contract.js +2 -0
- package/dist/core/resources/index.d.ts +6 -0
- package/dist/core/resources/index.d.ts.map +1 -0
- package/dist/core/resources/index.js +5 -0
- package/dist/core/resources/sampler.d.ts +27 -0
- package/dist/core/resources/sampler.d.ts.map +1 -0
- package/dist/core/resources/sampler.js +357 -0
- package/dist/core/resources/service-status.d.ts +3 -0
- package/dist/core/resources/service-status.d.ts.map +1 -0
- package/dist/core/resources/service-status.js +48 -0
- package/dist/core/resources/service.d.ts +26 -0
- package/dist/core/resources/service.d.ts.map +1 -0
- package/dist/core/resources/service.js +292 -0
- package/dist/core/resources/storage.d.ts +13 -0
- package/dist/core/resources/storage.d.ts.map +1 -0
- package/dist/core/resources/storage.js +30 -0
- package/dist/core/storage/atomic-json.d.ts +3 -0
- package/dist/core/storage/atomic-json.d.ts.map +1 -0
- package/dist/core/storage/atomic-json.js +15 -0
- package/dist/core/storage/builtins.d.ts.map +1 -1
- package/dist/core/storage/builtins.js +45 -6
- package/dist/core/storage/logger.d.ts +2 -0
- package/dist/core/storage/logger.d.ts.map +1 -1
- package/dist/core/storage/logger.js +2 -0
- package/dist/core/storage/query.d.ts +14 -0
- package/dist/core/storage/query.d.ts.map +1 -1
- package/dist/core/storage/query.js +51 -0
- package/dist/core/supervisor/activity.d.ts +4 -0
- package/dist/core/supervisor/activity.d.ts.map +1 -0
- package/dist/core/supervisor/activity.js +141 -0
- package/dist/core/supervisor/contract.d.ts +268 -0
- package/dist/core/supervisor/contract.d.ts.map +1 -0
- package/dist/core/supervisor/contract.js +35 -0
- package/dist/core/supervisor/explanations.d.ts +4 -0
- package/dist/core/supervisor/explanations.d.ts.map +1 -0
- package/dist/core/supervisor/explanations.js +71 -0
- package/dist/core/supervisor/findings.d.ts +20 -0
- package/dist/core/supervisor/findings.d.ts.map +1 -0
- package/dist/core/supervisor/findings.js +399 -0
- package/dist/core/supervisor/history.d.ts +9 -0
- package/dist/core/supervisor/history.d.ts.map +1 -0
- package/dist/core/supervisor/history.js +46 -0
- package/dist/core/supervisor/hook-health-alerts.d.ts +14 -0
- package/dist/core/supervisor/hook-health-alerts.d.ts.map +1 -0
- package/dist/core/supervisor/hook-health-alerts.js +70 -0
- package/dist/core/supervisor/hook-health-storage.d.ts +5 -0
- package/dist/core/supervisor/hook-health-storage.d.ts.map +1 -0
- package/dist/core/supervisor/hook-health-storage.js +24 -0
- package/dist/core/supervisor/hook-health.d.ts +72 -0
- package/dist/core/supervisor/hook-health.d.ts.map +1 -0
- package/dist/core/supervisor/hook-health.js +203 -0
- package/dist/core/supervisor/hooks.d.ts +10 -0
- package/dist/core/supervisor/hooks.d.ts.map +1 -0
- package/dist/core/supervisor/hooks.js +28 -0
- package/dist/core/supervisor/index.d.ts +15 -0
- package/dist/core/supervisor/index.d.ts.map +1 -0
- package/dist/core/supervisor/index.js +14 -0
- package/dist/core/supervisor/log-feed.d.ts +9 -0
- package/dist/core/supervisor/log-feed.d.ts.map +1 -0
- package/dist/core/supervisor/log-feed.js +103 -0
- package/dist/core/supervisor/service.d.ts +29 -0
- package/dist/core/supervisor/service.d.ts.map +1 -0
- package/dist/core/supervisor/service.js +450 -0
- package/dist/core/supervisor/services.d.ts +11 -0
- package/dist/core/supervisor/services.d.ts.map +1 -0
- package/dist/core/supervisor/services.js +155 -0
- package/dist/core/supervisor/status.d.ts +3 -0
- package/dist/core/supervisor/status.d.ts.map +1 -0
- package/dist/core/supervisor/status.js +48 -0
- package/dist/core/supervisor/storage.d.ts +30 -0
- package/dist/core/supervisor/storage.d.ts.map +1 -0
- package/dist/core/supervisor/storage.js +73 -0
- package/dist/core/supervisor/timeline.d.ts +3 -0
- package/dist/core/supervisor/timeline.d.ts.map +1 -0
- package/dist/core/supervisor/timeline.js +87 -0
- package/dist/core/workflow/admission.d.ts +23 -0
- package/dist/core/workflow/admission.d.ts.map +1 -0
- package/dist/core/workflow/admission.js +136 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +77 -2
- package/dist/core/workflow/index.d.ts +3 -2
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -1
- package/dist/core/workflow/proof.d.ts +2 -1
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +39 -1
- package/dist/core/workflow/run-state.d.ts +3 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +8 -1
- package/dist/core/workflow/types.d.ts +33 -1
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/types.js +2 -1
- package/dist/lib/browser/client.d.ts +5 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +1 -0
- package/dist/lib/browser/index.d.ts +2 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +173 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -0
- package/dist/lib/browser/qa-run-contracts.js +226 -0
- package/dist/lib/browser/qa-run.d.ts +51 -0
- package/dist/lib/browser/qa-run.d.ts.map +1 -0
- package/dist/lib/browser/qa-run.js +684 -0
- package/dist/lib/coord-root-id.d.ts +9 -0
- package/dist/lib/coord-root-id.d.ts.map +1 -0
- package/dist/lib/coord-root-id.js +17 -0
- package/dist/lib/instructions/apply.d.ts +3 -1
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +74 -3
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +6 -0
- package/package.json +12 -1
- package/src/commander.ts +26 -0
- package/src/commands/agents.ts +102 -49
- package/src/commands/artifacts.ts +40 -2
- package/src/commands/browse.ts +15 -0
- package/src/commands/diagnostics.ts +271 -0
- package/src/commands/files.ts +102 -2
- package/src/commands/governor.ts +8 -0
- package/src/commands/ledger-v3.ts +82 -0
- package/src/commands/qa-run.ts +184 -0
- package/src/commands/resources.ts +38 -0
- package/src/commands/supervisor.ts +141 -0
- package/src/commands/web.ts +19 -0
- package/src/commands/workflow.ts +8 -0
- package/src/core/agents/cli.ts +7 -3
- package/src/core/agents/health.ts +142 -0
- package/src/core/agents/mailbox.ts +373 -0
- package/src/core/agents/reconcile-coordination-v3.ts +77 -0
- package/src/core/agents/render/prompt-context.ts +42 -12
- package/src/core/agents/render/session-context.ts +37 -3
- package/src/core/agents/rules/commit-conflict.ts +58 -1
- package/src/core/agents/rules/stop-hook.ts +6 -12
- package/src/core/agents/session-finalizer-v3.ts +4 -1
- package/src/core/artifacts/index.ts +196 -10
- package/src/core/config.ts +111 -2
- package/src/core/diagnostics/advice.ts +147 -0
- package/src/core/diagnostics/bundle.ts +1142 -0
- package/src/core/diagnostics/comparison.ts +565 -0
- package/src/core/diagnostics/contract.ts +311 -0
- package/src/core/diagnostics/identity.ts +11 -0
- package/src/core/diagnostics/index.ts +7 -0
- package/src/core/diagnostics/replay.ts +314 -0
- package/src/core/diagnostics/sanitize.ts +97 -0
- package/src/core/events/legacy-storage/compression.ts +130 -0
- package/src/core/events/legacy-storage/index.ts +1 -0
- package/src/core/events/v3/archive-retention.ts +251 -0
- package/src/core/events/v3/capabilities.ts +2 -0
- package/src/core/events/v3/index.ts +9 -0
- package/src/core/events/v3/live-routing.ts +1 -0
- package/src/core/events/v3/producers/hook-base.ts +14 -2
- package/src/core/events/v3/producers/intake.ts +1 -0
- package/src/core/events/v3/producers/recorder.ts +110 -1
- package/src/core/hooks/adapter/output.ts +12 -1
- package/src/core/hooks/cli.ts +192 -215
- package/src/core/hooks/health.ts +146 -0
- package/src/core/resources/contract.ts +98 -0
- package/src/core/resources/index.ts +5 -0
- package/src/core/resources/sampler.ts +458 -0
- package/src/core/resources/service-status.ts +62 -0
- package/src/core/resources/service.ts +337 -0
- package/src/core/resources/storage.ts +44 -0
- package/src/core/storage/atomic-json.ts +15 -0
- package/src/core/storage/builtins.ts +62 -6
- package/src/core/storage/logger.ts +2 -0
- package/src/core/storage/query.ts +58 -0
- package/src/core/supervisor/activity.ts +193 -0
- package/src/core/supervisor/contract.ts +312 -0
- package/src/core/supervisor/explanations.ts +104 -0
- package/src/core/supervisor/findings.ts +788 -0
- package/src/core/supervisor/history.ts +61 -0
- package/src/core/supervisor/hook-health-alerts.ts +108 -0
- package/src/core/supervisor/hook-health-storage.ts +25 -0
- package/src/core/supervisor/hook-health.ts +301 -0
- package/src/core/supervisor/hooks.ts +35 -0
- package/src/core/supervisor/index.ts +14 -0
- package/src/core/supervisor/log-feed.ts +131 -0
- package/src/core/supervisor/service.ts +511 -0
- package/src/core/supervisor/services.ts +203 -0
- package/src/core/supervisor/status.ts +59 -0
- package/src/core/supervisor/storage.ts +126 -0
- package/src/core/supervisor/timeline.ts +133 -0
- package/src/core/workflow/admission.ts +215 -0
- package/src/core/workflow/engine.ts +93 -1
- package/src/core/workflow/index.ts +10 -0
- package/src/core/workflow/proof.ts +54 -1
- package/src/core/workflow/run-state.ts +13 -1
- package/src/core/workflow/types.ts +39 -1
- package/src/lib/browser/client.ts +6 -0
- package/src/lib/browser/index.ts +29 -0
- package/src/lib/browser/qa-run-contracts.ts +380 -0
- package/src/lib/browser/qa-run.ts +784 -0
- package/src/lib/coord-root-id.ts +23 -0
- package/src/lib/instructions/apply.ts +81 -2
- package/src/lib/instructions/templates.ts +6 -0
package/src/lib/browser/index.ts
CHANGED
|
@@ -108,6 +108,35 @@ export {
|
|
|
108
108
|
type QaSignature,
|
|
109
109
|
type QaStylesheetSignature,
|
|
110
110
|
} from "./qa-plan.js";
|
|
111
|
+
export {
|
|
112
|
+
defaultQaRunExec,
|
|
113
|
+
QA_RUN_HEADLESS_ONLY_ENV,
|
|
114
|
+
QA_RUN_RESULT_FILENAME,
|
|
115
|
+
type QaRunExec,
|
|
116
|
+
type QaRunExecOptions,
|
|
117
|
+
type QaRunExecResult,
|
|
118
|
+
type QaRunMatrixOptions,
|
|
119
|
+
runQaMatrix,
|
|
120
|
+
} from "./qa-run.js";
|
|
121
|
+
export {
|
|
122
|
+
computeVerdict,
|
|
123
|
+
contextIdFor,
|
|
124
|
+
mergeCoverage,
|
|
125
|
+
QA_RUN_JOB_SCHEMA_VERSION,
|
|
126
|
+
QA_RUN_RESULT_SCHEMA_VERSION,
|
|
127
|
+
type QaRunBlocker,
|
|
128
|
+
type QaRunCheck,
|
|
129
|
+
type QaRunCommandOutcome,
|
|
130
|
+
type QaRunContext,
|
|
131
|
+
type QaRunCritiqueOutcome,
|
|
132
|
+
type QaRunInteraction,
|
|
133
|
+
type QaRunJob,
|
|
134
|
+
type QaRunJobValidation,
|
|
135
|
+
type QaRunPolicy,
|
|
136
|
+
type QaRunResult,
|
|
137
|
+
type QaRunVerdict,
|
|
138
|
+
validateQaRunJob,
|
|
139
|
+
} from "./qa-run-contracts.js";
|
|
111
140
|
export {
|
|
112
141
|
listQaSnapshotTargets,
|
|
113
142
|
loadQaSnapshot,
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// Contracts for the qa-run matrix runner: one JSON job in, one JSON result
|
|
2
|
+
// out, with the coverage-merge and verdict rules as pure functions.
|
|
3
|
+
//
|
|
4
|
+
// The runner executes a whole page-QA matrix (planner, deterministic gates,
|
|
5
|
+
// interactions, critique, snapshot) in one process so the driving agent stops
|
|
6
|
+
// paying a model turn per browser command. These contracts are frozen first so
|
|
7
|
+
// the orchestrator, its tests, and downstream consumers build against the same
|
|
8
|
+
// shape.
|
|
9
|
+
//
|
|
10
|
+
// Two rules are load-bearing and enforced here, not in prose:
|
|
11
|
+
// - A job may WIDEN coverage beyond the planner's manifest but can never
|
|
12
|
+
// narrow it below the manifest (mergeCoverage is a union that keeps
|
|
13
|
+
// manifest order first).
|
|
14
|
+
// - A job must not carry credentials. Authentication is referenced through
|
|
15
|
+
// existing browser profiles and cookie stores; validation refuses
|
|
16
|
+
// secret-shaped fields before any browser or model work starts.
|
|
17
|
+
//
|
|
18
|
+
// Toolkit tier: this module must not import src/core (layering check).
|
|
19
|
+
|
|
20
|
+
import type { QaContext, QaManifest } from "./qa-plan.js";
|
|
21
|
+
|
|
22
|
+
export const QA_RUN_JOB_SCHEMA_VERSION = 1 as const;
|
|
23
|
+
export const QA_RUN_RESULT_SCHEMA_VERSION = 1 as const;
|
|
24
|
+
|
|
25
|
+
/** One rendering context the runner will capture and check. */
|
|
26
|
+
export interface QaRunContext {
|
|
27
|
+
/** Stable ID, unique within the job; also the artifact prefix component.
|
|
28
|
+
* Derived contexts use `<viewport>-<theme>-<state>`. */
|
|
29
|
+
id: string;
|
|
30
|
+
/** Viewport preset name (`desktop`, `mobile`, …) or `WxH`. */
|
|
31
|
+
viewport: string;
|
|
32
|
+
theme: "light" | "dark";
|
|
33
|
+
/** Named UI state; `default` for the plain page. */
|
|
34
|
+
state: string;
|
|
35
|
+
/** Extra browse arguments that render this context (theme forcing beyond
|
|
36
|
+
* `--color-scheme`, state setup, waits). Argument array by contract. */
|
|
37
|
+
args?: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One deterministic gate: extra browse arguments appended to the context's
|
|
41
|
+
* base capture command. Arguments are an array by contract — the runner never
|
|
42
|
+
* builds a shell string. */
|
|
43
|
+
export interface QaRunCheck {
|
|
44
|
+
/** Stable ID, unique within the job (e.g. `overflow`, `contrast-hero`). */
|
|
45
|
+
id: string;
|
|
46
|
+
/** Browse flags for this gate, e.g. ["--check-overflow", "--check-overflow-fail"]. */
|
|
47
|
+
args: string[];
|
|
48
|
+
/** Context IDs this gate applies to. Absent = every context. */
|
|
49
|
+
contexts?: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One named interaction state: setup actions plus outcome assertions that
|
|
53
|
+
* prove the state changed, not merely that something was clicked. */
|
|
54
|
+
export interface QaRunInteraction {
|
|
55
|
+
/** State name; must match a planner/state name when the planner declared one. */
|
|
56
|
+
name: string;
|
|
57
|
+
/** Browse flags that produce the state (e.g. ["--batch", "click #tab-2; wait 500"]). */
|
|
58
|
+
setup: string[];
|
|
59
|
+
/** Assertion specs passed as repeated --assert values. At least one is
|
|
60
|
+
* required — a click without a proven outcome is not an interaction gate. */
|
|
61
|
+
assertions: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface QaRunPolicy {
|
|
65
|
+
/** Permit the critique provider's metered-API fallback. Default false: an
|
|
66
|
+
* exhausted headless-harness list becomes an `incomplete` blocker. */
|
|
67
|
+
allow_metered_critique?: boolean;
|
|
68
|
+
/** Concurrent deterministic captures (default 2). Interactions always run
|
|
69
|
+
* serially regardless of this value. */
|
|
70
|
+
command_concurrency?: number;
|
|
71
|
+
/** Per-command timeout in milliseconds (default 120000). */
|
|
72
|
+
command_timeout_ms?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface QaRunJob {
|
|
76
|
+
schema_version: typeof QA_RUN_JOB_SCHEMA_VERSION;
|
|
77
|
+
/** URL, local file path, or framework route the runner will render. */
|
|
78
|
+
target: string;
|
|
79
|
+
/** Git SHA or content identifier of what is being tested. */
|
|
80
|
+
tested_revision?: string;
|
|
81
|
+
/** signoff persists a QA snapshot on a passing run; review never does. */
|
|
82
|
+
mode: "signoff" | "review";
|
|
83
|
+
/** Extra coverage beyond the planner manifest (union, never narrowing). */
|
|
84
|
+
contexts?: QaRunContext[];
|
|
85
|
+
/** Extra deterministic gates. The runner always executes every planner-
|
|
86
|
+
* required deterministic check; a job can add gates but cannot remove them. */
|
|
87
|
+
checks?: QaRunCheck[];
|
|
88
|
+
interaction_states?: QaRunInteraction[];
|
|
89
|
+
/** Forwarded to the planner as --qa-scope / --qa-states inputs. */
|
|
90
|
+
qa_hints?: { scopes?: string[]; states?: string[] };
|
|
91
|
+
policy?: QaRunPolicy;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Outcome of one executed browse command. */
|
|
95
|
+
export interface QaRunCommandOutcome {
|
|
96
|
+
context_id: string;
|
|
97
|
+
/** Gate ID, `capture` for the base capture, `plan`, `critique`, or an
|
|
98
|
+
* interaction state name prefixed with `interaction:`. */
|
|
99
|
+
check_id: string;
|
|
100
|
+
argv: string[];
|
|
101
|
+
exit_code: number | null;
|
|
102
|
+
outcome: "passed" | "failed" | "unknown";
|
|
103
|
+
/** Human-readable failure details parsed from the JSON artifact. */
|
|
104
|
+
failures: string[];
|
|
105
|
+
artifacts: { png?: string; html?: string; json?: string };
|
|
106
|
+
wall_time_ms: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface QaRunCritiqueOutcome {
|
|
110
|
+
context_id: string;
|
|
111
|
+
provider: string;
|
|
112
|
+
tiles_total: number;
|
|
113
|
+
tiles_reviewed: number;
|
|
114
|
+
tiles_reused: number;
|
|
115
|
+
outcome: "passed" | "failed" | "unknown";
|
|
116
|
+
findings: Array<{ severity: string; summary: string; selector?: string }>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface QaRunBlocker {
|
|
120
|
+
stage: "validate" | "plan" | "gates" | "interactions" | "critique" | "snapshot" | "result";
|
|
121
|
+
context_id?: string;
|
|
122
|
+
reason: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type QaRunVerdict = "passed" | "failed" | "incomplete";
|
|
126
|
+
|
|
127
|
+
export interface QaRunResult {
|
|
128
|
+
schema_version: typeof QA_RUN_RESULT_SCHEMA_VERSION;
|
|
129
|
+
target: string;
|
|
130
|
+
tested_revision?: string;
|
|
131
|
+
mode: "signoff" | "review";
|
|
132
|
+
/** The authoritative planner manifest, null when planning itself failed. */
|
|
133
|
+
qa_plan: QaManifest | null;
|
|
134
|
+
/** Merged coverage in manifest order (manifest contexts first). */
|
|
135
|
+
contexts: QaRunContext[];
|
|
136
|
+
commands: QaRunCommandOutcome[];
|
|
137
|
+
critique: QaRunCritiqueOutcome[];
|
|
138
|
+
snapshot: { saved: boolean; path?: string };
|
|
139
|
+
wall_time_ms: {
|
|
140
|
+
plan: number;
|
|
141
|
+
gates: number;
|
|
142
|
+
interactions: number;
|
|
143
|
+
critique: number;
|
|
144
|
+
snapshot: number;
|
|
145
|
+
total: number;
|
|
146
|
+
};
|
|
147
|
+
blockers: QaRunBlocker[];
|
|
148
|
+
verdict: QaRunVerdict;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Job validation
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
/** Field names that indicate an embedded credential. Matched on any object
|
|
156
|
+
* key anywhere in the job, case-insensitively. */
|
|
157
|
+
const SECRET_KEY_PATTERN =
|
|
158
|
+
/(password|passwd|secret|token|api[-_]?key|authorization|cookie|credential|bearer)/i;
|
|
159
|
+
|
|
160
|
+
/** Value shapes that indicate an embedded credential even under an innocent
|
|
161
|
+
* key: explicit auth headers and long unbroken high-entropy-looking blobs. */
|
|
162
|
+
const SECRET_VALUE_PATTERNS = [/\bBearer\s+[\w.~+/=-]{16,}/i, /\bBasic\s+[A-Za-z0-9+/=]{16,}/];
|
|
163
|
+
|
|
164
|
+
export type QaRunJobValidation = { ok: true; job: QaRunJob } | { ok: false; errors: string[] };
|
|
165
|
+
|
|
166
|
+
function scanForSecrets(value: unknown, path: string, errors: string[]): void {
|
|
167
|
+
if (typeof value === "string") {
|
|
168
|
+
for (const pattern of SECRET_VALUE_PATTERNS) {
|
|
169
|
+
if (pattern.test(value)) {
|
|
170
|
+
errors.push(
|
|
171
|
+
`${path}: value looks like a credential — reference a browser profile or cookie store instead`,
|
|
172
|
+
);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (Array.isArray(value)) {
|
|
179
|
+
value.forEach((item, i) => {
|
|
180
|
+
scanForSecrets(item, `${path}[${i}]`, errors);
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (value && typeof value === "object") {
|
|
185
|
+
for (const [key, child] of Object.entries(value)) {
|
|
186
|
+
if (SECRET_KEY_PATTERN.test(key)) {
|
|
187
|
+
errors.push(
|
|
188
|
+
`${path}.${key}: secret-bearing field names are refused — reference a browser profile or cookie store instead`,
|
|
189
|
+
);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
scanForSecrets(child, `${path}.${key}`, errors);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isStringArray(value: unknown): value is string[] {
|
|
198
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Validate an untrusted job document. Structural errors and secret-bearing
|
|
203
|
+
* fields are all reported at once so a caller can fix the job in one pass.
|
|
204
|
+
* Validation is deliberately strict: an underspecified job fails here, before
|
|
205
|
+
* any browser or model process starts.
|
|
206
|
+
*/
|
|
207
|
+
export function validateQaRunJob(value: unknown): QaRunJobValidation {
|
|
208
|
+
const errors: string[] = [];
|
|
209
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
210
|
+
return { ok: false, errors: ["job must be a JSON object"] };
|
|
211
|
+
}
|
|
212
|
+
const job = value as Record<string, unknown>;
|
|
213
|
+
|
|
214
|
+
if (job.schema_version !== QA_RUN_JOB_SCHEMA_VERSION) {
|
|
215
|
+
errors.push(`schema_version must be ${QA_RUN_JOB_SCHEMA_VERSION}`);
|
|
216
|
+
}
|
|
217
|
+
if (typeof job.target !== "string" || job.target.length === 0) {
|
|
218
|
+
errors.push("target is required (URL, file path, or route)");
|
|
219
|
+
}
|
|
220
|
+
if (job.mode !== "signoff" && job.mode !== "review") {
|
|
221
|
+
errors.push('mode must be "signoff" or "review"');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const ids = new Set<string>();
|
|
225
|
+
if (job.contexts !== undefined) {
|
|
226
|
+
if (!Array.isArray(job.contexts)) {
|
|
227
|
+
errors.push("contexts must be an array");
|
|
228
|
+
} else {
|
|
229
|
+
job.contexts.forEach((ctx, i) => {
|
|
230
|
+
const c = ctx as Record<string, unknown>;
|
|
231
|
+
if (typeof c?.id !== "string" || c.id.length === 0)
|
|
232
|
+
errors.push(`contexts[${i}].id is required`);
|
|
233
|
+
else if (ids.has(c.id)) errors.push(`contexts[${i}].id duplicates "${c.id}"`);
|
|
234
|
+
else ids.add(c.id);
|
|
235
|
+
if (typeof c?.viewport !== "string" || c.viewport.length === 0) {
|
|
236
|
+
errors.push(`contexts[${i}].viewport is required`);
|
|
237
|
+
}
|
|
238
|
+
if (c?.theme !== "light" && c?.theme !== "dark") {
|
|
239
|
+
errors.push(`contexts[${i}].theme must be "light" or "dark"`);
|
|
240
|
+
}
|
|
241
|
+
if (typeof c?.state !== "string" || c.state.length === 0) {
|
|
242
|
+
errors.push(`contexts[${i}].state is required ("default" for the plain page)`);
|
|
243
|
+
}
|
|
244
|
+
if (c?.args !== undefined && !isStringArray(c.args)) {
|
|
245
|
+
errors.push(`contexts[${i}].args must be an argument array (never a shell string)`);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (job.checks !== undefined) {
|
|
252
|
+
if (!Array.isArray(job.checks)) {
|
|
253
|
+
errors.push("checks must be an array");
|
|
254
|
+
} else {
|
|
255
|
+
const checkIds = new Set<string>();
|
|
256
|
+
job.checks.forEach((check, i) => {
|
|
257
|
+
const c = check as Record<string, unknown>;
|
|
258
|
+
if (typeof c?.id !== "string" || c.id.length === 0)
|
|
259
|
+
errors.push(`checks[${i}].id is required`);
|
|
260
|
+
else if (checkIds.has(c.id)) errors.push(`checks[${i}].id duplicates "${c.id}"`);
|
|
261
|
+
else checkIds.add(c.id);
|
|
262
|
+
if (!isStringArray(c?.args) || (c.args as string[]).length === 0) {
|
|
263
|
+
errors.push(
|
|
264
|
+
`checks[${i}].args must be a non-empty argument array (never a shell string)`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
if (c?.contexts !== undefined && !isStringArray(c.contexts)) {
|
|
268
|
+
errors.push(`checks[${i}].contexts must be an array of context IDs`);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (job.interaction_states !== undefined) {
|
|
275
|
+
if (!Array.isArray(job.interaction_states)) {
|
|
276
|
+
errors.push("interaction_states must be an array");
|
|
277
|
+
} else {
|
|
278
|
+
job.interaction_states.forEach((state, i) => {
|
|
279
|
+
const s = state as Record<string, unknown>;
|
|
280
|
+
if (typeof s?.name !== "string" || s.name.length === 0) {
|
|
281
|
+
errors.push(`interaction_states[${i}].name is required`);
|
|
282
|
+
}
|
|
283
|
+
if (!isStringArray(s?.setup) || (s.setup as string[]).length === 0) {
|
|
284
|
+
errors.push(`interaction_states[${i}].setup must be a non-empty argument array`);
|
|
285
|
+
}
|
|
286
|
+
if (!isStringArray(s?.assertions) || (s.assertions as string[]).length === 0) {
|
|
287
|
+
errors.push(
|
|
288
|
+
`interaction_states[${i}].assertions must name at least one outcome assertion — a click without a proven outcome is not a gate`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (job.policy !== undefined) {
|
|
296
|
+
const p = job.policy as Record<string, unknown>;
|
|
297
|
+
if (p?.command_concurrency !== undefined) {
|
|
298
|
+
const n = p.command_concurrency;
|
|
299
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > 8) {
|
|
300
|
+
errors.push("policy.command_concurrency must be an integer between 1 and 8");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (p?.command_timeout_ms !== undefined) {
|
|
304
|
+
const n = p.command_timeout_ms;
|
|
305
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 1000) {
|
|
306
|
+
errors.push("policy.command_timeout_ms must be an integer ≥ 1000");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
scanForSecrets(job, "job", errors);
|
|
312
|
+
|
|
313
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
314
|
+
return { ok: true, job: value as QaRunJob };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// Coverage merge — widen-only
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
export function contextIdFor(context: QaContext): string {
|
|
322
|
+
return `${context.viewport}-${context.theme}-${context.state}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Merge planner-manifest coverage with the job's extra contexts. The manifest
|
|
327
|
+
* is the floor: its contexts always run, in manifest order, and a job can only
|
|
328
|
+
* append. The union construction makes narrowing impossible by design; the
|
|
329
|
+
* returned list is what the runner executes and what the result reports.
|
|
330
|
+
*/
|
|
331
|
+
export function mergeCoverage(manifest: QaManifest, job: QaRunJob): QaRunContext[] {
|
|
332
|
+
const merged: QaRunContext[] = manifest.contexts.map((context) => ({
|
|
333
|
+
id: contextIdFor(context),
|
|
334
|
+
viewport: context.viewport,
|
|
335
|
+
theme: context.theme,
|
|
336
|
+
state: context.state,
|
|
337
|
+
}));
|
|
338
|
+
const seen = new Set(merged.map((context) => context.id));
|
|
339
|
+
for (const context of job.contexts ?? []) {
|
|
340
|
+
const canonical = contextIdFor(context);
|
|
341
|
+
if (seen.has(canonical) || seen.has(context.id)) continue;
|
|
342
|
+
seen.add(canonical);
|
|
343
|
+
seen.add(context.id);
|
|
344
|
+
merged.push(context);
|
|
345
|
+
}
|
|
346
|
+
return merged;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// Verdict — fail-closed
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Compute the terminal verdict from completed evidence. The rules are the
|
|
355
|
+
* existing page-QA signoff contract, fail-closed:
|
|
356
|
+
* - any blocker → incomplete (a blocker is a fact the run could not
|
|
357
|
+
* establish, so a defect cannot be ruled out);
|
|
358
|
+
* - any failed or unknown command/critique outcome → failed when the check
|
|
359
|
+
* completed and found a defect, incomplete when the outcome is unknown;
|
|
360
|
+
* - signoff mode additionally requires the snapshot to have been saved.
|
|
361
|
+
* `passed` is only reachable when every input proves out.
|
|
362
|
+
*/
|
|
363
|
+
export function computeVerdict(input: {
|
|
364
|
+
mode: QaRunJob["mode"];
|
|
365
|
+
blockers: QaRunBlocker[];
|
|
366
|
+
commands: QaRunCommandOutcome[];
|
|
367
|
+
critique: QaRunCritiqueOutcome[];
|
|
368
|
+
snapshotSaved: boolean;
|
|
369
|
+
}): QaRunVerdict {
|
|
370
|
+
const failed =
|
|
371
|
+
input.commands.some((command) => command.outcome === "failed") ||
|
|
372
|
+
input.critique.some((entry) => entry.outcome === "failed");
|
|
373
|
+
if (failed) return "failed";
|
|
374
|
+
const unknown =
|
|
375
|
+
input.commands.some((command) => command.outcome === "unknown") ||
|
|
376
|
+
input.critique.some((entry) => entry.outcome === "unknown");
|
|
377
|
+
if (input.blockers.length > 0 || unknown) return "incomplete";
|
|
378
|
+
if (input.mode === "signoff" && !input.snapshotSaved) return "incomplete";
|
|
379
|
+
return "passed";
|
|
380
|
+
}
|