@tea-agent/loop-agent 0.39.0-beta.5 → 0.39.0-beta.7

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 (130) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/commands/client-recovery.js +3 -0
  4. package/dist/commands/task-advance.js +19 -0
  5. package/dist/executors/dag-pi-executor.js +138 -36
  6. package/dist/executors/pi-executor.js +7 -0
  7. package/dist/executors/pi-playwright-cli-tool.js +23 -16
  8. package/dist/executors/shell-executor.js +26 -9
  9. package/dist/shared/dag-failure-category.js +150 -0
  10. package/dist/task/config-types.js +6 -0
  11. package/dist/task/frontend-project-capability.js +19 -0
  12. package/dist/task/source-references.js +22 -1
  13. package/dist/worker/console/chat/operation-card.js +8 -1
  14. package/dist/worker/console/chat/routes.js +5 -3
  15. package/dist/worker/console/inspect-split.js +18 -0
  16. package/dist/worker/console/operation-run-facts.js +19 -3
  17. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-zsNyvGaH.js → abnfDiagram-N423BO3Z-bPer1waP.js} +1 -1
  18. package/dist/worker/console/static/assets/{arc-BDjZ5kE1.js → arc-BLdkwucW.js} +1 -1
  19. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BkgaujnN.js → architectureDiagram-T3A2C74G-Bw4wo4I1.js} +1 -1
  20. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CqxOJi0j.js → blockDiagram-VBNYF7ZC-DUUkc0gr.js} +1 -1
  21. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C2JHLdRi.js → c4Diagram-5PPSVZJV-BC3O0nob.js} +1 -1
  22. package/dist/worker/console/static/assets/channel-AbFtVnFm.js +1 -0
  23. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DF_YSvOS.js → chunk-2GRJ4B5K-Dd4g2nti.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D3Ort5Vo.js → chunk-2Q5K7J3B-s2EDm7-n.js} +1 -1
  25. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-XfAfZeUC.js → chunk-5RXB4S5H-GqtHM2yQ.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-SLBibhf5.js → chunk-5VM5RSS4-BzRFrN8j.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dz29Ye_f.js → chunk-6Q2QTUOP-Bw9nFjCJ.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DB2G4H8V.js → chunk-GF5L2VYU-B4n-03Qm.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CIoGkxdu.js → chunk-JWPE2WC7-Dt75ZdBD.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CG78FGkn.js → chunk-KBJHAD2P-B_afMhHB.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ZDHAvXPd.js → chunk-RYQCIY6F-Bd8Rqqg9.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BKkkC4VA.js → chunk-XXDRQBXY-9boOMji4.js} +1 -1
  33. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-n1yf2Jcv.js +1 -0
  34. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-n1yf2Jcv.js +1 -0
  35. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-SposUZsO.js → cose-bilkent-JH36ORCC-BtCycfCc.js} +1 -1
  36. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BSCtQpD7.js → cynefin-VYW2F7L2-eTOts9L1.js} +1 -1
  37. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DgFHrw9l.js → cynefinDiagram-MW4NZA55-Cibd2mjg.js} +1 -1
  38. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BVBasYbj.js → dagre-VZM6K2ZE-CDjDZtDV.js} +1 -1
  39. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-6r_4rN_c.js → diagram-7IWD3JNH-Btexn-JJ.js} +1 -1
  40. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-JmuaB41P.js → diagram-B4RE2ZJO-Cyl8sdpM.js} +1 -1
  41. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-xXvAY3sk.js → diagram-LBJQPF4R-Ev-83E1t.js} +1 -1
  42. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D2B3lY2o.js → diagram-Q27KOJAE-41LqE0Mc.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-UB23O5K3-tK54tMEX.js → diagram-UB23O5K3-BC39T5R3.js} +1 -1
  44. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CNtELzV-.js → ebnfDiagram-BXEA7PRR-DzcNRuri.js} +1 -1
  45. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-C1xKMKcW.js → erDiagram-JOGREHBK-B5Frvvnz.js} +1 -1
  46. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZ7dlPMz.js → flowDiagram-UKHOOZJN-BapxROIM.js} +1 -1
  47. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BqGb42Xe.js → ganttDiagram-PKOTCBZU-Cd4d4wQ0.js} +1 -1
  48. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D_BlXEtt.js → gitGraphDiagram-DS77QQ5N-CrL6buFW.js} +1 -1
  49. package/dist/worker/console/static/assets/index-DBh_x_Ek.js +406 -0
  50. package/dist/worker/console/static/assets/index-DwOGauxU.css +1 -0
  51. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Vce2vmIe.js → infoDiagram-6WML65LV-BrSiNxyd.js} +1 -1
  52. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DDwVBAvI.js → ishikawaDiagram-WSZJBQD7-gvlvY-n9.js} +1 -1
  53. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-y_Ew-wv4.js → journeyDiagram-NVQOT4AX-D-YaPzRQ.js} +1 -1
  54. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CjXJFp1K.js → kanban-definition-27J2QSJJ-BsmDv9E_.js} +1 -1
  55. package/dist/worker/console/static/assets/{linear-1itMst1Q.js → linear-UWaoDN3h.js} +1 -1
  56. package/dist/worker/console/static/assets/{mermaid.core-C-D3ZRUa.js → mermaid.core-BlvpGVG8.js} +5 -5
  57. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BiYywmZc.js → mindmap-definition-FAOFIHXS-CUCIP5CP.js} +1 -1
  58. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-l2fydNWA.js → pegDiagram-VL7TDLO6-CB1aQUAK.js} +1 -1
  59. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Dz0hNbYW.js → pieDiagram-7S7Q4E2Y-BwehHxhf.js} +1 -1
  60. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BujI_mLZ.js → quadrantDiagram-CIZ2JOQS-CFz30APB.js} +1 -1
  61. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-pWe84beb.js → railroadDiagram-AXF67PYL-BwNmw1i6.js} +1 -1
  62. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-ByYu2frg.js → requirementDiagram-LRYGKXZP-jg-tPXlI.js} +1 -1
  63. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CoPnilFP.js → sankeyDiagram-W5VNT64P-BlyReDKl.js} +1 -1
  64. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-1Md2LnS0.js → sequenceDiagram-SI44F4Z6-DBUzW1f3.js} +1 -1
  65. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DqqqAoSH.js → sizeCapture-X5ZJPWSS-BYcI2RAr.js} +1 -1
  66. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-C4sGkf--.js → stateDiagram-OKZ733FA-D7AiyJdo.js} +1 -1
  67. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BBSH5c8m.js +1 -0
  68. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ByxJrUzW.js → swimlanes-SLNWSIFB-BHGt7Kkv.js} +2 -2
  69. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D2F7-U4H.js +8 -0
  70. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-D8MLgM0O.js → timeline-definition-Z64GVDOM-OPjooWcX.js} +1 -1
  71. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DnBis-wZ.js → vennDiagram-T6HMQDX7-ggsWDvrO.js} +1 -1
  72. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-uuyYT7UO.js → wardleyDiagram-T6FBY63Y-DjNe-BjZ.js} +1 -1
  73. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BjgabhwK.js → xychartDiagram-ELKLHX3M-Byp63Hei.js} +1 -1
  74. package/dist/worker/console/static/index.html +2 -2
  75. package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
  76. package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
  77. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  78. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  79. package/dist/worker/console/static-src/operator-chat/turn-submission.js +13 -1
  80. package/dist/worker/console/static-src/operator-chat/useChatStream.js +29 -9
  81. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  82. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  83. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  84. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  85. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  86. package/dist/worker/observability/read-model.js +22 -0
  87. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +29 -1
  88. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  89. package/dist/workflows/dag/backend-test-pytest-collection.js +87 -4
  90. package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
  91. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  92. package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
  93. package/dist/workflows/dag/failure-category.js +7 -128
  94. package/dist/workflows/dag/frontend-design-policy.js +31 -7
  95. package/dist/workflows/dag/frontend-implementation-contract.js +78 -3
  96. package/dist/workflows/dag/frontend-review-findings.js +238 -0
  97. package/dist/workflows/dag/frontend-shadow-dual-write.js +24 -10
  98. package/dist/workflows/dag/frontend-shape.js +5 -3
  99. package/dist/workflows/dag/frontend-test-case-checklist.js +4 -2
  100. package/dist/workflows/dag/frontend-test-case-manifest.js +6 -4
  101. package/dist/workflows/dag/frontend-test-case-quality.js +6 -3
  102. package/dist/workflows/dag/frontend-test-environment-probe.js +10 -7
  103. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  104. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  105. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  106. package/dist/workflows/dag/frontend-test-result-contract.js +28 -14
  107. package/dist/workflows/dag/frontend-test-standard-scenarios.js +3 -1
  108. package/dist/workflows/dag/init-hybrid.js +149 -15
  109. package/dist/workflows/dag/node-execution.js +18 -1
  110. package/dist/workflows/dag/report.js +37 -1
  111. package/dist/workflows/dag/rerun-feedback.js +101 -1
  112. package/dist/workflows/dag/retry-policy.js +31 -3
  113. package/dist/workflows/dag/runner.js +46 -1
  114. package/dist/workflows/dag/types.js +12 -0
  115. package/dist/workflows/dag/validate.js +32 -22
  116. package/docs/templates/agent-dag-report.schema.json +8 -2
  117. package/docs/templates/agent-dag.schema.json +1 -1
  118. package/docs/templates/frontend-test-case-checklist.md +2 -2
  119. package/package.json +1 -1
  120. package/skills/fe-test-ui-scout/SKILL.md +4 -4
  121. package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
  122. package/skills/playwright-cli/SKILL.md +1 -1
  123. package/skills/playwright-cli-case-generator/SKILL.md +10 -8
  124. package/dist/worker/console/static/assets/channel-BuqaGKAT.js +0 -1
  125. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +0 -1
  126. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +0 -1
  127. package/dist/worker/console/static/assets/index-D5zGX6fG.js +0 -404
  128. package/dist/worker/console/static/assets/index-DBbhESQ_.css +0 -1
  129. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +0 -1
  130. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +0 -8
@@ -200,14 +200,170 @@ export async function inspectBackendTestEnvironment(input) {
200
200
  serverEntries,
201
201
  };
202
202
  }
203
+ /**
204
+ * Filenames declared by the canonical `## Module Index` section of the backend
205
+ * README. Only a unique exact `## Module Index` heading is authoritative; a
206
+ * missing/duplicate section yields an empty declaration set (callers then fall
207
+ * back to the whole directory so legacy workspaces without a manifest keep
208
+ * their prior behaviour). Reads `[x](./x.md)`, `` `testcase/md/<x>.md` `` and
209
+ * inline `x.md` tokens inside the section; README.md itself is never a module.
210
+ */
211
+ export function extractDeclaredModuleIndexFilenames(readme) {
212
+ const lines = readme
213
+ .replaceAll("\r\n", "\n")
214
+ .replaceAll("\r", "\n")
215
+ .split("\n");
216
+ const headings = lines.flatMap((line, index) => line.trim() === "## Module Index" ? [index] : []);
217
+ if (headings.length !== 1)
218
+ return [];
219
+ const start = headings[0] + 1;
220
+ const relativeEnd = lines
221
+ .slice(start)
222
+ .findIndex((line) => /^##\s+\S/.test(line.trim()));
223
+ const end = relativeEnd < 0 ? lines.length : start + relativeEnd;
224
+ const section = lines.slice(start, end).join("\n");
225
+ return unique([...section.matchAll(/`?[A-Za-z0-9_.-]+\.md`?/g)]
226
+ .map((match) => match[0].replaceAll("`", ""))
227
+ .filter((name) => name.toLowerCase() !== "readme.md"));
228
+ }
229
+ /**
230
+ * Count exact `## Module Index` headings in the backend README.
231
+ */
232
+ function moduleIndexHeadingCountIn(readme) {
233
+ return readme
234
+ .replaceAll("\r\n", "\n")
235
+ .replaceAll("\r", "\n")
236
+ .split("\n")
237
+ .filter((line) => line.trim() === "## Module Index").length;
238
+ }
239
+ /**
240
+ * Machine-readable fail-closed finding for an invalid backend-test Module Index.
241
+ * These are distinct states (missing/unreadable/duplicate/empty-unparsable) so a
242
+ * stripped Module Index can never silently fall back to a whole-directory scan.
243
+ */
244
+ export function backendManifestStatusFinding(manifestStatus, moduleIndexHeadingCount = 0) {
245
+ switch (manifestStatus) {
246
+ case "missing":
247
+ return "README is missing; backend-test current round fails closed without a declarative Module Index";
248
+ case "unreadable":
249
+ return "README is unreadable; backend-test current round fails closed without a usable Module Index";
250
+ case "duplicate":
251
+ return moduleIndexHeadingCount === 0
252
+ ? "README Module Index heading is missing; backend-test current round fails closed without a declarative Module Index"
253
+ : `duplicate ## Module Index heading (${moduleIndexHeadingCount} occurrences); backend-test current round fails closed without a unique Module Index`;
254
+ case "empty-unparsable":
255
+ return "README Module Index is empty/unparsable; backend-test current round fails closed with no declared module";
256
+ case "valid":
257
+ return "";
258
+ }
259
+ }
260
+ /**
261
+ * Resolve the current-round Markdown module manifest. The README `## Module
262
+ * Index` is the authoritative source for which module files belong to this
263
+ * round. Fail closed on every non-`valid` state (missing/unreadable/duplicate/
264
+ * empty-unparsable): the backend-test current round must NEVER re-scan the whole
265
+ * directory, or historical `.md` files could leak back into Case/coverage/
266
+ * partition facts.
267
+ *
268
+ * - `manifestStatus==="valid"` — unique heading with >= 1 declared `.md` file.
269
+ * - `missing` — README file does not exist.
270
+ * - `unreadable` — README exists but could not be read.
271
+ * - `duplicate` — README has 0 or duplicate `## Module Index`.
272
+ * - `empty-unparsable` — unique heading but no `.md` filename parsed.
273
+ * - `declaredMdFiles` — declared filenames that exist on disk (relative paths).
274
+ * - `declaredMissing` — declared filenames with no file on disk (fail-closed
275
+ * evidence, never silently dropped).
276
+ * - `unmanifested` — existing `.md` files that the Module Index does not
277
+ * declare (non-silent advisory; they must not enter Case/coverage/partition).
278
+ */
279
+ export async function resolveBackendTestManifestModules(workspaceRoot, layout) {
280
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
281
+ const readmePath = path.join(workspaceRoot, ...resolved.readmePath.split("/"));
282
+ const invalid = (manifestStatus, moduleIndexHeadingCount) => ({
283
+ hasManifest: false,
284
+ manifestStatus,
285
+ moduleIndexHeadingCount,
286
+ declaredStems: [],
287
+ declaredMdFiles: [],
288
+ declaredMissing: [],
289
+ unmanifested: [],
290
+ });
291
+ let readme = "";
292
+ if (await exists(readmePath)) {
293
+ try {
294
+ readme = await readFile(readmePath, "utf8");
295
+ }
296
+ catch {
297
+ // Unreadable README is fail-closed (not silently treated as no manifest).
298
+ return invalid("unreadable", 0);
299
+ }
300
+ }
301
+ else {
302
+ return invalid("missing", 0);
303
+ }
304
+ const moduleIndexHeadingCount = moduleIndexHeadingCountIn(readme);
305
+ if (moduleIndexHeadingCount !== 1) {
306
+ return invalid("duplicate", moduleIndexHeadingCount);
307
+ }
308
+ const declaredStems = extractDeclaredModuleIndexFilenames(readme);
309
+ if (declaredStems.length === 0) {
310
+ return invalid("empty-unparsable", moduleIndexHeadingCount);
311
+ }
312
+ const hasManifest = true;
313
+ const directory = path.join(workspaceRoot, ...resolved.markdownDir.split("/"));
314
+ let existingNames = [];
315
+ try {
316
+ existingNames = (await readdir(directory, { withFileTypes: true }))
317
+ .filter((entry) => entry.isFile() &&
318
+ entry.name.toLowerCase().endsWith(".md") &&
319
+ entry.name.toLowerCase() !== "readme.md")
320
+ .map((entry) => entry.name);
321
+ }
322
+ catch {
323
+ // missing markdown directory -> no existing modules
324
+ }
325
+ const declaredByLower = new Map();
326
+ for (const name of declaredStems) {
327
+ const base = path.posix.basename(name);
328
+ declaredByLower.set(base.toLowerCase(), base);
329
+ }
330
+ const declaredMdFiles = [];
331
+ const declaredMissing = [];
332
+ for (const base of declaredByLower.values()) {
333
+ const disk = existingNames.find((name) => name.toLowerCase() === base.toLowerCase());
334
+ if (disk)
335
+ declaredMdFiles.push(`${resolved.markdownDir}/${disk}`);
336
+ else
337
+ declaredMissing.push(`${resolved.markdownDir}/${base}`);
338
+ }
339
+ const unmanifested = existingNames
340
+ .filter((name) => !declaredByLower.has(name.toLowerCase()))
341
+ .map((name) => `${resolved.markdownDir}/${name}`);
342
+ return {
343
+ hasManifest,
344
+ manifestStatus: "valid",
345
+ moduleIndexHeadingCount,
346
+ declaredStems,
347
+ declaredMdFiles: declaredMdFiles.sort(),
348
+ declaredMissing: declaredMissing.sort(),
349
+ unmanifested: unmanifested.sort(),
350
+ };
351
+ }
203
352
  export async function markdownFiles(root, layout) {
204
353
  const resolved = layout ?? resolveBackendTestLayout(undefined);
205
354
  const directory = path.join(root, ...resolved.markdownDir.split("/"));
206
355
  const entries = await readdir(directory, { withFileTypes: true });
207
- return entries
356
+ const all = entries
208
357
  .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"))
209
358
  .map((entry) => path.join(directory, entry.name))
210
359
  .sort();
360
+ const manifest = await resolveBackendTestManifestModules(root, resolved);
361
+ if (manifest.manifestStatus !== "valid")
362
+ return [];
363
+ const declaredAbs = new Set(manifest.declaredMdFiles.map((rel) => path.join(root, ...rel.split("/")).toLowerCase()));
364
+ return all
365
+ .filter((file) => declaredAbs.has(file.toLowerCase()))
366
+ .sort();
211
367
  }
212
368
  function canonicalBackendCaseId(value) {
213
369
  const upper = value.toUpperCase();
@@ -453,6 +609,17 @@ export async function validateBackendMarkdownCases(input) {
453
609
  const readme = path.join(input.workspaceRoot, ...layout.readmePath.split("/"));
454
610
  if (!(await exists(readme)))
455
611
  findings.push(`missing ${layout.readmePath}`);
612
+ const manifest = await resolveBackendTestManifestModules(input.workspaceRoot, layout);
613
+ // Fail closed on README Module Index entries that were declared but never
614
+ // materialized — never let a declared module silently vanish from the round.
615
+ if (manifest.declaredMissing.length > 0) {
616
+ findings.push(`declared Markdown module is missing (Module Index): ${manifest.declaredMissing.join(", ")}`);
617
+ }
618
+ // Fail closed on every non-valid Module Index state: without a valid unique
619
+ // Module Index the current round must not re-scan historical `.md` files.
620
+ if (manifest.manifestStatus !== "valid") {
621
+ findings.push(backendManifestStatusFinding(manifest.manifestStatus, manifest.moduleIndexHeadingCount));
622
+ }
456
623
  const moduleFiles = files.filter((file) => path.basename(file).toLowerCase() !== "readme.md");
457
624
  if (moduleFiles.length === 0)
458
625
  findings.push(`no ${layout.markdownDir} module case file found`);
@@ -562,6 +729,7 @@ export async function validateBackendMarkdownCases(input) {
562
729
  `- Cases: ${caseCount}`,
563
730
  `- Covered AC IDs: ${[...coveredAc].sort().join(", ") || "none"}`,
564
731
  `- Environment evidence present: ${input.environmentMarkdown.trim() ? "yes" : "no"}`,
732
+ `- Manifest scope: ${manifest.manifestStatus === "valid" ? "declared-modules" : `fails-closed:${manifest.manifestStatus}`} (declared-missing=${manifest.declaredMissing.length}, unmanifested-advisory=${manifest.unmanifested.length}, module-index-heading-count=${manifest.moduleIndexHeadingCount})`,
565
733
  `- Findings: ${findings.length}`,
566
734
  "- Required sections: Preconditions/前置条件, Steps/操作步骤, Expected Results/预期结果",
567
735
  "- Source-reference validity check: not applied",
@@ -583,6 +751,13 @@ export async function validateBackendMarkdownCases(input) {
583
751
  ? safetyFindings.map((finding) => `- ${finding}`)
584
752
  : ["- No secret-shaped values detected."]),
585
753
  "",
754
+ "## Unmanifested Markdown Modules",
755
+ "",
756
+ "- Advisory: existing .md files not declared by the README Module Index are excluded from this round's Case/coverage/partition scope.",
757
+ ...(manifest.unmanifested.length > 0
758
+ ? manifest.unmanifested.map((item) => `- ${item}`)
759
+ : ["- None"]),
760
+ "",
586
761
  "## Findings",
587
762
  "",
588
763
  ...(findings.length > 0 ? findings.map((finding) => `- ${finding}`) : ["- None"]),
@@ -131,6 +131,23 @@ function normalizeHazardOccurrenceIdentity(line) {
131
131
  // Unclosed quote or no out-of-string `#`: do not invent a comment boundary.
132
132
  return trimmed;
133
133
  }
134
+ /**
135
+ * Bare `__NNN_*` helper module names imported by a Python source snippet.
136
+ * Only the head token of an `import`/`from` statement is considered a module
137
+ * name; dotted submodules (`__551_manager_helpers.x`) collapse to the head.
138
+ */
139
+ function importedBareHelperModules(source) {
140
+ const names = [];
141
+ const seen = new Set();
142
+ for (const match of normalizeSourceNewlines(source).matchAll(/\b(?:import|from)\s+([A-Za-z0-9_.]+)/g)) {
143
+ const head = match[1].split(".")[0];
144
+ if (/^__\d+[A-Za-z0-9_]*$/.test(head) && !seen.has(head)) {
145
+ seen.add(head);
146
+ names.push(head);
147
+ }
148
+ }
149
+ return names;
150
+ }
134
151
  function listPatternOccurrences(source, patternSource, flags = "") {
135
152
  const normalized = normalizeSourceNewlines(source);
136
153
  const pattern = new RegExp(patternSource, flags.includes("g") ? flags : `${flags}g`);
@@ -221,6 +238,8 @@ export async function buildBackendPytestAssetInventory(workspaceRoot, mappedScri
221
238
  const inputHashes = {};
222
239
  const hazardOccurrenceCounts = {};
223
240
  const hazardOccurrences = {};
241
+ const scriptBareImports = {};
242
+ const mappedScriptSet = new Set(normalizedScripts);
224
243
  for (const file of assetFiles) {
225
244
  const bytes = await readFile(path.resolve(workspaceRoot, file));
226
245
  const source = bytes.toString("utf8");
@@ -228,8 +247,10 @@ export async function buildBackendPytestAssetInventory(workspaceRoot, mappedScri
228
247
  const lists = scanRepairHazardOccurrenceLists(source);
229
248
  hazardOccurrences[file] = lists;
230
249
  hazardOccurrenceCounts[file] = Object.fromEntries(Object.entries(lists).map(([label, identities]) => [label, identities.length]));
250
+ if (mappedScriptSet.has(file))
251
+ scriptBareImports[file] = importedBareHelperModules(source);
231
252
  }
232
- return { mappedScripts: normalizedScripts, assetFiles, inputHashes, hazardOccurrenceCounts, hazardOccurrences };
253
+ return { mappedScripts: normalizedScripts, assetFiles, inputHashes, hazardOccurrenceCounts, hazardOccurrences, scriptBareImports };
233
254
  }
234
255
  function bounded(value) {
235
256
  const normalized = value.replaceAll(/(Bearer|Basic)\s+[A-Za-z0-9._~+\/-]+/gi, "$1 [REDACTED]");
@@ -308,8 +329,68 @@ function classifyFixtureResolutionFailure(output, layout) {
308
329
  repairPaths: paths,
309
330
  };
310
331
  }
311
- function classifyCollectionFailure(output) {
332
+ /**
333
+ * Generated-local bare helper module signatures found in the bound pytest
334
+ * asset inventory. A file such as `testcase/__551_manager_helpers.py` in the
335
+ * test root/script dir is generated-local: when collection reports
336
+ * `No module named '__551_manager_helpers'`, the defect can be precisely
337
+ * attributed to the generated mapped script instead of a missing third-party
338
+ * package.
339
+ */
340
+ function generatedLocalBareHelperNames(inventory) {
341
+ if (!inventory)
342
+ return [];
343
+ const names = [];
344
+ for (const file of inventory.assetFiles) {
345
+ const stem = path.posix.basename(file).replace(/\.py$/, "");
346
+ if (/^__\d+[A-Za-z0-9_]*$/.test(stem))
347
+ names.push(stem);
348
+ }
349
+ return [...new Set(names)];
350
+ }
351
+ function classifyCollectionFailure(output, evidence) {
312
352
  const normalized = output.replaceAll("\\", "/");
353
+ const helperNames = generatedLocalBareHelperNames(evidence?.inventory);
354
+ const helperByLower = new Map(helperNames.map((name) => [name.toLowerCase(), name]));
355
+ // A bare `__NNN_...` module import that collection cannot find is only a
356
+ // candidate generated-local helper signature. REPAIRABLE requires two
357
+ // deterministic pieces of evidence: (1) a helper asset of the same name in
358
+ // the bound inventory, and (2) at least one mapped generated script whose
359
+ // source truly imports that bare module. Without both — or when attribution
360
+ // is impossible — fail closed as BLOCKED so an un-attributable dependency
361
+ // error is never widened into a repair. Genuine third-party modules (no
362
+ // `__NNN_` signature) stay BLOCKED below.
363
+ const missingModule = /No module named ['\"]([^'\"]+)['\"]/i.exec(normalized)?.[1];
364
+ if (missingModule && /^__\d+[A-Za-z0-9_]*$/.test(missingModule)) {
365
+ const helperAsset = helperByLower.get(missingModule.toLowerCase());
366
+ const inventory = evidence?.inventory;
367
+ const scriptBareImports = inventory?.scriptBareImports ?? {};
368
+ const missingLower = missingModule.toLowerCase();
369
+ const importedByMappedScript = inventory
370
+ ? inventory.mappedScripts.some((script) => (scriptBareImports[script] ?? []).some((name) => name.toLowerCase() === missingLower))
371
+ : false;
372
+ if (helperAsset && importedByMappedScript && inventory) {
373
+ const repairPaths = [...new Set([
374
+ ...(helperAsset
375
+ ? inventory.assetFiles.filter((file) => path.posix.basename(file).replace(/\.py$/, "") === helperAsset)
376
+ : []),
377
+ ...inventory.mappedScripts,
378
+ ])].sort();
379
+ return {
380
+ status: "REPAIRABLE",
381
+ kind: "missing-generated-local-helper",
382
+ detail: `generated testcase asset imports missing bare generated-local helper module ${missingModule}; inline the helper or remove the generated shared dependency`,
383
+ repairPaths,
384
+ };
385
+ }
386
+ return {
387
+ status: "BLOCKED",
388
+ kind: "missing-generated-local-helper-without-evidence",
389
+ detail: helperAsset
390
+ ? `missing bare generated-local helper module ${missingModule} has a name match but no mapped generated script imports it (no precise generated-local attribution)`
391
+ : `missing bare __NNN_* module ${missingModule} has no generated-local helper asset in the bound inventory (cannot be precisely attributed)`,
392
+ };
393
+ }
313
394
  const blockedPatterns = [
314
395
  [/No module named ['\"](?!testcase(?:\.|['\"]))/i, "missing-third-party-module"],
315
396
  [/pytest(?:_|-)plugin|pluggy|unknown hook|unrecognized arguments/i, "pytest-plugin-or-config"],
@@ -401,7 +482,7 @@ export function assessBackendPytestCollection(input) {
401
482
  fixtureStderrExcerpt: bounded(fixtureResolution.stderr),
402
483
  });
403
484
  }
404
- const classification = classifyCollectionFailure(`${input.stdout}\n${input.stderr}`);
485
+ const classification = classifyCollectionFailure(`${input.stdout}\n${input.stderr}`, { layout: input.layout, inventory: input.inventory });
405
486
  return backendPytestCollectionFactsSchema.parse({
406
487
  schemaId: "backend-test-pytest-collection-v3",
407
488
  phase: input.phase,
@@ -412,7 +493,9 @@ export function assessBackendPytestCollection(input) {
412
493
  fixtureResolutionAttempted: false,
413
494
  fixtureResolutionStatus: "NOT_RUN",
414
495
  fixtureResolutionExitCode: null,
415
- repairPaths: testcasePythonPaths(`${input.stdout}\n${input.stderr}`),
496
+ repairPaths: classification.repairPaths && classification.repairPaths.length > 0
497
+ ? classification.repairPaths
498
+ : testcasePythonPaths(`${input.stdout}\n${input.stderr}`),
416
499
  mappedScripts: input.inventory.mappedScripts,
417
500
  existingMappedScripts: input.inventory.mappedScripts,
418
501
  missingMappedScripts: [],
@@ -385,12 +385,17 @@ export const FORBIDDEN_GENERATED_IMPORT_NAMESPACES = [
385
385
  "testcase.helpers",
386
386
  "testcase.factories",
387
387
  ];
388
- const FORBIDDEN_GENERATED_IMPORT_PATTERN = /^\s*(?:from\s+(testcase_helpers|testcase\.helpers|testcase\.factories)\b|import\s+(testcase_helpers|testcase\.helpers|testcase\.factories)\b)/gm;
388
+ const FORBIDDEN_GENERATED_IMPORT_PATTERN = /^\s*(?:from\s+(testcase_helpers|testcase\.helpers|testcase\.factories|__\d+[A-Za-z0-9_]*)\b|import\s+(testcase_helpers|testcase\.helpers|testcase\.factories|__\d+[A-Za-z0-9_]*)\b)/gm;
389
389
  /**
390
- * Scan Python source for imports from forbidden generated shared namespaces.
391
- * Anchored at line start with a word boundary so `testcase.helpers_other` or a
392
- * substring inside a docstring sentence is not falsely flagged. Returns the
393
- * list of offending namespace module names (deduplicated, order-preserving).
390
+ * Scan Python source for imports from forbidden generated shared namespaces
391
+ * or bare generated-local helper modules. Anchored at line start with a word
392
+ * boundary so `testcase.helpers_other` or a substring inside a docstring is not
393
+ * falsely flagged. In addition to the `testcase_helpers`/`testcase.helpers`/
394
+ * `testcase.factories` namespaces, bare `__<digits>...` helper modules (for
395
+ * example `from __551_manager_helpers import ...` / `import __551_manager_helpers`)
396
+ * are generated-local shared dependencies: a self-contained pytest child must
397
+ * inline or remove them rather than depend on a generated shared module.
398
+ * Returns the list of offending module names (deduplicated, order-preserving).
394
399
  */
395
400
  export function findForbiddenGeneratedImports(source) {
396
401
  const hits = [];
@@ -76,7 +76,7 @@ export async function executeDynamicLoopUntil(input) {
76
76
  bodyIdMap,
77
77
  }));
78
78
  for (const child of children) {
79
- assertValidMaterializedDagTask(child);
79
+ assertValidMaterializedDagTask(child, input.spec);
80
80
  if (!input.tasksById.has(child.id)) {
81
81
  input.tasksById.set(child.id, child);
82
82
  input.spec.tasks.push(child);
@@ -170,7 +170,7 @@ export async function executeDynamicMapExpansion(input) {
170
170
  nodeId,
171
171
  }));
172
172
  for (const child of children) {
173
- assertValidMaterializedDagTask(child);
173
+ assertValidMaterializedDagTask(child, input.spec);
174
174
  }
175
175
  const writeSetConflicts = collectDynamicChildWriteSetConflicts(children);
176
176
  if (writeSetConflicts.length > 0) {
@@ -1,130 +1,9 @@
1
1
  import { z } from "zod";
2
- export const dagNormalizedFailureCategorySchema = z.enum([
3
- "success",
4
- "validation",
5
- "executor",
6
- "write-guard",
7
- "timeout",
8
- "auth",
9
- "human-required",
10
- "human-rejected",
11
- "shell-command",
12
- "static-error",
13
- "decision-envelope",
14
- "skipped",
15
- "unknown",
16
- ]);
2
+ import { DAG_NORMALIZED_FAILURE_CATEGORY_VALUES } from "../../shared/dag-failure-category.js";
3
+ // The pure classification core lives in src/shared/dag-failure-category.ts so
4
+ // the Operator Console projection can normalize raw CLI categories without
5
+ // importing the workflows kernel (console/workflows boundary test). This
6
+ // module keeps the schema surface for the DAG kernel's existing consumers.
7
+ export { isDagFailureCategory, normalizeDagFailureCategory, } from "../../shared/dag-failure-category.js";
8
+ export const dagNormalizedFailureCategorySchema = z.enum(DAG_NORMALIZED_FAILURE_CATEGORY_VALUES);
17
9
  export const DAG_NORMALIZED_FAILURE_CATEGORIES = dagNormalizedFailureCategorySchema.options;
18
- const RAW_TO_NORMALIZED = {
19
- success: "success",
20
- "write-guard": "write-guard",
21
- timeout: "timeout",
22
- // Clean implementer stall with zero writes — still a timeout class for report,
23
- // but retryable under WRITER_TRANSPORT_RETRY_POLICY.
24
- "writer-clean-timeout": "timeout",
25
- "missing-api-key": "auth",
26
- auth: "auth",
27
- "human-rejected": "human-rejected",
28
- "human-required": "human-required",
29
- "decision-envelope-invalid": "decision-envelope",
30
- "nonzero-exit": "shell-command",
31
- "spawn-error": "shell-command",
32
- "test-timeout": "timeout",
33
- "static-error": "static-error",
34
- "invalid-output": "validation",
35
- "structured-output-truncated": "validation",
36
- "output-too-large": "validation",
37
- "structured-repair-exhausted": "validation",
38
- "governance-blocked": "validation",
39
- "path-violation": "validation",
40
- "empty-output": "validation",
41
- "verify-failure": "validation",
42
- "test-failure": "validation",
43
- "verification-plan-stale": "validation",
44
- network: "executor",
45
- quota: "executor",
46
- "rate-limit": "executor",
47
- unavailable: "executor",
48
- // Deterministic capability/policy refusal: missing verified launcher JS
49
- // entry, capability allowlist rejection, or missing bounded write tool
50
- // profile. These are environment/config defects the operator repairs
51
- // before rerunning; classify as executor so recovery recommends
52
- // rerun-after-fix WITHOUT forcing a human-decision gate on
53
- // standaloneTaskRerun (run-failed same-task rerun).
54
- "tool-policy": "executor",
55
- unknown: "unknown",
56
- };
57
- function isFinishedStatus(status) {
58
- return status === "FINISHED" || status === "finished";
59
- }
60
- function isInProgressStatus(status) {
61
- return (status === "PENDING" ||
62
- status === "RUNNING" ||
63
- status === "pending" ||
64
- status === "running");
65
- }
66
- function isFailureStatus(status) {
67
- return (status === "ERROR" ||
68
- status === "failed" ||
69
- status === "partial_failed");
70
- }
71
- function mapRawCategory(raw) {
72
- const direct = RAW_TO_NORMALIZED[raw];
73
- if (direct)
74
- return direct;
75
- const lower = raw.toLowerCase();
76
- if (lower.includes("timeout"))
77
- return "timeout";
78
- if (lower.includes("validation") ||
79
- lower.includes("invalid") ||
80
- lower.includes("verification-plan-stale") ||
81
- lower.includes("plan-stale"))
82
- return "validation";
83
- if (lower.includes("auth") || lower.includes("api-key"))
84
- return "auth";
85
- if (lower.includes("write-guard") || lower.includes("write_guard"))
86
- return "write-guard";
87
- if (lower.includes("decision-envelope") || lower.includes("decision_envelope"))
88
- return "decision-envelope";
89
- if (lower.includes("human-rejected"))
90
- return "human-rejected";
91
- if (lower.includes("human-required"))
92
- return "human-required";
93
- if (lower.includes("static-error"))
94
- return "static-error";
95
- if (lower.includes("nonzero-exit") ||
96
- lower.includes("spawn-error") ||
97
- lower.includes("shell"))
98
- return "shell-command";
99
- return undefined;
100
- }
101
- export function isDagFailureCategory(value) {
102
- return DAG_NORMALIZED_FAILURE_CATEGORIES.includes(value);
103
- }
104
- export function normalizeDagFailureCategory(raw, status) {
105
- if (status === "SKIPPED") {
106
- return "skipped";
107
- }
108
- if (raw !== undefined) {
109
- const mapped = mapRawCategory(raw);
110
- if (mapped) {
111
- if (mapped === "success") {
112
- return isFinishedStatus(status) ? "success" : "unknown";
113
- }
114
- return mapped;
115
- }
116
- }
117
- if (raw === undefined && isFinishedStatus(status)) {
118
- return "success";
119
- }
120
- if (isInProgressStatus(status)) {
121
- return "unknown";
122
- }
123
- if (isFailureStatus(status)) {
124
- return "unknown";
125
- }
126
- if (raw === "success" && isFinishedStatus(status)) {
127
- return "success";
128
- }
129
- return "unknown";
130
- }
@@ -2,6 +2,10 @@ import { deterministicSha256, } from "./frontend-implementation-contract.js";
2
2
  import { pathMatchesPattern } from "../../shared/git-progress.js";
3
3
  const SHA256_HEX = /^[a-f0-9]{64}$/;
4
4
  const NPM_PACKAGE_NAME = /^(?:@[a-zA-Z0-9_.-]+\/)?[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
5
+ /** Max concrete implementation files a single writeSet may carry before the
6
+ * plan is asked to split. Kept at 15: beyond this, a single implement node's
7
+ * read/edit loop inflates provider context toward overflow. */
8
+ const MAX_WRITE_SET_FILES = 15;
5
9
  function normalizeRelativePath(value) {
6
10
  return value.replace(/\\/g, "/").replace(/^\.\//, "");
7
11
  }
@@ -26,6 +30,11 @@ export function checkUiDesignCoverage(contract) {
26
30
  const findings = [];
27
31
  const purposes = new Set((contract.uiComponentChoices ?? []).map((choice) => choice.purpose));
28
32
  const hasStyling = Boolean(contract.stylingStrategy?.trim());
33
+ // Reuse of an existing component/pattern covers behavioural interactions
34
+ // without forcing one uiComponentChoices row per interaction: the model
35
+ // should not enumerate every click/hover on a reused Button. Only
36
+ // interactions with no reuse and no explicit purpose are flagged.
37
+ const hasReuseChoice = (contract.uiComponentChoices ?? []).some((choice) => choice.decision === "reuse-existing");
29
38
  for (const state of contract.uiStates) {
30
39
  if (!state.applicable)
31
40
  continue;
@@ -39,13 +48,13 @@ export function checkUiDesignCoverage(contract) {
39
48
  }
40
49
  }
41
50
  for (const interaction of contract.interactions) {
42
- if (!purposes.has(interaction.name)) {
43
- findings.push({
44
- code: "ui-design-coverage-missing",
45
- message: `interaction "${interaction.name}" has no matching uiComponentChoices.purpose`,
46
- path: interaction.name,
47
- });
48
- }
51
+ if (purposes.has(interaction.name) || hasReuseChoice)
52
+ continue;
53
+ findings.push({
54
+ code: "ui-design-coverage-missing",
55
+ message: `interaction "${interaction.name}" has no matching uiComponentChoices.purpose and no reuse-existing choice covering it`,
56
+ path: interaction.name,
57
+ });
49
58
  }
50
59
  return findings;
51
60
  }
@@ -204,7 +213,22 @@ export function checkComponentChoice(contract, componentSpecCandidatePaths) {
204
213
  * must fall inside the declared writeSet patterns. */
205
214
  export function checkTargetPaths(contract, writeSetPatterns) {
206
215
  const findings = [];
216
+ // No frozen writeSet patterns (legacy / unconfigured run) → nothing to
217
+ // check; an empty pattern set must not report every file as out of scope.
218
+ if (writeSetPatterns.length === 0)
219
+ return findings;
207
220
  const matches = (file) => writeSetPatterns.some((pattern) => pathMatchesPattern(file, pattern));
221
+ // A writeSet this large forces a single implement node to read/edit many
222
+ // files, inflating the provider context until it overflows (400
223
+ // request-too-large). Flag it so the plan is asked to split (the contract
224
+ // node already has record_split_proposal) instead of letting the writer
225
+ // blow up mid-run.
226
+ if (contract.targets.files.length > MAX_WRITE_SET_FILES) {
227
+ findings.push({
228
+ code: "write-set-too-large",
229
+ message: `implementation writeSet has ${contract.targets.files.length} files (limit ${MAX_WRITE_SET_FILES}); split the task (record_split_proposal) so no single implement node exceeds the bound`,
230
+ });
231
+ }
208
232
  for (const file of contract.targets.files) {
209
233
  if (!matches(file)) {
210
234
  findings.push({