@tea-agent/loop-agent 0.39.0-beta.1 → 0.39.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/CHANGELOG.md +349 -97
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/advance.js +12 -0
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +23 -5
- package/dist/cli/program.js +21 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/cli.js +2 -1
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +455 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +2370 -64
- package/dist/executors/pi-executor.js +11 -2
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +74 -28
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/pi-sdk-executor.js +225 -33
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +1258 -224
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/shared/dag-failure-category.js +150 -0
- package/dist/shared/dag-prompt-override.js +27 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +39 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +70 -15
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +50 -5
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +2 -0
- package/dist/task/contract/hash.js +3 -0
- package/dist/task/contract/observe.js +25 -4
- package/dist/task/contract/paths.js +2 -1
- package/dist/task/contract/project.js +18 -1
- package/dist/task/contract/schema.js +4 -1
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/frontend-project-capability.js +199 -18
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +477 -0
- package/dist/task/source-prepare/index.js +3 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
- package/dist/task/source-prepare/ledger-review.js +214 -0
- package/dist/task/source-prepare/ledger.js +545 -0
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +298 -1
- package/dist/task/source-prepare/semantic-intake.js +25 -5
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/task/source-references.js +22 -1
- package/dist/worker/cli.js +18 -2
- package/dist/worker/console/chat/chat-event-store.js +75 -5
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/operation-card.js +8 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/pi-runtime.js +331 -18
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +395 -59
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +93 -63
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +79 -0
- package/dist/worker/console/console-handoff.js +82 -0
- package/dist/worker/console/console-update-and-init.js +115 -0
- package/dist/worker/console/console-update-runtime.js +84 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/inspect-split.js +18 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-run-facts.js +19 -3
- package/dist/worker/console/operation-store.js +227 -7
- package/dist/worker/console/operator-actions.js +388 -0
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/routes.js +355 -3
- package/dist/worker/console/security.js +67 -13
- package/dist/worker/console/server.js +609 -164
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-CXj_GnSb.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BZp6JAp7.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-DfpcEuYU.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-_Gf0xadb.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-Ct2QPCmv.js} +1 -1
- package/dist/worker/console/static/assets/channel-3TxJgYaH.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-BfklkzKl.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-Dy25vZJV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-BFlCRZep.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-op3oVxIE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-C0R7rzF2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-Bt44TCGy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-_uEE_XFx.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-C3TOYGZ9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-Cz60oBPV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-8Bik0qis.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BHIkXpp3.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BHIkXpp3.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-C2QIOA4a.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-CSH_yUUd.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-BDLw2XFM.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-CcD9ZtF4.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-DqwtkBsI.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BWVEcqsC.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-M-WAbtQi.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-DKW7SixP.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-PHHPPtrj.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-BQD-B4RX.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-BFvULb51.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-Bw-aXTCb.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BGKw04Qx.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-RqKaPR-I.js} +1 -1
- package/dist/worker/console/static/assets/index-B_D8rbWc.js +407 -0
- package/dist/worker/console/static/assets/index-BdNx6fj0.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-YO5dnzrJ.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-Bi1VZHMq.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-Bszls1DD.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-PhzeaZ09.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-BsjbDoXi.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-0B7NnWKk.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BJr4Fj-q.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-moC4fpGB.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-BEw37-2c.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-Cq6LyasU.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-DUCMcK0D.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-C3upTZm7.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-BI_gMsCW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-YFOIRzfN.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-dOnB7UDD.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-BXUniaIh.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cdi6UhLa.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-2tA4wTNu.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D-RJBbb0.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-DO0HkJXC.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DvIOzixv.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-DXDZ0cTj.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-B32Ark0D.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/app/console-types.js +68 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +45 -10
- package/dist/worker/console/static-src/app/useRunProgress.js +47 -0
- package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +16 -3
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +183 -48
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
- package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
- package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
- package/dist/worker/console/workspace-context.js +344 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/materialize/harness-task-lineage.js +5 -2
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observability/read-model.js +22 -0
- package/dist/worker/observe/node-input.js +160 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +224 -3
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +90 -4
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
- package/dist/worker/observe/static/dag-node-purpose.js +208 -0
- package/dist/worker/observe/static/format.js +60 -0
- package/dist/worker/observe/static/index.html +6 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -96
- package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +184 -1
- package/dist/worker/observe/static/styles.css +395 -0
- package/dist/worker/observe/static/views/dag-graph.js +4 -2
- package/dist/worker/observe/static/views/dag-inspector.js +1067 -9
- package/dist/worker/observe/static/views/dag.d.ts +13 -0
- package/dist/worker/observe/static/views/dag.js +14 -5
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/worker/observe/static/views/task.js +56 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +49 -3
- package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
- package/dist/workflows/dag/backend-test-writer-completeness.js +17 -17
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/map.js +2 -1
- package/dist/workflows/dag/failure-category.js +7 -116
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +538 -166
- package/dist/workflows/dag/frontend-plan-render.js +24 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +388 -392
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +7 -424
- package/dist/workflows/dag/frontend-review-context.js +288 -14
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +814 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +427 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
- package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
- package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
- package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
- package/dist/workflows/dag/frontend-test-html-report.js +3 -1
- package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
- package/dist/workflows/dag/frontend-test-layout.js +159 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +272 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +1072 -660
- package/dist/workflows/dag/interrupt-request.js +10 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +552 -58
- package/dist/workflows/dag/prompt.js +44 -19
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +124 -1
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +81 -6
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +318 -8
- package/dist/workflows/dag/runner.js +429 -122
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +338 -31
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +92 -32
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +9 -0
- package/docs/operations/README.md +1 -1
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/README.md +2 -1
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +63 -1
- package/docs/templates/backend-test-dag.json +265 -237
- package/docs/templates/frontend-implementation-contract.schema.json +684 -24
- package/docs/templates/frontend-test-case-checklist.md +2 -2
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +1 -1
- package/package.json +7 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-bounded-implement/SKILL.md +8 -14
- package/skills/frontend-design-review/SKILL.md +20 -41
- package/skills/frontend-implementation/SKILL.md +3 -4
- package/skills/frontend-implementation/references/design-spec.md +15 -10
- package/skills/frontend-implementation/references/node-contracts.md +23 -13
- package/skills/frontend-review/SKILL.md +22 -14
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/loop-agent/references/command-reference.md +12 -4
- package/skills/loop-agent/references/hybrid-dag.md +8 -1
- package/skills/playwright-cli/SKILL.md +23 -1
- package/skills/playwright-cli-case-generator/SKILL.md +29 -8
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/index-DTZOKgAn.js +0 -404
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
|
@@ -8,6 +8,15 @@ import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-sto
|
|
|
8
8
|
import { findPackageRoot } from "../../shared/package-metadata.js";
|
|
9
9
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
10
10
|
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
11
|
+
import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
|
|
12
|
+
// Cycle-safe value imports: the check functions are hoisted declarations used
|
|
13
|
+
// only at call time, so the contract module and the policy module can depend
|
|
14
|
+
// on each other without module-init ordering issues.
|
|
15
|
+
import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
|
|
16
|
+
import { isDagSourceBindingV2 } from "./types.js";
|
|
17
|
+
import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
|
|
18
|
+
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
19
|
+
import { isConfigurationVerificationFile, verificationSymbolMatchesContent, } from "./frontend-verification-trace.js";
|
|
11
20
|
export const frontendNormalizationActionSchema = z.enum([
|
|
12
21
|
"remove-trailing-commas",
|
|
13
22
|
"strip-comments",
|
|
@@ -102,6 +111,68 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
|
|
|
102
111
|
}
|
|
103
112
|
return [...labels];
|
|
104
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Extract the design-policy allowedDependencies set from the DAG run spec
|
|
116
|
+
* (run.json), mirroring `deriveFrozenCommandLabelsFromRun`. Used by the plan
|
|
117
|
+
* node's front-loaded policy pre-check so dependency declarations can be
|
|
118
|
+
* corrected through the retry ladder before the policy shell (the authority)
|
|
119
|
+
* re-runs the same check on committed facts.
|
|
120
|
+
*/
|
|
121
|
+
async function deriveAllowedDependenciesFromRun(runDir) {
|
|
122
|
+
const specPath = path.join(runDir, "run.json");
|
|
123
|
+
let raw;
|
|
124
|
+
try {
|
|
125
|
+
raw = await readFile(specPath, "utf8");
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
let spec;
|
|
131
|
+
try {
|
|
132
|
+
spec = JSON.parse(raw);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
const deps = new Set();
|
|
138
|
+
for (const task of spec.tasks ?? []) {
|
|
139
|
+
const allowed = task.shell?.frontendDesignPolicy?.allowedDependencies;
|
|
140
|
+
if (allowed) {
|
|
141
|
+
for (const dep of allowed)
|
|
142
|
+
deps.add(dep);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return [...deps];
|
|
146
|
+
}
|
|
147
|
+
/** Read the frozen implementation writeSet patterns from run.json so the plan
|
|
148
|
+
* validator can front-load checkTargetPaths (write-set-too-large) before the
|
|
149
|
+
* design-policy shell terminates the run. */
|
|
150
|
+
async function deriveWriteSetFromRun(runDir) {
|
|
151
|
+
const specPath = path.join(runDir, "run.json");
|
|
152
|
+
let raw;
|
|
153
|
+
try {
|
|
154
|
+
raw = await readFile(specPath, "utf8");
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
let spec;
|
|
160
|
+
try {
|
|
161
|
+
spec = JSON.parse(raw);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const writeSet = new Set();
|
|
167
|
+
for (const task of spec.tasks ?? []) {
|
|
168
|
+
const patterns = task.shell?.frontendDesignPolicy?.implementationWriteSet;
|
|
169
|
+
if (patterns) {
|
|
170
|
+
for (const pattern of patterns)
|
|
171
|
+
writeSet.add(pattern);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return [...writeSet];
|
|
175
|
+
}
|
|
105
176
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
|
|
106
177
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
|
|
107
178
|
/**
|
|
@@ -169,7 +240,6 @@ export function loadFrontendImplementationContractJsonSchema(startDir = path.dir
|
|
|
169
240
|
"mockApi",
|
|
170
241
|
"designEvidence",
|
|
171
242
|
"verificationTargets",
|
|
172
|
-
"evidenceGaps",
|
|
173
243
|
];
|
|
174
244
|
const actualRequired = Array.isArray(schema.required) ? schema.required : [];
|
|
175
245
|
const missing = expectedRequired.filter((key) => !actualRequired.includes(key));
|
|
@@ -218,13 +288,13 @@ const gap = z
|
|
|
218
288
|
* `specReference` is required (path/section/line hit).
|
|
219
289
|
* - `reuse-existing`: reuse an existing repo component/convention not named by
|
|
220
290
|
* the spec → `specReference` may be null, rationale explains the basis.
|
|
221
|
-
* - `new`:
|
|
222
|
-
*
|
|
291
|
+
* - `new`: the task source mandates a component absent from the repository →
|
|
292
|
+
* `specReference` is required and points at that task-source declaration;
|
|
293
|
+
* rationale explains the bounded addition.
|
|
223
294
|
*
|
|
224
|
-
* `specReference` is nullable + optional
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* identically.
|
|
295
|
+
* `specReference` is nullable + optional at the wire boundary because
|
|
296
|
+
* `stripNullValuesDeep` removes model-emitted nulls before validation. The
|
|
297
|
+
* decision-specific requirements are enforced by the superRefine below.
|
|
228
298
|
*/
|
|
229
299
|
const uiComponentSpecReferenceSchema = z
|
|
230
300
|
.object({
|
|
@@ -239,7 +309,11 @@ const uiComponentChoiceSchema = z
|
|
|
239
309
|
component: z.string().min(1),
|
|
240
310
|
decision: z.enum(["specified", "reuse-existing", "new"]),
|
|
241
311
|
specReference: uiComponentSpecReferenceSchema.nullable().optional(),
|
|
242
|
-
|
|
312
|
+
// Optional: the decision enum already carries the semantic (specified →
|
|
313
|
+
// specReference, new → task-source traceability, reuse-existing → no
|
|
314
|
+
// rationale needed).
|
|
315
|
+
// Omit for reuse-existing to keep plan output small.
|
|
316
|
+
rationale: z.string().min(1).optional(),
|
|
243
317
|
})
|
|
244
318
|
.strict();
|
|
245
319
|
const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
|
|
@@ -268,7 +342,12 @@ function stripNullValuesDeep(value) {
|
|
|
268
342
|
return value;
|
|
269
343
|
}
|
|
270
344
|
export const frontendImplementationContractSchema = z
|
|
271
|
-
.preprocess(
|
|
345
|
+
.preprocess((value) => {
|
|
346
|
+
const record = asRecord(value);
|
|
347
|
+
if (!record)
|
|
348
|
+
return stripNullValuesDeep(value);
|
|
349
|
+
return stripNullValuesDeep(record);
|
|
350
|
+
}, z
|
|
272
351
|
.object({
|
|
273
352
|
schemaVersion: z.literal(1),
|
|
274
353
|
sourceBinding: z
|
|
@@ -278,8 +357,33 @@ export const frontendImplementationContractSchema = z
|
|
|
278
357
|
requirementSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
279
358
|
referencePaths: z.array(safePath),
|
|
280
359
|
requirementIds: z.array(id),
|
|
360
|
+
// Source binding v2 (frontend): bound ledger pointer + fragment map.
|
|
361
|
+
ledgerPath: safePath.optional(),
|
|
362
|
+
ledgerSha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
363
|
+
inputDigest: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
364
|
+
requirementToFragments: z
|
|
365
|
+
.record(z.string(), z.array(z.string()))
|
|
366
|
+
.optional(),
|
|
281
367
|
})
|
|
282
|
-
.strict()
|
|
368
|
+
.strict()
|
|
369
|
+
.superRefine((value, ctx) => {
|
|
370
|
+
// v2 all-or-none:任一 ledger 字段出现则四项必须齐全,防止 v1+optional 冒充 v2。
|
|
371
|
+
const anyLedgerField = value.ledgerPath !== undefined ||
|
|
372
|
+
value.ledgerSha256 !== undefined ||
|
|
373
|
+
value.inputDigest !== undefined ||
|
|
374
|
+
value.requirementToFragments !== undefined;
|
|
375
|
+
const allLedgerFields = value.ledgerPath !== undefined &&
|
|
376
|
+
value.ledgerSha256 !== undefined &&
|
|
377
|
+
value.inputDigest !== undefined &&
|
|
378
|
+
value.requirementToFragments !== undefined;
|
|
379
|
+
if (anyLedgerField && !allLedgerFields) {
|
|
380
|
+
ctx.addIssue({
|
|
381
|
+
code: z.ZodIssueCode.custom,
|
|
382
|
+
path: ["sourceBinding"],
|
|
383
|
+
message: "source binding v2 ledger fields (ledgerPath/ledgerSha256/inputDigest/requirementToFragments) must be provided together; partial or optional v1+ledger bindings are rejected",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}),
|
|
283
387
|
riskLevel: z.enum(["small", "standard", "high-risk"]),
|
|
284
388
|
targets: z
|
|
285
389
|
.object({
|
|
@@ -295,6 +399,10 @@ export const frontendImplementationContractSchema = z
|
|
|
295
399
|
implementationTargets: z.array(safePath),
|
|
296
400
|
verificationTargetIds: z.array(z.string().min(1)),
|
|
297
401
|
evidenceGap: gap.optional(),
|
|
402
|
+
// Source fidelity ledger binding (AC-005/AC-006): 每个 requirement
|
|
403
|
+
// 携带来源 fragment 证据,prewrite 门禁校验其真实性与覆盖。
|
|
404
|
+
sourceFragmentIds: z.array(z.string().min(1)).optional(),
|
|
405
|
+
sourceRefs: z.array(safePath).optional(),
|
|
298
406
|
})
|
|
299
407
|
.strict()).min(1),
|
|
300
408
|
uiStates: z.array(z
|
|
@@ -367,7 +475,7 @@ export const frontendImplementationContractSchema = z
|
|
|
367
475
|
uiStates: z.array(z.string().min(1)),
|
|
368
476
|
})
|
|
369
477
|
.strict()).min(1),
|
|
370
|
-
evidenceGaps: z.array(gap),
|
|
478
|
+
evidenceGaps: z.array(gap).optional(),
|
|
371
479
|
implementationSteps: z.array(z.string().min(1)).optional(),
|
|
372
480
|
stylingStrategy: z.string().min(1).optional(),
|
|
373
481
|
uiComponentChoices: uiComponentChoicesSchema.optional(),
|
|
@@ -431,6 +539,32 @@ export const frontendImplementationContractSchema = z
|
|
|
431
539
|
message: `unknown verification target ${targetId}`,
|
|
432
540
|
path: ["requirements"],
|
|
433
541
|
});
|
|
542
|
+
// Source fidelity ledger binding (AC-005/AC-006): 绑定携带
|
|
543
|
+
// requirementToFragments(ledger v2)时,每个 requirement 必须携带
|
|
544
|
+
// 非空 sourceFragmentIds,否则 fail closed(防引用伪造/缺失)。
|
|
545
|
+
// sourceRefs(fragment→path 的派生展示)不设硬门槛:模型从 binding
|
|
546
|
+
// 只能拿到 fragment id 拿不到权威 path,强制只会诱导编造;防伪造的
|
|
547
|
+
// 真实性校验由 fidelity gate 按 sourceFragmentIds 执行(存在性 +
|
|
548
|
+
// 与 ledger boundFragments 一致)。
|
|
549
|
+
const boundFragments = value.sourceBinding.requirementToFragments?.[requirement.id];
|
|
550
|
+
if (value.sourceBinding.ledgerPath &&
|
|
551
|
+
(!requirement.sourceFragmentIds ||
|
|
552
|
+
requirement.sourceFragmentIds.length === 0)) {
|
|
553
|
+
ctx.addIssue({
|
|
554
|
+
code: "custom",
|
|
555
|
+
message: `requirement ${requirement.id} must carry sourceFragmentIds when a source fidelity ledger is bound`,
|
|
556
|
+
path: ["requirements", requirement.id, "sourceFragmentIds"],
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
else if (boundFragments !== undefined &&
|
|
560
|
+
(!requirement.sourceFragmentIds ||
|
|
561
|
+
requirement.sourceFragmentIds.length === 0)) {
|
|
562
|
+
ctx.addIssue({
|
|
563
|
+
code: "custom",
|
|
564
|
+
message: `requirement ${requirement.id} is bound to ledger fragments ${boundFragments.join(", ")} but declares none`,
|
|
565
|
+
path: ["requirements", requirement.id, "sourceFragmentIds"],
|
|
566
|
+
});
|
|
567
|
+
}
|
|
434
568
|
}
|
|
435
569
|
for (const state of value.uiStates) {
|
|
436
570
|
if (state.applicable &&
|
|
@@ -460,10 +594,10 @@ export const frontendImplementationContractSchema = z
|
|
|
460
594
|
}
|
|
461
595
|
}
|
|
462
596
|
else if (choice.decision === "new") {
|
|
463
|
-
if (choice.specReference) {
|
|
597
|
+
if (!choice.specReference || !choice.specReference.path) {
|
|
464
598
|
ctx.addIssue({
|
|
465
599
|
code: "custom",
|
|
466
|
-
message: "new component choice
|
|
600
|
+
message: "new component choice requires a task-source specReference.path",
|
|
467
601
|
path: ["uiComponentChoices", index, "specReference"],
|
|
468
602
|
});
|
|
469
603
|
}
|
|
@@ -567,6 +701,29 @@ export async function assertFrontendSourceBindingFresh(input) {
|
|
|
567
701
|
throw new Error(`frontend source binding is stale: ${source.path}`);
|
|
568
702
|
}
|
|
569
703
|
}
|
|
704
|
+
// Source binding v2 ledger 新鲜度(AC-FIDELITY-REPAIR-001/002):v2 绑定携带的
|
|
705
|
+
// ledger 文件必须存在且 sha256 与绑定一致,否则 fail closed;v1 绑定无此路径。
|
|
706
|
+
if (isDagSourceBindingV2(input.binding)) {
|
|
707
|
+
const absolute = resolveDagTaskSourcePath({
|
|
708
|
+
workspaceRoot: input.workspaceRoot,
|
|
709
|
+
taskId: input.binding.taskId,
|
|
710
|
+
sourcePath: input.binding.ledgerPath,
|
|
711
|
+
});
|
|
712
|
+
let content;
|
|
713
|
+
try {
|
|
714
|
+
content = await readFile(absolute);
|
|
715
|
+
const info = await stat(absolute);
|
|
716
|
+
if (!info.isFile())
|
|
717
|
+
throw new Error("not a regular file");
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
throw new Error(`frontend source fidelity ledger unavailable: ${input.binding.ledgerPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
721
|
+
}
|
|
722
|
+
const actual = createHash("sha256").update(content).digest("hex");
|
|
723
|
+
if (actual !== input.binding.ledgerSha256) {
|
|
724
|
+
throw new Error(`frontend source fidelity ledger is stale or tampered: ${input.binding.ledgerPath} (bound ${input.binding.ledgerSha256}, observed ${actual})`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
570
727
|
}
|
|
571
728
|
const SECRET_KEY = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|credential|authorization)/i;
|
|
572
729
|
const SECRET_VALUE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b)/;
|
|
@@ -741,7 +898,7 @@ export function canonicalFrontendContractSourceBinding(binding) {
|
|
|
741
898
|
const requirement = binding.sources.find((source) => source.kind === "requirement");
|
|
742
899
|
if (!requirement)
|
|
743
900
|
throw new Error("frontend implementation contract gate requires a requirement source in DAG sourceBinding");
|
|
744
|
-
|
|
901
|
+
const sourceBinding = {
|
|
745
902
|
taskId: binding.taskId,
|
|
746
903
|
requirementPath: requirement.path,
|
|
747
904
|
requirementSha256: requirement.sha256,
|
|
@@ -751,6 +908,15 @@ export function canonicalFrontendContractSourceBinding(binding) {
|
|
|
751
908
|
.sort(),
|
|
752
909
|
requirementIds: binding.requirementIds.map(canonicalizeRequirementId),
|
|
753
910
|
};
|
|
911
|
+
// Source binding v2(schemaVersion=2 强制字段):透传 ledger 指针与
|
|
912
|
+
// requirement→fragment 映射。v1 绑定不得以 optional 字段冒充 v2。
|
|
913
|
+
if (isDagSourceBindingV2(binding)) {
|
|
914
|
+
sourceBinding.ledgerPath = binding.ledgerPath;
|
|
915
|
+
sourceBinding.ledgerSha256 = binding.ledgerSha256;
|
|
916
|
+
sourceBinding.inputDigest = binding.inputDigest;
|
|
917
|
+
sourceBinding.requirementToFragments = binding.requirementToFragments;
|
|
918
|
+
}
|
|
919
|
+
return sourceBinding;
|
|
754
920
|
}
|
|
755
921
|
function asRecord(value) {
|
|
756
922
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -1079,6 +1245,40 @@ function looksLikeStrictFrontendContract(value) {
|
|
|
1079
1245
|
asRecord(record.mockApi) !== null &&
|
|
1080
1246
|
asRecord(record.designEvidence) !== null);
|
|
1081
1247
|
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Single shared completion layer for required-with-fallback contract fields.
|
|
1250
|
+
*
|
|
1251
|
+
* Structural invariant: the coerce normalizer has strict-shape and free-form
|
|
1252
|
+
* branches that reshape differently, but every required key that carries a
|
|
1253
|
+
* deterministic fallback (currently `designEvidence`) must be completed in
|
|
1254
|
+
* exactly ONE place — here — so the branches can never drift again (the
|
|
1255
|
+
* designEvidence Required and invented-uiStates failures both came from
|
|
1256
|
+
* branch-local rules diverging). This layer never invents values that later
|
|
1257
|
+
* refinements reject: it only fills documented fallbacks for keys already
|
|
1258
|
+
* present-but-partial, and is idempotent for already-complete inputs.
|
|
1259
|
+
*/
|
|
1260
|
+
export function completeCanonicalRequiredFields(value) {
|
|
1261
|
+
const record = asRecord(value);
|
|
1262
|
+
if (!record)
|
|
1263
|
+
return value;
|
|
1264
|
+
const designEvidence = asRecord(record.designEvidence);
|
|
1265
|
+
if (!designEvidence)
|
|
1266
|
+
return value;
|
|
1267
|
+
const source = asString(designEvidence.source);
|
|
1268
|
+
const paths = Array.isArray(designEvidence.paths);
|
|
1269
|
+
const conflicts = Array.isArray(designEvidence.conflicts);
|
|
1270
|
+
if (source && paths && conflicts)
|
|
1271
|
+
return value;
|
|
1272
|
+
return {
|
|
1273
|
+
...record,
|
|
1274
|
+
designEvidence: {
|
|
1275
|
+
...designEvidence,
|
|
1276
|
+
...(source ? {} : { source: "repository-fallback+task-source" }),
|
|
1277
|
+
...(paths ? {} : { paths: [] }),
|
|
1278
|
+
...(conflicts ? {} : { conflicts: [] }),
|
|
1279
|
+
},
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1082
1282
|
/**
|
|
1083
1283
|
* Coerce common free-form plan JSON into frontend-implementation-contract-v1.
|
|
1084
1284
|
* Near-schema payloads are left untouched so unknown-key fail-closed still holds.
|
|
@@ -1183,6 +1383,9 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1183
1383
|
...strictRecord,
|
|
1184
1384
|
requirements,
|
|
1185
1385
|
...(strictMockApi ? { mockApi: { ...strictMockApi, endpoints: mockEndpoints } } : {}),
|
|
1386
|
+
// Required-with-fallback keys (designEvidence etc.) are completed
|
|
1387
|
+
// by the single shared `completeCanonicalRequiredFields` layer in
|
|
1388
|
+
// the analyze pipeline — never per-branch.
|
|
1186
1389
|
};
|
|
1187
1390
|
}
|
|
1188
1391
|
}
|
|
@@ -1231,9 +1434,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1231
1434
|
: typeRaw.includes("type")
|
|
1232
1435
|
? "static"
|
|
1233
1436
|
: "unit";
|
|
1234
|
-
const commandLabel = asString(vt.commandLabel) ||
|
|
1235
|
-
asString(vt.command) ||
|
|
1236
|
-
(type === "static" ? "npm run typecheck" : "npm run test:unit:fe");
|
|
1437
|
+
const commandLabel = asString(vt.commandLabel) || asString(vt.command);
|
|
1237
1438
|
const file = asString(vt.file) ||
|
|
1238
1439
|
(Array.isArray(vt.symbols) ? targetFiles.find((p) => p.includes("__tests__")) : "") ||
|
|
1239
1440
|
targetFiles.find((p) => p.includes("__tests__")) ||
|
|
@@ -1249,7 +1450,11 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1249
1450
|
requirementIds: requirementIds.length > 0
|
|
1250
1451
|
? requirementIds
|
|
1251
1452
|
: [...canonicalBinding.requirementIds],
|
|
1252
|
-
|
|
1453
|
+
// Never invent UI state references: "success" is not a canonical
|
|
1454
|
+
// state name, so a defaulted ["success","error"] was always rejected
|
|
1455
|
+
// by the schema's unknown-uiState refinement — a verification
|
|
1456
|
+
// target that declares no UI binding stays unbound ([]).
|
|
1457
|
+
uiStates: uiStateNames,
|
|
1253
1458
|
});
|
|
1254
1459
|
}
|
|
1255
1460
|
if (verificationTargets.length === 0) {
|
|
@@ -1284,6 +1489,14 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1284
1489
|
verificationTargetIds: verificationTargetIds.length > 0
|
|
1285
1490
|
? verificationTargetIds
|
|
1286
1491
|
: defaultVerificationIds,
|
|
1492
|
+
// Source fidelity provenance 透传:兼容分支重建 requirement 时必须
|
|
1493
|
+
// 保留已声明的 sourceFragmentIds/sourceRefs(strict 分支通过
|
|
1494
|
+
// `...requirement` 保留,两边必须一致),否则 ledger-bound 契约在
|
|
1495
|
+
// designEvidence 缺失等情况下走兼容分支时丢失防伪造证据。
|
|
1496
|
+
...(Array.isArray(req.sourceFragmentIds)
|
|
1497
|
+
? { sourceFragmentIds: req.sourceFragmentIds }
|
|
1498
|
+
: {}),
|
|
1499
|
+
...(Array.isArray(req.sourceRefs) ? { sourceRefs: req.sourceRefs } : {}),
|
|
1287
1500
|
};
|
|
1288
1501
|
// Do not mark free-form realIntegrationGap as blocking; defer to FE-TEST/FINAL-VERIFY.
|
|
1289
1502
|
if (gapText) {
|
|
@@ -1485,7 +1698,13 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1485
1698
|
rawOutput = input.rawContractText;
|
|
1486
1699
|
}
|
|
1487
1700
|
else if (input.fromNodeId !== undefined) {
|
|
1488
|
-
|
|
1701
|
+
let record;
|
|
1702
|
+
try {
|
|
1703
|
+
record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
|
|
1704
|
+
}
|
|
1705
|
+
catch (error) {
|
|
1706
|
+
throw new Error(`cannot parse node record ${input.fromNodeId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1707
|
+
}
|
|
1489
1708
|
rawOutput = record.assistantText ?? record.stdout ?? "";
|
|
1490
1709
|
}
|
|
1491
1710
|
else {
|
|
@@ -1593,15 +1812,16 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1593
1812
|
// There is exactly one post-security candidate. A fallback candidate would
|
|
1594
1813
|
// allow malformed raw fields to bypass the boundary checks above.
|
|
1595
1814
|
const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
|
|
1815
|
+
const completedContract = completeCanonicalRequiredFields(normalizedContract);
|
|
1596
1816
|
const candidate = deriveFrontendVerificationCoverage({
|
|
1597
|
-
...(asRecord(
|
|
1817
|
+
...(asRecord(completedContract) ?? parsed),
|
|
1598
1818
|
sourceBinding: canonicalBinding,
|
|
1599
1819
|
}, canonicalBinding);
|
|
1600
1820
|
const result = frontendImplementationContractSchema.safeParse(candidate);
|
|
1601
1821
|
if (!result.success)
|
|
1602
1822
|
fail("retryable-invalid", `invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`, candidateJsonSha256);
|
|
1603
1823
|
const blockingGaps = [
|
|
1604
|
-
...result.data.evidenceGaps,
|
|
1824
|
+
...(result.data.evidenceGaps ?? []),
|
|
1605
1825
|
...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
|
|
1606
1826
|
].filter((item) => item.blocking);
|
|
1607
1827
|
if (blockingGaps.length > 0)
|
|
@@ -1610,7 +1830,7 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1610
1830
|
.join(", ")}`, candidateJsonSha256);
|
|
1611
1831
|
for (const requirementId of canonicalBinding.requirementIds)
|
|
1612
1832
|
if (!result.data.requirements.some((item) => item.id === requirementId) &&
|
|
1613
|
-
!result.data.evidenceGaps.some((item) => item.requirementId === requirementId))
|
|
1833
|
+
!(result.data.evidenceGaps ?? []).some((item) => item.requirementId === requirementId))
|
|
1614
1834
|
fail("blocked", `frontend contract does not cover ${requirementId}`, candidateJsonSha256);
|
|
1615
1835
|
return {
|
|
1616
1836
|
canonical: result.data,
|
|
@@ -1634,6 +1854,40 @@ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
|
1634
1854
|
"/targets/files",
|
|
1635
1855
|
"/mockApi/productionDefaultOff",
|
|
1636
1856
|
];
|
|
1857
|
+
const PLAN_LEDGER_FACT_KINDS_FOR_COMPILE = [
|
|
1858
|
+
"target-surface",
|
|
1859
|
+
"component-choice",
|
|
1860
|
+
"state-flow",
|
|
1861
|
+
"data-flow",
|
|
1862
|
+
"mock-api",
|
|
1863
|
+
"design-deviation",
|
|
1864
|
+
"dependency",
|
|
1865
|
+
"plan-requirement",
|
|
1866
|
+
"plan-verification-target",
|
|
1867
|
+
"plan-evidence-gap",
|
|
1868
|
+
];
|
|
1869
|
+
/**
|
|
1870
|
+
* Restore the editable RFC 7386 plan patch from a committed plan ledger.
|
|
1871
|
+
* This is the single compile authority for the frontend implementation
|
|
1872
|
+
* contract: the tool payload (`patch` on any committed plan fact) wins over
|
|
1873
|
+
* any narrative JSON in the plan node text.
|
|
1874
|
+
*/
|
|
1875
|
+
export function restorePlanPatchFromCommittedFacts(records) {
|
|
1876
|
+
const planFacts = records
|
|
1877
|
+
.filter((record) => record.phase === "committed")
|
|
1878
|
+
.map((record) => record.fact)
|
|
1879
|
+
.filter((fact) => fact.origin === "plan" &&
|
|
1880
|
+
typeof fact.kind === "string" &&
|
|
1881
|
+
PLAN_LEDGER_FACT_KINDS_FOR_COMPILE.includes(fact.kind));
|
|
1882
|
+
if (planFacts.length === 0)
|
|
1883
|
+
return undefined;
|
|
1884
|
+
for (const fact of planFacts) {
|
|
1885
|
+
const patch = fact.patch;
|
|
1886
|
+
if (isPlainObject(patch))
|
|
1887
|
+
return patch;
|
|
1888
|
+
}
|
|
1889
|
+
return undefined;
|
|
1890
|
+
}
|
|
1637
1891
|
function frontendPlanPatchProtectedPathViolations(patch) {
|
|
1638
1892
|
const record = asRecord(patch);
|
|
1639
1893
|
if (!record)
|
|
@@ -1663,34 +1917,34 @@ function workspaceRootFromDagRunDir(runDir) {
|
|
|
1663
1917
|
const joined = segments.slice(0, harnessIndex).join(path.sep);
|
|
1664
1918
|
return joined || path.parse(absolute).root;
|
|
1665
1919
|
}
|
|
1666
|
-
function verificationSymbolMatchesFile(symbol, content) {
|
|
1667
|
-
if (symbol.trim().toLowerCase() === "all describe blocks")
|
|
1668
|
-
return content.includes("describe(");
|
|
1669
|
-
const symbols = symbol.includes(" / ")
|
|
1670
|
-
? symbol
|
|
1671
|
-
.split(/[((]/, 1)[0]
|
|
1672
|
-
.split("/")
|
|
1673
|
-
.map((item) => item.trim())
|
|
1674
|
-
.filter(Boolean)
|
|
1675
|
-
: [symbol];
|
|
1676
|
-
return symbols.every((item) => {
|
|
1677
|
-
const describeTitle = item.match(/^describe\((?:['"])(.+?)(?:['"])/i)?.[1];
|
|
1678
|
-
return [item, describeTitle, describeTitle ? "describe(" : undefined]
|
|
1679
|
-
.filter((candidate) => Boolean(candidate))
|
|
1680
|
-
.some((candidate) => content.includes(candidate));
|
|
1681
|
-
});
|
|
1682
|
-
}
|
|
1683
1920
|
async function dropUnresolvedVerificationSymbols(input) {
|
|
1684
1921
|
const workspaceRoot = workspaceRootFromDagRunDir(input.runDir);
|
|
1685
1922
|
const targets = asRecord(input.contract)?.verificationTargets;
|
|
1686
1923
|
if (!workspaceRoot || !Array.isArray(targets))
|
|
1687
|
-
return
|
|
1688
|
-
|
|
1924
|
+
return [];
|
|
1925
|
+
const actions = [];
|
|
1689
1926
|
for (const targetValue of targets) {
|
|
1690
1927
|
const target = asRecord(targetValue);
|
|
1691
1928
|
const file = asString(target?.file);
|
|
1692
1929
|
const symbol = asString(target?.symbol);
|
|
1693
|
-
if (!target || !file
|
|
1930
|
+
if (!target || !file)
|
|
1931
|
+
continue;
|
|
1932
|
+
// Static targets (project-wide commands such as typecheck/build) verify
|
|
1933
|
+
// by command exit code, never by a per-file symbol. Strip any symbol
|
|
1934
|
+
// the planner attached — including fabricated placeholders like "null"
|
|
1935
|
+
// — so the plan shape check and the verify trace gate never evaluate
|
|
1936
|
+
// it. This is the root fix for the recurring fabricated-symbol error
|
|
1937
|
+
// class: the field was allowed for static, the prompt did not forbid
|
|
1938
|
+
// it, and models habitually fill it; the compiler removes it instead.
|
|
1939
|
+
if (target.type === "static") {
|
|
1940
|
+
if (symbol) {
|
|
1941
|
+
delete target.symbol;
|
|
1942
|
+
if (!actions.includes("strip-static-verification-symbols"))
|
|
1943
|
+
actions.push("strip-static-verification-symbols");
|
|
1944
|
+
}
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
if (!symbol)
|
|
1694
1948
|
continue;
|
|
1695
1949
|
const absolute = path.resolve(workspaceRoot, file);
|
|
1696
1950
|
const relative = path.relative(workspaceRoot, absolute);
|
|
@@ -1699,6 +1953,10 @@ async function dropUnresolvedVerificationSymbols(input) {
|
|
|
1699
1953
|
relative.includes("..")) {
|
|
1700
1954
|
continue;
|
|
1701
1955
|
}
|
|
1956
|
+
// Configuration files (tsconfig.json, vite.config.*, ...) do not expose
|
|
1957
|
+
// source symbols; the trace gate skips them, so the drop must too.
|
|
1958
|
+
if (isConfigurationVerificationFile(file))
|
|
1959
|
+
continue;
|
|
1702
1960
|
let content;
|
|
1703
1961
|
try {
|
|
1704
1962
|
content = await readFile(absolute, "utf8");
|
|
@@ -1708,60 +1966,78 @@ async function dropUnresolvedVerificationSymbols(input) {
|
|
|
1708
1966
|
// changing the optional symbol during candidate normalization.
|
|
1709
1967
|
continue;
|
|
1710
1968
|
}
|
|
1711
|
-
if (
|
|
1969
|
+
if (verificationSymbolMatchesContent(symbol, content))
|
|
1712
1970
|
continue;
|
|
1713
1971
|
delete target.symbol;
|
|
1714
|
-
|
|
1972
|
+
if (!actions.includes("drop-unresolved-verification-symbols"))
|
|
1973
|
+
actions.push("drop-unresolved-verification-symbols");
|
|
1715
1974
|
}
|
|
1716
|
-
return
|
|
1717
|
-
}
|
|
1718
|
-
function extractSingleOpenspecCitationsBlock(text) {
|
|
1719
|
-
const blocks = text.match(/```openspec-citations[ \t]*\r?\n[\s\S]*?\r?\n```/g) ?? [];
|
|
1720
|
-
if (blocks.length !== 1) {
|
|
1721
|
-
throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
|
|
1722
|
-
}
|
|
1723
|
-
return blocks[0];
|
|
1724
|
-
}
|
|
1725
|
-
/**
|
|
1726
|
-
* Frontend plan/revision output has two JSON-bearing regions: the primary
|
|
1727
|
-
* payload and line-oriented OpenSpec citations. Keep their parsing domains
|
|
1728
|
-
* separate so a citation row can never become a competing contract candidate.
|
|
1729
|
-
* A bare JSON payload is retained only for historical node-output compatibility.
|
|
1730
|
-
*/
|
|
1731
|
-
export function extractFrontendPlanPayload(text) {
|
|
1732
|
-
const citationsBlock = extractSingleOpenspecCitationsBlock(text);
|
|
1733
|
-
return {
|
|
1734
|
-
payload: extractFrontendPlanPrimaryPayload(text.replace(citationsBlock, "")),
|
|
1735
|
-
citationsBlock,
|
|
1736
|
-
};
|
|
1737
|
-
}
|
|
1738
|
-
function extractFrontendPlanPrimaryPayload(text) {
|
|
1739
|
-
const fences = [...text.matchAll(/```json[ \t]*\r?\n([\s\S]*?)\r?\n```/gi)];
|
|
1740
|
-
if (fences.length > 1) {
|
|
1741
|
-
throw new Error(`frontend plan output must contain exactly one primary fenced json payload (found ${fences.length})`);
|
|
1742
|
-
}
|
|
1743
|
-
if (fences.length === 1) {
|
|
1744
|
-
try {
|
|
1745
|
-
return JSON.parse(fences[0][1]);
|
|
1746
|
-
}
|
|
1747
|
-
catch (error) {
|
|
1748
|
-
throw new Error(`frontend plan primary json fence is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
return extractFrontendImplementationJson(text);
|
|
1975
|
+
return actions;
|
|
1752
1976
|
}
|
|
1753
1977
|
async function writeFrontendPlanCandidateRaw(input) {
|
|
1754
1978
|
const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
|
|
1755
|
-
await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text);
|
|
1979
|
+
await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text.endsWith("\n") ? input.text : `${input.text}\n`);
|
|
1756
1980
|
return relativePath;
|
|
1757
1981
|
}
|
|
1758
1982
|
/**
|
|
1759
1983
|
* Compile the planner's editable RFC 7386 patch against the deterministic
|
|
1760
1984
|
* runtime skeleton. Every attempt is preserved under contracts/candidates so
|
|
1761
1985
|
* format/schema failures can be inspected and replayed without trusting model
|
|
1762
|
-
* prose. A successful result returns
|
|
1986
|
+
* prose. A successful result returns a path-only pointer plus the hash-bound canonical artifact for
|
|
1763
1987
|
* downstream review and prewrite nodes.
|
|
1764
1988
|
*/
|
|
1989
|
+
/**
|
|
1990
|
+
* Placeholder tokens that are never valid verification symbols ("null" is the
|
|
1991
|
+
* classic model artifact: it serializes a JS null into the symbol string).
|
|
1992
|
+
*/
|
|
1993
|
+
const VERIFICATION_SYMBOL_PLACEHOLDERS = new Set([
|
|
1994
|
+
"null",
|
|
1995
|
+
"undefined",
|
|
1996
|
+
"none",
|
|
1997
|
+
"n/a",
|
|
1998
|
+
"na",
|
|
1999
|
+
"tbd",
|
|
2000
|
+
"todo",
|
|
2001
|
+
"...",
|
|
2002
|
+
"?",
|
|
2003
|
+
"-",
|
|
2004
|
+
]);
|
|
2005
|
+
/** An over-long symbol can never resolve to a real declaration. */
|
|
2006
|
+
const VERIFICATION_SYMBOL_MAX_CHARS = 60;
|
|
2007
|
+
/**
|
|
2008
|
+
* Deterministic shape check for a verification target symbol. Catches obvious
|
|
2009
|
+
* model fabrication (placeholder tokens, prose fragments) at plan time so the
|
|
2010
|
+
* plan retry ladder can fix the facts in-node instead of the verify trace gate
|
|
2011
|
+
* failing at the very end of the run.
|
|
2012
|
+
*
|
|
2013
|
+
* Deliberately conservative: bare test titles without punctuation ("renders
|
|
2014
|
+
* loading") and `describe(...)` / `it(...)` forms stay valid — a real symbol
|
|
2015
|
+
* may be a function name, a dotted path, or a describe/it title.
|
|
2016
|
+
*/
|
|
2017
|
+
function isSuspiciousVerificationSymbol(symbol) {
|
|
2018
|
+
const trimmed = symbol.trim();
|
|
2019
|
+
if (!trimmed)
|
|
2020
|
+
return false;
|
|
2021
|
+
if (VERIFICATION_SYMBOL_PLACEHOLDERS.has(trimmed.toLowerCase()))
|
|
2022
|
+
return true;
|
|
2023
|
+
if (trimmed.length > VERIFICATION_SYMBOL_MAX_CHARS)
|
|
2024
|
+
return true;
|
|
2025
|
+
// Deliberately conservative: bare test titles with punctuation (e.g.
|
|
2026
|
+
// "adds +1 button") are valid describe/it-style symbols and must pass;
|
|
2027
|
+
// the length + placeholder rules above already catch obvious fabrication
|
|
2028
|
+
// ("null", over-long prose fragments). Everything else is left to the
|
|
2029
|
+
// deterministic drop/trace resolvability check.
|
|
2030
|
+
return false;
|
|
2031
|
+
}
|
|
2032
|
+
function assertVerificationSymbolShapes(contract) {
|
|
2033
|
+
for (const target of contract.verificationTargets) {
|
|
2034
|
+
if (!target.symbol)
|
|
2035
|
+
continue;
|
|
2036
|
+
if (isSuspiciousVerificationSymbol(target.symbol)) {
|
|
2037
|
+
throw new Error(`frontend plan verification target "${target.id}" declares a fabricated symbol "${target.symbol}" for ${target.file}; fix it to a real symbol present in that file or leave the symbol empty (the deterministic trace gate verifies symbol resolvability after verification)`);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
1765
2041
|
export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
1766
2042
|
const nodeId = input.nodeId ?? "frontend-plan-pi";
|
|
1767
2043
|
const attempt = input.attempt ?? 1;
|
|
@@ -1798,20 +2074,18 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1798
2074
|
const skeleton = input.structuredContractOutput?.skeleton;
|
|
1799
2075
|
if (!skeleton)
|
|
1800
2076
|
throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
|
|
1801
|
-
const
|
|
2077
|
+
const committed = await readTypedEventStoreFromJsonl(path.join(input.runDir, nodeId, "plan-typed-facts.jsonl"));
|
|
2078
|
+
const patch = restorePlanPatchFromCommittedFacts(committed);
|
|
1802
2079
|
if (!isPlainObject(patch))
|
|
1803
|
-
throw new Error("frontend plan
|
|
2080
|
+
throw new Error("frontend plan ledger missing: no committed origin=plan facts (record_* + finalize_plan); narrative JSON is not the compile authority");
|
|
1804
2081
|
extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
|
|
1805
2082
|
const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
|
|
1806
2083
|
extractedPatchSha256 = extractedArtifact.sha256;
|
|
1807
|
-
const citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1808
|
-
const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
|
|
1809
|
-
await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
|
|
1810
2084
|
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1811
2085
|
if (protectedViolations.length > 0)
|
|
1812
2086
|
throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
|
|
1813
2087
|
const merged = applyFrontendContractMergePatch(skeleton, patch);
|
|
1814
|
-
const
|
|
2088
|
+
const symbolNormalizationActions = await dropUnresolvedVerificationSymbols({
|
|
1815
2089
|
runDir: input.runDir,
|
|
1816
2090
|
contract: merged,
|
|
1817
2091
|
});
|
|
@@ -1820,6 +2094,24 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1820
2094
|
rawContractText: serializeDeterministicJson(merged),
|
|
1821
2095
|
sourceBinding: input.sourceBinding,
|
|
1822
2096
|
});
|
|
2097
|
+
// Front-load the verification-symbol shape check so fabricated symbols
|
|
2098
|
+
// are fixed by the plan retry ladder in-node instead of failing the
|
|
2099
|
+
// verify trace gate at the end of the run.
|
|
2100
|
+
assertVerificationSymbolShapes(analysis.canonical);
|
|
2101
|
+
// Front-load the plan-attributable design-policy checks so the §5.1
|
|
2102
|
+
// retry ladder can fix these facts in-node with diagnostics instead of
|
|
2103
|
+
// the run terminating at the policy shell. The policy shell remains the
|
|
2104
|
+
// final authority and re-runs the identical checks on committed facts.
|
|
2105
|
+
const policyPreFindings = [
|
|
2106
|
+
...checkUiDesignCoverage(analysis.canonical),
|
|
2107
|
+
...checkUiStateAttribution(analysis.canonical),
|
|
2108
|
+
...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
|
|
2109
|
+
...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
|
|
2110
|
+
];
|
|
2111
|
+
if (policyPreFindings.length > 0)
|
|
2112
|
+
throw new Error(`frontend plan policy pre-check failed (fix these plan facts, then re-commit and finalize): ${policyPreFindings
|
|
2113
|
+
.map((finding) => `${finding.code}: ${finding.message}`)
|
|
2114
|
+
.join("; ")}`);
|
|
1823
2115
|
const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
|
|
1824
2116
|
await writeReport({
|
|
1825
2117
|
classification: "accepted-normalized",
|
|
@@ -1827,47 +2119,66 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1827
2119
|
normalizedContractSha256: normalizedArtifact.sha256,
|
|
1828
2120
|
normalizationActions: [
|
|
1829
2121
|
"apply-runtime-skeleton",
|
|
1830
|
-
...
|
|
1831
|
-
? ["drop-unresolved-verification-symbols"]
|
|
1832
|
-
: []),
|
|
2122
|
+
...symbolNormalizationActions,
|
|
1833
2123
|
...analysis.normalizationActions,
|
|
1834
2124
|
],
|
|
1835
2125
|
errors: [],
|
|
1836
2126
|
});
|
|
2127
|
+
const canonicalRel = canonicalContractRel(nodeId);
|
|
2128
|
+
const canonicalArtifact = await writeDeterministicJsonArtifact(input.runDir, canonicalRel, analysis.canonical);
|
|
2129
|
+
await persistRuntimeSkeleton({
|
|
2130
|
+
runDir: input.runDir,
|
|
2131
|
+
nodeId,
|
|
2132
|
+
skeleton,
|
|
2133
|
+
});
|
|
2134
|
+
const artifact = {
|
|
2135
|
+
path: canonicalArtifact.path,
|
|
2136
|
+
sha256: canonicalArtifact.sha256,
|
|
2137
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
2138
|
+
};
|
|
2139
|
+
// Human-readable plan summary appended after the artifact pointer. The
|
|
2140
|
+
// pointer remains the machine authority (path/schema/sha256); the
|
|
2141
|
+
// rendered summary is a deterministic view of the canonical contract so
|
|
2142
|
+
// Console/reviewers can read the plan without opening the JSON. It never
|
|
2143
|
+
// participates in downstream validation.
|
|
2144
|
+
const planSummary = renderFrontendPlanMarkdown(analysis.canonical);
|
|
1837
2145
|
return {
|
|
1838
2146
|
ok: true,
|
|
1839
2147
|
contract: analysis.canonical,
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
sha256: normalizedArtifact.sha256,
|
|
1843
|
-
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1844
|
-
},
|
|
1845
|
-
frontendPlanOutput: {
|
|
1846
|
-
schemaVersion: 1,
|
|
1847
|
-
payloadKind: "canonical-contract",
|
|
1848
|
-
citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
|
|
1849
|
-
citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
|
|
1850
|
-
rawCandidatePath: path.join(input.runDir, rawPath),
|
|
1851
|
-
rawCandidateSha256: sha256Hex(input.text),
|
|
1852
|
-
},
|
|
1853
|
-
normalizedText: [
|
|
1854
|
-
"Frontend implementation contract compiled from the runtime skeleton.",
|
|
1855
|
-
"",
|
|
1856
|
-
"```json",
|
|
1857
|
-
serializeDeterministicJson(analysis.canonical),
|
|
1858
|
-
"```",
|
|
1859
|
-
citationsBlock,
|
|
1860
|
-
].join("\n"),
|
|
2148
|
+
artifact,
|
|
2149
|
+
normalizedText: `${formatStructuredArtifactPointer(artifact)}\n\n${planSummary}`,
|
|
1861
2150
|
};
|
|
1862
2151
|
}
|
|
1863
2152
|
catch (error) {
|
|
1864
2153
|
const reason = error instanceof Error ? error.message : String(error);
|
|
2154
|
+
const classified = classifyStructuredContractFailure({
|
|
2155
|
+
reason,
|
|
2156
|
+
kind: error instanceof FrontendContractFailure ? error.kind : undefined,
|
|
2157
|
+
});
|
|
1865
2158
|
await writeReport({
|
|
1866
|
-
classification:
|
|
2159
|
+
classification: classified.classification,
|
|
1867
2160
|
normalizationActions: [],
|
|
1868
|
-
errors:
|
|
2161
|
+
errors: classified.errors,
|
|
1869
2162
|
});
|
|
1870
|
-
return {
|
|
2163
|
+
return {
|
|
2164
|
+
ok: false,
|
|
2165
|
+
reason,
|
|
2166
|
+
classification: classified.classification,
|
|
2167
|
+
errors: classified.errors,
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
export async function readFrontendCanonicalCandidate(runDir, nodeId) {
|
|
2172
|
+
const candidatePath = path.join(runDir, canonicalContractRel(nodeId));
|
|
2173
|
+
try {
|
|
2174
|
+
const raw = await readFile(candidatePath, "utf8");
|
|
2175
|
+
const parsed = frontendImplementationContractSchema.safeParse(JSON.parse(raw));
|
|
2176
|
+
return parsed.success ? parsed.data : undefined;
|
|
2177
|
+
}
|
|
2178
|
+
catch (error) {
|
|
2179
|
+
if (error.code === "ENOENT")
|
|
2180
|
+
return undefined;
|
|
2181
|
+
return undefined;
|
|
1871
2182
|
}
|
|
1872
2183
|
}
|
|
1873
2184
|
/**
|
|
@@ -1888,79 +2199,140 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1888
2199
|
return { ok: true, contract: analysis.canonical };
|
|
1889
2200
|
}
|
|
1890
2201
|
catch (error) {
|
|
2202
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2203
|
+
const classified = classifyStructuredContractFailure({
|
|
2204
|
+
reason,
|
|
2205
|
+
kind: error instanceof FrontendContractFailure ? error.kind : undefined,
|
|
2206
|
+
});
|
|
1891
2207
|
return {
|
|
1892
2208
|
ok: false,
|
|
1893
|
-
reason
|
|
2209
|
+
reason,
|
|
2210
|
+
classification: classified.classification,
|
|
2211
|
+
errors: classified.errors,
|
|
1894
2212
|
};
|
|
1895
2213
|
}
|
|
1896
2214
|
}
|
|
1897
2215
|
/**
|
|
1898
|
-
*
|
|
1899
|
-
*
|
|
1900
|
-
*
|
|
1901
|
-
*
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
1904
|
-
*
|
|
1905
|
-
*
|
|
2216
|
+
* Deterministically assemble the canonical frontend implementation contract
|
|
2217
|
+
* from committed typed facts, never from plan/contract node text.
|
|
2218
|
+
*
|
|
2219
|
+
* Inputs (all read-only, all fail-closed):
|
|
2220
|
+
* - `<planNodeId>/plan-typed-facts.jsonl` (phase=committed) restores the
|
|
2221
|
+
* editable RFC 7386 patch;
|
|
2222
|
+
* - `<contractNodeId>/contract-typed-facts.jsonl` (phase=committed) must carry
|
|
2223
|
+
* a `contract-finalized` terminal fact;
|
|
2224
|
+
* - `<contractNodeId>/frontend-task-contract-vNext.json` must exist and parse.
|
|
2225
|
+
*
|
|
2226
|
+
* The patch is applied onto the runtime skeleton and re-validated through the
|
|
2227
|
+
* same `analyzeFrontendImplementationContract` pipeline used by the legacy
|
|
2228
|
+
* text path, so the downstream artifact semantics are unchanged. The function
|
|
2229
|
+
* is pure: it never writes candidate/audit artifacts and always produces the
|
|
2230
|
+
* same canonical serialization for the same committed facts. It never falls
|
|
2231
|
+
* back to parsing Plan/Contract node text.
|
|
1906
2232
|
*/
|
|
1907
|
-
export async function
|
|
1908
|
-
const
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
const
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2233
|
+
export async function buildContractFromCommittedFacts(input) {
|
|
2234
|
+
const contractNodeId = input.contractNodeId ?? "frontend-contract-pi";
|
|
2235
|
+
// Skeleton guard: the runtime skeleton must carry the protected identity
|
|
2236
|
+
// fields so the merged contract can be validated against the full schema.
|
|
2237
|
+
const skeletonRecord = asRecord(input.skeleton);
|
|
2238
|
+
const skeletonTargets = asRecord(skeletonRecord?.targets);
|
|
2239
|
+
const skeletonRiskLevel = asString(skeletonRecord?.riskLevel);
|
|
2240
|
+
if (!skeletonRecord ||
|
|
2241
|
+
!["small", "standard", "high-risk"].includes(skeletonRiskLevel) ||
|
|
2242
|
+
!Array.isArray(skeletonTargets?.files) ||
|
|
2243
|
+
skeletonTargets.files.length === 0) {
|
|
2244
|
+
return {
|
|
2245
|
+
ok: false,
|
|
2246
|
+
reason: "frontend contract skeleton missing required runtime fields (riskLevel/targets.files)",
|
|
2247
|
+
failureCode: "missing-skeleton",
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
// a. Committed plan ledger restores the editable patch.
|
|
2251
|
+
const planRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.planNodeId, "plan-typed-facts.jsonl"));
|
|
2252
|
+
const patch = restorePlanPatchFromCommittedFacts(planRecords);
|
|
2253
|
+
if (!isPlainObject(patch)) {
|
|
2254
|
+
return {
|
|
2255
|
+
ok: false,
|
|
2256
|
+
reason: "frontend plan ledger missing: no committed origin=plan facts (record_* + finalize_plan); narrative JSON is not the compile authority",
|
|
2257
|
+
failureCode: "missing-plan-ledger",
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
// b. Committed contract terminal fact (contract-finalized).
|
|
2261
|
+
const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, contractNodeId, "contract-typed-facts.jsonl"));
|
|
2262
|
+
const contractKinds = new Set(contractRecords
|
|
2263
|
+
.filter((record) => record.phase === "committed")
|
|
2264
|
+
.map((record) => record.fact.kind)
|
|
2265
|
+
.filter((kind) => typeof kind === "string"));
|
|
2266
|
+
if (!contractKinds.has("contract-finalized")) {
|
|
2267
|
+
return {
|
|
2268
|
+
ok: false,
|
|
2269
|
+
reason: "frontend contract facts missing: no committed contract-finalized terminal fact",
|
|
2270
|
+
failureCode: "missing-contract-finalize",
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
// c. frontend-task-contract-vNext.json must exist and parse (input integrity).
|
|
2274
|
+
let rawVNext;
|
|
1919
2275
|
try {
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
2276
|
+
rawVNext = await readFile(path.join(input.runDir, contractNodeId, "frontend-task-contract-vNext.json"), "utf8");
|
|
2277
|
+
}
|
|
2278
|
+
catch {
|
|
2279
|
+
return {
|
|
2280
|
+
ok: false,
|
|
2281
|
+
reason: `frontend task contract vNext missing at ${contractNodeId}/frontend-task-contract-vNext.json`,
|
|
2282
|
+
failureCode: "contract-vnext-missing",
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
try {
|
|
2286
|
+
JSON.parse(rawVNext);
|
|
1923
2287
|
}
|
|
1924
2288
|
catch (error) {
|
|
1925
2289
|
return {
|
|
1926
2290
|
ok: false,
|
|
1927
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
2291
|
+
reason: `frontend task contract vNext is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
2292
|
+
failureCode: "contract-vnext-invalid",
|
|
1928
2293
|
};
|
|
1929
2294
|
}
|
|
1930
|
-
|
|
2295
|
+
// d. Merge the editable patch onto the protected skeleton and validate.
|
|
2296
|
+
let merged;
|
|
2297
|
+
try {
|
|
2298
|
+
merged = applyFrontendContractMergePatch(input.skeleton, patch);
|
|
2299
|
+
}
|
|
2300
|
+
catch (error) {
|
|
1931
2301
|
return {
|
|
1932
2302
|
ok: false,
|
|
1933
|
-
reason:
|
|
2303
|
+
reason: `frontend contract patch could not be applied to the runtime skeleton: ${error instanceof Error ? error.message : String(error)}`,
|
|
2304
|
+
failureCode: "schema-drift",
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
// The design-policy shell is the authoritative materializer of the
|
|
2308
|
+
// canonical contract; keep it on the same unresolved-symbol drop as the
|
|
2309
|
+
// plan validator so the verify trace gate never sees fabricated symbols.
|
|
2310
|
+
await dropUnresolvedVerificationSymbols({
|
|
2311
|
+
runDir: input.runDir,
|
|
2312
|
+
contract: merged,
|
|
2313
|
+
});
|
|
2314
|
+
try {
|
|
2315
|
+
const analysis = await analyzeFrontendImplementationContract({
|
|
2316
|
+
runDir: input.runDir,
|
|
2317
|
+
rawContractText: serializeDeterministicJson(merged),
|
|
2318
|
+
sourceBinding: input.sourceBinding,
|
|
2319
|
+
});
|
|
2320
|
+
return {
|
|
2321
|
+
ok: true,
|
|
2322
|
+
canonical: analysis.canonical,
|
|
2323
|
+
normalizationActions: analysis.normalizationActions,
|
|
2324
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
1934
2325
|
};
|
|
1935
2326
|
}
|
|
1936
|
-
|
|
1937
|
-
|
|
2327
|
+
catch (error) {
|
|
2328
|
+
const failureKind = error instanceof FrontendContractFailure ? error.kind : undefined;
|
|
1938
2329
|
return {
|
|
1939
2330
|
ok: false,
|
|
1940
|
-
reason: `frontend
|
|
2331
|
+
reason: `frontend contract schema drift: ${error instanceof Error ? error.message : String(error)}`,
|
|
2332
|
+
failureCode: "schema-drift",
|
|
2333
|
+
...(failureKind ? { failureKind } : {}),
|
|
1941
2334
|
};
|
|
1942
2335
|
}
|
|
1943
|
-
const payloadPath = path.posix.join(candidateDir, `attempt-${attempt}.patch.json`);
|
|
1944
|
-
const payloadArtifact = await writeDeterministicJsonArtifact(input.runDir, payloadPath, patch);
|
|
1945
|
-
const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
|
|
1946
|
-
await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
|
|
1947
|
-
return {
|
|
1948
|
-
ok: true,
|
|
1949
|
-
contract: patch,
|
|
1950
|
-
structuredArtifact: {
|
|
1951
|
-
path: payloadArtifact.path,
|
|
1952
|
-
sha256: payloadArtifact.sha256,
|
|
1953
|
-
schemaId: "frontend-implementation-contract-revision-patch-v1",
|
|
1954
|
-
},
|
|
1955
|
-
frontendPlanOutput: {
|
|
1956
|
-
schemaVersion: 1,
|
|
1957
|
-
payloadKind: "merge-patch",
|
|
1958
|
-
citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
|
|
1959
|
-
citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
|
|
1960
|
-
rawCandidatePath: path.join(input.runDir, rawPath),
|
|
1961
|
-
rawCandidateSha256: sha256Hex(input.text),
|
|
1962
|
-
},
|
|
1963
|
-
};
|
|
1964
2336
|
}
|
|
1965
2337
|
export async function materializeFrontendImplementationContract(input) {
|
|
1966
2338
|
const analysis = await analyzeFrontendImplementationContract(input);
|