@tea-agent/loop-agent 0.42.0-next.1 → 0.42.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +319 -1
- package/dist/application/dag/generate-task-dag.js +75 -19
- package/dist/application/dag/run-dag.js +41 -0
- package/dist/application/task-lifecycle/advance.js +24 -5
- package/dist/application/task-lifecycle/observe.js +171 -17
- package/dist/application/task-lifecycle/plan-transitions.js +42 -7
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +6 -1
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/dag-rerun-task.js +2 -0
- package/dist/commands/init.js +27 -1
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +3976 -76
- package/dist/executors/pi-executor.js +15 -4
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/shell-executor.js +978 -188
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/infrastructure/console/operation-store.js +226 -6
- package/dist/shared/dag-failure-category.js +12 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +7 -0
- package/dist/task/config-types.js +105 -5
- package/dist/task/contract/adopt.js +4 -0
- package/dist/task/contract/import-revision.js +4 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +203 -20
- package/dist/task/runtime.js +5 -2
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +64 -25
- package/dist/task/source-prepare/prepare.js +126 -21
- package/dist/task/source-prepare/semantic-intake.js +6 -2
- package/dist/task/source-references.js +22 -1
- package/dist/worker/console/chat/chat-ui-policy.js +4 -2
- package/dist/worker/console/chat/model-resolver.js +7 -4
- package/dist/worker/console/chat/pi-runtime.js +258 -11
- package/dist/worker/console/chat/repo-browser.js +10 -3
- package/dist/worker/console/chat/routes.js +165 -71
- package/dist/worker/console/chat/sdd-data-alignment.js +222 -0
- package/dist/worker/console/chat/session-catalog.js +2 -0
- package/dist/worker/console/chat/session-store.js +159 -70
- package/dist/worker/console/chat/shortcuts.js +19 -3
- package/dist/worker/console/chat/turn-process.js +111 -56
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operator-actions.js +169 -7
- package/dist/worker/console/pi-readiness.js +3 -2
- package/dist/worker/console/server.js +23 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DZZ8m3PO.js → abnfDiagram-N423BO3Z-CcS17TBr.js} +1 -1
- package/dist/worker/console/static/assets/{arc-D6PvaVd-.js → arc-COptKq2S.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B_OTOiI8.js → architectureDiagram-T3A2C74G-h4LKMHjP.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bv6rqHBg.js → blockDiagram-VBNYF7ZC-COA1MOH0.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8eHr0oz.js → c4Diagram-5PPSVZJV-DJUf0QPm.js} +1 -1
- package/dist/worker/console/static/assets/channel-DdBCaOJ6.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DYwR0im2.js → chunk-2GRJ4B5K-mtWfKrUX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D2WPGqXt.js → chunk-2Q5K7J3B-B8pXsxDQ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CQISJ_I7.js → chunk-5RXB4S5H-ipKzByl1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C0o2Du1e.js → chunk-5VM5RSS4-DYi3Ald_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4f8kr-U7.js → chunk-6Q2QTUOP-DQtGYoty.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D_OWvXzX.js → chunk-GF5L2VYU-BU0qS2YV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CasdPz5X.js → chunk-JWPE2WC7-DVH9dIGE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Cj8lRrla.js → chunk-KBJHAD2P-Dt60SmgM.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUvD1FSd.js → chunk-RYQCIY6F-BoaUGuEY.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CqLw_eqB.js → chunk-XXDRQBXY-_WHFDTp2.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DZFra1GO.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DZFra1GO.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ku-WqJPx.js → cose-bilkent-JH36ORCC-BxkbTIRd.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bq_PYDhl.js → cynefin-VYW2F7L2-Bf2UVnoG.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BiGZV641.js → cynefinDiagram-MW4NZA55-Bo23q1J_.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C4JRlglF.js → dagre-VZM6K2ZE-Dd_UU49i.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0dNeWYG.js → diagram-7IWD3JNH-ebUa1a9y.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cj35YvYM.js → diagram-B4RE2ZJO-BucthU8r.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-uzQoJ2-8.js → diagram-LBJQPF4R-BgqNpAkm.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D1a-Buoz.js → diagram-Q27KOJAE-DC4q5NGa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-XjRrLRSs.js → diagram-UB23O5K3-CBqCaMeb.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Da_O24cW.js → ebnfDiagram-BXEA7PRR-CGtnbQ3-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BLJ8jrYU.js → erDiagram-JOGREHBK-CG3LUao5.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B9GrLjM0.js → flowDiagram-UKHOOZJN-D2ZVgoFS.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CeJ0TqiK.js → ganttDiagram-PKOTCBZU-DoYFZiKt.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js → gitGraphDiagram-DS77QQ5N-CKoP1s6j.js} +1 -1
- package/dist/worker/console/static/assets/index-CAZ2fC_X.css +1 -0
- package/dist/worker/console/static/assets/index-vbTcFnFs.js +449 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DB26i3d8.js → infoDiagram-6WML65LV-Duofv8p2.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BOGRyeZT.js → ishikawaDiagram-WSZJBQD7-D2nlCkA1.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CZRoBO_6.js → journeyDiagram-NVQOT4AX-Dd4IHdus.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-qlWhJyoB.js → kanban-definition-27J2QSJJ-Bi20AOdt.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CfUiDDB3.js → linear-D59YJ9kB.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BQe6fpqj.js → mermaid.core-Bl13LpNa.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-zFzWHw64.js → mindmap-definition-FAOFIHXS-C6DLP5eY.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DFGUqjZO.js → pegDiagram-VL7TDLO6-BjO4cy64.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BB5i0l8Z.js → pieDiagram-7S7Q4E2Y-D7i2qZTG.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-obbVZ_X5.js → quadrantDiagram-CIZ2JOQS-Dx8o_h0y.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwdaNzc1.js → railroadDiagram-AXF67PYL-B55orI_t.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CkTJaugh.js → requirementDiagram-LRYGKXZP-zWaehp6f.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BwJ-hIgq.js → sankeyDiagram-W5VNT64P-CcA-pjvD.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BzSgmm7w.js → sequenceDiagram-SI44F4Z6-BOFbzNHI.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Bn3oj5Q1.js → sizeCapture-X5ZJPWSS-BjAejah1.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-dxhGtL8K.js → stateDiagram-OKZ733FA-DXYwxJwZ.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Clg3V9t1.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BLpEInya.js → swimlanes-SLNWSIFB-OYiOch8n.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DX1dxAAW.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ChJGYcXy.js → timeline-definition-Z64GVDOM-cZfH3nmU.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DK-qjRer.js → vennDiagram-T6HMQDX7-BDE7b3E1.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3qeaWAg-.js → wardleyDiagram-T6FBY63Y-kyyGy9WJ.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CootlyP9.js → xychartDiagram-ELKLHX3M-CJj6VTog.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
- package/dist/worker/console/static-src/operator-chat/input-history.js +8 -6
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +10 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +258 -23
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +112 -4
- package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -4
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +29 -6
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +8 -2
- package/dist/worker/console/static-src/shell/console-update-reload.js +36 -9
- package/dist/worker/console/static-src/shell/workspace-route.js +11 -0
- package/dist/worker/console/workspace-context.js +115 -1
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/routes.js +4 -0
- package/dist/worker/observe/static/dag-helpers.js +4 -2
- package/dist/worker/observe/static/dag-node-purpose.js +5 -0
- package/dist/worker/observe/static/inspect-workspace.js +23 -0
- package/dist/worker/observe/static/kpi.js +2 -2
- package/dist/worker/observe/static/operator-chrome.d.ts +10 -2
- package/dist/worker/observe/static/operator-chrome.js +37 -23
- package/dist/worker/observe/static/relations.js +2 -2
- package/dist/worker/observe/static/router.d.ts +12 -1
- package/dist/worker/observe/static/router.js +48 -6
- package/dist/worker/observe/static/run-processing.js +4 -2
- package/dist/worker/observe/static/shell-chrome.js +2 -2
- package/dist/worker/observe/static/state.js +5 -0
- package/dist/worker/observe/static/styles.css +95 -0
- package/dist/worker/observe/static/views/dag-inspector.js +14 -7
- package/dist/worker/observe/static/views/dag.js +10 -2
- package/dist/worker/observe/static/views/dags.js +3 -1
- package/dist/worker/observe/static/views/dashboard.js +14 -6
- package/dist/worker/observe/static/views/pool.js +7 -2
- package/dist/worker/observe/static/views/run.js +12 -3
- package/dist/worker/observe/static/views/session-timeline.js +69 -10
- package/dist/worker/observe/static/views/task.js +7 -2
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +215 -28
- package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
- package/dist/workflows/dag/backend-test-plan-protocol.js +106 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +172 -25
- package/dist/workflows/dag/backend-test-scenario-partitions.js +62 -1
- package/dist/workflows/dag/backend-test-writer-completeness.js +104 -12
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dag-retry-schema.js +138 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +1237 -192
- package/dist/workflows/dag/frontend-plan-render.js +2 -1
- package/dist/workflows/dag/frontend-prewrite-gate.js +256 -350
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +226 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +21 -10
- package/dist/workflows/dag/frontend-recovery-run.js +166 -34
- package/dist/workflows/dag/frontend-repair.js +1 -432
- package/dist/workflows/dag/frontend-review-context.js +261 -15
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +941 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +419 -0
- package/dist/workflows/dag/frontend-shape.js +435 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +252 -25
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +319 -0
- package/dist/workflows/dag/frontend-writer-status.js +256 -0
- package/dist/workflows/dag/init-hybrid.js +1028 -562
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +828 -4
- package/dist/workflows/dag/prompt.js +70 -3
- package/dist/workflows/dag/recovery-lease.js +170 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +315 -1
- package/dist/workflows/dag/rerun-plan.js +17 -6
- package/dist/workflows/dag/rerun-run.js +44 -6
- package/dist/workflows/dag/rerun-task.js +251 -13
- package/dist/workflows/dag/retry-policy.js +219 -104
- package/dist/workflows/dag/runner.js +580 -124
- package/dist/workflows/dag/scheduler.js +133 -20
- package/dist/workflows/dag/types.js +244 -16
- package/dist/workflows/dag/validate.js +28 -12
- package/docs/examples/README.md +5 -0
- package/docs/init-surface.manifest.json +30 -12
- package/docs/skills/vetted-skill-registry.md +4 -2
- package/docs/templates/README.md +2 -0
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/backend-test-dag.json +23 -20
- package/docs/templates/frontend-implementation-contract.schema.json +4 -1
- package/docs/templates/frontend-implementation-dag.json +89 -0
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +1 -1
- package/package.json +4 -3
- package/skills/frontend-bounded-implement/SKILL.md +15 -14
- package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
- package/skills/frontend-contract/SKILL.md +23 -0
- package/skills/frontend-contract/references/contract-protocol.md +34 -0
- package/skills/frontend-design-review/SKILL.md +22 -41
- package/skills/frontend-plan/SKILL.md +26 -0
- package/skills/frontend-plan/references/decision-contract.md +37 -0
- package/skills/frontend-plan/references/design-decisions.md +17 -0
- package/skills/frontend-review/SKILL.md +20 -15
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/frontend-scout/SKILL.md +25 -0
- package/skills/frontend-scout/references/design-evidence.md +16 -0
- package/skills/frontend-scout/references/scout-evidence.md +23 -0
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -0
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/worker/console/static/assets/channel-BU5gOilw.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +0 -1
- package/dist/worker/console/static/assets/index-H9rFJiGL.css +0 -1
- package/dist/worker/console/static/assets/index-xwu9GxEc.js +0 -451
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +0 -8
- package/skills/frontend-implementation/SKILL.md +0 -52
- package/skills/frontend-implementation/references/code-standards.md +0 -33
- package/skills/frontend-implementation/references/design-spec.md +0 -56
- package/skills/frontend-implementation/references/node-contracts.md +0 -31
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend task shape routing (M7) and shape-transition capsules.
|
|
3
|
+
*
|
|
4
|
+
* Pure and independent of the M4 risk classifier and the runtime branch
|
|
5
|
+
* control. The shape functions here are deterministic (no I/O, no model, no
|
|
6
|
+
* side effects). This module is the generation-time authority for frontend
|
|
7
|
+
* task-shape routing; `init-hybrid.ts` must consume it directly and must not
|
|
8
|
+
* import the closeout renderer.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
/** Fixture-only node counts. They are NOT the authoritative 15/13/11
|
|
13
|
+
* topology: split-required has no count. */
|
|
14
|
+
export const FRONTEND_SHAPE_NODE_COUNTS = {
|
|
15
|
+
micro: 7,
|
|
16
|
+
small: 10,
|
|
17
|
+
standard: 11,
|
|
18
|
+
high: 11,
|
|
19
|
+
};
|
|
20
|
+
const SHAPE_ESCALATION_PATTERNS = [
|
|
21
|
+
{ id: "auth-permission", re: /\bauth\b|\bpermission\b|\brbac\b|登录|权限|\bjwt\b|\bsession\b/i },
|
|
22
|
+
{ id: "sensitive-action", re: /\bpayment\b|delete\s+account|支付|删除用户|\bdestructive\b/i },
|
|
23
|
+
{ id: "ssr-boundary", re: /\bssr\b|\brsc\b|server\s+component|\bhydration\b|server-client/i },
|
|
24
|
+
{ id: "new-route", re: /new\s+route|新增路由|createBrowserRouter|add\s+route/i },
|
|
25
|
+
{ id: "shared-public-api", re: /public\s+api|shared\s+component|公共组件|破坏性|breaking\s+change/i },
|
|
26
|
+
{ id: "schema-api-change", re: /\bopenapi\b|graphql\s+schema|api\s+contract|接口变更|schema\s+change/i },
|
|
27
|
+
{ id: "new-dependency", re: /new\s+dependency|add\s+dependency|npm\s+install|新增依赖/i },
|
|
28
|
+
{ id: "cross-domain-paths", re: /多个领域|cross-module|multiple\s+packages/i },
|
|
29
|
+
];
|
|
30
|
+
const SHAPE_SMALL_PATTERNS = [
|
|
31
|
+
{ id: "single-component", re: /single\s+component|单一组件|局部样式|style\s+tweak|css\s+only|文案|copy\s+change/i },
|
|
32
|
+
{ id: "no-remote", re: /no\s+api|无\s*api|无\s*接口|本地状态|local\s+state\s+only|pure\s+ui/i },
|
|
33
|
+
];
|
|
34
|
+
const SHAPE_SPLIT_REQUIRED_PATTERN = /拆分|split\s+(?:requirement|task|需求)|large\s+requirement|大型需求|多步骤需求/i;
|
|
35
|
+
/**
|
|
36
|
+
* Negative phrasing the contract model routinely echoes as a disposition
|
|
37
|
+
* statement ("无需任务拆分" / "不需要拆分" / "no split needed"). These must
|
|
38
|
+
* NOT count as split-required signals — otherwise every contract that says it
|
|
39
|
+
* does NOT need a split false-escalates to `large-requirement-split`.
|
|
40
|
+
*/
|
|
41
|
+
const SHAPE_SPLIT_REQUIRED_NEGATION = /(?:无需|不需要|不用|无须|无)\s*(?:任务|需求)?\s*拆分|no\s+split|not\s+split|no\s+requirement\s+split|don'?t\s+split/i;
|
|
42
|
+
const EXECUTABLE_SHAPE_RANK = {
|
|
43
|
+
micro: 0,
|
|
44
|
+
small: 1,
|
|
45
|
+
standard: 2,
|
|
46
|
+
high: 3,
|
|
47
|
+
};
|
|
48
|
+
function pathSpreadScore(paths) {
|
|
49
|
+
const tops = new Set(paths
|
|
50
|
+
.map((p) => p.replace(/\\/g, "/").split("/").filter(Boolean)[0] ?? "")
|
|
51
|
+
.filter(Boolean));
|
|
52
|
+
return tops.size;
|
|
53
|
+
}
|
|
54
|
+
function collectEscalationSignals(blob, supervised) {
|
|
55
|
+
const signals = [];
|
|
56
|
+
for (const pattern of SHAPE_ESCALATION_PATTERNS) {
|
|
57
|
+
if (pattern.re.test(blob))
|
|
58
|
+
signals.push(pattern.id);
|
|
59
|
+
}
|
|
60
|
+
if (supervised)
|
|
61
|
+
signals.push("supervised-profile");
|
|
62
|
+
if (SHAPE_SPLIT_REQUIRED_PATTERN.test(blob) &&
|
|
63
|
+
!SHAPE_SPLIT_REQUIRED_NEGATION.test(blob)) {
|
|
64
|
+
signals.push("large-requirement-split");
|
|
65
|
+
}
|
|
66
|
+
return [...new Set(signals)];
|
|
67
|
+
}
|
|
68
|
+
function resolveBaselineFrontendTaskShape(input) {
|
|
69
|
+
const blob = [
|
|
70
|
+
input.requirementMarkdown ?? "",
|
|
71
|
+
input.constraintMarkdown ?? "",
|
|
72
|
+
(input.allowedPaths ?? []).join("\n"),
|
|
73
|
+
].join("\n");
|
|
74
|
+
const profile = (input.governanceProfile ?? "").toLowerCase();
|
|
75
|
+
const supervised = profile === "supervised" ||
|
|
76
|
+
profile.includes("supervised") ||
|
|
77
|
+
/\bsupervised\b/i.test(blob);
|
|
78
|
+
const escalationSignals = collectEscalationSignals(blob, supervised);
|
|
79
|
+
const smallHits = [];
|
|
80
|
+
for (const pattern of SHAPE_SMALL_PATTERNS) {
|
|
81
|
+
if (pattern.re.test(blob))
|
|
82
|
+
smallHits.push(pattern.id);
|
|
83
|
+
}
|
|
84
|
+
const paths = input.allowedPaths ?? [];
|
|
85
|
+
const spread = pathSpreadScore(paths);
|
|
86
|
+
const concentrated = spread <= 1 && paths.length <= 4;
|
|
87
|
+
const hasApiSignal = /\b(api|fetch|axios|graphql|endpoint|远程|接口)\b/i.test(blob) &&
|
|
88
|
+
!(/(?:^|[^\w])(?:no\s+api|not-needed)(?:$|[^\w])/i.test(blob) ||
|
|
89
|
+
/无\s*api|无\s*接口/i.test(blob));
|
|
90
|
+
const complexity = (input.complexity ?? "").toLowerCase();
|
|
91
|
+
// split-required wins over everything: it is a termination/escalation
|
|
92
|
+
// signal, not an executable topology.
|
|
93
|
+
if (input.splitSignal.splitRequired) {
|
|
94
|
+
return {
|
|
95
|
+
shape: "split-required",
|
|
96
|
+
signals: [...new Set([...escalationSignals, "split-required"])],
|
|
97
|
+
rejectedSignals: smallHits.map((id) => `small:${id}:blocked-by-split-required`),
|
|
98
|
+
reason: "split-required signal present: terminate/escalate before planning or writing",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (supervised) {
|
|
102
|
+
const shape = escalationSignals.filter((s) => s !== "supervised-profile").length > 0
|
|
103
|
+
? "high"
|
|
104
|
+
: "standard";
|
|
105
|
+
return {
|
|
106
|
+
shape,
|
|
107
|
+
signals: escalationSignals,
|
|
108
|
+
rejectedSignals: smallHits.map((id) => `small:${id}:blocked-by-supervised`),
|
|
109
|
+
reason: shape === "high"
|
|
110
|
+
? `high escalation under supervised: ${escalationSignals.join(", ")}`
|
|
111
|
+
: "supervised profile forces full gates (standard minimum)",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (escalationSignals.length > 0) {
|
|
115
|
+
return {
|
|
116
|
+
shape: "high",
|
|
117
|
+
signals: escalationSignals,
|
|
118
|
+
rejectedSignals: smallHits.map((id) => `small:${id}:blocked-by-escalation`),
|
|
119
|
+
reason: `escalation signals present: ${escalationSignals.join(", ")}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// micro: the smallest possible change — explicit micro complexity, no API
|
|
123
|
+
// signal, no escalation, concentrated paths.
|
|
124
|
+
if ((complexity === "micro" || complexity === "trivial") &&
|
|
125
|
+
concentrated &&
|
|
126
|
+
!hasApiSignal) {
|
|
127
|
+
return {
|
|
128
|
+
shape: "micro",
|
|
129
|
+
signals: smallHits.length > 0 ? smallHits : ["micro-scope"],
|
|
130
|
+
rejectedSignals: [],
|
|
131
|
+
reason: "micro scope: trivial single-file change without remote API",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// small: no escalation, no supervised, small keywords, concentrated paths,
|
|
135
|
+
// no remote API signal.
|
|
136
|
+
if (smallHits.length > 0 &&
|
|
137
|
+
concentrated &&
|
|
138
|
+
!hasApiSignal &&
|
|
139
|
+
(complexity === "small" || complexity === "low")) {
|
|
140
|
+
return {
|
|
141
|
+
shape: "small",
|
|
142
|
+
signals: smallHits,
|
|
143
|
+
rejectedSignals: [],
|
|
144
|
+
reason: `small signals without escalation: ${smallHits.join(", ")}`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const rejected = smallHits.map((id) => `small:${id}:blocked-by-${hasApiSignal ? "api-signal" : "path-spread"}`);
|
|
148
|
+
return {
|
|
149
|
+
shape: "standard",
|
|
150
|
+
signals: ["default-standard"],
|
|
151
|
+
rejectedSignals: rejected,
|
|
152
|
+
reason: smallHits.length > 0
|
|
153
|
+
? hasApiSignal
|
|
154
|
+
? "small keywords present but API/remote signals require standard topology"
|
|
155
|
+
: "small keywords present but path spread requires standard topology"
|
|
156
|
+
: "ambiguous or standard frontend work; default standard topology",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function reshapeFloorShape(capsule) {
|
|
160
|
+
if (capsule.requestedMinimumRisk === "high")
|
|
161
|
+
return "high";
|
|
162
|
+
if (capsule.requestedMinimumSize === "standard")
|
|
163
|
+
return "standard";
|
|
164
|
+
if (capsule.requestedMinimumRisk === "standard")
|
|
165
|
+
return "standard";
|
|
166
|
+
return "small";
|
|
167
|
+
}
|
|
168
|
+
function raiseExecutableShape(baseline, floor) {
|
|
169
|
+
if (baseline === "split-required")
|
|
170
|
+
return "split-required";
|
|
171
|
+
return EXECUTABLE_SHAPE_RANK[baseline] >= EXECUTABLE_SHAPE_RANK[floor]
|
|
172
|
+
? baseline
|
|
173
|
+
: floor;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Route a frontend task into one of five shapes. Pure and independent of the
|
|
177
|
+
* M4 risk classifier and the runtime branch control. Escalation signals
|
|
178
|
+
* (high-risk keywords, supervised profile, cross-domain, large requirement
|
|
179
|
+
* split) always raise the shape above `small` or produce `split-required` —
|
|
180
|
+
* they are never silently swallowed. A verified shape-transition capsule, when
|
|
181
|
+
* present, outranks natural-language keywords and complexity but never lowers
|
|
182
|
+
* an already-higher executable baseline.
|
|
183
|
+
*/
|
|
184
|
+
export function resolveFrontendTaskShape(input) {
|
|
185
|
+
const capsule = input.shapeTransitionCapsule != null
|
|
186
|
+
? parseFrontendShapeTransitionCapsule(input.shapeTransitionCapsule)
|
|
187
|
+
: undefined;
|
|
188
|
+
if (capsule) {
|
|
189
|
+
if (!input.taskId || !input.sourceDigest) {
|
|
190
|
+
throw new Error("shape transition capsule requires matching taskId and sourceDigest");
|
|
191
|
+
}
|
|
192
|
+
if (capsule.taskId !== input.taskId ||
|
|
193
|
+
capsule.sourceDigest !== input.sourceDigest) {
|
|
194
|
+
throw new Error("shape transition capsule task/source binding mismatch");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const baseline = resolveBaselineFrontendTaskShape(input);
|
|
198
|
+
if (!capsule)
|
|
199
|
+
return baseline;
|
|
200
|
+
if (capsule.kind === "split") {
|
|
201
|
+
return {
|
|
202
|
+
shape: "split-required",
|
|
203
|
+
signals: [...new Set([...baseline.signals, "shape-transition-split"])],
|
|
204
|
+
rejectedSignals: baseline.rejectedSignals,
|
|
205
|
+
reason: `shape transition capsule split: ${capsule.splitProposalRef}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const floor = reshapeFloorShape(capsule);
|
|
209
|
+
const shape = raiseExecutableShape(baseline.shape, floor);
|
|
210
|
+
return {
|
|
211
|
+
shape,
|
|
212
|
+
signals: [
|
|
213
|
+
...new Set([
|
|
214
|
+
...baseline.signals,
|
|
215
|
+
...capsule.escalationSignals,
|
|
216
|
+
"shape-transition-reshape",
|
|
217
|
+
]),
|
|
218
|
+
],
|
|
219
|
+
rejectedSignals: baseline.rejectedSignals,
|
|
220
|
+
reason: shape === baseline.shape
|
|
221
|
+
? baseline.reason
|
|
222
|
+
: `shape transition capsule reshape floor ${floor} raised ${baseline.shape}`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/* ------------------------------------------------------------------ *
|
|
226
|
+
* Frozen topology binding and runtime escalation evaluation *
|
|
227
|
+
* ------------------------------------------------------------------ */
|
|
228
|
+
export const FRONTEND_TOPOLOGY_BINDING_SCHEMA_VERSION = 1;
|
|
229
|
+
/** Freeze capability from the generated canonical task set. Node complexity
|
|
230
|
+
* is deliberately excluded: Standard and High share the same full topology. */
|
|
231
|
+
export function buildFrontendTopologyBinding(taskIds) {
|
|
232
|
+
const canonicalTaskIds = [...new Set(taskIds)].sort();
|
|
233
|
+
const capability = canonicalTaskIds.includes("frontend-design-review-pi")
|
|
234
|
+
? "full"
|
|
235
|
+
: "small";
|
|
236
|
+
const descriptor = {
|
|
237
|
+
schemaVersion: FRONTEND_TOPOLOGY_BINDING_SCHEMA_VERSION,
|
|
238
|
+
capability,
|
|
239
|
+
taskIds: canonicalTaskIds,
|
|
240
|
+
};
|
|
241
|
+
return {
|
|
242
|
+
...descriptor,
|
|
243
|
+
canonicalDigest: sha256Utf8(JSON.stringify(sortKeysDeep(descriptor))),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
export function assertFrontendTopologyBinding(binding, taskIds) {
|
|
247
|
+
if (!binding)
|
|
248
|
+
throw new Error("frontend topology binding is missing");
|
|
249
|
+
const expected = buildFrontendTopologyBinding(taskIds);
|
|
250
|
+
if (binding.schemaVersion !== expected.schemaVersion ||
|
|
251
|
+
binding.capability !== expected.capability ||
|
|
252
|
+
binding.canonicalDigest !== expected.canonicalDigest ||
|
|
253
|
+
!stringArraysEqual(binding.taskIds, expected.taskIds)) {
|
|
254
|
+
throw new Error("frontend topology binding drifted from immutable DagSpec tasks");
|
|
255
|
+
}
|
|
256
|
+
return binding;
|
|
257
|
+
}
|
|
258
|
+
/** Runtime-only fact derivation. This intentionally does not call the
|
|
259
|
+
* generation-time `resolveFrontendTaskShape` router. */
|
|
260
|
+
export function deriveFrontendRuntimeShapeFacts(input) {
|
|
261
|
+
const blob = [
|
|
262
|
+
input.requirementMarkdown ?? "",
|
|
263
|
+
input.constraintMarkdown ?? "",
|
|
264
|
+
(input.allowedPaths ?? []).join("\n"),
|
|
265
|
+
].join("\n");
|
|
266
|
+
const supervised = /\bsupervised\b/i.test(blob);
|
|
267
|
+
const escalationSignals = collectEscalationSignals(blob, supervised);
|
|
268
|
+
const paths = input.allowedPaths ?? [];
|
|
269
|
+
const hasApiSignal = /\b(api|fetch|axios|graphql|endpoint|远程|接口)\b/i.test(blob) &&
|
|
270
|
+
!(/(?:^|[^\w])(?:no\s+api|not-needed)(?:$|[^\w])/i.test(blob) ||
|
|
271
|
+
/无\s*api|无\s*接口/i.test(blob));
|
|
272
|
+
const requiresFull = escalationSignals.length > 0 || pathSpreadScore(paths) > 1 || hasApiSignal;
|
|
273
|
+
return {
|
|
274
|
+
splitRequired: input.splitRequired,
|
|
275
|
+
escalationSignals,
|
|
276
|
+
minimumTopologyCapability: requiresFull ? "full" : "small",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/** Compare committed runtime facts with the generation-frozen topology. */
|
|
280
|
+
export function evaluateFrontendShapeEscalation(input) {
|
|
281
|
+
if (input.splitRequired) {
|
|
282
|
+
return {
|
|
283
|
+
outcome: "split-required",
|
|
284
|
+
reason: "runtime facts require splitting before downstream dispatch",
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (input.minimumTopologyCapability === "full" &&
|
|
288
|
+
input.currentTopologyCapability === "small") {
|
|
289
|
+
return {
|
|
290
|
+
outcome: "reshape-required",
|
|
291
|
+
reason: `runtime facts require full frontend topology${input.escalationSignals.length > 0 ? `: ${input.escalationSignals.join(", ")}` : ""}`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
outcome: "continue",
|
|
296
|
+
reason: input.escalationSignals.length > 0
|
|
297
|
+
? `frozen topology already covers runtime risk signals: ${input.escalationSignals.join(", ")}`
|
|
298
|
+
: "frozen topology satisfies committed runtime facts",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/* ------------------------------------------------------------------ *
|
|
302
|
+
* Shape transition capsule (§7.1 fields; runtime five-tier mapping) *
|
|
303
|
+
* ------------------------------------------------------------------ */
|
|
304
|
+
const hash64 = z.string().regex(/^[a-f0-9]{64}$/);
|
|
305
|
+
const committedIdList = z.array(z.string().min(1)).min(1);
|
|
306
|
+
const frontendShapeTransitionCapsuleBaseFields = {
|
|
307
|
+
schemaVersion: z.literal(1),
|
|
308
|
+
taskId: z.string().min(1),
|
|
309
|
+
parentRunId: z.string().min(1),
|
|
310
|
+
sourceDigest: hash64,
|
|
311
|
+
fromShapeDigest: hash64,
|
|
312
|
+
shapeFactsDigest: hash64,
|
|
313
|
+
committedFactIds: committedIdList,
|
|
314
|
+
evidenceRefs: committedIdList,
|
|
315
|
+
committedAt: z.string().datetime(),
|
|
316
|
+
capsuleDigest: hash64,
|
|
317
|
+
};
|
|
318
|
+
export const frontendShapeTransitionReshapeCapsuleSchema = z
|
|
319
|
+
.object({
|
|
320
|
+
...frontendShapeTransitionCapsuleBaseFields,
|
|
321
|
+
kind: z.literal("reshape"),
|
|
322
|
+
requestedMinimumSize: z.enum(["small", "standard"]),
|
|
323
|
+
requestedMinimumRisk: z.enum(["low", "standard", "high"]),
|
|
324
|
+
escalationSignals: z.array(z.string().min(1)),
|
|
325
|
+
})
|
|
326
|
+
.strict();
|
|
327
|
+
export const frontendShapeTransitionSplitCapsuleSchema = z
|
|
328
|
+
.object({
|
|
329
|
+
...frontendShapeTransitionCapsuleBaseFields,
|
|
330
|
+
kind: z.literal("split"),
|
|
331
|
+
splitProposalRef: z.string().min(1),
|
|
332
|
+
splitProposalDigest: hash64,
|
|
333
|
+
requirementCoverageDigest: hash64,
|
|
334
|
+
})
|
|
335
|
+
.strict();
|
|
336
|
+
export const frontendShapeTransitionCapsuleSchema = z.discriminatedUnion("kind", [
|
|
337
|
+
frontendShapeTransitionReshapeCapsuleSchema,
|
|
338
|
+
frontendShapeTransitionSplitCapsuleSchema,
|
|
339
|
+
]);
|
|
340
|
+
function isPlainObject(value) {
|
|
341
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
342
|
+
}
|
|
343
|
+
function dedupeSortStrings(values) {
|
|
344
|
+
return [...new Set(values)].sort();
|
|
345
|
+
}
|
|
346
|
+
function stringArraysEqual(left, right) {
|
|
347
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
348
|
+
}
|
|
349
|
+
function sortKeysDeep(value) {
|
|
350
|
+
if (Array.isArray(value)) {
|
|
351
|
+
return value.map((item) => sortKeysDeep(item));
|
|
352
|
+
}
|
|
353
|
+
if (isPlainObject(value)) {
|
|
354
|
+
const keys = Object.keys(value).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
355
|
+
const out = {};
|
|
356
|
+
for (const key of keys) {
|
|
357
|
+
out[key] = sortKeysDeep(value[key]);
|
|
358
|
+
}
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
return value;
|
|
362
|
+
}
|
|
363
|
+
function sha256Utf8(value) {
|
|
364
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
365
|
+
}
|
|
366
|
+
function computeCapsuleDigest(fields) {
|
|
367
|
+
const { capsuleDigest: _ignored, ...rest } = fields;
|
|
368
|
+
return sha256Utf8(JSON.stringify(sortKeysDeep(rest)));
|
|
369
|
+
}
|
|
370
|
+
function requireNonEmptyStringArray(value, field) {
|
|
371
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
372
|
+
throw new Error(`${field} must be a non-empty array of committed facts`);
|
|
373
|
+
}
|
|
374
|
+
if (!value.every((entry) => typeof entry === "string" && entry.length > 0)) {
|
|
375
|
+
throw new Error(`${field} must contain only non-empty strings`);
|
|
376
|
+
}
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
379
|
+
function normalizeCapsuleRecord(input) {
|
|
380
|
+
const next = {
|
|
381
|
+
...input,
|
|
382
|
+
committedFactIds: dedupeSortStrings(requireNonEmptyStringArray(input.committedFactIds, "committedFactIds")),
|
|
383
|
+
evidenceRefs: dedupeSortStrings(requireNonEmptyStringArray(input.evidenceRefs, "evidenceRefs")),
|
|
384
|
+
};
|
|
385
|
+
if (next.kind === "reshape") {
|
|
386
|
+
const signals = next.escalationSignals;
|
|
387
|
+
if (signals === undefined) {
|
|
388
|
+
next.escalationSignals = [];
|
|
389
|
+
}
|
|
390
|
+
else if (!Array.isArray(signals)) {
|
|
391
|
+
throw new Error("escalationSignals must be a string array");
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
next.escalationSignals = dedupeSortStrings(signals.map((entry) => {
|
|
395
|
+
if (typeof entry !== "string") {
|
|
396
|
+
throw new Error("escalationSignals must contain only strings");
|
|
397
|
+
}
|
|
398
|
+
return entry;
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return next;
|
|
403
|
+
}
|
|
404
|
+
export function computeFrontendShapeSourceDigest(input) {
|
|
405
|
+
return sha256Utf8(JSON.stringify(sortKeysDeep({
|
|
406
|
+
constraintMarkdown: input.constraintMarkdown ?? "",
|
|
407
|
+
requirementMarkdown: input.requirementMarkdown,
|
|
408
|
+
})));
|
|
409
|
+
}
|
|
410
|
+
export function buildFrontendShapeTransitionCapsule(input) {
|
|
411
|
+
if (!isPlainObject(input)) {
|
|
412
|
+
throw new Error("FrontendShapeTransitionCapsule input must be an object");
|
|
413
|
+
}
|
|
414
|
+
const { capsuleDigest: _ignored, ...rest } = input;
|
|
415
|
+
const normalized = normalizeCapsuleRecord(rest);
|
|
416
|
+
return frontendShapeTransitionCapsuleSchema.parse({
|
|
417
|
+
...normalized,
|
|
418
|
+
capsuleDigest: computeCapsuleDigest(normalized),
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
export function parseFrontendShapeTransitionCapsule(value) {
|
|
422
|
+
const parsed = frontendShapeTransitionCapsuleSchema.parse(value);
|
|
423
|
+
const normalized = normalizeCapsuleRecord(parsed);
|
|
424
|
+
if (!stringArraysEqual(parsed.committedFactIds, normalized.committedFactIds) ||
|
|
425
|
+
!stringArraysEqual(parsed.evidenceRefs, normalized.evidenceRefs) ||
|
|
426
|
+
(parsed.kind === "reshape" &&
|
|
427
|
+
!stringArraysEqual(parsed.escalationSignals, normalized.escalationSignals))) {
|
|
428
|
+
throw new Error("FrontendShapeTransitionCapsule arrays must use canonical sorted unique representation");
|
|
429
|
+
}
|
|
430
|
+
const expected = computeCapsuleDigest(normalized);
|
|
431
|
+
if (parsed.capsuleDigest !== expected) {
|
|
432
|
+
throw new Error("capsuleDigest does not match canonical fields");
|
|
433
|
+
}
|
|
434
|
+
return parsed;
|
|
435
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
4
|
+
import { parseLedgerJson, validateRequirementLedger, } from "../../task/source-prepare/ledger.js";
|
|
5
|
+
import { isDagSourceBindingV2 } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Validate the semantic source-fidelity ledger binding for a canonical frontend
|
|
8
|
+
* contract. This is deliberately shared: the compatibility prewrite gate may
|
|
9
|
+
* report a structured prewrite failure, while the generated production DAG
|
|
10
|
+
* must deny writer admission on exactly the same evidence.
|
|
11
|
+
*/
|
|
12
|
+
export async function checkFrontendSourceFidelityLedger(input) {
|
|
13
|
+
const { binding } = input;
|
|
14
|
+
if (!isDagSourceBindingV2(binding)) {
|
|
15
|
+
return { ok: true, failureCode: null, failureReason: null, ledger: null };
|
|
16
|
+
}
|
|
17
|
+
const absolute = resolveDagTaskSourcePath({
|
|
18
|
+
workspaceRoot: input.workspaceRoot,
|
|
19
|
+
taskId: binding.taskId,
|
|
20
|
+
sourcePath: binding.ledgerPath,
|
|
21
|
+
});
|
|
22
|
+
let raw;
|
|
23
|
+
try {
|
|
24
|
+
raw = await readFile(absolute, "utf8");
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
failureCode: "source-fidelity-ledger-missing",
|
|
30
|
+
failureReason: `frontend source fidelity ledger missing: ${binding.ledgerPath} (${error instanceof Error ? error.message : String(error)})`,
|
|
31
|
+
ledger: null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const actualSha = createHash("sha256").update(raw).digest("hex");
|
|
35
|
+
if (actualSha !== binding.ledgerSha256) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
failureCode: "source-fidelity-ledger-stale",
|
|
39
|
+
failureReason: `frontend source fidelity ledger stale or tampered: ${binding.ledgerPath} (bound ${binding.ledgerSha256}, observed ${actualSha})`,
|
|
40
|
+
ledger: null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
let ledger;
|
|
44
|
+
try {
|
|
45
|
+
ledger = parseLedgerJson(raw);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
failureCode: "source-fidelity-ledger-stale",
|
|
51
|
+
failureReason: `frontend source fidelity ledger is not a valid v1 ledger: ${error instanceof Error ? error.message : String(error)}`,
|
|
52
|
+
ledger: null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (ledger.inputDigest !== binding.inputDigest) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
failureCode: "source-fidelity-ledger-stale",
|
|
59
|
+
failureReason: `frontend source fidelity ledger inputDigest mismatch: bound ${binding.inputDigest}, ledger ${ledger.inputDigest}`,
|
|
60
|
+
ledger,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const validation = validateRequirementLedger(ledger);
|
|
64
|
+
const firstError = (code) => validation.find((error) => error.code === code)?.message ?? null;
|
|
65
|
+
const invalid = (failureCode, failureReason) => ({ ok: false, failureCode, failureReason, ledger });
|
|
66
|
+
if (ledger.stats.unresolvedCount > 0 || validation.some((e) => e.code === "SOURCE_FIDELITY_UNRESOLVED")) {
|
|
67
|
+
return invalid("source-fidelity-unresolved", firstError("SOURCE_FIDELITY_UNRESOLVED") ?? `frontend source fidelity ledger has ${ledger.stats.unresolvedCount} unresolved fragment(s)`);
|
|
68
|
+
}
|
|
69
|
+
if (ledger.stats.conflictCount > 0 || validation.some((e) => e.code === "SOURCE_FIDELITY_CONFLICT")) {
|
|
70
|
+
return invalid("source-fidelity-conflict", firstError("SOURCE_FIDELITY_CONFLICT") ?? `frontend source fidelity ledger has ${ledger.stats.conflictCount} conflict(s)`);
|
|
71
|
+
}
|
|
72
|
+
if (ledger.stats.highRiskUncoveredCount > 0 || validation.some((e) => e.code === "SOURCE_FIDELITY_HIGH_RISK_UNCOVERED")) {
|
|
73
|
+
return invalid("source-fidelity-high-risk-uncovered", firstError("SOURCE_FIDELITY_HIGH_RISK_UNCOVERED") ?? `frontend source fidelity ledger has ${ledger.stats.highRiskUncoveredCount} high-risk uncovered fragment(s)`);
|
|
74
|
+
}
|
|
75
|
+
const notCoveredError = validation.find((e) => e.code === "SOURCE_FIDELITY_REQUIREMENT_NOT_COVERED");
|
|
76
|
+
if (notCoveredError) {
|
|
77
|
+
return invalid("source-fidelity-requirement-not-covered", notCoveredError.message);
|
|
78
|
+
}
|
|
79
|
+
const refInvalidError = validation.find((e) => e.code === "SOURCE_FIDELITY_SOURCE_REF_INVALID" || e.code === "SOURCE_FIDELITY_FRAGMENT_INVALID");
|
|
80
|
+
if (refInvalidError) {
|
|
81
|
+
return invalid("source-fidelity-source-ref-invalid", refInvalidError.message);
|
|
82
|
+
}
|
|
83
|
+
const ledgerIds = new Set(ledger.canonicalRequirements.map((requirement) => requirement.id));
|
|
84
|
+
const contractIds = new Set(input.contractRequirements.map((r) => r.id));
|
|
85
|
+
const uncoveredRequirements = [...ledgerIds].filter((id) => !contractIds.has(id));
|
|
86
|
+
if (uncoveredRequirements.length > 0) {
|
|
87
|
+
return invalid("source-fidelity-requirement-not-covered", `frontend contract requirements do not cover ledger canonical requirement ids: ${uncoveredRequirements.join(", ")}`);
|
|
88
|
+
}
|
|
89
|
+
const fragmentById = new Map(ledger.fragments.map((fragment) => [fragment.id, fragment]));
|
|
90
|
+
for (const requirement of input.contractRequirements) {
|
|
91
|
+
if (!ledgerIds.has(requirement.id))
|
|
92
|
+
continue;
|
|
93
|
+
const declared = requirement.sourceFragmentIds ?? [];
|
|
94
|
+
if (declared.length === 0) {
|
|
95
|
+
return invalid("source-fidelity-source-ref-invalid", `frontend contract requirement ${requirement.id} lacks sourceFragmentIds for the bound ledger`);
|
|
96
|
+
}
|
|
97
|
+
const boundFragments = new Set(ledger.canonicalRequirements.find((r) => r.id === requirement.id)?.sourceFragmentIds ?? []);
|
|
98
|
+
for (const fragmentId of declared) {
|
|
99
|
+
if (!fragmentById.has(fragmentId)) {
|
|
100
|
+
return invalid("source-fidelity-source-ref-invalid", `frontend contract requirement ${requirement.id} references unknown fragment ${fragmentId}`);
|
|
101
|
+
}
|
|
102
|
+
if (!boundFragments.has(fragmentId)) {
|
|
103
|
+
return invalid("source-fidelity-source-ref-invalid", `frontend contract requirement ${requirement.id} declares fabricated fragment reference ${fragmentId} (not bound in ledger)`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { ok: true, failureCode: null, failureReason: null, ledger };
|
|
108
|
+
}
|