@tea-agent/loop-agent 0.40.0-next.1 → 0.40.0-next.11

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 (213) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +114 -109
  3. package/README.md +2 -2
  4. package/dist/application/dag/generate-task-dag.js +6 -2
  5. package/dist/application/task-lifecycle/advance.js +12 -0
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/update/runtime-activity.js +1 -29
  8. package/dist/commands/dag-rerun.js +1 -1
  9. package/dist/executors/dag-pi-executor.js +69 -7
  10. package/dist/executors/pi-executor.js +4 -2
  11. package/dist/executors/pi-sdk-executor.js +46 -1
  12. package/dist/executors/pi-writer-tool-policy.js +57 -1
  13. package/dist/executors/shell-executor.js +107 -20
  14. package/dist/executors/workspace-write-snapshot.js +68 -0
  15. package/dist/governance/checks.js +1 -0
  16. package/dist/infrastructure/harness/atomic-write.js +23 -0
  17. package/dist/shared/dag-prompt-override.js +4 -5
  18. package/dist/shared/update/npm-client.js +33 -11
  19. package/dist/task/config-types.js +10 -0
  20. package/dist/task/contract/apply.js +11 -1
  21. package/dist/task/contract/constants.js +2 -0
  22. package/dist/task/contract/hash.js +3 -0
  23. package/dist/task/contract/observe.js +25 -4
  24. package/dist/task/contract/paths.js +2 -1
  25. package/dist/task/contract/project.js +15 -1
  26. package/dist/task/contract/schema.js +2 -0
  27. package/dist/task/contract/transaction.js +12 -0
  28. package/dist/task/source-prepare/completeness.js +188 -0
  29. package/dist/task/source-prepare/fragment-inventory.js +474 -0
  30. package/dist/task/source-prepare/index.js +3 -0
  31. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  32. package/dist/task/source-prepare/ledger-review.js +214 -0
  33. package/dist/task/source-prepare/ledger.js +545 -0
  34. package/dist/task/source-prepare/prepare.js +262 -1
  35. package/dist/task/source-prepare/semantic-intake.js +19 -3
  36. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  37. package/dist/task/source-prepare/types.js +22 -0
  38. package/dist/worker/cli.js +12 -26
  39. package/dist/worker/console/chat/chat-event-store.js +75 -5
  40. package/dist/worker/console/chat/context-panel.js +7 -0
  41. package/dist/worker/console/chat/deferred-turn.js +12 -0
  42. package/dist/worker/console/chat/pi-runtime.js +331 -18
  43. package/dist/worker/console/chat/provider-error.js +98 -0
  44. package/dist/worker/console/chat/routes.js +389 -56
  45. package/dist/worker/console/chat/session-stats.js +179 -0
  46. package/dist/worker/console/chat/session-store.js +93 -63
  47. package/dist/worker/console/chat/todos.js +115 -0
  48. package/dist/worker/console/chat/tool-preview.js +42 -0
  49. package/dist/worker/console/chat/turn-process.js +20 -55
  50. package/dist/worker/console/chat/user-questions.js +266 -0
  51. package/dist/worker/console/chat/workspace-landing.js +79 -0
  52. package/dist/worker/console/console-handoff.js +82 -0
  53. package/dist/worker/console/console-update-and-init.js +115 -0
  54. package/dist/worker/console/console-update-runtime.js +84 -0
  55. package/dist/worker/console/dag-execution-receipt.js +20 -2
  56. package/dist/worker/console/draft-store.js +49 -0
  57. package/dist/worker/console/native-directory-picker.js +13 -0
  58. package/dist/worker/console/operation-store.js +1 -1
  59. package/dist/worker/console/operator-actions.js +133 -7
  60. package/dist/worker/console/operator-surface-health.js +1 -0
  61. package/dist/worker/console/prd-intake-bridge.js +13 -0
  62. package/dist/worker/console/routes.js +312 -1
  63. package/dist/worker/console/security.js +4 -4
  64. package/dist/worker/console/server.js +583 -172
  65. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Br92EGb7.js → abnfDiagram-N423BO3Z-Bs-CDAXM.js} +1 -1
  66. package/dist/worker/console/static/assets/{arc-oulImtQq.js → arc-CAkA3We3.js} +1 -1
  67. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-tyHc6aqO.js → architectureDiagram-T3A2C74G-DEmI_zqr.js} +1 -1
  68. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CY86BSpT.js → blockDiagram-VBNYF7ZC-uDHANebE.js} +1 -1
  69. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BKKsDmNs.js → c4Diagram-5PPSVZJV-Bv97b4_B.js} +1 -1
  70. package/dist/worker/console/static/assets/channel-amxUpk7o.js +1 -0
  71. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CXZdjl3B.js → chunk-2GRJ4B5K-shmQKsFz.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B-aIFDQ5.js → chunk-2Q5K7J3B-BAraNvcN.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BDgAW_B2.js → chunk-5RXB4S5H-BirhxUop.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DABa4UPR.js → chunk-5VM5RSS4-C9LgUJpR.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-aGbysVOx.js → chunk-6Q2QTUOP-DrU-s-t7.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B8MSZDVs.js → chunk-GF5L2VYU-Tx-H_FA2.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CpelV73g.js → chunk-JWPE2WC7-BjLtNFrR.js} +1 -1
  78. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BE04ty3z.js → chunk-KBJHAD2P-UNLVwSsy.js} +1 -1
  79. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BUxTkaaR.js → chunk-RYQCIY6F-CBEUX8XD.js} +1 -1
  80. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CYYBVKfX.js → chunk-XXDRQBXY-WTL7IZOq.js} +1 -1
  81. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +1 -0
  82. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +1 -0
  83. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CEQF_xmQ.js → cose-bilkent-JH36ORCC-CkHG6W3s.js} +1 -1
  84. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CqpxN5IT.js → cynefin-VYW2F7L2-BRzFqTaL.js} +1 -1
  85. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-B3PzanVi.js → cynefinDiagram-MW4NZA55-2_CFp_i7.js} +1 -1
  86. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BSUGCNx1.js → dagre-VZM6K2ZE-BJtsmdkE.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdLevU5T.js → diagram-7IWD3JNH-DNEFFo6w.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BjpF_Kok.js → diagram-B4RE2ZJO-B_IWV7eo.js} +1 -1
  89. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C4I6NJny.js → diagram-LBJQPF4R-DvvRx2dz.js} +1 -1
  90. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C8hecCr5.js → diagram-Q27KOJAE-CsibsWLg.js} +1 -1
  91. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Daod6tOS.js → diagram-UB23O5K3-les_djBQ.js} +1 -1
  92. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BvvmazGv.js → ebnfDiagram-BXEA7PRR-BEm2n7qJ.js} +1 -1
  93. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DeN_vfV4.js → erDiagram-JOGREHBK-BI7vHHVm.js} +1 -1
  94. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D6dq1t9z.js → flowDiagram-UKHOOZJN-CUFCERrB.js} +1 -1
  95. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DziIdNY4.js → ganttDiagram-PKOTCBZU-wxyLiGWt.js} +1 -1
  96. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js → gitGraphDiagram-DS77QQ5N-Bt897K8Z.js} +1 -1
  97. package/dist/worker/console/static/assets/index-BHhUOTri.css +1 -0
  98. package/dist/worker/console/static/assets/index-DqJbO3-p.js +407 -0
  99. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czv5eYx2.js → infoDiagram-6WML65LV-DYNx_IPv.js} +1 -1
  100. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js → ishikawaDiagram-WSZJBQD7-CzpLNEu6.js} +1 -1
  101. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DxGmNj5K.js → journeyDiagram-NVQOT4AX-97f9owNU.js} +1 -1
  102. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CYCFqkO_.js → kanban-definition-27J2QSJJ-BxM9hyBn.js} +1 -1
  103. package/dist/worker/console/static/assets/{linear-B0YzRwrd.js → linear-ByuxcHvp.js} +1 -1
  104. package/dist/worker/console/static/assets/{mermaid.core-t3EZtIGh.js → mermaid.core-DwPGvpCv.js} +5 -5
  105. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BTXpTv0T.js → mindmap-definition-FAOFIHXS-DCoqG9RT.js} +1 -1
  106. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DU8UkKqA.js → pegDiagram-VL7TDLO6-BLSBmBMx.js} +1 -1
  107. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DP4y8ohp.js → pieDiagram-7S7Q4E2Y-B0t_0oJW.js} +1 -1
  108. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B07vNaYR.js → quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js} +1 -1
  109. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-xDWktb2i.js → railroadDiagram-AXF67PYL-B3jRdATC.js} +1 -1
  110. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BLNMKqeS.js → requirementDiagram-LRYGKXZP-D-cTtXEN.js} +1 -1
  111. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-n0adalxH.js → sankeyDiagram-W5VNT64P-B25igJRE.js} +1 -1
  112. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BbnS-2DL.js → sequenceDiagram-SI44F4Z6-jW_lM1_D.js} +1 -1
  113. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4uPpCLu.js → sizeCapture-X5ZJPWSS-CrzTgsHE.js} +1 -1
  114. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA--MMbB0fX.js → stateDiagram-OKZ733FA-B01MvlqE.js} +1 -1
  115. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +1 -0
  116. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BXUgp2QY.js → swimlanes-SLNWSIFB-YcF12FiU.js} +2 -2
  117. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +8 -0
  118. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CsaMxki7.js → timeline-definition-Z64GVDOM-ClnRcg2s.js} +1 -1
  119. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C2IuG9iH.js → vennDiagram-T6HMQDX7-B6gr_z-q.js} +1 -1
  120. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CmEWOXCW.js → wardleyDiagram-T6FBY63Y-CvSgguGH.js} +1 -1
  121. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D_9LE0MG.js → xychartDiagram-ELKLHX3M-23OtSZCb.js} +1 -1
  122. package/dist/worker/console/static/index.html +2 -2
  123. package/dist/worker/console/static-src/app/console-types.js +68 -4
  124. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  125. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  126. package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
  127. package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
  128. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  129. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  130. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
  131. package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
  132. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  133. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  134. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  135. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  136. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
  137. package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
  138. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
  139. package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
  140. package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
  141. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  142. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  143. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  144. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  145. package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
  146. package/dist/worker/console/workspace-context.js +234 -0
  147. package/dist/worker/console/workspace-registry.js +214 -0
  148. package/dist/worker/observability/init-runtime-activity.js +26 -0
  149. package/dist/worker/observe/node-input.js +88 -2
  150. package/dist/worker/observe/routes.js +188 -0
  151. package/dist/worker/observe/server.js +4 -0
  152. package/dist/worker/observe/static/api.js +29 -9
  153. package/dist/worker/observe/static/app.js +18 -70
  154. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  155. package/dist/worker/observe/static/dag-node-purpose.js +203 -0
  156. package/dist/worker/observe/static/format.js +37 -0
  157. package/dist/worker/observe/static/index.html +6 -1
  158. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  159. package/dist/worker/observe/static/operator-chrome.css +145 -20
  160. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  161. package/dist/worker/observe/static/operator-chrome.js +330 -100
  162. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  163. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  164. package/dist/worker/observe/static/router.d.ts +46 -0
  165. package/dist/worker/observe/static/router.js +25 -1
  166. package/dist/worker/observe/static/state.d.ts +50 -0
  167. package/dist/worker/observe/static/state.js +174 -2
  168. package/dist/worker/observe/static/styles.css +357 -35
  169. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  170. package/dist/worker/observe/static/views/dag-inspector.js +533 -79
  171. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  172. package/dist/worker/observe/static/views/dag.js +6 -4
  173. package/dist/worker/observe/static/views/dashboard.js +6 -4
  174. package/dist/worker/observe/static/views/night.js +2 -2
  175. package/dist/worker/observe/static/views/pool.js +1 -0
  176. package/dist/worker/observe/static/views/run.js +3 -1
  177. package/dist/worker/observe/static/views/task.js +56 -1
  178. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +186 -8
  179. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  180. package/dist/workflows/dag/backend-test-pytest-collection.js +168 -16
  181. package/dist/workflows/dag/backend-test-result-contract.js +4 -0
  182. package/dist/workflows/dag/backend-test-scenario-param.js +339 -53
  183. package/dist/workflows/dag/backend-test-writer-completeness.js +28 -17
  184. package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
  185. package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
  186. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  187. package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
  188. package/dist/workflows/dag/frontend-review-context.js +92 -10
  189. package/dist/workflows/dag/init-hybrid.js +239 -52
  190. package/dist/workflows/dag/interrupt-request.js +3 -3
  191. package/dist/workflows/dag/lifecycle.js +3 -3
  192. package/dist/workflows/dag/node-execution.js +45 -0
  193. package/dist/workflows/dag/types.js +62 -12
  194. package/docs/README.md +2 -0
  195. package/docs/architecture/evolution.md +45 -0
  196. package/docs/init-surface.manifest.json +1 -0
  197. package/docs/operations/local-development-environment.md +21 -0
  198. package/docs/templates/README.md +1 -1
  199. package/docs/templates/agent-dag.schema.json +18 -0
  200. package/docs/templates/backend-test-dag.json +267 -238
  201. package/docs/templates/frontend-implementation-contract.schema.json +681 -24
  202. package/harness.json +3 -3
  203. package/package.json +3 -1
  204. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  205. package/dist/worker/console/static/assets/channel-TY36zt-i.js +0 -1
  206. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +0 -1
  207. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +0 -1
  208. package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
  209. package/dist/worker/console/static/assets/index-Ci8ksvBm.js +0 -406
  210. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +0 -1
  211. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +0 -8
  212. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  213. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
@@ -0,0 +1,13 @@
1
+ export interface DagNodeRemark {
2
+ failed: boolean;
3
+ text: string;
4
+ isErrorSummary: boolean;
5
+ }
6
+
7
+ export function resolveNodeRemark(node: {
8
+ status?: string | null;
9
+ errorPreview?: string | null;
10
+ label?: string | null;
11
+ nodeId: string;
12
+ rank?: string | number | null;
13
+ } | null | undefined): DagNodeRemark;
@@ -70,6 +70,7 @@ import {
70
70
  dashboardVisibleFeatures,
71
71
  findDagForRun,
72
72
  } from "../dag-model.js";
73
+ import { describeDagNodePurpose } from "../dag-node-purpose.js";
73
74
  import { layoutDag } from "../dag-layout.js";
74
75
  import {
75
76
  showView,
@@ -136,7 +137,7 @@ export function extractFirstErrorSummary(errorPreview) {
136
137
  return effective ?? "";
137
138
  }
138
139
 
139
- /** Resolve 备注 cell content for a DAG node (failed → summary; else label). */
140
+ /** Resolve 备注 cell content for a DAG node (failed → summary; else purpose). */
140
141
  export function resolveNodeRemark(node) {
141
142
  const failed = isNodeFailed(node?.status);
142
143
  if (failed) {
@@ -147,10 +148,10 @@ export function resolveNodeRemark(node) {
147
148
  isErrorSummary: Boolean(summary),
148
149
  };
149
150
  }
150
- const label = typeof node?.label === "string" ? node.label.trim() : "";
151
+ const purpose = describeDagNodePurpose(node?.nodeId, node);
151
152
  return {
152
153
  failed: false,
153
- text: label || "—",
154
+ text: purpose || "—",
154
155
  isErrorSummary: false,
155
156
  };
156
157
  }
@@ -363,7 +364,7 @@ export async function renderDagDetail(dagRunId, initial = true) {
363
364
  "page-action-btn",
364
365
  "在操作台中止此运行 →",
365
366
  );
366
- interruptLink.href = `/#/recovery?dagRunId=${encodeURIComponent(dag.dagRunId)}&intent=interrupt`;
367
+ interruptLink.href = `/#/operate/recovery?dagRunId=${encodeURIComponent(dag.dagRunId)}&intent=interrupt`;
367
368
  interruptLink.setAttribute("data-testid", "inspect-dag-interrupt-link");
368
369
  interruptLink.title = "只跳转到操作台 Recovery,不会在观测页执行中止";
369
370
  primaryEntries.push(["协作中止", interruptLink]);
@@ -481,6 +482,7 @@ export function startDagPolling(dagRunId, initial = true) {
481
482
  const active = await renderDagDetail(dagRunId, isInitial);
482
483
  if (generation !== uiState.pollingGeneration || !active) return;
483
484
  const delay = detailPollDelay("running", isPageVisible());
485
+ if (delay == null) return;
484
486
  uiState.dagPollTimer = setTimeout(() => {
485
487
  uiState.dagPollTimer = null;
486
488
  void poll(false);
@@ -84,7 +84,7 @@ let dashboardLoadingNode = null;
84
84
 
85
85
  /**
86
86
  * 「昨夜班次」条带:夜间结果在观测侧的唯一一级呈现(替代旧「夜间任务」tab)。
87
- * 管理动作(晨审处置)始终落在操作侧 /#/night,保持 Inspect 只读不变式。
87
+ * 管理动作(晨审处置)始终落在操作侧 /#/operate/night,保持 Inspect 只读不变式。
88
88
  */
89
89
  async function renderNightStrip() {
90
90
  const strip = document.getElementById("dashboard-night");
@@ -118,14 +118,14 @@ async function renderNightStrip() {
118
118
 
119
119
  const actions = el("div", "night-strip-actions");
120
120
  const detail = el("a", "page-action-btn", "夜间详情");
121
- detail.href = "#/night";
121
+ detail.href = "/#/inspect/night";
122
122
  detail.addEventListener("click", (e) => {
123
123
  e.preventDefault();
124
124
  navigate("/night");
125
125
  });
126
126
  actions.appendChild(detail);
127
127
  const manage = el("a", "page-action-btn night-strip-manage", "晨审处置 →");
128
- manage.href = "/#/night";
128
+ manage.href = "/#/operate/night";
129
129
  manage.title = "夜间计划的管理与晨审处置在操作台完成(Inspect 保持只读)";
130
130
  actions.appendChild(manage);
131
131
  strip.appendChild(actions);
@@ -738,10 +738,12 @@ export function startDashboardPolling(scrollTo) {
738
738
  const poll = async (initialScrollTo) => {
739
739
  await renderDashboard(initialScrollTo);
740
740
  if (generation !== uiState.pollingGeneration) return;
741
+ const delay = dashboardPollDelay();
742
+ if (delay == null) return;
741
743
  uiState.dashboardTimer = setTimeout(() => {
742
744
  uiState.dashboardTimer = null;
743
745
  void poll();
744
- }, dashboardPollDelay());
746
+ }, delay);
745
747
  };
746
748
  void poll(scrollTo);
747
749
  }
@@ -46,8 +46,8 @@ export async function renderNightJobs(scheduleId) {
46
46
  showView("night");
47
47
  bindNightToolbar();
48
48
  const crumbs = [
49
- { label: UI_TEXT.dashboard, href: "#/" },
50
- { label: UI_TEXT.nightJobs, href: "#/night" },
49
+ { label: UI_TEXT.dashboard, href: "/#/inspect" },
50
+ { label: UI_TEXT.nightJobs, href: "/#/inspect/night" },
51
51
  ];
52
52
  if (scheduleId) crumbs.push({ label: scheduleId });
53
53
  setBreadcrumb(crumbs);
@@ -93,6 +93,7 @@ function startPoolAutoPolling() {
93
93
  if (generation !== uiState.pollingGeneration) return;
94
94
  if (!poolAutoRefreshEnabled()) return;
95
95
  const delay = isPageVisible() ? POOL_POLL_MS : dashboardPollDelay();
96
+ if (delay == null) return;
96
97
  uiState.poolTimer = setTimeout(() => {
97
98
  uiState.poolTimer = null;
98
99
  void poll();
@@ -50,6 +50,7 @@ import {
50
50
  } from "../relations.js";
51
51
  import { copyText, renderRecommendedCommand } from "../copy.js";
52
52
  import {
53
+ fetchWithWorkspaceScope,
53
54
  isPageVisible,
54
55
  detailPollDelay,
55
56
  dashboardPollDelay,
@@ -126,7 +127,7 @@ export function renderInlineArtifactList(container, artifacts) {
126
127
  list.appendChild(li);
127
128
  void (async () => {
128
129
  try {
129
- const res = await fetch(artifactUrl(artifactPath));
130
+ const res = await fetchWithWorkspaceScope(artifactUrl(artifactPath));
130
131
  const text = await res.text();
131
132
  if (!res.ok) {
132
133
  let message = text;
@@ -444,6 +445,7 @@ export function startRunPolling(workerRunId, initial = true) {
444
445
  const active = await renderRun(workerRunId, isInitial);
445
446
  if (generation !== uiState.pollingGeneration || !active) return;
446
447
  const delay = detailPollDelay("running", isPageVisible());
448
+ if (delay == null) return;
447
449
  uiState.runPollTimer = setTimeout(() => {
448
450
  uiState.runPollTimer = null;
449
451
  void poll(false);
@@ -34,6 +34,7 @@ import {
34
34
  repoBaseName,
35
35
  parseMarkdownBlocks,
36
36
  markdownLinkHref,
37
+ formatLedgerSummary,
37
38
  } from "../format.js";
38
39
  import { parseRoute, navigate, buildHashPath } from "../router.js";
39
40
  import {
@@ -85,7 +86,7 @@ export async function renderTaskDetail(featureId, taskId) {
85
86
  showView("task");
86
87
  setBreadcrumb([
87
88
  { label: UI_TEXT.dashboard, href: "#/" },
88
- { label: "资源池", href: "#/pool" },
89
+ { label: "资源池", href: "/#/inspect/pool" },
89
90
  { label: displayLabel },
90
91
  ]);
91
92
 
@@ -94,6 +95,7 @@ export async function renderTaskDetail(featureId, taskId) {
94
95
  const linksEl = document.getElementById("task-links");
95
96
  const evidenceEl = document.getElementById("task-evidence");
96
97
  const runsEl = document.getElementById("task-runs");
98
+ const ledgerEl = document.getElementById("task-ledger");
97
99
  if (!metaEl || !linksEl || !evidenceEl || !runsEl) return false;
98
100
  if (titleEl) titleEl.textContent = displayLabel;
99
101
  mountViewState(metaEl, "loading", "加载 Task…");
@@ -252,9 +254,62 @@ export async function renderTaskDetail(featureId, taskId) {
252
254
  const active = ["running", "pending", "blocked", "stale"].includes(
253
255
  (task.status ?? "").toLowerCase(),
254
256
  );
257
+ if (ledgerEl) {
258
+ void renderTaskLedger(ledgerEl, taskId);
259
+ }
255
260
  return active;
256
261
  }
257
262
 
263
+ /**
264
+ * Task 详情页 ledger 安全摘要(AC-009):从只读 GET /api/tasks/:id/ledger 拉取,
265
+ * 计数/状态/failure code + 有界排除理由;无 ledger 显示空态,失败安全降级。
266
+ */
267
+ async function renderTaskLedger(ledgerEl, taskId) {
268
+ clearNode(ledgerEl);
269
+ ledgerEl.appendChild(el("h3", "section-heading", "Source fidelity ledger"));
270
+ mountViewState(ledgerEl, "loading", "加载 ledger…");
271
+ let body = null;
272
+ try {
273
+ body = await fetchJson(`/api/tasks/${encodeURIComponent(taskId)}/ledger`);
274
+ } catch {
275
+ body = null;
276
+ }
277
+ if (!body) {
278
+ mountViewState(ledgerEl, "error", "ledger 只读接口失败(安全降级)。");
279
+ return;
280
+ }
281
+ if (body.present !== true) {
282
+ mountViewState(
283
+ ledgerEl,
284
+ body.error ? "error" : "empty",
285
+ body.error
286
+ ? `ledger 不可用(${body.error})。`
287
+ : "无 source fidelity ledger(任务无引用或 ledger 缺失,不伪造计数)。",
288
+ );
289
+ return;
290
+ }
291
+ const pre = el(
292
+ "pre",
293
+ "cmd",
294
+ formatLedgerSummary({
295
+ present: true,
296
+ fragmentCount: body.fragmentCount,
297
+ canonicalRequirementCount: body.canonicalRequirementCount,
298
+ contextOnlyCount: body.contextOnlyCount,
299
+ duplicateCount: body.duplicateCount,
300
+ unresolvedCount: body.unresolvedCount,
301
+ conflictCount: body.conflictCount,
302
+ gateStatus: body.gateStatus,
303
+ failureCode: body.failureCode,
304
+ exclusions: body.exclusions ?? [],
305
+ }),
306
+ );
307
+ pre.dataset.testid = "task-ledger-summary";
308
+ clearNode(ledgerEl);
309
+ ledgerEl.appendChild(el("h3", "section-heading", "Source fidelity ledger"));
310
+ ledgerEl.appendChild(pre);
311
+ }
312
+
258
313
  export async function loadOlderTaskRuns(featureId, taskId, before, runsEl) {
259
314
  if (runsEl === undefined) {
260
315
  runsEl = before;
@@ -5,8 +5,10 @@ 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 { resolveBackendTestManifestModules, backendManifestStatusFinding } from "./backend-test-markdown-workflow.js";
8
9
  import { applyOpenApiPartitionDomainPolicy, assessScenarioPartitionCoverage, parseScenarioPartitions, } from "./backend-test-scenario-partitions.js";
9
10
  import { backendTestCaseManifestSchema, computeCaseManifestCoverageSummary, } from "./backend-test-case-manifest.js";
11
+ import { inferScenarioParamIntent } from "./backend-test-scenario-param.js";
10
12
  const CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-\d{3})\b.*$/gm;
11
13
  const CASE_ID_IN_TEXT = /\bBE-[A-Z0-9_-]+-\d{3}\b/g;
12
14
  const NON_CANONICAL_CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-(?:\d{2}|\d{2,3}[A-Z]+))\b.*$/gm;
@@ -280,6 +282,80 @@ export const backendTestMarkdownPytestCorrespondenceFactsSchema = z.object({
280
282
  entries: z.array(correspondenceEntrySchema),
281
283
  findings: z.array(z.string()),
282
284
  }).strict();
285
+ function bindingCounts(entry) {
286
+ const counts = new Map();
287
+ for (const point of [...entry.variantTestPoints, ...entry.assertionTestPoints, ...entry.crossCuttingTestPoints]) {
288
+ counts.set(point, (counts.get(point) ?? 0) + 1);
289
+ }
290
+ return counts;
291
+ }
292
+ /**
293
+ * Classify correspondence defects by the only writer that can safely repair
294
+ * them. Markdown-owned contract defects must be fixed before pytest generation;
295
+ * pytest-owned generated-asset defects may enter the single bounded N11 repair.
296
+ */
297
+ export function classifyBackendTestCorrespondenceRepair(facts, layout) {
298
+ const resolved = layout ?? resolveBackendTestLayout(undefined);
299
+ const scriptRoot = resolved.scriptDir.replace(/\/$/, "");
300
+ const safePytestPath = (candidate) => {
301
+ const normalized = candidate.replaceAll("\\", "/").replace(/^\.\//, "");
302
+ return normalized.endsWith(".py") && (normalized === scriptRoot || normalized.startsWith(`${scriptRoot}/`));
303
+ };
304
+ const markdownBlockingEntries = [];
305
+ const pytestRepairEntries = [];
306
+ for (const entry of facts.entries) {
307
+ const markdownReasons = [];
308
+ const pytestReasons = [];
309
+ const counts = bindingCounts(entry);
310
+ if (entry.testPoints.some((point) => (counts.get(point) ?? 0) !== 1) ||
311
+ [...counts.keys()].some((point) => !entry.testPoints.includes(point))) {
312
+ markdownReasons.push("markdown-test-point-binding-invalid");
313
+ }
314
+ if (entry.declaredScript && entry.declaredScript !== entry.expectedScript) {
315
+ markdownReasons.push("markdown-script-declaration-invalid");
316
+ }
317
+ if (entry.cardinality === "1:0")
318
+ pytestReasons.push("missing-pytest-symbol");
319
+ if (entry.cardinality === "1:N")
320
+ pytestReasons.push("multiple-pytest-symbols");
321
+ if (entry.cardinality === "0:1")
322
+ pytestReasons.push("extra-pytest-symbol");
323
+ if (entry.actualScripts.some((script) => script !== entry.expectedScript))
324
+ pytestReasons.push("pytest-script-mismatch");
325
+ if (entry.declaredPrimarySymbol && entry.pytestSymbols.some((symbol) => symbol !== entry.declaredPrimarySymbol)) {
326
+ pytestReasons.push("pytest-primary-symbol-mismatch");
327
+ }
328
+ if (entry.variantTestPoints.some((point) => !entry.parameterIds.includes(point)))
329
+ pytestReasons.push("variant-parameter-missing");
330
+ if (entry.assertionTestPoints.some((point) => !entry.mappedTestPoints.includes(point)))
331
+ pytestReasons.push("assertion-binding-missing");
332
+ if (entry.crossCuttingTestPoints.some((point) => !entry.mappedTestPoints.includes(point)))
333
+ pytestReasons.push("cross-cutting-binding-missing");
334
+ if (entry.parameterIds.some((point) => !entry.testPoints.includes(point)))
335
+ pytestReasons.push("pytest-test-point-binding-extra");
336
+ if (entry.payloadAssessment.status === "UNSAFE")
337
+ pytestReasons.push("pytest-payload-shape-mismatch");
338
+ if (entry.status !== "EXACT_1_TO_1" &&
339
+ !["TEST_POINT_BINDING_DUPLICATE", "PAYLOAD_CONTRACT_UNAVAILABLE"].includes(entry.status)) {
340
+ pytestReasons.push(`correspondence-${entry.status.toLowerCase().replaceAll("_", "-")}`);
341
+ }
342
+ if (entry.status === "PAYLOAD_CONTRACT_UNAVAILABLE") {
343
+ markdownReasons.push("markdown-payload-contract-unavailable");
344
+ }
345
+ const paths = orderedUnique([entry.expectedScript, ...entry.actualScripts].filter(safePytestPath));
346
+ if (markdownReasons.length > 0) {
347
+ markdownBlockingEntries.push({ ...(entry.caseId ? { caseId: entry.caseId } : {}), reasonCodes: orderedUnique(markdownReasons), paths });
348
+ }
349
+ if (pytestReasons.length > 0 && paths.length > 0) {
350
+ pytestRepairEntries.push({ ...(entry.caseId ? { caseId: entry.caseId } : {}), reasonCodes: orderedUnique(pytestReasons), paths });
351
+ }
352
+ }
353
+ return {
354
+ markdownBlockingEntries,
355
+ pytestRepairEntries,
356
+ pytestRepairPaths: orderedUnique(pytestRepairEntries.flatMap((entry) => entry.paths)).sort(),
357
+ };
358
+ }
283
359
  function isRecord(value) {
284
360
  return typeof value === "object" && value !== null && !Array.isArray(value);
285
361
  }
@@ -592,6 +668,19 @@ function automationLabelBody(body, labels) {
592
668
  const match = new RegExp(`^\\s*[-*+]\\s*(?:${escaped})\\s*[::]\\s*(.*)$`, "mi").exec(automation);
593
669
  return match?.[1]?.replaceAll("`", "").trim() ?? "";
594
670
  }
671
+ export function inspectBackendTestMarkdownTestPointBindings(markdown) {
672
+ const headings = [...markdown.matchAll(CASE_HEADING)];
673
+ return headings.map((heading, index) => {
674
+ const body = markdown.slice(heading.index, headings[index + 1]?.index ?? markdown.length);
675
+ const testPoints = listTokens(body, CASE_SECTION_ALIASES.testPoints, TEST_POINT);
676
+ const bindings = testPointBindingFacts(body, testPoints);
677
+ return {
678
+ caseId: canonicalCaseId(heading[1]),
679
+ unclassifiedTestPoints: bindings.unclassifiedTestPoints,
680
+ duplicateBindingTestPoints: bindings.duplicateBindingTestPoints,
681
+ };
682
+ });
683
+ }
595
684
  function testPointBindingFacts(body, testPoints) {
596
685
  const bindings = TEST_POINT_BINDING_MODES.flatMap((mode) => {
597
686
  const value = automationLabelBody(body, AUTOMATION_BINDING_LABELS[mode]);
@@ -659,10 +748,18 @@ async function markdownModuleFiles(workspaceRoot, layout) {
659
748
  const resolved = layout ?? resolveBackendTestLayout(undefined);
660
749
  const dir = path.join(workspaceRoot, ...resolved.markdownDir.split("/"));
661
750
  const entries = await readdir(dir, { withFileTypes: true });
662
- return entries
751
+ const all = entries
663
752
  .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md") && entry.name.toLowerCase() !== "readme.md")
664
753
  .map((entry) => path.join(dir, entry.name))
665
754
  .sort();
755
+ // Current-round Markdown scope is the README Module Index manifest: unmanifested
756
+ // legacy .md files must not enter Case/coverage/partition statistics. Workspaces
757
+ // without a Module Index fall back to the whole directory (prior behaviour).
758
+ const manifest = await resolveBackendTestManifestModules(workspaceRoot, resolved);
759
+ if (manifest.manifestStatus !== "valid")
760
+ return [];
761
+ const declaredAbs = new Set(manifest.declaredMdFiles.map((rel) => path.join(workspaceRoot, ...rel.split("/")).toLowerCase()));
762
+ return all.filter((file) => declaredAbs.has(file.toLowerCase())).sort();
666
763
  }
667
764
  async function parseMarkdownCases(workspaceRoot, layout) {
668
765
  const result = [];
@@ -874,6 +971,16 @@ export async function analyzeBackendTestCaseCoverage(input) {
874
971
  findings.push(...coverageScope.findings);
875
972
  const matrix = parseCoverageMatrix(readme);
876
973
  findings.push(...matrix.findings);
974
+ const manifest = await resolveBackendTestManifestModules(input.workspaceRoot, layout);
975
+ // Fail closed on README Module Index entries declared but never materialized.
976
+ if (manifest.declaredMissing.length > 0) {
977
+ findings.push(`declared Markdown module is missing (Module Index): ${manifest.declaredMissing.join(", ")}`);
978
+ }
979
+ // Fail closed on every non-valid Module Index state: without a valid unique
980
+ // Module Index the current round must not re-scan historical `.md` files.
981
+ if (manifest.manifestStatus !== "valid") {
982
+ findings.push(backendManifestStatusFinding(manifest.manifestStatus, manifest.moduleIndexHeadingCount));
983
+ }
877
984
  const cases = await parseMarkdownCases(input.workspaceRoot, layout);
878
985
  for (const file of await markdownModuleFiles(input.workspaceRoot, layout)) {
879
986
  const markdown = await readFile(file, "utf8");
@@ -1109,6 +1216,15 @@ export async function analyzeBackendTestCaseCoverage(input) {
1109
1216
  "",
1110
1217
  facts.status,
1111
1218
  "",
1219
+ `- 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})`,
1220
+ "",
1221
+ "## Unmanifested Markdown Modules",
1222
+ "",
1223
+ "- Advisory: existing .md files not declared by the README Module Index are excluded from Case/coverage/partition statistics for this round.",
1224
+ ...(manifest.unmanifested.length > 0
1225
+ ? manifest.unmanifested.map((item) => `- ${item}`)
1226
+ : ["- None"]),
1227
+ "",
1112
1228
  "## Coverage Scope",
1113
1229
  "",
1114
1230
  `- Change Classification: ${facts.coverageScope.changeClassification}`,
@@ -1376,6 +1492,11 @@ def payload_expr_nodes(expr, assigned, parameters, seen=None):
1376
1492
  if isinstance(key,ast.Constant) and key.value == expr.slice.value:
1377
1493
  result.extend(shape_only(item) for item in payload_expr_nodes(value,assigned,parameters,set(seen)))
1378
1494
  return result
1495
+ if isinstance(expr,ast.IfExp):
1496
+ result=[]
1497
+ result.extend(payload_expr_nodes(expr.body,assigned,parameters,set(seen)))
1498
+ result.extend(payload_expr_nodes(expr.orelse,assigned,parameters,set(seen)))
1499
+ return result
1379
1500
  if isinstance(expr,ast.Call):
1380
1501
  result=[]
1381
1502
  if isinstance(expr.func,ast.Name) and expr.func.id in FUNCTION_PAYLOADS:
@@ -1416,6 +1537,30 @@ def decorator_payload_nodes(parameters,assigned):
1416
1537
  for value in values: result.extend(payload_expr_nodes(value,assigned,parameters))
1417
1538
  return result
1418
1539
 
1540
+ def decorator_payload_nodes_by_tp(fn,assigned):
1541
+ result={}
1542
+ for dec in fn.decorator_list:
1543
+ if not isinstance(dec,ast.Call) or not isinstance(dec.func,ast.Attribute) or dec.func.attr != 'parametrize' or len(dec.args) < 2: continue
1544
+ if not isinstance(dec.args[0],ast.Constant) or not isinstance(dec.args[0].value,str): continue
1545
+ names=[item.strip() for item in dec.args[0].value.split(',')]
1546
+ rows=dec.args[1]
1547
+ if isinstance(rows,ast.Name) and rows.id in GLOBAL_VALUES: rows=GLOBAL_VALUES[rows.id]
1548
+ for node in ast.walk(rows):
1549
+ if not isinstance(node,ast.Call) or not isinstance(node.func,ast.Attribute) or node.func.attr != 'param': continue
1550
+ tp=None
1551
+ for keyword in node.keywords:
1552
+ if keyword.arg == 'id' and isinstance(keyword.value,ast.Constant) and isinstance(keyword.value.value,str) and keyword.value.value.startswith('TP-'):
1553
+ tp=keyword.value.value
1554
+ if not tp: continue
1555
+ parameters={}
1556
+ for index,name in enumerate(names):
1557
+ if index < len(node.args): parameters.setdefault(name,[]).append(node.args[index])
1558
+ payload_nodes=decorator_payload_nodes(parameters,assigned)
1559
+ paths=set(); values={}
1560
+ for payload_node in payload_nodes: collect_dict(payload_node,'',paths,values)
1561
+ result[tp]={key:sorted(items) for key,items in sorted(values.items())}
1562
+ return result
1563
+
1419
1564
  def fallback_payload_nodes(fn):
1420
1565
  result=[]
1421
1566
  assigned={}
@@ -1445,7 +1590,7 @@ def fallback_payload_nodes(fn):
1445
1590
 
1446
1591
  def is_direct_transport_call(node):
1447
1592
  if not isinstance(node,ast.Call) or not isinstance(node.func,ast.Attribute): return False
1448
- if node.func.attr.lower() not in {'get','post','put','patch','delete','request'}: return False
1593
+ if node.func.attr.lower() not in {'get','post','put','patch','delete','request','call'}: return False
1449
1594
  receiver_tokens=[]
1450
1595
  current=node.func.value
1451
1596
  while isinstance(current,ast.Attribute):
@@ -1493,6 +1638,7 @@ for fn in ast.walk(tree):
1493
1638
  'values':{key:sorted(items) for key,items in sorted(values.items())},
1494
1639
  'fallbackPaths':sorted(fallback_paths),
1495
1640
  'fallbackValues':{key:sorted(items) for key,items in sorted(fallback_values.items())},
1641
+ 'valuesByTestPoint':decorator_payload_nodes_by_tp(fn,{}),
1496
1642
  }
1497
1643
  print(json.dumps(out,ensure_ascii=True))
1498
1644
  `;
@@ -1623,6 +1769,7 @@ function pytestSymbols(script, source) {
1623
1769
  payloadValues: payloadShape.values,
1624
1770
  fallbackPayloadPaths: payloadShape.fallbackPaths ?? [],
1625
1771
  fallbackPayloadValues: payloadShape.fallbackValues ?? {},
1772
+ payloadValuesByTestPoint: payloadShape.valuesByTestPoint ?? {},
1626
1773
  };
1627
1774
  });
1628
1775
  }
@@ -1636,6 +1783,33 @@ function executionSignature(testCase) {
1636
1783
  .join("\n");
1637
1784
  return createHash("sha256").update(normalized).digest("hex");
1638
1785
  }
1786
+ function normalizePythonLiteralValue(value) {
1787
+ const trimmed = value.trim();
1788
+ if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) {
1789
+ return trimmed.slice(1, -1);
1790
+ }
1791
+ return trimmed;
1792
+ }
1793
+ function intentionalInvalidEnumVariant(input) {
1794
+ if (!input.testCase.testPointBindings.some((binding) => binding.mode === "variant" && binding.testPoint === input.testPoint))
1795
+ return false;
1796
+ const inferred = inferScenarioParamIntent({ tpId: input.testPoint, caseBody: input.testCase.body });
1797
+ if (inferred.intentSource !== "machine-line" || inferred.field?.toLowerCase() !== input.field.toLowerCase())
1798
+ return false;
1799
+ const observed = normalizePythonLiteralValue(input.observedValue);
1800
+ if (input.allowed.includes(observed))
1801
+ return false;
1802
+ if (inferred.intent === "enum-invalid" || inferred.intent === "wrong-type")
1803
+ return true;
1804
+ if (inferred.intent === "null")
1805
+ return observed === "None" || observed === "null";
1806
+ if (inferred.intent === "empty")
1807
+ return observed === "";
1808
+ if (!inferred.intent.startsWith("custom-literal:"))
1809
+ return false;
1810
+ const expected = normalizePythonLiteralValue(inferred.example ?? inferred.intent.slice("custom-literal:".length));
1811
+ return observed === expected;
1812
+ }
1639
1813
  function assessPayloadContract(testCase, refs) {
1640
1814
  const requiredPaths = testCase.payloadContract.requiredPaths;
1641
1815
  const directObservedPaths = orderedUnique(refs.flatMap((item) => item.payloadPaths));
@@ -1657,13 +1831,17 @@ function assessPayloadContract(testCase, refs) {
1657
1831
  ? observedPaths.filter((item) => !allowedPaths.includes(item) && !allowedPaths.some((allowed) => item.startsWith(`${allowed}.`)))
1658
1832
  : [];
1659
1833
  const enumMismatches = [];
1660
- const delegatesInvalidEnumToScenarioGate = testCase.testPointBindings
1661
- .filter((binding) => binding.mode === "variant")
1662
- .some((binding) => /(?:ENUM|CATEGORY).*(?:INVALID|UNKNOWN|CASE|WHITESPACE|EMPTY|WRONG-TYPE)|(?:INVALID|UNKNOWN|CASE|WHITESPACE|EMPTY|WRONG-TYPE).*(?:ENUM|CATEGORY)/.test(binding.testPoint));
1663
1834
  for (const [field, allowed] of Object.entries(testCase.payloadContract.enumValues)) {
1664
- const observed = orderedUnique(refs.flatMap((item) => (useFallback ? item.fallbackPayloadValues[field] : item.payloadValues[field]) ?? []).map((value) => value.replace(/^['"]|['"]$/g, "")));
1665
- for (const value of observed) {
1666
- if (!allowed.includes(value) && !delegatesInvalidEnumToScenarioGate)
1835
+ const observed = orderedUnique(refs.flatMap((item) => (useFallback ? item.fallbackPayloadValues[field] : item.payloadValues[field]) ?? []));
1836
+ for (const rawValue of observed) {
1837
+ const value = normalizePythonLiteralValue(rawValue);
1838
+ if (allowed.includes(value))
1839
+ continue;
1840
+ const associatedTestPoints = orderedUnique(refs.flatMap((item) => Object.entries(item.payloadValuesByTestPoint)
1841
+ .filter(([, values]) => (values[field] ?? []).includes(rawValue))
1842
+ .map(([testPoint]) => testPoint)));
1843
+ const intentionallyInvalid = associatedTestPoints.length > 0 && associatedTestPoints.every((testPoint) => intentionalInvalidEnumVariant({ testCase, testPoint, field, observedValue: rawValue, allowed }));
1844
+ if (!intentionallyInvalid)
1667
1845
  enumMismatches.push(`${field}=${value}; allowed=${allowed.join("|")}`);
1668
1846
  }
1669
1847
  }