@tea-agent/loop-agent 0.16.1 → 0.16.3

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 (33) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/executors/dag-pi-executor.js +4 -2
  3. package/dist/executors/pi-sdk-executor.js +66 -3
  4. package/dist/executors/shell-executor.js +239 -29
  5. package/dist/executors/shell-presets.js +12 -2
  6. package/dist/executors/shell-write-guard.js +20 -1
  7. package/dist/shared/git-progress.js +9 -2
  8. package/dist/worker/observability/read-model.js +56 -0
  9. package/dist/worker/observe/server.js +6 -3
  10. package/dist/workflows/dag/backend-test-analysis-contract.js +87 -30
  11. package/dist/workflows/dag/backend-test-case-manifest.js +71 -8
  12. package/dist/workflows/dag/backend-test-execution-contract.js +63 -11
  13. package/dist/workflows/dag/backend-test-repair-contract.js +94 -0
  14. package/dist/workflows/dag/backend-test-result-contract.js +6 -4
  15. package/dist/workflows/dag/backend-test-semantic-review-contract.js +36 -0
  16. package/dist/workflows/dag/dynamic-runtime/condition.js +1 -1
  17. package/dist/workflows/dag/dynamic-runtime/shared.js +42 -0
  18. package/dist/workflows/dag/failure-routing.js +1 -1
  19. package/dist/workflows/dag/frontend-implementation-contract.js +32 -16
  20. package/dist/workflows/dag/frontend-worktree-diff.js +127 -0
  21. package/dist/workflows/dag/init-hybrid.js +616 -120
  22. package/dist/workflows/dag/lifecycle.js +33 -2
  23. package/dist/workflows/dag/scheduler.js +87 -17
  24. package/dist/workflows/dag/types.js +31 -0
  25. package/dist/workflows/dag/validate.js +20 -14
  26. package/docs/templates/agent-dag.schema.json +25 -2
  27. package/docs/templates/backend-test-analysis.schema.json +9 -16
  28. package/docs/templates/backend-test-dag.json +493 -197
  29. package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -4
  30. package/docs/templates/backend-test-execution.schema.json +6 -1
  31. package/package.json +1 -1
  32. package/skills/frontend-review/SKILL.md +6 -2
  33. package/skills/loop-agent/references/hybrid-dag.md +4 -1
@@ -18,6 +18,7 @@ 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";
@@ -932,6 +933,22 @@ function buildDagSourceBinding(sources) {
932
933
  requirementIds: extractExplicitRequirementIds(sources.requirementMarkdown, sources.constraintMarkdown, ...(sources.referenceDocuments ?? []).map((reference) => reference.markdown)),
933
934
  };
934
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
+ }
935
952
  function buildSourceContextBlock(sources) {
936
953
  const requirementRef = toTaskRelativeSourcePath(sources, sources.requirementPath);
937
954
  const requirementExcerpt = excerptMarkdown(sources.requirementMarkdown, {
@@ -2311,9 +2328,33 @@ function buildFrontendHybridDagFromTask(sources) {
2311
2328
  timeoutMs: 120000,
2312
2329
  },
2313
2330
  },
2331
+ {
2332
+ id: "frontend-worktree-diff-shell",
2333
+ depends_on: [
2334
+ "frontend-verification-retrace-shell",
2335
+ "frontend-behavior-reverify-shell",
2336
+ "frontend-static-reverify-shell",
2337
+ "frontend-repair-pi",
2338
+ implementId,
2339
+ ],
2340
+ role: "verifier",
2341
+ executor: "shell",
2342
+ complexity: "LOW",
2343
+ writePolicy: "read-only",
2344
+ allowedPaths: readOnlyPaths,
2345
+ forbiddenPaths,
2346
+ outputContract: "Run-owned actual worktree diff_patch (artifacts/diff_patch.patch) plus contracts/frontend-worktree-diff.json inventory/hash for review. No product worktree writes.",
2347
+ subtask_prompt: "Capture the authoritative actual diff after implement/repair/reverify so frontend-review-pi can audit changed files without relying on failure-path patches or model summaries.",
2348
+ shell: {
2349
+ commands: ["frontend-worktree-diff-gate"],
2350
+ cwd: ".",
2351
+ timeoutMs: 120000,
2352
+ },
2353
+ },
2314
2354
  {
2315
2355
  id: "frontend-review-pi",
2316
2356
  depends_on: [
2357
+ "frontend-worktree-diff-shell",
2317
2358
  "frontend-verification-retrace-shell",
2318
2359
  "frontend-static-reverify-shell",
2319
2360
  "frontend-behavior-reverify-shell",
@@ -2342,7 +2383,7 @@ function buildFrontendHybridDagFromTask(sources) {
2342
2383
  "Review the frontend implementation and verification evidence.",
2343
2384
  "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
2344
2385
  "Any Critical or Important finding must force VERDICT: request-revision.",
2345
- "Read the validated frontend-implementation-contract, frontend-verification-trace evidence, static/behavior shell facts, and actual diff. Trace proves command/file/symbol binding only—not semantic correctness.",
2386
+ "Read the validated frontend-implementation-contract, frontend-verification-trace evidence, static/behavior shell facts, and the run-owned actual diff from frontend-worktree-diff-shell (contracts/frontend-worktree-diff.json + artifacts/diff_patch.patch). Do not claim actual diff is missing when those artifacts exist; do not invent a diff from the implementation summary alone. Trace proves command/file/symbol binding only—not semantic correctness.",
2346
2387
  "Flag .skip/.only, deleted or weakened tests, unauthorized config changes, Mock-only evidence claimed as real integration, and Browser/visual claims (always not-run in this workflow).",
2347
2388
  "Use the direct contract, original plan, revision/no-op result, and final design review to reconstruct the approved plan and design verdict; do not infer them from the implementation summary.",
2348
2389
  "Treat a commented-out real request, default-enabled Mock, production entrypoint importing test mocks, API/fixture contract drift, unauthorized Mock dependency/path, or missing behavior evidence for the selected strategy as at least Important. Mock strategies require Mock-backed evidence. not-needed requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case verify that the real request remains the default and the Real Integration Gap is preserved.",
@@ -2424,6 +2465,7 @@ function buildFrontendHybridDagFromTask(sources) {
2424
2465
  // Backend test DAG template
2425
2466
  // ---------------------------------------------------------------------------
2426
2467
  function buildAnalyzeInputsNode(sources) {
2468
+ const sourceBindingContract = buildBackendTestAnalysisSourceBindingContract(sources);
2427
2469
  return {
2428
2470
  id: "analyze-inputs-pi",
2429
2471
  depends_on: [],
@@ -2433,14 +2475,19 @@ function buildAnalyzeInputsNode(sources) {
2433
2475
  writePolicy: "read-only",
2434
2476
  allowedPaths: commonReadOnlyPaths(sources),
2435
2477
  forbiddenPaths: commonForbiddenPaths(sources),
2436
- outputContract: "Pure Backend Test Analysis v1 JSON object matching docs/templates/backend-test-analysis.schema.json. No Markdown prose and no file writes.",
2478
+ outputContract: "Pure Backend Test Analysis v2 JSON object matching docs/templates/backend-test-analysis.schema.json. No Markdown prose and no file writes.",
2437
2479
  subtask_prompt: [
2438
- "Read the task source materials and return exactly one JSON object matching Backend Test Analysis v1.",
2480
+ "Read the task source materials and return exactly one JSON object matching Backend Test Analysis v2.",
2439
2481
  "Do not wrap it in explanatory prose. A single fenced json block is tolerated, but pure JSON is preferred.",
2440
- "Copy taskId, requirementPath, requirementSha256, referencePaths, and requirementIds exactly from the DAG source binding shown below.",
2441
- "Preserve existing AC IDs. Do not invent endpoint methods, paths, fields, errors, boundaries, or business rules; record unknowns in evidenceGaps.",
2482
+ "Copy the sourceBinding object exactly from the JSON block below; do not infer, add, remove, or reclassify source paths.",
2483
+ "Only kind=reference sources belong in referencePaths; kind=constraint sources MUST NOT be included in referencePaths.",
2484
+ "## Exact Backend Test Analysis sourceBinding JSON",
2485
+ JSON.stringify(sourceBindingContract, null, 2),
2486
+ "For every endpoint, explicitly set responseBody.kind=array|object|scalar|empty|unknown and ordering=specified|unspecified|not-applicable. Add itemSchemaRef for arrays when documented.",
2487
+ "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.",
2488
+ "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.",
2442
2489
  "Use empty arrays for categories not documented. Never include credentials, tokens, private keys, or secret values.",
2443
- "Required top-level keys: schemaVersion, sourceBinding, acceptanceCriteria, endpoints, dataModels, businessRules, stateTransitions, boundaryConstraints, externalDependencies, risks, evidenceGaps.",
2490
+ "Required top-level keys: schemaVersion=2, sourceBinding, acceptanceCriteria, endpoints, dataModels, businessRules, stateTransitions, boundaryConstraints, externalDependencies, risks, evidenceGaps.",
2444
2491
  "Read-only: do not modify code, docs, artifacts, or repository files.",
2445
2492
  buildSourceContextBlock(sources),
2446
2493
  ].join("\n\n"),
@@ -2456,13 +2503,13 @@ function buildBackendTestAnalysisContractGateNode(sources) {
2456
2503
  writePolicy: "read-only",
2457
2504
  allowedPaths: commonReadOnlyPaths(sources),
2458
2505
  forbiddenPaths: commonForbiddenPaths(sources),
2459
- outputContract: "Validated run-owned Backend Test Analysis v1 artifact pointer, schema ID, and SHA-256.",
2506
+ outputContract: "Validated run-owned Backend Test Analysis v2 artifact pointer, schema ID, and SHA-256 (legacy v1 input is normalized to v2).",
2460
2507
  subtask_prompt: "Materialize and validate the backend-test analysis contract under the current DAG run.",
2461
2508
  shell: {
2462
2509
  commands: [],
2463
2510
  jsonArtifactGate: {
2464
2511
  fromNodeId: "analyze-inputs-pi",
2465
- schemaId: "backend-test-analysis-v1",
2512
+ schemaId: "backend-test-analysis-v2",
2466
2513
  artifactName: "backend-test-analysis.json",
2467
2514
  outputDir: "contracts",
2468
2515
  },
@@ -2489,11 +2536,12 @@ function buildBackendTestEnvironmentScoutNode(sources) {
2489
2536
  "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).",
2490
2537
  "Do NOT search the whole repo for secrets, .env values, tokens, private keys, or production credentials.",
2491
2538
  '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.',
2492
- '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.',
2539
+ '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.',
2493
2540
  "testRoot and workingDirectory must be repo-relative posix paths without .. or absolute form. Adapter default testRoot is testcase when evidence is incomplete.",
2494
2541
  "runner must not include secret values. report.format must be junit with a relativeHint under the run (e.g. reports/backend-test-junit.xml).",
2495
2542
  "requiredEnvNames lists env NAMES only. baseUrlEnvName is required only for external-running-service and must match ^[A-Z_][A-Z0-9_]*$.",
2496
- "Record incomplete discovery in evidenceGaps. Populate evidenceRefs with repo-relative paths actually read.",
2543
+ "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.",
2544
+ "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.",
2497
2545
  "Required top-level keys: schemaVersion, framework, runner, testRoot, workingDirectory, report, targetMode, existingFixtures, authenticationMode, requiredEnvNames, dataIsolation, evidenceGaps, evidenceRefs.",
2498
2546
  "Read-only: do not modify code, docs, artifacts, or repository files.",
2499
2547
  buildSourceContextBlock(sources),
@@ -2528,7 +2576,10 @@ function buildBackendTestExecutionContractGateNode(sources) {
2528
2576
  function buildGenerateBackendFunctionalCasesNode(sources) {
2529
2577
  return {
2530
2578
  id: "generate-backend-functional-cases-pi",
2531
- depends_on: ["backend-test-execution-contract-shell"],
2579
+ depends_on: [
2580
+ "backend-test-analysis-contract-shell",
2581
+ "backend-test-execution-contract-shell",
2582
+ ],
2532
2583
  role: "implementer",
2533
2584
  executor: "pi",
2534
2585
  toolProfile: "write",
@@ -2540,19 +2591,31 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
2540
2591
  // 注意:Pi 节点超时由 executor 层控制(默认 30 分钟)
2541
2592
  // 如需调整,在 harness.json 的 executors.pi 中配置 modelConfig.timeoutMs
2542
2593
  subtask_prompt: [
2543
- "Read the validated structured artifact pointer from backend-test-analysis-contract-shell and generate cases only from that JSON contract.",
2544
- ,
2594
+ "Read both validated run-owned contracts before generating functional cases:",
2595
+ "- contracts/backend-test-analysis.json: authoritative requirements, AC IDs, endpoints, fields, rules, boundaries, risks, and evidence gaps.",
2596
+ "- contracts/backend-test-execution.json: pytest target mode, base URL env name, readiness, fixtures, and data-isolation constraints.",
2597
+ "Generate cases from the analysis contract; use the execution contract only to keep preconditions and automation feasibility realistic.",
2598
+ "Do not proceed from the execution contract alone. Do not re-read source documents or fall back to free-form analysis.",
2545
2599
  "",
2546
2600
  "## Output Steps (do in order):",
2547
2601
  "1. First, output a brief summary: how many modules, how many cases planned per module",
2548
2602
  "2. Then write each test case file under testcase/md/",
2549
2603
  "",
2550
2604
  "## Format Rules:",
2551
- "- Each test case ID: BE-<MODULE>-<NNN> (e.g. BE-ORDER-001)",
2605
+ "- Each test case ID: BE-<MODULE>-<NNN> (e.g. BE-ORDER-001) — always write the FULL id; never abbreviate as 002, 003 in matrices",
2552
2606
  "- Each file covers one module",
2553
- "- Case structure: ID, Title, Precondition, Steps, Expected Result",
2607
+ "- Case structure: ID, Title, Acceptance Criteria, Business Rules, Precondition, Steps, Expected Result",
2608
+ "- Every emitted case MUST declare at least one semantically applicable explicit AC-* under Acceptance Criteria; list BR-* separately under Business Rules",
2609
+ "- 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",
2610
+ "- Never relabel a negative/boundary/BR-only behavior as AC-002 or another unrelated AC merely to make acIds non-empty",
2554
2611
  "- Map each case to acceptance criteria (AC-xxx)",
2555
2612
  "",
2613
+ "## AC ↔ case consistency (CRITICAL — prevents review request-revision):",
2614
+ "- Every AC-xxx listed on a case body MUST appear only on cases that truly exercise that AC",
2615
+ "- 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",
2616
+ "- Prefer one primary BE-* case for suite-level ACs (e.g. AC-008 pytest exit 0) rather than tagging every case",
2617
+ "- Out-of-scope ACs (Flyway, frontend e2e, mvn test, etc.) must NOT be claimed in MD; leave them for manifest evidenceGaps",
2618
+ "",
2556
2619
  "## Coverage Requirements:",
2557
2620
  "- Positive paths: happy path for each acceptance criterion",
2558
2621
  "- Negative paths: error scenarios (invalid input, not found, state violations)",
@@ -2567,16 +2630,28 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
2567
2630
  "",
2568
2631
  "## Constraints:",
2569
2632
  "- Stay within writeSet: testcase/md/**",
2570
- "- Do NOT re-read source documents or fall back to free-form analysis use the validated structured artifact only",
2571
- ,
2633
+ "- Do NOT re-read source documents or fall back to free-form analysis; use the two validated run-owned contracts only",
2572
2634
  "- Do not write root artifacts/**",
2573
2635
  ].join("\n\n"),
2574
2636
  };
2575
2637
  }
2576
- function buildEmitBackendCaseManifestNode(sources) {
2638
+ const BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS = [
2639
+ "The final fenced JSON block is authoritative and MUST conform exactly to Backend Test Case Manifest v1.",
2640
+ "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.",
2641
+ "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.",
2642
+ "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.",
2643
+ "category MUST be exactly one of: positive, negative, boundary, state-transition, auth, timeout, concurrency, other.",
2644
+ "Before pytest generation, set automationStatus=planned. Use generated only with both file and symbol. Use skipped or unsupported only with gapReason.",
2645
+ "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.",
2646
+ "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.",
2647
+ "acIds MUST exactly match the explicit AC-* values in the written case body; do not infer ACs from Business Rules or summary matrices.",
2648
+ "Use full BE-<MODULE>-<NNN> caseId strings. Do not invent coverage percentages; omit coverageSummary unless all deterministic counts are exact.",
2649
+ "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\":[]}",
2650
+ ].join("\n\n");
2651
+ function buildEmitBackendCaseManifestNode(sources, options) {
2577
2652
  return {
2578
- id: "emit-backend-case-manifest-pi",
2579
- depends_on: [
2653
+ id: options?.id ?? "emit-backend-case-manifest-pi",
2654
+ depends_on: options?.dependsOn ?? [
2580
2655
  "generate-backend-functional-cases-pi",
2581
2656
  "backend-test-analysis-contract-shell",
2582
2657
  ],
@@ -2589,20 +2664,17 @@ function buildEmitBackendCaseManifestNode(sources) {
2589
2664
  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/**.",
2590
2665
  subtask_prompt: [
2591
2666
  "Emit Backend Test Case Manifest v1 as pure JSON (or one fenced json block with no trailing text).",
2667
+ BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS,
2592
2668
  "Read-only: use validated contracts/backend-test-analysis.json pointer + testcase/md/** only. Do not write repository files or .harness/**.",
2593
- "sourceBinding must match the analysis contract / DAG source binding exactly (taskId, requirementPath, requirementSha256, referencePaths, requirementIds).",
2594
- "For each functional case under testcase/md/: caseId BE-<MODULE>-<NNN>, acIds[], title, category, automationStatus.",
2595
- "After case generation (pre-pytest), default automationStatus=planned. Use skipped/unsupported only with gapReason. Use generated only when file+symbol already exist.",
2596
- "evidenceGaps: structured gaps for explicit AC-* that cannot be mapped to a case.",
2597
- "Do NOT invent coverage percentages. Optional coverageSummary must match deterministic counts (gate recomputes/validates).",
2598
2669
  "No secrets or credential-shaped fields.",
2599
2670
  ].join("\n\n"),
2600
2671
  };
2601
2672
  }
2602
- function buildBackendTestCaseManifestGateNode(sources) {
2673
+ function buildBackendTestCaseManifestGateNode(sources, options) {
2674
+ const fromNodeId = options?.fromNodeId ?? "emit-backend-case-manifest-pi";
2603
2675
  return {
2604
- id: "backend-test-case-manifest-shell",
2605
- depends_on: ["emit-backend-case-manifest-pi"],
2676
+ id: options?.id ?? "backend-test-case-manifest-shell",
2677
+ depends_on: options?.dependsOn ?? ["emit-backend-case-manifest-pi"],
2606
2678
  role: "verifier",
2607
2679
  executor: "shell",
2608
2680
  complexity: "LOW",
@@ -2614,7 +2686,7 @@ function buildBackendTestCaseManifestGateNode(sources) {
2614
2686
  shell: {
2615
2687
  commands: [],
2616
2688
  jsonArtifactGate: {
2617
- fromNodeId: "emit-backend-case-manifest-pi",
2689
+ fromNodeId,
2618
2690
  schemaId: "backend-test-case-manifest-v1",
2619
2691
  artifactName: "backend-test-case-manifest.json",
2620
2692
  outputDir: "contracts",
@@ -2624,13 +2696,18 @@ function buildBackendTestCaseManifestGateNode(sources) {
2624
2696
  },
2625
2697
  };
2626
2698
  }
2627
- function buildBackendTestTraceabilityGateNode(sources) {
2699
+ function buildBackendTestTraceabilityGateNode(sources, options = {}) {
2628
2700
  return {
2629
- id: "backend-test-traceability-gate-shell",
2630
- depends_on: [
2701
+ id: options.id ?? "backend-test-traceability-gate-shell",
2702
+ depends_on: options.dependsOn ?? [
2631
2703
  "generate-backend-pytest-pi",
2704
+ // Effective Case Manifest v1 path (exclusive condition branches):
2705
+ // pass → first manifest shell; request-revision → final manifest shell.
2706
+ // Artifact path is always contracts/backend-test-case-manifest.json.
2632
2707
  "backend-test-case-manifest-shell",
2708
+ "backend-test-case-manifest-final-shell",
2633
2709
  ],
2710
+ dependsPolicy: "all-or-condition-skip",
2634
2711
  role: "verifier",
2635
2712
  executor: "shell",
2636
2713
  complexity: "LOW",
@@ -2646,10 +2723,12 @@ function buildBackendTestTraceabilityGateNode(sources) {
2646
2723
  },
2647
2724
  };
2648
2725
  }
2649
- function buildReviewBackendCasesNode(sources) {
2726
+ function buildReviewBackendCasesNode(sources, options) {
2727
+ const phase = options?.phase ?? "first";
2728
+ const isFinal = phase === "final";
2650
2729
  return {
2651
- id: "review-backend-cases-pi",
2652
- depends_on: [
2730
+ id: options?.id ?? "review-backend-cases-pi",
2731
+ depends_on: options?.dependsOn ?? [
2653
2732
  "backend-test-case-manifest-shell",
2654
2733
  "backend-test-analysis-contract-shell",
2655
2734
  ],
@@ -2659,21 +2738,25 @@ function buildReviewBackendCasesNode(sources) {
2659
2738
  writePolicy: "read-only",
2660
2739
  allowedPaths: commonReadOnlyPaths(sources),
2661
2740
  forbiddenPaths: commonForbiddenPaths(sources),
2662
- outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision; followed by Findings and Coverage Assessment. No file writes.",
2741
+ outputContract: isFinal
2742
+ ? "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."
2743
+ : "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.",
2663
2744
  subtask_prompt: [
2664
- "Review the generated backend functional test cases under testcase/md/ and the validated Case Manifest v1.",
2745
+ isFinal
2746
+ ? "Final review of backend functional cases under testcase/md/ and the re-validated Case Manifest v1 after at most one revision pass."
2747
+ : "Review the generated backend functional test cases under testcase/md/ and the validated Case Manifest v1.",
2665
2748
  "",
2666
2749
  "## Mandatory First Line:",
2667
2750
  "First non-empty line must be exactly: VERDICT: pass or VERDICT: request-revision",
2668
2751
  "",
2669
2752
  "## Review Checklist:",
2670
- "- ID format: every case uses BE-<MODULE>-<NNN>",
2671
- "- Positive coverage: each acceptance criterion (AC-xxx) has happy-path case",
2753
+ "- ID format: every case uses BE-<MODULE>-<NNN> (full ids only in bodies and matrices)",
2754
+ "- Positive coverage: each in-scope acceptance criterion (AC-xxx) has happy-path case",
2672
2755
  "- Negative coverage: error scenarios (invalid input, not found, state violations)",
2673
- "- Traceability: each AC maps to at least one case ID (prefer contracts/backend-test-case-manifest.json coverageSummary)",
2756
+ "- Traceability: each explicit AC maps to a case ID or an evidenceGap in contracts/backend-test-case-manifest.json",
2674
2757
  "- Case structure: ID, Title, Precondition, Steps, Expected Result",
2675
2758
  "- No duplicate IDs across files",
2676
- "- Manifest consistency: MD cases align with manifest caseId/acIds; do not invent coverage %",
2759
+ "- 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",
2677
2760
  "",
2678
2761
  "## Conditional Coverage (check ONLY if mentioned in upstream analysis):",
2679
2762
  "- Boundary coverage: check ONLY if analyze-inputs-pi mentions value ranges, length limits, numeric bounds, or format constraints",
@@ -2683,40 +2766,123 @@ function buildReviewBackendCasesNode(sources) {
2683
2766
  "- Concurrency coverage: check ONLY if analyze-inputs-pi mentions concurrency/idempotency rules",
2684
2767
  "- If not mentioned, do NOT flag as missing",
2685
2768
  "",
2769
+ "## Do NOT treat as Critical alone:",
2770
+ "- Missing test_*.py / automation still planned (expected before generate-backend-pytest-pi)",
2771
+ "- Out-of-scope ACs already listed in manifest evidenceGaps (Flyway, frontend e2e, mvn test)",
2772
+ "",
2686
2773
  "## Verdict Rules:",
2687
2774
  "- All Critical checks pass + Important findings ≤ 2 → VERDICT: pass",
2688
2775
  "- Any Critical fails OR Important > 2 → VERDICT: request-revision",
2776
+ isFinal
2777
+ ? "- This is the FINAL review after one revision opportunity; remaining Critical issues must still request-revision (final gate will stop the DAG)."
2778
+ : "- When requesting revision, list numbered Required revisions concrete enough for revise-backend-cases-pi to edit testcase/md/**.",
2689
2779
  "",
2690
2780
  "## Output After Verdict:",
2691
- "1. Coverage Assessment table (AC → case IDs) using manifest + MD",
2781
+ "1. Coverage Assessment table (AC → full BE-* case IDs) using manifest + MD",
2692
2782
  "2. Findings list (Critical/Important/Informational)",
2693
2783
  "3. Statistics (total cases, positive/negative/boundary breakdown)",
2784
+ isFinal ? "" : "4. Required revisions (only when request-revision)",
2694
2785
  "",
2695
2786
  "## Constraints:",
2696
2787
  "- Read-only: do not modify files",
2697
2788
  "- Read validated analysis + case manifest artifacts; do not recompute coverage percentages",
2698
2789
  "- Use testcase/md/ files for case review",
2790
+ ]
2791
+ .filter((line) => line !== "")
2792
+ .join("\n\n"),
2793
+ };
2794
+ }
2795
+ function buildReviewBackendCasesBranchConditionNode(sources) {
2796
+ return {
2797
+ id: "review-backend-cases-branch-condition",
2798
+ depends_on: ["review-backend-cases-pi"],
2799
+ role: "verifier",
2800
+ executor: "static",
2801
+ complexity: "LOW",
2802
+ writePolicy: "none",
2803
+ allowedPaths: commonReadOnlyPaths(sources),
2804
+ forbiddenPaths: commonForbiddenPaths(sources),
2805
+ 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.",
2806
+ subtask_prompt: "Branch on review-backend-cases-pi VERDICT: pass → already-finished first review (revision chain skipped); request-revision → revise-backend-cases-pi.",
2807
+ static: {
2808
+ resultMarkdown: "Backend case review branch condition (direct pass vs single revision).",
2809
+ },
2810
+ dynamicCondition: {
2811
+ workflowNodeId: "review-backend-cases-branch-condition",
2812
+ cases: [
2813
+ {
2814
+ when: "$.nodes['review-backend-cases-pi'].firstVerdictLine == 'VERDICT: pass'",
2815
+ then: "review-backend-cases-pi",
2816
+ },
2817
+ {
2818
+ when: "$.nodes['review-backend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'",
2819
+ then: "revise-backend-cases-pi",
2820
+ },
2821
+ ],
2822
+ },
2823
+ };
2824
+ }
2825
+ function buildReviseBackendCasesNode(sources) {
2826
+ return {
2827
+ id: "revise-backend-cases-pi",
2828
+ depends_on: ["review-backend-cases-branch-condition"],
2829
+ role: "implementer",
2830
+ executor: "pi",
2831
+ toolProfile: "write",
2832
+ complexity: "MED",
2833
+ writePolicy: "exclusive",
2834
+ writeSet: ["testcase/md/**"],
2835
+ allowedPaths: ["testcase/md/**"],
2836
+ forbiddenPaths: commonForbiddenPaths(sources),
2837
+ outputContract: "Only scheduled when first review is VERDICT: request-revision. Apply Required revisions under testcase/md/** then summarize changes (single revision pass).",
2838
+ subtask_prompt: [
2839
+ "You are the single backend case revision pass (max one per DAG run).",
2840
+ "This node is only scheduled when review-backend-cases-pi emitted VERDICT: request-revision.",
2841
+ "",
2842
+ "## Inputs",
2843
+ "- First review: review-backend-cases-pi Markdown (VERDICT + Findings + Required revisions)",
2844
+ "- Current cases: testcase/md/**",
2845
+ "- Validated analysis + case manifest under the current run contracts/",
2846
+ "",
2847
+ "## Required work",
2848
+ "1. Edit only testcase/md/** to address every Critical finding and Required revision item",
2849
+ "2. Fix AC matrices to list FULL BE-* ids matching case bodies; remove false 'all cases' AC claims",
2850
+ "3. Keep BE-<MODULE>-<NNN> ids stable when possible; do not invent out-of-scope AC coverage",
2851
+ "4. Stay within writeSet; do not write pytest or production code",
2852
+ "5. End with a short summary of files touched",
2853
+ "",
2854
+ "Downstream will re-emit and re-validate Case Manifest v1, then run a FINAL review (pass-only gate).",
2699
2855
  ].join("\n\n"),
2700
2856
  };
2701
2857
  }
2702
2858
  function buildReviewBackendCasesGateNode(sources) {
2703
2859
  return {
2704
2860
  id: "review-backend-cases-gate-shell",
2705
- depends_on: ["review-backend-cases-pi"],
2861
+ // OR-join tips: pass-path barrier vs final review after one revise.
2862
+ // Soft condition-skip on the unused tip still allows the gate to run.
2863
+ depends_on: [
2864
+ "review-backend-cases-branch-condition",
2865
+ "review-backend-cases-final-pi",
2866
+ // Always finished; also required for verdictGate.fallbackFromNodeIds validate.
2867
+ "review-backend-cases-pi",
2868
+ ],
2869
+ dependsPolicy: "all-or-condition-skip",
2706
2870
  role: "verifier",
2707
2871
  executor: "shell",
2708
2872
  complexity: "LOW",
2709
2873
  writePolicy: "read-only",
2710
2874
  allowedPaths: commonReadOnlyPaths(sources),
2711
2875
  forbiddenPaths: commonForbiddenPaths(sources),
2712
- outputContract: "Deterministic backend case review gate: exit 0 only when review-backend-cases-pi emits VERDICT: pass.",
2713
- subtask_prompt: "Deterministic gate: block pytest generation unless backend case review emitted VERDICT: pass.",
2876
+ 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).",
2877
+ subtask_prompt: "Deterministic gate: block pytest generation unless the effective backend case review (final after revision, else first) emitted VERDICT: pass.",
2714
2878
  shell: {
2715
2879
  commands: [],
2716
2880
  verdictGate: {
2717
- fromNodeId: "review-backend-cases-pi",
2881
+ // Prefer final (revision path) when its artifact exists; fall back to first review.
2882
+ fromNodeId: "review-backend-cases-final-pi",
2883
+ fallbackFromNodeIds: ["review-backend-cases-pi"],
2718
2884
  accept: ["VERDICT: pass"],
2719
- label: "backend case review",
2885
+ label: "backend case effective review",
2720
2886
  lineMode: "first-verdict-line",
2721
2887
  },
2722
2888
  cwd: ".",
@@ -2845,7 +3011,149 @@ function buildGenerateBackendPytestNode(sources) {
2845
3011
  ].join("\n\n"),
2846
3012
  };
2847
3013
  }
2848
- function buildExecuteBackendPytestNode(sources) {
3014
+ function buildBackendTestSemanticReviewNode(sources, options = {}) {
3015
+ const final = options.final ?? false;
3016
+ return {
3017
+ id: options.id ?? "review-generated-backend-pytest-pi",
3018
+ depends_on: options.dependsOn ?? [
3019
+ "generate-backend-pytest-pi",
3020
+ "backend-test-analysis-contract-shell",
3021
+ "backend-test-case-manifest-shell",
3022
+ "backend-test-case-manifest-final-shell",
3023
+ ],
3024
+ dependsPolicy: "all-or-condition-skip",
3025
+ role: "reviewer",
3026
+ executor: "pi",
3027
+ complexity: "MED",
3028
+ writePolicy: "read-only",
3029
+ allowedPaths: ["testcase/**"],
3030
+ forbiddenPaths: commonForbiddenPaths(sources),
3031
+ outputContract: "Pure Backend Test Semantic Review v1 JSON: verdict, findings[], summary. No file writes.",
3032
+ subtask_prompt: [
3033
+ final ? "Final semantic review after the single generated-pytest revision." : "Review generated pytest semantics before the first execution.",
3034
+ "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.",
3035
+ "Return exactly one pure JSON object with only verdict, findings, summary; no Markdown fence or surrounding prose.",
3036
+ "verdict must be pass or request-revision. Each findings[] item must contain exactly severity, caseId, testFile, testSymbol, contractRefs, issue, requiredChange.",
3037
+ "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.",
3038
+ "Minimal shape: {\"verdict\":\"pass\",\"findings\":[],\"summary\":\"No contract-backed semantic contradiction found.\"}",
3039
+ "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.",
3040
+ "Do not use aliases such as file, symbol, refs, finding, or requiredFix; the strict contract requires testFile, testSymbol, contractRefs, issue, requiredChange.",
3041
+ "request-revision only for concrete semantic contradiction with reviewed cases/formal analysis evidence. No style findings.",
3042
+ "Read-only; do not edit tests or production code.",
3043
+ ].join("\n\n"),
3044
+ };
3045
+ }
3046
+ function buildBackendTestSemanticReviewGateNode(sources, options = {}) {
3047
+ const fromNodeId = options.fromNodeId ?? "review-generated-backend-pytest-pi";
3048
+ return {
3049
+ id: options.id ?? "backend-test-semantic-review-shell",
3050
+ depends_on: options.dependsOn ?? [fromNodeId],
3051
+ role: "verifier",
3052
+ executor: "shell",
3053
+ complexity: "LOW",
3054
+ writePolicy: "read-only",
3055
+ allowedPaths: commonReadOnlyPaths(sources),
3056
+ forbiddenPaths: commonForbiddenPaths(sources),
3057
+ outputContract: "Validated run-owned Backend Test Semantic Review v1 artifact.",
3058
+ subtask_prompt: "Validate semantic review JSON before branch selection.",
3059
+ shell: {
3060
+ commands: [],
3061
+ jsonArtifactGate: {
3062
+ fromNodeId,
3063
+ schemaId: "backend-test-semantic-review-v1",
3064
+ artifactName: options.id?.includes("final") ? "backend-test-semantic-review-final.json" : "backend-test-semantic-review.json",
3065
+ outputDir: "contracts",
3066
+ },
3067
+ cwd: ".",
3068
+ timeoutMs: 60000,
3069
+ },
3070
+ };
3071
+ }
3072
+ function buildBackendTestSemanticReviewConditionNode(sources) {
3073
+ return {
3074
+ id: "backend-test-semantic-review-condition",
3075
+ depends_on: ["backend-test-semantic-review-shell"],
3076
+ role: "verifier",
3077
+ executor: "static",
3078
+ complexity: "LOW",
3079
+ writePolicy: "none",
3080
+ allowedPaths: [],
3081
+ forbiddenPaths: commonForbiddenPaths(sources),
3082
+ 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.",
3083
+ subtask_prompt: "Branch deterministically from semantic review verdict.",
3084
+ static: { resultMarkdown: "Backend pytest semantic review selector." },
3085
+ dynamicCondition: {
3086
+ workflowNodeId: "backend-test-semantic-review-condition",
3087
+ cases: [
3088
+ { when: "$.nodes['backend-test-semantic-review-shell'].json.verdict == 'pass'", then: "backend-test-semantic-review-shell" },
3089
+ ],
3090
+ default: "revise-generated-backend-pytest-pi",
3091
+ },
3092
+ };
3093
+ }
3094
+ function buildReviseGeneratedBackendPytestNode(sources) {
3095
+ const writeSet = ["testcase/**/test_*.py", "testcase/**/helpers/**", "testcase/**/factories/**"];
3096
+ return {
3097
+ id: "revise-generated-backend-pytest-pi",
3098
+ depends_on: ["backend-test-semantic-review-condition"],
3099
+ role: "implementer",
3100
+ executor: "pi",
3101
+ toolProfile: "write",
3102
+ complexity: "MED",
3103
+ writePolicy: "exclusive",
3104
+ writeSet,
3105
+ allowedPaths: writeSet,
3106
+ forbiddenPaths: [...commonForbiddenPaths(sources), "apps/**", "src/**", "testcase/md/**", "**/conftest.py", "**/pytest.ini"],
3107
+ outputContract: "Apply one bounded semantic correction to generated pytest from validated findings only.",
3108
+ subtask_prompt: [
3109
+ "This is the only pre-execution generated-pytest semantic revision (1/1).",
3110
+ "Read contracts/backend-test-semantic-review.json and edit only the cited generated test files/helpers/factories.",
3111
+ "Preserve case IDs, symbols, test count, target mode, base URL and real-service path.",
3112
+ "Do not delete tests, add skip/xfail, swallow failures, substitute mocks, or weaken assertions beyond the formal comparison/shape contract.",
3113
+ "Do not modify product code, testcase/md/**, conftest.py, pytest.ini, or .harness/**.",
3114
+ ].join("\n\n"),
3115
+ };
3116
+ }
3117
+ function buildBackendTestSemanticFinalGateNode(sources) {
3118
+ return {
3119
+ id: "backend-test-semantic-final-gate-shell",
3120
+ depends_on: ["backend-test-semantic-review-condition", "backend-test-semantic-review-final-shell"],
3121
+ dependsPolicy: "all-or-condition-skip",
3122
+ role: "verifier",
3123
+ executor: "shell",
3124
+ complexity: "LOW",
3125
+ writePolicy: "read-only",
3126
+ allowedPaths: commonReadOnlyPaths(sources),
3127
+ forbiddenPaths: commonForbiddenPaths(sources),
3128
+ outputContract: "Pass-only semantic authorization for traceability and initial pytest.",
3129
+ subtask_prompt: "Accept initial pass path or final semantic review pass; fail closed otherwise.",
3130
+ shell: {
3131
+ commands: [[
3132
+ 'test -n "${HARNESS_DAG_RUN_DIR:-}" || exit 2',
3133
+ '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");\'',
3134
+ ].join("; ")],
3135
+ cwd: ".",
3136
+ timeoutMs: 60000,
3137
+ },
3138
+ };
3139
+ }
3140
+ function collectBackendTestShellEnvAllowlist(sources) {
3141
+ const names = new Set();
3142
+ for (const verify of sources.taskConfig.verifyCommands) {
3143
+ const assignmentPattern = /(?:^|[\s;&|])([A-Z_][A-Z0-9_]*)\s*=/g;
3144
+ for (const match of verify.command.matchAll(assignmentPattern)) {
3145
+ if (match[1])
3146
+ names.add(match[1]);
3147
+ }
3148
+ }
3149
+ return [...names].sort();
3150
+ }
3151
+ function taskAllowsBackendTestReportWrite(sources) {
3152
+ return sources.taskConfig.allowedPaths.some((pattern) => pattern === "docs/test-reports/**" ||
3153
+ pattern === "docs/**" ||
3154
+ pattern === "**");
3155
+ }
3156
+ function buildExecuteBackendPytestNode(sources, options = {}) {
2849
3157
  // Keep the target worktree read-only: JUnit is runner-owned evidence under
2850
3158
  // the current DAG run and moves with active → completed/paused lifecycle.
2851
3159
  // Adapter default testRoot is frozen at DAG generation time (auditable) and
@@ -2856,12 +3164,16 @@ function buildExecuteBackendPytestNode(sources) {
2856
3164
  });
2857
3165
  // Map pytest exit 0/1 → node success ONLY when JUnit exists (assertion-fail is a
2858
3166
  // legal result). Do not change global shell ok semantics. Persist raw exit for parse.
3167
+ const nodeId = options.id ?? "execute-backend-pytest-shell";
3168
+ const reportStem = options.reportStem ?? "backend-test";
3169
+ const reportName = `${reportStem}-junit.xml`;
3170
+ const exitName = `${reportStem}-pytest-exit.txt`;
2859
3171
  const pytestCommand = [
2860
3172
  preflightCommand,
2861
- 'REPORT="${HARNESS_DAG_RUN_DIR}/reports/backend-test-junit.xml"',
2862
- 'EXIT_FILE="${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
3173
+ `REPORT="\${HARNESS_DAG_RUN_DIR}/reports/${reportName}"`,
3174
+ `EXIT_FILE="\${HARNESS_DAG_RUN_DIR}/reports/${exitName}"`,
2863
3175
  'mkdir -p "$(dirname "${REPORT}")"',
2864
- `PYTHONDONTWRITEBYTECODE=1 python -m pytest ${frozenTestRoot}/ -v -p no:cacheprovider --junitxml="\${REPORT}"`,
3176
+ `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 python -m pytest ${frozenTestRoot}/ -v -p no:cacheprovider --junitxml="\${REPORT}"`,
2865
3177
  "STATUS=$?",
2866
3178
  'printf "%s" "${STATUS}" > "${EXIT_FILE}"',
2867
3179
  'printf "JUnit report: %s\\n" "${REPORT}"',
@@ -2870,9 +3182,9 @@ function buildExecuteBackendPytestNode(sources) {
2870
3182
  'exit "${STATUS}"',
2871
3183
  ].join("; ");
2872
3184
  return {
2873
- id: "execute-backend-pytest-shell",
2874
- depends_on: [
2875
- "backend-test-traceability-gate-shell",
3185
+ id: nodeId,
3186
+ depends_on: options.dependsOn ?? [
3187
+ "backend-test-semantic-final-gate-shell",
2876
3188
  "backend-test-execution-contract-shell",
2877
3189
  ],
2878
3190
  role: "verifier",
@@ -2885,6 +3197,7 @@ function buildExecuteBackendPytestNode(sources) {
2885
3197
  subtask_prompt: "Run pytest for the backend test suite; write JUnit + pytestExitCode evidence only under the current HARNESS_DAG_RUN_DIR/reports/.",
2886
3198
  shell: {
2887
3199
  commands: [pytestCommand],
3200
+ envAllowlist: collectBackendTestShellEnvAllowlist(sources),
2888
3201
  verifyEvidence: buildVerifyEvidence({
2889
3202
  phase: "final",
2890
3203
  quota: "full",
@@ -2897,10 +3210,13 @@ function buildExecuteBackendPytestNode(sources) {
2897
3210
  },
2898
3211
  };
2899
3212
  }
2900
- function buildParseBackendTestResultNode(sources) {
3213
+ function buildParseBackendTestResultNode(sources, options = {}) {
3214
+ const id = options.id ?? "parse-backend-test-result-shell";
3215
+ const fromNodeId = options.fromNodeId ?? "execute-backend-pytest-shell";
3216
+ const artifactName = options.artifactName ?? "backend-test-result.json";
2901
3217
  return {
2902
- id: "parse-backend-test-result-shell",
2903
- depends_on: ["execute-backend-pytest-shell"],
3218
+ id,
3219
+ depends_on: options.dependsOn ?? [fromNodeId],
2904
3220
  role: "verifier",
2905
3221
  executor: "shell",
2906
3222
  complexity: "LOW",
@@ -2912,10 +3228,13 @@ function buildParseBackendTestResultNode(sources) {
2912
3228
  shell: {
2913
3229
  commands: [],
2914
3230
  jsonArtifactGate: {
2915
- fromNodeId: "execute-backend-pytest-shell",
3231
+ fromNodeId,
2916
3232
  schemaId: "backend-test-result-v1",
2917
- artifactName: "backend-test-result.json",
3233
+ artifactName,
2918
3234
  outputDir: "contracts",
3235
+ ...(options.junitRelativePath
3236
+ ? { junitRelativePath: options.junitRelativePath }
3237
+ : {}),
2919
3238
  },
2920
3239
  cwd: ".",
2921
3240
  timeoutMs: 60000,
@@ -2925,7 +3244,7 @@ function buildParseBackendTestResultNode(sources) {
2925
3244
  function buildClassifyBackendTestResultNode(sources) {
2926
3245
  return {
2927
3246
  id: "classify-backend-test-result-pi",
2928
- depends_on: ["parse-backend-test-result-shell"],
3247
+ depends_on: ["parse-backend-test-result-initial-shell"],
2929
3248
  role: "reviewer",
2930
3249
  executor: "pi",
2931
3250
  complexity: "MED",
@@ -2936,7 +3255,9 @@ function buildClassifyBackendTestResultNode(sources) {
2936
3255
  subtask_prompt: [
2937
3256
  "Read-only classifier for Backend Test Result v1.",
2938
3257
  "Return exactly one JSON object (prefer pure JSON; single fenced json block tolerated; no trailing prose).",
2939
- "Read contracts/backend-test-result.json (run-owned Result v1). Do NOT invent pass rates from raw logs.",
3258
+ "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.",
3259
+ "Minimal shape: {\"category\":\"Unknown\",\"evidence\":[\"outcome=completed-with-failures\"],\"confidence\":0.5,\"notes\":\"Single-run evidence is insufficient for a stronger classification.\"}",
3260
+ "Read contracts/backend-test-result-initial.json (run-owned initial Result v1). Do NOT invent pass rates from raw logs.",
2940
3261
  "category must be one of: ProductBug, TestBug, EnvFailure, ContractMismatch, FlakyTest, Unknown.",
2941
3262
  "Hard constraints:",
2942
3263
  "- Single-run failure MUST NOT use FlakyTest (use Unknown, TestBug, or ProductBug).",
@@ -2948,24 +3269,187 @@ function buildClassifyBackendTestResultNode(sources) {
2948
3269
  ].join("\n\n"),
2949
3270
  };
2950
3271
  }
3272
+ function buildBackendTestClassificationGateNode(sources) {
3273
+ return {
3274
+ id: "backend-test-classification-shell",
3275
+ depends_on: ["classify-backend-test-result-pi"],
3276
+ role: "verifier",
3277
+ executor: "shell",
3278
+ complexity: "LOW",
3279
+ writePolicy: "read-only",
3280
+ allowedPaths: commonReadOnlyPaths(sources),
3281
+ forbiddenPaths: commonForbiddenPaths(sources),
3282
+ outputContract: "Validated run-owned Backend Test Classification v1 at contracts/backend-test-classification.json.",
3283
+ subtask_prompt: "Validate and materialize the read-only backend-test classification for deterministic repair routing.",
3284
+ shell: {
3285
+ commands: [],
3286
+ jsonArtifactGate: {
3287
+ fromNodeId: "classify-backend-test-result-pi",
3288
+ schemaId: "backend-test-classification-v1",
3289
+ artifactName: "backend-test-classification.json",
3290
+ outputDir: "contracts",
3291
+ },
3292
+ cwd: ".",
3293
+ timeoutMs: 60000,
3294
+ },
3295
+ };
3296
+ }
3297
+ function buildBackendTestRepairEligibilityNode(sources) {
3298
+ const command = buildBackendTestRepairEligibilityShellSnippet();
3299
+ return {
3300
+ id: "backend-test-repair-eligibility-shell",
3301
+ depends_on: ["backend-test-classification-shell"],
3302
+ role: "verifier",
3303
+ executor: "shell",
3304
+ complexity: "LOW",
3305
+ writePolicy: "read-only",
3306
+ allowedPaths: commonReadOnlyPaths(sources),
3307
+ forbiddenPaths: commonForbiddenPaths(sources),
3308
+ outputContract: "Pure JSON {schemaVersion,eligible,reason,revisionAttempt,category,confidence}; eligible only for completed TestBug assertion failures with error=0.",
3309
+ subtask_prompt: "Deterministically decide whether this run may use its single testcase-only repair attempt.",
3310
+ shell: { commands: [command], cwd: ".", timeoutMs: 60000 },
3311
+ };
3312
+ }
3313
+ function buildBackendTestRepairConditionNode(sources) {
3314
+ return {
3315
+ id: "backend-test-repair-condition",
3316
+ depends_on: ["backend-test-repair-eligibility-shell"],
3317
+ role: "verifier",
3318
+ executor: "static",
3319
+ complexity: "LOW",
3320
+ writePolicy: "none",
3321
+ allowedPaths: [],
3322
+ forbiddenPaths: commonForbiddenPaths(sources),
3323
+ 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.",
3324
+ subtask_prompt: "Route eligible TestBug to one repair attempt; otherwise select the already-finished eligibility evidence and skip repair.",
3325
+ static: { resultMarkdown: "Backend-test repair branch selector." },
3326
+ dynamicCondition: {
3327
+ workflowNodeId: "backend-test-repair-condition",
3328
+ cases: [
3329
+ {
3330
+ when: "$.nodes['backend-test-repair-eligibility-shell'].json.eligible == true",
3331
+ then: "repair-backend-pytest-pi",
3332
+ },
3333
+ ],
3334
+ default: "backend-test-repair-eligibility-shell",
3335
+ },
3336
+ };
3337
+ }
3338
+ function buildRepairBackendPytestNode(sources) {
3339
+ const writeSet = [
3340
+ "testcase/**/test_*.py",
3341
+ "testcase/**/helpers/**",
3342
+ "testcase/**/factories/**",
3343
+ ];
3344
+ return {
3345
+ id: "repair-backend-pytest-pi",
3346
+ depends_on: ["backend-test-repair-condition"],
3347
+ role: "implementer",
3348
+ executor: "pi",
3349
+ toolProfile: "write",
3350
+ complexity: "HIGH",
3351
+ writePolicy: "exclusive",
3352
+ writeSet,
3353
+ allowedPaths: writeSet,
3354
+ forbiddenPaths: [
3355
+ ...commonForbiddenPaths(sources),
3356
+ "apps/**",
3357
+ "src/**",
3358
+ ".env*",
3359
+ "**/migrations/**",
3360
+ ],
3361
+ outputContract: "Repair only existing generated pytest tests/helpers/factories for classified TestBug findings. No production/config/runtime evidence writes.",
3362
+ subtask_prompt: [
3363
+ "This is the only automatic TestBug repair attempt (1/1).",
3364
+ "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.",
3365
+ "Do not re-read the full task source tree: the run-owned analysis/manifest are the compact authoritative context.",
3366
+ "Repair only test implementation defects directly supported by reviewed Expected Results or formal API contract evidence.",
3367
+ "Never modify product code, migrations, service configuration, conftest.py, pytest.ini, pyproject.toml, setup.cfg, source requirements, testcase/md/**, or .harness/**.",
3368
+ "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.",
3369
+ "Keep the full suite runnable and preserve each BE-* case ID and pytest symbol mapping.",
3370
+ "Stay within writeSet: testcase/**/test_*.py, helpers/**, factories/**.",
3371
+ ].join("\n\n"),
3372
+ };
3373
+ }
3374
+ function buildBackendTestRepairSafetyGateNode(sources) {
3375
+ const command = buildBackendTestRepairSafetyShellSnippet();
3376
+ return {
3377
+ id: "backend-test-repair-safety-gate-shell",
3378
+ depends_on: ["repair-backend-pytest-pi"],
3379
+ role: "verifier",
3380
+ executor: "shell",
3381
+ complexity: "LOW",
3382
+ writePolicy: "read-only",
3383
+ allowedPaths: ["testcase/**"],
3384
+ forbiddenPaths: commonForbiddenPaths(sources),
3385
+ outputContract: "Fail-closed repair safety gate rejecting skip/xfail and broad failure swallowing before final traceability and pytest.",
3386
+ subtask_prompt: "Check that automatic TestBug repair did not manufacture success.",
3387
+ shell: { commands: [command], cwd: ".", timeoutMs: 60000 },
3388
+ };
3389
+ }
3390
+ function buildBackendTestEffectiveResultNode(sources) {
3391
+ const command = buildBackendTestEffectiveResultSelectorShellSnippet();
3392
+ // Inline retrospective context: relocated verbatim from the former
3393
+ // buildBackendTestRetrospectiveContextNode so test-retrospect-pi keeps a
3394
+ // single upstream. Runs after `command` which materializes
3395
+ // contracts/backend-test-result.json from the effective (final|initial) result.
3396
+ const retroCommand = [
3397
+ 'test -n "${HARNESS_DAG_RUN_DIR:-}" || { echo "missing HARNESS_DAG_RUN_DIR for backend-test retrospective context" >&2; exit 2; }',
3398
+ '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));\'',
3399
+ ].join("; ");
3400
+ return {
3401
+ id: "select-effective-backend-test-result-shell",
3402
+ depends_on: [
3403
+ "backend-test-repair-condition",
3404
+ "parse-backend-test-result-final-shell",
3405
+ "backend-test-case-manifest-shell",
3406
+ "backend-test-case-manifest-final-shell",
3407
+ "backend-test-classification-shell",
3408
+ ],
3409
+ dependsPolicy: "all-or-condition-skip",
3410
+ role: "verifier",
3411
+ executor: "shell",
3412
+ complexity: "LOW",
3413
+ writePolicy: "read-only",
3414
+ allowedPaths: commonReadOnlyPaths(sources),
3415
+ forbiddenPaths: commonForbiddenPaths(sources),
3416
+ 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.",
3417
+ subtask_prompt: "Select the effective backend-test result deterministically without changing initial/final evidence, then emit the complete retrospective context.",
3418
+ shell: { commands: [command, retroCommand], cwd: ".", timeoutMs: 60000 },
3419
+ };
3420
+ }
2951
3421
  function buildTestRetrospectNode(sources) {
3422
+ const canWriteReport = taskAllowsBackendTestReportWrite(sources);
2952
3423
  return {
2953
3424
  id: "test-retrospect-pi",
2954
- depends_on: ["classify-backend-test-result-pi"],
3425
+ depends_on: ["select-effective-backend-test-result-shell"],
2955
3426
  role: "closeout",
2956
3427
  executor: "pi",
2957
- toolProfile: "write",
2958
3428
  complexity: "MED",
2959
- writePolicy: "exclusive",
2960
- writeSet: ["docs/test-reports/**"],
2961
- allowedPaths: ["docs/test-reports/**"],
3429
+ ...(canWriteReport
3430
+ ? {
3431
+ toolProfile: "write",
3432
+ writePolicy: "exclusive",
3433
+ writeSet: ["docs/test-reports/**"],
3434
+ allowedPaths: ["docs/test-reports/**"],
3435
+ }
3436
+ : {
3437
+ writePolicy: "read-only",
3438
+ allowedPaths: commonReadOnlyPaths(sources),
3439
+ }),
2962
3440
  forbiddenPaths: commonForbiddenPaths(sources),
3441
+ outputContract: canWriteReport
3442
+ ? "Maturity rating in assistant output plus a report written under docs/test-reports/**."
3443
+ : "Read-only maturity rating and retrospective in assistant output; no repository file writes because task allowedPaths do not authorize docs/test-reports/**.",
2963
3444
  subtask_prompt: [
2964
- "Read upstream Result v1 + Case Manifest coverageSummary + classification and generate a test retrospective report.",
3445
+ "Read the complete JSON from direct upstream select-effective-backend-test-result-shell and generate a test retrospective report.",
3446
+ "That JSON contains result, manifest (including coverageSummary), and classification. Treat those fields as authoritative; do not rely on pointer/hash summaries.",
2965
3447
  "",
2966
3448
  "## Output Steps (do in order):",
2967
3449
  "1. First, output the maturity rating on the first line: Rating: A/B/C/D",
2968
- "2. Then write the full report under docs/test-reports/",
3450
+ canWriteReport
3451
+ ? "2. Then write the full report under docs/test-reports/"
3452
+ : "2. Keep the full retrospective in assistant output only; do not write repository files because docs/test-reports/** is outside task allowedPaths.",
2969
3453
  "",
2970
3454
  "## Stats authority (deterministic only):",
2971
3455
  "- Pass rate, failed/error/skipped counts, and failure list MUST come from contracts/backend-test-result.json only.",
@@ -2987,7 +3471,9 @@ function buildTestRetrospectNode(sources) {
2987
3471
  "- D: below C thresholds",
2988
3472
  "",
2989
3473
  "## Constraints:",
2990
- "- Stay within writeSet: docs/test-reports/**",
3474
+ canWriteReport
3475
+ ? "- Stay within writeSet: docs/test-reports/**"
3476
+ : "- Read-only: do not modify repository files",
2991
3477
  "- Do NOT re-read source documents — use upstream outputs only",
2992
3478
  "- Do not write root artifacts/**",
2993
3479
  ].join("\n\n"),
@@ -3034,63 +3520,73 @@ const BACKEND_TEST_SKILLS_BY_ROLE = {
3034
3520
  };
3035
3521
  function buildBackendTestHybridDag(sources) {
3036
3522
  const { taskConfig } = sources;
3037
- const sourceContext = buildSourceContextBlock(sources);
3038
- const readOnlyPaths = commonReadOnlyPaths(sources);
3039
- const forbiddenPaths = commonForbiddenPaths(sources);
3040
3523
  const globalConstraints = [
3041
3524
  ...taskConfig.hardConstraints,
3042
- ...(sources.constraintMarkdown
3043
- ? [`See 执行约束.md in task source (${sources.taskId})`]
3044
- : []),
3045
3525
  ...STANDARD_GLOBAL_CONSTRAINTS,
3046
- "backend-test-dag nodes must maintain traceability from requirements to functional cases to pytest automation.",
3526
+ "backend-test-dag uses exactly 24 real top-level tasks; bounded revision/repair branches are controlled by fail-closed runIf expressions.",
3527
+ "Analysis, execution, manifest, semantic review, initial/final/effective results, classification, eligibility, repair safety, traceability and outcome evidence remain run-owned and fail-closed.",
3047
3528
  "Functional test case IDs must use BE-<MODULE>-<NNN> format.",
3048
- "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).",
3049
- "pytest automation scripts must use test_ filename prefix for pytest discovery.",
3050
- "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.",
3051
- "review-backend-cases-gate-shell must block pytest generation unless the review verdict is exactly VERDICT: pass.",
3052
- "If a target test filename already exists under testcase/, add a numeric suffix (_01, _02, ...); never overwrite or append to existing files.",
3053
- "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.",
3054
- "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.",
3055
- "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.",
3056
- "backend-test-traceability-gate-shell verifies generated file/symbol existence after pytest generation and before execute; models must not invent coverage percentages.",
3529
+ "pytest writers may only change declared testcase assets; production code, config, skip/xfail, swallowed failures and mock substitution are forbidden.",
3057
3530
  ];
3058
- const spec = {
3059
- version: 3,
3060
- title: `Backend test DAG: ${taskConfig.title}`,
3061
- runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
3062
- outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
3063
- objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
3064
- successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
3065
- globalConstraints,
3066
- // No convergence loop: review gate is fail-closed. request-revision stops
3067
- // the DAG; regenerate after fixing cases. Controller still keys off
3068
- // hard-verify-shell, which this template does not include.
3069
- defaults: {
3070
- ...BACKEND_TEST_DEFAULTS,
3071
- contextProfile: taskConfig.contextProfile,
3072
- },
3073
- skillsByRole: BACKEND_TEST_SKILLS_BY_ROLE,
3074
- executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
3075
- tasks: [
3076
- buildAnalyzeInputsNode(sources),
3077
- buildBackendTestAnalysisContractGateNode(sources),
3078
- buildBackendTestEnvironmentScoutNode(sources),
3079
- buildBackendTestExecutionContractGateNode(sources),
3080
- buildGenerateBackendFunctionalCasesNode(sources),
3081
- buildEmitBackendCaseManifestNode(sources),
3082
- buildBackendTestCaseManifestGateNode(sources),
3083
- buildReviewBackendCasesNode(sources),
3084
- buildReviewBackendCasesGateNode(sources),
3085
- buildGenerateBackendPytestNode(sources),
3086
- buildBackendTestTraceabilityGateNode(sources),
3087
- buildExecuteBackendPytestNode(sources),
3088
- buildParseBackendTestResultNode(sources),
3089
- buildClassifyBackendTestResultNode(sources),
3090
- buildTestRetrospectNode(sources),
3091
- buildBackendTestOutcomeGateNode(sources),
3092
- ],
3531
+ const analyze = buildAnalyzeInputsNode(sources);
3532
+ analyze.id = "analyze-and-discover-backend-test-pi";
3533
+ analyze.outputContract = "Pure JSON envelope {analysis: Backend Test Analysis v2, execution: Backend Test Execution Contract v1}; no prose or writes.";
3534
+ 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.`;
3535
+ const contracts = {
3536
+ id: "validate-backend-test-contracts-shell", depends_on: [analyze.id], role: "verifier", executor: "shell", complexity: "LOW",
3537
+ writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources),
3538
+ outputContract: "Materialize and validate contracts/backend-test-analysis.json and contracts/backend-test-execution.json.",
3539
+ subtask_prompt: "Validate both backend-test intake contracts fail-closed.", shell: { commands: [], backendTestPipeline: "contracts", cwd: ".", timeoutMs: 60000 },
3093
3540
  };
3541
+ const generateCases = buildGenerateBackendFunctionalCasesNode(sources);
3542
+ generateCases.id = "generate-backend-cases-and-manifest-pi";
3543
+ generateCases.depends_on = [contracts.id];
3544
+ generateCases.outputContract = "Write testcase/md/** and end with one fenced json Backend Test Case Manifest v1 block matching the strict field contract.";
3545
+ 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}`;
3546
+ const manifest = buildBackendTestCaseManifestGateNode(sources, { dependsOn: [generateCases.id], fromNodeId: generateCases.id });
3547
+ const reviewCases = buildReviewBackendCasesNode(sources, { dependsOn: [manifest.id, contracts.id] });
3548
+ const reviseCases = buildReviseBackendCasesNode(sources);
3549
+ reviseCases.depends_on = [reviewCases.id];
3550
+ reviseCases.runIf = "$.nodes['review-backend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'";
3551
+ reviseCases.outputContract = "Apply one case revision and end with one fenced json final Case Manifest v1 block matching the strict field contract.";
3552
+ 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}`;
3553
+ const finalManifest = buildBackendTestCaseManifestGateNode(sources, { id: "backend-test-case-manifest-final-shell", dependsOn: [reviseCases.id], fromNodeId: reviseCases.id });
3554
+ finalManifest.runIf = reviseCases.runIf;
3555
+ const finalCaseReview = buildReviewBackendCasesNode(sources, { id: "review-backend-cases-final-pi", phase: "final", dependsOn: [finalManifest.id, contracts.id] });
3556
+ finalCaseReview.runIf = reviseCases.runIf;
3557
+ const caseGate = buildReviewBackendCasesGateNode(sources);
3558
+ caseGate.depends_on = [reviewCases.id, finalCaseReview.id];
3559
+ caseGate.shell.verdictGate = { fromNodeId: finalCaseReview.id, fallbackFromNodeIds: [reviewCases.id], accept: ["VERDICT: pass"], label: "backend case effective review", lineMode: "first-verdict-line" };
3560
+ const generatePytest = buildGenerateBackendPytestNode(sources);
3561
+ generatePytest.depends_on = [caseGate.id, contracts.id];
3562
+ const semanticReview = buildBackendTestSemanticReviewNode(sources, { dependsOn: [generatePytest.id, contracts.id, manifest.id, finalManifest.id] });
3563
+ 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 } };
3564
+ const revisePytest = buildReviseGeneratedBackendPytestNode(sources);
3565
+ revisePytest.depends_on = [semanticInitial.id];
3566
+ revisePytest.runIf = "$.nodes['validate-semantic-review-and-traceability-shell'].json.verdict == 'request-revision'";
3567
+ const finalSemanticReview = buildBackendTestSemanticReviewNode(sources, { id: "review-generated-backend-pytest-final-pi", dependsOn: [revisePytest.id], final: true });
3568
+ finalSemanticReview.runIf = revisePytest.runIf;
3569
+ 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 } };
3570
+ 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 } };
3571
+ semanticFinal.depends_on = [semanticInitial.id, finalSemanticMaterialize.id];
3572
+ const executeInitial = buildExecuteBackendPytestNode(sources, { id: "execute-and-parse-backend-pytest-initial-shell", dependsOn: [semanticFinal.id, contracts.id], reportStem: "backend-test-initial" });
3573
+ executeInitial.shell.backendTestPipeline = "execute-parse-initial";
3574
+ const classify = buildClassifyBackendTestResultNode(sources);
3575
+ classify.depends_on = [executeInitial.id];
3576
+ 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 } };
3577
+ const repair = buildRepairBackendPytestNode(sources);
3578
+ repair.depends_on = [classifyEligibility.id];
3579
+ repair.runIf = "$.nodes['materialize-classification-and-eligibility-shell'].json.eligible == true";
3580
+ 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 } };
3581
+ const finalize = buildExecuteBackendPytestNode(sources, { id: "finalize-effective-backend-test-result-shell", dependsOn: [classifyEligibility.id, repairVerify.id, contracts.id], reportStem: "backend-test-final" });
3582
+ finalize.dependsPolicy = "all-or-condition-skip";
3583
+ finalize.shell.backendTestPipeline = "finalize-effective-result";
3584
+ finalize.outputContract = "If repaired, execute/parse final pytest; always materialize contracts/backend-test-result.json from final or initial result.";
3585
+ const retrospect = buildTestRetrospectNode(sources);
3586
+ retrospect.depends_on = [finalize.id];
3587
+ const outcome = buildBackendTestOutcomeGateNode(sources);
3588
+ 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];
3589
+ 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 };
3094
3590
  applyDefaultReadOnlyRetryPolicy(spec);
3095
3591
  parseDagSpec(spec);
3096
3592
  assertValidDagSpec(spec);