@tea-agent/loop-agent 0.41.1-beta.0 → 0.42.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -207
- package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
- package/dist/adapters/context-transfer/pi-session.js +61 -0
- package/dist/application/dag/generate-task-dag.js +19 -75
- package/dist/application/task-lifecycle/advance.js +5 -24
- package/dist/application/task-lifecycle/observe.js +17 -171
- package/dist/application/task-lifecycle/plan-transitions.js +7 -42
- package/dist/application/task-lifecycle/recommendations.js +2 -13
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +12 -7
- package/dist/cli/program.js +23 -6
- package/dist/commands/client-recovery.js +0 -3
- package/dist/commands/dag-artifact.js +284 -0
- package/dist/commands/dag-context.js +184 -0
- package/dist/commands/dag-rerun.js +206 -1
- package/dist/commands/init.js +1 -27
- package/dist/commands/task-advance.js +0 -19
- package/dist/executors/dag-pi-executor.js +266 -3792
- package/dist/executors/pi-executor.js +4 -15
- package/dist/executors/pi-sdk-executor.js +3 -0
- package/dist/executors/shell-executor.js +217 -945
- package/dist/executors/shell-write-guard.js +0 -7
- package/dist/{worker → infrastructure}/console/app-data.js +4 -0
- package/dist/infrastructure/console/artifact-revision-store.js +430 -0
- package/dist/infrastructure/console/context-export-store.js +160 -0
- package/dist/infrastructure/console/dir-lock.js +132 -0
- package/dist/infrastructure/console/operation-store.js +197 -0
- package/dist/infrastructure/harness/artifact-store.js +10 -1
- package/dist/infrastructure/harness/atomic-write.js +12 -2
- package/dist/shared/context-transfer/artifact-revision.js +172 -0
- package/dist/shared/context-transfer.js +418 -0
- package/dist/shared/dag-failure-category.js +0 -12
- package/dist/shared/openspec-spec.js +4 -70
- package/dist/shared/operator/capabilities.js +14 -7
- package/dist/shared/operator/safe-run-summary.js +1 -0
- package/dist/shared/path-safety.js +93 -0
- package/dist/shared/preview.js +28 -4
- package/dist/task/config-types.js +9 -35
- package/dist/task/contract/adopt.js +0 -4
- package/dist/task/contract/import-revision.js +0 -4
- package/dist/task/contract/project.js +0 -3
- package/dist/task/contract/schema.js +1 -2
- package/dist/task/frontend-project-capability.js +20 -203
- package/dist/task/runtime.js +2 -5
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +9 -15
- package/dist/task/source-prepare/prepare.js +1 -89
- package/dist/task/source-prepare/semantic-intake.js +2 -6
- package/dist/task/source-references.js +1 -22
- package/dist/task/task-demand-routing.js +3 -0
- package/dist/worker/console/chat/chat-event-store.js +2 -2
- package/dist/worker/console/chat/pi-runtime.js +3 -2
- package/dist/worker/console/chat/resource-preferences-store.js +1 -1
- package/dist/worker/console/chat/routes.js +13 -4
- package/dist/worker/console/chat/semantic-activity.js +12 -11
- package/dist/worker/console/chat/session-stats.js +96 -0
- package/dist/worker/console/chat/session-store.js +1 -1
- package/dist/worker/console/chat/turn-process.js +28 -4
- package/dist/worker/console/chat/user-questions.js +1 -1
- package/dist/worker/console/console-update-runtime.js +1 -1
- package/dist/worker/console/context-transfer-diagnostics.js +198 -0
- package/dist/worker/console/dag-confirmation.js +1 -1
- package/dist/worker/console/dag-execution-receipt.js +1 -1
- package/dist/worker/console/doctor.js +1 -1
- package/dist/worker/console/draft-store.js +1 -1
- package/dist/worker/console/human-gate-token.js +1 -1
- package/dist/worker/console/index.js +3 -6
- package/dist/worker/console/interview/assessment.js +1 -1
- package/dist/worker/console/interview/session.js +1 -1
- package/dist/worker/console/operation-runner.js +45 -4
- package/dist/worker/console/operation-sse.js +1 -1
- package/dist/worker/console/operation-wait.js +1 -1
- package/dist/worker/console/operator-actions.js +107 -168
- package/dist/worker/console/operator-surface-health.js +1 -1
- package/dist/worker/console/operator-user-error.js +169 -0
- package/dist/worker/console/pi-plugins.js +60 -1
- package/dist/worker/console/routes.js +637 -8
- package/dist/worker/console/security.js +35 -0
- package/dist/worker/console/server.js +6 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-DZZ8m3PO.js} +1 -1
- package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-D6PvaVd-.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-B_OTOiI8.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-Bv6rqHBg.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-B8eHr0oz.js} +1 -1
- package/dist/worker/console/static/assets/channel-BU5gOilw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-DYwR0im2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-D2WPGqXt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-CQISJ_I7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-C0o2Du1e.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-4f8kr-U7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-D_OWvXzX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-CasdPz5X.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-Cj8lRrla.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-CUvD1FSd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-CqLw_eqB.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-ku-WqJPx.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-Bq_PYDhl.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-BiGZV641.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-C4JRlglF.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-B0dNeWYG.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-Cj35YvYM.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-uzQoJ2-8.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-D1a-Buoz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-XjRrLRSs.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-Da_O24cW.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-BLJ8jrYU.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-B9GrLjM0.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-CeJ0TqiK.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js} +1 -1
- package/dist/worker/console/static/assets/index-H9rFJiGL.css +1 -0
- package/dist/worker/console/static/assets/index-xwu9GxEc.js +451 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-DB26i3d8.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-BOGRyeZT.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-CZRoBO_6.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-qlWhJyoB.js} +1 -1
- package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-CfUiDDB3.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-BQe6fpqj.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-zFzWHw64.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-DFGUqjZO.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-BB5i0l8Z.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-obbVZ_X5.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-BwdaNzc1.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-CkTJaugh.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-BwJ-hIgq.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-BzSgmm7w.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-Bn3oj5Q1.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-dxhGtL8K.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-BLpEInya.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-ChJGYcXy.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-DK-qjRer.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-3qeaWAg-.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-CootlyP9.js} +1 -1
- package/dist/worker/console/static/index.html +7 -2
- package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
- package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
- package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
- package/dist/worker/console/static-src/shell/console-update-reload.js +25 -0
- package/dist/worker/console/workspace-context.js +28 -112
- package/dist/worker/console/workspace-registry.js +2 -2
- package/dist/worker/continuation/worker-continuation.js +278 -0
- package/dist/worker/observability/read-model.js +4 -0
- package/dist/worker/observe/health.js +1 -1
- package/dist/worker/observe/node-transparency.js +572 -0
- package/dist/worker/observe/routes.js +51 -1
- package/dist/worker/observe/static/api.js +69 -5
- package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
- package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
- package/dist/worker/observe/static/dag-node-purpose.js +0 -5
- package/dist/worker/observe/static/state.js +3 -1
- package/dist/worker/observe/static/styles.css +1037 -126
- package/dist/worker/observe/static/views/dag-inspector.js +1217 -65
- package/dist/worker/observe/static/views/session-timeline.js +121 -25
- package/dist/worker/outcomes/adapters.js +19 -0
- package/dist/worker/outcomes/types.js +1 -0
- package/dist/worker/pool/attempt-lease.js +97 -66
- package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
- package/dist/worker/pool/reconcile.js +46 -1
- package/dist/worker/pool/run-store.js +2 -0
- package/dist/worker/pool/state-projection.js +2 -0
- package/dist/worker/task-spec/workflow-routing.js +8 -3
- package/dist/workflows/dag/artifact-bindings.js +149 -0
- package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/budget-enforcement.js +10 -0
- package/dist/workflows/dag/context-receipt.js +305 -0
- package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
- package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
- package/dist/workflows/dag/context-transfer/renderers.js +93 -0
- package/dist/workflows/dag/contract-validator-registrations.js +2 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +162 -932
- package/dist/workflows/dag/frontend-plan-render.js +1 -2
- package/dist/workflows/dag/frontend-prewrite-gate.js +349 -255
- package/dist/workflows/dag/frontend-recovery-plan.js +10 -17
- package/dist/workflows/dag/frontend-recovery-run.js +20 -33
- package/dist/workflows/dag/frontend-repair.js +432 -1
- package/dist/workflows/dag/frontend-review-context.js +15 -261
- package/dist/workflows/dag/frontend-verification-trace.js +24 -249
- package/dist/workflows/dag/frontend-worktree-diff.js +17 -250
- package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
- package/dist/workflows/dag/init-hybrid.js +640 -685
- package/dist/workflows/dag/interrupt-request.js +0 -7
- package/dist/workflows/dag/node-execution.js +116 -759
- package/dist/workflows/dag/path-safety.js +1 -0
- package/dist/workflows/dag/prompt.js +30 -38
- package/dist/workflows/dag/report.js +1 -37
- package/dist/workflows/dag/rerun-feedback.js +1 -256
- package/dist/workflows/dag/rerun-plan.js +557 -11
- package/dist/workflows/dag/rerun-run.js +601 -38
- package/dist/workflows/dag/rerun-task.js +0 -29
- package/dist/workflows/dag/retry-policy.js +104 -214
- package/dist/workflows/dag/runner.js +142 -430
- package/dist/workflows/dag/scheduler.js +16 -114
- package/dist/workflows/dag/skill-snapshot.js +17 -0
- package/dist/workflows/dag/types.js +195 -246
- package/dist/workflows/dag/validate.js +12 -28
- package/docs/architecture/runtime-boundaries.md +6 -6
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/init-surface.manifest.json +12 -30
- package/docs/skills/vetted-skill-registry.md +2 -4
- package/docs/templates/README.md +0 -2
- package/docs/templates/agent-dag-report.schema.json +2 -8
- package/docs/templates/agent-dag.schema.json +33 -1
- package/docs/templates/frontend-implementation-contract.schema.json +1 -4
- package/docs/templates/product-line/task.yaml +1 -1
- package/harness.json +3 -3
- package/package.json +3 -1
- package/skills/frontend-bounded-implement/SKILL.md +14 -15
- package/skills/frontend-design-review/SKILL.md +41 -22
- package/skills/frontend-implementation/SKILL.md +52 -0
- package/skills/frontend-implementation/references/code-standards.md +33 -0
- package/skills/frontend-implementation/references/design-spec.md +56 -0
- package/skills/frontend-implementation/references/node-contracts.md +31 -0
- package/skills/frontend-review/SKILL.md +15 -20
- package/skills/frontend-review/references/review-findings.md +7 -6
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +6 -1
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/commands/dag-follow-up.js +0 -138
- package/dist/executors/pi-read-budget-policy.js +0 -239
- package/dist/worker/console/frontend-human-decision-adapter.js +0 -19
- package/dist/worker/console/frontend-split-operation-adapter.js +0 -20
- package/dist/worker/console/operation-store.js +0 -389
- package/dist/worker/console/static/assets/channel-bnXW0U3C.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +0 -1
- package/dist/worker/console/static/assets/index-D3CC4eUz.css +0 -1
- package/dist/worker/console/static/assets/index-DLt4ZFvD.js +0 -437
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +0 -8
- package/dist/worker/console/static-src/active-run-badge.js +0 -17
- package/dist/worker/materialize/frontend-split-task-materializer.js +0 -72
- package/dist/workflows/dag/dag-retry-schema.js +0 -138
- package/dist/workflows/dag/frontend-closeout.js +0 -221
- package/dist/workflows/dag/frontend-design-policy.js +0 -400
- package/dist/workflows/dag/frontend-human-decision.js +0 -182
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +0 -159
- package/dist/workflows/dag/frontend-read-budgets.js +0 -12
- package/dist/workflows/dag/frontend-recovery-capsule.js +0 -455
- package/dist/workflows/dag/frontend-recovery-controller.js +0 -202
- package/dist/workflows/dag/frontend-recovery-lineage.js +0 -178
- package/dist/workflows/dag/frontend-review-findings.js +0 -270
- package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -914
- package/dist/workflows/dag/frontend-shape-capsule-store.js +0 -191
- package/dist/workflows/dag/frontend-shape-facts.js +0 -409
- package/dist/workflows/dag/frontend-shape.js +0 -427
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +0 -108
- package/dist/workflows/dag/frontend-split-application-service.js +0 -203
- package/dist/workflows/dag/frontend-split-orchestrator.js +0 -899
- package/dist/workflows/dag/frontend-typed-event-store.js +0 -452
- package/dist/workflows/dag/frontend-typed-event-transaction.js +0 -180
- package/dist/workflows/dag/frontend-writer-admission.js +0 -285
- package/dist/workflows/dag/frontend-writer-status.js +0 -256
- package/dist/workflows/dag/recovery-lease.js +0 -80
- package/docs/templates/frontend-implementation-dag.json +0 -89
- package/docs/templates/spec-registry.schema.json +0 -45
- package/skills/frontend-bounded-implement/references/code-standards.md +0 -19
- package/skills/frontend-contract/SKILL.md +0 -23
- package/skills/frontend-contract/references/contract-protocol.md +0 -34
- package/skills/frontend-plan/SKILL.md +0 -26
- package/skills/frontend-plan/references/decision-contract.md +0 -37
- package/skills/frontend-plan/references/design-decisions.md +0 -17
- package/skills/frontend-scout/SKILL.md +0 -25
- package/skills/frontend-scout/references/design-evidence.md +0 -16
- package/skills/frontend-scout/references/scout-evidence.md +0 -23
- /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
|
@@ -2,17 +2,21 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
5
|
+
import { isDagSourceBindingV2 } from "./types.js";
|
|
6
|
+
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
6
7
|
import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
|
|
7
8
|
import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
9
|
+
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
10
|
+
import { parseLedgerJson, validateRequirementLedger, } from "../../task/source-prepare/ledger.js";
|
|
11
|
+
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
10
12
|
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
11
13
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
12
14
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
13
15
|
import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
|
|
14
16
|
export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
|
|
15
17
|
export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
|
|
18
|
+
/** Canonical contract serialization must stay at or below this byte size. */
|
|
19
|
+
export const FRONTEND_CONTRACT_MAX_BYTES = 65536;
|
|
16
20
|
/**
|
|
17
21
|
* Machine-readable failure classification for blocked/retryable-invalid
|
|
18
22
|
* prewrite outcomes. Kept alongside the human-readable failureReason so
|
|
@@ -33,12 +37,13 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
|
|
|
33
37
|
"verification-target-outside-write-set",
|
|
34
38
|
"openspec-not-read",
|
|
35
39
|
"candidate-missing-drift",
|
|
36
|
-
"
|
|
37
|
-
"openspec-
|
|
40
|
+
"openspec-citation-block-unparseable",
|
|
41
|
+
"openspec-not-cited",
|
|
42
|
+
"openspec-citation-not-read",
|
|
38
43
|
"component-spec-reference-invalid",
|
|
39
44
|
"component-spec-not-cited",
|
|
40
|
-
"component-spec-choice-unbound",
|
|
41
45
|
"component-choices-missing",
|
|
46
|
+
"contract-too-large",
|
|
42
47
|
"revision-patch-unmergeable",
|
|
43
48
|
// Source fidelity ledger 类目(SOURCE_FIDELITY_*/source-* 稳定错误码)。
|
|
44
49
|
"source-fidelity-ledger-missing",
|
|
@@ -135,7 +140,7 @@ export const frontendPrewriteResultV1Schema = z
|
|
|
135
140
|
export const frontendWriterAdmissionSchema = z
|
|
136
141
|
.object({
|
|
137
142
|
schemaVersion: z.literal(1),
|
|
138
|
-
writerNodeId: z.enum(["frontend-implement-pi"]),
|
|
143
|
+
writerNodeId: z.enum(["frontend-implement-pi", "frontend-repair-pi"]),
|
|
139
144
|
decision: z.enum(["authorized", "denied"]),
|
|
140
145
|
sourceArtifact: z.string().min(1),
|
|
141
146
|
artifactHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
@@ -269,7 +274,6 @@ function isFailedToolResult(event) {
|
|
|
269
274
|
}
|
|
270
275
|
async function collectOpenspecReadPaths(input) {
|
|
271
276
|
const { runDir, planNodeId, reviewNodeId, repoRoot } = input;
|
|
272
|
-
const candidateSet = input.candidatePaths ? new Set(input.candidatePaths) : undefined;
|
|
273
277
|
const matched = new Set();
|
|
274
278
|
for (const nodeId of [planNodeId, reviewNodeId]) {
|
|
275
279
|
const eventsPath = path.join(runDir, nodeId, "session-events.jsonl");
|
|
@@ -304,7 +308,7 @@ async function collectOpenspecReadPaths(input) {
|
|
|
304
308
|
: null;
|
|
305
309
|
if (relativePath &&
|
|
306
310
|
!isFailedToolResult(event) &&
|
|
307
|
-
(
|
|
311
|
+
isOpenspecSpecFilePath(relativePath)) {
|
|
308
312
|
matched.add(relativePath);
|
|
309
313
|
}
|
|
310
314
|
startedReads.delete(event.toolCallId);
|
|
@@ -319,6 +323,21 @@ async function collectOpenspecReadPaths(input) {
|
|
|
319
323
|
}
|
|
320
324
|
return [...matched];
|
|
321
325
|
}
|
|
326
|
+
async function checkOpenspecReadEvidence(input) {
|
|
327
|
+
const { runDir, candidatePaths, planNodeId, reviewNodeId, repoRoot } = input;
|
|
328
|
+
if (candidatePaths.length === 0)
|
|
329
|
+
return [];
|
|
330
|
+
const allReadPaths = await collectOpenspecReadPaths({
|
|
331
|
+
runDir,
|
|
332
|
+
planNodeId,
|
|
333
|
+
reviewNodeId,
|
|
334
|
+
repoRoot,
|
|
335
|
+
});
|
|
336
|
+
const normalizedCandidates = new Set(candidatePaths
|
|
337
|
+
.map((candidate) => toRepoRelativePath(candidate, repoRoot))
|
|
338
|
+
.filter((candidate) => Boolean(candidate && isOpenspecSpecFilePath(candidate))));
|
|
339
|
+
return allReadPaths.filter((readPath) => normalizedCandidates.has(readPath));
|
|
340
|
+
}
|
|
322
341
|
function normalizeCitationPath(rawPath, repoRoot) {
|
|
323
342
|
let candidate = rawPath.trim();
|
|
324
343
|
const hashIndex = candidate.indexOf("#");
|
|
@@ -333,132 +352,57 @@ function normalizeCitationPath(rawPath, repoRoot) {
|
|
|
333
352
|
}
|
|
334
353
|
return candidate;
|
|
335
354
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
.map((fact) => ({
|
|
360
|
-
path: String(fact.path ?? ""),
|
|
361
|
-
disposition: String(fact.disposition ?? ""),
|
|
362
|
-
rationale: String(fact.rationale ?? ""),
|
|
363
|
-
}));
|
|
364
|
-
}
|
|
365
|
-
catch {
|
|
366
|
-
selections = [];
|
|
367
|
-
}
|
|
368
|
-
if (selections.length === 0) {
|
|
369
|
-
const legacy = await readLegacyOpenspecSelection({
|
|
370
|
-
runDir: input.runDir,
|
|
371
|
-
selectionNodeId,
|
|
372
|
-
candidatePaths: input.candidatePaths,
|
|
373
|
-
mandatoryPaths: input.mandatoryPaths,
|
|
374
|
-
});
|
|
375
|
-
if (!legacy.ok)
|
|
376
|
-
return legacy;
|
|
377
|
-
selections = legacy.selections;
|
|
378
|
-
allowIrrelevant = true;
|
|
379
|
-
}
|
|
380
|
-
try {
|
|
381
|
-
const byPath = new Map();
|
|
382
|
-
for (const row of selections) {
|
|
383
|
-
const legalDispositions = allowIrrelevant
|
|
384
|
-
? ["required", "relevant", "irrelevant"]
|
|
385
|
-
: ["required", "relevant"];
|
|
386
|
-
if (typeof row.path !== "string" ||
|
|
387
|
-
!legalDispositions.includes(row.disposition) ||
|
|
388
|
-
typeof row.rationale !== "string" ||
|
|
389
|
-
!row.rationale.trim()) {
|
|
390
|
-
throw new Error(`invalid selection row (path+${legalDispositions.join("|")}+non-empty rationale expected): ${JSON.stringify(row)}`);
|
|
355
|
+
/**
|
|
356
|
+
* Parses the deterministic openspec-citations fence from the effective plan
|
|
357
|
+
* output. Each non-empty line must be one JSON object {"path","section","line"}.
|
|
358
|
+
* Rows whose normalized path is not a supported openspec spec file are ignored
|
|
359
|
+
* (out of scope); rows that are invalid JSON or lack a string path mark the
|
|
360
|
+
* block unparseable (fail-closed when candidates are non-empty).
|
|
361
|
+
*/
|
|
362
|
+
export function parseOpenspecCitations(planText, repoRoot) {
|
|
363
|
+
const citations = [];
|
|
364
|
+
let blockPresent = false;
|
|
365
|
+
let unparseable = false;
|
|
366
|
+
const fenceRe = /```openspec-citations[ \t]*\r?\n([\s\S]*?)\r?\n```/g;
|
|
367
|
+
let match;
|
|
368
|
+
while ((match = fenceRe.exec(planText)) !== null) {
|
|
369
|
+
blockPresent = true;
|
|
370
|
+
const body = match[1] ?? "";
|
|
371
|
+
for (const rawLine of body.split(/\r?\n/)) {
|
|
372
|
+
const line = rawLine.trim();
|
|
373
|
+
if (!line)
|
|
374
|
+
continue;
|
|
375
|
+
let parsed;
|
|
376
|
+
try {
|
|
377
|
+
parsed = JSON.parse(line);
|
|
391
378
|
}
|
|
392
|
-
|
|
393
|
-
|
|
379
|
+
catch {
|
|
380
|
+
unparseable = true;
|
|
381
|
+
continue;
|
|
394
382
|
}
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
async function readCommittedContractFacts(runDir, nodeId) {
|
|
417
|
-
const { readTypedEventStoreFromJsonl } = await import("./frontend-typed-event-store.js");
|
|
418
|
-
const records = await readTypedEventStoreFromJsonl(path.join(runDir, nodeId, "contract-typed-facts.jsonl"));
|
|
419
|
-
return records
|
|
420
|
-
.filter((record) => record.phase === "committed")
|
|
421
|
-
.map((record) => record.fact)
|
|
422
|
-
.filter((fact) => Boolean(fact));
|
|
423
|
-
}
|
|
424
|
-
async function readLegacyOpenspecSelection(input) {
|
|
425
|
-
let text;
|
|
426
|
-
try {
|
|
427
|
-
text = await readNodeText(input.runDir, input.selectionNodeId);
|
|
428
|
-
}
|
|
429
|
-
catch (error) {
|
|
430
|
-
return {
|
|
431
|
-
ok: false,
|
|
432
|
-
reason: `openspec selector output unreadable: ${error instanceof Error ? error.message : String(error)}`,
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
const fenced = /```json\s*\n([\s\S]*?)\n```/.exec(text);
|
|
436
|
-
if (!fenced) {
|
|
437
|
-
return {
|
|
438
|
-
ok: false,
|
|
439
|
-
reason: "openspec selector emitted neither typed openspec-selection facts nor a legacy fenced JSON selection object",
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
try {
|
|
443
|
-
const parsed = JSON.parse(fenced[1] ?? "");
|
|
444
|
-
if (parsed.schemaVersion !== 1 ||
|
|
445
|
-
parsed.schemaId !== "frontend-openspec-selection-v1" ||
|
|
446
|
-
!Array.isArray(parsed.selections)) {
|
|
447
|
-
throw new Error("schemaVersion/schemaId/selections invalid");
|
|
383
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
384
|
+
unparseable = true;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const rawPath = parsed.path;
|
|
388
|
+
if (typeof rawPath !== "string" || !rawPath.trim()) {
|
|
389
|
+
unparseable = true;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const normalized = normalizeCitationPath(rawPath, repoRoot);
|
|
393
|
+
if (!normalized || !isOpenspecSpecFilePath(normalized))
|
|
394
|
+
continue;
|
|
395
|
+
const section = typeof parsed.section === "string"
|
|
396
|
+
? parsed.section
|
|
397
|
+
: "";
|
|
398
|
+
const lineValue = parsed.line;
|
|
399
|
+
const citationLine = typeof lineValue === "number" && Number.isInteger(lineValue)
|
|
400
|
+
? lineValue
|
|
401
|
+
: null;
|
|
402
|
+
citations.push({ path: normalized, section, line: citationLine });
|
|
448
403
|
}
|
|
449
|
-
const rows = parsed.selections.map((row) => ({
|
|
450
|
-
path: String(row.path ?? ""),
|
|
451
|
-
disposition: String(row.disposition ?? ""),
|
|
452
|
-
rationale: String(row.rationale ?? ""),
|
|
453
|
-
}));
|
|
454
|
-
return { ok: true, selections: rows };
|
|
455
|
-
}
|
|
456
|
-
catch (error) {
|
|
457
|
-
return {
|
|
458
|
-
ok: false,
|
|
459
|
-
reason: `openspec selector invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
460
|
-
};
|
|
461
404
|
}
|
|
405
|
+
return { blockPresent, unparseable, citations };
|
|
462
406
|
}
|
|
463
407
|
async function finalizePrewrite(input, pending) {
|
|
464
408
|
const sourceBindingSha256 = input.sourceBinding
|
|
@@ -509,7 +453,162 @@ async function finalizePrewrite(input, pending) {
|
|
|
509
453
|
openspecPolicy: pending.openspecPolicy,
|
|
510
454
|
};
|
|
511
455
|
}
|
|
512
|
-
|
|
456
|
+
/**
|
|
457
|
+
* Source fidelity ledger 门禁检查(AC-FIDELITY-002/003/006):绑定携带 ledger 指针时,
|
|
458
|
+
* 依次核验 ledger 存在、绑定 hash 一致、inputDigest 新鲜、无 unresolved/conflict/
|
|
459
|
+
* 高风险未覆盖、canonical requirement 全覆盖、来源引用有效且非伪造。任一失败 fail closed。
|
|
460
|
+
*/
|
|
461
|
+
export async function checkFrontendSourceFidelityLedger(input) {
|
|
462
|
+
const { binding } = input;
|
|
463
|
+
// v1 绑定不携带 ledger 指针:非 frontend task / 无可解析引用 → 无 v2 校验路径。
|
|
464
|
+
if (!isDagSourceBindingV2(binding)) {
|
|
465
|
+
return { ok: true, failureCode: null, failureReason: null, ledger: null };
|
|
466
|
+
}
|
|
467
|
+
const absolute = resolveDagTaskSourcePath({
|
|
468
|
+
workspaceRoot: input.workspaceRoot,
|
|
469
|
+
taskId: binding.taskId,
|
|
470
|
+
sourcePath: binding.ledgerPath,
|
|
471
|
+
});
|
|
472
|
+
let raw;
|
|
473
|
+
try {
|
|
474
|
+
raw = await readFile(absolute, "utf8");
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
failureCode: "source-fidelity-ledger-missing",
|
|
480
|
+
failureReason: `frontend source fidelity ledger missing: ${binding.ledgerPath} (${error instanceof Error ? error.message : String(error)})`,
|
|
481
|
+
ledger: null,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
const actualSha = createHash("sha256").update(raw).digest("hex");
|
|
485
|
+
if (actualSha !== binding.ledgerSha256) {
|
|
486
|
+
return {
|
|
487
|
+
ok: false,
|
|
488
|
+
failureCode: "source-fidelity-ledger-stale",
|
|
489
|
+
failureReason: `frontend source fidelity ledger stale or tampered: ${binding.ledgerPath} (bound ${binding.ledgerSha256}, observed ${actualSha})`,
|
|
490
|
+
ledger: null,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
let ledger;
|
|
494
|
+
try {
|
|
495
|
+
ledger = parseLedgerJson(raw);
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
return {
|
|
499
|
+
ok: false,
|
|
500
|
+
failureCode: "source-fidelity-ledger-stale",
|
|
501
|
+
failureReason: `frontend source fidelity ledger is not a valid v1 ledger: ${error instanceof Error ? error.message : String(error)}`,
|
|
502
|
+
ledger: null,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
if (ledger.inputDigest !== binding.inputDigest) {
|
|
506
|
+
return {
|
|
507
|
+
ok: false,
|
|
508
|
+
failureCode: "source-fidelity-ledger-stale",
|
|
509
|
+
failureReason: `frontend source fidelity ledger inputDigest mismatch: bound ${binding.inputDigest}, ledger ${ledger.inputDigest}`,
|
|
510
|
+
ledger,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
const validation = validateRequirementLedger(ledger);
|
|
514
|
+
const firstError = (code) => validation.find((error) => error.code === code)?.message ?? null;
|
|
515
|
+
if (ledger.stats.unresolvedCount > 0 || validation.some((e) => e.code === "SOURCE_FIDELITY_UNRESOLVED")) {
|
|
516
|
+
return {
|
|
517
|
+
ok: false,
|
|
518
|
+
failureCode: "source-fidelity-unresolved",
|
|
519
|
+
failureReason: firstError("SOURCE_FIDELITY_UNRESOLVED") ??
|
|
520
|
+
`frontend source fidelity ledger has ${ledger.stats.unresolvedCount} unresolved fragment(s)`,
|
|
521
|
+
ledger,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
if (ledger.stats.conflictCount > 0 || validation.some((e) => e.code === "SOURCE_FIDELITY_CONFLICT")) {
|
|
525
|
+
return {
|
|
526
|
+
ok: false,
|
|
527
|
+
failureCode: "source-fidelity-conflict",
|
|
528
|
+
failureReason: firstError("SOURCE_FIDELITY_CONFLICT") ??
|
|
529
|
+
`frontend source fidelity ledger has ${ledger.stats.conflictCount} conflict(s)`,
|
|
530
|
+
ledger,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
if (ledger.stats.highRiskUncoveredCount > 0 ||
|
|
534
|
+
validation.some((e) => e.code === "SOURCE_FIDELITY_HIGH_RISK_UNCOVERED")) {
|
|
535
|
+
return {
|
|
536
|
+
ok: false,
|
|
537
|
+
failureCode: "source-fidelity-high-risk-uncovered",
|
|
538
|
+
failureReason: firstError("SOURCE_FIDELITY_HIGH_RISK_UNCOVERED") ??
|
|
539
|
+
`frontend source fidelity ledger has ${ledger.stats.highRiskUncoveredCount} high-risk uncovered fragment(s)`,
|
|
540
|
+
ledger,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
// AC-BLOCK-001:canonical requirement 缺少 fragment/sourceRef 证据(ledger 侧
|
|
544
|
+
// REQUIREMENT_NOT_COVERED)与来源引用/片段无效(SOURCE_REF_INVALID /
|
|
545
|
+
// FRAGMENT_INVALID)均为阻断性失败,结构化失败结果不得产生可继续执行的成功。
|
|
546
|
+
const notCoveredError = validation.find((e) => e.code === "SOURCE_FIDELITY_REQUIREMENT_NOT_COVERED");
|
|
547
|
+
if (notCoveredError) {
|
|
548
|
+
return {
|
|
549
|
+
ok: false,
|
|
550
|
+
failureCode: "source-fidelity-requirement-not-covered",
|
|
551
|
+
failureReason: notCoveredError.message,
|
|
552
|
+
ledger,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
const refInvalidError = validation.find((e) => e.code === "SOURCE_FIDELITY_SOURCE_REF_INVALID" ||
|
|
556
|
+
e.code === "SOURCE_FIDELITY_FRAGMENT_INVALID");
|
|
557
|
+
if (refInvalidError) {
|
|
558
|
+
return {
|
|
559
|
+
ok: false,
|
|
560
|
+
failureCode: "source-fidelity-source-ref-invalid",
|
|
561
|
+
failureReason: refInvalidError.message,
|
|
562
|
+
ledger,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
const ledgerIds = new Set(ledger.canonicalRequirements.map((requirement) => requirement.id));
|
|
566
|
+
const contractIds = new Set(input.contractRequirements.map((r) => r.id));
|
|
567
|
+
const uncoveredRequirements = [...ledgerIds].filter((id) => !contractIds.has(id));
|
|
568
|
+
if (uncoveredRequirements.length > 0) {
|
|
569
|
+
return {
|
|
570
|
+
ok: false,
|
|
571
|
+
failureCode: "source-fidelity-requirement-not-covered",
|
|
572
|
+
failureReason: `frontend contract requirements do not cover ledger canonical requirement ids: ${uncoveredRequirements.join(", ")}`,
|
|
573
|
+
ledger,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
const fragmentById = new Map(ledger.fragments.map((fragment) => [fragment.id, fragment]));
|
|
577
|
+
for (const requirement of input.contractRequirements) {
|
|
578
|
+
if (!ledgerIds.has(requirement.id))
|
|
579
|
+
continue;
|
|
580
|
+
const declared = requirement.sourceFragmentIds ?? [];
|
|
581
|
+
if (declared.length === 0) {
|
|
582
|
+
return {
|
|
583
|
+
ok: false,
|
|
584
|
+
failureCode: "source-fidelity-source-ref-invalid",
|
|
585
|
+
failureReason: `frontend contract requirement ${requirement.id} lacks sourceFragmentIds for the bound ledger`,
|
|
586
|
+
ledger,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
const boundFragments = new Set(ledger.canonicalRequirements.find((r) => r.id === requirement.id)
|
|
590
|
+
?.sourceFragmentIds ?? []);
|
|
591
|
+
for (const fragmentId of declared) {
|
|
592
|
+
if (!fragmentById.has(fragmentId)) {
|
|
593
|
+
return {
|
|
594
|
+
ok: false,
|
|
595
|
+
failureCode: "source-fidelity-source-ref-invalid",
|
|
596
|
+
failureReason: `frontend contract requirement ${requirement.id} references unknown fragment ${fragmentId}`,
|
|
597
|
+
ledger,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
if (!boundFragments.has(fragmentId)) {
|
|
601
|
+
return {
|
|
602
|
+
ok: false,
|
|
603
|
+
failureCode: "source-fidelity-source-ref-invalid",
|
|
604
|
+
failureReason: `frontend contract requirement ${requirement.id} declares fabricated fragment reference ${fragmentId} (not bound in ledger)`,
|
|
605
|
+
ledger,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return { ok: true, failureCode: null, failureReason: null, ledger };
|
|
611
|
+
}
|
|
513
612
|
export async function runFrontendPrewriteGate(input) {
|
|
514
613
|
const workspaceRoot = input.workspaceRoot ?? input.repoRoot;
|
|
515
614
|
// selectNode throws for a scheduling defect (no FINISHED plan/review). That
|
|
@@ -558,6 +657,9 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
558
657
|
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
559
658
|
openspecPolicy,
|
|
560
659
|
};
|
|
660
|
+
const planText = [planArtifact.contractText, planArtifact.citationsText]
|
|
661
|
+
.filter(Boolean)
|
|
662
|
+
.join("\n");
|
|
561
663
|
let reviewText;
|
|
562
664
|
try {
|
|
563
665
|
reviewText = await readNodeText(input.runDir, reviewNodeId);
|
|
@@ -681,6 +783,21 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
681
783
|
});
|
|
682
784
|
}
|
|
683
785
|
}
|
|
786
|
+
// Size guard: a canonical contract above 64KB is a retryable-invalid defect
|
|
787
|
+
// (compress the contract) rather than a writer authorization failure.
|
|
788
|
+
const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8");
|
|
789
|
+
if (contractSizeBytes > FRONTEND_CONTRACT_MAX_BYTES) {
|
|
790
|
+
return finalizePrewrite(input, {
|
|
791
|
+
...basePending,
|
|
792
|
+
verdict,
|
|
793
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
794
|
+
normalizationActions: [],
|
|
795
|
+
mockStrategy: analysis.canonical.mockApi.strategy,
|
|
796
|
+
classification: "retryable-invalid",
|
|
797
|
+
failureReason: `frontend prewrite gate: canonical contract serialization is ${contractSizeBytes} bytes, exceeding the ${FRONTEND_CONTRACT_MAX_BYTES}-byte limit. 压缩契约(移除冗余 prose、重复目标或过长描述)后重试。`,
|
|
798
|
+
failureCode: "contract-too-large",
|
|
799
|
+
});
|
|
800
|
+
}
|
|
684
801
|
const mockStrategy = analysis.canonical.mockApi.strategy;
|
|
685
802
|
if (!input.config.allowedMockStrategies.includes(mockStrategy)) {
|
|
686
803
|
const quotedAllowed = input.config.allowedMockStrategies
|
|
@@ -752,31 +869,14 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
752
869
|
// Effective plan/review must actually read the candidate openspec paths when
|
|
753
870
|
// any exist. This fail-closes BEFORE the canonical contract is materialized
|
|
754
871
|
// so a writer is never authorized on top of unread normative evidence.
|
|
755
|
-
const
|
|
756
|
-
const selection = await resolveRequiredOpenspecPaths({
|
|
757
|
-
runDir: input.runDir,
|
|
758
|
-
selectionNodeId: input.config.openspecSelectionNodeId,
|
|
759
|
-
candidatePaths: frozenCandidatePaths,
|
|
760
|
-
mandatoryPaths: input.config.openspecMandatoryPaths ?? [],
|
|
761
|
-
policy: openspecPolicy,
|
|
762
|
-
});
|
|
763
|
-
if (!selection.ok) {
|
|
764
|
-
return finalizePrewrite(input, {
|
|
765
|
-
...basePending, verdict, candidateJsonSha256: analysis.candidateJsonSha256,
|
|
766
|
-
normalizationActions: [], mockStrategy, classification: "retryable-invalid",
|
|
767
|
-
failureReason: `frontend prewrite gate: ${selection.reason}`,
|
|
768
|
-
failureCode: "openspec-selection-invalid",
|
|
769
|
-
openspecCandidatePaths: frozenCandidatePaths,
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
const candidatePaths = selection.paths;
|
|
872
|
+
const candidatePaths = input.config.openspecCandidatePaths ?? [];
|
|
773
873
|
const candidateRoot = workspaceRoot ?? process.cwd();
|
|
774
874
|
// Drift precheck: candidates are frozen at generation time. A candidate that
|
|
775
875
|
// no longer exists on disk was deleted/renamed after generation and is a
|
|
776
876
|
// generation-contract defect, not an "unread" failure.
|
|
777
877
|
const missingCandidates = [];
|
|
778
|
-
if (
|
|
779
|
-
for (const candidate of
|
|
878
|
+
if (candidatePaths.length > 0) {
|
|
879
|
+
for (const candidate of candidatePaths) {
|
|
780
880
|
try {
|
|
781
881
|
await stat(path.join(candidateRoot, candidate));
|
|
782
882
|
}
|
|
@@ -799,78 +899,99 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
799
899
|
openspecCandidatePaths: candidatePaths,
|
|
800
900
|
});
|
|
801
901
|
}
|
|
802
|
-
|
|
803
|
-
if (
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
const
|
|
812
|
-
if (!
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
902
|
+
let openspecReadPaths;
|
|
903
|
+
if (openspecPolicy === "cited") {
|
|
904
|
+
openspecReadPaths = await collectOpenspecReadPaths({
|
|
905
|
+
runDir: input.runDir,
|
|
906
|
+
planNodeId,
|
|
907
|
+
reviewNodeId,
|
|
908
|
+
repoRoot: candidateRoot,
|
|
909
|
+
});
|
|
910
|
+
if (candidatePaths.length > 0) {
|
|
911
|
+
const parsed = parseOpenspecCitations(planText, candidateRoot);
|
|
912
|
+
if (!parsed.blockPresent || parsed.unparseable) {
|
|
913
|
+
return finalizePrewrite(input, {
|
|
914
|
+
...basePending,
|
|
915
|
+
verdict,
|
|
916
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
917
|
+
normalizationActions: [],
|
|
918
|
+
mockStrategy,
|
|
919
|
+
classification: "retryable-invalid",
|
|
920
|
+
failureReason: `frontend prewrite gate: the effective plan output must end with a parseable \`\`\`openspec-citations fenced block (one JSON {"path","section","line"} per line). ${parsed.blockPresent
|
|
921
|
+
? "The block is present but contains unparseable lines."
|
|
922
|
+
: "No openspec-citations block was found."}`,
|
|
923
|
+
failureCode: "openspec-citation-block-unparseable",
|
|
924
|
+
openspecReadPaths,
|
|
925
|
+
openspecCandidatePaths: candidatePaths,
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
const citedPaths = new Set(parsed.citations.map((citation) => citation.path));
|
|
929
|
+
const notCited = candidatePaths.filter((candidate) => !citedPaths.has(candidate));
|
|
930
|
+
if (notCited.length > 0) {
|
|
931
|
+
return finalizePrewrite(input, {
|
|
932
|
+
...basePending,
|
|
933
|
+
verdict,
|
|
934
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
935
|
+
normalizationActions: [],
|
|
936
|
+
mockStrategy,
|
|
937
|
+
classification: "retryable-invalid",
|
|
938
|
+
failureReason: `frontend prewrite gate: these openspec candidate paths were not cited in the plan openspec-citations block: ${notCited.join(", ")}`,
|
|
939
|
+
failureCode: "openspec-not-cited",
|
|
940
|
+
openspecReadPaths,
|
|
941
|
+
openspecCandidatePaths: candidatePaths,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
const readSet = new Set(openspecReadPaths);
|
|
945
|
+
const uncorroborated = [
|
|
946
|
+
...new Set(parsed.citations
|
|
947
|
+
.filter((citation) => !readSet.has(citation.path))
|
|
948
|
+
.map((citation) => citation.path)),
|
|
949
|
+
];
|
|
950
|
+
if (uncorroborated.length > 0) {
|
|
951
|
+
return finalizePrewrite(input, {
|
|
952
|
+
...basePending,
|
|
953
|
+
verdict,
|
|
954
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
955
|
+
normalizationActions: [],
|
|
956
|
+
mockStrategy,
|
|
957
|
+
classification: "retryable-invalid",
|
|
958
|
+
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(", ")}`,
|
|
959
|
+
failureCode: "openspec-citation-not-read",
|
|
960
|
+
openspecReadPaths,
|
|
961
|
+
openspecCandidatePaths: candidatePaths,
|
|
962
|
+
});
|
|
963
|
+
}
|
|
846
964
|
}
|
|
965
|
+
// cited with empty candidates: no read enforcement. The generation step
|
|
966
|
+
// emits an advisory instead of failing the gate.
|
|
847
967
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
968
|
+
else {
|
|
969
|
+
openspecReadPaths = await checkOpenspecReadEvidence({
|
|
970
|
+
runDir: input.runDir,
|
|
971
|
+
candidatePaths,
|
|
972
|
+
planNodeId,
|
|
973
|
+
reviewNodeId,
|
|
974
|
+
repoRoot: candidateRoot,
|
|
975
|
+
});
|
|
976
|
+
if (candidatePaths.length > 0) {
|
|
977
|
+
const readSet = new Set(openspecReadPaths);
|
|
978
|
+
const unread = candidatePaths.filter((candidate) => !readSet.has(candidate));
|
|
979
|
+
if (unread.length > 0) {
|
|
980
|
+
const governanceRoot = await resolveOpenspecGovernanceRoot(candidateRoot);
|
|
981
|
+
const excludedGovernanceCount = await countExcludedGovernanceSpecFiles(candidateRoot, governanceRoot);
|
|
982
|
+
return finalizePrewrite(input, {
|
|
983
|
+
...basePending,
|
|
984
|
+
verdict,
|
|
985
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
986
|
+
normalizationActions: [],
|
|
987
|
+
mockStrategy,
|
|
988
|
+
classification: "retryable-invalid",
|
|
989
|
+
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。`,
|
|
990
|
+
failureCode: "openspec-not-read",
|
|
991
|
+
openspecReadPaths,
|
|
992
|
+
openspecCandidatePaths: candidatePaths,
|
|
993
|
+
});
|
|
994
|
+
}
|
|
874
995
|
}
|
|
875
996
|
}
|
|
876
997
|
// Component selection spec conformance (ADR 0016). Deterministic cross-check
|
|
@@ -892,17 +1013,13 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
892
1013
|
normalizationActions: [],
|
|
893
1014
|
mockStrategy,
|
|
894
1015
|
classification: "retryable-invalid",
|
|
895
|
-
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
|
|
1016
|
+
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 必须给 specReference.path/section/line,new 必须给 rationale 声明偏差。",
|
|
896
1017
|
failureCode: "component-choices-missing",
|
|
897
1018
|
openspecReadPaths,
|
|
898
1019
|
openspecCandidatePaths: candidatePaths,
|
|
899
1020
|
});
|
|
900
1021
|
}
|
|
901
|
-
// Only `specified` choices cite the frozen OpenSpec candidate bucket. A
|
|
902
|
-
// `new` choice instead cites its task-source/PRD declaration and must not be
|
|
903
|
-
// rejected merely because that source is outside the OpenSpec roots.
|
|
904
1022
|
const componentSpecReferences = componentChoices
|
|
905
|
-
.filter((choice) => choice.decision === "specified")
|
|
906
1023
|
.map((choice) => ({
|
|
907
1024
|
raw: choice.specReference?.path ?? "",
|
|
908
1025
|
normalized: choice.specReference?.path
|
|
@@ -910,37 +1027,11 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
910
1027
|
: null,
|
|
911
1028
|
}))
|
|
912
1029
|
.filter((ref) => ref.raw.length > 0);
|
|
913
|
-
if (componentSpecCandidates.length > 0 && hasUiVisibleWork) {
|
|
914
|
-
const componentCandidateSet = new Set(componentSpecCandidates);
|
|
915
|
-
const hasBoundSpecifiedChoice = componentChoices.some((choice) => {
|
|
916
|
-
if (choice.decision !== "specified" || !choice.specReference?.path) {
|
|
917
|
-
return false;
|
|
918
|
-
}
|
|
919
|
-
const normalized = normalizeCitationPath(choice.specReference.path, candidateRoot);
|
|
920
|
-
return Boolean(normalized && componentCandidateSet.has(normalized));
|
|
921
|
-
});
|
|
922
|
-
if (!hasBoundSpecifiedChoice) {
|
|
923
|
-
return finalizePrewrite(input, {
|
|
924
|
-
...basePending,
|
|
925
|
-
verdict,
|
|
926
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
927
|
-
normalizationActions: [],
|
|
928
|
-
mockStrategy,
|
|
929
|
-
classification: "retryable-invalid",
|
|
930
|
-
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.",
|
|
931
|
-
failureCode: "component-spec-choice-unbound",
|
|
932
|
-
openspecReadPaths,
|
|
933
|
-
openspecCandidatePaths: candidatePaths,
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
1030
|
if (componentSpecReferences.length > 0) {
|
|
938
1031
|
const invalidRefs = [
|
|
939
1032
|
...new Set(componentSpecReferences
|
|
940
1033
|
.filter((ref) => !ref.normalized ||
|
|
941
|
-
!(
|
|
942
|
-
? candidatePaths.includes(ref.normalized)
|
|
943
|
-
: isOpenspecSpecFilePath(ref.normalized)))
|
|
1034
|
+
!isOpenspecSpecFilePath(ref.normalized))
|
|
944
1035
|
.map((ref) => ref.raw)),
|
|
945
1036
|
];
|
|
946
1037
|
if (invalidRefs.length > 0) {
|
|
@@ -958,10 +1049,13 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
958
1049
|
});
|
|
959
1050
|
}
|
|
960
1051
|
if (openspecPolicy === "cited") {
|
|
1052
|
+
const parsedCitations = parseOpenspecCitations(planText, candidateRoot);
|
|
1053
|
+
const citedPaths = new Set(parsedCitations.citations.map((citation) => citation.path));
|
|
961
1054
|
const readSet = new Set(openspecReadPaths);
|
|
962
1055
|
const uncorroborated = [
|
|
963
1056
|
...new Set(componentSpecReferences
|
|
964
1057
|
.filter((ref) => !ref.normalized ||
|
|
1058
|
+
!citedPaths.has(ref.normalized) ||
|
|
965
1059
|
!readSet.has(ref.normalized))
|
|
966
1060
|
.map((ref) => ref.raw)),
|
|
967
1061
|
];
|
|
@@ -973,7 +1067,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
973
1067
|
normalizationActions: [],
|
|
974
1068
|
mockStrategy,
|
|
975
1069
|
classification: "retryable-invalid",
|
|
976
|
-
failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths
|
|
1070
|
+
failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths were not cited in the plan openspec-citations block or lack a successful read event: ${uncorroborated.join(", ")}`,
|
|
977
1071
|
failureCode: "component-spec-not-cited",
|
|
978
1072
|
openspecReadPaths,
|
|
979
1073
|
openspecCandidatePaths: candidatePaths,
|