@tea-agent/loop-agent 0.13.0-alpha.0 → 0.13.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/AGENTS.md +4 -0
- package/CHANGELOG.md +57 -53
- package/README.md +12 -3
- package/dist/application/dag/generate-task-dag.js +28 -58
- package/dist/application/evaluation/candidate-hash.js +75 -0
- package/dist/application/evaluation/candidate.js +52 -0
- package/dist/application/evaluation/replay.js +289 -0
- package/dist/application/evaluation/types.js +130 -0
- package/dist/cli/command-definitions.js +17 -4
- package/dist/cli/program.js +8 -4
- package/dist/commands/eval.js +235 -0
- package/dist/commands/init.js +131 -24
- package/dist/executors/pi-sdk-executor.js +38 -24
- package/dist/executors/shell-executor.js +226 -15
- package/dist/executors/shell-presets.js +20 -0
- package/dist/executors/shell-verification.js +7 -0
- package/dist/governance/manifest-types.js +1 -0
- package/dist/infrastructure/evaluation/candidate-store.js +439 -0
- package/dist/infrastructure/evaluation/store.js +40 -0
- package/dist/task/config-types.js +23 -0
- package/dist/worker/observe/routes.js +18 -3
- package/dist/worker/observe/spec-evidence.js +1 -1
- package/dist/worker/observe/static/dom.js +160 -1
- package/dist/worker/observe/static/state.js +14 -0
- package/dist/worker/observe/static/views/dag-inspector.js +35 -4
- package/dist/worker/observe/static/views/dag.js +9 -0
- package/dist/worker/observe/static/views/dashboard.js +702 -445
- package/dist/worker/observe/static/views/session-timeline.js +15 -1
- package/dist/workflows/dag/backend-test-analysis-contract.js +120 -0
- package/dist/workflows/dag/backend-test-case-manifest.js +503 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +353 -0
- package/dist/workflows/dag/backend-test-result-contract.js +568 -0
- package/dist/workflows/dag/decision-envelope.js +57 -2
- package/dist/workflows/dag/dynamic-runtime/map.js +90 -2
- package/dist/workflows/dag/frontend-implementation-contract.js +240 -0
- package/dist/workflows/dag/frontend-project-capability.js +309 -0
- package/dist/workflows/dag/frontend-repair.js +341 -0
- package/dist/workflows/dag/frontend-risk.js +161 -0
- package/dist/workflows/dag/frontend-verification-trace.js +190 -0
- package/dist/workflows/dag/init-hybrid.js +2407 -297
- package/dist/workflows/dag/node-execution.js +9 -0
- package/dist/workflows/dag/prompt.js +9 -0
- package/dist/workflows/dag/repair-artifact.js +43 -3
- package/dist/workflows/dag/report.js +35 -1
- package/dist/workflows/dag/runner.js +28 -2
- package/dist/workflows/dag/skill-instructions.js +4 -2
- package/dist/workflows/dag/task-demand-routing.js +383 -0
- package/dist/workflows/dag/types.js +71 -13
- package/dist/workflows/dag/upstream-artifacts.js +1 -0
- package/dist/workflows/dag/validate.js +59 -1
- package/docs/README.md +6 -3
- package/docs/agent-dag-recovery-playbook.md +5 -3
- package/docs/agent-dag-runner.md +3 -3
- package/docs/architecture/README.md +3 -3
- package/docs/architecture/dag-execution.md +1 -1
- package/docs/architecture/evolution.md +13 -13
- package/docs/architecture/facts-and-state.md +1 -1
- package/docs/architecture/runtime-boundaries.md +7 -7
- package/docs/architecture/system-overview.md +3 -3
- package/docs/architecture/worker-and-feature.md +3 -3
- package/docs/design/README.md +124 -42
- package/docs/development-principles.md +4 -4
- package/docs/exec-plans/active/README.md +12 -11
- package/docs/exec-plans/completed/README.md +33 -0
- package/docs/feature-workflow.md +114 -39
- package/docs/init-surface.manifest.json +30 -3
- package/docs/loop-agent-harness.md +9 -8
- package/docs/production-readiness.md +1 -1
- package/docs/progress/README.md +23 -1
- package/docs/reports/README.md +65 -6
- package/docs/skills/vetted-skill-registry.md +2 -0
- package/docs/templates/agent-dag.schema.json +29 -1
- package/docs/templates/agent-dag.supervised-implementation.json +127 -8
- package/docs/templates/backend-test-analysis.schema.json +44 -0
- package/docs/templates/backend-test-case-manifest.schema.json +190 -0
- package/docs/templates/backend-test-dag.classify.prompt.md +75 -0
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +81 -16
- package/docs/templates/backend-test-dag.json +311 -40
- package/docs/templates/backend-test-dag.retrospect.prompt.md +44 -30
- package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -8
- package/docs/templates/backend-test-execution.schema.json +133 -0
- package/docs/templates/backend-test-result.schema.json +99 -0
- package/docs/templates/branch-merge-report.md +93 -0
- package/docs/templates/frontend-design-contract.md +9 -0
- package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -0
- package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -0
- package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -0
- package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -0
- package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -0
- package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -0
- package/docs/templates/frontend-eval/metrics.md +138 -0
- package/docs/templates/frontend-eval/smoke-targets.md +53 -0
- package/docs/templates/frontend-implementation-contract.schema.json +27 -0
- package/docs/templates/frontend-task-constraints.md +10 -0
- package/docs/templates/frontend-task-requirement.md +9 -0
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -0
- package/docs/templates/frontend-test-dag.json +23 -0
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -0
- package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -0
- package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -0
- package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -0
- package/docs/templates/knowledge-graph-bootstrap-dag.json +1 -1
- package/docs/templates/knowledge-sync-dag.json +1 -0
- package/docs/verification-matrix.md +4 -1
- package/examples/decision-gate-agent-dag.json +4 -4
- package/examples/hybrid-loop-agent-dag.json +1 -1
- package/package.json +2 -2
- package/scripts/kb-bootstrap-init-skeleton.sh +2 -1
- package/scripts/kb-graph-incremental-prepare.mjs +19 -5
- package/scripts/kb-graph-promote.mjs +12 -1
- package/skills/ai-engineering-context/SKILL.md +2 -2
- package/skills/analyze-product-dependencies/SKILL.md +67 -0
- package/skills/analyze-product-dependencies/agents/openai.yaml +4 -0
- package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -0
- package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -0
- package/skills/analyze-product-dependencies/references/example.md +76 -0
- package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -0
- package/skills/analyze-product-dependencies/references/input-contract.md +11 -0
- package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -0
- package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -0
- package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -0
- package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -0
- package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -0
- package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -0
- package/skills/analyze-product-requirements/SKILL.md +90 -0
- package/skills/analyze-product-requirements/agents/openai.yaml +4 -0
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -0
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -0
- package/skills/analyze-product-requirements/references/example.md +86 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -0
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -0
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -0
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -0
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -0
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -0
- package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -0
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -0
- package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -0
- package/skills/browser-tools/SKILL.md +196 -0
- package/skills/browser-tools/browser-content.js +103 -0
- package/skills/browser-tools/browser-cookies.js +35 -0
- package/skills/browser-tools/browser-eval.js +53 -0
- package/skills/browser-tools/browser-hn-scraper.js +108 -0
- package/skills/browser-tools/browser-nav.js +44 -0
- package/skills/browser-tools/browser-pick.js +162 -0
- package/skills/browser-tools/browser-screenshot.js +34 -0
- package/skills/browser-tools/browser-start.js +86 -0
- package/skills/browser-tools/package-lock.json +2556 -0
- package/skills/browser-tools/package.json +19 -0
- package/skills/frontend-design-review/SKILL.md +6 -1
- package/skills/frontend-design-review/references/review-checklist.md +25 -4
- package/skills/frontend-implementation/SKILL.md +25 -30
- package/skills/frontend-implementation/references/code-standards.md +20 -22
- package/skills/frontend-implementation/references/node-contracts.md +17 -53
- package/skills/frontend-review/SKILL.md +10 -4
- package/skills/frontend-review/references/review-findings.md +8 -3
- package/skills/frontend-verification/SKILL.md +22 -9
- package/skills/frontend-verification/references/verification-checklist.md +17 -5
- package/skills/grill-with-docs/SKILL.md +5 -5
- package/skills/grill-with-docs/adr-format.md +3 -3
- package/skills/init-capability-evolution/SKILL.md +5 -5
- package/skills/loop-agent/SKILL.md +5 -5
- package/skills/loop-agent/references/README.md +3 -3
- package/skills/loop-agent/references/command-reference.md +98 -24
- package/skills/loop-agent/references/docs-converge.md +15 -15
- package/skills/loop-agent/references/harness-policy.md +2 -2
- package/skills/loop-agent/references/hybrid-dag.md +32 -22
- package/skills/loop-agent/references/multi-worktree.md +1 -1
- package/skills/loop-agent/references/orchestrator-and-interventions.md +8 -8
- package/skills/loop-agent/references/task-workflow.md +1 -1
- package/skills/loop-agent/references/verification-and-failure-handling.md +6 -4
- package/skills/playwright-cli/SKILL.md +420 -0
- package/skills/playwright-cli/references/element-attributes.md +23 -0
- package/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/skills/playwright-cli/references/request-mocking.md +87 -0
- package/skills/playwright-cli/references/running-code.md +241 -0
- package/skills/playwright-cli/references/session-management.md +225 -0
- package/skills/playwright-cli/references/storage-state.md +275 -0
- package/skills/playwright-cli/references/test-generation.md +433 -0
- package/skills/playwright-cli/references/tracing.md +139 -0
- package/skills/playwright-cli/references/video-recording.md +143 -0
- package/skills/playwright-cli-case-generator/SKILL.md +74 -0
- package/skills/requesting-code-review/SKILL.md +1 -1
- package/skills/systematic-debugging/CREATION-LOG.md +3 -3
- package/skills/systematic-debugging/SKILL.md +1 -1
- package/skills/systematic-debugging/test-academic.md +1 -1
- package/skills/systematic-debugging/test-pressure-1.md +1 -1
- package/skills/systematic-debugging/test-pressure-2.md +1 -1
- package/skills/systematic-debugging/test-pressure-3.md +1 -1
- package/skills/verification-before-completion/SKILL.md +1 -1
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
5
|
+
import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
|
|
6
|
+
export const FRONTEND_REPAIR_ASSESSMENT_SCHEMA_ID = "frontend-repair-assessment-v1";
|
|
7
|
+
export const frontendRepairFailureClassSchema = z.enum([
|
|
8
|
+
"typecheck",
|
|
9
|
+
"build",
|
|
10
|
+
"lint",
|
|
11
|
+
"component-test",
|
|
12
|
+
"unit-test",
|
|
13
|
+
"trace",
|
|
14
|
+
"review",
|
|
15
|
+
"contract",
|
|
16
|
+
"path",
|
|
17
|
+
"dependency",
|
|
18
|
+
"credential",
|
|
19
|
+
"deploy",
|
|
20
|
+
"spec-unclear",
|
|
21
|
+
"unknown",
|
|
22
|
+
]);
|
|
23
|
+
const REPAIRABLE = new Set([
|
|
24
|
+
"typecheck",
|
|
25
|
+
"build",
|
|
26
|
+
"lint",
|
|
27
|
+
"component-test",
|
|
28
|
+
"unit-test",
|
|
29
|
+
"trace",
|
|
30
|
+
"review",
|
|
31
|
+
]);
|
|
32
|
+
export function isFrontendRepairable(failureClass) {
|
|
33
|
+
return REPAIRABLE.has(failureClass);
|
|
34
|
+
}
|
|
35
|
+
export function classifyFrontendFailure(input) {
|
|
36
|
+
const blob = `${input.nodeId}\n${input.failureCategory ?? ""}\n${input.stdout ?? ""}\n${input.stderr ?? ""}`.toLowerCase();
|
|
37
|
+
if (input.nodeId.includes("contract") ||
|
|
38
|
+
blob.includes("contract mismatch") ||
|
|
39
|
+
blob.includes("source binding")) {
|
|
40
|
+
return "contract";
|
|
41
|
+
}
|
|
42
|
+
if (blob.includes("forbidden") ||
|
|
43
|
+
blob.includes("write guard") ||
|
|
44
|
+
blob.includes("write-set") ||
|
|
45
|
+
blob.includes("writeset")) {
|
|
46
|
+
return "path";
|
|
47
|
+
}
|
|
48
|
+
if (blob.includes("package.json") ||
|
|
49
|
+
blob.includes("unapproved dependency") ||
|
|
50
|
+
blob.includes("npm install")) {
|
|
51
|
+
return "dependency";
|
|
52
|
+
}
|
|
53
|
+
if (blob.includes("credential") ||
|
|
54
|
+
blob.includes("api key") ||
|
|
55
|
+
blob.includes("secret")) {
|
|
56
|
+
return "credential";
|
|
57
|
+
}
|
|
58
|
+
if (blob.includes("deploy") || blob.includes("production release")) {
|
|
59
|
+
return "deploy";
|
|
60
|
+
}
|
|
61
|
+
if (blob.includes("spec unclear") ||
|
|
62
|
+
blob.includes("requirement ambiguous") ||
|
|
63
|
+
blob.includes("needs human")) {
|
|
64
|
+
return "spec-unclear";
|
|
65
|
+
}
|
|
66
|
+
if (input.nodeId.includes("trace") || blob.includes("trace:")) {
|
|
67
|
+
return "trace";
|
|
68
|
+
}
|
|
69
|
+
if (input.nodeId.includes("review")) {
|
|
70
|
+
return "review";
|
|
71
|
+
}
|
|
72
|
+
if (blob.includes("typecheck") ||
|
|
73
|
+
blob.includes("tsc") ||
|
|
74
|
+
/error ts\d+/i.test(blob)) {
|
|
75
|
+
return "typecheck";
|
|
76
|
+
}
|
|
77
|
+
if (blob.includes("eslint") || blob.includes("lint")) {
|
|
78
|
+
return "lint";
|
|
79
|
+
}
|
|
80
|
+
if (blob.includes("build") ||
|
|
81
|
+
blob.includes("vite build") ||
|
|
82
|
+
blob.includes("next build")) {
|
|
83
|
+
return "build";
|
|
84
|
+
}
|
|
85
|
+
if (blob.includes("testing-library") ||
|
|
86
|
+
blob.includes("component") ||
|
|
87
|
+
blob.includes("render(")) {
|
|
88
|
+
return "component-test";
|
|
89
|
+
}
|
|
90
|
+
if (blob.includes("vitest") ||
|
|
91
|
+
blob.includes("jest") ||
|
|
92
|
+
blob.includes("npm test") ||
|
|
93
|
+
input.nodeId.includes("behavior")) {
|
|
94
|
+
return "unit-test";
|
|
95
|
+
}
|
|
96
|
+
if (input.failureCategory === "invalid-output")
|
|
97
|
+
return "contract";
|
|
98
|
+
if (input.failureCategory === "write-guard")
|
|
99
|
+
return "path";
|
|
100
|
+
return "unknown";
|
|
101
|
+
}
|
|
102
|
+
export const frontendRepairAssessmentSchema = z
|
|
103
|
+
.object({
|
|
104
|
+
schemaVersion: z.literal(1),
|
|
105
|
+
schemaId: z.literal(FRONTEND_REPAIR_ASSESSMENT_SCHEMA_ID),
|
|
106
|
+
contract: z
|
|
107
|
+
.object({
|
|
108
|
+
relativePath: z.string().min(1),
|
|
109
|
+
schemaId: z.string().min(1),
|
|
110
|
+
})
|
|
111
|
+
.strict(),
|
|
112
|
+
failedNodeIds: z.array(z.string().min(1)),
|
|
113
|
+
failureClass: frontendRepairFailureClassSchema,
|
|
114
|
+
eligible: z.boolean(),
|
|
115
|
+
reason: z.string().min(1),
|
|
116
|
+
attempt: z.number().int().min(1).max(2),
|
|
117
|
+
maxAttempts: z.literal(1),
|
|
118
|
+
allowedRepairWriteSet: z.array(z.string().min(1)),
|
|
119
|
+
evidenceRefs: z.array(z.string().min(1)),
|
|
120
|
+
browserStatus: z.literal("not-run"),
|
|
121
|
+
})
|
|
122
|
+
.strict();
|
|
123
|
+
function nodeHadFailure(record) {
|
|
124
|
+
if (record.status === "ERROR")
|
|
125
|
+
return true;
|
|
126
|
+
if (recordIndicatesCommandFailure(record))
|
|
127
|
+
return true;
|
|
128
|
+
const results = record.commandResults ?? [];
|
|
129
|
+
if (results.some((item) => item.ok === false))
|
|
130
|
+
return true;
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
function normalize(value) {
|
|
134
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
135
|
+
}
|
|
136
|
+
export function assertWriteSetSubset(repairWriteSet, implementWriteSet) {
|
|
137
|
+
for (const entry of repairWriteSet) {
|
|
138
|
+
const ok = implementWriteSet.some((pattern) => normalize(entry) === normalize(pattern) ||
|
|
139
|
+
pathMatchesPattern(normalize(entry), pattern) ||
|
|
140
|
+
pathMatchesPattern(normalize(pattern), entry));
|
|
141
|
+
// exact equality preferred for generation-time copy
|
|
142
|
+
if (!implementWriteSet.map(normalize).includes(normalize(entry))) {
|
|
143
|
+
// allow identical globs only
|
|
144
|
+
if (!ok || normalize(entry) !== normalize(entry)) {
|
|
145
|
+
// require exact membership for exclusive writeSet entries
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!implementWriteSet.map(normalize).includes(normalize(entry))) {
|
|
149
|
+
throw new Error(`repair writeSet entry "${entry}" is not ⊆ implement writeSet`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function loadImplementWriteSet(runDir, fallback) {
|
|
154
|
+
if (fallback.length > 0)
|
|
155
|
+
return fallback;
|
|
156
|
+
const candidates = ["run.json", "dag.json", "spec.json"];
|
|
157
|
+
for (const name of candidates) {
|
|
158
|
+
try {
|
|
159
|
+
const raw = JSON.parse(await readFile(path.join(runDir, name), "utf8"));
|
|
160
|
+
const tasks = raw.spec?.tasks ?? raw.tasks ?? [];
|
|
161
|
+
const implement = tasks.find((task) => task.id === "frontend-implement-pi");
|
|
162
|
+
if (implement?.writeSet?.length)
|
|
163
|
+
return implement.writeSet;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// continue
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return fallback;
|
|
170
|
+
}
|
|
171
|
+
export async function runFrontendFailureAssessGate(input) {
|
|
172
|
+
const attempt = input.attempt ?? 1;
|
|
173
|
+
const implementWriteSet = await loadImplementWriteSet(input.runDir, input.implementWriteSet ?? []);
|
|
174
|
+
if (implementWriteSet.length === 0) {
|
|
175
|
+
throw new Error("assess: implement writeSet unavailable");
|
|
176
|
+
}
|
|
177
|
+
const contractRel = "contracts/frontend-implementation-contract.json";
|
|
178
|
+
const contractPath = path.join(input.runDir, contractRel);
|
|
179
|
+
let contractSchemaId = FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID;
|
|
180
|
+
try {
|
|
181
|
+
const raw = JSON.parse(await readFile(contractPath, "utf8"));
|
|
182
|
+
const parsed = frontendImplementationContractSchema.safeParse(raw);
|
|
183
|
+
if (!parsed.success) {
|
|
184
|
+
throw new Error("invalid frontend implementation contract");
|
|
185
|
+
}
|
|
186
|
+
contractSchemaId = FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// still allow assessment of failures without contract? fail-closed to contract
|
|
190
|
+
throw new Error("assess: missing or invalid contracts/frontend-implementation-contract.json");
|
|
191
|
+
}
|
|
192
|
+
const candidateNodeIds = [
|
|
193
|
+
"frontend-static-verify-shell",
|
|
194
|
+
"frontend-behavior-verify-shell",
|
|
195
|
+
"frontend-verification-trace-shell",
|
|
196
|
+
"frontend-static-reverify-shell",
|
|
197
|
+
"frontend-behavior-reverify-shell",
|
|
198
|
+
"frontend-verification-retrace-shell",
|
|
199
|
+
];
|
|
200
|
+
const failed = [];
|
|
201
|
+
for (const nodeId of candidateNodeIds) {
|
|
202
|
+
const nodePath = path.join(input.runDir, `${nodeId}.json`);
|
|
203
|
+
try {
|
|
204
|
+
const record = JSON.parse(await readFile(nodePath, "utf8"));
|
|
205
|
+
if (nodeHadFailure(record) || recordIndicatesCommandFailure(record)) {
|
|
206
|
+
failed.push({ nodeId, record });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// missing optional nodes ok
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
assertWriteSetSubset(implementWriteSet, implementWriteSet);
|
|
214
|
+
if (failed.length === 0) {
|
|
215
|
+
const assessment = {
|
|
216
|
+
schemaVersion: 1,
|
|
217
|
+
schemaId: FRONTEND_REPAIR_ASSESSMENT_SCHEMA_ID,
|
|
218
|
+
contract: {
|
|
219
|
+
relativePath: contractRel,
|
|
220
|
+
schemaId: contractSchemaId,
|
|
221
|
+
},
|
|
222
|
+
failedNodeIds: [],
|
|
223
|
+
failureClass: "unknown",
|
|
224
|
+
eligible: false,
|
|
225
|
+
reason: "no failed frontend verify/trace nodes in current run",
|
|
226
|
+
attempt,
|
|
227
|
+
maxAttempts: 1,
|
|
228
|
+
allowedRepairWriteSet: [...implementWriteSet],
|
|
229
|
+
evidenceRefs: [contractRel],
|
|
230
|
+
browserStatus: "not-run",
|
|
231
|
+
};
|
|
232
|
+
await writeAssessment(input.runDir, assessment);
|
|
233
|
+
return assessment;
|
|
234
|
+
}
|
|
235
|
+
const primary = failed[0];
|
|
236
|
+
const failureClass = classifyFrontendFailure({
|
|
237
|
+
nodeId: primary.nodeId,
|
|
238
|
+
failureCategory: primary.record.failureCategory,
|
|
239
|
+
stdout: primary.record.stdout,
|
|
240
|
+
stderr: primary.record.stderr,
|
|
241
|
+
});
|
|
242
|
+
const eligible = isFrontendRepairable(failureClass) && attempt <= 1;
|
|
243
|
+
const reason = eligible
|
|
244
|
+
? `repairable failureClass=${failureClass} on ${primary.nodeId}`
|
|
245
|
+
: `non-repairable or ineligible: failureClass=${failureClass} attempt=${attempt}`;
|
|
246
|
+
const assessment = {
|
|
247
|
+
schemaVersion: 1,
|
|
248
|
+
schemaId: FRONTEND_REPAIR_ASSESSMENT_SCHEMA_ID,
|
|
249
|
+
contract: {
|
|
250
|
+
relativePath: contractRel,
|
|
251
|
+
schemaId: contractSchemaId,
|
|
252
|
+
},
|
|
253
|
+
failedNodeIds: failed.map((item) => item.nodeId),
|
|
254
|
+
failureClass,
|
|
255
|
+
eligible,
|
|
256
|
+
reason,
|
|
257
|
+
attempt,
|
|
258
|
+
maxAttempts: 1,
|
|
259
|
+
allowedRepairWriteSet: [...implementWriteSet],
|
|
260
|
+
evidenceRefs: [contractRel, ...failed.map((item) => `${item.nodeId}.json`)],
|
|
261
|
+
browserStatus: "not-run",
|
|
262
|
+
};
|
|
263
|
+
await writeAssessment(input.runDir, assessment);
|
|
264
|
+
return assessment;
|
|
265
|
+
}
|
|
266
|
+
function recordIndicatesCommandFailure(record) {
|
|
267
|
+
// Prefer explicit commandResults when present
|
|
268
|
+
if (record.commandResults?.some((item) => item.ok === false))
|
|
269
|
+
return true;
|
|
270
|
+
// Node may be FINISHED with nonZeroExitPolicy=record; look for harness markers
|
|
271
|
+
const stdout = record.stdout ?? "";
|
|
272
|
+
if (/nonZeroExitPolicy=record/.test(stdout) &&
|
|
273
|
+
/exitCode=[1-9]/.test(stdout)) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
if (record.failureCategory && record.failureCategory !== "success") {
|
|
277
|
+
// recorded failures may still be FINISHED
|
|
278
|
+
if (record.status === "FINISHED") {
|
|
279
|
+
return (record.failureCategory === "nonzero-exit" ||
|
|
280
|
+
record.failureCategory === "invalid-output" ||
|
|
281
|
+
record.failureCategory === "timeout");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
async function writeAssessment(runDir, assessment) {
|
|
287
|
+
const relative = "contracts/frontend-repair-assessment.json";
|
|
288
|
+
const absolute = path.join(runDir, relative);
|
|
289
|
+
await mkdir(path.dirname(absolute), { recursive: true });
|
|
290
|
+
await writeFile(absolute, `${JSON.stringify(assessment, null, 2)}\n`, "utf8");
|
|
291
|
+
return absolute;
|
|
292
|
+
}
|
|
293
|
+
export async function runFrontendRepairContractGate(input) {
|
|
294
|
+
const relative = "contracts/frontend-repair-assessment.json";
|
|
295
|
+
const absolute = path.join(input.runDir, relative);
|
|
296
|
+
const raw = JSON.parse(await readFile(absolute, "utf8"));
|
|
297
|
+
const parsed = frontendRepairAssessmentSchema.safeParse(raw);
|
|
298
|
+
if (!parsed.success) {
|
|
299
|
+
throw new Error(`repair-contract: invalid assessment: ${parsed.error.issues
|
|
300
|
+
.map((i) => i.message)
|
|
301
|
+
.join("; ")}`);
|
|
302
|
+
}
|
|
303
|
+
const assessment = parsed.data;
|
|
304
|
+
const implementWriteSet = await loadImplementWriteSet(input.runDir, input.implementWriteSet ?? []);
|
|
305
|
+
assertWriteSetSubset(assessment.allowedRepairWriteSet, implementWriteSet.length
|
|
306
|
+
? implementWriteSet
|
|
307
|
+
: assessment.allowedRepairWriteSet);
|
|
308
|
+
if (assessment.attempt > assessment.maxAttempts) {
|
|
309
|
+
throw new Error("repair-contract: attempt exceeds maxAttempts");
|
|
310
|
+
}
|
|
311
|
+
if (assessment.failedNodeIds.length === 0) {
|
|
312
|
+
return { ok: true, assessment };
|
|
313
|
+
}
|
|
314
|
+
if (!assessment.eligible) {
|
|
315
|
+
throw new Error(`repair-contract: non-repairable failure (${assessment.failureClass}): ${assessment.reason}`);
|
|
316
|
+
}
|
|
317
|
+
if (!isFrontendRepairable(assessment.failureClass)) {
|
|
318
|
+
throw new Error(`repair-contract: failureClass ${assessment.failureClass} is not repairable`);
|
|
319
|
+
}
|
|
320
|
+
return { ok: true, assessment };
|
|
321
|
+
}
|
|
322
|
+
export function formatFrontendFailureAssessStdout(assessment) {
|
|
323
|
+
return [
|
|
324
|
+
"Frontend failure assess: ok",
|
|
325
|
+
`eligible=${assessment.eligible}`,
|
|
326
|
+
`failureClass=${assessment.failureClass}`,
|
|
327
|
+
`failedNodeIds=${assessment.failedNodeIds.join(",") || "(none)"}`,
|
|
328
|
+
`attempt=${assessment.attempt}/${assessment.maxAttempts}`,
|
|
329
|
+
`reason=${assessment.reason}`,
|
|
330
|
+
`browser=${assessment.browserStatus}`,
|
|
331
|
+
`artifact=contracts/frontend-repair-assessment.json`,
|
|
332
|
+
].join("\n");
|
|
333
|
+
}
|
|
334
|
+
export function formatFrontendRepairContractStdout(assessment) {
|
|
335
|
+
return [
|
|
336
|
+
"Frontend repair contract: pass",
|
|
337
|
+
`eligible=${assessment.eligible}`,
|
|
338
|
+
`failureClass=${assessment.failureClass}`,
|
|
339
|
+
`writeSetSize=${assessment.allowedRepairWriteSet.length}`,
|
|
340
|
+
].join("\n");
|
|
341
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic frontend-implementation risk classifier (M4).
|
|
3
|
+
* Pure function: no I/O, no model, no side effects.
|
|
4
|
+
*/
|
|
5
|
+
const HIGH_RISK_PATTERNS = [
|
|
6
|
+
{
|
|
7
|
+
id: "new-route",
|
|
8
|
+
re: /new\s+route|新增路由|createBrowserRouter|add\s+route|新建页面路由/i,
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
id: "shared-public-api",
|
|
12
|
+
re: /public\s+api|shared\s+component|公共组件|破坏性|breaking\s+change/i,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "auth-permission",
|
|
16
|
+
re: /\bauth\b|\bpermission\b|\brbac\b|登录|权限|\bjwt\b|\bsession\b/i,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "sensitive-action",
|
|
20
|
+
re: /\bpayment\b|delete\s+account|支付|删除用户|\bdestructive\b/i,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "ssr-boundary",
|
|
24
|
+
re: /\bssr\b|\brsc\b|server\s+component|use client|getServerSideProps|\bhydration\b|server-client/i,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "new-dependency",
|
|
28
|
+
re: /new\s+dependency|add\s+dependency|npm\s+install|新增依赖|package\.json/i,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "schema-api-change",
|
|
32
|
+
re: /\bopenapi\b|graphql\s+schema|api\s+contract|接口变更|schema\s+change/i,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "design-conflict",
|
|
36
|
+
re: /design\s+conflict|规范冲突|openSpec\s+conflict/i,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "cross-domain-paths",
|
|
40
|
+
re: /多个领域|cross-module|multiple\s+packages/i,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "new-state-infra",
|
|
44
|
+
re: /\bredux\b|\bzustand\b|\bpinia\b|\bjotai\b|\brecoil\b|新状态库|state\s+management\s+introduc/i,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
const SMALL_PATTERNS = [
|
|
48
|
+
{
|
|
49
|
+
id: "single-component",
|
|
50
|
+
re: /single\s+component|单一组件|局部样式|style\s+tweak|css\s+only|文案|copy\s+change/i,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "no-remote",
|
|
54
|
+
re: /no\s+api|无接口|本地状态|local\s+state\s+only|pure\s+ui/i,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
function blobOf(input) {
|
|
58
|
+
return [
|
|
59
|
+
input.title ?? "",
|
|
60
|
+
input.requirementMarkdown ?? "",
|
|
61
|
+
input.constraintMarkdown ?? "",
|
|
62
|
+
(input.allowedPaths ?? []).join("\n"),
|
|
63
|
+
input.manifestEvidence ?? "",
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
66
|
+
function pathSpreadScore(paths) {
|
|
67
|
+
const tops = new Set(paths
|
|
68
|
+
.map((p) => p.replace(/\\/g, "/").split("/").filter(Boolean)[0] ?? "")
|
|
69
|
+
.filter(Boolean));
|
|
70
|
+
return tops.size;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Classify frontend task risk. High-risk signals always win over small keywords.
|
|
74
|
+
* supervised / explicit high governance never selects small topology.
|
|
75
|
+
*/
|
|
76
|
+
export function classifyFrontendRisk(input) {
|
|
77
|
+
const blob = blobOf(input);
|
|
78
|
+
const signals = [];
|
|
79
|
+
const rejectedSignals = [];
|
|
80
|
+
const profile = (input.governanceProfile ?? "").toLowerCase();
|
|
81
|
+
const supervised = profile === "supervised" ||
|
|
82
|
+
profile.includes("supervised") ||
|
|
83
|
+
/\bsupervised\b/i.test(blob);
|
|
84
|
+
for (const pattern of HIGH_RISK_PATTERNS) {
|
|
85
|
+
if (pattern.re.test(blob))
|
|
86
|
+
signals.push(pattern.id);
|
|
87
|
+
}
|
|
88
|
+
const paths = input.allowedPaths ?? [];
|
|
89
|
+
const spread = pathSpreadScore(paths);
|
|
90
|
+
if (paths.some((p) => /package\.json|pnpm-lock|yarn\.lock|package-lock/.test(p))) {
|
|
91
|
+
signals.push("new-dependency");
|
|
92
|
+
}
|
|
93
|
+
// Explicit multi-domain language only — path count alone is not high-risk
|
|
94
|
+
if (spread >= 3 &&
|
|
95
|
+
/\b(多个领域|cross-module|multiple\s+packages)\b/i.test(blob)) {
|
|
96
|
+
signals.push("cross-domain-paths");
|
|
97
|
+
}
|
|
98
|
+
const highRisk = signals.length > 0;
|
|
99
|
+
const smallHits = [];
|
|
100
|
+
for (const pattern of SMALL_PATTERNS) {
|
|
101
|
+
if (pattern.re.test(blob))
|
|
102
|
+
smallHits.push(pattern.id);
|
|
103
|
+
}
|
|
104
|
+
// small only if: no high-risk, paths concentrated, no remote API signals, not supervised
|
|
105
|
+
const hasApiSignal = /\b(api|fetch|axios|graphql|endpoint|远程|接口)\b/i.test(blob) &&
|
|
106
|
+
!/\b(no\s+api|无接口|not-needed)\b/i.test(blob);
|
|
107
|
+
const concentrated = pathSpreadScore(paths) <= 1 && paths.length <= 4;
|
|
108
|
+
if (supervised) {
|
|
109
|
+
rejectedSignals.push(...smallHits.map((id) => `small:${id}:blocked-by-supervised`));
|
|
110
|
+
return {
|
|
111
|
+
selectedRisk: highRisk ? "high-risk" : "standard",
|
|
112
|
+
signals: supervised ? [...signals, "supervised-profile"] : signals,
|
|
113
|
+
rejectedSignals,
|
|
114
|
+
reason: highRisk
|
|
115
|
+
? `high-risk signals present under supervised: ${signals.join(", ")}`
|
|
116
|
+
: "supervised profile forces full gates (standard minimum)",
|
|
117
|
+
forceFullGates: true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (highRisk) {
|
|
121
|
+
rejectedSignals.push(...smallHits.map((id) => `small:${id}:blocked-by-high-risk`));
|
|
122
|
+
return {
|
|
123
|
+
selectedRisk: "high-risk",
|
|
124
|
+
signals,
|
|
125
|
+
rejectedSignals,
|
|
126
|
+
reason: `high-risk signals: ${[...new Set(signals)].join(", ")}`,
|
|
127
|
+
forceFullGates: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (smallHits.length > 0 &&
|
|
131
|
+
concentrated &&
|
|
132
|
+
!hasApiSignal &&
|
|
133
|
+
(input.complexity === "small" || smallHits.length >= 1)) {
|
|
134
|
+
return {
|
|
135
|
+
selectedRisk: "small",
|
|
136
|
+
signals: smallHits,
|
|
137
|
+
rejectedSignals,
|
|
138
|
+
reason: `small signals without high-risk: ${smallHits.join(", ")}`,
|
|
139
|
+
forceFullGates: false,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (smallHits.length > 0 && (hasApiSignal || !concentrated)) {
|
|
143
|
+
rejectedSignals.push(...smallHits.map((id) => `small:${id}:blocked-by-${hasApiSignal ? "api-signal" : "path-spread"}`));
|
|
144
|
+
return {
|
|
145
|
+
selectedRisk: "standard",
|
|
146
|
+
signals: smallHits,
|
|
147
|
+
rejectedSignals,
|
|
148
|
+
reason: hasApiSignal
|
|
149
|
+
? "small keywords present but API/remote signals require standard topology"
|
|
150
|
+
: "small keywords present but path spread requires standard topology",
|
|
151
|
+
forceFullGates: false,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
selectedRisk: "standard",
|
|
156
|
+
signals: ["default-standard"],
|
|
157
|
+
rejectedSignals,
|
|
158
|
+
reason: "ambiguous or standard frontend work; default standard topology",
|
|
159
|
+
forceFullGates: false,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { readFile, stat, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
|
|
4
|
+
export const FRONTEND_VERIFICATION_TRACE_SCHEMA_ID = "frontend-verification-trace-v1";
|
|
5
|
+
async function readJsonFile(filePath) {
|
|
6
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
7
|
+
}
|
|
8
|
+
function collectCommandLabels(...nodes) {
|
|
9
|
+
const map = new Map();
|
|
10
|
+
for (const { nodeId, record } of nodes) {
|
|
11
|
+
const labels = record.verifyEvidence?.commandLabels ?? [];
|
|
12
|
+
for (const label of labels) {
|
|
13
|
+
const owners = map.get(label) ?? [];
|
|
14
|
+
owners.push(nodeId);
|
|
15
|
+
map.set(label, owners);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return map;
|
|
19
|
+
}
|
|
20
|
+
function assertNodeFinished(nodeId, record) {
|
|
21
|
+
if (record.status !== "FINISHED") {
|
|
22
|
+
throw new Error(`trace: node ${nodeId} status must be FINISHED (got ${String(record.status)})`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function parseMockStrategyFromAssess(text) {
|
|
26
|
+
const match = text.match(/^\s*MOCK_STRATEGY:\s*([a-z0-9-]+)\s*$/im);
|
|
27
|
+
return match?.[1] ?? null;
|
|
28
|
+
}
|
|
29
|
+
async function assertFileAndSymbol(input) {
|
|
30
|
+
const issues = [];
|
|
31
|
+
const absolute = path.resolve(input.workspaceRoot, input.file);
|
|
32
|
+
const relative = path.relative(input.workspaceRoot, absolute);
|
|
33
|
+
if (relative.startsWith("..") ||
|
|
34
|
+
path.isAbsolute(relative) ||
|
|
35
|
+
relative.includes("..")) {
|
|
36
|
+
issues.push(`unsafe file path: ${input.file}`);
|
|
37
|
+
return issues;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const info = await stat(absolute);
|
|
41
|
+
if (!info.isFile()) {
|
|
42
|
+
issues.push(`path is not a file: ${input.file}`);
|
|
43
|
+
return issues;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
issues.push(`file not found: ${input.file}`);
|
|
48
|
+
return issues;
|
|
49
|
+
}
|
|
50
|
+
if (input.symbol) {
|
|
51
|
+
const content = await readFile(absolute, "utf8");
|
|
52
|
+
if (!content.includes(input.symbol)) {
|
|
53
|
+
issues.push(`symbol not found: ${input.symbol} in ${input.file}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return issues;
|
|
57
|
+
}
|
|
58
|
+
export async function runFrontendVerificationTraceGate(input) {
|
|
59
|
+
const contractRelative = input.contractRelativePath ??
|
|
60
|
+
"contracts/frontend-implementation-contract.json";
|
|
61
|
+
const contractPath = path.join(input.runDir, contractRelative);
|
|
62
|
+
let contractRaw;
|
|
63
|
+
try {
|
|
64
|
+
contractRaw = await readJsonFile(contractPath);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new Error(`trace: missing frontend implementation contract at ${contractRelative}`);
|
|
68
|
+
}
|
|
69
|
+
const parsed = frontendImplementationContractSchema.safeParse(contractRaw);
|
|
70
|
+
if (!parsed.success) {
|
|
71
|
+
throw new Error(`trace: invalid contract: ${parsed.error.issues
|
|
72
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
73
|
+
.join("; ")}`);
|
|
74
|
+
}
|
|
75
|
+
const contract = parsed.data;
|
|
76
|
+
// Prefer post-repair reverify nodes when present (M3); else initial verify nodes (M2).
|
|
77
|
+
const staticCandidates = [
|
|
78
|
+
"frontend-static-reverify-shell",
|
|
79
|
+
"frontend-static-verify-shell",
|
|
80
|
+
];
|
|
81
|
+
const behaviorCandidates = [
|
|
82
|
+
"frontend-behavior-reverify-shell",
|
|
83
|
+
"frontend-behavior-verify-shell",
|
|
84
|
+
];
|
|
85
|
+
async function loadFirstNode(candidates) {
|
|
86
|
+
const errors = [];
|
|
87
|
+
for (const nodeId of candidates) {
|
|
88
|
+
try {
|
|
89
|
+
const record = (await readJsonFile(path.join(input.runDir, `${nodeId}.json`)));
|
|
90
|
+
return { nodeId, record };
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
errors.push(nodeId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`trace: missing static/behavior verify node records (tried ${errors.join(", ")})`);
|
|
97
|
+
}
|
|
98
|
+
const staticNode = await loadFirstNode(staticCandidates);
|
|
99
|
+
const behaviorNode = await loadFirstNode(behaviorCandidates);
|
|
100
|
+
assertNodeFinished(staticNode.nodeId, staticNode.record);
|
|
101
|
+
assertNodeFinished(behaviorNode.nodeId, behaviorNode.record);
|
|
102
|
+
const labelOwners = collectCommandLabels({ nodeId: staticNode.nodeId, record: staticNode.record }, { nodeId: behaviorNode.nodeId, record: behaviorNode.record });
|
|
103
|
+
const assessPath = path.join(input.runDir, "frontend-mock-assess-pi.json");
|
|
104
|
+
try {
|
|
105
|
+
const assess = (await readJsonFile(assessPath));
|
|
106
|
+
const text = assess.assistantText?.trim() || assess.stdout?.trim() || "";
|
|
107
|
+
const strategy = parseMockStrategyFromAssess(text);
|
|
108
|
+
if (strategy && strategy !== contract.mockApi.strategy) {
|
|
109
|
+
throw new Error(`trace: mock strategy mismatch: assess=${strategy} contract=${contract.mockApi.strategy}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error instanceof Error &&
|
|
114
|
+
error.message.startsWith("trace: mock strategy mismatch")) {
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
// assess optional for pure unit fixtures without mock node
|
|
118
|
+
}
|
|
119
|
+
const targets = [];
|
|
120
|
+
const hardIssues = [];
|
|
121
|
+
for (const target of contract.verificationTargets) {
|
|
122
|
+
const issues = [];
|
|
123
|
+
const owners = labelOwners.get(target.commandLabel);
|
|
124
|
+
if (!owners?.length) {
|
|
125
|
+
issues.push(`commandLabel not executed in current-run frozen evidence: ${target.commandLabel}`);
|
|
126
|
+
}
|
|
127
|
+
const fileIssues = await assertFileAndSymbol({
|
|
128
|
+
workspaceRoot: input.workspaceRoot,
|
|
129
|
+
file: target.file,
|
|
130
|
+
symbol: target.symbol,
|
|
131
|
+
});
|
|
132
|
+
issues.push(...fileIssues);
|
|
133
|
+
const status = issues.length ? "failed" : "ok";
|
|
134
|
+
if (issues.length)
|
|
135
|
+
hardIssues.push(...issues.map((i) => `${target.id}: ${i}`));
|
|
136
|
+
targets.push({
|
|
137
|
+
id: target.id,
|
|
138
|
+
commandLabel: target.commandLabel,
|
|
139
|
+
file: target.file,
|
|
140
|
+
symbol: target.symbol,
|
|
141
|
+
status,
|
|
142
|
+
matchedNodeIds: owners ?? [],
|
|
143
|
+
issues,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (hardIssues.length) {
|
|
147
|
+
throw new Error(`trace: ${hardIssues.join("; ")}`);
|
|
148
|
+
}
|
|
149
|
+
const result = {
|
|
150
|
+
ok: true,
|
|
151
|
+
schemaId: FRONTEND_VERIFICATION_TRACE_SCHEMA_ID,
|
|
152
|
+
contractSchemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
153
|
+
browserStatus: "not-run",
|
|
154
|
+
visualStatus: "not-run",
|
|
155
|
+
targets,
|
|
156
|
+
mockStrategy: contract.mockApi.strategy,
|
|
157
|
+
};
|
|
158
|
+
if (input.writeArtifact !== false) {
|
|
159
|
+
const artifactRel = "contracts/frontend-verification-trace.json";
|
|
160
|
+
const artifactPath = path.join(input.runDir, artifactRel);
|
|
161
|
+
await mkdir(path.dirname(artifactPath), { recursive: true });
|
|
162
|
+
await writeFile(artifactPath, `${JSON.stringify({
|
|
163
|
+
schemaVersion: 1,
|
|
164
|
+
schemaId: result.schemaId,
|
|
165
|
+
contractSchemaId: result.contractSchemaId,
|
|
166
|
+
browserStatus: result.browserStatus,
|
|
167
|
+
visualStatus: result.visualStatus,
|
|
168
|
+
mockStrategy: result.mockStrategy,
|
|
169
|
+
targets: result.targets,
|
|
170
|
+
}, null, 2)}\n`, "utf8");
|
|
171
|
+
result.artifactPath = artifactPath;
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
export function formatFrontendVerificationTraceStdout(result) {
|
|
176
|
+
const lines = [
|
|
177
|
+
"Frontend verification trace: pass",
|
|
178
|
+
`Schema: ${result.schemaId}`,
|
|
179
|
+
`Contract: ${result.contractSchemaId}`,
|
|
180
|
+
`Mock strategy: ${result.mockStrategy}`,
|
|
181
|
+
`Browser: ${result.browserStatus}`,
|
|
182
|
+
`Visual: ${result.visualStatus}`,
|
|
183
|
+
`Targets: ${result.targets.length}`,
|
|
184
|
+
...result.targets.map((target) => `- ${target.id}: ${target.status} label=${JSON.stringify(target.commandLabel)} file=${target.file}`),
|
|
185
|
+
];
|
|
186
|
+
if (result.artifactPath) {
|
|
187
|
+
lines.push(`Artifact: ${result.artifactPath}`);
|
|
188
|
+
}
|
|
189
|
+
return lines.join("\n");
|
|
190
|
+
}
|