@tea-agent/loop-agent 0.39.0-next.9 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/CHANGELOG.md +262 -69
- package/README.md +11 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/dag/generate-task-dag.js +9 -0
- package/dist/application/task-lifecycle/advance.js +4 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +428 -27
- package/dist/commands/inspect-next.js +8 -0
- package/dist/commands/task-advance.js +31 -0
- package/dist/executors/dag-pi-executor.js +124 -18
- package/dist/executors/pi-event-serializer.js +42 -1
- package/dist/executors/pi-executor.js +51 -0
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +74 -28
- package/dist/executors/pi-sdk-executor.js +187 -59
- package/dist/executors/shell-executor.js +554 -33
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/dag-failure-category.js +138 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/operator/capabilities.js +181 -27
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +28 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/diff.js +26 -0
- package/dist/task/contract/project.js +5 -0
- package/dist/task/contract/schema.js +6 -1
- package/dist/task/source-prepare/build-draft.js +15 -0
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/deferred-turn.js +91 -0
- package/dist/worker/console/chat/mcp-inventory.js +221 -0
- package/dist/worker/console/chat/operation-card.js +8 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/pi-runtime.js +165 -83
- package/dist/worker/console/chat/repo-walk.js +10 -1
- package/dist/worker/console/chat/routes.js +480 -55
- package/dist/worker/console/chat/semantic-activity.js +20 -8
- package/dist/worker/console/chat/session-store.js +146 -2
- package/dist/worker/console/chat/turn-process.js +10 -10
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/inspect-split.js +18 -0
- package/dist/worker/console/operation-run-facts.js +19 -3
- package/dist/worker/console/operator-actions.js +195 -4
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/pi-readiness.js +5 -4
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/recovery-error-copy.js +4 -4
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +42 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BLXFe4qI.js → abnfDiagram-N423BO3Z-Br92EGb7.js} +1 -1
- package/dist/worker/console/static/assets/{arc-7iOJeqho.js → arc-oulImtQq.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CYoyj5K7.js → architectureDiagram-T3A2C74G-tyHc6aqO.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-fK01JNlR.js → blockDiagram-VBNYF7ZC-CY86BSpT.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-bzbw5D_Z.js → c4Diagram-5PPSVZJV-BKKsDmNs.js} +1 -1
- package/dist/worker/console/static/assets/channel-TY36zt-i.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Cf6hJFFG.js → chunk-2GRJ4B5K-CXZdjl3B.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DSFXR-et.js → chunk-2Q5K7J3B-B-aIFDQ5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BagJechl.js → chunk-5RXB4S5H-BDgAW_B2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-E7QNMFaW.js → chunk-5VM5RSS4-DABa4UPR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BG_eOBrC.js → chunk-6Q2QTUOP-aGbysVOx.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bo42dqol.js → chunk-GF5L2VYU-B8MSZDVs.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-dibM-87v.js → chunk-JWPE2WC7-CpelV73g.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Bytrp3Oe.js → chunk-KBJHAD2P-BE04ty3z.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DBABILrr.js → chunk-RYQCIY6F-BUxTkaaR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DCUk5Pf9.js → chunk-XXDRQBXY-CYYBVKfX.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BlGE9v6E.js → cose-bilkent-JH36ORCC-CEQF_xmQ.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DGl5ewph.js → cynefin-VYW2F7L2-CqpxN5IT.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DsJ_SVH_.js → cynefinDiagram-MW4NZA55-B3PzanVi.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BIhjFdzv.js → dagre-VZM6K2ZE-BSUGCNx1.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dmy1ctTb.js → diagram-7IWD3JNH-CdLevU5T.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DYCtWUF3.js → diagram-B4RE2ZJO-BjpF_Kok.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BMZFHVd2.js → diagram-LBJQPF4R-C4I6NJny.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-b_U8gfaO.js → diagram-Q27KOJAE-C8hecCr5.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dzy_bn-I.js → diagram-UB23O5K3-Daod6tOS.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DJCiMNEI.js → ebnfDiagram-BXEA7PRR-BvvmazGv.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-28lD016B.js → erDiagram-JOGREHBK-DeN_vfV4.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DGyHJmIw.js → flowDiagram-UKHOOZJN-D6dq1t9z.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CzdwUjrO.js → ganttDiagram-PKOTCBZU-DziIdNY4.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D9bGwuo8.js → gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js} +1 -1
- package/dist/worker/console/static/assets/index-C3GgHg-g.css +1 -0
- package/dist/worker/console/static/assets/index-Ci8ksvBm.js +406 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-LJgJUQFC.js → infoDiagram-6WML65LV-Czv5eYx2.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-98LaB8zb.js → ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-ISzwlQ.js → journeyDiagram-NVQOT4AX-DxGmNj5K.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-gfYOI-EL.js → kanban-definition-27J2QSJJ-CYCFqkO_.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CyntZgKV.js → linear-B0YzRwrd.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BBOR7dg0.js → mermaid.core-t3EZtIGh.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CQAgwPMk.js → mindmap-definition-FAOFIHXS-BTXpTv0T.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DkMiZFuV.js → pegDiagram-VL7TDLO6-DU8UkKqA.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DLNXa_0J.js → pieDiagram-7S7Q4E2Y-DP4y8ohp.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Dc7bdzjm.js → quadrantDiagram-CIZ2JOQS-B07vNaYR.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BxMB0QqW.js → railroadDiagram-AXF67PYL-xDWktb2i.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-FjumoKn4.js → requirementDiagram-LRYGKXZP-BLNMKqeS.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CkhKc_E3.js → sankeyDiagram-W5VNT64P-n0adalxH.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DoBWqTTH.js → sequenceDiagram-SI44F4Z6-BbnS-2DL.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CQeh_YeE.js → sizeCapture-X5ZJPWSS-B4uPpCLu.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-LuHmFW-I.js → stateDiagram-OKZ733FA--MMbB0fX.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CHwbDctY.js → swimlanes-SLNWSIFB-BXUgp2QY.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CoMngk0X.js → timeline-definition-Z64GVDOM-CsaMxki7.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CURYbtbO.js → vennDiagram-T6HMQDX7-C2IuG9iH.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BdqSECjU.js → wardleyDiagram-T6FBY63Y-CmEWOXCW.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DbMUYuAg.js → xychartDiagram-ELKLHX3M-D_9LE0MG.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
- package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
- package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +74 -26
- package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
- package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +54 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +237 -11
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
- package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
- package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
- package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
- package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/harness-task-lineage.js +5 -2
- package/dist/worker/observability/read-model.js +22 -0
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
- package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
- package/dist/workflows/dag/backend-test-intake-context.js +32 -0
- package/dist/workflows/dag/backend-test-layout.js +133 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
- package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +3 -1
- package/dist/workflows/dag/convergence/controller.js +141 -0
- package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/map.js +25 -9
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
- package/dist/workflows/dag/failure-category.js +7 -116
- package/dist/workflows/dag/frontend-implementation-contract.js +377 -4
- package/dist/workflows/dag/frontend-prewrite-gate.js +96 -84
- package/dist/workflows/dag/frontend-repair.js +14 -0
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
- package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
- package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
- package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
- package/dist/workflows/dag/frontend-test-html-report.js +3 -1
- package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
- package/dist/workflows/dag/frontend-test-layout.js +159 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
- package/dist/workflows/dag/frontend-verification-trace.js +29 -4
- package/dist/workflows/dag/init-hybrid.js +894 -205
- package/dist/workflows/dag/node-execution.js +183 -15
- package/dist/workflows/dag/prompt.js +60 -1
- package/dist/workflows/dag/rerun-feedback.js +212 -0
- package/dist/workflows/dag/rerun-plan.js +224 -27
- package/dist/workflows/dag/rerun-run.js +61 -6
- package/dist/workflows/dag/rerun-task.js +61 -2
- package/dist/workflows/dag/retry-policy.js +109 -0
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +143 -1
- package/dist/workflows/dag/validate.js +64 -20
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/operations/local-development-environment.md +1 -5
- package/docs/skills/vetted-skill-registry.md +14 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/backend-test-dag.json +60 -35
- package/docs/templates/frontend-test-case-checklist.md +2 -2
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +6 -4
- package/package.json +12 -4
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/analyze-product-requirements/SKILL.md +8 -5
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
- package/skills/analyze-product-requirements/references/example.md +50 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
- package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/skills/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/improve-codebase-architecture/deepening.md +37 -0
- package/skills/improve-codebase-architecture/html-report.md +123 -0
- package/skills/improve-codebase-architecture/interface-design.md +44 -0
- package/skills/improve-codebase-architecture/language.md +53 -0
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/playwright-cli/SKILL.md +23 -1
- package/skills/playwright-cli-case-generator/SKILL.md +29 -8
- package/dist/worker/console/static/assets/channel-BBOTJHvD.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cm8Yf8tD.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cm8Yf8tD.js +0 -1
- package/dist/worker/console/static/assets/index-B-qWGThd.js +0 -325
- package/dist/worker/console/static/assets/index-C-xRgY_0.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DZihfdKc.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CKnXCjHX.js +0 -8
|
@@ -5,12 +5,13 @@ import path from "node:path";
|
|
|
5
5
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
6
6
|
import { assertValidDagSpec } from "./validate.js";
|
|
7
7
|
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
|
+
import { bindDagRerunFeedback } from "./rerun-feedback.js";
|
|
8
9
|
import { planMavenVerification, } from "../../verification/maven/index.js";
|
|
9
10
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
10
11
|
import { extractTaskSourceOpenspecPaths } from "../../shared/openspec-spec.js";
|
|
11
12
|
import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
|
|
12
13
|
import { buildDecisionEnvelopePromptContract } from "./decision-envelope.js";
|
|
13
|
-
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
14
|
+
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY, isCanonicalFinalVerifyShellRetryCandidate, isSafeReadOnlyPiRetryCandidate, isTargetTemplateImplementPi, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
14
15
|
import { REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL, REVIEW_VERDICT_OUTPUT_PROTOCOL, } from "./output-protocol.js";
|
|
15
16
|
import { resolveAdapter } from "../../adapters/index.js";
|
|
16
17
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
@@ -20,19 +21,21 @@ import { applySddEmbeddedEnhancements, probeRepoLocalSddSkills, } from "./sdd-em
|
|
|
20
21
|
import { discoverProjectGovernancePresence } from "./project-governance-context.js";
|
|
21
22
|
import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
22
23
|
import { materializeTaskReferenceDocs } from "../../task/source-references.js";
|
|
23
|
-
import { REQUIREMENT_FACT_ROLES } from "../../task/source-prepare/artifact-meta.js";
|
|
24
24
|
import { observeTaskContract } from "../../task/contract/observe.js";
|
|
25
25
|
import { dagHasWriterExecution } from "./task-contract-binding.js";
|
|
26
26
|
import { DEFAULT_VERIFY_TIMEOUT_MS, resolveVerifyPreset, } from "../../executors/shell-verification.js";
|
|
27
27
|
import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
|
|
28
28
|
import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "./task-demand-routing.js";
|
|
29
29
|
import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPreflightShellSnippet, } from "./backend-test-execution-contract.js";
|
|
30
|
+
import { resolveBackendTestLayout, } from "./backend-test-layout.js";
|
|
31
|
+
import { applyFrontendTestLayoutToText, resolveFrontendTestLayout, } from "./frontend-test-layout.js";
|
|
30
32
|
import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-contract.js";
|
|
31
33
|
import { buildBackendTestIntakeContext } from "./backend-test-intake-context.js";
|
|
32
34
|
import { buildFrontendTestOutcomeGateShellSnippet } from "./frontend-test-result-contract.js";
|
|
33
35
|
import { classifyFrontendRisk, } from "./frontend-risk.js";
|
|
34
36
|
import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
|
|
35
|
-
import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
|
|
37
|
+
import { buildFrontendImplementationContractSkeleton, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
|
|
38
|
+
import { FRONTEND_NO_VERIFICATION_MARKER_TEXT } from "./frontend-verification-trace.js";
|
|
36
39
|
import { serializeDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
37
40
|
const REQUIREMENT_FILE = "需求.md";
|
|
38
41
|
const CONSTRAINT_FILE = "执行约束.md";
|
|
@@ -1280,11 +1283,15 @@ function deriveParallelScoutPaths(taskConfig) {
|
|
|
1280
1283
|
: allowed,
|
|
1281
1284
|
};
|
|
1282
1285
|
}
|
|
1286
|
+
const FRONTEND_NO_STATIC_VERIFICATION_MARKER = `node -e "console.log('${FRONTEND_NO_VERIFICATION_MARKER_TEXT}; static/behavior verification not-run')"`;
|
|
1283
1287
|
/**
|
|
1284
1288
|
* Resolve frontend verification fallbacks from the target project's own
|
|
1285
1289
|
* package scripts. The DAG builder is also used by unit fixtures without a
|
|
1286
|
-
*
|
|
1287
|
-
*
|
|
1290
|
+
* repoRoot, so those fixtures retain the historical generic fallback. A real
|
|
1291
|
+
* project (repoRoot set) without a readable package.json has no scripts to
|
|
1292
|
+
* invoke; the generic fallback would only inject commands guaranteed to fail
|
|
1293
|
+
* at verify time, so such projects fall through to the tsc probe and may end
|
|
1294
|
+
* up with no fallback commands plus a generation-time advisory.
|
|
1288
1295
|
*/
|
|
1289
1296
|
async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
1290
1297
|
const genericFallback = {
|
|
@@ -1301,10 +1308,8 @@ async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
|
1301
1308
|
}
|
|
1302
1309
|
}
|
|
1303
1310
|
catch {
|
|
1304
|
-
|
|
1311
|
+
// No readable manifest: leave scripts unset and keep probing.
|
|
1305
1312
|
}
|
|
1306
|
-
if (!scripts)
|
|
1307
|
-
return genericFallback;
|
|
1308
1313
|
let packageManager = "npm";
|
|
1309
1314
|
for (const [lockfile, manager] of [
|
|
1310
1315
|
["pnpm-lock.yaml", "pnpm"],
|
|
@@ -1381,19 +1386,66 @@ function toDagSourcePath(sources, absolutePath) {
|
|
|
1381
1386
|
absolutePath,
|
|
1382
1387
|
});
|
|
1383
1388
|
}
|
|
1384
|
-
|
|
1389
|
+
/**
|
|
1390
|
+
* Declaration-line shapes that assert a requirement id: list items
|
|
1391
|
+
* ("- AC-1: ..."), numbered/ordered acceptance entries ("1. AC-1 ...",
|
|
1392
|
+
* "1、AC-1"), table rows ("| AC-1 | ..."), and explicit key/value or
|
|
1393
|
+
* heading declarations ("AC-1:...", "id: AC-1"). Ids that merely appear
|
|
1394
|
+
* inside narrative prose (e.g. a fixture run id mentioned in background:
|
|
1395
|
+
* "夹具 run 2026-08-23-req-f03535aa 已存在") are incidental mentions, not
|
|
1396
|
+
* declared requirements, and must not pollute sourceBinding.requirementIds.
|
|
1397
|
+
*/
|
|
1398
|
+
const REQUIREMENT_ID_DECLARATION_LINE = /^\s*(?:[-+*]\s+|\d+[.、))]\s*|\|\s*)?(?:[-A-Z0-9]+\s*[::]\s*)?(?:\*\*)?(?:REQ|BR|AC)-/i;
|
|
1399
|
+
function isRequirementIdDeclarationLine(line) {
|
|
1400
|
+
return REQUIREMENT_ID_DECLARATION_LINE.test(line);
|
|
1401
|
+
}
|
|
1402
|
+
function extractExplicitRequirementIds(requirementMarkdown, ...fallbackMarkdown) {
|
|
1385
1403
|
const ids = [];
|
|
1386
1404
|
const seen = new Set();
|
|
1387
|
-
|
|
1405
|
+
const collect = (markdown, declarationsOnly) => {
|
|
1388
1406
|
if (!markdown)
|
|
1389
|
-
|
|
1390
|
-
|
|
1407
|
+
return;
|
|
1408
|
+
const source = declarationsOnly
|
|
1409
|
+
? markdown
|
|
1410
|
+
.split(/\r?\n/)
|
|
1411
|
+
.filter((line) => isRequirementIdDeclarationLine(line))
|
|
1412
|
+
.join("\n")
|
|
1413
|
+
: markdown;
|
|
1414
|
+
for (const match of source.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
|
|
1391
1415
|
const id = match[0].toUpperCase();
|
|
1392
1416
|
if (!seen.has(id)) {
|
|
1393
1417
|
seen.add(id);
|
|
1394
1418
|
ids.push(id);
|
|
1395
1419
|
}
|
|
1396
1420
|
}
|
|
1421
|
+
};
|
|
1422
|
+
// The requirement document itself is human-authored acceptance prose:
|
|
1423
|
+
// only declaration lines declare requirements there. If the document has
|
|
1424
|
+
// no declaration-shaped lines at all (minimal free-form tasks like
|
|
1425
|
+
// "covers AC-001"), fall back to its full text so bindings never go
|
|
1426
|
+
// missing for unstructured input. References and constraints are bound
|
|
1427
|
+
// documents (acceptance yaml, analysis docs) whose ids are authoritative
|
|
1428
|
+
// wherever they appear — keep full scan for them.
|
|
1429
|
+
const declarationLines = requirementMarkdown
|
|
1430
|
+
.split(/\r?\n/)
|
|
1431
|
+
.filter((line) => isRequirementIdDeclarationLine(line));
|
|
1432
|
+
collect(requirementMarkdown, declarationLines.length > 0);
|
|
1433
|
+
for (const markdown of fallbackMarkdown) {
|
|
1434
|
+
if (markdown)
|
|
1435
|
+
collect(markdown, false);
|
|
1436
|
+
}
|
|
1437
|
+
return ids;
|
|
1438
|
+
}
|
|
1439
|
+
/** Extract ids from an explicit authoritative section (full scan inside it). */
|
|
1440
|
+
function extractSectionRequirementIds(sectionMarkdown) {
|
|
1441
|
+
const ids = [];
|
|
1442
|
+
const seen = new Set();
|
|
1443
|
+
for (const match of sectionMarkdown.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
|
|
1444
|
+
const id = match[0].toUpperCase();
|
|
1445
|
+
if (!seen.has(id)) {
|
|
1446
|
+
seen.add(id);
|
|
1447
|
+
ids.push(id);
|
|
1448
|
+
}
|
|
1397
1449
|
}
|
|
1398
1450
|
return ids;
|
|
1399
1451
|
}
|
|
@@ -1407,7 +1459,7 @@ export function extractTaskScopedRequirementIds(requirementMarkdown, ...fallback
|
|
|
1407
1459
|
// Without it, preserve legacy full-scan behavior (requirement + references).
|
|
1408
1460
|
const sectionMatch = requirementMarkdown.match(/(?:^|\n)##\s*Acceptance References\s*\n([\s\S]*?)(?=\n##\s+|\n#\s+|$)/i);
|
|
1409
1461
|
if (sectionMatch?.[1]) {
|
|
1410
|
-
const fromSection =
|
|
1462
|
+
const fromSection = extractSectionRequirementIds(sectionMatch[1]);
|
|
1411
1463
|
if (fromSection.length > 0)
|
|
1412
1464
|
return fromSection;
|
|
1413
1465
|
}
|
|
@@ -1500,27 +1552,11 @@ function buildSourceContextBlock(sources) {
|
|
|
1500
1552
|
.relative(path.join(sources.taskDir, "source"), reference.path)
|
|
1501
1553
|
.replaceAll(path.sep, "/");
|
|
1502
1554
|
const referenceRef = toDagSourcePath(sources, reference.path);
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
const referenceExcerpt = isFactRole
|
|
1509
|
-
? {
|
|
1510
|
-
text: reference.markdown.trim(),
|
|
1511
|
-
truncated: false,
|
|
1512
|
-
originalChars: reference.markdown.trim().length,
|
|
1513
|
-
maxChars: Number.POSITIVE_INFINITY,
|
|
1514
|
-
}
|
|
1515
|
-
: excerptMarkdown(reference.markdown, {
|
|
1516
|
-
sourceRef: referenceRef,
|
|
1517
|
-
});
|
|
1518
|
-
boundReadPaths.push(`- reference ${relativePath}${isFactRole ? " (full source)" : ""}: ${referenceRef}`);
|
|
1519
|
-
parts.push(`## Task source reference: ${relativePath}`, `Bound readPath (use for Pi read-tool calls): ${referenceRef}`, ...(isFactRole
|
|
1520
|
-
? [
|
|
1521
|
-
`Full source injected (role: ${reference.role}) — complete and authoritative; no excerpt truncation applied.`,
|
|
1522
|
-
]
|
|
1523
|
-
: []), referenceExcerpt.text);
|
|
1555
|
+
const referenceExcerpt = excerptMarkdown(reference.markdown, {
|
|
1556
|
+
sourceRef: referenceRef,
|
|
1557
|
+
});
|
|
1558
|
+
boundReadPaths.push(`- reference ${relativePath}: ${referenceRef}`);
|
|
1559
|
+
parts.push(`## Task source reference: ${relativePath}`, `Bound readPath (use for Pi read-tool calls): ${referenceRef}`, referenceExcerpt.text);
|
|
1524
1560
|
}
|
|
1525
1561
|
parts.push("## Bound source read paths", ...boundReadPaths, "Use these repository-readable paths for any Pi read-tool calls. Bound files under `.harness/tasks/<taskId>/source/**` are read-only inputs: reading them is allowed even though writing `.harness/**` is forbidden.", "Never resolve task-relative citations such as `source/需求.md` or `source/references/*` against the repository root, invent `source/<taskId>/...`, search for substitutes, or fall back to `docs/**` when a bound read fails.", "## Task config summary", `- taskId: ${sources.taskConfig.taskId}`, `- flow: ${sources.taskConfig.flow}`, `- complexity: ${sources.taskConfig.complexity}`, `- contextProfile: ${sources.taskConfig.contextProfile}`, `- allowedPaths: ${sources.taskConfig.allowedPaths.join(", ") || "(none — review before execute)"}`, `- forbiddenPaths: ${sources.taskConfig.forbiddenPaths.join(", ") || "(none)"}`, '- Pi DAG nodes are read-only unless toolProfile="write" is explicitly selected for a bounded writer node.', "- Agent DAG read-only nodes must not write root artifacts/**; root artifacts/ is not a per-node scratchpad.", `- Derived execution contract and immutable references live under the Bound source read paths above (not as repo-root \`source/...\`).`);
|
|
1526
1562
|
if (sources.taskConfig.hardConstraints.length > 0) {
|
|
@@ -1558,49 +1594,10 @@ async function loadMaterializedSourceReferences(sourceDir) {
|
|
|
1558
1594
|
}
|
|
1559
1595
|
await collect(referenceDir);
|
|
1560
1596
|
referencePaths.sort((left, right) => left.localeCompare(right));
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
.replaceAll(path.sep, "/");
|
|
1566
|
-
const role = roleByPath.get(relative);
|
|
1567
|
-
return {
|
|
1568
|
-
path: filePath,
|
|
1569
|
-
markdown: await readFile(filePath, "utf-8"),
|
|
1570
|
-
...(role !== undefined ? { role } : {}),
|
|
1571
|
-
};
|
|
1572
|
-
}));
|
|
1573
|
-
}
|
|
1574
|
-
/**
|
|
1575
|
-
* Reads `source-manifest.json` into a materializedPath → role map so the DAG
|
|
1576
|
-
* generator can tell fact-source references (requirement/acceptance) apart
|
|
1577
|
-
* from archival ones (analysis/clarification/design). A missing or malformed
|
|
1578
|
-
* manifest yields an empty map; such references keep the bounded-excerpt
|
|
1579
|
-
* treatment instead of being injected in full.
|
|
1580
|
-
*/
|
|
1581
|
-
async function loadReferenceRoles(sourceDir) {
|
|
1582
|
-
const roleByPath = new Map();
|
|
1583
|
-
const manifestPath = path.join(sourceDir, "source-manifest.json");
|
|
1584
|
-
let raw;
|
|
1585
|
-
try {
|
|
1586
|
-
raw = await readFile(manifestPath, "utf-8");
|
|
1587
|
-
}
|
|
1588
|
-
catch {
|
|
1589
|
-
return roleByPath;
|
|
1590
|
-
}
|
|
1591
|
-
try {
|
|
1592
|
-
const manifest = JSON.parse(raw);
|
|
1593
|
-
for (const document of manifest.documents ?? []) {
|
|
1594
|
-
if (typeof document.materializedPath === "string" &&
|
|
1595
|
-
typeof document.role === "string") {
|
|
1596
|
-
roleByPath.set(document.materializedPath.replaceAll(path.sep, "/"), document.role);
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
catch {
|
|
1601
|
-
// A malformed manifest must never break reference injection.
|
|
1602
|
-
}
|
|
1603
|
-
return roleByPath;
|
|
1597
|
+
return Promise.all(referencePaths.map(async (filePath) => ({
|
|
1598
|
+
path: filePath,
|
|
1599
|
+
markdown: await readFile(filePath, "utf-8"),
|
|
1600
|
+
})));
|
|
1604
1601
|
}
|
|
1605
1602
|
export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
1606
1603
|
const paths = getTaskPaths(repoRoot, taskId);
|
|
@@ -1773,6 +1770,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1773
1770
|
forbiddenPaths,
|
|
1774
1771
|
outputContract: "Archived final shell verification stdout/stderr with exit codes; no worktree writes.",
|
|
1775
1772
|
subtask_prompt: "Run the adapter-resolved final verification commands before read-only verification review.",
|
|
1773
|
+
transientRetryProfile: TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE,
|
|
1776
1774
|
shell: {
|
|
1777
1775
|
commands: verifyShellCommands,
|
|
1778
1776
|
verifyEvidence: buildVerifyEvidence({
|
|
@@ -1893,6 +1891,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1893
1891
|
allowedPaths: implementPaths.allowedPaths,
|
|
1894
1892
|
forbiddenPaths,
|
|
1895
1893
|
writerOutcomePolicy: { type: "implementation-outcome-v1" },
|
|
1894
|
+
transientRetryProfile: TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE,
|
|
1896
1895
|
subtask_prompt: [
|
|
1897
1896
|
"Implement the approved plan with minimal focused changes.",
|
|
1898
1897
|
"Stay within writeSet. Do not write root artifacts/** unless artifacts paths are explicitly declared in writeSet.",
|
|
@@ -1952,6 +1951,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1952
1951
|
],
|
|
1953
1952
|
};
|
|
1954
1953
|
applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
|
|
1954
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
1955
1955
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
1956
1956
|
parseDagSpec(spec);
|
|
1957
1957
|
assertValidDagSpec(spec);
|
|
@@ -2417,33 +2417,21 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2417
2417
|
complexity: taskConfig.complexity,
|
|
2418
2418
|
});
|
|
2419
2419
|
const frontendSourceBinding = buildDagSourceBinding(sources);
|
|
2420
|
+
const frontendContractSkeleton = buildFrontendImplementationContractSkeleton({
|
|
2421
|
+
sourceBinding: frontendSourceBinding,
|
|
2422
|
+
riskLevel: frontendRisk.selectedRisk,
|
|
2423
|
+
targetFiles: implementPaths.writeSet,
|
|
2424
|
+
});
|
|
2420
2425
|
const frontendContractSchemaBlock = (() => {
|
|
2421
2426
|
const schema = loadFrontendImplementationContractJsonSchema();
|
|
2422
|
-
const requirement = frontendSourceBinding.sources.find((source) => source.kind === "requirement");
|
|
2423
|
-
if (!requirement) {
|
|
2424
|
-
throw new Error("frontend implementation contract context requires a bound requirement source");
|
|
2425
|
-
}
|
|
2426
|
-
const referencePaths = frontendSourceBinding.sources
|
|
2427
|
-
.filter((s) => s.kind === "reference")
|
|
2428
|
-
.map((s) => s.path);
|
|
2429
|
-
const fixedFields = {
|
|
2430
|
-
schemaVersion: 1,
|
|
2431
|
-
sourceBinding: {
|
|
2432
|
-
taskId: frontendSourceBinding.taskId,
|
|
2433
|
-
requirementPath: requirement.path,
|
|
2434
|
-
requirementSha256: requirement.sha256,
|
|
2435
|
-
referencePaths,
|
|
2436
|
-
requirementIds: frontendSourceBinding.requirementIds,
|
|
2437
|
-
},
|
|
2438
|
-
riskLevel: frontendRisk.selectedRisk,
|
|
2439
|
-
targets: { files: implementPaths.writeSet },
|
|
2440
|
-
};
|
|
2441
2427
|
return [
|
|
2442
|
-
`## ${FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID} JSON Schema (authoritative
|
|
2428
|
+
`## Final ${FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID} JSON Schema (authoritative after runtime merge)`,
|
|
2443
2429
|
schema,
|
|
2444
2430
|
"",
|
|
2445
|
-
"##
|
|
2446
|
-
JSON.stringify(
|
|
2431
|
+
"## Runtime contract skeleton (deterministic and protected)",
|
|
2432
|
+
JSON.stringify(frontendContractSkeleton),
|
|
2433
|
+
"",
|
|
2434
|
+
"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.",
|
|
2447
2435
|
"",
|
|
2448
2436
|
"## Forbidden fields (these are NOT in the schema; do not emit)",
|
|
2449
2437
|
"- schemaId",
|
|
@@ -2560,11 +2548,6 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2560
2548
|
`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.`,
|
|
2561
2549
|
`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.`,
|
|
2562
2550
|
].join("\n");
|
|
2563
|
-
const mandatorySourceReadInstruction = [
|
|
2564
|
-
"## Mandatory full source read before contracting",
|
|
2565
|
-
"Before producing this contract/plan, use the Pi read tool to read the FULL bound source files (需求.md, 执行约束.md, and every `references/*` Bound readPath listed above) — the inline copies above may be truncated excerpts, and requirement/acceptance references are authoritative only in their full form.",
|
|
2566
|
-
"Do not drop scope fields, acceptance criteria, non-goals, UI states, or column/field definitions that exist in the full sources but are absent from the inline excerpts; if a field appears in the full source, it belongs in the contract.",
|
|
2567
|
-
].join("\n");
|
|
2568
2551
|
const strategy = resolveDagVerifyStrategy(taskConfig);
|
|
2569
2552
|
const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
|
|
2570
2553
|
const behaviorPaths = deriveFrontendBehaviorPaths(taskConfig);
|
|
@@ -2598,7 +2581,14 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2598
2581
|
return buildBlockedFrontendMockDag(frontendSources, readOnlyPaths, forbiddenPaths, globalConstraints, blockedReason);
|
|
2599
2582
|
}
|
|
2600
2583
|
const fallbackVerifyCommands = await discoverFrontendFallbackVerifyCommands(sources.repoRoot);
|
|
2601
|
-
|
|
2584
|
+
// The verification bundle schema requires at least one static command. When
|
|
2585
|
+
// a real project exposes no usable verification command at all, run an
|
|
2586
|
+
// explicit no-op marker instead of a command that is guaranteed to fail:
|
|
2587
|
+
// the trace then records not-run honestly and the advisory asks the task
|
|
2588
|
+
// to declare verification commands and regenerate.
|
|
2589
|
+
const staticFallbackCommands = fallbackVerifyCommands.staticCommands.length > 0
|
|
2590
|
+
? fallbackVerifyCommands.staticCommands
|
|
2591
|
+
: [FRONTEND_NO_STATIC_VERIFICATION_MARKER];
|
|
2602
2592
|
const behaviorFallbackCommands = fallbackVerifyCommands.behaviorCommands;
|
|
2603
2593
|
const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
|
|
2604
2594
|
repoRoot: sources.repoRoot,
|
|
@@ -2697,6 +2687,11 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2697
2687
|
...behaviorVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
|
|
2698
2688
|
].join("\n");
|
|
2699
2689
|
const advisories = [];
|
|
2690
|
+
if (!hasDeclaredFrontendVerification &&
|
|
2691
|
+
fallbackVerifyCommands.staticCommands.length === 0 &&
|
|
2692
|
+
fallbackVerifyCommands.behaviorCommands.length === 0) {
|
|
2693
|
+
advisories.push("未发现可用的前端验证命令:目标项目没有可读取的 package.json scripts,也未探测到本地 TypeScript,verify 节点将没有静态/行为命令可执行。请在 task.json --verify 或执行约束.md 的验证约束中显式声明命令(例如 node --check src/app.js),然后重新生成 DAG。");
|
|
2694
|
+
}
|
|
2700
2695
|
if (frontendSourceMentionsMock(frontendSources) &&
|
|
2701
2696
|
frontendMockStrategyMustBeNotNeeded(frontendSources)) {
|
|
2702
2697
|
advisories.push("auto 模式已将 Mock 策略收窄为 not-needed:任务源提到接口/API/Mock 需求,但仓库无确认 Mock 能力或无确定性 Mock 验证命令。若项目规范要求 Mock,请声明 frontendMock.verifyCommands 或 policy:required 后重新生成 DAG。");
|
|
@@ -2761,7 +2756,6 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2761
2756
|
"Read task source and produce a concise frontend implementation contract.",
|
|
2762
2757
|
"Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations.",
|
|
2763
2758
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
2764
|
-
mandatorySourceReadInstruction,
|
|
2765
2759
|
sourceContext,
|
|
2766
2760
|
].join("\n\n"),
|
|
2767
2761
|
},
|
|
@@ -2795,26 +2789,27 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2795
2789
|
outputMode: "structured-required",
|
|
2796
2790
|
retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
|
|
2797
2791
|
structuredContractOutput: {
|
|
2798
|
-
schemaId:
|
|
2792
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID,
|
|
2799
2793
|
retryOnInvalid: true,
|
|
2794
|
+
skeleton: frontendContractSkeleton,
|
|
2800
2795
|
},
|
|
2801
2796
|
allowedPaths: readOnlyPaths,
|
|
2802
2797
|
forbiddenPaths,
|
|
2803
2798
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2804
|
-
outputContract: "
|
|
2799
|
+
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.",
|
|
2805
2800
|
subtask_prompt: [
|
|
2806
|
-
"
|
|
2807
|
-
"
|
|
2801
|
+
"Use frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence to fill the runtime-owned frontend contract skeleton. Return JSON-only output containing only an editable RFC 7386 plan patch. The runtime already owns schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff; omit those protected paths even when their values look obvious.",
|
|
2802
|
+
"The patch fields become the complete implementation plan after deterministic merge. Do not produce a separate plan document, prose mirror, or full contract.",
|
|
2803
|
+
"Select the Mock / API strategy only in the patch. Encode endpoint/fixture mapping, explicit activation, verification commands, and Real Integration Gap in schema-defined editable fields; productionDefaultOff comes from the protected skeleton and there is no second plan output.",
|
|
2808
2804
|
"Encode ordered steps (implementationSteps), target files, UI state handling, styling/component strategy (stylingStrategy), interaction notes, Mock/API strategy, dependency policy (dependencyPolicy), deterministic verification entrypoints, Real Integration Gap (realIntegrationGap), and residual risks (residualRisks) into the contract JSON fields. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
|
|
2809
2805
|
"Every target file and verification target must be selected from the current target workspace and task scope. Do not reuse paths or symbols from examples, prior tasks, or loop-agent itself; if the project uses app/, packages/, spec/, __tests__, or another layout, preserve that layout.",
|
|
2810
2806
|
"Consume the Scout TARGET_SURFACE evidence before selecting files. Preserve the discovered existing entrypoint and data source. If implementationPaths or testPaths are outside task allowedPaths, record a blocking scope conflict; do not substitute a new page or silently broaden the writeSet.",
|
|
2811
|
-
"Output in this exact order: (1) exactly one fenced json object
|
|
2807
|
+
"Output in this exact order: (1) exactly one fenced json object containing the editable plan patch; (2) exactly one openspec-citations citation fenced block appended immediately after it. Do NOT emit protected skeleton fields, a full contract, Markdown plan explanation, raw JSON, or any other fenced block.",
|
|
2812
2808
|
"Each requirement must state its user-observable or logic-observable expectedOutcome. Each interaction must state its trigger and expectedBehavior. IDs plus file paths are not sufficient behavior semantics.",
|
|
2813
2809
|
requirementCoverageInstruction,
|
|
2814
2810
|
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2815
2811
|
verificationTargetFileInstruction,
|
|
2816
2812
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
2817
|
-
mandatorySourceReadInstruction,
|
|
2818
2813
|
fixedVerificationContext,
|
|
2819
2814
|
sourceContext,
|
|
2820
2815
|
mockContextBlock,
|
|
@@ -2836,7 +2831,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2836
2831
|
outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
|
|
2837
2832
|
outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings, Required Plan Corrections, and Checked Items. No file writes.",
|
|
2838
2833
|
subtask_prompt: [
|
|
2839
|
-
"Audit the frontend plan before implementation.
|
|
2834
|
+
"Audit the frontend plan before implementation. frontend-plan-pi is emitted as a hash-bound canonical JSON artifact after the runtime applied and validated the planner's editable patch against its protected skeleton; read that artifact with the read tool and do not infer the contract from stdout. There is no separate plan prose.",
|
|
2840
2835
|
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
2841
2836
|
"Request revision 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.",
|
|
2842
2837
|
"Also request revision 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.",
|
|
@@ -2859,21 +2854,21 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2859
2854
|
outputMode: "structured-required",
|
|
2860
2855
|
retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
|
|
2861
2856
|
structuredContractOutput: {
|
|
2862
|
-
schemaId: "frontend-implementation-contract-v1",
|
|
2857
|
+
schemaId: "frontend-implementation-contract-revision-patch-v1",
|
|
2863
2858
|
retryOnInvalid: true,
|
|
2864
2859
|
},
|
|
2865
2860
|
allowedPaths: readOnlyPaths,
|
|
2866
2861
|
forbiddenPaths,
|
|
2867
2862
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2868
|
-
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;
|
|
2863
|
+
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.",
|
|
2869
2864
|
subtask_prompt: [
|
|
2870
2865
|
"Consume frontend-plan-pi (original contract JSON) and frontend-design-review-pi (first design review findings).",
|
|
2871
2866
|
"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.",
|
|
2872
|
-
"The patch delta may update
|
|
2867
|
+
"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.",
|
|
2873
2868
|
requirementCoverageInstruction,
|
|
2874
2869
|
"Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
|
|
2875
2870
|
"Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
|
|
2876
|
-
"Output in this exact order: (1) exactly one fenced json object containing the merge-patch delta — this
|
|
2871
|
+
"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.",
|
|
2877
2872
|
"Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the effective (merged) contract; do not reduce behavior semantics to IDs and paths.",
|
|
2878
2873
|
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2879
2874
|
verificationTargetFileInstruction,
|
|
@@ -3592,7 +3587,7 @@ function buildExecuteBackendPytestNode(sources, options = {}) {
|
|
|
3592
3587
|
// the current DAG run and moves with active → completed/paused lifecycle.
|
|
3593
3588
|
// Adapter default testRoot is frozen at DAG generation time (auditable) and
|
|
3594
3589
|
// cross-checked against the materialized execution contract in preflight.
|
|
3595
|
-
const frozenTestRoot = BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT;
|
|
3590
|
+
const frozenTestRoot = options.testRoot ?? BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT;
|
|
3596
3591
|
const preflightCommand = buildBackendTestExecutionPreflightShellSnippet({
|
|
3597
3592
|
expectedTestRoot: frozenTestRoot,
|
|
3598
3593
|
});
|
|
@@ -3794,6 +3789,45 @@ const BACKEND_TEST_DEFAULTS = {
|
|
|
3794
3789
|
...HYBRID_DEFAULTS,
|
|
3795
3790
|
writePolicy: "read-only",
|
|
3796
3791
|
};
|
|
3792
|
+
/**
|
|
3793
|
+
* Runtime layout rewriter for backend-test node prompts and contracts (plan A).
|
|
3794
|
+
*
|
|
3795
|
+
* Node prompts and output contracts historically hardcode the default
|
|
3796
|
+
* `testcase/` layout. Instead of duplicating every prompt string as a
|
|
3797
|
+
* template, builders keep the historical literal text and this helper
|
|
3798
|
+
* rewrites it against the resolved layout at DAG compile time. The default
|
|
3799
|
+
* layout maps every token to itself, so the packaged template stays
|
|
3800
|
+
* byte-identical with the historical contract.
|
|
3801
|
+
*/
|
|
3802
|
+
export function applyBackendTestLayoutToText(text, layout) {
|
|
3803
|
+
if (layout.isDefault)
|
|
3804
|
+
return text;
|
|
3805
|
+
const replacements = [
|
|
3806
|
+
["testcase/test_", `${layout.scriptDir}/test_`],
|
|
3807
|
+
["testcase/md/", `${layout.markdownDir}/`],
|
|
3808
|
+
["testcase/**", `${layout.testRoot}/**`],
|
|
3809
|
+
["testcase/", `${layout.testRoot}/`],
|
|
3810
|
+
];
|
|
3811
|
+
replacements.sort((a, b) => b[0].length - a[0].length);
|
|
3812
|
+
let output = "";
|
|
3813
|
+
const resolvedPrefix = `${layout.testRoot}/`;
|
|
3814
|
+
for (let i = 0; i < text.length;) {
|
|
3815
|
+
if (text.startsWith(resolvedPrefix, i)) {
|
|
3816
|
+
output += resolvedPrefix;
|
|
3817
|
+
i += resolvedPrefix.length;
|
|
3818
|
+
continue;
|
|
3819
|
+
}
|
|
3820
|
+
const hit = replacements.find(([token]) => text.startsWith(token, i));
|
|
3821
|
+
if (hit) {
|
|
3822
|
+
output += hit[1];
|
|
3823
|
+
i += hit[0].length;
|
|
3824
|
+
continue;
|
|
3825
|
+
}
|
|
3826
|
+
output += text[i];
|
|
3827
|
+
i += 1;
|
|
3828
|
+
}
|
|
3829
|
+
return output;
|
|
3830
|
+
}
|
|
3797
3831
|
/**
|
|
3798
3832
|
* Builds the `node -e` command for the backend-test module manifest shell.
|
|
3799
3833
|
* The inline JS mirrors `extractModuleStemsFromReadme` so the map_agent
|
|
@@ -3804,34 +3838,50 @@ const BACKEND_TEST_DEFAULTS = {
|
|
|
3804
3838
|
* normalization. Output is exactly one trailing JSON line `{modules:[{stem}]}`
|
|
3805
3839
|
* that `parseJsonFromText` accepts after shell command echoes.
|
|
3806
3840
|
*/
|
|
3807
|
-
function buildBackendTestModuleManifestShellCommand() {
|
|
3841
|
+
function buildBackendTestModuleManifestShellCommand(layout) {
|
|
3808
3842
|
// The extractor is base64-encoded so the shell command is fully opaque to
|
|
3809
3843
|
// bash: no backticks (command substitution), no regex \/ escaping, no
|
|
3810
3844
|
// backslash-counting through TS-string -> JSON.stringify -> bash -c -> node -e.
|
|
3811
3845
|
// Backticks in the README body are stripped at runtime via
|
|
3812
3846
|
// String.fromCharCode(96), so the extractor source contains no backtick.
|
|
3847
|
+
// mdDir/testPrefix are injected as JSON literals so the same extractor
|
|
3848
|
+
// works for any configured backendTest layout (plan A).
|
|
3849
|
+
const mdDirLiteral = JSON.stringify(layout.markdownDir);
|
|
3850
|
+
const testPrefixLiteral = JSON.stringify(`${layout.scriptDir}/test_`);
|
|
3851
|
+
const escOpen = String.fromCharCode(92, 91); // \[
|
|
3852
|
+
const escClose = String.fromCharCode(92, 93); // \]
|
|
3853
|
+
const escBslash = String.fromCharCode(92, 92); // \\
|
|
3813
3854
|
const script = `const fs=require('fs');
|
|
3814
|
-
const
|
|
3855
|
+
const mdDir=${mdDirLiteral};
|
|
3856
|
+
const testPrefix=${testPrefixLiteral};
|
|
3857
|
+
const esc=s=>s.replace(/[${escOpen}${escClose}{}()*+?^$|${escBslash}]/g,'${escBslash}$&');
|
|
3858
|
+
const rxMdPath=new RegExp(esc(mdDir)+'${escBslash}/([A-Za-z0-9_.-]+)${escBslash}.md','g');
|
|
3859
|
+
const rxTableRow=new RegExp('${escBslash}|${escBslash}s*([A-Za-z0-9_.-]+)${escBslash}s*${escBslash}|${escBslash}s*'+esc(testPrefix),'g');
|
|
3860
|
+
const readme=fs.existsSync(mdDir+'/README.md')?fs.readFileSync(mdDir+'/README.md','utf8'):'';
|
|
3815
3861
|
const norm=s=>String(s).toLowerCase().replace(/[^a-z0-9]+/g,'_').replace(/^_+|_+$/g,'').replace(/_+/g,'_');
|
|
3816
3862
|
const bt=String.fromCharCode(96);
|
|
3817
3863
|
const stripBackticks=s=>s.split(bt).join('');
|
|
3818
|
-
const invalidReason=raw=>{const st=norm(raw);if(/^p[0-2]$/.test(st))return 'priority-only-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;};
|
|
3864
|
+
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;};
|
|
3819
3865
|
const valid=raw=>invalidReason(raw)===null;
|
|
3820
|
-
const rxMdPath=/testcase\\/md\\/([A-Za-z0-9_.-]+)\\.md/g;
|
|
3821
|
-
const rxTableRow=/\\|\\s*([A-Za-z0-9_.-]+)\\s*\\|\\s*testcase\\/test_/g;
|
|
3822
3866
|
const rxRelLink=/\\[[^\\]]+\\]\\(\\.\\/([A-Za-z0-9_.-]+)\\.md\\)/g;
|
|
3867
|
+
const allLines=readme.replace(/\\r\\n/g,'\\n').replace(/\\r/g,'\\n').split('\\n');
|
|
3868
|
+
const headings=[];for(let i=0;i<allLines.length;i++){if(allLines[i].trim()==='## Module Index')headings.push(i);}
|
|
3869
|
+
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);}
|
|
3870
|
+
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;}}
|
|
3871
|
+
const section=allLines.slice(start,end).join('\\n');
|
|
3823
3872
|
const raw=[];
|
|
3824
|
-
const lines=
|
|
3873
|
+
const lines=section.split('\\n').filter(l=>l.includes('|'));
|
|
3825
3874
|
for(const line of lines){
|
|
3826
3875
|
const bare=stripBackticks(line);
|
|
3827
3876
|
for(const m of bare.matchAll(rxMdPath)){raw.push(m[1]);}
|
|
3828
|
-
for(const m of bare.matchAll(rxTableRow)){if(valid(m[1])||invalidReason(m[1])
|
|
3877
|
+
for(const m of bare.matchAll(rxTableRow)){if(valid(m[1])||invalidReason(m[1])!=='invalid-syntax')raw.push(m[1]);}
|
|
3829
3878
|
}
|
|
3830
|
-
for(const m of
|
|
3879
|
+
for(const m of section.matchAll(rxRelLink)){raw.push(m[1]);}
|
|
3831
3880
|
const invalid=[];for(const r of raw){const reason=invalidReason(r);if(reason)invalid.push({stem:norm(r),reason});}
|
|
3832
3881
|
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);}
|
|
3833
3882
|
const seen=new Set();const modules=[];
|
|
3834
3883
|
for(const r of raw){const st=norm(r);if(valid(r)&&!seen.has(st)){seen.add(st);modules.push({stem:st});}}
|
|
3884
|
+
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);}
|
|
3835
3885
|
process.stdout.write(JSON.stringify({modules}));
|
|
3836
3886
|
`;
|
|
3837
3887
|
const encoded = Buffer.from(script, "utf8").toString("base64");
|
|
@@ -3845,11 +3895,443 @@ const BACKEND_TEST_SKILLS_BY_ROLE = {
|
|
|
3845
3895
|
verifier: ["verification-before-completion", "systematic-debugging"],
|
|
3846
3896
|
closeout: ["loop-agent", "verification-before-completion"],
|
|
3847
3897
|
};
|
|
3898
|
+
/**
|
|
3899
|
+
* Plan D: gap-fill incremental DAG (mode=gap-fill).
|
|
3900
|
+
*
|
|
3901
|
+
* Re-runs the SAME requirement against a `backend-test-gap-v1` document and
|
|
3902
|
+
* the previous N6 coverage facts. Deterministic ingest plans exact
|
|
3903
|
+
* targetPaths; writers only patch planned files, never renumber Case IDs,
|
|
3904
|
+
* and a satisfied plan short-circuits writers with already-satisfied. The
|
|
3905
|
+
* heavy full-chain nodes (README rewrite, full-source N5 sync, helpers)
|
|
3906
|
+
* are replaced by patch writers whose writeSet equals the planned files.
|
|
3907
|
+
*/
|
|
3908
|
+
async function buildBackendTestGapFillDag(sources) {
|
|
3909
|
+
const { taskConfig } = sources;
|
|
3910
|
+
const ro = commonReadOnlyPaths(sources);
|
|
3911
|
+
const forbidden = commonForbiddenPaths(sources);
|
|
3912
|
+
const intake = await buildBackendTestIntakeContext(sources);
|
|
3913
|
+
const layout = resolveBackendTestLayout(taskConfig.backendTest);
|
|
3914
|
+
const sharedSetup = intake.sharedSetup;
|
|
3915
|
+
const gapDocPath = taskConfig.backendTest?.gapDoc?.trim();
|
|
3916
|
+
if (!gapDocPath) {
|
|
3917
|
+
throw new Error("backendTest.mode=gap-fill requires backendTest.gapDoc pointing at the user missing-scenario document");
|
|
3918
|
+
}
|
|
3919
|
+
const envAllowlist = collectBackendTestShellEnvAllowlist(sources);
|
|
3920
|
+
// N1: reuse the full-chain environment validation unchanged.
|
|
3921
|
+
const environment = {
|
|
3922
|
+
id: "validate-backend-test-environment-shell",
|
|
3923
|
+
depends_on: [],
|
|
3924
|
+
role: "verifier",
|
|
3925
|
+
executor: "shell",
|
|
3926
|
+
complexity: "LOW",
|
|
3927
|
+
writePolicy: "read-only",
|
|
3928
|
+
allowedPaths: ro,
|
|
3929
|
+
forbiddenPaths: forbidden,
|
|
3930
|
+
outputContract: "Run-owned reports/backend-test-environment.md with PASS/FAIL runtime, bounded project discovery, fixture and HTML-renderer facts; no secret values.",
|
|
3931
|
+
subtask_prompt: "Fail fast before model work when Python/pytest cannot run in the clean shell. Inspect only bounded common config, conftest, test-root and server-entry candidates; never read .env values or credentials.",
|
|
3932
|
+
shell: {
|
|
3933
|
+
commands: ["python --version", "python -m pytest --version", "python -m pytest --help"],
|
|
3934
|
+
backendTestPipeline: "markdown-environment",
|
|
3935
|
+
cwd: ".",
|
|
3936
|
+
timeoutMs: 60000,
|
|
3937
|
+
envAllowlist,
|
|
3938
|
+
},
|
|
3939
|
+
};
|
|
3940
|
+
// N2': deterministic gap ingest (shell pipeline owns parsing + planning).
|
|
3941
|
+
const ingestGap = {
|
|
3942
|
+
id: "ingest-backend-test-gap-shell",
|
|
3943
|
+
depends_on: [environment.id],
|
|
3944
|
+
role: "verifier",
|
|
3945
|
+
executor: "shell",
|
|
3946
|
+
complexity: "LOW",
|
|
3947
|
+
writePolicy: "read-only",
|
|
3948
|
+
allowedPaths: ro,
|
|
3949
|
+
forbiddenPaths: forbidden,
|
|
3950
|
+
outputContract: "Run-owned contracts/backend-test-gap-plan-v1.json with targetModules, newTestPoints, reuseSetupRefs, conflicts, alreadyPresent and exact targetPaths; identity mismatch (taskId/requirement hash) fails closed before any writer runs.",
|
|
3951
|
+
subtask_prompt: "Parse the bound gap document (backend-test-gap-v1) and union it with previous coverage facts missingSlots; emit the deterministic gap plan. No file writes to testcase assets.",
|
|
3952
|
+
shell: {
|
|
3953
|
+
commands: [],
|
|
3954
|
+
backendTestPipeline: "ingest-backend-test-gap",
|
|
3955
|
+
cwd: ".",
|
|
3956
|
+
timeoutMs: 60000,
|
|
3957
|
+
envAllowlist,
|
|
3958
|
+
},
|
|
3959
|
+
};
|
|
3960
|
+
// N3': patch README Matrix/Partitions for planned slots only.
|
|
3961
|
+
const patchReadme = {
|
|
3962
|
+
id: "patch-backend-readme-matrix-pi",
|
|
3963
|
+
depends_on: [ingestGap.id],
|
|
3964
|
+
role: "implementer",
|
|
3965
|
+
executor: "pi",
|
|
3966
|
+
toolProfile: "write",
|
|
3967
|
+
complexity: "LOW",
|
|
3968
|
+
writePolicy: "exclusive",
|
|
3969
|
+
writeSet: [layout.readmePath],
|
|
3970
|
+
allowedPaths: [layout.readmePath],
|
|
3971
|
+
forbiddenPaths: forbidden,
|
|
3972
|
+
writerOutcomePolicy: {
|
|
3973
|
+
type: "implementation-outcome-v1",
|
|
3974
|
+
requireChangedFiles: false,
|
|
3975
|
+
},
|
|
3976
|
+
retryPolicy: BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY,
|
|
3977
|
+
outputContract: "First non-empty line is IMPLEMENTATION_OUTCOME: changed|already-satisfied|blocked. Patch ONLY the Coverage Matrix rows and Scenario Partitions table entries named by contracts/backend-test-gap-plan-v1.json newTestPoints; never rewrite unrelated README sections, never renumber Cases, never add Module Index modules.",
|
|
3978
|
+
subtask_prompt: [
|
|
3979
|
+
"Read contracts/backend-test-gap-plan-v1.json. For each planned slot in newTestPoints, patch the owning Coverage Matrix row (Required Test Points/Case IDs) and, for TP-SP slots, the Scenario Partitions table so the slot is declared. already-satisfied is valid when every planned slot is already present; changed requires a bounded diff limited to the slot rows.",
|
|
3980
|
+
"When every planned slot is already declared, return already-satisfied without editing. Never expand scope beyond newTestPoints; never touch module Markdown or pytest files here.",
|
|
3981
|
+
intake.boundedSourceContext,
|
|
3982
|
+
].join("\n\n"),
|
|
3983
|
+
};
|
|
3984
|
+
// N4': patch only the planned module Markdown files (map over targetModules).
|
|
3985
|
+
const patchMdCases = {
|
|
3986
|
+
id: "patch-backend-md-cases-map",
|
|
3987
|
+
depends_on: [patchReadme.id],
|
|
3988
|
+
role: "verifier",
|
|
3989
|
+
executor: "static",
|
|
3990
|
+
complexity: "LOW",
|
|
3991
|
+
writePolicy: "none",
|
|
3992
|
+
allowedPaths: [],
|
|
3993
|
+
forbiddenPaths: forbidden,
|
|
3994
|
+
outputContract: "Serial aggregate of per-module Markdown patch writers; each child writes exactly one planned testcase module file with a bounded append/patch diff.",
|
|
3995
|
+
subtask_prompt: "Expand the gap plan targetModules into one sharded Markdown patch writer child per module and run them serially. Child failures fail-close the map barrier.",
|
|
3996
|
+
static: {
|
|
3997
|
+
resultMarkdown: "Backend-test gap-fill Markdown patch map expansion barrier.",
|
|
3998
|
+
},
|
|
3999
|
+
dynamicExpansion: {
|
|
4000
|
+
type: "map_agent",
|
|
4001
|
+
workflowNodeId: "patch-backend-md-cases-map",
|
|
4002
|
+
itemsFrom: "$.nodes['ingest-backend-test-gap-shell'].json.plan.targetModules",
|
|
4003
|
+
itemName: "item",
|
|
4004
|
+
maxItems: 8,
|
|
4005
|
+
maxExpandedNodes: 8,
|
|
4006
|
+
childIdPrefix: "patch-backend-md-case",
|
|
4007
|
+
tokenBudget: { maxTotalTokens: 1000000 },
|
|
4008
|
+
failOnTokenBudgetExhaustion: true,
|
|
4009
|
+
childTask: {
|
|
4010
|
+
executor: "pi",
|
|
4011
|
+
role: "implementer",
|
|
4012
|
+
skills: BACKEND_TEST_SKILLS_BY_ROLE.implementer,
|
|
4013
|
+
toolProfile: "write",
|
|
4014
|
+
complexity: "LOW",
|
|
4015
|
+
writePolicy: "exclusive",
|
|
4016
|
+
allowedPaths: [`${layout.markdownDir}/{{item.stem}}.md`],
|
|
4017
|
+
forbiddenPaths: forbidden,
|
|
4018
|
+
writeSet: [`${layout.markdownDir}/{{item.stem}}.md`],
|
|
4019
|
+
writerOutcomePolicy: {
|
|
4020
|
+
type: "implementation-outcome-v1",
|
|
4021
|
+
requireChangedFiles: false,
|
|
4022
|
+
},
|
|
4023
|
+
retryPolicy: BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY,
|
|
4024
|
+
outputContract: "Patch exactly one planned module Markdown file: add missing slot Test Points (existing Case variant lists first; new BE-<MODULE>-<max+1> Case only when no Case can own the slot). Case IDs are never renumbered; sections follow the full-chain contract.",
|
|
4025
|
+
subtaskPromptTemplate: [
|
|
4026
|
+
"Read contracts/backend-test-gap-plan-v1.json and patch exactly testcase/md/{{item.stem}}.md. For every planned slot owned by this module: prefer appending the variant to the Case named by the plan (or the most related existing Case); create a new Case only when no existing Case can own it, numbering BE-<MODULE>-<NNN> from the module's current maximum +1. Keep every required h3 section; cite Matrix Rule Keys exactly; TP-SP slots use the exact deterministic slot IDs from the plan.",
|
|
4027
|
+
sharedSetup
|
|
4028
|
+
? `When the slot needs shared pre-steps defined by the bound user document, reference them with 引用前置: ${sharedSetup.path}#<SS-ID|SS-DEFAULT> instead of duplicating steps.`
|
|
4029
|
+
: "Prepare any needed state locally inside this Case (本地准备); no shared setup document is bound.",
|
|
4030
|
+
"already-satisfied is valid when every planned slot of this module is already covered. Never modify other modules, README, or pytest files.",
|
|
4031
|
+
intake.boundedSourceContext,
|
|
4032
|
+
].join("\n\n"),
|
|
4033
|
+
},
|
|
4034
|
+
},
|
|
4035
|
+
};
|
|
4036
|
+
// N5': validate patched Markdown with the same N6 pipeline (partition slots fail-closed).
|
|
4037
|
+
const validateMd = {
|
|
4038
|
+
id: "validate-backend-md-cases-shell",
|
|
4039
|
+
depends_on: [patchMdCases.id],
|
|
4040
|
+
role: "verifier",
|
|
4041
|
+
executor: "shell",
|
|
4042
|
+
complexity: "LOW",
|
|
4043
|
+
writePolicy: "read-only",
|
|
4044
|
+
allowedPaths: ro,
|
|
4045
|
+
forbiddenPaths: forbidden,
|
|
4046
|
+
outputContract: "Run-owned reports/backend-md-case-validation.md, coverage analysis and facts v4 with scenarioPartitions; planned slots must be covered (missingSlots for planned entries fail closed).",
|
|
4047
|
+
subtask_prompt: "Record advisory findings for Markdown structure and deterministically analyze the final README Coverage Scope, Coverage Matrix and Scenario Partitions against final Case rule/test-point bindings. Planned gap slots that remain uncovered fail this node closed.",
|
|
4048
|
+
shell: {
|
|
4049
|
+
commands: [],
|
|
4050
|
+
backendTestPipeline: "markdown-cases",
|
|
4051
|
+
cwd: ".",
|
|
4052
|
+
timeoutMs: 60000,
|
|
4053
|
+
envAllowlist,
|
|
4054
|
+
},
|
|
4055
|
+
};
|
|
4056
|
+
// N6': patch only the planned pytest modules (append params/functions).
|
|
4057
|
+
const patchPytest = {
|
|
4058
|
+
id: "patch-backend-pytest-cases-map",
|
|
4059
|
+
depends_on: [validateMd.id],
|
|
4060
|
+
role: "verifier",
|
|
4061
|
+
executor: "static",
|
|
4062
|
+
complexity: "LOW",
|
|
4063
|
+
writePolicy: "none",
|
|
4064
|
+
allowedPaths: [],
|
|
4065
|
+
forbiddenPaths: forbidden,
|
|
4066
|
+
outputContract: "Serial aggregate of per-module pytest patch writers; each child appends to exactly one planned testcase/test_<stem>.py without rewriting the file.",
|
|
4067
|
+
subtask_prompt: "Expand the gap plan targetModules into one sharded pytest patch writer child per module and run them serially. Child failures fail-close the map barrier.",
|
|
4068
|
+
static: {
|
|
4069
|
+
resultMarkdown: "Backend-test gap-fill pytest patch map expansion barrier.",
|
|
4070
|
+
},
|
|
4071
|
+
dynamicExpansion: {
|
|
4072
|
+
type: "map_agent",
|
|
4073
|
+
workflowNodeId: "patch-backend-pytest-cases-map",
|
|
4074
|
+
itemsFrom: "$.nodes['ingest-backend-test-gap-shell'].json.plan.targetModules",
|
|
4075
|
+
itemName: "item",
|
|
4076
|
+
maxItems: 8,
|
|
4077
|
+
maxExpandedNodes: 8,
|
|
4078
|
+
childIdPrefix: "patch-backend-pytest-case",
|
|
4079
|
+
tokenBudget: { maxTotalTokens: 1000000 },
|
|
4080
|
+
failOnTokenBudgetExhaustion: true,
|
|
4081
|
+
childTask: {
|
|
4082
|
+
executor: "pi",
|
|
4083
|
+
role: "implementer",
|
|
4084
|
+
skills: BACKEND_TEST_SKILLS_BY_ROLE.implementer,
|
|
4085
|
+
toolProfile: "write",
|
|
4086
|
+
complexity: "LOW",
|
|
4087
|
+
writePolicy: "exclusive",
|
|
4088
|
+
allowedPaths: [`${layout.scriptDir}/test_{{item.stem}}.py`],
|
|
4089
|
+
forbiddenPaths: Array.from(new Set([
|
|
4090
|
+
...forbidden,
|
|
4091
|
+
`${layout.markdownDir}/**`,
|
|
4092
|
+
"conftest.py",
|
|
4093
|
+
"pytest.ini",
|
|
4094
|
+
"pyproject.toml",
|
|
4095
|
+
"setup.cfg",
|
|
4096
|
+
])),
|
|
4097
|
+
writeSet: [`${layout.scriptDir}/test_{{item.stem}}.py`],
|
|
4098
|
+
writerOutcomePolicy: {
|
|
4099
|
+
type: "implementation-outcome-v1",
|
|
4100
|
+
requireChangedFiles: false,
|
|
4101
|
+
},
|
|
4102
|
+
retryPolicy: BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY,
|
|
4103
|
+
outputContract: "Append-only patch of exactly one planned pytest module: add pytest.param rows / test functions for planned slots with exact TP ids; never rewrite or delete existing functions.",
|
|
4104
|
+
subtaskPromptTemplate: [
|
|
4105
|
+
"Read contracts/backend-test-gap-plan-v1.json and the patched module Markdown, then patch only testcase/test_{{item.stem}}.py. For each planned slot owned by this module: append an exact literal pytest.param(..., id=\"<slot-id>\") row to the owning Case's primary symbol, or append a new test function test_BE_<MODULE>_<NNN>_<desc> when a new Case was created. Existing functions, params and assertions must remain byte-stable; append-only edits.",
|
|
4106
|
+
sharedSetup
|
|
4107
|
+
? "Reuse the module-top user_shared_setup fixture for shared pre-steps; never re-create documented setup steps inside test bodies, and never create or modify conftest.py."
|
|
4108
|
+
: "Keep any needed setup local to the new function; never create or modify conftest.py.",
|
|
4109
|
+
"already-satisfied is valid when every planned slot already has its exact pytest.param id collected. Do not modify Markdown or other modules.",
|
|
4110
|
+
].join("\n\n"),
|
|
4111
|
+
},
|
|
4112
|
+
},
|
|
4113
|
+
};
|
|
4114
|
+
// N7'-N13': reuse the deterministic collection/repair/gate/trace/manifest chain.
|
|
4115
|
+
const collectionAssess = {
|
|
4116
|
+
id: "assess-backend-pytest-collection-shell",
|
|
4117
|
+
depends_on: [patchPytest.id],
|
|
4118
|
+
role: "verifier",
|
|
4119
|
+
executor: "shell",
|
|
4120
|
+
complexity: "LOW",
|
|
4121
|
+
writePolicy: "read-only",
|
|
4122
|
+
allowedPaths: ro,
|
|
4123
|
+
forbiddenPaths: forbidden,
|
|
4124
|
+
outputContract: "Run-owned collection-v3 facts with repairPaths limited to the gap plan target pytest files.",
|
|
4125
|
+
subtask_prompt: "Resolve final Markdown-mapped scripts; scenario-param assess, pytest --collect-only and no-business-body fixture preflight. Repair eligibility is limited to planned pytest files.",
|
|
4126
|
+
shell: {
|
|
4127
|
+
commands: [],
|
|
4128
|
+
backendTestPipeline: "markdown-collection-assess",
|
|
4129
|
+
cwd: ".",
|
|
4130
|
+
timeoutMs: 120000,
|
|
4131
|
+
envAllowlist,
|
|
4132
|
+
},
|
|
4133
|
+
};
|
|
4134
|
+
const repairPytest = {
|
|
4135
|
+
id: "repair-backend-pytest-collection-pi",
|
|
4136
|
+
depends_on: [collectionAssess.id],
|
|
4137
|
+
runIf: "$.nodes['assess-backend-pytest-collection-shell'].json.repairEligible == true",
|
|
4138
|
+
role: "implementer",
|
|
4139
|
+
executor: "pi",
|
|
4140
|
+
toolProfile: "write",
|
|
4141
|
+
complexity: "MED",
|
|
4142
|
+
writePolicy: "exclusive",
|
|
4143
|
+
writeSet: [`${layout.scriptDir}/test_*.py`],
|
|
4144
|
+
allowedPaths: Array.from(new Set([...ro, layout.scriptDir])),
|
|
4145
|
+
forbiddenPaths: Array.from(new Set([
|
|
4146
|
+
...forbidden,
|
|
4147
|
+
`${layout.markdownDir}/**`,
|
|
4148
|
+
"conftest.py",
|
|
4149
|
+
"pytest.ini",
|
|
4150
|
+
"pyproject.toml",
|
|
4151
|
+
"setup.cfg",
|
|
4152
|
+
])),
|
|
4153
|
+
writerOutcomePolicy: { type: "implementation-outcome-v1", requireChangedFiles: true },
|
|
4154
|
+
outputContract: "First non-empty line is IMPLEMENTATION_OUTCOME: changed|blocked. Repair only generated pytest defects on initial facts repairPaths; preserve every Markdown Case, Test Point, primary symbol and assertion meaning.",
|
|
4155
|
+
subtask_prompt: [
|
|
4156
|
+
"Repair the generated backend pytest asset as one bounded program using the direct upstream collection assessment. Treat any upstream `Repair paths:` line as complete authoritative repairPaths evidence. Only planned testcase test_*.py files may change.",
|
|
4157
|
+
"Do not modify Markdown, conftest, pytest config, production code or dependencies. Do not add skip/xfail, remove tests, loosen assertions or replace the real API with mocks.",
|
|
4158
|
+
].join("\n\n"),
|
|
4159
|
+
};
|
|
4160
|
+
const collectionEffective = {
|
|
4161
|
+
id: "effective-backend-pytest-collection-gate-shell",
|
|
4162
|
+
depends_on: [collectionAssess.id, repairPytest.id],
|
|
4163
|
+
role: "verifier",
|
|
4164
|
+
executor: "shell",
|
|
4165
|
+
complexity: "LOW",
|
|
4166
|
+
writePolicy: "read-only",
|
|
4167
|
+
allowedPaths: ro,
|
|
4168
|
+
forbiddenPaths: forbidden,
|
|
4169
|
+
outputContract: "Canonical backend-test-execution-readiness.json v2 bound to final asset hashes.",
|
|
4170
|
+
subtask_prompt: "Materialize effective collection facts; eligibility excludes non-exact mappings and unsafe payloads as in the full chain.",
|
|
4171
|
+
shell: {
|
|
4172
|
+
commands: [],
|
|
4173
|
+
backendTestPipeline: "markdown-collection-effective",
|
|
4174
|
+
cwd: ".",
|
|
4175
|
+
timeoutMs: 120000,
|
|
4176
|
+
envAllowlist,
|
|
4177
|
+
},
|
|
4178
|
+
};
|
|
4179
|
+
collectionEffective.dependsPolicy = "all-or-condition-skip";
|
|
4180
|
+
const traceability = {
|
|
4181
|
+
id: "backend-test-traceability-gate-shell",
|
|
4182
|
+
depends_on: [collectionEffective.id],
|
|
4183
|
+
role: "verifier",
|
|
4184
|
+
executor: "shell",
|
|
4185
|
+
complexity: "LOW",
|
|
4186
|
+
writePolicy: "read-only",
|
|
4187
|
+
allowedPaths: ro,
|
|
4188
|
+
forbiddenPaths: forbidden,
|
|
4189
|
+
outputContract: "Run-owned traceability and correspondence facts bound after effective collection.",
|
|
4190
|
+
subtask_prompt: "Deterministically scan final readiness-authorized Markdown-mapped pytest scripts; correspondence findings stay advisory.",
|
|
4191
|
+
shell: {
|
|
4192
|
+
commands: [],
|
|
4193
|
+
backendTestPipeline: "markdown-traceability",
|
|
4194
|
+
cwd: ".",
|
|
4195
|
+
timeoutMs: 60000,
|
|
4196
|
+
envAllowlist,
|
|
4197
|
+
},
|
|
4198
|
+
};
|
|
4199
|
+
const manifest = {
|
|
4200
|
+
id: "backend-test-case-manifest-shell",
|
|
4201
|
+
depends_on: [traceability.id],
|
|
4202
|
+
role: "verifier",
|
|
4203
|
+
executor: "shell",
|
|
4204
|
+
complexity: "LOW",
|
|
4205
|
+
writePolicy: "read-only",
|
|
4206
|
+
allowedPaths: ro,
|
|
4207
|
+
forbiddenPaths: forbidden,
|
|
4208
|
+
outputContract: "Run-owned contracts/backend-test-case-manifest.json materialized only from facts; the single machine input for L-5 and closeout.",
|
|
4209
|
+
subtask_prompt: "Materialize the canonical Backend Test Case Manifest only from coverage and correspondence facts; never re-analyze sources.",
|
|
4210
|
+
shell: {
|
|
4211
|
+
commands: [],
|
|
4212
|
+
backendTestPipeline: "markdown-manifest",
|
|
4213
|
+
cwd: ".",
|
|
4214
|
+
timeoutMs: 60000,
|
|
4215
|
+
envAllowlist,
|
|
4216
|
+
},
|
|
4217
|
+
};
|
|
4218
|
+
const execute = {
|
|
4219
|
+
id: "execute-backend-pytest-and-html-report-shell",
|
|
4220
|
+
depends_on: [manifest.id],
|
|
4221
|
+
role: "verifier",
|
|
4222
|
+
executor: "shell",
|
|
4223
|
+
complexity: "LOW",
|
|
4224
|
+
writePolicy: "read-only",
|
|
4225
|
+
allowedPaths: ro,
|
|
4226
|
+
forbiddenPaths: forbidden,
|
|
4227
|
+
outputContract: "One scoped pytest execution over readiness-authorized eligible items producing pytest-html plus the Chinese HTML report and L-5 dashboard; exit 0/1 with valid evidence continues.",
|
|
4228
|
+
subtask_prompt: `Execute readiness-authorized eligible pytest items once${taskConfig.backendTest?.executeScope === "all"
|
|
4229
|
+
? " (executeScope=all: every eligible item)"
|
|
4230
|
+
: " (executeScope=affected: prefer newly added planned items when readiness marks them; otherwise every eligible item)"}, render the report, and keep evidence run-owned.`,
|
|
4231
|
+
shell: {
|
|
4232
|
+
commands: [],
|
|
4233
|
+
backendTestPipeline: "markdown-execute-html",
|
|
4234
|
+
cwd: ".",
|
|
4235
|
+
timeoutMs: 300000,
|
|
4236
|
+
envAllowlist,
|
|
4237
|
+
},
|
|
4238
|
+
};
|
|
4239
|
+
const report = {
|
|
4240
|
+
id: "backend-test-report-shell",
|
|
4241
|
+
depends_on: [execute.id],
|
|
4242
|
+
role: "verifier",
|
|
4243
|
+
executor: "shell",
|
|
4244
|
+
complexity: "LOW",
|
|
4245
|
+
writePolicy: "read-only",
|
|
4246
|
+
allowedPaths: ro,
|
|
4247
|
+
forbiddenPaths: forbidden,
|
|
4248
|
+
outputContract: "Run-owned backend test Result v1 with pytestExitCode, junit and evidence hashes; failure categories keep product/automation semantics.",
|
|
4249
|
+
subtask_prompt: "Materialize the backend-test Result v1 from the execution evidence; never fabricate metrics.",
|
|
4250
|
+
shell: {
|
|
4251
|
+
commands: [],
|
|
4252
|
+
backendTestPipeline: "markdown-report",
|
|
4253
|
+
cwd: ".",
|
|
4254
|
+
timeoutMs: 60000,
|
|
4255
|
+
envAllowlist,
|
|
4256
|
+
},
|
|
4257
|
+
};
|
|
4258
|
+
const spec = {
|
|
4259
|
+
version: 3,
|
|
4260
|
+
title: `Backend test gap-fill: ${sources.taskId}`,
|
|
4261
|
+
runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
|
|
4262
|
+
outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
|
|
4263
|
+
objective: `Incrementally fill backend-test gap scenarios for task ${sources.taskId} from the bound gap document without renumbering existing Cases.`,
|
|
4264
|
+
successCriteria: [
|
|
4265
|
+
"Every planned gap slot is declared in README (Matrix/Scenario Partitions), covered by a Case test point, and collected as an exact pytest.param id",
|
|
4266
|
+
"Existing Case IDs and unrelated modules remain byte-stable; no conftest.py is created or modified",
|
|
4267
|
+
"A second run over a satisfied plan returns already-satisfied with no asset diff",
|
|
4268
|
+
],
|
|
4269
|
+
globalConstraints: [
|
|
4270
|
+
...sources.taskConfig.hardConstraints.filter(Boolean),
|
|
4271
|
+
`backendTestGapDoc=${gapDocPath}`,
|
|
4272
|
+
`backendTestExecuteScope=${taskConfig.backendTest?.executeScope ?? "affected"}`,
|
|
4273
|
+
],
|
|
4274
|
+
skillsByRole: BACKEND_TEST_SKILLS_BY_ROLE,
|
|
4275
|
+
executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
|
|
4276
|
+
verifyStrategy: resolveDagVerifyStrategy(taskConfig),
|
|
4277
|
+
tasks: [
|
|
4278
|
+
environment,
|
|
4279
|
+
ingestGap,
|
|
4280
|
+
patchReadme,
|
|
4281
|
+
patchMdCases,
|
|
4282
|
+
validateMd,
|
|
4283
|
+
patchPytest,
|
|
4284
|
+
collectionAssess,
|
|
4285
|
+
repairPytest,
|
|
4286
|
+
collectionEffective,
|
|
4287
|
+
traceability,
|
|
4288
|
+
manifest,
|
|
4289
|
+
execute,
|
|
4290
|
+
report,
|
|
4291
|
+
],
|
|
4292
|
+
};
|
|
4293
|
+
spec.backendTestLayout = layout;
|
|
4294
|
+
if (sharedSetup) {
|
|
4295
|
+
spec.backendTestSharedSetup = { ...sharedSetup };
|
|
4296
|
+
}
|
|
4297
|
+
applyDefaultReadOnlyRetryPolicy(spec);
|
|
4298
|
+
parseDagSpec(spec);
|
|
4299
|
+
assertValidDagSpec(spec);
|
|
4300
|
+
return spec;
|
|
4301
|
+
}
|
|
3848
4302
|
async function buildBackendTestHybridDag(sources) {
|
|
3849
4303
|
const { taskConfig } = sources;
|
|
4304
|
+
if (taskConfig.backendTest?.mode === "gap-fill") {
|
|
4305
|
+
return buildBackendTestGapFillDag(sources);
|
|
4306
|
+
}
|
|
3850
4307
|
const ro = commonReadOnlyPaths(sources);
|
|
3851
4308
|
const forbidden = commonForbiddenPaths(sources);
|
|
3852
4309
|
const intake = await buildBackendTestIntakeContext(sources);
|
|
4310
|
+
// Plan A: resolve the frozen artifact layout once; every generated path
|
|
4311
|
+
// (README, module Markdown, pytest script, pytest target) derives from it.
|
|
4312
|
+
// Default config resolves to the historical testcase/ layout.
|
|
4313
|
+
const layout = resolveBackendTestLayout(taskConfig.backendTest);
|
|
4314
|
+
const applyLayout = (text) => applyBackendTestLayoutToText(text, layout);
|
|
4315
|
+
// Plan B: when the user binds exactly one shared-setup document, generated
|
|
4316
|
+
// Markdown references it (引用前置/SS-*) and each pytest module hoists the
|
|
4317
|
+
// documented steps into a module-top `user_shared_setup` fixture. Absent the
|
|
4318
|
+
// binding, every prompt below stays byte-identical to the pre-B baseline.
|
|
4319
|
+
const sharedSetup = intake.sharedSetup;
|
|
4320
|
+
const sharedSetupPrompt = sharedSetup
|
|
4321
|
+
? [
|
|
4322
|
+
"## User shared setup document (bound, read-only)",
|
|
4323
|
+
`A user-provided shared pre-step document is bound at \`${sharedSetup.path}\` (readPath \`${sharedSetup.readPath}\`, sha256 ${sharedSetup.sha256}). Treat it as an authoritative read-only source: never rewrite, split or renumber it, and never invent steps absent from it.`,
|
|
4324
|
+
"When a Case needs a pre-step that this document already defines, the Case MUST reference it instead of duplicating the steps: in `### 前置条件` write `引用前置: <bound-path>#<SS-ID or heading>` lines (plus a `Shared Setup Refs` list of the referenced SS IDs). Extract stable step IDs only from explicit machine IDs (e.g. `SS-1`, `SS-REGISTER-01`); when the document is prose without stable IDs, reference the whole document as `引用前置: <bound-path>#SS-DEFAULT` and do not guess splits. Preparation unique to a Case stays inline but must be labeled `本地准备:`.",
|
|
4325
|
+
"Payload Contract still describes ONLY the target request; setup POST/PUT steps defined by the shared document never redefine the target Case payload contract.",
|
|
4326
|
+
].join("\n\n")
|
|
4327
|
+
: null;
|
|
4328
|
+
const sharedSetupPytestPrompt = sharedSetup
|
|
4329
|
+
? [
|
|
4330
|
+
"## User shared setup hoisting (module top)",
|
|
4331
|
+
`The user shared setup document \`${sharedSetup.path}\` (readPath \`${sharedSetup.readPath}\`) defines the common pre-steps. At the TOP of this module file (before any test function), generate exactly one module-scoped fixture named \`user_shared_setup\` implemented by a private helper \`_user_shared_setup()\` that performs the documented shared steps in document order, using the exact resource names/fields from the document. Cases reference these steps via their \`引用前置\` lines; each \`test_BE_*\` body then performs ONLY its target operation and must not repeat documented shared create/setup steps. Generate teardown code only when the user document explicitly documents a cleanup step; never invent a DELETE.`,
|
|
4332
|
+
"Duplication of this prefix across module files is accepted by design: do not extract it into conftest.py, a shared helper module, pytest_plugins, or a `_shared_setup.py`; the module file stays self-contained. NEVER create or modify conftest.py.",
|
|
4333
|
+
].join("\n\n")
|
|
4334
|
+
: null;
|
|
3853
4335
|
const shellNode = (id, depends_on, pipeline, prompt, outputContract, commands = [], timeoutMs = 60000) => ({
|
|
3854
4336
|
id,
|
|
3855
4337
|
depends_on,
|
|
@@ -3909,8 +4391,10 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3909
4391
|
"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.",
|
|
3910
4392
|
"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.",
|
|
3911
4393
|
"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.",
|
|
3912
|
-
"Mandatory module index: include a `## Module Index` table in README that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output
|
|
4394
|
+
"Mandatory module index: include a `## Module Index` table in README that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output protocol, keep the total module count at the smallest safe value, and never exceed 8 modules. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Pure hexadecimal/hash-like opaque stems such as `a401606` or `deadbeef` are forbidden. Do not use priority-only stems `p0`, `p1` or `p2`; Priority belongs only in the Coverage Matrix and never defines module files. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. The relative link target MUST equal the on-disk filename stem the sharded writer will create. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
|
|
4395
|
+
"Scenario Partitions (query/filter axes): for every affected GET/list operation, declare one row per enum or classification axis used for filtering (query/path parameters such as type/status/category). Add a mandatory 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. 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); Required Slots writes `each-value` plus `omitted` only when the parameter is optional; 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.",
|
|
3913
4396
|
"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.",
|
|
4397
|
+
...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
|
|
3914
4398
|
intake.boundedSourceContext,
|
|
3915
4399
|
"## Authoritative reference index",
|
|
3916
4400
|
JSON.stringify(intake.referenceIndex, null, 2),
|
|
@@ -3930,7 +4414,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3930
4414
|
outputContract: "Stdout JSON {modules:[{stem}]} parsed from testcase/md/README.md using the same module-stem extractor as the Completeness Gate, so the map_agent shard set deterministically matches the README module index.",
|
|
3931
4415
|
subtask_prompt: "Parse testcase/md/README.md and emit exactly one trailing JSON line {modules:[{stem}]} listing every trusted module stem (table-row testcase/md/<stem>.md mentions and canonical [label](./<stem>.md) relative links only). No file writes.",
|
|
3932
4416
|
shell: {
|
|
3933
|
-
commands: [buildBackendTestModuleManifestShellCommand()],
|
|
4417
|
+
commands: [buildBackendTestModuleManifestShellCommand(layout)],
|
|
3934
4418
|
cwd: ".",
|
|
3935
4419
|
timeoutMs: 60000,
|
|
3936
4420
|
},
|
|
@@ -3954,10 +4438,11 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3954
4438
|
workflowNodeId: "generate-backend-md-cases-map",
|
|
3955
4439
|
itemsFrom: "$.nodes['materialize-backend-md-module-manifest-shell'].output.modules",
|
|
3956
4440
|
itemName: "item",
|
|
3957
|
-
maxItems:
|
|
3958
|
-
maxExpandedNodes:
|
|
4441
|
+
maxItems: 8,
|
|
4442
|
+
maxExpandedNodes: 8,
|
|
3959
4443
|
childIdPrefix: "generate-backend-md-case",
|
|
3960
|
-
tokenBudget: {
|
|
4444
|
+
tokenBudget: { maxTotalTokens: 3000000 },
|
|
4445
|
+
failOnTokenBudgetExhaustion: true,
|
|
3961
4446
|
childTask: {
|
|
3962
4447
|
executor: "pi",
|
|
3963
4448
|
role: "implementer",
|
|
@@ -3979,10 +4464,13 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3979
4464
|
"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.",
|
|
3980
4465
|
"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.",
|
|
3981
4466
|
"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.",
|
|
3982
|
-
'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.',
|
|
3983
|
-
"Name this module file with the stable lowercase business stem `{{item.stem}}` (filename `testcase/md/{{item.stem}}.md`). Priority-only stems `p0`, `p1` and `p2` are forbidden and must never produce `p0.md` or `test_p0.py`. Do not use Case-ID-like module filenames. For every automatable case, `自动化映射` must name exactly `testcase/test_{{item.stem}}.py`, 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.",
|
|
4467
|
+
'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.',
|
|
4468
|
+
"Name this module file with the stable lowercase business stem `{{item.stem}}` (filename `testcase/md/{{item.stem}}.md`). 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 `testcase/test_{{item.stem}}.py`, 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.",
|
|
4469
|
+
"Scenario Partition slots: when README declares `## Scenario Partitions`, every slot of each declared partition MUST appear in this module's Cases as exactly one variant Test Point with the deterministic ID `TP-<Partition ID>-<VALUE-TOKEN>` (each-value), `TP-<Partition ID>-OMITTED` (optional axis only) and exactly one `TP-<Partition ID>-NOT-IN-SET` complement slot with `intent=enum-invalid`. Example: Partition ID `SP-GET-API-RESOURCE-NOTES-STATUS` → `TP-SP-GET-API-RESOURCE-NOTES-STATUS-ACTIVE`. Slot IDs copy the declared Partition ID exactly; never drop the HTTP method, invent, merge, renumber or split slot IDs. 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.",
|
|
4470
|
+
"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.",
|
|
3984
4471
|
"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 ‘符合预期’.",
|
|
3985
|
-
"In every `自动化映射`, use exactly these machine-readable list labels: `脚本`, `primary symbol`, `变体测试点`, `场景断言测试点`,
|
|
4472
|
+
"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.",
|
|
4473
|
+
...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
|
|
3986
4474
|
intake.boundedSourceContext,
|
|
3987
4475
|
"## Authoritative reference index",
|
|
3988
4476
|
JSON.stringify(intake.referenceIndex, null, 2),
|
|
@@ -3995,32 +4483,39 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3995
4483
|
const reviewCases = {
|
|
3996
4484
|
id: "review-and-revise-backend-md-cases-pi",
|
|
3997
4485
|
depends_on: [generateMdCasesMap.id],
|
|
3998
|
-
role: "
|
|
3999
|
-
executor: "
|
|
4000
|
-
|
|
4486
|
+
role: "implementer",
|
|
4487
|
+
executor: "pi",
|
|
4488
|
+
toolProfile: "write",
|
|
4489
|
+
complexity: "MED",
|
|
4001
4490
|
writePolicy: "exclusive",
|
|
4002
4491
|
writeSet: ["testcase/md/**"],
|
|
4003
|
-
allowedPaths: ["testcase/md/**"],
|
|
4492
|
+
allowedPaths: Array.from(new Set([...ro, "testcase/md/**"])),
|
|
4004
4493
|
forbiddenPaths: forbidden,
|
|
4005
|
-
|
|
4494
|
+
writerOutcomePolicy: { type: "implementation-outcome-v1" },
|
|
4495
|
+
outputContract: "First non-empty line is IMPLEMENTATION_OUTCOME: changed|already-satisfied|blocked. Perform exactly one bounded incremental synchronization of testcase/md/** against all bound source references; preserve valid Cases and report a concise summary.",
|
|
4006
4496
|
subtask_prompt: [
|
|
4007
|
-
"
|
|
4008
|
-
"Output budget protocol:
|
|
4009
|
-
"For every variant Test Point, ensure the Markdown scenario intent is machine-checkable
|
|
4497
|
+
"Perform one gap-targeted synchronization, not a full-suite rewrite or stylistic review. Start from explicit bound source IDs/error codes/DTO fields/normative quoted rules and the README Matrix; open and edit only modules that own a missing or conflicting rule. Preserve unrelated valid modules byte-for-byte and avoid optional wording cleanup.",
|
|
4498
|
+
"Output budget protocol: never dump full Matrix/case bodies into assistant chat. Inspect README first, build a concise target list, then read/write only target modules one file per tool call. Do not traverse every module when the Matrix and source token inventory show no gap; return `already-satisfied`. When adding omitted in-scope cases, keep every required section. Do not bulk-delete in-scope cases to save tokens.",
|
|
4499
|
+
"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`.",
|
|
4010
4500
|
"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.",
|
|
4011
4501
|
"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 `testcase/md/README.md`, 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.",
|
|
4012
|
-
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, 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 case points at `testcase/test_<module>.py` derived from that module filename and declares exactly one primary symbol, assign every Test Point exactly one of `变体测试点`/`场景断言测试点`/`横切证据测试点`, ensure every binding-list Test Point is
|
|
4013
|
-
"Read
|
|
4502
|
+
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, 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.",
|
|
4503
|
+
"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.",
|
|
4504
|
+
"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.",
|
|
4505
|
+
"Scenario Partitions synchronization: when README 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). Directly add missing slot rows/Cases. You may delete an illegal Partition row that has no source-backed finite domain, together with its derived `TP-SP-*` slots/Cases. 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.",
|
|
4506
|
+
"Before returning, verify that every explicit source AC/REQ/BR, error code and strong DTO field token appears in README 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.",
|
|
4507
|
+
"Read only indexed source paths. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.",
|
|
4508
|
+
...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
|
|
4014
4509
|
intake.boundedSourceContext,
|
|
4015
4510
|
"## Authoritative reference index",
|
|
4016
4511
|
JSON.stringify(intake.referenceIndex, null, 2),
|
|
4017
4512
|
"For each index entry, use `readPath` for Pi read-tool calls and keep `path` as the exact Markdown Source References citation. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.",
|
|
4018
4513
|
].join("\n\n"),
|
|
4019
|
-
static: {
|
|
4020
|
-
resultMarkdown: "Markdown module writers completed. Deterministic node 6 validation owns advisory structure/coverage findings; no model review or rewrite was invoked.",
|
|
4021
|
-
},
|
|
4022
4514
|
};
|
|
4023
|
-
const validateCases = shellNode("validate-backend-md-cases-shell", [reviewCases.id], "markdown-cases", "Record advisory findings for Markdown structure and deterministically analyze the final README Coverage Scope and Coverage Matrix against final Case rule/test-point bindings. Validate the classification-policy pair, affected operations/rules, scope evidence and regression floor; require documented OpenAPI completeness only for declared affected operations, while all explicit AC/REQ/BR remain in scope. Detect missing in-scope product/API rules, enum values, invalid equivalence classes, boundaries, format classes, business lifecycle states, GAP/CONFLICT, bidirectional Matrix/Case drift, non-canonical Case IDs, unclassified Test Points, duplicate binding modes and non-cross-cutting Test Points bound by multiple Cases. Do not validate source-reference existence. Write human and machine evidence from the same facts. Keep quality findings advisory, but fail closed after writing the report when secret-shaped values are detected. Coverage FAIL stays advisory.", "Run-owned reports/backend-md-case-validation.md, reports/backend-test-case-coverage-analysis.md and contracts/backend-test-case-coverage-facts.json
|
|
4515
|
+
const validateCases = shellNode("validate-backend-md-cases-shell", [reviewCases.id], "markdown-cases", "Record advisory findings for Markdown structure and deterministically analyze the final README Coverage Scope and Coverage Matrix against final Case rule/test-point bindings. Validate the classification-policy pair, affected operations/rules, scope evidence and regression floor; require documented OpenAPI completeness only for declared affected operations, while all explicit AC/REQ/BR remain in scope. Detect missing in-scope product/API rules, enum values, invalid equivalence classes, boundaries, format classes, business lifecycle states, GAP/CONFLICT, bidirectional Matrix/Case drift, non-canonical Case IDs, unclassified Test Points, duplicate binding modes and non-cross-cutting Test Points bound by multiple Cases. Do not validate source-reference existence. Write human and machine evidence from the same facts. Keep quality findings advisory, but fail closed after writing the report when secret-shaped values are detected. Coverage FAIL stays advisory.", "Run-owned reports/backend-md-case-validation.md, reports/backend-test-case-coverage-analysis.md and contracts/backend-test-case-coverage-facts.json v4 with Coverage Scope plus PASS/FAIL/UNAVAILABLE advisory facts; downstream execution continues.");
|
|
4516
|
+
if (sharedSetup) {
|
|
4517
|
+
validateCases.subtask_prompt += ` Deterministically verify shared setup references: every 引用前置 line must cite exactly the bound path ${sharedSetup.path} with a stable SS anchor (or SS-DEFAULT); any other citation, unbound path, invented step ID or 引用前置 line without a bound document is a FAIL finding. Never rewrite the user document.`;
|
|
4518
|
+
}
|
|
4024
4519
|
// N5 line (sharded): pytest shared-asset plan → manifest shell → map_agent barrier.
|
|
4025
4520
|
// Shared helpers/factories are written once by the plan node; each module's
|
|
4026
4521
|
// test_<stem>.py is written by an independent Pi child (own 16K budget).
|
|
@@ -4035,9 +4530,9 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4035
4530
|
forbiddenPaths: forbidden,
|
|
4036
4531
|
outputContract: "Deterministic pytest generation handoff. Per-module map children write self-contained test_<module>.py files with bounded local fixtures, HTTP logging/redaction and payload builders; no model invocation, JSON, shared-asset writes or pytest execution.",
|
|
4037
4532
|
subtask_prompt: [
|
|
4038
|
-
"Convert testcase/md/** into pytest using upstream environment and advisory validation evidence plus only bounded pytest config/conftest. This node
|
|
4039
|
-
"Output budget protocol (hard, max output <=16K per turn):
|
|
4040
|
-
"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.",
|
|
4533
|
+
"Convert testcase/md/** into pytest using upstream environment and advisory validation evidence plus only bounded pytest config/conftest. This node is a deterministic handoff: NO shared helper/factory/fixture files are generated anywhere in the pipeline; each module's self-contained test_<module>.py (module-local fixtures, HTTP logging/redaction, payload builders) is written by a downstream sharded node. A FAIL advisory report does not authorize inventing missing behavior; use the final Markdown facts that are present.",
|
|
4534
|
+
"Output budget protocol (hard, max output <=16K per turn): downstream module writers write exactly one test_<module>.py per write/edit tool call. Never paste full Python modules into assistant chat. Do not reduce params/assertions/skips semantics to fit.",
|
|
4535
|
+
"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.",
|
|
4041
4536
|
"Generate a reusable HTTP logging helper (or equivalent client wrapper) and call it for every interface request. 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. If shared pytest fixtures are generated, keep their dependency graph in one provider module and require each downstream test module to register that provider with an exact pytest_plugins tuple; importing only the outer fixture is insufficient and will be rejected by fixture-resolution preflight.",
|
|
4042
4537
|
"HTTP response header names are case-insensitive. If the helper stores a lower-case normalized header map, every Content-Type or other header assertion must query the lower-case key (for example `content-type`) or use an explicitly case-insensitive accessor; never call a case-sensitive plain dict with `Content-Type` when the stored key is lower-case. Preserve the actual media-type assertion rather than dropping it.",
|
|
4043
4538
|
"Compare timestamps and other semantically equivalent protocol values by parsed meaning, not byte-for-byte serialization. In particular, normalize valid ISO-8601 instants before equality/order assertions so differences such as omitted trailing fractional seconds do not create TestBug failures; preserve exact-string assertions only when the Markdown explicitly requires representation equality.",
|
|
@@ -4060,7 +4555,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4060
4555
|
outputContract: "Stdout JSON {modules:[{stem}]} parsed from testcase/md/README.md using the same module-stem extractor as the Completeness Gate, so the map_agent shard set deterministically matches the Markdown module index.",
|
|
4061
4556
|
subtask_prompt: "Parse testcase/md/README.md and emit exactly one trailing JSON line {modules:[{stem}]} listing every trusted module stem (table-row testcase/md/<stem>.md mentions and canonical [label](./<stem>.md) relative links only). No file writes.",
|
|
4062
4557
|
shell: {
|
|
4063
|
-
commands: [buildBackendTestModuleManifestShellCommand()],
|
|
4558
|
+
commands: [buildBackendTestModuleManifestShellCommand(layout)],
|
|
4064
4559
|
cwd: ".",
|
|
4065
4560
|
timeoutMs: 60000,
|
|
4066
4561
|
},
|
|
@@ -4084,10 +4579,11 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4084
4579
|
workflowNodeId: "generate-backend-pytest-cases-map",
|
|
4085
4580
|
itemsFrom: "$.nodes['materialize-backend-pytest-module-manifest-shell'].output.modules",
|
|
4086
4581
|
itemName: "item",
|
|
4087
|
-
maxItems:
|
|
4088
|
-
maxExpandedNodes:
|
|
4582
|
+
maxItems: 8,
|
|
4583
|
+
maxExpandedNodes: 8,
|
|
4089
4584
|
childIdPrefix: "generate-backend-pytest-case",
|
|
4090
|
-
tokenBudget: {
|
|
4585
|
+
tokenBudget: { maxTotalTokens: 3000000 },
|
|
4586
|
+
failOnTokenBudgetExhaustion: true,
|
|
4091
4587
|
childTask: {
|
|
4092
4588
|
executor: "pi",
|
|
4093
4589
|
role: "implementer",
|
|
@@ -4112,13 +4608,17 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4112
4608
|
retryPolicy: BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY,
|
|
4113
4609
|
outputContract: "Write exactly one pytest module file testcase/test_<stem>.py whose actual test function region contains the exact Case ID, preferably in the function name or docstring. testcase/md/<module>.md (excluding README.md) maps one-to-one to testcase/test_<module>.py; never merge or split modules. No JSON and no pytest execution.",
|
|
4114
4610
|
subtaskPromptTemplate: [
|
|
4115
|
-
"Convert the single Markdown module testcase/md/{{item.stem}}.md into one self-contained pytest module. After reading the module Markdown and the bounded pytest config/conftest, immediately use write tools to create the single file testcase/test_{{item.stem}}.py. 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.",
|
|
4611
|
+
"Convert the single Markdown module testcase/md/{{item.stem}}.md into one self-contained pytest module. Before writing, also read testcase/md/README.md 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, testcase/md/README.md, and the bounded pytest config/conftest, immediately use write tools to create the single file testcase/test_{{item.stem}}.py. 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.",
|
|
4116
4612
|
"Output budget protocol (hard, max output <=16K per turn): Write exactly one test_{{item.stem}}.py. 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.",
|
|
4117
|
-
"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.",
|
|
4118
|
-
|
|
4613
|
+
"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.",
|
|
4614
|
+
"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.",
|
|
4615
|
+
'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 前置条件/测试数据/自动化映射.',
|
|
4119
4616
|
"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 one `testcase/test_{{item.stem}}.py`. 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 module stem 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.",
|
|
4617
|
+
"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.",
|
|
4120
4618
|
"Keep this module self-contained: define module-local fixtures and helpers directly in testcase/test_{{item.stem}}.py, 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.",
|
|
4619
|
+
"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.",
|
|
4121
4620
|
"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`.",
|
|
4621
|
+
...(sharedSetupPytestPrompt ? [sharedSetupPytestPrompt] : []),
|
|
4122
4622
|
].join("\n\n"),
|
|
4123
4623
|
},
|
|
4124
4624
|
},
|
|
@@ -4131,13 +4631,9 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4131
4631
|
role: "implementer",
|
|
4132
4632
|
executor: "pi",
|
|
4133
4633
|
toolProfile: "write",
|
|
4134
|
-
complexity: "
|
|
4634
|
+
complexity: "MED",
|
|
4135
4635
|
writePolicy: "exclusive",
|
|
4136
|
-
writeSet: [
|
|
4137
|
-
"testcase/**/test_*.py",
|
|
4138
|
-
"testcase/**/helpers/**",
|
|
4139
|
-
"testcase/**/factories/**",
|
|
4140
|
-
],
|
|
4636
|
+
writeSet: ["testcase/**/test_*.py"],
|
|
4141
4637
|
allowedPaths: Array.from(new Set([...ro, "testcase/**"])),
|
|
4142
4638
|
forbiddenPaths: Array.from(new Set([
|
|
4143
4639
|
...forbidden,
|
|
@@ -4147,27 +4643,28 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4147
4643
|
"pyproject.toml",
|
|
4148
4644
|
"setup.cfg",
|
|
4149
4645
|
])),
|
|
4150
|
-
writerOutcomePolicy: { type: "implementation-outcome-v1" },
|
|
4151
|
-
outputContract: "First non-empty line is IMPLEMENTATION_OUTCOME: changed|
|
|
4646
|
+
writerOutcomePolicy: { type: "implementation-outcome-v1", requireChangedFiles: true },
|
|
4647
|
+
outputContract: "First non-empty line is IMPLEMENTATION_OUTCOME: changed|blocked, followed by a concise repair summary. This node runs only for REPAIRABLE initial facts, so already-satisfied is invalid and a successful outcome requires a non-empty bounded diff. Modify only generated pytest scripts/helpers/factories and preserve every Markdown Case, Test Point, primary symbol and assertion meaning.",
|
|
4152
4648
|
subtask_prompt: [
|
|
4153
|
-
"Repair the generated backend pytest asset as one bounded program using the direct upstream collection assessment. This is the only repair attempt and happens before any business test body execution.",
|
|
4154
|
-
"Fix only readiness-proven generated testcase-local defects on initial facts repairPaths: create exact safe missing mapped test_*.py paths, repair syntax/import/symbol/decorator/parameterization,
|
|
4649
|
+
"Repair the generated backend pytest asset as one bounded program using the direct upstream collection assessment. This is the only repair attempt and happens before any business test body execution. Treat any upstream line such as `Repair paths: testcase/test_x.py` as complete authoritative repairPaths evidence. Directly read and edit that testcase path; do not search for separate root-level `contracts/**`, guess a DAG run directory, or require another report artifact. If the read tool successfully returns the testcase file, the path exists—continue the bounded repair and never later claim that file is absent.",
|
|
4650
|
+
"Initial status REPAIRABLE means at least one listed finding remains: `already-satisfied` is forbidden, and you must produce a non-empty bounded diff on repairPaths before returning `IMPLEMENTATION_OUTCOME: changed`. Fix only readiness-proven generated testcase-local defects on initial facts repairPaths: create exact safe missing mapped test_*.py paths, repair syntax/import/symbol/decorator/parameterization, close generated fixture dependencies/plugin registration, and repair initial Markdown-to-pytest correspondence findings (missing/multiple primary symbol, script mismatch, parameter ID or assertion binding). Never invent a business pytest symbol for evidence-only Markdown Cases that declare `脚本/primary symbol=无` with empty variants. For fixture defects inspect both provider and importer; fix ScopeMismatch by aligning fixture scopes or inlining request-scoped values so module fixtures never depend on function fixtures; when a shared fixture depends on sibling fixtures, register the whole provider module through an exact pytest_plugins declaration rather than importing only the outer fixture. Do not create unrelated pytest scripts.",
|
|
4651
|
+
"This is the single pytest incremental synchronization round. The `Findings` in `reports/backend-test-pytest-collection-initial.md` are the mandatory repair checklist: resolve every repairable listed finding on every authoritative `Repair paths` file before considering any other advisory evidence, and never substitute an unrelated scenario-param cleanup for a listed correspondence/collection defect. For every assessment-listed path, compare the effective Markdown Case/Test Points/test data and its `Payload Contract`/`Payload Required Paths`/`Payload Allowed Paths`/`Payload Enum` labels with the generated module. Incrementally add or repair only missing symbols, params, assertions and payload builders. Repair every assessment-listed missing nested path, unexpected key and enum mismatch; preserve exact DTO keys, nested shapes, enum/boundary literals, operation transport and business preconditions; remove guessed replacement keys only when the effective Markdown proves the exact contract. Keep path/query/header identifiers and scenario-control metadata separate from DTO patches and JSON bodies; an `id` used for a path target must be passed to the request path/helper, never inserted into a body patch unless `id` is explicitly listed in Payload Allowed Paths. Flatten every variant into a literal direct `pytest.param(..., id=\"TP-...\")` row; replace `_post_case`/`_put_case` or other parameter-row factories because correspondence and scenario readiness require the actual row values and IDs to be statically visible. Also repair helper call sites to match their defined return signatures; do not tuple-unpack a helper that returns one scalar value.",
|
|
4155
4652
|
"Preserve final testcase/md/** semantics, every Case ID, Rule/Test Point binding, primary symbol, parameter ID, expected status/body/schema assertion, HTTP logging, redaction and truncation behavior.",
|
|
4156
4653
|
"Use local edit only on assessment-listed paths; keep summaries short; never rewrite unrelated modules.",
|
|
4157
|
-
"Do not
|
|
4654
|
+
"Do not reinterpret requirements beyond the effective Markdown and bounded assessment diagnostics. Do not modify Markdown, conftest, pytest config, production code or dependencies.",
|
|
4158
4655
|
"Do not add skip/skipif/xfail, remove tests, reduce collected items, loosen assertions, swallow exceptions, use try/except ImportError fallback, mutate sys.path/PYTHONPATH, or replace the real API with mocks.",
|
|
4159
4656
|
"Do not execute pytest; the deterministic effective collection gate owns the final collection attempt.",
|
|
4160
4657
|
].join("\n\n"),
|
|
4161
4658
|
};
|
|
4162
|
-
const collectionEffective = shellNode("effective-backend-pytest-collection-gate-shell", [collectionAssess.id, repairPytest.id], "markdown-collection-effective", "If initial collection+fixture readiness passed, verify unchanged asset hashes and reuse it. If the single repair ran, rerun scenario-param preflight, final collection and no-business-body fixture resolution once. BLOCKED facts, repair failure, residual fixture failure or hash drift prevent business pytest execution. Materialize canonical backend-test-execution-readiness.json.", "Run-owned effective collection-v3 facts
|
|
4659
|
+
const collectionEffective = shellNode("effective-backend-pytest-collection-gate-shell", [collectionAssess.id, repairPytest.id], "markdown-collection-effective", "If initial collection+fixture readiness passed, verify unchanged asset hashes and reuse it. If the single repair ran, rerun scenario-param preflight, final collection and no-business-body fixture resolution once. Recompute effective correspondence/payload shape and materialize Case/symbol/item `ELIGIBLE|NOT_ELIGIBLE` facts; Payload-contract-bound field-target MISMATCH/UNDETERMINED scenario items, non-exact mappings and payload UNSAFE/UNAVAILABLE items are excluded. Generic request-level nominal/health observations remain advisory when payload shape is SAFE and correspondence exact. BLOCKED facts, repair failure, residual fixture failure, zero eligible items or hash drift prevent business pytest execution. Materialize canonical backend-test-execution-readiness.json v2.", "Run-owned effective collection-v3 facts, reports/backend-test-execution-eligibility.md, contracts/backend-test-execution-eligibility.json and contracts/backend-test-execution-readiness.json v2 proving exact final assets are collectable, fixture-resolvable, payload-safe at item level and hash-bound; initial PASS is reused, repair path records attempt=1.", [], 120000);
|
|
4163
4660
|
collectionEffective.dependsPolicy = "all-or-condition-skip";
|
|
4164
|
-
const traceability = shellNode("backend-test-traceability-gate-shell", [collectionEffective.id], "markdown-traceability", "Deterministically scan only final readiness-authorized Markdown-mapped pytest scripts. Produce bidirectional Markdown module/Case/Test Point ↔ pytest file/primary symbol correspondence and logging findings. scenario-param assessment/repair already ran before collection; consume and display its final PASS/PARTIAL/FAIL/UNAVAILABLE facts without modifying pytest assets after readiness was frozen. Correspondence findings remain advisory; Never block pytest solely on correspondence FAIL.", "Run-owned reports/backend-test-traceability.md, reports/backend-test-markdown-pytest-correspondence.md, contracts/backend-test-markdown-pytest-correspondence-facts.json, reports/backend-test-scenario-param-consistency.md and contracts/backend-test-scenario-param-consistency-facts.json (initial+final) with optional repair audit; PASS/FAIL/UNAVAILABLE correspondence facts bound after effective collection.");
|
|
4661
|
+
const traceability = shellNode("backend-test-traceability-gate-shell", [collectionEffective.id], "markdown-traceability", "Deterministically scan only final readiness-authorized Markdown-mapped pytest scripts. Produce bidirectional Markdown module/Case/Test Point ↔ pytest file/primary symbol correspondence, payload-shape safety, per-item execution eligibility and logging findings. scenario-param assessment/repair already ran before collection; consume and display its final PASS/PARTIAL/FAIL/UNAVAILABLE facts without modifying pytest assets after readiness was frozen. Correspondence findings remain advisory; Never block pytest solely on correspondence FAIL.", "Run-owned reports/backend-test-traceability.md, reports/backend-test-markdown-pytest-correspondence.md, contracts/backend-test-markdown-pytest-correspondence-facts.json, reports/backend-test-scenario-param-consistency.md and contracts/backend-test-scenario-param-consistency-facts.json (initial+final) with optional repair audit; PASS/FAIL/UNAVAILABLE correspondence facts bound after effective collection.");
|
|
4165
4662
|
const manifest = shellNode("backend-test-case-manifest-shell", [traceability.id], "markdown-manifest", "Materialize the canonical Backend Test Case Manifest only from contracts/backend-test-case-coverage-facts.json and contracts/backend-test-markdown-pytest-correspondence-facts.json. Validate schema, task binding, input hashes and freshness; never re-read source semantics, re-analyze Coverage Matrix, rescan pytest symbols or recompute a second set of metrics. Missing/stale/conflicting facts produce partial/unavailable diagnostics rather than fabricated zeros.", "Run-owned contracts/backend-test-case-manifest.json with materializationStatus, sourceFactsIssues, validated coverageScope, coverageSummary, ruleCoverageSummary and correspondenceSummary; this is the single machine input for L-5 and closeout.");
|
|
4166
4663
|
const pytestCommand = [
|
|
4167
4664
|
'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
|
|
4168
|
-
'echo "pytest targets are resolved at runtime from
|
|
4665
|
+
'echo "pytest targets are resolved at runtime from execution-readiness v2 eligibleItemIds"',
|
|
4169
4666
|
].join("; ");
|
|
4170
|
-
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [manifest.id], "markdown-execute-html", "Read canonical contracts/backend-test-execution-readiness.json, verify final asset hashes, then
|
|
4667
|
+
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [manifest.id], "markdown-execute-html", "Read canonical contracts/backend-test-execution-readiness.json v2, verify final asset and eligibility-input hashes, then execute exactly its `eligibleItemIds` pytest node IDs once. Never execute `excludedItems`; retain each exclusion reason as residual TestBug/automation evidence rather than ProductBug. Prefer the deterministic module one-to-one path when a mapped script is missing but the module stem file exists. Generate a native pytest-html self-contained report, then render the primary self-contained Chinese HTML report from the same pytest-html plus final Markdown case metadata without rerun. Keep 测试结论 and quality status; make node 6 Markdown validation + case coverage and node 13 traceability + Markdown-to-pytest correspondence expandable to their full escaped details; show each failure overview item with its original pytest message plus deterministic evidence-based reason analysis; list failure/error case cards before the remaining cases while preserving stable order. Each polished per-case result card includes concise scenario, automation test name, result, duration, and redacted bounded HTTP request parameters/response results for both passed and failed cases. Do not render a technical/execution evidence section in HTML; retain auditable paths and hashes in facts.", "One scoped pytest execution over readiness-authorized eligible pytest items producing a valid pytest-html report with per-case captured output, self-contained reports/backend-test.html, reports/backend-test.md, reports/backend-test-facts.md, a deterministic self-contained reports/backend-test-l5-dashboard.html (machine-computed L-5 metrics, no JSON), and an optional contracts/code-coverage-v1.json when jacocoCoverage is configured (JaCoCo TCP dump → jacoco.xml → parsed; failure-safe); exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
|
|
4171
4668
|
if (execute.shell) {
|
|
4172
4669
|
execute.shell.envAllowlist = collectBackendTestShellEnvAllowlist(sources);
|
|
4173
4670
|
}
|
|
@@ -4245,16 +4742,114 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4245
4742
|
report,
|
|
4246
4743
|
],
|
|
4247
4744
|
};
|
|
4745
|
+
// Plan A: rewrite every backend-test node's layout-dependent strings
|
|
4746
|
+
// (prompts, output contracts, writeSet/allowedPaths, shell commands)
|
|
4747
|
+
// against the resolved layout. The default layout is identity, so the
|
|
4748
|
+
// packaged template stays byte-identical with the historical contract.
|
|
4749
|
+
applyBackendTestLayoutToDagSpec(spec, layout);
|
|
4750
|
+
// Plan B: carry the bound shared-setup document on the spec so runtime
|
|
4751
|
+
// validators (N6 markdown case validation) see the same binding as prompts.
|
|
4752
|
+
if (intake.sharedSetup) {
|
|
4753
|
+
spec.backendTestSharedSetup = { ...intake.sharedSetup };
|
|
4754
|
+
}
|
|
4248
4755
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
4249
4756
|
parseDagSpec(spec);
|
|
4250
4757
|
assertValidDagSpec(spec);
|
|
4251
4758
|
return spec;
|
|
4252
4759
|
}
|
|
4760
|
+
/** Rewrite layout-dependent strings across a compiled backend-test DAG spec (plan A). */
|
|
4761
|
+
function applyBackendTestLayoutToDagSpec(spec, layout) {
|
|
4762
|
+
if (layout.isDefault) {
|
|
4763
|
+
spec.backendTestLayout = layout;
|
|
4764
|
+
return;
|
|
4765
|
+
}
|
|
4766
|
+
const rewrite = (value) => applyBackendTestLayoutToText(value, layout);
|
|
4767
|
+
for (const task of spec.tasks) {
|
|
4768
|
+
task.subtask_prompt = rewrite(task.subtask_prompt);
|
|
4769
|
+
if (task.outputContract)
|
|
4770
|
+
task.outputContract = rewrite(task.outputContract);
|
|
4771
|
+
task.writeSet = task.writeSet?.map(rewrite);
|
|
4772
|
+
task.allowedPaths = task.allowedPaths?.map(rewrite);
|
|
4773
|
+
task.forbiddenPaths = task.forbiddenPaths?.map(rewrite);
|
|
4774
|
+
if (task.dynamicExpansion?.childTask) {
|
|
4775
|
+
const child = task.dynamicExpansion.childTask;
|
|
4776
|
+
if (typeof child.subtaskPromptTemplate === "string") {
|
|
4777
|
+
child.subtaskPromptTemplate = rewrite(child.subtaskPromptTemplate);
|
|
4778
|
+
}
|
|
4779
|
+
if (typeof child.outputContract === "string") {
|
|
4780
|
+
child.outputContract = rewrite(child.outputContract);
|
|
4781
|
+
}
|
|
4782
|
+
if (Array.isArray(child.allowedPaths)) {
|
|
4783
|
+
child.allowedPaths = child.allowedPaths.map(rewrite);
|
|
4784
|
+
}
|
|
4785
|
+
if (Array.isArray(child.writeSet)) {
|
|
4786
|
+
child.writeSet = child.writeSet.map(rewrite);
|
|
4787
|
+
}
|
|
4788
|
+
if (Array.isArray(child.forbiddenPaths)) {
|
|
4789
|
+
child.forbiddenPaths = child.forbiddenPaths.map(rewrite);
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
if (task.shell?.commands) {
|
|
4793
|
+
task.shell.commands = task.shell.commands.map(rewrite);
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
// Rewrite remaining prose-level constraints and success criteria that
|
|
4797
|
+
// reference the generated asset tree.
|
|
4798
|
+
spec.globalConstraints = spec.globalConstraints?.map(rewrite);
|
|
4799
|
+
spec.successCriteria = spec.successCriteria?.map(rewrite);
|
|
4800
|
+
spec.backendTestLayout = layout;
|
|
4801
|
+
}
|
|
4802
|
+
/** Rewrite layout-dependent strings across a compiled frontend-test DAG spec. */
|
|
4803
|
+
function applyFrontendTestLayoutToDagSpec(spec, layout) {
|
|
4804
|
+
if (layout.isDefault) {
|
|
4805
|
+
spec.frontendTestLayout = layout;
|
|
4806
|
+
return;
|
|
4807
|
+
}
|
|
4808
|
+
const rewrite = (value) => applyFrontendTestLayoutToText(value, layout);
|
|
4809
|
+
for (const task of spec.tasks) {
|
|
4810
|
+
task.subtask_prompt = rewrite(task.subtask_prompt);
|
|
4811
|
+
if (task.outputContract)
|
|
4812
|
+
task.outputContract = rewrite(task.outputContract);
|
|
4813
|
+
task.writeSet = task.writeSet?.map(rewrite);
|
|
4814
|
+
task.allowedPaths = task.allowedPaths?.map(rewrite);
|
|
4815
|
+
task.forbiddenPaths = task.forbiddenPaths?.map(rewrite);
|
|
4816
|
+
if (task.dynamicExpansion?.childTask) {
|
|
4817
|
+
const child = task.dynamicExpansion.childTask;
|
|
4818
|
+
if (typeof child.subtaskPromptTemplate === "string") {
|
|
4819
|
+
child.subtaskPromptTemplate = rewrite(child.subtaskPromptTemplate);
|
|
4820
|
+
}
|
|
4821
|
+
if (typeof child.outputContract === "string") {
|
|
4822
|
+
child.outputContract = rewrite(child.outputContract);
|
|
4823
|
+
}
|
|
4824
|
+
if (Array.isArray(child.allowedPaths)) {
|
|
4825
|
+
child.allowedPaths = child.allowedPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
4826
|
+
}
|
|
4827
|
+
if (Array.isArray(child.writeSet)) {
|
|
4828
|
+
child.writeSet = child.writeSet.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
4829
|
+
}
|
|
4830
|
+
if (Array.isArray(child.forbiddenPaths)) {
|
|
4831
|
+
child.forbiddenPaths = child.forbiddenPaths.map((entry) => typeof entry === "string" ? rewrite(entry) : entry);
|
|
4832
|
+
}
|
|
4833
|
+
}
|
|
4834
|
+
if (task.shell?.commands) {
|
|
4835
|
+
task.shell.commands = task.shell.commands.map(rewrite);
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
spec.globalConstraints = spec.globalConstraints?.map(rewrite);
|
|
4839
|
+
spec.successCriteria = spec.successCriteria?.map(rewrite);
|
|
4840
|
+
spec.frontendTestLayout = layout;
|
|
4841
|
+
}
|
|
4842
|
+
function allowedPathsCoverFrontendTestRoot(allowedPaths, testRoot) {
|
|
4843
|
+
const probe = `${testRoot}/_layout_probe_`;
|
|
4844
|
+
return allowedPaths.some((pattern) => pathMatchesPattern(testRoot, pattern) ||
|
|
4845
|
+
pathMatchesPattern(probe, pattern));
|
|
4846
|
+
}
|
|
4253
4847
|
// ---------------------------------------------------------------------------
|
|
4254
4848
|
// Frontend browser-test RAG DAG template
|
|
4255
4849
|
// ---------------------------------------------------------------------------
|
|
4256
4850
|
function buildFrontendTestHybridDag(sources) {
|
|
4257
4851
|
const rawFrontendTest = sources.taskConfig.frontendTest;
|
|
4852
|
+
const layout = resolveFrontendTestLayout(rawFrontendTest);
|
|
4258
4853
|
const config = {
|
|
4259
4854
|
// Default 32: common FE suites cover ~24 AC with multi-dimension cases; 20 caused map maxExpandedNodes failures.
|
|
4260
4855
|
maxCasesPerBatch: rawFrontendTest?.maxCasesPerBatch ?? 32,
|
|
@@ -4276,28 +4871,32 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4276
4871
|
};
|
|
4277
4872
|
const declaredRequirementIds = buildDagSourceBinding(sources).requirementIds;
|
|
4278
4873
|
const declaredAcIds = declaredRequirementIds.filter((id) => /^AC(?:-[A-Z0-9]+)+$/i.test(id));
|
|
4874
|
+
// Optional project-local UI anchor ledger: only mention it in prompts when it
|
|
4875
|
+
// exists so ledger-less projects keep generating without noise.
|
|
4876
|
+
const hasUiAnchorsLedger = Boolean(sources.repoRoot &&
|
|
4877
|
+
existsSync(path.join(sources.repoRoot, layout.ragDir, "ui-anchors.md")));
|
|
4878
|
+
const uiAnchorsLedgerInstruction = hasUiAnchorsLedger
|
|
4879
|
+
? `Also read ${layout.ragDir}/ui-anchors.md (UI anchor ledger). Every passing find assertion in generated cases must quote a ledger row whose 状态 is 有效 for the matching page x state section; rows marked 不可断言/失效 must not be used as passing assertions. When an AC names a control absent from the ledger section for that state, emit a blocked case note (blockedReason unique-control-unavailable) instead of exploratory find steps, and follow ledger 备注 alternatives (split-node short literals) exactly.`
|
|
4880
|
+
: "";
|
|
4279
4881
|
const reviewMode = config.reviewMode;
|
|
4280
4882
|
const blockingReview = reviewMode === "blocking";
|
|
4281
4883
|
const strictOutcomeGate = config.strictOutcomeGate;
|
|
4282
4884
|
const maxRerunAttempts = config.maxRerunAttempts;
|
|
4283
4885
|
const enableRetrospect = config.reports?.retrospect === true;
|
|
4284
4886
|
const enableL5Report = config.reports?.l5 !== false;
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
pattern === "**");
|
|
4288
|
-
if (!hasFrontendTestWriteScope) {
|
|
4289
|
-
throw new Error('frontend-test requires task.json allowedPaths to include "testcase/frontend/**" (or an explicit containing glob).');
|
|
4887
|
+
if (!allowedPathsCoverFrontendTestRoot(sources.taskConfig.allowedPaths, layout.testRoot)) {
|
|
4888
|
+
throw new Error(`frontend-test requires task.json allowedPaths to cover "${layout.testRoot}/**" (or an explicit containing glob).`);
|
|
4290
4889
|
}
|
|
4291
4890
|
const forbidden = commonForbiddenPaths(sources);
|
|
4292
|
-
const ragWriteSet = [
|
|
4891
|
+
const ragWriteSet = [`${layout.ragDir}/**`];
|
|
4293
4892
|
const caseDraftWriteSet = [
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4893
|
+
`${layout.casesDir}/FE-*.md`,
|
|
4894
|
+
`${layout.casesDir}/index.md`,
|
|
4895
|
+
`${layout.casesDir}/manifest.draft.json`,
|
|
4297
4896
|
];
|
|
4298
4897
|
// The shell materializer alone owns the final manifest boundary.
|
|
4299
|
-
const casesWriteSet = [
|
|
4300
|
-
const evidenceRoot =
|
|
4898
|
+
const casesWriteSet = [`${layout.casesDir}/**`];
|
|
4899
|
+
const evidenceRoot = layout.evidenceDir;
|
|
4301
4900
|
const declaredAcIdsLiteral = JSON.stringify(declaredAcIds);
|
|
4302
4901
|
const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
|
|
4303
4902
|
const checklistScript = [
|
|
@@ -4316,6 +4915,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4316
4915
|
"const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;",
|
|
4317
4916
|
,
|
|
4318
4917
|
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
4918
|
+
"const truncatedAcRe=/^AC-FE-[A-Z]+$/i;",
|
|
4919
|
+
"const truncatedUndeclaredAcRe=/^(?:AC-\\d{3}|AC-FE-[A-Z]+)$/i;",
|
|
4319
4920
|
"for(const c of manifest.cases){",
|
|
4320
4921
|
" const id=c&&c.caseId||'?';",
|
|
4321
4922
|
" if(typeof c.caseId!=='string'||!caseIdRe.test(c.caseId))issues.push({ruleId:'case-id-shape',caseId:id,detail:'caseId must be FE-<FEATURE>-<NNN>-<dimension>, never AC-FE-*'});",
|
|
@@ -4334,6 +4935,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4334
4935
|
" if(typeof ac!=='string'){issues.push({ruleId:'ac-id-shape',caseId:id,detail:String(ac)+' must look like AC-FE-001'});continue;}",
|
|
4335
4936
|
" if(/^FE-/i.test(ac)){issues.push({ruleId:'ac-id-is-case',caseId:id,detail:ac+' looks like caseId; acIds must be AC-*'});continue;}",
|
|
4336
4937
|
" if(!acIdRe.test(ac)){issues.push({ruleId:'ac-id-shape',caseId:id,detail:ac+' must look like AC-FE-001'});continue;}",
|
|
4938
|
+
" if(truncatedAcRe.test(ac)||(declaredAc.size===0&&truncatedUndeclaredAcRe.test(ac))){issues.push({ruleId:'ac-id-truncated',caseId:id,detail:ac+' is a truncated acceptance id (missing feature prefix or sequence number)'});continue;}",
|
|
4337
4939
|
" if(declaredAc.size>0&&!declaredAc.has(ac))issues.push({ruleId:'unknown-ac',caseId:id,detail:ac+' not in task sourceBinding.requirementIds'});",
|
|
4338
4940
|
" }",
|
|
4339
4941
|
" }",
|
|
@@ -4365,6 +4967,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4365
4967
|
`const declaredAc=new Set(${declaredAcIdsLiteral});`,
|
|
4366
4968
|
"const seen=new Set(); const seenCasePath=new Set(); const seenEvidenceDir=new Set();",
|
|
4367
4969
|
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
4970
|
+
"const truncatedAcRe=/^AC-FE-[A-Z]+$/i;",
|
|
4971
|
+
"const truncatedUndeclaredAcRe=/^(?:AC-\\d{3}|AC-FE-[A-Z]+)$/i;",
|
|
4368
4972
|
"for(const c of manifest.cases){",
|
|
4369
4973
|
" if(!c||typeof c.caseId!=='string'||!/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/.test(c.caseId)) fail('case-id-shape','caseId must be FE-*, never AC-FE-*: '+String(c&&c.caseId));",
|
|
4370
4974
|
" if(/^AC-/i.test(c.caseId)) fail('case-id-is-ac','caseId must not be an acceptance id: '+c.caseId);",
|
|
@@ -4372,7 +4976,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4372
4976
|
" seen.add(c.caseId);",
|
|
4373
4977
|
" if(typeof c.dimension!=='string'||!dims.has(c.dimension)) fail('invalid-dimension',String(c.dimension));",
|
|
4374
4978
|
" if(!Array.isArray(c.acIds)||c.acIds.length===0||c.acIds.some(a=>typeof a!=='string'||!a.trim())) fail('ac-mapping','invalid acIds for '+c.caseId);",
|
|
4375
|
-
" for(const ac of c.acIds){ if(!acIdRe.test(ac)) fail('ac-id-shape','acIds entry must be AC-* acceptance id, not caseId: '+ac); if(declaredAc.size>0&&!declaredAc.has(ac)) fail('unknown-ac',ac+' not in sourceBinding; repair generator input or AC list'); }",
|
|
4979
|
+
" for(const ac of c.acIds){ if(!acIdRe.test(ac)) fail('ac-id-shape','acIds entry must be AC-* acceptance id, not caseId: '+ac); if(truncatedAcRe.test(ac)||(declaredAc.size===0&&truncatedUndeclaredAcRe.test(ac))) fail('ac-id-truncated','acIds entry is a truncated acceptance id (missing feature prefix or sequence number): '+ac); if(declaredAc.size>0&&!declaredAc.has(ac)) fail('unknown-ac',ac+' not in sourceBinding; repair generator input or AC list'); }",
|
|
4376
4980
|
" c.casePath='testcase/frontend/cases/'+c.caseId+'.md';",
|
|
4377
4981
|
" c.evidenceDir='testcase/frontend/evidence/'+c.caseId+'/';",
|
|
4378
4982
|
" for(const k of ['casePath','evidenceDir']){ const v=c[k]; if(typeof v!=='string'||path.isAbsolute(v)||v.includes('..')) fail('unsafe-path',k+': '+String(v)); }",
|
|
@@ -4432,15 +5036,11 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4432
5036
|
writeSet: ragWriteSet,
|
|
4433
5037
|
allowedPaths: [...ragWriteSet],
|
|
4434
5038
|
forbiddenPaths: forbidden,
|
|
4435
|
-
outputContract: "Write testcase/frontend/rag/standard-scenarios.v1.json for generate-time standard scenario coverage.",
|
|
4436
|
-
subtask_prompt: "Prepare frontend-test package: materialize standard-scenarios.v1.json into the RAG package.",
|
|
5039
|
+
outputContract: "Write testcase/frontend/rag/standard-scenarios.v1.json for generate-time standard scenario coverage. Copy docs/templates or harness.json governanceRoot templates (including ai_workspace/loop-agent/templates) when present; otherwise write the minimal STD-FE-SMOKE-ENTRY fallback.",
|
|
5040
|
+
subtask_prompt: "Prepare frontend-test package: materialize standard-scenarios.v1.json into the RAG package from docs/templates, governanceRoot/templates, or the init-projected ai_workspace/loop-agent/templates path.",
|
|
4437
5041
|
shell: {
|
|
4438
|
-
commands: [
|
|
4439
|
-
|
|
4440
|
-
"node -e",
|
|
4441
|
-
JSON.stringify("const fs=require('fs'),path=require('path');const dest='testcase/frontend/rag/standard-scenarios.v1.json';const candidates=[path.join('docs','templates','frontend-test-standard-scenarios.v1.json')];let src=null;for(const c of candidates){if(fs.existsSync(c)){src=c;break;}}fs.mkdirSync(path.dirname(dest),{recursive:true});if(src){fs.copyFileSync(src,dest);process.stdout.write(JSON.stringify({status:'copied',from:src,to:dest}));}else{const minimal={schemaVersion:1,id:'frontend-test-standard-scenarios-v1',scenarios:[{id:'STD-FE-SMOKE-ENTRY',title:'入口可打开',category:'smoke',priority:'must',testPoints:['open'],minCases:1}]};fs.writeFileSync(dest,JSON.stringify(minimal,null,2)+'\n');process.stdout.write(JSON.stringify({status:'fallback',to:dest}));}"),
|
|
4442
|
-
].join(" "),
|
|
4443
|
-
],
|
|
5042
|
+
commands: [],
|
|
5043
|
+
frontendTestStandardScenarios: {},
|
|
4444
5044
|
cwd: ".",
|
|
4445
5045
|
timeoutMs: 60_000,
|
|
4446
5046
|
},
|
|
@@ -4476,15 +5076,11 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4476
5076
|
writeSet: ragWriteSet,
|
|
4477
5077
|
allowedPaths: [...ragWriteSet],
|
|
4478
5078
|
forbiddenPaths: forbidden,
|
|
4479
|
-
outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable (
|
|
4480
|
-
subtask_prompt: "Parse the resolved absolute baseUrl from testcase/frontend/rag/context.md, reject production/non-http(s)/credential/query/fragment URLs, then probe it with curl HEAD and GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. 4xx/5xx/DNS/timeout/
|
|
5079
|
+
outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable with errorClass (connection-refused / dns-unresolved / connect-timeout / http-N / curl-exit-N). Node ERROR so generate/map do not run. Does not start the app.",
|
|
5080
|
+
subtask_prompt: "Parse the resolved absolute baseUrl from testcase/frontend/rag/context.md, reject production/non-http(s)/credential/query/fragment URLs, then probe it with curl HEAD and GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. Connection refused (curl 7) records errorClass=connection-refused and tells the operator to start the local app (scripts/serve.sh or npm start) then rerun from this node. 4xx/5xx/DNS/timeout/TLS => blockedReason frontend-base-url-unreachable with a distinct errorClass. Missing curl => blockedReason curl-unavailable. Do not start the app. Fixture/reset remain soft guidance.",
|
|
4481
5081
|
shell: {
|
|
4482
|
-
commands: [
|
|
4483
|
-
|
|
4484
|
-
"node -e",
|
|
4485
|
-
JSON.stringify(`const fs=require('fs');const {spawnSync}=require('child_process');const p='testcase/frontend/rag/context.md';const probePath='testcase/frontend/rag/environment-probe.json';function writeProbe(obj){try{fs.mkdirSync('testcase/frontend/rag',{recursive:true});fs.writeFileSync(probePath,JSON.stringify(obj,null,2)+'\\n');let ctx=fs.existsSync(p)?fs.readFileSync(p,'utf8'):'';const line='environmentProbe: '+obj.status+(obj.blockedReason?(' ('+obj.blockedReason+')'):'');if(/environmentProbe\\s*[:=]/i.test(ctx)){ctx=ctx.replace(/environmentProbe\\s*[:=]\\s*.*/i,line);}else{ctx=ctx.trimEnd()+'\\n\\n'+line+'\\n';}fs.writeFileSync(p,ctx);}catch(e){console.error('probe-write-failed',e&&e.message||e);}}function redactUrl(u){try{const x=new URL(u);x.username='';x.password='';if(x.search){x.search='';}return x.toString();}catch(_){return String(u).replace(/\\/\\/[^@\\s]+@/g,'//');}}function failBlocked(reason,extra){const payload=Object.assign({status:'unreachable',blockedReason:reason,baseUrlRedacted:extra&&extra.baseUrlRedacted||null,httpStatus:extra&&extra.httpStatus||null,method:extra&&extra.method||null,curlExit:extra&&extra.curlExit||null,errorClass:extra&&extra.errorClass||null},extra||{});writeProbe(payload);console.error('frontend-test preflight blocked: '+JSON.stringify({blockedReason:reason,baseUrl:payload.baseUrlRedacted,httpStatus:payload.httpStatus,errorClass:payload.errorClass}));throw new Error('frontend-test preflight blocked: '+reason);}if(!fs.existsSync(p))throw new Error('missing '+p);const s=fs.readFileSync(p,'utf8');const patterns=[/baseUrl\\s*[:=]\\s*["'\\x60]?((?:https?):\\/\\/[^\\s"'\\x60<>]+)/i,/base[-_ ]url\\s*[:=]\\s*["'\\x60]?((?:https?):\\/\\/[^\\s"'\\x60<>]+)/i,/playwright-cli open --browser=chrome\\s+((?:https?):\\/\\/[^\\s"'\\x60<>]+)/i,/(https?:\\/\\/(?:localhost|127\\.0\\.0\\.1)[^\\s)\\}\\],"']*)/i];let baseUrl=null;for(const re of patterns){const m=s.match(re);if(m){baseUrl=m[1];break;}}if(!baseUrl)throw new Error('frontend-test preflight missing absolute baseUrl from context.md');baseUrl=baseUrl.replace(/[)\\}\\],."'\\x60]+$/,'');const sourceMatch=s.match(/baseUrlSource\\s*[:=]\\s*([^\\r\\n]+)/i);const baseUrlSource=sourceMatch?sourceMatch[1].trim():'context.md';let parsed;try{parsed=new URL(baseUrl);}catch(_){throw new Error('baseUrl must be absolute http(s): '+baseUrl);}if((parsed.protocol!=='http:'&&parsed.protocol!=='https:')||parsed.username||parsed.password||parsed.search||parsed.hash)throw new Error('unsafe baseUrl from context.md: '+redactUrl(baseUrl));if(/(?:^|\\.)(?:www\\.)?[^.]*(?:prod|production)/i.test(parsed.hostname))throw new Error('production URL forbidden: '+redactUrl(baseUrl));baseUrl=parsed.toString();const safe=redactUrl(baseUrl);const curlCheck=spawnSync('curl',['--version'],{encoding:'utf8'});if(curlCheck.error||curlCheck.status!==0){failBlocked('curl-unavailable',{baseUrlRedacted:safe,errorClass:'curl-missing'});}function probe(method){const args=['-sS','-o','/dev/null','-w','%{http_code}','--connect-timeout','3','--max-time','8','-X',method,'-L','--max-redirs','3','--http1.1','--proto-redir','=http,https',safe];const r=spawnSync('curl',args,{encoding:'utf8'});return r;}let used='HEAD';let r=probe('HEAD');let code=String(r.stdout||'').trim();let statusNum=parseInt(code,10);const headRejected=r.status!==0||!statusNum||statusNum===405||statusNum===501;if(headRejected){used='GET';r=probe('GET');code=String(r.stdout||'').trim();statusNum=parseInt(code,10);}const ok=statusNum>=200&&statusNum<400;if(!ok){const errClass=r.error?'spawn-error':(r.status!==0?'curl-exit-'+r.status:('http-'+statusNum));failBlocked('frontend-base-url-unreachable',{baseUrlRedacted:safe,httpStatus:statusNum||null,method:used,curlExit:r.status,errorClass:errClass});}writeProbe({status:'reachable',blockedReason:null,baseUrl:safe,baseUrlRedacted:safe,baseUrlSource,httpStatus:statusNum,method:used,curlExit:r.status});console.log('frontend-test-execution-v1 validated contextBaseUrl='+safe+' source='+baseUrlSource+' probe=reachable method='+used+' httpStatus='+statusNum);`),
|
|
4486
|
-
].join(" "),
|
|
4487
|
-
],
|
|
5082
|
+
commands: [],
|
|
5083
|
+
frontendTestEnvironmentProbe: {},
|
|
4488
5084
|
cwd: ".",
|
|
4489
5085
|
timeoutMs: 60000,
|
|
4490
5086
|
},
|
|
@@ -4505,6 +5101,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4505
5101
|
subtask_prompt: [
|
|
4506
5102
|
"Use skill playwright-cli-case-generator.",
|
|
4507
5103
|
"Read testcase/frontend/rag/standard-scenarios.v1.json and cover priority=must scenarios (or record GAP in coverage-map). Include ## 测试点 and ## 测试步骤 in each case.",
|
|
5104
|
+
...(uiAnchorsLedgerInstruction ? [uiAnchorsLedgerInstruction] : []),
|
|
4508
5105
|
"Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and the draft case paths testcase/frontend/cases/FE-*.md, testcase/frontend/cases/index.md, and testcase/frontend/cases/manifest.draft.json. Write only those same draft paths. Do not write testcase/frontend/cases/manifest.json.",
|
|
4509
5106
|
"Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir).",
|
|
4510
5107
|
"HARD ID CONTRACT (do not confuse these):",
|
|
@@ -4664,7 +5261,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4664
5261
|
subtaskPromptTemplate: [
|
|
4665
5262
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new). playwright-cli-only: never bare Playwright CLI/API/test runner; no fallback.",
|
|
4666
5263
|
"Use the structured playwright_cli tool for every browser action. Do not request or search for bash. Do not execute raw shell commands. Translate each playwright-cli line in the case Markdown into one playwright_cli tool call ({command, args?, timeoutSeconds?}).",
|
|
4667
|
-
"1) Read the concrete baseUrl from testcase/frontend/rag/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal `eX`/`eY`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.",
|
|
5264
|
+
"1) Read the concrete baseUrl from testcase/frontend/rag/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal `eX`/`eY`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. Unique-control early stop: when an AC names a specific control that is absent from the first post-navigation snapshot after reaching the required state (and no setup-required gate appeared), do ONE find with the case's literal as evidence; if it returns 0 matches, record status=blocked with blockedReason unique-control-unavailable citing that single find - do NOT re-explore with alternative literals, repeated snapshots, navigation detours, or rerun loops. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.",
|
|
4668
5265
|
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId={{case.caseId}}, status passed|failed|blocked, evidencePaths (relative under evidenceDir). blocked needs non-empty blockedReason. After writing, self-check the same contract; if self-check fails, rewrite both files as status=blocked blockedReason=invalid-evidence-shape (never leave missing/malformed evidence).",
|
|
4669
5266
|
"Business failed/blocked is a recorded result, not a node failure. Close browser via playwright_cli command=close. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
|
|
4670
5267
|
].join("\n\n"),
|
|
@@ -4687,7 +5284,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4687
5284
|
"const manifestPath='testcase/frontend/cases/manifest.json';",
|
|
4688
5285
|
"if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}",
|
|
4689
5286
|
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];",
|
|
4690
|
-
"for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||missing)&&attempt<" + String(round) + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
|
|
5287
|
+
"for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(r.status==='failed')reason='failed-retry';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||reason==='failed-retry'||missing)&&attempt<" + String(round) + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
|
|
4691
5288
|
`fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/${candidateArtifact}',JSON.stringify({schemaVersion:1,cases},null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));`,
|
|
4692
5289
|
].join("");
|
|
4693
5290
|
tasks.push({
|
|
@@ -4755,6 +5352,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4755
5352
|
"RERUN attempt {{case.rerunAttempt}} for {{case.caseId}} (reason={{case.reason}}). Rewrite the authoritative execution.md and case-result.json; its final status replaces the earlier case result.",
|
|
4756
5353
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session). Use structured playwright_cli only; headless open.",
|
|
4757
5354
|
"Read the concrete baseUrl from testcase/frontend/rag/context.md and start via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>]. Passed requires open → find → cleanup receipts.",
|
|
5355
|
+
"Unique-control early stop: if the first post-navigation snapshot lacks the control an AC names (and no setup-required gate appeared), one find returning 0 matches is sufficient evidence - write status=blocked blockedReason unique-control-unavailable instead of exploratory retries.",
|
|
4758
5356
|
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId, status, evidencePaths, rerunAttempt={{case.rerunAttempt}}. Write fixed sections `### 执行摘要` and `### 实际执行步骤` to execution.md when available.",
|
|
4759
5357
|
].join("\n\n"),
|
|
4760
5358
|
},
|
|
@@ -4890,6 +5488,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4890
5488
|
verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig),
|
|
4891
5489
|
tasks,
|
|
4892
5490
|
};
|
|
5491
|
+
applyFrontendTestLayoutToDagSpec(spec, layout);
|
|
4893
5492
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
4894
5493
|
parseDagSpec(spec);
|
|
4895
5494
|
assertValidDagSpec(spec);
|
|
@@ -5928,7 +6527,7 @@ async function resolveTaskContractBindingForGenerate(sources) {
|
|
|
5928
6527
|
}
|
|
5929
6528
|
return synthesizeInMemoryTaskContractBinding(sources);
|
|
5930
6529
|
}
|
|
5931
|
-
async function buildHybridDagForTemplate(sources, template) {
|
|
6530
|
+
async function buildHybridDagForTemplate(sources, template, options = {}) {
|
|
5932
6531
|
const taskContractBinding = await resolveTaskContractBindingForGenerate(sources);
|
|
5933
6532
|
let spec;
|
|
5934
6533
|
if (template === "frontend-implementation") {
|
|
@@ -5952,6 +6551,13 @@ async function buildHybridDagForTemplate(sources, template) {
|
|
|
5952
6551
|
spec = await buildSupervisedHybridDag(standard, sources);
|
|
5953
6552
|
}
|
|
5954
6553
|
applyProjectGovernanceReview(spec, template, sources);
|
|
6554
|
+
// Backend implementation templates only (D1 outer gate): assign the two
|
|
6555
|
+
// Pi extension buckets after all template-specific nodes exist.
|
|
6556
|
+
if (template === "standard-dag" ||
|
|
6557
|
+
template === "review-gated-dag" ||
|
|
6558
|
+
template === "supervised-implementation") {
|
|
6559
|
+
applyBackendPiExtensionBuckets(spec);
|
|
6560
|
+
}
|
|
5955
6561
|
// New generate path always emits DagSpec v4 + bindings.
|
|
5956
6562
|
spec.version = 4;
|
|
5957
6563
|
if (!spec.runtimeContract) {
|
|
@@ -5960,7 +6566,12 @@ async function buildHybridDagForTemplate(sources, template) {
|
|
|
5960
6566
|
spec.sourceBinding = buildDagSourceBinding(sources);
|
|
5961
6567
|
spec.taskContractBinding = taskContractBinding;
|
|
5962
6568
|
assertNoGovernanceFlagOnDisallowedTemplate(spec, template);
|
|
5963
|
-
|
|
6569
|
+
if (template === "standard-dag" ||
|
|
6570
|
+
template === "review-gated-dag" ||
|
|
6571
|
+
template === "supervised-implementation") {
|
|
6572
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
6573
|
+
}
|
|
6574
|
+
spec = parseDagSpec(spec);
|
|
5964
6575
|
assertValidDagSpec(spec);
|
|
5965
6576
|
return spec;
|
|
5966
6577
|
}
|
|
@@ -6011,6 +6622,36 @@ export async function buildHybridDagFromTask(sources, options = {}) {
|
|
|
6011
6622
|
function cloneTask(task, patch = {}) {
|
|
6012
6623
|
return { ...task, ...patch };
|
|
6013
6624
|
}
|
|
6625
|
+
/**
|
|
6626
|
+
* Backend-implementation Pi extension buckets (plan 2026-08-21 D3):
|
|
6627
|
+
* - read side: every non-writer, non-closeout Pi node gets navigation
|
|
6628
|
+
* extensions (pi-codegraph + pi-lens);
|
|
6629
|
+
* - write side: bounded writers get only pi-codegraph (pi-lens registers
|
|
6630
|
+
* ast_grep_replace, which can bypass the writeSet — not loading it is
|
|
6631
|
+
* simpler and safer than filtering after load);
|
|
6632
|
+
* - everything else (closeout, shell, static, other templates) stays closed.
|
|
6633
|
+
* Idempotent: never overwrites an explicit piExtensions a task declares.
|
|
6634
|
+
*/
|
|
6635
|
+
function applyBackendPiExtensionBuckets(spec) {
|
|
6636
|
+
for (const task of spec.tasks) {
|
|
6637
|
+
if (task.piExtensions !== undefined)
|
|
6638
|
+
continue;
|
|
6639
|
+
if (task.executor !== "pi")
|
|
6640
|
+
continue;
|
|
6641
|
+
if (task.id === "closeout-pi")
|
|
6642
|
+
continue;
|
|
6643
|
+
task.piExtensions =
|
|
6644
|
+
task.toolProfile === "write" ? ["pi-codegraph"] : ["pi-codegraph", "pi-lens"];
|
|
6645
|
+
}
|
|
6646
|
+
}
|
|
6647
|
+
function stampTargetTemplateTransientRetryProfile(spec) {
|
|
6648
|
+
for (const task of spec.tasks) {
|
|
6649
|
+
if (isTargetTemplateImplementPi(task) ||
|
|
6650
|
+
isCanonicalFinalVerifyShellRetryCandidate(task)) {
|
|
6651
|
+
task.transientRetryProfile = TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE;
|
|
6652
|
+
}
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
6014
6655
|
/**
|
|
6015
6656
|
* Apply default Pi retry policies to generated DAG nodes:
|
|
6016
6657
|
* - safe read-only planner/scout/reviewer/verifier/supervisor/closeout
|
|
@@ -6031,6 +6672,10 @@ function applyDefaultReadOnlyRetryPolicy(spec) {
|
|
|
6031
6672
|
: DEFAULT_READ_ONLY_PI_RETRY_POLICY;
|
|
6032
6673
|
continue;
|
|
6033
6674
|
}
|
|
6675
|
+
if (isTargetTemplateImplementPi(task)) {
|
|
6676
|
+
task.retryPolicy = TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY;
|
|
6677
|
+
continue;
|
|
6678
|
+
}
|
|
6034
6679
|
if (isWriterTransportRetryCandidate(task)) {
|
|
6035
6680
|
task.retryPolicy = WRITER_TRANSPORT_RETRY_POLICY;
|
|
6036
6681
|
}
|
|
@@ -6214,6 +6859,12 @@ function buildGovernanceStandardGateNode(sources) {
|
|
|
6214
6859
|
verdictGate: {
|
|
6215
6860
|
fromNodeId: "verify-pi",
|
|
6216
6861
|
accept: ["VERDICT: pass"],
|
|
6862
|
+
// A legitimate request-revision is a convergence routing result only
|
|
6863
|
+
// when the task actually has a bounded fix loop. Without that loop the
|
|
6864
|
+
// gate must fail closed instead of letting closeout run.
|
|
6865
|
+
...((sources.taskConfig.maxFixLoops ?? 0) > 0
|
|
6866
|
+
? { routingAccept: ["VERDICT: request-revision"] }
|
|
6867
|
+
: {}),
|
|
6217
6868
|
label: "governance-standard",
|
|
6218
6869
|
lineMode: "first-verdict-line",
|
|
6219
6870
|
},
|
|
@@ -6239,6 +6890,33 @@ function insertGovernanceStandardGate(spec, sources) {
|
|
|
6239
6890
|
spec.tasks.splice(closeoutIndex, 0, gate);
|
|
6240
6891
|
closeout.depends_on = ["governance-standard-gate-shell"];
|
|
6241
6892
|
closeout.failureAwareDependsOn = ["governance-standard-gate-shell"];
|
|
6893
|
+
// Governance verify convergence: when the task's bounded-repair budget
|
|
6894
|
+
// allows it, a legitimate verify-pi `request-revision` re-enters a bounded
|
|
6895
|
+
// implement-repair loop (see convergence controller) instead of terminating
|
|
6896
|
+
// the run at the gate. The gate routes `request-revision` as a non-error
|
|
6897
|
+
// result so closeout stays failure-aware rather than cascade-skipped.
|
|
6898
|
+
const maxFixLoops = sources.taskConfig.maxFixLoops ?? 0;
|
|
6899
|
+
if (maxFixLoops > 0) {
|
|
6900
|
+
// verify-shell exists only when the task planned final verify commands;
|
|
6901
|
+
// build the chain from nodes this spec actually contains so
|
|
6902
|
+
// hasConvergenceChain never rejects the governance shape.
|
|
6903
|
+
const governanceChainIds = [
|
|
6904
|
+
"implement-pi",
|
|
6905
|
+
...(spec.tasks.some((task) => task.id === "verify-shell")
|
|
6906
|
+
? ["verify-shell"]
|
|
6907
|
+
: []),
|
|
6908
|
+
"verify-pi",
|
|
6909
|
+
"governance-standard-gate-shell",
|
|
6910
|
+
];
|
|
6911
|
+
spec.convergence = {
|
|
6912
|
+
enabled: true,
|
|
6913
|
+
maxPasses: maxFixLoops + 1,
|
|
6914
|
+
stopOnVerdictPass: true,
|
|
6915
|
+
stopOnHardVerifyPass: true,
|
|
6916
|
+
pauseOnRegression: sources.taskConfig.convergence?.pauseOnRegression ?? true,
|
|
6917
|
+
chainNodeIds: governanceChainIds,
|
|
6918
|
+
};
|
|
6919
|
+
}
|
|
6242
6920
|
}
|
|
6243
6921
|
function buildReviewGatedHybridDag(standard, sources) {
|
|
6244
6922
|
const spec = {
|
|
@@ -6258,6 +6936,7 @@ function buildReviewGatedHybridDag(standard, sources) {
|
|
|
6258
6936
|
}));
|
|
6259
6937
|
spec.tasks.splice(spec.tasks.length - 1, 0, buildReviewNode(sources), buildReviewVerdictRecoveryNode(sources), buildReviewGateNode(sources));
|
|
6260
6938
|
applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
|
|
6939
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
6261
6940
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
6262
6941
|
parseDagSpec(spec);
|
|
6263
6942
|
assertValidDagSpec(spec);
|
|
@@ -6589,6 +7268,9 @@ async function buildHardVerifyNode(sources) {
|
|
|
6589
7268
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
6590
7269
|
outputContract: "Archived hard verification stdout/stderr with exit codes; no worktree writes.",
|
|
6591
7270
|
subtask_prompt: "Run hard verification after repair round.",
|
|
7271
|
+
transientRetryProfile: sources.verifyCommands && commands.length > 0
|
|
7272
|
+
? TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE
|
|
7273
|
+
: undefined,
|
|
6592
7274
|
shell: {
|
|
6593
7275
|
commands,
|
|
6594
7276
|
verifyEvidence: buildVerifyEvidence({
|
|
@@ -6759,6 +7441,7 @@ async function buildSupervisedHybridDag(standard, sources) {
|
|
|
6759
7441
|
],
|
|
6760
7442
|
};
|
|
6761
7443
|
applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
|
|
7444
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
6762
7445
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
6763
7446
|
parseDagSpec(spec);
|
|
6764
7447
|
assertValidDagSpec(spec);
|
|
@@ -6789,6 +7472,11 @@ export async function writeHybridDagDraft(sources, outputPath, options = {}) {
|
|
|
6789
7472
|
? await prepareFrontendMockSources(sources, routingProjectCapability)
|
|
6790
7473
|
: sources;
|
|
6791
7474
|
const spec = await buildHybridDagForTemplate(preparedSources, template);
|
|
7475
|
+
if (options.rerunFeedback) {
|
|
7476
|
+
spec.rerunFeedback = bindDagRerunFeedback(options.rerunFeedback, spec);
|
|
7477
|
+
parseDagSpec(spec);
|
|
7478
|
+
assertValidDagSpec(spec);
|
|
7479
|
+
}
|
|
6792
7480
|
if (preparedSources.repoRoot && dagHasWriterExecution(spec)) {
|
|
6793
7481
|
await requireManagedTaskContractBinding({
|
|
6794
7482
|
repoRoot: preparedSources.repoRoot,
|
|
@@ -6816,5 +7504,6 @@ export async function initHybridDagFromTask(repoRoot, taskId, options = {}) {
|
|
|
6816
7504
|
const outputPath = options.outputPath ?? getTaskPaths(repoRoot, taskId).dagDraftPath;
|
|
6817
7505
|
return writeHybridDagDraft(sources, outputPath, {
|
|
6818
7506
|
template: options.template,
|
|
7507
|
+
rerunFeedback: options.rerunFeedback,
|
|
6819
7508
|
});
|
|
6820
7509
|
}
|