@tea-agent/loop-agent 0.40.0-next.9 → 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 (143) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +21 -21
  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/pi-sdk-executor.js +33 -5
  14. package/dist/executors/shell-executor.js +94 -20
  15. package/dist/governance/checks.js +1 -0
  16. package/dist/shared/operator/capabilities.js +9 -1
  17. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  18. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  19. package/dist/shared/pi-context-pressure/env.js +58 -0
  20. package/dist/shared/pi-context-pressure/extension.js +100 -0
  21. package/dist/shared/pi-context-pressure/index.js +7 -0
  22. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  23. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  24. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  25. package/dist/worker/console/chat/assistant-content.js +7 -0
  26. package/dist/worker/console/chat/chat-event-store.js +82 -11
  27. package/dist/worker/console/chat/goal-round-driver.js +72 -0
  28. package/dist/worker/console/chat/goals.js +300 -0
  29. package/dist/worker/console/chat/pi-runtime.js +285 -8
  30. package/dist/worker/console/chat/provider-error.js +2 -1
  31. package/dist/worker/console/chat/repo-browser.js +94 -12
  32. package/dist/worker/console/chat/routes.js +495 -30
  33. package/dist/worker/console/chat/session-catalog.js +48 -0
  34. package/dist/worker/console/chat/session-stats.js +96 -0
  35. package/dist/worker/console/chat/session-store.js +27 -18
  36. package/dist/worker/console/chat/shortcuts.js +7 -0
  37. package/dist/worker/console/chat/sift-bridge.js +1 -0
  38. package/dist/worker/console/chat/turn-process.js +42 -5
  39. package/dist/worker/console/dag-execution-receipt.js +20 -2
  40. package/dist/worker/console/operator-actions.js +20 -5
  41. package/dist/worker/console/operator-selection.js +6 -2
  42. package/dist/worker/console/pi-plugins.catalog.json +73658 -0
  43. package/dist/worker/console/pi-plugins.js +179 -0
  44. package/dist/worker/console/routes.js +246 -6
  45. package/dist/worker/console/server.js +173 -42
  46. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bs-CDAXM.js → abnfDiagram-N423BO3Z-Dy71QJp3.js} +1 -1
  47. package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-M25fNMTE.js} +1 -1
  48. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-CUWlVl1a.js} +1 -1
  49. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-Dd0Whpx2.js} +1 -1
  50. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.js → c4Diagram-5PPSVZJV-BdfeCkI5.js} +1 -1
  51. package/dist/worker/console/static/assets/channel-Ba0AKiEZ.js +1 -0
  52. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-shmQKsFz.js → chunk-2GRJ4B5K-D_1sUkyD.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-Cxnxf36E.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-D9RG3G_x.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-Zz9jgjLX.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-BCggDRCH.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-CFg-5iAS.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-DRQTE8x_.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-C_Dw7uZ8.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-BjIiApsk.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.js → chunk-XXDRQBXY-B5XQD4KG.js} +1 -1
  62. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BU3IjVEe.js +1 -0
  63. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BU3IjVEe.js +1 -0
  64. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CkHG6W3s.js → cose-bilkent-JH36ORCC-huHqH6WQ.js} +1 -1
  65. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-BSi6yZqP.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-Bs_Jp31y.js} +1 -1
  67. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-CYxavikT.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-B6TErl71.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-CvH4J2ii.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-v1NBAqXS.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-CUNHVAn2.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-Dzz1gUu-.js} +1 -1
  73. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR-CCF8_LjK.js} +1 -1
  74. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-DObdPVLa.js} +1 -1
  75. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-DlN8Pxrj.js} +1 -1
  76. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-BJKIPTGX.js} +1 -1
  77. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.js → gitGraphDiagram-DS77QQ5N-XcSSnObi.js} +1 -1
  78. package/dist/worker/console/static/assets/index-CIYzS16i.css +1 -0
  79. package/dist/worker/console/static/assets/index-CS3QOTfl.js +451 -0
  80. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DYNx_IPv.js → infoDiagram-6WML65LV-BKrRKMww.js} +1 -1
  81. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-CN5dk-yb.js} +1 -1
  82. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-B4P4KZ-Y.js} +1 -1
  83. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-BRlteP32.js} +1 -1
  84. package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-CG8D9fuL.js} +1 -1
  85. package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-BuB6P2rX.js} +5 -5
  86. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-DYXGl_DR.js} +1 -1
  87. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-BM48yEVG.js} +1 -1
  88. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-mT8NL-Ow.js} +1 -1
  89. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-C4pZ6DH_.js} +1 -1
  90. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-BKAcr70m.js} +1 -1
  91. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-CRxcKpoO.js} +1 -1
  92. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-Ds4LxQDI.js} +1 -1
  93. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-CPLwPf_p.js} +1 -1
  94. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-CL65uij0.js} +1 -1
  95. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.js → stateDiagram-OKZ733FA-CO0Mgd1W.js} +1 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BUewAVYS.js +1 -0
  97. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-YcF12FiU.js → swimlanes-SLNWSIFB-Cxv3TSNw.js} +2 -2
  98. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bhe67Wjl.js +8 -0
  99. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ClnRcg2s.js → timeline-definition-Z64GVDOM-Dw85f9L2.js} +1 -1
  100. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-B4QLCqCy.js} +1 -1
  101. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-B_yKCWwn.js} +1 -1
  102. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.js → xychartDiagram-ELKLHX3M-nniBEGv2.js} +1 -1
  103. package/dist/worker/console/static/index.html +2 -2
  104. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +95 -25
  105. package/dist/worker/console/static-src/operator-chat/compaction-message.js +35 -1
  106. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +3 -1
  107. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +26 -19
  108. package/dist/worker/console/static-src/operator-chat/useChatStream.js +0 -11
  109. package/dist/worker/console/static-src/operator-chat/useChatThread.js +14 -1
  110. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +34 -4
  111. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  112. package/dist/worker/console/static-src/operator-chat/workspace-reorder.js +18 -0
  113. package/dist/worker/console/static-src/shell/useWorkspaces.js +150 -34
  114. package/dist/worker/console/static-src/shell/workspace-route.js +3 -1
  115. package/dist/worker/console/workspace-context.js +28 -3
  116. package/dist/worker/console/workspace-registry.js +333 -60
  117. package/dist/worker/observe/static/inspect-bootstrap.d.ts +8 -0
  118. package/dist/worker/observe/static/inspect-bootstrap.js +57 -0
  119. package/dist/worker/observe/static/inspect-workspace.js +54 -10
  120. package/dist/worker/observe/static/operator-chrome.css +23 -4
  121. package/dist/worker/observe/static/operator-chrome.js +7 -2
  122. package/dist/worker/observe/static/styles.css +87 -25
  123. package/dist/worker/observe/static/views/dag-inspector.js +42 -1
  124. package/dist/worker/observe/static/views/dags.js +7 -5
  125. package/dist/worker/observe/static/views/session-timeline.js +135 -0
  126. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +162 -8
  127. package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
  128. package/dist/workflows/dag/backend-test-result-contract.js +5 -1
  129. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  130. package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
  131. package/dist/workflows/dag/init-hybrid.js +5 -4
  132. package/docs/architecture/runtime-boundaries.md +2 -1
  133. package/docs/templates/backend-test-dag.json +6 -5
  134. package/docs/templates/init-managed-agents.md +1 -1
  135. package/package.json +3 -2
  136. package/skills/loop-agent/references/command-reference.md +15 -1
  137. package/dist/worker/console/static/assets/channel-amxUpk7o.js +0 -1
  138. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
  139. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
  140. package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
  141. package/dist/worker/console/static/assets/index-DqJbO3-p.js +0 -407
  142. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
  143. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +0 -8
@@ -8,6 +8,7 @@ import { resolveBackendTestLayout, } from "./backend-test-layout.js";
8
8
  import { resolveBackendTestManifestModules, backendManifestStatusFinding } from "./backend-test-markdown-workflow.js";
9
9
  import { applyOpenApiPartitionDomainPolicy, assessScenarioPartitionCoverage, parseScenarioPartitions, } from "./backend-test-scenario-partitions.js";
10
10
  import { backendTestCaseManifestSchema, computeCaseManifestCoverageSummary, } from "./backend-test-case-manifest.js";
11
+ import { inferScenarioParamIntent } from "./backend-test-scenario-param.js";
11
12
  const CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-\d{3})\b.*$/gm;
12
13
  const CASE_ID_IN_TEXT = /\bBE-[A-Z0-9_-]+-\d{3}\b/g;
13
14
  const NON_CANONICAL_CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-(?:\d{2}|\d{2,3}[A-Z]+))\b.*$/gm;
@@ -281,6 +282,80 @@ export const backendTestMarkdownPytestCorrespondenceFactsSchema = z.object({
281
282
  entries: z.array(correspondenceEntrySchema),
282
283
  findings: z.array(z.string()),
283
284
  }).strict();
285
+ function bindingCounts(entry) {
286
+ const counts = new Map();
287
+ for (const point of [...entry.variantTestPoints, ...entry.assertionTestPoints, ...entry.crossCuttingTestPoints]) {
288
+ counts.set(point, (counts.get(point) ?? 0) + 1);
289
+ }
290
+ return counts;
291
+ }
292
+ /**
293
+ * Classify correspondence defects by the only writer that can safely repair
294
+ * them. Markdown-owned contract defects must be fixed before pytest generation;
295
+ * pytest-owned generated-asset defects may enter the single bounded N11 repair.
296
+ */
297
+ export function classifyBackendTestCorrespondenceRepair(facts, layout) {
298
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
299
+ const scriptRoot = resolved.scriptDir.replace(/\/$/, "");
300
+ const safePytestPath = (candidate) => {
301
+ const normalized = candidate.replaceAll("\\", "/").replace(/^\.\//, "");
302
+ return normalized.endsWith(".py") && (normalized === scriptRoot || normalized.startsWith(`${scriptRoot}/`));
303
+ };
304
+ const markdownBlockingEntries = [];
305
+ const pytestRepairEntries = [];
306
+ for (const entry of facts.entries) {
307
+ const markdownReasons = [];
308
+ const pytestReasons = [];
309
+ const counts = bindingCounts(entry);
310
+ if (entry.testPoints.some((point) => (counts.get(point) ?? 0) !== 1) ||
311
+ [...counts.keys()].some((point) => !entry.testPoints.includes(point))) {
312
+ markdownReasons.push("markdown-test-point-binding-invalid");
313
+ }
314
+ if (entry.declaredScript && entry.declaredScript !== entry.expectedScript) {
315
+ markdownReasons.push("markdown-script-declaration-invalid");
316
+ }
317
+ if (entry.cardinality === "1:0")
318
+ pytestReasons.push("missing-pytest-symbol");
319
+ if (entry.cardinality === "1:N")
320
+ pytestReasons.push("multiple-pytest-symbols");
321
+ if (entry.cardinality === "0:1")
322
+ pytestReasons.push("extra-pytest-symbol");
323
+ if (entry.actualScripts.some((script) => script !== entry.expectedScript))
324
+ pytestReasons.push("pytest-script-mismatch");
325
+ if (entry.declaredPrimarySymbol && entry.pytestSymbols.some((symbol) => symbol !== entry.declaredPrimarySymbol)) {
326
+ pytestReasons.push("pytest-primary-symbol-mismatch");
327
+ }
328
+ if (entry.variantTestPoints.some((point) => !entry.parameterIds.includes(point)))
329
+ pytestReasons.push("variant-parameter-missing");
330
+ if (entry.assertionTestPoints.some((point) => !entry.mappedTestPoints.includes(point)))
331
+ pytestReasons.push("assertion-binding-missing");
332
+ if (entry.crossCuttingTestPoints.some((point) => !entry.mappedTestPoints.includes(point)))
333
+ pytestReasons.push("cross-cutting-binding-missing");
334
+ if (entry.parameterIds.some((point) => !entry.testPoints.includes(point)))
335
+ pytestReasons.push("pytest-test-point-binding-extra");
336
+ if (entry.payloadAssessment.status === "UNSAFE")
337
+ pytestReasons.push("pytest-payload-shape-mismatch");
338
+ if (entry.status !== "EXACT_1_TO_1" &&
339
+ !["TEST_POINT_BINDING_DUPLICATE", "PAYLOAD_CONTRACT_UNAVAILABLE"].includes(entry.status)) {
340
+ pytestReasons.push(`correspondence-${entry.status.toLowerCase().replaceAll("_", "-")}`);
341
+ }
342
+ if (entry.status === "PAYLOAD_CONTRACT_UNAVAILABLE") {
343
+ markdownReasons.push("markdown-payload-contract-unavailable");
344
+ }
345
+ const paths = orderedUnique([entry.expectedScript, ...entry.actualScripts].filter(safePytestPath));
346
+ if (markdownReasons.length > 0) {
347
+ markdownBlockingEntries.push({ ...(entry.caseId ? { caseId: entry.caseId } : {}), reasonCodes: orderedUnique(markdownReasons), paths });
348
+ }
349
+ if (pytestReasons.length > 0 && paths.length > 0) {
350
+ pytestRepairEntries.push({ ...(entry.caseId ? { caseId: entry.caseId } : {}), reasonCodes: orderedUnique(pytestReasons), paths });
351
+ }
352
+ }
353
+ return {
354
+ markdownBlockingEntries,
355
+ pytestRepairEntries,
356
+ pytestRepairPaths: orderedUnique(pytestRepairEntries.flatMap((entry) => entry.paths)).sort(),
357
+ };
358
+ }
284
359
  function isRecord(value) {
285
360
  return typeof value === "object" && value !== null && !Array.isArray(value);
286
361
  }
@@ -593,6 +668,19 @@ function automationLabelBody(body, labels) {
593
668
  const match = new RegExp(`^\\s*[-*+]\\s*(?:${escaped})\\s*[::]\\s*(.*)$`, "mi").exec(automation);
594
669
  return match?.[1]?.replaceAll("`", "").trim() ?? "";
595
670
  }
671
+ export function inspectBackendTestMarkdownTestPointBindings(markdown) {
672
+ const headings = [...markdown.matchAll(CASE_HEADING)];
673
+ return headings.map((heading, index) => {
674
+ const body = markdown.slice(heading.index, headings[index + 1]?.index ?? markdown.length);
675
+ const testPoints = listTokens(body, CASE_SECTION_ALIASES.testPoints, TEST_POINT);
676
+ const bindings = testPointBindingFacts(body, testPoints);
677
+ return {
678
+ caseId: canonicalCaseId(heading[1]),
679
+ unclassifiedTestPoints: bindings.unclassifiedTestPoints,
680
+ duplicateBindingTestPoints: bindings.duplicateBindingTestPoints,
681
+ };
682
+ });
683
+ }
596
684
  function testPointBindingFacts(body, testPoints) {
597
685
  const bindings = TEST_POINT_BINDING_MODES.flatMap((mode) => {
598
686
  const value = automationLabelBody(body, AUTOMATION_BINDING_LABELS[mode]);
@@ -627,7 +715,11 @@ function payloadContract(body) {
627
715
  const requiredValue = automationLabelBody(body, ["Payload Required Paths", "载荷必需路径"]);
628
716
  const allowedValue = automationLabelBody(body, ["Payload Allowed Paths", "载荷允许路径"]);
629
717
  const enumValue = automationLabelBody(body, ["Payload Enum", "载荷枚举"]);
630
- const tokens = (value) => orderedUnique(value.split(/[,;,;]/).map((item) => item.trim()).filter((item) => /^[A-Za-z_][A-Za-z0-9_.]*$/.test(item)));
718
+ const tokens = (value) => orderedUnique(value
719
+ .split(/[,;,;]/)
720
+ .map((item) => item.trim())
721
+ .filter((item) => !/^(?:none|无|not-applicable|n\/a)$/i.test(item))
722
+ .filter((item) => /^[A-Za-z_][A-Za-z0-9_.]*$/.test(item)));
631
723
  const enumValues = {};
632
724
  for (const assignment of enumValue.split(/[,;,;]/).map((item) => item.trim()).filter(Boolean)) {
633
725
  const match = /^([A-Za-z_][A-Za-z0-9_.]*)\s*(?:=|:)\s*(.+)$/.exec(assignment);
@@ -1404,6 +1496,11 @@ def payload_expr_nodes(expr, assigned, parameters, seen=None):
1404
1496
  if isinstance(key,ast.Constant) and key.value == expr.slice.value:
1405
1497
  result.extend(shape_only(item) for item in payload_expr_nodes(value,assigned,parameters,set(seen)))
1406
1498
  return result
1499
+ if isinstance(expr,ast.IfExp):
1500
+ result=[]
1501
+ result.extend(payload_expr_nodes(expr.body,assigned,parameters,set(seen)))
1502
+ result.extend(payload_expr_nodes(expr.orelse,assigned,parameters,set(seen)))
1503
+ return result
1407
1504
  if isinstance(expr,ast.Call):
1408
1505
  result=[]
1409
1506
  if isinstance(expr.func,ast.Name) and expr.func.id in FUNCTION_PAYLOADS:
@@ -1444,6 +1541,30 @@ def decorator_payload_nodes(parameters,assigned):
1444
1541
  for value in values: result.extend(payload_expr_nodes(value,assigned,parameters))
1445
1542
  return result
1446
1543
 
1544
+ def decorator_payload_nodes_by_tp(fn,assigned):
1545
+ result={}
1546
+ for dec in fn.decorator_list:
1547
+ if not isinstance(dec,ast.Call) or not isinstance(dec.func,ast.Attribute) or dec.func.attr != 'parametrize' or len(dec.args) < 2: continue
1548
+ if not isinstance(dec.args[0],ast.Constant) or not isinstance(dec.args[0].value,str): continue
1549
+ names=[item.strip() for item in dec.args[0].value.split(',')]
1550
+ rows=dec.args[1]
1551
+ if isinstance(rows,ast.Name) and rows.id in GLOBAL_VALUES: rows=GLOBAL_VALUES[rows.id]
1552
+ for node in ast.walk(rows):
1553
+ if not isinstance(node,ast.Call) or not isinstance(node.func,ast.Attribute) or node.func.attr != 'param': continue
1554
+ tp=None
1555
+ for keyword in node.keywords:
1556
+ if keyword.arg == 'id' and isinstance(keyword.value,ast.Constant) and isinstance(keyword.value.value,str) and keyword.value.value.startswith('TP-'):
1557
+ tp=keyword.value.value
1558
+ if not tp: continue
1559
+ parameters={}
1560
+ for index,name in enumerate(names):
1561
+ if index < len(node.args): parameters.setdefault(name,[]).append(node.args[index])
1562
+ payload_nodes=decorator_payload_nodes(parameters,assigned)
1563
+ paths=set(); values={}
1564
+ for payload_node in payload_nodes: collect_dict(payload_node,'',paths,values)
1565
+ result[tp]={key:sorted(items) for key,items in sorted(values.items())}
1566
+ return result
1567
+
1447
1568
  def fallback_payload_nodes(fn):
1448
1569
  result=[]
1449
1570
  assigned={}
@@ -1473,7 +1594,7 @@ def fallback_payload_nodes(fn):
1473
1594
 
1474
1595
  def is_direct_transport_call(node):
1475
1596
  if not isinstance(node,ast.Call) or not isinstance(node.func,ast.Attribute): return False
1476
- if node.func.attr.lower() not in {'get','post','put','patch','delete','request'}: return False
1597
+ if node.func.attr.lower() not in {'get','post','put','patch','delete','request','call'}: return False
1477
1598
  receiver_tokens=[]
1478
1599
  current=node.func.value
1479
1600
  while isinstance(current,ast.Attribute):
@@ -1521,6 +1642,7 @@ for fn in ast.walk(tree):
1521
1642
  'values':{key:sorted(items) for key,items in sorted(values.items())},
1522
1643
  'fallbackPaths':sorted(fallback_paths),
1523
1644
  'fallbackValues':{key:sorted(items) for key,items in sorted(fallback_values.items())},
1645
+ 'valuesByTestPoint':decorator_payload_nodes_by_tp(fn,{}),
1524
1646
  }
1525
1647
  print(json.dumps(out,ensure_ascii=True))
1526
1648
  `;
@@ -1651,6 +1773,7 @@ function pytestSymbols(script, source) {
1651
1773
  payloadValues: payloadShape.values,
1652
1774
  fallbackPayloadPaths: payloadShape.fallbackPaths ?? [],
1653
1775
  fallbackPayloadValues: payloadShape.fallbackValues ?? {},
1776
+ payloadValuesByTestPoint: payloadShape.valuesByTestPoint ?? {},
1654
1777
  };
1655
1778
  });
1656
1779
  }
@@ -1664,6 +1787,33 @@ function executionSignature(testCase) {
1664
1787
  .join("\n");
1665
1788
  return createHash("sha256").update(normalized).digest("hex");
1666
1789
  }
1790
+ function normalizePythonLiteralValue(value) {
1791
+ const trimmed = value.trim();
1792
+ if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) {
1793
+ return trimmed.slice(1, -1);
1794
+ }
1795
+ return trimmed;
1796
+ }
1797
+ function intentionalInvalidEnumVariant(input) {
1798
+ if (!input.testCase.testPointBindings.some((binding) => binding.mode === "variant" && binding.testPoint === input.testPoint))
1799
+ return false;
1800
+ const inferred = inferScenarioParamIntent({ tpId: input.testPoint, caseBody: input.testCase.body });
1801
+ if (inferred.intentSource !== "machine-line" || inferred.field?.toLowerCase() !== input.field.toLowerCase())
1802
+ return false;
1803
+ const observed = normalizePythonLiteralValue(input.observedValue);
1804
+ if (input.allowed.includes(observed))
1805
+ return false;
1806
+ if (inferred.intent === "enum-invalid" || inferred.intent === "wrong-type")
1807
+ return true;
1808
+ if (inferred.intent === "null")
1809
+ return observed === "None" || observed === "null";
1810
+ if (inferred.intent === "empty")
1811
+ return observed === "";
1812
+ if (!inferred.intent.startsWith("custom-literal:"))
1813
+ return false;
1814
+ const expected = normalizePythonLiteralValue(inferred.example ?? inferred.intent.slice("custom-literal:".length));
1815
+ return observed === expected;
1816
+ }
1667
1817
  function assessPayloadContract(testCase, refs) {
1668
1818
  const requiredPaths = testCase.payloadContract.requiredPaths;
1669
1819
  const directObservedPaths = orderedUnique(refs.flatMap((item) => item.payloadPaths));
@@ -1685,13 +1835,17 @@ function assessPayloadContract(testCase, refs) {
1685
1835
  ? observedPaths.filter((item) => !allowedPaths.includes(item) && !allowedPaths.some((allowed) => item.startsWith(`${allowed}.`)))
1686
1836
  : [];
1687
1837
  const enumMismatches = [];
1688
- const delegatesInvalidEnumToScenarioGate = testCase.testPointBindings
1689
- .filter((binding) => binding.mode === "variant")
1690
- .some((binding) => /(?:ENUM|CATEGORY).*(?:INVALID|UNKNOWN|CASE|WHITESPACE|EMPTY|WRONG-TYPE)|(?:INVALID|UNKNOWN|CASE|WHITESPACE|EMPTY|WRONG-TYPE).*(?:ENUM|CATEGORY)/.test(binding.testPoint));
1691
1838
  for (const [field, allowed] of Object.entries(testCase.payloadContract.enumValues)) {
1692
- const observed = orderedUnique(refs.flatMap((item) => (useFallback ? item.fallbackPayloadValues[field] : item.payloadValues[field]) ?? []).map((value) => value.replace(/^['"]|['"]$/g, "")));
1693
- for (const value of observed) {
1694
- if (!allowed.includes(value) && !delegatesInvalidEnumToScenarioGate)
1839
+ const observed = orderedUnique(refs.flatMap((item) => (useFallback ? item.fallbackPayloadValues[field] : item.payloadValues[field]) ?? []));
1840
+ for (const rawValue of observed) {
1841
+ const value = normalizePythonLiteralValue(rawValue);
1842
+ if (allowed.includes(value))
1843
+ continue;
1844
+ const associatedTestPoints = orderedUnique(refs.flatMap((item) => Object.entries(item.payloadValuesByTestPoint)
1845
+ .filter(([, values]) => (values[field] ?? []).includes(rawValue))
1846
+ .map(([testPoint]) => testPoint)));
1847
+ const intentionallyInvalid = associatedTestPoints.length > 0 && associatedTestPoints.every((testPoint) => intentionalInvalidEnumVariant({ testCase, testPoint, field, observedValue: rawValue, allowed }));
1848
+ if (!intentionallyInvalid)
1695
1849
  enumMismatches.push(`${field}=${value}; allowed=${allowed.join("|")}`);
1696
1850
  }
1697
1851
  }
@@ -20,6 +20,14 @@ export const backendPytestCollectionFindingSchema = z.object({
20
20
  classification: z.literal("test-asset-defect"),
21
21
  repairability: z.enum(["repairable", "blocked"]),
22
22
  detail: z.string().min(1),
23
+ caseId: z.string().min(1).optional(),
24
+ reasonCodes: z.array(z.string().min(1)).optional(),
25
+ repairPaths: z.array(z.string().min(1)).optional(),
26
+ payloadExpectedPaths: z.array(z.string()).optional(),
27
+ payloadObservedPaths: z.array(z.string()).optional(),
28
+ payloadMissingPaths: z.array(z.string()).optional(),
29
+ payloadUnexpectedPaths: z.array(z.string()).optional(),
30
+ payloadEnumMismatches: z.array(z.string()).optional(),
23
31
  }).strict();
24
32
  export const backendPytestCollectionFactsSchema = z.object({
25
33
  schemaId: z.literal("backend-test-pytest-collection-v3"),
@@ -61,11 +69,22 @@ export const backendPytestCollectionFactsSchema = z.object({
61
69
  context.addIssue({ code: z.ZodIssueCode.custom, message: "backend pytest fixture resolution without an attempt cannot have an exit code" });
62
70
  }
63
71
  });
72
+ const backendTestScenarioParamDiagnosticSchema = z.object({
73
+ reasonCode: z.enum(["SCENARIO_PARAM_MISMATCH", "SCENARIO_PARAM_UNDETERMINED"]),
74
+ tpId: z.string().min(1).max(256),
75
+ field: z.string().min(1).max(256).optional(),
76
+ expectedRaw: z.string().max(512).optional(),
77
+ observedRaw: z.string().max(512),
78
+ expectedNormalized: z.string().max(512).optional(),
79
+ observedNormalized: z.string().max(512).optional(),
80
+ sourceLocation: z.string().max(512).optional(),
81
+ }).strict();
64
82
  const backendTestExcludedItemSchema = z.object({
65
83
  itemId: z.string().min(1),
66
84
  caseId: z.string().optional(),
67
85
  symbol: z.string().optional(),
68
86
  reasons: z.array(z.string().min(1)).min(1),
87
+ scenarioParamDiagnostics: z.array(backendTestScenarioParamDiagnosticSchema).max(16).optional(),
69
88
  }).strict();
70
89
  export const backendTestExecutionReadinessSchema = z.object({
71
90
  schemaId: z.literal("backend-test-execution-readiness-v2"),
@@ -81,6 +100,14 @@ export const backendTestExecutionReadinessSchema = z.object({
81
100
  collectedItemIds: z.array(z.string()),
82
101
  eligibleItemIds: z.array(z.string()),
83
102
  excludedItems: z.array(backendTestExcludedItemSchema),
103
+ exclusionSummary: z.object({
104
+ excludedItemCount: z.number().int().min(0),
105
+ excludedCaseCount: z.number().int().min(0),
106
+ excludedByReason: z.record(z.string(), z.number().int().min(1)),
107
+ excludedByCase: z.record(z.string(), z.number().int().min(1)),
108
+ }).strict(),
109
+ pytestSpawned: z.literal(false),
110
+ businessTestBodyExecutedCount: z.literal(0),
84
111
  fixtureIssues: z.array(z.string()),
85
112
  assetHashes: z.record(z.string(), z.string().regex(SHA256)),
86
113
  eligibilityInputHashes: z.record(z.string(), z.string().regex(SHA256)),
@@ -702,6 +729,7 @@ export function buildBackendTestItemEligibility(collectedItemIds, input) {
702
729
  const parameterTokens = orderedUnique((itemId.match(/TP-[A-Z0-9-]+/g) ?? []));
703
730
  const mapping = input.correspondenceEntries.find((entry) => symbol && (entry.declaredPrimarySymbol === symbol || entry.pytestSymbols.includes(symbol)));
704
731
  const reasons = [];
732
+ const scenarioParamDiagnostics = [];
705
733
  if (!mapping)
706
734
  reasons.push("no exact Markdown Case/primary-symbol correspondence");
707
735
  if (mapping && mapping.status !== "EXACT_1_TO_1")
@@ -724,13 +752,30 @@ export function buildBackendTestItemEligibility(collectedItemIds, input) {
724
752
  // or observed Python payload shape.
725
753
  const normalizedField = entry.field?.toLowerCase();
726
754
  const fieldIsPayloadBound = Boolean(normalizedField && payloadPaths.some((item) => item === normalizedField || item.endsWith(`.${normalizedField}`)));
727
- if (fieldIsPayloadBound && entry.status !== "MATCH")
755
+ if (fieldIsPayloadBound && entry.status !== "MATCH") {
728
756
  reasons.push(`scenario-param ${entry.tpId} field ${entry.field} is ${entry.status}`);
757
+ scenarioParamDiagnostics.push({
758
+ reasonCode: entry.status === "MISMATCH" ? "SCENARIO_PARAM_MISMATCH" : "SCENARIO_PARAM_UNDETERMINED",
759
+ tpId: entry.tpId,
760
+ ...(entry.field ? { field: entry.field } : {}),
761
+ ...(entry.expectedRaw !== undefined ? { expectedRaw: entry.expectedRaw.slice(0, 512) } : {}),
762
+ observedRaw: (entry.observedRaw ?? "unavailable").slice(0, 512),
763
+ ...(entry.expectedNormalized !== undefined ? { expectedNormalized: entry.expectedNormalized.slice(0, 512) } : {}),
764
+ ...(entry.observedNormalized !== undefined ? { observedNormalized: entry.observedNormalized.slice(0, 512) } : {}),
765
+ ...(entry.sourceLocation ? { sourceLocation: entry.sourceLocation.slice(0, 512) } : {}),
766
+ });
767
+ }
729
768
  }
730
769
  if (reasons.length === 0)
731
770
  eligibleItemIds.push(itemId);
732
771
  else
733
- excludedItems.push({ itemId, ...(mapping?.caseId ? { caseId: mapping.caseId } : {}), ...(symbol ? { symbol } : {}), reasons: orderedUnique(reasons) });
772
+ excludedItems.push({
773
+ itemId,
774
+ ...(mapping?.caseId ? { caseId: mapping.caseId } : {}),
775
+ ...(symbol ? { symbol } : {}),
776
+ reasons: orderedUnique(reasons),
777
+ ...(scenarioParamDiagnostics.length > 0 ? { scenarioParamDiagnostics: scenarioParamDiagnostics.slice(0, 16) } : {}),
778
+ });
734
779
  }
735
780
  return { eligibleItemIds, excludedItems };
736
781
  }
@@ -763,6 +808,9 @@ export async function materializeBackendTestExecutionReadiness(input) {
763
808
  collectedItemIds: input.effective.collectedItemIds,
764
809
  eligibleItemIds: eligibility.eligibleItemIds,
765
810
  excludedItems: eligibility.excludedItems,
811
+ exclusionSummary: summarizeBackendTestExclusions(eligibility.excludedItems),
812
+ pytestSpawned: false,
813
+ businessTestBodyExecutedCount: 0,
766
814
  fixtureIssues: input.effective.findings.filter((item) => /fixture/i.test(item.kind)).map((item) => item.detail),
767
815
  assetHashes: input.effective.inputHashes,
768
816
  eligibilityInputHashes: input.eligibilityInputHashes ?? {},
@@ -778,6 +826,26 @@ export async function readBackendTestExecutionReadiness(filePath) {
778
826
  function formatReadinessCounts(readiness) {
779
827
  return `mapped=${readiness.mappedScripts.length} collected=${readiness.collectedItemIds.length} eligible=${readiness.eligibleItemIds.length} excluded=${readiness.excludedItems.length}`;
780
828
  }
829
+ export function summarizeBackendTestExclusions(excludedItems) {
830
+ const reasonCounts = new Map();
831
+ const caseCounts = new Map();
832
+ for (const item of excludedItems) {
833
+ if (item.caseId)
834
+ caseCounts.set(item.caseId, (caseCounts.get(item.caseId) ?? 0) + 1);
835
+ for (const reason of item.reasons) {
836
+ const category = classifyExclusionReason(reason);
837
+ reasonCounts.set(category, (reasonCounts.get(category) ?? 0) + 1);
838
+ }
839
+ }
840
+ return {
841
+ excludedItemCount: excludedItems.length,
842
+ excludedCaseCount: caseCounts.size,
843
+ excludedByReason: Object.fromEntries(READINESS_EXCLUSION_CATEGORIES
844
+ .filter((category) => (reasonCounts.get(category) ?? 0) > 0)
845
+ .map((category) => [category, reasonCounts.get(category)])),
846
+ excludedByCase: Object.fromEntries([...caseCounts.entries()].sort(([left], [right]) => left.localeCompare(right))),
847
+ };
848
+ }
781
849
  export function classifyExclusionReason(reason) {
782
850
  if (reason === "no exact Markdown Case/primary-symbol correspondence")
783
851
  return "correspondence-missing";
@@ -70,6 +70,7 @@ export const backendTestResultContractSchema = z
70
70
  schemaVersion: z.literal(1),
71
71
  /** Process-level status of the pytest invocation. Task Pool: use with outcome. */
72
72
  executionStatus: backendTestExecutionStatusSchema,
73
+ pytestSpawned: z.literal(true),
73
74
  pytestExitCode: z.number().int().min(0).max(255),
74
75
  collectionStatus: backendTestCollectionStatusSchema,
75
76
  /** Total tests counted from JUnit (or 0 when report unavailable). */
@@ -659,6 +660,7 @@ export function deriveBackendTestResult(input) {
659
660
  const result = {
660
661
  schemaVersion: 1,
661
662
  executionStatus,
663
+ pytestSpawned: true,
662
664
  pytestExitCode: input.pytestExitCode,
663
665
  collectionStatus: "unknown",
664
666
  tests: 0,
@@ -733,6 +735,7 @@ export function deriveBackendTestResult(input) {
733
735
  const result = {
734
736
  schemaVersion: 1,
735
737
  executionStatus,
738
+ pytestSpawned: true,
736
739
  pytestExitCode: exit,
737
740
  collectionStatus,
738
741
  tests: parsed.tests,
@@ -931,11 +934,12 @@ export async function materializeBackendTestResultFromPytestHtml(input) {
931
934
  outcome = "command-error";
932
935
  }
933
936
  const commandSummary = input.commandSummary ??
934
- `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 python -m pytest -v -p no:cacheprovider --html=${htmlRelativePath} --self-contained-html`;
937
+ `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 python reports/backend-test-pytest-launcher.py reports/backend-test-pytest-targets.txt ${htmlRelativePath} -> pytest.main(-v -p no:cacheprovider --self-contained-html)`;
935
938
  assertNoSecrets("commandSummary", commandSummary);
936
939
  const result = backendTestResultContractSchema.parse({
937
940
  schemaVersion: 1,
938
941
  executionStatus,
942
+ pytestSpawned: true,
939
943
  pytestExitCode: exit,
940
944
  collectionStatus,
941
945
  tests: parsed.tests,