@tea-agent/loop-agent 0.38.2 → 0.38.3

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 (133) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +2 -0
  3. package/bin/loop-agent.js +7 -3
  4. package/dist/application/task-lifecycle/advance.js +4 -0
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/commands/task-advance.js +31 -0
  7. package/dist/executors/dag-pi-executor.js +58 -1
  8. package/dist/executors/pi-event-serializer.js +42 -1
  9. package/dist/executors/pi-executor.js +51 -0
  10. package/dist/executors/pi-extension-resolver.js +233 -0
  11. package/dist/executors/pi-sdk-executor.js +187 -59
  12. package/dist/executors/shell-executor.js +56 -2
  13. package/dist/executors/shell-write-guard.js +7 -0
  14. package/dist/task/contract/constants.js +1 -0
  15. package/dist/task/contract/diff.js +26 -0
  16. package/dist/task/contract/project.js +5 -0
  17. package/dist/task/contract/schema.js +6 -1
  18. package/dist/task/source-prepare/build-draft.js +15 -0
  19. package/dist/worker/console/pi-readiness.js +4 -4
  20. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4bmo3iY.js → abnfDiagram-N423BO3Z-C4kVYweA.js} +1 -1
  21. package/dist/worker/console/static/assets/{arc-SAD7McOS.js → arc-Bj2M8iO5.js} +1 -1
  22. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DAVLUdK1.js → architectureDiagram-T3A2C74G-CLlXe4-6.js} +1 -1
  23. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CPPdv4vj.js → blockDiagram-VBNYF7ZC-psEp5xH0.js} +1 -1
  24. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D1GRGZax.js → c4Diagram-5PPSVZJV-DeKcOCGJ.js} +1 -1
  25. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +1 -0
  26. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D_Bq2ZqK.js → chunk-2GRJ4B5K-K0C744p1.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CPLVtamK.js → chunk-2Q5K7J3B-xbNw4J9-.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DnaZWcL3.js → chunk-5RXB4S5H-DjLaSDUO.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BRTmFgnq.js → chunk-5VM5RSS4-CtiPlKel.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BRlfYZoH.js → chunk-6Q2QTUOP-DZtolir7.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjNybD4u.js → chunk-GF5L2VYU-CPcdNeew.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3e-74fG.js → chunk-JWPE2WC7-NgHc6V37.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Br3ir6f2.js → chunk-KBJHAD2P-DQ_T-hg8.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-1NgVuRbh.js → chunk-RYQCIY6F-DgLsxcVP.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BTYuvuWN.js → chunk-XXDRQBXY-UrVoM9z1.js} +1 -1
  36. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +1 -0
  37. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +1 -0
  38. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BC6Bgosf.js → cose-bilkent-JH36ORCC-CLkYvTb3.js} +1 -1
  39. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DV-HypfL.js → cynefin-VYW2F7L2-BwT_xKrE.js} +1 -1
  40. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DhTaddO-.js → cynefinDiagram-MW4NZA55-Bi9tuzif.js} +1 -1
  41. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BnEcOmiI.js → dagre-VZM6K2ZE-BCIqkWBV.js} +1 -1
  42. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-xphGRVKp.js → diagram-7IWD3JNH-Dip6l9_Z.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CrdD1mRG.js → diagram-B4RE2ZJO-B-xkh_wK.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D4YdLsaF.js → diagram-LBJQPF4R-DZO0kTt_.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LCKyGZ5U.js → diagram-Q27KOJAE-D6ZplNbZ.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BeiTe7t0.js → diagram-UB23O5K3-BPekbhoS.js} +1 -1
  47. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BO2cq8GS.js → ebnfDiagram-BXEA7PRR-BtmaJpK5.js} +1 -1
  48. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CmWww3Nt.js → erDiagram-JOGREHBK-6DS0Xc44.js} +1 -1
  49. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C2kkUYg5.js → flowDiagram-UKHOOZJN-CihnROSm.js} +1 -1
  50. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DY_JZ2iQ.js → ganttDiagram-PKOTCBZU-CZC4zOE2.js} +1 -1
  51. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BizhCSw9.js → gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js} +1 -1
  52. package/dist/worker/console/static/assets/{index-BXN89VPX.js → index-DTZOKgAn.js} +142 -63
  53. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +1 -0
  54. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Dlo2eK_d.js → infoDiagram-6WML65LV-DlSl4BGx.js} +1 -1
  55. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js → ishikawaDiagram-WSZJBQD7-snnzFl_U.js} +1 -1
  56. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DiMDe5Yh.js → journeyDiagram-NVQOT4AX-BDE7qpMx.js} +1 -1
  57. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-sN553oIN.js → kanban-definition-27J2QSJJ-CD2Ci04G.js} +1 -1
  58. package/dist/worker/console/static/assets/{linear-D9Py31Ld.js → linear-DdnapKIH.js} +1 -1
  59. package/dist/worker/console/static/assets/{mermaid.core-D7Occzk_.js → mermaid.core-BVjAT9b8.js} +5 -5
  60. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DjDRboiR.js → mindmap-definition-FAOFIHXS-D0yJk-zA.js} +1 -1
  61. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Cgc4qlzp.js → pegDiagram-VL7TDLO6-BVo9tFP-.js} +1 -1
  62. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DxZj8pKd.js → pieDiagram-7S7Q4E2Y-DnOV-mZ_.js} +1 -1
  63. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CJ50MngR.js → quadrantDiagram-CIZ2JOQS-CzUJo58i.js} +1 -1
  64. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BY-OD8Tr.js → railroadDiagram-AXF67PYL-Bvikrolh.js} +1 -1
  65. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQFButfK.js → requirementDiagram-LRYGKXZP-DtaSVCap.js} +1 -1
  66. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BB_QUK_b.js → sankeyDiagram-W5VNT64P-C2c9A0wW.js} +1 -1
  67. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CqhXZjyx.js → sequenceDiagram-SI44F4Z6-DmXJcU7r.js} +1 -1
  68. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-2l1U4rgJ.js → sizeCapture-X5ZJPWSS-B4GUFW92.js} +1 -1
  69. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BX1FbrhJ.js → stateDiagram-OKZ733FA-Dsad2MXf.js} +1 -1
  70. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +1 -0
  71. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DOSnT7hb.js → swimlanes-SLNWSIFB-DGq48Fbi.js} +2 -2
  72. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +8 -0
  73. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DVJQA7AN.js → timeline-definition-Z64GVDOM-CSSi6OFf.js} +1 -1
  74. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-74uxl9gx.js → vennDiagram-T6HMQDX7-BqyzPrWv.js} +1 -1
  75. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsBxlcg_.js → wardleyDiagram-T6FBY63Y-DlznVb6S.js} +1 -1
  76. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-snuW9Mjw.js → xychartDiagram-ELKLHX3M-CfBcgJ_K.js} +1 -1
  77. package/dist/worker/console/static/index.html +2 -2
  78. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  79. package/dist/worker/observe/node-input.js +72 -3
  80. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  81. package/dist/worker/observe/static/state.js +2 -2
  82. package/dist/worker/observe/static/views/dag-inspector.js +51 -0
  83. package/dist/worker/observe/static/views/session-timeline.js +18 -5
  84. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +56 -4
  85. package/dist/workflows/dag/backend-test-scenario-partitions.js +73 -1
  86. package/dist/workflows/dag/contract-output-registry.js +15 -0
  87. package/dist/workflows/dag/failure-category.js +4 -0
  88. package/dist/workflows/dag/frontend-implementation-contract.js +140 -39
  89. package/dist/workflows/dag/frontend-prewrite-gate.js +87 -135
  90. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  91. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  92. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  93. package/dist/workflows/dag/frontend-test-result-contract.js +10 -18
  94. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  95. package/dist/workflows/dag/init-hybrid.js +79 -101
  96. package/dist/workflows/dag/node-execution.js +119 -8
  97. package/dist/workflows/dag/prompt.js +15 -1
  98. package/dist/workflows/dag/rerun-plan.js +22 -3
  99. package/dist/workflows/dag/structured-output-repair.js +712 -0
  100. package/dist/workflows/dag/types.js +23 -0
  101. package/dist/workflows/dag/validate.js +41 -1
  102. package/docs/operations/local-development-environment.md +1 -5
  103. package/docs/skills/vetted-skill-registry.md +14 -0
  104. package/docs/templates/README.md +1 -1
  105. package/docs/templates/agent-dag.schema.json +44 -0
  106. package/docs/templates/backend-test-dag.json +2 -2
  107. package/docs/templates/frontend-test-dag.json +8 -10
  108. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  109. package/package.json +8 -4
  110. package/skills/analyze-product-requirements/SKILL.md +8 -5
  111. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  112. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  113. package/skills/analyze-product-requirements/references/example.md +50 -0
  114. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  115. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  116. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  117. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  118. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  119. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  120. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  121. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  122. package/skills/codebase-scout/SKILL.md +1 -1
  123. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  124. package/skills/improve-codebase-architecture/deepening.md +37 -0
  125. package/skills/improve-codebase-architecture/html-report.md +123 -0
  126. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  127. package/skills/improve-codebase-architecture/language.md +53 -0
  128. package/dist/worker/console/static/assets/channel-R6oQIDkw.js +0 -1
  129. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +0 -1
  130. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +0 -1
  131. package/dist/worker/console/static/assets/index-CuH5CNFu.css +0 -1
  132. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +0 -1
  133. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +0 -8
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
4
4
  import { resolveDagNodeSkills } from "../../workflows/dag/skills.js";
5
- import { resolveDagNodePromptRedacted, resolveDagRunJson, } from "./dag-run-artifacts.js";
5
+ import { resolveDagNodePromptRedacted, resolveDagRunArtifact, resolveDagRunJson, } from "./dag-run-artifacts.js";
6
6
  export const NODE_INPUT_SCHEMA_VERSION = 1;
7
7
  export const NODE_INPUT_PREVIEW_MAX_BYTES = 64 * 1024;
8
8
  const SHELL_GATE_KEYS = [
@@ -33,6 +33,7 @@ function unavailableBody(dagRunId, nodeId, reason, warnings = []) {
33
33
  executorInput: null,
34
34
  boundaries: null,
35
35
  promptFingerprint: null,
36
+ piExtensionRunFacts: null,
36
37
  warnings,
37
38
  };
38
39
  }
@@ -137,7 +138,7 @@ function resolveModelHint(spec, task) {
137
138
  }
138
139
  return null;
139
140
  }
140
- function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, warnings) {
141
+ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, piExtensionRunFacts, warnings) {
141
142
  const defaults = spec.defaults && typeof spec.defaults === "object"
142
143
  ? spec.defaults
143
144
  : undefined;
@@ -268,6 +269,7 @@ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, warni
268
269
  kind,
269
270
  skills,
270
271
  modelHint: resolveModelHint(spec, task),
272
+ piExtensions: redactStringList(task.piExtensions),
271
273
  shell,
272
274
  static: staticBlock,
273
275
  },
@@ -284,9 +286,72 @@ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, warni
284
286
  : null,
285
287
  },
286
288
  promptFingerprint: fingerprint,
289
+ piExtensionRunFacts,
287
290
  warnings,
288
291
  };
289
292
  }
293
+ /**
294
+ * Load run-time Pi extension facts from <node>/pi-extensions.json (plan
295
+ * 2026-08-22 R2). Returns null when the node declares no piExtensions
296
+ * (no expected artifact). Never throws: unreadable/corrupt artifacts degrade
297
+ * to parseError stubs so the node-input endpoint stays 200.
298
+ */
299
+ async function loadPiExtensionRunFacts(repoRoot, dagRunId, nodeId, declared, warnings) {
300
+ if (declared.length === 0)
301
+ return null;
302
+ const artifact = path.posix.join(nodeId, "pi-extensions.json");
303
+ const resolved = resolveDagRunArtifact(repoRoot, dagRunId, [
304
+ nodeId,
305
+ "pi-extensions.json",
306
+ ]);
307
+ if (!resolved.ok) {
308
+ if (resolved.reason === "ambiguous") {
309
+ warnings.push("Ambiguous pi-extensions.json lifecycle copies.");
310
+ }
311
+ return {
312
+ artifact,
313
+ exists: false,
314
+ requested: [...declared],
315
+ resolved: [],
316
+ missing: [],
317
+ };
318
+ }
319
+ const empty = {
320
+ artifact,
321
+ exists: false,
322
+ requested: [...declared],
323
+ resolved: [],
324
+ missing: [],
325
+ };
326
+ try {
327
+ const raw = JSON.parse(await readFile(resolved.result.absolutePath, "utf-8"));
328
+ const resolvedList = Array.isArray(raw.resolved) ? raw.resolved : [];
329
+ const missingList = Array.isArray(raw.missing) ? raw.missing : [];
330
+ const projected = resolvedList
331
+ .filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry))
332
+ .map((entry) => ({
333
+ id: redactSecrets(String(entry.id ?? "")),
334
+ packageName: redactSecrets(String(entry.packageName ?? "")),
335
+ version: redactSecrets(String(entry.version ?? "")),
336
+ }));
337
+ const missingProjected = missingList
338
+ .filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry))
339
+ .map((entry) => ({
340
+ id: redactSecrets(String(entry.id ?? "")),
341
+ reason: redactSecrets(String(entry.reason ?? "")),
342
+ }));
343
+ return {
344
+ artifact,
345
+ exists: true,
346
+ requested: redactStringList(raw.requested ?? declared),
347
+ resolved: projected,
348
+ missing: missingProjected,
349
+ };
350
+ }
351
+ catch {
352
+ return { ...empty, parseError: "read-failed" };
353
+ }
354
+ }
290
355
  async function loadPromptFingerprint(repoRoot, dagRunId, nodeId, warnings) {
291
356
  const resolved = resolveDagNodePromptRedacted(repoRoot, dagRunId, nodeId);
292
357
  if (!resolved.ok) {
@@ -415,8 +480,12 @@ export async function buildDagNodeInput(repoRoot, dagRunId, nodeId) {
415
480
  }
416
481
  const warnings = [];
417
482
  const fingerprint = await loadPromptFingerprint(repoRoot, dagRunId, nodeId, warnings);
483
+ const declaredPiExtensions = Array.isArray(task.piExtensions)
484
+ ? task.piExtensions.filter((item) => typeof item === "string")
485
+ : [];
486
+ const piExtensionRunFacts = await loadPiExtensionRunFacts(repoRoot, dagRunId, nodeId, declaredPiExtensions, warnings);
418
487
  // Reject unknown nested leakage by projecting only through allowlist helpers.
419
- const body = projectTask(dagRunId, nodeId, runResolved.result.lifecycle, spec, task, fingerprint, warnings);
488
+ const body = projectTask(dagRunId, nodeId, runResolved.result.lifecycle, spec, task, fingerprint, piExtensionRunFacts, warnings);
420
489
  // Defense-in-depth: ensure raw task never sneaks in.
421
490
  if ("task" in body) {
422
491
  return {
@@ -14,6 +14,10 @@ const ATTEMPT_FAILURE_LABELS = {
14
14
  timeout: "执行超时",
15
15
  "nonzero-exit": "命令执行失败",
16
16
  "invalid-output": "输出格式无效",
17
+ "structured-output-truncated": "结构化输出被截断",
18
+ "output-too-large": "输出超出体积上限",
19
+ "structured-repair-exhausted": "结构化修复耗尽",
20
+ "governance-blocked": "治理规则阻断",
17
21
  "empty-output": "输出为空",
18
22
  auth: "认证失败",
19
23
  quota: "额度或限流",
@@ -76,8 +76,8 @@ export const uiState = {
76
76
  dagNodeExecutionOutputLoading: false,
77
77
  dagNodeExecutionOutputError: null,
78
78
  dagNodeExecutionOutputKey: null,
79
- /** When true,「执行过程」展开 message_start/end 原始协议事件。 */
80
- sessionTimelineShowProtocol: false,
79
+ /** When true,「执行过程」展开 message_start/end 原始协议事件。默认展开,用户可点选隐藏。 */
80
+ sessionTimelineShowProtocol: true,
81
81
  /** Open <details> keys restored across timeline re-renders. */
82
82
  sessionTimelineOpenDetails: null,
83
83
  lastSnapshot: null,
@@ -725,6 +725,11 @@ function renderDagNodeInputDetail(content, data) {
725
725
  }
726
726
  section.appendChild(dl);
727
727
  appendCodeList(section, "skills", data.executorInput.skills);
728
+ appendCodeList(
729
+ section,
730
+ "piExtensions(声明)",
731
+ data.executorInput.piExtensions,
732
+ );
728
733
  if (data.executorInput.shell) {
729
734
  const shell = data.executorInput.shell;
730
735
  const shellDl = el("dl", "node-input-summary");
@@ -799,6 +804,52 @@ function renderDagNodeInputDetail(content, data) {
799
804
  wrap.appendChild(section);
800
805
  }
801
806
 
807
+ if (data.piExtensionRunFacts) {
808
+ const facts = data.piExtensionRunFacts;
809
+ const section = el("section", "node-input-section");
810
+ section.appendChild(el("h4", null, "Pi 扩展运行事实"));
811
+ if (!facts.exists) {
812
+ section.appendChild(
813
+ el(
814
+ "p",
815
+ "muted",
816
+ facts.parseError
817
+ ? `产物 ${facts.artifact} 读取失败(${facts.parseError})。`
818
+ : `产物 ${facts.artifact} 尚未生成(节点未执行或缺失)。`,
819
+ ),
820
+ );
821
+ } else {
822
+ const dl = el("dl", "node-input-summary");
823
+ appendMetaRow(
824
+ dl,
825
+ "resolved",
826
+ facts.resolved
827
+ .map((entry) => `${entry.id} → ${entry.packageName}@${entry.version}`)
828
+ .join(", ") || "(none)",
829
+ null,
830
+ );
831
+ section.appendChild(dl);
832
+ appendCodeList(
833
+ section,
834
+ "requested",
835
+ facts.requested,
836
+ );
837
+ for (const missingEntry of facts.missing ?? []) {
838
+ section.appendChild(
839
+ el(
840
+ "p",
841
+ "node-input-warning",
842
+ `降级:${missingEntry.id} 未加载(${missingEntry.reason || "unknown"})`,
843
+ ),
844
+ );
845
+ }
846
+ if ((facts.missing ?? []).length === 0) {
847
+ section.appendChild(el("p", "muted", "全部声明扩展均已解析加载。"));
848
+ }
849
+ }
850
+ wrap.appendChild(section);
851
+ }
852
+
802
853
  if (data.promptFingerprint?.exists) {
803
854
  const section = el("section", "node-input-section");
804
855
  section.appendChild(el("h4", null, "Assembled prompt 指纹"));
@@ -439,12 +439,25 @@ function ensureOpenDetailsSet() {
439
439
  return uiState.sessionTimelineOpenDetails;
440
440
  }
441
441
 
442
- function bindDetailsToggle(details, detailKey) {
442
+ function closedDetailKey(detailKey) {
443
+ return `${detailKey}::user-closed`;
444
+ }
445
+
446
+ function bindDetailsToggle(details, detailKey, defaultOpen = false) {
443
447
  const openSet = ensureOpenDetailsSet();
444
- if (openSet.has(detailKey)) details.open = true;
448
+ if (defaultOpen) {
449
+ if (!openSet.has(closedDetailKey(detailKey))) details.open = true;
450
+ } else if (openSet.has(detailKey)) {
451
+ details.open = true;
452
+ }
445
453
  details.addEventListener("toggle", () => {
446
- if (details.open) openSet.add(detailKey);
447
- else openSet.delete(detailKey);
454
+ if (details.open) {
455
+ openSet.add(detailKey);
456
+ openSet.delete(closedDetailKey(detailKey));
457
+ } else {
458
+ openSet.delete(detailKey);
459
+ openSet.add(closedDetailKey(detailKey));
460
+ }
448
461
  });
449
462
  }
450
463
 
@@ -599,7 +612,7 @@ function renderProtocolCard(item) {
599
612
  const details = document.createElement("details");
600
613
  details.className = "process-timeline-details";
601
614
  details.dataset.detailKey = item.detailKey;
602
- bindDetailsToggle(details, item.detailKey);
615
+ bindDetailsToggle(details, item.detailKey, true);
603
616
  const summary = document.createElement("summary");
604
617
  summary.className = "process-timeline-summary";
605
618
  summary.textContent = `消息收发 · ${item.protocolCount} 条协议事件`;
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  import { z } from "zod";
6
6
  import YAML from "yaml";
7
7
  import { resolveBackendTestLayout, } from "./backend-test-layout.js";
8
- import { assessScenarioPartitionCoverage, parseScenarioPartitions, } from "./backend-test-scenario-partitions.js";
8
+ import { applyOpenApiPartitionDomainPolicy, assessScenarioPartitionCoverage, parseScenarioPartitions, } from "./backend-test-scenario-partitions.js";
9
9
  import { backendTestCaseManifestSchema, computeCaseManifestCoverageSummary, } from "./backend-test-case-manifest.js";
10
10
  const CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-\d{3})\b.*$/gm;
11
11
  const CASE_ID_IN_TEXT = /\bBE-[A-Z0-9_-]+-\d{3}\b/g;
@@ -486,6 +486,55 @@ export async function extractBackendTestOpenApiRules(input) {
486
486
  }
487
487
  return rules.sort((left, right) => left.ruleKey.localeCompare(right.ruleKey));
488
488
  }
489
+ export async function extractBackendTestOpenApiPartitionAxes(input) {
490
+ const axes = [];
491
+ const seen = new Set();
492
+ for (const referencePath of input.sourceBinding.referencePaths) {
493
+ const physicalPath = resolveBoundSourcePath(input.workspaceRoot, input.sourceBinding.taskId, referencePath);
494
+ let document;
495
+ try {
496
+ const parsed = YAML.parse(await readFile(physicalPath, "utf8"));
497
+ if (!isRecord(parsed) || !isRecord(parsed.paths))
498
+ continue;
499
+ document = parsed;
500
+ }
501
+ catch {
502
+ continue;
503
+ }
504
+ for (const [apiPath, pathItem] of Object.entries(document.paths)) {
505
+ if (!isRecord(pathItem))
506
+ continue;
507
+ const operation = pathItem.get;
508
+ if (!isRecord(operation))
509
+ continue;
510
+ const parameterValues = [
511
+ ...(Array.isArray(pathItem.parameters) ? pathItem.parameters : []),
512
+ ...(Array.isArray(operation.parameters) ? operation.parameters : []),
513
+ ];
514
+ for (const rawParameter of parameterValues) {
515
+ const parameter = resolveOpenApiSchema(document, rawParameter);
516
+ if (!parameter || typeof parameter.name !== "string")
517
+ continue;
518
+ const location = parameter.in === "path" ? "path" : parameter.in === "query" ? "query" : undefined;
519
+ if (!location)
520
+ continue;
521
+ const property = resolveOpenApiSchema(document, parameter.schema);
522
+ const enumValues = property && Array.isArray(property.enum) ? property.enum.map(String) : null;
523
+ const identity = `GET ${apiPath}|${location}|${parameter.name}`;
524
+ if (seen.has(identity))
525
+ continue;
526
+ seen.add(identity);
527
+ axes.push({
528
+ operation: `GET ${apiPath}`,
529
+ name: parameter.name,
530
+ in: location,
531
+ enumValues,
532
+ });
533
+ }
534
+ }
535
+ }
536
+ return axes;
537
+ }
489
538
  function orderedUnique(values) {
490
539
  const result = [];
491
540
  const seen = new Set();
@@ -985,8 +1034,11 @@ export async function analyzeBackendTestCaseCoverage(input) {
985
1034
  // Plan C: deterministic query/filter axis partitions (fail-closed on missing slots).
986
1035
  const parsedPartitions = parseScenarioPartitions(readme);
987
1036
  findings.push(...parsedPartitions.issues);
1037
+ const openApiPartitionAxes = await extractBackendTestOpenApiPartitionAxes(input);
1038
+ const partitionedBySource = applyOpenApiPartitionDomainPolicy(parsedPartitions.partitions, openApiPartitionAxes);
1039
+ findings.push(...partitionedBySource.issues);
988
1040
  const declaredPointUniverse = new Set(cases.flatMap((item) => item.testPoints));
989
- const partitionAssessment = assessScenarioPartitionCoverage(parsedPartitions.partitions, declaredPointUniverse);
1041
+ const partitionAssessment = assessScenarioPartitionCoverage(partitionedBySource.partitions, declaredPointUniverse);
990
1042
  findings.push(...partitionAssessment.findings);
991
1043
  const [enumValueCount, coveredEnumValueCount] = dimensionCounts(rules, /enum/);
992
1044
  const [boundaryPointCount, coveredBoundaryPointCount] = dimensionCounts(rules, /boundary|length|numeric/);
@@ -1003,7 +1055,7 @@ export async function analyzeBackendTestCaseCoverage(input) {
1003
1055
  sourceBinding: input.sourceBinding,
1004
1056
  inputFiles: await inputFileFacts(input.workspaceRoot, [
1005
1057
  ...((await exists(readmePath)) ? [readmePath] : []),
1006
- ...(await markdownModuleFiles(input.workspaceRoot)),
1058
+ ...(await markdownModuleFiles(input.workspaceRoot, layout)),
1007
1059
  ...(await Promise.all(input.sourceBinding.referencePaths.map(async (referencePath) => {
1008
1060
  const physicalPath = resolveBoundSourcePath(input.workspaceRoot, input.sourceBinding.taskId, referencePath);
1009
1061
  return (await exists(physicalPath)) ? physicalPath : "";
@@ -1794,7 +1846,7 @@ export async function analyzeBackendTestMarkdownPytestCorrespondence(input) {
1794
1846
  status: findings.length === 0 ? "PASS" : "FAIL",
1795
1847
  inputFiles: await inputFileFacts(input.workspaceRoot, [
1796
1848
  path.join(input.workspaceRoot, ...layout.readmePath.split("/")),
1797
- ...(await markdownModuleFiles(input.workspaceRoot)),
1849
+ ...(await markdownModuleFiles(input.workspaceRoot, layout)),
1798
1850
  ...scriptFiles,
1799
1851
  ].filter((file) => file && path.isAbsolute(file))),
1800
1852
  summary: {
@@ -35,6 +35,8 @@ export const scenarioPartitionRowSchema = z.object({
35
35
  const PARTITION_HEADING = /^##\s+Scenario Partitions\s*$/;
36
36
  const PARTITION_TABLE_HEADER = /^\|\s*Partition ID\s*\|\s*Operation\s*\|\s*Axis\s*\|\s*Domain\s*\|\s*Required Slots\s*\|\s*Expected by Slot\s*\|\s*Bind Rule\s*\|\s*$/;
37
37
  const TABLE_ROW = /^\|(.+)\|$/;
38
+ const FINITE_DOMAIN_MEMBER = /^[A-Za-z0-9]+(?:[._\- ][A-Za-z0-9]+)*$/;
39
+ const RESERVED_SLOT_TOKENS = new Set(["OMITTED", "NOT-IN-SET"]);
38
40
  function splitCells(raw) {
39
41
  return raw
40
42
  .split("|")
@@ -100,6 +102,11 @@ export function parseScenarioPartitions(readmeMarkdown) {
100
102
  continue;
101
103
  }
102
104
  seen.add(partitionId);
105
+ const domainIssue = describeInvalidPartitionDomain(domain);
106
+ if (domainIssue) {
107
+ issues.push(`INVALID_PARTITION_DOMAIN: partition ${partitionId} ${domainIssue}`);
108
+ continue;
109
+ }
103
110
  partitions.push({
104
111
  partitionId,
105
112
  operation,
@@ -119,6 +126,40 @@ function slotToken(value) {
119
126
  .replace(/[^A-Z0-9]+/g, "-")
120
127
  .replace(/^-+|-+$/g, "");
121
128
  }
129
+ function describeInvalidPartitionDomain(domain) {
130
+ const tokens = [];
131
+ const seenTokens = new Set();
132
+ for (const value of domain) {
133
+ if (!FINITE_DOMAIN_MEMBER.test(value)) {
134
+ return `value is not a finite identifier member: ${value}`;
135
+ }
136
+ const token = slotToken(value);
137
+ if (!token) {
138
+ return `value cannot produce a stable non-empty slot token: ${value}`;
139
+ }
140
+ if (RESERVED_SLOT_TOKENS.has(token)) {
141
+ return `value maps to reserved slot token ${token}: ${value}`;
142
+ }
143
+ if (seenTokens.has(token)) {
144
+ return `values collapse to duplicate slot token ${token}`;
145
+ }
146
+ seenTokens.add(token);
147
+ tokens.push(token);
148
+ }
149
+ return tokens.length === 0 ? "domain produced no slot tokens" : undefined;
150
+ }
151
+ function normalizePartitionAxisName(value) {
152
+ return value.trim().replace(/^(?:query|path)\s*:\s*/i, "").toLowerCase();
153
+ }
154
+ function normalizePartitionOperation(value) {
155
+ return value.trim().replace(/\s+/g, " ").toUpperCase();
156
+ }
157
+ function sameFiniteSet(left, right) {
158
+ if (left.length !== right.length)
159
+ return false;
160
+ const expected = new Set(left);
161
+ return right.every((value) => expected.has(value));
162
+ }
122
163
  const STABLE_PARTITION_ID = /^SP-[A-Z0-9][A-Z0-9._-]*$/i;
123
164
  /**
124
165
  * Slot IDs are `TP-<Partition ID>-<VALUE|OMITTED|NOT-IN-SET>`.
@@ -138,13 +179,16 @@ function partitionToken(row) {
138
179
  export function expandScenarioPartitions(partitions) {
139
180
  const slots = [];
140
181
  for (const row of partitions) {
182
+ if (describeInvalidPartitionDomain(row.domain))
183
+ continue;
141
184
  const prefix = partitionToken(row);
142
185
  for (const value of row.domain) {
186
+ const token = slotToken(value);
143
187
  slots.push({
144
188
  partitionId: row.partitionId,
145
189
  operation: row.operation,
146
190
  axis: row.axis,
147
- slotId: `${prefix}-${slotToken(value)}`,
191
+ slotId: `${prefix}-${token}`,
148
192
  kind: "each-value",
149
193
  value,
150
194
  intent: "nominal-filter",
@@ -176,6 +220,34 @@ export function expandScenarioPartitions(partitions) {
176
220
  }
177
221
  return slots;
178
222
  }
223
+ /**
224
+ * Bind parsed partitions to GET query/path OpenAPI axes.
225
+ * Unconfirmed axes keep identifier-valid Domain rows (requirement finite sets).
226
+ */
227
+ export function applyOpenApiPartitionDomainPolicy(partitions, axes) {
228
+ const issues = [];
229
+ const valid = [];
230
+ for (const row of partitions) {
231
+ const operation = normalizePartitionOperation(row.operation);
232
+ const axisName = normalizePartitionAxisName(row.axis);
233
+ const matched = axes.find((axis) => normalizePartitionOperation(axis.operation) === operation &&
234
+ normalizePartitionAxisName(axis.name) === axisName);
235
+ if (!matched) {
236
+ valid.push(row);
237
+ continue;
238
+ }
239
+ if (matched.enumValues === null) {
240
+ issues.push(`NO_FINITE_DOMAIN: partition ${row.partitionId} axis ${row.axis} exists on ${row.operation} without a documented enum`);
241
+ continue;
242
+ }
243
+ if (!sameFiniteSet(matched.enumValues, row.domain)) {
244
+ issues.push(`PARTITION_SOURCE_MISMATCH: partition ${row.partitionId} domain does not match OpenAPI enum for ${row.operation} ${row.axis}`);
245
+ continue;
246
+ }
247
+ valid.push(row);
248
+ }
249
+ return { partitions: valid, issues };
250
+ }
179
251
  /** Facts projection consumed by N6 and plan D gap-fill. */
180
252
  export const scenarioPartitionFactsSchema = z.object({
181
253
  partitions: z.array(z.object({
@@ -1,4 +1,19 @@
1
1
  import { structuredContractOutputSchemaIds, } from "./types.js";
2
+ /**
3
+ * Registry of node-level structured contract validators, keyed by the
4
+ * `structuredContractOutput.schemaId` declared on a DAG task.
5
+ *
6
+ * The executor looks the validator up here instead of hardcoding per-contract
7
+ * imports, so new contracts (backend-test manifests, coverage contracts, ...)
8
+ * can opt into producing-node self-validation without touching the executor.
9
+ */
10
+ export const structuredContractValidationClassifications = [
11
+ "format",
12
+ "schema",
13
+ "truncated",
14
+ "semantic-repair",
15
+ "governance-blocked",
16
+ ];
2
17
  const validators = new Map();
3
18
  export function registerStructuredContractValidator(schemaId, validator) {
4
19
  if (validators.has(schemaId)) {
@@ -31,6 +31,10 @@ const RAW_TO_NORMALIZED = {
31
31
  "spawn-error": "shell-command",
32
32
  "static-error": "static-error",
33
33
  "invalid-output": "validation",
34
+ "structured-output-truncated": "validation",
35
+ "output-too-large": "validation",
36
+ "structured-repair-exhausted": "validation",
37
+ "governance-blocked": "validation",
34
38
  "path-violation": "validation",
35
39
  "empty-output": "validation",
36
40
  "verify-failure": "validation",