@tea-agent/loop-agent 0.41.1-beta.0 → 0.42.0-next.2
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 +78 -206
- 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 +80 -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-case-coverage-analysis.js +99 -4
- package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/backend-test-writer-completeness.js +45 -3
- 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 +740 -718
- package/dist/workflows/dag/interrupt-request.js +0 -7
- package/dist/workflows/dag/node-execution.js +136 -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 +109 -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/backend-test-dag.json +18 -18
- 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
|
@@ -8,16 +8,8 @@ import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-sto
|
|
|
8
8
|
import { findPackageRoot } from "../../shared/package-metadata.js";
|
|
9
9
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
10
10
|
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
11
|
-
import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
|
|
12
|
-
// Cycle-safe value imports: the check functions are hoisted declarations used
|
|
13
|
-
// only at call time, so the contract module and the policy module can depend
|
|
14
|
-
// on each other without module-init ordering issues.
|
|
15
|
-
import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
|
|
16
11
|
import { isDagSourceBindingV2 } from "./types.js";
|
|
17
|
-
import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
|
|
18
|
-
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
19
|
-
import { isConfigurationVerificationFile, verificationSymbolMatchesContent, } from "./frontend-verification-trace.js";
|
|
20
|
-
import { FRONTEND_READ_BUDGETS } from "./frontend-read-budgets.js";
|
|
12
|
+
import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, openspecCitationsRel, parseOpenspecCitationsBlock, persistRuntimeSkeleton, } from "./structured-output-repair.js";
|
|
21
13
|
export const frontendNormalizationActionSchema = z.enum([
|
|
22
14
|
"remove-trailing-commas",
|
|
23
15
|
"strip-comments",
|
|
@@ -25,7 +17,6 @@ export const frontendNormalizationActionSchema = z.enum([
|
|
|
25
17
|
"canonicalize-verification-alias",
|
|
26
18
|
"drop-unresolved-verification-symbols",
|
|
27
19
|
"inject-source-binding",
|
|
28
|
-
"compact-canonical-contract",
|
|
29
20
|
]);
|
|
30
21
|
export function sha256Hex(input) {
|
|
31
22
|
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
@@ -49,15 +40,6 @@ function sortKeysDeep(value) {
|
|
|
49
40
|
export function serializeDeterministicJson(value) {
|
|
50
41
|
return JSON.stringify(sortKeysDeep(value), null, 2);
|
|
51
42
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Deterministic compact JSON for the canonical contract artifact. The pretty
|
|
54
|
-
* serializer remains the default for human-facing audit artifacts, while the
|
|
55
|
-
* writer contract uses this representation to reduce artifact size and
|
|
56
|
-
* downstream read pressure without changing contract semantics.
|
|
57
|
-
*/
|
|
58
|
-
export function serializeCompactDeterministicJson(value) {
|
|
59
|
-
return JSON.stringify(sortKeysDeep(value));
|
|
60
|
-
}
|
|
61
43
|
export function deterministicSha256(value) {
|
|
62
44
|
return sha256Hex(serializeDeterministicJson(value));
|
|
63
45
|
}
|
|
@@ -88,13 +70,6 @@ export async function writeDeterministicJsonArtifact(runDir, relativePath, value
|
|
|
88
70
|
await writeTextArtifactFile(targetPath, json);
|
|
89
71
|
return { path: targetPath, sha256: sha256Hex(json) };
|
|
90
72
|
}
|
|
91
|
-
/** Write a canonical contract artifact with compact deterministic JSON. */
|
|
92
|
-
export async function writeCompactDeterministicJsonArtifact(runDir, relativePath, value) {
|
|
93
|
-
const json = `${serializeCompactDeterministicJson(value)}\n`;
|
|
94
|
-
const targetPath = path.join(runDir, relativePath);
|
|
95
|
-
await writeTextArtifactFile(targetPath, json);
|
|
96
|
-
return { path: targetPath, sha256: sha256Hex(json) };
|
|
97
|
-
}
|
|
98
73
|
/**
|
|
99
74
|
* Extract frozen command labels from the DAG run spec (run.json).
|
|
100
75
|
* The run spec is written before any node executes, so it is always available
|
|
@@ -129,93 +104,8 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
|
|
|
129
104
|
}
|
|
130
105
|
return [...labels];
|
|
131
106
|
}
|
|
132
|
-
/**
|
|
133
|
-
* Extract the design-policy allowedDependencies set from the DAG run spec
|
|
134
|
-
* (run.json), mirroring `deriveFrozenCommandLabelsFromRun`. Used by the plan
|
|
135
|
-
* node's front-loaded policy pre-check so dependency declarations can be
|
|
136
|
-
* corrected through the retry ladder before the policy shell (the authority)
|
|
137
|
-
* re-runs the same check on committed facts.
|
|
138
|
-
*/
|
|
139
|
-
async function deriveAllowedDependenciesFromRun(runDir) {
|
|
140
|
-
const specPath = path.join(runDir, "run.json");
|
|
141
|
-
let raw;
|
|
142
|
-
try {
|
|
143
|
-
raw = await readFile(specPath, "utf8");
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
return [];
|
|
147
|
-
}
|
|
148
|
-
let spec;
|
|
149
|
-
try {
|
|
150
|
-
spec = JSON.parse(raw);
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
return [];
|
|
154
|
-
}
|
|
155
|
-
const deps = new Set();
|
|
156
|
-
for (const task of spec.tasks ?? []) {
|
|
157
|
-
const allowed = task.shell?.frontendDesignPolicy?.allowedDependencies;
|
|
158
|
-
if (allowed) {
|
|
159
|
-
for (const dep of allowed)
|
|
160
|
-
deps.add(dep);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return [...deps];
|
|
164
|
-
}
|
|
165
|
-
/** Read the frozen implementation writeSet patterns from run.json so the plan
|
|
166
|
-
* validator can front-load checkTargetPaths (write-set-too-large) before the
|
|
167
|
-
* design-policy shell terminates the run. */
|
|
168
|
-
async function deriveWriteSetFromRun(runDir) {
|
|
169
|
-
const specPath = path.join(runDir, "run.json");
|
|
170
|
-
let raw;
|
|
171
|
-
try {
|
|
172
|
-
raw = await readFile(specPath, "utf8");
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
return [];
|
|
176
|
-
}
|
|
177
|
-
let spec;
|
|
178
|
-
try {
|
|
179
|
-
spec = JSON.parse(raw);
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
return [];
|
|
183
|
-
}
|
|
184
|
-
const writeSet = new Set();
|
|
185
|
-
for (const task of spec.tasks ?? []) {
|
|
186
|
-
const patterns = task.shell?.frontendDesignPolicy?.implementationWriteSet;
|
|
187
|
-
if (patterns) {
|
|
188
|
-
for (const pattern of patterns)
|
|
189
|
-
writeSet.add(pattern);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return [...writeSet];
|
|
193
|
-
}
|
|
194
107
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
|
|
195
108
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
|
|
196
|
-
export const FRONTEND_CONTRACT_REVIEW_INDEX_SCHEMA_ID = "frontend-contract-review-index-v1";
|
|
197
|
-
export const FRONTEND_CONTRACT_SECTION_SCHEMA_ID = "frontend-contract-section-v1";
|
|
198
|
-
export const FRONTEND_CONTRACT_CAPACITY_SCHEMA_ID = "frontend-contract-capacity-v1";
|
|
199
|
-
const FRONTEND_CONTRACT_REVIEW_GROUPS = [
|
|
200
|
-
{
|
|
201
|
-
id: "core",
|
|
202
|
-
fields: ["schemaVersion", "sourceBinding", "riskLevel", "targets", "requirements"],
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
id: "ux",
|
|
206
|
-
fields: ["uiStates", "interactions", "uiComponentChoices", "stylingStrategy"],
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
id: "data-design",
|
|
210
|
-
fields: ["mockApi", "designEvidence", "dependencyPolicy", "realIntegrationGap"],
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
id: "verification",
|
|
214
|
-
fields: ["verificationTargets", "evidenceGaps", "implementationSteps", "residualRisks"],
|
|
215
|
-
},
|
|
216
|
-
];
|
|
217
|
-
const FRONTEND_CONTRACT_CAPACITY_WARNING_RATIO = 0.6;
|
|
218
|
-
const FRONTEND_CONTRACT_SECTION_DATA_TARGET_BYTES = 40 * 1024;
|
|
219
109
|
/**
|
|
220
110
|
* Build the deterministic, run-owned portion of the frontend contract. The
|
|
221
111
|
* planner is allowed to fill semantic fields with an RFC 7386 merge patch but
|
|
@@ -281,6 +171,7 @@ export function loadFrontendImplementationContractJsonSchema(startDir = path.dir
|
|
|
281
171
|
"mockApi",
|
|
282
172
|
"designEvidence",
|
|
283
173
|
"verificationTargets",
|
|
174
|
+
"evidenceGaps",
|
|
284
175
|
];
|
|
285
176
|
const actualRequired = Array.isArray(schema.required) ? schema.required : [];
|
|
286
177
|
const missing = expectedRequired.filter((key) => !actualRequired.includes(key));
|
|
@@ -329,13 +220,13 @@ const gap = z
|
|
|
329
220
|
* `specReference` is required (path/section/line hit).
|
|
330
221
|
* - `reuse-existing`: reuse an existing repo component/convention not named by
|
|
331
222
|
* the spec → `specReference` may be null, rationale explains the basis.
|
|
332
|
-
* - `new`:
|
|
333
|
-
*
|
|
334
|
-
* rationale explains the bounded addition.
|
|
223
|
+
* - `new`: neither spec nor existing code fits → `specReference` must be null,
|
|
224
|
+
* rationale must declare the deviation (design-review approves it).
|
|
335
225
|
*
|
|
336
|
-
* `specReference` is nullable + optional
|
|
337
|
-
*
|
|
338
|
-
*
|
|
226
|
+
* `specReference` is nullable + optional because `stripNullValuesDeep` strips
|
|
227
|
+
* null object values before validation, so a `new` choice's `specReference:
|
|
228
|
+
* null` becomes an absent field; the superRefine below treats absent and null
|
|
229
|
+
* identically.
|
|
339
230
|
*/
|
|
340
231
|
const uiComponentSpecReferenceSchema = z
|
|
341
232
|
.object({
|
|
@@ -350,11 +241,7 @@ const uiComponentChoiceSchema = z
|
|
|
350
241
|
component: z.string().min(1),
|
|
351
242
|
decision: z.enum(["specified", "reuse-existing", "new"]),
|
|
352
243
|
specReference: uiComponentSpecReferenceSchema.nullable().optional(),
|
|
353
|
-
|
|
354
|
-
// specReference, new → task-source traceability, reuse-existing → no
|
|
355
|
-
// rationale needed).
|
|
356
|
-
// Omit for reuse-existing to keep plan output small.
|
|
357
|
-
rationale: z.string().min(1).optional(),
|
|
244
|
+
rationale: z.string().min(1),
|
|
358
245
|
})
|
|
359
246
|
.strict();
|
|
360
247
|
const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
|
|
@@ -383,12 +270,7 @@ function stripNullValuesDeep(value) {
|
|
|
383
270
|
return value;
|
|
384
271
|
}
|
|
385
272
|
export const frontendImplementationContractSchema = z
|
|
386
|
-
.preprocess(
|
|
387
|
-
const record = asRecord(value);
|
|
388
|
-
if (!record)
|
|
389
|
-
return stripNullValuesDeep(value);
|
|
390
|
-
return stripNullValuesDeep(record);
|
|
391
|
-
}, z
|
|
273
|
+
.preprocess(stripNullValuesDeep, z
|
|
392
274
|
.object({
|
|
393
275
|
schemaVersion: z.literal(1),
|
|
394
276
|
sourceBinding: z
|
|
@@ -516,7 +398,7 @@ export const frontendImplementationContractSchema = z
|
|
|
516
398
|
uiStates: z.array(z.string().min(1)),
|
|
517
399
|
})
|
|
518
400
|
.strict()).min(1),
|
|
519
|
-
evidenceGaps: z.array(gap)
|
|
401
|
+
evidenceGaps: z.array(gap),
|
|
520
402
|
implementationSteps: z.array(z.string().min(1)).optional(),
|
|
521
403
|
stylingStrategy: z.string().min(1).optional(),
|
|
522
404
|
uiComponentChoices: uiComponentChoicesSchema.optional(),
|
|
@@ -526,37 +408,6 @@ export const frontendImplementationContractSchema = z
|
|
|
526
408
|
})
|
|
527
409
|
.strict()
|
|
528
410
|
.superRefine((value, ctx) => {
|
|
529
|
-
const stableIdentities = [
|
|
530
|
-
{
|
|
531
|
-
path: "requirements",
|
|
532
|
-
label: "requirement id",
|
|
533
|
-
values: value.requirements.map((item) => item.id),
|
|
534
|
-
},
|
|
535
|
-
{
|
|
536
|
-
path: "uiStates",
|
|
537
|
-
label: "UI state name",
|
|
538
|
-
values: value.uiStates.map((item) => item.name),
|
|
539
|
-
},
|
|
540
|
-
{
|
|
541
|
-
path: "interactions",
|
|
542
|
-
label: "interaction name",
|
|
543
|
-
values: value.interactions.map((item) => item.name),
|
|
544
|
-
},
|
|
545
|
-
{
|
|
546
|
-
path: "uiComponentChoices",
|
|
547
|
-
label: "component choice purpose",
|
|
548
|
-
values: (value.uiComponentChoices ?? []).map((item) => item.purpose),
|
|
549
|
-
},
|
|
550
|
-
];
|
|
551
|
-
for (const identity of stableIdentities) {
|
|
552
|
-
if (new Set(identity.values).size !== identity.values.length) {
|
|
553
|
-
ctx.addIssue({
|
|
554
|
-
code: "custom",
|
|
555
|
-
message: `duplicate ${identity.label}`,
|
|
556
|
-
path: [identity.path],
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
411
|
const verificationIds = value.verificationTargets.map((target) => target.id);
|
|
561
412
|
if (new Set(verificationIds).size !== verificationIds.length)
|
|
562
413
|
ctx.addIssue({
|
|
@@ -611,32 +462,18 @@ export const frontendImplementationContractSchema = z
|
|
|
611
462
|
message: `unknown verification target ${targetId}`,
|
|
612
463
|
path: ["requirements"],
|
|
613
464
|
});
|
|
614
|
-
// Interactions bind to VTs too: an interaction whose behavior
|
|
615
|
-
// verification points at an unmaterialized VT is untraceable —
|
|
616
|
-
// r20 review finding (dangling *-BEHAVIOR references sailed
|
|
617
|
-
// through plan/design/implement to the final review).
|
|
618
|
-
for (const interaction of value.interactions)
|
|
619
|
-
for (const targetId of interaction.verificationTargetIds)
|
|
620
|
-
if (!verificationIds.includes(targetId))
|
|
621
|
-
ctx.addIssue({
|
|
622
|
-
code: "custom",
|
|
623
|
-
message: `interaction "${interaction.name}" references unknown verification target ${targetId}`,
|
|
624
|
-
path: ["interactions"],
|
|
625
|
-
});
|
|
626
465
|
// Source fidelity ledger binding (AC-005/AC-006): 绑定携带
|
|
627
466
|
// requirementToFragments(ledger v2)时,每个 requirement 必须携带
|
|
628
|
-
//
|
|
629
|
-
// sourceRefs(fragment→path 的派生展示)不设硬门槛:模型从 binding
|
|
630
|
-
// 只能拿到 fragment id 拿不到权威 path,强制只会诱导编造;防伪造的
|
|
631
|
-
// 真实性校验由 fidelity gate 按 sourceFragmentIds 执行(存在性 +
|
|
632
|
-
// 与 ledger boundFragments 一致)。
|
|
467
|
+
// 有效 sourceFragmentIds/sourceRefs,否则 fail closed(防引用伪造/缺失)。
|
|
633
468
|
const boundFragments = value.sourceBinding.requirementToFragments?.[requirement.id];
|
|
634
469
|
if (value.sourceBinding.ledgerPath &&
|
|
635
470
|
(!requirement.sourceFragmentIds ||
|
|
636
|
-
requirement.sourceFragmentIds.length === 0
|
|
471
|
+
requirement.sourceFragmentIds.length === 0 ||
|
|
472
|
+
!requirement.sourceRefs ||
|
|
473
|
+
requirement.sourceRefs.length === 0)) {
|
|
637
474
|
ctx.addIssue({
|
|
638
475
|
code: "custom",
|
|
639
|
-
message: `requirement ${requirement.id} must carry sourceFragmentIds when a source fidelity ledger is bound`,
|
|
476
|
+
message: `requirement ${requirement.id} must carry sourceFragmentIds/sourceRefs when a source fidelity ledger is bound`,
|
|
640
477
|
path: ["requirements", requirement.id, "sourceFragmentIds"],
|
|
641
478
|
});
|
|
642
479
|
}
|
|
@@ -654,23 +491,12 @@ export const frontendImplementationContractSchema = z
|
|
|
654
491
|
if (state.applicable &&
|
|
655
492
|
(!state.expectedBehavior ||
|
|
656
493
|
!state.implementationTargets?.length ||
|
|
657
|
-
!state.verificationTargetIds?.length))
|
|
658
|
-
// Name the state and the exact missing fields: the fixer is a
|
|
659
|
-
// model iterating on finalize receipts — it cannot fix a
|
|
660
|
-
// defect it cannot locate (r18: 39 blind finalize retries).
|
|
661
|
-
const missing = [];
|
|
662
|
-
if (!state.expectedBehavior)
|
|
663
|
-
missing.push("expectedBehavior");
|
|
664
|
-
if (!state.implementationTargets?.length)
|
|
665
|
-
missing.push("implementationTargets");
|
|
666
|
-
if (!state.verificationTargetIds?.length)
|
|
667
|
-
missing.push("verificationTargetIds");
|
|
494
|
+
!state.verificationTargetIds?.length))
|
|
668
495
|
ctx.addIssue({
|
|
669
496
|
code: "custom",
|
|
670
|
-
message:
|
|
497
|
+
message: "applicable UI state requires behavior, implementation, and verification",
|
|
671
498
|
path: ["uiStates"],
|
|
672
499
|
});
|
|
673
|
-
}
|
|
674
500
|
if (!state.applicable && !state.notApplicableReason)
|
|
675
501
|
ctx.addIssue({
|
|
676
502
|
code: "custom",
|
|
@@ -689,10 +515,10 @@ export const frontendImplementationContractSchema = z
|
|
|
689
515
|
}
|
|
690
516
|
}
|
|
691
517
|
else if (choice.decision === "new") {
|
|
692
|
-
if (
|
|
518
|
+
if (choice.specReference) {
|
|
693
519
|
ctx.addIssue({
|
|
694
520
|
code: "custom",
|
|
695
|
-
message: "new component choice
|
|
521
|
+
message: "new component choice must not carry a specReference",
|
|
696
522
|
path: ["uiComponentChoices", index, "specReference"],
|
|
697
523
|
});
|
|
698
524
|
}
|
|
@@ -1219,40 +1045,11 @@ function deriveFrontendVerificationCoverage(value, canonicalBinding) {
|
|
|
1219
1045
|
])];
|
|
1220
1046
|
const evidenceGap = asRecord(requirement.evidenceGap);
|
|
1221
1047
|
const hasProof = provenRequirementIds.has(requirementId);
|
|
1222
|
-
// A committed evidenceGap with a blank description means "no gap":
|
|
1223
|
-
// small-output models emit the slot defensively with description
|
|
1224
|
-
// "" and the strict gap schema (description min 1 char) would
|
|
1225
|
-
// fail the whole compile. Strip the incoming slot first and only
|
|
1226
|
-
// re-add it when usable — the requirement either has proof (no
|
|
1227
|
-
// gap needed) or receives the derived blocking gap below.
|
|
1228
|
-
const evidenceGapDescription = asString(evidenceGap?.description).trim();
|
|
1229
|
-
const hasUsableEvidenceGap = evidenceGap !== undefined && evidenceGapDescription !== "";
|
|
1230
|
-
// Source fidelity bindings are deterministic ledger data, not
|
|
1231
|
-
// model-authored content: when the plan requirement carries no
|
|
1232
|
-
// binding (r6 — the contract input block degraded, the model
|
|
1233
|
-
// correctly refused to invent ids), inject the ledger binding
|
|
1234
|
-
// for the requirement id. The strict schema fails a ledger-bound
|
|
1235
|
-
// contract on any requirement without sourceFragmentIds.
|
|
1236
|
-
const declaredSourceFragmentIds = Array.isArray(requirement.sourceFragmentIds)
|
|
1237
|
-
? requirement.sourceFragmentIds
|
|
1238
|
-
: undefined;
|
|
1239
|
-
const ledgerBoundFragmentIds = Array.isArray(canonicalBinding.requirementToFragments?.[requirementId])
|
|
1240
|
-
? canonicalBinding.requirementToFragments[requirementId]
|
|
1241
|
-
: undefined;
|
|
1242
|
-
const sourceFragmentIds = declaredSourceFragmentIds && declaredSourceFragmentIds.length > 0
|
|
1243
|
-
? declaredSourceFragmentIds
|
|
1244
|
-
: ledgerBoundFragmentIds;
|
|
1245
|
-
const { evidenceGap: _incomingGap, sourceFragmentIds: _incomingFragmentIds, ...requirementWithoutGap } = requirement;
|
|
1246
|
-
void _incomingGap;
|
|
1247
|
-
void _incomingFragmentIds;
|
|
1248
1048
|
return {
|
|
1249
|
-
...
|
|
1250
|
-
...(sourceFragmentIds ? { sourceFragmentIds } : {}),
|
|
1049
|
+
...requirement,
|
|
1251
1050
|
...(verificationTargetIds.length > 0 ? { verificationTargetIds } : {}),
|
|
1252
1051
|
...(hasProof
|
|
1253
|
-
? (
|
|
1254
|
-
? { evidenceGap: { ...evidenceGap, blocking: false } }
|
|
1255
|
-
: {})
|
|
1052
|
+
? (evidenceGap ? { evidenceGap: { ...evidenceGap, blocking: false } } : {})
|
|
1256
1053
|
: {
|
|
1257
1054
|
evidenceGap: {
|
|
1258
1055
|
requirementId,
|
|
@@ -1264,16 +1061,10 @@ function deriveFrontendVerificationCoverage(value, canonicalBinding) {
|
|
|
1264
1061
|
})
|
|
1265
1062
|
: record.requirements;
|
|
1266
1063
|
const modelEvidenceGaps = Array.isArray(record.evidenceGaps)
|
|
1267
|
-
? record.evidenceGaps
|
|
1268
|
-
.map((item) => {
|
|
1064
|
+
? record.evidenceGaps.map((item) => {
|
|
1269
1065
|
const gap = asRecord(item);
|
|
1270
1066
|
if (!gap)
|
|
1271
1067
|
return item;
|
|
1272
|
-
// Blank-description gaps are meaningless statements that would
|
|
1273
|
-
// fail the strict gap schema; drop them like the embedded
|
|
1274
|
-
// per-requirement empty slots.
|
|
1275
|
-
if (asString(gap.description).trim() === "")
|
|
1276
|
-
return undefined;
|
|
1277
1068
|
const requirementId = canonicalizeRequirementId(asString(gap.requirementId));
|
|
1278
1069
|
// Model gaps are advisory. Blocking status is reconstructed below
|
|
1279
1070
|
// from the source binding and executable verification targets.
|
|
@@ -1286,7 +1077,6 @@ function deriveFrontendVerificationCoverage(value, canonicalBinding) {
|
|
|
1286
1077
|
blocking: false,
|
|
1287
1078
|
};
|
|
1288
1079
|
})
|
|
1289
|
-
.filter((item) => item !== undefined)
|
|
1290
1080
|
: [];
|
|
1291
1081
|
const derivedBlockingGaps = canonicalBinding.requirementIds
|
|
1292
1082
|
.filter((requirementId) => !provenRequirementIds.has(requirementId))
|
|
@@ -1301,114 +1091,6 @@ function deriveFrontendVerificationCoverage(value, canonicalBinding) {
|
|
|
1301
1091
|
evidenceGaps: [...modelEvidenceGaps, ...derivedBlockingGaps],
|
|
1302
1092
|
};
|
|
1303
1093
|
}
|
|
1304
|
-
function dedupeJsonArray(value) {
|
|
1305
|
-
if (!Array.isArray(value))
|
|
1306
|
-
return value;
|
|
1307
|
-
const seen = new Set();
|
|
1308
|
-
return value.filter((item) => {
|
|
1309
|
-
const key = serializeCompactDeterministicJson(item);
|
|
1310
|
-
if (seen.has(key))
|
|
1311
|
-
return false;
|
|
1312
|
-
seen.add(key);
|
|
1313
|
-
return true;
|
|
1314
|
-
});
|
|
1315
|
-
}
|
|
1316
|
-
function compactRecordArrayField(value, fields) {
|
|
1317
|
-
if (!Array.isArray(value))
|
|
1318
|
-
return value;
|
|
1319
|
-
return dedupeJsonArray(value.map((item) => {
|
|
1320
|
-
const record = asRecord(item);
|
|
1321
|
-
if (!record)
|
|
1322
|
-
return item;
|
|
1323
|
-
const compact = { ...record };
|
|
1324
|
-
for (const field of fields) {
|
|
1325
|
-
if (Object.hasOwn(compact, field)) {
|
|
1326
|
-
compact[field] = dedupeJsonArray(compact[field]);
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
return compact;
|
|
1330
|
-
}));
|
|
1331
|
-
}
|
|
1332
|
-
/**
|
|
1333
|
-
* Compact the canonical contract without weakening its schema or coverage
|
|
1334
|
-
* semantics. This is deliberately a closed list of transformations:
|
|
1335
|
-
* byte-identical records and repeated set members are deduplicated, and
|
|
1336
|
-
* mutually exclusive UI-state fields are normalized. Conflicting stable
|
|
1337
|
-
* identities remain visible so strict schema validation can reject them.
|
|
1338
|
-
*/
|
|
1339
|
-
export function compactFrontendImplementationContract(value) {
|
|
1340
|
-
const record = asRecord(value);
|
|
1341
|
-
if (!record)
|
|
1342
|
-
return value;
|
|
1343
|
-
const compact = { ...record };
|
|
1344
|
-
const sourceBinding = asRecord(record.sourceBinding);
|
|
1345
|
-
if (sourceBinding) {
|
|
1346
|
-
const compactBinding = { ...sourceBinding };
|
|
1347
|
-
for (const field of ["referencePaths", "requirementIds"]) {
|
|
1348
|
-
if (Object.hasOwn(compactBinding, field)) {
|
|
1349
|
-
compactBinding[field] = dedupeJsonArray(compactBinding[field]);
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
const fragments = asRecord(sourceBinding.requirementToFragments);
|
|
1353
|
-
if (fragments) {
|
|
1354
|
-
compactBinding.requirementToFragments = Object.fromEntries(Object.entries(fragments).map(([id, ids]) => [id, dedupeJsonArray(ids)]));
|
|
1355
|
-
}
|
|
1356
|
-
compact.sourceBinding = compactBinding;
|
|
1357
|
-
}
|
|
1358
|
-
const targets = asRecord(record.targets);
|
|
1359
|
-
if (targets) {
|
|
1360
|
-
const compactTargets = { ...targets };
|
|
1361
|
-
for (const field of ["files", "routes", "publicApiChanges"]) {
|
|
1362
|
-
if (Object.hasOwn(compactTargets, field)) {
|
|
1363
|
-
compactTargets[field] = dedupeJsonArray(compactTargets[field]);
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
compact.targets = compactTargets;
|
|
1367
|
-
}
|
|
1368
|
-
compact.requirements = compactRecordArrayField(record.requirements, [
|
|
1369
|
-
"implementationTargets",
|
|
1370
|
-
"verificationTargetIds",
|
|
1371
|
-
"sourceFragmentIds",
|
|
1372
|
-
"sourceRefs",
|
|
1373
|
-
]);
|
|
1374
|
-
compact.uiStates = compactRecordArrayField(record.uiStates, ["implementationTargets", "verificationTargetIds"]);
|
|
1375
|
-
if (Array.isArray(compact.uiStates)) {
|
|
1376
|
-
compact.uiStates = dedupeJsonArray(compact.uiStates.map((item) => {
|
|
1377
|
-
const state = asRecord(item);
|
|
1378
|
-
if (!state)
|
|
1379
|
-
return item;
|
|
1380
|
-
const normalized = { ...state };
|
|
1381
|
-
if (normalized.applicable === true)
|
|
1382
|
-
delete normalized.notApplicableReason;
|
|
1383
|
-
if (normalized.applicable === false)
|
|
1384
|
-
delete normalized.expectedBehavior;
|
|
1385
|
-
return normalized;
|
|
1386
|
-
}));
|
|
1387
|
-
}
|
|
1388
|
-
compact.interactions = compactRecordArrayField(record.interactions, ["implementationTargets", "verificationTargetIds"]);
|
|
1389
|
-
compact.verificationTargets = compactRecordArrayField(record.verificationTargets, ["requirementIds", "uiStates"]);
|
|
1390
|
-
const mockApi = asRecord(record.mockApi);
|
|
1391
|
-
if (mockApi) {
|
|
1392
|
-
compact.mockApi = {
|
|
1393
|
-
...mockApi,
|
|
1394
|
-
endpoints: dedupeJsonArray(mockApi.endpoints),
|
|
1395
|
-
};
|
|
1396
|
-
}
|
|
1397
|
-
const designEvidence = asRecord(record.designEvidence);
|
|
1398
|
-
if (designEvidence) {
|
|
1399
|
-
compact.designEvidence = {
|
|
1400
|
-
...designEvidence,
|
|
1401
|
-
paths: dedupeJsonArray(designEvidence.paths),
|
|
1402
|
-
conflicts: dedupeJsonArray(designEvidence.conflicts),
|
|
1403
|
-
};
|
|
1404
|
-
}
|
|
1405
|
-
for (const field of ["evidenceGaps", "implementationSteps", "residualRisks"]) {
|
|
1406
|
-
if (Object.hasOwn(compact, field))
|
|
1407
|
-
compact[field] = dedupeJsonArray(compact[field]);
|
|
1408
|
-
}
|
|
1409
|
-
compact.uiComponentChoices = dedupeJsonArray(record.uiComponentChoices);
|
|
1410
|
-
return compact;
|
|
1411
|
-
}
|
|
1412
1094
|
function assertFrontendContractPathsSafe(value) {
|
|
1413
1095
|
const record = asRecord(value);
|
|
1414
1096
|
if (!record)
|
|
@@ -1484,40 +1166,6 @@ function looksLikeStrictFrontendContract(value) {
|
|
|
1484
1166
|
asRecord(record.mockApi) !== null &&
|
|
1485
1167
|
asRecord(record.designEvidence) !== null);
|
|
1486
1168
|
}
|
|
1487
|
-
/**
|
|
1488
|
-
* Single shared completion layer for required-with-fallback contract fields.
|
|
1489
|
-
*
|
|
1490
|
-
* Structural invariant: the coerce normalizer has strict-shape and free-form
|
|
1491
|
-
* branches that reshape differently, but every required key that carries a
|
|
1492
|
-
* deterministic fallback (currently `designEvidence`) must be completed in
|
|
1493
|
-
* exactly ONE place — here — so the branches can never drift again (the
|
|
1494
|
-
* designEvidence Required and invented-uiStates failures both came from
|
|
1495
|
-
* branch-local rules diverging). This layer never invents values that later
|
|
1496
|
-
* refinements reject: it only fills documented fallbacks for keys already
|
|
1497
|
-
* present-but-partial, and is idempotent for already-complete inputs.
|
|
1498
|
-
*/
|
|
1499
|
-
export function completeCanonicalRequiredFields(value) {
|
|
1500
|
-
const record = asRecord(value);
|
|
1501
|
-
if (!record)
|
|
1502
|
-
return value;
|
|
1503
|
-
const designEvidence = asRecord(record.designEvidence);
|
|
1504
|
-
if (!designEvidence)
|
|
1505
|
-
return value;
|
|
1506
|
-
const source = asString(designEvidence.source);
|
|
1507
|
-
const paths = Array.isArray(designEvidence.paths);
|
|
1508
|
-
const conflicts = Array.isArray(designEvidence.conflicts);
|
|
1509
|
-
if (source && paths && conflicts)
|
|
1510
|
-
return value;
|
|
1511
|
-
return {
|
|
1512
|
-
...record,
|
|
1513
|
-
designEvidence: {
|
|
1514
|
-
...designEvidence,
|
|
1515
|
-
...(source ? {} : { source: "repository-fallback+task-source" }),
|
|
1516
|
-
...(paths ? {} : { paths: [] }),
|
|
1517
|
-
...(conflicts ? {} : { conflicts: [] }),
|
|
1518
|
-
},
|
|
1519
|
-
};
|
|
1520
|
-
}
|
|
1521
1169
|
/**
|
|
1522
1170
|
* Coerce common free-form plan JSON into frontend-implementation-contract-v1.
|
|
1523
1171
|
* Near-schema payloads are left untouched so unknown-key fail-closed still holds.
|
|
@@ -1622,9 +1270,6 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1622
1270
|
...strictRecord,
|
|
1623
1271
|
requirements,
|
|
1624
1272
|
...(strictMockApi ? { mockApi: { ...strictMockApi, endpoints: mockEndpoints } } : {}),
|
|
1625
|
-
// Required-with-fallback keys (designEvidence etc.) are completed
|
|
1626
|
-
// by the single shared `completeCanonicalRequiredFields` layer in
|
|
1627
|
-
// the analyze pipeline — never per-branch.
|
|
1628
1273
|
};
|
|
1629
1274
|
}
|
|
1630
1275
|
}
|
|
@@ -1673,7 +1318,9 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1673
1318
|
: typeRaw.includes("type")
|
|
1674
1319
|
? "static"
|
|
1675
1320
|
: "unit";
|
|
1676
|
-
const commandLabel = asString(vt.commandLabel) ||
|
|
1321
|
+
const commandLabel = asString(vt.commandLabel) ||
|
|
1322
|
+
asString(vt.command) ||
|
|
1323
|
+
(type === "static" ? "npm run typecheck" : "npm run test:unit:fe");
|
|
1677
1324
|
const file = asString(vt.file) ||
|
|
1678
1325
|
(Array.isArray(vt.symbols) ? targetFiles.find((p) => p.includes("__tests__")) : "") ||
|
|
1679
1326
|
targetFiles.find((p) => p.includes("__tests__")) ||
|
|
@@ -1689,11 +1336,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1689
1336
|
requirementIds: requirementIds.length > 0
|
|
1690
1337
|
? requirementIds
|
|
1691
1338
|
: [...canonicalBinding.requirementIds],
|
|
1692
|
-
|
|
1693
|
-
// state name, so a defaulted ["success","error"] was always rejected
|
|
1694
|
-
// by the schema's unknown-uiState refinement — a verification
|
|
1695
|
-
// target that declares no UI binding stays unbound ([]).
|
|
1696
|
-
uiStates: uiStateNames,
|
|
1339
|
+
uiStates: uiStateNames.length > 0 ? uiStateNames : ["success", "error"],
|
|
1697
1340
|
});
|
|
1698
1341
|
}
|
|
1699
1342
|
if (verificationTargets.length === 0) {
|
|
@@ -1728,14 +1371,6 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1728
1371
|
verificationTargetIds: verificationTargetIds.length > 0
|
|
1729
1372
|
? verificationTargetIds
|
|
1730
1373
|
: defaultVerificationIds,
|
|
1731
|
-
// Source fidelity provenance 透传:兼容分支重建 requirement 时必须
|
|
1732
|
-
// 保留已声明的 sourceFragmentIds/sourceRefs(strict 分支通过
|
|
1733
|
-
// `...requirement` 保留,两边必须一致),否则 ledger-bound 契约在
|
|
1734
|
-
// designEvidence 缺失等情况下走兼容分支时丢失防伪造证据。
|
|
1735
|
-
...(Array.isArray(req.sourceFragmentIds)
|
|
1736
|
-
? { sourceFragmentIds: req.sourceFragmentIds }
|
|
1737
|
-
: {}),
|
|
1738
|
-
...(Array.isArray(req.sourceRefs) ? { sourceRefs: req.sourceRefs } : {}),
|
|
1739
1374
|
};
|
|
1740
1375
|
// Do not mark free-form realIntegrationGap as blocking; defer to FE-TEST/FINAL-VERIFY.
|
|
1741
1376
|
if (gapText) {
|
|
@@ -2029,25 +1664,12 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2029
1664
|
if (parsedTargetFiles.some((file) => file.startsWith("/") || file.includes("\\")))
|
|
2030
1665
|
fail("blocked", "invalid-output: frontend contract target paths must be relative POSIX paths", candidateJsonSha256);
|
|
2031
1666
|
const parsedStates = asRecord(parsed)?.uiStates;
|
|
2032
|
-
if (Array.isArray(parsedStates)) {
|
|
2033
|
-
const
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
const missing = [];
|
|
2039
|
-
if (!asString(state.expectedBehavior))
|
|
2040
|
-
missing.push("expectedBehavior");
|
|
2041
|
-
if (asStringArray(state.implementationTargets).length === 0)
|
|
2042
|
-
missing.push("implementationTargets");
|
|
2043
|
-
if (asStringArray(state.verificationTargetIds).length === 0)
|
|
2044
|
-
missing.push("verificationTargetIds");
|
|
2045
|
-
if (missing.length > 0)
|
|
2046
|
-
incompleteStates.push(`"${asString(state.name)}": missing ${missing.join(", ")}`);
|
|
2047
|
-
}
|
|
2048
|
-
if (incompleteStates.length > 0)
|
|
2049
|
-
fail("retryable-invalid", `invalid-output: applicable UI states incomplete — re-record each with record_state_flow filling the named fields: ${incompleteStates.join("; ")}`, candidateJsonSha256);
|
|
2050
|
-
}
|
|
1667
|
+
if (Array.isArray(parsedStates) && parsedStates.some((item) => {
|
|
1668
|
+
const state = asRecord(item);
|
|
1669
|
+
return state?.applicable === true &&
|
|
1670
|
+
(!asString(state.expectedBehavior) || asStringArray(state.implementationTargets).length === 0 || asStringArray(state.verificationTargetIds).length === 0);
|
|
1671
|
+
}))
|
|
1672
|
+
fail("retryable-invalid", "invalid-output: applicable UI state requires behavior, implementation, and verification", candidateJsonSha256);
|
|
2051
1673
|
const parsedMockApi = asRecord(parsed)?.mockApi;
|
|
2052
1674
|
if (asRecord(parsedMockApi) &&
|
|
2053
1675
|
typeof asRecord(parsedMockApi)?.strategy === "string" &&
|
|
@@ -2064,49 +1686,15 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2064
1686
|
// There is exactly one post-security candidate. A fallback candidate would
|
|
2065
1687
|
// allow malformed raw fields to bypass the boundary checks above.
|
|
2066
1688
|
const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
|
|
2067
|
-
const completedContract = completeCanonicalRequiredFields(normalizedContract);
|
|
2068
1689
|
const candidate = deriveFrontendVerificationCoverage({
|
|
2069
|
-
...(asRecord(
|
|
1690
|
+
...(asRecord(normalizedContract) ?? parsed),
|
|
2070
1691
|
sourceBinding: canonicalBinding,
|
|
2071
1692
|
}, canonicalBinding);
|
|
2072
|
-
const
|
|
2073
|
-
if (serializeCompactDeterministicJson(compactedCandidate) !==
|
|
2074
|
-
serializeCompactDeterministicJson(candidate))
|
|
2075
|
-
pushAction("compact-canonical-contract");
|
|
2076
|
-
const result = frontendImplementationContractSchema.safeParse(compactedCandidate);
|
|
1693
|
+
const result = frontendImplementationContractSchema.safeParse(candidate);
|
|
2077
1694
|
if (!result.success)
|
|
2078
1695
|
fail("retryable-invalid", `invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`, candidateJsonSha256);
|
|
2079
|
-
// targets.files is runtime-owned (the skeleton derives it from the task
|
|
2080
|
-
// writeSet, a glob the model must not edit). Narrow it AFTER the full
|
|
2081
|
-
// schema validation — never before, so model-authored unsafe/absolute
|
|
2082
|
-
// declarations are still rejected — to every CONCRETE path the validated
|
|
2083
|
-
// contract references (requirement implementationTargets,
|
|
2084
|
-
// verification-target files, Mock endpoint fixture/consumer paths). The
|
|
2085
|
-
// prewrite containment semantics keep covering everything the contract
|
|
2086
|
-
// references because every narrowed entry already matched the declared
|
|
2087
|
-
// patterns during validation. Satisfies frozen requirements like "deliver
|
|
2088
|
-
// exactly these files" without asking the model to edit a protected
|
|
2089
|
-
// field (r10/r11 design-review findings).
|
|
2090
|
-
const referencedConcretePaths = [
|
|
2091
|
-
...new Set([
|
|
2092
|
-
...result.data.requirements.flatMap((requirement) => requirement.implementationTargets),
|
|
2093
|
-
// Mock endpoint paths stay in the set: the zod refine validated
|
|
2094
|
-
// them against the declared targets.files patterns, so the
|
|
2095
|
-
// narrowing must keep covering them. Verification-target files
|
|
2096
|
-
// deliberately do NOT join: they are test artifacts, not
|
|
2097
|
-
// deliverables, and pulling them in would change which gate
|
|
2098
|
-
// fires for an out-of-writeSet VT (AC-3a semantics).
|
|
2099
|
-
...(result.data.mockApi?.endpoints ?? []).flatMap((endpoint) => [endpoint.fixture, endpoint.consumer].filter((value) => typeof value === "string" && value.length > 0)),
|
|
2100
|
-
].filter((value) => value.length > 0)),
|
|
2101
|
-
].sort();
|
|
2102
|
-
if (referencedConcretePaths.length > 0) {
|
|
2103
|
-
result.data.targets = {
|
|
2104
|
-
...result.data.targets,
|
|
2105
|
-
files: referencedConcretePaths,
|
|
2106
|
-
};
|
|
2107
|
-
}
|
|
2108
1696
|
const blockingGaps = [
|
|
2109
|
-
...
|
|
1697
|
+
...result.data.evidenceGaps,
|
|
2110
1698
|
...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
|
|
2111
1699
|
].filter((item) => item.blocking);
|
|
2112
1700
|
if (blockingGaps.length > 0)
|
|
@@ -2115,7 +1703,7 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2115
1703
|
.join(", ")}`, candidateJsonSha256);
|
|
2116
1704
|
for (const requirementId of canonicalBinding.requirementIds)
|
|
2117
1705
|
if (!result.data.requirements.some((item) => item.id === requirementId) &&
|
|
2118
|
-
!
|
|
1706
|
+
!result.data.evidenceGaps.some((item) => item.requirementId === requirementId))
|
|
2119
1707
|
fail("blocked", `frontend contract does not cover ${requirementId}`, candidateJsonSha256);
|
|
2120
1708
|
return {
|
|
2121
1709
|
canonical: result.data,
|
|
@@ -2124,211 +1712,12 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2124
1712
|
normalizationActions,
|
|
2125
1713
|
};
|
|
2126
1714
|
}
|
|
2127
|
-
function frontendCapacityEntry(consumer, maxFiles, maxBytes, contractBytes, sectionCount) {
|
|
2128
|
-
const utilizationRatio = contractBytes / maxBytes;
|
|
2129
|
-
const indexedFiles = sectionCount + 2;
|
|
2130
|
-
const byteLevel = utilizationRatio >= 1
|
|
2131
|
-
? "over-budget"
|
|
2132
|
-
: utilizationRatio >= FRONTEND_CONTRACT_CAPACITY_WARNING_RATIO
|
|
2133
|
-
? "warning"
|
|
2134
|
-
: "ok";
|
|
2135
|
-
return {
|
|
2136
|
-
consumer,
|
|
2137
|
-
maxFiles,
|
|
2138
|
-
maxBytes,
|
|
2139
|
-
contractBytes,
|
|
2140
|
-
indexedFiles,
|
|
2141
|
-
headroomBytes: Math.max(0, maxBytes - contractBytes),
|
|
2142
|
-
utilizationRatio: Number(utilizationRatio.toFixed(4)),
|
|
2143
|
-
level: byteLevel !== "ok" && indexedFiles > maxFiles
|
|
2144
|
-
? "over-budget"
|
|
2145
|
-
: byteLevel,
|
|
2146
|
-
recommendedReadMode: byteLevel === "ok"
|
|
2147
|
-
? "full-contract"
|
|
2148
|
-
: "indexed-sections",
|
|
2149
|
-
};
|
|
2150
|
-
}
|
|
2151
|
-
function splitFrontendContractReviewGroup(canonical, fields) {
|
|
2152
|
-
const parts = [];
|
|
2153
|
-
let data = {};
|
|
2154
|
-
let slices = [];
|
|
2155
|
-
const dataBytes = (candidate) => Buffer.byteLength(serializeCompactDeterministicJson(candidate), "utf8");
|
|
2156
|
-
const flush = () => {
|
|
2157
|
-
if (Object.keys(data).length === 0)
|
|
2158
|
-
return;
|
|
2159
|
-
parts.push({ data, slices });
|
|
2160
|
-
data = {};
|
|
2161
|
-
slices = [];
|
|
2162
|
-
};
|
|
2163
|
-
for (const field of fields) {
|
|
2164
|
-
if (!Object.hasOwn(canonical, field))
|
|
2165
|
-
continue;
|
|
2166
|
-
const fieldValue = canonical[field];
|
|
2167
|
-
if (!Array.isArray(fieldValue)) {
|
|
2168
|
-
const candidate = { ...data, [field]: fieldValue };
|
|
2169
|
-
if (Object.keys(data).length > 0 &&
|
|
2170
|
-
dataBytes(candidate) > FRONTEND_CONTRACT_SECTION_DATA_TARGET_BYTES) {
|
|
2171
|
-
flush();
|
|
2172
|
-
}
|
|
2173
|
-
data[field] = fieldValue;
|
|
2174
|
-
slices.push({ field });
|
|
2175
|
-
continue;
|
|
2176
|
-
}
|
|
2177
|
-
if (fieldValue.length === 0) {
|
|
2178
|
-
const candidate = { ...data, [field]: [] };
|
|
2179
|
-
if (Object.keys(data).length > 0 &&
|
|
2180
|
-
dataBytes(candidate) > FRONTEND_CONTRACT_SECTION_DATA_TARGET_BYTES) {
|
|
2181
|
-
flush();
|
|
2182
|
-
}
|
|
2183
|
-
data[field] = [];
|
|
2184
|
-
slices.push({ field, start: 0, end: 0, total: 0 });
|
|
2185
|
-
continue;
|
|
2186
|
-
}
|
|
2187
|
-
let start = 0;
|
|
2188
|
-
while (start < fieldValue.length) {
|
|
2189
|
-
let end = start;
|
|
2190
|
-
let accepted = [];
|
|
2191
|
-
while (end < fieldValue.length) {
|
|
2192
|
-
const next = [...accepted, fieldValue[end]];
|
|
2193
|
-
const candidate = { ...data, [field]: next };
|
|
2194
|
-
if (accepted.length > 0 &&
|
|
2195
|
-
dataBytes(candidate) > FRONTEND_CONTRACT_SECTION_DATA_TARGET_BYTES) {
|
|
2196
|
-
break;
|
|
2197
|
-
}
|
|
2198
|
-
if (accepted.length === 0 &&
|
|
2199
|
-
Object.keys(data).length > 0 &&
|
|
2200
|
-
dataBytes(candidate) > FRONTEND_CONTRACT_SECTION_DATA_TARGET_BYTES) {
|
|
2201
|
-
flush();
|
|
2202
|
-
continue;
|
|
2203
|
-
}
|
|
2204
|
-
accepted = next;
|
|
2205
|
-
end += 1;
|
|
2206
|
-
}
|
|
2207
|
-
data[field] = accepted;
|
|
2208
|
-
slices.push({ field, start, end, total: fieldValue.length });
|
|
2209
|
-
start = end;
|
|
2210
|
-
if (start < fieldValue.length)
|
|
2211
|
-
flush();
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
flush();
|
|
2215
|
-
return parts;
|
|
2216
|
-
}
|
|
2217
|
-
/**
|
|
2218
|
-
* Materialize a hash-bound field index and bounded contract sections.
|
|
2219
|
-
* These artifacts are an alternate read surface only: the canonical contract
|
|
2220
|
-
* remains authoritative and capacity diagnostics never affect admission.
|
|
2221
|
-
*/
|
|
2222
|
-
export async function writeFrontendContractReviewArtifacts(input) {
|
|
2223
|
-
const contractRelativePath = path.posix.join(input.outputDir, input.artifactName);
|
|
2224
|
-
const baseName = input.artifactName.endsWith(".json")
|
|
2225
|
-
? input.artifactName.slice(0, -".json".length)
|
|
2226
|
-
: input.artifactName;
|
|
2227
|
-
const sectionDir = path.posix.join(input.outputDir, `${baseName}.sections`);
|
|
2228
|
-
const indexRelativePath = path.posix.join(input.outputDir, `${baseName}.index.json`);
|
|
2229
|
-
const capacityRelativePath = path.posix.join(input.outputDir, `${baseName}.capacity.json`);
|
|
2230
|
-
const compactContract = `${serializeCompactDeterministicJson(input.canonical)}\n`;
|
|
2231
|
-
const canonicalArtifact = input.canonicalArtifact ?? {
|
|
2232
|
-
path: path.join(input.runDir, contractRelativePath),
|
|
2233
|
-
sha256: sha256Hex(compactContract),
|
|
2234
|
-
bytes: Buffer.byteLength(compactContract, "utf8"),
|
|
2235
|
-
};
|
|
2236
|
-
const sections = [];
|
|
2237
|
-
for (const [index, group] of FRONTEND_CONTRACT_REVIEW_GROUPS.entries()) {
|
|
2238
|
-
const parts = splitFrontendContractReviewGroup(input.canonical, group.fields);
|
|
2239
|
-
for (const [partIndex, part] of parts.entries()) {
|
|
2240
|
-
const relativePath = path.posix.join(sectionDir, `${String(index + 1).padStart(2, "0")}-${group.id}-part-${String(partIndex + 1).padStart(2, "0")}.json`);
|
|
2241
|
-
const sectionPayload = {
|
|
2242
|
-
schemaVersion: 1,
|
|
2243
|
-
schemaId: FRONTEND_CONTRACT_SECTION_SCHEMA_ID,
|
|
2244
|
-
contractSchemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
2245
|
-
contractSha256: canonicalArtifact.sha256,
|
|
2246
|
-
group: group.id,
|
|
2247
|
-
part: partIndex + 1,
|
|
2248
|
-
partCount: parts.length,
|
|
2249
|
-
fields: Object.keys(part.data),
|
|
2250
|
-
slices: part.slices,
|
|
2251
|
-
data: part.data,
|
|
2252
|
-
};
|
|
2253
|
-
const written = await writeCompactDeterministicJsonArtifact(input.runDir, relativePath, sectionPayload);
|
|
2254
|
-
sections.push({
|
|
2255
|
-
id: group.id,
|
|
2256
|
-
fields: Object.keys(part.data),
|
|
2257
|
-
part: partIndex + 1,
|
|
2258
|
-
partCount: parts.length,
|
|
2259
|
-
slices: part.slices,
|
|
2260
|
-
path: relativePath,
|
|
2261
|
-
sha256: written.sha256,
|
|
2262
|
-
bytes: Buffer.byteLength(`${serializeCompactDeterministicJson(sectionPayload)}\n`, "utf8"),
|
|
2263
|
-
});
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
const indexPayload = {
|
|
2267
|
-
schemaVersion: 1,
|
|
2268
|
-
schemaId: FRONTEND_CONTRACT_REVIEW_INDEX_SCHEMA_ID,
|
|
2269
|
-
contract: {
|
|
2270
|
-
path: contractRelativePath,
|
|
2271
|
-
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
2272
|
-
sha256: canonicalArtifact.sha256,
|
|
2273
|
-
bytes: canonicalArtifact.bytes,
|
|
2274
|
-
},
|
|
2275
|
-
sections,
|
|
2276
|
-
};
|
|
2277
|
-
const writtenIndex = await writeCompactDeterministicJsonArtifact(input.runDir, indexRelativePath, indexPayload);
|
|
2278
|
-
const consumers = [
|
|
2279
|
-
frontendCapacityEntry("design-review", FRONTEND_READ_BUDGETS.designReview.maxFiles, FRONTEND_READ_BUDGETS.designReview.maxBytes, canonicalArtifact.bytes, sections.length),
|
|
2280
|
-
frontendCapacityEntry("final-review", FRONTEND_READ_BUDGETS.finalReview.maxFiles, FRONTEND_READ_BUDGETS.finalReview.maxBytes, canonicalArtifact.bytes, sections.length),
|
|
2281
|
-
];
|
|
2282
|
-
const capacityPayload = {
|
|
2283
|
-
schemaVersion: 1,
|
|
2284
|
-
schemaId: FRONTEND_CONTRACT_CAPACITY_SCHEMA_ID,
|
|
2285
|
-
nonBlocking: true,
|
|
2286
|
-
warningThresholdRatio: FRONTEND_CONTRACT_CAPACITY_WARNING_RATIO,
|
|
2287
|
-
contract: indexPayload.contract,
|
|
2288
|
-
index: {
|
|
2289
|
-
path: indexRelativePath,
|
|
2290
|
-
sha256: writtenIndex.sha256,
|
|
2291
|
-
sectionCount: sections.length,
|
|
2292
|
-
sectionBytes: sections.reduce((total, section) => total + section.bytes, 0),
|
|
2293
|
-
maxSectionBytes: Math.max(...sections.map((section) => section.bytes)),
|
|
2294
|
-
oversizedSectionCount: sections.filter((section) => section.bytes >= 48 * 1024).length,
|
|
2295
|
-
},
|
|
2296
|
-
consumers,
|
|
2297
|
-
overallLevel: consumers.some((entry) => entry.level === "over-budget")
|
|
2298
|
-
? "over-budget"
|
|
2299
|
-
: consumers.some((entry) => entry.level === "warning")
|
|
2300
|
-
? "warning"
|
|
2301
|
-
: "ok",
|
|
2302
|
-
};
|
|
2303
|
-
const writtenCapacity = await writeCompactDeterministicJsonArtifact(input.runDir, capacityRelativePath, capacityPayload);
|
|
2304
|
-
return {
|
|
2305
|
-
index: {
|
|
2306
|
-
path: writtenIndex.path,
|
|
2307
|
-
relativePath: indexRelativePath,
|
|
2308
|
-
artifactSha256: writtenIndex.sha256,
|
|
2309
|
-
...indexPayload,
|
|
2310
|
-
},
|
|
2311
|
-
capacity: {
|
|
2312
|
-
path: writtenCapacity.path,
|
|
2313
|
-
relativePath: capacityRelativePath,
|
|
2314
|
-
artifactSha256: writtenCapacity.sha256,
|
|
2315
|
-
...capacityPayload,
|
|
2316
|
-
},
|
|
2317
|
-
};
|
|
2318
|
-
}
|
|
2319
1715
|
export async function writeFrontendImplementationContractArtifact(input) {
|
|
2320
|
-
const written = await
|
|
2321
|
-
const bytes = Buffer.byteLength(`${serializeCompactDeterministicJson(input.canonical)}\n`, "utf8");
|
|
2322
|
-
const reviewArtifacts = await writeFrontendContractReviewArtifacts({
|
|
2323
|
-
...input,
|
|
2324
|
-
canonicalArtifact: { ...written, bytes },
|
|
2325
|
-
});
|
|
1716
|
+
const written = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(input.outputDir, input.artifactName), input.canonical);
|
|
2326
1717
|
return {
|
|
2327
1718
|
path: written.path,
|
|
2328
1719
|
sha256: written.sha256,
|
|
2329
1720
|
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
2330
|
-
reviewIndexPath: reviewArtifacts.index.path,
|
|
2331
|
-
capacityDiagnosticPath: reviewArtifacts.capacity.path,
|
|
2332
1721
|
};
|
|
2333
1722
|
}
|
|
2334
1723
|
const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
@@ -2338,40 +1727,6 @@ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
|
2338
1727
|
"/targets/files",
|
|
2339
1728
|
"/mockApi/productionDefaultOff",
|
|
2340
1729
|
];
|
|
2341
|
-
const PLAN_LEDGER_FACT_KINDS_FOR_COMPILE = [
|
|
2342
|
-
"target-surface",
|
|
2343
|
-
"component-choice",
|
|
2344
|
-
"state-flow",
|
|
2345
|
-
"data-flow",
|
|
2346
|
-
"mock-api",
|
|
2347
|
-
"design-deviation",
|
|
2348
|
-
"dependency",
|
|
2349
|
-
"plan-requirement",
|
|
2350
|
-
"plan-verification-target",
|
|
2351
|
-
"plan-evidence-gap",
|
|
2352
|
-
];
|
|
2353
|
-
/**
|
|
2354
|
-
* Restore the editable RFC 7386 plan patch from a committed plan ledger.
|
|
2355
|
-
* This is the single compile authority for the frontend implementation
|
|
2356
|
-
* contract: the tool payload (`patch` on any committed plan fact) wins over
|
|
2357
|
-
* any narrative JSON in the plan node text.
|
|
2358
|
-
*/
|
|
2359
|
-
export function restorePlanPatchFromCommittedFacts(records) {
|
|
2360
|
-
const planFacts = records
|
|
2361
|
-
.filter((record) => record.phase === "committed")
|
|
2362
|
-
.map((record) => record.fact)
|
|
2363
|
-
.filter((fact) => fact.origin === "plan" &&
|
|
2364
|
-
typeof fact.kind === "string" &&
|
|
2365
|
-
PLAN_LEDGER_FACT_KINDS_FOR_COMPILE.includes(fact.kind));
|
|
2366
|
-
if (planFacts.length === 0)
|
|
2367
|
-
return undefined;
|
|
2368
|
-
for (const fact of planFacts) {
|
|
2369
|
-
const patch = fact.patch;
|
|
2370
|
-
if (isPlainObject(patch))
|
|
2371
|
-
return patch;
|
|
2372
|
-
}
|
|
2373
|
-
return undefined;
|
|
2374
|
-
}
|
|
2375
1730
|
function frontendPlanPatchProtectedPathViolations(patch) {
|
|
2376
1731
|
const record = asRecord(patch);
|
|
2377
1732
|
if (!record)
|
|
@@ -2401,34 +1756,34 @@ function workspaceRootFromDagRunDir(runDir) {
|
|
|
2401
1756
|
const joined = segments.slice(0, harnessIndex).join(path.sep);
|
|
2402
1757
|
return joined || path.parse(absolute).root;
|
|
2403
1758
|
}
|
|
1759
|
+
function verificationSymbolMatchesFile(symbol, content) {
|
|
1760
|
+
if (symbol.trim().toLowerCase() === "all describe blocks")
|
|
1761
|
+
return content.includes("describe(");
|
|
1762
|
+
const symbols = symbol.includes(" / ")
|
|
1763
|
+
? symbol
|
|
1764
|
+
.split(/[((]/, 1)[0]
|
|
1765
|
+
.split("/")
|
|
1766
|
+
.map((item) => item.trim())
|
|
1767
|
+
.filter(Boolean)
|
|
1768
|
+
: [symbol];
|
|
1769
|
+
return symbols.every((item) => {
|
|
1770
|
+
const describeTitle = item.match(/^describe\((?:['"])(.+?)(?:['"])/i)?.[1];
|
|
1771
|
+
return [item, describeTitle, describeTitle ? "describe(" : undefined]
|
|
1772
|
+
.filter((candidate) => Boolean(candidate))
|
|
1773
|
+
.some((candidate) => content.includes(candidate));
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
2404
1776
|
async function dropUnresolvedVerificationSymbols(input) {
|
|
2405
1777
|
const workspaceRoot = workspaceRootFromDagRunDir(input.runDir);
|
|
2406
1778
|
const targets = asRecord(input.contract)?.verificationTargets;
|
|
2407
1779
|
if (!workspaceRoot || !Array.isArray(targets))
|
|
2408
|
-
return
|
|
2409
|
-
|
|
1780
|
+
return false;
|
|
1781
|
+
let changed = false;
|
|
2410
1782
|
for (const targetValue of targets) {
|
|
2411
1783
|
const target = asRecord(targetValue);
|
|
2412
1784
|
const file = asString(target?.file);
|
|
2413
1785
|
const symbol = asString(target?.symbol);
|
|
2414
|
-
if (!target || !file)
|
|
2415
|
-
continue;
|
|
2416
|
-
// Static targets (project-wide commands such as typecheck/build) verify
|
|
2417
|
-
// by command exit code, never by a per-file symbol. Strip any symbol
|
|
2418
|
-
// the planner attached — including fabricated placeholders like "null"
|
|
2419
|
-
// — so the plan shape check and the verify trace gate never evaluate
|
|
2420
|
-
// it. This is the root fix for the recurring fabricated-symbol error
|
|
2421
|
-
// class: the field was allowed for static, the prompt did not forbid
|
|
2422
|
-
// it, and models habitually fill it; the compiler removes it instead.
|
|
2423
|
-
if (target.type === "static") {
|
|
2424
|
-
if (symbol) {
|
|
2425
|
-
delete target.symbol;
|
|
2426
|
-
if (!actions.includes("strip-static-verification-symbols"))
|
|
2427
|
-
actions.push("strip-static-verification-symbols");
|
|
2428
|
-
}
|
|
2429
|
-
continue;
|
|
2430
|
-
}
|
|
2431
|
-
if (!symbol)
|
|
1786
|
+
if (!target || !file || !symbol)
|
|
2432
1787
|
continue;
|
|
2433
1788
|
const absolute = path.resolve(workspaceRoot, file);
|
|
2434
1789
|
const relative = path.relative(workspaceRoot, absolute);
|
|
@@ -2437,11 +1792,6 @@ async function dropUnresolvedVerificationSymbols(input) {
|
|
|
2437
1792
|
relative.includes("..")) {
|
|
2438
1793
|
continue;
|
|
2439
1794
|
}
|
|
2440
|
-
// Configuration files (tsconfig.json, vite.config.*, ...) do not expose
|
|
2441
|
-
// source symbols; legacy symbol normalization leaves them alone. Runtime
|
|
2442
|
-
// trace binding handles static targets through file + command only.
|
|
2443
|
-
if (isConfigurationVerificationFile(file))
|
|
2444
|
-
continue;
|
|
2445
1795
|
let content;
|
|
2446
1796
|
try {
|
|
2447
1797
|
content = await readFile(absolute, "utf8");
|
|
@@ -2451,13 +1801,23 @@ async function dropUnresolvedVerificationSymbols(input) {
|
|
|
2451
1801
|
// changing the optional symbol during candidate normalization.
|
|
2452
1802
|
continue;
|
|
2453
1803
|
}
|
|
2454
|
-
if (
|
|
1804
|
+
if (verificationSymbolMatchesFile(symbol, content))
|
|
2455
1805
|
continue;
|
|
2456
1806
|
delete target.symbol;
|
|
2457
|
-
|
|
2458
|
-
actions.push("drop-unresolved-verification-symbols");
|
|
1807
|
+
changed = true;
|
|
2459
1808
|
}
|
|
2460
|
-
return
|
|
1809
|
+
return changed;
|
|
1810
|
+
}
|
|
1811
|
+
function extractSingleOpenspecCitationsBlock(text) {
|
|
1812
|
+
// Line-anchored so a directly closed empty fence (no candidate rows) still
|
|
1813
|
+
// counts as the one required block instead of "found 0" retry loops.
|
|
1814
|
+
const blocks = [
|
|
1815
|
+
...text.matchAll(/^```openspec-citations[ \t]*\r?\n([\s\S]*?)^```[ \t]*$/gim),
|
|
1816
|
+
].map((match) => match[0]);
|
|
1817
|
+
if (blocks.length !== 1) {
|
|
1818
|
+
throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
|
|
1819
|
+
}
|
|
1820
|
+
return blocks[0];
|
|
2461
1821
|
}
|
|
2462
1822
|
async function writeFrontendPlanCandidateRaw(input) {
|
|
2463
1823
|
const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
|
|
@@ -2471,118 +1831,6 @@ async function writeFrontendPlanCandidateRaw(input) {
|
|
|
2471
1831
|
* prose. A successful result returns a path-only pointer plus the hash-bound canonical artifact for
|
|
2472
1832
|
* downstream review and prewrite nodes.
|
|
2473
1833
|
*/
|
|
2474
|
-
/**
|
|
2475
|
-
* Placeholder tokens that are never valid verification symbols ("null" is the
|
|
2476
|
-
* classic model artifact: it serializes a JS null into the symbol string).
|
|
2477
|
-
*/
|
|
2478
|
-
const VERIFICATION_SYMBOL_PLACEHOLDERS = new Set([
|
|
2479
|
-
"null",
|
|
2480
|
-
"undefined",
|
|
2481
|
-
"none",
|
|
2482
|
-
"n/a",
|
|
2483
|
-
"na",
|
|
2484
|
-
"tbd",
|
|
2485
|
-
"todo",
|
|
2486
|
-
"...",
|
|
2487
|
-
"?",
|
|
2488
|
-
"-",
|
|
2489
|
-
]);
|
|
2490
|
-
/** An over-long symbol can never resolve to a real declaration. */
|
|
2491
|
-
const VERIFICATION_SYMBOL_MAX_CHARS = 60;
|
|
2492
|
-
/**
|
|
2493
|
-
* Deterministic shape check for a verification target symbol. Catches obvious
|
|
2494
|
-
* model fabrication (placeholder tokens, prose fragments) when reading a
|
|
2495
|
-
* legacy contract. New plan tool calls do not expose this field; runtime trace
|
|
2496
|
-
* binding uses the stable verification-target id instead.
|
|
2497
|
-
*
|
|
2498
|
-
* Deliberately conservative: bare test titles without punctuation ("renders
|
|
2499
|
-
* loading") and `describe(...)` / `it(...)` forms stay valid — a real symbol
|
|
2500
|
-
* may be a function name, a dotted path, or a describe/it title.
|
|
2501
|
-
*/
|
|
2502
|
-
export function isSuspiciousVerificationSymbol(symbol) {
|
|
2503
|
-
const trimmed = symbol.trim();
|
|
2504
|
-
if (!trimmed)
|
|
2505
|
-
return false;
|
|
2506
|
-
if (VERIFICATION_SYMBOL_PLACEHOLDERS.has(trimmed.toLowerCase()))
|
|
2507
|
-
return true;
|
|
2508
|
-
if (trimmed.length > VERIFICATION_SYMBOL_MAX_CHARS)
|
|
2509
|
-
return true;
|
|
2510
|
-
// Deliberately conservative: bare test titles with punctuation (e.g.
|
|
2511
|
-
// "adds +1 button") are valid describe/it-style symbols and must pass;
|
|
2512
|
-
// the length + placeholder rules above already catch obvious fabrication
|
|
2513
|
-
// ("null", over-long prose fragments). Everything else is left to the
|
|
2514
|
-
// deterministic drop/trace resolvability check.
|
|
2515
|
-
return false;
|
|
2516
|
-
}
|
|
2517
|
-
function assertVerificationSymbolShapes(contract) {
|
|
2518
|
-
for (const target of contract.verificationTargets) {
|
|
2519
|
-
if (!target.symbol)
|
|
2520
|
-
continue;
|
|
2521
|
-
if (isSuspiciousVerificationSymbol(target.symbol)) {
|
|
2522
|
-
throw new Error(`frontend plan verification target "${target.id}" declares a fabricated legacy symbol "${target.symbol}" for ${target.file}; remove the symbol (deterministic trace binding uses the stable target id)`);
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
/**
|
|
2527
|
-
* Thrown by analyzeFrontendPlanPatchCandidate when the design-policy
|
|
2528
|
-
* pre-checks report findings. Carries the structured findings so callers
|
|
2529
|
-
* (finalize_plan receipt) can synthesize fix suggestions instead of making
|
|
2530
|
-
* the model re-derive them from prose.
|
|
2531
|
-
*/
|
|
2532
|
-
export class PlanPolicyPrecheckFailure extends Error {
|
|
2533
|
-
findings;
|
|
2534
|
-
constructor(message, findings) {
|
|
2535
|
-
super(message);
|
|
2536
|
-
this.name = "PlanPolicyPrecheckFailure";
|
|
2537
|
-
this.findings = findings;
|
|
2538
|
-
}
|
|
2539
|
-
}
|
|
2540
|
-
/**
|
|
2541
|
-
* Shared tail of the plan patch validation: analyze the merged contract and
|
|
2542
|
-
* front-load the plan-attributable design-policy checks. Throws the same
|
|
2543
|
-
* errors the node self-check throws (FrontendContractFailure for schema
|
|
2544
|
-
* failures, Error for policy findings) so every caller — the node validator
|
|
2545
|
-
* and the finalize_plan receipt — surfaces identical diagnostics.
|
|
2546
|
-
*/
|
|
2547
|
-
export async function analyzeFrontendPlanPatchCandidate(input) {
|
|
2548
|
-
const analysis = await analyzeFrontendImplementationContract(input);
|
|
2549
|
-
// A committed VT with a fabricated symbol is an immutable ledger fact —
|
|
2550
|
-
// the record boundary rejects duplicate ids, so the model cannot overwrite
|
|
2551
|
-
// it and throwing here deadlocks the receipt loop (r19: VT-AC006-BEHAVIOR).
|
|
2552
|
-
// Drop suspicious symbols deterministically instead: the VT stays valid and
|
|
2553
|
-
// the deterministic trace gate verifies file+command (and resolvability)
|
|
2554
|
-
// after verification. Mirrors the shell materialization's drop semantics.
|
|
2555
|
-
for (const target of analysis.canonical.verificationTargets) {
|
|
2556
|
-
if (target.symbol && isSuspiciousVerificationSymbol(target.symbol)) {
|
|
2557
|
-
target.symbol = undefined;
|
|
2558
|
-
}
|
|
2559
|
-
}
|
|
2560
|
-
// Front-load the verification-symbol shape check so fabricated symbols
|
|
2561
|
-
// are fixed by the plan retry ladder in-node instead of failing the
|
|
2562
|
-
// verify trace gate at the end of the run.
|
|
2563
|
-
assertVerificationSymbolShapes(analysis.canonical);
|
|
2564
|
-
// Front-load the plan-attributable design-policy checks so the §5.1
|
|
2565
|
-
// retry ladder can fix these facts in-node with diagnostics instead of
|
|
2566
|
-
// the run terminating at the policy shell. The policy shell remains the
|
|
2567
|
-
// final authority and re-runs the identical checks on committed facts.
|
|
2568
|
-
const policyPreFindings = [
|
|
2569
|
-
...checkUiDesignCoverage(analysis.canonical),
|
|
2570
|
-
...checkUiStateAttribution(analysis.canonical),
|
|
2571
|
-
...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
|
|
2572
|
-
...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
|
|
2573
|
-
];
|
|
2574
|
-
if (policyPreFindings.length > 0) {
|
|
2575
|
-
const message = `frontend plan policy pre-check failed (fix these plan facts, then re-commit and finalize): ${policyPreFindings
|
|
2576
|
-
.map((finding) => `${finding.code}: ${finding.message}`)
|
|
2577
|
-
.join("; ")}`;
|
|
2578
|
-
throw new PlanPolicyPrecheckFailure(message, policyPreFindings.map((finding) => ({
|
|
2579
|
-
code: finding.code,
|
|
2580
|
-
message: finding.message,
|
|
2581
|
-
...(finding.path ? { path: finding.path } : {}),
|
|
2582
|
-
})));
|
|
2583
|
-
}
|
|
2584
|
-
return analysis;
|
|
2585
|
-
}
|
|
2586
1834
|
export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
2587
1835
|
const nodeId = input.nodeId ?? "frontend-plan-pi";
|
|
2588
1836
|
const attempt = input.attempt ?? 1;
|
|
@@ -2619,22 +1867,22 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
2619
1867
|
const skeleton = input.structuredContractOutput?.skeleton;
|
|
2620
1868
|
if (!skeleton)
|
|
2621
1869
|
throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
|
|
2622
|
-
const
|
|
2623
|
-
const patch = restorePlanPatchFromCommittedFacts(committed);
|
|
1870
|
+
const patch = extractFrontendImplementationJson(input.text);
|
|
2624
1871
|
if (!isPlainObject(patch))
|
|
2625
|
-
throw new Error("frontend plan
|
|
1872
|
+
throw new Error("frontend plan patch must extract to one JSON object");
|
|
2626
1873
|
extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
|
|
2627
1874
|
const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
|
|
2628
1875
|
extractedPatchSha256 = extractedArtifact.sha256;
|
|
1876
|
+
const citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
2629
1877
|
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
2630
1878
|
if (protectedViolations.length > 0)
|
|
2631
1879
|
throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
|
|
2632
1880
|
const merged = applyFrontendContractMergePatch(skeleton, patch);
|
|
2633
|
-
const
|
|
1881
|
+
const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
|
|
2634
1882
|
runDir: input.runDir,
|
|
2635
1883
|
contract: merged,
|
|
2636
1884
|
});
|
|
2637
|
-
const analysis = await
|
|
1885
|
+
const analysis = await analyzeFrontendImplementationContract({
|
|
2638
1886
|
runDir: input.runDir,
|
|
2639
1887
|
rawContractText: serializeDeterministicJson(merged),
|
|
2640
1888
|
sourceBinding: input.sourceBinding,
|
|
@@ -2646,13 +1894,20 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
2646
1894
|
normalizedContractSha256: normalizedArtifact.sha256,
|
|
2647
1895
|
normalizationActions: [
|
|
2648
1896
|
"apply-runtime-skeleton",
|
|
2649
|
-
...
|
|
1897
|
+
...(droppedUnresolvedSymbols
|
|
1898
|
+
? ["drop-unresolved-verification-symbols"]
|
|
1899
|
+
: []),
|
|
2650
1900
|
...analysis.normalizationActions,
|
|
2651
1901
|
],
|
|
2652
1902
|
errors: [],
|
|
2653
1903
|
});
|
|
2654
1904
|
const canonicalRel = canonicalContractRel(nodeId);
|
|
2655
1905
|
const canonicalArtifact = await writeDeterministicJsonArtifact(input.runDir, canonicalRel, analysis.canonical);
|
|
1906
|
+
const parsedCitations = parseOpenspecCitationsBlock(citationsBlock);
|
|
1907
|
+
await writeDeterministicJsonArtifact(input.runDir, openspecCitationsRel(nodeId), {
|
|
1908
|
+
citations: parsedCitations.citations,
|
|
1909
|
+
block: citationsBlock,
|
|
1910
|
+
});
|
|
2656
1911
|
await persistRuntimeSkeleton({
|
|
2657
1912
|
runDir: input.runDir,
|
|
2658
1913
|
nodeId,
|
|
@@ -2663,17 +1918,11 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
2663
1918
|
sha256: canonicalArtifact.sha256,
|
|
2664
1919
|
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
2665
1920
|
};
|
|
2666
|
-
// Human-readable plan summary appended after the artifact pointer. The
|
|
2667
|
-
// pointer remains the machine authority (path/schema/sha256); the
|
|
2668
|
-
// rendered summary is a deterministic view of the canonical contract so
|
|
2669
|
-
// Console/reviewers can read the plan without opening the JSON. It never
|
|
2670
|
-
// participates in downstream validation.
|
|
2671
|
-
const planSummary = renderFrontendPlanMarkdown(analysis.canonical);
|
|
2672
1921
|
return {
|
|
2673
1922
|
ok: true,
|
|
2674
1923
|
contract: analysis.canonical,
|
|
2675
1924
|
artifact,
|
|
2676
|
-
normalizedText:
|
|
1925
|
+
normalizedText: formatStructuredArtifactPointer(artifact),
|
|
2677
1926
|
};
|
|
2678
1927
|
}
|
|
2679
1928
|
catch (error) {
|
|
@@ -2740,125 +1989,106 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
2740
1989
|
}
|
|
2741
1990
|
}
|
|
2742
1991
|
/**
|
|
2743
|
-
*
|
|
2744
|
-
*
|
|
2745
|
-
*
|
|
2746
|
-
*
|
|
2747
|
-
*
|
|
2748
|
-
* editable RFC 7386 patch;
|
|
2749
|
-
* - `<contractNodeId>/contract-typed-facts.jsonl` (phase=committed) must carry
|
|
2750
|
-
* a `contract-finalized` terminal fact;
|
|
2751
|
-
* - `<contractNodeId>/frontend-task-contract-vNext.json` must exist and parse.
|
|
2752
|
-
*
|
|
2753
|
-
* The patch is applied onto the runtime skeleton and re-validated through the
|
|
2754
|
-
* same `analyzeFrontendImplementationContract` pipeline used by the legacy
|
|
2755
|
-
* text path, so the downstream artifact semantics are unchanged. The function
|
|
2756
|
-
* is pure: it never writes candidate/audit artifacts and always produces the
|
|
2757
|
-
* same canonical serialization for the same committed facts. It never falls
|
|
2758
|
-
* back to parsing Plan/Contract node text.
|
|
1992
|
+
* Node-output self-check for the plan revision node. Format-level checks still
|
|
1993
|
+
* require one openspec-citations fence and one JSON object. When the original
|
|
1994
|
+
* canonical contract and sourceBinding are present, this node is the merge
|
|
1995
|
+
* authority: it applies the RFC 7386 patch, validates the full contract, and
|
|
1996
|
+
* writes the revision node's own hash-bound canonical artifact.
|
|
2759
1997
|
*/
|
|
2760
|
-
export async function
|
|
2761
|
-
const
|
|
2762
|
-
|
|
2763
|
-
// fields so the merged contract can be validated against the full schema.
|
|
2764
|
-
const skeletonRecord = asRecord(input.skeleton);
|
|
2765
|
-
const skeletonTargets = asRecord(skeletonRecord?.targets);
|
|
2766
|
-
const skeletonRiskLevel = asString(skeletonRecord?.riskLevel);
|
|
2767
|
-
if (!skeletonRecord ||
|
|
2768
|
-
!["small", "standard", "high-risk"].includes(skeletonRiskLevel) ||
|
|
2769
|
-
!Array.isArray(skeletonTargets?.files) ||
|
|
2770
|
-
skeletonTargets.files.length === 0) {
|
|
1998
|
+
export async function validateFrontendRevisionPatchNodeOutput(input) {
|
|
1999
|
+
const fail = (reason) => {
|
|
2000
|
+
const classified = classifyStructuredContractFailure({ reason });
|
|
2771
2001
|
return {
|
|
2772
2002
|
ok: false,
|
|
2773
|
-
reason
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
}
|
|
2777
|
-
// a. Committed plan ledger restores the editable patch.
|
|
2778
|
-
const planRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.planNodeId, "plan-typed-facts.jsonl"));
|
|
2779
|
-
const patch = restorePlanPatchFromCommittedFacts(planRecords);
|
|
2780
|
-
if (!isPlainObject(patch)) {
|
|
2781
|
-
return {
|
|
2782
|
-
ok: false,
|
|
2783
|
-
reason: "frontend plan ledger missing: no committed origin=plan facts (record_* + finalize_plan); narrative JSON is not the compile authority",
|
|
2784
|
-
failureCode: "missing-plan-ledger",
|
|
2785
|
-
};
|
|
2786
|
-
}
|
|
2787
|
-
// b. Committed contract terminal fact (contract-finalized).
|
|
2788
|
-
const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, contractNodeId, "contract-typed-facts.jsonl"));
|
|
2789
|
-
const contractKinds = new Set(contractRecords
|
|
2790
|
-
.filter((record) => record.phase === "committed")
|
|
2791
|
-
.map((record) => record.fact.kind)
|
|
2792
|
-
.filter((kind) => typeof kind === "string"));
|
|
2793
|
-
if (!contractKinds.has("contract-finalized")) {
|
|
2794
|
-
return {
|
|
2795
|
-
ok: false,
|
|
2796
|
-
reason: "frontend contract facts missing: no committed contract-finalized terminal fact",
|
|
2797
|
-
failureCode: "missing-contract-finalize",
|
|
2003
|
+
reason,
|
|
2004
|
+
classification: classified.classification,
|
|
2005
|
+
errors: classified.errors,
|
|
2798
2006
|
};
|
|
2799
|
-
}
|
|
2800
|
-
|
|
2801
|
-
|
|
2007
|
+
};
|
|
2008
|
+
const nodeId = input.nodeId ?? "frontend-plan-revision-pi";
|
|
2009
|
+
const attempt = input.attempt ?? 1;
|
|
2010
|
+
await writeFrontendPlanCandidateRaw({
|
|
2011
|
+
runDir: input.runDir,
|
|
2012
|
+
nodeId,
|
|
2013
|
+
attempt,
|
|
2014
|
+
text: input.text,
|
|
2015
|
+
});
|
|
2016
|
+
let citationsBlock;
|
|
2802
2017
|
try {
|
|
2803
|
-
|
|
2018
|
+
citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
2804
2019
|
}
|
|
2805
|
-
catch {
|
|
2806
|
-
return
|
|
2807
|
-
ok: false,
|
|
2808
|
-
reason: `frontend task contract vNext missing at ${contractNodeId}/frontend-task-contract-vNext.json`,
|
|
2809
|
-
failureCode: "contract-vnext-missing",
|
|
2810
|
-
};
|
|
2020
|
+
catch (error) {
|
|
2021
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
2811
2022
|
}
|
|
2023
|
+
const textWithoutCitations = input.text.replace(citationsBlock, "");
|
|
2024
|
+
let patch;
|
|
2812
2025
|
try {
|
|
2813
|
-
|
|
2026
|
+
patch = extractFrontendImplementationJson(textWithoutCitations);
|
|
2814
2027
|
}
|
|
2815
2028
|
catch (error) {
|
|
2816
|
-
return {
|
|
2817
|
-
ok: false,
|
|
2818
|
-
reason: `frontend task contract vNext is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
2819
|
-
failureCode: "contract-vnext-invalid",
|
|
2820
|
-
};
|
|
2029
|
+
return fail(`revision patch output must contain exactly one valid JSON object: ${error instanceof Error ? error.message : String(error)}`);
|
|
2821
2030
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
try {
|
|
2825
|
-
merged = applyFrontendContractMergePatch(input.skeleton, patch);
|
|
2031
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
|
|
2032
|
+
return fail("revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)");
|
|
2826
2033
|
}
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2034
|
+
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
2035
|
+
if (protectedViolations.length > 0) {
|
|
2036
|
+
return fail(`frontend revision patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
|
|
2037
|
+
}
|
|
2038
|
+
const original = (await readFrontendCanonicalCandidate(input.runDir, "frontend-plan-pi")) ??
|
|
2039
|
+
(await readFrontendCanonicalCandidate(input.runDir, nodeId));
|
|
2040
|
+
if (!input.sourceBinding) {
|
|
2041
|
+
return { ok: true, contract: patch };
|
|
2042
|
+
}
|
|
2043
|
+
if (!original) {
|
|
2044
|
+
return fail("frontend revision patch requires the original frontend-plan-pi canonical contract");
|
|
2833
2045
|
}
|
|
2834
|
-
// The design-policy shell is the authoritative materializer of the
|
|
2835
|
-
// canonical contract; keep legacy symbol normalization identical to the
|
|
2836
|
-
// plan validator even though trace binding now uses stable target ids.
|
|
2837
|
-
await dropUnresolvedVerificationSymbols({
|
|
2838
|
-
runDir: input.runDir,
|
|
2839
|
-
contract: merged,
|
|
2840
|
-
});
|
|
2841
2046
|
try {
|
|
2047
|
+
const merged = applyFrontendContractMergePatch(original, patch);
|
|
2048
|
+
const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
|
|
2049
|
+
runDir: input.runDir,
|
|
2050
|
+
contract: merged,
|
|
2051
|
+
});
|
|
2842
2052
|
const analysis = await analyzeFrontendImplementationContract({
|
|
2843
2053
|
runDir: input.runDir,
|
|
2844
2054
|
rawContractText: serializeDeterministicJson(merged),
|
|
2845
2055
|
sourceBinding: input.sourceBinding,
|
|
2846
2056
|
});
|
|
2057
|
+
const candidateDir = path.posix.join("contracts", "candidates", nodeId);
|
|
2058
|
+
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`), patch);
|
|
2059
|
+
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
|
|
2060
|
+
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.validation.json`), {
|
|
2061
|
+
classification: "accepted-normalized",
|
|
2062
|
+
normalizationActions: [
|
|
2063
|
+
"apply-original-canonical",
|
|
2064
|
+
...(droppedUnresolvedSymbols
|
|
2065
|
+
? ["drop-unresolved-verification-symbols"]
|
|
2066
|
+
: []),
|
|
2067
|
+
...analysis.normalizationActions,
|
|
2068
|
+
],
|
|
2069
|
+
errors: [],
|
|
2070
|
+
});
|
|
2071
|
+
const canonicalArtifact = await writeDeterministicJsonArtifact(input.runDir, canonicalContractRel(nodeId), analysis.canonical);
|
|
2072
|
+
const parsedCitations = parseOpenspecCitationsBlock(citationsBlock);
|
|
2073
|
+
await writeDeterministicJsonArtifact(input.runDir, openspecCitationsRel(nodeId), {
|
|
2074
|
+
citations: parsedCitations.citations,
|
|
2075
|
+
block: citationsBlock,
|
|
2076
|
+
});
|
|
2077
|
+
const artifact = {
|
|
2078
|
+
path: canonicalArtifact.path,
|
|
2079
|
+
sha256: canonicalArtifact.sha256,
|
|
2080
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
2081
|
+
};
|
|
2847
2082
|
return {
|
|
2848
2083
|
ok: true,
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2084
|
+
contract: analysis.canonical,
|
|
2085
|
+
artifact,
|
|
2086
|
+
normalizedText: formatStructuredArtifactPointer(artifact),
|
|
2852
2087
|
};
|
|
2853
2088
|
}
|
|
2854
2089
|
catch (error) {
|
|
2855
|
-
const
|
|
2856
|
-
return
|
|
2857
|
-
ok: false,
|
|
2858
|
-
reason: `frontend contract schema drift: ${error instanceof Error ? error.message : String(error)}`,
|
|
2859
|
-
failureCode: "schema-drift",
|
|
2860
|
-
...(failureKind ? { failureKind } : {}),
|
|
2861
|
-
};
|
|
2090
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2091
|
+
return fail(reason);
|
|
2862
2092
|
}
|
|
2863
2093
|
}
|
|
2864
2094
|
export async function materializeFrontendImplementationContract(input) {
|