@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
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { readFile, stat } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
|
+
import { OPENSPEC_SPEC_EXT_RE, isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
6
|
import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
|
|
6
7
|
import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
|
|
7
|
-
import {
|
|
8
|
+
import { checkFrontendSourceFidelityLedger, } from "./frontend-source-fidelity-ledger.js";
|
|
9
|
+
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
8
10
|
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
9
11
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
10
12
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
@@ -12,7 +14,7 @@ import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
|
|
|
12
14
|
export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
|
|
13
15
|
export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
|
|
14
16
|
/** Canonical contract serialization must stay at or below this byte size. */
|
|
15
|
-
export const FRONTEND_CONTRACT_MAX_BYTES =
|
|
17
|
+
export const FRONTEND_CONTRACT_MAX_BYTES = 65536;
|
|
16
18
|
/**
|
|
17
19
|
* Machine-readable failure classification for blocked/retryable-invalid
|
|
18
20
|
* prewrite outcomes. Kept alongside the human-readable failureReason so
|
|
@@ -33,14 +35,22 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
|
|
|
33
35
|
"verification-target-outside-write-set",
|
|
34
36
|
"openspec-not-read",
|
|
35
37
|
"candidate-missing-drift",
|
|
36
|
-
"
|
|
37
|
-
"openspec-
|
|
38
|
-
"openspec-citation-not-read",
|
|
38
|
+
"candidate-content-drift",
|
|
39
|
+
"openspec-selection-invalid",
|
|
39
40
|
"component-spec-reference-invalid",
|
|
40
41
|
"component-spec-not-cited",
|
|
42
|
+
"component-spec-choice-unbound",
|
|
41
43
|
"component-choices-missing",
|
|
42
44
|
"contract-too-large",
|
|
43
45
|
"revision-patch-unmergeable",
|
|
46
|
+
// Source fidelity ledger 类目(SOURCE_FIDELITY_*/source-* 稳定错误码)。
|
|
47
|
+
"source-fidelity-ledger-missing",
|
|
48
|
+
"source-fidelity-ledger-stale",
|
|
49
|
+
"source-fidelity-unresolved",
|
|
50
|
+
"source-fidelity-conflict",
|
|
51
|
+
"source-fidelity-high-risk-uncovered",
|
|
52
|
+
"source-fidelity-requirement-not-covered",
|
|
53
|
+
"source-fidelity-source-ref-invalid",
|
|
44
54
|
]);
|
|
45
55
|
export const frontendPrewriteResultV1Schema = z
|
|
46
56
|
.object({
|
|
@@ -128,7 +138,7 @@ export const frontendPrewriteResultV1Schema = z
|
|
|
128
138
|
export const frontendWriterAdmissionSchema = z
|
|
129
139
|
.object({
|
|
130
140
|
schemaVersion: z.literal(1),
|
|
131
|
-
writerNodeId: z.enum(["frontend-implement-pi"
|
|
141
|
+
writerNodeId: z.enum(["frontend-implement-pi"]),
|
|
132
142
|
decision: z.enum(["authorized", "denied"]),
|
|
133
143
|
sourceArtifact: z.string().min(1),
|
|
134
144
|
artifactHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
@@ -157,136 +167,68 @@ async function selectNode(runDir, primary, fallbacks) {
|
|
|
157
167
|
}
|
|
158
168
|
throw new Error(`frontend prewrite gate no FINISHED candidate: ${rejections.join("; ")}`);
|
|
159
169
|
}
|
|
160
|
-
async function
|
|
161
|
-
|
|
170
|
+
async function readFinishedNodeRecord(runDir, nodeId) {
|
|
171
|
+
let record;
|
|
172
|
+
try {
|
|
173
|
+
record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
throw new Error(`frontend prewrite gate cannot parse node record ${nodeId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
177
|
+
}
|
|
162
178
|
if (record.status !== "FINISHED") {
|
|
163
179
|
throw new Error(`frontend prewrite gate requires FINISHED node ${nodeId} (got ${String(record.status)})`);
|
|
164
180
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// canonical plan appear as two complete JSON contracts to the prewrite gate.
|
|
181
|
+
return record;
|
|
182
|
+
}
|
|
183
|
+
async function readNodeText(runDir, nodeId) {
|
|
184
|
+
const record = await readFinishedNodeRecord(runDir, nodeId);
|
|
170
185
|
const text = record.assistantText?.trim() || record.stdout?.trim() || "";
|
|
171
186
|
if (!text)
|
|
172
187
|
throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
|
|
173
188
|
return text;
|
|
174
189
|
}
|
|
175
|
-
async function
|
|
176
|
-
const record =
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
function resolveRunOwnedArtifact(runDir, candidate, label) {
|
|
180
|
-
const root = path.resolve(runDir);
|
|
181
|
-
const resolved = path.resolve(candidate);
|
|
182
|
-
const relative = path.relative(root, resolved);
|
|
183
|
-
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
184
|
-
throw new Error(`frontend prewrite gate ${label} must be contained by the current run directory`);
|
|
185
|
-
}
|
|
186
|
-
return resolved;
|
|
187
|
-
}
|
|
188
|
-
async function readHashBoundArtifact(input) {
|
|
189
|
-
const resolved = resolveRunOwnedArtifact(input.runDir, input.artifactPath, input.label);
|
|
190
|
-
const text = await readFile(resolved, "utf8");
|
|
191
|
-
const actual = sha256Hex(text);
|
|
192
|
-
if (actual !== input.expectedSha256) {
|
|
193
|
-
throw new Error(`frontend prewrite gate ${input.label} SHA-256 mismatch (expected ${input.expectedSha256}, got ${actual})`);
|
|
190
|
+
async function readCanonicalContractFromNode(runDir, nodeId) {
|
|
191
|
+
const record = await readFinishedNodeRecord(runDir, nodeId);
|
|
192
|
+
if (!record.structuredArtifactPath) {
|
|
193
|
+
throw new Error(`frontend prewrite gate missing structured artifact for ${nodeId}`);
|
|
194
194
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
const artifactPath = path.isAbsolute(record.structuredArtifactPath)
|
|
196
|
+
? record.structuredArtifactPath
|
|
197
|
+
: path.join(runDir, record.structuredArtifactPath);
|
|
198
|
+
const resolvedRun = path.resolve(runDir);
|
|
199
|
+
const resolvedArtifact = path.resolve(artifactPath);
|
|
200
|
+
const relative = path.relative(resolvedRun, resolvedArtifact);
|
|
201
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
202
|
+
throw new Error(`frontend prewrite gate structured artifact escapes runDir: ${record.structuredArtifactPath}`);
|
|
201
203
|
}
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return {
|
|
208
|
-
artifactFirst: false,
|
|
209
|
-
payloadText,
|
|
210
|
-
citationsText: payloadText,
|
|
211
|
-
rawCandidateSha256: sha256Hex(record.assistantText ?? record.stdout ?? ""),
|
|
212
|
-
payloadKind: "canonical-contract",
|
|
213
|
-
};
|
|
204
|
+
const bytes = await readFile(resolvedArtifact);
|
|
205
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
206
|
+
if (record.structuredArtifactSha256 &&
|
|
207
|
+
record.structuredArtifactSha256 !== digest) {
|
|
208
|
+
throw new Error(`frontend prewrite gate structured artifact hash mismatch for ${nodeId}`);
|
|
214
209
|
}
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
!record.structuredArtifactSchemaId) {
|
|
219
|
-
throw new Error(`frontend prewrite gate ${nodeId} has incomplete artifact-first metadata`);
|
|
210
|
+
if (record.structuredArtifactSchemaId &&
|
|
211
|
+
record.structuredArtifactSchemaId !== FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID) {
|
|
212
|
+
throw new Error(`frontend prewrite gate structured artifact schema mismatch for ${nodeId}: ${record.structuredArtifactSchemaId}`);
|
|
220
213
|
}
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
214
|
+
const contractText = bytes.toString("utf8");
|
|
215
|
+
let citationsText = "";
|
|
216
|
+
try {
|
|
217
|
+
const citationsRaw = await readFile(path.join(path.dirname(resolvedArtifact), "openspec-citations.json"), "utf8");
|
|
218
|
+
try {
|
|
219
|
+
const parsed = JSON.parse(citationsRaw);
|
|
220
|
+
citationsText =
|
|
221
|
+
typeof parsed.block === "string" ? parsed.block : citationsRaw;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
citationsText = citationsRaw;
|
|
225
|
+
}
|
|
226
226
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
artifactPath: record.structuredArtifactPath,
|
|
231
|
-
expectedSha256: record.structuredArtifactSha256,
|
|
232
|
-
label: `${nodeId} primary payload`,
|
|
233
|
-
}),
|
|
234
|
-
readHashBoundArtifact({
|
|
235
|
-
runDir,
|
|
236
|
-
artifactPath: frontend.citationsArtifactPath,
|
|
237
|
-
expectedSha256: frontend.citationsArtifactSha256,
|
|
238
|
-
label: `${nodeId} citations`,
|
|
239
|
-
}),
|
|
240
|
-
readHashBoundArtifact({
|
|
241
|
-
runDir,
|
|
242
|
-
artifactPath: frontend.rawCandidatePath,
|
|
243
|
-
expectedSha256: frontend.rawCandidateSha256,
|
|
244
|
-
label: `${nodeId} raw candidate`,
|
|
245
|
-
}).then(() => undefined),
|
|
246
|
-
]);
|
|
247
|
-
return {
|
|
248
|
-
artifactFirst: true,
|
|
249
|
-
payloadText,
|
|
250
|
-
citationsText,
|
|
251
|
-
rawCandidateSha256: frontend.rawCandidateSha256,
|
|
252
|
-
payloadKind: frontend.payloadKind,
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* Same shape predicate as the internal `isContract` inside
|
|
257
|
-
* `extractFrontendImplementationJson` (frontend-implementation-contract.ts):
|
|
258
|
-
* schemaVersion 1 + a plain `targets` object + `requirements` and
|
|
259
|
-
* `verificationTargets` arrays. A merge-patch delta does not carry these
|
|
260
|
-
* fields, so this is the deterministic boundary between a full contract and a
|
|
261
|
-
* patch. Keep the two predicates in sync.
|
|
262
|
-
*/
|
|
263
|
-
function isFullContractShape(value) {
|
|
264
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
265
|
-
return false;
|
|
227
|
+
catch (error) {
|
|
228
|
+
if (error.code !== "ENOENT")
|
|
229
|
+
throw error;
|
|
266
230
|
}
|
|
267
|
-
|
|
268
|
-
const targets = record.targets;
|
|
269
|
-
return (record.schemaVersion === 1 &&
|
|
270
|
-
targets !== null &&
|
|
271
|
-
typeof targets === "object" &&
|
|
272
|
-
!Array.isArray(targets) &&
|
|
273
|
-
Array.isArray(record.requirements) &&
|
|
274
|
-
Array.isArray(record.verificationTargets));
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Shared merge pipeline for revisionPatch mode and content-driven
|
|
278
|
-
* compatibility: read the original contract node, extract the original
|
|
279
|
-
* contract and the effective node's patch, apply the RFC 7386 merge-patch, and
|
|
280
|
-
* serialize deterministically. Throws on any failure; callers map the error to
|
|
281
|
-
* their own classification (retryable-invalid for explicit revisionPatch mode,
|
|
282
|
-
* blocked for content-driven fallback).
|
|
283
|
-
*/
|
|
284
|
-
async function mergeRevisionPatch(runDir, planText, originalNodeId) {
|
|
285
|
-
const originalText = await readNodeText(runDir, originalNodeId);
|
|
286
|
-
const originalValue = extractFrontendImplementationJson(originalText);
|
|
287
|
-
const patchValue = extractFrontendImplementationJson(planText);
|
|
288
|
-
const merged = applyFrontendImplementationContractPatch(originalValue, patchValue);
|
|
289
|
-
return serializeDeterministicJson(merged);
|
|
231
|
+
return { contractText, citationsText, rawSha256: digest };
|
|
290
232
|
}
|
|
291
233
|
function firstNonEmptyVerdictLine(text) {
|
|
292
234
|
const lines = text
|
|
@@ -330,6 +272,7 @@ function isFailedToolResult(event) {
|
|
|
330
272
|
}
|
|
331
273
|
async function collectOpenspecReadPaths(input) {
|
|
332
274
|
const { runDir, planNodeId, reviewNodeId, repoRoot } = input;
|
|
275
|
+
const candidateSet = input.candidatePaths ? new Set(input.candidatePaths) : undefined;
|
|
333
276
|
const matched = new Set();
|
|
334
277
|
for (const nodeId of [planNodeId, reviewNodeId]) {
|
|
335
278
|
const eventsPath = path.join(runDir, nodeId, "session-events.jsonl");
|
|
@@ -364,7 +307,7 @@ async function collectOpenspecReadPaths(input) {
|
|
|
364
307
|
: null;
|
|
365
308
|
if (relativePath &&
|
|
366
309
|
!isFailedToolResult(event) &&
|
|
367
|
-
|
|
310
|
+
(candidateSet ? candidateSet.has(relativePath) : OPENSPEC_SPEC_EXT_RE.test(relativePath))) {
|
|
368
311
|
matched.add(relativePath);
|
|
369
312
|
}
|
|
370
313
|
startedReads.delete(event.toolCallId);
|
|
@@ -379,21 +322,6 @@ async function collectOpenspecReadPaths(input) {
|
|
|
379
322
|
}
|
|
380
323
|
return [...matched];
|
|
381
324
|
}
|
|
382
|
-
async function checkOpenspecReadEvidence(input) {
|
|
383
|
-
const { runDir, candidatePaths, planNodeId, reviewNodeId, repoRoot } = input;
|
|
384
|
-
if (candidatePaths.length === 0)
|
|
385
|
-
return [];
|
|
386
|
-
const allReadPaths = await collectOpenspecReadPaths({
|
|
387
|
-
runDir,
|
|
388
|
-
planNodeId,
|
|
389
|
-
reviewNodeId,
|
|
390
|
-
repoRoot,
|
|
391
|
-
});
|
|
392
|
-
const normalizedCandidates = new Set(candidatePaths
|
|
393
|
-
.map((candidate) => toRepoRelativePath(candidate, repoRoot))
|
|
394
|
-
.filter((candidate) => Boolean(candidate && isOpenspecSpecFilePath(candidate))));
|
|
395
|
-
return allReadPaths.filter((readPath) => normalizedCandidates.has(readPath));
|
|
396
|
-
}
|
|
397
325
|
function normalizeCitationPath(rawPath, repoRoot) {
|
|
398
326
|
let candidate = rawPath.trim();
|
|
399
327
|
const hashIndex = candidate.indexOf("#");
|
|
@@ -408,57 +336,132 @@ function normalizeCitationPath(rawPath, repoRoot) {
|
|
|
408
336
|
}
|
|
409
337
|
return candidate;
|
|
410
338
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
339
|
+
async function resolveRequiredOpenspecPaths(input) {
|
|
340
|
+
if (input.policy === "scan-strict")
|
|
341
|
+
return { ok: true, paths: input.candidatePaths };
|
|
342
|
+
// Legacy cited DAGs had no selector: preserve their full candidate must-read
|
|
343
|
+
// semantics instead of silently weakening an existing run.
|
|
344
|
+
if (!input.selectionNodeId)
|
|
345
|
+
return { ok: true, paths: input.candidatePaths };
|
|
346
|
+
// OpenSpec classifications arrive as individual typed `openspec-selection`
|
|
347
|
+
// facts (one path per record_openspec_selection call) so a large candidate
|
|
348
|
+
// set never exceeds a single model output budget. Only positive
|
|
349
|
+
// classifications (required | relevant) are legal facts; candidates that
|
|
350
|
+
// were never mentioned default to irrelevant. Explicit task declarations /
|
|
351
|
+
// source citations (`mandatoryPaths`) are never downgraded: they are
|
|
352
|
+
// always required and always must-read, whether or not the model declared
|
|
353
|
+
// them. Fall back to the legacy single fenced JSON for older runs (which
|
|
354
|
+
// may still carry explicit irrelevant rows).
|
|
355
|
+
let selections;
|
|
356
|
+
let allowIrrelevant = false;
|
|
357
|
+
const selectionNodeId = input.selectionNodeId; // guarded above: no selector -> full candidate semantics
|
|
358
|
+
try {
|
|
359
|
+
const facts = await readCommittedContractFacts(input.runDir, selectionNodeId);
|
|
360
|
+
selections = facts
|
|
361
|
+
.filter((fact) => fact.kind === "openspec-selection")
|
|
362
|
+
.map((fact) => ({
|
|
363
|
+
path: String(fact.path ?? ""),
|
|
364
|
+
disposition: String(fact.disposition ?? ""),
|
|
365
|
+
rationale: String(fact.rationale ?? ""),
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
selections = [];
|
|
370
|
+
}
|
|
371
|
+
if (selections.length === 0) {
|
|
372
|
+
const legacy = await readLegacyOpenspecSelection({
|
|
373
|
+
runDir: input.runDir,
|
|
374
|
+
selectionNodeId,
|
|
375
|
+
candidatePaths: input.candidatePaths,
|
|
376
|
+
mandatoryPaths: input.mandatoryPaths,
|
|
377
|
+
});
|
|
378
|
+
if (!legacy.ok)
|
|
379
|
+
return legacy;
|
|
380
|
+
selections = legacy.selections;
|
|
381
|
+
allowIrrelevant = true;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
const byPath = new Map();
|
|
385
|
+
for (const row of selections) {
|
|
386
|
+
const legalDispositions = allowIrrelevant
|
|
387
|
+
? ["required", "relevant", "irrelevant"]
|
|
388
|
+
: ["required", "relevant"];
|
|
389
|
+
if (typeof row.path !== "string" ||
|
|
390
|
+
!legalDispositions.includes(row.disposition) ||
|
|
391
|
+
typeof row.rationale !== "string" ||
|
|
392
|
+
!row.rationale.trim()) {
|
|
393
|
+
throw new Error(`invalid selection row (path+${legalDispositions.join("|")}+non-empty rationale expected): ${JSON.stringify(row)}`);
|
|
442
394
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
unparseable = true;
|
|
446
|
-
continue;
|
|
395
|
+
if (!input.candidatePaths.includes(row.path)) {
|
|
396
|
+
throw new Error(`invalid selection ${row.path}: not a candidate path`);
|
|
447
397
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
398
|
+
if (byPath.has(row.path))
|
|
399
|
+
throw new Error(`duplicate selection ${row.path}`);
|
|
400
|
+
byPath.set(row.path, row.disposition);
|
|
401
|
+
}
|
|
402
|
+
// Only explicitly required/relevant candidates must be read; everything
|
|
403
|
+
// unmentioned defaults to irrelevant. mandatoryPaths are intrinsic:
|
|
404
|
+
// they must be read even if the model omitted them, and they can never
|
|
405
|
+
// be downgraded by a declaration.
|
|
406
|
+
const mustRead = [
|
|
407
|
+
...input.mandatoryPaths,
|
|
408
|
+
...input.candidatePaths.filter((candidate) => byPath.get(candidate) === "required"),
|
|
409
|
+
];
|
|
410
|
+
return { ok: true, paths: [...new Set(mustRead)].sort() };
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
return {
|
|
414
|
+
ok: false,
|
|
415
|
+
reason: `openspec selector invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async function readCommittedContractFacts(runDir, nodeId) {
|
|
420
|
+
const { readTypedEventStoreFromJsonl } = await import("./frontend-typed-event-store.js");
|
|
421
|
+
const records = await readTypedEventStoreFromJsonl(path.join(runDir, nodeId, "contract-typed-facts.jsonl"));
|
|
422
|
+
return records
|
|
423
|
+
.filter((record) => record.phase === "committed")
|
|
424
|
+
.map((record) => record.fact)
|
|
425
|
+
.filter((fact) => Boolean(fact));
|
|
426
|
+
}
|
|
427
|
+
async function readLegacyOpenspecSelection(input) {
|
|
428
|
+
let text;
|
|
429
|
+
try {
|
|
430
|
+
text = await readNodeText(input.runDir, input.selectionNodeId);
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
reason: `openspec selector output unreadable: ${error instanceof Error ? error.message : String(error)}`,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const fenced = /```json\s*\n([\s\S]*?)\n```/.exec(text);
|
|
439
|
+
if (!fenced) {
|
|
440
|
+
return {
|
|
441
|
+
ok: false,
|
|
442
|
+
reason: "openspec selector emitted neither typed openspec-selection facts nor a legacy fenced JSON selection object",
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
const parsed = JSON.parse(fenced[1] ?? "");
|
|
447
|
+
if (parsed.schemaVersion !== 1 ||
|
|
448
|
+
parsed.schemaId !== "frontend-openspec-selection-v1" ||
|
|
449
|
+
!Array.isArray(parsed.selections)) {
|
|
450
|
+
throw new Error("schemaVersion/schemaId/selections invalid");
|
|
459
451
|
}
|
|
452
|
+
const rows = parsed.selections.map((row) => ({
|
|
453
|
+
path: String(row.path ?? ""),
|
|
454
|
+
disposition: String(row.disposition ?? ""),
|
|
455
|
+
rationale: String(row.rationale ?? ""),
|
|
456
|
+
}));
|
|
457
|
+
return { ok: true, selections: rows };
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
return {
|
|
461
|
+
ok: false,
|
|
462
|
+
reason: `openspec selector invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
463
|
+
};
|
|
460
464
|
}
|
|
461
|
-
return { blockPresent, unparseable, citations };
|
|
462
465
|
}
|
|
463
466
|
async function finalizePrewrite(input, pending) {
|
|
464
467
|
const sourceBindingSha256 = input.sourceBinding
|
|
@@ -509,13 +512,37 @@ async function finalizePrewrite(input, pending) {
|
|
|
509
512
|
openspecPolicy: pending.openspecPolicy,
|
|
510
513
|
};
|
|
511
514
|
}
|
|
515
|
+
export { checkFrontendSourceFidelityLedger } from "./frontend-source-fidelity-ledger.js";
|
|
512
516
|
export async function runFrontendPrewriteGate(input) {
|
|
513
517
|
const workspaceRoot = input.workspaceRoot ?? input.repoRoot;
|
|
514
518
|
// selectNode throws for a scheduling defect (no FINISHED plan/review). That
|
|
515
519
|
// remains an ERROR and is intentionally not classified into prewrite-result.
|
|
516
520
|
const planNodeId = await selectNode(input.runDir, input.config.planFromNodeId, input.config.planFallbackFromNodeIds);
|
|
517
521
|
const reviewNodeId = await selectNode(input.runDir, input.config.reviewFromNodeId, input.config.reviewFallbackFromNodeIds);
|
|
518
|
-
|
|
522
|
+
let planArtifact;
|
|
523
|
+
try {
|
|
524
|
+
planArtifact = await readCanonicalContractFromNode(input.runDir, planNodeId);
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
return finalizePrewrite(input, {
|
|
528
|
+
planNodeId,
|
|
529
|
+
reviewNodeId,
|
|
530
|
+
verdict: "",
|
|
531
|
+
candidateRawSha256: sha256Hex(""),
|
|
532
|
+
candidateJsonSha256: null,
|
|
533
|
+
canonicalSha256: null,
|
|
534
|
+
normalizationActions: [],
|
|
535
|
+
mockStrategy: undefined,
|
|
536
|
+
artifact: undefined,
|
|
537
|
+
failureReason: error instanceof Error ? error.message : String(error),
|
|
538
|
+
failureCode: "plan-text-unreadable",
|
|
539
|
+
classification: "retryable-invalid",
|
|
540
|
+
openspecReadPaths: [],
|
|
541
|
+
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
542
|
+
openspecPolicy: input.config.openspecPolicy ?? "scan-strict",
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
const candidateRawSha256 = planArtifact.rawSha256;
|
|
519
546
|
const openspecPolicy = input.config.openspecPolicy ?? "scan-strict";
|
|
520
547
|
const basePending = {
|
|
521
548
|
planNodeId,
|
|
@@ -534,21 +561,6 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
534
561
|
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
535
562
|
openspecPolicy,
|
|
536
563
|
};
|
|
537
|
-
let planInput;
|
|
538
|
-
try {
|
|
539
|
-
planInput = await readFrontendPlanNodeInput(input.runDir, planNodeId);
|
|
540
|
-
}
|
|
541
|
-
catch (error) {
|
|
542
|
-
return finalizePrewrite(input, {
|
|
543
|
-
...basePending,
|
|
544
|
-
classification: "retryable-invalid",
|
|
545
|
-
failureReason: error instanceof Error ? error.message : String(error),
|
|
546
|
-
failureCode: "plan-text-unreadable",
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
const planText = planInput.payloadText;
|
|
550
|
-
const citationsText = planInput.citationsText;
|
|
551
|
-
basePending.candidateRawSha256 = planInput.rawCandidateSha256;
|
|
552
564
|
let reviewText;
|
|
553
565
|
try {
|
|
554
566
|
reviewText = await readNodeText(input.runDir, reviewNodeId);
|
|
@@ -571,90 +583,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
571
583
|
failureCode: "verdict-not-pass",
|
|
572
584
|
});
|
|
573
585
|
}
|
|
574
|
-
|
|
575
|
-
// delta against the original contract node (planFallbackFromNodeIds[0]).
|
|
576
|
-
// Legacy DAGs without revisionPatch (undefined) keep full-contract semantics.
|
|
577
|
-
const revisionPatchMode = input.config.revisionPatch === true &&
|
|
578
|
-
planNodeId === input.config.planFromNodeId &&
|
|
579
|
-
input.config.planFallbackFromNodeIds.length > 0;
|
|
580
|
-
let effectiveContractText = null;
|
|
581
|
-
if (revisionPatchMode) {
|
|
582
|
-
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
583
|
-
try {
|
|
584
|
-
const originalInput = await readFrontendPlanNodeInput(input.runDir, originalNodeId);
|
|
585
|
-
if (planInput.artifactFirst && !originalInput.artifactFirst) {
|
|
586
|
-
throw new Error(`artifact-first revision ${planNodeId} requires artifact-first original contract ${originalNodeId}`);
|
|
587
|
-
}
|
|
588
|
-
if (planInput.artifactFirst) {
|
|
589
|
-
if (planInput.payloadKind !== "merge-patch" ||
|
|
590
|
-
originalInput.payloadKind !== "canonical-contract") {
|
|
591
|
-
throw new Error("artifact-first revision/original payload kinds are invalid");
|
|
592
|
-
}
|
|
593
|
-
effectiveContractText = serializeDeterministicJson(applyFrontendImplementationContractPatch(JSON.parse(originalInput.payloadText), JSON.parse(planInput.payloadText)));
|
|
594
|
-
}
|
|
595
|
-
else {
|
|
596
|
-
effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
catch (error) {
|
|
600
|
-
return finalizePrewrite(input, {
|
|
601
|
-
...basePending,
|
|
602
|
-
verdict,
|
|
603
|
-
classification: "retryable-invalid",
|
|
604
|
-
failureReason: `frontend prewrite gate revisionPatch mode requires original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
605
|
-
failureCode: "contract-invalid",
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
// Content-driven compatibility (Bug 1): legacy DAGs without
|
|
611
|
-
// revisionPatch can still receive a merge-patch delta from the revision
|
|
612
|
-
// node. Detect the output shape instead of trusting the DAG flag alone.
|
|
613
|
-
// - Full contract shape → keep legacy full-contract semantics unchanged.
|
|
614
|
-
// - Patch shape with a usable original contract node → auto-merge and
|
|
615
|
-
// run the existing analysis/materialization pipeline.
|
|
616
|
-
// - Patch shape but unmergeable → blocked + revision-patch-unmergeable
|
|
617
|
-
// (no retryable loop).
|
|
618
|
-
let planJsonValue = null;
|
|
619
|
-
try {
|
|
620
|
-
planJsonValue = extractFrontendImplementationJson(planText);
|
|
621
|
-
}
|
|
622
|
-
catch {
|
|
623
|
-
// Not machine-readable JSON: fall back to the legacy full-contract
|
|
624
|
-
// path where analyzeFrontendImplementationContract surfaces the
|
|
625
|
-
// original diagnostic (fail-closed without a new failure mode).
|
|
626
|
-
}
|
|
627
|
-
if (planJsonValue !== null && !isFullContractShape(planJsonValue)) {
|
|
628
|
-
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
629
|
-
if (!originalNodeId || originalNodeId === planNodeId) {
|
|
630
|
-
return finalizePrewrite(input, {
|
|
631
|
-
...basePending,
|
|
632
|
-
verdict,
|
|
633
|
-
classification: "blocked",
|
|
634
|
-
failureReason: "frontend prewrite gate: the effective plan output is a merge-patch delta but no distinct original contract node is available. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.",
|
|
635
|
-
failureCode: "revision-patch-unmergeable",
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
try {
|
|
639
|
-
const originalText = await readNodeText(input.runDir, originalNodeId);
|
|
640
|
-
const originalValue = extractFrontendImplementationJson(originalText);
|
|
641
|
-
if (!isFullContractShape(originalValue)) {
|
|
642
|
-
throw new Error(`original contract node ${originalNodeId} is not a full frontend implementation contract`);
|
|
643
|
-
}
|
|
644
|
-
effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
|
|
645
|
-
}
|
|
646
|
-
catch (error) {
|
|
647
|
-
return finalizePrewrite(input, {
|
|
648
|
-
...basePending,
|
|
649
|
-
verdict,
|
|
650
|
-
classification: "blocked",
|
|
651
|
-
failureReason: `frontend prewrite gate: the effective plan output is a merge-patch delta but it cannot be merged onto the original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.`,
|
|
652
|
-
failureCode: "revision-patch-unmergeable",
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
const requirementIdSearchText = effectiveContractText ?? planText;
|
|
586
|
+
const requirementIdSearchText = planArtifact.contractText;
|
|
658
587
|
const missingIds = input.config.requiredRequirementIds.filter((id) => !requirementIdSearchText.includes(id));
|
|
659
588
|
if (missingIds.length > 0) {
|
|
660
589
|
return finalizePrewrite(input, {
|
|
@@ -702,17 +631,13 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
702
631
|
workspaceRoot,
|
|
703
632
|
});
|
|
704
633
|
}
|
|
705
|
-
// Candidate -> canonical conversion.
|
|
706
|
-
//
|
|
707
|
-
// here would silently fall back to its prose and defeat the integrity gate.
|
|
708
|
-
// Historical records intentionally retain their node-text compatibility path.
|
|
634
|
+
// Candidate -> canonical conversion. The gate consumes the producing node's
|
|
635
|
+
// hash-bound canonical artifact and never re-merges plan/revision stdout.
|
|
709
636
|
let analysis;
|
|
710
637
|
try {
|
|
711
|
-
const rawContractText = effectiveContractText ?? (planInput.artifactFirst ? planText : undefined);
|
|
712
638
|
analysis = await analyzeFrontendImplementationContract({
|
|
713
639
|
runDir: input.runDir,
|
|
714
|
-
|
|
715
|
-
rawContractText,
|
|
640
|
+
rawContractText: planArtifact.contractText,
|
|
716
641
|
sourceBinding: input.sourceBinding,
|
|
717
642
|
});
|
|
718
643
|
}
|
|
@@ -732,6 +657,33 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
732
657
|
}
|
|
733
658
|
throw error;
|
|
734
659
|
}
|
|
660
|
+
// Source fidelity ledger gate (20260824-frontend-source-fidelity-ledger-v1):
|
|
661
|
+
// 在 verdict/missing-requirement-ids/source-binding-not-fresh 之后插入 ledger
|
|
662
|
+
// 检查。缺失即 fail closed 且 writer 不获授权;所有检查通过才继续物化契约。
|
|
663
|
+
if (input.sourceBinding) {
|
|
664
|
+
const ledgerCheck = await checkFrontendSourceFidelityLedger({
|
|
665
|
+
workspaceRoot: workspaceRoot ?? "",
|
|
666
|
+
binding: input.sourceBinding,
|
|
667
|
+
contractRequirements: analysis.canonical.requirements.map((requirement) => ({
|
|
668
|
+
id: requirement.id,
|
|
669
|
+
sourceFragmentIds: requirement.sourceFragmentIds,
|
|
670
|
+
sourceRefs: requirement.sourceRefs,
|
|
671
|
+
})),
|
|
672
|
+
});
|
|
673
|
+
if (!ledgerCheck.ok && ledgerCheck.failureCode) {
|
|
674
|
+
return finalizePrewrite(input, {
|
|
675
|
+
...basePending,
|
|
676
|
+
verdict,
|
|
677
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
678
|
+
normalizationActions: [],
|
|
679
|
+
mockStrategy: analysis.canonical.mockApi.strategy,
|
|
680
|
+
classification: "blocked",
|
|
681
|
+
failureReason: ledgerCheck.failureReason ??
|
|
682
|
+
"frontend source fidelity ledger gate failed",
|
|
683
|
+
failureCode: ledgerCheck.failureCode,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
735
687
|
// Size guard: a canonical contract above 64KB is a retryable-invalid defect
|
|
736
688
|
// (compress the contract) rather than a writer authorization failure.
|
|
737
689
|
const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8");
|
|
@@ -818,14 +770,31 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
818
770
|
// Effective plan/review must actually read the candidate openspec paths when
|
|
819
771
|
// any exist. This fail-closes BEFORE the canonical contract is materialized
|
|
820
772
|
// so a writer is never authorized on top of unread normative evidence.
|
|
821
|
-
const
|
|
773
|
+
const frozenCandidatePaths = input.config.openspecCandidatePaths ?? [];
|
|
774
|
+
const selection = await resolveRequiredOpenspecPaths({
|
|
775
|
+
runDir: input.runDir,
|
|
776
|
+
selectionNodeId: input.config.openspecSelectionNodeId,
|
|
777
|
+
candidatePaths: frozenCandidatePaths,
|
|
778
|
+
mandatoryPaths: input.config.openspecMandatoryPaths ?? [],
|
|
779
|
+
policy: openspecPolicy,
|
|
780
|
+
});
|
|
781
|
+
if (!selection.ok) {
|
|
782
|
+
return finalizePrewrite(input, {
|
|
783
|
+
...basePending, verdict, candidateJsonSha256: analysis.candidateJsonSha256,
|
|
784
|
+
normalizationActions: [], mockStrategy, classification: "retryable-invalid",
|
|
785
|
+
failureReason: `frontend prewrite gate: ${selection.reason}`,
|
|
786
|
+
failureCode: "openspec-selection-invalid",
|
|
787
|
+
openspecCandidatePaths: frozenCandidatePaths,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
const candidatePaths = selection.paths;
|
|
822
791
|
const candidateRoot = workspaceRoot ?? process.cwd();
|
|
823
792
|
// Drift precheck: candidates are frozen at generation time. A candidate that
|
|
824
793
|
// no longer exists on disk was deleted/renamed after generation and is a
|
|
825
794
|
// generation-contract defect, not an "unread" failure.
|
|
826
795
|
const missingCandidates = [];
|
|
827
|
-
if (
|
|
828
|
-
for (const candidate of
|
|
796
|
+
if (frozenCandidatePaths.length > 0) {
|
|
797
|
+
for (const candidate of frozenCandidatePaths) {
|
|
829
798
|
try {
|
|
830
799
|
await stat(path.join(candidateRoot, candidate));
|
|
831
800
|
}
|
|
@@ -848,99 +817,78 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
848
817
|
openspecCandidatePaths: candidatePaths,
|
|
849
818
|
});
|
|
850
819
|
}
|
|
851
|
-
|
|
852
|
-
if (
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
if (!
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
...new Set(parsed.citations
|
|
896
|
-
.filter((citation) => !readSet.has(citation.path))
|
|
897
|
-
.map((citation) => citation.path)),
|
|
898
|
-
];
|
|
899
|
-
if (uncorroborated.length > 0) {
|
|
900
|
-
return finalizePrewrite(input, {
|
|
901
|
-
...basePending,
|
|
902
|
-
verdict,
|
|
903
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
904
|
-
normalizationActions: [],
|
|
905
|
-
mockStrategy,
|
|
906
|
-
classification: "retryable-invalid",
|
|
907
|
-
failureReason: `frontend prewrite gate: these openspec paths were cited in the plan but have no successful read event in the effective plan/review: ${uncorroborated.join(", ")}`,
|
|
908
|
-
failureCode: "openspec-citation-not-read",
|
|
909
|
-
openspecReadPaths,
|
|
910
|
-
openspecCandidatePaths: candidatePaths,
|
|
911
|
-
});
|
|
912
|
-
}
|
|
820
|
+
const frozenSnapshots = input.config.openspecCandidateSnapshots;
|
|
821
|
+
if (frozenSnapshots) {
|
|
822
|
+
const snapshotByPath = new Map(frozenSnapshots.map((snapshot) => [snapshot.path, snapshot.sha256]));
|
|
823
|
+
const incompleteSnapshots = frozenCandidatePaths.filter((candidate) => !snapshotByPath.has(candidate));
|
|
824
|
+
const unexpectedSnapshots = frozenSnapshots
|
|
825
|
+
.map((snapshot) => snapshot.path)
|
|
826
|
+
.filter((candidate) => !frozenCandidatePaths.includes(candidate));
|
|
827
|
+
const changedCandidates = [];
|
|
828
|
+
for (const candidate of frozenCandidatePaths) {
|
|
829
|
+
const expected = snapshotByPath.get(candidate);
|
|
830
|
+
if (!expected)
|
|
831
|
+
continue;
|
|
832
|
+
const actual = createHash("sha256")
|
|
833
|
+
.update(await readFile(path.join(candidateRoot, candidate)))
|
|
834
|
+
.digest("hex");
|
|
835
|
+
if (actual !== expected)
|
|
836
|
+
changedCandidates.push(candidate);
|
|
837
|
+
}
|
|
838
|
+
if (incompleteSnapshots.length > 0 ||
|
|
839
|
+
unexpectedSnapshots.length > 0 ||
|
|
840
|
+
changedCandidates.length > 0) {
|
|
841
|
+
const details = [
|
|
842
|
+
...(incompleteSnapshots.length > 0
|
|
843
|
+
? [`missing snapshots: ${incompleteSnapshots.join(", ")}`]
|
|
844
|
+
: []),
|
|
845
|
+
...(unexpectedSnapshots.length > 0
|
|
846
|
+
? [`unexpected snapshots: ${unexpectedSnapshots.join(", ")}`]
|
|
847
|
+
: []),
|
|
848
|
+
...(changedCandidates.length > 0
|
|
849
|
+
? [`content changed: ${changedCandidates.join(", ")}`]
|
|
850
|
+
: []),
|
|
851
|
+
].join("; ");
|
|
852
|
+
return finalizePrewrite(input, {
|
|
853
|
+
...basePending,
|
|
854
|
+
verdict,
|
|
855
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
856
|
+
normalizationActions: [],
|
|
857
|
+
mockStrategy,
|
|
858
|
+
classification: "blocked",
|
|
859
|
+
failureReason: `frontend prewrite gate blocked: the generation-frozen OpenSpec content snapshot no longer matches the workspace (${details}). Re-read the changed specification and regenerate the DAG.`,
|
|
860
|
+
failureCode: "candidate-content-drift",
|
|
861
|
+
openspecReadPaths: [],
|
|
862
|
+
openspecCandidatePaths: candidatePaths,
|
|
863
|
+
});
|
|
913
864
|
}
|
|
914
|
-
// cited with empty candidates: no read enforcement. The generation step
|
|
915
|
-
// emits an advisory instead of failing the gate.
|
|
916
865
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
}
|
|
866
|
+
let openspecReadPaths;
|
|
867
|
+
openspecReadPaths = await collectOpenspecReadPaths({
|
|
868
|
+
runDir: input.runDir,
|
|
869
|
+
planNodeId,
|
|
870
|
+
reviewNodeId,
|
|
871
|
+
repoRoot: candidateRoot,
|
|
872
|
+
...(candidatePaths.length > 0 ? { candidatePaths } : {}),
|
|
873
|
+
});
|
|
874
|
+
if (candidatePaths.length > 0) {
|
|
875
|
+
const readSet = new Set(openspecReadPaths);
|
|
876
|
+
const unread = candidatePaths.filter((candidate) => !readSet.has(candidate));
|
|
877
|
+
if (unread.length > 0) {
|
|
878
|
+
const governanceRoot = await resolveOpenspecGovernanceRoot(candidateRoot);
|
|
879
|
+
const excludedGovernanceCount = await countExcludedGovernanceSpecFiles(candidateRoot, governanceRoot);
|
|
880
|
+
return finalizePrewrite(input, {
|
|
881
|
+
...basePending,
|
|
882
|
+
verdict,
|
|
883
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
884
|
+
normalizationActions: [],
|
|
885
|
+
mockStrategy,
|
|
886
|
+
classification: "retryable-invalid",
|
|
887
|
+
failureReason: `frontend prewrite gate: effective plan/review did not read these openspec candidate paths: ${unread.join(", ")}. Read each candidate and report applicable rules plus hit path/section/line number, conflicts, and missing specifications, then rerun. 实际成功读取: ${openspecReadPaths.length > 0 ? openspecReadPaths.join(", ") : "(none)"}; 候选总数: ${candidatePaths.length}; 排除治理目录 ${governanceRoot} 下 ${excludedGovernanceCount} 个文件(运行期诊断口径); 候选集在生成期冻结,若候选与本任务无关属于生成期契约配置问题,应修正后重新生成 DAG。`,
|
|
888
|
+
failureCode: "openspec-not-read",
|
|
889
|
+
openspecReadPaths,
|
|
890
|
+
openspecCandidatePaths: candidatePaths,
|
|
891
|
+
});
|
|
944
892
|
}
|
|
945
893
|
}
|
|
946
894
|
// Component selection spec conformance (ADR 0016). Deterministic cross-check
|
|
@@ -962,13 +910,17 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
962
910
|
normalizationActions: [],
|
|
963
911
|
mockStrategy,
|
|
964
912
|
classification: "retryable-invalid",
|
|
965
|
-
failureReason: "frontend prewrite gate: the canonical contract has UI-visible work but uiComponentChoices is missing or empty while the generation-frozen component/theme specification bucket is non-empty. 为每个 UI 用途声明组件选型(decision: specified | reuse-existing | new
|
|
913
|
+
failureReason: "frontend prewrite gate: the canonical contract has UI-visible work but uiComponentChoices is missing or empty while the generation-frozen component/theme specification bucket is non-empty. 为每个 UI 用途声明组件选型(decision: specified | reuse-existing | new);specified 必须给 OpenSpec specReference.path/section/line,new 必须给任务源 PRD specReference.path/section/line。",
|
|
966
914
|
failureCode: "component-choices-missing",
|
|
967
915
|
openspecReadPaths,
|
|
968
916
|
openspecCandidatePaths: candidatePaths,
|
|
969
917
|
});
|
|
970
918
|
}
|
|
919
|
+
// Only `specified` choices cite the frozen OpenSpec candidate bucket. A
|
|
920
|
+
// `new` choice instead cites its task-source/PRD declaration and must not be
|
|
921
|
+
// rejected merely because that source is outside the OpenSpec roots.
|
|
971
922
|
const componentSpecReferences = componentChoices
|
|
923
|
+
.filter((choice) => choice.decision === "specified")
|
|
972
924
|
.map((choice) => ({
|
|
973
925
|
raw: choice.specReference?.path ?? "",
|
|
974
926
|
normalized: choice.specReference?.path
|
|
@@ -976,11 +928,37 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
976
928
|
: null,
|
|
977
929
|
}))
|
|
978
930
|
.filter((ref) => ref.raw.length > 0);
|
|
931
|
+
if (componentSpecCandidates.length > 0 && hasUiVisibleWork) {
|
|
932
|
+
const componentCandidateSet = new Set(componentSpecCandidates);
|
|
933
|
+
const hasBoundSpecifiedChoice = componentChoices.some((choice) => {
|
|
934
|
+
if (choice.decision !== "specified" || !choice.specReference?.path) {
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
const normalized = normalizeCitationPath(choice.specReference.path, candidateRoot);
|
|
938
|
+
return Boolean(normalized && componentCandidateSet.has(normalized));
|
|
939
|
+
});
|
|
940
|
+
if (!hasBoundSpecifiedChoice) {
|
|
941
|
+
return finalizePrewrite(input, {
|
|
942
|
+
...basePending,
|
|
943
|
+
verdict,
|
|
944
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
945
|
+
normalizationActions: [],
|
|
946
|
+
mockStrategy,
|
|
947
|
+
classification: "retryable-invalid",
|
|
948
|
+
failureReason: "frontend prewrite gate: UI-visible work has a generation-frozen component/theme specification bucket, but no uiComponentChoices decision=specified is bound to a path in that bucket. At least one declared UI purpose must use and cite the applicable component/theme specification; reuse-existing/new alone cannot bypass the bucket.",
|
|
949
|
+
failureCode: "component-spec-choice-unbound",
|
|
950
|
+
openspecReadPaths,
|
|
951
|
+
openspecCandidatePaths: candidatePaths,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
}
|
|
979
955
|
if (componentSpecReferences.length > 0) {
|
|
980
956
|
const invalidRefs = [
|
|
981
957
|
...new Set(componentSpecReferences
|
|
982
958
|
.filter((ref) => !ref.normalized ||
|
|
983
|
-
!
|
|
959
|
+
!(candidatePaths.length > 0
|
|
960
|
+
? candidatePaths.includes(ref.normalized)
|
|
961
|
+
: isOpenspecSpecFilePath(ref.normalized)))
|
|
984
962
|
.map((ref) => ref.raw)),
|
|
985
963
|
];
|
|
986
964
|
if (invalidRefs.length > 0) {
|
|
@@ -998,13 +976,10 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
998
976
|
});
|
|
999
977
|
}
|
|
1000
978
|
if (openspecPolicy === "cited") {
|
|
1001
|
-
const parsedCitations = parseOpenspecCitations(citationsText, candidateRoot);
|
|
1002
|
-
const citedPaths = new Set(parsedCitations.citations.map((citation) => citation.path));
|
|
1003
979
|
const readSet = new Set(openspecReadPaths);
|
|
1004
980
|
const uncorroborated = [
|
|
1005
981
|
...new Set(componentSpecReferences
|
|
1006
982
|
.filter((ref) => !ref.normalized ||
|
|
1007
|
-
!citedPaths.has(ref.normalized) ||
|
|
1008
983
|
!readSet.has(ref.normalized))
|
|
1009
984
|
.map((ref) => ref.raw)),
|
|
1010
985
|
];
|
|
@@ -1016,7 +991,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
1016
991
|
normalizationActions: [],
|
|
1017
992
|
mockStrategy,
|
|
1018
993
|
classification: "retryable-invalid",
|
|
1019
|
-
failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths
|
|
994
|
+
failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths are missing a ledger specReference or lack a successful read event: ${uncorroborated.join(", ")}`,
|
|
1020
995
|
failureCode: "component-spec-not-cited",
|
|
1021
996
|
openspecReadPaths,
|
|
1022
997
|
openspecCandidatePaths: candidatePaths,
|
|
@@ -1090,6 +1065,9 @@ export function formatFrontendPrewriteGateStdout(result) {
|
|
|
1090
1065
|
}
|
|
1091
1066
|
lines.push(`Prewrite result: ${result.prewriteResult.path}`);
|
|
1092
1067
|
lines.push(`Openspec policy: ${result.openspecPolicy}`);
|
|
1068
|
+
if (result.failureCode?.startsWith("source-fidelity-")) {
|
|
1069
|
+
lines.push(`Source fidelity ledger: blocked (${result.failureCode})`);
|
|
1070
|
+
}
|
|
1093
1071
|
if (result.failureReason)
|
|
1094
1072
|
lines.push(`Failure reason: ${result.failureReason}`);
|
|
1095
1073
|
if (result.openspecReadPaths.length > 0) {
|