@tea-agent/loop-agent 0.42.0-next.1 → 0.42.0-next.11
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 +321 -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 +4010 -78
- package/dist/executors/pi-executor.js +15 -4
- package/dist/executors/pi-extension-resolver.js +14 -2
- 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/constants.js +22 -22
- package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
- package/dist/worker/observe/static/dag-helpers.js +4 -2
- package/dist/worker/observe/static/dag-history-labels.js +1 -0
- package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
- package/dist/worker/observe/static/dag-inspector-humanize.js +339 -0
- package/dist/worker/observe/static/dag-node-purpose.js +13 -8
- package/dist/worker/observe/static/index.html +4 -4
- 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/prompt-restart-candidates.js +4 -2
- package/dist/worker/observe/static/relations.js +7 -7
- 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 +231 -0
- package/dist/worker/observe/static/views/dag-graph.js +1 -1
- package/dist/worker/observe/static/views/dag-inspector.js +336 -181
- package/dist/worker/observe/static/views/dag.js +26 -14
- 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 +74 -13
- 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 +210 -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 +1031 -564
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +854 -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 +230 -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 +25 -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
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { access, readdir, readFile, realpath } from "node:fs/promises";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { deflateRawSync } from "node:zlib";
|
|
4
5
|
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
5
7
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
6
8
|
import { assertValidDagSpec } from "./validate.js";
|
|
7
9
|
import { DAG_AGENT_RUNTIME_PI_ONLY, DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1, DAG_RUNTIME_CONTRACT_SCHEMA_VERSION, DEFAULT_DAG_OUTPUT_LANGUAGE, DEFAULT_DAG_EXECUTOR_MODELS, parseDagSpec, } from "./types.js";
|
|
8
10
|
import { bindDagRerunFeedback } from "./rerun-feedback.js";
|
|
9
11
|
import { planMavenVerification, } from "../../verification/maven/index.js";
|
|
10
12
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
11
|
-
import {
|
|
13
|
+
import { DEFAULT_OPENSPEC_GOVERNANCE_ROOT, DEFAULT_FRONTEND_SPEC_ROOTS, extractTaskSourceFrontendSpecPaths, } from "../../shared/openspec-spec.js";
|
|
14
|
+
import { readFrontendSpecRegistry, scoreFrontendSpecCandidate, } from "../../task/frontend-project-capability.js";
|
|
12
15
|
import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
|
|
13
16
|
import { buildDecisionEnvelopePromptContract } from "./decision-envelope.js";
|
|
14
|
-
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY,
|
|
17
|
+
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, FRONTEND_SCOUT_COMPLETENESS_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, BACKEND_TEST_MD_PLAN_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, BACKEND_TEST_MARKDOWN_BINDING_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, FRONTEND_PLAN_LADDER_RETRY_POLICY, FRONTEND_REVIEW_TERMINAL_RETRY_POLICY, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY, isCanonicalFinalVerifyShellRetryCandidate, isSafeReadOnlyPiRetryCandidate, isTargetTemplateImplementPi, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
15
18
|
import { REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL, REVIEW_VERDICT_OUTPUT_PROTOCOL, } from "./output-protocol.js";
|
|
16
19
|
import { resolveAdapter } from "../../adapters/index.js";
|
|
17
20
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
@@ -19,11 +22,11 @@ import { mergeDocumentIndexCompanions } from "../../governance/document-index-cl
|
|
|
19
22
|
import { buildAuthoritySurfaceAuditNode, buildAuthoritySurfaceGateNode, resolveAuthoritySurfaceAudit, } from "./authority-surface.js";
|
|
20
23
|
import { applySddEmbeddedEnhancements, probeRepoLocalSddSkills, } from "./sdd-embedded.js";
|
|
21
24
|
import { discoverProjectGovernancePresence } from "./project-governance-context.js";
|
|
22
|
-
import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
25
|
+
import { CANONICAL_TASK_ID_PATTERN, getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
23
26
|
import { materializeTaskReferenceDocs } from "../../task/source-references.js";
|
|
24
27
|
import { observeTaskContract } from "../../task/contract/observe.js";
|
|
25
28
|
import { extractRequirementFactsFromMarkdown } from "../../task/source-prepare/parse-intent.js";
|
|
26
|
-
import { computeLedgerInputDigest, parseLedgerJson, recoverLedgerInputContract, } from "../../task/source-prepare/ledger.js";
|
|
29
|
+
import { computeLedgerInputDigest, parseLedgerJson, recoverLedgerInputContract, validateRequirementLedger, } from "../../task/source-prepare/ledger.js";
|
|
27
30
|
import { REQUIREMENT_LEDGER_FILE_NAME } from "../../task/contract/constants.js";
|
|
28
31
|
import { dagHasWriterExecution } from "./task-contract-binding.js";
|
|
29
32
|
import { DEFAULT_VERIFY_TIMEOUT_MS, resolveVerifyPreset, } from "../../executors/shell-verification.js";
|
|
@@ -36,8 +39,10 @@ import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-c
|
|
|
36
39
|
import { buildBackendTestIntakeContext } from "./backend-test-intake-context.js";
|
|
37
40
|
import { buildFrontendTestOutcomeGateShellSnippet } from "./frontend-test-result-contract.js";
|
|
38
41
|
import { classifyFrontendRisk, } from "./frontend-risk.js";
|
|
39
|
-
import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
|
|
40
|
-
import { buildFrontendImplementationContractSkeleton,
|
|
42
|
+
import { discoverFrontendProjectCapability, resolveFrontendSpecRootAliases, } from "./frontend-project-capability.js";
|
|
43
|
+
import { buildFrontendImplementationContractSkeleton, buildFrontendVerifyCommandDirectory, classifyFrontendVerifyCommandText, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, isRecognizedFrontendVerifyCommandText, } from "./frontend-implementation-contract.js";
|
|
44
|
+
import { computeFrontendShapeSourceDigest, parseFrontendShapeTransitionCapsule, resolveFrontendTaskShape, } from "./frontend-shape.js";
|
|
45
|
+
import { discoverLatestCommittedFrontendShapeCapsule } from "./frontend-shape-capsule-store.js";
|
|
41
46
|
import { FRONTEND_NO_VERIFICATION_MARKER_TEXT } from "./frontend-verification-trace.js";
|
|
42
47
|
import { serializeDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
43
48
|
const REQUIREMENT_FILE = "需求.md";
|
|
@@ -153,7 +158,9 @@ const FRONTEND_SKILLS_BY_ROLE = {
|
|
|
153
158
|
verifier: [],
|
|
154
159
|
closeout: [],
|
|
155
160
|
};
|
|
156
|
-
const
|
|
161
|
+
const FRONTEND_CONTRACT_SKILLS = ["frontend-contract"];
|
|
162
|
+
const FRONTEND_SCOUT_SKILLS = ["frontend-scout"];
|
|
163
|
+
const FRONTEND_PLAN_SKILLS = ["frontend-plan"];
|
|
157
164
|
const FRONTEND_BOUNDED_IMPLEMENT_SKILLS = ["frontend-bounded-implement"];
|
|
158
165
|
const FRONTEND_DESIGN_REVIEW_SKILLS = ["frontend-design-review"];
|
|
159
166
|
const FRONTEND_REVIEW_SKILLS = ["frontend-review"];
|
|
@@ -274,6 +281,28 @@ async function hasDirectDependency(repoRoot, depName) {
|
|
|
274
281
|
return false;
|
|
275
282
|
}
|
|
276
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Collect the project's declared direct dependencies (dependencies +
|
|
286
|
+
* devDependencies names) from package.json. Used to freeze the frontend
|
|
287
|
+
* design-policy allowedDependencies set: the model's dependency policy must
|
|
288
|
+
* not be rejected for mentioning an already-declared dependency, and a
|
|
289
|
+
* dependency not present in the manifest is a genuine unauthorized addition.
|
|
290
|
+
* Returns [] when package.json is unreadable (no allowlist → the dependency
|
|
291
|
+
* check is skipped rather than rejecting prose tokens).
|
|
292
|
+
*/
|
|
293
|
+
async function collectDeclaredDependencies(repoRoot) {
|
|
294
|
+
try {
|
|
295
|
+
const raw = await readFile(path.join(repoRoot, "package.json"), "utf-8");
|
|
296
|
+
const pkg = JSON.parse(raw);
|
|
297
|
+
return [
|
|
298
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
299
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
277
306
|
/** Check whether handler/fixture/bootstrap files exist for known mock frameworks. */
|
|
278
307
|
async function discoverMockHandlerFiles(repoRoot, serviceRoot) {
|
|
279
308
|
const exactCandidates = [
|
|
@@ -978,11 +1007,19 @@ function extractFrontendVerifyCommandsFromMarkdown(input) {
|
|
|
978
1007
|
const verifyCommand = markdownVerifyCommand(input.repoRoot, command);
|
|
979
1008
|
if (!verifyCommand)
|
|
980
1009
|
continue;
|
|
981
|
-
|
|
982
|
-
|
|
1010
|
+
// Markdown is untrusted prose: admit only commands with known verification
|
|
1011
|
+
// semantics before using the shared classifier. Explicit task verifyCommands
|
|
1012
|
+
// are operator-authorized separately and do not pass through this filter.
|
|
1013
|
+
if (!isRecognizedFrontendVerifyCommandText(command))
|
|
983
1014
|
continue;
|
|
1015
|
+
// Single shared classifier: never fork lane regexes here. The old
|
|
1016
|
+
// inline static-first regex misclassified behavior commands whose
|
|
1017
|
+
// arguments mention build paths (e.g. `npx vitest run
|
|
1018
|
+
// test/build.test.ts`).
|
|
1019
|
+
if (classifyFrontendVerifyCommandText(command) === "static") {
|
|
1020
|
+
staticCommands.push(verifyCommand);
|
|
984
1021
|
}
|
|
985
|
-
|
|
1022
|
+
else {
|
|
986
1023
|
behaviorCommands.push(verifyCommand);
|
|
987
1024
|
}
|
|
988
1025
|
}
|
|
@@ -1022,10 +1059,13 @@ function chooseFrontendVerifyCommands(input) {
|
|
|
1022
1059
|
}
|
|
1023
1060
|
return { commandSource: "inline" };
|
|
1024
1061
|
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Generation-time lane split for frontend verify commands. The mode itself
|
|
1064
|
+
* is owned by `classifyFrontendVerifyCommandText` (contract module); this
|
|
1065
|
+
* wrapper only preserves the local call shape.
|
|
1066
|
+
*/
|
|
1025
1067
|
function classifyFrontendVerifyCommand(command) {
|
|
1026
|
-
return
|
|
1027
|
-
? "static"
|
|
1028
|
-
: "behavior";
|
|
1068
|
+
return classifyFrontendVerifyCommandText(command);
|
|
1029
1069
|
}
|
|
1030
1070
|
function buildExplicitFrontendVerifyCommands(taskConfig, repoRoot) {
|
|
1031
1071
|
const staticCommands = [];
|
|
@@ -1195,21 +1235,121 @@ function canonicalizeVerificationCommands(commands) {
|
|
|
1195
1235
|
}
|
|
1196
1236
|
function assertVerificationPlanPreflight(input) {
|
|
1197
1237
|
const root = path.resolve(input.repoRoot);
|
|
1238
|
+
const statuses = [];
|
|
1198
1239
|
for (const command of input.commands) {
|
|
1199
1240
|
const cwd = path.resolve(command.cwd);
|
|
1200
1241
|
if (!isWithinRepo(root, cwd) || !existsSync(cwd)) {
|
|
1201
1242
|
throw new Error(`verification preflight rejected cwd for ${command.label}: ${command.cwd}`);
|
|
1202
1243
|
}
|
|
1244
|
+
const vitestStatus = verifyVitestCommandPreflight({
|
|
1245
|
+
command,
|
|
1246
|
+
root,
|
|
1247
|
+
taskConfig: input.taskConfig,
|
|
1248
|
+
});
|
|
1249
|
+
if (vitestStatus) {
|
|
1250
|
+
statuses.push({ label: command.label, status: vitestStatus });
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1203
1253
|
const script = localScriptTarget(command);
|
|
1204
|
-
if (!script)
|
|
1254
|
+
if (!script) {
|
|
1255
|
+
statuses.push({ label: command.label, status: "ok" });
|
|
1205
1256
|
continue;
|
|
1257
|
+
}
|
|
1206
1258
|
const scriptPath = path.resolve(cwd, script);
|
|
1207
|
-
if (existsSync(scriptPath))
|
|
1259
|
+
if (existsSync(scriptPath)) {
|
|
1260
|
+
statuses.push({ label: command.label, status: "ok" });
|
|
1208
1261
|
continue;
|
|
1262
|
+
}
|
|
1209
1263
|
const writerMayCreate = input.taskConfig.allowedPaths.some((allowed) => pathMatchesPattern(script, allowed.replace(/^\.\//, "")));
|
|
1210
1264
|
if (!writerMayCreate) {
|
|
1211
1265
|
throw new Error(`verification preflight missing local script for ${command.label}: ${script}`);
|
|
1212
1266
|
}
|
|
1267
|
+
statuses.push({ label: command.label, status: "future-delivery-script" });
|
|
1268
|
+
}
|
|
1269
|
+
return statuses;
|
|
1270
|
+
}
|
|
1271
|
+
function verifyVitestCommandPreflight(input) {
|
|
1272
|
+
const args = normalizeVerifyCommandArgs(input.command.args);
|
|
1273
|
+
const vitestIndex = args.findIndex((arg) => path.basename(arg).replace(/\.(?:exe|cmd)$/i, "") === "vitest");
|
|
1274
|
+
if (vitestIndex < 0)
|
|
1275
|
+
return undefined;
|
|
1276
|
+
const configArgIndex = args.findIndex((arg, index) => index > vitestIndex &&
|
|
1277
|
+
(arg === "--config" || arg === "-c" || arg.startsWith("--config=")));
|
|
1278
|
+
let configPath;
|
|
1279
|
+
if (configArgIndex >= 0) {
|
|
1280
|
+
const arg = args[configArgIndex];
|
|
1281
|
+
configPath = arg.startsWith("--config=")
|
|
1282
|
+
? arg.slice("--config=".length)
|
|
1283
|
+
: args[configArgIndex + 1];
|
|
1284
|
+
}
|
|
1285
|
+
const cwd = path.resolve(input.command.cwd);
|
|
1286
|
+
const configAbsolute = configPath
|
|
1287
|
+
? path.resolve(cwd, configPath)
|
|
1288
|
+
: findRootVitestConfig(input.root);
|
|
1289
|
+
const configRelative = configAbsolute
|
|
1290
|
+
? path.relative(input.root, configAbsolute).replace(/\\/g, "/")
|
|
1291
|
+
: undefined;
|
|
1292
|
+
if (configAbsolute && !existsSync(configAbsolute)) {
|
|
1293
|
+
const writerMayCreate = Boolean(configRelative &&
|
|
1294
|
+
input.taskConfig.allowedPaths.some((allowed) => pathMatchesPattern(configRelative, allowed.replace(/^\.\//, ""))));
|
|
1295
|
+
if (!writerMayCreate) {
|
|
1296
|
+
throw new Error(`verification preflight missing Vitest config for ${input.command.label}: ${configRelative ?? configPath}`);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
const targetPaths = vitestTargetPaths(args.slice(vitestIndex + 1));
|
|
1300
|
+
if (targetPaths.length === 0)
|
|
1301
|
+
return "ok";
|
|
1302
|
+
const inheritedConfig = !configPath && configAbsolute && existsSync(configAbsolute);
|
|
1303
|
+
if (inheritedConfig && vitestConfigExcludesDogfood(configAbsolute, targetPaths)) {
|
|
1304
|
+
throw new Error(`verification preflight Vitest target ${targetPaths.join(", ")} is excluded by ${path.relative(input.root, configAbsolute).replace(/\\/g, "/")}`);
|
|
1305
|
+
}
|
|
1306
|
+
const futureTargets = targetPaths.filter((target) => {
|
|
1307
|
+
const absolute = path.resolve(cwd, target);
|
|
1308
|
+
if (existsSync(absolute))
|
|
1309
|
+
return false;
|
|
1310
|
+
return !input.taskConfig.allowedPaths.some((allowed) => pathMatchesPattern(target, allowed.replace(/^\.\//, "")));
|
|
1311
|
+
});
|
|
1312
|
+
if (futureTargets.length > 0) {
|
|
1313
|
+
throw new Error(`verification preflight missing Vitest target for ${input.command.label}: ${futureTargets.join(", ")}`);
|
|
1314
|
+
}
|
|
1315
|
+
const missingAllowedTargets = targetPaths.filter((target) => !existsSync(path.resolve(cwd, target)));
|
|
1316
|
+
return missingAllowedTargets.length > 0 ? "future-test-target" : "ok";
|
|
1317
|
+
}
|
|
1318
|
+
function findRootVitestConfig(root) {
|
|
1319
|
+
for (const name of ["vitest.config.ts", "vitest.config.js", "vitest.config.mjs", "vitest.config.cjs"]) {
|
|
1320
|
+
const candidate = path.join(root, name);
|
|
1321
|
+
if (existsSync(candidate))
|
|
1322
|
+
return candidate;
|
|
1323
|
+
}
|
|
1324
|
+
return undefined;
|
|
1325
|
+
}
|
|
1326
|
+
function vitestTargetPaths(args) {
|
|
1327
|
+
const targets = [];
|
|
1328
|
+
let afterRun = false;
|
|
1329
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1330
|
+
const arg = args[index];
|
|
1331
|
+
if (arg === "run" || arg === "related") {
|
|
1332
|
+
afterRun = true;
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
if (!afterRun || arg.startsWith("-")) {
|
|
1336
|
+
if ((arg === "--config" || arg === "-c" || arg === "--reporter") && !arg.includes("="))
|
|
1337
|
+
index += 1;
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
targets.push(arg);
|
|
1341
|
+
}
|
|
1342
|
+
return targets.filter((target) => target.includes("/") || /\.(?:test|spec)\.[^/]+$/.test(target));
|
|
1343
|
+
}
|
|
1344
|
+
function vitestConfigExcludesDogfood(configPath, targets) {
|
|
1345
|
+
if (!targets.some((target) => target.replace(/\\/g, "/").startsWith("dogfood/")))
|
|
1346
|
+
return false;
|
|
1347
|
+
try {
|
|
1348
|
+
const text = readFileSync(configPath, "utf8");
|
|
1349
|
+
return /exclude\s*:[\s\S]{0,500}dogfood\/\*\*/.test(text);
|
|
1350
|
+
}
|
|
1351
|
+
catch {
|
|
1352
|
+
return false;
|
|
1213
1353
|
}
|
|
1214
1354
|
}
|
|
1215
1355
|
function isWithinRepo(root, candidate) {
|
|
@@ -1278,10 +1418,11 @@ function buildVerifyEvidence(input) {
|
|
|
1278
1418
|
selectionReasons: selectedCommands?.map((command) => input.phase === "intermediate" && isFullSuiteVerifyCommand(command)
|
|
1279
1419
|
? `${command.label}: deferred-heavy`
|
|
1280
1420
|
: `${command.label}: kept`) ?? [],
|
|
1281
|
-
preflight:
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1421
|
+
preflight: input.preflight ??
|
|
1422
|
+
(selectedCommands?.map((command) => ({
|
|
1423
|
+
label: command.label,
|
|
1424
|
+
status: "ok",
|
|
1425
|
+
})) ?? []),
|
|
1285
1426
|
commandTimeoutMs: input.commandTimeoutMs,
|
|
1286
1427
|
totalTimeoutBudgetMs: commandCount * input.commandTimeoutMs,
|
|
1287
1428
|
finalFullRequired: input.finalFullRequired,
|
|
@@ -1470,6 +1611,8 @@ function extractExplicitRequirementIds(requirementMarkdown, ...fallbackMarkdown)
|
|
|
1470
1611
|
: markdown;
|
|
1471
1612
|
for (const match of source.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
|
|
1472
1613
|
const id = match[0].toUpperCase();
|
|
1614
|
+
if (isPlaceholderRequirementId(id))
|
|
1615
|
+
continue;
|
|
1473
1616
|
if (!seen.has(id)) {
|
|
1474
1617
|
seen.add(id);
|
|
1475
1618
|
ids.push(id);
|
|
@@ -1499,6 +1642,8 @@ function extractSectionRequirementIds(sectionMarkdown) {
|
|
|
1499
1642
|
const seen = new Set();
|
|
1500
1643
|
for (const match of sectionMarkdown.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
|
|
1501
1644
|
const id = match[0].toUpperCase();
|
|
1645
|
+
if (isPlaceholderRequirementId(id))
|
|
1646
|
+
continue;
|
|
1502
1647
|
if (!seen.has(id)) {
|
|
1503
1648
|
seen.add(id);
|
|
1504
1649
|
ids.push(id);
|
|
@@ -1506,6 +1651,9 @@ function extractSectionRequirementIds(sectionMarkdown) {
|
|
|
1506
1651
|
}
|
|
1507
1652
|
return ids;
|
|
1508
1653
|
}
|
|
1654
|
+
function isPlaceholderRequirementId(id) {
|
|
1655
|
+
return /(?:^|-)X{2,}$/.test(id);
|
|
1656
|
+
}
|
|
1509
1657
|
/**
|
|
1510
1658
|
* Prefer TaskSpec-scoped acceptance refs from the derived 需求.md section when
|
|
1511
1659
|
* present. Full Feature acceptance.yaml may list sibling ACs that this task is
|
|
@@ -1584,21 +1732,14 @@ function buildDagSourceBinding(sources, taskKind) {
|
|
|
1584
1732
|
const ledgerBinding = loadPersistedFrontendSourceBindingLedger(sources);
|
|
1585
1733
|
if (!ledgerBinding)
|
|
1586
1734
|
return base;
|
|
1587
|
-
//
|
|
1588
|
-
//
|
|
1589
|
-
//
|
|
1590
|
-
// 不误伤(不要求文本可导出)也不漏检(物化 id 必须被契约覆盖)。
|
|
1591
|
-
const materializedRequirementIds = Object.keys(ledgerBinding.requirementToFragments)
|
|
1592
|
-
.filter((id) => /^REQ-SRC-/.test(id) && !base.requirementIds.includes(id))
|
|
1593
|
-
.sort();
|
|
1735
|
+
// v2 is ledger-owned: the persisted canonical set is the only requirement
|
|
1736
|
+
// identity source. In particular, explanatory AC-XXX mentions from a
|
|
1737
|
+
// materialized reference document must not leak in beside ledger ids.
|
|
1594
1738
|
return {
|
|
1595
1739
|
schemaVersion: 2,
|
|
1596
1740
|
taskId: base.taskId,
|
|
1597
1741
|
sources: base.sources,
|
|
1598
|
-
requirementIds:
|
|
1599
|
-
...base.requirementIds,
|
|
1600
|
-
...materializedRequirementIds,
|
|
1601
|
-
],
|
|
1742
|
+
requirementIds: ledgerBinding.requirementIds,
|
|
1602
1743
|
ledgerPath: ledgerBinding.ledgerPath,
|
|
1603
1744
|
ledgerSha256: ledgerBinding.ledgerSha256,
|
|
1604
1745
|
inputDigest: ledgerBinding.inputDigest,
|
|
@@ -1615,15 +1756,19 @@ function buildDagSourceBinding(sources, taskKind) {
|
|
|
1615
1756
|
*/
|
|
1616
1757
|
function loadPersistedFrontendSourceBindingLedger(sources) {
|
|
1617
1758
|
const referenceDocs = (sources.referenceDocuments ?? []).filter((doc) => doc.markdown.trim().length > 0);
|
|
1618
|
-
|
|
1619
|
-
|
|
1759
|
+
const ledgerAbsolutePath = path.join(sources.taskDir, "source", REQUIREMENT_LEDGER_FILE_NAME);
|
|
1760
|
+
// Text-created frontend tasks have no imported reference manifest. Their
|
|
1761
|
+
// task-owned projected requirement is nevertheless a valid canonical source
|
|
1762
|
+
// document and is persisted in the v2 ledger by source preparation. Keep it
|
|
1763
|
+
// in the current-input map below instead of silently downgrading to v1.
|
|
1620
1764
|
// 存在性守卫(保持既有 v1 回退行为):派生视图无可抽取 requirement/acceptance
|
|
1621
1765
|
// 引用时不要求持久化 ledger,直接回退 base binding。此守卫只用于"是否 ledger
|
|
1622
1766
|
// 任务"判定,绝不参与 digest 键计算——键输入完全来自持久化 ledger 恢复。
|
|
1623
1767
|
const guardFacts = extractRequirementFactsFromMarkdown(sources.requirementMarkdown);
|
|
1624
|
-
if (guardFacts.acceptanceCriteria.length === 0)
|
|
1768
|
+
if (guardFacts.acceptanceCriteria.length === 0 && referenceDocs.length > 0)
|
|
1769
|
+
return null;
|
|
1770
|
+
if (referenceDocs.length === 0 && !existsSync(ledgerAbsolutePath))
|
|
1625
1771
|
return null;
|
|
1626
|
-
const ledgerAbsolutePath = path.join(sources.taskDir, "source", REQUIREMENT_LEDGER_FILE_NAME);
|
|
1627
1772
|
let raw;
|
|
1628
1773
|
try {
|
|
1629
1774
|
raw = readFileSync(ledgerAbsolutePath, "utf8");
|
|
@@ -1639,6 +1784,13 @@ function loadPersistedFrontendSourceBindingLedger(sources) {
|
|
|
1639
1784
|
catch (error) {
|
|
1640
1785
|
throw new Error(`SOURCE_FIDELITY_LEDGER_MISSING: persisted ${REQUIREMENT_LEDGER_FILE_NAME} is not a valid v1 ledger: ${error instanceof Error ? error.message : String(error)}`);
|
|
1641
1786
|
}
|
|
1787
|
+
const ledgerErrors = validateRequirementLedger(ledger);
|
|
1788
|
+
if (ledgerErrors.length > 0) {
|
|
1789
|
+
const first = ledgerErrors[0];
|
|
1790
|
+
throw new Error(`${first.code}: persisted ${REQUIREMENT_LEDGER_FILE_NAME} is invalid; refusing to start a frontend DAG with incomplete source bindings (${ledgerErrors
|
|
1791
|
+
.map((error) => error.message)
|
|
1792
|
+
.join("; ")})`);
|
|
1793
|
+
}
|
|
1642
1794
|
// canonical 侧:ledger.canonicalRequirements 按 build 顺序原样保留输入 canonical
|
|
1643
1795
|
// requirements(id/text 逐字节),过滤物化 REQ-SRC-*(AC-HARD-002 "物化不入键")
|
|
1644
1796
|
// 即精确恢复 digest 输入集;派生导航视图 需求.md 投影不参与键计算。
|
|
@@ -1657,6 +1809,13 @@ function loadPersistedFrontendSourceBindingLedger(sources) {
|
|
|
1657
1809
|
for (const doc of referenceDocs) {
|
|
1658
1810
|
currentByPath.set(toDagSourcePath(sources, doc.path), doc.markdown);
|
|
1659
1811
|
}
|
|
1812
|
+
if (referenceDocs.length === 0) {
|
|
1813
|
+
currentByPath.set(toDagSourcePath(sources, sources.requirementPath),
|
|
1814
|
+
// applyTaskContract appends a derived `## Source ledger` navigation
|
|
1815
|
+
// block after the ledger input has been hashed. Remove that block when
|
|
1816
|
+
// reconstructing the canonical text digest.
|
|
1817
|
+
sources.requirementMarkdown.replace(/\n## Source ledger\b[\s\S]*$/u, ""));
|
|
1818
|
+
}
|
|
1660
1819
|
const missingAuthoritativePaths = contract.sourcePaths.filter((sourcePath) => !currentByPath.has(sourcePath));
|
|
1661
1820
|
if (missingAuthoritativePaths.length > 0) {
|
|
1662
1821
|
throw new Error(`SOURCE_FIDELITY_LEDGER_STALE: persisted ${REQUIREMENT_LEDGER_FILE_NAME} was built from authoritative source documents no longer present (${missingAuthoritativePaths.join(", ")}); re-run task advance (intake) to rebuild the ledger`);
|
|
@@ -1684,6 +1843,7 @@ function loadPersistedFrontendSourceBindingLedger(sources) {
|
|
|
1684
1843
|
ledgerPath: toDagSourcePath(sources, ledgerAbsolutePath),
|
|
1685
1844
|
ledgerSha256,
|
|
1686
1845
|
inputDigest: ledger.inputDigest,
|
|
1846
|
+
requirementIds: ledger.canonicalRequirements.map((requirement) => requirement.id),
|
|
1687
1847
|
requirementToFragments,
|
|
1688
1848
|
};
|
|
1689
1849
|
}
|
|
@@ -1703,7 +1863,10 @@ function buildBackendTestAnalysisSourceBindingContract(sources) {
|
|
|
1703
1863
|
requirementIds: binding.requirementIds,
|
|
1704
1864
|
};
|
|
1705
1865
|
}
|
|
1706
|
-
function buildSourceContextBlock(sources) {
|
|
1866
|
+
function buildSourceContextBlock(sources, options = {}) {
|
|
1867
|
+
const includeRequirementExcerpt = options.includeRequirementExcerpt ?? true;
|
|
1868
|
+
const includeConstraintExcerpt = options.includeConstraintExcerpt ?? true;
|
|
1869
|
+
const includeReferenceDocuments = options.includeReferenceDocuments ?? true;
|
|
1707
1870
|
const requirementRef = toDagSourcePath(sources, sources.requirementPath);
|
|
1708
1871
|
const requirementExcerpt = excerptMarkdown(sources.requirementMarkdown, {
|
|
1709
1872
|
sourceRef: requirementRef,
|
|
@@ -1712,7 +1875,7 @@ function buildSourceContextBlock(sources) {
|
|
|
1712
1875
|
const parts = [
|
|
1713
1876
|
`## Task source: 需求.md`,
|
|
1714
1877
|
`Bound readPath (use for Pi read-tool calls): ${requirementRef}`,
|
|
1715
|
-
requirementExcerpt.text,
|
|
1878
|
+
...(includeRequirementExcerpt ? [requirementExcerpt.text] : []),
|
|
1716
1879
|
];
|
|
1717
1880
|
if (sources.constraintMarkdown) {
|
|
1718
1881
|
const constraintRef = toDagSourcePath(sources, sources.constraintPath);
|
|
@@ -1720,9 +1883,11 @@ function buildSourceContextBlock(sources) {
|
|
|
1720
1883
|
sourceRef: constraintRef,
|
|
1721
1884
|
});
|
|
1722
1885
|
boundReadPaths.push(`- constraints: ${constraintRef}`);
|
|
1723
|
-
parts.push("## Task source: 执行约束.md", `Bound readPath (use for Pi read-tool calls): ${constraintRef}`, constraintExcerpt.text);
|
|
1886
|
+
parts.push("## Task source: 执行约束.md", `Bound readPath (use for Pi read-tool calls): ${constraintRef}`, ...(includeConstraintExcerpt ? [constraintExcerpt.text] : []));
|
|
1724
1887
|
}
|
|
1725
|
-
for (const reference of (
|
|
1888
|
+
for (const reference of (includeReferenceDocuments
|
|
1889
|
+
? sources.referenceDocuments ?? []
|
|
1890
|
+
: []).slice(0, MAX_INLINE_SOURCE_REFERENCE_DOCUMENTS)) {
|
|
1726
1891
|
const relativePath = path
|
|
1727
1892
|
.relative(path.join(sources.taskDir, "source"), reference.path)
|
|
1728
1893
|
.replaceAll(path.sep, "/");
|
|
@@ -1811,6 +1976,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
1811
1976
|
const manifest = await loadHarnessManifest(repoRoot);
|
|
1812
1977
|
const strategy = resolveDagVerifyStrategy(taskConfig);
|
|
1813
1978
|
let verifyCommands;
|
|
1979
|
+
let verificationPreflight = [];
|
|
1814
1980
|
try {
|
|
1815
1981
|
const adapter = await resolveAdapter(repoRoot);
|
|
1816
1982
|
const preset = resolveVerifyPreset(taskConfig.verifyPreset, taskConfig);
|
|
@@ -1834,7 +2000,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
1834
2000
|
...taskVerifyCommands(repoRoot, taskConfig),
|
|
1835
2001
|
...adapterFinal,
|
|
1836
2002
|
]);
|
|
1837
|
-
assertVerificationPlanPreflight({
|
|
2003
|
+
verificationPreflight = assertVerificationPlanPreflight({
|
|
1838
2004
|
repoRoot,
|
|
1839
2005
|
commands: [...intermediatePlan.commands, ...finalPlan.commands],
|
|
1840
2006
|
taskConfig,
|
|
@@ -1853,6 +2019,16 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
1853
2019
|
catch (error) {
|
|
1854
2020
|
throw new Error(`failed to load verification commands for task "${taskId}": ${error instanceof Error ? error.message : String(error)}`);
|
|
1855
2021
|
}
|
|
2022
|
+
const frontendShapeTransitionCapsule = taskConfig.taskKind === "frontend-implementation" && CANONICAL_TASK_ID_PATTERN.test(taskId)
|
|
2023
|
+
? await discoverLatestCommittedFrontendShapeCapsule({
|
|
2024
|
+
repoRoot,
|
|
2025
|
+
taskId,
|
|
2026
|
+
sourceDigest: computeFrontendShapeSourceDigest({
|
|
2027
|
+
requirementMarkdown,
|
|
2028
|
+
constraintMarkdown: constraintMarkdown ?? "",
|
|
2029
|
+
}),
|
|
2030
|
+
})
|
|
2031
|
+
: undefined;
|
|
1856
2032
|
const sources = {
|
|
1857
2033
|
taskId,
|
|
1858
2034
|
repoRoot,
|
|
@@ -1867,8 +2043,10 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
1867
2043
|
enabledExecutors: resolveEnabledExecutors(manifest.executors),
|
|
1868
2044
|
executorModelMatrix: resolveExecutorModelMatrices(manifest),
|
|
1869
2045
|
verifyCommands,
|
|
2046
|
+
verificationPreflight,
|
|
1870
2047
|
sddEmbeddedSkills: await probeRepoLocalSddSkills(repoRoot),
|
|
1871
2048
|
projectGovernancePresent: await discoverProjectGovernancePresence(repoRoot),
|
|
2049
|
+
...(frontendShapeTransitionCapsule ? { autoloadedFrontendShapeTransitionCapsule: frontendShapeTransitionCapsule } : {}),
|
|
1872
2050
|
};
|
|
1873
2051
|
return sources;
|
|
1874
2052
|
}
|
|
@@ -1886,7 +2064,9 @@ async function prepareFrontendMockSources(sources, discoveredProjectCapability)
|
|
|
1886
2064
|
}
|
|
1887
2065
|
}
|
|
1888
2066
|
const projectCapability = discoveredProjectCapability ??
|
|
1889
|
-
(await discoverFrontendProjectCapability(repoRoot
|
|
2067
|
+
(await discoverFrontendProjectCapability(repoRoot, {
|
|
2068
|
+
specRoots: sources.taskConfig.frontendOpenspec?.specRoots,
|
|
2069
|
+
}));
|
|
1890
2070
|
const frontendRisk = classifyFrontendRisk({
|
|
1891
2071
|
title: sources.taskConfig.title,
|
|
1892
2072
|
requirementMarkdown: sources.requirementMarkdown,
|
|
@@ -1957,6 +2137,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1957
2137
|
finalFullRequired: true,
|
|
1958
2138
|
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
1959
2139
|
plan: finalVerifyPlan,
|
|
2140
|
+
preflight: sources.verificationPreflight,
|
|
1960
2141
|
}),
|
|
1961
2142
|
cwd: ".",
|
|
1962
2143
|
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
@@ -2149,7 +2330,7 @@ function buildFrontendMockAssessNode(sources, sourceContext, mockContextBlock, f
|
|
|
2149
2330
|
writePolicy: "read-only",
|
|
2150
2331
|
allowedPaths: readOnlyPaths,
|
|
2151
2332
|
forbiddenPaths,
|
|
2152
|
-
skills:
|
|
2333
|
+
skills: FRONTEND_PLAN_SKILLS,
|
|
2153
2334
|
firstProtocolLine: "MOCK_STRATEGY:",
|
|
2154
2335
|
outputContract: "Plain Markdown whose first line is MOCK_STRATEGY: native|browser-intercept|request-adapter|not-needed|blocked, followed by Mock Decision, API Contract Evidence, Specification Evidence, Service Evidence, Backend Readiness, Selection Evidence, Endpoint / Fixture Matrix, Activation, Target Files, Production Safety, Verification Plan, Real Integration Gap, and Blocking Issues. No file writes.",
|
|
2155
2336
|
subtask_prompt: [
|
|
@@ -2284,6 +2465,7 @@ function buildFrontendMockVerifyNode(sources, implementId, readOnlyPaths, forbid
|
|
|
2284
2465
|
fallbackCommands: [],
|
|
2285
2466
|
commandTexts: commands,
|
|
2286
2467
|
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
2468
|
+
preflight: sources.verificationPreflight,
|
|
2287
2469
|
}),
|
|
2288
2470
|
cwd: ".",
|
|
2289
2471
|
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
@@ -2397,40 +2579,181 @@ function frontendMockStrategyMustBeNotNeeded(sources) {
|
|
|
2397
2579
|
capabilityStatus === "ambiguous" ||
|
|
2398
2580
|
!hasDeterministicMockVerification));
|
|
2399
2581
|
}
|
|
2400
|
-
|
|
2582
|
+
/**
|
|
2583
|
+
* 候选规范懒加载:prompt 只内联与任务相关的候选,不全部塞给模型。
|
|
2584
|
+
*
|
|
2585
|
+
* - mandatory(任务源显式声明/引用)始终保留——模型必须知道它们存在。
|
|
2586
|
+
* - scan-strict 候选按「路径段是否命中任务源关键词」过滤:列表页任务只
|
|
2587
|
+
* 带出列表相关组件/页面规范,创建页规范不进 prompt。
|
|
2588
|
+
* - runtime 的选型/read 门禁仍消费完整 openspecCandidatePaths(prewrite
|
|
2589
|
+
* gate 用全量);这里只缩小 prompt 体积,不改变门禁语义。未提及的候选
|
|
2590
|
+
* 由 runtime 默认 irrelevant,模型无需枚举。
|
|
2591
|
+
* - 真正读取发生在 plan 侧:模型对 required 选型调用 read 工具,prewrite
|
|
2592
|
+
* 门禁验证 read 事件——按需读取,不预加载正文。
|
|
2593
|
+
*/
|
|
2594
|
+
export function filterRelevantOpenspecCandidates(input) {
|
|
2595
|
+
const mandatory = new Set(input.mandatoryPaths);
|
|
2596
|
+
const relevant = [];
|
|
2597
|
+
for (const candidate of input.candidates) {
|
|
2598
|
+
if (mandatory.has(candidate)) {
|
|
2599
|
+
relevant.push(candidate);
|
|
2600
|
+
continue;
|
|
2601
|
+
}
|
|
2602
|
+
if (openspecCandidateMatchesSource(candidate, input.sourceMarkdown)) {
|
|
2603
|
+
relevant.push(candidate);
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
// 保持候选发现顺序(确定性);mandatory 与相关候选都按原始顺序出现。
|
|
2607
|
+
const maxCandidates = input.maxCandidates ?? 24;
|
|
2608
|
+
if (relevant.length <= maxCandidates)
|
|
2609
|
+
return relevant;
|
|
2610
|
+
const mandatoryRelevant = relevant.filter((candidate) => mandatory.has(candidate));
|
|
2611
|
+
const optionalRelevant = relevant.filter((candidate) => !mandatory.has(candidate));
|
|
2612
|
+
return [
|
|
2613
|
+
...mandatoryRelevant,
|
|
2614
|
+
...optionalRelevant.slice(0, Math.max(0, maxCandidates - mandatoryRelevant.length)),
|
|
2615
|
+
];
|
|
2616
|
+
}
|
|
2617
|
+
function buildFrontendSpecCandidateSummaries(input) {
|
|
2618
|
+
const mandatory = new Set(input.mandatoryPaths);
|
|
2619
|
+
return input.paths
|
|
2620
|
+
.map((candidate) => scoreFrontendSpecCandidate(candidate, "", {
|
|
2621
|
+
registryMode: input.registryModes.get(candidate),
|
|
2622
|
+
taskRelated: mandatory.has(candidate) || openspecCandidateMatchesSource(candidate, input.sourceMarkdown),
|
|
2623
|
+
}))
|
|
2624
|
+
.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
2625
|
+
}
|
|
2626
|
+
/**
|
|
2627
|
+
* 候选路径是否与任务源相关:从任务源提取文件名/路径 token(去扩展名、
|
|
2628
|
+
* 去连字符/下划线),若候选的路径段包含任一 token 即视为相关。纯字符串
|
|
2629
|
+
* 判定,无 IO;找不到 token 时保守保留(避免漏掉模型可能需要的规范)。
|
|
2630
|
+
*/
|
|
2631
|
+
function openspecCandidateMatchesSource(candidate, sourceMarkdown) {
|
|
2632
|
+
const sourceTokens = extractOpenspecSourceTokens(sourceMarkdown);
|
|
2633
|
+
if (sourceTokens.size === 0)
|
|
2634
|
+
return true;
|
|
2635
|
+
const candidateLower = candidate.toLowerCase().replace(/\\/g, "/");
|
|
2636
|
+
const candidateSegments = candidateLower.split("/");
|
|
2637
|
+
const candidateBasename = candidateSegments[candidateSegments.length - 1] ?? "";
|
|
2638
|
+
for (const token of sourceTokens) {
|
|
2639
|
+
if (candidateBasename.includes(token) ||
|
|
2640
|
+
candidateLower.includes(`/${token}/`) ||
|
|
2641
|
+
candidateLower.includes(`${token}.`)) {
|
|
2642
|
+
return true;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
return false;
|
|
2646
|
+
}
|
|
2647
|
+
/** 从任务源 markdown 提取显著 token:反引号代码段、路径、组件/页面名。 */
|
|
2648
|
+
function extractOpenspecSourceTokens(markdown) {
|
|
2649
|
+
const tokens = new Set();
|
|
2650
|
+
const push = (raw) => {
|
|
2651
|
+
const cleaned = raw
|
|
2652
|
+
.trim()
|
|
2653
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "")
|
|
2654
|
+
.toLowerCase();
|
|
2655
|
+
if (cleaned.length >= 2 && cleaned.length <= 40)
|
|
2656
|
+
tokens.add(cleaned);
|
|
2657
|
+
};
|
|
2658
|
+
// 反引号内联代码(文件名/组件名)
|
|
2659
|
+
for (const match of markdown.matchAll(/`([^`\n]+)`/g)) {
|
|
2660
|
+
push(match[1] ?? "");
|
|
2661
|
+
}
|
|
2662
|
+
// markdown 链接文本
|
|
2663
|
+
for (const match of markdown.matchAll(/\[([^\]]+)\]\([^)]+\)/g)) {
|
|
2664
|
+
push(match[1] ?? "");
|
|
2665
|
+
}
|
|
2666
|
+
// 路径 token(openspec/ 或 xxx/xxx.md)
|
|
2667
|
+
for (const match of markdown.matchAll(/(?:[A-Za-z0-9_-]+\/)+[A-Za-z0-9_.-]+/g)) {
|
|
2668
|
+
const segments = (match[0] ?? "").split("/");
|
|
2669
|
+
const basename = segments[segments.length - 1] ?? "";
|
|
2670
|
+
push(basename.replace(/\.[a-z0-9]+$/i, ""));
|
|
2671
|
+
for (const segment of segments)
|
|
2672
|
+
push(segment);
|
|
2673
|
+
}
|
|
2674
|
+
return tokens;
|
|
2675
|
+
}
|
|
2676
|
+
async function resolveFrontendOpenspecGateConfig(sources) {
|
|
2401
2677
|
const taskConfig = sources.taskConfig;
|
|
2402
2678
|
const policy = taskConfig.frontendOpenspec?.policy ?? "cited";
|
|
2403
2679
|
const declared = taskConfig.frontendOpenspec?.requiredReadPaths ?? [];
|
|
2404
|
-
const
|
|
2680
|
+
const governanceRoot = sources.frontendProjectCapability?.openspecDiscovery?.governanceRoot ??
|
|
2681
|
+
DEFAULT_OPENSPEC_GOVERNANCE_ROOT;
|
|
2682
|
+
const specRoots = taskConfig.frontendOpenspec?.specRoots ?? DEFAULT_FRONTEND_SPEC_ROOTS;
|
|
2683
|
+
const rootAliases = sources.repoRoot
|
|
2684
|
+
? await resolveFrontendSpecRootAliases(sources.repoRoot, [...specRoots])
|
|
2685
|
+
: [];
|
|
2686
|
+
const remappedSourceMarkdown = rootAliases.reduce((markdown, { alias, logicalRoot }) => markdown.replace(new RegExp(`(^|[^A-Za-z0-9_.-])${alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`, "g"), `$1${logicalRoot}/`), [sources.requirementMarkdown, sources.constraintMarkdown ?? ""].join("\n"));
|
|
2687
|
+
const taskSourceCited = extractTaskSourceFrontendSpecPaths(remappedSourceMarkdown, specRoots, governanceRoot);
|
|
2405
2688
|
const scanStrict = sources.frontendProjectCapability?.designEvidence.normativePaths ?? [];
|
|
2689
|
+
const registry = sources.repoRoot
|
|
2690
|
+
? await readFrontendSpecRegistry(sources.repoRoot)
|
|
2691
|
+
: null;
|
|
2692
|
+
const registryModes = new Map();
|
|
2693
|
+
for (const root of registry?.roots ?? []) {
|
|
2694
|
+
if (root.scope && !root.scope.includes("frontend"))
|
|
2695
|
+
continue;
|
|
2696
|
+
if (!root.mode)
|
|
2697
|
+
continue;
|
|
2698
|
+
for (const candidate of scanStrict) {
|
|
2699
|
+
if (candidate === root.path || candidate.startsWith(`${root.path}/`)) {
|
|
2700
|
+
registryModes.set(candidate, root.mode);
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2406
2704
|
const dedupeSorted = (paths) => [...new Set(paths)].sort();
|
|
2407
2705
|
const openspecCandidateSources = {
|
|
2408
2706
|
declared: dedupeSorted(declared),
|
|
2409
2707
|
taskSourceCited: dedupeSorted(taskSourceCited),
|
|
2410
2708
|
scanStrict: dedupeSorted(scanStrict),
|
|
2411
2709
|
};
|
|
2710
|
+
// Candidate discovery is frozen independently from the eventual must-read
|
|
2711
|
+
// set. The selector may mark candidates irrelevant, while explicit task
|
|
2712
|
+
// declarations/source citations are never allowed to be downgraded.
|
|
2713
|
+
const openspecMandatoryPaths = dedupeSorted([
|
|
2714
|
+
...declared,
|
|
2715
|
+
...taskSourceCited,
|
|
2716
|
+
]);
|
|
2717
|
+
// `cited` must stay demand-driven: a normative registry makes a path
|
|
2718
|
+
// discoverable, not implicitly task-mandatory. Only scan-strict may offer
|
|
2719
|
+
// the global discovery set to the bounded task-relevance selector.
|
|
2412
2720
|
const openspecCandidatePaths = policy === "cited"
|
|
2413
|
-
?
|
|
2414
|
-
:
|
|
2415
|
-
|
|
2721
|
+
? openspecMandatoryPaths
|
|
2722
|
+
: dedupeSorted([
|
|
2723
|
+
...openspecCandidateSources.scanStrict,
|
|
2724
|
+
...openspecMandatoryPaths,
|
|
2725
|
+
]);
|
|
2726
|
+
const result = {
|
|
2416
2727
|
openspecPolicy: policy,
|
|
2417
2728
|
openspecCandidatePaths,
|
|
2418
2729
|
openspecCandidateSources,
|
|
2730
|
+
openspecMandatoryPaths,
|
|
2731
|
+
openspecCandidateSummaries: buildFrontendSpecCandidateSummaries({
|
|
2732
|
+
paths: openspecCandidatePaths,
|
|
2733
|
+
mandatoryPaths: openspecMandatoryPaths,
|
|
2734
|
+
registryModes,
|
|
2735
|
+
sourceMarkdown: [sources.requirementMarkdown, sources.constraintMarkdown ?? ""].join("\n"),
|
|
2736
|
+
}),
|
|
2419
2737
|
};
|
|
2738
|
+
if (sources.repoRoot && openspecCandidatePaths.length > 0) {
|
|
2739
|
+
result.openspecCandidateSnapshots = await Promise.all(openspecCandidatePaths.map(async (candidate) => ({
|
|
2740
|
+
path: candidate,
|
|
2741
|
+
sha256: createHash("sha256")
|
|
2742
|
+
.update(await readFile(path.join(sources.repoRoot, candidate)))
|
|
2743
|
+
.digest("hex"),
|
|
2744
|
+
})));
|
|
2745
|
+
}
|
|
2746
|
+
return result;
|
|
2420
2747
|
}
|
|
2421
|
-
const openspecCitationInstruction = [
|
|
2422
|
-
"OpenSpec 引用块(citation block):在 fenced json 契约块之后,追加**恰好一个** ```openspec-citations 围栏代码块(三反引号 + openspec-citations)。",
|
|
2423
|
-
'该块内每行一个 JSON 对象 {"path":"<repo 相对 openspec 路径>","section":"<命中章节或空串>","line":<int 或 null>},必须逐条列出你在本计划中实际读取并应用的每个 openspec 规范文件。',
|
|
2424
|
-
"prewrite gate 会用真实 read 事件核验每条引用:引用存在但无成功 read 事件 → openspec-citation-not-read;契约冻结的必读候选未被引用 → openspec-not-cited;两者都 fail-closed。不要引用未读取的路径。",
|
|
2425
|
-
].join("\n");
|
|
2426
2748
|
const frontendComponentConformanceInstruction = [
|
|
2427
2749
|
"## Component Selection conformance (uiComponentChoices; hard rule)",
|
|
2428
2750
|
"每个 UI 用途必须在契约的 uiComponentChoices[] 中声明组件选型:{ purpose, component, decision, specReference, rationale }。",
|
|
2751
|
+
"purpose is the stable coverage key:它应精确匹配 interaction.name 或 uiState.name;职责语义由对应 interaction.expectedBehavior / uiState.expectedBehavior 与 rationale 表达。不得仅因 purpose 与 interaction 或 component 标识符相同而判缺陷。",
|
|
2429
2752
|
"- decision=specified:前端规范(候选组件/主题桶 + 任务源显式引用)已定义该用途组件 → 必须使用该组件,并给精确 specReference { path, section, line }(path 必须是 openspec/ai_workspace 受支持规范路径)。",
|
|
2430
|
-
"- decision=reuse-existing
|
|
2431
|
-
"- decision=new
|
|
2753
|
+
"- decision=reuse-existing:仅当该组件/惯例**确实已存在于仓库当前代码**(如复用现有 ActiveRunBadge 的 oc- class 惯例)→ specReference 可为 null,rationale 必须指明复用的具体现有组件/文件与依据。",
|
|
2754
|
+
"- decision=new:任务源/PRD 要求**新增**该组件(仓库当前不存在该组件文件)→ decision 必须为 new,不得标 reuse-existing;调用 record_component_choice 时传 sourceRequirementIds(关联的 frozen requirement ID)与 plan checklist 列出的 sourceFragmentId,runtime 校验其隶属关系并物化精确的任务源 PRD { path, section, line }。不要读取 PRD 或手填/猜测 specReference;rationale 说明新增纯展示组件、复用既有 CSS 命名与主题变量约定。",
|
|
2432
2755
|
"不得静默替换规范组件或自创组件而无偏差声明;spec 已定义该用途组件时不得改选其它组件。",
|
|
2433
|
-
"prewrite gate
|
|
2756
|
+
"prewrite gate 确定性交叉校验:仅 decision=specified 的 specReference 必须是候选 OpenSpec 路径、在 typed decision ledger 中声明且有成功 read 事件;decision=new 的 PRD 引用走任务源可追溯性审查,不得按 OpenSpec 候选拒绝。候选桶非空且契约有 UI 可见工作而 uiComponentChoices 缺失/空 → component-choices-missing。",
|
|
2434
2757
|
].join("\n");
|
|
2435
2758
|
function resolveFrontendCapabilityContextBlock(sources) {
|
|
2436
2759
|
const risk = sources.frontendRisk;
|
|
@@ -2444,34 +2767,7 @@ function resolveFrontendCapabilityContextBlock(sources) {
|
|
|
2444
2767
|
}
|
|
2445
2768
|
if (capability) {
|
|
2446
2769
|
parts.push("", capability.adapterGuidance);
|
|
2447
|
-
|
|
2448
|
-
const bucketLines = [];
|
|
2449
|
-
const pushBucket = (label, paths) => {
|
|
2450
|
-
if (paths.length > 0)
|
|
2451
|
-
bucketLines.push(`${label}: ${paths.join(", ")}`);
|
|
2452
|
-
};
|
|
2453
|
-
pushBucket("schemas", classified.schemas);
|
|
2454
|
-
pushBucket("code-template", classified.codeTemplate);
|
|
2455
|
-
pushBucket("rule.api", classified.rule.api);
|
|
2456
|
-
pushBucket("rule.mock", classified.rule.mock);
|
|
2457
|
-
pushBucket("rule.router", classified.rule.router);
|
|
2458
|
-
pushBucket("rule.hooks", classified.rule.hooks);
|
|
2459
|
-
pushBucket("rule.utils", classified.rule.utils);
|
|
2460
|
-
pushBucket("rule.components", classified.rule.components);
|
|
2461
|
-
pushBucket("rule.other", classified.rule.other);
|
|
2462
|
-
pushBucket("theme", classified.theme);
|
|
2463
|
-
pushBucket("component", classified.component);
|
|
2464
|
-
pushBucket("ui-other", classified.uiOther);
|
|
2465
|
-
pushBucket("advisory-other", classified.advisoryOther);
|
|
2466
|
-
parts.push("## Classified openspec specification paths (role semantics)");
|
|
2467
|
-
if (bucketLines.length > 0) {
|
|
2468
|
-
parts.push(...bucketLines);
|
|
2469
|
-
}
|
|
2470
|
-
else {
|
|
2471
|
-
parts.push("(no openspec specification paths discovered — greenfield)");
|
|
2472
|
-
}
|
|
2473
|
-
parts.push("component / theme / rule.components 是「组件/主题规范」必读语义桶:规范已定义某用途组件时必须使用它(uiComponentChoices 用 decision=specified + 精确 path/section/line),不得静默替换为自认更合适的组件;无规范定义时才允许 reuse-existing 或 new(new 必须声明偏差 rationale)。这些路径在生成期冻结为 prewrite gate 的 componentSpecCandidatePaths。");
|
|
2474
|
-
parts.push("Each consuming node MUST report in its output: applicable rules, the hit path/section/line number for every applied specification, and any conflicts or missing specifications. Missing or conflicting required specifications must fail closed rather than silently substituting nearby repository conventions.");
|
|
2770
|
+
parts.push("Task-relevant OpenSpec candidates are injected separately as a bounded Top-K index. The deterministic prewrite gate retains the complete frozen candidate set; unlisted candidates are neither silently required nor evidence of a missing specification.");
|
|
2475
2771
|
parts.push(`A11y capability: ${capability.a11y.status}` +
|
|
2476
2772
|
(capability.a11y.tools.length
|
|
2477
2773
|
? ` (${capability.a11y.tools.join(", ")})`
|
|
@@ -2480,17 +2776,102 @@ function resolveFrontendCapabilityContextBlock(sources) {
|
|
|
2480
2776
|
}
|
|
2481
2777
|
return parts.join("\n");
|
|
2482
2778
|
}
|
|
2779
|
+
function pruneFrontendTasksForMicro(tasks) {
|
|
2780
|
+
// Micro topology (7 nodes): contract-import-shell → writer-admission →
|
|
2781
|
+
// implement → verify → review-context → review → closeout. Drops scout/plan/
|
|
2782
|
+
// design-policy/design-review and replaces the model contract node with a
|
|
2783
|
+
// deterministic contract-import shell (§7.3: no model, no requirement
|
|
2784
|
+
// rewrites — micro consumes a pre-validated managed Contract).
|
|
2785
|
+
const drop = new Set([
|
|
2786
|
+
"frontend-contract-pi",
|
|
2787
|
+
"frontend-scout-pi",
|
|
2788
|
+
"frontend-plan-pi",
|
|
2789
|
+
"frontend-design-policy-shell",
|
|
2790
|
+
"frontend-design-review-pi",
|
|
2791
|
+
]);
|
|
2792
|
+
const contractPi = tasks.find((task) => task.id === "frontend-contract-pi");
|
|
2793
|
+
const contractImportShell = contractPi
|
|
2794
|
+
? {
|
|
2795
|
+
id: "frontend-contract-import-shell",
|
|
2796
|
+
role: "verifier",
|
|
2797
|
+
executor: "shell",
|
|
2798
|
+
complexity: "LOW",
|
|
2799
|
+
writePolicy: "read-only",
|
|
2800
|
+
depends_on: [],
|
|
2801
|
+
allowedPaths: contractPi.allowedPaths,
|
|
2802
|
+
forbiddenPaths: contractPi.forbiddenPaths,
|
|
2803
|
+
subtask_prompt: "Deterministic managed-Contract import + source/binding/schema freshness re-validation; no model, no requirement rewrite.",
|
|
2804
|
+
shell: {
|
|
2805
|
+
commands: [],
|
|
2806
|
+
frontendContractImport: {
|
|
2807
|
+
schemaVersion: 1,
|
|
2808
|
+
artifactName: "frontend-task-contract.json",
|
|
2809
|
+
outputDir: "contracts",
|
|
2810
|
+
requireSourceFreshness: true,
|
|
2811
|
+
},
|
|
2812
|
+
cwd: ".",
|
|
2813
|
+
timeoutMs: 60000,
|
|
2814
|
+
},
|
|
2815
|
+
}
|
|
2816
|
+
: undefined;
|
|
2817
|
+
const filtered = [
|
|
2818
|
+
...(contractImportShell ? [contractImportShell] : []),
|
|
2819
|
+
...tasks.filter((task) => !drop.has(task.id)),
|
|
2820
|
+
];
|
|
2821
|
+
const byId = new Map(filtered.map((task) => [task.id, task]));
|
|
2822
|
+
const remap = (deps) => {
|
|
2823
|
+
if (!deps)
|
|
2824
|
+
return [];
|
|
2825
|
+
const next = [];
|
|
2826
|
+
for (const dep of deps) {
|
|
2827
|
+
if (drop.has(dep))
|
|
2828
|
+
continue;
|
|
2829
|
+
if (byId.has(dep))
|
|
2830
|
+
next.push(dep);
|
|
2831
|
+
}
|
|
2832
|
+
return [...new Set(next)];
|
|
2833
|
+
};
|
|
2834
|
+
return filtered.map((task) => {
|
|
2835
|
+
const depends_on = remap(task.depends_on);
|
|
2836
|
+
if (task.id === "frontend-writer-admission-shell") {
|
|
2837
|
+
const admission = task.shell?.frontendWriterAdmission;
|
|
2838
|
+
return {
|
|
2839
|
+
...task,
|
|
2840
|
+
depends_on: ["frontend-contract-import-shell"],
|
|
2841
|
+
shell: admission
|
|
2842
|
+
? {
|
|
2843
|
+
...task.shell,
|
|
2844
|
+
commands: task.shell?.commands ?? [],
|
|
2845
|
+
frontendWriterAdmission: {
|
|
2846
|
+
...admission,
|
|
2847
|
+
designReviewFromNodeId: undefined,
|
|
2848
|
+
},
|
|
2849
|
+
}
|
|
2850
|
+
: task.shell,
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
return { ...task, depends_on };
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
function pruneFrontendTasksForSplitRequired(tasks) {
|
|
2857
|
+
const writerChain = new Set([
|
|
2858
|
+
"frontend-writer-admission-shell",
|
|
2859
|
+
"frontend-implement-pi",
|
|
2860
|
+
"frontend-verify-shell",
|
|
2861
|
+
"frontend-review-context-shell",
|
|
2862
|
+
"frontend-review-pi",
|
|
2863
|
+
"frontend-closeout-shell",
|
|
2864
|
+
]);
|
|
2865
|
+
return tasks.filter((task) => !writerChain.has(task.id));
|
|
2866
|
+
}
|
|
2483
2867
|
function pruneFrontendTasksForRisk(tasks, risk) {
|
|
2484
2868
|
if (risk.forceFullGates || risk.selectedRisk !== "small") {
|
|
2485
2869
|
return tasks;
|
|
2486
2870
|
}
|
|
2487
|
-
// Small topology keeps one design review and removes only the
|
|
2488
|
-
//
|
|
2489
|
-
//
|
|
2490
|
-
const drop = new Set([
|
|
2491
|
-
"frontend-plan-revision-pi",
|
|
2492
|
-
"frontend-final-design-review-pi",
|
|
2493
|
-
]);
|
|
2871
|
+
// Small topology keeps one design review and removes only the redundant
|
|
2872
|
+
// design-review node; the deterministic design-policy and writer-admission
|
|
2873
|
+
// shells consume the surviving plan directly.
|
|
2874
|
+
const drop = new Set(["frontend-design-review-pi"]);
|
|
2494
2875
|
const filtered = tasks.filter((task) => !drop.has(task.id));
|
|
2495
2876
|
const byId = new Map(filtered.map((task) => [task.id, task]));
|
|
2496
2877
|
const remap = (deps) => {
|
|
@@ -2498,11 +2879,8 @@ function pruneFrontendTasksForRisk(tasks, risk) {
|
|
|
2498
2879
|
return [];
|
|
2499
2880
|
const next = [];
|
|
2500
2881
|
for (const dep of deps) {
|
|
2501
|
-
if (dep === "frontend-
|
|
2502
|
-
if (byId.has("frontend-plan-pi"))
|
|
2503
|
-
next.push("frontend-plan-pi");
|
|
2882
|
+
if (dep === "frontend-design-review-pi")
|
|
2504
2883
|
continue;
|
|
2505
|
-
}
|
|
2506
2884
|
if (byId.has(dep) || dep === "frontend-implement-pi")
|
|
2507
2885
|
next.push(dep);
|
|
2508
2886
|
}
|
|
@@ -2510,30 +2888,27 @@ function pruneFrontendTasksForRisk(tasks, risk) {
|
|
|
2510
2888
|
};
|
|
2511
2889
|
return filtered.map((task) => {
|
|
2512
2890
|
const depends_on = remap(task.depends_on);
|
|
2513
|
-
if (task.id === "frontend-
|
|
2514
|
-
|
|
2891
|
+
if (task.id === "frontend-writer-admission-shell") {
|
|
2892
|
+
// Small topology: no design review node, so the admission shell drops
|
|
2893
|
+
// the design-review dependency and the verdict requirement.
|
|
2894
|
+
const admission = task.shell?.frontendWriterAdmission;
|
|
2515
2895
|
return {
|
|
2516
2896
|
...task,
|
|
2517
|
-
depends_on: ["frontend-
|
|
2518
|
-
|
|
2519
|
-
shell: gate
|
|
2897
|
+
depends_on: ["frontend-design-policy-shell"],
|
|
2898
|
+
shell: admission
|
|
2520
2899
|
? {
|
|
2521
2900
|
...task.shell,
|
|
2522
2901
|
commands: task.shell?.commands ?? [],
|
|
2523
|
-
|
|
2524
|
-
...
|
|
2525
|
-
|
|
2526
|
-
planFallbackFromNodeIds: [],
|
|
2527
|
-
reviewFromNodeId: "frontend-design-review-pi",
|
|
2528
|
-
reviewFallbackFromNodeIds: [],
|
|
2529
|
-
revisionPatch: false,
|
|
2902
|
+
frontendWriterAdmission: {
|
|
2903
|
+
...admission,
|
|
2904
|
+
designReviewFromNodeId: undefined,
|
|
2530
2905
|
},
|
|
2531
2906
|
}
|
|
2532
2907
|
: task.shell,
|
|
2533
2908
|
};
|
|
2534
2909
|
}
|
|
2535
2910
|
if (task.id === "frontend-implement-pi") {
|
|
2536
|
-
for (const need of ["frontend-
|
|
2911
|
+
for (const need of ["frontend-writer-admission-shell"]) {
|
|
2537
2912
|
if (byId.has(need) && !depends_on.includes(need))
|
|
2538
2913
|
depends_on.push(need);
|
|
2539
2914
|
}
|
|
@@ -2558,9 +2933,54 @@ function buildFrontendWriterNodeDefaults(input) {
|
|
|
2558
2933
|
allowedPaths: input.allowedPaths,
|
|
2559
2934
|
forbiddenPaths: input.forbiddenPaths,
|
|
2560
2935
|
skills: FRONTEND_BOUNDED_IMPLEMENT_SKILLS,
|
|
2561
|
-
writerOutcomePolicy: {
|
|
2936
|
+
writerOutcomePolicy: {
|
|
2937
|
+
type: input.writerOutcomePolicyType ?? "implementation-outcome-v1",
|
|
2938
|
+
},
|
|
2562
2939
|
};
|
|
2563
2940
|
}
|
|
2941
|
+
function resolveFrontendShapeCapsuleGenerationInput(sources) {
|
|
2942
|
+
const explicit = sources.frontendShapeTransitionCapsule == null
|
|
2943
|
+
? undefined
|
|
2944
|
+
: parseFrontendShapeTransitionCapsule(sources.frontendShapeTransitionCapsule);
|
|
2945
|
+
const autoloaded = sources.autoloadedFrontendShapeTransitionCapsule == null
|
|
2946
|
+
? undefined
|
|
2947
|
+
: parseFrontendShapeTransitionCapsule(sources.autoloadedFrontendShapeTransitionCapsule);
|
|
2948
|
+
if (explicit && autoloaded && explicit.capsuleDigest !== autoloaded.capsuleDigest) {
|
|
2949
|
+
throw new Error("explicit frontend shape transition capsule conflicts with runtime autoload");
|
|
2950
|
+
}
|
|
2951
|
+
return explicit ?? autoloaded;
|
|
2952
|
+
}
|
|
2953
|
+
/** Standard frontend DAG template (docs/templates/frontend-implementation-dag.json).
|
|
2954
|
+
* It is the topology source of truth: node set, execution order, and depends_on
|
|
2955
|
+
* come from the template; the runtime generator assembles each node's full
|
|
2956
|
+
* configuration (budgets, retry, skeleton, skills, dynamic prompt sections).
|
|
2957
|
+
* Resolution order: repoRoot copy (dev workspace) → bundled package copy. */
|
|
2958
|
+
export async function loadFrontendDagTemplate(repoRoot) {
|
|
2959
|
+
const candidates = [];
|
|
2960
|
+
if (repoRoot) {
|
|
2961
|
+
candidates.push(path.join(repoRoot, "docs", "templates", "frontend-implementation-dag.json"));
|
|
2962
|
+
}
|
|
2963
|
+
candidates.push(fileURLToPath(new URL("../../../docs/templates/frontend-implementation-dag.json", import.meta.url)));
|
|
2964
|
+
for (const candidate of candidates) {
|
|
2965
|
+
try {
|
|
2966
|
+
const parsed = JSON.parse(await readFile(candidate, "utf8"));
|
|
2967
|
+
if (!Array.isArray(parsed.tasks))
|
|
2968
|
+
continue;
|
|
2969
|
+
const tasks = parsed.tasks.filter((item) => typeof item === "object" &&
|
|
2970
|
+
item !== null &&
|
|
2971
|
+
typeof item.id === "string" &&
|
|
2972
|
+
Array.isArray(item.depends_on) &&
|
|
2973
|
+
typeof item.executor === "string");
|
|
2974
|
+
if (tasks.length === 0)
|
|
2975
|
+
continue;
|
|
2976
|
+
return { tasks };
|
|
2977
|
+
}
|
|
2978
|
+
catch {
|
|
2979
|
+
// try next candidate
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
return null;
|
|
2983
|
+
}
|
|
2564
2984
|
async function buildFrontendHybridDagFromTask(sources) {
|
|
2565
2985
|
const { taskConfig } = sources;
|
|
2566
2986
|
const mockCapability = sources.frontendMockCapability ?? {
|
|
@@ -2585,6 +3005,11 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2585
3005
|
const implementId = frontendImplementationNodeId();
|
|
2586
3006
|
const mockContextBlock = resolveFrontendMockContextBlock(frontendSources);
|
|
2587
3007
|
const capabilityContextBlock = resolveFrontendCapabilityContextBlock(frontendSources);
|
|
3008
|
+
// Freeze the design-policy dependency allowlist from the project manifest:
|
|
3009
|
+
// already-declared deps are authorized; anything else in the model's
|
|
3010
|
+
// dependency policy that looks like a package name is an unauthorized new
|
|
3011
|
+
// dependency (fail-closed at the policy shell and the plan pre-check).
|
|
3012
|
+
const declaredDependencies = await collectDeclaredDependencies(sources.repoRoot ?? process.cwd());
|
|
2588
3013
|
const frontendRisk = frontendSources.frontendRisk ??
|
|
2589
3014
|
classifyFrontendRisk({
|
|
2590
3015
|
title: taskConfig.title,
|
|
@@ -2593,138 +3018,63 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2593
3018
|
allowedPaths: taskConfig.allowedPaths,
|
|
2594
3019
|
complexity: taskConfig.complexity,
|
|
2595
3020
|
});
|
|
3021
|
+
const frontendTaskShape = resolveFrontendTaskShape({
|
|
3022
|
+
complexity: taskConfig.complexity,
|
|
3023
|
+
allowedPaths: taskConfig.allowedPaths,
|
|
3024
|
+
requirementMarkdown: sources.requirementMarkdown,
|
|
3025
|
+
constraintMarkdown: sources.constraintMarkdown ?? undefined,
|
|
3026
|
+
splitSignal: { splitRequired: false, runtimeSupportsSplit: true },
|
|
3027
|
+
taskId: sources.taskId,
|
|
3028
|
+
sourceDigest: computeFrontendShapeSourceDigest({
|
|
3029
|
+
requirementMarkdown: sources.requirementMarkdown,
|
|
3030
|
+
constraintMarkdown: sources.constraintMarkdown ?? "",
|
|
3031
|
+
}),
|
|
3032
|
+
shapeTransitionCapsule: resolveFrontendShapeCapsuleGenerationInput(sources),
|
|
3033
|
+
});
|
|
2596
3034
|
const frontendSourceBinding = buildDagSourceBinding(sources, taskConfig.taskKind);
|
|
2597
3035
|
const frontendContractSkeleton = buildFrontendImplementationContractSkeleton({
|
|
2598
3036
|
sourceBinding: frontendSourceBinding,
|
|
2599
3037
|
riskLevel: frontendRisk.selectedRisk,
|
|
2600
3038
|
targetFiles: implementPaths.writeSet,
|
|
2601
3039
|
});
|
|
2602
|
-
const frontendContractSchemaBlock = (() => {
|
|
2603
|
-
const schema = loadFrontendImplementationContractJsonSchema();
|
|
2604
|
-
return [
|
|
2605
|
-
`## Final ${FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID} JSON Schema (authoritative after runtime merge)`,
|
|
2606
|
-
schema,
|
|
2607
|
-
"",
|
|
2608
|
-
"## Runtime contract skeleton (deterministic and protected)",
|
|
2609
|
-
JSON.stringify(frontendContractSkeleton),
|
|
2610
|
-
"",
|
|
2611
|
-
"The initial planner emits an editable RFC 7386 patch against this skeleton. It MUST omit schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime merges and validates the final contract, then writes a hash-bound canonical JSON artifact; downstream review and prewrite consume that artifact path, not planner stdout.",
|
|
2612
|
-
"",
|
|
2613
|
-
"## Forbidden fields (these are NOT in the schema; do not emit)",
|
|
2614
|
-
"- schemaId",
|
|
2615
|
-
"- targetFiles",
|
|
2616
|
-
"- requirementCoverage",
|
|
2617
|
-
"",
|
|
2618
|
-
"## Critical rules",
|
|
2619
|
-
"- verificationTargets is a TOP-LEVEL required array",
|
|
2620
|
-
"- uiStates items use name/applicable/expectedBehavior/implementationTargets/verificationTargetIds/notApplicableReason",
|
|
2621
|
-
"- Use uiStates: [] for frontend logic changes with no user-visible UI state. Do not invent UI states.",
|
|
2622
|
-
"- For applicable=true, provide non-empty expectedBehavior plus non-empty implementationTargets and verificationTargetIds. For applicable=false, provide non-empty notApplicableReason and omit expectedBehavior instead of emitting an empty string.",
|
|
2623
|
-
"- mockApi.productionDefaultOff must always be true (including strategy: not-needed)",
|
|
2624
|
-
"- All implementation files, verification files, symbols, and commands must be discovered from the current target workspace and current task. Never copy paths, symbols, or commands from the loop-agent repository, an example task, or prior run output.",
|
|
2625
|
-
"- Use relative POSIX paths rooted at the target workspace. Do not assume a particular src/test directory layout; preserve the target project's actual app/, packages/, spec/, __tests__, or other layout.",
|
|
2626
|
-
"",
|
|
2627
|
-
"## Bad / Good contract field examples",
|
|
2628
|
-
"",
|
|
2629
|
-
"### verificationTargets - BAD (invented commandLabel, missing file):",
|
|
2630
|
-
'{"id":"vt-1","type":"static","commandLabel":"lint","file":"","requirementIds":["AC-001"],"uiStates":[]} <-- REJECTED: commandLabel not in frozen command set; empty file path',
|
|
2631
|
-
"",
|
|
2632
|
-
'### verificationTargets - GOOD (real frozen label, real file):',
|
|
2633
|
-
'{"id":"vt-1","type":"static","commandLabel":"npm run typecheck","file":"tsconfig.json","requirementIds":["AC-001"],"uiStates":[]} <-- Matches frozen command set; real file path',
|
|
2634
|
-
"",
|
|
2635
|
-
"### requirements - BAD (missing expectedOutcome):",
|
|
2636
|
-
'{"id":"AC-001","expectedOutcome":"","implementationTargets":["src/app.tsx"],"verificationTargetIds":["vt-1"]} <-- REJECTED: empty expectedOutcome',
|
|
2637
|
-
"",
|
|
2638
|
-
"### requirements - GOOD (concrete expectedOutcome):",
|
|
2639
|
-
'{"id":"AC-001","expectedOutcome":"TypeScript compilation exits with code 0 and produces no errors in dist/","implementationTargets":["src/app.tsx"],"verificationTargetIds":["vt-1"]}',
|
|
2640
|
-
"",
|
|
2641
|
-
"### interactions - BAD (empty trigger/expectedBehavior):",
|
|
2642
|
-
'{"name":"save-click","trigger":"","expectedBehavior":"","implementationTargets":["src/button.tsx"],"verificationTargetIds":["vt-3"]} <-- REJECTED: empty trigger and expectedBehavior',
|
|
2643
|
-
"",
|
|
2644
|
-
"### interactions - GOOD:",
|
|
2645
|
-
'{"name":"save-click","trigger":"User clicks the Save button in the editor toolbar","expectedBehavior":"POST /api/save is called with editor content; success toast appears; button enters disabled+spinner state until response","implementationTargets":["src/editor/save-button.tsx"],"verificationTargetIds":["vt-3"]}',
|
|
2646
|
-
"",
|
|
2647
|
-
"### uiStates - BAD (applicable=true but missing expectedBehavior):",
|
|
2648
|
-
'{"name":"loading","applicable":true,"expectedBehavior":"","implementationTargets":[],"verificationTargetIds":[]} <-- REJECTED: applicable UI state requires non-empty expectedBehavior, implementationTargets, and verificationTargetIds',
|
|
2649
|
-
"",
|
|
2650
|
-
"### uiStates - GOOD (applicable=true with complete fields):",
|
|
2651
|
-
'{"name":"loading","applicable":true,"expectedBehavior":"Skeleton placeholder visible while data fetches; aria-busy=true on the list container","implementationTargets":["src/dashboard/list-view.tsx"],"verificationTargetIds":["vt-3"]}',
|
|
2652
|
-
"",
|
|
2653
|
-
"### uiStates - BAD (applicable=false without notApplicableReason):",
|
|
2654
|
-
'{"name":"dark-mode","applicable":false} <-- REJECTED: non-applicable UI state requires notApplicableReason',
|
|
2655
|
-
"",
|
|
2656
|
-
"### uiStates - GOOD (applicable=false with reason):",
|
|
2657
|
-
'{"name":"dark-mode","applicable":false,"notApplicableReason":"Dark mode toggle is out of scope for this task; only light theme is targeted"}',
|
|
2658
|
-
"",
|
|
2659
|
-
"### mockApi.endpoints - BAD (strategy=native but empty endpoints):",
|
|
2660
|
-
'{"strategy":"native","productionDefaultOff":true,"activation":"env flag","endpoints":[]} <-- REJECTED: native strategy requires at least one endpoint with method, path, fixture, and consumer',
|
|
2661
|
-
"",
|
|
2662
|
-
"### mockApi.endpoints - GOOD (strategy=native with complete endpoint):",
|
|
2663
|
-
'{"strategy":"native","productionDefaultOff":true,"activation":"VITE_ENABLE_MOCK=true","endpoints":[{"method":"GET","path":"/api/users","fixture":"mocks/fixtures/users.json","consumer":"src/api/users.ts"}]}',
|
|
2664
|
-
"",
|
|
2665
|
-
"### optional plan fields - GOOD (all optional; omit when absent):",
|
|
2666
|
-
'{"implementationSteps":["confirm contract","sync tests"],"stylingStrategy":"reuse existing design tokens","dependencyPolicy":"no new runtime deps","residualRisks":["browser a11y not-run"],"realIntegrationGap":"FE-TEST owns live HTTP"}',
|
|
2667
|
-
"",
|
|
2668
|
-
"### optional plan fields - BAD (present-but-empty strings are rejected):",
|
|
2669
|
-
'{"stylingStrategy":"","dependencyPolicy":""} <-- REJECTED: optional string fields must be non-empty when present; omit them instead',
|
|
2670
|
-
"",
|
|
2671
|
-
"### uiComponentChoices - GOOD (specified with precise spec hit):",
|
|
2672
|
-
'{"purpose":"primary action button","component":"Button","decision":"specified","specReference":{"path":"openspec/schemas/button.md","section":"Variants","line":12},"rationale":"spec mandates Button for primary actions"}',
|
|
2673
|
-
"",
|
|
2674
|
-
"### uiComponentChoices - GOOD (new with deviation rationale; specReference null):",
|
|
2675
|
-
'{"purpose":"loading skeleton","component":"SkeletonCard","decision":"new","specReference":null,"rationale":"no spec or existing component covers skeleton; deviation pending design-review approval"}',
|
|
2676
|
-
"",
|
|
2677
|
-
"### uiComponentChoices - BAD (specified without specReference, or new with specReference):",
|
|
2678
|
-
'{"purpose":"primary action","component":"MyButton","decision":"specified","specReference":null,"rationale":"..."} <-- REJECTED: specified requires specReference',
|
|
2679
|
-
'{"purpose":"primary action","component":"MyButton","decision":"new","specReference":{"path":"openspec/schemas/button.md","section":"","line":null},"rationale":"..."} <-- REJECTED: new must not carry specReference',
|
|
2680
|
-
].join("\n");
|
|
2681
|
-
})();
|
|
2682
3040
|
const frontendContractFieldSummary = [
|
|
2683
3041
|
"## Contract field summary (authoritative JSON; no plan prose)",
|
|
2684
|
-
"
|
|
3042
|
+
"frontend-plan-pi records typed facts; frontend-design-policy-shell applies the runtime skeleton, validates, and materializes the canonical full contract JSON supplied here. There is no separate plan prose authority. Review these fields:",
|
|
2685
3043
|
"- requirements[]: id, expectedOutcome, implementationTargets, verificationTargetIds, evidenceGap",
|
|
2686
3044
|
"- uiStates[]: name, applicable, expectedBehavior, implementationTargets, verificationTargetIds, notApplicableReason",
|
|
2687
3045
|
"- interactions[]: name, trigger, expectedBehavior, implementationTargets, verificationTargetIds",
|
|
2688
|
-
"- targets:
|
|
3046
|
+
"- targets: routes, publicApiChanges (files are runtime-owned)",
|
|
2689
3047
|
"- mockApi: strategy, productionDefaultOff, activation, endpoints[]",
|
|
2690
|
-
"- verificationTargets[]: id,
|
|
2691
|
-
"- designEvidence: source, paths, conflicts; evidenceGaps[]",
|
|
2692
|
-
"- optional:
|
|
3048
|
+
"- verificationTargets[]: id (stable test-title trace token for behavior targets), commandId (frozen directory key), mode + commandLabel (runtime-resolved), file, requirementIds, uiStates, scope? (display-only)",
|
|
3049
|
+
"- designEvidence: source, paths, conflicts; evidenceGaps[] (optional)",
|
|
3050
|
+
"- optional: stylingStrategy, uiComponentChoices[], dependencyPolicy, residualRisks[], realIntegrationGap",
|
|
2693
3051
|
"- uiComponentChoices[]: purpose, component, decision (specified|reuse-existing|new), specReference { path, section, line } | null, rationale",
|
|
2694
3052
|
"Do not require or read a separate plan prose section; the contract JSON is the only plan surface.",
|
|
2695
3053
|
].join("\n");
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
3054
|
+
// Do not carry every attachment through the whole frontend pipeline. The
|
|
3055
|
+
// contract node is the sole requirements/materials synthesis point; scout
|
|
3056
|
+
// and plan only need the canonical request plus constraints, while design
|
|
3057
|
+
// review consumes the materialized contract and only needs source provenance.
|
|
3058
|
+
// This prevents attachment content from accumulating on later review calls.
|
|
3059
|
+
const sourceContexts = {
|
|
3060
|
+
contract: [buildSourceContextBlock(sources), capabilityContextBlock]
|
|
3061
|
+
.filter(Boolean)
|
|
3062
|
+
.join("\n\n"),
|
|
3063
|
+
scout: [
|
|
3064
|
+
buildSourceContextBlock(sources, { includeReferenceDocuments: false }),
|
|
3065
|
+
capabilityContextBlock,
|
|
3066
|
+
]
|
|
3067
|
+
.filter(Boolean)
|
|
3068
|
+
.join("\n\n"),
|
|
3069
|
+
designReview: buildSourceContextBlock(sources, {
|
|
3070
|
+
includeRequirementExcerpt: false,
|
|
3071
|
+
includeConstraintExcerpt: false,
|
|
3072
|
+
includeReferenceDocuments: false,
|
|
3073
|
+
}),
|
|
3074
|
+
};
|
|
2702
3075
|
const hasMockVerifyCommands = (taskConfig.frontendMock?.verifyCommands.length ?? 0) > 0 ||
|
|
2703
3076
|
mockCapability.verifyCommands.length > 0;
|
|
2704
3077
|
const requirementIds = frontendSourceBinding.requirementIds;
|
|
2705
|
-
const requirementCoverageInstruction = requirementIds.length > 0
|
|
2706
|
-
? [
|
|
2707
|
-
`## Requirement Coverage (per-AC echo with bad/good examples)`,
|
|
2708
|
-
`For each requirement ID below, echo the ID verbatim and confirm: expectedOutcome (user-observable or logic-observable), implementation targets (files), and verification targets (commandLabel + file).`,
|
|
2709
|
-
`Do not skip any ID. Use the bad/good patterns below as reference for each field.`,
|
|
2710
|
-
``,
|
|
2711
|
-
`Bad example (empty expectedOutcome, empty targets -- REJECTED at contract materialization):`,
|
|
2712
|
-
`- AC-001: expectedOutcome="" implementationTargets=[] verificationTargets=[]`,
|
|
2713
|
-
``,
|
|
2714
|
-
`Good example (concrete expectedOutcome, real files, real verification targets):`,
|
|
2715
|
-
`- AC-001: expectedOutcome="TypeScript compilation exits with code 0 and produces no errors in dist/" implementationTargets=["src/app.tsx"] verificationTargets=["vt-typecheck":"npm run typecheck","tsconfig.json"]`,
|
|
2716
|
-
``,
|
|
2717
|
-
...requirementIds.map((id) => `- ${id}: [expectedOutcome] [implementation files] [verification targets]`),
|
|
2718
|
-
``,
|
|
2719
|
-
`Every requirement MUST have a non-empty expectedOutcome. Every interaction MUST have non-empty trigger and expectedBehavior. UI states with applicable=true MUST have non-empty expectedBehavior. Empty strings or omitted fields for these will cause contract rejection.`,
|
|
2720
|
-
].join("\n")
|
|
2721
|
-
: "";
|
|
2722
|
-
const verificationTargetFileInstruction = [
|
|
2723
|
-
`## Verification target file semantics`,
|
|
2724
|
-
`verificationTargets[].file is the code file that the target verifies (the file the writer changes), NOT where the command is defined.`,
|
|
2725
|
-
`Non-static targets (type unit/component/integration/mock) MUST set file to a concrete code file inside the implementation writeSet (task allowedPaths); the prewrite gate rejects any non-static target whose file falls outside the writeSet.`,
|
|
2726
|
-
`Command-level checks that run project-wide (all tests, typecheck, build, governance) MUST use type "static" and must NOT be bound as non-static targets with file=package.json/tsconfig.json/vite.config.ts/scripts/*. Static targets are exempt from the writeSet containment check.`,
|
|
2727
|
-
].join("\n");
|
|
2728
3078
|
const strategy = resolveDagVerifyStrategy(taskConfig);
|
|
2729
3079
|
const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
|
|
2730
3080
|
const behaviorPaths = deriveFrontendBehaviorPaths(taskConfig);
|
|
@@ -2734,15 +3084,15 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2734
3084
|
? [`See 执行约束.md in task source (${sources.taskId})`]
|
|
2735
3085
|
: []),
|
|
2736
3086
|
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
2737
|
-
"Frontend
|
|
2738
|
-
"
|
|
2739
|
-
"
|
|
2740
|
-
"
|
|
3087
|
+
"Frontend design policy (frontend-design-policy-shell) and writer admission (frontend-writer-admission-shell) are the only write authorization; the writer runs only after both materialized the canonical contract and admitted a concrete writeSet.",
|
|
3088
|
+
"Design review verdict is consumed only as admission data input; it never drives branch selection.",
|
|
3089
|
+
"Verification failure is terminal for the run: frontend-verify-shell fails closed, routes to recovery, and never selects a same-run repair branch.",
|
|
3090
|
+
"Frontend review is decided exclusively by the committed typed terminal tools (approve_review / request_review_changes); response-text JSON verdicts and first-line VERDICT markers carry no control-flow weight.",
|
|
2741
3091
|
"Frontend planning must consume the read-only Mock assessment strategy produced after scouting; MOCK_STRATEGY: blocked must not pass the deterministic Mock contract gate.",
|
|
2742
3092
|
"Mock implementations must preserve the real request path as the default, require explicit test/dev activation, and never rely on commenting out the real request.",
|
|
2743
3093
|
"Mock-backed behavior evidence proves only the documented frontend contract, never real API integration.",
|
|
2744
|
-
"frontend-implementation DAGs must complete deterministic static verification before final review. Behavior verification is also required when the task declares a behavior entrypoint or the implementation contract contains a
|
|
2745
|
-
"
|
|
3094
|
+
"frontend-implementation DAGs must complete deterministic static verification before final review. Behavior verification is also required when the task declares a behavior entrypoint or the implementation contract contains a behavior-mode verification target; static-only contracts must map every target to the declared static entrypoint.",
|
|
3095
|
+
"Frontend closeout renders only from committed facts; a weak status (failed / not-run / baseline-debt / mock-backed / pending) can never be rewritten into a stronger one (passed / real-integrated).",
|
|
2746
3096
|
`Frontend risk classification: ${frontendRisk.selectedRisk} — ${frontendRisk.reason}`,
|
|
2747
3097
|
frontendRisk.forceFullGates
|
|
2748
3098
|
? "High-risk or supervised: keep full design gates; do not weaken write boundaries."
|
|
@@ -2828,6 +3178,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2828
3178
|
fallbackCommands: staticFallbackCommands,
|
|
2829
3179
|
commandTexts: staticShellCommands,
|
|
2830
3180
|
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
3181
|
+
preflight: sources.verificationPreflight,
|
|
2831
3182
|
});
|
|
2832
3183
|
const lintVerifyEvidence = lintShellCommands.length > 0
|
|
2833
3184
|
? buildVerifyEvidence({
|
|
@@ -2838,6 +3189,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2838
3189
|
fallbackCommands: [],
|
|
2839
3190
|
commandTexts: lintShellCommands,
|
|
2840
3191
|
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
3192
|
+
preflight: sources.verificationPreflight,
|
|
2841
3193
|
})
|
|
2842
3194
|
: undefined;
|
|
2843
3195
|
const behaviorVerifyEvidence = buildVerifyEvidence({
|
|
@@ -2849,15 +3201,30 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2849
3201
|
commandTexts: behaviorShellCommands,
|
|
2850
3202
|
finalFullRequired: true,
|
|
2851
3203
|
commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
3204
|
+
preflight: sources.verificationPreflight,
|
|
2852
3205
|
});
|
|
2853
3206
|
const mockVerifyTemplate = mockMode === "required" && hasMockVerifyCommands
|
|
2854
3207
|
? buildFrontendMockVerifyNode(frontendSources, implementId, readOnlyPaths, forbiddenPaths)
|
|
2855
3208
|
: undefined;
|
|
2856
3209
|
const mockShellCommands = mockVerifyTemplate?.shell?.commands ?? [];
|
|
2857
3210
|
const mockVerifyEvidence = mockVerifyTemplate?.shell?.verifyEvidence;
|
|
3211
|
+
// Contract v2: the frozen command directory is the only command reference
|
|
3212
|
+
// the plan may use. Modes are assigned here once from the generation-time
|
|
3213
|
+
// lane split; downstream materialization resolves the same directory from
|
|
3214
|
+
// the run spec, so plan and runtime can never disagree on a mode.
|
|
3215
|
+
const frontendVerifyDirectory = buildFrontendVerifyCommandDirectory({
|
|
3216
|
+
staticLabels: staticVerifyEvidence.commandLabels,
|
|
3217
|
+
behaviorLabels: behaviorVerifyEvidence.commandLabels,
|
|
3218
|
+
mockLabels: mockVerifyEvidence?.commandLabels ?? [],
|
|
3219
|
+
staticCommandTexts: staticVerifyEvidence.commandTexts,
|
|
3220
|
+
behaviorCommandTexts: behaviorVerifyEvidence.commandTexts,
|
|
3221
|
+
mockCommandTexts: mockVerifyEvidence?.commandTexts ?? [],
|
|
3222
|
+
});
|
|
2858
3223
|
const fixedVerificationContext = [
|
|
2859
3224
|
"## Fixed frontend verification entrypoints",
|
|
2860
3225
|
"These shell entrypoints are fixed at DAG generation and are the only commands the static and behavior shell nodes execute. A strategy or plan may add tests behind an existing entrypoint inside writeSet, but must not invent or replace commands or assume subtask_prompt executes a command.",
|
|
3226
|
+
"Reference frozen commands ONLY by commandId (record_plan_verification_target entry.commandId). The runtime resolves mode and label; never invent a mode or type.",
|
|
3227
|
+
...frontendVerifyDirectory.map((entry) => ` - ${entry.commandId} [${entry.mode}]: ${JSON.stringify(entry.label)}`),
|
|
2861
3228
|
`- Static command source: ${staticVerifyEvidence.commandSource}`,
|
|
2862
3229
|
...staticVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
|
|
2863
3230
|
`- Behavior command source: ${behaviorVerifyEvidence.commandSource}`,
|
|
@@ -2873,7 +3240,56 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2873
3240
|
frontendMockStrategyMustBeNotNeeded(frontendSources)) {
|
|
2874
3241
|
advisories.push("auto 模式已将 Mock 策略收窄为 not-needed:任务源提到接口/API/Mock 需求,但仓库无确认 Mock 能力或无确定性 Mock 验证命令。若项目规范要求 Mock,请声明 frontendMock.verifyCommands 或 policy:required 后重新生成 DAG。");
|
|
2875
3242
|
}
|
|
2876
|
-
const openspecGate = resolveFrontendOpenspecGateConfig(sources);
|
|
3243
|
+
const openspecGate = await resolveFrontendOpenspecGateConfig(sources);
|
|
3244
|
+
const requiresOpenspecClassification = openspecGate.openspecPolicy === "cited" &&
|
|
3245
|
+
openspecGate.openspecCandidatePaths.length > 0;
|
|
3246
|
+
// Prompt 内联的候选只保留与任务相关的子集:mandatory(任务显式声明/
|
|
3247
|
+
// 引用)始终保留;scan-strict 候选按路径段是否命中任务源关键词过滤。
|
|
3248
|
+
// runtime 的选型/read 门禁仍消费完整 openspecCandidatePaths——这里只
|
|
3249
|
+
// 减小 prompt 体积,不改变门禁语义;未提及的候选 runtime 默认 irrelevant。
|
|
3250
|
+
const promptCandidatePaths = filterRelevantOpenspecCandidates({
|
|
3251
|
+
candidates: openspecGate.openspecCandidateSummaries.map((candidate) => candidate.path),
|
|
3252
|
+
mandatoryPaths: openspecGate.openspecMandatoryPaths,
|
|
3253
|
+
sourceMarkdown: [
|
|
3254
|
+
sources.requirementMarkdown,
|
|
3255
|
+
sources.constraintMarkdown ?? "",
|
|
3256
|
+
].join("\n"),
|
|
3257
|
+
maxCandidates: 24,
|
|
3258
|
+
});
|
|
3259
|
+
const candidateByPath = new Map(openspecGate.openspecCandidateSummaries.map((candidate) => [
|
|
3260
|
+
candidate.path,
|
|
3261
|
+
candidate,
|
|
3262
|
+
]));
|
|
3263
|
+
// Preserve the filter's mandatory/relevance order. Sorting then slicing here
|
|
3264
|
+
// used to be able to drop a mandatory path after it passed the Top-K filter.
|
|
3265
|
+
const promptCandidates = promptCandidatePaths.flatMap((candidatePath) => {
|
|
3266
|
+
const candidate = candidateByPath.get(candidatePath);
|
|
3267
|
+
return candidate
|
|
3268
|
+
? [
|
|
3269
|
+
{
|
|
3270
|
+
path: candidate.path,
|
|
3271
|
+
kind: candidate.kind,
|
|
3272
|
+
score: candidate.score,
|
|
3273
|
+
reasons: candidate.reasons,
|
|
3274
|
+
source: candidate.source,
|
|
3275
|
+
},
|
|
3276
|
+
]
|
|
3277
|
+
: [];
|
|
3278
|
+
});
|
|
3279
|
+
const openspecSelectionContext = JSON.stringify({
|
|
3280
|
+
schemaVersion: 1,
|
|
3281
|
+
schemaId: "frontend-openspec-selection-v1",
|
|
3282
|
+
candidates: promptCandidates,
|
|
3283
|
+
mandatoryPaths: openspecGate.openspecMandatoryPaths,
|
|
3284
|
+
});
|
|
3285
|
+
const scopedOpenspecContext = promptCandidates.length > 0
|
|
3286
|
+
? [
|
|
3287
|
+
"## Task-relevant OpenSpec Top-K (generation frozen)",
|
|
3288
|
+
`Policy: ${openspecGate.openspecPolicy}. This is the bounded prompt index; the deterministic gate retains ${openspecGate.openspecCandidatePaths.length} frozen candidates.`,
|
|
3289
|
+
...promptCandidates.map((candidate) => `- ${openspecGate.openspecMandatoryPaths.includes(candidate.path) ? "mandatory" : "candidate"}: ${candidate.path} (${candidate.kind}; ${candidate.source})`),
|
|
3290
|
+
"Apply or cite only paths relevant to the concrete contract. Report applied rules with path/section/line and surface conflicts or missing specifications; unlisted candidates default to irrelevant unless the deterministic gate requires them.",
|
|
3291
|
+
].join("\n")
|
|
3292
|
+
: "";
|
|
2877
3293
|
// Generation-frozen component/theme specification bucket (ADR 0016). Derived
|
|
2878
3294
|
// from the classified component/theme/rule.components buckets; the prewrite
|
|
2879
3295
|
// gate consumes it to enforce uiComponentChoices presence and specReference
|
|
@@ -2890,7 +3306,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2890
3306
|
advisories.push("openspec 策略 cited:契约声明的 requiredReadPaths 与任务源引用均为空,prewrite gate 不强制读取 openspec;如需增强规范门禁,请在 task.json.frontendOpenspec.requiredReadPaths 声明必读路径或在任务源中显式引用 openspec 文件。");
|
|
2891
3307
|
}
|
|
2892
3308
|
else {
|
|
2893
|
-
advisories.push(`openspec 策略 cited:候选 ${openspecGate.openspecCandidatePaths.length} 个(declared ${openspecGate.openspecCandidateSources.declared.length} / task-source-cited ${openspecGate.openspecCandidateSources.taskSourceCited.length}),plan
|
|
3309
|
+
advisories.push(`openspec 策略 cited:候选 ${openspecGate.openspecCandidatePaths.length} 个(declared ${openspecGate.openspecCandidateSources.declared.length} / task-source-cited ${openspecGate.openspecCandidateSources.taskSourceCited.length}),plan 必须在 typed decision ledger 的 uiComponentChoices.specReference 中声明并通过真实 read 事件佐证。`);
|
|
2894
3310
|
}
|
|
2895
3311
|
}
|
|
2896
3312
|
else {
|
|
@@ -2927,13 +3343,25 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2927
3343
|
writePolicy: "read-only",
|
|
2928
3344
|
allowedPaths: readOnlyPaths,
|
|
2929
3345
|
forbiddenPaths,
|
|
2930
|
-
skills:
|
|
2931
|
-
outputContract: "Markdown contract with
|
|
3346
|
+
skills: FRONTEND_CONTRACT_SKILLS,
|
|
3347
|
+
outputContract: "Typed requirement facts plus a concise Markdown contract. Submit through the incremental typed tools record_requirement / record_constraint / record_evidence_expectation / record_handoff_intent / record_open_question / record_split_proposal / record_openspec_selection, then call finalize_contract exactly once. Requirements use stable REQ/BR/AC identifiers with source spans and a disposition (explicit | repository-resolvable | assumption | blocking); each requirement registers evidence expectations across static/behavior/Mock/real-integration (required | optional | not-applicable), and UI-visible or interactive requirements register a non-blocking frontend-test handoff intent. End finalize_contract with a single contract disposition of ready | ready-with-assumptions | blocked. Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations; do not fix target files, components, or implementation methods as requirements. When OpenSpec candidates exist, classify only the ones you actually use: call record_openspec_selection once per required/relevant path; never enumerate irrelevant candidates (unmentioned defaults to irrelevant) and never emit a fenced selection JSON. No file writes.",
|
|
2932
3348
|
subtask_prompt: [
|
|
2933
|
-
"
|
|
2934
|
-
"
|
|
3349
|
+
"OUTPUT BUDGET DISCIPLINE (hard requirement, extreme-environment safe): the provider output window is small — NEVER attempt to emit the whole contract in one response; a single large JSON dump will be truncated and rejected. Incremental submission through the typed tools is the ONLY supported output mode. Start submitting with the FIRST tool call: after each read, call record_requirement for the requirements you have already confirmed, one or a few per call. Every tool-call round MUST make progress by submitting at least one record_* fact. Do not re-read the same source file that is already materialized in this session; read each file at most once.",
|
|
3350
|
+
"Read task source and produce a concise frontend implementation contract as typed requirement facts plus narrative Markdown.",
|
|
3351
|
+
"Assign each requirement the SAME id as the ledger canonical requirement it covers (sourceBinding.requirementIds, e.g. AC-001) — do NOT invent new REQ/BR prefixed ids for canonical requirements: the compiled contract must match the ledger canonical requirement ids exactly or schema validation rejects it (unknown requirement id). Requirements use the canonical id with a source span (task-source section or repository file:line). Label each requirement's disposition as explicit | repository-resolvable | assumption | blocking; a blocking requirement must name its owner (human-decision or external-state) and evidence refs.",
|
|
3352
|
+
"Source fidelity ledger: when the DAG sourceBinding carries a requirement→fragment mapping (requirementToFragments, e.g. REQ-SRC-* ids from the managed ledger), each record_requirement MUST declare the fragments that requirement is bound to: set sourceFragmentIds to the mapped fragment ids (the authoritative provenance evidence the design policy verifies). sourceRefs (fragment→path display refs) are optional — declare them only when you have the exact path from the materialized source; otherwise omit them rather than inventing paths. Declare exactly what the ledger binds — do not invent ids, do not omit them, and do not re-derive them from prose. A requirement that the ledger binds but the contract omits (or fabricates) fails writer admission.",
|
|
3353
|
+
"Register evidence expectations for each requirement across static, behavior, Mock, and real integration as required | optional | not-applicable; required must follow from user requirements, task risk, or project governance, never from model convenience. For UI-visible or interactive requirements, register a non-blocking frontend-test handoff intent.",
|
|
3354
|
+
"Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations. Do not fix target files, components, styling, or implementation methods as requirements; leave those to Scout and Plan.",
|
|
3355
|
+
"If the task is too large for one bounded writer, record a task split proposal instead of silently widening scope.",
|
|
3356
|
+
"End the contract with a single disposition: ready, ready-with-assumptions (bounded assumptions that do not change product behavior), or blocked.",
|
|
3357
|
+
scopedOpenspecContext,
|
|
3358
|
+
...(requiresOpenspecClassification ? [
|
|
3359
|
+
"Classify OpenSpec candidates incrementally while contracting — only the ones you actually use. Call record_openspec_selection once per path with disposition required (must be read and cited by the plan) or relevant (may inform planning). Never call it for irrelevant candidates and never list them: candidates you do not mention are treated as irrelevant by the runtime. Explicit task declarations / source citations are already required and must-read regardless; you never need to re-declare them.",
|
|
3360
|
+
"Mandatory paths are enforced by the runtime from the frozen task configuration — do not enumerate them, do not downgrade them.",
|
|
3361
|
+
openspecSelectionContext,
|
|
3362
|
+
] : []),
|
|
2935
3363
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
2936
|
-
|
|
3364
|
+
sourceContexts.contract,
|
|
2937
3365
|
].join("\n\n"),
|
|
2938
3366
|
},
|
|
2939
3367
|
{
|
|
@@ -2943,17 +3371,19 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2943
3371
|
executor: "pi",
|
|
2944
3372
|
complexity: mapTaskComplexity(taskConfig.complexity),
|
|
2945
3373
|
writePolicy: "read-only",
|
|
3374
|
+
retryPolicy: FRONTEND_SCOUT_COMPLETENESS_RETRY_POLICY,
|
|
2946
3375
|
allowedPaths: readOnlyPaths,
|
|
2947
3376
|
forbiddenPaths,
|
|
2948
|
-
skills:
|
|
2949
|
-
outputContract: "Markdown scout report
|
|
3377
|
+
skills: FRONTEND_SCOUT_SKILLS,
|
|
3378
|
+
outputContract: "Markdown scout report covering target surface and design evidence (frontend stack, routes, components, styling system, existing design conventions, state/data flow, test entry points, reuse opportunities, risks). Submit through the incremental evidence tools record_target_surface / record_design_evidence. A complete target surface is mandatory before Plan; if it cannot be proven, commit blocked with unresolved paths so this Scout node retries rather than shifting discovery to Plan. No fixed TARGET_SURFACE section title is required — target surface and design evidence are reported as committed typed facts. No file writes.",
|
|
2950
3379
|
subtask_prompt: [
|
|
2951
3380
|
"Inspect frontend code, routing, components, styles, package scripts, and tests.",
|
|
2952
3381
|
"Return code and design observations, existing reuse opportunities, and verification entry points.",
|
|
2953
|
-
"
|
|
3382
|
+
"Report target surface and design evidence as facts (no fixed section title required): completeness, entrypoint, routeOrMount, implementationPaths, testPaths, dataSource, allowedPathConflicts, unresolvedPaths. Use repository-relative POSIX paths. A complete surface must name at least one target candidate and set unresolvedPaths to []; if any target ownership remains unknown, commit completeness=blocked with every unresolved path instead of guessing. Prefer existing files/directories when they exist. For a greenfield target explicitly pinned by the task source, future implementation/test paths are allowed, but every such path must be named by the source and runtime-enriched as sourceDeclared; do not invent paths merely because they fit allowedPaths. allowedPathConflicts must list every discovered path not covered by task allowedPaths, or [] when none exists.",
|
|
2954
3383
|
"Derive all file paths from this target workspace. Do not assume the project uses src/, test/, React, or the loop-agent repository layout.",
|
|
2955
3384
|
"Read-only: do not modify repository files.",
|
|
2956
|
-
|
|
3385
|
+
sourceContexts.scout,
|
|
3386
|
+
scopedOpenspecContext,
|
|
2957
3387
|
].join("\n\n"),
|
|
2958
3388
|
},
|
|
2959
3389
|
{
|
|
@@ -2963,239 +3393,198 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2963
3393
|
executor: "pi",
|
|
2964
3394
|
complexity: "MED",
|
|
2965
3395
|
writePolicy: "read-only",
|
|
2966
|
-
|
|
2967
|
-
|
|
3396
|
+
retryPolicy: FRONTEND_PLAN_LADDER_RETRY_POLICY,
|
|
3397
|
+
allowedPaths: readOnlyPaths,
|
|
3398
|
+
forbiddenPaths,
|
|
3399
|
+
skills: FRONTEND_PLAN_SKILLS,
|
|
2968
3400
|
structuredContractOutput: {
|
|
2969
|
-
schemaId:
|
|
3401
|
+
schemaId: "frontend-implementation-contract-plan-patch-v1",
|
|
2970
3402
|
retryOnInvalid: true,
|
|
2971
3403
|
skeleton: frontendContractSkeleton,
|
|
2972
3404
|
},
|
|
2973
|
-
|
|
2974
|
-
forbiddenPaths,
|
|
2975
|
-
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2976
|
-
outputContract: "JSON-only patch output: one-line lead-in, then exactly ONE fenced json object (```json ... ```) containing only the editable RFC 7386 plan patch for the runtime contract skeleton. Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. Immediately after it, append exactly one ```openspec-citations``` fenced citation block. The runtime applies the patch, validates it, and writes a hash-bound canonical JSON artifact for downstream review. Do NOT emit a full contract, Markdown plan explanation, raw JSON, or any other fenced block. No file writes.",
|
|
3405
|
+
outputContract: "Typed decision patch only: map frozen requirements to implementation/verification targets and select the needed component, state, data/Mock, styling, and dependency decisions. Use only the record_* tools needed to express those decisions, then call finalize_plan exactly once. Contract owns requirement semantics; Scout owns repository discovery; deterministic runtime owns schema, protected fields, path containment, and command validation. No Markdown narrative or file writes.",
|
|
2977
3406
|
subtask_prompt: [
|
|
2978
|
-
"
|
|
2979
|
-
"
|
|
2980
|
-
"
|
|
2981
|
-
"
|
|
2982
|
-
|
|
2983
|
-
"
|
|
2984
|
-
"
|
|
2985
|
-
"
|
|
2986
|
-
requirementCoverageInstruction,
|
|
2987
|
-
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2988
|
-
verificationTargetFileInstruction,
|
|
2989
|
-
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
3407
|
+
"Plan only the delta between the frozen frontend-contract-pi facts and frontend-scout-pi target surface. Do not reinterpret the task, repeat requirements, search the repository, or choose implementation order.",
|
|
3408
|
+
"Record only: requirement-to-file/verification coverage; component/styling choices; applicable UI state and interaction behavior; data/Mock strategy; and a dependency policy or genuine evidence gap. Reuse Scout paths. If scope is missing, record a blocking gap instead of inventing a path.",
|
|
3409
|
+
"Use the typed tool schemas as the field contract. Runtime owns schemaVersion, sourceBinding, riskLevel, targets.files, mockApi.productionDefaultOff, aliases, command allowlisting, path containment, and final validation; do not restate those rules or emit a full JSON contract.",
|
|
3410
|
+
`Cover each frozen requirement ID exactly once: ${requirementIds.join(", ") || "(none)"}. Bind every verification target to a frozen commandId from the directory above plus a Scout-confirmed file. Behavior commands prove observable behavior: one target may cover multiple related requirementIds when one test behavior proves them together; do not mechanically create one target per requirement. A behavior target id is the stable machine trace token and its file must be a test file. Static commands are project-wide checks traced by file and command only.`,
|
|
3411
|
+
...(requiresOpenspecClassification ? ["When a component choice uses an OpenSpec selection, cite that selection; otherwise do not classify unrelated candidates."] : []),
|
|
3412
|
+
"Call finalize_plan exactly once after the necessary typed facts. Return no Markdown narrative.",
|
|
3413
|
+
"TOOL-ONLY PLAN: Do not read Contract/Scout stdout, task sources, or Scout-confirmed target files. Contract and Scout already own evidence discovery; use the injected upstream facts, record a genuine evidence gap when those facts are insufficient, and start committing record_* facts immediately. For decision=new, pass sourceRequirementIds to record_component_choice; runtime derives the exact PRD citation from the frozen ledger.",
|
|
3414
|
+
"Output budget protocol (hard, max output <=16K per turn): never enumerate-reason the whole requirement list before your first record_* call — that reasoning burns the entire output budget and the attempt dies with zero committed facts. Process requirements in order: think about ONE requirement briefly, immediately emit its record calls (up to 5 per message), then move to the next. If your budget runs low, stop recording and call finalize_plan with what is committed — the retry ladder continues the remainder in a fresh session.",
|
|
2990
3415
|
fixedVerificationContext,
|
|
2991
|
-
|
|
3416
|
+
scopedOpenspecContext,
|
|
2992
3417
|
mockContextBlock,
|
|
2993
|
-
|
|
3418
|
+
frontendContractFieldSummary,
|
|
2994
3419
|
frontendComponentConformanceInstruction,
|
|
2995
|
-
openspecCitationInstruction,
|
|
2996
3420
|
].join("\n\n"),
|
|
2997
3421
|
},
|
|
2998
3422
|
{
|
|
2999
|
-
id: "frontend-design-
|
|
3423
|
+
id: "frontend-design-policy-shell",
|
|
3000
3424
|
depends_on: ["frontend-plan-pi"],
|
|
3001
|
-
role: "
|
|
3002
|
-
executor: "
|
|
3003
|
-
complexity: "
|
|
3425
|
+
role: "verifier",
|
|
3426
|
+
executor: "shell",
|
|
3427
|
+
complexity: "LOW",
|
|
3004
3428
|
writePolicy: "read-only",
|
|
3005
3429
|
allowedPaths: readOnlyPaths,
|
|
3006
3430
|
forbiddenPaths,
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3431
|
+
outputContract: "Deterministic design policy: materialize the canonical frontend implementation contract from the plan patch, enforce requirement-id retention, Mock strategy/frozen-command binding, writeSet containment, source freshness, and openspec invariants, then evaluate the design policy (write policy result).",
|
|
3432
|
+
subtask_prompt: "Materialize the canonical contract and fail closed unless the deterministic design policy approves. The design verdict is not available at this stage; the writer admission shell enforces it.",
|
|
3433
|
+
shell: {
|
|
3434
|
+
commands: [],
|
|
3435
|
+
frontendDesignPolicy: {
|
|
3436
|
+
schemaVersion: 1,
|
|
3437
|
+
planFromNodeId: "frontend-plan-pi",
|
|
3438
|
+
requiredRequirementIds: requirementIds,
|
|
3439
|
+
allowedMockStrategies: taskConfig.frontendMock?.policy === "disabled" ||
|
|
3440
|
+
frontendMockStrategyMustBeNotNeeded(frontendSources)
|
|
3441
|
+
? ["not-needed"]
|
|
3442
|
+
: taskConfig.frontendMock?.policy === "required"
|
|
3443
|
+
? [
|
|
3444
|
+
"native",
|
|
3445
|
+
"browser-intercept",
|
|
3446
|
+
"request-adapter",
|
|
3447
|
+
]
|
|
3448
|
+
: [
|
|
3449
|
+
"native",
|
|
3450
|
+
"browser-intercept",
|
|
3451
|
+
"request-adapter",
|
|
3452
|
+
"not-needed",
|
|
3453
|
+
],
|
|
3454
|
+
mockCommandLabels: mockVerifyEvidence?.commandLabels ?? [],
|
|
3455
|
+
artifactName: "frontend-implementation-contract.json",
|
|
3456
|
+
outputDir: "contracts",
|
|
3457
|
+
requireSourceFreshness: true,
|
|
3458
|
+
implementationWriteSet: implementPaths.writeSet,
|
|
3459
|
+
openspecPolicy: openspecGate.openspecPolicy,
|
|
3460
|
+
openspecSpecRoots: taskConfig.frontendOpenspec?.specRoots ?? [
|
|
3461
|
+
...DEFAULT_FRONTEND_SPEC_ROOTS,
|
|
3462
|
+
],
|
|
3463
|
+
...(requiresOpenspecClassification
|
|
3464
|
+
? { openspecSelectionNodeId: "frontend-contract-pi" }
|
|
3465
|
+
: {}),
|
|
3466
|
+
openspecMandatoryPaths: openspecGate.openspecMandatoryPaths,
|
|
3467
|
+
openspecCandidateSources: {
|
|
3468
|
+
declared: openspecGate.openspecCandidateSources.declared,
|
|
3469
|
+
taskSourceCited: openspecGate.openspecCandidateSources.taskSourceCited,
|
|
3470
|
+
scanStrict: openspecGate.openspecCandidateSources.scanStrict,
|
|
3471
|
+
},
|
|
3472
|
+
openspecCandidatePaths: openspecGate.openspecCandidatePaths,
|
|
3473
|
+
...(openspecGate.openspecCandidateSnapshots
|
|
3474
|
+
? {
|
|
3475
|
+
openspecCandidateSnapshots: openspecGate.openspecCandidateSnapshots,
|
|
3476
|
+
}
|
|
3477
|
+
: {}),
|
|
3478
|
+
componentSpecCandidatePaths,
|
|
3479
|
+
allowedDependencies: declaredDependencies,
|
|
3480
|
+
},
|
|
3481
|
+
cwd: ".",
|
|
3482
|
+
timeoutMs: 60000,
|
|
3036
3483
|
},
|
|
3037
|
-
allowedPaths: readOnlyPaths,
|
|
3038
|
-
forbiddenPaths,
|
|
3039
|
-
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
3040
|
-
outputContract: "When the initial design review requests revision, return a one-line lead-in followed by exactly ONE fenced json object (```json ... ```) containing an RFC 7386 merge-patch delta against the original frontend-implementation-contract-v1 (only the fields you change; null deletes a key; arrays and scalars replace; plain objects merge recursively). Immediately after it, append exactly one ```openspec-citations``` fenced citation block. Do NOT emit a full contract, Markdown explanation, or prose — the output is JSON-only; this node compiles the patch onto the original canonical artifact and writes a hash-bound revised contract. Apart from the patch JSON fenced block and the openspec-citations block, do not emit any other fenced block or raw JSON. No file writes.",
|
|
3041
|
-
subtask_prompt: [
|
|
3042
|
-
"Consume frontend-plan-pi (original contract JSON) and frontend-design-review-pi (first design review findings).",
|
|
3043
|
-
"This node runs only when frontend-design-review-pi emitted VERDICT: request-revision. Produce an RFC 7386 merge-patch delta against the original contract JSON that addresses every Required Plan Correction from the design findings.",
|
|
3044
|
-
"The patch delta may update editable contract fields such as requirements, implementationSteps, targets.routes/publicApiChanges, uiStates, interactions, mockApi.strategy/activation/endpoints, dependencyPolicy, stylingStrategy, uiComponentChoices, verificationTargets, evidenceGaps, residualRisks, and realIntegrationGap. It must not modify protected schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff. Only include fields you change; omit unchanged fields (this node applies the patch on the original canonical artifact). null deletes a key; arrays and scalars replace; plain objects merge recursively.",
|
|
3045
|
-
requirementCoverageInstruction,
|
|
3046
|
-
"Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
|
|
3047
|
-
"Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
|
|
3048
|
-
"Output in this exact order: (1) exactly one fenced json object containing the merge-patch delta — this node compiles it onto the original canonical artifact; (2) exactly one openspec-citations citation fenced block appended immediately after it. Do NOT emit a full contract, Markdown explanation, or prose — the output is JSON-only. Do not emit any raw JSON or JSON objects in prose. Apart from the patch JSON fenced block and the openspec-citations block, do not emit any other fenced block. Do not include secrets or unsafe paths.",
|
|
3049
|
-
"Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the effective (merged) contract; do not reduce behavior semantics to IDs and paths.",
|
|
3050
|
-
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
3051
|
-
verificationTargetFileInstruction,
|
|
3052
|
-
fixedVerificationContext,
|
|
3053
|
-
sourceContext,
|
|
3054
|
-
frontendContractSchemaBlock,
|
|
3055
|
-
mockContextBlock,
|
|
3056
|
-
frontendComponentConformanceInstruction,
|
|
3057
|
-
openspecCitationInstruction,
|
|
3058
|
-
].join("\n\n"),
|
|
3059
3484
|
},
|
|
3060
3485
|
{
|
|
3061
|
-
id: "frontend-
|
|
3062
|
-
depends_on: [
|
|
3063
|
-
"frontend-plan-revision-pi",
|
|
3064
|
-
"frontend-plan-pi",
|
|
3065
|
-
"frontend-design-review-pi",
|
|
3066
|
-
],
|
|
3067
|
-
dependsPolicy: "all-or-condition-skip",
|
|
3068
|
-
runIf: "$.nodes['frontend-design-review-pi'].firstVerdictLine == 'VERDICT: request-revision'",
|
|
3486
|
+
id: "frontend-design-review-pi",
|
|
3487
|
+
depends_on: ["frontend-design-policy-shell"],
|
|
3069
3488
|
role: "reviewer",
|
|
3070
3489
|
executor: "pi",
|
|
3071
3490
|
complexity: "MED",
|
|
3072
3491
|
writePolicy: "read-only",
|
|
3492
|
+
retryPolicy: DEFAULT_READ_ONLY_PI_RETRY_POLICY,
|
|
3073
3493
|
allowedPaths: readOnlyPaths,
|
|
3074
3494
|
forbiddenPaths,
|
|
3075
3495
|
skills: FRONTEND_DESIGN_REVIEW_SKILLS,
|
|
3076
|
-
|
|
3077
|
-
outputContract: "For the effective frontend plan, return plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings and Checked Items. No file writes.",
|
|
3496
|
+
outputContract: "Authoritative typed design terminal via approve_design / request_design_changes tools. No JSON verdict; the committed typed design fact is the only authority. No file writes.",
|
|
3078
3497
|
subtask_prompt: [
|
|
3079
|
-
"Audit the
|
|
3080
|
-
"
|
|
3081
|
-
"
|
|
3082
|
-
"
|
|
3083
|
-
"
|
|
3084
|
-
"
|
|
3085
|
-
"
|
|
3498
|
+
"Audit the frontend plan before implementation. frontend-plan-pi is emitted to you as canonical full-contract JSON after the runtime applied and validated the planner's editable patch against its protected skeleton; there is no separate plan prose.",
|
|
3499
|
+
"Your authoritative terminal verdict is exactly one committed typed tool call: approve_design or request_design_changes. Call exactly one of them; after calling one, do not call the other.",
|
|
3500
|
+
"request_design_changes must carry a typed issueCategory, at least one evidenceRef, and non-empty findings.",
|
|
3501
|
+
"Your verdict is consumed as deterministic data input by frontend-writer-admission-shell. approve_design permits admission; request_design_changes blocks writer admission until a recovery plan incorporates every Critical/Important finding.",
|
|
3502
|
+
"Request design changes when the Mock strategy is MOCK_STRATEGY: blocked, missing, unsupported by repository evidence, inconsistent with the API contract, outside authorized paths/dependencies, unable to prove production-default-off behavior with the fixed production/default-real-path static check, or missing deterministic behavior verification for a declared behavior target or selected Mock strategy. Mock strategies require Mock-backed evidence. A static-only contract is allowed only when every verification target is static and maps to a declared static entrypoint. not-needed otherwise requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case the plan must preserve the real request path and record the Real Integration Gap.",
|
|
3503
|
+
"Also request design changes for missing applicable UI states, unsupported dependency additions, design-system drift without reason, weak interaction coverage, broad scope, inline fake data, schema drift, or missing deterministic verification commands.",
|
|
3504
|
+
"Component selection conformance is a hard blocking condition: request_design_changes when the frontend spec (component/theme/rule.components bucket) already defines a component for a purpose but the plan selects another or self-invents one without a declared deviation; when uiComponentChoices is missing/empty for UI-visible work while the frozen component/theme bucket is non-empty; when a decision=specified specReference.path is missing a ledger OpenSpec reference or successful read event; or when a decision=new component lacks a traceable task-source/PRD specReference. A PRD reference for decision=new is not an OpenSpec citation and must not be rejected merely for lacking an OpenSpec read event.",
|
|
3505
|
+
"For uiComponentChoices, purpose is the stable coverage key and must match interaction.name or uiState.name. Responsibility is expressed by the matched expectedBehavior plus rationale; you must not reject it merely for matching an interaction or component identifier.",
|
|
3506
|
+
"You must NOT make authoritative assertions about the execution result of frozen verification commands (typecheck/test/build/lint/etc.). Predicting that a command will necessarily pass or fail, or declaring an acceptance criterion unreachable on that basis, is out of your authority: command results are deterministically established by frontend-verify-shell. Any concern about verification feasibility must be recorded only as a non-blocking verification concern in findings (severity must not be Critical, and it must never be the sole fatal basis for request_design_changes). Only semantic design defects (component selection, state flow, interaction contract, or conflicts with the specification) may be Critical. A pure command-will-fail prediction must not be classified as contract-requirement-gap.",
|
|
3086
3507
|
"Read-only: do not modify repository files.",
|
|
3508
|
+
"LARGE-FILE AUDIT (avoid full reads): style/theme audit files can be large (e.g. styles.css is often hundreds of KB). Prefer grep to locate the exact rules/variables you must verify (e.g. grep the oc- class, is-* modifier, or --oc- theme variables with their line numbers), then read only the narrow line range when surrounding context is needed. Do not read a large style/test file in full — a single full read can exhaust the read budget and fail the attempt.",
|
|
3509
|
+
"Canonical contract reading: frontend-design-policy-shell prints absolute paths for Contract, Contract index, and the non-blocking Capacity diagnostic. Read the capacity diagnostic first. When it recommends full-contract, read the exact Contract path. When it recommends indexed-sections, read the Contract index and its hash-bound section files instead of opening the full contract. Never resolve a bare contracts/... path against the repository root or hunt for substitutes. Implementation target files inside the writeSet are created later by the implement node: do not read them and do not treat their absence as a design defect.",
|
|
3087
3510
|
fixedVerificationContext,
|
|
3088
|
-
|
|
3511
|
+
sourceContexts.designReview,
|
|
3512
|
+
scopedOpenspecContext,
|
|
3089
3513
|
frontendContractFieldSummary,
|
|
3090
3514
|
mockContextBlock,
|
|
3091
3515
|
].join("\n\n"),
|
|
3092
3516
|
},
|
|
3093
3517
|
{
|
|
3094
|
-
id: "frontend-
|
|
3095
|
-
depends_on: [
|
|
3096
|
-
"frontend-final-design-review-pi",
|
|
3097
|
-
"frontend-design-review-pi",
|
|
3098
|
-
"frontend-plan-revision-pi",
|
|
3099
|
-
"frontend-plan-pi",
|
|
3100
|
-
],
|
|
3101
|
-
dependsPolicy: "all-or-condition-skip",
|
|
3518
|
+
id: "frontend-writer-admission-shell",
|
|
3519
|
+
depends_on: ["frontend-design-policy-shell", "frontend-design-review-pi"],
|
|
3102
3520
|
role: "verifier",
|
|
3103
3521
|
executor: "shell",
|
|
3104
3522
|
complexity: "LOW",
|
|
3105
3523
|
writePolicy: "read-only",
|
|
3106
3524
|
allowedPaths: readOnlyPaths,
|
|
3107
3525
|
forbiddenPaths,
|
|
3108
|
-
outputContract: "Deterministic
|
|
3109
|
-
subtask_prompt: "Fail closed unless the
|
|
3526
|
+
outputContract: "Deterministic writer admission: require design review verdict pass, freeze the pre-writer worktree/lint baselines, derive the concrete writeSet + admission digest, and write contracts/frontend-writer-admission-result.json (schemaId frontend-writer-admission-shell-v1).",
|
|
3527
|
+
subtask_prompt: "Fail closed unless the design review approved and the deterministic admission derived a concrete, non-empty writeSet. The admission result is the only write authorization.",
|
|
3110
3528
|
shell: {
|
|
3111
3529
|
commands: [],
|
|
3112
|
-
|
|
3530
|
+
frontendWriterAdmission: {
|
|
3113
3531
|
schemaVersion: 1,
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3532
|
+
designReviewFromNodeId: "frontend-design-review-pi",
|
|
3533
|
+
frozenCommandLabels: [
|
|
3534
|
+
...staticVerifyEvidence.commandLabels,
|
|
3535
|
+
...behaviorVerifyEvidence.commandLabels,
|
|
3536
|
+
...(mockVerifyEvidence?.commandLabels ?? []),
|
|
3537
|
+
],
|
|
3120
3538
|
allowedMockStrategies: taskConfig.frontendMock?.policy === "disabled" ||
|
|
3121
3539
|
frontendMockStrategyMustBeNotNeeded(frontendSources)
|
|
3122
3540
|
? ["not-needed"]
|
|
3123
3541
|
: taskConfig.frontendMock?.policy === "required"
|
|
3124
|
-
? [
|
|
3542
|
+
? [
|
|
3543
|
+
"native",
|
|
3544
|
+
"browser-intercept",
|
|
3545
|
+
"request-adapter",
|
|
3546
|
+
]
|
|
3125
3547
|
: [
|
|
3126
3548
|
"native",
|
|
3127
3549
|
"browser-intercept",
|
|
3128
3550
|
"request-adapter",
|
|
3129
3551
|
"not-needed",
|
|
3130
3552
|
],
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
openspecPolicy: openspecGate.openspecPolicy,
|
|
3138
|
-
openspecCandidateSources: {
|
|
3139
|
-
declared: openspecGate.openspecCandidateSources.declared,
|
|
3140
|
-
taskSourceCited: openspecGate.openspecCandidateSources.taskSourceCited,
|
|
3141
|
-
scanStrict: openspecGate.openspecCandidateSources.scanStrict,
|
|
3142
|
-
},
|
|
3143
|
-
openspecCandidatePaths: openspecGate.openspecCandidatePaths,
|
|
3144
|
-
componentSpecCandidatePaths,
|
|
3553
|
+
...(lintShellCommands.length > 0 && lintVerifyEvidence
|
|
3554
|
+
? {
|
|
3555
|
+
lintCommands: lintShellCommands,
|
|
3556
|
+
lintEvidence: lintVerifyEvidence,
|
|
3557
|
+
}
|
|
3558
|
+
: {}),
|
|
3145
3559
|
},
|
|
3146
3560
|
cwd: ".",
|
|
3147
3561
|
timeoutMs: 60000,
|
|
3148
3562
|
},
|
|
3149
3563
|
},
|
|
3150
|
-
...(lintShellCommands.length > 0 && lintVerifyEvidence
|
|
3151
|
-
? [
|
|
3152
|
-
{
|
|
3153
|
-
id: "frontend-lint-baseline-shell",
|
|
3154
|
-
depends_on: ["frontend-prewrite-gate-shell"],
|
|
3155
|
-
role: "verifier",
|
|
3156
|
-
executor: "shell",
|
|
3157
|
-
complexity: "LOW",
|
|
3158
|
-
writePolicy: "read-only",
|
|
3159
|
-
allowedPaths: readOnlyPaths,
|
|
3160
|
-
forbiddenPaths,
|
|
3161
|
-
outputContract: "Capture writer-preceding lint output as frontend-lint-baseline-v1 without treating existing lint diagnostics as writer failure.",
|
|
3162
|
-
subtask_prompt: "Run the frozen lint commands read-only. Preserve raw output and mark the baseline unavailable on timeout, execution failure, unparseable output, or worktree mutation.",
|
|
3163
|
-
shell: {
|
|
3164
|
-
commands: lintShellCommands,
|
|
3165
|
-
frontendLintBaseline: {
|
|
3166
|
-
schemaVersion: 1,
|
|
3167
|
-
lintCommands: lintShellCommands,
|
|
3168
|
-
lintEvidence: lintVerifyEvidence,
|
|
3169
|
-
},
|
|
3170
|
-
cwd: ".",
|
|
3171
|
-
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
3172
|
-
},
|
|
3173
|
-
},
|
|
3174
|
-
]
|
|
3175
|
-
: []),
|
|
3176
3564
|
{
|
|
3177
3565
|
id: implementId,
|
|
3178
|
-
depends_on: [
|
|
3179
|
-
"frontend-prewrite-gate-shell",
|
|
3180
|
-
...(lintShellCommands.length > 0
|
|
3181
|
-
? ["frontend-lint-baseline-shell"]
|
|
3182
|
-
: []),
|
|
3183
|
-
],
|
|
3566
|
+
depends_on: ["frontend-writer-admission-shell"],
|
|
3184
3567
|
...buildFrontendWriterNodeDefaults({
|
|
3185
3568
|
complexity: resolveWriterComplexity(taskConfig),
|
|
3186
3569
|
writeSet: implementPaths.writeSet,
|
|
3187
3570
|
allowedPaths: implementPaths.allowedPaths,
|
|
3188
3571
|
forbiddenPaths,
|
|
3572
|
+
writerOutcomePolicyType: "frontend-facts-v1",
|
|
3189
3573
|
}),
|
|
3190
|
-
outputContract: "
|
|
3574
|
+
outputContract: "The implementation status is derived by the executor from mechanical facts (write-tool events, run delta, write guard, requirement coverage, focused-check), not from any IMPLEMENTATION_OUTCOME first line. Deliver a Markdown summary with Contract Ref (path/schema/hash), Changed Files, Requirements Implemented, UI States, Tests Changed, Verification Attempts, Deviations, and Residual Risks. Follow fixed stages: contract confirm → tests → component/state → API/Mock → focused checks → diff cleanup.",
|
|
3191
3575
|
subtask_prompt: [
|
|
3192
|
-
"Implement against the validated run-owned Frontend Implementation Contract
|
|
3576
|
+
"Implement against the validated run-owned Frontend Implementation Contract materialized by frontend-design-policy-shell (path/schema/hash) and authorized by frontend-writer-admission-shell. Do not rebuild the contract from Markdown alone.",
|
|
3577
|
+
"WRITER TOOL PROTOCOL (hard): the response text is not delivery. Never paste source code, test code, or full file contents into chat. After reading the canonical contract, make the first implementation change with the structured write/edit tool (one file per call); continue writing through those tools until the writeSet is complete. If a write/edit tool is unavailable, stop and report the blocked capability instead of drafting code in the response.",
|
|
3193
3578
|
"The canonical contract already contains the approved requirement, target-file, UI-state, verification, design, and Mock/API decisions. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, or design-review prose, and do not repeat broad repository research. Inspect only contract target files and directly related local code needed to implement them.",
|
|
3194
3579
|
"Execute in fixed stages and report each in the delivery summary: (1) Contract confirm, (2) Tests sync, (3) Component/UI state implementation, (4) API/Mock wiring per contract.mockApi, (5) Focused checks behind frozen entrypoints only, (6) Diff cleanup.",
|
|
3195
3580
|
"Map every requirement id, expectedOutcome, interaction trigger/expectedBehavior, and applicable UI state from the contract to concrete files. Do not invent shell verification commands; only frozen static/behavior entrypoints will run.",
|
|
3196
|
-
"
|
|
3581
|
+
"For every behavior verification target, treat target.id as a stable trace token and include that exact token in a real describe/it/test literal title (for example, it('[VT-DASHBOARD-SHELL] renders the dashboard', ...)). One test title may carry multiple target ids when it proves multiple grouped behaviors; comments and ordinary strings do not count as trace evidence.",
|
|
3582
|
+
"Before reporting Tests Changed as done, self-audit with the executor's rule: collect ONLY the string literals passed directly to describe(/it(/test( calls in each test file and confirm every behavior target id for that file appears inside one of those literals. A token in a comment, a variable, or a non-title string does not satisfy the trace check; if any id is missing from the literal titles, edit the title strings before finishing.",
|
|
3583
|
+
"Begin implementation after the contract and its target files are confirmed. Do not spend the turn collecting optional context. If the canonical contract lacks behavior needed to edit safely, stop and state the blocking reason in the summary instead of reopening broad discovery.",
|
|
3584
|
+
"Your implementation status is derived by the executor from mechanical facts (persisted write-tool events, run delta, write guard, requirement coverage, focused-check failures), never from any IMPLEMENTATION_OUTCOME first line. Do not emit an IMPLEMENTATION_OUTCOME first line.",
|
|
3585
|
+
"The node runs a bounded micro-loop: after each write attempt the executor re-runs frozen focused checks and records a per-round diff checkpoint; the write guard stays active every round. Only repair local issues attributable to the current diff (syntax/type/import/format/unit-assert/obvious omission). Never change requirements, design, writeSet, or verification strictness inside the loop.",
|
|
3197
3586
|
"Implement only the approved Mock strategy carried by the validated contract. Preserve the real request path as the default, require explicit test/dev activation, and never comment out or replace the real request with inline data.",
|
|
3198
|
-
"frontend-
|
|
3587
|
+
"frontend-design-policy-shell materialized and validated the canonical contract; frontend-writer-admission-shell authorized the writeSet. Stay within writeSet and preserve unrelated files.",
|
|
3199
3588
|
"For native, browser-intercept, or request-adapter, implement contract-aligned fixtures/states and a dev/test-only activation boundary in this same writer. For not-needed, do not add Mock files or a framework and state the positive reason.",
|
|
3200
3589
|
"Do not write root artifacts/** unless explicitly included in writeSet. Do not claim Browser/visual verification.",
|
|
3201
3590
|
"Edit existing files with the structured edit/write tools. NEVER rewrite Markdown (or any file with quoting/backticks/indentation-sensitive content) via bash sed/awk/echo redirection: escaping mistakes silently corrupt the file and self-repair loops burn the run.",
|
|
@@ -3210,7 +3599,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3210
3599
|
.join("\n\n"),
|
|
3211
3600
|
},
|
|
3212
3601
|
{
|
|
3213
|
-
id: "frontend-verify-
|
|
3602
|
+
id: "frontend-verify-shell",
|
|
3214
3603
|
depends_on: [implementId],
|
|
3215
3604
|
role: "verifier",
|
|
3216
3605
|
executor: "shell",
|
|
@@ -3218,8 +3607,8 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3218
3607
|
writePolicy: "read-only",
|
|
3219
3608
|
allowedPaths: readOnlyPaths,
|
|
3220
3609
|
forbiddenPaths,
|
|
3221
|
-
outputContract: "Run frozen Mock/static/behavior commands
|
|
3222
|
-
subtask_prompt: "Execute the frontend verification bundle. Preserve per-command evidence;
|
|
3610
|
+
outputContract: "Run frozen Mock/static/behavior commands and materialize the verification trace. Any failure is terminal: no same-run repair branch, failure ownership facts are materialized for recovery.",
|
|
3611
|
+
subtask_prompt: "Execute the frontend verification bundle. Preserve per-command evidence; a failure fails this node (terminal) and routes to recovery.",
|
|
3223
3612
|
shell: {
|
|
3224
3613
|
commands: [],
|
|
3225
3614
|
frontendVerificationBundle: {
|
|
@@ -3233,7 +3622,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3233
3622
|
staticEvidence: staticVerifyEvidence,
|
|
3234
3623
|
behaviorEvidence: behaviorVerifyEvidence,
|
|
3235
3624
|
lintBaselineNodeId: lintShellCommands.length > 0
|
|
3236
|
-
? "frontend-
|
|
3625
|
+
? "frontend-writer-admission-shell"
|
|
3237
3626
|
: undefined,
|
|
3238
3627
|
writerNodeIds: lintShellCommands.length > 0 ? [implementId] : [],
|
|
3239
3628
|
mode: "initial",
|
|
@@ -3242,85 +3631,17 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3242
3631
|
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
3243
3632
|
},
|
|
3244
3633
|
},
|
|
3245
|
-
{
|
|
3246
|
-
id: "frontend-repair-pi",
|
|
3247
|
-
depends_on: ["frontend-verify-assess-shell", implementId],
|
|
3248
|
-
runIf: "$.nodes['frontend-verify-assess-shell'].json.eligible == true",
|
|
3249
|
-
...buildFrontendWriterNodeDefaults({
|
|
3250
|
-
complexity: resolveWriterComplexity(taskConfig),
|
|
3251
|
-
writeSet: implementPaths.writeSet,
|
|
3252
|
-
allowedPaths: implementPaths.allowedPaths,
|
|
3253
|
-
forbiddenPaths,
|
|
3254
|
-
}),
|
|
3255
|
-
outputContract: "First non-empty line must be exactly one of: IMPLEMENTATION_OUTCOME: changed; IMPLEMENTATION_OUTCOME: already-satisfied; IMPLEMENTATION_OUTCOME: blocked. Then a repair summary for an eligible repairable assessment. Must not expand writeSet, re-interpret requirements, skip tests, or enable Mock by default.",
|
|
3256
|
-
subtask_prompt: [
|
|
3257
|
-
"Read contracts/frontend-repair-assessment.json and the validated frontend implementation contract.",
|
|
3258
|
-
"This node runs only for eligible=true. Apply the smallest fix for the classified repairable failure inside the original implement writeSet only.",
|
|
3259
|
-
"The repair assessment and canonical implementation contract are complete inputs for this phase. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, or design-review prose, and do not repeat repository-wide discovery.",
|
|
3260
|
-
"Do not change lint/type/test config, do not add .skip/.only, do not comment out real requests, do not default-enable Mock, do not add dependencies.",
|
|
3261
|
-
"Do not re-plan requirements or expand allowed paths. Browser/visual remain not-run.",
|
|
3262
|
-
...(implementPaths.docIndexCompanions.length > 0
|
|
3263
|
-
? [
|
|
3264
|
-
`Doc index sync is MANDATORY: ${implementPaths.docIndexCompanions.join(", ")} are catalog index files for this writeSet. When your repair adds, renames, or removes any indexed file, update ${implementPaths.docIndexCompanions.join(" and ")} in the same run; verification runs check-doc-index and fails the run on a missing index entry.`,
|
|
3265
|
-
]
|
|
3266
|
-
: []),
|
|
3267
|
-
writerDeliveryContract(taskConfig),
|
|
3268
|
-
]
|
|
3269
|
-
.filter((value) => Boolean(value))
|
|
3270
|
-
.join("\n\n"),
|
|
3271
|
-
},
|
|
3272
|
-
{
|
|
3273
|
-
id: "frontend-reverify-shell",
|
|
3274
|
-
depends_on: ["frontend-repair-pi"],
|
|
3275
|
-
role: "verifier",
|
|
3276
|
-
executor: "shell",
|
|
3277
|
-
complexity: "LOW",
|
|
3278
|
-
writePolicy: "read-only",
|
|
3279
|
-
allowedPaths: readOnlyPaths,
|
|
3280
|
-
forbiddenPaths,
|
|
3281
|
-
outputContract: "Post-repair Mock/static/behavior re-verification plus refreshed canonical trace; any failure blocks review.",
|
|
3282
|
-
subtask_prompt: "Re-run the frozen frontend verification bundle after bounded repair and fail on any command or trace failure.",
|
|
3283
|
-
shell: {
|
|
3284
|
-
commands: [],
|
|
3285
|
-
frontendVerificationBundle: {
|
|
3286
|
-
schemaVersion: 1,
|
|
3287
|
-
mockCommands: mockShellCommands,
|
|
3288
|
-
lintCommands: lintShellCommands,
|
|
3289
|
-
staticCommands: staticShellCommands,
|
|
3290
|
-
behaviorCommands: behaviorShellCommands,
|
|
3291
|
-
mockEvidence: mockVerifyEvidence,
|
|
3292
|
-
lintEvidence: lintVerifyEvidence,
|
|
3293
|
-
staticEvidence: staticVerifyEvidence,
|
|
3294
|
-
behaviorEvidence: behaviorVerifyEvidence,
|
|
3295
|
-
lintBaselineNodeId: lintShellCommands.length > 0
|
|
3296
|
-
? "frontend-lint-baseline-shell"
|
|
3297
|
-
: undefined,
|
|
3298
|
-
writerNodeIds: lintShellCommands.length > 0
|
|
3299
|
-
? [implementId, "frontend-repair-pi"]
|
|
3300
|
-
: [],
|
|
3301
|
-
mode: "repair",
|
|
3302
|
-
},
|
|
3303
|
-
cwd: ".",
|
|
3304
|
-
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
3305
|
-
},
|
|
3306
|
-
},
|
|
3307
3634
|
{
|
|
3308
3635
|
id: "frontend-review-context-shell",
|
|
3309
|
-
depends_on: [
|
|
3310
|
-
"frontend-reverify-shell",
|
|
3311
|
-
"frontend-repair-pi",
|
|
3312
|
-
"frontend-verify-assess-shell",
|
|
3313
|
-
implementId,
|
|
3314
|
-
],
|
|
3315
|
-
dependsPolicy: "all-or-condition-skip",
|
|
3636
|
+
depends_on: ["frontend-verify-shell", implementId],
|
|
3316
3637
|
role: "verifier",
|
|
3317
3638
|
executor: "shell",
|
|
3318
3639
|
complexity: "LOW",
|
|
3319
3640
|
writePolicy: "read-only",
|
|
3320
3641
|
allowedPaths: readOnlyPaths,
|
|
3321
3642
|
forbiddenPaths,
|
|
3322
|
-
outputContract: "Canonical frontend review context containing
|
|
3323
|
-
subtask_prompt: "Capture the actual diff and bind it to the effective
|
|
3643
|
+
outputContract: "Canonical frontend review context containing a hash-bound contract reference and field index, lint assessment when configured, effective verification trace, optional verify-failure facts, and actual worktree diff.",
|
|
3644
|
+
subtask_prompt: "Capture the actual diff and bind it to the effective verification evidence for final review.",
|
|
3324
3645
|
shell: {
|
|
3325
3646
|
commands: [],
|
|
3326
3647
|
frontendReviewContext: { schemaVersion: 1, requireBaseline: true },
|
|
@@ -3335,79 +3656,98 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3335
3656
|
executor: "pi",
|
|
3336
3657
|
complexity: "HIGH",
|
|
3337
3658
|
writePolicy: "read-only",
|
|
3659
|
+
retryPolicy: FRONTEND_REVIEW_TERMINAL_RETRY_POLICY,
|
|
3338
3660
|
allowedPaths: readOnlyPaths,
|
|
3339
3661
|
forbiddenPaths,
|
|
3340
3662
|
skills: FRONTEND_REVIEW_SKILLS,
|
|
3341
|
-
outputContract: '
|
|
3342
|
-
outputProtocol: REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL,
|
|
3663
|
+
outputContract: 'Authoritative typed review terminal via approve_review / request_review_changes tools. No JSON verdict is required in the response text; the typed terminal fact is the only authority. No file writes.',
|
|
3343
3664
|
subtask_prompt: [
|
|
3344
3665
|
"Review the frontend implementation and verification evidence.",
|
|
3345
|
-
"
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
"Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds the validated implementation contract, frontend lint assessment when lint is configured, effective initial-or-post-repair verification trace, repair assessment, and the run-owned actual diff (contracts/frontend-worktree-diff.json + artifacts/diff_patch.patch). Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
|
|
3666
|
+
"Your authoritative terminal verdict is exactly one committed typed tool call: approve_review or request_review_changes. Call it once and do not call the other afterwards.",
|
|
3667
|
+
"approve_review means the implementation passes; it must not carry Critical or Important findings. request_review_changes must carry a typed issueCategory, at least one evidenceRef, and non-empty findings.",
|
|
3668
|
+
"Do NOT emit an equivalent JSON verdict in the response text: the committed typed terminal fact is the only authority and no branch or gate reads response-text JSON verdicts.",
|
|
3669
|
+
"Read contracts/frontend-review-context.json from frontend-review-context-shell. It binds a hash-verified canonical contract reference, a field-to-section index, frontend lint assessment when configured, the effective verification trace, and the run-owned actual diff. Read contractRef.capacityDiagnosticPath first: use contractRef.path only when full-contract is recommended; otherwise read only the hash-bound contractRef.sections needed for the changed surface and verification claims. Then read diff.reviewSummaryPath. The full artifacts/diff_patch.patch is retained only as audit evidence: do NOT read it in full. For semantic review, read only the named per-file diff fragment in the summary/index (in part order when needed) and then the current source file when necessary. Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/stable-target-id binding only—not semantic correctness.",
|
|
3350
3670
|
"Treat lint status exactly as passed | baseline-debt | failed | unavailable. baseline-debt may continue only with intact evidence and zero diagnostics on writer-changed files; report the tolerated debt count and never rewrite it as lint passed. Typecheck, build, and test still require successful final exits.",
|
|
3351
3671
|
"Flag .skip/.only, deleted or weakened tests, unauthorized config changes, Mock-only evidence claimed as real integration, and Browser/visual claims (always not-run in this workflow).",
|
|
3352
|
-
"The contract
|
|
3672
|
+
"The contract referenced and hash-bound by frontend-review-context.json is the effective plan materialized by frontend-design-policy-shell. Do not re-open task sources, OpenSpec, AI workspace, design-review, writer summary, or verification node prose. Inspect only the canonical review context, its indexed contract sections, its bound diff, and diff-referenced files when semantic review requires source code.",
|
|
3673
|
+
"For uiComponentChoices, purpose is the stable coverage key and must match interaction.name or uiState.name. Responsibility is expressed by the matched expectedBehavior plus rationale; you must not reject it merely for matching an interaction or component identifier.",
|
|
3353
3674
|
"Treat a commented-out real request, default-enabled Mock, production entrypoint importing test mocks, API/fixture contract drift, unauthorized Mock dependency/path, or missing behavior evidence for the selected strategy as at least Important. Mock strategies require Mock-backed evidence. not-needed requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case verify that the real request remains the default and the Real Integration Gap is preserved.",
|
|
3354
|
-
"Inspect the frontend-verify-
|
|
3675
|
+
"Inspect the frontend-verify-shell evidence in the review context directly, including the production/default-real-path static check, and require Mock activation to be off for that check.",
|
|
3355
3676
|
"Distinguish Mock-backed evidence from real API integration evidence and preserve the Real Integration Gap when the backend was not exercised.",
|
|
3356
3677
|
"Review implementation quality, behavior/state coverage, verification evidence, and maintainability. Read-only: do not modify files.",
|
|
3357
3678
|
].join("\n\n"),
|
|
3358
3679
|
},
|
|
3359
3680
|
{
|
|
3360
|
-
id: "frontend-
|
|
3361
|
-
depends_on: ["frontend-review-pi"],
|
|
3362
|
-
role: "
|
|
3681
|
+
id: "frontend-closeout-shell",
|
|
3682
|
+
depends_on: ["frontend-review-context-shell", "frontend-review-pi"],
|
|
3683
|
+
role: "closeout",
|
|
3363
3684
|
executor: "shell",
|
|
3364
3685
|
complexity: "LOW",
|
|
3365
3686
|
writePolicy: "read-only",
|
|
3366
3687
|
allowedPaths: readOnlyPaths,
|
|
3367
3688
|
forbiddenPaths,
|
|
3368
|
-
outputContract:
|
|
3369
|
-
subtask_prompt:
|
|
3689
|
+
outputContract: "Deterministic closeout rendered from committed facts only: coverage matrix, lint status, integration facts, typed review verdict, cumulative diff, browser/visual not-run, risks, and follow-up. No model summaries are re-interpreted.",
|
|
3690
|
+
subtask_prompt: "Render the closeout deterministically from frontend-review-context.json, the verification trace, and the committed typed review terminal fact.",
|
|
3370
3691
|
shell: {
|
|
3371
3692
|
commands: [],
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
routingAccept: ["request-revision"],
|
|
3376
|
-
label: "frontend review",
|
|
3377
|
-
source: "json-review-verdict",
|
|
3693
|
+
frontendCloseout: {
|
|
3694
|
+
schemaVersion: 1,
|
|
3695
|
+
reviewFromNodeId: "frontend-review-pi",
|
|
3378
3696
|
},
|
|
3379
3697
|
cwd: ".",
|
|
3380
3698
|
timeoutMs: 60000,
|
|
3381
3699
|
},
|
|
3382
3700
|
},
|
|
3383
|
-
{
|
|
3384
|
-
id: "frontend-closeout-pi",
|
|
3385
|
-
depends_on: [
|
|
3386
|
-
"frontend-review-context-shell",
|
|
3387
|
-
"frontend-review-gate-shell",
|
|
3388
|
-
],
|
|
3389
|
-
role: "closeout",
|
|
3390
|
-
executor: "pi",
|
|
3391
|
-
complexity: "MED",
|
|
3392
|
-
writePolicy: "read-only",
|
|
3393
|
-
allowedPaths: taskConfig.allowedPaths.length > 0
|
|
3394
|
-
? [...taskConfig.allowedPaths, "docs/**"]
|
|
3395
|
-
: ["**", "docs/**"],
|
|
3396
|
-
forbiddenPaths,
|
|
3397
|
-
skills: FRONTEND_VERIFICATION_SKILLS,
|
|
3398
|
-
outputContract: "Markdown closeout summary with Changes, Mock Decision / Strategy / Files / Verification / Production Boundary, Verification Evidence, Review Result, Frontend Status, Real Integration Status, Known Risks, and Follow-up. No file writes.",
|
|
3399
|
-
subtask_prompt: [
|
|
3400
|
-
"Return a frontend closeout summary covering Mock decision/strategy/files/verification/production boundary, changes, verification evidence, review result, known risks, and follow-up.",
|
|
3401
|
-
"Use only the canonical frontend-review-context.json plus the final frontend review gate verdict. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, design-review, writer, repair, or verification prose, and do not perform new repository research during closeout.",
|
|
3402
|
-
"Include a coverage matrix for each requirement id, applicable UI state, and verification target/check with status passed|failed|not-run|blocked|unavailable. Report lint separately as passed|baseline-debt|failed|unavailable; baseline-debt is explicit debt, not passed. Always state Browser accessibility verification: not-run and Visual regression: not-run. Use contracts/frontend-review-context.json and the effective frontend-verify-assess-shell or frontend-reverify-shell facts; do not invent Browser evidence from component tests.",
|
|
3403
|
-
`When only Mock-backed evidence passed, state exactly Frontend status: mock-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task to create/run after backend readiness. This follow-up is not auto-created or auto-executed. Never describe Mock evidence as real API integration.`,
|
|
3404
|
-
`When Mock was skipped in auto mode and no real API evidence passed, state exactly Frontend status: locally-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task when backend readiness matters.`,
|
|
3405
|
-
"Read-only: do not modify code, docs, artifacts, or .harness/dag-runs/.",
|
|
3406
|
-
].join("\n\n"),
|
|
3407
|
-
},
|
|
3408
3701
|
],
|
|
3409
3702
|
};
|
|
3410
|
-
|
|
3703
|
+
// The static DAG template is the topology source of truth: the generated
|
|
3704
|
+
// chain must match the template's node set and order, and the template's
|
|
3705
|
+
// depends_on must be a subset of the generated one (the runtime may add
|
|
3706
|
+
// dependencies, e.g. Mock-required planning depending on the contract
|
|
3707
|
+
// node's Mock facts). The template's own tasks carry simplified placeholder
|
|
3708
|
+
// configs; the generated node definitions (budgets, retry, skeleton,
|
|
3709
|
+
// skills, prompts) and any runtime-added dependencies win.
|
|
3710
|
+
const frontendTemplate = await loadFrontendDagTemplate(sources.repoRoot);
|
|
3711
|
+
if (frontendTemplate) {
|
|
3712
|
+
const byId = new Map(spec.tasks.map((task) => [task.id, task]));
|
|
3713
|
+
const templateIds = frontendTemplate.tasks.map((task) => task.id);
|
|
3714
|
+
const missing = templateIds.filter((id) => !byId.has(id));
|
|
3715
|
+
if (missing.length > 0) {
|
|
3716
|
+
throw new Error(`frontend DAG template topology drift: generated chain is missing template node(s): ${missing.join(", ")}`);
|
|
3717
|
+
}
|
|
3718
|
+
for (const templateTask of frontendTemplate.tasks) {
|
|
3719
|
+
const generated = byId.get(templateTask.id);
|
|
3720
|
+
const generatedDeps = new Set(generated.depends_on);
|
|
3721
|
+
const templateOnly = templateTask.depends_on.filter((dep) => !generatedDeps.has(dep));
|
|
3722
|
+
if (templateOnly.length > 0) {
|
|
3723
|
+
throw new Error(`frontend DAG template topology drift: generated node ${templateTask.id} is missing template dependency ${templateOnly.join(", ")}`);
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
const templateSet = new Set(templateIds);
|
|
3727
|
+
const ordered = templateIds.map((id) => byId.get(id));
|
|
3728
|
+
const extra = spec.tasks.filter((task) => !templateSet.has(task.id));
|
|
3729
|
+
spec.tasks = [...ordered, ...extra];
|
|
3730
|
+
}
|
|
3731
|
+
if (frontendTaskShape.shape === "split-required") {
|
|
3732
|
+
spec.tasks = pruneFrontendTasksForSplitRequired(spec.tasks);
|
|
3733
|
+
}
|
|
3734
|
+
else {
|
|
3735
|
+
const reshapeRaisedTopology = frontendTaskShape.signals.includes("shape-transition-reshape");
|
|
3736
|
+
if (!reshapeRaisedTopology) {
|
|
3737
|
+
spec.tasks = pruneFrontendTasksForRisk(spec.tasks, frontendRisk);
|
|
3738
|
+
}
|
|
3739
|
+
if (frontendTaskShape.shape === "micro") {
|
|
3740
|
+
spec.tasks = pruneFrontendTasksForMicro(spec.tasks);
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
spec.advisories = [
|
|
3744
|
+
...(spec.advisories ?? []),
|
|
3745
|
+
`frontend-task-shape: ${frontendTaskShape.shape} (${frontendTaskShape.reason})`,
|
|
3746
|
+
];
|
|
3747
|
+
// Freeze the frontend recovery continuation quota from task.json so the
|
|
3748
|
+
// runner can bound M6 auto-recovery without re-reading the task config.
|
|
3749
|
+
const frontendMaxContinuations = sources.taskConfig.frontendRecovery?.maxContinuations ?? 1;
|
|
3750
|
+
spec.frontendRecovery = { maxContinuations: frontendMaxContinuations };
|
|
3411
3751
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
3412
3752
|
stampGeneratedArtifactBindings(spec);
|
|
3413
3753
|
parseDagSpec(spec);
|
|
@@ -4017,7 +4357,7 @@ export function applyBackendTestLayoutToText(text, layout) {
|
|
|
4017
4357
|
* normalization. Output is exactly one trailing JSON line `{modules:[{stem}]}`
|
|
4018
4358
|
* that `parseJsonFromText` accepts after shell command echoes.
|
|
4019
4359
|
*/
|
|
4020
|
-
function buildBackendTestModuleManifestShellCommand(layout) {
|
|
4360
|
+
function buildBackendTestModuleManifestShellCommand(layout, moduleLayout) {
|
|
4021
4361
|
// The extractor is base64-encoded so the shell command is fully opaque to
|
|
4022
4362
|
// bash: no backticks (command substitution), no regex \/ escaping, no
|
|
4023
4363
|
// backslash-counting through TS-string -> JSON.stringify -> bash -c -> node -e.
|
|
@@ -4026,48 +4366,166 @@ function buildBackendTestModuleManifestShellCommand(layout) {
|
|
|
4026
4366
|
// mdDir/testPrefix are injected as JSON literals so the same extractor
|
|
4027
4367
|
// works for any configured backendTest layout (plan A).
|
|
4028
4368
|
const mdDirLiteral = JSON.stringify(layout.markdownDir);
|
|
4369
|
+
const scriptDirLiteral = JSON.stringify(layout.scriptDir);
|
|
4370
|
+
const strictLayoutCompressed = moduleLayout
|
|
4371
|
+
? deflateRawSync(Buffer.from(JSON.stringify(moduleLayout), "utf8")).toString("base64")
|
|
4372
|
+
: null;
|
|
4373
|
+
const strictLayoutExpression = strictLayoutCompressed
|
|
4374
|
+
? `JSON.parse(zlib.inflateRawSync(Buffer.from(${JSON.stringify(strictLayoutCompressed)},'base64')).toString('utf8'))`
|
|
4375
|
+
: "null";
|
|
4376
|
+
const strictLayoutShaLiteral = JSON.stringify(moduleLayout
|
|
4377
|
+
? createHash("sha256").update(JSON.stringify(moduleLayout)).digest("hex")
|
|
4378
|
+
: null);
|
|
4029
4379
|
const escOpen = String.fromCharCode(92, 91); // \[
|
|
4030
4380
|
const escClose = String.fromCharCode(92, 93); // \]
|
|
4031
4381
|
const escBslash = String.fromCharCode(92, 92); // \\
|
|
4032
|
-
const script = `const fs=require('fs'),path=require('path'),crypto=require('crypto');
|
|
4382
|
+
const script = `const fs=require('fs'),path=require('path'),crypto=require('crypto'),zlib=require('zlib');
|
|
4033
4383
|
const mdDir=${mdDirLiteral};
|
|
4384
|
+
const scriptDir=${scriptDirLiteral};
|
|
4385
|
+
const strictLayout=${strictLayoutExpression};
|
|
4386
|
+
const strictLayoutSha256=${strictLayoutShaLiteral};
|
|
4034
4387
|
const esc=s=>s.replace(/[${escOpen}${escClose}{}()*+?^$|${escBslash}]/g,'${escBslash}$&');
|
|
4035
4388
|
const rxMdPath=new RegExp(esc(mdDir)+'${escBslash}/([A-Za-z0-9_.-]+)${escBslash}.md','g');
|
|
4036
4389
|
const runDir=process.env.HARNESS_DAG_RUN_DIR||'';
|
|
4037
4390
|
if(!runDir){process.stderr.write('missing HARNESS_DAG_RUN_DIR for backend-test Markdown plan artifact\\n');process.exit(2);}
|
|
4038
4391
|
const planPath=path.join(runDir,'generate-backend-md-plan-pi','plan.md');
|
|
4039
4392
|
if(!fs.existsSync(planPath)){process.stderr.write('missing backend-test Markdown plan artifact: '+planPath+'\\n');process.exit(2);}
|
|
4040
|
-
|
|
4393
|
+
let readme=fs.readFileSync(planPath,'utf8');
|
|
4041
4394
|
const norm=s=>String(s).toLowerCase().replace(/[^a-z0-9]+/g,'_').replace(/^_+|_+$/g,'').replace(/_+/g,'_');
|
|
4042
4395
|
const bt=String.fromCharCode(96);
|
|
4043
4396
|
const stripBackticks=s=>s.split(bt).join('');
|
|
4044
4397
|
const invalidReason=raw=>{const st=norm(raw);if(/^p[0-2]$/.test(st))return 'priority-only-module-stem';if(/^[a-f][a-f0-9]{6,63}$/.test(st))return 'opaque-hash-module-stem';if(st==='readme')return 'reserved-module-stem';if(!/^[a-z][a-z0-9_]*$/.test(st))return 'invalid-syntax';if(/^(?:be|tp|ac|req|br)[_-]/i.test(st))return 'case-like-module-stem';return null;};
|
|
4045
4398
|
const valid=raw=>invalidReason(raw)===null;
|
|
4046
4399
|
const rxRelLink=/\\[[^\\]]+\\]\\(\\.\\/([A-Za-z0-9_.-]+)\\.md\\)/g;
|
|
4400
|
+
const headingAlias={'\u6a21\u5757\u7d22\u5f15':'Module Index','\u8986\u76d6\u8303\u56f4':'Coverage Scope','\u8986\u76d6\u77e9\u9635':'Coverage Matrix','\u573a\u666f\u5206\u533a':'Scenario Partitions'};
|
|
4047
4401
|
const allLines=readme.replace(/\\r\\n/g,'\\n').replace(/\\r/g,'\\n').split('\\n');
|
|
4402
|
+
let headingRepair=false;
|
|
4403
|
+
for(let i=0;i<allLines.length;i++){const alias=/^##\\s+(模块索引|覆盖范围|覆盖矩阵|场景分区)\\s*$/.exec(allLines[i].trim());if(alias){allLines[i]='## '+headingAlias[alias[1]];headingRepair=true;}}
|
|
4404
|
+
let partitionRepair=false;
|
|
4405
|
+
const partHeadTrim=[];for(let i=0;i<allLines.length;i++){if(allLines[i].trim()==='## Scenario Partitions')partHeadTrim.push(i);}
|
|
4406
|
+
if(partHeadTrim.length===1){
|
|
4407
|
+
const pStart=partHeadTrim[0]+1;let pEnd=allLines.length;for(let i=pStart;i<allLines.length;i++){if(/^##\\s+\\S/.test(allLines[i].trim())){pEnd=i;break;}}
|
|
4408
|
+
const pCells=line=>line.split('|').slice(1,-1).map(v=>String(v||'').trim());
|
|
4409
|
+
const pHeader=['Partition ID','Operation','Axis','Domain','Required Slots','Expected by Slot','Bind Rule'];
|
|
4410
|
+
for(let i=pStart;i<pEnd;i++){
|
|
4411
|
+
if(!allLines[i].includes('|')) continue;
|
|
4412
|
+
const row=pCells(allLines[i]);
|
|
4413
|
+
if(row.length<=7) continue;
|
|
4414
|
+
if(pHeader.every((v,idx)=>row[idx]===v)) continue;
|
|
4415
|
+
const pid=String(row[0]||'').trim();
|
|
4416
|
+
const extras=row.slice(7).join(';').split(/[;,,;]/).map(v=>v.trim().split(bt).join('')).filter(Boolean);
|
|
4417
|
+
const prefix='TP-'+pid.toUpperCase()+'-';
|
|
4418
|
+
if(extras.length && extras.every(t=>/^TP-[A-Z0-9-]+$/i.test(t)&&(t.toUpperCase().startsWith(prefix)||t.toUpperCase().startsWith('TP-SP-')))){
|
|
4419
|
+
allLines[i]='| '+row.slice(0,7).join(' | ')+' |';
|
|
4420
|
+
partitionRepair=true;
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
if(headingRepair||partitionRepair)readme=allLines.join('\\n');
|
|
4048
4425
|
const headings=[];for(let i=0;i<allLines.length;i++){if(allLines[i].trim()==='## Module Index')headings.push(i);}
|
|
4049
4426
|
if(headings.length!==1){process.stderr.write((headings.length===0?'missing-module-index':'duplicate-module-index')+'; require exactly one exact ## Module Index section\\n');process.exit(2);}
|
|
4050
4427
|
const start=headings[0]+1;let end=allLines.length;for(let i=start;i<allLines.length;i++){if(/^##\\s+\\S/.test(allLines[i].trim())){end=i;break;}}
|
|
4051
4428
|
const section=allLines.slice(start,end).join('\\n');
|
|
4052
4429
|
const raw=[];
|
|
4053
4430
|
const lines=section.split('\\n').filter(l=>l.includes('|'));
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4431
|
+
const cells=line=>line.split('|').slice(1,-1).map(value=>stripBackticks(value).trim());
|
|
4432
|
+
const expectedHeader=['Module Stem','Business Resource','Owned Operations','Owned Rule Keys','Case IDs','Split Reason','Markdown Path','Pytest Path'];
|
|
4433
|
+
const headerIndex=lines.findIndex(line=>{const row=cells(line);return expectedHeader.every((value,index)=>row[index]===value);});
|
|
4434
|
+
if(headerIndex<0){process.stderr.write('invalid-module-index-header: require exact business ownership and path columns\\n');process.exit(2);}
|
|
4435
|
+
const dataRows=lines.slice(headerIndex+2).map(cells).filter(row=>row.length>=8&&row[0]&&row[0]!=='Module Stem');
|
|
4436
|
+
const allowedSplit=new Set(['explicit-user-layout','primary-business-resource','independent-business-resource','output-budget']);
|
|
4437
|
+
const operationOwners=new Map();const canonicalSeen=new Set();const declaredModules=[];let planRepairApplied=headingRepair||partitionRepair;
|
|
4438
|
+
for(const row of dataRows){
|
|
4439
|
+
const rawStem=String(row[0]||'').trim(),resource=String(row[1]||'').trim(),operations=String(row[2]||'').split(';').map(value=>value.trim()).filter(Boolean),split=String(row[5]||'').trim();
|
|
4440
|
+
const mdMatches=[...String(row[6]||'').matchAll(rxMdPath)];
|
|
4441
|
+
if(mdMatches.length!==1){process.stderr.write('module-markdown-path-mismatch: '+(rawStem||'unknown')+'; require exactly one canonical Markdown Path\\n');process.exit(2);}
|
|
4442
|
+
let stem=norm(mdMatches[0][1]),markdownPath=mdMatches[0][0];
|
|
4443
|
+
if(strictLayout&&!strictLayout.modules.some(item=>item.markdownPath===markdownPath)){planRepairApplied=true;continue;}
|
|
4444
|
+
if(!resource){process.stderr.write('missing-business-resource: '+stem+'\\n');process.exit(2);}
|
|
4445
|
+
if(/^(?:response|resp|regression|positive|negative|boundary|error|combo|filter)(?:[_ -]|$)/i.test(resource)){process.stderr.write('test-purpose-business-resource: '+stem+' -> '+resource+'\\n');process.exit(2);}
|
|
4446
|
+
if(!allowedSplit.has(split)){process.stderr.write('invalid-module-split-reason: '+stem+' -> '+split+'\\n');process.exit(2);}
|
|
4447
|
+
if(operations.length===0){process.stderr.write('missing-owned-operation: '+stem+'\\n');process.exit(2);}
|
|
4448
|
+
let pytestPath=String(row[7]||'').replace(/^\\.\\//,'').replace(/\\\\/g,'/');
|
|
4449
|
+
if(!/^[A-Za-z0-9_./-]+\\.py$/.test(pytestPath)||pytestPath.includes('..')){process.stderr.write('invalid-module-pytest-path: '+stem+' -> '+pytestPath+'\\n');process.exit(2);}
|
|
4450
|
+
const requiredCandidates=strictLayout?strictLayout.modules.filter(item=>item.markdownPath===markdownPath):[];
|
|
4451
|
+
if(strictLayout&&requiredCandidates.length!==1){process.stderr.write('MODULE_LAYOUT_CONFLICT: no unique required module for Markdown Path '+markdownPath+'\\n');process.exit(2);}
|
|
4452
|
+
const required=requiredCandidates[0];
|
|
4453
|
+
if(!required&&norm(rawStem)!==stem)planRepairApplied=true;
|
|
4454
|
+
if(required){
|
|
4455
|
+
stem=required.stem;
|
|
4456
|
+
if(norm(rawStem)!==stem)planRepairApplied=true;
|
|
4457
|
+
if(!required.markdownPath.startsWith(mdDir+'/')||!required.pytestPath.startsWith(scriptDir+'/')||required.markdownPath.includes('..')||required.pytestPath.includes('..')){process.stderr.write('MODULE_LAYOUT_CONFLICT: strict paths outside frozen roots for '+stem+'\\n');process.exit(2);}
|
|
4458
|
+
if(markdownPath!==required.markdownPath||pytestPath!==required.pytestPath||resource!==required.businessResource||JSON.stringify(operations)!==JSON.stringify(required.ownedOperations)||split!==required.splitReason){planRepairApplied=true;}
|
|
4459
|
+
markdownPath=required.markdownPath;pytestPath=required.pytestPath;
|
|
4460
|
+
if(required.splitReason==='output-budget'){
|
|
4461
|
+
const proof=required.budgetProof;const peers=strictLayout.modules.filter(item=>item.budgetProof&&item.budgetProof.groupId===proof.groupId);
|
|
4462
|
+
if(!proof||proof.estimatedOutputChars<=proof.maxOutputCharsPerWriter||Math.ceil(proof.estimatedOutputChars/Math.max(1,peers.length))>proof.maxOutputCharsPerWriter||peers.some(item=>item.budgetProof.inputSha256!==proof.inputSha256||item.budgetProof.estimatorVersion!==proof.estimatorVersion)){process.stderr.write('INVALID_OUTPUT_BUDGET_PROOF: '+stem+'\\n');process.exit(2);}
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
const item={stem,markdownPath,pytestPath,businessResource:required?required.businessResource:resource,ownedOperations:required?required.ownedOperations:operations,ownedRuleKeys:String(row[3]||'').split(';').map(value=>value.trim()).filter(Boolean),caseIds:String(row[4]||'').split(';').map(value=>value.trim()).filter(Boolean),splitReason:required?required.splitReason:split,...(required&&required.budgetProof?{budgetProof:required.budgetProof}:{})};
|
|
4466
|
+
if(canonicalSeen.has(item.stem)){process.stderr.write('duplicate-canonical-module-stem: '+item.stem+'; module identity must be unique\\n');process.exit(2);}
|
|
4467
|
+
canonicalSeen.add(item.stem);
|
|
4468
|
+
declaredModules.push(item);
|
|
4469
|
+
for(const operation of item.ownedOperations){const owners=operationOwners.get(operation)||[];owners.push({stem,split:item.splitReason});operationOwners.set(operation,owners);}
|
|
4470
|
+
}
|
|
4471
|
+
if(strictLayout){
|
|
4472
|
+
const missing=strictLayout.modules.filter(item=>!declaredModules.some(actual=>actual.stem===item.stem));
|
|
4473
|
+
if(missing.length){process.stderr.write('MODULE_LAYOUT_CONFLICT: plan missing required modules '+missing.map(item=>item.stem).join(',')+'; deterministic Plan-only repair cannot invent Rule/Case ownership\\n');process.exit(2);}
|
|
4474
|
+
}
|
|
4475
|
+
if(planRepairApplied){
|
|
4476
|
+
const table=['| '+expectedHeader.join(' | ')+' |','|'+expectedHeader.map(()=>'---').join('|')+'|',...declaredModules.map(item=>'| '+[item.stem,item.businessResource,item.ownedOperations.join('; '),item.ownedRuleKeys.join('; '),item.caseIds.join('; '),item.splitReason,'['+item.stem+'](./'+item.stem+'.md) '+item.markdownPath,item.pytestPath].join(' | ')+' |')].join('\\n');
|
|
4477
|
+
readme=[...allLines.slice(0,headings[0]+1),table,...allLines.slice(end)].join('\\n');
|
|
4478
|
+
fs.writeFileSync(planPath,readme,'utf8');
|
|
4479
|
+
} else if(headingRepair||partitionRepair){
|
|
4480
|
+
fs.writeFileSync(planPath,readme,'utf8');
|
|
4481
|
+
}
|
|
4482
|
+
for(const [operation,owners] of operationOwners){if(owners.length>1&&!owners.every(owner=>owner.split==='explicit-user-layout'||owner.split==='output-budget')){process.stderr.write('overlapping-operation-modules: '+operation+' -> '+owners.map(owner=>owner.stem).join(',')+'; merge by business resource or use an authoritative explicit-user-layout\\n');process.exit(2);}}
|
|
4483
|
+
if(!strictLayout){
|
|
4484
|
+
for(const line of lines){
|
|
4485
|
+
const bare=stripBackticks(line);
|
|
4486
|
+
for(const m of bare.matchAll(rxMdPath)){raw.push(m[1]);}
|
|
4487
|
+
}
|
|
4488
|
+
for(const m of section.matchAll(rxRelLink)){raw.push(m[1]);}
|
|
4057
4489
|
}
|
|
4058
|
-
for(const m of section.matchAll(rxRelLink)){raw.push(m[1]);}
|
|
4059
4490
|
const invalid=[];for(const r of raw){const reason=invalidReason(r);if(reason)invalid.push({stem:norm(r),reason});}
|
|
4060
4491
|
if(invalid.length){for(const item of invalid)process.stderr.write(item.reason+': '+item.stem+'; use a stable business resource/domain stem\\n');process.exit(2);}
|
|
4061
|
-
const
|
|
4062
|
-
for(const
|
|
4492
|
+
const modules=[];
|
|
4493
|
+
for(const item of declaredModules){if(valid(item.stem))modules.push(item);}
|
|
4063
4494
|
if(modules.length===0){process.stderr.write('empty-module-index: require at least one stable business module\\n');process.exit(2);}
|
|
4064
4495
|
if(modules.length>8){process.stderr.write('excessive-module-count: '+modules.length+' > 8; merge by the smallest stable business resource/domain set\\n');process.exit(2);}
|
|
4065
4496
|
const planReadPath=path.relative(process.cwd(),planPath).split(path.sep).join('/');
|
|
4066
4497
|
const planSha256=crypto.createHash('sha256').update(readme).digest('hex');
|
|
4067
|
-
|
|
4498
|
+
const slotTok=v=>String(v).trim().toUpperCase().replace(/[^A-Z0-9]+/g,'-').replace(/^-+|-+$/g,'');
|
|
4499
|
+
const partHead=[];for(let i=0;i<allLines.length;i++){if(allLines[i].trim()==='## Scenario Partitions')partHead.push(i);}
|
|
4500
|
+
if(partHead.length>1){process.stderr.write('duplicate-scenario-partitions; require at most one exact ## Scenario Partitions section\\n');process.exit(2);}
|
|
4501
|
+
const slotByModule=new Map(modules.map(m=>[m.stem,[]]));const unassigned=[];
|
|
4502
|
+
if(partHead.length===1){
|
|
4503
|
+
const pStart=partHead[0]+1;let pEnd=allLines.length;for(let i=pStart;i<allLines.length;i++){if(/^##\\s+\\S/.test(allLines[i].trim())){pEnd=i;break;}}
|
|
4504
|
+
const pLines=allLines.slice(pStart,pEnd).filter(l=>l.includes('|'));
|
|
4505
|
+
const pCells=line=>line.split('|').slice(1,-1).map(v=>stripBackticks(v).trim());
|
|
4506
|
+
const pHeader=['Partition ID','Operation','Axis','Domain','Required Slots','Expected by Slot','Bind Rule'];
|
|
4507
|
+
const pH=pLines.findIndex(line=>pHeader.every((v,i)=>pCells(line)[i]===v));
|
|
4508
|
+
if(pH<0){process.stderr.write('MISSING_PARTITION_TABLE: Scenario Partitions section has no canonical header row\\n');process.exit(2);}
|
|
4509
|
+
const pRows=pLines.slice(pH+2).map(pCells).filter(r=>r.length>=7&&r[0]&&r[0]!=='Partition ID');
|
|
4510
|
+
for(const row of pRows){
|
|
4511
|
+
const pid=String(row[0]||'').trim(),op=String(row[1]||'').trim(),domain=String(row[3]||'').split(/[;,,;]/).map(v=>v.trim()).filter(Boolean),optional=/omit/i.test(String(row[4]||'')),bind=String(row[6]||'').trim();
|
|
4512
|
+
const prefix=/^SP-[A-Z0-9][A-Z0-9._-]*$/i.test(pid)?('TP-'+pid.toUpperCase()):('TP-SP-'+slotTok(op)+'-'+slotTok(String(row[2]||'')));
|
|
4513
|
+
const slotIds=[...domain.map(v=>prefix+'-'+slotTok(v)),...(optional?[prefix+'-OMITTED']:[]),prefix+'-NOT-IN-SET'];
|
|
4514
|
+
const owners=modules.filter(m=>(m.ownedRuleKeys||[]).includes(bind)||(m.ownedOperations||[]).some(o=>String(o).replace(/\\s+/g,' ').toUpperCase()===op.replace(/\\s+/g,' ').toUpperCase()));
|
|
4515
|
+
const uniqueOwners=[...new Set(owners.map(o=>o.stem))];
|
|
4516
|
+
if(uniqueOwners.length===1){for(const id of slotIds)slotByModule.get(uniqueOwners[0]).push(id);}
|
|
4517
|
+
else if(modules.length===1){for(const id of slotIds)slotByModule.get(modules[0].stem).push(id);}
|
|
4518
|
+
else unassigned.push(...slotIds);
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
if(unassigned.length){process.stderr.write('unassigned-partition-slots: '+unassigned.join(',')+'\\n');process.exit(2);}
|
|
4522
|
+
const slotInventory={schemaId:'backend-test-scenario-partition-slots-v1',planSha256,modules:modules.map(m=>({stem:m.stem,requiredVariantSlots:[...new Set(slotByModule.get(m.stem)||[])]})),unassignedSlotIds:[]};
|
|
4523
|
+
fs.mkdirSync(path.join(runDir,'contracts'),{recursive:true});
|
|
4524
|
+
fs.writeFileSync(path.join(runDir,'contracts','backend-test-scenario-partition-slots.json'),JSON.stringify(slotInventory,null,2));
|
|
4525
|
+
process.stdout.write(JSON.stringify({modules:modules.map(item=>({...item,planReadPath,requiredVariantSlots:(slotInventory.modules.find(m=>m.stem===item.stem)||{requiredVariantSlots:[]}).requiredVariantSlots})),planReadPath,planSha256,moduleLayout:{schemaId:'backend-test-module-layout-facts-v1',source:strictLayout?'task-contract':'plan-derived',contractSha256:strictLayoutSha256,mode:strictLayout&&strictLayout.mode||'business-resource-layout',planRepairAttemptCount:planRepairApplied?1:0,planRepairOutcome:planRepairApplied?'changed':'not-required'},partitionSlots:slotInventory}));
|
|
4068
4526
|
`;
|
|
4069
|
-
const encoded = Buffer.from(script, "utf8").toString("base64");
|
|
4070
|
-
return `node -e "eval(Buffer.from('${encoded}','base64').toString('utf8'))"`;
|
|
4527
|
+
const encoded = deflateRawSync(Buffer.from(script, "utf8")).toString("base64");
|
|
4528
|
+
return `node -e "eval(require('zlib').inflateRawSync(Buffer.from('${encoded}','base64')).toString('utf8'))"`;
|
|
4071
4529
|
}
|
|
4072
4530
|
const BACKEND_TEST_SKILLS_BY_ROLE = {
|
|
4073
4531
|
planner: ["loop-agent"],
|
|
@@ -4641,13 +5099,14 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4641
5099
|
".harness/dag-runs/**/reports/backend-test-environment.md",
|
|
4642
5100
|
],
|
|
4643
5101
|
forbiddenPaths: forbidden,
|
|
5102
|
+
retryPolicy: BACKEND_TEST_MD_PLAN_RETRY_POLICY,
|
|
4644
5103
|
outputContract: "Return a Chinese, human-readable Markdown-first plan with Coverage Scope, Coverage Matrix, Scenario Partitions when applicable, and a machine-parseable Module Index. The runtime persists it as a run-owned Harness artifact; do not write testcase/md/README.md, execute pytest, or modify project files.",
|
|
4645
5104
|
subtask_prompt: [
|
|
4646
|
-
"This is a required plan-generation node. Read only the strict read set and return the complete Markdown plan in the assistant response. The runtime persists the response as a run-owned Harness artifact named generate-backend-md-plan-pi/plan.md. Do not write testcase/md/README.md or any project file; module case cards are written by downstream sharded nodes.",
|
|
4647
|
-
"Output budget protocol (hard, max output <=16K per turn): Never paste full Matrix, case bodies,
|
|
4648
|
-
"Return the complete plan as plain Markdown. Do not emit JSON or code fences. The plan must contain the exact ## Coverage Scope, ## Coverage Matrix and ## Module Index
|
|
5105
|
+
"This is a required plan-generation node. Read only the strict read set and return the complete Markdown plan in the assistant response. Start the response with the final Markdown artifact immediately; never narrate analysis, reasoning, source summaries, or plans for producing the plan. The runtime persists the response as a run-owned Harness artifact named generate-backend-md-plan-pi/plan.md. Do not write testcase/md/README.md or any project file; module case cards are written by downstream sharded nodes.",
|
|
5106
|
+
"Output budget protocol (hard, max output <=16K per turn): Emit the complete required section skeleton before filling long table rows, including exactly one Module Index table with at least one module row. Never paste full Matrix, case bodies, source text, analysis or reasoning into assistant chat. README holds only Scope+Matrix+module index; never inline full case bodies. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.",
|
|
5107
|
+
"Return the complete plan as plain Markdown. Do not emit JSON or code fences. The plan must contain the exact English protocol headings ## Coverage Scope, ## Coverage Matrix, ## Scenario Partitions when applicable, and ## Module Index. Never translate those headings into 覆盖范围/覆盖矩阵/场景分区/模块索引.",
|
|
4649
5108
|
"Read the upstream environment report only through the strict read set. Generate the Markdown-first backend test plan; it will be persisted under the current DAG run's Harness artifacts, not under testcase/md/.",
|
|
4650
|
-
"Write human-readable content in Simplified Chinese by default. Keep English
|
|
5109
|
+
"Write human-readable content in Simplified Chinese by default. Keep English protocol literals exact: section headings, table headers, Partition IDs, TP IDs, Case IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and source citations. Never translate ## Module Index into ## 模块索引.",
|
|
4651
5110
|
"Create the concise plan entry page: test objective, target/environment, isolation/cleanup, module summary and a linked case index table with Case ID, Chinese case name, scenario type, endpoint and expected status/result. Avoid repeating every case body in the plan artifact.",
|
|
4652
5111
|
"Before the Coverage Matrix, write a mandatory machine-readable `## Coverage Scope` section in the plan artifact using exactly `| Field | Value |`, immediately followed by the separator row `|---|---|`, and these six unique rows: `Change Classification`, `Coverage Policy`, `Affected Operations`, `Affected Rule Keys`, `Regression Floor`, `Scope Evidence`. Always set `Change Classification` to `new-operation` and `Coverage Policy` to `full-contract`; do NOT reason about whether operations are new or existing. Cover all in-scope rules from the requirement document at full depth; treat the product requirement as the coverage baseline and use API contract evidence (fields/status/enum/boundary/format) to supplement scenario dimensions. Scope is limited to operations/rules the requirement document (or its referenced API contract) explicitly describes; do not expand to unrelated operations that the requirement does not mention. List affected operations exactly as `METHOD /path`, stable rule keys separated by semicolons, and precise source pointers as Scope Evidence.",
|
|
4653
5112
|
"Coverage depth is full over the in-scope rules: fully cover every documented status, request/response field rule, requiredness, enum, boundary, format, auth and business state of each affected operation the requirement describes, but do not re-test unrelated operations the requirement does not mention. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same affected path can affect them; unresolved impact stays visible as GAP/CONFLICT.",
|
|
@@ -4655,8 +5114,15 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4655
5114
|
"Each Rule Key must appear in exactly one Matrix row. Preserve each AC/REQ/BR Rule Key as one row; if one product rule spans multiple dimensions, use a concise composite Dimension in that single row instead of duplicating the key. Derive OpenAPI Rule Keys exactly as the deterministic analyzer does: operation token is `<HTTP-METHOD>-<PATH>` with braces removed and every non-alphanumeric run replaced by a hyphen, uppercase (for example POST `/api/resource-notes` → `POST-API-RESOURCE-NOTES`); response statuses use `API-<OPERATION>-RESPONSE-STATUS`; body/parameter fields use `API-<OPERATION>-<FIELD>-REQUIRED|ENUM|MIN-LENGTH|MAX-LENGTH|MINIMUM|MAXIMUM|PATTERN|FORMAT`. Do not invent aliases such as API-CREATE-FIELDS when a deterministic key applies.",
|
|
4656
5115
|
"Coverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.",
|
|
4657
5116
|
"For uniqueness/lifecycle rules cover absent, active-existing, deleted-existing, create-delete-recreate, restore-then-recreate and documented scope/case-normalization states. For every enum cover every valid value plus bounded invalid equivalence classes (unknown, case variant, whitespace, empty, null/missing and wrong types as applicable). For every length/number rule cover min-1, min, nominal, max and max+1. For format rules cover each allowed class separately plus a valid mixed value, and representative forbidden classes including uppercase, internal/leading/trailing whitespace, tab/newline, unsupported punctuation, slash, emoji or control characters when the source contract supports that expectation.",
|
|
4658
|
-
"Mandatory module
|
|
4659
|
-
|
|
5117
|
+
"Mandatory module layout contract: first inspect the PRIMARY requirement for an explicit list of required Markdown/Python output path pairs. When explicit paths are present, they are authoritative `explicit-user-layout`: reproduce their exact filenames, count and one-to-one pairs in Module Index; do not rename, merge, split, omit or add a module from reference/example scripts. Only when the primary requirement has no explicit file layout may you derive the smallest `business-resource-layout`. Existing examples, historical regression functions and shared setup may add evidence/assertions to an existing required module, but never create an extra physical module by themselves. A reference-only `resp_regression`, positive/negative/boundary/error/response module is forbidden. The Module Stem cell must contain only the plain filename stem; never put Markdown link syntax or a path in that cell.",
|
|
5118
|
+
...(taskConfig.backendTest?.moduleLayout
|
|
5119
|
+
? [
|
|
5120
|
+
"A strict task-contract `backendTest.moduleLayout` is bound and is authoritative over model-derived layout. Reproduce every stem, businessResource, ownedOperations, splitReason, markdownPath and pytestPath exactly; do not add, omit, rename or reorder physical modules. The downstream preflight compares exact path sets and may perform at most one deterministic Plan-only pruning/path normalization; it cannot invent missing Rule/Case ownership.",
|
|
5121
|
+
`STRICT_BACKEND_TEST_MODULE_LAYOUT=${JSON.stringify(taskConfig.backendTest.moduleLayout)}`,
|
|
5122
|
+
]
|
|
5123
|
+
: []),
|
|
5124
|
+
"Include exactly one `## Module Index` table with this exact header: `| Module Stem | Business Resource | Owned Operations | Owned Rule Keys | Case IDs | Split Reason | Markdown Path | Pytest Path |`. Use canonical relative links `[label](./<stem>.md)` inside the Markdown Path cell followed by the resolved `${layout.markdownDir}/<stem>.md` path. Split Reason is exactly one of `explicit-user-layout`, `primary-business-resource`, `independent-business-resource`, or `output-budget`. Group by stable business resource/domain, not by CRUD operation, AC, parameter/field axis, scenario type or regression purpose: one resource's list/detail/create/update/delete and its filters/response assertions/regression floor belong in one module. Multiple modules owning the same exact `METHOD /path` are forbidden unless every such row is `explicit-user-layout` from primary-requirement path pairs or has a documented `output-budget` proof. Keep the total module count at the smallest safe value and never exceed 8 modules. Name model-derived modules with stable lowercase business stems such as `health` or `resource_notes`; explicit-user-layout preserves the primary requirement filename stem even when it is more specific. Do not use priority-only stems `p0`, `p1` or `p2`; Priority belongs only in the Coverage Matrix. Pure hexadecimal/hash-like opaque stems and test-purpose-only stems are forbidden. Do not use Case-ID-like module filenames. The relative link target, Markdown Path, Pytest Path and downstream automation mapping must be one-to-one and exact; for model-derived modules the default pair remains `testcase/md/<module>.md` and `testcase/test_<module>.py`, while explicit-user-layout preserves the primary requirement paths. Do not hand-write a conflicting module count in prose; the Module Index row count is the only count truth.",
|
|
5125
|
+
"Scenario Partitions (query/filter axes): inspect every affected GET/list operation for query/path parameters whose bound source documents a finite enum or classification domain. If at least one such axis exists, add exactly one machine-readable `## Scenario Partitions` section after the Coverage Matrix using exactly `| Partition ID | Operation | Axis | Domain | Required Slots | Expected by Slot | Bind Rule |` with the separator row and one row per eligible axis. If no affected axis has a source-backed finite domain, omit the entire `## Scenario Partitions` heading and section; do not emit an explanatory prose-only section. Partition ID is a stable `SP-<OPERATION>-<AXIS>` token; Domain must copy the legal values verbatim from the bound OpenAPI enum or requirement sentence (never guess), using bare semicolon-separated identifier values inside the single table cell (for example `ACTIVE; ARCHIVED`, with no Markdown backticks or prose); Required Slots must contain `each-value` and exactly one `not-in-set`, plus `omitted` only when the parameter is optional. Before returning, expand every declared partition into its complete deterministic exact slot ID set: one `TP-<Partition ID>-<VALUE-TOKEN>` per Domain value, `TP-<Partition ID>-OMITTED` only for an optional axis, and exactly one `TP-<Partition ID>-NOT-IN-SET`. Every expanded slot ID must appear verbatim in the binding Rule's `Required Test Points` cell and be assigned to concrete Case IDs in that same Coverage Matrix row; ordinary alias/family Test Points do not replace this inventory. Scheme A: Case count may be smaller than the enum count, but every exact slot still needs an independent variant Test Point and pytest.param id; never use SINGLE/MULTIPLE aliases as coverage. Expected by Slot states the documented expectation per slot kind (`domain-value`, `default-behavior`, `empty-result`/`excluded-result` when documented, or `GAP` when the source does not document the complement expectation — never invent 空列表/400). POST/PUT body field-validation enums stay in the Coverage Matrix as `TP-<FIELD>-ENUM-*` and MUST NOT get a Scenario Partition row. Do not create partitions for axes without a documented legal-value domain. Only GET/list query or path parameters whose bound source documents a finite enum or classification set may become a Scenario Partition. Do not create partitions for free-form strings, primary keys, required-or-optional-only parameters, or boundary/format-only axes. If an axis has no finite legal-value domain, do not declare a Partition row and do not invent NOT-IN-SET cases. Cross-axis combinations stay as ONE nominal Case; never declare a cross-axis cartesian partition.",
|
|
4660
5126
|
"Before finalizing README, calculate the predicted collected-item count as `sum(max(1, number of variant Test Points in each Case))`. If the task declares an item budget, the prediction must not exceed it. Reduce excess only by removing duplicate execution and converting same-request checkpoints to assertions; never drop required rules, boundaries, enums, operation-specific inputs, or business states. Record the prediction in README. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
|
|
4661
5127
|
...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
|
|
4662
5128
|
intake.boundedSourceContext,
|
|
@@ -4675,10 +5141,10 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4675
5141
|
writePolicy: "read-only",
|
|
4676
5142
|
allowedPaths: ro,
|
|
4677
5143
|
forbiddenPaths: forbidden,
|
|
4678
|
-
outputContract: "Stdout JSON {modules:[{stem,planReadPath}],planReadPath,planSha256} parsed from the run-owned generate-backend-md-plan-pi/plan.md artifact
|
|
4679
|
-
subtask_prompt: "Parse only $HARNESS_DAG_RUN_DIR/generate-backend-md-plan-pi/plan.md and
|
|
5144
|
+
outputContract: "Stdout JSON {modules:[{stem,planReadPath}],planReadPath,planSha256,moduleLayout} parsed from the run-owned generate-backend-md-plan-pi/plan.md artifact after strict-layout validation and at most one deterministic Plan-only repair.",
|
|
5145
|
+
subtask_prompt: "Parse only $HARNESS_DAG_RUN_DIR/generate-backend-md-plan-pi/plan.md, validate the optional strict module layout and output-budget proof, apply at most one deterministic Plan-only Module Index repair without project writes, then emit one JSON line. No testcase/md/README.md fallback.",
|
|
4680
5146
|
shell: {
|
|
4681
|
-
commands: [buildBackendTestModuleManifestShellCommand(layout)],
|
|
5147
|
+
commands: [buildBackendTestModuleManifestShellCommand(layout, taskConfig.backendTest?.moduleLayout)],
|
|
4682
5148
|
cwd: ".",
|
|
4683
5149
|
timeoutMs: 60000,
|
|
4684
5150
|
},
|
|
@@ -4714,9 +5180,9 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4714
5180
|
toolProfile: "write",
|
|
4715
5181
|
complexity: "MED",
|
|
4716
5182
|
writePolicy: "exclusive",
|
|
4717
|
-
allowedPaths: ["
|
|
5183
|
+
allowedPaths: ["{{item.markdownPath}}"],
|
|
4718
5184
|
forbiddenPaths: forbidden,
|
|
4719
|
-
writeSet: ["
|
|
5185
|
+
writeSet: ["{{item.markdownPath}}"],
|
|
4720
5186
|
readSet: [
|
|
4721
5187
|
"{{item.planReadPath}}",
|
|
4722
5188
|
toDagSourcePath(sources, sources.requirementPath),
|
|
@@ -4724,22 +5190,23 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4724
5190
|
? [toDagSourcePath(sources, sources.constraintPath)]
|
|
4725
5191
|
: []),
|
|
4726
5192
|
...intake.referenceIndex.map((entry) => entry.readPath),
|
|
4727
|
-
|
|
5193
|
+
"{{item.markdownPath}}",
|
|
4728
5194
|
],
|
|
4729
5195
|
writerOutcomePolicy: {
|
|
4730
5196
|
type: "implementation-outcome-v1",
|
|
4731
5197
|
requireChangedFiles: true,
|
|
4732
5198
|
},
|
|
4733
|
-
retryPolicy:
|
|
4734
|
-
outputContract: "Write exactly
|
|
5199
|
+
retryPolicy: BACKEND_TEST_MARKDOWN_BINDING_RETRY_POLICY,
|
|
5200
|
+
outputContract: "Write exactly the frozen `{{item.markdownPath}}` Chinese module Markdown case-card file with BE-<MODULE>-<NNN> cases and the seven required h3 sections; keep machine IDs/literals exact and do not execute pytest or modify production code/config or the README.",
|
|
4735
5201
|
subtaskPromptTemplate: [
|
|
4736
|
-
"This is a required file-generation node for exactly one Markdown module. Read the upstream run-owned Markdown plan artifact at `{{item.planReadPath}}` (Coverage Scope + Coverage Matrix + Module Index) and the bounded references, then immediately use write tools to create the single file
|
|
5202
|
+
"This is a required file-generation node for exactly one Markdown module. Read the upstream run-owned Markdown plan artifact at `{{item.planReadPath}}` (Coverage Scope + Coverage Matrix + Module Index) and the bounded references, then immediately use write tools to create the single frozen file `{{item.markdownPath}}`. Do not read or recreate testcase/md/README.md. Do not end after analysis or planning, and do not return before a non-empty bounded diff exists. Do not modify any other module file.",
|
|
4737
5203
|
"Output budget protocol (hard, max output <=16K per turn): Never paste full Matrix, other modules' case bodies, or source text into assistant chat. Each write/edit tool call touches at most one file (this module). Compact tables/lists are required; omitting required sections or in-scope variants is forbidden. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.",
|
|
4738
5204
|
"The first non-empty response line must be exactly IMPLEMENTATION_OUTCOME: changed after the module file has been written, or IMPLEMENTATION_OUTCOME: blocked when precise missing evidence prevents safe generation. already-satisfied is not valid for this node.",
|
|
4739
5205
|
"Write human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.",
|
|
4740
5206
|
'Write the module {{item.stem}} as readable case cards covering every in-scope rule/Test Point the README Coverage Matrix assigns to this module. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. `<NNN>` is exactly three zero-padded digits (`001`, `002`, ...), never two digits (`01`), a bare number, or an alphabetic suffix such as `011A`. Every case must include `### 覆盖规则`, `### 测试点`, `### 场景类型`, `### 前置条件`, `### 操作步骤`, `### 预期结果`, and `### 自动化映射` Do not group cases under "## 测试类 ..." (or any h2 grouping) headings that force Cases down to h3; each Case must be a direct h2 (`##`), and its seven sections must be h3 (`###`) children of that Case. If you need to convey a pytest class, state it inside the Case\'s `### 自动化映射` instead. Forbidden: `## 测试类 X` then `### BE-PD-001` and `### 覆盖规则` at the same h3 level. Required: `## BE-PD-001` then `### 覆盖规则`.; `覆盖规则` and `测试点` must reference exact Matrix Rule Keys/Test Points. Add `测试目的`, `验收标准`, `需求依据`, and `测试数据` for readable evidence. The `验收标准` section must list the exact applicable `AC-...` IDs, and every explicit task AC must appear in at least one Case. Every automatable case explicitly names its target pytest script and exactly one primary symbol so traceability scans only that script/symbol. Evidence-only meta cases that exist solely for non-executable assertion/cross-cutting process evidence may declare `脚本:无` and `primary symbol:无` with empty `变体测试点`, and must not invent a business pytest item.',
|
|
4741
|
-
"Name this module file with the
|
|
4742
|
-
"
|
|
5207
|
+
"Name this module file with the exact frozen Module Index stem `{{item.stem}}` (filename `{{item.markdownPath}}`). Never reinterpret or rename an explicit-user-layout stem. Priority-only stems `p0`, `p1` and `p2` are forbidden and must never produce `p0.md` or `test_p0.py`. Pure hexadecimal/hash-like opaque stems such as `a401606` and `deadbeef` are also forbidden. Do not use Case-ID-like module filenames. For every automatable case, `自动化映射` must name exactly `{{item.pytestPath}}`, where the module stem is this Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `health` → `testcase/test_health.py`; `resource_notes` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
|
|
5208
|
+
"AUTOMATION_BINDING_FORMAT_V1 is a literal machine contract. Under every Case's `### 自动化映射`, write these independent lines exactly: `- 脚本:<path|无>`, `- primary symbol:<symbol|无>`, `- 变体测试点:<semicolon-separated TP IDs|无>`, `- 场景断言测试点:<semicolon-separated TP IDs|无>`, `- 横切证据测试点:<semicolon-separated TP IDs|无>`. TP IDs must be on the same line after the colon. Forbidden classification forms include `TP-X(变体测试点)`, `[变体测试点] TP-X`, `【变体测试点】:TP-X`, pipe-delimited annotations, tables, or nested TP lists. Before returning, verify that the Case `### 测试点` exact set equals the pairwise-disjoint union of the three canonical binding lines; do not add, remove, rename or duplicate a TP to make the format pass.",
|
|
5209
|
+
"Scenario Partition slots: requiredVariantSlots for this module are `{{item.requiredVariantSlots}}`. Every listed ID MUST appear in this module's Cases as exactly one variant Test Point in both `### 测试点` and `变体测试点`. Slot IDs copy the declared Partition ID exactly; never drop the HTTP method, invent, merge, renumber or split slot IDs. Ordinary alias Test Points do not satisfy a partition slot, and aggregate aliases such as `SINGLE`/`MULTIPLE` are forbidden. Scheme A: one Case may carry many slots; do not create one Case per enum value just to match Case count. Prefer ONE Case per partition with a parameter table over duplicated Cases per value. The not-in-set slot value must be a concrete literal absent from the Domain (e.g. `UNKNOWN_TYPE`) and its expected result must come from the bound source — when Expected by Slot is GAP, the Case states the expectation as GAP evidence, never a guessed 空列表/400. Never create cross-axis combination variants beyond the single documented nominal.",
|
|
4743
5210
|
"For every variant Test Point, write its machine-checkable `场景意图: <TP-ID>; operation=...; target=...; intent=...` line inside that same Case body/自动化映射. Never collect Scenario Intent lines in a file-level appendix, implementation-details block, or another Case; local TP ownership is mandatory.",
|
|
4744
5211
|
"Every Case must keep at least one numbered executable line under `### 操作步骤`; a compact variant/result table may follow but must not replace the numbered action anchor. Keep numbered/bulleted independently assertable results under `### 预期结果`. The exact `### 操作步骤` and `### 预期结果` headings must remain present for every Case, including compact/table-based Cases; never compress later Cases by dropping required headings. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.",
|
|
4745
5212
|
"In every `自动化映射`, use exactly these machine-readable list labels: `脚本`, `primary symbol`, `变体测试点`, `场景断言测试点`, `横切证据测试点`, plus a deterministic payload contract. For operations without a request body write `Payload Contract: none`. Otherwise write `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum` (write `none` when there is no enum); nested fields use dot paths such as `approver.name`. Each Case describes exactly one target request payload contract: put every payload label on its own list line, never concatenate multiple operations or setup POST/PUT contracts into one label line, and never repeat a `Payload Contract:` token inside explanatory prose/details after the machine-readable line. Values must come only from bound API/DTO evidence, never guesses. Each Test Point from `### 测试点` must appear in exactly one binding list, and every Test Point named in any binding list must also be declared in that Case's `### 测试点`; write `无` for an empty list. A variant Test Point is atomic: one exact endpoint/input/precondition/outcome row equals one exact pytest item and one exact TP ID. If a parameter table has five rows, declare five distinct variant TP IDs in Markdown; never declare one family TP and append row suffixes only in pytest. Classify as `variant` only when endpoint, request input, precondition business state, or expected outcome genuinely changes and therefore needs an independent pytest parameter item. Classify CRUD checkpoints, status/body/header/schema assertions and multiple checks over the same response/journey as `assertion`; classify shared HTTP logging/redaction/truncation evidence as `cross-cutting`. Never create a Test Point merely to parameterize a checkpoint. Every non-cross-cutting TP ID is owned by exactly one Case; when the same response/schema/error assertion is needed in different Cases, use distinct Case-specific TP IDs instead of reusing one assertion TP across Cases. Keep the script path identical to the module one-to-one path and declare exactly one primary symbol named with the canonical Case prefix, for example `BE-RN-003` → `test_BE_RN_003_<description>`; non-Case-prefixed primary symbols are forbidden because parameterized item association must remain deterministic. For evidence-only meta Cases with no executable business journey, write `脚本:无` and `primary symbol:无`, keep `变体测试点:无`, and place process evidence only in assertion/cross-cutting lists. If the bound contract only says an identifier is returned/present, do not declare a concrete identifier type. If a 404 Case needs a nonexistent path identifier but its syntax/type is unspecified, define a create-delete-derived valid identifier journey instead of an arbitrary UUID/text placeholder. For redaction scenarios, list sensitive header/field key names only. Never write any header-name-and-value pair, credential placeholder, fake token, anti-example, or other secret-shaped literal in Markdown; state only that a test-only value is supplied at runtime and omitted. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
|
|
@@ -4781,10 +5248,10 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4781
5248
|
"For every variant Test Point, ensure the Markdown scenario intent is machine-checkable and located inside that same Case body/自动化映射, never in a file-level appendix, implementation-details block, or another Case. Use an exact transport target: `场景意图: <TP-ID>; operation=<METHOD /path>; target=<body.field|query.field|path.field|header.field|request>; intent=<empty|missing|null|min-1|min|max|max+1|pattern-invalid|enum-invalid|wrong-type|nominal-operation|custom-literal:V>; bound=<n optional>; example=<optional>; expectedCode=<optional>`. Never use vague targets such as field=resource/health. Keep pytest params aligned to the exact target. For intent=missing/empty/default-omit, pytest may use `_OMIT` or delete the key; for intent=enum-invalid use a concrete invalid enum literal (for example `UNKNOWN_STATUS`), never `_OMIT`/missing-key; for trim/padded samples use `custom-literal:trim` or a real padded string, not a bare token like `filter-active` when the intent is `custom-literal:ACTIVE`.",
|
|
4782
5249
|
"Treat the requirement document as the coverage baseline; scope is limited to operations/rules it (or its referenced API contract) describes, and API contract evidence supplements scenario dimensions. For every in-scope operation, check applicable lifecycle/uniqueness states (including deleted-existing when in scope), valid enum values, bounded invalid classes, min-1/min/nominal/max/max+1, allowed/forbidden format classes, required/null/missing/wrong-type semantics, status/error codes, auth and state transitions. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same affected path can affect them; unresolved impact stays visible as GAP/CONFLICT. Directly add in-scope omissions; reject scope expansion to operations absent from the requirement document; undefined impact remains GAP/CONFLICT rather than invented behavior.",
|
|
4783
5250
|
"Check AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Require the exact `## Coverage Scope` Field/Value table with the `|---|---|` separator row, a valid classification-policy pair, non-empty Affected Operations/Rule Keys/Scope Evidence, and the classification-specific Regression Floor. Require the exact unnumbered `## Coverage Matrix` heading in the immutable run-owned plan artifact, exact headers, exactly 9 cells in every data row (including a non-empty Dimension), deterministic OpenAPI Rule Keys for every in-scope affected operation, exactly one Matrix row per Rule Key (merge multi-dimension product rows), and bidirectional Matrix Rule/Test Point ↔ Case bindings. Never describe affected-scope coverage as whole-API completeness. Every explicit AC ID must appear in at least one Case `验收标准`; every explicit in-scope AC/REQ/BR Rule Key cited by a Case must have exactly one Coverage Matrix row, and no Case may cite a source Rule Key omitted from the Matrix. Every Matrix Case ID must share at least one of that row's Required Test Points and the Case must cite that Rule Key. Perform an explicit execution-redundancy review: merge checkpoint-only parameter rows, repeated default/read-back assertions, DELETE status/body/follow-up-read checks, response schema/Content-Type checks, PUT full-update/timestamp checks, repeated list setup and identical null/empty inputs when endpoint, input partition, precondition state and expected outcome are the same. Preserve separate POST/PUT, boundary, enum, wrong-type, role/tenant and distinct business-state variants. Directly repair malformed headings/rows/keys and binding modes rather than merely commenting on them. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, a `### 操作步骤` section that contains only a table without any numbered executable line, vague results such as ‘符合预期’, Case-ID-like module filenames (for example `BE-HEALTH.md`), dropped exact `### 操作步骤`/`### 预期结果` headings, and missing or drifted script/function mapping where it can be derived.",
|
|
4784
|
-
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases, rename
|
|
5251
|
+
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases, preserve every frozen Module Index filename exactly (never rename an explicit-user-layout module; model-derived invalid stems must have been rejected before map expansion), normalize every Case ID to hyphen-separated module segments plus exactly three zero-padded digits (`BE-RESOURCE_NOTES-01` → `BE-RESOURCE-NOTES-001`; `BE-RN-011A` must be renumbered or merged) consistently across headings/index/mappings, fix automation mappings so each automatable case points at `testcase/test_<module>.py` derived from that module filename and declares exactly one primary symbol (evidence-only meta cases may keep `脚本/primary symbol=无` with empty variants), assign every Test Point exactly one of `变体测试点`/`场景断言测试点`/`横切证据测试点`, then perform an exact-set check: each Case's `### 测试点` set must equal (not merely contain) the union of those three binding lists; delete stale/legacy aliases and ensure every binding-list Test Point is present, expand every variant parameter row into its own atomic TP ID, make every non-cross-cutting TP Case-specific and owned by exactly one Case, require every primary symbol to start with the canonical Case prefix, ensure every explicit AC ID appears in an applicable Case `验收标准`, merge execution duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Remove every credential/header value, placeholder, fake token and anti-example from Markdown. Sensitive key names may remain only as a plain list; values must be described as runtime-only and omitted, with no colon/value pair or literal example anywhere, including details blocks and explanatory text. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations as exact machine-readable identifiers; only normalize Case ID separator/sequence formatting as specified above. Recalculate predicted collected items as `sum(max(1, variant count per Case))`; when the task declares a budget, directly merge redundant journeys/reclassify same-request checkpoints until the prediction is within budget, while preserving all required coverage. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
|
|
4785
5252
|
"This is the single Markdown incremental synchronization round. Read every authoritative reference index entry whose role hints include acceptance-criteria, api-contract, data-contract or business-rule; do not rely on the derived PRD as a complete inventory. Preserve every explicit AC/REQ/BR ID, every documented HTTP/business error code, every DTO/JSON field, enum value, boundary, format, nested shape, transaction/state/idempotency/uniqueness/auth/tenant/cross-field rule. For each natural-language normative business rule preserved as required scope, include its exact source sentence without paraphrase together with source path and line/heading anchor so the deterministic ledger can verify quote/hash provenance. Ensure every Case declares exactly `Payload Contract: none` or the three labels `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum`; every label must occupy its own machine-readable list line, and a Case must never concatenate target/setup operations or multiple `Payload Contract` tokens onto one line, and explanatory prose/details must not repeat any `Payload Contract:` token; never infer missing keys or enum values. A target GET/DELETE operation with no request body must remain `Payload Contract: none` even when its setup journey performs POST/PUT with a DTO; setup payloads never redefine the target Case payload contract. Add only missing Matrix rows/Test Points/Cases/assertions or repair exact drift; do not rewrite already-valid unrelated modules. Work gap-targeted: inspect source anchors and affected modules first, leave unrelated valid modules byte-stable, and return `already-satisfied` without restating the full suite when no gap exists.",
|
|
4786
5253
|
"For affected API fields, use one valid nominal payload plus atomic required/missing/null/empty/wrong-type, every documented enum value plus bounded invalid classes, documented min-1/min/nominal/max/max+1, formats and nested object/array constraints. Do not generate a Cartesian product or invent undocumented constraints. Do not invent a concrete identifier type when the source only requires presence; for a missing-resource 404 path with unspecified identifier syntax/type, synchronize the Case to a create-delete-derived valid identifier journey rather than an arbitrary UUID/text placeholder.",
|
|
4787
|
-
"Scenario Partitions synchronization: when the run-owned plan declares `## Scenario Partitions`, verify each declared partition's slots are fully materialized as variant Test Points with exact `TP-<Partition ID>-...` IDs (each-value per Domain value, OMITTED only for optional axes, exactly one NOT-IN-SET with intent=enum-invalid). Before returning, derive the complete exact slot set from every legal Scenario Partitions row and compare it with both the binding Coverage Matrix Rule's Required Test Points and the final Case `### 测试点`/`变体测试点` sets; directly add every missing exact slot to the already-assigned Case IDs; ordinary alias Test Points do not satisfy a partition slot, and aggregate aliases such as `SINGLE`/`MULTIPLE` are forbidden. Directly add missing slot rows/Cases. Record an illegal plan Partition row that has no source-backed finite domain as GAP/CONFLICT and remove only its derived `TP-SP-*` slots/Cases from target modules; never modify the immutable plan artifact. Never delete a legal source-backed partition or drop its complement slot to force coverage green. When the bound source does not document the complement expectation, keep the slot with GAP expected instead of guessing. Body-field validation enums (`TP-<FIELD>-ENUM-*`) are NOT partitions — do not add partition rows for them.",
|
|
5254
|
+
"Scenario Partitions synchronization: when the run-owned plan declares `## Scenario Partitions`, verify each declared partition's slots are fully materialized as variant Test Points with exact `TP-<Partition ID>-...` IDs (each-value per Domain value, OMITTED only for optional axes, exactly one NOT-IN-SET with intent=enum-invalid). Before returning, derive the complete exact slot set from every legal Scenario Partitions row and compare it with both the binding Coverage Matrix Rule's Required Test Points and the final Case `### 测试点`/`变体测试点` sets; directly add every missing exact slot to the already-assigned Case IDs; ordinary alias Test Points do not satisfy a partition slot, and aggregate aliases such as `SINGLE`/`MULTIPLE` are forbidden. Scheme A: keep existing Case structure and add missing exact variant Test Points to already-assigned Cases instead of creating one Case per enum value. Directly add missing slot rows/Cases. Record an illegal plan Partition row that has no source-backed finite domain as GAP/CONFLICT and remove only its derived `TP-SP-*` slots/Cases from target modules; never modify the immutable plan artifact. Never delete a legal source-backed partition or drop its complement slot to force coverage green. When the bound source does not document the complement expectation, keep the slot with GAP expected instead of guessing. Body-field validation enums (`TP-<FIELD>-ENUM-*`) are NOT partitions — do not add partition rows for them.",
|
|
4788
5255
|
"Before returning, verify that every explicit source AC/REQ/BR, error code and strong DTO field token appears in the run-owned plan or an applicable module Case. If a fact cannot be safely automated, retain it as GAP/CONFLICT with its exact source pointer instead of dropping it. Return already-satisfied only when no target file needs an incremental edit.",
|
|
4789
5256
|
"Read only indexed source paths. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.",
|
|
4790
5257
|
...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
|
|
@@ -4834,10 +5301,10 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4834
5301
|
writePolicy: "read-only",
|
|
4835
5302
|
allowedPaths: ro,
|
|
4836
5303
|
forbiddenPaths: forbidden,
|
|
4837
|
-
outputContract: "Stdout JSON {modules:[{stem,planReadPath}],planReadPath,planSha256} parsed from the run-owned Markdown plan artifact,
|
|
4838
|
-
subtask_prompt: "Parse only the run-owned generate-backend-md-plan-pi/plan.md artifact
|
|
5304
|
+
outputContract: "Stdout JSON {modules:[{stem,planReadPath}],planReadPath,planSha256,moduleLayout} parsed from the final strict-layout-validated run-owned Markdown plan artifact, matching the Markdown map manifest.",
|
|
5305
|
+
subtask_prompt: "Parse only the final run-owned generate-backend-md-plan-pi/plan.md artifact with the same strict module-layout and output-budget contract; do not search for or fall back to testcase/**/README.md. No project file writes.",
|
|
4839
5306
|
shell: {
|
|
4840
|
-
commands: [buildBackendTestModuleManifestShellCommand(layout)],
|
|
5307
|
+
commands: [buildBackendTestModuleManifestShellCommand(layout, taskConfig.backendTest?.moduleLayout)],
|
|
4841
5308
|
cwd: ".",
|
|
4842
5309
|
timeoutMs: 60000,
|
|
4843
5310
|
},
|
|
@@ -4873,7 +5340,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4873
5340
|
toolProfile: "write",
|
|
4874
5341
|
complexity: "MED",
|
|
4875
5342
|
writePolicy: "exclusive",
|
|
4876
|
-
allowedPaths: ["
|
|
5343
|
+
allowedPaths: ["{{item.pytestPath}}"],
|
|
4877
5344
|
forbiddenPaths: Array.from(new Set([
|
|
4878
5345
|
...forbidden,
|
|
4879
5346
|
"testcase/md/**",
|
|
@@ -4882,22 +5349,22 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4882
5349
|
"pyproject.toml",
|
|
4883
5350
|
"setup.cfg",
|
|
4884
5351
|
])),
|
|
4885
|
-
writeSet: ["
|
|
5352
|
+
writeSet: ["{{item.pytestPath}}"],
|
|
4886
5353
|
writerOutcomePolicy: {
|
|
4887
5354
|
type: "implementation-outcome-v1",
|
|
4888
5355
|
requireChangedFiles: true,
|
|
4889
5356
|
},
|
|
4890
5357
|
retryPolicy: BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY,
|
|
4891
|
-
outputContract: "Write exactly
|
|
5358
|
+
outputContract: "Write exactly the frozen pytest module file `{{item.pytestPath}}` whose actual test function region contains the exact Case ID, preferably in the function name or docstring. the frozen `{{item.markdownPath}}` maps one-to-one to `{{item.pytestPath}}`; never merge or split modules. No JSON and no pytest execution.",
|
|
4892
5359
|
subtaskPromptTemplate: [
|
|
4893
|
-
"Convert the single Markdown module
|
|
4894
|
-
"Output budget protocol (hard, max output <=16K per turn): Write exactly
|
|
5360
|
+
"Convert the single frozen Markdown module `{{item.markdownPath}}` into one self-contained pytest module. Before writing, also read the run-owned Markdown plan artifact at `{{item.planReadPath}}` and use its explicit API target/environment table as the authoritative fallback base URL for every module. A task/Markdown `API_BASE_URL` target takes precedence over project README dev-server URLs; never infer a backend API fallback from a frontend/Vite port such as localhost:3000. After reading the module Markdown, the run-owned plan artifact, and the bounded pytest config/conftest, immediately use write tools to create the single frozen file `{{item.pytestPath}}`. Define any bounded HTTP client fixture, request logging/redaction/truncation helper and payload builders needed by this module inside that same file; do not import generated testcase/**/helpers/** or testcase/**/factories/** assets. Do not end after analysis or planning. Do not modify Markdown, conftest, helpers/factories, or any other module's pytest script.",
|
|
5361
|
+
"Output budget protocol (hard, max output <=16K per turn): Write exactly the frozen `{{item.pytestPath}}`. Never paste full Python modules into assistant chat. Do not merge or split modules. Do not reduce params/assertions/skips to fit. If OUTPUT_LIMIT_RECOVERY is injected, continue only listed missing/broken scripts.",
|
|
4895
5362
|
"Align every variant pytest.param payload with the Markdown scenario intent (empty/missing/null/length/pattern/enum/wrong-type/nominal). Prefer literal payloads over Faker for intent-critical fields so pre-execution scenario-param checks can verify them. Hard contract: intent=enum-invalid MUST pass a concrete invalid value literal (string/number/boolean), never `_OMIT`/None/missing key; intent=missing/empty may use `_OMIT` or delete the key; intent=custom-literal:trim|whitespace-padded requires a leading/trailing whitespace string with non-empty trimmed content (all-whitespace belongs to empty/whitespace-only, not trim); intent=custom-literal:ACTIVE|ARCHIVED requires the exact enum string, never descriptive tokens like filter-active; intent=max/min/max+1 should pass a repeated-string length expression, a bare length number N, or a helper named _*_LEN{N} / _*_MAX_LENGTH / _*_OVER_LENGTH — never a bare 1 for oversize. Hard contract: request payload dicts may only contain DTO field keys from Payload Allowed Paths; never put expect/expected/echo_* helper keys inside the JSON body dict. Path/query/header identifiers and scenario-control metadata (including `id`, expected codes, and selector labels) must stay in separate pytest parameters and helper arguments; never merge them into a DTO patch or JSON body unless that exact path is allowed by the Markdown payload contract. Normalize the configured API base URL with `rstrip(\"/\")` (or equivalently join exactly one slash) before appending endpoint paths; generated requests must never contain a `//api/...` path. When the bound source documents a concrete non-secret local API URL, generated clients must use it as the fallback in `os.environ.get(\"API_BASE_URL\", \"<documented-url>\")`; do not require an otherwise-uninjected environment variable or fail setup solely because it is absent. Missing-field helpers must remove keys idempotently with `payload.pop(field, None)`, never `del payload[field]`, because optional fields may already be absent.",
|
|
4896
5363
|
"For every response contract that requires an object or pagination envelope, first assert that each envelope/data value is a dict and that required keys exist, then index fields and assert values. Never let an incidental KeyError or list/string TypeError stand in for the explicit response-shape contract failure.",
|
|
4897
|
-
'Ensure every automatable final Markdown Case ID in this module appears in exactly one primary pytest test function or pytest test class method region, using the exact `primary symbol` declared by Markdown. Skip evidence-only meta Cases that declare `脚本/primary symbol=无` with empty variants; do not invent a business pytest symbol for them. The symbol must start with `test_BE_<MODULE>_<NNN>_` so every parameterized collected item remains associated with its Case. Module-level functions and class-based pytest methods are both supported. Only `变体测试点` may use stable `pytest.param(..., id="TP-...")` IDs, and every atomic variant ID must appear exactly once with a genuine input/state/outcome change. Use a literal direct `pytest.param(..., id=...)` expression for every row; never hide or wrap it behind `_post_case`, `_put_case`, row-factory functions, comprehensions, generators, or dynamically returned parameter lists; do not use decorator-level `ids=[...]`, generated suffixes, or IDs that extend/shorten the exact Markdown TP. Do not parameterize `场景断言测试点` or `横切证据测试点`; execute all assertion checkpoints within the same business journey/item and use shared helpers for cross-cutting evidence. The primary symbol docstring must contain exact metadata lines `Case-ID: BE-...`, `Assertion-Test-Points: TP-...;TP-...` and `Cross-Cutting-Test-Points: TP-...;TP-...` (use `none` when empty). Implement request dictionaries so their direct and nested key paths and enum literals exactly satisfy the Case `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum`; for `Payload Contract: none`, do not invent a JSON/body DTO. GET/DELETE setup journeys may create resources, but their setup DTO must not change the target operation\'s no-body payload contract. No Test Point may be invented, renamed, omitted or bound in two modes. The generated pytest collection shape must equal the Markdown prediction `sum(max(1, variant count per Case))`; keep it at or below the task\'s explicit budget by removing duplicate execution, never by collapsing multiple parameter rows under a coarse family TP. Assertions come only from 预期结果 and setup comes only from 前置条件/测试数据/自动化映射.',
|
|
4898
|
-
"Name the generated pytest file so it corresponds one-to-one with its source Markdown module file: this module stem `{{item.stem}}` maps to exactly
|
|
5364
|
+
'Ensure every automatable final Markdown Case ID in this module appears in exactly one primary pytest test function or pytest test class method region, using the exact `primary symbol` declared by Markdown. Skip evidence-only meta Cases that declare `脚本/primary symbol=无` with empty variants; do not invent a business pytest symbol for them. The symbol must start with `test_BE_<MODULE>_<NNN>_` so every parameterized collected item remains associated with its Case. Module-level functions and class-based pytest methods are both supported. Only `变体测试点` may use stable `pytest.param(..., id="TP-...")` IDs, and every atomic variant ID must appear exactly once with a genuine input/state/outcome change. A Case with exactly one variant Test Point still needs one literal `pytest.param(..., id="TP-...")` row; never leave a single-variant Case as a bare function with the TP only in the docstring. Use a literal direct `pytest.param(..., id=...)` expression for every row; never hide or wrap it behind `_post_case`, `_put_case`, row-factory functions, comprehensions, generators, or dynamically returned parameter lists; do not use decorator-level `ids=[...]`, generated suffixes, or IDs that extend/shorten the exact Markdown TP. Do not parameterize `场景断言测试点` or `横切证据测试点`; execute all assertion checkpoints within the same business journey/item and use shared helpers for cross-cutting evidence. The primary symbol docstring must contain exact metadata lines `Case-ID: BE-...`, `Assertion-Test-Points: TP-...;TP-...` and `Cross-Cutting-Test-Points: TP-...;TP-...` (use `none` when empty). Implement request dictionaries so their direct and nested key paths and enum literals exactly satisfy the Case `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum`; for `Payload Contract: none`, do not invent a JSON/body DTO. GET/list filters still declare query fields in those payload labels when the Case varies `params=`/`query=` keys. Python `True`/`False` may implement JSON/OpenAPI `true`/`false` query or body booleans. GET/DELETE setup journeys may create resources, but their setup DTO must not change the target operation\'s no-body payload contract. No Test Point may be invented, renamed, omitted or bound in two modes. The generated pytest collection shape must equal the Markdown prediction `sum(max(1, variant count per Case))`; keep it at or below the task\'s explicit budget by removing duplicate execution, never by collapsing multiple parameter rows under a coarse family TP. Assertions come only from 预期结果 and setup comes only from 前置条件/测试数据/自动化映射.',
|
|
5365
|
+
"Name the generated pytest file so it corresponds one-to-one with its source Markdown module file: this module stem `{{item.stem}}` maps to exactly the frozen `{{item.pytestPath}}`. The <module> stem is the Markdown filename without the `.md` extension, lowercased and with non-alphanumeric characters replaced by underscores. For example, `resource_notes` → `testcase/test_resource_notes.py`, `health` → `testcase/test_health.py`. If Markdown automation mapping names a different path than this module stem path, still write the frozen manifest pytest path and do not invent prefixes. Never merge multiple Markdown modules into one pytest file, never split one module across several files, and never invent pytest filenames unrelated to the Markdown modules.",
|
|
4899
5366
|
"Scenario Partition slots: every `TP-<Partition ID>-...` variant Test Point declared by this module's Markdown MUST become exactly one literal direct `pytest.param(..., id=\"TP-<Partition ID>-...\")` row with the exact slot ID; the not-in-set slot passes a concrete literal absent from the documented Domain (e.g. `UNKNOWN_TYPE`) — never `_OMIT`, never a descriptive token. Never split one slot into multiple params or merge several slots under a family TP id. Slot filtering requests hit the documented list endpoint with the slot value as the query/path filter.",
|
|
4900
|
-
"Keep this module self-contained: define module-local fixtures and helpers directly in
|
|
5367
|
+
"Keep this module self-contained: define module-local fixtures and helpers directly in `{{item.pytestPath}}`, so pytest discovers every fixture dependency without external plugin registration. The request log must include method, URL/path, and request parameters (query plus JSON/body/payload summary). The response log must include status code and response result (JSON/text/body summary), and both records must be visible in pytest stdout/stderr without changing assertions. Recursively redact sensitive values and apply bounded truncation before logging.",
|
|
4901
5368
|
"Materialize every automatable Markdown Case exactly once as one canonical primary pytest symbol. Preserve every explicit variant Test Point as a stable pytest.param id and every assertion/cross-cutting binding as declared. Build request payloads from the effective Markdown test data literally: keep all declared DTO keys, nested shapes, enum values, missing/null/boundary variants and business-state preconditions; never substitute guessed convenience fields or rename contract fields. Never assert an identifier's concrete Python/JSON type unless the Markdown or bound contract explicitly declares that type; when only presence is required, accept any non-null scalar identifier and serialize it safely into the path. For a nonexistent-resource 404 Case whose identifier syntax/type is not declared, obtain a syntactically valid identifier from a live create response and delete it before the 404 request; never invent an arbitrary UUID/text identifier that may fail path conversion with 400. Respect every local helper's actual return signature: never tuple-unpack a scalar status/id/helper result, and never treat a tuple response as a scalar.",
|
|
4902
5369
|
"Do not read source/**, add cases, reassign ACs, modify conftest/config/production code, use skip/xfail, swallow assertions, execute pytest, or emit JSON. For best-effort cleanup, catch only the narrow transport exception actually raised by the selected HTTP client (for example `requests.RequestException` or `urllib.error.URLError`); never use bare `except`, `Exception`, or `BaseException` with `pass`.",
|
|
4903
5370
|
...(sharedSetupPytestPrompt ? [sharedSetupPytestPrompt] : []),
|