@tea-agent/loop-agent 0.40.0-next.8 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +21 -20
  3. package/README.md +2 -2
  4. package/dist/application/dag/generate-task-dag.js +6 -2
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/cli/command-definitions.js +9 -5
  7. package/dist/cli/program.js +38 -3
  8. package/dist/commands/client-recovery.js +8 -36
  9. package/dist/commands/init-model-catalog.js +22 -0
  10. package/dist/commands/task-advance.js +23 -1
  11. package/dist/commands/task-source-prepare.js +5 -0
  12. package/dist/commands/task-status.js +1 -0
  13. package/dist/executors/dag-pi-executor.js +3 -0
  14. package/dist/executors/pi-executor.js +4 -2
  15. package/dist/executors/pi-sdk-executor.js +79 -6
  16. package/dist/executors/shell-executor.js +94 -20
  17. package/dist/governance/checks.js +1 -0
  18. package/dist/shared/operator/capabilities.js +9 -1
  19. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  20. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  21. package/dist/shared/pi-context-pressure/env.js +58 -0
  22. package/dist/shared/pi-context-pressure/extension.js +100 -0
  23. package/dist/shared/pi-context-pressure/index.js +7 -0
  24. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  25. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  26. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  27. package/dist/worker/console/chat/assistant-content.js +7 -0
  28. package/dist/worker/console/chat/chat-event-store.js +82 -11
  29. package/dist/worker/console/chat/goal-round-driver.js +72 -0
  30. package/dist/worker/console/chat/goals.js +300 -0
  31. package/dist/worker/console/chat/pi-runtime.js +285 -8
  32. package/dist/worker/console/chat/provider-error.js +2 -1
  33. package/dist/worker/console/chat/repo-browser.js +94 -12
  34. package/dist/worker/console/chat/routes.js +495 -30
  35. package/dist/worker/console/chat/session-catalog.js +48 -0
  36. package/dist/worker/console/chat/session-stats.js +96 -0
  37. package/dist/worker/console/chat/session-store.js +27 -18
  38. package/dist/worker/console/chat/shortcuts.js +7 -0
  39. package/dist/worker/console/chat/sift-bridge.js +1 -0
  40. package/dist/worker/console/chat/turn-process.js +42 -5
  41. package/dist/worker/console/dag-execution-receipt.js +20 -2
  42. package/dist/worker/console/operator-actions.js +20 -5
  43. package/dist/worker/console/operator-selection.js +6 -2
  44. package/dist/worker/console/pi-plugins.catalog.json +73658 -0
  45. package/dist/worker/console/pi-plugins.js +179 -0
  46. package/dist/worker/console/routes.js +246 -6
  47. package/dist/worker/console/server.js +173 -42
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BYBmDfPW.js → abnfDiagram-N423BO3Z-Dy71QJp3.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-BKMmxpGF.js → arc-M25fNMTE.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DJpwxr1d.js → architectureDiagram-T3A2C74G-CUWlVl1a.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BaPj_ZlY.js → blockDiagram-VBNYF7ZC-Dd0Whpx2.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8iGmHlm.js → c4Diagram-5PPSVZJV-BdfeCkI5.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-Ba0AKiEZ.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DaxXQayc.js → chunk-2GRJ4B5K-D_1sUkyD.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DqQKGA7q.js → chunk-2Q5K7J3B-Cxnxf36E.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CgMKpJBB.js → chunk-5RXB4S5H-D9RG3G_x.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BpToUlaW.js → chunk-5VM5RSS4-Zz9jgjLX.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BsEWOHxv.js → chunk-6Q2QTUOP-BCggDRCH.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CKGSCe71.js → chunk-GF5L2VYU-CFg-5iAS.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-Bm1U5299.js → chunk-JWPE2WC7-DRQTE8x_.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BI0ov870.js → chunk-KBJHAD2P-C_Dw7uZ8.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BmnQo0iJ.js → chunk-RYQCIY6F-BjIiApsk.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BJBqa6Xc.js → chunk-XXDRQBXY-B5XQD4KG.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BU3IjVEe.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BU3IjVEe.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BuN9-7AR.js → cose-bilkent-JH36ORCC-huHqH6WQ.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bw22jc2k.js → cynefin-VYW2F7L2-BSi6yZqP.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BHnSH8Dr.js → cynefinDiagram-MW4NZA55-Bs_Jp31y.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BoXXIegj.js → dagre-VZM6K2ZE-CYxavikT.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CU8qyPhT.js → diagram-7IWD3JNH-B6TErl71.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-lrjhF0S8.js → diagram-B4RE2ZJO-CvH4J2ii.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DL0QDEpo.js → diagram-LBJQPF4R-v1NBAqXS.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DxVMJMBH.js → diagram-Q27KOJAE-CUNHVAn2.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CFg9FzvG.js → diagram-UB23O5K3-Dzz1gUu-.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CQgP9fTG.js → ebnfDiagram-BXEA7PRR-CCF8_LjK.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-A5vQXx9U.js → erDiagram-JOGREHBK-DObdPVLa.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-ytXnLyXD.js → flowDiagram-UKHOOZJN-DlN8Pxrj.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CYuksfnS.js → ganttDiagram-PKOTCBZU-BJKIPTGX.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-f_jW3zgU.js → gitGraphDiagram-DS77QQ5N-XcSSnObi.js} +1 -1
  80. package/dist/worker/console/static/assets/index-CIYzS16i.css +1 -0
  81. package/dist/worker/console/static/assets/index-CS3QOTfl.js +451 -0
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C3HLE-xV.js → infoDiagram-6WML65LV-BKrRKMww.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DwpgejS6.js → ishikawaDiagram-WSZJBQD7-CN5dk-yb.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-ll0YWX1y.js → journeyDiagram-NVQOT4AX-B4P4KZ-Y.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Hq3rchvU.js → kanban-definition-27J2QSJJ-BRlteP32.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-BaGjJx8z.js → linear-CG8D9fuL.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-CKGQHWX_.js → mermaid.core-BuB6P2rX.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DM1FA6Dr.js → mindmap-definition-FAOFIHXS-DYXGl_DR.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Xa9fHkE2.js → pegDiagram-VL7TDLO6-BM48yEVG.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-pCd4PDqJ.js → pieDiagram-7S7Q4E2Y-mT8NL-Ow.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-KqjpEBLk.js → quadrantDiagram-CIZ2JOQS-C4pZ6DH_.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DgvR7LZD.js → railroadDiagram-AXF67PYL-BKAcr70m.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Bys1UCWA.js → requirementDiagram-LRYGKXZP-CRxcKpoO.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Ms9npnZt.js → sankeyDiagram-W5VNT64P-Ds4LxQDI.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DEIWrtQZ.js → sequenceDiagram-SI44F4Z6-CPLwPf_p.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DELJ0AhZ.js → sizeCapture-X5ZJPWSS-CL65uij0.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BpHt9wKh.js → stateDiagram-OKZ733FA-CO0Mgd1W.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BUewAVYS.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-GKRsBluT.js → swimlanes-SLNWSIFB-Cxv3TSNw.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bhe67Wjl.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cwsq1DxL.js → timeline-definition-Z64GVDOM-Dw85f9L2.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CfaqtKx0.js → vennDiagram-T6HMQDX7-B4QLCqCy.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BlEUtK_g.js → wardleyDiagram-T6FBY63Y-B_yKCWwn.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CHSRXPvO.js → xychartDiagram-ELKLHX3M-nniBEGv2.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
  107. package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
  108. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
  109. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  110. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +26 -19
  111. package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
  112. package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -1
  113. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
  114. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  115. package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
  116. package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
  117. package/dist/worker/console/static-src/shell/workspace-route.js +3 -1
  118. package/dist/worker/console/workspace-context.js +28 -3
  119. package/dist/worker/console/workspace-registry.js +333 -60
  120. package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
  121. package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
  122. package/dist/worker/observe/static/inspect-workspace.js +54 -10
  123. package/dist/worker/observe/static/operator-chrome.css +23 -4
  124. package/dist/worker/observe/static/operator-chrome.js +7 -2
  125. package/dist/worker/observe/static/styles.css +87 -25
  126. package/dist/worker/observe/static/views/dag-inspector.js +42 -1
  127. package/dist/worker/observe/static/views/dags.js +7 -5
  128. package/dist/worker/observe/static/views/session-timeline.js +135 -0
  129. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +162 -8
  130. package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
  131. package/dist/workflows/dag/backend-test-result-contract.js +5 -1
  132. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  133. package/dist/workflows/dag/backend-test-writer-completeness.js +18 -12
  134. package/dist/workflows/dag/init-hybrid.js +32 -9
  135. package/dist/workflows/dag/types.js +13 -0
  136. package/docs/architecture/runtime-boundaries.md +2 -1
  137. package/docs/templates/agent-dag.schema.json +18 -0
  138. package/docs/templates/backend-test-dag.json +27 -9
  139. package/docs/templates/init-managed-agents.md +1 -1
  140. package/package.json +3 -2
  141. package/skills/loop-agent/references/command-reference.md +15 -1
  142. package/dist/worker/console/static/assets/channel-CrzAa3FA.js +0 -1
  143. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-B7QwCCb4.js +0 -1
  144. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-B7QwCCb4.js +0 -1
  145. package/dist/worker/console/static/assets/index-BB6KxwT_.css +0 -1
  146. package/dist/worker/console/static/assets/index-D_wxbadT.js +0 -407
  147. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BEeSswlv.js +0 -1
  148. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B2m_wca8.js +0 -8
  149. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  150. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
@@ -20,7 +20,8 @@ const factsSchema = z
20
20
  tpId: z.string(),
21
21
  field: z.string().optional(),
22
22
  intent: z.string(),
23
- observed: z.string(),
23
+ observed: z.string().max(200),
24
+ observedLength: z.number().int().min(0).optional(),
24
25
  status: z.enum(["MATCH", "MISMATCH", "UNDETERMINED"]),
25
26
  repairability: z.enum([
26
27
  "repairable",
@@ -28,6 +29,12 @@ const factsSchema = z
28
29
  "none",
29
30
  "undetermined-no-repair",
30
31
  ]),
32
+ reasonCode: z.enum(["SCENARIO_PARAM_MATCH", "SCENARIO_PARAM_MISMATCH", "SCENARIO_PARAM_UNDETERMINED"]),
33
+ expectedRaw: z.string().max(512).optional(),
34
+ observedRaw: z.string().max(512),
35
+ expectedNormalized: z.string().max(512).optional(),
36
+ observedNormalized: z.string().max(512).optional(),
37
+ sourceLocation: z.string().max(512).optional(),
31
38
  suggestedFix: z.string().optional(),
32
39
  scriptPath: z.string().optional(),
33
40
  bound: z.number().optional(),
@@ -435,8 +442,16 @@ function isWhitespacePaddedString(value) {
435
442
  }
436
443
  function normalizeScenarioLiteralText(value) {
437
444
  // Writers sometimes emit visible-space placeholders (U+2420), NBSP variants,
438
- // or URL-encoded spaces in machine-readable Markdown.
445
+ // URL-encoded spaces, or explicit Python/JSON whitespace escapes in
446
+ // machine-readable Markdown. Decode only escapes that resolve to whitespace;
447
+ // ordinary business characters such as `\\u0041` remain literal and fail closed.
448
+ const decodeWhitespaceEscape = (token, hex) => {
449
+ const decoded = String.fromCodePoint(Number.parseInt(hex, 16));
450
+ return /^\s$/u.test(decoded) || /[   ]/u.test(decoded) ? decoded : token;
451
+ };
439
452
  const normalized = value
453
+ .replace(/\\u([0-9a-f]{4})/gi, decodeWhitespaceEscape)
454
+ .replace(/\\x([0-9a-f]{2})/gi, decodeWhitespaceEscape)
440
455
  .replace(/␠/g, " ")
441
456
  .replace(/ /g, " ")
442
457
  .replace(/ /g, " ")
@@ -677,6 +692,50 @@ export function extractPytestParamBlock(source, tpId) {
677
692
  }
678
693
  return undefined;
679
694
  }
695
+ function extractPytestParamTopLevelArgs(block) {
696
+ if (!block)
697
+ return [];
698
+ const open = block.indexOf("(");
699
+ const close = block.lastIndexOf(")");
700
+ if (open < 0 || close <= open)
701
+ return [];
702
+ const input = block.slice(open + 1, close);
703
+ const args = [];
704
+ let start = 0;
705
+ let depth = 0;
706
+ let quote = "";
707
+ let escaped = false;
708
+ for (let index = 0; index < input.length; index += 1) {
709
+ const ch = input[index];
710
+ if (escaped) {
711
+ escaped = false;
712
+ continue;
713
+ }
714
+ if (ch === "\\") {
715
+ escaped = true;
716
+ continue;
717
+ }
718
+ if (quote) {
719
+ if (ch === quote)
720
+ quote = "";
721
+ continue;
722
+ }
723
+ if (ch === '"' || ch === "'") {
724
+ quote = ch;
725
+ continue;
726
+ }
727
+ if (ch === "[" || ch === "{" || ch === "(")
728
+ depth += 1;
729
+ else if (ch === "]" || ch === "}" || ch === ")")
730
+ depth -= 1;
731
+ else if (ch === "," && depth === 0) {
732
+ args.push(input.slice(start, index).trim());
733
+ start = index + 1;
734
+ }
735
+ }
736
+ args.push(input.slice(start).trim());
737
+ return args.filter((arg) => arg && !/^(?:id|marks)\s*=/.test(arg));
738
+ }
680
739
  function extractPytestParamParameterNames(source, tpId) {
681
740
  const block = extractPytestParamBlock(source, tpId);
682
741
  if (!block)
@@ -688,8 +747,11 @@ function extractPytestParamParameterNames(source, tpId) {
688
747
  if (decoratorIndex < 0)
689
748
  return [];
690
749
  const prefix = source.slice(decoratorIndex, blockIndex);
691
- const match = /@pytest\.mark\.parametrize\s*\(\s*["']([^"']+)["']/s.exec(prefix);
692
- return match?.[1]?.split(",").map((name) => name.trim()).filter(Boolean) ?? [];
750
+ const scalar = /@pytest\.mark\.parametrize\s*\(\s*["']([^"']+)["']/s.exec(prefix)?.[1];
751
+ if (scalar)
752
+ return scalar.split(",").map((name) => name.trim()).filter(Boolean);
753
+ const tuple = /@pytest\.mark\.parametrize\s*\(\s*\(\s*((?:["'][^"']+["']\s*,?\s*)+)\)/s.exec(prefix)?.[1];
754
+ return tuple ? [...tuple.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]).filter(Boolean) : [];
693
755
  }
694
756
  export function observeParamFeatures(block, field, constants, parameterNames) {
695
757
  if (!block)
@@ -749,48 +811,7 @@ export function observeParamFeatures(block, field, constants, parameterNames) {
749
811
  }
750
812
  // Parse top-level positional args before the legacy token scanner so list
751
813
  // cardinality and field/value rows remain statically visible.
752
- const topLevelArgs = (() => {
753
- const open = block.indexOf("(");
754
- const close = block.lastIndexOf(")");
755
- if (open < 0 || close <= open)
756
- return [];
757
- const input = block.slice(open + 1, close);
758
- const args = [];
759
- let start = 0;
760
- let depth = 0;
761
- let quote = "";
762
- let escaped = false;
763
- for (let i = 0; i < input.length; i += 1) {
764
- const ch = input[i];
765
- if (escaped) {
766
- escaped = false;
767
- continue;
768
- }
769
- if (ch === "\\") {
770
- escaped = true;
771
- continue;
772
- }
773
- if (quote) {
774
- if (ch === quote)
775
- quote = "";
776
- continue;
777
- }
778
- if (ch === '"' || ch === "'") {
779
- quote = ch;
780
- continue;
781
- }
782
- if (ch === "[" || ch === "{" || ch === "(")
783
- depth += 1;
784
- else if (ch === "]" || ch === "}" || ch === ")")
785
- depth -= 1;
786
- else if (ch === "," && depth === 0) {
787
- args.push(input.slice(start, i).trim());
788
- start = i + 1;
789
- }
790
- }
791
- args.push(input.slice(start).trim());
792
- return args.filter((arg) => arg && !/^(?:id|marks)\s*=/.test(arg));
793
- })();
814
+ const topLevelArgs = extractPytestParamTopLevelArgs(block);
794
815
  if (topLevelArgs.length > 0) {
795
816
  const idTp = /\bid\s*=\s*["'](TP-[A-Z0-9-]+)["']/i.exec(block)?.[1]?.toUpperCase();
796
817
  const meaningfulArgs = topLevelArgs.filter((arg) => {
@@ -1161,11 +1182,13 @@ function suggestedFixFor(intent, field, bound, example) {
1161
1182
  : undefined;
1162
1183
  default:
1163
1184
  if (intent.startsWith("custom-literal:")) {
1164
- const expected = intent.slice("custom-literal:".length);
1165
- if (isWhitespaceSemanticLiteral(expected)) {
1166
- return field ? `set ${field}=" "` : 'set value=" "';
1185
+ const expectedRaw = intent.slice("custom-literal:".length);
1186
+ const expected = normalizeCustomLiteralExpected(expectedRaw);
1187
+ if (isWhitespaceSemanticLiteral(expectedRaw) || isWhitespaceOnlyString(expected)) {
1188
+ const length = isWhitespaceOnlyString(expected) ? expected.length : 3;
1189
+ return field ? `set ${field} to ${length} literal spaces` : `set value to ${length} literal spaces`;
1167
1190
  }
1168
- if (isWhitespacePaddedSemanticLiteral(expected)) {
1191
+ if (isWhitespacePaddedSemanticLiteral(expectedRaw)) {
1169
1192
  return field
1170
1193
  ? `set ${field}=" ${field}-value "`
1171
1194
  : 'set value=" value "';
@@ -1177,6 +1200,200 @@ function suggestedFixFor(intent, field, bound, example) {
1177
1200
  return undefined;
1178
1201
  }
1179
1202
  }
1203
+ const MAX_PERSISTED_SCENARIO_TEXT = 192;
1204
+ function boundedScenarioText(value, logicalLength) {
1205
+ if (value.length <= MAX_PERSISTED_SCENARIO_TEXT)
1206
+ return value;
1207
+ const suffix = `<truncated length=${logicalLength ?? value.length}>`;
1208
+ return `${value.slice(0, Math.max(0, MAX_PERSISTED_SCENARIO_TEXT - suffix.length - 3))}...${suffix}`;
1209
+ }
1210
+ function localPythonFunctionRegion(source, functionName) {
1211
+ const escaped = functionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1212
+ const startMatch = new RegExp(`^def\\s+${escaped}\\s*\\(`, "m").exec(source);
1213
+ if (!startMatch || startMatch.index === undefined)
1214
+ return undefined;
1215
+ const start = startMatch.index;
1216
+ const remainder = source.slice(start + startMatch[0].length);
1217
+ const nextTopLevel = /^(?:def|class|@pytest\b|@(?:[A-Za-z_][A-Za-z0-9_.]*))\s*/m.exec(remainder);
1218
+ return source.slice(start, nextTopLevel?.index === undefined ? source.length : start + startMatch[0].length + nextTopLevel.index);
1219
+ }
1220
+ function splitPythonTopLevelArguments(input) {
1221
+ const args = [];
1222
+ let start = 0;
1223
+ let depth = 0;
1224
+ let quote = "";
1225
+ let escaped = false;
1226
+ for (let index = 0; index < input.length; index += 1) {
1227
+ const ch = input[index];
1228
+ if (escaped) {
1229
+ escaped = false;
1230
+ continue;
1231
+ }
1232
+ if (ch === "\\") {
1233
+ escaped = true;
1234
+ continue;
1235
+ }
1236
+ if (quote) {
1237
+ if (ch === quote)
1238
+ quote = "";
1239
+ continue;
1240
+ }
1241
+ if (ch === '"' || ch === "'") {
1242
+ quote = ch;
1243
+ continue;
1244
+ }
1245
+ if (ch === "[" || ch === "{" || ch === "(")
1246
+ depth += 1;
1247
+ else if (ch === "]" || ch === "}" || ch === ")")
1248
+ depth -= 1;
1249
+ else if (ch === "," && depth === 0) {
1250
+ args.push(input.slice(start, index).trim());
1251
+ start = index + 1;
1252
+ }
1253
+ }
1254
+ args.push(input.slice(start).trim());
1255
+ return args.filter(Boolean);
1256
+ }
1257
+ function parseSimplePythonCall(expression) {
1258
+ const call = /^([A-Za-z_][A-Za-z0-9_]*)\s*\(([\s\S]*)\)$/.exec(expression.trim());
1259
+ if (!call)
1260
+ return undefined;
1261
+ const positional = [];
1262
+ const keywords = new Map();
1263
+ for (const argument of splitPythonTopLevelArguments(call[2] ?? "")) {
1264
+ const keyword = /^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([\s\S]+)$/.exec(argument);
1265
+ if (keyword) {
1266
+ if (keywords.has(keyword[1]))
1267
+ return undefined;
1268
+ keywords.set(keyword[1], keyword[2].trim());
1269
+ }
1270
+ else {
1271
+ if (keywords.size > 0 || /^\*{1,2}/.test(argument))
1272
+ return undefined;
1273
+ positional.push(argument);
1274
+ }
1275
+ }
1276
+ return { name: call[1], positional, keywords };
1277
+ }
1278
+ function safeSameModuleDictHelper(source, helperName) {
1279
+ if (!helperName.startsWith("_"))
1280
+ return undefined;
1281
+ const region = localPythonFunctionRegion(source, helperName);
1282
+ if (!region)
1283
+ return undefined;
1284
+ const functionBody = region.slice(region.indexOf(":") + 1);
1285
+ const escapedName = helperName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1286
+ if (new RegExp(`\\b${escapedName}\\s*\\(`).test(functionBody))
1287
+ return undefined;
1288
+ if (/\b(?:Faker|fake|random|randint|uuid4|environ|getenv|requests|httpx|urlopen|subprocess|Popen|socket|open)\b/i.test(region))
1289
+ return undefined;
1290
+ const signature = new RegExp(`^def\\s+${escapedName}\\s*\\(([\\s\\S]*?)\\)\\s*(?:->[^:]+)?\\s*:`, "m").exec(region);
1291
+ if (!signature)
1292
+ return undefined;
1293
+ const parameters = [];
1294
+ for (const rawParameter of splitPythonTopLevelArguments(signature[1] ?? "")) {
1295
+ if (rawParameter === "*" || rawParameter === "/")
1296
+ continue;
1297
+ if (/^\*{1,2}/.test(rawParameter))
1298
+ return undefined;
1299
+ const parameter = /^([A-Za-z_][A-Za-z0-9_]*)(?:\s*:[^=]+)?(?:\s*=\s*([\s\S]+))?$/.exec(rawParameter);
1300
+ if (!parameter)
1301
+ return undefined;
1302
+ parameters.push({ name: parameter[1], defaultExpression: parameter[2]?.trim() });
1303
+ }
1304
+ const directReturn = /\breturn\s*(\{[\s\S]*?\})/.exec(functionBody)?.[1];
1305
+ const assignment = /\b([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=\n]+)?=\s*(\{[\s\S]*?\})/.exec(functionBody);
1306
+ const assignedReturn = assignment && new RegExp(`\\breturn\\s+${assignment[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(functionBody)
1307
+ ? assignment[2]
1308
+ : undefined;
1309
+ const dictExpression = directReturn ?? assignedReturn;
1310
+ if (!dictExpression)
1311
+ return undefined;
1312
+ const fieldBindings = new Map();
1313
+ for (const item of splitPythonTopLevelArguments(dictExpression.slice(1, -1))) {
1314
+ const binding = /^(?:["']([^"']+)["'])\s*:\s*([A-Za-z_][A-Za-z0-9_]*)$/.exec(item);
1315
+ if (!binding || !parameters.some((parameter) => parameter.name === binding[2]))
1316
+ return undefined;
1317
+ fieldBindings.set(binding[1], binding[2]);
1318
+ }
1319
+ return { parameters, fieldBindings };
1320
+ }
1321
+ function observeSameModulePayloadHelperField(input) {
1322
+ const payloadIndex = input.parameterNames.findIndex((name) => /^(?:payload|body|data|json)$/i.test(name));
1323
+ if (payloadIndex < 0)
1324
+ return undefined;
1325
+ let expression = extractPytestParamTopLevelArgs(input.block)[payloadIndex]?.trim() ?? "";
1326
+ const wrapper = parseSimplePythonCall(expression);
1327
+ if (wrapper?.name === "_without") {
1328
+ if (wrapper.positional.length !== 2 || wrapper.keywords.size > 0)
1329
+ return undefined;
1330
+ const removed = observeLiteralToken(wrapper.positional[1], input.field, input.constants);
1331
+ if (removed?.kind !== "string" || removed.literal !== input.field)
1332
+ return undefined;
1333
+ expression = wrapper.positional[0];
1334
+ const inner = parseSimplePythonCall(expression);
1335
+ if (!inner || !safeSameModuleDictHelper(input.source, inner.name))
1336
+ return undefined;
1337
+ return { kind: "missing-key", text: `missing:${input.field}` };
1338
+ }
1339
+ const call = parseSimplePythonCall(expression);
1340
+ if (!call)
1341
+ return undefined;
1342
+ const helper = safeSameModuleDictHelper(input.source, call.name);
1343
+ if (!helper)
1344
+ return undefined;
1345
+ const parameterName = helper.fieldBindings.get(input.field);
1346
+ if (!parameterName)
1347
+ return { kind: "missing-key", text: `missing:${input.field}` };
1348
+ const parameterIndex = helper.parameters.findIndex((parameter) => parameter.name === parameterName);
1349
+ if (parameterIndex < 0 || call.positional.length > helper.parameters.length)
1350
+ return undefined;
1351
+ const expressionForField = call.keywords.get(parameterName) ?? call.positional[parameterIndex] ?? helper.parameters[parameterIndex]?.defaultExpression;
1352
+ return expressionForField
1353
+ ? observeLiteralToken(expressionForField, input.field, input.constants)
1354
+ : undefined;
1355
+ }
1356
+ function hasSafeSameModulePayloadHelper(source, block, parameterNames) {
1357
+ const payloadIndex = parameterNames.findIndex((name) => /^(?:payload|body|data|json)$/i.test(name));
1358
+ if (payloadIndex < 0)
1359
+ return false;
1360
+ const expression = extractPytestParamTopLevelArgs(block)[payloadIndex]?.trim() ?? "";
1361
+ const call = parseSimplePythonCall(expression);
1362
+ if (!call)
1363
+ return false;
1364
+ if (/\b(?:Faker|fake|random|randint|uuid4|environ|getenv|requests|httpx|urlopen|subprocess|Popen|socket|open)\b/i.test(expression))
1365
+ return false;
1366
+ return Boolean(safeSameModuleDictHelper(source, call.name));
1367
+ }
1368
+ function observeDeterministicFunctionFieldOverride(input) {
1369
+ const region = scenarioFunctionRegion(input.source, input.tpId);
1370
+ if (!region)
1371
+ return undefined;
1372
+ const escapedField = input.field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1373
+ const assignment = new RegExp(`^([ \\t]*)([A-Za-z_][A-Za-z0-9_]*)\\s*\\[\\s*["']${escapedField}["']\\s*\\]\\s*=\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*$`, "m").exec(region);
1374
+ if (!assignment)
1375
+ return undefined;
1376
+ const indentation = assignment[1]?.length ?? 0;
1377
+ const valueParameter = assignment[3];
1378
+ const valueIndex = input.parameterNames.indexOf(valueParameter);
1379
+ if (valueIndex < 0)
1380
+ return undefined;
1381
+ const beforeAssignment = region.slice(0, assignment.index);
1382
+ const guardMatches = [...beforeAssignment.matchAll(/^([ \\t]*)if\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*$/gm)];
1383
+ const nearestGuard = guardMatches.at(-1);
1384
+ if (nearestGuard && (nearestGuard[1]?.length ?? 0) < indentation) {
1385
+ const guardIndex = input.parameterNames.indexOf(nearestGuard[2]);
1386
+ if (guardIndex < 0)
1387
+ return undefined;
1388
+ const guardExpression = extractPytestParamTopLevelArgs(input.block)[guardIndex]?.trim();
1389
+ if (guardExpression !== "True")
1390
+ return undefined;
1391
+ }
1392
+ const valueExpression = extractPytestParamTopLevelArgs(input.block)[valueIndex]?.trim();
1393
+ return valueExpression
1394
+ ? observeLiteralToken(valueExpression, input.field, input.constants)
1395
+ : undefined;
1396
+ }
1180
1397
  function scenarioFunctionRegion(source, tpId) {
1181
1398
  const idIndex = source.indexOf(`id="${tpId}"`) >= 0
1182
1399
  ? source.indexOf(`id="${tpId}"`)
@@ -1202,6 +1419,43 @@ function hasCreateDeleteDerivedJourney(source, tpId) {
1202
1419
  (/\b_create[A-Za-z0-9_]*\s*\(/.test(region) &&
1203
1420
  (/\b_delete[A-Za-z0-9_]*\s*\(/.test(region) || /\.delete\s*\(/.test(region) || /\.request\s*\(\s*["']DELETE["']/i.test(region)));
1204
1421
  }
1422
+ function hasConditionalFieldOmission(source, tpId, field, parameterNames) {
1423
+ if (!field)
1424
+ return false;
1425
+ const region = scenarioFunctionRegion(source, tpId);
1426
+ const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1427
+ const candidateNames = parameterNames.filter((name) => {
1428
+ const normalized = name.toLowerCase();
1429
+ const normalizedField = field.toLowerCase();
1430
+ return normalized === normalizedField || normalized.startsWith(`${normalizedField}_`) || normalized.endsWith(`_${normalizedField}`);
1431
+ });
1432
+ return candidateNames.some((name) => {
1433
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1434
+ return new RegExp(`\\{\\s*\\}\\s*if\\s+${escapedName}\\s+is\\s+None\\s+else\\s+\\{[\\s\\S]{0,200}?["']${escapedField}["']\\s*:\\s*${escapedName}\\b`).test(region) ||
1435
+ new RegExp(`\\{[\\s\\S]{0,200}?["']${escapedField}["']\\s*:\\s*${escapedName}\\b[\\s\\S]{0,200}?\\}\\s*if\\s+${escapedName}\\s+is\\s+not\\s+None\\s+else\\s+\\{\\s*\\}`).test(region) ||
1436
+ new RegExp(`if\\s+${escapedName}\\s+is\\s+not\\s+None\\s*:[\\s\\S]{0,200}?["']${escapedField}["']\\s*\\]\\s*=\\s*${escapedName}\\b`).test(region);
1437
+ });
1438
+ }
1439
+ function scenarioParamSourceLocation(scriptPath, source, tpId) {
1440
+ const block = extractPytestParamBlock(source, tpId);
1441
+ if (!block)
1442
+ return undefined;
1443
+ const index = source.indexOf(block);
1444
+ if (index < 0)
1445
+ return undefined;
1446
+ return `${scriptPath}:${source.slice(0, index).split("\n").length}`;
1447
+ }
1448
+ function scenarioParamDiagnosticValues(intent, observed) {
1449
+ const expectedRaw = intent.startsWith("custom-literal:")
1450
+ ? intent.slice("custom-literal:".length)
1451
+ : intent === "unknown" ? undefined : intent;
1452
+ return {
1453
+ expectedRaw,
1454
+ observedRaw: boundedScenarioText(observed.text, observed.length),
1455
+ expectedNormalized: expectedRaw === undefined ? undefined : normalizeCustomLiteralExpected(expectedRaw).slice(0, 512),
1456
+ observedNormalized: observed.literal === undefined ? undefined : boundedScenarioText(normalizeScenarioLiteralText(observed.literal), observed.length),
1457
+ };
1458
+ }
1205
1459
  export async function assessBackendScenarioParamConsistency(input) {
1206
1460
  const cases = await listMarkdownCases(input.workspaceRoot, input.layout);
1207
1461
  const entries = [];
@@ -1223,7 +1477,26 @@ export async function assessBackendScenarioParamConsistency(input) {
1223
1477
  const constants = source ? collectPythonStringConstants(source) : undefined;
1224
1478
  const block = source ? extractPytestParamBlock(source, tpId) : undefined;
1225
1479
  const parameterNames = source ? extractPytestParamParameterNames(source, tpId) : [];
1226
- const observed = observeParamFeatures(block, inferred.field, constants, parameterNames);
1480
+ const functionFieldOverride = source && inferred.field
1481
+ ? observeDeterministicFunctionFieldOverride({
1482
+ source,
1483
+ tpId,
1484
+ block,
1485
+ parameterNames,
1486
+ field: inferred.field,
1487
+ constants,
1488
+ })
1489
+ : undefined;
1490
+ const helperFieldObserved = source && inferred.field
1491
+ ? observeSameModulePayloadHelperField({
1492
+ source,
1493
+ block,
1494
+ parameterNames,
1495
+ field: inferred.field,
1496
+ constants,
1497
+ })
1498
+ : undefined;
1499
+ const observed = functionFieldOverride ?? helperFieldObserved ?? observeParamFeatures(block, inferred.field, constants, parameterNames);
1227
1500
  // Request-level / health checks often have no pytest.param payload row,
1228
1501
  // or only a TP-id label positional (no field value).
1229
1502
  const requestLevelNoParam = Boolean(source) &&
@@ -1242,9 +1515,17 @@ export async function assessBackendScenarioParamConsistency(input) {
1242
1515
  hasCreateDeleteDerivedJourney(source, tpId)) ||
1243
1516
  (/^custom-literal:(?:create-derived(?:-active-id|-id)?|active-existing-derived)$/i.test(inferred.intent) &&
1244
1517
  hasCreateDerivedJourney(source, tpId)));
1518
+ const conditionalOmissionMatch = Boolean(source) &&
1519
+ inferred.intent === "missing" &&
1520
+ observed.kind === "none" &&
1521
+ hasConditionalFieldOmission(source, tpId, inferred.field, parameterNames);
1522
+ const localPayloadHelperMatch = Boolean(source) &&
1523
+ inferred.intent === "nominal" &&
1524
+ !inferred.field &&
1525
+ hasSafeSameModulePayloadHelper(source, block, parameterNames);
1245
1526
  const status = !source
1246
1527
  ? "UNDETERMINED"
1247
- : derivedJourneyMatch
1528
+ : derivedJourneyMatch || conditionalOmissionMatch || localPayloadHelperMatch
1248
1529
  ? "MATCH"
1249
1530
  : requestLevelNoParam
1250
1531
  ? "MATCH"
@@ -1252,14 +1533,19 @@ export async function assessBackendScenarioParamConsistency(input) {
1252
1533
  ? "UNDETERMINED"
1253
1534
  : compareIntent(inferred.intent, observed, inferred.bound, inferred.example, inferred.field);
1254
1535
  const repairability = repairabilityFor(status, inferred.intent, observed, inferred.example);
1536
+ const diagnostics = scenarioParamDiagnosticValues(inferred.intent, observed);
1255
1537
  entries.push({
1256
1538
  caseId: testCase.caseId,
1257
1539
  tpId,
1258
1540
  field: inferred.field,
1259
1541
  intent: inferred.intent,
1260
- observed: observed.text,
1542
+ observed: boundedScenarioText(observed.text, observed.length),
1543
+ observedLength: observed.length,
1261
1544
  status,
1262
1545
  repairability,
1546
+ reasonCode: status === "MATCH" ? "SCENARIO_PARAM_MATCH" : status === "MISMATCH" ? "SCENARIO_PARAM_MISMATCH" : "SCENARIO_PARAM_UNDETERMINED",
1547
+ ...diagnostics,
1548
+ sourceLocation: source ? scenarioParamSourceLocation(testCase.scriptPath, source, tpId) : undefined,
1263
1549
  suggestedFix: fallbackSourced
1264
1550
  ? undefined
1265
1551
  : suggestedFixFor(inferred.intent, inferred.field, inferred.bound, inferred.example),
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { z } from "zod";
5
5
  import { BACKEND_TEST_MAX_MODULE_COUNT, isOpaqueHashBackendTestModuleStem, isPriorityOnlyBackendTestModuleStem, } from "./backend-test-module-stem.js";
6
6
  import { resolveBackendTestLayout, } from "./backend-test-layout.js";
7
+ import { inspectBackendTestMarkdownTestPointBindings } from "./backend-test-case-coverage-analysis.js";
7
8
  /**
8
9
  * Maps a backend-test generation writer task id to its writer-progress role.
9
10
  * Returns undefined for nodes that are not completeness-gated generators.
@@ -276,6 +277,16 @@ function moduleStructurallyComplete(markdown) {
276
277
  reasons: [`missing required sections: [${missing.join(", ")}]`],
277
278
  };
278
279
  }
280
+ const bindingReasons = inspectBackendTestMarkdownTestPointBindings(markdown).flatMap((entry) => [
281
+ ...(entry.unclassifiedTestPoints.length > 0
282
+ ? [`${entry.caseId} has unclassified Test Points: ${entry.unclassifiedTestPoints.join(", ")}`]
283
+ : []),
284
+ ...(entry.duplicateBindingTestPoints.length > 0
285
+ ? [`${entry.caseId} has duplicate Test Point bindings: ${entry.duplicateBindingTestPoints.join(", ")}`]
286
+ : []),
287
+ ]);
288
+ if (bindingReasons.length > 0)
289
+ return { ok: false, reasons: bindingReasons };
279
290
  return { ok: true, reasons: [] };
280
291
  }
281
292
  function moduleStructuralDetail(result) {
@@ -884,28 +895,23 @@ export function isBackendTestShardChildTask(task) {
884
895
  task.id?.startsWith("generate-backend-pytest-case-"));
885
896
  }
886
897
  /**
887
- * Is this task the README-only plan node of the map-sharded md topology?
888
- * The plan node writes testcase/md/README.md alone; module case cards are
889
- * produced by the downstream map_agent children, so the Completeness Gate
890
- * must NOT require module files to exist at plan time.
898
+ * Historical classifier for the Markdown plan node. Current backend-test N2
899
+ * is read-only and persists a run-owned plan.md, so writer completeness never
900
+ * invokes the workspace README assessment below for newly generated DAGs.
891
901
  */
892
902
  export function isBackendTestMdPlanTask(task) {
893
903
  return task.id === "generate-backend-md-plan-pi";
894
904
  }
895
905
  /**
896
- * Is this task the shared-resources plan node of the map-sharded pytest
897
- * topology? The plan node writes helpers/factories only; per-module pytest
898
- * scripts are produced by the downstream map_agent children, so the Gate
899
- * must NOT require mapped pytest scripts to exist at plan time.
906
+ * Historical classifier for the former shared-resources pytest writer. Current
907
+ * N7 is static and every map child writes one self-contained pytest module.
900
908
  */
901
909
  export function isBackendTestPytestPlanTask(task) {
902
910
  return task.id === "generate-backend-pytest-plan-pi";
903
911
  }
904
912
  /**
905
- * Plan-node completeness for the md topology: verify the README itself is
906
- * structurally complete (Coverage Scope, Coverage Matrix, a non-empty module
907
- * index), WITHOUT requiring the indexed module files to exist yet — those are
908
- * written by the downstream map_agent shard children.
913
+ * Legacy workspace-plan completeness helper retained for isolated historical
914
+ * fixtures. New DAGs validate the run-owned plan artifact in N3 instead.
909
915
  */
910
916
  export async function assessBackendTestMdPlanCompleteness(workspaceRoot, layout) {
911
917
  const resolvedPlan = layout ?? resolveBackendTestLayout(undefined);