@tea-agent/loop-agent 0.42.0-next.6 → 0.42.0-next.8

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 (115) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/executors/dag-pi-executor.js +74 -13
  5. package/dist/executors/shell-executor.js +3 -1
  6. package/dist/task/source-prepare/fragment-inventory.js +49 -16
  7. package/dist/task/source-prepare/prepare.js +37 -20
  8. package/dist/worker/console/chat/model-resolver.js +7 -4
  9. package/dist/worker/console/chat/pi-runtime.js +60 -5
  10. package/dist/worker/console/chat/routes.js +69 -46
  11. package/dist/worker/console/chat/sdd-data-alignment.js +6 -3
  12. package/dist/worker/console/chat/session-store.js +16 -0
  13. package/dist/worker/console/pi-readiness.js +3 -2
  14. package/dist/worker/console/server.js +19 -1
  15. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BJpsGgAv.js → abnfDiagram-N423BO3Z-C6n9_m0P.js} +1 -1
  16. package/dist/worker/console/static/assets/{arc-DjMFPsEd.js → arc-DQh-IfZ1.js} +1 -1
  17. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DqdZlr1O.js → architectureDiagram-T3A2C74G-54NnrwUC.js} +1 -1
  18. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BmZnfFEO.js → blockDiagram-VBNYF7ZC-pivRALGK.js} +1 -1
  19. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-3cKberpL.js → c4Diagram-5PPSVZJV-BR7OV2NJ.js} +1 -1
  20. package/dist/worker/console/static/assets/channel-DsZxrgqe.js +1 -0
  21. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-q-0yjQ1h.js → chunk-2GRJ4B5K-B1Aq6BcK.js} +1 -1
  22. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DnONBaDW.js → chunk-2Q5K7J3B-CbdU0rjo.js} +1 -1
  23. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-v3a6PCOS.js → chunk-5RXB4S5H-Dqi2GJeD.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D1kLrlU_.js → chunk-5VM5RSS4-BYn1Hu4R.js} +1 -1
  25. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BPelzihi.js → chunk-6Q2QTUOP-DRLjDL0k.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DauNE69H.js → chunk-GF5L2VYU-CY9Xx2jV.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-RIg5_bFJ.js → chunk-JWPE2WC7-BNCZs7_z.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BuILuoxs.js → chunk-KBJHAD2P-DZ8AStLL.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-VHORV5oW.js → chunk-RYQCIY6F-DsxjYrzz.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BJYr4xHC.js → chunk-XXDRQBXY-Ddx1KC1l.js} +1 -1
  31. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +1 -0
  32. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +1 -0
  33. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BS6nuqQn.js → cose-bilkent-JH36ORCC-W9TveCnK.js} +1 -1
  34. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BBhF0NAT.js → cynefin-VYW2F7L2-l9j_ztZH.js} +1 -1
  35. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D1135Ifg.js → cynefinDiagram-MW4NZA55-BMJMKi4G.js} +1 -1
  36. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-vAOYPUWJ.js → dagre-VZM6K2ZE-Bb4mG9pH.js} +1 -1
  37. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DuaSPkfa.js → diagram-7IWD3JNH-BMgL_Qv0.js} +1 -1
  38. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-mMvXvGU6.js → diagram-B4RE2ZJO-Bvo2T4OQ.js} +1 -1
  39. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDIEJtlL.js → diagram-LBJQPF4R-_5kWRN9c.js} +1 -1
  40. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Bi_6JvCW.js → diagram-Q27KOJAE-DqdMrltM.js} +1 -1
  41. package/dist/worker/console/static/assets/{diagram-UB23O5K3-DAqj57yN.js → diagram-UB23O5K3-CDDYsNkp.js} +1 -1
  42. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DfWaxNEo.js → ebnfDiagram-BXEA7PRR-c4nfnZUV.js} +1 -1
  43. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bt7QqkNG.js → erDiagram-JOGREHBK-DnkUcNMs.js} +1 -1
  44. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CRRhQwT2.js → flowDiagram-UKHOOZJN-Dz0KGLZE.js} +1 -1
  45. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DAV1O5B6.js → ganttDiagram-PKOTCBZU-Cj1t1uka.js} +1 -1
  46. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-0X2Qo4Lp.js → gitGraphDiagram-DS77QQ5N-tp2FrBHd.js} +1 -1
  47. package/dist/worker/console/static/assets/index-D9Sc0f0y.js +449 -0
  48. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C5W5eILC.js → infoDiagram-6WML65LV-DQwJS8DH.js} +1 -1
  49. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-5SfqU-Tb.js → ishikawaDiagram-WSZJBQD7-eimCQWD4.js} +1 -1
  50. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CKDRcaKf.js → journeyDiagram-NVQOT4AX-Dd9Gbwgv.js} +1 -1
  51. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Cc7_TuSb.js → kanban-definition-27J2QSJJ-Qve3cyft.js} +1 -1
  52. package/dist/worker/console/static/assets/{linear-BcQYYmeT.js → linear-UXKSe36Z.js} +1 -1
  53. package/dist/worker/console/static/assets/{mermaid.core-CwLZPtQo.js → mermaid.core-CWhj4JXN.js} +5 -5
  54. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D29M025z.js → mindmap-definition-FAOFIHXS-yTcwf4SJ.js} +1 -1
  55. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CV-ZAWYe.js → pegDiagram-VL7TDLO6-_7qToaZP.js} +1 -1
  56. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-C6iPS_o5.js → pieDiagram-7S7Q4E2Y-CXmrKmDm.js} +1 -1
  57. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BFCD1sTg.js → quadrantDiagram-CIZ2JOQS-hMmrJyaS.js} +1 -1
  58. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CztNj4O9.js → railroadDiagram-AXF67PYL-BXw8tCBe.js} +1 -1
  59. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BWIELSNd.js → requirementDiagram-LRYGKXZP-BmjgnLZl.js} +1 -1
  60. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CPtxrjJQ.js → sankeyDiagram-W5VNT64P-Bj77SPpN.js} +1 -1
  61. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BEWVSFsu.js → sequenceDiagram-SI44F4Z6-B2FDVysL.js} +1 -1
  62. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DyhzoX_J.js → sizeCapture-X5ZJPWSS-UChNY9ZZ.js} +1 -1
  63. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-xLox0nu7.js → stateDiagram-OKZ733FA-wbAEkbd7.js} +1 -1
  64. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +1 -0
  65. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CLC_ow9A.js → swimlanes-SLNWSIFB-DCEkU8HR.js} +2 -2
  66. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +8 -0
  67. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DmJZ4uyY.js → timeline-definition-Z64GVDOM-BGp7H06U.js} +1 -1
  68. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DD7GGi9Z.js → vennDiagram-T6HMQDX7-DsCIQuWR.js} +1 -1
  69. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Bi8PQoGt.js → wardleyDiagram-T6FBY63Y-cRZLP4Xe.js} +1 -1
  70. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-uKdAY8Dp.js → xychartDiagram-ELKLHX3M-CP0xLR9Y.js} +1 -1
  71. package/dist/worker/console/static/index.html +1 -1
  72. package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -5
  73. package/dist/worker/console/static-src/operator-chat/input-history.js +8 -6
  74. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +10 -0
  75. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +31 -2
  76. package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -3
  77. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +29 -6
  78. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +8 -2
  79. package/dist/worker/console/static-src/shell/console-update-reload.js +36 -9
  80. package/dist/worker/console/static-src/shell/workspace-route.js +11 -0
  81. package/dist/worker/observe/static/dag-helpers.js +4 -2
  82. package/dist/worker/observe/static/inspect-workspace.js +23 -0
  83. package/dist/worker/observe/static/kpi.js +2 -2
  84. package/dist/worker/observe/static/operator-chrome.d.ts +10 -2
  85. package/dist/worker/observe/static/operator-chrome.js +37 -23
  86. package/dist/worker/observe/static/relations.js +2 -2
  87. package/dist/worker/observe/static/router.d.ts +12 -1
  88. package/dist/worker/observe/static/router.js +48 -6
  89. package/dist/worker/observe/static/run-processing.js +4 -2
  90. package/dist/worker/observe/static/shell-chrome.js +2 -2
  91. package/dist/worker/observe/static/state.js +5 -0
  92. package/dist/worker/observe/static/styles.css +95 -0
  93. package/dist/worker/observe/static/views/dag-inspector.js +14 -7
  94. package/dist/worker/observe/static/views/dag.js +10 -2
  95. package/dist/worker/observe/static/views/dags.js +3 -1
  96. package/dist/worker/observe/static/views/dashboard.js +14 -6
  97. package/dist/worker/observe/static/views/pool.js +7 -2
  98. package/dist/worker/observe/static/views/run.js +12 -3
  99. package/dist/worker/observe/static/views/session-timeline.js +69 -10
  100. package/dist/worker/observe/static/views/task.js +7 -2
  101. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +40 -9
  102. package/dist/workflows/dag/backend-test-scenario-param.js +126 -19
  103. package/dist/workflows/dag/backend-test-scenario-partitions.js +62 -1
  104. package/dist/workflows/dag/backend-test-writer-completeness.js +7 -3
  105. package/dist/workflows/dag/frontend-shadow-dual-write.js +33 -6
  106. package/dist/workflows/dag/init-hybrid.js +175 -27
  107. package/dist/workflows/dag/node-execution.js +1 -1
  108. package/dist/workflows/dag/types.js +1 -1
  109. package/package.json +3 -2
  110. package/dist/worker/console/static/assets/channel-DWYwciiv.js +0 -1
  111. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D6jOf_l4.js +0 -1
  112. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D6jOf_l4.js +0 -1
  113. package/dist/worker/console/static/assets/index-CVdddQsA.js +0 -451
  114. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DS5ZRvJG.js +0 -1
  115. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Dvpxn-bf.js +0 -8
@@ -59,6 +59,7 @@ const CLOSED_SET = new Set([
59
59
  "null",
60
60
  "min-1",
61
61
  "min",
62
+ "max-1",
62
63
  "max",
63
64
  "max+1",
64
65
  "pattern-invalid",
@@ -155,14 +156,19 @@ function parseScenarioBoundToken(raw) {
155
156
  function parseScenarioIntentLine(line) {
156
157
  // Target may contain commas (body.title,body.status). Intent may contain `|` / spaces.
157
158
  // Stop field values only at known trailing keys or end-of-line.
158
- const precise = /场景意图\s*[::]\s*(TP-[A-Z0-9-]+)\s*[|;]\s*operation\s*=\s*([^;\n]+?)\s*[|;]\s*target\s*=\s*([^;\n]+?)\s*[|;]\s*intent\s*=\s*([^;\n]+?)(?:\s*[|;]\s*bound\s*=\s*([^\s;|\n]+))?(?:\s*[|;]\s*example\s*=\s*([^;\n]+))?(?:\s*[|;]\s*expectedCode\s*=\s*[^;\n]+)?\s*$/i.exec(line.trim());
159
+ const unfenced = line
160
+ .trim()
161
+ .replace(/^[-*+]\s+/, "")
162
+ .replace(/^`(.+)`$/, "$1")
163
+ .trim();
164
+ const precise = /场景意图\s*[::]\s*(TP-[A-Z0-9-]+)\s*[|;]\s*operation\s*=\s*([^;\n]+?)\s*[|;]\s*target\s*=\s*([^;\n]+?)\s*[|;]\s*intent\s*=\s*([^;\n]+?)(?:\s*[|;]\s*bound\s*=\s*([^\s;`|\n]+))?(?:\s*[|;]\s*example\s*=\s*([^;\n]*))?(?:\s*[|;]\s*expectedCode\s*=\s*[^;\n]+)?\s*$/i.exec(unfenced);
159
165
  if (precise) {
160
166
  return {
161
167
  tpId: precise[1],
162
168
  rawIntent: (precise[4] ?? "unknown").trim(),
163
169
  target: precise[3]?.trim(),
164
170
  bound: parseScenarioBoundToken(precise[5]),
165
- example: precise[6]?.trim(),
171
+ example: precise[6] === undefined ? undefined : precise[6].trim(),
166
172
  legacy: false,
167
173
  };
168
174
  }
@@ -192,6 +198,53 @@ function resolveScenarioIntentField(target, field) {
192
198
  target.split(/\s*,\s*/)[0];
193
199
  return preferred?.split(".").at(-1);
194
200
  }
201
+ function normalizeLengthIntentAlias(rawIntent) {
202
+ const lower = rawIntent.trim().toLowerCase();
203
+ if (lower === "max-1" || lower === "maxminus1" || lower === "max_minus_1") {
204
+ return "max-1";
205
+ }
206
+ if (lower === "min-1" || lower === "minminus1" || lower === "min_minus_1") {
207
+ return "min-1";
208
+ }
209
+ return lower;
210
+ }
211
+ function proseLengthBound(haystack, field) {
212
+ const fieldAlt = field ? field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : undefined;
213
+ const scoped = fieldAlt ? new RegExp(`${fieldAlt}[^\\n]{0,160}`, "ig") : undefined;
214
+ const windows = [];
215
+ if (scoped) {
216
+ for (const match of haystack.matchAll(scoped)) {
217
+ windows.push(match[0] ?? "");
218
+ }
219
+ }
220
+ windows.push(haystack);
221
+ let max;
222
+ let min;
223
+ const proseMaxRe = /(\d{1,4})\s*个字符的最大/i;
224
+ for (const window of windows) {
225
+ const proseMax = proseMaxRe.exec(window);
226
+ if (proseMax && max === undefined)
227
+ max = Number(proseMax[1]);
228
+ const nums = [...window.matchAll(/(?<![+\-])\d{1,4}/g)]
229
+ .map((item) => Number(item[0]))
230
+ .filter((item) => Number.isFinite(item));
231
+ for (let i = 0; i + 2 < nums.length; i += 1) {
232
+ const a = nums[i];
233
+ const b = nums[i + 1];
234
+ const c = nums[i + 2];
235
+ // Prefer n-1,n,n+1 around a documented max (100/2000), not 0,1,99 or 99,100,101's first two plus leftover.
236
+ if (b === a + 1 && c === b + 1 && b >= 10) {
237
+ max ??= b;
238
+ break;
239
+ }
240
+ }
241
+ if (nums.includes(0) && nums.includes(1))
242
+ min ??= 1;
243
+ if (max !== undefined && min !== undefined)
244
+ break;
245
+ }
246
+ return { max, min };
247
+ }
195
248
  function resolveCustomLiteralIntent(rawIntent, field) {
196
249
  const lower = rawIntent.toLowerCase();
197
250
  if (!lower.startsWith("custom-literal:")) {
@@ -250,7 +303,7 @@ export function inferScenarioParamIntent(input) {
250
303
  const field = resolveScenarioIntentField(machine.target, machine.field);
251
304
  const rawIntent = machine.rawIntent.trim();
252
305
  const example = machine.example;
253
- const lower = rawIntent.toLowerCase();
306
+ const lower = normalizeLengthIntentAlias(rawIntent);
254
307
  let normalizedIntent = lower === "nominal-operation" ? "nominal" : lower;
255
308
  // Writers sometimes mark a short "typical" length sample as min/max with bound=LEN<nominal>.
256
309
  // Treat that as nominal so a short helper/string is not forced to equal maxLength.
@@ -273,16 +326,16 @@ export function inferScenarioParamIntent(input) {
273
326
  new RegExp(`(?:minLength|min(?:imum)?|下限|最小长度)\\s*[=::]?\\s*(\\d{1,4})[^\\n]{0,80}?${fieldEsc}`, "i").exec(haystack) ??
274
327
  /(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(haystack)
275
328
  : /(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(haystack);
329
+ const proseBound = proseLengthBound(haystack, field);
276
330
  const prefersMinBound = normalizedIntent === "min" ||
277
331
  normalizedIntent === "min-1" ||
278
332
  /(?:MIN|LEN1|LENGTH-?1|最短|最小)/i.test(tpId);
279
- const boundFromBody = prefersMinBound
280
- ? minBoundFromBody ?? maxBoundFromBody
281
- : maxBoundFromBody ?? minBoundFromBody;
333
+ const boundFromBody = prefersMinBound ? minBoundFromBody : maxBoundFromBody ?? minBoundFromBody;
282
334
  const boundFromTp = /(?:LEN(?:GTH)?|长度)[_-]?(\d{1,5})(?:[_-]|$)/i.exec(tpId) ??
283
- /(?:MIN|MAX)[_-]?(\d{1,5})(?:[_-]|$)/i.exec(tpId);
335
+ /(?:MIN|MAX)[_-](\d{2,5})(?:[_-]|$)/i.exec(tpId);
284
336
  const lengthIntent = normalizedIntent === "min" ||
285
337
  normalizedIntent === "min-1" ||
338
+ normalizedIntent === "max-1" ||
286
339
  normalizedIntent === "max" ||
287
340
  normalizedIntent === "max+1";
288
341
  // Length intents: explicit machine bound >0, then TP-encoded LEN1/LEN100,
@@ -294,13 +347,35 @@ export function inferScenarioParamIntent(input) {
294
347
  : normalizedIntent === "max+1"
295
348
  ? Math.max(0, boundFromTpNumber - 1)
296
349
  : boundFromTpNumber;
297
- const bound = machine.bound && machine.bound > 0
298
- ? machine.bound
299
- : lengthIntent && boundFromTpNormalized !== undefined
300
- ? boundFromTpNormalized
301
- : boundFromBody
302
- ? Number(boundFromBody[1])
303
- : machine.bound;
350
+ const boundFromProse = prefersMinBound ? proseBound.min : proseBound.max;
351
+ const offsetIntent = normalizedIntent === "max-1" ||
352
+ normalizedIntent === "max+1" ||
353
+ normalizedIntent === "min-1";
354
+ const contractBound = boundFromBody
355
+ ? Number(boundFromBody[1])
356
+ : boundFromProse !== undefined && boundFromProse >= 0
357
+ ? boundFromProse
358
+ : undefined;
359
+ const machineBoundPositive = typeof machine.bound === "number" && Number.isFinite(machine.bound) && machine.bound > 0;
360
+ const minLengthFromBody = prefersMinBound && minBoundFromBody ? Number(minBoundFromBody[1]) : undefined;
361
+ const machineBoundZeroMin = prefersMinBound &&
362
+ machine.bound === 0 &&
363
+ minLengthFromBody === undefined &&
364
+ boundFromTpNormalized === undefined;
365
+ const bound = offsetIntent && contractBound !== undefined && contractBound > 0
366
+ ? contractBound
367
+ : machineBoundPositive && !offsetIntent
368
+ ? machine.bound
369
+ : lengthIntent && boundFromTpNormalized !== undefined
370
+ ? boundFromTpNormalized
371
+ : minLengthFromBody !== undefined && minLengthFromBody >= 0
372
+ ? minLengthFromBody
373
+ : machineBoundZeroMin
374
+ ? 0
375
+ : contractBound !== undefined &&
376
+ (prefersMinBound ? contractBound >= 0 : contractBound > 0)
377
+ ? contractBound
378
+ : undefined;
304
379
  if (rawIntent.toLowerCase().startsWith("custom-literal:")) {
305
380
  const resolved = resolveCustomLiteralIntent(rawIntent, field);
306
381
  const rawLiteral = rawIntent.slice(rawIntent.indexOf(":") + 1).trim();
@@ -387,13 +462,15 @@ export function inferScenarioParamIntent(input) {
387
462
  const boundMatch = fieldScopedBound ??
388
463
  /(?:maxLength|max(?:imum)?|上限|最大长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`) ??
389
464
  /(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`);
390
- const bound = boundMatch ? Number(boundMatch[1]) : undefined;
465
+ const proseBound = proseLengthBound(`${caseBody}
466
+ ${dataSection}`, field);
467
+ const bound = boundMatch ? Number(boundMatch[1]) : proseBound.max ?? proseBound.min;
391
468
  const exampleMatch = /(?:反例|invalid example|example)\s*[=::]\s*[`"]?([^`"\n]+)[`"]?/i.exec(caseBody);
392
469
  const example = exampleMatch?.[1]?.trim();
393
470
  if (/EMPTY|空白|空串|空字符串/.test(upper) || /空字符串|空串|empty string/i.test(caseBody)) {
394
471
  return { intent: "empty", field, bound, example, intentSource: "tp-fallback" };
395
472
  }
396
- if (/MISSING|缺省|缺失|省略/.test(upper) || /缺少字段|缺失字段|missing field/i.test(caseBody)) {
473
+ if (/MISSING|OMITTED|\bOMIT\b|缺省|缺失|省略/.test(upper) || /缺少字段|缺失字段|missing field/i.test(caseBody)) {
397
474
  return { intent: "missing", field, bound, example, intentSource: "tp-fallback" };
398
475
  }
399
476
  if (/\bNULL\b|空值/.test(upper) || /\bnull\b/i.test(caseBody)) {
@@ -402,6 +479,9 @@ export function inferScenarioParamIntent(input) {
402
479
  if (/MAX[-_]?PLUS[-_]?1|MAX\+1|超长|越界|LENGTH[-_]?OVER|OVER[-_]?LENGTH|OVER/.test(upper)) {
403
480
  return { intent: "max+1", field, bound, example, intentSource: "tp-fallback" };
404
481
  }
482
+ if (/MAX[-_]1(?:$|[-_])|max-1/i.test(upper)) {
483
+ return { intent: "max-1", field, bound, example, intentSource: "tp-fallback" };
484
+ }
405
485
  if (/\bMAX\b|最大/.test(upper)) {
406
486
  return { intent: "max", field, bound, example, intentSource: "tp-fallback" };
407
487
  }
@@ -425,7 +505,7 @@ export function inferScenarioParamIntent(input) {
425
505
  }
426
506
  return { intent: "unknown", field, bound, example, intentSource: "tp-fallback" };
427
507
  }
428
- const OMIT_SENTINEL_RE = /^(?:_OMIT|OMIT|MISSING|Ellipsis|\.\.\.)\b/;
508
+ const OMIT_SENTINEL_RE = /^(?:_OMIT|_MISSING|OMIT|MISSING|Ellipsis|\.\.\.)\b/;
429
509
  const HTTP_EXPECTED_CODE_RE = /^(?:2xx|3xx|4xx|5xx|[1-5]\d{2}|HTTP_[1-5]\d{2})$/i;
430
510
  /** Semantic custom-literal tokens that describe a value class, not a raw string. */
431
511
  function isWhitespaceSemanticLiteral(token) {
@@ -774,6 +854,10 @@ export function observeParamFeatures(block, field, constants, parameterNames) {
774
854
  (/["'][A-Za-z_][A-Za-z0-9_]*["']\s*:/.test(dictText) ||
775
855
  /(?:payload|body|data|json)\s*=/.test(block));
776
856
  if (dictLooksLikePayload && !field) {
857
+ const emptyField = /["']([A-Za-z_][A-Za-z0-9_.]*)["']\s*:\s*(?:""|'')/.exec(dictText);
858
+ if (emptyField?.[1]) {
859
+ return { kind: "empty-string", text: '""', length: 0, literal: "" };
860
+ }
777
861
  return { kind: "dict", text: dictText.replace(/\s+/g, " ").slice(0, 160) };
778
862
  }
779
863
  if (dictLooksLikePayload && field) {
@@ -803,7 +887,7 @@ export function observeParamFeatures(block, field, constants, parameterNames) {
803
887
  };
804
888
  }
805
889
  // Bare omit sentinel before id=...
806
- if (/pytest\.param\s*\(\s*(?:_OMIT|OMIT|MISSING|\.\.\.|Ellipsis)\s*(?:,|\)|$)/.test(block)) {
890
+ if (/pytest\.param\s*\(\s*(?:_OMIT|_MISSING|OMIT|MISSING|\.\.\.|Ellipsis)\s*(?:,|\)|$)/.test(block)) {
807
891
  return {
808
892
  kind: "missing-key",
809
893
  text: field ? `missing:${field}` : "missing-key",
@@ -865,7 +949,7 @@ export function observeParamFeatures(block, field, constants, parameterNames) {
865
949
  if (paramOpen) {
866
950
  const afterOpen = block.slice((paramOpen.index ?? 0) + paramOpen[0].length);
867
951
  const positionalTokens = [];
868
- const tokenRe = /\s*(_OMIT|OMIT|MISSING|Ellipsis|\.\.\.|None|True|False|-?\d+(?:\.\d+)?|"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|(?:["'][^"'\\]["']\s*\*\s*(?:\d{1,5}|[A-Za-z_][A-Za-z0-9_]*))|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[A-Za-z_][A-Za-z0-9_]*)\s*(?:,|\)|$)/g;
952
+ const tokenRe = /\s*(_OMIT|_MISSING|OMIT|MISSING|Ellipsis|\.\.\.|None|True|False|-?\d+(?:\.\d+)?|"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|(?:["'][^"'\\]["']\s*\*\s*(?:\d{1,5}|[A-Za-z_][A-Za-z0-9_]*))|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[A-Za-z_][A-Za-z0-9_]*)\s*(?:,|\)|$)/g;
869
953
  let match;
870
954
  while ((match = tokenRe.exec(afterOpen)) !== null) {
871
955
  const between = afterOpen.slice(0, match.index);
@@ -1008,12 +1092,25 @@ function compareIntent(intent, observed, bound, example, field) {
1008
1092
  return "MATCH";
1009
1093
  return len !== undefined ? "MISMATCH" : "UNDETERMINED";
1010
1094
  }
1095
+ case "max-1":
1096
+ if (bound === undefined)
1097
+ return "UNDETERMINED";
1098
+ {
1099
+ const expectedMax1 = Math.max(0, bound - 1);
1100
+ const len = observedLengthValue(observed);
1101
+ if (len === expectedMax1)
1102
+ return "MATCH";
1103
+ return len !== undefined ? "MISMATCH" : "UNDETERMINED";
1104
+ }
1011
1105
  case "min":
1012
1106
  if (bound === undefined)
1013
1107
  return "UNDETERMINED";
1014
1108
  {
1015
1109
  if (isMinLengthHelper(observed))
1016
1110
  return "MATCH";
1111
+ // Optional min=0 may omit the key instead of sending "".
1112
+ if (bound === 0 && observed.kind === "missing-key")
1113
+ return "MATCH";
1017
1114
  const len = observedLengthValue(observed);
1018
1115
  if (len === bound)
1019
1116
  return "MATCH";
@@ -1167,6 +1264,10 @@ function suggestedFixFor(intent, field, bound, example) {
1167
1264
  return field && bound !== undefined
1168
1265
  ? `set ${field}="x"*${bound + 1}`
1169
1266
  : undefined;
1267
+ case "max-1":
1268
+ return field && bound !== undefined
1269
+ ? `set ${field}="x"*${Math.max(0, bound - 1)}`
1270
+ : undefined;
1170
1271
  case "min":
1171
1272
  return field && bound !== undefined
1172
1273
  ? `set ${field}="x"*${bound}`
@@ -1720,6 +1821,12 @@ export function deterministicRewriteScenarioParam(input) {
1720
1821
  }
1721
1822
  valueLiteral = `"x" * ${bound + 1}`;
1722
1823
  break;
1824
+ case "max-1":
1825
+ if (bound === undefined) {
1826
+ return { ok: false, source: input.source, detail: "missing bound for max-1" };
1827
+ }
1828
+ valueLiteral = `"x" * ${Math.max(0, bound - 1)}`;
1829
+ break;
1723
1830
  case "min-1":
1724
1831
  if (bound === undefined) {
1725
1832
  return { ok: false, source: input.source, detail: "missing bound for min-1" };
@@ -43,6 +43,67 @@ function splitCells(raw) {
43
43
  .map((cell) => cell.trim())
44
44
  .filter((cell, index, cells) => !(cell === "" && (index === 0 || index === cells.length - 1)));
45
45
  }
46
+ function isExactSlotInventory(extras, partitionId) {
47
+ const tokens = extras
48
+ .join(";")
49
+ .split(/[;,,;]/)
50
+ .map((token) => token.trim().replace(/`/g, ""))
51
+ .filter((token) => token.length > 0);
52
+ if (tokens.length === 0)
53
+ return false;
54
+ const prefix = `TP-${partitionId.trim().toUpperCase()}-`;
55
+ return tokens.every((token) => /^TP-[A-Z0-9-]+$/i.test(token) &&
56
+ (token.toUpperCase().startsWith(prefix) || token.toUpperCase().startsWith("TP-SP-")));
57
+ }
58
+ function trimExactSlotInventoryCells(cells) {
59
+ if (cells.length <= 7)
60
+ return { cells, trimmed: false };
61
+ const partitionId = cells[0] ?? "";
62
+ if (!isExactSlotInventory(cells.slice(7), partitionId)) {
63
+ return { cells, trimmed: false };
64
+ }
65
+ return { cells: cells.slice(0, 7), trimmed: true };
66
+ }
67
+ export function canonicalizeScenarioPartitionTable(markdown) {
68
+ const lines = markdown.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
69
+ const headingIndex = lines.findIndex((line) => PARTITION_HEADING.test(line.trim()));
70
+ if (headingIndex === -1) {
71
+ return { markdown: lines.join("\n"), changed: false, trimmedRows: 0 };
72
+ }
73
+ let headerIndex = -1;
74
+ let sectionEnd = lines.length;
75
+ for (let i = headingIndex + 1; i < lines.length; i++) {
76
+ if (/^##\s+/.test(lines[i].trim()) && !PARTITION_HEADING.test(lines[i].trim())) {
77
+ sectionEnd = i;
78
+ break;
79
+ }
80
+ if (PARTITION_TABLE_HEADER.test(lines[i])) {
81
+ headerIndex = i;
82
+ }
83
+ }
84
+ if (headerIndex === -1) {
85
+ return { markdown: lines.join("\n"), changed: false, trimmedRows: 0 };
86
+ }
87
+ let trimmedRows = 0;
88
+ for (let i = headerIndex + 2; i < sectionEnd; i++) {
89
+ const line = lines[i];
90
+ if (!TABLE_ROW.test(line)) {
91
+ if (/^##\s+/.test(line) || /^#\s+/.test(line))
92
+ break;
93
+ continue;
94
+ }
95
+ const { cells, trimmed } = trimExactSlotInventoryCells(splitCells(line));
96
+ if (!trimmed)
97
+ continue;
98
+ lines[i] = `| ${cells.join(" | ")} |`;
99
+ trimmedRows += 1;
100
+ }
101
+ return {
102
+ markdown: lines.join("\n"),
103
+ changed: trimmedRows > 0,
104
+ trimmedRows,
105
+ };
106
+ }
46
107
  /**
47
108
  * Parse the `## Scenario Partitions` section of the README into declared
48
109
  * partitions. Returns { partitions, issues }; issues are fail-closed parse
@@ -77,7 +138,7 @@ export function parseScenarioPartitions(readmeMarkdown) {
77
138
  break;
78
139
  continue;
79
140
  }
80
- const cells = splitCells(line);
141
+ const { cells } = trimExactSlotInventoryCells(splitCells(line));
81
142
  if (cells.length !== 7) {
82
143
  issues.push(`MISSING_PARTITION: row has ${cells.length} cells, expected 7: ${line.trim()}`);
83
144
  continue;
@@ -369,8 +369,6 @@ function pythonParseable(source) {
369
369
  const normalized = source.replace(/\r\n/g, "\n");
370
370
  if (!normalized.trim())
371
371
  return false;
372
- if (/^\s*(?:def|class|import|from)\b/m.test(normalized) === false)
373
- return false;
374
372
  const structural = pythonStructureOutsideStrings(normalized);
375
373
  if (!structural.stringsClosed)
376
374
  return false;
@@ -387,7 +385,13 @@ function pythonParseable(source) {
387
385
  return false;
388
386
  if (openBraces !== closeBraces)
389
387
  return false;
390
- return true;
388
+ // Business modules still need a statement. Evidence-only mapped scripts may
389
+ // be a closed module docstring + Case-ID with no `def test_` (R10 health).
390
+ if (/^\s*(?:def|class|import|from|__all__\s*=)/m.test(normalized))
391
+ return true;
392
+ return (/^\s*(?:'''|""")/.test(normalized) &&
393
+ /\bCase-ID\s*:/i.test(normalized) &&
394
+ !/^\s*(?:async\s+)?def\s+test_/m.test(normalized));
391
395
  }
392
396
  /**
393
397
  * Forbidden generated shared helper/factory namespaces for the self-contained
@@ -659,10 +659,13 @@ function latestCommittedScoutTargetSurface(records) {
659
659
  }
660
660
  /**
661
661
  * Scout owns discovery. A complete fact must explicitly say so, name at least
662
- * one target candidate, leave no unresolved paths, and include fresh runtime
663
- * evidence for a named target file. This deliberately accepts an empty test
664
- * path list: some small changes have no existing focused test file, but they
665
- * still need a proven implementation entrypoint.
662
+ * one target candidate and leave no unresolved paths. Existing surfaces must
663
+ * include fresh runtime evidence for a named target path. Greenfield
664
+ * surfaces are the one explicit exception: when runtime-injected source
665
+ * declarations prove that every named path was specified by the task source,
666
+ * all paths may be future (not yet on disk). This keeps create-new work
667
+ * possible without allowing Scout to invent an arbitrary path under the
668
+ * writer boundary. An empty test path list remains valid.
666
669
  */
667
670
  export function readCompleteScoutTargetSurface(records) {
668
671
  const fact = latestCommittedScoutTargetSurface(records);
@@ -704,11 +707,35 @@ export function readCompleteScoutTargetSurface(records) {
704
707
  .map((item) => asString(item.path))
705
708
  .filter(Boolean)
706
709
  : []);
707
- if (!namedPaths.some((candidate) => freshPaths.has(candidate)))
710
+ const pathEvidence = Array.isArray(fact.pathEvidence)
711
+ ? fact.pathEvidence.filter(isRecord)
712
+ : [];
713
+ const hasSourceDeclarationLedger = Array.isArray(fact.sourceDeclaredPaths);
714
+ const undeclaredFuturePaths = hasSourceDeclarationLedger
715
+ ? namedPaths.filter((candidate) => {
716
+ const evidence = pathEvidence.find((item) => asString(item.path) === candidate);
717
+ return evidence?.fresh !== true && evidence?.sourceDeclared !== true;
718
+ })
719
+ : [];
720
+ if (undeclaredFuturePaths.length > 0)
708
721
  return {
709
722
  ok: false,
710
- reason: "frontend scout target surface lacks fresh runtime evidence for a named target path; record_target_surface enriches every declared entrypoint/implementation/test path against the real workspace, so at least one named path must exist on disk (an existing directory counts; a file to be created later cannot count)",
723
+ reason: `frontend scout target surface has future paths not declared by the task source: ${undeclaredFuturePaths.join(", ")}; test path is not declared by the task source, so do not guess test/ vs tests/ or other target directories`,
711
724
  };
725
+ if (!namedPaths.some((candidate) => freshPaths.has(candidate))) {
726
+ const allPathsAreSourceDeclaredFuturePaths = hasSourceDeclarationLedger &&
727
+ namedPaths.length > 0 &&
728
+ namedPaths.every((candidate) => {
729
+ const evidence = pathEvidence.find((item) => asString(item.path) === candidate);
730
+ return evidence?.fresh !== true && evidence?.sourceDeclared === true;
731
+ });
732
+ if (allPathsAreSourceDeclaredFuturePaths)
733
+ return { ok: true, surface };
734
+ return {
735
+ ok: false,
736
+ reason: "frontend scout target surface lacks fresh runtime evidence for a named target path; greenfield exceptions require runtime-injected source declarations for every future path",
737
+ };
738
+ }
712
739
  return { ok: true, surface };
713
740
  }
714
741
  /** Paths with Scout ownership that Plan may read for a specific decision. */