@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
@@ -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) {
@@ -4520,7 +4520,7 @@ async function buildBackendTestHybridDag(sources) {
4520
4520
  "Coverage priority is strict inside the declared scope: P0 product requirements/task hard constraints always remain in scope; P1 exhaustively supplements documented operations, fields, business rules, statuses and errors only for Affected Operations; P2 adds bounded protocol robustness only when it is relevant to the change and does not invent product behavior. Coverage percentages describe the declared affected scope, never whole-API completeness unless every operation is explicitly listed. Conflicts or undefined expectations must stay visible as GAP/CONFLICT with precise source pointers, never guessed.",
4521
4521
  "For uniqueness/lifecycle rules cover absent, active-existing, deleted-existing, create-delete-recreate, restore-then-recreate and documented scope/case-normalization states. For every enum cover every valid value plus bounded invalid equivalence classes (unknown, case variant, whitespace, empty, null/missing and wrong types as applicable). For every length/number rule cover min-1, min, nominal, max and max+1. For format rules cover each allowed class separately plus a valid mixed value, and representative forbidden classes including uppercase, internal/leading/trailing whitespace, tab/newline, unsupported punctuation, slash, emoji or control characters when the source contract supports that expectation.",
4522
4522
  "Mandatory module index: include a `## Module Index` table in the plan artifact that lists every planned module as a canonical relative link of the exact form `[label](./<stem>.md)` plus a `testcase/md/<stem>.md` path cell, so a downstream deterministic manifest can parse the module list. Group by stable business resource/domain, not by CRUD operation: one resource's list/detail/create/update/delete cases belong in one module such as `resource_notes`; split only when a single module would exceed the per-child 16K output protocol, keep the total module count at the smallest safe value, and never exceed 8 modules. Name each module file with a stable lowercase business stem such as `health` or `resource_notes`. Pure hexadecimal/hash-like opaque stems such as `a401606` or `deadbeef` are forbidden. Do not use priority-only stems `p0`, `p1` or `p2`; Priority belongs only in the Coverage Matrix and never defines module files. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. The relative link target MUST equal the on-disk filename stem the sharded writer will create. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
4523
- "Scenario Partitions (query/filter axes): for every affected GET/list operation, declare one row per enum or classification axis used for filtering (query/path parameters such as type/status/category). Add a mandatory machine-readable `## Scenario Partitions` section after the Coverage Matrix using exactly `| Partition ID | Operation | Axis | Domain | Required Slots | Expected by Slot | Bind Rule |` with the separator row. Partition ID is a stable `SP-<OPERATION>-<AXIS>` token; Domain must copy the legal values verbatim from the bound OpenAPI enum or requirement sentence (never guess), using bare semicolon-separated identifier values inside the single table cell (for example `ACTIVE; ARCHIVED`, with no Markdown backticks or prose); Required Slots writes `each-value` plus `omitted` only when the parameter is optional; Expected by Slot states the documented expectation per slot kind (`domain-value`, `default-behavior`, `empty-result`/`excluded-result` when documented, or `GAP` when the source does not document the complement expectation — never invent 空列表/400). POST/PUT body field-validation enums stay in the Coverage Matrix as `TP-<FIELD>-ENUM-*` and MUST NOT get a Scenario Partition row. Do not create partitions for axes without a documented legal-value domain. Only GET/list query or path parameters whose bound source documents a finite enum or classification set may become a Scenario Partition. Do not create partitions for free-form strings, primary keys, required-or-optional-only parameters, or boundary/format-only axes. If an axis has no finite legal-value domain, do not declare a Partition row and do not invent NOT-IN-SET cases. Cross-axis combinations stay as ONE nominal Case; never declare a cross-axis cartesian partition.",
4523
+ "Scenario Partitions (query/filter axes): for every affected GET/list operation, declare one row per enum or classification axis used for filtering (query/path parameters such as type/status/category). Add a mandatory machine-readable `## Scenario Partitions` section after the Coverage Matrix using exactly `| Partition ID | Operation | Axis | Domain | Required Slots | Expected by Slot | Bind Rule |` with the separator row. Partition ID is a stable `SP-<OPERATION>-<AXIS>` token; Domain must copy the legal values verbatim from the bound OpenAPI enum or requirement sentence (never guess), using bare semicolon-separated identifier values inside the single table cell (for example `ACTIVE; ARCHIVED`, with no Markdown backticks or prose); Required Slots must contain `each-value` and exactly one `not-in-set`, plus `omitted` only when the parameter is optional; Expected by Slot states the documented expectation per slot kind (`domain-value`, `default-behavior`, `empty-result`/`excluded-result` when documented, or `GAP` when the source does not document the complement expectation — never invent 空列表/400). POST/PUT body field-validation enums stay in the Coverage Matrix as `TP-<FIELD>-ENUM-*` and MUST NOT get a Scenario Partition row. Do not create partitions for axes without a documented legal-value domain. Only GET/list query or path parameters whose bound source documents a finite enum or classification set may become a Scenario Partition. Do not create partitions for free-form strings, primary keys, required-or-optional-only parameters, or boundary/format-only axes. If an axis has no finite legal-value domain, do not declare a Partition row and do not invent NOT-IN-SET cases. Cross-axis combinations stay as ONE nominal Case; never declare a cross-axis cartesian partition.",
4524
4524
  "Before finalizing README, calculate the predicted collected-item count as `sum(max(1, number of variant Test Points in each Case))`. If the task declares an item budget, the prediction must not exceed it. Reduce excess only by removing duplicate execution and converting same-request checkpoints to assertions; never drop required rules, boundaries, enums, operation-specific inputs, or business states. Record the prediction in README. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
4525
4525
  ...(sharedSetupPrompt ? [sharedSetupPrompt] : []),
4526
4526
  intake.boundedSourceContext,
@@ -4815,7 +4815,7 @@ async function buildBackendTestHybridDag(sources) {
4815
4815
  writerOutcomePolicy: { type: "implementation-outcome-v1", requireChangedFiles: true },
4816
4816
  outputContract: "First non-empty line is IMPLEMENTATION_OUTCOME: changed|blocked, followed by a concise repair summary. This node runs only for REPAIRABLE initial facts, so already-satisfied is invalid and a successful outcome requires a non-empty bounded diff. Modify only generated pytest scripts/helpers/factories and preserve every Markdown Case, Test Point, primary symbol and assertion meaning.",
4817
4817
  subtask_prompt: [
4818
- "Repair the generated backend pytest asset as one bounded program using the direct upstream collection assessment. This is the only repair attempt and happens before any business test body execution. Treat any upstream line such as `Repair paths: testcase/test_x.py` as complete authoritative repairPaths evidence. Directly read and edit that testcase path; do not search for separate root-level `contracts/**`, guess a DAG run directory, or require another report artifact. If the read tool successfully returns the testcase file, the path exists—continue the bounded repair and never later claim that file is absent.",
4818
+ "Repair the generated backend pytest asset as one bounded program using the direct upstream collection assessment. This is the only repair attempt and happens before any business test body execution. The direct upstream JSON includes authoritative `repairPaths` and bounded `repairFindings`; treat both as the complete mandatory checklist without searching for a run directory or report file. Treat any upstream line such as `Repair paths: testcase/test_x.py` as equivalent authoritative repairPaths evidence. Directly read and edit that testcase path; do not search for separate root-level `contracts/**`, guess a DAG run directory, or require another report artifact. If the read tool successfully returns the testcase file, the path exists—continue the bounded repair and never later claim that file is absent.",
4819
4819
  "Initial status REPAIRABLE means at least one listed finding remains: `already-satisfied` is forbidden, and you must produce a non-empty bounded diff on repairPaths before returning `IMPLEMENTATION_OUTCOME: changed`. Fix only readiness-proven generated testcase-local defects on initial facts repairPaths: create exact safe missing mapped test_*.py paths, repair syntax/import/symbol/decorator/parameterization, close generated fixture dependencies/plugin registration, and repair initial Markdown-to-pytest correspondence findings. Use this deterministic repair map instead of reading analyzer implementation: findings about `Case-ID`, `Assertion-Test-Points`, or `Cross-Cutting-Test-Points` are fixed by editing the declared primary symbol docstring metadata lines; variant binding findings are fixed in the literal direct `pytest.param(..., id=\"TP-...\")` row; primary-symbol cardinality/name findings are fixed in the function name or duplicate primary symbols; script mismatch is fixed only on the authoritative assessment repairPaths; payload findings are fixed in request payload construction. Do not read controller `src/**` or inspect JS/TS analyzer code. Do not search for `testcase/**/README.md`. Never invent a business pytest symbol for evidence-only Markdown Cases that declare `脚本/primary symbol=无` with empty variants. For fixture defects inspect both provider and importer listed by repairPaths; fix ScopeMismatch by aligning fixture scopes or inlining request-scoped values so module fixtures never depend on function fixtures; when a shared fixture depends on sibling fixtures, register the whole provider module through an exact pytest_plugins declaration rather than importing only the outer fixture. Do not create unrelated pytest scripts.",
4820
4820
  "This is the single pytest incremental synchronization round. The `Findings` in `reports/backend-test-pytest-collection-initial.md` are the mandatory repair checklist: resolve every repairable listed finding on every authoritative `Repair paths` file before considering any other advisory evidence, and never substitute an unrelated scenario-param cleanup for a listed correspondence/collection defect. For every assessment-listed path, compare the effective Markdown Case/Test Points/test data and its `Payload Contract`/`Payload Required Paths`/`Payload Allowed Paths`/`Payload Enum` labels with the generated module. Incrementally add or repair only missing symbols, params, assertions and payload builders. Repair every assessment-listed missing nested path, unexpected key and enum mismatch; preserve exact DTO keys, nested shapes, enum/boundary literals, operation transport and business preconditions; remove guessed replacement keys only when the effective Markdown proves the exact contract. Keep path/query/header identifiers and scenario-control metadata separate from DTO patches and JSON bodies; an `id` used for a path target must be passed to the request path/helper, never inserted into a body patch unless `id` is explicitly listed in Payload Allowed Paths. Flatten every variant into a literal direct `pytest.param(..., id=\"TP-...\")` row; replace `_post_case`/`_put_case` or other parameter-row factories because correspondence and scenario readiness require the actual row values and IDs to be statically visible. Also repair helper call sites to match their defined return signatures; do not tuple-unpack a helper that returns one scalar value.",
4821
4821
  "Preserve final testcase/md/** semantics, every Case ID, Rule/Test Point binding, primary symbol, parameter ID, expected status/body/schema assertion, HTTP logging, redaction and truncation behavior.",
@@ -207,7 +207,8 @@ Runtime 变更另需 `npm run typecheck` 及对应 targeted Vitest(见 exec pl
207
207
  | --- | --- | --- |
208
208
  | OpenCode transient-retry | `.opencode/plugins/loop-agent-transient-retry.js`(init surface `generated`) | 仅瞬态 UnknownError resume;busy/retry 门控 + backoff;overflow/401/配额/取消/业务标 permanent(`plugin-ignore-permanent-error`),**不** compact |
209
209
  | OpenCode overflow-compact | `.opencode/plugins/loop-agent-context-overflow-compact.js`(init surface `generated`) | 仅 context overflow → `session.compact`(无则 `summarize`)+ 有限 `promptAsync` 续接;`MAX_OVERFLOW_RECOVERIES ≤ 2`;session 锁;非 overflow 不动作 |
210
- | Pi project extension | `.pi/extensions/loop-agent-context-overflow.js`(init surface `generated`) | `message_end` 上将 overflow `errorMessage` 幂等改写为 `context_length_exceeded:` 前缀,触发 Pi 原生 compact/retry;**不**写 `~/.pi` settings / compaction / 模型窗口 |
210
+ | Pi project extension | `.pi/extensions/loop-agent-context-overflow.js`(init surface `generated`) | `message_end` 上将 overflow `errorMessage` 幂等改写为 `context_length_exceeded:` 前缀,触发 Pi 原生 compact/retry;分类器与控制器共享 P1–P10 / N1–N5 语料(含嵌套 JSON);**不**写 `~/.pi` settings / compaction / 模型窗口,**不**投影 sift native |
211
+ | Console / DAG 运行时 | npm 内置 inline extension + 可选 `@agent-context/sift` | 默认开启 L0/L1/L2;`HARNESS_CONTEXT_PRESSURE_MODE=off` 关 L0/L2;`HARNESS_CONSOLE_SIFT` / `HARNESS_DAG_SIFT` 的 `0\|false\|off` 关 L1。DAG 不做 streaming 主动 compact。不进入目标项目 init 文件 |
211
212
  | Pi 用户配置 | `~/.pi/agent/settings.json` | **不**进入项目 `.harness/init-surface.json` hash;仅 `--client-recovery=user` 可字段级补缺并原子写;`auto`/`project`/`off` 与 `check-update` 默认零写 home;读取时只有 `ENOENT` 视为缺文件,其他 I/O 错误 fail closed |
212
213
  | CLI mode | `--client-recovery=auto\|project\|user\|off`(默认 `auto`) | `auto`/`project` 安装上述三份项目产物;`user` = 三产物 + 显式 Pi retry 合并;`off` 全跳过 |
213
214
  | Ownership | recorded sha256 + apply-safe | 三份 generated 缺失可补、与 recorded hash 一致可升级;用户改过 → model merge / human decision,禁止静默覆盖 |