@tea-agent/loop-agent 0.39.0-beta.0 → 0.39.0-beta.10
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 +248 -95
- 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 +2306 -61
- package/dist/executors/pi-executor.js +7 -0
- 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 +180 -33
- package/dist/executors/shell-executor.js +1249 -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 +62 -4
- 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 +264 -16
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +298 -32
- 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 +14 -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-BAsjg7ew.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-CyIlrwZw.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-Bo4tobhH.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-Dfpow9gu.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-DPmzG_B4.js} +1 -1
- package/dist/worker/console/static/assets/channel-DSrJAn0-.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-pKQpB1UW.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-5n9YYquZ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-DP3CtA2a.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-DYxMJPUy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-CS3D50Io.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-gSdDfo5_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-A48q0ZNt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-ow8Mm3DY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-CMKfLWYE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BiTBdQPX.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DeMmPl0_.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DeMmPl0_.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-Cm7exRz_.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-D9O5qGOu.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-CkvQnako.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-B1rzp7CB.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-BP07Ipcc.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BlPzweQb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-CxPa7n0S.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-CMTZs53A.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-CxG_X52r.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-D8axtJM0.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-DIsGzn7b.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CU9zkDG0.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-DCjTmz5P.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-BksdmFzg.js} +1 -1
- package/dist/worker/console/static/assets/index-B-O5ChYq.css +1 -0
- package/dist/worker/console/static/assets/index-CfnY8AQx.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-CGzzzJtL.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-BdseB80N.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-DM9gsgha.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-3q-_9R2b.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-B8N-Mxsd.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-EPIHUUsu.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-CxwO12aa.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-bPqbVX6Y.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-KzqIvzeX.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-CyofpWiV.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-cTWJvLTP.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-BK--q27B.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-C0SDOHXK.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-Cong6YIo.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DUVkgZx8.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C7XgxuZg.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dzbnd5oX.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-D9Ayvwcy.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BY0hHk6G.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-Byb000cH.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DMLegPt4.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-ChMMeOsP.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-bEMEPisW.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 +140 -8
- 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/turn-submission.js +16 -3
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +102 -21
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
- 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 +337 -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 +10 -5
- 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 +1 -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 +534 -178
- package/dist/workflows/dag/frontend-plan-render.js +24 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +371 -393
- 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 +984 -618
- package/dist/workflows/dag/interrupt-request.js +10 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +468 -34
- 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 +321 -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 +45 -1
- 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 +3 -3
- 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
|
@@ -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",
|
|
@@ -41,9 +50,6 @@ export function serializeDeterministicJson(value) {
|
|
|
41
50
|
export function deterministicSha256(value) {
|
|
42
51
|
return sha256Hex(serializeDeterministicJson(value));
|
|
43
52
|
}
|
|
44
|
-
/** Provider-safe limits for the two planner outputs. Larger work must split. */
|
|
45
|
-
export const FRONTEND_PLAN_RESPONSE_MAX_BYTES = 14 * 1024;
|
|
46
|
-
export const FRONTEND_CANONICAL_CONTRACT_MAX_BYTES = 16 * 1024;
|
|
47
53
|
/** A contract materialization failure carries its classification plus the
|
|
48
54
|
* audit context collected up to the failure point, so the prewrite gate can
|
|
49
55
|
* persist a complete frontend-prewrite-result-v1 even when the canonical
|
|
@@ -105,6 +111,68 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
|
|
|
105
111
|
}
|
|
106
112
|
return [...labels];
|
|
107
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
|
+
}
|
|
108
176
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
|
|
109
177
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
|
|
110
178
|
/**
|
|
@@ -172,7 +240,6 @@ export function loadFrontendImplementationContractJsonSchema(startDir = path.dir
|
|
|
172
240
|
"mockApi",
|
|
173
241
|
"designEvidence",
|
|
174
242
|
"verificationTargets",
|
|
175
|
-
"evidenceGaps",
|
|
176
243
|
];
|
|
177
244
|
const actualRequired = Array.isArray(schema.required) ? schema.required : [];
|
|
178
245
|
const missing = expectedRequired.filter((key) => !actualRequired.includes(key));
|
|
@@ -221,13 +288,13 @@ const gap = z
|
|
|
221
288
|
* `specReference` is required (path/section/line hit).
|
|
222
289
|
* - `reuse-existing`: reuse an existing repo component/convention not named by
|
|
223
290
|
* the spec → `specReference` may be null, rationale explains the basis.
|
|
224
|
-
* - `new`:
|
|
225
|
-
*
|
|
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.
|
|
226
294
|
*
|
|
227
|
-
* `specReference` is nullable + optional
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* 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.
|
|
231
298
|
*/
|
|
232
299
|
const uiComponentSpecReferenceSchema = z
|
|
233
300
|
.object({
|
|
@@ -242,7 +309,11 @@ const uiComponentChoiceSchema = z
|
|
|
242
309
|
component: z.string().min(1),
|
|
243
310
|
decision: z.enum(["specified", "reuse-existing", "new"]),
|
|
244
311
|
specReference: uiComponentSpecReferenceSchema.nullable().optional(),
|
|
245
|
-
|
|
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(),
|
|
246
317
|
})
|
|
247
318
|
.strict();
|
|
248
319
|
const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
|
|
@@ -271,7 +342,12 @@ function stripNullValuesDeep(value) {
|
|
|
271
342
|
return value;
|
|
272
343
|
}
|
|
273
344
|
export const frontendImplementationContractSchema = z
|
|
274
|
-
.preprocess(
|
|
345
|
+
.preprocess((value) => {
|
|
346
|
+
const record = asRecord(value);
|
|
347
|
+
if (!record)
|
|
348
|
+
return stripNullValuesDeep(value);
|
|
349
|
+
return stripNullValuesDeep(record);
|
|
350
|
+
}, z
|
|
275
351
|
.object({
|
|
276
352
|
schemaVersion: z.literal(1),
|
|
277
353
|
sourceBinding: z
|
|
@@ -281,8 +357,33 @@ export const frontendImplementationContractSchema = z
|
|
|
281
357
|
requirementSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
282
358
|
referencePaths: z.array(safePath),
|
|
283
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(),
|
|
284
367
|
})
|
|
285
|
-
.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
|
+
}),
|
|
286
387
|
riskLevel: z.enum(["small", "standard", "high-risk"]),
|
|
287
388
|
targets: z
|
|
288
389
|
.object({
|
|
@@ -298,6 +399,10 @@ export const frontendImplementationContractSchema = z
|
|
|
298
399
|
implementationTargets: z.array(safePath),
|
|
299
400
|
verificationTargetIds: z.array(z.string().min(1)),
|
|
300
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(),
|
|
301
406
|
})
|
|
302
407
|
.strict()).min(1),
|
|
303
408
|
uiStates: z.array(z
|
|
@@ -370,7 +475,7 @@ export const frontendImplementationContractSchema = z
|
|
|
370
475
|
uiStates: z.array(z.string().min(1)),
|
|
371
476
|
})
|
|
372
477
|
.strict()).min(1),
|
|
373
|
-
evidenceGaps: z.array(gap),
|
|
478
|
+
evidenceGaps: z.array(gap).optional(),
|
|
374
479
|
implementationSteps: z.array(z.string().min(1)).optional(),
|
|
375
480
|
stylingStrategy: z.string().min(1).optional(),
|
|
376
481
|
uiComponentChoices: uiComponentChoicesSchema.optional(),
|
|
@@ -434,6 +539,32 @@ export const frontendImplementationContractSchema = z
|
|
|
434
539
|
message: `unknown verification target ${targetId}`,
|
|
435
540
|
path: ["requirements"],
|
|
436
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
|
+
}
|
|
437
568
|
}
|
|
438
569
|
for (const state of value.uiStates) {
|
|
439
570
|
if (state.applicable &&
|
|
@@ -463,10 +594,10 @@ export const frontendImplementationContractSchema = z
|
|
|
463
594
|
}
|
|
464
595
|
}
|
|
465
596
|
else if (choice.decision === "new") {
|
|
466
|
-
if (choice.specReference) {
|
|
597
|
+
if (!choice.specReference || !choice.specReference.path) {
|
|
467
598
|
ctx.addIssue({
|
|
468
599
|
code: "custom",
|
|
469
|
-
message: "new component choice
|
|
600
|
+
message: "new component choice requires a task-source specReference.path",
|
|
470
601
|
path: ["uiComponentChoices", index, "specReference"],
|
|
471
602
|
});
|
|
472
603
|
}
|
|
@@ -570,6 +701,29 @@ export async function assertFrontendSourceBindingFresh(input) {
|
|
|
570
701
|
throw new Error(`frontend source binding is stale: ${source.path}`);
|
|
571
702
|
}
|
|
572
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
|
+
}
|
|
573
727
|
}
|
|
574
728
|
const SECRET_KEY = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|credential|authorization)/i;
|
|
575
729
|
const SECRET_VALUE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b)/;
|
|
@@ -744,7 +898,7 @@ export function canonicalFrontendContractSourceBinding(binding) {
|
|
|
744
898
|
const requirement = binding.sources.find((source) => source.kind === "requirement");
|
|
745
899
|
if (!requirement)
|
|
746
900
|
throw new Error("frontend implementation contract gate requires a requirement source in DAG sourceBinding");
|
|
747
|
-
|
|
901
|
+
const sourceBinding = {
|
|
748
902
|
taskId: binding.taskId,
|
|
749
903
|
requirementPath: requirement.path,
|
|
750
904
|
requirementSha256: requirement.sha256,
|
|
@@ -754,6 +908,15 @@ export function canonicalFrontendContractSourceBinding(binding) {
|
|
|
754
908
|
.sort(),
|
|
755
909
|
requirementIds: binding.requirementIds.map(canonicalizeRequirementId),
|
|
756
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;
|
|
757
920
|
}
|
|
758
921
|
function asRecord(value) {
|
|
759
922
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -1082,6 +1245,40 @@ function looksLikeStrictFrontendContract(value) {
|
|
|
1082
1245
|
asRecord(record.mockApi) !== null &&
|
|
1083
1246
|
asRecord(record.designEvidence) !== null);
|
|
1084
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
|
+
}
|
|
1085
1282
|
/**
|
|
1086
1283
|
* Coerce common free-form plan JSON into frontend-implementation-contract-v1.
|
|
1087
1284
|
* Near-schema payloads are left untouched so unknown-key fail-closed still holds.
|
|
@@ -1186,6 +1383,9 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1186
1383
|
...strictRecord,
|
|
1187
1384
|
requirements,
|
|
1188
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.
|
|
1189
1389
|
};
|
|
1190
1390
|
}
|
|
1191
1391
|
}
|
|
@@ -1234,9 +1434,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1234
1434
|
: typeRaw.includes("type")
|
|
1235
1435
|
? "static"
|
|
1236
1436
|
: "unit";
|
|
1237
|
-
const commandLabel = asString(vt.commandLabel) ||
|
|
1238
|
-
asString(vt.command) ||
|
|
1239
|
-
(type === "static" ? "npm run typecheck" : "npm run test:unit:fe");
|
|
1437
|
+
const commandLabel = asString(vt.commandLabel) || asString(vt.command);
|
|
1240
1438
|
const file = asString(vt.file) ||
|
|
1241
1439
|
(Array.isArray(vt.symbols) ? targetFiles.find((p) => p.includes("__tests__")) : "") ||
|
|
1242
1440
|
targetFiles.find((p) => p.includes("__tests__")) ||
|
|
@@ -1252,7 +1450,11 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1252
1450
|
requirementIds: requirementIds.length > 0
|
|
1253
1451
|
? requirementIds
|
|
1254
1452
|
: [...canonicalBinding.requirementIds],
|
|
1255
|
-
|
|
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,
|
|
1256
1458
|
});
|
|
1257
1459
|
}
|
|
1258
1460
|
if (verificationTargets.length === 0) {
|
|
@@ -1287,6 +1489,14 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1287
1489
|
verificationTargetIds: verificationTargetIds.length > 0
|
|
1288
1490
|
? verificationTargetIds
|
|
1289
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 } : {}),
|
|
1290
1500
|
};
|
|
1291
1501
|
// Do not mark free-form realIntegrationGap as blocking; defer to FE-TEST/FINAL-VERIFY.
|
|
1292
1502
|
if (gapText) {
|
|
@@ -1488,7 +1698,13 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1488
1698
|
rawOutput = input.rawContractText;
|
|
1489
1699
|
}
|
|
1490
1700
|
else if (input.fromNodeId !== undefined) {
|
|
1491
|
-
|
|
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
|
+
}
|
|
1492
1708
|
rawOutput = record.assistantText ?? record.stdout ?? "";
|
|
1493
1709
|
}
|
|
1494
1710
|
else {
|
|
@@ -1596,15 +1812,16 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1596
1812
|
// There is exactly one post-security candidate. A fallback candidate would
|
|
1597
1813
|
// allow malformed raw fields to bypass the boundary checks above.
|
|
1598
1814
|
const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
|
|
1815
|
+
const completedContract = completeCanonicalRequiredFields(normalizedContract);
|
|
1599
1816
|
const candidate = deriveFrontendVerificationCoverage({
|
|
1600
|
-
...(asRecord(
|
|
1817
|
+
...(asRecord(completedContract) ?? parsed),
|
|
1601
1818
|
sourceBinding: canonicalBinding,
|
|
1602
1819
|
}, canonicalBinding);
|
|
1603
1820
|
const result = frontendImplementationContractSchema.safeParse(candidate);
|
|
1604
1821
|
if (!result.success)
|
|
1605
1822
|
fail("retryable-invalid", `invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`, candidateJsonSha256);
|
|
1606
1823
|
const blockingGaps = [
|
|
1607
|
-
...result.data.evidenceGaps,
|
|
1824
|
+
...(result.data.evidenceGaps ?? []),
|
|
1608
1825
|
...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
|
|
1609
1826
|
].filter((item) => item.blocking);
|
|
1610
1827
|
if (blockingGaps.length > 0)
|
|
@@ -1613,7 +1830,7 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1613
1830
|
.join(", ")}`, candidateJsonSha256);
|
|
1614
1831
|
for (const requirementId of canonicalBinding.requirementIds)
|
|
1615
1832
|
if (!result.data.requirements.some((item) => item.id === requirementId) &&
|
|
1616
|
-
!result.data.evidenceGaps.some((item) => item.requirementId === requirementId))
|
|
1833
|
+
!(result.data.evidenceGaps ?? []).some((item) => item.requirementId === requirementId))
|
|
1617
1834
|
fail("blocked", `frontend contract does not cover ${requirementId}`, candidateJsonSha256);
|
|
1618
1835
|
return {
|
|
1619
1836
|
canonical: result.data,
|
|
@@ -1637,6 +1854,40 @@ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
|
1637
1854
|
"/targets/files",
|
|
1638
1855
|
"/mockApi/productionDefaultOff",
|
|
1639
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
|
+
}
|
|
1640
1891
|
function frontendPlanPatchProtectedPathViolations(patch) {
|
|
1641
1892
|
const record = asRecord(patch);
|
|
1642
1893
|
if (!record)
|
|
@@ -1666,34 +1917,34 @@ function workspaceRootFromDagRunDir(runDir) {
|
|
|
1666
1917
|
const joined = segments.slice(0, harnessIndex).join(path.sep);
|
|
1667
1918
|
return joined || path.parse(absolute).root;
|
|
1668
1919
|
}
|
|
1669
|
-
function verificationSymbolMatchesFile(symbol, content) {
|
|
1670
|
-
if (symbol.trim().toLowerCase() === "all describe blocks")
|
|
1671
|
-
return content.includes("describe(");
|
|
1672
|
-
const symbols = symbol.includes(" / ")
|
|
1673
|
-
? symbol
|
|
1674
|
-
.split(/[((]/, 1)[0]
|
|
1675
|
-
.split("/")
|
|
1676
|
-
.map((item) => item.trim())
|
|
1677
|
-
.filter(Boolean)
|
|
1678
|
-
: [symbol];
|
|
1679
|
-
return symbols.every((item) => {
|
|
1680
|
-
const describeTitle = item.match(/^describe\((?:['"])(.+?)(?:['"])/i)?.[1];
|
|
1681
|
-
return [item, describeTitle, describeTitle ? "describe(" : undefined]
|
|
1682
|
-
.filter((candidate) => Boolean(candidate))
|
|
1683
|
-
.some((candidate) => content.includes(candidate));
|
|
1684
|
-
});
|
|
1685
|
-
}
|
|
1686
1920
|
async function dropUnresolvedVerificationSymbols(input) {
|
|
1687
1921
|
const workspaceRoot = workspaceRootFromDagRunDir(input.runDir);
|
|
1688
1922
|
const targets = asRecord(input.contract)?.verificationTargets;
|
|
1689
1923
|
if (!workspaceRoot || !Array.isArray(targets))
|
|
1690
|
-
return
|
|
1691
|
-
|
|
1924
|
+
return [];
|
|
1925
|
+
const actions = [];
|
|
1692
1926
|
for (const targetValue of targets) {
|
|
1693
1927
|
const target = asRecord(targetValue);
|
|
1694
1928
|
const file = asString(target?.file);
|
|
1695
1929
|
const symbol = asString(target?.symbol);
|
|
1696
|
-
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)
|
|
1697
1948
|
continue;
|
|
1698
1949
|
const absolute = path.resolve(workspaceRoot, file);
|
|
1699
1950
|
const relative = path.relative(workspaceRoot, absolute);
|
|
@@ -1702,6 +1953,10 @@ async function dropUnresolvedVerificationSymbols(input) {
|
|
|
1702
1953
|
relative.includes("..")) {
|
|
1703
1954
|
continue;
|
|
1704
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;
|
|
1705
1960
|
let content;
|
|
1706
1961
|
try {
|
|
1707
1962
|
content = await readFile(absolute, "utf8");
|
|
@@ -1711,60 +1966,78 @@ async function dropUnresolvedVerificationSymbols(input) {
|
|
|
1711
1966
|
// changing the optional symbol during candidate normalization.
|
|
1712
1967
|
continue;
|
|
1713
1968
|
}
|
|
1714
|
-
if (
|
|
1969
|
+
if (verificationSymbolMatchesContent(symbol, content))
|
|
1715
1970
|
continue;
|
|
1716
1971
|
delete target.symbol;
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
return changed;
|
|
1720
|
-
}
|
|
1721
|
-
function extractSingleOpenspecCitationsBlock(text) {
|
|
1722
|
-
const blocks = text.match(/```openspec-citations[ \t]*\r?\n[\s\S]*?\r?\n```/g) ?? [];
|
|
1723
|
-
if (blocks.length !== 1) {
|
|
1724
|
-
throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
|
|
1972
|
+
if (!actions.includes("drop-unresolved-verification-symbols"))
|
|
1973
|
+
actions.push("drop-unresolved-verification-symbols");
|
|
1725
1974
|
}
|
|
1726
|
-
return
|
|
1727
|
-
}
|
|
1728
|
-
/**
|
|
1729
|
-
* Frontend plan/revision output has two JSON-bearing regions: the primary
|
|
1730
|
-
* payload and line-oriented OpenSpec citations. Keep their parsing domains
|
|
1731
|
-
* separate so a citation row can never become a competing contract candidate.
|
|
1732
|
-
* A bare JSON payload is retained only for historical node-output compatibility.
|
|
1733
|
-
*/
|
|
1734
|
-
export function extractFrontendPlanPayload(text) {
|
|
1735
|
-
const citationsBlock = extractSingleOpenspecCitationsBlock(text);
|
|
1736
|
-
return {
|
|
1737
|
-
payload: extractFrontendPlanPrimaryPayload(text.replace(citationsBlock, "")),
|
|
1738
|
-
citationsBlock,
|
|
1739
|
-
};
|
|
1740
|
-
}
|
|
1741
|
-
function extractFrontendPlanPrimaryPayload(text) {
|
|
1742
|
-
const fences = [...text.matchAll(/```json[ \t]*\r?\n([\s\S]*?)\r?\n```/gi)];
|
|
1743
|
-
if (fences.length > 1) {
|
|
1744
|
-
throw new Error(`frontend plan output must contain exactly one primary fenced json payload (found ${fences.length})`);
|
|
1745
|
-
}
|
|
1746
|
-
if (fences.length === 1) {
|
|
1747
|
-
try {
|
|
1748
|
-
return JSON.parse(fences[0][1]);
|
|
1749
|
-
}
|
|
1750
|
-
catch (error) {
|
|
1751
|
-
throw new Error(`frontend plan primary json fence is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
return extractFrontendImplementationJson(text);
|
|
1975
|
+
return actions;
|
|
1755
1976
|
}
|
|
1756
1977
|
async function writeFrontendPlanCandidateRaw(input) {
|
|
1757
1978
|
const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
|
|
1758
|
-
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`);
|
|
1759
1980
|
return relativePath;
|
|
1760
1981
|
}
|
|
1761
1982
|
/**
|
|
1762
1983
|
* Compile the planner's editable RFC 7386 patch against the deterministic
|
|
1763
1984
|
* runtime skeleton. Every attempt is preserved under contracts/candidates so
|
|
1764
1985
|
* format/schema failures can be inspected and replayed without trusting model
|
|
1765
|
-
* prose. A successful result returns
|
|
1986
|
+
* prose. A successful result returns a path-only pointer plus the hash-bound canonical artifact for
|
|
1766
1987
|
* downstream review and prewrite nodes.
|
|
1767
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
|
+
}
|
|
1768
2041
|
export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
1769
2042
|
const nodeId = input.nodeId ?? "frontend-plan-pi";
|
|
1770
2043
|
const attempt = input.attempt ?? 1;
|
|
@@ -1796,28 +2069,23 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1796
2069
|
});
|
|
1797
2070
|
};
|
|
1798
2071
|
try {
|
|
1799
|
-
if (Buffer.byteLength(input.text, "utf8") > FRONTEND_PLAN_RESPONSE_MAX_BYTES) {
|
|
1800
|
-
throw new Error(`frontend plan response exceeds the ${FRONTEND_PLAN_RESPONSE_MAX_BYTES}-byte limit`);
|
|
1801
|
-
}
|
|
1802
2072
|
if (!input.sourceBinding)
|
|
1803
2073
|
throw new Error("frontend plan patch validator requires DAG sourceBinding");
|
|
1804
2074
|
const skeleton = input.structuredContractOutput?.skeleton;
|
|
1805
2075
|
if (!skeleton)
|
|
1806
2076
|
throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
|
|
1807
|
-
const
|
|
2077
|
+
const committed = await readTypedEventStoreFromJsonl(path.join(input.runDir, nodeId, "plan-typed-facts.jsonl"));
|
|
2078
|
+
const patch = restorePlanPatchFromCommittedFacts(committed);
|
|
1808
2079
|
if (!isPlainObject(patch))
|
|
1809
|
-
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");
|
|
1810
2081
|
extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
|
|
1811
2082
|
const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
|
|
1812
2083
|
extractedPatchSha256 = extractedArtifact.sha256;
|
|
1813
|
-
const citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1814
|
-
const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
|
|
1815
|
-
await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
|
|
1816
2084
|
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1817
2085
|
if (protectedViolations.length > 0)
|
|
1818
2086
|
throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
|
|
1819
2087
|
const merged = applyFrontendContractMergePatch(skeleton, patch);
|
|
1820
|
-
const
|
|
2088
|
+
const symbolNormalizationActions = await dropUnresolvedVerificationSymbols({
|
|
1821
2089
|
runDir: input.runDir,
|
|
1822
2090
|
contract: merged,
|
|
1823
2091
|
});
|
|
@@ -1826,10 +2094,24 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1826
2094
|
rawContractText: serializeDeterministicJson(merged),
|
|
1827
2095
|
sourceBinding: input.sourceBinding,
|
|
1828
2096
|
});
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
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("; ")}`);
|
|
1833
2115
|
const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
|
|
1834
2116
|
await writeReport({
|
|
1835
2117
|
classification: "accepted-normalized",
|
|
@@ -1837,47 +2119,66 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1837
2119
|
normalizedContractSha256: normalizedArtifact.sha256,
|
|
1838
2120
|
normalizationActions: [
|
|
1839
2121
|
"apply-runtime-skeleton",
|
|
1840
|
-
...
|
|
1841
|
-
? ["drop-unresolved-verification-symbols"]
|
|
1842
|
-
: []),
|
|
2122
|
+
...symbolNormalizationActions,
|
|
1843
2123
|
...analysis.normalizationActions,
|
|
1844
2124
|
],
|
|
1845
2125
|
errors: [],
|
|
1846
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);
|
|
1847
2145
|
return {
|
|
1848
2146
|
ok: true,
|
|
1849
2147
|
contract: analysis.canonical,
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
sha256: normalizedArtifact.sha256,
|
|
1853
|
-
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1854
|
-
},
|
|
1855
|
-
frontendPlanOutput: {
|
|
1856
|
-
schemaVersion: 1,
|
|
1857
|
-
payloadKind: "canonical-contract",
|
|
1858
|
-
citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
|
|
1859
|
-
citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
|
|
1860
|
-
rawCandidatePath: path.join(input.runDir, rawPath),
|
|
1861
|
-
rawCandidateSha256: sha256Hex(input.text),
|
|
1862
|
-
},
|
|
1863
|
-
normalizedText: [
|
|
1864
|
-
"Frontend implementation contract compiled from the runtime skeleton.",
|
|
1865
|
-
"",
|
|
1866
|
-
"```json",
|
|
1867
|
-
serializeDeterministicJson(analysis.canonical),
|
|
1868
|
-
"```",
|
|
1869
|
-
citationsBlock,
|
|
1870
|
-
].join("\n"),
|
|
2148
|
+
artifact,
|
|
2149
|
+
normalizedText: `${formatStructuredArtifactPointer(artifact)}\n\n${planSummary}`,
|
|
1871
2150
|
};
|
|
1872
2151
|
}
|
|
1873
2152
|
catch (error) {
|
|
1874
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
|
+
});
|
|
1875
2158
|
await writeReport({
|
|
1876
|
-
classification:
|
|
2159
|
+
classification: classified.classification,
|
|
1877
2160
|
normalizationActions: [],
|
|
1878
|
-
errors:
|
|
2161
|
+
errors: classified.errors,
|
|
1879
2162
|
});
|
|
1880
|
-
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;
|
|
1881
2182
|
}
|
|
1882
2183
|
}
|
|
1883
2184
|
/**
|
|
@@ -1898,85 +2199,140 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1898
2199
|
return { ok: true, contract: analysis.canonical };
|
|
1899
2200
|
}
|
|
1900
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
|
+
});
|
|
1901
2207
|
return {
|
|
1902
2208
|
ok: false,
|
|
1903
|
-
reason
|
|
2209
|
+
reason,
|
|
2210
|
+
classification: classified.classification,
|
|
2211
|
+
errors: classified.errors,
|
|
1904
2212
|
};
|
|
1905
2213
|
}
|
|
1906
2214
|
}
|
|
1907
2215
|
/**
|
|
1908
|
-
*
|
|
1909
|
-
*
|
|
1910
|
-
*
|
|
1911
|
-
*
|
|
1912
|
-
*
|
|
1913
|
-
*
|
|
1914
|
-
*
|
|
1915
|
-
*
|
|
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.
|
|
1916
2232
|
*/
|
|
1917
|
-
export async function
|
|
1918
|
-
const
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
const
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
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) {
|
|
1928
2244
|
return {
|
|
1929
2245
|
ok: false,
|
|
1930
|
-
reason:
|
|
2246
|
+
reason: "frontend contract skeleton missing required runtime fields (riskLevel/targets.files)",
|
|
2247
|
+
failureCode: "missing-skeleton",
|
|
1931
2248
|
};
|
|
1932
2249
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
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;
|
|
1935
2275
|
try {
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
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);
|
|
1939
2287
|
}
|
|
1940
2288
|
catch (error) {
|
|
1941
2289
|
return {
|
|
1942
2290
|
ok: false,
|
|
1943
|
-
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",
|
|
1944
2293
|
};
|
|
1945
2294
|
}
|
|
1946
|
-
|
|
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) {
|
|
1947
2301
|
return {
|
|
1948
2302
|
ok: false,
|
|
1949
|
-
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,
|
|
1950
2325
|
};
|
|
1951
2326
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
2327
|
+
catch (error) {
|
|
2328
|
+
const failureKind = error instanceof FrontendContractFailure ? error.kind : undefined;
|
|
1954
2329
|
return {
|
|
1955
2330
|
ok: false,
|
|
1956
|
-
reason: `frontend
|
|
2331
|
+
reason: `frontend contract schema drift: ${error instanceof Error ? error.message : String(error)}`,
|
|
2332
|
+
failureCode: "schema-drift",
|
|
2333
|
+
...(failureKind ? { failureKind } : {}),
|
|
1957
2334
|
};
|
|
1958
2335
|
}
|
|
1959
|
-
const payloadPath = path.posix.join(candidateDir, `attempt-${attempt}.patch.json`);
|
|
1960
|
-
const payloadArtifact = await writeDeterministicJsonArtifact(input.runDir, payloadPath, patch);
|
|
1961
|
-
const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
|
|
1962
|
-
await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
|
|
1963
|
-
return {
|
|
1964
|
-
ok: true,
|
|
1965
|
-
contract: patch,
|
|
1966
|
-
structuredArtifact: {
|
|
1967
|
-
path: payloadArtifact.path,
|
|
1968
|
-
sha256: payloadArtifact.sha256,
|
|
1969
|
-
schemaId: "frontend-implementation-contract-revision-patch-v1",
|
|
1970
|
-
},
|
|
1971
|
-
frontendPlanOutput: {
|
|
1972
|
-
schemaVersion: 1,
|
|
1973
|
-
payloadKind: "merge-patch",
|
|
1974
|
-
citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
|
|
1975
|
-
citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
|
|
1976
|
-
rawCandidatePath: path.join(input.runDir, rawPath),
|
|
1977
|
-
rawCandidateSha256: sha256Hex(input.text),
|
|
1978
|
-
},
|
|
1979
|
-
};
|
|
1980
2336
|
}
|
|
1981
2337
|
export async function materializeFrontendImplementationContract(input) {
|
|
1982
2338
|
const analysis = await analyzeFrontendImplementationContract(input);
|