@tea-agent/loop-agent 0.42.0-next.5 → 0.42.0-next.7

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.
Files changed (108) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +22 -0
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/executors/dag-pi-executor.js +75 -13
  5. package/dist/task/source-prepare/fragment-inventory.js +49 -16
  6. package/dist/task/source-prepare/prepare.js +37 -20
  7. package/dist/worker/console/chat/model-resolver.js +7 -4
  8. package/dist/worker/console/chat/pi-runtime.js +40 -1
  9. package/dist/worker/console/chat/routes.js +25 -24
  10. package/dist/worker/console/chat/sdd-data-alignment.js +6 -3
  11. package/dist/worker/console/pi-readiness.js +3 -2
  12. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BJpsGgAv.js → abnfDiagram-N423BO3Z-BQBHX31n.js} +1 -1
  13. package/dist/worker/console/static/assets/{arc-DjMFPsEd.js → arc-MSJ199eR.js} +1 -1
  14. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DqdZlr1O.js → architectureDiagram-T3A2C74G-DiRq2qMX.js} +1 -1
  15. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BmZnfFEO.js → blockDiagram-VBNYF7ZC-rlaxFOvi.js} +1 -1
  16. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-3cKberpL.js → c4Diagram-5PPSVZJV-BSpi9ump.js} +1 -1
  17. package/dist/worker/console/static/assets/channel-BdTaMnYP.js +1 -0
  18. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-q-0yjQ1h.js → chunk-2GRJ4B5K-BN8VNeh5.js} +1 -1
  19. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DnONBaDW.js → chunk-2Q5K7J3B-CDa1OVRA.js} +1 -1
  20. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-v3a6PCOS.js → chunk-5RXB4S5H-zxMQ5b8E.js} +1 -1
  21. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D1kLrlU_.js → chunk-5VM5RSS4-XTqNlzH4.js} +1 -1
  22. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BPelzihi.js → chunk-6Q2QTUOP-DpZQfFTq.js} +1 -1
  23. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DauNE69H.js → chunk-GF5L2VYU-DPv743zu.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-RIg5_bFJ.js → chunk-JWPE2WC7-CFsCFoF4.js} +1 -1
  25. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BuILuoxs.js → chunk-KBJHAD2P-CV__aJTt.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-VHORV5oW.js → chunk-RYQCIY6F-BshKlNir.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BJYr4xHC.js → chunk-XXDRQBXY-B-A3ErBP.js} +1 -1
  28. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cid7CSRK.js +1 -0
  29. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cid7CSRK.js +1 -0
  30. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BS6nuqQn.js → cose-bilkent-JH36ORCC-25xj8928.js} +1 -1
  31. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BBhF0NAT.js → cynefin-VYW2F7L2-DFmv38cl.js} +1 -1
  32. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D1135Ifg.js → cynefinDiagram-MW4NZA55-hReuWDQP.js} +1 -1
  33. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-vAOYPUWJ.js → dagre-VZM6K2ZE-C9X3YPUI.js} +1 -1
  34. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DuaSPkfa.js → diagram-7IWD3JNH-CAVmqjg9.js} +1 -1
  35. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-mMvXvGU6.js → diagram-B4RE2ZJO-DYNlLsIm.js} +1 -1
  36. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDIEJtlL.js → diagram-LBJQPF4R-BP5mlbfb.js} +1 -1
  37. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Bi_6JvCW.js → diagram-Q27KOJAE-KWOHK5ZM.js} +1 -1
  38. package/dist/worker/console/static/assets/{diagram-UB23O5K3-DAqj57yN.js → diagram-UB23O5K3-D25Ro5y-.js} +1 -1
  39. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DfWaxNEo.js → ebnfDiagram-BXEA7PRR-kLaahtbh.js} +1 -1
  40. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bt7QqkNG.js → erDiagram-JOGREHBK-1tgKuLez.js} +1 -1
  41. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CRRhQwT2.js → flowDiagram-UKHOOZJN-DARr-Cvn.js} +1 -1
  42. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DAV1O5B6.js → ganttDiagram-PKOTCBZU-BgwjBv_V.js} +1 -1
  43. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-0X2Qo4Lp.js → gitGraphDiagram-DS77QQ5N-DneIlYSI.js} +1 -1
  44. package/dist/worker/console/static/assets/{index-CVdddQsA.js → index-Dkv0ezuE.js} +75 -77
  45. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C5W5eILC.js → infoDiagram-6WML65LV-DA7ATqY6.js} +1 -1
  46. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-5SfqU-Tb.js → ishikawaDiagram-WSZJBQD7-DnOS-E1Z.js} +1 -1
  47. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CKDRcaKf.js → journeyDiagram-NVQOT4AX-C8JnwJ_s.js} +1 -1
  48. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Cc7_TuSb.js → kanban-definition-27J2QSJJ-Cw0_OjEC.js} +1 -1
  49. package/dist/worker/console/static/assets/{linear-BcQYYmeT.js → linear-mrxq3fyn.js} +1 -1
  50. package/dist/worker/console/static/assets/{mermaid.core-CwLZPtQo.js → mermaid.core-Bagus6zF.js} +5 -5
  51. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D29M025z.js → mindmap-definition-FAOFIHXS-Bf95bLuW.js} +1 -1
  52. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CV-ZAWYe.js → pegDiagram-VL7TDLO6-BvtUW6w2.js} +1 -1
  53. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-C6iPS_o5.js → pieDiagram-7S7Q4E2Y-CbWtT-Ry.js} +1 -1
  54. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BFCD1sTg.js → quadrantDiagram-CIZ2JOQS-ctnxxIdD.js} +1 -1
  55. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CztNj4O9.js → railroadDiagram-AXF67PYL-BZ7Z3X0-.js} +1 -1
  56. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BWIELSNd.js → requirementDiagram-LRYGKXZP-BMX1xnIy.js} +1 -1
  57. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CPtxrjJQ.js → sankeyDiagram-W5VNT64P-MShOQznz.js} +1 -1
  58. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BEWVSFsu.js → sequenceDiagram-SI44F4Z6-CTGOazaB.js} +1 -1
  59. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DyhzoX_J.js → sizeCapture-X5ZJPWSS-zGbfM4nV.js} +1 -1
  60. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-xLox0nu7.js → stateDiagram-OKZ733FA-Brf8Wwzr.js} +1 -1
  61. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CIkoculx.js +1 -0
  62. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CLC_ow9A.js → swimlanes-SLNWSIFB-CvNw3ZqF.js} +2 -2
  63. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-pJ-V5EdT.js +8 -0
  64. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DmJZ4uyY.js → timeline-definition-Z64GVDOM-z2LyZpDt.js} +1 -1
  65. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DD7GGi9Z.js → vennDiagram-T6HMQDX7-Bc_coprH.js} +1 -1
  66. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Bi8PQoGt.js → wardleyDiagram-T6FBY63Y-M1NSmWq-.js} +1 -1
  67. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-uKdAY8Dp.js → xychartDiagram-ELKLHX3M-Ct0rjGXW.js} +1 -1
  68. package/dist/worker/console/static/index.html +1 -1
  69. package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -5
  70. package/dist/worker/console/static-src/operator-chat/input-history.js +8 -6
  71. package/dist/worker/console/static-src/shell/console-update-reload.js +36 -9
  72. package/dist/worker/console/static-src/shell/workspace-route.js +11 -0
  73. package/dist/worker/observe/static/dag-helpers.js +4 -2
  74. package/dist/worker/observe/static/inspect-workspace.js +23 -0
  75. package/dist/worker/observe/static/kpi.js +2 -2
  76. package/dist/worker/observe/static/operator-chrome.d.ts +10 -2
  77. package/dist/worker/observe/static/operator-chrome.js +37 -23
  78. package/dist/worker/observe/static/relations.js +2 -2
  79. package/dist/worker/observe/static/router.d.ts +12 -1
  80. package/dist/worker/observe/static/router.js +48 -6
  81. package/dist/worker/observe/static/run-processing.js +4 -2
  82. package/dist/worker/observe/static/shell-chrome.js +2 -2
  83. package/dist/worker/observe/static/state.js +5 -0
  84. package/dist/worker/observe/static/styles.css +95 -0
  85. package/dist/worker/observe/static/views/dag-inspector.js +14 -7
  86. package/dist/worker/observe/static/views/dag.js +10 -2
  87. package/dist/worker/observe/static/views/dags.js +3 -1
  88. package/dist/worker/observe/static/views/dashboard.js +14 -6
  89. package/dist/worker/observe/static/views/pool.js +7 -2
  90. package/dist/worker/observe/static/views/run.js +12 -3
  91. package/dist/worker/observe/static/views/session-timeline.js +69 -10
  92. package/dist/worker/observe/static/views/task.js +7 -2
  93. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +58 -15
  94. package/dist/workflows/dag/backend-test-plan-protocol.js +106 -0
  95. package/dist/workflows/dag/backend-test-scenario-param.js +16 -5
  96. package/dist/workflows/dag/backend-test-writer-completeness.js +42 -6
  97. package/dist/workflows/dag/frontend-shadow-dual-write.js +33 -6
  98. package/dist/workflows/dag/init-hybrid.js +193 -32
  99. package/dist/workflows/dag/node-execution.js +1 -1
  100. package/dist/workflows/dag/prompt.js +36 -2
  101. package/dist/workflows/dag/types.js +1 -1
  102. package/docs/templates/backend-test-dag.json +5 -5
  103. package/package.json +3 -2
  104. package/dist/worker/console/static/assets/channel-DWYwciiv.js +0 -1
  105. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D6jOf_l4.js +0 -1
  106. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D6jOf_l4.js +0 -1
  107. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DS5ZRvJG.js +0 -1
  108. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Dvpxn-bf.js +0 -8
@@ -26,7 +26,7 @@ import { CANONICAL_TASK_ID_PATTERN, getTaskPaths, loadTaskConfig } from "../../t
26
26
  import { materializeTaskReferenceDocs } from "../../task/source-references.js";
27
27
  import { observeTaskContract } from "../../task/contract/observe.js";
28
28
  import { extractRequirementFactsFromMarkdown } from "../../task/source-prepare/parse-intent.js";
29
- import { computeLedgerInputDigest, parseLedgerJson, recoverLedgerInputContract, } from "../../task/source-prepare/ledger.js";
29
+ import { computeLedgerInputDigest, parseLedgerJson, recoverLedgerInputContract, validateRequirementLedger, } from "../../task/source-prepare/ledger.js";
30
30
  import { REQUIREMENT_LEDGER_FILE_NAME } from "../../task/contract/constants.js";
31
31
  import { dagHasWriterExecution } from "./task-contract-binding.js";
32
32
  import { DEFAULT_VERIFY_TIMEOUT_MS, resolveVerifyPreset, } from "../../executors/shell-verification.js";
@@ -1224,21 +1224,121 @@ function canonicalizeVerificationCommands(commands) {
1224
1224
  }
1225
1225
  function assertVerificationPlanPreflight(input) {
1226
1226
  const root = path.resolve(input.repoRoot);
1227
+ const statuses = [];
1227
1228
  for (const command of input.commands) {
1228
1229
  const cwd = path.resolve(command.cwd);
1229
1230
  if (!isWithinRepo(root, cwd) || !existsSync(cwd)) {
1230
1231
  throw new Error(`verification preflight rejected cwd for ${command.label}: ${command.cwd}`);
1231
1232
  }
1233
+ const vitestStatus = verifyVitestCommandPreflight({
1234
+ command,
1235
+ root,
1236
+ taskConfig: input.taskConfig,
1237
+ });
1238
+ if (vitestStatus) {
1239
+ statuses.push({ label: command.label, status: vitestStatus });
1240
+ continue;
1241
+ }
1232
1242
  const script = localScriptTarget(command);
1233
- if (!script)
1243
+ if (!script) {
1244
+ statuses.push({ label: command.label, status: "ok" });
1234
1245
  continue;
1246
+ }
1235
1247
  const scriptPath = path.resolve(cwd, script);
1236
- if (existsSync(scriptPath))
1248
+ if (existsSync(scriptPath)) {
1249
+ statuses.push({ label: command.label, status: "ok" });
1237
1250
  continue;
1251
+ }
1238
1252
  const writerMayCreate = input.taskConfig.allowedPaths.some((allowed) => pathMatchesPattern(script, allowed.replace(/^\.\//, "")));
1239
1253
  if (!writerMayCreate) {
1240
1254
  throw new Error(`verification preflight missing local script for ${command.label}: ${script}`);
1241
1255
  }
1256
+ statuses.push({ label: command.label, status: "future-delivery-script" });
1257
+ }
1258
+ return statuses;
1259
+ }
1260
+ function verifyVitestCommandPreflight(input) {
1261
+ const args = normalizeVerifyCommandArgs(input.command.args);
1262
+ const vitestIndex = args.findIndex((arg) => path.basename(arg).replace(/\.(?:exe|cmd)$/i, "") === "vitest");
1263
+ if (vitestIndex < 0)
1264
+ return undefined;
1265
+ const configArgIndex = args.findIndex((arg, index) => index > vitestIndex &&
1266
+ (arg === "--config" || arg === "-c" || arg.startsWith("--config=")));
1267
+ let configPath;
1268
+ if (configArgIndex >= 0) {
1269
+ const arg = args[configArgIndex];
1270
+ configPath = arg.startsWith("--config=")
1271
+ ? arg.slice("--config=".length)
1272
+ : args[configArgIndex + 1];
1273
+ }
1274
+ const cwd = path.resolve(input.command.cwd);
1275
+ const configAbsolute = configPath
1276
+ ? path.resolve(cwd, configPath)
1277
+ : findRootVitestConfig(input.root);
1278
+ const configRelative = configAbsolute
1279
+ ? path.relative(input.root, configAbsolute).replace(/\\/g, "/")
1280
+ : undefined;
1281
+ if (configAbsolute && !existsSync(configAbsolute)) {
1282
+ const writerMayCreate = Boolean(configRelative &&
1283
+ input.taskConfig.allowedPaths.some((allowed) => pathMatchesPattern(configRelative, allowed.replace(/^\.\//, ""))));
1284
+ if (!writerMayCreate) {
1285
+ throw new Error(`verification preflight missing Vitest config for ${input.command.label}: ${configRelative ?? configPath}`);
1286
+ }
1287
+ }
1288
+ const targetPaths = vitestTargetPaths(args.slice(vitestIndex + 1));
1289
+ if (targetPaths.length === 0)
1290
+ return "ok";
1291
+ const inheritedConfig = !configPath && configAbsolute && existsSync(configAbsolute);
1292
+ if (inheritedConfig && vitestConfigExcludesDogfood(configAbsolute, targetPaths)) {
1293
+ throw new Error(`verification preflight Vitest target ${targetPaths.join(", ")} is excluded by ${path.relative(input.root, configAbsolute).replace(/\\/g, "/")}`);
1294
+ }
1295
+ const futureTargets = targetPaths.filter((target) => {
1296
+ const absolute = path.resolve(cwd, target);
1297
+ if (existsSync(absolute))
1298
+ return false;
1299
+ return !input.taskConfig.allowedPaths.some((allowed) => pathMatchesPattern(target, allowed.replace(/^\.\//, "")));
1300
+ });
1301
+ if (futureTargets.length > 0) {
1302
+ throw new Error(`verification preflight missing Vitest target for ${input.command.label}: ${futureTargets.join(", ")}`);
1303
+ }
1304
+ const missingAllowedTargets = targetPaths.filter((target) => !existsSync(path.resolve(cwd, target)));
1305
+ return missingAllowedTargets.length > 0 ? "future-test-target" : "ok";
1306
+ }
1307
+ function findRootVitestConfig(root) {
1308
+ for (const name of ["vitest.config.ts", "vitest.config.js", "vitest.config.mjs", "vitest.config.cjs"]) {
1309
+ const candidate = path.join(root, name);
1310
+ if (existsSync(candidate))
1311
+ return candidate;
1312
+ }
1313
+ return undefined;
1314
+ }
1315
+ function vitestTargetPaths(args) {
1316
+ const targets = [];
1317
+ let afterRun = false;
1318
+ for (let index = 0; index < args.length; index += 1) {
1319
+ const arg = args[index];
1320
+ if (arg === "run" || arg === "related") {
1321
+ afterRun = true;
1322
+ continue;
1323
+ }
1324
+ if (!afterRun || arg.startsWith("-")) {
1325
+ if ((arg === "--config" || arg === "-c" || arg === "--reporter") && !arg.includes("="))
1326
+ index += 1;
1327
+ continue;
1328
+ }
1329
+ targets.push(arg);
1330
+ }
1331
+ return targets.filter((target) => target.includes("/") || /\.(?:test|spec)\.[^/]+$/.test(target));
1332
+ }
1333
+ function vitestConfigExcludesDogfood(configPath, targets) {
1334
+ if (!targets.some((target) => target.replace(/\\/g, "/").startsWith("dogfood/")))
1335
+ return false;
1336
+ try {
1337
+ const text = readFileSync(configPath, "utf8");
1338
+ return /exclude\s*:[\s\S]{0,500}dogfood\/\*\*/.test(text);
1339
+ }
1340
+ catch {
1341
+ return false;
1242
1342
  }
1243
1343
  }
1244
1344
  function isWithinRepo(root, candidate) {
@@ -1307,10 +1407,11 @@ function buildVerifyEvidence(input) {
1307
1407
  selectionReasons: selectedCommands?.map((command) => input.phase === "intermediate" && isFullSuiteVerifyCommand(command)
1308
1408
  ? `${command.label}: deferred-heavy`
1309
1409
  : `${command.label}: kept`) ?? [],
1310
- preflight: selectedCommands?.map((command) => ({
1311
- label: command.label,
1312
- status: "ok",
1313
- })) ?? [],
1410
+ preflight: input.preflight ??
1411
+ (selectedCommands?.map((command) => ({
1412
+ label: command.label,
1413
+ status: "ok",
1414
+ })) ?? []),
1314
1415
  commandTimeoutMs: input.commandTimeoutMs,
1315
1416
  totalTimeoutBudgetMs: commandCount * input.commandTimeoutMs,
1316
1417
  finalFullRequired: input.finalFullRequired,
@@ -1499,6 +1600,8 @@ function extractExplicitRequirementIds(requirementMarkdown, ...fallbackMarkdown)
1499
1600
  : markdown;
1500
1601
  for (const match of source.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
1501
1602
  const id = match[0].toUpperCase();
1603
+ if (isPlaceholderRequirementId(id))
1604
+ continue;
1502
1605
  if (!seen.has(id)) {
1503
1606
  seen.add(id);
1504
1607
  ids.push(id);
@@ -1528,6 +1631,8 @@ function extractSectionRequirementIds(sectionMarkdown) {
1528
1631
  const seen = new Set();
1529
1632
  for (const match of sectionMarkdown.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
1530
1633
  const id = match[0].toUpperCase();
1634
+ if (isPlaceholderRequirementId(id))
1635
+ continue;
1531
1636
  if (!seen.has(id)) {
1532
1637
  seen.add(id);
1533
1638
  ids.push(id);
@@ -1535,6 +1640,9 @@ function extractSectionRequirementIds(sectionMarkdown) {
1535
1640
  }
1536
1641
  return ids;
1537
1642
  }
1643
+ function isPlaceholderRequirementId(id) {
1644
+ return /(?:^|-)X{2,}$/.test(id);
1645
+ }
1538
1646
  /**
1539
1647
  * Prefer TaskSpec-scoped acceptance refs from the derived 需求.md section when
1540
1648
  * present. Full Feature acceptance.yaml may list sibling ACs that this task is
@@ -1613,21 +1721,14 @@ function buildDagSourceBinding(sources, taskKind) {
1613
1721
  const ledgerBinding = loadPersistedFrontendSourceBindingLedger(sources);
1614
1722
  if (!ledgerBinding)
1615
1723
  return base;
1616
- // AC-002 / AC-HARD-002:物化 REQ-SRC-* canonical id 确定性并入 v2 requirementIds。
1617
- // 生成型 REQ-SRC-* id 无法从 markdown 文本导出(必须来自持久化 ledger),此处
1618
- // 在 required requirement ids 侧确定性注入,确保 prewrite missing-requirement-ids
1619
- // 不误伤(不要求文本可导出)也不漏检(物化 id 必须被契约覆盖)。
1620
- const materializedRequirementIds = Object.keys(ledgerBinding.requirementToFragments)
1621
- .filter((id) => /^REQ-SRC-/.test(id) && !base.requirementIds.includes(id))
1622
- .sort();
1724
+ // v2 is ledger-owned: the persisted canonical set is the only requirement
1725
+ // identity source. In particular, explanatory AC-XXX mentions from a
1726
+ // materialized reference document must not leak in beside ledger ids.
1623
1727
  return {
1624
1728
  schemaVersion: 2,
1625
1729
  taskId: base.taskId,
1626
1730
  sources: base.sources,
1627
- requirementIds: [
1628
- ...base.requirementIds,
1629
- ...materializedRequirementIds,
1630
- ],
1731
+ requirementIds: ledgerBinding.requirementIds,
1631
1732
  ledgerPath: ledgerBinding.ledgerPath,
1632
1733
  ledgerSha256: ledgerBinding.ledgerSha256,
1633
1734
  inputDigest: ledgerBinding.inputDigest,
@@ -1644,15 +1745,19 @@ function buildDagSourceBinding(sources, taskKind) {
1644
1745
  */
1645
1746
  function loadPersistedFrontendSourceBindingLedger(sources) {
1646
1747
  const referenceDocs = (sources.referenceDocuments ?? []).filter((doc) => doc.markdown.trim().length > 0);
1647
- if (referenceDocs.length === 0)
1648
- return null;
1748
+ const ledgerAbsolutePath = path.join(sources.taskDir, "source", REQUIREMENT_LEDGER_FILE_NAME);
1749
+ // Text-created frontend tasks have no imported reference manifest. Their
1750
+ // task-owned projected requirement is nevertheless a valid canonical source
1751
+ // document and is persisted in the v2 ledger by source preparation. Keep it
1752
+ // in the current-input map below instead of silently downgrading to v1.
1649
1753
  // 存在性守卫(保持既有 v1 回退行为):派生视图无可抽取 requirement/acceptance
1650
1754
  // 引用时不要求持久化 ledger,直接回退 base binding。此守卫只用于"是否 ledger
1651
1755
  // 任务"判定,绝不参与 digest 键计算——键输入完全来自持久化 ledger 恢复。
1652
1756
  const guardFacts = extractRequirementFactsFromMarkdown(sources.requirementMarkdown);
1653
- if (guardFacts.acceptanceCriteria.length === 0)
1757
+ if (guardFacts.acceptanceCriteria.length === 0 && referenceDocs.length > 0)
1758
+ return null;
1759
+ if (referenceDocs.length === 0 && !existsSync(ledgerAbsolutePath))
1654
1760
  return null;
1655
- const ledgerAbsolutePath = path.join(sources.taskDir, "source", REQUIREMENT_LEDGER_FILE_NAME);
1656
1761
  let raw;
1657
1762
  try {
1658
1763
  raw = readFileSync(ledgerAbsolutePath, "utf8");
@@ -1668,6 +1773,13 @@ function loadPersistedFrontendSourceBindingLedger(sources) {
1668
1773
  catch (error) {
1669
1774
  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)}`);
1670
1775
  }
1776
+ const ledgerErrors = validateRequirementLedger(ledger);
1777
+ if (ledgerErrors.length > 0) {
1778
+ const first = ledgerErrors[0];
1779
+ throw new Error(`${first.code}: persisted ${REQUIREMENT_LEDGER_FILE_NAME} is invalid; refusing to start a frontend DAG with incomplete source bindings (${ledgerErrors
1780
+ .map((error) => error.message)
1781
+ .join("; ")})`);
1782
+ }
1671
1783
  // canonical 侧:ledger.canonicalRequirements 按 build 顺序原样保留输入 canonical
1672
1784
  // requirements(id/text 逐字节),过滤物化 REQ-SRC-*(AC-HARD-002 "物化不入键")
1673
1785
  // 即精确恢复 digest 输入集;派生导航视图 需求.md 投影不参与键计算。
@@ -1686,6 +1798,13 @@ function loadPersistedFrontendSourceBindingLedger(sources) {
1686
1798
  for (const doc of referenceDocs) {
1687
1799
  currentByPath.set(toDagSourcePath(sources, doc.path), doc.markdown);
1688
1800
  }
1801
+ if (referenceDocs.length === 0) {
1802
+ currentByPath.set(toDagSourcePath(sources, sources.requirementPath),
1803
+ // applyTaskContract appends a derived `## Source ledger` navigation
1804
+ // block after the ledger input has been hashed. Remove that block when
1805
+ // reconstructing the canonical text digest.
1806
+ sources.requirementMarkdown.replace(/\n## Source ledger\b[\s\S]*$/u, ""));
1807
+ }
1689
1808
  const missingAuthoritativePaths = contract.sourcePaths.filter((sourcePath) => !currentByPath.has(sourcePath));
1690
1809
  if (missingAuthoritativePaths.length > 0) {
1691
1810
  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`);
@@ -1713,6 +1832,7 @@ function loadPersistedFrontendSourceBindingLedger(sources) {
1713
1832
  ledgerPath: toDagSourcePath(sources, ledgerAbsolutePath),
1714
1833
  ledgerSha256,
1715
1834
  inputDigest: ledger.inputDigest,
1835
+ requirementIds: ledger.canonicalRequirements.map((requirement) => requirement.id),
1716
1836
  requirementToFragments,
1717
1837
  };
1718
1838
  }
@@ -1845,6 +1965,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
1845
1965
  const manifest = await loadHarnessManifest(repoRoot);
1846
1966
  const strategy = resolveDagVerifyStrategy(taskConfig);
1847
1967
  let verifyCommands;
1968
+ let verificationPreflight = [];
1848
1969
  try {
1849
1970
  const adapter = await resolveAdapter(repoRoot);
1850
1971
  const preset = resolveVerifyPreset(taskConfig.verifyPreset, taskConfig);
@@ -1868,7 +1989,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
1868
1989
  ...taskVerifyCommands(repoRoot, taskConfig),
1869
1990
  ...adapterFinal,
1870
1991
  ]);
1871
- assertVerificationPlanPreflight({
1992
+ verificationPreflight = assertVerificationPlanPreflight({
1872
1993
  repoRoot,
1873
1994
  commands: [...intermediatePlan.commands, ...finalPlan.commands],
1874
1995
  taskConfig,
@@ -1911,6 +2032,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
1911
2032
  enabledExecutors: resolveEnabledExecutors(manifest.executors),
1912
2033
  executorModelMatrix: resolveExecutorModelMatrices(manifest),
1913
2034
  verifyCommands,
2035
+ verificationPreflight,
1914
2036
  sddEmbeddedSkills: await probeRepoLocalSddSkills(repoRoot),
1915
2037
  projectGovernancePresent: await discoverProjectGovernancePresence(repoRoot),
1916
2038
  ...(frontendShapeTransitionCapsule ? { autoloadedFrontendShapeTransitionCapsule: frontendShapeTransitionCapsule } : {}),
@@ -2004,6 +2126,7 @@ export function buildStandardHybridDagFromTask(sources) {
2004
2126
  finalFullRequired: true,
2005
2127
  commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
2006
2128
  plan: finalVerifyPlan,
2129
+ preflight: sources.verificationPreflight,
2007
2130
  }),
2008
2131
  cwd: ".",
2009
2132
  timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
@@ -2331,6 +2454,7 @@ function buildFrontendMockVerifyNode(sources, implementId, readOnlyPaths, forbid
2331
2454
  fallbackCommands: [],
2332
2455
  commandTexts: commands,
2333
2456
  commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
2457
+ preflight: sources.verificationPreflight,
2334
2458
  }),
2335
2459
  cwd: ".",
2336
2460
  timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
@@ -3043,6 +3167,7 @@ async function buildFrontendHybridDagFromTask(sources) {
3043
3167
  fallbackCommands: staticFallbackCommands,
3044
3168
  commandTexts: staticShellCommands,
3045
3169
  commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
3170
+ preflight: sources.verificationPreflight,
3046
3171
  });
3047
3172
  const lintVerifyEvidence = lintShellCommands.length > 0
3048
3173
  ? buildVerifyEvidence({
@@ -3053,6 +3178,7 @@ async function buildFrontendHybridDagFromTask(sources) {
3053
3178
  fallbackCommands: [],
3054
3179
  commandTexts: lintShellCommands,
3055
3180
  commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
3181
+ preflight: sources.verificationPreflight,
3056
3182
  })
3057
3183
  : undefined;
3058
3184
  const behaviorVerifyEvidence = buildVerifyEvidence({
@@ -3064,6 +3190,7 @@ async function buildFrontendHybridDagFromTask(sources) {
3064
3190
  commandTexts: behaviorShellCommands,
3065
3191
  finalFullRequired: true,
3066
3192
  commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
3193
+ preflight: sources.verificationPreflight,
3067
3194
  });
3068
3195
  const mockVerifyTemplate = mockMode === "required" && hasMockVerifyCommands
3069
3196
  ? buildFrontendMockVerifyNode(frontendSources, implementId, readOnlyPaths, forbiddenPaths)
@@ -3227,7 +3354,7 @@ async function buildFrontendHybridDagFromTask(sources) {
3227
3354
  subtask_prompt: [
3228
3355
  "Inspect frontend code, routing, components, styles, package scripts, and tests.",
3229
3356
  "Return code and design observations, existing reuse opportunities, and verification entry points.",
3230
- "Report target surface and design evidence as facts (no fixed section title required): completeness, entrypoint, routeOrMount, implementationPaths, testPaths, dataSource, allowedPathConflicts, unresolvedPaths. Use repository-relative POSIX paths. A complete surface must name at least one proven entrypoint, implementation path, or test path and set unresolvedPaths to []; if any target ownership remains unknown, commit completeness=blocked with every unresolved path instead of guessing. implementationPaths and testPaths must name the existing files/directories that actually own the requested behavior; allowedPathConflicts must list every discovered path not covered by task allowedPaths, or [] when none exists.",
3357
+ "Report target surface and design evidence as facts (no fixed section title required): completeness, entrypoint, routeOrMount, implementationPaths, testPaths, dataSource, allowedPathConflicts, unresolvedPaths. Use repository-relative POSIX paths. A complete surface must name at least one target candidate and set unresolvedPaths to []; if any target ownership remains unknown, commit completeness=blocked with every unresolved path instead of guessing. Prefer existing files/directories when they exist. For a greenfield target explicitly pinned by the task source, future implementation/test paths are allowed, but every such path must be named by the source and runtime-enriched as sourceDeclared; do not invent paths merely because they fit allowedPaths. allowedPathConflicts must list every discovered path not covered by task allowedPaths, or [] when none exists.",
3231
3358
  "Derive all file paths from this target workspace. Do not assume the project uses src/, test/, React, or the loop-agent repository layout.",
3232
3359
  "Read-only: do not modify repository files.",
3233
3360
  sourceContexts.scout,
@@ -3422,6 +3549,7 @@ async function buildFrontendHybridDagFromTask(sources) {
3422
3549
  outputContract: "The implementation status is derived by the executor from mechanical facts (write-tool events, run delta, write guard, requirement coverage, focused-check), not from any IMPLEMENTATION_OUTCOME first line. Deliver a Markdown summary with Contract Ref (path/schema/hash), Changed Files, Requirements Implemented, UI States, Tests Changed, Verification Attempts, Deviations, and Residual Risks. Follow fixed stages: contract confirm → tests → component/state → API/Mock → focused checks → diff cleanup.",
3423
3550
  subtask_prompt: [
3424
3551
  "Implement against the validated run-owned Frontend Implementation Contract materialized by frontend-design-policy-shell (path/schema/hash) and authorized by frontend-writer-admission-shell. Do not rebuild the contract from Markdown alone.",
3552
+ "WRITER TOOL PROTOCOL (hard): the response text is not delivery. Never paste source code, test code, or full file contents into chat. After reading the canonical contract, make the first implementation change with the structured write/edit tool (one file per call); continue writing through those tools until the writeSet is complete. If a write/edit tool is unavailable, stop and report the blocked capability instead of drafting code in the response.",
3425
3553
  "The canonical contract already contains the approved requirement, target-file, UI-state, verification, design, and Mock/API decisions. Do not re-open task sources, OpenSpec, AI workspace, plan/revision, or design-review prose, and do not repeat broad repository research. Inspect only contract target files and directly related local code needed to implement them.",
3426
3554
  "Execute in fixed stages and report each in the delivery summary: (1) Contract confirm, (2) Tests sync, (3) Component/UI state implementation, (4) API/Mock wiring per contract.mockApi, (5) Focused checks behind frozen entrypoints only, (6) Diff cleanup.",
3427
3555
  "Map every requirement id, expectedOutcome, interaction trigger/expectedBehavior, and applicable UI state from the contract to concrete files. Do not invent shell verification commands; only frozen static/behavior entrypoints will run.",
@@ -4243,7 +4371,11 @@ const stripBackticks=s=>s.split(bt).join('');
4243
4371
  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;};
4244
4372
  const valid=raw=>invalidReason(raw)===null;
4245
4373
  const rxRelLink=/\\[[^\\]]+\\]\\(\\.\\/([A-Za-z0-9_.-]+)\\.md\\)/g;
4374
+ const headingAlias={'\u6a21\u5757\u7d22\u5f15':'Module Index','\u8986\u76d6\u8303\u56f4':'Coverage Scope','\u8986\u76d6\u77e9\u9635':'Coverage Matrix','\u573a\u666f\u5206\u533a':'Scenario Partitions'};
4246
4375
  const allLines=readme.replace(/\\r\\n/g,'\\n').replace(/\\r/g,'\\n').split('\\n');
4376
+ let headingRepair=false;
4377
+ for(let i=0;i<allLines.length;i++){const alias=/^##\\s+(模块索引|覆盖范围|覆盖矩阵|场景分区)\\s*$/.exec(allLines[i].trim());if(alias){allLines[i]='## '+headingAlias[alias[1]];headingRepair=true;}}
4378
+ if(headingRepair)readme=allLines.join('\\n');
4247
4379
  const headings=[];for(let i=0;i<allLines.length;i++){if(allLines[i].trim()==='## Module Index')headings.push(i);}
4248
4380
  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);}
4249
4381
  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;}}
@@ -4256,7 +4388,7 @@ const headerIndex=lines.findIndex(line=>{const row=cells(line);return expectedHe
4256
4388
  if(headerIndex<0){process.stderr.write('invalid-module-index-header: require exact business ownership and path columns\\n');process.exit(2);}
4257
4389
  const dataRows=lines.slice(headerIndex+2).map(cells).filter(row=>row.length>=8&&row[0]&&row[0]!=='Module Stem');
4258
4390
  const allowedSplit=new Set(['explicit-user-layout','primary-business-resource','independent-business-resource','output-budget']);
4259
- const operationOwners=new Map();const canonicalSeen=new Set();const declaredModules=[];let planRepairApplied=false;
4391
+ const operationOwners=new Map();const canonicalSeen=new Set();const declaredModules=[];let planRepairApplied=headingRepair;
4260
4392
  for(const row of dataRows){
4261
4393
  const rawStem=String(row[0]||'').trim(),resource=String(row[1]||'').trim(),operations=String(row[2]||'').split(';').map(value=>value.trim()).filter(Boolean),split=String(row[5]||'').trim();
4262
4394
  const mdMatches=[...String(row[6]||'').matchAll(rxMdPath)];
@@ -4298,6 +4430,8 @@ if(planRepairApplied){
4298
4430
  const table=['| '+expectedHeader.join(' | ')+' |','|'+expectedHeader.map(()=>'---').join('|')+'|',...declaredModules.map(item=>'| '+[item.stem,item.businessResource,item.ownedOperations.join('; '),item.ownedRuleKeys.join('; '),item.caseIds.join('; '),item.splitReason,'['+item.stem+'](./'+item.stem+'.md) '+item.markdownPath,item.pytestPath].join(' | ')+' |')].join('\\n');
4299
4431
  readme=[...allLines.slice(0,headings[0]+1),table,...allLines.slice(end)].join('\\n');
4300
4432
  fs.writeFileSync(planPath,readme,'utf8');
4433
+ } else if(headingRepair){
4434
+ fs.writeFileSync(planPath,readme,'utf8');
4301
4435
  }
4302
4436
  for(const [operation,owners] of operationOwners){if(owners.length>1&&!owners.every(owner=>owner.split==='explicit-user-layout'||owner.split==='output-budget')){process.stderr.write('overlapping-operation-modules: '+operation+' -> '+owners.map(owner=>owner.stem).join(',')+'; merge by business resource or use an authoritative explicit-user-layout\\n');process.exit(2);}}
4303
4437
  if(!strictLayout){
@@ -4315,7 +4449,34 @@ if(modules.length===0){process.stderr.write('empty-module-index: require at leas
4315
4449
  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);}
4316
4450
  const planReadPath=path.relative(process.cwd(),planPath).split(path.sep).join('/');
4317
4451
  const planSha256=crypto.createHash('sha256').update(readme).digest('hex');
4318
- process.stdout.write(JSON.stringify({modules:modules.map(item=>({...item,planReadPath})),planReadPath,planSha256,moduleLayout:{schemaId:'backend-test-module-layout-facts-v1',source:strictLayout?'task-contract':'plan-derived',contractSha256:strictLayoutSha256,mode:strictLayout&&strictLayout.mode||'business-resource-layout',planRepairAttemptCount:planRepairApplied?1:0,planRepairOutcome:planRepairApplied?'changed':'not-required'}}));
4452
+ const slotTok=v=>String(v).trim().toUpperCase().replace(/[^A-Z0-9]+/g,'-').replace(/^-+|-+$/g,'');
4453
+ const partHead=[];for(let i=0;i<allLines.length;i++){if(allLines[i].trim()==='## Scenario Partitions')partHead.push(i);}
4454
+ if(partHead.length>1){process.stderr.write('duplicate-scenario-partitions; require at most one exact ## Scenario Partitions section\\n');process.exit(2);}
4455
+ const slotByModule=new Map(modules.map(m=>[m.stem,[]]));const unassigned=[];
4456
+ if(partHead.length===1){
4457
+ const pStart=partHead[0]+1;let pEnd=allLines.length;for(let i=pStart;i<allLines.length;i++){if(/^##\\s+\\S/.test(allLines[i].trim())){pEnd=i;break;}}
4458
+ const pLines=allLines.slice(pStart,pEnd).filter(l=>l.includes('|'));
4459
+ const pCells=line=>line.split('|').slice(1,-1).map(v=>stripBackticks(v).trim());
4460
+ const pHeader=['Partition ID','Operation','Axis','Domain','Required Slots','Expected by Slot','Bind Rule'];
4461
+ const pH=pLines.findIndex(line=>pHeader.every((v,i)=>pCells(line)[i]===v));
4462
+ if(pH<0){process.stderr.write('MISSING_PARTITION_TABLE: Scenario Partitions section has no canonical header row\\n');process.exit(2);}
4463
+ const pRows=pLines.slice(pH+2).map(pCells).filter(r=>r.length>=7&&r[0]&&r[0]!=='Partition ID');
4464
+ for(const row of pRows){
4465
+ const pid=String(row[0]||'').trim(),op=String(row[1]||'').trim(),domain=String(row[3]||'').split(/[;,,;]/).map(v=>v.trim()).filter(Boolean),optional=/omit/i.test(String(row[4]||'')),bind=String(row[6]||'').trim();
4466
+ const prefix=/^SP-[A-Z0-9][A-Z0-9._-]*$/i.test(pid)?('TP-'+pid.toUpperCase()):('TP-SP-'+slotTok(op)+'-'+slotTok(String(row[2]||'')));
4467
+ const slotIds=[...domain.map(v=>prefix+'-'+slotTok(v)),...(optional?[prefix+'-OMITTED']:[]),prefix+'-NOT-IN-SET'];
4468
+ const owners=modules.filter(m=>(m.ownedRuleKeys||[]).includes(bind)||(m.ownedOperations||[]).some(o=>String(o).replace(/\\s+/g,' ').toUpperCase()===op.replace(/\\s+/g,' ').toUpperCase()));
4469
+ const uniqueOwners=[...new Set(owners.map(o=>o.stem))];
4470
+ if(uniqueOwners.length===1){for(const id of slotIds)slotByModule.get(uniqueOwners[0]).push(id);}
4471
+ else if(modules.length===1){for(const id of slotIds)slotByModule.get(modules[0].stem).push(id);}
4472
+ else unassigned.push(...slotIds);
4473
+ }
4474
+ }
4475
+ if(unassigned.length){process.stderr.write('unassigned-partition-slots: '+unassigned.join(',')+'\\n');process.exit(2);}
4476
+ const slotInventory={schemaId:'backend-test-scenario-partition-slots-v1',planSha256,modules:modules.map(m=>({stem:m.stem,requiredVariantSlots:[...new Set(slotByModule.get(m.stem)||[])]})),unassignedSlotIds:[]};
4477
+ fs.mkdirSync(path.join(runDir,'contracts'),{recursive:true});
4478
+ fs.writeFileSync(path.join(runDir,'contracts','backend-test-scenario-partition-slots.json'),JSON.stringify(slotInventory,null,2));
4479
+ process.stdout.write(JSON.stringify({modules:modules.map(item=>({...item,planReadPath,requiredVariantSlots:(slotInventory.modules.find(m=>m.stem===item.stem)||{requiredVariantSlots:[]}).requiredVariantSlots})),planReadPath,planSha256,moduleLayout:{schemaId:'backend-test-module-layout-facts-v1',source:strictLayout?'task-contract':'plan-derived',contractSha256:strictLayoutSha256,mode:strictLayout&&strictLayout.mode||'business-resource-layout',planRepairAttemptCount:planRepairApplied?1:0,planRepairOutcome:planRepairApplied?'changed':'not-required'},partitionSlots:slotInventory}));
4319
4480
  `;
4320
4481
  const encoded = deflateRawSync(Buffer.from(script, "utf8")).toString("base64");
4321
4482
  return `node -e "eval(require('zlib').inflateRawSync(Buffer.from('${encoded}','base64')).toString('utf8'))"`;
@@ -4896,9 +5057,9 @@ async function buildBackendTestHybridDag(sources) {
4896
5057
  subtask_prompt: [
4897
5058
  "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.",
4898
5059
  "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.",
4899
- "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.",
5060
+ "Return the complete plan as plain Markdown. Do not emit JSON or code fences. The plan must contain the exact English protocol headings ## Coverage Scope, ## Coverage Matrix, ## Scenario Partitions when applicable, and ## Module Index. Never translate those headings into 覆盖范围/覆盖矩阵/场景分区/模块索引.",
4900
5061
  "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/.",
4901
- "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.",
5062
+ "Write human-readable content in Simplified Chinese by default. Keep English protocol literals exact: section headings, table headers, Partition IDs, TP IDs, Case IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and source citations. Never translate ## Module Index into ## 模块索引.",
4902
5063
  "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.",
4903
5064
  "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.",
4904
5065
  "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.",
@@ -4914,7 +5075,7 @@ async function buildBackendTestHybridDag(sources) {
4914
5075
  ]
4915
5076
  : []),
4916
5077
  "Include exactly one `## Module Index` table with this exact header: `| Module Stem | Business Resource | Owned Operations | Owned Rule Keys | Case IDs | Split Reason | Markdown Path | Pytest Path |`. Use canonical relative links `[label](./<stem>.md)` inside the Markdown Path cell followed by the resolved `${layout.markdownDir}/<stem>.md` path. Split Reason is exactly one of `explicit-user-layout`, `primary-business-resource`, `independent-business-resource`, or `output-budget`. Group by stable business resource/domain, not by CRUD operation, AC, parameter/field axis, scenario type or regression purpose: one resource's list/detail/create/update/delete and its filters/response assertions/regression floor belong in one module. Multiple modules owning the same exact `METHOD /path` are forbidden unless every such row is `explicit-user-layout` from primary-requirement path pairs or has a documented `output-budget` proof. Keep the total module count at the smallest safe value and never exceed 8 modules. Name model-derived modules with stable lowercase business stems such as `health` or `resource_notes`; explicit-user-layout preserves the primary requirement filename stem even when it is more specific. Do not use priority-only stems `p0`, `p1` or `p2`; Priority belongs only in the Coverage Matrix. Pure hexadecimal/hash-like opaque stems and test-purpose-only stems are forbidden. Do not use Case-ID-like module filenames. The relative link target, Markdown Path, Pytest Path and downstream automation mapping must be one-to-one and exact; for model-derived modules the default pair remains `testcase/md/<module>.md` and `testcase/test_<module>.py`, while explicit-user-layout preserves the primary requirement paths. Do not hand-write a conflicting module count in prose; the Module Index row count is the only count truth.",
4917
- "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 must contain `each-value` and exactly one `not-in-set`, plus `omitted` only when the parameter is optional. Before returning, expand every declared partition into its complete deterministic exact slot ID set: one `TP-<Partition ID>-<VALUE-TOKEN>` per Domain value, `TP-<Partition ID>-OMITTED` only for an optional axis, and exactly one `TP-<Partition ID>-NOT-IN-SET`. Every expanded slot ID must appear verbatim in the binding Rule's `Required Test Points` cell and be assigned to concrete Case IDs in that same Coverage Matrix row; ordinary alias/family Test Points do not replace this inventory. 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.",
5078
+ "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 must contain `each-value` and exactly one `not-in-set`, plus `omitted` only when the parameter is optional. Before returning, expand every declared partition into its complete deterministic exact slot ID set: one `TP-<Partition ID>-<VALUE-TOKEN>` per Domain value, `TP-<Partition ID>-OMITTED` only for an optional axis, and exactly one `TP-<Partition ID>-NOT-IN-SET`. Every expanded slot ID must appear verbatim in the binding Rule's `Required Test Points` cell and be assigned to concrete Case IDs in that same Coverage Matrix row; ordinary alias/family Test Points do not replace this inventory. Scheme A: Case count may be smaller than the enum count, but every exact slot still needs an independent variant Test Point and pytest.param id; never use SINGLE/MULTIPLE aliases as coverage. Expected by Slot states the documented expectation per slot kind (`domain-value`, `default-behavior`, `empty-result`/`excluded-result` when documented, or `GAP` when the source does not document the complement expectation — never invent 空列表/400). POST/PUT body field-validation enums stay in the Coverage Matrix as `TP-<FIELD>-ENUM-*` and MUST NOT get a Scenario Partition row. Do not create partitions for axes without a documented legal-value domain. Only GET/list query or path parameters whose bound source documents a finite enum or classification set may become a Scenario Partition. Do not create partitions for free-form strings, primary keys, required-or-optional-only parameters, or boundary/format-only axes. If an axis has no finite legal-value domain, do not declare a Partition row and do not invent NOT-IN-SET cases. Cross-axis combinations stay as ONE nominal Case; never declare a cross-axis cartesian partition.",
4918
5079
  "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.",
4919
5080
  ...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
4920
5081
  intake.boundedSourceContext,
@@ -4998,7 +5159,7 @@ async function buildBackendTestHybridDag(sources) {
4998
5159
  '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.',
4999
5160
  "Name this module file with the exact frozen Module Index stem `{{item.stem}}` (filename `{{item.markdownPath}}`). Never reinterpret or rename an explicit-user-layout stem. Priority-only stems `p0`, `p1` and `p2` are forbidden and must never produce `p0.md` or `test_p0.py`. Pure hexadecimal/hash-like opaque stems such as `a401606` and `deadbeef` are also forbidden. Do not use Case-ID-like module filenames. For every automatable case, `自动化映射` must name exactly `{{item.pytestPath}}`, where the module stem is this Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `health` → `testcase/test_health.py`; `resource_notes` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
5000
5161
  "AUTOMATION_BINDING_FORMAT_V1 is a literal machine contract. Under every Case's `### 自动化映射`, write these independent lines exactly: `- 脚本:<path|无>`, `- primary symbol:<symbol|无>`, `- 变体测试点:<semicolon-separated TP IDs|无>`, `- 场景断言测试点:<semicolon-separated TP IDs|无>`, `- 横切证据测试点:<semicolon-separated TP IDs|无>`. TP IDs must be on the same line after the colon. Forbidden classification forms include `TP-X(变体测试点)`, `[变体测试点] TP-X`, `【变体测试点】:TP-X`, pipe-delimited annotations, tables, or nested TP lists. Before returning, verify that the Case `### 测试点` exact set equals the pairwise-disjoint union of the three canonical binding lines; do not add, remove, rename or duplicate a TP to make the format pass.",
5001
- "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. Before returning, derive the complete exact slot set from every applicable Scenario Partitions row and verify that the module Cases declare and bind every slot assigned by the Coverage Matrix; ordinary alias Test Points do not satisfy a partition slot, and aggregate aliases such as `SINGLE`/`MULTIPLE` are forbidden. 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.",
5162
+ "Scenario Partition slots: requiredVariantSlots for this module are `{{item.requiredVariantSlots}}`. Every listed ID MUST appear in this module's Cases as exactly one variant Test Point in both `### 测试点` and `变体测试点`. Slot IDs copy the declared Partition ID exactly; never drop the HTTP method, invent, merge, renumber or split slot IDs. Ordinary alias Test Points do not satisfy a partition slot, and aggregate aliases such as `SINGLE`/`MULTIPLE` are forbidden. Scheme A: one Case may carry many slots; do not create one Case per enum value just to match Case count. Prefer ONE Case per partition with a parameter table over duplicated Cases per value. The not-in-set slot value must be a concrete literal absent from the Domain (e.g. `UNKNOWN_TYPE`) and its expected result must come from the bound source — when Expected by Slot is GAP, the Case states the expectation as GAP evidence, never a guessed 空列表/400. Never create cross-axis combination variants beyond the single documented nominal.",
5002
5163
  "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.",
5003
5164
  "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 ‘符合预期’.",
5004
5165
  "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.",
@@ -5043,7 +5204,7 @@ async function buildBackendTestHybridDag(sources) {
5043
5204
  "Correct testcase/md/** directly: add documented omissions, remove unsupported cases, preserve every frozen Module Index filename exactly (never rename an explicit-user-layout module; model-derived invalid stems must have been rejected before map expansion), normalize every Case ID to hyphen-separated module segments plus exactly three zero-padded digits (`BE-RESOURCE_NOTES-01` → `BE-RESOURCE-NOTES-001`; `BE-RN-011A` must be renumbered or merged) consistently across headings/index/mappings, fix automation mappings so each automatable case points at `testcase/test_<module>.py` derived from that module filename and declares exactly one primary symbol (evidence-only meta cases may keep `脚本/primary symbol=无` with empty variants), assign every Test Point exactly one of `变体测试点`/`场景断言测试点`/`横切证据测试点`, then perform an exact-set check: each Case's `### 测试点` set must equal (not merely contain) the union of those three binding lists; delete stale/legacy aliases and ensure every binding-list Test Point is present, expand every variant parameter row into its own atomic TP ID, make every non-cross-cutting TP Case-specific and owned by exactly one Case, require every primary symbol to start with the canonical Case prefix, ensure every explicit AC ID appears in an applicable Case `验收标准`, merge execution duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Remove every credential/header value, placeholder, fake token and anti-example from Markdown. Sensitive key names may remain only as a plain list; values must be described as runtime-only and omitted, with no colon/value pair or literal example anywhere, including details blocks and explanatory text. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations as exact machine-readable identifiers; only normalize Case ID separator/sequence formatting as specified above. Recalculate predicted collected items as `sum(max(1, variant count per Case))`; when the task declares a budget, directly merge redundant journeys/reclassify same-request checkpoints until the prediction is within budget, while preserving all required coverage. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
5044
5205
  "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.",
5045
5206
  "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.",
5046
- "Scenario Partitions synchronization: when the run-owned plan declares `## Scenario Partitions`, verify each declared partition's slots are fully materialized as variant Test Points with exact `TP-<Partition ID>-...` IDs (each-value per Domain value, OMITTED only for optional axes, exactly one NOT-IN-SET with intent=enum-invalid). Before returning, derive the complete exact slot set from every legal Scenario Partitions row and compare it with both the binding Coverage Matrix Rule's Required Test Points and the final Case `### 测试点`/`变体测试点` sets; directly add every missing exact slot to the already-assigned Case IDs; ordinary alias Test Points do not satisfy a partition slot, and aggregate aliases such as `SINGLE`/`MULTIPLE` are forbidden. Directly add missing slot rows/Cases. Record an illegal plan Partition row that has no source-backed finite domain as GAP/CONFLICT and remove only its derived `TP-SP-*` slots/Cases from target modules; never modify the immutable plan artifact. Never delete a legal source-backed partition or drop its complement slot to force coverage green. When the bound source does not document the complement expectation, keep the slot with GAP expected instead of guessing. Body-field validation enums (`TP-<FIELD>-ENUM-*`) are NOT partitions — do not add partition rows for them.",
5207
+ "Scenario Partitions synchronization: when the run-owned plan declares `## Scenario Partitions`, verify each declared partition's slots are fully materialized as variant Test Points with exact `TP-<Partition ID>-...` IDs (each-value per Domain value, OMITTED only for optional axes, exactly one NOT-IN-SET with intent=enum-invalid). Before returning, derive the complete exact slot set from every legal Scenario Partitions row and compare it with both the binding Coverage Matrix Rule's Required Test Points and the final Case `### 测试点`/`变体测试点` sets; directly add every missing exact slot to the already-assigned Case IDs; ordinary alias Test Points do not satisfy a partition slot, and aggregate aliases such as `SINGLE`/`MULTIPLE` are forbidden. Scheme A: keep existing Case structure and add missing exact variant Test Points to already-assigned Cases instead of creating one Case per enum value. Directly add missing slot rows/Cases. Record an illegal plan Partition row that has no source-backed finite domain as GAP/CONFLICT and remove only its derived `TP-SP-*` slots/Cases from target modules; never modify the immutable plan artifact. Never delete a legal source-backed partition or drop its complement slot to force coverage green. When the bound source does not document the complement expectation, keep the slot with GAP expected instead of guessing. Body-field validation enums (`TP-<FIELD>-ENUM-*`) are NOT partitions — do not add partition rows for them.",
5047
5208
  "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.",
5048
5209
  "Read only indexed source paths. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.",
5049
5210
  ...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
@@ -5153,7 +5314,7 @@ async function buildBackendTestHybridDag(sources) {
5153
5314
  "Output budget protocol (hard, max output <=16K per turn): Write exactly the frozen `{{item.pytestPath}}`. Never paste full Python modules into assistant chat. Do not merge or split modules. Do not reduce params/assertions/skips to fit. If OUTPUT_LIMIT_RECOVERY is injected, continue only listed missing/broken scripts.",
5154
5315
  "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.",
5155
5316
  "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.",
5156
- '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 前置条件/测试数据/自动化映射.',
5317
+ 'Ensure every automatable final Markdown Case ID in this module appears in exactly one primary pytest test function or pytest test class method region, using the exact `primary symbol` declared by Markdown. Skip evidence-only meta Cases that declare `脚本/primary symbol=无` with empty variants; do not invent a business pytest symbol for them. The symbol must start with `test_BE_<MODULE>_<NNN>_` so every parameterized collected item remains associated with its Case. Module-level functions and class-based pytest methods are both supported. Only `变体测试点` may use stable `pytest.param(..., id="TP-...")` IDs, and every atomic variant ID must appear exactly once with a genuine input/state/outcome change. A Case with exactly one variant Test Point still needs one literal `pytest.param(..., id="TP-...")` row; never leave a single-variant Case as a bare function with the TP only in the docstring. Use a literal direct `pytest.param(..., id=...)` expression for every row; never hide or wrap it behind `_post_case`, `_put_case`, row-factory functions, comprehensions, generators, or dynamically returned parameter lists; do not use decorator-level `ids=[...]`, generated suffixes, or IDs that extend/shorten the exact Markdown TP. Do not parameterize `场景断言测试点` or `横切证据测试点`; execute all assertion checkpoints within the same business journey/item and use shared helpers for cross-cutting evidence. The primary symbol docstring must contain exact metadata lines `Case-ID: BE-...`, `Assertion-Test-Points: TP-...;TP-...` and `Cross-Cutting-Test-Points: TP-...;TP-...` (use `none` when empty). Implement request dictionaries so their direct and nested key paths and enum literals exactly satisfy the Case `Payload Required Paths`, `Payload Allowed Paths`, and `Payload Enum`; for `Payload Contract: none`, do not invent a JSON/body DTO. GET/list filters still declare query fields in those payload labels when the Case varies `params=`/`query=` keys. Python `True`/`False` may implement JSON/OpenAPI `true`/`false` query or body booleans. GET/DELETE setup journeys may create resources, but their setup DTO must not change the target operation\'s no-body payload contract. No Test Point may be invented, renamed, omitted or bound in two modes. The generated pytest collection shape must equal the Markdown prediction `sum(max(1, variant count per Case))`; keep it at or below the task\'s explicit budget by removing duplicate execution, never by collapsing multiple parameter rows under a coarse family TP. Assertions come only from 预期结果 and setup comes only from 前置条件/测试数据/自动化映射.',
5157
5318
  "Name the generated pytest file so it corresponds one-to-one with its source Markdown module file: this module stem `{{item.stem}}` maps to exactly the frozen `{{item.pytestPath}}`. The <module> stem is the Markdown filename without the `.md` extension, lowercased and with non-alphanumeric characters replaced by underscores. For example, `resource_notes` → `testcase/test_resource_notes.py`, `health` → `testcase/test_health.py`. If Markdown automation mapping names a different path than this module stem path, still write the frozen manifest pytest path and do not invent prefixes. Never merge multiple Markdown modules into one pytest file, never split one module across several files, and never invent pytest filenames unrelated to the Markdown modules.",
5158
5319
  "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.",
5159
5320
  "Keep this module self-contained: define module-local fixtures and helpers directly in `{{item.pytestPath}}`, so pytest discovers every fixture dependency without external plugin registration. The request log must include method, URL/path, and request parameters (query plus JSON/body/payload summary). The response log must include status code and response result (JSON/text/body summary), and both records must be visible in pytest stdout/stderr without changing assertions. Recursively redact sensitive values and apply bounded truncation before logging.",
@@ -838,7 +838,7 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
838
838
  "",
839
839
  "<retry_instruction>",
840
840
  "The previous Scout attempt did not commit a complete, runtime-evidenced target surface.",
841
- "Search the repository only as needed to establish the real entrypoint, implementation ownership, and applicable test path. Commit record_target_surface with completeness=complete and unresolvedPaths=[] only after at least one named target has fresh runtime evidence. If ownership truly cannot be established, record completeness=blocked with each unresolved path; do not make Plan discover it.",
841
+ "Search the repository only as needed to establish the real entrypoint, implementation ownership, and applicable test path. Commit record_target_surface with completeness=complete and unresolvedPaths=[] only after at least one named target has fresh runtime evidence, unless the task source explicitly declares a greenfield target: in that case every future path must be source-declared by the runtime-enriched fact. If ownership truly cannot be established, record completeness=blocked with each unresolved path; do not make Plan discover it.",
842
842
  "</retry_instruction>",
843
843
  ].join("\n");
844
844
  }
@@ -19,6 +19,40 @@ const RERUN_FEEDBACK_ROLES = new Set([
19
19
  "verifier",
20
20
  "reviewer",
21
21
  ]);
22
+ function globPatternToRegExp(pattern) {
23
+ const normalized = pattern.replace(/\\/g, "/");
24
+ let source = "";
25
+ for (let index = 0; index < normalized.length; index += 1) {
26
+ const char = normalized[index];
27
+ if (char === "*" && normalized[index + 1] === "*") {
28
+ source += ".*";
29
+ index += 1;
30
+ }
31
+ else if (char === "*") {
32
+ source += "[^/]*";
33
+ }
34
+ else {
35
+ source += char.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
36
+ }
37
+ }
38
+ return new RegExp(`^${source}$`);
39
+ }
40
+ function readSetAllowsArtifactPath(task, artifactPath) {
41
+ const readSet = task.readSet ?? [];
42
+ if (readSet.length === 0)
43
+ return true;
44
+ const candidate = artifactPath.replace(/\\/g, "/");
45
+ const candidates = [candidate];
46
+ for (const marker of ["/.harness/", "/source/", "/testcase/"]) {
47
+ const index = candidate.indexOf(marker);
48
+ if (index >= 0)
49
+ candidates.push(candidate.slice(index + 1));
50
+ }
51
+ return readSet.some((pattern) => {
52
+ const regex = globPatternToRegExp(pattern);
53
+ return candidates.some((value) => regex.test(value));
54
+ });
55
+ }
22
56
  function formatBulletList(items, emptyLabel) {
23
57
  if (!items || items.length === 0)
24
58
  return emptyLabel;
@@ -129,7 +163,7 @@ export function buildUpstreamContext(task, upstream, maxChars = MAX_UPSTREAM_CHA
129
163
  : artifactKind === "assistant"
130
164
  ? record.assistantArtifactPath
131
165
  : undefined;
132
- if (truncated && artifactPath) {
166
+ if (truncated && artifactPath && readSetAllowsArtifactPath(task, artifactPath)) {
133
167
  section += formatUpstreamArtifactPointerMap(record, artifactKind === "stdout" ? "stdout" : "assistant");
134
168
  }
135
169
  else if (truncated && record.nodeRecordPath) {
@@ -205,7 +239,7 @@ export function buildUpstreamContext(task, upstream, maxChars = MAX_UPSTREAM_CHA
205
239
  if (consumedArtifactSection) {
206
240
  section += `\n\n${consumedArtifactSection}`;
207
241
  }
208
- if (truncated && artifactPath) {
242
+ if (truncated && artifactPath && readSetAllowsArtifactPath(task, artifactPath)) {
209
243
  section += formatUpstreamArtifactPointerMap(record, artifactKind);
210
244
  }
211
245
  sections.push(section);
@@ -87,7 +87,7 @@ export const dagShellVerifyEvidenceSchema = z.object({
87
87
  preflight: z
88
88
  .array(z.object({
89
89
  label: z.string(),
90
- status: z.enum(["ok", "future-delivery-script"]),
90
+ status: z.enum(["ok", "future-delivery-script", "future-test-target"]),
91
91
  }))
92
92
  .default([]),
93
93
  /** Effective timeout applied independently to each shell command. */