@tea-agent/loop-agent 0.16.1-beta.2 → 0.16.2

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 (113) hide show
  1. package/AGENTS.md +4 -8
  2. package/CHANGELOG.md +76 -18
  3. package/README.md +76 -299
  4. package/dist/application/evaluation/alias.js +184 -0
  5. package/dist/application/evaluation/budget.js +192 -0
  6. package/dist/application/evaluation/campaign-hash.js +47 -0
  7. package/dist/application/evaluation/campaign-matrix.js +372 -0
  8. package/dist/application/evaluation/campaign-scorecard.js +135 -0
  9. package/dist/application/evaluation/campaign.js +370 -0
  10. package/dist/application/evaluation/candidate.js +23 -6
  11. package/dist/application/evaluation/corpus-hash.js +38 -0
  12. package/dist/application/evaluation/corpus.js +56 -0
  13. package/dist/application/evaluation/experiment.js +294 -0
  14. package/dist/application/evaluation/ignition.js +198 -0
  15. package/dist/application/evaluation/integrity-audit.js +162 -0
  16. package/dist/application/evaluation/outer-loop.js +132 -0
  17. package/dist/application/evaluation/pi-cell-executor.js +39 -0
  18. package/dist/application/evaluation/private-verifier.js +46 -0
  19. package/dist/application/evaluation/promotion-policy.js +151 -0
  20. package/dist/application/evaluation/proposer.js +98 -0
  21. package/dist/application/evaluation/types.js +522 -0
  22. package/dist/cli/command-definitions.js +19 -3
  23. package/dist/commands/dag-reconcile-run.js +3 -116
  24. package/dist/commands/eval.js +1176 -13
  25. package/dist/commands/init.js +7 -1
  26. package/dist/executors/dag-pi-executor.js +8 -46
  27. package/dist/executors/pi-sdk-executor.js +66 -3
  28. package/dist/executors/shell-executor.js +213 -30
  29. package/dist/executors/shell-presets.js +12 -2
  30. package/dist/executors/shell-write-guard.js +20 -1
  31. package/dist/infrastructure/evaluation/alias-store.js +199 -0
  32. package/dist/infrastructure/evaluation/campaign-store.js +154 -0
  33. package/dist/infrastructure/evaluation/corpus-store.js +181 -0
  34. package/dist/infrastructure/evaluation/experiment-store.js +124 -0
  35. package/dist/infrastructure/evaluation/ignition-store.js +82 -0
  36. package/dist/infrastructure/evaluation/private-verifier-store.js +145 -0
  37. package/dist/infrastructure/evaluation/proposer-store.js +78 -0
  38. package/dist/records/promotion.js +3 -1
  39. package/dist/shared/git-progress.js +9 -2
  40. package/dist/worker/cli.js +83 -0
  41. package/dist/worker/delivery/git-transaction.js +75 -0
  42. package/dist/worker/delivery/verification-bundle.js +13 -2
  43. package/dist/worker/feature/review.js +3 -2
  44. package/dist/worker/observability/read-model.js +56 -0
  45. package/dist/worker/observe/server.js +6 -3
  46. package/dist/worker/observe/static/dag-helpers.js +0 -62
  47. package/dist/worker/observe/static/styles.css +18 -55
  48. package/dist/worker/observe/static/views/dag.js +13 -5
  49. package/dist/worker/outcomes/adapters.js +4 -1
  50. package/dist/worker/outcomes/declared-artifacts.js +103 -0
  51. package/dist/worker/outcomes/evidence-tokens.js +29 -0
  52. package/dist/worker/outcomes/gate.js +10 -11
  53. package/dist/worker/outcomes/projector.js +30 -4
  54. package/dist/worker/outcomes/types.js +3 -0
  55. package/dist/worker/pool/reconcile.js +285 -0
  56. package/dist/worker/run-task/run-task.js +81 -4
  57. package/dist/worker/runner/run-ready.js +25 -2
  58. package/dist/worker/task-graph/ready-planner.js +14 -8
  59. package/dist/worker/task-graph/task-graph-schema.js +5 -3
  60. package/dist/workflows/dag/backend-test-analysis-contract.js +87 -30
  61. package/dist/workflows/dag/backend-test-case-manifest.js +71 -8
  62. package/dist/workflows/dag/backend-test-execution-contract.js +63 -11
  63. package/dist/workflows/dag/backend-test-repair-contract.js +94 -0
  64. package/dist/workflows/dag/backend-test-result-contract.js +6 -4
  65. package/dist/workflows/dag/backend-test-semantic-review-contract.js +36 -0
  66. package/dist/workflows/dag/budget-enforcement.js +67 -0
  67. package/dist/workflows/dag/context-policy.js +137 -0
  68. package/dist/workflows/dag/dynamic-runtime/condition.js +1 -1
  69. package/dist/workflows/dag/dynamic-runtime/shared.js +42 -0
  70. package/dist/workflows/dag/failure-routing.js +8 -1
  71. package/dist/workflows/dag/frontend-implementation-contract.js +32 -93
  72. package/dist/workflows/dag/init-hybrid.js +624 -172
  73. package/dist/workflows/dag/knowledge-curator.js +3 -0
  74. package/dist/workflows/dag/lifecycle.js +33 -2
  75. package/dist/workflows/dag/node-execution.js +11 -4
  76. package/dist/workflows/dag/prompt.js +1 -1
  77. package/dist/workflows/dag/reconcile-run.js +121 -0
  78. package/dist/workflows/dag/report.js +12 -0
  79. package/dist/workflows/dag/runner.js +43 -16
  80. package/dist/workflows/dag/scheduler.js +87 -17
  81. package/dist/workflows/dag/skill-snapshot.js +11 -7
  82. package/dist/workflows/dag/types.js +49 -1
  83. package/dist/workflows/dag/validate.js +35 -15
  84. package/docs/README.md +3 -1
  85. package/docs/architecture/runtime-boundaries.md +3 -2
  86. package/docs/init-surface.manifest.json +4 -0
  87. package/docs/local-development-environment.md +52 -0
  88. package/docs/templates/agent-dag.schema.json +25 -7
  89. package/docs/templates/agent-dag.supervised-implementation.json +23 -4
  90. package/docs/templates/backend-test-analysis.schema.json +9 -16
  91. package/docs/templates/backend-test-dag.json +493 -197
  92. package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -4
  93. package/docs/templates/backend-test-execution.schema.json +6 -1
  94. package/docs/templates/branch-merge-report.md +14 -0
  95. package/docs/templates/evaluation/campaign-budget-v1.json +12 -0
  96. package/docs/templates/evaluation/campaign-dogfood-v0.json +24 -0
  97. package/docs/templates/evaluation/campaign-evidence-v1.json +44 -0
  98. package/docs/templates/evaluation/context-policy-baseline-v1.json +17 -0
  99. package/docs/templates/evaluation/context-policy-role-specialized-v1.json +28 -0
  100. package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +118 -0
  101. package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +21 -0
  102. package/docs/templates/evaluation/matrix-fixture-v1.json +10 -0
  103. package/docs/templates/evaluation/private-verifier-dogfood-v0.json +16 -0
  104. package/docs/templates/product-line/AGENTS.md +1 -0
  105. package/docs/templates/product-line/README.md +17 -0
  106. package/docs/templates/product-line/acceptance.yaml +9 -0
  107. package/docs/templates/product-line/feature.yaml +11 -0
  108. package/docs/templates/product-line/task-graph.yaml +8 -0
  109. package/docs/templates/product-line/task.yaml +4 -0
  110. package/package.json +2 -1
  111. package/skills/frontend-implementation/references/node-contracts.md +3 -3
  112. package/skills/loop-agent/references/command-reference.md +5 -0
  113. package/skills/loop-agent/references/hybrid-dag.md +7 -4
@@ -18,10 +18,10 @@ import { resolveVerifyPreset } from "../../executors/shell-verification.js";
18
18
  import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
19
19
  import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "./task-demand-routing.js";
20
20
  import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPreflightShellSnippet, } from "./backend-test-execution-contract.js";
21
+ import { buildBackendTestEffectiveResultSelectorShellSnippet, buildBackendTestRepairEligibilityShellSnippet, buildBackendTestRepairSafetyShellSnippet, } from "./backend-test-repair-contract.js";
21
22
  import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-contract.js";
22
23
  import { classifyFrontendRisk, } from "./frontend-risk.js";
23
24
  import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
24
- import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
25
25
  const REQUIREMENT_FILE = "需求.md";
26
26
  const CONSTRAINT_FILE = "执行约束.md";
27
27
  const REFERENCE_DIRECTORY = "references";
@@ -933,6 +933,22 @@ function buildDagSourceBinding(sources) {
933
933
  requirementIds: extractExplicitRequirementIds(sources.requirementMarkdown, sources.constraintMarkdown, ...(sources.referenceDocuments ?? []).map((reference) => reference.markdown)),
934
934
  };
935
935
  }
936
+ function buildBackendTestAnalysisSourceBindingContract(sources) {
937
+ const binding = buildDagSourceBinding(sources);
938
+ const requirement = binding.sources.find((source) => source.kind === "requirement");
939
+ if (!requirement) {
940
+ throw new Error("backend-test analysis requires a requirement source binding");
941
+ }
942
+ return {
943
+ taskId: binding.taskId,
944
+ requirementPath: requirement.path,
945
+ requirementSha256: requirement.sha256,
946
+ referencePaths: binding.sources
947
+ .filter((source) => source.kind === "reference")
948
+ .map((source) => source.path),
949
+ requirementIds: binding.requirementIds,
950
+ };
951
+ }
936
952
  function buildSourceContextBlock(sources) {
937
953
  const requirementRef = toTaskRelativeSourcePath(sources, sources.requirementPath);
938
954
  const requirementExcerpt = excerptMarkdown(sources.requirementMarkdown, {
@@ -1339,11 +1355,10 @@ function buildFrontendMockAssessNode(sources, sourceContext, mockContextBlock, f
1339
1355
  allowedPaths: readOnlyPaths,
1340
1356
  forbiddenPaths,
1341
1357
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
1342
- firstProtocolLine: "MOCK_STRATEGY:",
1343
- outputContract: "Plain Markdown whose first line is MOCK_STRATEGY: native|browser-intercept|request-adapter|not-needed|blocked, followed by Mock Decision, API Contract Evidence, Specification Evidence, Service Evidence, Backend Readiness, Selection Evidence, Endpoint / Fixture Matrix, Activation, Target Files, Production Safety, Verification Plan, Real Integration Gap, and Blocking Issues. No file writes.",
1358
+ outputContract: "Plain Markdown whose first non-empty line is MOCK_STRATEGY: native|browser-intercept|request-adapter|not-needed|blocked, followed by Mock Decision, API Contract Evidence, Specification Evidence, Service Evidence, Backend Readiness, Selection Evidence, Endpoint / Fixture Matrix, Activation, Target Files, Production Safety, Verification Plan, Real Integration Gap, and Blocking Issues. No file writes.",
1344
1359
  subtask_prompt: [
1345
1360
  "Perform read-only Mock assessment and select one safe frontend data strategy.",
1346
- "The first line must be exactly one of: MOCK_STRATEGY: native, MOCK_STRATEGY: browser-intercept, MOCK_STRATEGY: request-adapter, MOCK_STRATEGY: not-needed, or MOCK_STRATEGY: blocked. Do not emit blank lines, headings, or explanatory preamble before it.",
1361
+ "The first non-empty line must be exactly one of: MOCK_STRATEGY: native, MOCK_STRATEGY: browser-intercept, MOCK_STRATEGY: request-adapter, MOCK_STRATEGY: not-needed, or MOCK_STRATEGY: blocked.",
1347
1362
  "Prefer an existing native Mock facility. Use browser-intercept only with an existing browser/e2e harness. When no Mock exists but the API layer is writable, use request-adapter by adding a minimal reversible adapter/DI seam within the approved writeSet; the real adapter must remain the production default.",
1348
1363
  autoMaySkipMissingMock
1349
1364
  ? "Auto mode may skip Mock when no project Mock capability is confirmed. Select not-needed with positive evidence from contract/scout that no project Mock capability is confirmed, continue without adding Mock files or dependencies, run the fixed verification entrypoints, and record any unproved real API behavior in Real Integration Gap. Do not block solely because no project Mock capability, browser interception harness, or request adapter exists."
@@ -1734,46 +1749,6 @@ function buildFrontendHybridDagFromTask(sources) {
1734
1749
  allowedPaths: taskConfig.allowedPaths,
1735
1750
  complexity: taskConfig.complexity,
1736
1751
  });
1737
- const frontendSourceBinding = buildDagSourceBinding(sources);
1738
- const frontendContractSchemaBlock = (() => {
1739
- const schema = loadFrontendImplementationContractJsonSchema();
1740
- const requirement = frontendSourceBinding.sources.find((source) => source.kind === "requirement");
1741
- if (!requirement) {
1742
- throw new Error("frontend implementation contract context requires a bound requirement source");
1743
- }
1744
- const referencePaths = frontendSourceBinding.sources
1745
- .filter((s) => s.kind === "reference")
1746
- .map((s) => s.path);
1747
- const fixedFields = {
1748
- schemaVersion: 1,
1749
- sourceBinding: {
1750
- taskId: frontendSourceBinding.taskId,
1751
- requirementPath: requirement.path,
1752
- requirementSha256: requirement.sha256,
1753
- referencePaths,
1754
- requirementIds: frontendSourceBinding.requirementIds,
1755
- },
1756
- riskLevel: frontendRisk.selectedRisk,
1757
- targets: { files: implementPaths.writeSet },
1758
- };
1759
- return [
1760
- `## ${FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID} JSON Schema (authoritative; do not guess fields)`,
1761
- schema,
1762
- "",
1763
- "## Fixed contract fields (deterministic; copy exactly and do not modify)",
1764
- JSON.stringify(fixedFields),
1765
- "",
1766
- "## Forbidden fields (these are NOT in the schema; do not emit)",
1767
- "- schemaId",
1768
- "- targetFiles",
1769
- "- requirementCoverage",
1770
- "",
1771
- "## Critical rules",
1772
- "- verificationTargets is a TOP-LEVEL required array",
1773
- "- uiStates items use name/applicable/expectedBehavior/implementationTargets/verificationTargetIds/notApplicableReason",
1774
- "- mockApi.productionDefaultOff must always be true (including strategy: not-needed)",
1775
- ].join("\n");
1776
- })();
1777
1752
  const sourceContext = [
1778
1753
  buildSourceContextBlock(sources),
1779
1754
  capabilityContextBlock,
@@ -1782,7 +1757,7 @@ function buildFrontendHybridDagFromTask(sources) {
1782
1757
  .join("\n\n");
1783
1758
  const hasMockVerifyCommands = (taskConfig.frontendMock?.verifyCommands.length ?? 0) > 0 ||
1784
1759
  mockCapability.verifyCommands.length > 0;
1785
- const requirementIds = frontendSourceBinding.requirementIds;
1760
+ const requirementIds = buildDagSourceBinding(sources).requirementIds;
1786
1761
  const requirementCoverageInstruction = requirementIds.length > 0
1787
1762
  ? `Include a Requirement Coverage section that lists every exact source identifier: ${requirementIds.join(", ")}. Preserve each identifier verbatim and map it to concrete implementation and verification steps.`
1788
1763
  : "";
@@ -1941,7 +1916,6 @@ function buildFrontendHybridDagFromTask(sources) {
1941
1916
  fixedVerificationContext,
1942
1917
  sourceContext,
1943
1918
  mockContextBlock,
1944
- frontendContractSchemaBlock,
1945
1919
  ].join("\n\n"),
1946
1920
  },
1947
1921
  {
@@ -2018,7 +1992,6 @@ function buildFrontendHybridDagFromTask(sources) {
2018
1992
  "Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
2019
1993
  "End the response with exactly one fenced json object conforming to frontend-implementation-contract-v1. Bind it to the supplied task sources; map every requirement and applicable UI state to concrete implementation and verification targets or an explicit blocking evidence gap. Do not include secrets or unsafe paths.",
2020
1994
  sourceContext,
2021
- frontendContractSchemaBlock,
2022
1995
  ].join("\n\n"),
2023
1996
  },
2024
1997
  ...(requirementIds.length > 0
@@ -2468,6 +2441,7 @@ function buildFrontendHybridDagFromTask(sources) {
2468
2441
  // Backend test DAG template
2469
2442
  // ---------------------------------------------------------------------------
2470
2443
  function buildAnalyzeInputsNode(sources) {
2444
+ const sourceBindingContract = buildBackendTestAnalysisSourceBindingContract(sources);
2471
2445
  return {
2472
2446
  id: "analyze-inputs-pi",
2473
2447
  depends_on: [],
@@ -2477,14 +2451,19 @@ function buildAnalyzeInputsNode(sources) {
2477
2451
  writePolicy: "read-only",
2478
2452
  allowedPaths: commonReadOnlyPaths(sources),
2479
2453
  forbiddenPaths: commonForbiddenPaths(sources),
2480
- outputContract: "Pure Backend Test Analysis v1 JSON object matching docs/templates/backend-test-analysis.schema.json. No Markdown prose and no file writes.",
2454
+ outputContract: "Pure Backend Test Analysis v2 JSON object matching docs/templates/backend-test-analysis.schema.json. No Markdown prose and no file writes.",
2481
2455
  subtask_prompt: [
2482
- "Read the task source materials and return exactly one JSON object matching Backend Test Analysis v1.",
2456
+ "Read the task source materials and return exactly one JSON object matching Backend Test Analysis v2.",
2483
2457
  "Do not wrap it in explanatory prose. A single fenced json block is tolerated, but pure JSON is preferred.",
2484
- "Copy taskId, requirementPath, requirementSha256, referencePaths, and requirementIds exactly from the DAG source binding shown below.",
2485
- "Preserve existing AC IDs. Do not invent endpoint methods, paths, fields, errors, boundaries, or business rules; record unknowns in evidenceGaps.",
2458
+ "Copy the sourceBinding object exactly from the JSON block below; do not infer, add, remove, or reclassify source paths.",
2459
+ "Only kind=reference sources belong in referencePaths; kind=constraint sources MUST NOT be included in referencePaths.",
2460
+ "## Exact Backend Test Analysis sourceBinding JSON",
2461
+ JSON.stringify(sourceBindingContract, null, 2),
2462
+ "For every endpoint, explicitly set responseBody.kind=array|object|scalar|empty|unknown and ordering=specified|unspecified|not-applicable. Add itemSchemaRef for arrays when documented.",
2463
+ "For response fields, use comparison=exact|parseable-only|semantic when the source defines assertion semantics; date-time fields whose precision is unspecified should use parseable-only, not string equality.",
2464
+ "Endpoint sourceRefs and field sourceRefs must cite only requirement/reference evidence actually read. Empty sourceRefs are allowed only when normalizing legacy v1 input; newly generated v2 should cite evidence.",
2486
2465
  "Use empty arrays for categories not documented. Never include credentials, tokens, private keys, or secret values.",
2487
- "Required top-level keys: schemaVersion, sourceBinding, acceptanceCriteria, endpoints, dataModels, businessRules, stateTransitions, boundaryConstraints, externalDependencies, risks, evidenceGaps.",
2466
+ "Required top-level keys: schemaVersion=2, sourceBinding, acceptanceCriteria, endpoints, dataModels, businessRules, stateTransitions, boundaryConstraints, externalDependencies, risks, evidenceGaps.",
2488
2467
  "Read-only: do not modify code, docs, artifacts, or repository files.",
2489
2468
  buildSourceContextBlock(sources),
2490
2469
  ].join("\n\n"),
@@ -2500,13 +2479,13 @@ function buildBackendTestAnalysisContractGateNode(sources) {
2500
2479
  writePolicy: "read-only",
2501
2480
  allowedPaths: commonReadOnlyPaths(sources),
2502
2481
  forbiddenPaths: commonForbiddenPaths(sources),
2503
- outputContract: "Validated run-owned Backend Test Analysis v1 artifact pointer, schema ID, and SHA-256.",
2482
+ outputContract: "Validated run-owned Backend Test Analysis v2 artifact pointer, schema ID, and SHA-256 (legacy v1 input is normalized to v2).",
2504
2483
  subtask_prompt: "Materialize and validate the backend-test analysis contract under the current DAG run.",
2505
2484
  shell: {
2506
2485
  commands: [],
2507
2486
  jsonArtifactGate: {
2508
2487
  fromNodeId: "analyze-inputs-pi",
2509
- schemaId: "backend-test-analysis-v1",
2488
+ schemaId: "backend-test-analysis-v2",
2510
2489
  artifactName: "backend-test-analysis.json",
2511
2490
  outputDir: "contracts",
2512
2491
  },
@@ -2533,11 +2512,12 @@ function buildBackendTestEnvironmentScoutNode(sources) {
2533
2512
  "Discover only non-secret evidence: pytest config files (pytest.ini / pyproject.toml / setup.cfg test paths), candidate test roots, existing fixtures/clients, documented run commands, and env *names* (not values).",
2534
2513
  "Do NOT search the whole repo for secrets, .env values, tokens, private keys, or production credentials.",
2535
2514
  'framework must be "pytest". Default targetMode to "in-process" unless evidence clearly shows an external service base URL env name or documented managed start/stop with sourceRef.',
2536
- 'Do NOT select targetMode "managed-command" unless task source documents a safe start/stop command with an explicit sourceRef; otherwise leave managedCommand absent and record the gap in evidenceGaps.',
2515
+ 'Do NOT select targetMode "managed-command" unless task source documents a safe start/stop command with an explicit sourceRef; otherwise leave managedCommand absent (do not invent managed mode). For external-running-service, missing managed start/stop is expected and is NOT an evidenceGap.',
2537
2516
  "testRoot and workingDirectory must be repo-relative posix paths without .. or absolute form. Adapter default testRoot is testcase when evidence is incomplete.",
2538
2517
  "runner must not include secret values. report.format must be junit with a relativeHint under the run (e.g. reports/backend-test-junit.xml).",
2539
2518
  "requiredEnvNames lists env NAMES only. baseUrlEnvName is required only for external-running-service and must match ^[A-Z_][A-Z0-9_]*$.",
2540
- "Record incomplete discovery in evidenceGaps. Populate evidenceRefs with repo-relative paths actually read.",
2519
+ "evidenceGaps are optional notes only. Do NOT list greenfield/expected-later items as gaps: missing test_*.py / conftest (generate-pytest will create them), missing pytest.ini when testRoot defaults to testcase/, projected schema under ai_workspace/** instead of docs/templates/**, or optional API_BASE_URL when a documented default base URL exists.",
2520
+ "Prefer evidenceGaps: [] for MVP greenfield external pytest. Use evidenceGaps only for true blockers the later generate nodes cannot fix (e.g. no viable testRoot at all). Populate evidenceRefs with repo-relative paths actually read.",
2541
2521
  "Required top-level keys: schemaVersion, framework, runner, testRoot, workingDirectory, report, targetMode, existingFixtures, authenticationMode, requiredEnvNames, dataIsolation, evidenceGaps, evidenceRefs.",
2542
2522
  "Read-only: do not modify code, docs, artifacts, or repository files.",
2543
2523
  buildSourceContextBlock(sources),
@@ -2572,7 +2552,10 @@ function buildBackendTestExecutionContractGateNode(sources) {
2572
2552
  function buildGenerateBackendFunctionalCasesNode(sources) {
2573
2553
  return {
2574
2554
  id: "generate-backend-functional-cases-pi",
2575
- depends_on: ["backend-test-execution-contract-shell"],
2555
+ depends_on: [
2556
+ "backend-test-analysis-contract-shell",
2557
+ "backend-test-execution-contract-shell",
2558
+ ],
2576
2559
  role: "implementer",
2577
2560
  executor: "pi",
2578
2561
  toolProfile: "write",
@@ -2584,19 +2567,31 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
2584
2567
  // 注意:Pi 节点超时由 executor 层控制(默认 30 分钟)
2585
2568
  // 如需调整,在 harness.json 的 executors.pi 中配置 modelConfig.timeoutMs
2586
2569
  subtask_prompt: [
2587
- "Read the validated structured artifact pointer from backend-test-analysis-contract-shell and generate cases only from that JSON contract.",
2588
- ,
2570
+ "Read both validated run-owned contracts before generating functional cases:",
2571
+ "- contracts/backend-test-analysis.json: authoritative requirements, AC IDs, endpoints, fields, rules, boundaries, risks, and evidence gaps.",
2572
+ "- contracts/backend-test-execution.json: pytest target mode, base URL env name, readiness, fixtures, and data-isolation constraints.",
2573
+ "Generate cases from the analysis contract; use the execution contract only to keep preconditions and automation feasibility realistic.",
2574
+ "Do not proceed from the execution contract alone. Do not re-read source documents or fall back to free-form analysis.",
2589
2575
  "",
2590
2576
  "## Output Steps (do in order):",
2591
2577
  "1. First, output a brief summary: how many modules, how many cases planned per module",
2592
2578
  "2. Then write each test case file under testcase/md/",
2593
2579
  "",
2594
2580
  "## Format Rules:",
2595
- "- Each test case ID: BE-<MODULE>-<NNN> (e.g. BE-ORDER-001)",
2581
+ "- Each test case ID: BE-<MODULE>-<NNN> (e.g. BE-ORDER-001) — always write the FULL id; never abbreviate as 002, 003 in matrices",
2596
2582
  "- Each file covers one module",
2597
- "- Case structure: ID, Title, Precondition, Steps, Expected Result",
2583
+ "- Case structure: ID, Title, Acceptance Criteria, Business Rules, Precondition, Steps, Expected Result",
2584
+ "- Every emitted case MUST declare at least one semantically applicable explicit AC-* under Acceptance Criteria; list BR-* separately under Business Rules",
2585
+ "- If a BR-only scenario has no semantically valid in-scope AC, do not create a standalone case for it; record the limitation in the summary for the manifest evidenceGaps instead",
2586
+ "- Never relabel a negative/boundary/BR-only behavior as AC-002 or another unrelated AC merely to make acIds non-empty",
2598
2587
  "- Map each case to acceptance criteria (AC-xxx)",
2599
2588
  "",
2589
+ "## AC ↔ case consistency (CRITICAL — prevents review request-revision):",
2590
+ "- Every AC-xxx listed on a case body MUST appear only on cases that truly exercise that AC",
2591
+ "- Any AC-coverage matrix / summary table MUST list the same full BE-* case IDs that the case bodies claim — never 'all cases' / '全部用例' unless every case body maps that AC",
2592
+ "- Prefer one primary BE-* case for suite-level ACs (e.g. AC-008 pytest exit 0) rather than tagging every case",
2593
+ "- Out-of-scope ACs (Flyway, frontend e2e, mvn test, etc.) must NOT be claimed in MD; leave them for manifest evidenceGaps",
2594
+ "",
2600
2595
  "## Coverage Requirements:",
2601
2596
  "- Positive paths: happy path for each acceptance criterion",
2602
2597
  "- Negative paths: error scenarios (invalid input, not found, state violations)",
@@ -2611,16 +2606,28 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
2611
2606
  "",
2612
2607
  "## Constraints:",
2613
2608
  "- Stay within writeSet: testcase/md/**",
2614
- "- Do NOT re-read source documents or fall back to free-form analysis use the validated structured artifact only",
2615
- ,
2609
+ "- Do NOT re-read source documents or fall back to free-form analysis; use the two validated run-owned contracts only",
2616
2610
  "- Do not write root artifacts/**",
2617
2611
  ].join("\n\n"),
2618
2612
  };
2619
2613
  }
2620
- function buildEmitBackendCaseManifestNode(sources) {
2614
+ const BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS = [
2615
+ "The final fenced JSON block is authoritative and MUST conform exactly to Backend Test Case Manifest v1.",
2616
+ "Top-level keys MUST be exactly: schemaVersion, sourceBinding, cases, evidenceGaps, and optional coverageSummary. Set schemaVersion to numeric 1. Do NOT emit schemaId, manifestType, taskId, modules, acCoverage, brCoverage, dataIsolation, readiness, or other custom top-level keys.",
2617
+ "Copy sourceBinding exactly from contracts/backend-test-analysis.json: taskId, requirementPath, requirementSha256, referencePaths, requirementIds. Preserve Unicode paths exactly; never replace characters in source/需求.md or other paths.",
2618
+ "Each cases[] item MUST use exactly: caseId, non-empty acIds, title, category, automationStatus; optional endpointRef, ruleRefs, file, symbol, gapReason, evidenceRef. Do NOT use id, module, brIds, endpoint, or priority.",
2619
+ "category MUST be exactly one of: positive, negative, boundary, state-transition, auth, timeout, concurrency, other.",
2620
+ "Before pytest generation, set automationStatus=planned. Use generated only with both file and symbol. Use skipped or unsupported only with gapReason.",
2621
+ "Each evidenceGaps[] item MUST use exactly: optional acId, optional caseId, required description, optional evidenceRef. Every gap requires at least acId or caseId. Do NOT use requirementId, relatedBrIds, or sourceRef.",
2622
+ "Every case must map to at least one semantically applicable explicit AC-* in acIds. If no AC applies, omit that case and bind an evidence gap to the nearest applicable acId or caseId; never emit an unbound informational gap.",
2623
+ "acIds MUST exactly match the explicit AC-* values in the written case body; do not infer ACs from Business Rules or summary matrices.",
2624
+ "Use full BE-<MODULE>-<NNN> caseId strings. Do not invent coverage percentages; omit coverageSummary unless all deterministic counts are exact.",
2625
+ "Minimal shape example: {\"schemaVersion\":1,\"sourceBinding\":{\"taskId\":\"...\",\"requirementPath\":\"source/需求.md\",\"requirementSha256\":\"<64 lowercase hex>\",\"referencePaths\":[],\"requirementIds\":[\"AC-001\"]},\"cases\":[{\"caseId\":\"BE-MODULE-001\",\"acIds\":[\"AC-001\"],\"title\":\"...\",\"category\":\"positive\",\"automationStatus\":\"planned\",\"evidenceRef\":\"testcase/md/module.md\"}],\"evidenceGaps\":[]}",
2626
+ ].join("\n\n");
2627
+ function buildEmitBackendCaseManifestNode(sources, options) {
2621
2628
  return {
2622
- id: "emit-backend-case-manifest-pi",
2623
- depends_on: [
2629
+ id: options?.id ?? "emit-backend-case-manifest-pi",
2630
+ depends_on: options?.dependsOn ?? [
2624
2631
  "generate-backend-functional-cases-pi",
2625
2632
  "backend-test-analysis-contract-shell",
2626
2633
  ],
@@ -2633,20 +2640,17 @@ function buildEmitBackendCaseManifestNode(sources) {
2633
2640
  outputContract: "Pure Backend Test Case Manifest v1 JSON (schema docs/templates/backend-test-case-manifest.schema.json). No file writes; model must not write .harness/**.",
2634
2641
  subtask_prompt: [
2635
2642
  "Emit Backend Test Case Manifest v1 as pure JSON (or one fenced json block with no trailing text).",
2643
+ BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS,
2636
2644
  "Read-only: use validated contracts/backend-test-analysis.json pointer + testcase/md/** only. Do not write repository files or .harness/**.",
2637
- "sourceBinding must match the analysis contract / DAG source binding exactly (taskId, requirementPath, requirementSha256, referencePaths, requirementIds).",
2638
- "For each functional case under testcase/md/: caseId BE-<MODULE>-<NNN>, acIds[], title, category, automationStatus.",
2639
- "After case generation (pre-pytest), default automationStatus=planned. Use skipped/unsupported only with gapReason. Use generated only when file+symbol already exist.",
2640
- "evidenceGaps: structured gaps for explicit AC-* that cannot be mapped to a case.",
2641
- "Do NOT invent coverage percentages. Optional coverageSummary must match deterministic counts (gate recomputes/validates).",
2642
2645
  "No secrets or credential-shaped fields.",
2643
2646
  ].join("\n\n"),
2644
2647
  };
2645
2648
  }
2646
- function buildBackendTestCaseManifestGateNode(sources) {
2649
+ function buildBackendTestCaseManifestGateNode(sources, options) {
2650
+ const fromNodeId = options?.fromNodeId ?? "emit-backend-case-manifest-pi";
2647
2651
  return {
2648
- id: "backend-test-case-manifest-shell",
2649
- depends_on: ["emit-backend-case-manifest-pi"],
2652
+ id: options?.id ?? "backend-test-case-manifest-shell",
2653
+ depends_on: options?.dependsOn ?? ["emit-backend-case-manifest-pi"],
2650
2654
  role: "verifier",
2651
2655
  executor: "shell",
2652
2656
  complexity: "LOW",
@@ -2658,7 +2662,7 @@ function buildBackendTestCaseManifestGateNode(sources) {
2658
2662
  shell: {
2659
2663
  commands: [],
2660
2664
  jsonArtifactGate: {
2661
- fromNodeId: "emit-backend-case-manifest-pi",
2665
+ fromNodeId,
2662
2666
  schemaId: "backend-test-case-manifest-v1",
2663
2667
  artifactName: "backend-test-case-manifest.json",
2664
2668
  outputDir: "contracts",
@@ -2668,13 +2672,18 @@ function buildBackendTestCaseManifestGateNode(sources) {
2668
2672
  },
2669
2673
  };
2670
2674
  }
2671
- function buildBackendTestTraceabilityGateNode(sources) {
2675
+ function buildBackendTestTraceabilityGateNode(sources, options = {}) {
2672
2676
  return {
2673
- id: "backend-test-traceability-gate-shell",
2674
- depends_on: [
2677
+ id: options.id ?? "backend-test-traceability-gate-shell",
2678
+ depends_on: options.dependsOn ?? [
2675
2679
  "generate-backend-pytest-pi",
2680
+ // Effective Case Manifest v1 path (exclusive condition branches):
2681
+ // pass → first manifest shell; request-revision → final manifest shell.
2682
+ // Artifact path is always contracts/backend-test-case-manifest.json.
2676
2683
  "backend-test-case-manifest-shell",
2684
+ "backend-test-case-manifest-final-shell",
2677
2685
  ],
2686
+ dependsPolicy: "all-or-condition-skip",
2678
2687
  role: "verifier",
2679
2688
  executor: "shell",
2680
2689
  complexity: "LOW",
@@ -2690,10 +2699,12 @@ function buildBackendTestTraceabilityGateNode(sources) {
2690
2699
  },
2691
2700
  };
2692
2701
  }
2693
- function buildReviewBackendCasesNode(sources) {
2702
+ function buildReviewBackendCasesNode(sources, options) {
2703
+ const phase = options?.phase ?? "first";
2704
+ const isFinal = phase === "final";
2694
2705
  return {
2695
- id: "review-backend-cases-pi",
2696
- depends_on: [
2706
+ id: options?.id ?? "review-backend-cases-pi",
2707
+ depends_on: options?.dependsOn ?? [
2697
2708
  "backend-test-case-manifest-shell",
2698
2709
  "backend-test-analysis-contract-shell",
2699
2710
  ],
@@ -2703,21 +2714,25 @@ function buildReviewBackendCasesNode(sources) {
2703
2714
  writePolicy: "read-only",
2704
2715
  allowedPaths: commonReadOnlyPaths(sources),
2705
2716
  forbiddenPaths: commonForbiddenPaths(sources),
2706
- outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision; followed by Findings and Coverage Assessment. No file writes.",
2717
+ outputContract: isFinal
2718
+ ? "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision after at most one revision; followed by Findings and Coverage Assessment. No file writes. Final gate accepts pass only."
2719
+ : "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision; followed by Findings, Coverage Assessment, and Required revisions when requesting revision. No file writes.",
2707
2720
  subtask_prompt: [
2708
- "Review the generated backend functional test cases under testcase/md/ and the validated Case Manifest v1.",
2721
+ isFinal
2722
+ ? "Final review of backend functional cases under testcase/md/ and the re-validated Case Manifest v1 after at most one revision pass."
2723
+ : "Review the generated backend functional test cases under testcase/md/ and the validated Case Manifest v1.",
2709
2724
  "",
2710
2725
  "## Mandatory First Line:",
2711
2726
  "First non-empty line must be exactly: VERDICT: pass or VERDICT: request-revision",
2712
2727
  "",
2713
2728
  "## Review Checklist:",
2714
- "- ID format: every case uses BE-<MODULE>-<NNN>",
2715
- "- Positive coverage: each acceptance criterion (AC-xxx) has happy-path case",
2729
+ "- ID format: every case uses BE-<MODULE>-<NNN> (full ids only in bodies and matrices)",
2730
+ "- Positive coverage: each in-scope acceptance criterion (AC-xxx) has happy-path case",
2716
2731
  "- Negative coverage: error scenarios (invalid input, not found, state violations)",
2717
- "- Traceability: each AC maps to at least one case ID (prefer contracts/backend-test-case-manifest.json coverageSummary)",
2732
+ "- Traceability: each explicit AC maps to a case ID or an evidenceGap in contracts/backend-test-case-manifest.json",
2718
2733
  "- Case structure: ID, Title, Precondition, Steps, Expected Result",
2719
2734
  "- No duplicate IDs across files",
2720
- "- Manifest consistency: MD cases align with manifest caseId/acIds; do not invent coverage %",
2735
+ "- Manifest consistency (Critical): every AC claimed in MD case bodies/matrices must match manifest caseIdacIds; never accept 'all cases cover AC-xxx' unless every case maps that AC",
2721
2736
  "",
2722
2737
  "## Conditional Coverage (check ONLY if mentioned in upstream analysis):",
2723
2738
  "- Boundary coverage: check ONLY if analyze-inputs-pi mentions value ranges, length limits, numeric bounds, or format constraints",
@@ -2727,40 +2742,123 @@ function buildReviewBackendCasesNode(sources) {
2727
2742
  "- Concurrency coverage: check ONLY if analyze-inputs-pi mentions concurrency/idempotency rules",
2728
2743
  "- If not mentioned, do NOT flag as missing",
2729
2744
  "",
2745
+ "## Do NOT treat as Critical alone:",
2746
+ "- Missing test_*.py / automation still planned (expected before generate-backend-pytest-pi)",
2747
+ "- Out-of-scope ACs already listed in manifest evidenceGaps (Flyway, frontend e2e, mvn test)",
2748
+ "",
2730
2749
  "## Verdict Rules:",
2731
2750
  "- All Critical checks pass + Important findings ≤ 2 → VERDICT: pass",
2732
2751
  "- Any Critical fails OR Important > 2 → VERDICT: request-revision",
2752
+ isFinal
2753
+ ? "- This is the FINAL review after one revision opportunity; remaining Critical issues must still request-revision (final gate will stop the DAG)."
2754
+ : "- When requesting revision, list numbered Required revisions concrete enough for revise-backend-cases-pi to edit testcase/md/**.",
2733
2755
  "",
2734
2756
  "## Output After Verdict:",
2735
- "1. Coverage Assessment table (AC → case IDs) using manifest + MD",
2757
+ "1. Coverage Assessment table (AC → full BE-* case IDs) using manifest + MD",
2736
2758
  "2. Findings list (Critical/Important/Informational)",
2737
2759
  "3. Statistics (total cases, positive/negative/boundary breakdown)",
2760
+ isFinal ? "" : "4. Required revisions (only when request-revision)",
2738
2761
  "",
2739
2762
  "## Constraints:",
2740
2763
  "- Read-only: do not modify files",
2741
2764
  "- Read validated analysis + case manifest artifacts; do not recompute coverage percentages",
2742
2765
  "- Use testcase/md/ files for case review",
2766
+ ]
2767
+ .filter((line) => line !== "")
2768
+ .join("\n\n"),
2769
+ };
2770
+ }
2771
+ function buildReviewBackendCasesBranchConditionNode(sources) {
2772
+ return {
2773
+ id: "review-backend-cases-branch-condition",
2774
+ depends_on: ["review-backend-cases-pi"],
2775
+ role: "verifier",
2776
+ executor: "static",
2777
+ complexity: "LOW",
2778
+ writePolicy: "none",
2779
+ allowedPaths: commonReadOnlyPaths(sources),
2780
+ forbiddenPaths: commonForbiddenPaths(sources),
2781
+ outputContract: "Dynamic condition: select direct pass vs single revision chain from first review firstVerdictLine; malformed VERDICT fails closed (no default). The pass target is the already-finished first review, so the effective gate remains outside the exclusive branch targets.",
2782
+ subtask_prompt: "Branch on review-backend-cases-pi VERDICT: pass → already-finished first review (revision chain skipped); request-revision → revise-backend-cases-pi.",
2783
+ static: {
2784
+ resultMarkdown: "Backend case review branch condition (direct pass vs single revision).",
2785
+ },
2786
+ dynamicCondition: {
2787
+ workflowNodeId: "review-backend-cases-branch-condition",
2788
+ cases: [
2789
+ {
2790
+ when: "$.nodes['review-backend-cases-pi'].firstVerdictLine == 'VERDICT: pass'",
2791
+ then: "review-backend-cases-pi",
2792
+ },
2793
+ {
2794
+ when: "$.nodes['review-backend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'",
2795
+ then: "revise-backend-cases-pi",
2796
+ },
2797
+ ],
2798
+ },
2799
+ };
2800
+ }
2801
+ function buildReviseBackendCasesNode(sources) {
2802
+ return {
2803
+ id: "revise-backend-cases-pi",
2804
+ depends_on: ["review-backend-cases-branch-condition"],
2805
+ role: "implementer",
2806
+ executor: "pi",
2807
+ toolProfile: "write",
2808
+ complexity: "MED",
2809
+ writePolicy: "exclusive",
2810
+ writeSet: ["testcase/md/**"],
2811
+ allowedPaths: ["testcase/md/**"],
2812
+ forbiddenPaths: commonForbiddenPaths(sources),
2813
+ outputContract: "Only scheduled when first review is VERDICT: request-revision. Apply Required revisions under testcase/md/** then summarize changes (single revision pass).",
2814
+ subtask_prompt: [
2815
+ "You are the single backend case revision pass (max one per DAG run).",
2816
+ "This node is only scheduled when review-backend-cases-pi emitted VERDICT: request-revision.",
2817
+ "",
2818
+ "## Inputs",
2819
+ "- First review: review-backend-cases-pi Markdown (VERDICT + Findings + Required revisions)",
2820
+ "- Current cases: testcase/md/**",
2821
+ "- Validated analysis + case manifest under the current run contracts/",
2822
+ "",
2823
+ "## Required work",
2824
+ "1. Edit only testcase/md/** to address every Critical finding and Required revision item",
2825
+ "2. Fix AC matrices to list FULL BE-* ids matching case bodies; remove false 'all cases' AC claims",
2826
+ "3. Keep BE-<MODULE>-<NNN> ids stable when possible; do not invent out-of-scope AC coverage",
2827
+ "4. Stay within writeSet; do not write pytest or production code",
2828
+ "5. End with a short summary of files touched",
2829
+ "",
2830
+ "Downstream will re-emit and re-validate Case Manifest v1, then run a FINAL review (pass-only gate).",
2743
2831
  ].join("\n\n"),
2744
2832
  };
2745
2833
  }
2746
2834
  function buildReviewBackendCasesGateNode(sources) {
2747
2835
  return {
2748
2836
  id: "review-backend-cases-gate-shell",
2749
- depends_on: ["review-backend-cases-pi"],
2837
+ // OR-join tips: pass-path barrier vs final review after one revise.
2838
+ // Soft condition-skip on the unused tip still allows the gate to run.
2839
+ depends_on: [
2840
+ "review-backend-cases-branch-condition",
2841
+ "review-backend-cases-final-pi",
2842
+ // Always finished; also required for verdictGate.fallbackFromNodeIds validate.
2843
+ "review-backend-cases-pi",
2844
+ ],
2845
+ dependsPolicy: "all-or-condition-skip",
2750
2846
  role: "verifier",
2751
2847
  executor: "shell",
2752
2848
  complexity: "LOW",
2753
2849
  writePolicy: "read-only",
2754
2850
  allowedPaths: commonReadOnlyPaths(sources),
2755
2851
  forbiddenPaths: commonForbiddenPaths(sources),
2756
- outputContract: "Deterministic backend case review gate: exit 0 only when review-backend-cases-pi emits VERDICT: pass.",
2757
- subtask_prompt: "Deterministic gate: block pytest generation unless backend case review emitted VERDICT: pass.",
2852
+ outputContract: "Deterministic backend case review gate: exit 0 only when the effective review emits VERDICT: pass (sole authorization for generate-backend-pytest-pi). Prefers final review JSON when present (revision path); else first review (pass path).",
2853
+ subtask_prompt: "Deterministic gate: block pytest generation unless the effective backend case review (final after revision, else first) emitted VERDICT: pass.",
2758
2854
  shell: {
2759
2855
  commands: [],
2760
2856
  verdictGate: {
2761
- fromNodeId: "review-backend-cases-pi",
2857
+ // Prefer final (revision path) when its artifact exists; fall back to first review.
2858
+ fromNodeId: "review-backend-cases-final-pi",
2859
+ fallbackFromNodeIds: ["review-backend-cases-pi"],
2762
2860
  accept: ["VERDICT: pass"],
2763
- label: "backend case review",
2861
+ label: "backend case effective review",
2764
2862
  lineMode: "first-verdict-line",
2765
2863
  },
2766
2864
  cwd: ".",
@@ -2889,7 +2987,149 @@ function buildGenerateBackendPytestNode(sources) {
2889
2987
  ].join("\n\n"),
2890
2988
  };
2891
2989
  }
2892
- function buildExecuteBackendPytestNode(sources) {
2990
+ function buildBackendTestSemanticReviewNode(sources, options = {}) {
2991
+ const final = options.final ?? false;
2992
+ return {
2993
+ id: options.id ?? "review-generated-backend-pytest-pi",
2994
+ depends_on: options.dependsOn ?? [
2995
+ "generate-backend-pytest-pi",
2996
+ "backend-test-analysis-contract-shell",
2997
+ "backend-test-case-manifest-shell",
2998
+ "backend-test-case-manifest-final-shell",
2999
+ ],
3000
+ dependsPolicy: "all-or-condition-skip",
3001
+ role: "reviewer",
3002
+ executor: "pi",
3003
+ complexity: "MED",
3004
+ writePolicy: "read-only",
3005
+ allowedPaths: ["testcase/**"],
3006
+ forbiddenPaths: commonForbiddenPaths(sources),
3007
+ outputContract: "Pure Backend Test Semantic Review v1 JSON: verdict, findings[], summary. No file writes.",
3008
+ subtask_prompt: [
3009
+ final ? "Final semantic review after the single generated-pytest revision." : "Review generated pytest semantics before the first execution.",
3010
+ "Use only compact authoritative inputs: contracts/backend-test-analysis.json, contracts/backend-test-case-manifest.json, testcase/md/**, and generated testcase/**/test_*.py/helpers/factories.",
3011
+ "Return exactly one pure JSON object with only verdict, findings, summary; no Markdown fence or surrounding prose.",
3012
+ "verdict must be pass or request-revision. Each findings[] item must contain exactly severity, caseId, testFile, testSymbol, contractRefs, issue, requiredChange.",
3013
+ "severity must be exactly Critical, Important, or Informational; contractRefs must be a non-empty string array. A request-revision verdict requires at least one finding; pass must not contain Critical findings.",
3014
+ "Minimal shape: {\"verdict\":\"pass\",\"findings\":[],\"summary\":\"No contract-backed semantic contradiction found.\"}",
3015
+ "Check responseBody.kind (array vs object/items), ordering, field comparison (especially parseable-only date-time precision), documented status/error fields, and each caseId→symbol assertion meaning.",
3016
+ "Do not use aliases such as file, symbol, refs, finding, or requiredFix; the strict contract requires testFile, testSymbol, contractRefs, issue, requiredChange.",
3017
+ "request-revision only for concrete semantic contradiction with reviewed cases/formal analysis evidence. No style findings.",
3018
+ "Read-only; do not edit tests or production code.",
3019
+ ].join("\n\n"),
3020
+ };
3021
+ }
3022
+ function buildBackendTestSemanticReviewGateNode(sources, options = {}) {
3023
+ const fromNodeId = options.fromNodeId ?? "review-generated-backend-pytest-pi";
3024
+ return {
3025
+ id: options.id ?? "backend-test-semantic-review-shell",
3026
+ depends_on: options.dependsOn ?? [fromNodeId],
3027
+ role: "verifier",
3028
+ executor: "shell",
3029
+ complexity: "LOW",
3030
+ writePolicy: "read-only",
3031
+ allowedPaths: commonReadOnlyPaths(sources),
3032
+ forbiddenPaths: commonForbiddenPaths(sources),
3033
+ outputContract: "Validated run-owned Backend Test Semantic Review v1 artifact.",
3034
+ subtask_prompt: "Validate semantic review JSON before branch selection.",
3035
+ shell: {
3036
+ commands: [],
3037
+ jsonArtifactGate: {
3038
+ fromNodeId,
3039
+ schemaId: "backend-test-semantic-review-v1",
3040
+ artifactName: options.id?.includes("final") ? "backend-test-semantic-review-final.json" : "backend-test-semantic-review.json",
3041
+ outputDir: "contracts",
3042
+ },
3043
+ cwd: ".",
3044
+ timeoutMs: 60000,
3045
+ },
3046
+ };
3047
+ }
3048
+ function buildBackendTestSemanticReviewConditionNode(sources) {
3049
+ return {
3050
+ id: "backend-test-semantic-review-condition",
3051
+ depends_on: ["backend-test-semantic-review-shell"],
3052
+ role: "verifier",
3053
+ executor: "static",
3054
+ complexity: "LOW",
3055
+ writePolicy: "none",
3056
+ allowedPaths: [],
3057
+ forbiddenPaths: commonForbiddenPaths(sources),
3058
+ outputContract: "Select direct semantic pass or one testcase-only revision; the pass target is the already-finished validated semantic review so the final gate remains outside the exclusive branch targets.",
3059
+ subtask_prompt: "Branch deterministically from semantic review verdict.",
3060
+ static: { resultMarkdown: "Backend pytest semantic review selector." },
3061
+ dynamicCondition: {
3062
+ workflowNodeId: "backend-test-semantic-review-condition",
3063
+ cases: [
3064
+ { when: "$.nodes['backend-test-semantic-review-shell'].json.verdict == 'pass'", then: "backend-test-semantic-review-shell" },
3065
+ ],
3066
+ default: "revise-generated-backend-pytest-pi",
3067
+ },
3068
+ };
3069
+ }
3070
+ function buildReviseGeneratedBackendPytestNode(sources) {
3071
+ const writeSet = ["testcase/**/test_*.py", "testcase/**/helpers/**", "testcase/**/factories/**"];
3072
+ return {
3073
+ id: "revise-generated-backend-pytest-pi",
3074
+ depends_on: ["backend-test-semantic-review-condition"],
3075
+ role: "implementer",
3076
+ executor: "pi",
3077
+ toolProfile: "write",
3078
+ complexity: "MED",
3079
+ writePolicy: "exclusive",
3080
+ writeSet,
3081
+ allowedPaths: writeSet,
3082
+ forbiddenPaths: [...commonForbiddenPaths(sources), "apps/**", "src/**", "testcase/md/**", "**/conftest.py", "**/pytest.ini"],
3083
+ outputContract: "Apply one bounded semantic correction to generated pytest from validated findings only.",
3084
+ subtask_prompt: [
3085
+ "This is the only pre-execution generated-pytest semantic revision (1/1).",
3086
+ "Read contracts/backend-test-semantic-review.json and edit only the cited generated test files/helpers/factories.",
3087
+ "Preserve case IDs, symbols, test count, target mode, base URL and real-service path.",
3088
+ "Do not delete tests, add skip/xfail, swallow failures, substitute mocks, or weaken assertions beyond the formal comparison/shape contract.",
3089
+ "Do not modify product code, testcase/md/**, conftest.py, pytest.ini, or .harness/**.",
3090
+ ].join("\n\n"),
3091
+ };
3092
+ }
3093
+ function buildBackendTestSemanticFinalGateNode(sources) {
3094
+ return {
3095
+ id: "backend-test-semantic-final-gate-shell",
3096
+ depends_on: ["backend-test-semantic-review-condition", "backend-test-semantic-review-final-shell"],
3097
+ dependsPolicy: "all-or-condition-skip",
3098
+ role: "verifier",
3099
+ executor: "shell",
3100
+ complexity: "LOW",
3101
+ writePolicy: "read-only",
3102
+ allowedPaths: commonReadOnlyPaths(sources),
3103
+ forbiddenPaths: commonForbiddenPaths(sources),
3104
+ outputContract: "Pass-only semantic authorization for traceability and initial pytest.",
3105
+ subtask_prompt: "Accept initial pass path or final semantic review pass; fail closed otherwise.",
3106
+ shell: {
3107
+ commands: [[
3108
+ 'test -n "${HARNESS_DAG_RUN_DIR:-}" || exit 2',
3109
+ 'node -e \'const fs=require("fs"),path=require("path");const r=process.env.HARNESS_DAG_RUN_DIR;const final=path.join(r,"contracts","backend-test-semantic-review-final.json");const first=path.join(r,"contracts","backend-test-semantic-review.json");const p=fs.existsSync(final)?final:first;const v=JSON.parse(fs.readFileSync(p,"utf8"));if(v.verdict!=="pass")throw new Error("backend pytest semantic review did not pass");console.log("backend pytest semantic gate: pass");\'',
3110
+ ].join("; ")],
3111
+ cwd: ".",
3112
+ timeoutMs: 60000,
3113
+ },
3114
+ };
3115
+ }
3116
+ function collectBackendTestShellEnvAllowlist(sources) {
3117
+ const names = new Set();
3118
+ for (const verify of sources.taskConfig.verifyCommands) {
3119
+ const assignmentPattern = /(?:^|[\s;&|])([A-Z_][A-Z0-9_]*)\s*=/g;
3120
+ for (const match of verify.command.matchAll(assignmentPattern)) {
3121
+ if (match[1])
3122
+ names.add(match[1]);
3123
+ }
3124
+ }
3125
+ return [...names].sort();
3126
+ }
3127
+ function taskAllowsBackendTestReportWrite(sources) {
3128
+ return sources.taskConfig.allowedPaths.some((pattern) => pattern === "docs/test-reports/**" ||
3129
+ pattern === "docs/**" ||
3130
+ pattern === "**");
3131
+ }
3132
+ function buildExecuteBackendPytestNode(sources, options = {}) {
2893
3133
  // Keep the target worktree read-only: JUnit is runner-owned evidence under
2894
3134
  // the current DAG run and moves with active → completed/paused lifecycle.
2895
3135
  // Adapter default testRoot is frozen at DAG generation time (auditable) and
@@ -2900,12 +3140,16 @@ function buildExecuteBackendPytestNode(sources) {
2900
3140
  });
2901
3141
  // Map pytest exit 0/1 → node success ONLY when JUnit exists (assertion-fail is a
2902
3142
  // legal result). Do not change global shell ok semantics. Persist raw exit for parse.
3143
+ const nodeId = options.id ?? "execute-backend-pytest-shell";
3144
+ const reportStem = options.reportStem ?? "backend-test";
3145
+ const reportName = `${reportStem}-junit.xml`;
3146
+ const exitName = `${reportStem}-pytest-exit.txt`;
2903
3147
  const pytestCommand = [
2904
3148
  preflightCommand,
2905
- 'REPORT="${HARNESS_DAG_RUN_DIR}/reports/backend-test-junit.xml"',
2906
- 'EXIT_FILE="${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
3149
+ `REPORT="\${HARNESS_DAG_RUN_DIR}/reports/${reportName}"`,
3150
+ `EXIT_FILE="\${HARNESS_DAG_RUN_DIR}/reports/${exitName}"`,
2907
3151
  'mkdir -p "$(dirname "${REPORT}")"',
2908
- `PYTHONDONTWRITEBYTECODE=1 python -m pytest ${frozenTestRoot}/ -v -p no:cacheprovider --junitxml="\${REPORT}"`,
3152
+ `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 python -m pytest ${frozenTestRoot}/ -v -p no:cacheprovider --junitxml="\${REPORT}"`,
2909
3153
  "STATUS=$?",
2910
3154
  'printf "%s" "${STATUS}" > "${EXIT_FILE}"',
2911
3155
  'printf "JUnit report: %s\\n" "${REPORT}"',
@@ -2914,9 +3158,9 @@ function buildExecuteBackendPytestNode(sources) {
2914
3158
  'exit "${STATUS}"',
2915
3159
  ].join("; ");
2916
3160
  return {
2917
- id: "execute-backend-pytest-shell",
2918
- depends_on: [
2919
- "backend-test-traceability-gate-shell",
3161
+ id: nodeId,
3162
+ depends_on: options.dependsOn ?? [
3163
+ "backend-test-semantic-final-gate-shell",
2920
3164
  "backend-test-execution-contract-shell",
2921
3165
  ],
2922
3166
  role: "verifier",
@@ -2929,6 +3173,7 @@ function buildExecuteBackendPytestNode(sources) {
2929
3173
  subtask_prompt: "Run pytest for the backend test suite; write JUnit + pytestExitCode evidence only under the current HARNESS_DAG_RUN_DIR/reports/.",
2930
3174
  shell: {
2931
3175
  commands: [pytestCommand],
3176
+ envAllowlist: collectBackendTestShellEnvAllowlist(sources),
2932
3177
  verifyEvidence: buildVerifyEvidence({
2933
3178
  phase: "final",
2934
3179
  quota: "full",
@@ -2941,10 +3186,13 @@ function buildExecuteBackendPytestNode(sources) {
2941
3186
  },
2942
3187
  };
2943
3188
  }
2944
- function buildParseBackendTestResultNode(sources) {
3189
+ function buildParseBackendTestResultNode(sources, options = {}) {
3190
+ const id = options.id ?? "parse-backend-test-result-shell";
3191
+ const fromNodeId = options.fromNodeId ?? "execute-backend-pytest-shell";
3192
+ const artifactName = options.artifactName ?? "backend-test-result.json";
2945
3193
  return {
2946
- id: "parse-backend-test-result-shell",
2947
- depends_on: ["execute-backend-pytest-shell"],
3194
+ id,
3195
+ depends_on: options.dependsOn ?? [fromNodeId],
2948
3196
  role: "verifier",
2949
3197
  executor: "shell",
2950
3198
  complexity: "LOW",
@@ -2956,10 +3204,13 @@ function buildParseBackendTestResultNode(sources) {
2956
3204
  shell: {
2957
3205
  commands: [],
2958
3206
  jsonArtifactGate: {
2959
- fromNodeId: "execute-backend-pytest-shell",
3207
+ fromNodeId,
2960
3208
  schemaId: "backend-test-result-v1",
2961
- artifactName: "backend-test-result.json",
3209
+ artifactName,
2962
3210
  outputDir: "contracts",
3211
+ ...(options.junitRelativePath
3212
+ ? { junitRelativePath: options.junitRelativePath }
3213
+ : {}),
2963
3214
  },
2964
3215
  cwd: ".",
2965
3216
  timeoutMs: 60000,
@@ -2969,7 +3220,7 @@ function buildParseBackendTestResultNode(sources) {
2969
3220
  function buildClassifyBackendTestResultNode(sources) {
2970
3221
  return {
2971
3222
  id: "classify-backend-test-result-pi",
2972
- depends_on: ["parse-backend-test-result-shell"],
3223
+ depends_on: ["parse-backend-test-result-initial-shell"],
2973
3224
  role: "reviewer",
2974
3225
  executor: "pi",
2975
3226
  complexity: "MED",
@@ -2980,7 +3231,9 @@ function buildClassifyBackendTestResultNode(sources) {
2980
3231
  subtask_prompt: [
2981
3232
  "Read-only classifier for Backend Test Result v1.",
2982
3233
  "Return exactly one JSON object (prefer pure JSON; single fenced json block tolerated; no trailing prose).",
2983
- "Read contracts/backend-test-result.json (run-owned Result v1). Do NOT invent pass rates from raw logs.",
3234
+ "The object must contain exactly category, evidence, confidence, notes. evidence must be a non-empty array of strings, confidence must be a number from 0 through 1, and notes must be a non-empty string. Do not emit schemaVersion or custom fields.",
3235
+ "Minimal shape: {\"category\":\"Unknown\",\"evidence\":[\"outcome=completed-with-failures\"],\"confidence\":0.5,\"notes\":\"Single-run evidence is insufficient for a stronger classification.\"}",
3236
+ "Read contracts/backend-test-result-initial.json (run-owned initial Result v1). Do NOT invent pass rates from raw logs.",
2984
3237
  "category must be one of: ProductBug, TestBug, EnvFailure, ContractMismatch, FlakyTest, Unknown.",
2985
3238
  "Hard constraints:",
2986
3239
  "- Single-run failure MUST NOT use FlakyTest (use Unknown, TestBug, or ProductBug).",
@@ -2992,24 +3245,187 @@ function buildClassifyBackendTestResultNode(sources) {
2992
3245
  ].join("\n\n"),
2993
3246
  };
2994
3247
  }
3248
+ function buildBackendTestClassificationGateNode(sources) {
3249
+ return {
3250
+ id: "backend-test-classification-shell",
3251
+ depends_on: ["classify-backend-test-result-pi"],
3252
+ role: "verifier",
3253
+ executor: "shell",
3254
+ complexity: "LOW",
3255
+ writePolicy: "read-only",
3256
+ allowedPaths: commonReadOnlyPaths(sources),
3257
+ forbiddenPaths: commonForbiddenPaths(sources),
3258
+ outputContract: "Validated run-owned Backend Test Classification v1 at contracts/backend-test-classification.json.",
3259
+ subtask_prompt: "Validate and materialize the read-only backend-test classification for deterministic repair routing.",
3260
+ shell: {
3261
+ commands: [],
3262
+ jsonArtifactGate: {
3263
+ fromNodeId: "classify-backend-test-result-pi",
3264
+ schemaId: "backend-test-classification-v1",
3265
+ artifactName: "backend-test-classification.json",
3266
+ outputDir: "contracts",
3267
+ },
3268
+ cwd: ".",
3269
+ timeoutMs: 60000,
3270
+ },
3271
+ };
3272
+ }
3273
+ function buildBackendTestRepairEligibilityNode(sources) {
3274
+ const command = buildBackendTestRepairEligibilityShellSnippet();
3275
+ return {
3276
+ id: "backend-test-repair-eligibility-shell",
3277
+ depends_on: ["backend-test-classification-shell"],
3278
+ role: "verifier",
3279
+ executor: "shell",
3280
+ complexity: "LOW",
3281
+ writePolicy: "read-only",
3282
+ allowedPaths: commonReadOnlyPaths(sources),
3283
+ forbiddenPaths: commonForbiddenPaths(sources),
3284
+ outputContract: "Pure JSON {schemaVersion,eligible,reason,revisionAttempt,category,confidence}; eligible only for completed TestBug assertion failures with error=0.",
3285
+ subtask_prompt: "Deterministically decide whether this run may use its single testcase-only repair attempt.",
3286
+ shell: { commands: [command], cwd: ".", timeoutMs: 60000 },
3287
+ };
3288
+ }
3289
+ function buildBackendTestRepairConditionNode(sources) {
3290
+ return {
3291
+ id: "backend-test-repair-condition",
3292
+ depends_on: ["backend-test-repair-eligibility-shell"],
3293
+ role: "verifier",
3294
+ executor: "static",
3295
+ complexity: "LOW",
3296
+ writePolicy: "none",
3297
+ allowedPaths: [],
3298
+ forbiddenPaths: commonForbiddenPaths(sources),
3299
+ outputContract: "Select repair or skip repair exactly once; ineligible runs select the already-finished eligibility evidence so the effective-result shell remains outside the exclusive branch targets.",
3300
+ subtask_prompt: "Route eligible TestBug to one repair attempt; otherwise select the already-finished eligibility evidence and skip repair.",
3301
+ static: { resultMarkdown: "Backend-test repair branch selector." },
3302
+ dynamicCondition: {
3303
+ workflowNodeId: "backend-test-repair-condition",
3304
+ cases: [
3305
+ {
3306
+ when: "$.nodes['backend-test-repair-eligibility-shell'].json.eligible == true",
3307
+ then: "repair-backend-pytest-pi",
3308
+ },
3309
+ ],
3310
+ default: "backend-test-repair-eligibility-shell",
3311
+ },
3312
+ };
3313
+ }
3314
+ function buildRepairBackendPytestNode(sources) {
3315
+ const writeSet = [
3316
+ "testcase/**/test_*.py",
3317
+ "testcase/**/helpers/**",
3318
+ "testcase/**/factories/**",
3319
+ ];
3320
+ return {
3321
+ id: "repair-backend-pytest-pi",
3322
+ depends_on: ["backend-test-repair-condition"],
3323
+ role: "implementer",
3324
+ executor: "pi",
3325
+ toolProfile: "write",
3326
+ complexity: "HIGH",
3327
+ writePolicy: "exclusive",
3328
+ writeSet,
3329
+ allowedPaths: writeSet,
3330
+ forbiddenPaths: [
3331
+ ...commonForbiddenPaths(sources),
3332
+ "apps/**",
3333
+ "src/**",
3334
+ ".env*",
3335
+ "**/migrations/**",
3336
+ ],
3337
+ outputContract: "Repair only existing generated pytest tests/helpers/factories for classified TestBug findings. No production/config/runtime evidence writes.",
3338
+ subtask_prompt: [
3339
+ "This is the only automatic TestBug repair attempt (1/1).",
3340
+ "Read contracts/backend-test-result-initial.json, contracts/backend-test-classification.json, contracts/backend-test-analysis.json, contracts/backend-test-case-manifest.json, and only the generated pytest files named in failures[].name or manifest mappings.",
3341
+ "Do not re-read the full task source tree: the run-owned analysis/manifest are the compact authoritative context.",
3342
+ "Repair only test implementation defects directly supported by reviewed Expected Results or formal API contract evidence.",
3343
+ "Never modify product code, migrations, service configuration, conftest.py, pytest.ini, pyproject.toml, setup.cfg, source requirements, testcase/md/**, or .harness/**.",
3344
+ "Never delete a test, remove a case mapping, add skip/skipif/xfail, swallow AssertionError/network exceptions, switch to a mock server, or weaken documented status/value assertions.",
3345
+ "Keep the full suite runnable and preserve each BE-* case ID and pytest symbol mapping.",
3346
+ "Stay within writeSet: testcase/**/test_*.py, helpers/**, factories/**.",
3347
+ ].join("\n\n"),
3348
+ };
3349
+ }
3350
+ function buildBackendTestRepairSafetyGateNode(sources) {
3351
+ const command = buildBackendTestRepairSafetyShellSnippet();
3352
+ return {
3353
+ id: "backend-test-repair-safety-gate-shell",
3354
+ depends_on: ["repair-backend-pytest-pi"],
3355
+ role: "verifier",
3356
+ executor: "shell",
3357
+ complexity: "LOW",
3358
+ writePolicy: "read-only",
3359
+ allowedPaths: ["testcase/**"],
3360
+ forbiddenPaths: commonForbiddenPaths(sources),
3361
+ outputContract: "Fail-closed repair safety gate rejecting skip/xfail and broad failure swallowing before final traceability and pytest.",
3362
+ subtask_prompt: "Check that automatic TestBug repair did not manufacture success.",
3363
+ shell: { commands: [command], cwd: ".", timeoutMs: 60000 },
3364
+ };
3365
+ }
3366
+ function buildBackendTestEffectiveResultNode(sources) {
3367
+ const command = buildBackendTestEffectiveResultSelectorShellSnippet();
3368
+ // Inline retrospective context: relocated verbatim from the former
3369
+ // buildBackendTestRetrospectiveContextNode so test-retrospect-pi keeps a
3370
+ // single upstream. Runs after `command` which materializes
3371
+ // contracts/backend-test-result.json from the effective (final|initial) result.
3372
+ const retroCommand = [
3373
+ 'test -n "${HARNESS_DAG_RUN_DIR:-}" || { echo "missing HARNESS_DAG_RUN_DIR for backend-test retrospective context" >&2; exit 2; }',
3374
+ 'node -e \'const fs=require("fs"),path=require("path"); const root=process.env.HARNESS_DAG_RUN_DIR; const read=(name)=>JSON.parse(fs.readFileSync(path.join(root,"contracts",name),"utf8")); process.stdout.write(JSON.stringify({schemaVersion:1,result:read("backend-test-result.json"),initialResult:read("backend-test-result-initial.json"),finalResult:fs.existsSync(path.join(root,"contracts","backend-test-result-final.json"))?read("backend-test-result-final.json"):null,manifest:read("backend-test-case-manifest.json"),classification:read("backend-test-classification.json")},null,2));\'',
3375
+ ].join("; ");
3376
+ return {
3377
+ id: "select-effective-backend-test-result-shell",
3378
+ depends_on: [
3379
+ "backend-test-repair-condition",
3380
+ "parse-backend-test-result-final-shell",
3381
+ "backend-test-case-manifest-shell",
3382
+ "backend-test-case-manifest-final-shell",
3383
+ "backend-test-classification-shell",
3384
+ ],
3385
+ dependsPolicy: "all-or-condition-skip",
3386
+ role: "verifier",
3387
+ executor: "shell",
3388
+ complexity: "LOW",
3389
+ writePolicy: "read-only",
3390
+ allowedPaths: commonReadOnlyPaths(sources),
3391
+ forbiddenPaths: commonForbiddenPaths(sources),
3392
+ outputContract: "Materialize contracts/backend-test-result.json from final result when present, otherwise initial result; emit effective result metadata, and emit the complete retrospective context (Result v1 + Case Manifest v1 + classifier output) for downstream retrospective consumption.",
3393
+ subtask_prompt: "Select the effective backend-test result deterministically without changing initial/final evidence, then emit the complete retrospective context.",
3394
+ shell: { commands: [command, retroCommand], cwd: ".", timeoutMs: 60000 },
3395
+ };
3396
+ }
2995
3397
  function buildTestRetrospectNode(sources) {
3398
+ const canWriteReport = taskAllowsBackendTestReportWrite(sources);
2996
3399
  return {
2997
3400
  id: "test-retrospect-pi",
2998
- depends_on: ["classify-backend-test-result-pi"],
3401
+ depends_on: ["select-effective-backend-test-result-shell"],
2999
3402
  role: "closeout",
3000
3403
  executor: "pi",
3001
- toolProfile: "write",
3002
3404
  complexity: "MED",
3003
- writePolicy: "exclusive",
3004
- writeSet: ["docs/test-reports/**"],
3005
- allowedPaths: ["docs/test-reports/**"],
3405
+ ...(canWriteReport
3406
+ ? {
3407
+ toolProfile: "write",
3408
+ writePolicy: "exclusive",
3409
+ writeSet: ["docs/test-reports/**"],
3410
+ allowedPaths: ["docs/test-reports/**"],
3411
+ }
3412
+ : {
3413
+ writePolicy: "read-only",
3414
+ allowedPaths: commonReadOnlyPaths(sources),
3415
+ }),
3006
3416
  forbiddenPaths: commonForbiddenPaths(sources),
3417
+ outputContract: canWriteReport
3418
+ ? "Maturity rating in assistant output plus a report written under docs/test-reports/**."
3419
+ : "Read-only maturity rating and retrospective in assistant output; no repository file writes because task allowedPaths do not authorize docs/test-reports/**.",
3007
3420
  subtask_prompt: [
3008
- "Read upstream Result v1 + Case Manifest coverageSummary + classification and generate a test retrospective report.",
3421
+ "Read the complete JSON from direct upstream select-effective-backend-test-result-shell and generate a test retrospective report.",
3422
+ "That JSON contains result, manifest (including coverageSummary), and classification. Treat those fields as authoritative; do not rely on pointer/hash summaries.",
3009
3423
  "",
3010
3424
  "## Output Steps (do in order):",
3011
3425
  "1. First, output the maturity rating on the first line: Rating: A/B/C/D",
3012
- "2. Then write the full report under docs/test-reports/",
3426
+ canWriteReport
3427
+ ? "2. Then write the full report under docs/test-reports/"
3428
+ : "2. Keep the full retrospective in assistant output only; do not write repository files because docs/test-reports/** is outside task allowedPaths.",
3013
3429
  "",
3014
3430
  "## Stats authority (deterministic only):",
3015
3431
  "- Pass rate, failed/error/skipped counts, and failure list MUST come from contracts/backend-test-result.json only.",
@@ -3031,7 +3447,9 @@ function buildTestRetrospectNode(sources) {
3031
3447
  "- D: below C thresholds",
3032
3448
  "",
3033
3449
  "## Constraints:",
3034
- "- Stay within writeSet: docs/test-reports/**",
3450
+ canWriteReport
3451
+ ? "- Stay within writeSet: docs/test-reports/**"
3452
+ : "- Read-only: do not modify repository files",
3035
3453
  "- Do NOT re-read source documents — use upstream outputs only",
3036
3454
  "- Do not write root artifacts/**",
3037
3455
  ].join("\n\n"),
@@ -3078,63 +3496,73 @@ const BACKEND_TEST_SKILLS_BY_ROLE = {
3078
3496
  };
3079
3497
  function buildBackendTestHybridDag(sources) {
3080
3498
  const { taskConfig } = sources;
3081
- const sourceContext = buildSourceContextBlock(sources);
3082
- const readOnlyPaths = commonReadOnlyPaths(sources);
3083
- const forbiddenPaths = commonForbiddenPaths(sources);
3084
3499
  const globalConstraints = [
3085
3500
  ...taskConfig.hardConstraints,
3086
- ...(sources.constraintMarkdown
3087
- ? [`See 执行约束.md in task source (${sources.taskId})`]
3088
- : []),
3089
3501
  ...STANDARD_GLOBAL_CONSTRAINTS,
3090
- "backend-test-dag nodes must maintain traceability from requirements to functional cases to pytest automation.",
3502
+ "backend-test-dag uses exactly 24 real top-level tasks; bounded revision/repair branches are controlled by fail-closed runIf expressions.",
3503
+ "Analysis, execution, manifest, semantic review, initial/final/effective results, classification, eligibility, repair safety, traceability and outcome evidence remain run-owned and fail-closed.",
3091
3504
  "Functional test case IDs must use BE-<MODULE>-<NNN> format.",
3092
- "pytest execution must keep the target worktree read-only and write machine-readable results only under the current HARNESS_DAG_RUN_DIR/reports/** (e.g. JUnit XML).",
3093
- "pytest automation scripts must use test_ filename prefix for pytest discovery.",
3094
- "generate-backend-pytest-pi may create only new files under testcase/**/test_*.py, testcase/**/helpers/**, and testcase/**/factories/**; modifying conftest.py, pytest.ini, pyproject.toml, or production code is forbidden.",
3095
- "review-backend-cases-gate-shell must block pytest generation unless the review verdict is exactly VERDICT: pass.",
3096
- "If a target test filename already exists under testcase/, add a numeric suffix (_01, _02, ...); never overwrite or append to existing files.",
3097
- "execute-backend-pytest-shell must not modify test assertions or production code to make tests pass; test failures indicate potential implementation issues and must be reported honestly.",
3098
- "parse-backend-test-result-shell materializes Backend Test Result v1 from JUnit + pytestExitCode; classify/retrospect run on pass and assertion-fail; backend-test-outcome-gate-shell uses result.outcome only.",
3099
- "backend-test-case-manifest-shell validates schemaId backend-test-case-manifest-v1 and materializes contracts/backend-test-case-manifest.json; AC coverage is fail-closed and deterministic.",
3100
- "backend-test-traceability-gate-shell verifies generated file/symbol existence after pytest generation and before execute; models must not invent coverage percentages.",
3505
+ "pytest writers may only change declared testcase assets; production code, config, skip/xfail, swallowed failures and mock substitution are forbidden.",
3101
3506
  ];
3102
- const spec = {
3103
- version: 3,
3104
- title: `Backend test DAG: ${taskConfig.title}`,
3105
- runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
3106
- outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
3107
- objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
3108
- successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
3109
- globalConstraints,
3110
- // No convergence loop: review gate is fail-closed. request-revision stops
3111
- // the DAG; regenerate after fixing cases. Controller still keys off
3112
- // hard-verify-shell, which this template does not include.
3113
- defaults: {
3114
- ...BACKEND_TEST_DEFAULTS,
3115
- contextProfile: taskConfig.contextProfile,
3116
- },
3117
- skillsByRole: BACKEND_TEST_SKILLS_BY_ROLE,
3118
- executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
3119
- tasks: [
3120
- buildAnalyzeInputsNode(sources),
3121
- buildBackendTestAnalysisContractGateNode(sources),
3122
- buildBackendTestEnvironmentScoutNode(sources),
3123
- buildBackendTestExecutionContractGateNode(sources),
3124
- buildGenerateBackendFunctionalCasesNode(sources),
3125
- buildEmitBackendCaseManifestNode(sources),
3126
- buildBackendTestCaseManifestGateNode(sources),
3127
- buildReviewBackendCasesNode(sources),
3128
- buildReviewBackendCasesGateNode(sources),
3129
- buildGenerateBackendPytestNode(sources),
3130
- buildBackendTestTraceabilityGateNode(sources),
3131
- buildExecuteBackendPytestNode(sources),
3132
- buildParseBackendTestResultNode(sources),
3133
- buildClassifyBackendTestResultNode(sources),
3134
- buildTestRetrospectNode(sources),
3135
- buildBackendTestOutcomeGateNode(sources),
3136
- ],
3507
+ const analyze = buildAnalyzeInputsNode(sources);
3508
+ analyze.id = "analyze-and-discover-backend-test-pi";
3509
+ analyze.outputContract = "Pure JSON envelope {analysis: Backend Test Analysis v2, execution: Backend Test Execution Contract v1}; no prose or writes.";
3510
+ analyze.subtask_prompt = `${analyze.subtask_prompt}\n\nAlso perform the read-only environment discovery described by Backend Test Execution Contract v1. Return exactly one JSON envelope with top-level keys analysis and execution; analysis must satisfy v2 and execution must satisfy v1.`;
3511
+ const contracts = {
3512
+ id: "validate-backend-test-contracts-shell", depends_on: [analyze.id], role: "verifier", executor: "shell", complexity: "LOW",
3513
+ writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources),
3514
+ outputContract: "Materialize and validate contracts/backend-test-analysis.json and contracts/backend-test-execution.json.",
3515
+ subtask_prompt: "Validate both backend-test intake contracts fail-closed.", shell: { commands: [], backendTestPipeline: "contracts", cwd: ".", timeoutMs: 60000 },
3137
3516
  };
3517
+ const generateCases = buildGenerateBackendFunctionalCasesNode(sources);
3518
+ generateCases.id = "generate-backend-cases-and-manifest-pi";
3519
+ generateCases.depends_on = [contracts.id];
3520
+ generateCases.outputContract = "Write testcase/md/** and end with one fenced json Backend Test Case Manifest v1 block matching the strict field contract.";
3521
+ generateCases.subtask_prompt += `\n\nAfter writing Markdown, end assistant output with exactly one fenced json block containing Backend Test Case Manifest v1 derived from the written cases.\n\n${BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS}`;
3522
+ const manifest = buildBackendTestCaseManifestGateNode(sources, { dependsOn: [generateCases.id], fromNodeId: generateCases.id });
3523
+ const reviewCases = buildReviewBackendCasesNode(sources, { dependsOn: [manifest.id, contracts.id] });
3524
+ const reviseCases = buildReviseBackendCasesNode(sources);
3525
+ reviseCases.depends_on = [reviewCases.id];
3526
+ reviseCases.runIf = "$.nodes['review-backend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'";
3527
+ reviseCases.outputContract = "Apply one case revision and end with one fenced json final Case Manifest v1 block matching the strict field contract.";
3528
+ reviseCases.subtask_prompt += `\n\nAfter edits, end assistant output with exactly one fenced json block containing the complete final Backend Test Case Manifest v1.\n\n${BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS}`;
3529
+ const finalManifest = buildBackendTestCaseManifestGateNode(sources, { id: "backend-test-case-manifest-final-shell", dependsOn: [reviseCases.id], fromNodeId: reviseCases.id });
3530
+ finalManifest.runIf = reviseCases.runIf;
3531
+ const finalCaseReview = buildReviewBackendCasesNode(sources, { id: "review-backend-cases-final-pi", phase: "final", dependsOn: [finalManifest.id, contracts.id] });
3532
+ finalCaseReview.runIf = reviseCases.runIf;
3533
+ const caseGate = buildReviewBackendCasesGateNode(sources);
3534
+ caseGate.depends_on = [reviewCases.id, finalCaseReview.id];
3535
+ caseGate.shell.verdictGate = { fromNodeId: finalCaseReview.id, fallbackFromNodeIds: [reviewCases.id], accept: ["VERDICT: pass"], label: "backend case effective review", lineMode: "first-verdict-line" };
3536
+ const generatePytest = buildGenerateBackendPytestNode(sources);
3537
+ generatePytest.depends_on = [caseGate.id, contracts.id];
3538
+ const semanticReview = buildBackendTestSemanticReviewNode(sources, { dependsOn: [generatePytest.id, contracts.id, manifest.id, finalManifest.id] });
3539
+ const semanticInitial = { id: "validate-semantic-review-and-traceability-shell", depends_on: [semanticReview.id, manifest.id, finalManifest.id], dependsPolicy: "all-or-condition-skip", role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources), outputContract: "Materialize initial semantic review and validate pytest traceability.", subtask_prompt: "Validate initial semantic review and traceability fail-closed.", shell: { commands: [], backendTestPipeline: "semantic-initial", cwd: ".", timeoutMs: 60000 } };
3540
+ const revisePytest = buildReviseGeneratedBackendPytestNode(sources);
3541
+ revisePytest.depends_on = [semanticInitial.id];
3542
+ revisePytest.runIf = "$.nodes['validate-semantic-review-and-traceability-shell'].json.verdict == 'request-revision'";
3543
+ const finalSemanticReview = buildBackendTestSemanticReviewNode(sources, { id: "review-generated-backend-pytest-final-pi", dependsOn: [revisePytest.id], final: true });
3544
+ finalSemanticReview.runIf = revisePytest.runIf;
3545
+ const semanticFinal = { id: "backend-test-semantic-final-gate-shell", depends_on: [semanticInitial.id, finalSemanticReview.id], dependsPolicy: "all-or-condition-skip", role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources), outputContract: "Pass-only effective semantic review gate with final traceability after revision.", subtask_prompt: "Accept initial semantic pass or validate the single final review and traceability.", shell: { commands: [['test -n "${HARNESS_DAG_RUN_DIR:-}" || exit 2', 'node -e \'const fs=require("fs"),path=require("path");const r=process.env.HARNESS_DAG_RUN_DIR;const f=path.join(r,"contracts","backend-test-semantic-review-final.json");const i=path.join(r,"contracts","backend-test-semantic-review.json");const v=JSON.parse(fs.readFileSync(fs.existsSync(f)?f:i,"utf8"));if(v.verdict!=="pass")throw new Error("backend pytest semantic review did not pass");\''].join("; ")], cwd: ".", timeoutMs: 60000 } };
3546
+ const finalSemanticMaterialize = { id: "materialize-final-semantic-review-shell", depends_on: [finalSemanticReview.id], role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources), runIf: revisePytest.runIf, outputContract: "Materialize final semantic review and re-check traceability.", subtask_prompt: "Validate final semantic review and traceability.", shell: { commands: [], backendTestPipeline: "semantic-final", cwd: ".", timeoutMs: 60000 } };
3547
+ semanticFinal.depends_on = [semanticInitial.id, finalSemanticMaterialize.id];
3548
+ const executeInitial = buildExecuteBackendPytestNode(sources, { id: "execute-and-parse-backend-pytest-initial-shell", dependsOn: [semanticFinal.id, contracts.id], reportStem: "backend-test-initial" });
3549
+ executeInitial.shell.backendTestPipeline = "execute-parse-initial";
3550
+ const classify = buildClassifyBackendTestResultNode(sources);
3551
+ classify.depends_on = [executeInitial.id];
3552
+ const classifyEligibility = { id: "materialize-classification-and-eligibility-shell", depends_on: [classify.id], role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources), outputContract: "Materialize Classification v1 and repair eligibility JSON.", subtask_prompt: "Validate classification and determine bounded repair eligibility.", shell: { commands: [], backendTestPipeline: "classification-eligibility", cwd: ".", timeoutMs: 60000 } };
3553
+ const repair = buildRepairBackendPytestNode(sources);
3554
+ repair.depends_on = [classifyEligibility.id];
3555
+ repair.runIf = "$.nodes['materialize-classification-and-eligibility-shell'].json.eligible == true";
3556
+ const repairVerify = { id: "validate-repair-safety-and-traceability-shell", depends_on: [repair.id], role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: ["testcase/**"], forbiddenPaths: commonForbiddenPaths(sources), runIf: repair.runIf, outputContract: "Validate repair safety and final traceability.", subtask_prompt: "Reject manufactured success and revalidate mapping.", shell: { commands: [], backendTestPipeline: "repair-safety-traceability", cwd: ".", timeoutMs: 60000 } };
3557
+ const finalize = buildExecuteBackendPytestNode(sources, { id: "finalize-effective-backend-test-result-shell", dependsOn: [classifyEligibility.id, repairVerify.id, contracts.id], reportStem: "backend-test-final" });
3558
+ finalize.dependsPolicy = "all-or-condition-skip";
3559
+ finalize.shell.backendTestPipeline = "finalize-effective-result";
3560
+ finalize.outputContract = "If repaired, execute/parse final pytest; always materialize contracts/backend-test-result.json from final or initial result.";
3561
+ const retrospect = buildTestRetrospectNode(sources);
3562
+ retrospect.depends_on = [finalize.id];
3563
+ const outcome = buildBackendTestOutcomeGateNode(sources);
3564
+ const tasks = [analyze, contracts, generateCases, manifest, reviewCases, reviseCases, finalManifest, finalCaseReview, caseGate, generatePytest, semanticReview, semanticInitial, revisePytest, finalSemanticReview, finalSemanticMaterialize, semanticFinal, executeInitial, classify, classifyEligibility, repair, repairVerify, finalize, retrospect, outcome];
3565
+ const spec = { version: 3, title: `Backend test DAG: ${taskConfig.title}`, runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT, outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE, objective: extractObjective(sources.requirementMarkdown, taskConfig.title), successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId), globalConstraints, defaults: { ...BACKEND_TEST_DEFAULTS, contextProfile: taskConfig.contextProfile }, skillsByRole: BACKEND_TEST_SKILLS_BY_ROLE, executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS, tasks };
3138
3566
  applyDefaultReadOnlyRetryPolicy(spec);
3139
3567
  parseDagSpec(spec);
3140
3568
  assertValidDagSpec(spec);
@@ -4356,22 +4784,45 @@ function buildReviewNode(sources) {
4356
4784
  ].join("\n\n"),
4357
4785
  };
4358
4786
  }
4787
+ function buildReviewVerdictRecoveryNode(sources) {
4788
+ return {
4789
+ id: "review-verdict-recovery-pi",
4790
+ depends_on: ["review-pi"],
4791
+ role: "reviewer",
4792
+ executor: "pi",
4793
+ complexity: "LOW",
4794
+ writePolicy: "read-only",
4795
+ allowedPaths: commonReadOnlyPaths(sources),
4796
+ forbiddenPaths: commonForbiddenPaths(sources),
4797
+ outputContract: "Plain Markdown whose first non-empty line is exactly VERDICT: pass or VERDICT: request-revision, followed by the original review findings without substantive changes. No file writes.",
4798
+ subtask_prompt: [
4799
+ "Normalize the output format of review-pi; this is the single read-only format-recovery attempt for the review verdict protocol.",
4800
+ "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
4801
+ "If review-pi already contains a valid VERDICT line, preserve that verdict exactly and keep the original findings.",
4802
+ "If it omitted or malformed the VERDICT line but states an unambiguous request-revision conclusion, emit VERDICT: request-revision and preserve the findings.",
4803
+ "Do not invent VERDICT: pass from natural-language phrases such as 通过、PASS、✅, or general approval prose.",
4804
+ "If the upstream conclusion is ambiguous or cannot be preserved safely, emit VERDICT: request-revision and report the format ambiguity.",
4805
+ "Do not re-review code, expand task allowedPaths, or edit files.",
4806
+ buildSourceContextBlock(sources),
4807
+ ].join("\n\n"),
4808
+ };
4809
+ }
4359
4810
  function buildReviewGateNode(sources) {
4360
4811
  return {
4361
4812
  id: "review-gate-shell",
4362
- depends_on: ["review-pi"],
4813
+ depends_on: ["review-verdict-recovery-pi"],
4363
4814
  role: "verifier",
4364
4815
  executor: "shell",
4365
4816
  complexity: "LOW",
4366
4817
  writePolicy: "read-only",
4367
4818
  allowedPaths: commonReadOnlyPaths(sources),
4368
4819
  forbiddenPaths: commonForbiddenPaths(sources),
4369
- outputContract: "Deterministic review verdict gate: exit 0 only when review-pi first non-empty assistant output line is pass.",
4370
- subtask_prompt: "Deterministic gate: block downstream closeout unless review-pi emitted VERDICT: pass.",
4820
+ outputContract: "Deterministic review verdict gate: exit 0 only when review-verdict-recovery-pi first VERDICT line is pass.",
4821
+ subtask_prompt: "Deterministic gate: block downstream closeout unless review-verdict-recovery-pi emitted VERDICT: pass.",
4371
4822
  shell: {
4372
4823
  commands: [],
4373
4824
  verdictGate: {
4374
- fromNodeId: "review-pi",
4825
+ fromNodeId: "review-verdict-recovery-pi",
4375
4826
  accept: ["VERDICT: pass"],
4376
4827
  label: "review",
4377
4828
  lineMode: "first-verdict-line",
@@ -4388,13 +4839,13 @@ function buildReviewGatedHybridDag(standard, sources) {
4388
4839
  objective: `${standard.objective ?? ""}\n\nRoute: review-gated DAG selected by workflowPolicy/governanceProfile.`.trim(),
4389
4840
  globalConstraints: [
4390
4841
  ...(standard.globalConstraints ?? []),
4391
- "Review-gated DAGs must block closeout unless review-pi emits first-line VERDICT: pass.",
4842
+ "Review-gated DAGs must block closeout unless review-verdict-recovery-pi emits first-line VERDICT: pass.",
4392
4843
  ],
4393
4844
  tasks: standard.tasks.map((task) => ({ ...task })),
4394
4845
  };
4395
4846
  const closeout = getTaskOrThrow(spec, "closeout-pi");
4396
4847
  replaceTask(spec, cloneTask(closeout, { depends_on: ["review-gate-shell"] }));
4397
- spec.tasks.splice(spec.tasks.length - 1, 0, buildReviewNode(sources), buildReviewGateNode(sources));
4848
+ spec.tasks.splice(spec.tasks.length - 1, 0, buildReviewNode(sources), buildReviewVerdictRecoveryNode(sources), buildReviewGateNode(sources));
4398
4849
  applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
4399
4850
  applyDefaultReadOnlyRetryPolicy(spec);
4400
4851
  parseDagSpec(spec);
@@ -4778,6 +5229,7 @@ function buildSupervisedHybridDag(standard, sources) {
4778
5229
  cloneTask(buildReviewNode(sources), {
4779
5230
  depends_on: reviewDependsOn,
4780
5231
  }),
5232
+ buildReviewVerdictRecoveryNode(sources),
4781
5233
  buildReviewGateNode(sources),
4782
5234
  buildDecisionNode(sources),
4783
5235
  cloneTask(closeout, { depends_on: ["decision-pi"] }),