@tea-agent/loop-agent 0.13.0-beta.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.
Files changed (282) hide show
  1. package/AGENTS.md +157 -155
  2. package/CHANGELOG.md +301 -322
  3. package/README.md +335 -345
  4. package/bin/agent-worker.js +22 -22
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/commands/cursor-prompt.js +6 -6
  7. package/dist/commands/init.js +597 -528
  8. package/dist/commands/loop-benchmark.js +11 -11
  9. package/dist/commands/pi-reuse-benchmark.js +16 -16
  10. package/dist/executors/shell-executor.js +200 -21
  11. package/dist/infrastructure/evaluation/candidate-store.js +5 -1
  12. package/dist/sidecars/cursor-prompt/executor.js +1 -1
  13. package/dist/task/runtime.js +27 -27
  14. package/dist/worker/observe/static/api.js +46 -46
  15. package/dist/worker/observe/static/app.js +150 -150
  16. package/dist/worker/observe/static/constants.js +148 -148
  17. package/dist/worker/observe/static/copy.js +67 -67
  18. package/dist/worker/observe/static/dag-helpers.js +172 -172
  19. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  20. package/dist/worker/observe/static/dag-layout.js +83 -83
  21. package/dist/worker/observe/static/dag-model.js +72 -72
  22. package/dist/worker/observe/static/dom.js +212 -53
  23. package/dist/worker/observe/static/format-pool.js +67 -67
  24. package/dist/worker/observe/static/format.js +292 -292
  25. package/dist/worker/observe/static/index.html +308 -308
  26. package/dist/worker/observe/static/kpi.js +94 -94
  27. package/dist/worker/observe/static/relations.js +133 -133
  28. package/dist/worker/observe/static/router.js +93 -93
  29. package/dist/worker/observe/static/run-processing.js +148 -148
  30. package/dist/worker/observe/static/shell-chrome.js +68 -68
  31. package/dist/worker/observe/static/state.js +267 -253
  32. package/dist/worker/observe/static/styles.css +1902 -1902
  33. package/dist/worker/observe/static/views/batch.js +227 -227
  34. package/dist/worker/observe/static/views/dag-graph.js +172 -172
  35. package/dist/worker/observe/static/views/dag-inspector.js +627 -607
  36. package/dist/worker/observe/static/views/dag.js +371 -362
  37. package/dist/worker/observe/static/views/dashboard.js +509 -252
  38. package/dist/worker/observe/static/views/failures.js +143 -143
  39. package/dist/worker/observe/static/views/feature.js +492 -492
  40. package/dist/worker/observe/static/views/pool.js +350 -350
  41. package/dist/worker/observe/static/views/run.js +453 -453
  42. package/dist/worker/observe/static/views/session-timeline.js +219 -205
  43. package/dist/worker/observe/static/views/shell.js +7 -7
  44. package/dist/worker/observe/static/views/task.js +314 -314
  45. package/dist/worker/observe/static/views/timeline.js +163 -163
  46. package/dist/workflows/dag/backend-test-case-manifest.js +503 -0
  47. package/dist/workflows/dag/backend-test-execution-contract.js +353 -0
  48. package/dist/workflows/dag/backend-test-result-contract.js +568 -0
  49. package/dist/workflows/dag/canvas-observer.js +275 -275
  50. package/dist/workflows/dag/decision-envelope.js +57 -2
  51. package/dist/workflows/dag/frontend-implementation-contract.js +240 -0
  52. package/dist/workflows/dag/frontend-project-capability.js +309 -0
  53. package/dist/workflows/dag/frontend-repair.js +341 -0
  54. package/dist/workflows/dag/frontend-risk.js +161 -0
  55. package/dist/workflows/dag/frontend-verification-trace.js +190 -0
  56. package/dist/workflows/dag/init-hybrid.js +1020 -125
  57. package/dist/workflows/dag/repair-artifact.js +43 -3
  58. package/dist/workflows/dag/skill-instructions.js +4 -2
  59. package/dist/workflows/dag/types.js +29 -8
  60. package/docs/README.md +105 -104
  61. package/docs/agent-dag-recovery-playbook.md +195 -195
  62. package/docs/agent-dag-runner.md +67 -67
  63. package/docs/architecture/README.md +26 -26
  64. package/docs/architecture/dag-execution.md +140 -140
  65. package/docs/architecture/evolution.md +54 -54
  66. package/docs/architecture/facts-and-state.md +71 -71
  67. package/docs/architecture/runtime-boundaries.md +191 -191
  68. package/docs/architecture/system-overview.md +93 -93
  69. package/docs/architecture/worker-and-feature.md +85 -85
  70. package/docs/cursor-prompt-sidecar.md +36 -36
  71. package/docs/decisions/README.md +18 -18
  72. package/docs/design/README.md +167 -167
  73. package/docs/development-principles.md +73 -73
  74. package/docs/exec-plans/README.md +6 -6
  75. package/docs/exec-plans/active/README.md +1 -4
  76. package/docs/exec-plans/completed/README.md +106 -84
  77. package/docs/feature-workflow.md +414 -389
  78. package/docs/harness-methodology-debugging.md +153 -153
  79. package/docs/harness-methodology-tdd.md +130 -130
  80. package/docs/harness-methodology-verification.md +27 -27
  81. package/docs/init-surface.manifest.json +307 -289
  82. package/docs/loop-agent-harness.md +142 -142
  83. package/docs/production-readiness.md +96 -96
  84. package/docs/progress/README.md +76 -60
  85. package/docs/reports/README.md +150 -108
  86. package/docs/skills/README.md +7 -7
  87. package/docs/skills/vetted-skill-registry.md +29 -29
  88. package/docs/templates/adr.md +60 -60
  89. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  90. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  91. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  92. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  93. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  94. package/docs/templates/agent-dag-report.schema.json +473 -473
  95. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  96. package/docs/templates/agent-dag.base.json +190 -190
  97. package/docs/templates/agent-dag.final-verification.json +185 -185
  98. package/docs/templates/agent-dag.schema.json +411 -411
  99. package/docs/templates/agent-dag.supervised-implementation.json +620 -501
  100. package/docs/templates/backend-test-analysis.schema.json +44 -44
  101. package/docs/templates/backend-test-case-manifest.schema.json +190 -0
  102. package/docs/templates/backend-test-dag.classify.prompt.md +75 -0
  103. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +204 -202
  104. package/docs/templates/backend-test-dag.json +559 -311
  105. package/docs/templates/backend-test-dag.retrospect.prompt.md +139 -125
  106. package/docs/templates/backend-test-dag.review-cases.prompt.md +83 -81
  107. package/docs/templates/backend-test-execution.schema.json +133 -0
  108. package/docs/templates/backend-test-result.schema.json +99 -0
  109. package/docs/templates/branch-merge-report.md +93 -0
  110. package/docs/templates/exec-plan.md +64 -64
  111. package/docs/templates/feature-spec.md +53 -53
  112. package/docs/templates/frontend-design-contract.md +42 -42
  113. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -0
  114. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -0
  115. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -0
  116. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -0
  117. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -0
  118. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -0
  119. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -0
  120. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -0
  121. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -0
  122. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -0
  123. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -0
  124. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -0
  125. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -0
  126. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -0
  127. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -0
  128. package/docs/templates/frontend-eval/metrics.md +138 -0
  129. package/docs/templates/frontend-eval/smoke-targets.md +53 -0
  130. package/docs/templates/frontend-implementation-contract.schema.json +27 -0
  131. package/docs/templates/frontend-task-constraints.md +35 -35
  132. package/docs/templates/frontend-task-requirement.md +70 -70
  133. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -5
  134. package/docs/templates/frontend-test-dag.json +23 -23
  135. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -3
  136. package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -3
  137. package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -3
  138. package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -3
  139. package/docs/templates/harness.schema.json +221 -221
  140. package/docs/templates/hybrid-dag.json +188 -188
  141. package/docs/templates/init-evolution-review.md +35 -35
  142. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  143. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
  144. package/docs/templates/knowledge-sync-dag.json +178 -178
  145. package/docs/templates/knowledge-sync-draft.schema.json +71 -71
  146. package/docs/templates/product-line/AGENTS.md +8 -8
  147. package/docs/templates/product-line/README.md +9 -9
  148. package/docs/templates/product-line/acceptance.yaml +14 -14
  149. package/docs/templates/product-line/closeout.yaml +9 -9
  150. package/docs/templates/product-line/design.md +13 -13
  151. package/docs/templates/product-line/links.md +10 -10
  152. package/docs/templates/product-line/requirement.md +17 -17
  153. package/docs/templates/product-line/task-graph.yaml +15 -15
  154. package/docs/templates/product-line/task.yaml +64 -64
  155. package/docs/templates/product-line/test-plan.md +7 -7
  156. package/docs/templates/production-readiness-checklist.md +57 -57
  157. package/docs/templates/progress-log.md +17 -17
  158. package/docs/templates/project-start-checklist.md +9 -9
  159. package/docs/templates/qa-report.md +48 -48
  160. package/docs/templates/sprint-contract.md +29 -29
  161. package/docs/templates/worker-dogfood-evidence.md +80 -80
  162. package/docs/templates/worker-dogfood-setup.md +68 -68
  163. package/docs/verification-matrix.md +70 -70
  164. package/examples/decision-gate-agent-dag.json +177 -177
  165. package/examples/example-dag.json +46 -46
  166. package/examples/hybrid-loop-agent-dag.json +189 -189
  167. package/harness.json +66 -66
  168. package/package.json +52 -88
  169. package/scripts/check-product-line-docs.sh +29 -29
  170. package/scripts/check-task-pool-root.sh +32 -32
  171. package/scripts/kb-bootstrap-init-skeleton.sh +240 -240
  172. package/scripts/kb-graph-incremental-prepare.mjs +386 -386
  173. package/scripts/kb-graph-incremental-prepare.sh +5 -5
  174. package/scripts/kb-graph-materialize.mjs +105 -105
  175. package/scripts/kb-graph-materialize.sh +4 -4
  176. package/scripts/kb-graph-promote.mjs +164 -164
  177. package/scripts/kb-graph-promote.sh +4 -4
  178. package/scripts/kb-query.mjs +554 -554
  179. package/scripts/kb-query.sh +5 -5
  180. package/skills/agent-worker/SKILL.md +39 -39
  181. package/skills/agent-worker/references/agent-worker-operator.md +60 -60
  182. package/skills/ai-engineering-context/SKILL.md +48 -48
  183. package/skills/analyze-product-dependencies/SKILL.md +67 -67
  184. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
  185. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
  186. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
  187. package/skills/analyze-product-dependencies/references/example.md +76 -76
  188. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
  189. package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
  190. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
  191. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
  192. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
  193. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
  194. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
  195. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
  196. package/skills/analyze-product-requirements/SKILL.md +90 -90
  197. package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
  198. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
  199. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
  200. package/skills/analyze-product-requirements/references/example.md +86 -86
  201. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
  202. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
  203. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
  204. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
  205. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
  206. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
  207. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
  208. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
  209. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
  210. package/skills/browser-tools/SKILL.md +196 -0
  211. package/skills/browser-tools/browser-content.js +103 -0
  212. package/skills/browser-tools/browser-cookies.js +35 -0
  213. package/skills/browser-tools/browser-eval.js +53 -0
  214. package/skills/browser-tools/browser-hn-scraper.js +108 -0
  215. package/skills/browser-tools/browser-nav.js +44 -0
  216. package/skills/browser-tools/browser-pick.js +162 -0
  217. package/skills/browser-tools/browser-screenshot.js +34 -0
  218. package/skills/browser-tools/browser-start.js +86 -0
  219. package/skills/browser-tools/package-lock.json +2556 -0
  220. package/skills/browser-tools/package.json +19 -0
  221. package/skills/code-review-core/SKILL.md +20 -20
  222. package/skills/codebase-scout/SKILL.md +19 -19
  223. package/skills/frontend-design-review/SKILL.md +66 -66
  224. package/skills/frontend-design-review/references/review-checklist.md +58 -58
  225. package/skills/frontend-implementation/SKILL.md +49 -47
  226. package/skills/frontend-implementation/references/code-standards.md +32 -32
  227. package/skills/frontend-implementation/references/design-spec.md +46 -46
  228. package/skills/frontend-implementation/references/node-contracts.md +27 -76
  229. package/skills/frontend-review/SKILL.md +59 -59
  230. package/skills/frontend-review/references/review-findings.md +47 -47
  231. package/skills/frontend-verification/SKILL.md +53 -53
  232. package/skills/frontend-verification/references/verification-checklist.md +68 -68
  233. package/skills/grill-me/SKILL.md +10 -10
  234. package/skills/grill-with-docs/SKILL.md +88 -88
  235. package/skills/grill-with-docs/adr-format.md +47 -47
  236. package/skills/grill-with-docs/context-format.md +60 -60
  237. package/skills/init-capability-evolution/SKILL.md +70 -70
  238. package/skills/loop-agent/SKILL.md +151 -151
  239. package/skills/loop-agent/references/README.md +67 -67
  240. package/skills/loop-agent/references/command-reference.md +527 -505
  241. package/skills/loop-agent/references/docs-converge.md +126 -126
  242. package/skills/loop-agent/references/harness-policy.md +263 -263
  243. package/skills/loop-agent/references/hybrid-dag.md +243 -238
  244. package/skills/loop-agent/references/learned/README.md +21 -21
  245. package/skills/loop-agent/references/long-running-loop.md +57 -57
  246. package/skills/loop-agent/references/model-routing.md +36 -36
  247. package/skills/loop-agent/references/multi-worktree.md +54 -54
  248. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  249. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  250. package/skills/loop-agent/references/pi-prompt.md +23 -23
  251. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  252. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  253. package/skills/loop-agent/references/task-workflow.md +89 -89
  254. package/skills/loop-agent/references/verification-and-failure-handling.md +141 -139
  255. package/skills/playwright-cli/SKILL.md +420 -420
  256. package/skills/playwright-cli/references/element-attributes.md +23 -23
  257. package/skills/playwright-cli/references/playwright-tests.md +39 -39
  258. package/skills/playwright-cli/references/request-mocking.md +87 -87
  259. package/skills/playwright-cli/references/running-code.md +241 -241
  260. package/skills/playwright-cli/references/session-management.md +225 -225
  261. package/skills/playwright-cli/references/storage-state.md +275 -275
  262. package/skills/playwright-cli/references/test-generation.md +433 -433
  263. package/skills/playwright-cli/references/tracing.md +139 -139
  264. package/skills/playwright-cli/references/video-recording.md +143 -143
  265. package/skills/playwright-cli-case-generator/SKILL.md +74 -74
  266. package/skills/requesting-code-review/SKILL.md +101 -101
  267. package/skills/requesting-code-review/code-reviewer.md +168 -168
  268. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  269. package/skills/systematic-debugging/SKILL.md +296 -296
  270. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  271. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  272. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  273. package/skills/systematic-debugging/find-polluter.sh +63 -63
  274. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  275. package/skills/systematic-debugging/test-academic.md +14 -14
  276. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  277. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  278. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  279. package/skills/test-driven-development/SKILL.md +20 -20
  280. package/skills/using-git-worktrees/SKILL.md +215 -215
  281. package/skills/verification-before-completion/SKILL.md +154 -154
  282. package/skills/webapp-testing/SKILL.md +19 -19
@@ -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
+ }