@tea-agent/loop-agent 0.40.0-next.1 → 0.40.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +106 -109
- package/README.md +2 -2
- package/dist/application/task-lifecycle/advance.js +12 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/commands/dag-rerun.js +1 -1
- package/dist/executors/dag-pi-executor.js +69 -7
- package/dist/executors/pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +46 -1
- package/dist/executors/pi-writer-tool-policy.js +57 -1
- package/dist/executors/shell-executor.js +43 -3
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/shared/dag-prompt-override.js +4 -5
- package/dist/shared/update/npm-client.js +33 -11
- package/dist/task/config-types.js +10 -0
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +2 -0
- package/dist/task/contract/hash.js +3 -0
- package/dist/task/contract/observe.js +25 -4
- package/dist/task/contract/paths.js +2 -1
- package/dist/task/contract/project.js +15 -1
- package/dist/task/contract/schema.js +2 -0
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +474 -0
- package/dist/task/source-prepare/index.js +3 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
- package/dist/task/source-prepare/ledger-review.js +214 -0
- package/dist/task/source-prepare/ledger.js +545 -0
- package/dist/task/source-prepare/prepare.js +262 -1
- package/dist/task/source-prepare/semantic-intake.js +19 -3
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/worker/cli.js +12 -26
- package/dist/worker/console/chat/chat-event-store.js +75 -5
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/pi-runtime.js +331 -18
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +389 -56
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +93 -63
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +79 -0
- package/dist/worker/console/console-handoff.js +82 -0
- package/dist/worker/console/console-update-and-init.js +115 -0
- package/dist/worker/console/console-update-runtime.js +84 -0
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/operation-store.js +1 -1
- package/dist/worker/console/operator-actions.js +129 -4
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/routes.js +312 -1
- package/dist/worker/console/security.js +4 -4
- package/dist/worker/console/server.js +583 -172
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Br92EGb7.js → abnfDiagram-N423BO3Z-Bs-CDAXM.js} +1 -1
- package/dist/worker/console/static/assets/{arc-oulImtQq.js → arc-CAkA3We3.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-tyHc6aqO.js → architectureDiagram-T3A2C74G-DEmI_zqr.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CY86BSpT.js → blockDiagram-VBNYF7ZC-uDHANebE.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BKKsDmNs.js → c4Diagram-5PPSVZJV-Bv97b4_B.js} +1 -1
- package/dist/worker/console/static/assets/channel-amxUpk7o.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CXZdjl3B.js → chunk-2GRJ4B5K-shmQKsFz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B-aIFDQ5.js → chunk-2Q5K7J3B-BAraNvcN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BDgAW_B2.js → chunk-5RXB4S5H-BirhxUop.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DABa4UPR.js → chunk-5VM5RSS4-C9LgUJpR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-aGbysVOx.js → chunk-6Q2QTUOP-DrU-s-t7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B8MSZDVs.js → chunk-GF5L2VYU-Tx-H_FA2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CpelV73g.js → chunk-JWPE2WC7-BjLtNFrR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BE04ty3z.js → chunk-KBJHAD2P-UNLVwSsy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BUxTkaaR.js → chunk-RYQCIY6F-CBEUX8XD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CYYBVKfX.js → chunk-XXDRQBXY-WTL7IZOq.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CEQF_xmQ.js → cose-bilkent-JH36ORCC-CkHG6W3s.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CqpxN5IT.js → cynefin-VYW2F7L2-BRzFqTaL.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-B3PzanVi.js → cynefinDiagram-MW4NZA55-2_CFp_i7.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BSUGCNx1.js → dagre-VZM6K2ZE-BJtsmdkE.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdLevU5T.js → diagram-7IWD3JNH-DNEFFo6w.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BjpF_Kok.js → diagram-B4RE2ZJO-B_IWV7eo.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C4I6NJny.js → diagram-LBJQPF4R-DvvRx2dz.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C8hecCr5.js → diagram-Q27KOJAE-CsibsWLg.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Daod6tOS.js → diagram-UB23O5K3-les_djBQ.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BvvmazGv.js → ebnfDiagram-BXEA7PRR-BEm2n7qJ.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DeN_vfV4.js → erDiagram-JOGREHBK-BI7vHHVm.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D6dq1t9z.js → flowDiagram-UKHOOZJN-CUFCERrB.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DziIdNY4.js → ganttDiagram-PKOTCBZU-wxyLiGWt.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js → gitGraphDiagram-DS77QQ5N-Bt897K8Z.js} +1 -1
- package/dist/worker/console/static/assets/index-BHhUOTri.css +1 -0
- package/dist/worker/console/static/assets/index-DqJbO3-p.js +407 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czv5eYx2.js → infoDiagram-6WML65LV-DYNx_IPv.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js → ishikawaDiagram-WSZJBQD7-CzpLNEu6.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DxGmNj5K.js → journeyDiagram-NVQOT4AX-97f9owNU.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CYCFqkO_.js → kanban-definition-27J2QSJJ-BxM9hyBn.js} +1 -1
- package/dist/worker/console/static/assets/{linear-B0YzRwrd.js → linear-ByuxcHvp.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-t3EZtIGh.js → mermaid.core-DwPGvpCv.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BTXpTv0T.js → mindmap-definition-FAOFIHXS-DCoqG9RT.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DU8UkKqA.js → pegDiagram-VL7TDLO6-BLSBmBMx.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DP4y8ohp.js → pieDiagram-7S7Q4E2Y-B0t_0oJW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B07vNaYR.js → quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-xDWktb2i.js → railroadDiagram-AXF67PYL-B3jRdATC.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BLNMKqeS.js → requirementDiagram-LRYGKXZP-D-cTtXEN.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-n0adalxH.js → sankeyDiagram-W5VNT64P-B25igJRE.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BbnS-2DL.js → sequenceDiagram-SI44F4Z6-jW_lM1_D.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4uPpCLu.js → sizeCapture-X5ZJPWSS-CrzTgsHE.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA--MMbB0fX.js → stateDiagram-OKZ733FA-B01MvlqE.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BXUgp2QY.js → swimlanes-SLNWSIFB-YcF12FiU.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CsaMxki7.js → timeline-definition-Z64GVDOM-ClnRcg2s.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C2IuG9iH.js → vennDiagram-T6HMQDX7-B6gr_z-q.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CmEWOXCW.js → wardleyDiagram-T6FBY63Y-CvSgguGH.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D_9LE0MG.js → xychartDiagram-ELKLHX3M-23OtSZCb.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +68 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
- package/dist/worker/console/workspace-context.js +234 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observe/node-input.js +88 -2
- package/dist/worker/observe/routes.js +188 -0
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +29 -9
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
- package/dist/worker/observe/static/dag-node-purpose.js +203 -0
- package/dist/worker/observe/static/format.js +37 -0
- package/dist/worker/observe/static/index.html +6 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -100
- package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +174 -2
- package/dist/worker/observe/static/styles.css +357 -35
- package/dist/worker/observe/static/views/dag-graph.js +4 -2
- package/dist/worker/observe/static/views/dag-inspector.js +533 -79
- package/dist/worker/observe/static/views/dag.d.ts +13 -0
- package/dist/worker/observe/static/views/dag.js +6 -4
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/task.js +56 -1
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +29 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
- package/dist/workflows/dag/backend-test-writer-completeness.js +17 -17
- package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
- package/dist/workflows/dag/frontend-plan-render.js +24 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
- package/dist/workflows/dag/frontend-review-context.js +92 -10
- package/dist/workflows/dag/init-hybrid.js +238 -51
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/node-execution.js +45 -0
- package/dist/workflows/dag/types.js +62 -12
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +1 -0
- package/docs/operations/local-development-environment.md +21 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +18 -0
- package/docs/templates/backend-test-dag.json +265 -237
- package/docs/templates/frontend-implementation-contract.schema.json +681 -24
- package/harness.json +3 -3
- package/package.json +3 -1
- package/skills/loop-agent/references/hybrid-dag.md +7 -0
- package/dist/worker/console/static/assets/channel-TY36zt-i.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +0 -1
- package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
- package/dist/worker/console/static/assets/index-Ci8ksvBm.js +0 -406
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +0 -8
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { access, readdir, readFile, realpath } from "node:fs/promises";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
6
6
|
import { assertValidDagSpec } from "./validate.js";
|
|
@@ -22,6 +22,9 @@ import { discoverProjectGovernancePresence } from "./project-governance-context.
|
|
|
22
22
|
import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
23
23
|
import { materializeTaskReferenceDocs } from "../../task/source-references.js";
|
|
24
24
|
import { observeTaskContract } from "../../task/contract/observe.js";
|
|
25
|
+
import { extractRequirementFactsFromMarkdown } from "../../task/source-prepare/parse-intent.js";
|
|
26
|
+
import { computeLedgerInputDigest, parseLedgerJson, recoverLedgerInputContract, } from "../../task/source-prepare/ledger.js";
|
|
27
|
+
import { REQUIREMENT_LEDGER_FILE_NAME } from "../../task/contract/constants.js";
|
|
25
28
|
import { dagHasWriterExecution } from "./task-contract-binding.js";
|
|
26
29
|
import { DEFAULT_VERIFY_TIMEOUT_MS, resolveVerifyPreset, } from "../../executors/shell-verification.js";
|
|
27
30
|
import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
|
|
@@ -1477,7 +1480,7 @@ export function extractTaskScopedRequirementIds(requirementMarkdown, ...fallback
|
|
|
1477
1480
|
.sort((a, b) => a - b);
|
|
1478
1481
|
return numbered.map((value) => `AC-${value}`);
|
|
1479
1482
|
}
|
|
1480
|
-
function
|
|
1483
|
+
function buildDagSourceBindingBase(sources) {
|
|
1481
1484
|
const sourceEntries = [
|
|
1482
1485
|
{
|
|
1483
1486
|
kind: "requirement",
|
|
@@ -1512,6 +1515,124 @@ function buildDagSourceBinding(sources) {
|
|
|
1512
1515
|
requirementIds: extractTaskScopedRequirementIds(sources.requirementMarkdown, sources.constraintMarkdown, ...(sources.referenceDocuments ?? []).map((reference) => reference.markdown)),
|
|
1513
1516
|
};
|
|
1514
1517
|
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Source binding v2(AC-001 / AC-FIDELITY-REPAIR-001):仅 frontend taskKind 且存在
|
|
1520
|
+
* 可解析 requirement/acceptance 引用时产出 schemaVersion=2 绑定(ledger 指针 +
|
|
1521
|
+
* requirement→fragment 映射全部必填)。绑定完全来自 task advance 已持久化的
|
|
1522
|
+
* source/requirement-ledger.json(intake 侧同一份受管账本的原始字节/hash/inputDigest),
|
|
1523
|
+
* 绝不从 materialized reference path 重建语义等价但 path/hash 不同的 ledger;
|
|
1524
|
+
* 缺失/损坏/漂移 fail closed(SOURCE_FIDELITY_LEDGER_MISSING / _STALE)。
|
|
1525
|
+
*/
|
|
1526
|
+
function buildDagSourceBinding(sources, taskKind) {
|
|
1527
|
+
const base = buildDagSourceBindingBase(sources);
|
|
1528
|
+
if (taskKind !== "frontend-implementation")
|
|
1529
|
+
return base;
|
|
1530
|
+
const ledgerBinding = loadPersistedFrontendSourceBindingLedger(sources);
|
|
1531
|
+
if (!ledgerBinding)
|
|
1532
|
+
return base;
|
|
1533
|
+
// AC-002 / AC-HARD-002:物化 REQ-SRC-* canonical id 确定性并入 v2 requirementIds。
|
|
1534
|
+
// 生成型 REQ-SRC-* id 无法从 markdown 文本导出(必须来自持久化 ledger),此处
|
|
1535
|
+
// 在 required requirement ids 侧确定性注入,确保 prewrite missing-requirement-ids
|
|
1536
|
+
// 不误伤(不要求文本可导出)也不漏检(物化 id 必须被契约覆盖)。
|
|
1537
|
+
const materializedRequirementIds = Object.keys(ledgerBinding.requirementToFragments)
|
|
1538
|
+
.filter((id) => /^REQ-SRC-/.test(id) && !base.requirementIds.includes(id))
|
|
1539
|
+
.sort();
|
|
1540
|
+
return {
|
|
1541
|
+
schemaVersion: 2,
|
|
1542
|
+
taskId: base.taskId,
|
|
1543
|
+
sources: base.sources,
|
|
1544
|
+
requirementIds: [
|
|
1545
|
+
...base.requirementIds,
|
|
1546
|
+
...materializedRequirementIds,
|
|
1547
|
+
],
|
|
1548
|
+
ledgerPath: ledgerBinding.ledgerPath,
|
|
1549
|
+
ledgerSha256: ledgerBinding.ledgerSha256,
|
|
1550
|
+
inputDigest: ledgerBinding.inputDigest,
|
|
1551
|
+
requirementToFragments: ledgerBinding.requirementToFragments,
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* 读取 Task Contract 已持久化的 source/requirement-ledger.json,核验字节 sha256
|
|
1556
|
+
* 与 inputDigest(与当前源输入 digest 一致)后产出 v2 绑定字段。缺失/损坏/漂移
|
|
1557
|
+
* 抛稳定错误(SOURCE_FIDELITY_LEDGER_MISSING / SOURCE_FIDELITY_LEDGER_STALE)。
|
|
1558
|
+
* 新鲜度侧不再"重新推导"权威输入,而是从持久化 ledger 恢复 build 侧的输入合同
|
|
1559
|
+
* (与 ledger build 使用完全相同的输入),避免 contract apply 重投影派生导航视图
|
|
1560
|
+
* 需求.md 导致重抽取漂移误判 stale(AC-DIGEST-001/002/003)。
|
|
1561
|
+
*/
|
|
1562
|
+
function loadPersistedFrontendSourceBindingLedger(sources) {
|
|
1563
|
+
const referenceDocs = (sources.referenceDocuments ?? []).filter((doc) => doc.markdown.trim().length > 0);
|
|
1564
|
+
if (referenceDocs.length === 0)
|
|
1565
|
+
return null;
|
|
1566
|
+
// 存在性守卫(保持既有 v1 回退行为):派生视图无可抽取 requirement/acceptance
|
|
1567
|
+
// 引用时不要求持久化 ledger,直接回退 base binding。此守卫只用于"是否 ledger
|
|
1568
|
+
// 任务"判定,绝不参与 digest 键计算——键输入完全来自持久化 ledger 恢复。
|
|
1569
|
+
const guardFacts = extractRequirementFactsFromMarkdown(sources.requirementMarkdown);
|
|
1570
|
+
if (guardFacts.acceptanceCriteria.length === 0)
|
|
1571
|
+
return null;
|
|
1572
|
+
const ledgerAbsolutePath = path.join(sources.taskDir, "source", REQUIREMENT_LEDGER_FILE_NAME);
|
|
1573
|
+
let raw;
|
|
1574
|
+
try {
|
|
1575
|
+
raw = readFileSync(ledgerAbsolutePath, "utf8");
|
|
1576
|
+
}
|
|
1577
|
+
catch (error) {
|
|
1578
|
+
throw new Error(`SOURCE_FIDELITY_LEDGER_MISSING: frontend task ${sources.taskId} has resolvable requirement/acceptance references but no persisted ${REQUIREMENT_LEDGER_FILE_NAME} at ${ledgerAbsolutePath}; run task advance (intake) to persist the ledger before dag init-hybrid (${error instanceof Error ? error.message : String(error)})`);
|
|
1579
|
+
}
|
|
1580
|
+
const ledgerSha256 = createHash("sha256").update(raw).digest("hex");
|
|
1581
|
+
let ledger;
|
|
1582
|
+
try {
|
|
1583
|
+
ledger = parseLedgerJson(raw);
|
|
1584
|
+
}
|
|
1585
|
+
catch (error) {
|
|
1586
|
+
throw new Error(`SOURCE_FIDELITY_LEDGER_MISSING: persisted ${REQUIREMENT_LEDGER_FILE_NAME} is not a valid v1 ledger: ${error instanceof Error ? error.message : String(error)}`);
|
|
1587
|
+
}
|
|
1588
|
+
// canonical 侧:ledger.canonicalRequirements 按 build 顺序原样保留输入 canonical
|
|
1589
|
+
// requirements(id/text 逐字节),过滤物化 REQ-SRC-*(AC-HARD-002 "物化不入键")
|
|
1590
|
+
// 即精确恢复 digest 输入集;派生导航视图 需求.md 投影不参与键计算。
|
|
1591
|
+
const contract = recoverLedgerInputContract(ledger);
|
|
1592
|
+
if (contract.canonicalRequirements.length === 0) {
|
|
1593
|
+
// AC-HARD-003:存在 v2 ledger 但 canonical requirements 为空时 fail closed,
|
|
1594
|
+
// 绝不回退 v1 base 绑定(buildDagSourceBinding 不降级)。空 canonical 的 ledger
|
|
1595
|
+
// 无法提供 requirement→fragment 映射证据,任何 v1 回退都会静默放行 fail-open。
|
|
1596
|
+
throw new Error(`SOURCE_FIDELITY_LEDGER_STALE: persisted ${REQUIREMENT_LEDGER_FILE_NAME} has an empty canonical requirement set; refusing to fall back to a v1 base binding; re-run task advance (intake) to rebuild the ledger`);
|
|
1597
|
+
}
|
|
1598
|
+
// source 侧:ledger.sourcePaths(build 时权威 sourceDocuments 的 repo-root
|
|
1599
|
+
// relative POSIX 路径集合)作为权威身份集合,对当前 materialized reference 文件
|
|
1600
|
+
// 做交集过滤;内容取当前字节(漂移检测)。权威路径缺失(删除/清空/重命名)→
|
|
1601
|
+
// fail-closed STALE;新增的非权威 role 文件被正确排除(不入键)。
|
|
1602
|
+
const currentByPath = new Map();
|
|
1603
|
+
for (const doc of referenceDocs) {
|
|
1604
|
+
currentByPath.set(toDagSourcePath(sources, doc.path), doc.markdown);
|
|
1605
|
+
}
|
|
1606
|
+
const missingAuthoritativePaths = contract.sourcePaths.filter((sourcePath) => !currentByPath.has(sourcePath));
|
|
1607
|
+
if (missingAuthoritativePaths.length > 0) {
|
|
1608
|
+
throw new Error(`SOURCE_FIDELITY_LEDGER_STALE: persisted ${REQUIREMENT_LEDGER_FILE_NAME} was built from authoritative source documents no longer present (${missingAuthoritativePaths.join(", ")}); re-run task advance (intake) to rebuild the ledger`);
|
|
1609
|
+
}
|
|
1610
|
+
// inputDigest 新鲜度:与当前源输入(canonical repo-root relative identity)一致。
|
|
1611
|
+
// digest 只绑定语义权威输入(规范化源文档 + canonical requirements +
|
|
1612
|
+
// schema/reconciler version);派生导航视图 需求.md 投影不参与键计算。
|
|
1613
|
+
const expectedDigest = computeLedgerInputDigest({
|
|
1614
|
+
sourceDocuments: contract.sourcePaths.map((sourcePath) => ({
|
|
1615
|
+
path: sourcePath,
|
|
1616
|
+
content: currentByPath.get(sourcePath),
|
|
1617
|
+
})),
|
|
1618
|
+
canonicalRequirements: contract.canonicalRequirements,
|
|
1619
|
+
});
|
|
1620
|
+
if (ledger.inputDigest !== expectedDigest) {
|
|
1621
|
+
throw new Error(`SOURCE_FIDELITY_LEDGER_STALE: persisted ${REQUIREMENT_LEDGER_FILE_NAME} inputDigest ${ledger.inputDigest} does not match current source input ${expectedDigest}; re-run task advance (intake) to rebuild the ledger`);
|
|
1622
|
+
}
|
|
1623
|
+
const requirementToFragments = {};
|
|
1624
|
+
for (const requirement of ledger.canonicalRequirements) {
|
|
1625
|
+
if (requirement.sourceFragmentIds.length > 0) {
|
|
1626
|
+
requirementToFragments[requirement.id] = requirement.sourceFragmentIds;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
return {
|
|
1630
|
+
ledgerPath: toDagSourcePath(sources, ledgerAbsolutePath),
|
|
1631
|
+
ledgerSha256,
|
|
1632
|
+
inputDigest: ledger.inputDigest,
|
|
1633
|
+
requirementToFragments,
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1515
1636
|
function buildBackendTestAnalysisSourceBindingContract(sources) {
|
|
1516
1637
|
const binding = buildDagSourceBinding(sources);
|
|
1517
1638
|
const requirement = binding.sources.find((source) => source.kind === "requirement");
|
|
@@ -2416,7 +2537,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2416
2537
|
allowedPaths: taskConfig.allowedPaths,
|
|
2417
2538
|
complexity: taskConfig.complexity,
|
|
2418
2539
|
});
|
|
2419
|
-
const frontendSourceBinding = buildDagSourceBinding(sources);
|
|
2540
|
+
const frontendSourceBinding = buildDagSourceBinding(sources, taskConfig.taskKind);
|
|
2420
2541
|
const frontendContractSkeleton = buildFrontendImplementationContractSkeleton({
|
|
2421
2542
|
sourceBinding: frontendSourceBinding,
|
|
2422
2543
|
riskLevel: frontendRisk.selectedRisk,
|
|
@@ -3831,9 +3952,10 @@ export function applyBackendTestLayoutToText(text, layout) {
|
|
|
3831
3952
|
/**
|
|
3832
3953
|
* Builds the `node -e` command for the backend-test module manifest shell.
|
|
3833
3954
|
* The inline JS mirrors `extractModuleStemsFromReadme` so the map_agent
|
|
3834
|
-
* shard set deterministically matches the
|
|
3835
|
-
* Gate trusts: only
|
|
3836
|
-
* relative links `[label](./<stem>.md)` count
|
|
3955
|
+
* shard set deterministically matches the plan index the Completeness
|
|
3956
|
+
* Gate trusts: only `testcase/md/<stem>.md` path mentions and canonical
|
|
3957
|
+
* relative links `[label](./<stem>.md)` count. Adjacent table cells such as
|
|
3958
|
+
* Case Range or pytest assets are never interpreted as module stems. The same
|
|
3837
3959
|
* `looksLikeValidModuleStem` filter and `normalizeBackendTestModuleStem`
|
|
3838
3960
|
* normalization. Output is exactly one trailing JSON line `{modules:[{stem}]}`
|
|
3839
3961
|
* that `parseJsonFromText` accepts after shell command echoes.
|
|
@@ -3847,17 +3969,18 @@ function buildBackendTestModuleManifestShellCommand(layout) {
|
|
|
3847
3969
|
// mdDir/testPrefix are injected as JSON literals so the same extractor
|
|
3848
3970
|
// works for any configured backendTest layout (plan A).
|
|
3849
3971
|
const mdDirLiteral = JSON.stringify(layout.markdownDir);
|
|
3850
|
-
const testPrefixLiteral = JSON.stringify(`${layout.scriptDir}/test_`);
|
|
3851
3972
|
const escOpen = String.fromCharCode(92, 91); // \[
|
|
3852
3973
|
const escClose = String.fromCharCode(92, 93); // \]
|
|
3853
3974
|
const escBslash = String.fromCharCode(92, 92); // \\
|
|
3854
|
-
const script = `const fs=require('fs');
|
|
3975
|
+
const script = `const fs=require('fs'),path=require('path'),crypto=require('crypto');
|
|
3855
3976
|
const mdDir=${mdDirLiteral};
|
|
3856
|
-
const testPrefix=${testPrefixLiteral};
|
|
3857
3977
|
const esc=s=>s.replace(/[${escOpen}${escClose}{}()*+?^$|${escBslash}]/g,'${escBslash}$&');
|
|
3858
3978
|
const rxMdPath=new RegExp(esc(mdDir)+'${escBslash}/([A-Za-z0-9_.-]+)${escBslash}.md','g');
|
|
3859
|
-
const
|
|
3860
|
-
|
|
3979
|
+
const runDir=process.env.HARNESS_DAG_RUN_DIR||'';
|
|
3980
|
+
if(!runDir){process.stderr.write('missing HARNESS_DAG_RUN_DIR for backend-test Markdown plan artifact\\n');process.exit(2);}
|
|
3981
|
+
const planPath=path.join(runDir,'generate-backend-md-plan-pi','plan.md');
|
|
3982
|
+
if(!fs.existsSync(planPath)){process.stderr.write('missing backend-test Markdown plan artifact: '+planPath+'\\n');process.exit(2);}
|
|
3983
|
+
const readme=fs.readFileSync(planPath,'utf8');
|
|
3861
3984
|
const norm=s=>String(s).toLowerCase().replace(/[^a-z0-9]+/g,'_').replace(/^_+|_+$/g,'').replace(/_+/g,'_');
|
|
3862
3985
|
const bt=String.fromCharCode(96);
|
|
3863
3986
|
const stripBackticks=s=>s.split(bt).join('');
|
|
@@ -3874,15 +3997,17 @@ const lines=section.split('\\n').filter(l=>l.includes('|'));
|
|
|
3874
3997
|
for(const line of lines){
|
|
3875
3998
|
const bare=stripBackticks(line);
|
|
3876
3999
|
for(const m of bare.matchAll(rxMdPath)){raw.push(m[1]);}
|
|
3877
|
-
for(const m of bare.matchAll(rxTableRow)){if(valid(m[1])||invalidReason(m[1])!=='invalid-syntax')raw.push(m[1]);}
|
|
3878
4000
|
}
|
|
3879
4001
|
for(const m of section.matchAll(rxRelLink)){raw.push(m[1]);}
|
|
3880
4002
|
const invalid=[];for(const r of raw){const reason=invalidReason(r);if(reason)invalid.push({stem:norm(r),reason});}
|
|
3881
4003
|
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);}
|
|
3882
4004
|
const seen=new Set();const modules=[];
|
|
3883
4005
|
for(const r of raw){const st=norm(r);if(valid(r)&&!seen.has(st)){seen.add(st);modules.push({stem:st});}}
|
|
4006
|
+
if(modules.length===0){process.stderr.write('empty-module-index: require at least one stable business module\\n');process.exit(2);}
|
|
3884
4007
|
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);}
|
|
3885
|
-
process.
|
|
4008
|
+
const planReadPath=path.relative(process.cwd(),planPath).split(path.sep).join('/');
|
|
4009
|
+
const planSha256=crypto.createHash('sha256').update(readme).digest('hex');
|
|
4010
|
+
process.stdout.write(JSON.stringify({modules:modules.map(item=>({...item,planReadPath})),planReadPath,planSha256}));
|
|
3886
4011
|
`;
|
|
3887
4012
|
const encoded = Buffer.from(script, "utf8").toString("base64");
|
|
3888
4013
|
return `node -e "eval(Buffer.from('${encoded}','base64').toString('utf8'))"`;
|
|
@@ -4291,6 +4416,7 @@ async function buildBackendTestGapFillDag(sources) {
|
|
|
4291
4416
|
],
|
|
4292
4417
|
};
|
|
4293
4418
|
spec.backendTestLayout = layout;
|
|
4419
|
+
applyBackendTestWorkspaceControl(spec, taskConfig.backendTest?.workspaceControl ?? "git");
|
|
4294
4420
|
if (sharedSetup) {
|
|
4295
4421
|
spec.backendTestSharedSetup = { ...sharedSetup };
|
|
4296
4422
|
}
|
|
@@ -4364,35 +4490,37 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4364
4490
|
const generateMdPlan = {
|
|
4365
4491
|
id: "generate-backend-md-plan-pi",
|
|
4366
4492
|
depends_on: [environment.id],
|
|
4367
|
-
role: "
|
|
4493
|
+
role: "planner",
|
|
4368
4494
|
executor: "pi",
|
|
4369
|
-
toolProfile: "
|
|
4495
|
+
toolProfile: "read-only",
|
|
4370
4496
|
complexity: "MED",
|
|
4371
|
-
writePolicy: "
|
|
4372
|
-
|
|
4373
|
-
|
|
4497
|
+
writePolicy: "read-only",
|
|
4498
|
+
allowedPaths: [],
|
|
4499
|
+
readSet: [
|
|
4500
|
+
toDagSourcePath(sources, sources.requirementPath),
|
|
4501
|
+
...(sources.constraintMarkdown
|
|
4502
|
+
? [toDagSourcePath(sources, sources.constraintPath)]
|
|
4503
|
+
: []),
|
|
4504
|
+
...intake.referenceIndex.map((entry) => entry.readPath),
|
|
4505
|
+
".harness/dag-runs/**/reports/backend-test-environment.md",
|
|
4506
|
+
],
|
|
4374
4507
|
forbiddenPaths: forbidden,
|
|
4375
|
-
|
|
4376
|
-
type: "implementation-outcome-v1",
|
|
4377
|
-
requireChangedFiles: true,
|
|
4378
|
-
},
|
|
4379
|
-
retryPolicy: BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY,
|
|
4380
|
-
outputContract: "Write a Chinese, human-readable testcase/md/README.md as the single Markdown-first entry page with Coverage Scope, Coverage Matrix and a machine-parseable module index. Do not write module case cards here; do not execute pytest or modify production code/config.",
|
|
4508
|
+
outputContract: "Return a Chinese, human-readable Markdown-first plan with Coverage Scope, Coverage Matrix, Scenario Partitions when applicable, and a machine-parseable Module Index. The runtime persists it as a run-owned Harness artifact; do not write testcase/md/README.md, execute pytest, or modify project files.",
|
|
4381
4509
|
subtask_prompt: [
|
|
4382
|
-
"This is a required
|
|
4510
|
+
"This is a required plan-generation node. Read only the strict read set and return the complete Markdown plan in the assistant response. The runtime persists the response as a run-owned Harness artifact named generate-backend-md-plan-pi/plan.md. Do not write testcase/md/README.md or any project file; module case cards are written by downstream sharded nodes.",
|
|
4383
4511
|
"Output budget protocol (hard, max output <=16K per turn): Never paste full Matrix, case bodies, or source text into assistant chat. README holds only Scope+Matrix+module index; never inline full case bodies. If a Completeness Gate / OUTPUT_LIMIT_RECOVERY retry is injected, continue only listed target paths.",
|
|
4384
|
-
"
|
|
4385
|
-
"Read the upstream environment report. Generate the Markdown-first backend test
|
|
4512
|
+
"Return the complete plan as plain Markdown. Do not emit JSON or code fences. The plan must contain the exact ## Coverage Scope, ## Coverage Matrix and ## Module Index sections required by the downstream manifest.",
|
|
4513
|
+
"Read the upstream environment report only through the strict read set. Generate the Markdown-first backend test plan; it will be persisted under the current DAG run's Harness artifacts, not under testcase/md/.",
|
|
4386
4514
|
"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.",
|
|
4387
|
-
"Create
|
|
4388
|
-
"Before the Coverage Matrix, write a mandatory machine-readable `## Coverage Scope` section in
|
|
4515
|
+
"Create the concise plan entry page: test objective, target/environment, isolation/cleanup, module summary and a linked case index table with Case ID, Chinese case name, scenario type, endpoint and expected status/result. Avoid repeating every case body in the plan artifact.",
|
|
4516
|
+
"Before the Coverage Matrix, write a mandatory machine-readable `## Coverage Scope` section in the plan artifact using exactly `| Field | Value |`, immediately followed by the separator row `|---|---|`, and these six unique rows: `Change Classification`, `Coverage Policy`, `Affected Operations`, `Affected Rule Keys`, `Regression Floor`, `Scope Evidence`. Always set `Change Classification` to `new-operation` and `Coverage Policy` to `full-contract`; do NOT reason about whether operations are new or existing. Cover all in-scope rules from the requirement document at full depth; treat the product requirement as the coverage baseline and use API contract evidence (fields/status/enum/boundary/format) to supplement scenario dimensions. Scope is limited to operations/rules the requirement document (or its referenced API contract) explicitly describes; do not expand to unrelated operations that the requirement does not mention. List affected operations exactly as `METHOD /path`, stable rule keys separated by semicolons, and precise source pointers as Scope Evidence.",
|
|
4389
4517
|
"Coverage depth is full over the in-scope rules: fully cover every documented status, request/response field rule, requiredness, enum, boundary, format, auth and business state of each affected operation the requirement describes, but do not re-test unrelated operations the requirement does not mention. Inspect shared validator/helper/DTO/query builder evidence and expand Affected Operations when the same affected path can affect them; unresolved impact stays visible as GAP/CONFLICT.",
|
|
4390
|
-
"Before writing cases, build the mandatory machine-readable Coverage Matrix inside
|
|
4518
|
+
"Before writing cases, build the mandatory machine-readable Coverage Matrix inside the plan artifact itself. Its section heading line must be exactly `## Coverage Matrix` with no numeric prefix/suffix; never place the canonical Matrix only in a module file. Use this exact header: `| Rule Key | Priority | Source | Endpoint/Field | Dimension | Rule | Required Test Points | Case IDs | Status |`. Every data row must contain exactly 9 pipe-delimited cells and must never omit `Dimension`; use concise dimensions such as requirement, operation, response-status, requiredness, enum, boundary, format, business-state or error. Use only P0/P1/P2 and COVERED/PARTIAL/GAP/CONFLICT. Use stable `TP-<UPPERCASE-HYPHENATED-ID>` test points separated by semicolons.",
|
|
4391
4519
|
"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.",
|
|
4392
4520
|
"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.",
|
|
4393
4521
|
"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.",
|
|
4394
|
-
"Mandatory module index: include a `## Module Index` table in
|
|
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.",
|
|
4522
|
+
"Mandatory module index: include a `## Module Index` table in the plan artifact 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.",
|
|
4523
|
+
"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), using bare semicolon-separated identifier values inside the single table cell (for example `ACTIVE; ARCHIVED`, with no Markdown backticks or prose); 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.",
|
|
4396
4524
|
"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
4525
|
...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
|
|
4398
4526
|
intake.boundedSourceContext,
|
|
@@ -4411,8 +4539,8 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4411
4539
|
writePolicy: "read-only",
|
|
4412
4540
|
allowedPaths: ro,
|
|
4413
4541
|
forbiddenPaths: forbidden,
|
|
4414
|
-
outputContract: "Stdout JSON {modules:[{stem}]} parsed from
|
|
4415
|
-
subtask_prompt: "Parse
|
|
4542
|
+
outputContract: "Stdout JSON {modules:[{stem,planReadPath}],planReadPath,planSha256} parsed from the run-owned generate-backend-md-plan-pi/plan.md artifact using the same module-stem extractor as the Completeness Gate.",
|
|
4543
|
+
subtask_prompt: "Parse only $HARNESS_DAG_RUN_DIR/generate-backend-md-plan-pi/plan.md and emit exactly one trailing JSON line {modules:[{stem,planReadPath}],planReadPath,planSha256}. No file writes and no testcase/md/README.md fallback.",
|
|
4416
4544
|
shell: {
|
|
4417
4545
|
commands: [buildBackendTestModuleManifestShellCommand(layout)],
|
|
4418
4546
|
cwd: ".",
|
|
@@ -4453,6 +4581,15 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4453
4581
|
allowedPaths: ["testcase/md/{{item.stem}}.md"],
|
|
4454
4582
|
forbiddenPaths: forbidden,
|
|
4455
4583
|
writeSet: ["testcase/md/{{item.stem}}.md"],
|
|
4584
|
+
readSet: [
|
|
4585
|
+
"{{item.planReadPath}}",
|
|
4586
|
+
toDagSourcePath(sources, sources.requirementPath),
|
|
4587
|
+
...(sources.constraintMarkdown
|
|
4588
|
+
? [toDagSourcePath(sources, sources.constraintPath)]
|
|
4589
|
+
: []),
|
|
4590
|
+
...intake.referenceIndex.map((entry) => entry.readPath),
|
|
4591
|
+
`${layout.markdownDir}/{{item.stem}}.md`,
|
|
4592
|
+
],
|
|
4456
4593
|
writerOutcomePolicy: {
|
|
4457
4594
|
type: "implementation-outcome-v1",
|
|
4458
4595
|
requireChangedFiles: true,
|
|
@@ -4460,7 +4597,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4460
4597
|
retryPolicy: BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY,
|
|
4461
4598
|
outputContract: "Write exactly one Chinese module Markdown case-card file testcase/md/<stem>.md with BE-<MODULE>-<NNN> cases and the seven required h3 sections; keep machine IDs/literals exact and do not execute pytest or modify production code/config or the README.",
|
|
4462
4599
|
subtaskPromptTemplate: [
|
|
4463
|
-
"This is a required file-generation node for exactly one Markdown module.
|
|
4600
|
+
"This is a required file-generation node for exactly one Markdown module. Read the upstream run-owned Markdown plan artifact at `{{item.planReadPath}}` (Coverage Scope + Coverage Matrix + Module Index) and the bounded references, then immediately use write tools to create the single file testcase/md/{{item.stem}}.md. Do not read or recreate testcase/md/README.md. Do not end after analysis or planning, and do not return before a non-empty bounded diff exists. Do not modify any other module file.",
|
|
4464
4601
|
"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.",
|
|
4465
4602
|
"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.",
|
|
4466
4603
|
"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.",
|
|
@@ -4482,28 +4619,37 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4482
4619
|
};
|
|
4483
4620
|
const reviewCases = {
|
|
4484
4621
|
id: "review-and-revise-backend-md-cases-pi",
|
|
4485
|
-
depends_on: [generateMdCasesMap.id],
|
|
4622
|
+
depends_on: [generateMdCasesMap.id, materializeMdManifest.id],
|
|
4486
4623
|
role: "implementer",
|
|
4487
4624
|
executor: "pi",
|
|
4488
4625
|
toolProfile: "write",
|
|
4489
4626
|
complexity: "MED",
|
|
4490
4627
|
writePolicy: "exclusive",
|
|
4491
|
-
writeSet: [
|
|
4492
|
-
|
|
4493
|
-
|
|
4628
|
+
writeSet: [`${layout.markdownDir}/**`],
|
|
4629
|
+
readSet: [
|
|
4630
|
+
".harness/dag-runs/**/generate-backend-md-plan-pi/plan.md",
|
|
4631
|
+
toDagSourcePath(sources, sources.requirementPath),
|
|
4632
|
+
...(sources.constraintMarkdown
|
|
4633
|
+
? [toDagSourcePath(sources, sources.constraintPath)]
|
|
4634
|
+
: []),
|
|
4635
|
+
...intake.referenceIndex.map((entry) => entry.readPath),
|
|
4636
|
+
`${layout.markdownDir}/*.md`,
|
|
4637
|
+
],
|
|
4638
|
+
allowedPaths: Array.from(new Set([...ro, `${layout.markdownDir}/**`])),
|
|
4639
|
+
forbiddenPaths: Array.from(new Set([...forbidden, layout.readmePath])),
|
|
4494
4640
|
writerOutcomePolicy: { type: "implementation-outcome-v1" },
|
|
4495
4641
|
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.",
|
|
4496
4642
|
subtask_prompt: [
|
|
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
|
|
4498
|
-
"Output budget protocol: never dump full Matrix/case bodies into assistant chat. Inspect
|
|
4643
|
+
"Perform one gap-targeted synchronization, not a full-suite rewrite or stylistic review. Read the immutable run-owned Markdown plan from the direct upstream manifest's planReadPath. Start from explicit bound source IDs/error codes/DTO fields/normative quoted rules and the plan Coverage Matrix; open and edit only modules that own a missing or conflicting rule. Never create or edit testcase/md/README.md and never modify the run-owned plan artifact. Preserve unrelated valid modules byte-for-byte and avoid optional wording cleanup.",
|
|
4644
|
+
"Output budget protocol: never dump full Matrix/case bodies into assistant chat. Inspect the immutable run-owned plan first, build a concise target list from its Matrix and Module Index, 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
4645
|
"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`.",
|
|
4500
4646
|
"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.",
|
|
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
|
|
4647
|
+
"Check AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Require the exact `## Coverage Scope` Field/Value table with the `|---|---|` separator row, a valid classification-policy pair, non-empty Affected Operations/Rule Keys/Scope Evidence, and the classification-specific Regression Floor. Require the exact unnumbered `## Coverage Matrix` heading in the immutable run-owned plan artifact, exact headers, exactly 9 cells in every data row (including a non-empty Dimension), deterministic OpenAPI Rule Keys for every in-scope affected operation, exactly one Matrix row per Rule Key (merge multi-dimension product rows), and bidirectional Matrix Rule/Test Point ↔ Case bindings. Never describe affected-scope coverage as whole-API completeness. Every explicit AC ID must appear in at least one Case `验收标准`; every explicit in-scope AC/REQ/BR Rule Key cited by a Case must have exactly one Coverage Matrix row, and no Case may cite a source Rule Key omitted from the Matrix. Every Matrix Case ID must share at least one of that row's Required Test Points and the Case must cite that Rule Key. Perform an explicit execution-redundancy review: merge checkpoint-only parameter rows, repeated default/read-back assertions, DELETE status/body/follow-up-read checks, response schema/Content-Type checks, PUT full-update/timestamp checks, repeated list setup and identical null/empty inputs when endpoint, input partition, precondition state and expected outcome are the same. Preserve separate POST/PUT, boundary, enum, wrong-type, role/tenant and distinct business-state variants. Directly repair malformed headings/rows/keys and binding modes rather than merely commenting on them. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, a `### 操作步骤` section that contains only a table without any numbered executable line, vague results such as ‘符合预期’, Case-ID-like module filenames (for example `BE-HEALTH.md`), dropped exact `### 操作步骤`/`### 预期结果` headings, and missing or drifted script/function mapping where it can be derived.",
|
|
4502
4648
|
"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
4649
|
"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
4650
|
"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
|
|
4506
|
-
"Before returning, verify that every explicit source AC/REQ/BR, error code and strong DTO field token appears in
|
|
4651
|
+
"Scenario Partitions synchronization: when the run-owned plan declares `## Scenario Partitions`, verify each declared partition's slots are fully materialized as variant Test Points with exact `TP-<Partition ID>-...` IDs (each-value per Domain value, OMITTED only for optional axes, exactly one NOT-IN-SET with intent=enum-invalid). Directly add missing slot rows/Cases. Record an illegal plan Partition row that has no source-backed finite domain as GAP/CONFLICT and remove only its derived `TP-SP-*` slots/Cases from target modules; never modify the immutable plan artifact. Never delete a legal source-backed partition or drop its complement slot to force coverage green. When the bound source does not document the complement expectation, keep the slot with GAP expected instead of guessing. Body-field validation enums (`TP-<FIELD>-ENUM-*`) are NOT partitions — do not add partition rows for them.",
|
|
4652
|
+
"Before returning, verify that every explicit source AC/REQ/BR, error code and strong DTO field token appears in the run-owned plan or an applicable module Case. If a fact cannot be safely automated, retain it as GAP/CONFLICT with its exact source pointer instead of dropping it. Return already-satisfied only when no target file needs an incremental edit.",
|
|
4507
4653
|
"Read only indexed source paths. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.",
|
|
4508
4654
|
...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
|
|
4509
4655
|
intake.boundedSourceContext,
|
|
@@ -4552,8 +4698,8 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4552
4698
|
writePolicy: "read-only",
|
|
4553
4699
|
allowedPaths: ro,
|
|
4554
4700
|
forbiddenPaths: forbidden,
|
|
4555
|
-
outputContract: "Stdout JSON {modules:[{stem}]} parsed from
|
|
4556
|
-
subtask_prompt: "Parse
|
|
4701
|
+
outputContract: "Stdout JSON {modules:[{stem,planReadPath}],planReadPath,planSha256} parsed from the run-owned Markdown plan artifact, so the pytest map shard set and every child planReadPath deterministically match the Markdown map manifest.",
|
|
4702
|
+
subtask_prompt: "Parse only the run-owned generate-backend-md-plan-pi/plan.md artifact and emit exactly one trailing JSON line {modules:[{stem,planReadPath}],planReadPath,planSha256}. Reuse the same plan-derived Module Index contract as the Markdown manifest; do not search for or fall back to testcase/**/README.md. No file writes.",
|
|
4557
4703
|
shell: {
|
|
4558
4704
|
commands: [buildBackendTestModuleManifestShellCommand(layout)],
|
|
4559
4705
|
cwd: ".",
|
|
@@ -4608,7 +4754,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4608
4754
|
retryPolicy: BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY,
|
|
4609
4755
|
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.",
|
|
4610
4756
|
subtaskPromptTemplate: [
|
|
4611
|
-
"Convert the single Markdown module testcase/md/{{item.stem}}.md into one self-contained pytest module. Before writing, also read
|
|
4757
|
+
"Convert the single Markdown module testcase/md/{{item.stem}}.md into one self-contained pytest module. Before writing, also read the run-owned Markdown plan artifact at `{{item.planReadPath}}` and use its explicit API target/environment table as the authoritative fallback base URL for every module. A task/Markdown `API_BASE_URL` target takes precedence over project README dev-server URLs; never infer a backend API fallback from a frontend/Vite port such as localhost:3000. After reading the module Markdown, the run-owned plan artifact, and the bounded pytest config/conftest, immediately use write tools to create the single 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.",
|
|
4612
4758
|
"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.",
|
|
4613
4759
|
"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
4760
|
"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.",
|
|
@@ -4624,6 +4770,13 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4624
4770
|
},
|
|
4625
4771
|
};
|
|
4626
4772
|
const collectionAssess = shellNode("assess-backend-pytest-collection-shell", [generatePytestCasesMap.id], "markdown-collection-assess", "Resolve final Markdown-mapped scripts before any business test body execution. Run scenario-param assess/at-most-one deterministic repair first, then pytest --collect-only and a no-business-body pytest --setup-plan fixture-resolution preflight. A safe missing mapped script, generated-local syntax/import defect, or generated fixture dependency/plugin-registration defect is REPAIRABLE; dependency, third-party plugin, production-module, environment, safety and unknown failures remain blocked. Materialize hash-bound collection-v3 facts with repairPaths and fixtureResolutionStatus.", "Run-owned reports/backend-test-pytest-collection-initial.md and contracts/backend-test-pytest-collection-initial.json (collection-v3) with bounded collection/fixture diagnostics, repairPaths, asset hashes, collected item IDs and deterministic repair eligibility.", [], 120000);
|
|
4773
|
+
// N10 owns the deterministic scenario-param rewrite that may update mapped
|
|
4774
|
+
// generated test modules before collection. Declare that bounded shell write
|
|
4775
|
+
// authority so filesystem-only workspace control does not misclassify the
|
|
4776
|
+
// intentional repair as an out-of-bounds mutation.
|
|
4777
|
+
collectionAssess.writePolicy = "exclusive";
|
|
4778
|
+
collectionAssess.writeSet = [applyLayout("testcase/**/test_*.py")];
|
|
4779
|
+
collectionAssess.allowedPaths = Array.from(new Set([...ro, ...collectionAssess.writeSet]));
|
|
4627
4780
|
const repairPytest = {
|
|
4628
4781
|
id: "repair-backend-pytest-collection-pi",
|
|
4629
4782
|
depends_on: [collectionAssess.id],
|
|
@@ -4633,8 +4786,24 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4633
4786
|
toolProfile: "write",
|
|
4634
4787
|
complexity: "MED",
|
|
4635
4788
|
writePolicy: "exclusive",
|
|
4636
|
-
writeSet: [
|
|
4637
|
-
|
|
4789
|
+
writeSet: Array.from(new Set([
|
|
4790
|
+
applyLayout("testcase/**/test_*.py"),
|
|
4791
|
+
`${layout.testRoot}/helpers/**/*.py`,
|
|
4792
|
+
`${layout.testRoot}/factories/**/*.py`,
|
|
4793
|
+
`${layout.testRoot}/__*_helpers.py`,
|
|
4794
|
+
`${layout.scriptDir}/helpers/**/*.py`,
|
|
4795
|
+
`${layout.scriptDir}/factories/**/*.py`,
|
|
4796
|
+
`${layout.scriptDir}/__*_helpers.py`,
|
|
4797
|
+
])),
|
|
4798
|
+
readSet: [
|
|
4799
|
+
".harness/dag-runs/**/reports/backend-test-pytest-collection-initial.md",
|
|
4800
|
+
".harness/dag-runs/**/reports/backend-test-markdown-pytest-correspondence-initial.md",
|
|
4801
|
+
".harness/dag-runs/**/generate-backend-md-plan-pi/plan.md",
|
|
4802
|
+
`${layout.markdownDir}/**`,
|
|
4803
|
+
`${layout.testRoot}/**/*.py`,
|
|
4804
|
+
],
|
|
4805
|
+
contextBudget: { maxTurns: 24, maxTokens: 80_000 },
|
|
4806
|
+
allowedPaths: Array.from(new Set([...ro, `${layout.testRoot}/**`])),
|
|
4638
4807
|
forbiddenPaths: Array.from(new Set([
|
|
4639
4808
|
...forbidden,
|
|
4640
4809
|
"testcase/md/**",
|
|
@@ -4647,7 +4816,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4647
4816
|
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.",
|
|
4648
4817
|
subtask_prompt: [
|
|
4649
4818
|
"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 (
|
|
4819
|
+
"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. Use this deterministic repair map instead of reading analyzer implementation: findings about `Case-ID`, `Assertion-Test-Points`, or `Cross-Cutting-Test-Points` are fixed by editing the declared primary symbol docstring metadata lines; variant binding findings are fixed in the literal direct `pytest.param(..., id=\"TP-...\")` row; primary-symbol cardinality/name findings are fixed in the function name or duplicate primary symbols; script mismatch is fixed only on the authoritative assessment repairPaths; payload findings are fixed in request payload construction. Do not read controller `src/**` or inspect JS/TS analyzer code. Do not search for `testcase/**/README.md`. 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 listed by repairPaths; 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
4820
|
"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.",
|
|
4652
4821
|
"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.",
|
|
4653
4822
|
"Use local edit only on assessment-listed paths; keep summaries short; never rewrite unrelated modules.",
|
|
@@ -4747,6 +4916,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4747
4916
|
// against the resolved layout. The default layout is identity, so the
|
|
4748
4917
|
// packaged template stays byte-identical with the historical contract.
|
|
4749
4918
|
applyBackendTestLayoutToDagSpec(spec, layout);
|
|
4919
|
+
applyBackendTestWorkspaceControl(spec, taskConfig.backendTest?.workspaceControl ?? "git");
|
|
4750
4920
|
// Plan B: carry the bound shared-setup document on the spec so runtime
|
|
4751
4921
|
// validators (N6 markdown case validation) see the same binding as prompts.
|
|
4752
4922
|
if (intake.sharedSetup) {
|
|
@@ -4757,6 +4927,23 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
4757
4927
|
assertValidDagSpec(spec);
|
|
4758
4928
|
return spec;
|
|
4759
4929
|
}
|
|
4930
|
+
function applyBackendTestWorkspaceControl(spec, control) {
|
|
4931
|
+
if (control !== "filesystem-only") {
|
|
4932
|
+
delete spec.backendTestWorkspaceControl;
|
|
4933
|
+
return;
|
|
4934
|
+
}
|
|
4935
|
+
spec.backendTestWorkspaceControl = control;
|
|
4936
|
+
for (const task of spec.tasks) {
|
|
4937
|
+
if ((task.executor === "pi" && task.toolProfile === "write") ||
|
|
4938
|
+
task.shell?.backendTestPipeline) {
|
|
4939
|
+
task.writeGuardPolicy = "filesystem-only";
|
|
4940
|
+
}
|
|
4941
|
+
const child = task.dynamicExpansion?.childTask;
|
|
4942
|
+
if (child?.executor === "pi" && child.toolProfile === "write") {
|
|
4943
|
+
child.writeGuardPolicy = "filesystem-only";
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4760
4947
|
/** Rewrite layout-dependent strings across a compiled backend-test DAG spec (plan A). */
|
|
4761
4948
|
function applyBackendTestLayoutToDagSpec(spec, layout) {
|
|
4762
4949
|
if (layout.isDefault) {
|
|
@@ -6563,7 +6750,7 @@ async function buildHybridDagForTemplate(sources, template, options = {}) {
|
|
|
6563
6750
|
if (!spec.runtimeContract) {
|
|
6564
6751
|
spec.runtimeContract = GENERATED_DAG_RUNTIME_CONTRACT;
|
|
6565
6752
|
}
|
|
6566
|
-
spec.sourceBinding = buildDagSourceBinding(sources);
|
|
6753
|
+
spec.sourceBinding = buildDagSourceBinding(sources, template === "frontend-implementation" ? "frontend-implementation" : undefined);
|
|
6567
6754
|
spec.taskContractBinding = taskContractBinding;
|
|
6568
6755
|
assertNoGovernanceFlagOnDisallowedTemplate(spec, template);
|
|
6569
6756
|
if (template === "standard-dag" ||
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { watch } from "node:fs";
|
|
2
|
-
import { mkdir
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import { hostname as localHostname } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
|
+
import { readJsonWithTransientRetry, writeJsonAtomic, } from "../../infrastructure/harness/atomic-write.js";
|
|
6
6
|
import { assessDagRunLiveness, getDagRunDir, locateDagRun, readDagRunState, } from "./lifecycle.js";
|
|
7
7
|
export const INTERRUPT_ARTIFACT_REL = path.posix.join(".runtime", "interrupt.json");
|
|
8
8
|
export const INTERRUPT_REASON_CODES = [
|
|
@@ -140,7 +140,7 @@ export function parseInterruptRequest(raw) {
|
|
|
140
140
|
}
|
|
141
141
|
export async function readInterruptRequest(runDir) {
|
|
142
142
|
try {
|
|
143
|
-
const raw =
|
|
143
|
+
const raw = await readJsonWithTransientRetry(interruptArtifactPath(runDir));
|
|
144
144
|
return parseInterruptRequest(raw);
|
|
145
145
|
}
|
|
146
146
|
catch (error) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { access, mkdir, readFile, readdir, rename } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { hostname as localHostname } from "node:os";
|
|
4
|
-
import { writeJsonAtomic, } from "../../infrastructure/harness/atomic-write.js";
|
|
4
|
+
import { readJsonWithTransientRetry, writeJsonAtomic, } from "../../infrastructure/harness/atomic-write.js";
|
|
5
5
|
import { parseDagSpec } from "./types.js";
|
|
6
6
|
import { normalizeDagFailureCategory, } from "./failure-category.js";
|
|
7
7
|
import { dagProductLineFailureCategoryValues, routeDagFailure, } from "./failure-routing.js";
|
|
@@ -43,14 +43,14 @@ export async function dagRunDirExists(runDir) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
export async function readDagRunState(runDir) {
|
|
46
|
-
const raw =
|
|
46
|
+
const raw = await readJsonWithTransientRetry(path.join(runDir, "state.json"));
|
|
47
47
|
return raw;
|
|
48
48
|
}
|
|
49
49
|
export async function writeDagRunState(runDir, state, options) {
|
|
50
50
|
await writeJsonAtomic(path.join(runDir, "state.json"), state, options);
|
|
51
51
|
}
|
|
52
52
|
export async function readDagRunSpec(runDir) {
|
|
53
|
-
const raw =
|
|
53
|
+
const raw = await readJsonWithTransientRetry(path.join(runDir, "run.json"));
|
|
54
54
|
return parseDagSpec(raw);
|
|
55
55
|
}
|
|
56
56
|
export async function locateDagRun(cwd, runId) {
|