@tea-agent/loop-agent 0.42.0-next.9 → 0.42.1
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/CHANGELOG.md +111 -45
- package/dist/application/dag/run-dag.js +8 -2
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/application/task-lifecycle/advance.js +26 -8
- package/dist/application/task-lifecycle/observe.js +43 -29
- package/dist/application/task-lifecycle/plan-transitions.js +5 -4
- package/dist/build-stamp.json +3 -3
- package/dist/cli/program.js +1 -1
- package/dist/commands/dag-rerun-task.js +2 -0
- package/dist/commands/task-source-prepare.js +3 -1
- package/dist/executors/dag-pi-executor.js +2584 -600
- package/dist/executors/pi-executor.js +22 -1
- package/dist/executors/pi-extension-resolver.js +14 -2
- package/dist/executors/pi-sdk-executor.js +140 -39
- package/dist/executors/shell-executor.js +135 -55
- package/dist/shared/dag-failure-category.js +6 -0
- package/dist/shared/frontend-execution-policy.js +23 -0
- package/dist/task/config-types.js +4 -0
- package/dist/task/contract/apply.js +36 -2
- package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
- package/dist/task/source-prepare/ledger-review.js +6 -9
- package/dist/task/source-prepare/parse-intent.js +7 -0
- package/dist/task/source-prepare/semantic-intake.js +16 -26
- package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
- package/dist/task/source-references.js +48 -23
- package/dist/worker/console/chat/assistant-content.js +23 -2
- package/dist/worker/console/chat/browser-policy.js +143 -0
- package/dist/worker/console/chat/browser-routes.js +148 -0
- package/dist/worker/console/chat/chat-event-store.js +4 -2
- package/dist/worker/console/chat/explore-tools.js +13 -0
- package/dist/worker/console/chat/pi-runtime.js +173 -94
- package/dist/worker/console/chat/resource-loader.js +4 -1
- package/dist/worker/console/chat/routes.js +208 -66
- package/dist/worker/console/chat/scm-routes.js +217 -0
- package/dist/worker/console/chat/scm-service.js +283 -0
- package/dist/worker/console/chat/scm-tools.js +111 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +78 -11
- package/dist/worker/console/chat/session-catalog.js +32 -0
- package/dist/worker/console/chat/session-mode-view.js +48 -0
- package/dist/worker/console/chat/session-mode.js +218 -0
- package/dist/worker/console/chat/session-store.js +27 -7
- package/dist/worker/console/chat/shortcuts.js +6 -0
- package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
- package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
- package/dist/worker/console/chat/subagents/index.js +5 -0
- package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
- package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
- package/dist/worker/console/chat/subagents/types.js +13 -0
- package/dist/worker/console/chat/terminal-routes.js +216 -0
- package/dist/worker/console/chat/terminal-sessions.js +284 -0
- package/dist/worker/console/chat/terminal-tools.js +199 -0
- package/dist/worker/console/chat/tool-preview.js +21 -0
- package/dist/worker/console/chat/tools.js +13 -1
- package/dist/worker/console/chat/turn-process.js +1 -0
- package/dist/worker/console/interview/tools.js +1 -0
- package/dist/worker/console/server.js +2 -28
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-C9eI7nEo.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-VJWsxWhB.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-BGcyiMSo.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-BScnFwyi.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Bl9_BsLi.js} +1 -1
- package/dist/worker/console/static/assets/channel-B6sQYuOE.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-F1uKxiUt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-rSRMynVu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-DAsJm1WD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-DQIdVI0a.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-4A_8rJ-Q.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-BHnT-vjh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-Rq7QKBkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BXh-AI2u.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-BblcLy8m.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-zHZmVnB6.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Z6s5QoeI.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Z6s5QoeI.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-Cmwz0Wi4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-Dq76MHQU.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-zPahV1fM.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-DQJ3xPc_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BwX7u6fv.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CodnDCco.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-gAjtWkxG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-CGCqz0cS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-RHwYEywd.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-B6seb_p_.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-CmwCKYe8.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-BfNsaZ0V.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-zmYgi_Z3.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-BDEzzSKF.js} +1 -1
- package/dist/worker/console/static/assets/index-DgenUAfc.js +468 -0
- package/dist/worker/console/static/assets/index-aF4u-Y__.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-DeSz8l42.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-B1LV29aH.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-CCw4W2oU.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BozaMoRJ.js} +1 -1
- package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-G1JR40SX.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-CCgPF0oA.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-CEqnCHnD.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-DpmQPKuI.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-BP501AxR.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-D1dBhl_A.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-DgZDS5pY.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-BzB3pyIq.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-B9ZprFFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CC2-_XrA.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-UVtAUqgB.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-CWcS5JYG.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BooX8u1Q.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-C4p-dn06.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D64_eqqE.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-Dnz52OHX.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-CzTE0Nfu.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-NAR9cjQQ.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-DVMk8413.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/chat-view-types.js +1 -1
- package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +142 -7
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +44 -0
- package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
- package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +33 -18
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +48 -16
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +170 -7
- package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
- package/dist/worker/console/workspace-context.js +15 -1
- package/dist/worker/observe/node-transparency.js +81 -72
- package/dist/worker/observe/routes.js +20 -1
- package/dist/worker/observe/static/constants.js +22 -22
- package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
- package/dist/worker/observe/static/dag-history-labels.js +1 -0
- package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
- package/dist/worker/observe/static/dag-inspector-humanize.js +342 -0
- package/dist/worker/observe/static/dag-node-purpose.js +10 -10
- package/dist/worker/observe/static/dom.js +20 -1
- package/dist/worker/observe/static/format-pool.d.ts +2 -0
- package/dist/worker/observe/static/format-pool.js +6 -0
- package/dist/worker/observe/static/format.js +7 -0
- package/dist/worker/observe/static/index.html +4 -4
- package/dist/worker/observe/static/inspect-workspace.js +34 -7
- package/dist/worker/observe/static/inspector-submission.js +32 -0
- package/dist/worker/observe/static/kpi.js +1 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
- package/dist/worker/observe/static/relations.js +7 -5
- package/dist/worker/observe/static/router.js +13 -0
- package/dist/worker/observe/static/run-processing.js +2 -0
- package/dist/worker/observe/static/shell-chrome.js +36 -3
- package/dist/worker/observe/static/state.js +35 -2
- package/dist/worker/observe/static/styles.css +431 -39
- package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
- package/dist/worker/observe/static/task-failure-labels.js +67 -0
- package/dist/worker/observe/static/task-history.js +12 -0
- package/dist/worker/observe/static/views/batch.js +6 -13
- package/dist/worker/observe/static/views/dag-graph.js +51 -4
- package/dist/worker/observe/static/views/dag-inspector.js +1069 -426
- package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
- package/dist/worker/observe/static/views/dag.d.ts +6 -0
- package/dist/worker/observe/static/views/dag.js +64 -22
- package/dist/worker/observe/static/views/dags.js +2 -0
- package/dist/worker/observe/static/views/dashboard.js +21 -12
- package/dist/worker/observe/static/views/failures.js +21 -11
- package/dist/worker/observe/static/views/feature.js +11 -29
- package/dist/worker/observe/static/views/pool.js +37 -28
- package/dist/worker/observe/static/views/run.js +48 -5
- package/dist/worker/observe/static/views/session-timeline.js +194 -243
- package/dist/worker/observe/static/views/task.js +81 -62
- package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
- package/dist/workflows/dag/budget-enforcement.js +53 -3
- package/dist/workflows/dag/dag-retry-schema.js +3 -0
- package/dist/workflows/dag/frontend-capacity.js +9 -0
- package/dist/workflows/dag/frontend-contract-facts.js +130 -0
- package/dist/workflows/dag/frontend-design-policy.js +103 -17
- package/dist/workflows/dag/frontend-durable-tools.js +193 -0
- package/dist/workflows/dag/frontend-execution-groups.js +24 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +423 -41
- package/dist/workflows/dag/frontend-input-projection.js +76 -0
- package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
- package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
- package/dist/workflows/dag/frontend-plan-render.js +21 -3
- package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
- package/dist/workflows/dag/frontend-recovery-controller.js +32 -8
- package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +4 -1
- package/dist/workflows/dag/frontend-recovery-run.js +146 -16
- package/dist/workflows/dag/frontend-review-scopes.js +139 -0
- package/dist/workflows/dag/frontend-risk.js +2 -0
- package/dist/workflows/dag/frontend-session-budget.js +249 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +37 -3
- package/dist/workflows/dag/frontend-shape-facts.js +12 -2
- package/dist/workflows/dag/frontend-shape.js +46 -7
- package/dist/workflows/dag/frontend-test-execution-evidence.js +49 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +14 -0
- package/dist/workflows/dag/frontend-verification-trace.js +38 -4
- package/dist/workflows/dag/frontend-writer-admission.js +2 -2
- package/dist/workflows/dag/init-hybrid.js +70 -35
- package/dist/workflows/dag/node-execution.js +225 -226
- package/dist/workflows/dag/prompt.js +4 -0
- package/dist/workflows/dag/recovery-lease.js +106 -16
- package/dist/workflows/dag/rerun-feedback.js +1 -0
- package/dist/workflows/dag/rerun-plan.js +96 -4
- package/dist/workflows/dag/rerun-run.js +28 -6
- package/dist/workflows/dag/rerun-task.js +221 -18
- package/dist/workflows/dag/retry-policy.js +27 -10
- package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
- package/dist/workflows/dag/runner.js +246 -19
- package/dist/workflows/dag/structured-output-repair.js +4 -1
- package/dist/workflows/dag/types.js +10 -3
- package/dist/workflows/dag/validate.js +10 -8
- package/dist/workflows/dag/workspace-checkpoint.js +66 -0
- package/docs/operations/README.md +2 -0
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/backend-test-dag.json +6 -4
- package/docs/templates/frontend-design-contract.md +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +68 -4
- package/docs/templates/frontend-implementation-dag.json +5 -5
- package/harness.json +1 -1
- package/package.json +8 -6
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +19 -3
- package/skills/frontend-design-review/SKILL.md +12 -11
- package/skills/frontend-plan/SKILL.md +22 -2
- package/skills/frontend-plan/references/decision-contract.md +114 -5
- package/skills/frontend-plan/references/design-decisions.md +32 -0
- package/skills/frontend-review/SKILL.md +10 -11
- package/skills/frontend-scout/references/scout-evidence.md +4 -0
- package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
- package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { frontendExecutionSchema, collectFrontendExecutionGroups } from "./frontend-execution-groups.js";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
3
4
|
import { readFile, stat } from "node:fs/promises";
|
|
@@ -9,10 +10,12 @@ import { findPackageRoot } from "../../shared/package-metadata.js";
|
|
|
9
10
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
10
11
|
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
11
12
|
import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
|
|
13
|
+
import { extractRequiredDeliverablePaths } from "./frontend-contract-facts.js";
|
|
14
|
+
export { extractRequiredDeliverablePaths } from "./frontend-contract-facts.js";
|
|
12
15
|
// Cycle-safe value imports: the check functions are hoisted declarations used
|
|
13
16
|
// only at call time, so the contract module and the policy module can depend
|
|
14
17
|
// on each other without module-init ordering issues.
|
|
15
|
-
import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
|
|
18
|
+
import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, checkRequiredDeliverables, checkUxNameCollisions, } from "./frontend-design-policy.js";
|
|
16
19
|
import { isDagSourceBindingV2 } from "./types.js";
|
|
17
20
|
import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
|
|
18
21
|
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
@@ -25,6 +28,7 @@ export const frontendNormalizationActionSchema = z.enum([
|
|
|
25
28
|
"drop-unresolved-verification-symbols",
|
|
26
29
|
"inject-source-binding",
|
|
27
30
|
"compact-canonical-contract",
|
|
31
|
+
"resolve-verification-command-ids",
|
|
28
32
|
]);
|
|
29
33
|
export function sha256Hex(input) {
|
|
30
34
|
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
@@ -95,20 +99,200 @@ export async function writeCompactDeterministicJsonArtifact(runDir, relativePath
|
|
|
95
99
|
return { path: targetPath, sha256: sha256Hex(json) };
|
|
96
100
|
}
|
|
97
101
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
102
|
+
* Verification mode v2 (AC-V2-001..004).
|
|
103
|
+
*
|
|
104
|
+
* The mode is owned by the runtime, never by the model: plan tools submit a
|
|
105
|
+
* `commandId` referencing the generation-time frozen command directory, and
|
|
106
|
+
* materialization resolves `mode` + `commandLabel` from that directory. This
|
|
107
|
+
* makes contradictory combinations such as `type: component` bound to
|
|
108
|
+
* `npm run build` unrepresentable instead of failing late at the writer
|
|
109
|
+
* focused-check.
|
|
110
|
+
*/
|
|
111
|
+
export const frontendVerificationModeSchema = z.enum(["static", "behavior"]);
|
|
112
|
+
/**
|
|
113
|
+
* Optional display-only test scope. It never participates in control flow:
|
|
114
|
+
* trace checks, writeSet derivation and behavior-required evaluation read
|
|
115
|
+
* only the runtime-derived `mode`.
|
|
116
|
+
*/
|
|
117
|
+
export const frontendVerificationScopeSchema = z.enum([
|
|
118
|
+
"unit",
|
|
119
|
+
"component",
|
|
120
|
+
"integration",
|
|
121
|
+
]);
|
|
122
|
+
function laneToVerificationMode(lane) {
|
|
123
|
+
return lane === "static" || lane === "lint" ? "static" : "behavior";
|
|
124
|
+
}
|
|
125
|
+
const VERIFY_RUNNER_RE = /\b(vitest|jest|mocha|ava|tap|playwright|cypress|testcafe|nightwatch|webdriverio|pytest|go test)\b/i;
|
|
126
|
+
const VERIFY_E2E_RE = /(^|[\s:"'=/-])e2e([\s"'/-]|$)/i;
|
|
127
|
+
const VERIFY_STATIC_SCRIPT_WORDS = new Set([
|
|
128
|
+
"typecheck",
|
|
129
|
+
"check",
|
|
130
|
+
"lint",
|
|
131
|
+
"eslint",
|
|
132
|
+
"tsc",
|
|
133
|
+
"build",
|
|
134
|
+
]);
|
|
135
|
+
// A script segment carrying test semantics wins over static words:
|
|
136
|
+
// `test:build` builds nothing observable by itself — it runs tests, and
|
|
137
|
+
// `check:unit` is a unit-test entrypoint, not a project-wide check.
|
|
138
|
+
const VERIFY_BEHAVIOR_SCRIPT_WORDS = new Set([
|
|
139
|
+
"test",
|
|
140
|
+
"tests",
|
|
141
|
+
"unit",
|
|
142
|
+
"spec",
|
|
143
|
+
"specs",
|
|
144
|
+
"e2e",
|
|
145
|
+
"integration",
|
|
146
|
+
"coverage",
|
|
147
|
+
]);
|
|
148
|
+
function extractRunScriptName(command) {
|
|
149
|
+
// Skip package-manager flags between the binary and the script, e.g.
|
|
150
|
+
// `npm --prefix website run build` executes the `build` script.
|
|
151
|
+
const tokens = command.match(/(?:npm|pnpm|yarn|bun)(?:\.cmd|\.ps1)?\s+(.+)/i)?.[1];
|
|
152
|
+
if (!tokens)
|
|
153
|
+
return undefined;
|
|
154
|
+
const parts = tokens.split(/\s+/).filter(Boolean);
|
|
155
|
+
let index = 0;
|
|
156
|
+
while (index < parts.length) {
|
|
157
|
+
const part = parts[index].replace(/["']/g, "");
|
|
158
|
+
if (part === "--prefix" ||
|
|
159
|
+
part === "--cwd" ||
|
|
160
|
+
part === "-C" ||
|
|
161
|
+
part === "--workspace") {
|
|
162
|
+
index += 2;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (part.startsWith("-")) {
|
|
166
|
+
index += 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (/^(run|exec)$/i.test(part)) {
|
|
170
|
+
index += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
return part;
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Whether a command carries known verification semantics. Markdown intake
|
|
179
|
+
* uses this before lane classification so prose cannot authorize an arbitrary
|
|
180
|
+
* package script such as `npm run deploy`. Explicit task verifyCommands remain
|
|
181
|
+
* operator-authorized and intentionally do not use this admission check.
|
|
182
|
+
*/
|
|
183
|
+
export function isRecognizedFrontendVerifyCommandText(command) {
|
|
184
|
+
if (VERIFY_RUNNER_RE.test(command) || VERIFY_E2E_RE.test(command))
|
|
185
|
+
return true;
|
|
186
|
+
const scriptName = extractRunScriptName(command);
|
|
187
|
+
if (scriptName) {
|
|
188
|
+
const segments = scriptName.toLowerCase().split(/[:/_-]+/);
|
|
189
|
+
return segments.some((segment) => VERIFY_BEHAVIOR_SCRIPT_WORDS.has(segment) ||
|
|
190
|
+
VERIFY_STATIC_SCRIPT_WORDS.has(segment));
|
|
191
|
+
}
|
|
192
|
+
const head = command.trim().split(/\s+/, 1)[0] ?? "";
|
|
193
|
+
const binary = head.split(/[\\/]/).pop() ?? "";
|
|
194
|
+
return (/^(tsc|eslint|stylelint|lint|typecheck|check|build)$/i.test(binary) ||
|
|
195
|
+
/\b(tsc|eslint|typecheck|check-types)\b/i.test(command) ||
|
|
196
|
+
/\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command));
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Classify a raw verification command text into the runtime-owned mode.
|
|
200
|
+
* Shared single implementation used by DAG generation (lane split) and
|
|
201
|
+
* documented here so generation and validation can never disagree.
|
|
202
|
+
*
|
|
203
|
+
* Order matters: test runners and e2e harnesses always prove behavior, even
|
|
204
|
+
* when their arguments mention build/check/lint paths (e.g.
|
|
205
|
+
* `npx vitest run test/build.test.ts`). For npm scripts, a segment with
|
|
206
|
+
* test semantics (`test:build`, `check:unit`) wins over static words; pure
|
|
207
|
+
* static scripts (`npm run check`, `check:types`) and leading binaries
|
|
208
|
+
* (`tsc`, `eslint`) stay static. Package-manager flags (`npm --prefix
|
|
209
|
+
* website run build`) are skipped when locating the script.
|
|
210
|
+
*/
|
|
211
|
+
export function classifyFrontendVerifyCommandText(command) {
|
|
212
|
+
if (VERIFY_RUNNER_RE.test(command) || VERIFY_E2E_RE.test(command)) {
|
|
213
|
+
return "behavior";
|
|
214
|
+
}
|
|
215
|
+
const scriptName = extractRunScriptName(command);
|
|
216
|
+
if (scriptName) {
|
|
217
|
+
const segments = scriptName.toLowerCase().split(/[:/_-]+/);
|
|
218
|
+
if (segments.some((segment) => VERIFY_BEHAVIOR_SCRIPT_WORDS.has(segment))) {
|
|
219
|
+
return "behavior";
|
|
220
|
+
}
|
|
221
|
+
return segments.some((segment) => VERIFY_STATIC_SCRIPT_WORDS.has(segment))
|
|
222
|
+
? "static"
|
|
223
|
+
: "behavior";
|
|
224
|
+
}
|
|
225
|
+
const head = command.trim().split(/\s+/, 1)[0] ?? "";
|
|
226
|
+
const binary = head.split(/[\\/]/).pop() ?? "";
|
|
227
|
+
if (/^(tsc|eslint|stylelint|lint|typecheck|check|build)$/i.test(binary) ||
|
|
228
|
+
/\b(tsc|eslint|typecheck|check-types)\b/i.test(command) ||
|
|
229
|
+
/\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command)) {
|
|
230
|
+
return "static";
|
|
231
|
+
}
|
|
232
|
+
return "behavior";
|
|
233
|
+
}
|
|
234
|
+
export function slugFrontendVerifyCommandId(label, taken) {
|
|
235
|
+
const slug = label
|
|
236
|
+
.toLowerCase()
|
|
237
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
238
|
+
.replace(/^-+|-+$/g, "")
|
|
239
|
+
.slice(0, 48) || "verify";
|
|
240
|
+
let candidate = `verify-${slug}`;
|
|
241
|
+
let counter = 2;
|
|
242
|
+
while (taken.has(candidate)) {
|
|
243
|
+
candidate = `verify-${slug}-${counter++}`;
|
|
244
|
+
}
|
|
245
|
+
taken.add(candidate);
|
|
246
|
+
return candidate;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Build the generation-time frozen command directory. Lanes come from the
|
|
250
|
+
* DAG generation split (init-hybrid classifies each verify command once);
|
|
251
|
+
* labels stay human-readable while `commandId` is the stable machine key
|
|
252
|
+
* the plan references. Mock/lint lanes keep their provenance in `lane` but
|
|
253
|
+
* map to the behavior/static mode the trace gate enforces. Command texts
|
|
254
|
+
* (parallel to labels; usually from the bundle evidence) bind each id to
|
|
255
|
+
* the exact frozen command so same-label entries across lanes cannot drift
|
|
256
|
+
* into the same identity silently.
|
|
257
|
+
*/
|
|
258
|
+
export function buildFrontendVerifyCommandDirectory(lanes) {
|
|
259
|
+
const taken = new Set();
|
|
260
|
+
const entries = [];
|
|
261
|
+
const push = (labels, lane, texts) => {
|
|
262
|
+
labels.forEach((label, index) => {
|
|
263
|
+
const trimmed = label.trim();
|
|
264
|
+
if (!trimmed)
|
|
265
|
+
return;
|
|
266
|
+
entries.push({
|
|
267
|
+
commandId: slugFrontendVerifyCommandId(trimmed, taken),
|
|
268
|
+
label: trimmed,
|
|
269
|
+
mode: laneToVerificationMode(lane),
|
|
270
|
+
lane,
|
|
271
|
+
command: texts?.[index]?.trim() ?? "",
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
push(lanes.staticLabels, "static", lanes.staticCommandTexts);
|
|
276
|
+
push(lanes.lintLabels ?? [], "lint", lanes.lintCommandTexts);
|
|
277
|
+
push(lanes.behaviorLabels, "behavior", lanes.behaviorCommandTexts);
|
|
278
|
+
push(lanes.mockLabels ?? [], "mock", lanes.mockCommandTexts);
|
|
279
|
+
return entries;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Derive the frozen command directory from the DAG run spec (run.json),
|
|
283
|
+
* reading the per-lane evidence of the `frontend-verify-shell` bundle. The
|
|
284
|
+
* run spec is written before any node executes, so it is always available
|
|
285
|
+
* when the prewrite gate materializes the contract. When absent (e.g. test
|
|
286
|
+
* fixtures), there is no directory to validate against and callers skip
|
|
287
|
+
* directory validation downstream — mirroring the legacy label-set behavior.
|
|
101
288
|
*/
|
|
102
|
-
async function
|
|
289
|
+
export async function deriveFrontendVerifyCommandDirectoryFromRun(runDir) {
|
|
103
290
|
const specPath = path.join(runDir, "run.json");
|
|
104
291
|
let raw;
|
|
105
292
|
try {
|
|
106
293
|
raw = await readFile(specPath, "utf8");
|
|
107
294
|
}
|
|
108
295
|
catch {
|
|
109
|
-
// run.json is guaranteed to exist in production (DAG runner writes it
|
|
110
|
-
// before any node executes). When absent (e.g. test fixtures), there is
|
|
111
|
-
// no frozen set to validate against and the check is skipped downstream.
|
|
112
296
|
return [];
|
|
113
297
|
}
|
|
114
298
|
let spec;
|
|
@@ -116,21 +300,90 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
|
|
|
116
300
|
spec = JSON.parse(raw);
|
|
117
301
|
}
|
|
118
302
|
catch (error) {
|
|
119
|
-
throw new Error(`cannot derive
|
|
120
|
-
}
|
|
121
|
-
const
|
|
303
|
+
throw new Error(`cannot derive frontend verify command directory: run.json is not valid JSON at ${specPath}: ${error.message}`);
|
|
304
|
+
}
|
|
305
|
+
const lanes = {
|
|
306
|
+
staticLabels: [],
|
|
307
|
+
behaviorLabels: [],
|
|
308
|
+
mockLabels: [],
|
|
309
|
+
lintLabels: [],
|
|
310
|
+
staticCommandTexts: [],
|
|
311
|
+
behaviorCommandTexts: [],
|
|
312
|
+
mockCommandTexts: [],
|
|
313
|
+
lintCommandTexts: [],
|
|
314
|
+
};
|
|
122
315
|
for (const task of spec.tasks ?? []) {
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
316
|
+
const bundle = task.shell?.frontendVerificationBundle;
|
|
317
|
+
if (!bundle)
|
|
318
|
+
continue;
|
|
319
|
+
const lanePairs = [
|
|
320
|
+
["static", bundle.staticEvidence],
|
|
321
|
+
["behavior", bundle.behaviorEvidence],
|
|
322
|
+
["mock", bundle.mockEvidence],
|
|
323
|
+
["lint", bundle.lintEvidence],
|
|
324
|
+
];
|
|
325
|
+
for (const [lane, evidence] of lanePairs) {
|
|
326
|
+
const labels = evidence?.commandLabels ?? [];
|
|
327
|
+
const texts = evidence?.commandTexts ?? [];
|
|
328
|
+
// run.json is untrusted input: labels and texts must zip 1:1.
|
|
329
|
+
// A lane that violates the alignment fails closed instead of
|
|
330
|
+
// binding ids to the wrong commands.
|
|
331
|
+
if (texts.length > 0 && texts.length !== labels.length) {
|
|
332
|
+
throw new Error(`cannot derive frontend verify command directory: ${lane} evidence commandTexts length (${texts.length}) does not match commandLabels length (${labels.length}) in ${specPath}`);
|
|
333
|
+
}
|
|
334
|
+
lanes[`${lane}Labels`].push(...labels);
|
|
335
|
+
lanes[`${lane}CommandTexts`].push(...texts);
|
|
127
336
|
}
|
|
128
337
|
}
|
|
129
|
-
|
|
338
|
+
if (lanes.staticLabels.length === 0 &&
|
|
339
|
+
lanes.behaviorLabels.length === 0 &&
|
|
340
|
+
lanes.mockLabels.length === 0 &&
|
|
341
|
+
lanes.lintLabels.length === 0) {
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
return buildFrontendVerifyCommandDirectory(lanes);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* A behavior verification target must bind a test file: the trace gate
|
|
348
|
+
* proves behavior targets by finding the target id in a real
|
|
349
|
+
* describe/it/test title inside `file`. Enforced at the plan boundary and
|
|
350
|
+
* at materialization (path shape only — the writer may create the file).
|
|
351
|
+
*
|
|
352
|
+
* Only code files count (the gate reads titles from source): fixtures and
|
|
353
|
+
* data files such as `test/fixtures/data.json` are rejected even under a
|
|
354
|
+
* test directory, as are helper/support/setup files (`cypress/fixtures/*`,
|
|
355
|
+
* `tests/support/*`, `setup.*`) that never carry describe/it/test titles.
|
|
356
|
+
* Cypress (`*.cy.*`, `cypress/`) and e2e (`e2e/`) entries are accepted —
|
|
357
|
+
* the repo supports those harnesses.
|
|
358
|
+
*/
|
|
359
|
+
export function isFrontendTestFilePath(filePath) {
|
|
360
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
361
|
+
const base = normalized.split("/").pop() ?? "";
|
|
362
|
+
if (!/\.(c|m)?[jt]sx?$/.test(base))
|
|
363
|
+
return false;
|
|
364
|
+
const segments = normalized.toLowerCase().split("/");
|
|
365
|
+
// `helpers` is deliberately NOT excluded: source helper directories
|
|
366
|
+
// (e.g. `src/helpers/format.test.ts`) legitimately contain unit tests.
|
|
367
|
+
if (segments.some((segment) => segment === "fixtures" || segment === "support")) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
if (/^setup\.[^/]+$/.test(base))
|
|
371
|
+
return false;
|
|
372
|
+
if (/\.cy\.[^/]+$/.test(base))
|
|
373
|
+
return true;
|
|
374
|
+
if (/(^|\/)__tests__\//.test(normalized))
|
|
375
|
+
return true;
|
|
376
|
+
if (/(^|\/)(tests?|e2e|cypress)\//.test(normalized))
|
|
377
|
+
return true;
|
|
378
|
+
if (/\.test\.[^/]+$/.test(normalized))
|
|
379
|
+
return true;
|
|
380
|
+
if (/\.spec\.[^/]+$/.test(normalized))
|
|
381
|
+
return true;
|
|
382
|
+
return false;
|
|
130
383
|
}
|
|
131
384
|
/**
|
|
132
385
|
* Extract the design-policy allowedDependencies set from the DAG run spec
|
|
133
|
-
* (run.json), mirroring `
|
|
386
|
+
* (run.json), mirroring `deriveFrontendVerifyCommandDirectoryFromRun`. Used by the plan
|
|
134
387
|
* node's front-loaded policy pre-check so dependency declarations can be
|
|
135
388
|
* corrected through the retry ladder before the policy shell (the authority)
|
|
136
389
|
* re-runs the same check on committed facts.
|
|
@@ -350,12 +603,30 @@ const uiComponentChoiceSchema = z
|
|
|
350
603
|
decision: z.enum(["specified", "reuse-existing", "new"]),
|
|
351
604
|
specReference: uiComponentSpecReferenceSchema.nullable().optional(),
|
|
352
605
|
// Optional: the decision enum already carries the semantic (specified →
|
|
353
|
-
// specReference, new → task-source traceability, reuse-existing →
|
|
354
|
-
//
|
|
355
|
-
// Omit for reuse-existing to keep plan output small.
|
|
606
|
+
// specReference, new → task-source traceability, reuse-existing →
|
|
607
|
+
// evidencePath). Omit for reuse-existing to keep plan output small.
|
|
356
608
|
rationale: z.string().min(1).optional(),
|
|
609
|
+
// Multi-id coverage (dogfood r-plan-quality): ONE choice may cover many
|
|
610
|
+
// UI states/interactions explicitly instead of the old implicit
|
|
611
|
+
// "reuse-existing covers everything" wildcard.
|
|
612
|
+
covers: z.array(z.string().min(1)).optional(),
|
|
613
|
+
// reuse-existing evidence (dogfood dag-1788504923861-0f7b17a9: 18
|
|
614
|
+
// reuse-existing rows cited conventions in a not-yet-written
|
|
615
|
+
// src/planner.ts): a repo-relative path to the existing file that
|
|
616
|
+
// proves the reuse. Enforced by superRefine + fresh-existence checks.
|
|
617
|
+
evidencePath: safePath.optional(),
|
|
357
618
|
})
|
|
358
|
-
.strict()
|
|
619
|
+
.strict()
|
|
620
|
+
.superRefine((choice, ctx) => {
|
|
621
|
+
if (choice.decision === "reuse-existing" &&
|
|
622
|
+
!(choice.evidencePath && choice.evidencePath.trim().length > 0)) {
|
|
623
|
+
ctx.addIssue({
|
|
624
|
+
code: z.ZodIssueCode.custom,
|
|
625
|
+
path: ["evidencePath"],
|
|
626
|
+
message: "decision=reuse-existing requires evidencePath naming the existing repo file that proves the reuse; use decision=new for greenfield paths",
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
});
|
|
359
630
|
const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
|
|
360
631
|
/**
|
|
361
632
|
* Normalize unstable model emissions before the strict schema sees them:
|
|
@@ -430,12 +701,18 @@ export const frontendImplementationContractSchema = z
|
|
|
430
701
|
files: z.array(safePath).min(1),
|
|
431
702
|
routes: z.array(z.string().startsWith("/")).optional(),
|
|
432
703
|
publicApiChanges: z.array(z.string().min(1)).optional(),
|
|
704
|
+
// Runtime-owned deliverable contract (dogfood r-plan-quality):
|
|
705
|
+
// explicit, source-bound file inventory committed by Contract.
|
|
706
|
+
// The design-policy completeness gate checks ONLY these —
|
|
707
|
+
// allowedPaths is a write boundary, not a coverage obligation.
|
|
708
|
+
requiredDeliverables: z.array(safePath).optional(),
|
|
433
709
|
})
|
|
434
710
|
.strict(),
|
|
435
711
|
requirements: z.array(z
|
|
436
712
|
.object({
|
|
437
713
|
id,
|
|
438
714
|
expectedOutcome: z.string().min(1),
|
|
715
|
+
execution: frontendExecutionSchema.optional(),
|
|
439
716
|
implementationTargets: z.array(safePath),
|
|
440
717
|
verificationTargetIds: z.array(z.string().min(1)),
|
|
441
718
|
evidenceGap: gap.optional(),
|
|
@@ -507,12 +784,18 @@ export const frontendImplementationContractSchema = z
|
|
|
507
784
|
verificationTargets: z.array(z
|
|
508
785
|
.object({
|
|
509
786
|
id: z.string().min(1),
|
|
510
|
-
|
|
787
|
+
// Runtime-owned verification mode, resolved from commandId
|
|
788
|
+
// against the frozen command directory at materialization.
|
|
789
|
+
mode: frontendVerificationModeSchema,
|
|
790
|
+
// Stable machine key into the frozen command directory.
|
|
791
|
+
commandId: z.string().min(1),
|
|
511
792
|
commandLabel: z.string().min(1),
|
|
512
793
|
file: safePath,
|
|
513
794
|
symbol: z.preprocess((value) => (value === "" || value === null ? undefined : value), z.string().min(1).optional()),
|
|
514
795
|
requirementIds: z.array(id),
|
|
515
796
|
uiStates: z.array(z.string().min(1)),
|
|
797
|
+
// Display-only test scope; never read by control flow.
|
|
798
|
+
scope: frontendVerificationScopeSchema.optional(),
|
|
516
799
|
})
|
|
517
800
|
.strict()).min(1),
|
|
518
801
|
evidenceGaps: z.array(gap).optional(),
|
|
@@ -525,6 +808,12 @@ export const frontendImplementationContractSchema = z
|
|
|
525
808
|
})
|
|
526
809
|
.strict()
|
|
527
810
|
.superRefine((value, ctx) => {
|
|
811
|
+
try {
|
|
812
|
+
collectFrontendExecutionGroups(value.requirements);
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
ctx.addIssue({ code: "custom", path: ["requirements"], message: String(error) });
|
|
816
|
+
}
|
|
528
817
|
const stableIdentities = [
|
|
529
818
|
{
|
|
530
819
|
path: "requirements",
|
|
@@ -587,6 +876,27 @@ export const frontendImplementationContractSchema = z
|
|
|
587
876
|
});
|
|
588
877
|
}
|
|
589
878
|
}
|
|
879
|
+
// Behavior evidence must be anchored to be machine-traceable: a
|
|
880
|
+
// behavior target that names no requirement and no UI state produces
|
|
881
|
+
// coverage the trace gate and review cannot verify. Contracts that
|
|
882
|
+
// declare no UI state and no interaction (pure-logic) are exempt from
|
|
883
|
+
// the UI-state half — the schema allows `uiStates: []` there.
|
|
884
|
+
if (target.mode === "behavior" && target.requirementIds.length === 0) {
|
|
885
|
+
ctx.addIssue({
|
|
886
|
+
code: "custom",
|
|
887
|
+
message: `behavior verification target ${target.id} must reference at least one requirement`,
|
|
888
|
+
path: ["verificationTargets"],
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
if (target.mode === "behavior" &&
|
|
892
|
+
target.uiStates.length === 0 &&
|
|
893
|
+
(value.uiStates.length > 0 || value.interactions.length > 0)) {
|
|
894
|
+
ctx.addIssue({
|
|
895
|
+
code: "custom",
|
|
896
|
+
message: `behavior verification target ${target.id} must reference at least one declared UI state`,
|
|
897
|
+
path: ["verificationTargets"],
|
|
898
|
+
});
|
|
899
|
+
}
|
|
590
900
|
}
|
|
591
901
|
for (const requirement of value.requirements) {
|
|
592
902
|
if (!known.has(requirement.id))
|
|
@@ -1357,7 +1667,12 @@ export function compactFrontendImplementationContract(value) {
|
|
|
1357
1667
|
const targets = asRecord(record.targets);
|
|
1358
1668
|
if (targets) {
|
|
1359
1669
|
const compactTargets = { ...targets };
|
|
1360
|
-
for (const field of [
|
|
1670
|
+
for (const field of [
|
|
1671
|
+
"files",
|
|
1672
|
+
"routes",
|
|
1673
|
+
"publicApiChanges",
|
|
1674
|
+
"requiredDeliverables",
|
|
1675
|
+
]) {
|
|
1361
1676
|
if (Object.hasOwn(compactTargets, field)) {
|
|
1362
1677
|
compactTargets[field] = dedupeJsonArray(compactTargets[field]);
|
|
1363
1678
|
}
|
|
@@ -1666,13 +1981,32 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1666
1981
|
for (const [index, item] of rawVerification.entries()) {
|
|
1667
1982
|
const vt = asRecord(item) ?? {};
|
|
1668
1983
|
const id = asString(vt.id) || `VT-${String(index + 1).padStart(3, "0")}`;
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1984
|
+
// Legacy compatibility input may still carry a model-written `type`
|
|
1985
|
+
// and a bare `commandLabel`. Map them onto the v2 shape: the five
|
|
1986
|
+
// legacy types collapse to the runtime mode (only `static` stays
|
|
1987
|
+
// static), the commandId is slugged from the label, and a legacy
|
|
1988
|
+
// unit/component/integration type is preserved as display-only scope.
|
|
1989
|
+
// Production plan facts already carry commandId and are resolved
|
|
1990
|
+
// against the frozen directory during materialization instead.
|
|
1991
|
+
const legacyType = asString(vt.type) || asString(vt.phase);
|
|
1992
|
+
const mode = asString(vt.mode) === "static" ||
|
|
1993
|
+
asString(vt.mode) === "behavior"
|
|
1994
|
+
? asString(vt.mode)
|
|
1995
|
+
: legacyType === "static"
|
|
1673
1996
|
? "static"
|
|
1674
|
-
: "
|
|
1997
|
+
: "behavior";
|
|
1675
1998
|
const commandLabel = asString(vt.commandLabel) || asString(vt.command);
|
|
1999
|
+
const commandId = asString(vt.commandId) ||
|
|
2000
|
+
slugFrontendVerifyCommandId(commandLabel || id, new Set());
|
|
2001
|
+
const scope = asString(vt.scope) === "unit" ||
|
|
2002
|
+
asString(vt.scope) === "component" ||
|
|
2003
|
+
asString(vt.scope) === "integration"
|
|
2004
|
+
? asString(vt.scope)
|
|
2005
|
+
: legacyType === "unit" ||
|
|
2006
|
+
legacyType === "component" ||
|
|
2007
|
+
legacyType === "integration"
|
|
2008
|
+
? legacyType
|
|
2009
|
+
: undefined;
|
|
1676
2010
|
const file = asString(vt.file) ||
|
|
1677
2011
|
(Array.isArray(vt.symbols) ? targetFiles.find((p) => p.includes("__tests__")) : "") ||
|
|
1678
2012
|
targetFiles.find((p) => p.includes("__tests__")) ||
|
|
@@ -1681,10 +2015,12 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1681
2015
|
const uiStateNames = asStringArray(vt.uiStates);
|
|
1682
2016
|
verificationTargets.push({
|
|
1683
2017
|
id,
|
|
1684
|
-
|
|
2018
|
+
mode,
|
|
2019
|
+
commandId,
|
|
1685
2020
|
commandLabel,
|
|
1686
2021
|
file,
|
|
1687
2022
|
symbol: asString(vt.symbol) || undefined,
|
|
2023
|
+
...(scope ? { scope } : {}),
|
|
1688
2024
|
requirementIds: requirementIds.length > 0
|
|
1689
2025
|
? requirementIds
|
|
1690
2026
|
: [...canonicalBinding.requirementIds],
|
|
@@ -1735,6 +2071,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1735
2071
|
? { sourceFragmentIds: req.sourceFragmentIds }
|
|
1736
2072
|
: {}),
|
|
1737
2073
|
...(Array.isArray(req.sourceRefs) ? { sourceRefs: req.sourceRefs } : {}),
|
|
2074
|
+
...(req.execution !== undefined ? { execution: req.execution } : {}),
|
|
1738
2075
|
};
|
|
1739
2076
|
// Do not mark free-form realIntegrationGap as blocking; defer to FE-TEST/FINAL-VERIFY.
|
|
1740
2077
|
if (gapText) {
|
|
@@ -2008,19 +2345,42 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2008
2345
|
catch (error) {
|
|
2009
2346
|
fail("blocked", error instanceof Error ? error.message : String(error), candidateJsonSha256);
|
|
2010
2347
|
}
|
|
2011
|
-
//
|
|
2012
|
-
//
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2348
|
+
// Resolve verificationTarget commandIds against the frozen command
|
|
2349
|
+
// directory (per-lane evidence of the frontend-verify-shell bundle in
|
|
2350
|
+
// run.json). The directory is the only authority for mode and label: a
|
|
2351
|
+
// plan that references an unknown commandId, or binds a behavior command
|
|
2352
|
+
// to a non-test file, is rejected here with plan-attributable codes
|
|
2353
|
+
// instead of failing late at the writer focused-check.
|
|
2354
|
+
const verifyDirectory = await deriveFrontendVerifyCommandDirectoryFromRun(input.runDir);
|
|
2355
|
+
if (verifyDirectory.length > 0) {
|
|
2356
|
+
const byCommandId = new Map(verifyDirectory.map((entry) => [entry.commandId, entry]));
|
|
2357
|
+
const available = verifyDirectory
|
|
2358
|
+
.map((entry) => `${entry.commandId} (${entry.mode}: ${entry.label})`)
|
|
2359
|
+
.join(", ");
|
|
2360
|
+
const parsedRecord = asRecord(parsed);
|
|
2361
|
+
const parsedVt = parsedRecord?.verificationTargets;
|
|
2017
2362
|
if (Array.isArray(parsedVt)) {
|
|
2018
2363
|
for (const vt of parsedVt) {
|
|
2019
|
-
const
|
|
2020
|
-
if (
|
|
2021
|
-
|
|
2364
|
+
const record = asRecord(vt);
|
|
2365
|
+
if (!record)
|
|
2366
|
+
continue;
|
|
2367
|
+
const vtId = asString(record.id) || "(missing id)";
|
|
2368
|
+
const commandId = asString(record.commandId);
|
|
2369
|
+
if (!commandId) {
|
|
2370
|
+
fail("retryable-invalid", `invalid-output: verification target "${vtId}" must reference a frozen command: set commandId to one of [${available}]`, candidateJsonSha256);
|
|
2371
|
+
}
|
|
2372
|
+
const entry = byCommandId.get(commandId);
|
|
2373
|
+
if (!entry) {
|
|
2374
|
+
fail("retryable-invalid", `invalid-output: verification-target-unknown-command: verification target "${vtId}" references unknown commandId "${commandId}"; available frozen commands: [${available}]`, candidateJsonSha256);
|
|
2375
|
+
}
|
|
2376
|
+
record.mode = entry.mode;
|
|
2377
|
+
record.commandLabel = entry.label;
|
|
2378
|
+
const file = asString(record.file);
|
|
2379
|
+
if (entry.mode === "behavior" && file && !isFrontendTestFilePath(file)) {
|
|
2380
|
+
fail("retryable-invalid", `invalid-output: verification-target-phase-mismatch: verification target "${vtId}" binds behavior command "${entry.label}" (${entry.commandId}) to non-test file "${file}"; behavior targets must bind a test file (__tests__/, tests?/, e2e/, cypress/, *.test.*, *.spec.*, *.cy.*) so the trace gate can match the target id in a real describe/it/test title`, candidateJsonSha256);
|
|
2022
2381
|
}
|
|
2023
2382
|
}
|
|
2383
|
+
pushAction("resolve-verification-command-ids");
|
|
2024
2384
|
}
|
|
2025
2385
|
}
|
|
2026
2386
|
const parsedTargets = asRecord(parsed)?.targets;
|
|
@@ -2104,6 +2464,16 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2104
2464
|
files: referencedConcretePaths,
|
|
2105
2465
|
};
|
|
2106
2466
|
}
|
|
2467
|
+
// Both Plan precheck and policy materialization consume this exact inventory.
|
|
2468
|
+
// Never trust a Plan patch or infer delivery obligations from prose.
|
|
2469
|
+
const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.contractNodeId ?? "frontend-contract-pi", "contract-typed-facts.jsonl"));
|
|
2470
|
+
const requiredDeliverables = extractRequiredDeliverablePaths(contractRecords
|
|
2471
|
+
.filter((record) => record.phase === "committed")
|
|
2472
|
+
.map((record) => record.fact));
|
|
2473
|
+
delete result.data.targets.requiredDeliverables;
|
|
2474
|
+
if (requiredDeliverables.length > 0) {
|
|
2475
|
+
result.data.targets.requiredDeliverables = requiredDeliverables;
|
|
2476
|
+
}
|
|
2107
2477
|
const blockingGaps = [
|
|
2108
2478
|
...(result.data.evidenceGaps ?? []),
|
|
2109
2479
|
...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
|
|
@@ -2139,6 +2509,10 @@ function splitFrontendContractReviewGroup(canonical, fields) {
|
|
|
2139
2509
|
if (!Object.hasOwn(canonical, field))
|
|
2140
2510
|
continue;
|
|
2141
2511
|
const fieldValue = canonical[field];
|
|
2512
|
+
// JSON omits undefined object properties; the index must describe the
|
|
2513
|
+
// persisted contract, not phantom keys retained by normalization.
|
|
2514
|
+
if (fieldValue === undefined)
|
|
2515
|
+
continue;
|
|
2142
2516
|
if (!Array.isArray(fieldValue)) {
|
|
2143
2517
|
const candidate = { ...data, [field]: fieldValue };
|
|
2144
2518
|
if (Object.keys(data).length > 0 &&
|
|
@@ -2311,6 +2685,7 @@ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
|
2311
2685
|
"/sourceBinding",
|
|
2312
2686
|
"/riskLevel",
|
|
2313
2687
|
"/targets/files",
|
|
2688
|
+
"/targets/requiredDeliverables",
|
|
2314
2689
|
"/mockApi/productionDefaultOff",
|
|
2315
2690
|
];
|
|
2316
2691
|
const PLAN_LEDGER_FACT_KINDS_FOR_COMPILE = [
|
|
@@ -2359,6 +2734,8 @@ function frontendPlanPatchProtectedPathViolations(patch) {
|
|
|
2359
2734
|
const targets = asRecord(record.targets);
|
|
2360
2735
|
if (targets && Object.hasOwn(targets, "files"))
|
|
2361
2736
|
violations.push("/targets/files");
|
|
2737
|
+
if (targets && Object.hasOwn(targets, "requiredDeliverables"))
|
|
2738
|
+
violations.push("/targets/requiredDeliverables");
|
|
2362
2739
|
const mockApi = asRecord(record.mockApi);
|
|
2363
2740
|
if (mockApi && Object.hasOwn(mockApi, "productionDefaultOff"))
|
|
2364
2741
|
violations.push("/mockApi/productionDefaultOff");
|
|
@@ -2395,7 +2772,7 @@ async function dropUnresolvedVerificationSymbols(input) {
|
|
|
2395
2772
|
// it. This is the root fix for the recurring fabricated-symbol error
|
|
2396
2773
|
// class: the field was allowed for static, the prompt did not forbid
|
|
2397
2774
|
// it, and models habitually fill it; the compiler removes it instead.
|
|
2398
|
-
if (target.
|
|
2775
|
+
if (target.mode === "static") {
|
|
2399
2776
|
if (symbol) {
|
|
2400
2777
|
delete target.symbol;
|
|
2401
2778
|
if (!actions.includes("strip-static-verification-symbols"))
|
|
@@ -2543,6 +2920,8 @@ export async function analyzeFrontendPlanPatchCandidate(input) {
|
|
|
2543
2920
|
const policyPreFindings = [
|
|
2544
2921
|
...checkUiDesignCoverage(analysis.canonical),
|
|
2545
2922
|
...checkUiStateAttribution(analysis.canonical),
|
|
2923
|
+
...checkRequiredDeliverables(analysis.canonical),
|
|
2924
|
+
...checkUxNameCollisions(analysis.canonical),
|
|
2546
2925
|
...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
|
|
2547
2926
|
...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
|
|
2548
2927
|
];
|
|
@@ -2761,9 +3140,11 @@ export async function buildContractFromCommittedFacts(input) {
|
|
|
2761
3140
|
}
|
|
2762
3141
|
// b. Committed contract terminal fact (contract-finalized).
|
|
2763
3142
|
const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, contractNodeId, "contract-typed-facts.jsonl"));
|
|
2764
|
-
const
|
|
3143
|
+
const committedContractFacts = contractRecords
|
|
2765
3144
|
.filter((record) => record.phase === "committed")
|
|
2766
|
-
.map((record) => record.fact
|
|
3145
|
+
.map((record) => record.fact);
|
|
3146
|
+
const contractKinds = new Set(committedContractFacts
|
|
3147
|
+
.map((fact) => fact?.kind)
|
|
2767
3148
|
.filter((kind) => typeof kind === "string"));
|
|
2768
3149
|
if (!contractKinds.has("contract-finalized")) {
|
|
2769
3150
|
return {
|
|
@@ -2818,6 +3199,7 @@ export async function buildContractFromCommittedFacts(input) {
|
|
|
2818
3199
|
runDir: input.runDir,
|
|
2819
3200
|
rawContractText: serializeDeterministicJson(merged),
|
|
2820
3201
|
sourceBinding: input.sourceBinding,
|
|
3202
|
+
contractNodeId,
|
|
2821
3203
|
});
|
|
2822
3204
|
return {
|
|
2823
3205
|
ok: true,
|