@tea-agent/loop-agent 0.40.0-next.10 → 0.40.0-next.12

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 (97) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/application/dag/generate-task-dag.js +6 -2
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/commands/client-recovery.js +8 -36
  5. package/dist/executors/pi-sdk-executor.js +33 -5
  6. package/dist/executors/shell-executor.js +64 -17
  7. package/dist/governance/checks.js +1 -0
  8. package/dist/shared/pi-context-pressure/checkpoint.js +116 -0
  9. package/dist/shared/pi-context-pressure/compaction-policy.js +151 -0
  10. package/dist/shared/pi-context-pressure/env.js +58 -0
  11. package/dist/shared/pi-context-pressure/extension.js +100 -0
  12. package/dist/shared/pi-context-pressure/index.js +7 -0
  13. package/dist/shared/pi-context-pressure/overflow.js +252 -0
  14. package/dist/shared/pi-context-pressure/sift-bridge.js +386 -0
  15. package/dist/shared/pi-context-pressure/telemetry.js +51 -0
  16. package/dist/worker/console/chat/pi-runtime.js +146 -5
  17. package/dist/worker/console/chat/provider-error.js +2 -1
  18. package/dist/worker/console/chat/routes.js +3 -0
  19. package/dist/worker/console/chat/sift-bridge.js +1 -0
  20. package/dist/worker/console/dag-execution-receipt.js +20 -2
  21. package/dist/worker/console/operator-actions.js +4 -3
  22. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bs-CDAXM.js → abnfDiagram-N423BO3Z-C_hkwt2J.js} +1 -1
  23. package/dist/worker/console/static/assets/{arc-CAkA3We3.js → arc-s2Q6r96B.js} +1 -1
  24. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DEmI_zqr.js → architectureDiagram-T3A2C74G-CtgsrwU1.js} +1 -1
  25. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-uDHANebE.js → blockDiagram-VBNYF7ZC-DZ-A7EiJ.js} +1 -1
  26. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Bv97b4_B.js → c4Diagram-5PPSVZJV-D2qv0Nyo.js} +1 -1
  27. package/dist/worker/console/static/assets/channel-BluaMT3-.js +1 -0
  28. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-shmQKsFz.js → chunk-2GRJ4B5K-D-UYpSaW.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BAraNvcN.js → chunk-2Q5K7J3B-DAS3jrrt.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BirhxUop.js → chunk-5RXB4S5H-CSbuRcGB.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C9LgUJpR.js → chunk-5VM5RSS4-BDH1ZO40.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DrU-s-t7.js → chunk-6Q2QTUOP-Dc2cqQIb.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Tx-H_FA2.js → chunk-GF5L2VYU-CkZuemIW.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BjLtNFrR.js → chunk-JWPE2WC7-URwjQnte.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-UNLVwSsy.js → chunk-KBJHAD2P-04RHylJK.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CBEUX8XD.js → chunk-RYQCIY6F-CvQX56Hh.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-WTL7IZOq.js → chunk-XXDRQBXY-dUF7qura.js} +1 -1
  38. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-H-ZpSafr.js +1 -0
  39. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-H-ZpSafr.js +1 -0
  40. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CkHG6W3s.js → cose-bilkent-JH36ORCC-COwbfxJE.js} +1 -1
  41. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BRzFqTaL.js → cynefin-VYW2F7L2-CaBEJekQ.js} +1 -1
  42. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2_CFp_i7.js → cynefinDiagram-MW4NZA55-COWUBdet.js} +1 -1
  43. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJtsmdkE.js → dagre-VZM6K2ZE-BLxVqTn4.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DNEFFo6w.js → diagram-7IWD3JNH-WL0yKfEQ.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B_IWV7eo.js → diagram-B4RE2ZJO-DaVkKCle.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DvvRx2dz.js → diagram-LBJQPF4R-DdTgnRGf.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CsibsWLg.js → diagram-Q27KOJAE-PG2Axo6A.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-UB23O5K3-les_djBQ.js → diagram-UB23O5K3-6vuQzlzI.js} +1 -1
  49. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BEm2n7qJ.js → ebnfDiagram-BXEA7PRR-BpXeBz-g.js} +1 -1
  50. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BI7vHHVm.js → erDiagram-JOGREHBK-PxtUnaa1.js} +1 -1
  51. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CUFCERrB.js → flowDiagram-UKHOOZJN-B--Qgvs1.js} +1 -1
  52. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-wxyLiGWt.js → ganttDiagram-PKOTCBZU-BIfQCgtx.js} +1 -1
  53. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bt897K8Z.js → gitGraphDiagram-DS77QQ5N-BRaX_E4j.js} +1 -1
  54. package/dist/worker/console/static/assets/index-Crrgl8Mx.css +1 -0
  55. package/dist/worker/console/static/assets/{index-DqJbO3-p.js → index-vPsp-idE.js} +102 -72
  56. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DYNx_IPv.js → infoDiagram-6WML65LV-Cd93r4Ab.js} +1 -1
  57. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CzpLNEu6.js → ishikawaDiagram-WSZJBQD7-C4c80iHt.js} +1 -1
  58. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-97f9owNU.js → journeyDiagram-NVQOT4AX-Ch5ajf3s.js} +1 -1
  59. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BxM9hyBn.js → kanban-definition-27J2QSJJ-DbpyW1Ut.js} +1 -1
  60. package/dist/worker/console/static/assets/{linear-ByuxcHvp.js → linear-BHVjBnTW.js} +1 -1
  61. package/dist/worker/console/static/assets/{mermaid.core-DwPGvpCv.js → mermaid.core-DO70kJ34.js} +5 -5
  62. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DCoqG9RT.js → mindmap-definition-FAOFIHXS-_P44Zpvs.js} +1 -1
  63. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BLSBmBMx.js → pegDiagram-VL7TDLO6-Bgi9FmEd.js} +1 -1
  64. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B0t_0oJW.js → pieDiagram-7S7Q4E2Y-C9InDfJn.js} +1 -1
  65. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js → quadrantDiagram-CIZ2JOQS-DtKrUouv.js} +1 -1
  66. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-B3jRdATC.js → railroadDiagram-AXF67PYL-zgAD_6Yo.js} +1 -1
  67. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-cTtXEN.js → requirementDiagram-LRYGKXZP-CQNO6kj4.js} +1 -1
  68. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B25igJRE.js → sankeyDiagram-W5VNT64P-knKu111P.js} +1 -1
  69. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-jW_lM1_D.js → sequenceDiagram-SI44F4Z6-D6tshiQ6.js} +1 -1
  70. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CrzTgsHE.js → sizeCapture-X5ZJPWSS-BMIWJcir.js} +1 -1
  71. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-B01MvlqE.js → stateDiagram-OKZ733FA-BP9wRDqc.js} +1 -1
  72. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-ClEhytQG.js +1 -0
  73. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-YcF12FiU.js → swimlanes-SLNWSIFB-DEkM5Dhe.js} +2 -2
  74. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPg5IMuu.js +8 -0
  75. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ClnRcg2s.js → timeline-definition-Z64GVDOM-ByBKsL35.js} +1 -1
  76. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B6gr_z-q.js → vennDiagram-T6HMQDX7-waFbNCUy.js} +1 -1
  77. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CvSgguGH.js → wardleyDiagram-T6FBY63Y-1ljiY0eZ.js} +1 -1
  78. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-23OtSZCb.js → xychartDiagram-ELKLHX3M-B4ZLHr_L.js} +1 -1
  79. package/dist/worker/console/static/index.html +2 -2
  80. package/dist/worker/observe/static/styles.css +9 -0
  81. package/dist/worker/observe/static/views/dag-inspector.js +40 -0
  82. package/dist/worker/observe/static/views/session-timeline.js +135 -0
  83. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +157 -7
  84. package/dist/workflows/dag/backend-test-pytest-collection.js +70 -2
  85. package/dist/workflows/dag/backend-test-result-contract.js +4 -0
  86. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  87. package/dist/workflows/dag/backend-test-writer-completeness.js +11 -0
  88. package/dist/workflows/dag/init-hybrid.js +2 -2
  89. package/docs/architecture/runtime-boundaries.md +2 -1
  90. package/docs/templates/backend-test-dag.json +4 -3
  91. package/package.json +3 -2
  92. package/dist/worker/console/static/assets/channel-amxUpk7o.js +0 -1
  93. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +0 -1
  94. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +0 -1
  95. package/dist/worker/console/static/assets/index-BHhUOTri.css +0 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +0 -1
  97. 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]);
@@ -1404,6 +1492,11 @@ def payload_expr_nodes(expr, assigned, parameters, seen=None):
1404
1492
  if isinstance(key,ast.Constant) and key.value == expr.slice.value:
1405
1493
  result.extend(shape_only(item) for item in payload_expr_nodes(value,assigned,parameters,set(seen)))
1406
1494
  return result
1495
+ if isinstance(expr,ast.IfExp):
1496
+ result=[]
1497
+ result.extend(payload_expr_nodes(expr.body,assigned,parameters,set(seen)))
1498
+ result.extend(payload_expr_nodes(expr.orelse,assigned,parameters,set(seen)))
1499
+ return result
1407
1500
  if isinstance(expr,ast.Call):
1408
1501
  result=[]
1409
1502
  if isinstance(expr.func,ast.Name) and expr.func.id in FUNCTION_PAYLOADS:
@@ -1444,6 +1537,30 @@ def decorator_payload_nodes(parameters,assigned):
1444
1537
  for value in values: result.extend(payload_expr_nodes(value,assigned,parameters))
1445
1538
  return result
1446
1539
 
1540
+ def decorator_payload_nodes_by_tp(fn,assigned):
1541
+ result={}
1542
+ for dec in fn.decorator_list:
1543
+ if not isinstance(dec,ast.Call) or not isinstance(dec.func,ast.Attribute) or dec.func.attr != 'parametrize' or len(dec.args) < 2: continue
1544
+ if not isinstance(dec.args[0],ast.Constant) or not isinstance(dec.args[0].value,str): continue
1545
+ names=[item.strip() for item in dec.args[0].value.split(',')]
1546
+ rows=dec.args[1]
1547
+ if isinstance(rows,ast.Name) and rows.id in GLOBAL_VALUES: rows=GLOBAL_VALUES[rows.id]
1548
+ for node in ast.walk(rows):
1549
+ if not isinstance(node,ast.Call) or not isinstance(node.func,ast.Attribute) or node.func.attr != 'param': continue
1550
+ tp=None
1551
+ for keyword in node.keywords:
1552
+ if keyword.arg == 'id' and isinstance(keyword.value,ast.Constant) and isinstance(keyword.value.value,str) and keyword.value.value.startswith('TP-'):
1553
+ tp=keyword.value.value
1554
+ if not tp: continue
1555
+ parameters={}
1556
+ for index,name in enumerate(names):
1557
+ if index < len(node.args): parameters.setdefault(name,[]).append(node.args[index])
1558
+ payload_nodes=decorator_payload_nodes(parameters,assigned)
1559
+ paths=set(); values={}
1560
+ for payload_node in payload_nodes: collect_dict(payload_node,'',paths,values)
1561
+ result[tp]={key:sorted(items) for key,items in sorted(values.items())}
1562
+ return result
1563
+
1447
1564
  def fallback_payload_nodes(fn):
1448
1565
  result=[]
1449
1566
  assigned={}
@@ -1473,7 +1590,7 @@ def fallback_payload_nodes(fn):
1473
1590
 
1474
1591
  def is_direct_transport_call(node):
1475
1592
  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
1593
+ if node.func.attr.lower() not in {'get','post','put','patch','delete','request','call'}: return False
1477
1594
  receiver_tokens=[]
1478
1595
  current=node.func.value
1479
1596
  while isinstance(current,ast.Attribute):
@@ -1521,6 +1638,7 @@ for fn in ast.walk(tree):
1521
1638
  'values':{key:sorted(items) for key,items in sorted(values.items())},
1522
1639
  'fallbackPaths':sorted(fallback_paths),
1523
1640
  'fallbackValues':{key:sorted(items) for key,items in sorted(fallback_values.items())},
1641
+ 'valuesByTestPoint':decorator_payload_nodes_by_tp(fn,{}),
1524
1642
  }
1525
1643
  print(json.dumps(out,ensure_ascii=True))
1526
1644
  `;
@@ -1651,6 +1769,7 @@ function pytestSymbols(script, source) {
1651
1769
  payloadValues: payloadShape.values,
1652
1770
  fallbackPayloadPaths: payloadShape.fallbackPaths ?? [],
1653
1771
  fallbackPayloadValues: payloadShape.fallbackValues ?? {},
1772
+ payloadValuesByTestPoint: payloadShape.valuesByTestPoint ?? {},
1654
1773
  };
1655
1774
  });
1656
1775
  }
@@ -1664,6 +1783,33 @@ function executionSignature(testCase) {
1664
1783
  .join("\n");
1665
1784
  return createHash("sha256").update(normalized).digest("hex");
1666
1785
  }
1786
+ function normalizePythonLiteralValue(value) {
1787
+ const trimmed = value.trim();
1788
+ if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) {
1789
+ return trimmed.slice(1, -1);
1790
+ }
1791
+ return trimmed;
1792
+ }
1793
+ function intentionalInvalidEnumVariant(input) {
1794
+ if (!input.testCase.testPointBindings.some((binding) => binding.mode === "variant" && binding.testPoint === input.testPoint))
1795
+ return false;
1796
+ const inferred = inferScenarioParamIntent({ tpId: input.testPoint, caseBody: input.testCase.body });
1797
+ if (inferred.intentSource !== "machine-line" || inferred.field?.toLowerCase() !== input.field.toLowerCase())
1798
+ return false;
1799
+ const observed = normalizePythonLiteralValue(input.observedValue);
1800
+ if (input.allowed.includes(observed))
1801
+ return false;
1802
+ if (inferred.intent === "enum-invalid" || inferred.intent === "wrong-type")
1803
+ return true;
1804
+ if (inferred.intent === "null")
1805
+ return observed === "None" || observed === "null";
1806
+ if (inferred.intent === "empty")
1807
+ return observed === "";
1808
+ if (!inferred.intent.startsWith("custom-literal:"))
1809
+ return false;
1810
+ const expected = normalizePythonLiteralValue(inferred.example ?? inferred.intent.slice("custom-literal:".length));
1811
+ return observed === expected;
1812
+ }
1667
1813
  function assessPayloadContract(testCase, refs) {
1668
1814
  const requiredPaths = testCase.payloadContract.requiredPaths;
1669
1815
  const directObservedPaths = orderedUnique(refs.flatMap((item) => item.payloadPaths));
@@ -1685,13 +1831,17 @@ function assessPayloadContract(testCase, refs) {
1685
1831
  ? observedPaths.filter((item) => !allowedPaths.includes(item) && !allowedPaths.some((allowed) => item.startsWith(`${allowed}.`)))
1686
1832
  : [];
1687
1833
  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
1834
  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)
1835
+ const observed = orderedUnique(refs.flatMap((item) => (useFallback ? item.fallbackPayloadValues[field] : item.payloadValues[field]) ?? []));
1836
+ for (const rawValue of observed) {
1837
+ const value = normalizePythonLiteralValue(rawValue);
1838
+ if (allowed.includes(value))
1839
+ continue;
1840
+ const associatedTestPoints = orderedUnique(refs.flatMap((item) => Object.entries(item.payloadValuesByTestPoint)
1841
+ .filter(([, values]) => (values[field] ?? []).includes(rawValue))
1842
+ .map(([testPoint]) => testPoint)));
1843
+ const intentionallyInvalid = associatedTestPoints.length > 0 && associatedTestPoints.every((testPoint) => intentionalInvalidEnumVariant({ testCase, testPoint, field, observedValue: rawValue, allowed }));
1844
+ if (!intentionallyInvalid)
1695
1845
  enumMismatches.push(`${field}=${value}; allowed=${allowed.join("|")}`);
1696
1846
  }
1697
1847
  }
@@ -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,
@@ -936,6 +939,7 @@ export async function materializeBackendTestResultFromPytestHtml(input) {
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,