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

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 (208) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +106 -109
  3. package/README.md +2 -2
  4. package/dist/application/task-lifecycle/advance.js +12 -0
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/cli/update/runtime-activity.js +1 -29
  7. package/dist/commands/dag-rerun.js +1 -1
  8. package/dist/executors/dag-pi-executor.js +69 -7
  9. package/dist/executors/pi-executor.js +4 -2
  10. package/dist/executors/pi-sdk-executor.js +46 -1
  11. package/dist/executors/pi-writer-tool-policy.js +57 -1
  12. package/dist/executors/shell-executor.js +43 -3
  13. package/dist/executors/workspace-write-snapshot.js +68 -0
  14. package/dist/infrastructure/harness/atomic-write.js +23 -0
  15. package/dist/shared/dag-prompt-override.js +4 -5
  16. package/dist/shared/update/npm-client.js +33 -11
  17. package/dist/task/config-types.js +10 -0
  18. package/dist/task/contract/apply.js +11 -1
  19. package/dist/task/contract/constants.js +2 -0
  20. package/dist/task/contract/hash.js +3 -0
  21. package/dist/task/contract/observe.js +25 -4
  22. package/dist/task/contract/paths.js +2 -1
  23. package/dist/task/contract/project.js +15 -1
  24. package/dist/task/contract/schema.js +2 -0
  25. package/dist/task/contract/transaction.js +12 -0
  26. package/dist/task/source-prepare/completeness.js +188 -0
  27. package/dist/task/source-prepare/fragment-inventory.js +474 -0
  28. package/dist/task/source-prepare/index.js +3 -0
  29. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  30. package/dist/task/source-prepare/ledger-review.js +214 -0
  31. package/dist/task/source-prepare/ledger.js +545 -0
  32. package/dist/task/source-prepare/prepare.js +262 -1
  33. package/dist/task/source-prepare/semantic-intake.js +19 -3
  34. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  35. package/dist/task/source-prepare/types.js +22 -0
  36. package/dist/worker/cli.js +12 -26
  37. package/dist/worker/console/chat/chat-event-store.js +75 -5
  38. package/dist/worker/console/chat/context-panel.js +7 -0
  39. package/dist/worker/console/chat/deferred-turn.js +12 -0
  40. package/dist/worker/console/chat/pi-runtime.js +331 -18
  41. package/dist/worker/console/chat/provider-error.js +98 -0
  42. package/dist/worker/console/chat/routes.js +389 -56
  43. package/dist/worker/console/chat/session-stats.js +179 -0
  44. package/dist/worker/console/chat/session-store.js +93 -63
  45. package/dist/worker/console/chat/todos.js +115 -0
  46. package/dist/worker/console/chat/tool-preview.js +42 -0
  47. package/dist/worker/console/chat/turn-process.js +20 -55
  48. package/dist/worker/console/chat/user-questions.js +266 -0
  49. package/dist/worker/console/chat/workspace-landing.js +79 -0
  50. package/dist/worker/console/console-handoff.js +82 -0
  51. package/dist/worker/console/console-update-and-init.js +115 -0
  52. package/dist/worker/console/console-update-runtime.js +84 -0
  53. package/dist/worker/console/draft-store.js +49 -0
  54. package/dist/worker/console/native-directory-picker.js +13 -0
  55. package/dist/worker/console/operation-store.js +1 -1
  56. package/dist/worker/console/operator-actions.js +129 -4
  57. package/dist/worker/console/operator-surface-health.js +1 -0
  58. package/dist/worker/console/prd-intake-bridge.js +13 -0
  59. package/dist/worker/console/routes.js +312 -1
  60. package/dist/worker/console/security.js +4 -4
  61. package/dist/worker/console/server.js +583 -172
  62. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Br92EGb7.js → abnfDiagram-N423BO3Z-Bs-CDAXM.js} +1 -1
  63. package/dist/worker/console/static/assets/{arc-oulImtQq.js → arc-CAkA3We3.js} +1 -1
  64. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-tyHc6aqO.js → architectureDiagram-T3A2C74G-DEmI_zqr.js} +1 -1
  65. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CY86BSpT.js → blockDiagram-VBNYF7ZC-uDHANebE.js} +1 -1
  66. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BKKsDmNs.js → c4Diagram-5PPSVZJV-Bv97b4_B.js} +1 -1
  67. package/dist/worker/console/static/assets/channel-amxUpk7o.js +1 -0
  68. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CXZdjl3B.js → chunk-2GRJ4B5K-shmQKsFz.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B-aIFDQ5.js → chunk-2Q5K7J3B-BAraNvcN.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BDgAW_B2.js → chunk-5RXB4S5H-BirhxUop.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DABa4UPR.js → chunk-5VM5RSS4-C9LgUJpR.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-aGbysVOx.js → chunk-6Q2QTUOP-DrU-s-t7.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B8MSZDVs.js → chunk-GF5L2VYU-Tx-H_FA2.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CpelV73g.js → chunk-JWPE2WC7-BjLtNFrR.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BE04ty3z.js → chunk-KBJHAD2P-UNLVwSsy.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BUxTkaaR.js → chunk-RYQCIY6F-CBEUX8XD.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CYYBVKfX.js → chunk-XXDRQBXY-WTL7IZOq.js} +1 -1
  78. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +1 -0
  79. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +1 -0
  80. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CEQF_xmQ.js → cose-bilkent-JH36ORCC-CkHG6W3s.js} +1 -1
  81. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CqpxN5IT.js → cynefin-VYW2F7L2-BRzFqTaL.js} +1 -1
  82. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-B3PzanVi.js → cynefinDiagram-MW4NZA55-2_CFp_i7.js} +1 -1
  83. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BSUGCNx1.js → dagre-VZM6K2ZE-BJtsmdkE.js} +1 -1
  84. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdLevU5T.js → diagram-7IWD3JNH-DNEFFo6w.js} +1 -1
  85. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BjpF_Kok.js → diagram-B4RE2ZJO-B_IWV7eo.js} +1 -1
  86. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C4I6NJny.js → diagram-LBJQPF4R-DvvRx2dz.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C8hecCr5.js → diagram-Q27KOJAE-CsibsWLg.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Daod6tOS.js → diagram-UB23O5K3-les_djBQ.js} +1 -1
  89. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BvvmazGv.js → ebnfDiagram-BXEA7PRR-BEm2n7qJ.js} +1 -1
  90. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DeN_vfV4.js → erDiagram-JOGREHBK-BI7vHHVm.js} +1 -1
  91. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D6dq1t9z.js → flowDiagram-UKHOOZJN-CUFCERrB.js} +1 -1
  92. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DziIdNY4.js → ganttDiagram-PKOTCBZU-wxyLiGWt.js} +1 -1
  93. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js → gitGraphDiagram-DS77QQ5N-Bt897K8Z.js} +1 -1
  94. package/dist/worker/console/static/assets/index-BHhUOTri.css +1 -0
  95. package/dist/worker/console/static/assets/index-DqJbO3-p.js +407 -0
  96. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czv5eYx2.js → infoDiagram-6WML65LV-DYNx_IPv.js} +1 -1
  97. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js → ishikawaDiagram-WSZJBQD7-CzpLNEu6.js} +1 -1
  98. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DxGmNj5K.js → journeyDiagram-NVQOT4AX-97f9owNU.js} +1 -1
  99. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CYCFqkO_.js → kanban-definition-27J2QSJJ-BxM9hyBn.js} +1 -1
  100. package/dist/worker/console/static/assets/{linear-B0YzRwrd.js → linear-ByuxcHvp.js} +1 -1
  101. package/dist/worker/console/static/assets/{mermaid.core-t3EZtIGh.js → mermaid.core-DwPGvpCv.js} +5 -5
  102. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BTXpTv0T.js → mindmap-definition-FAOFIHXS-DCoqG9RT.js} +1 -1
  103. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DU8UkKqA.js → pegDiagram-VL7TDLO6-BLSBmBMx.js} +1 -1
  104. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DP4y8ohp.js → pieDiagram-7S7Q4E2Y-B0t_0oJW.js} +1 -1
  105. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B07vNaYR.js → quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js} +1 -1
  106. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-xDWktb2i.js → railroadDiagram-AXF67PYL-B3jRdATC.js} +1 -1
  107. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BLNMKqeS.js → requirementDiagram-LRYGKXZP-D-cTtXEN.js} +1 -1
  108. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-n0adalxH.js → sankeyDiagram-W5VNT64P-B25igJRE.js} +1 -1
  109. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BbnS-2DL.js → sequenceDiagram-SI44F4Z6-jW_lM1_D.js} +1 -1
  110. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4uPpCLu.js → sizeCapture-X5ZJPWSS-CrzTgsHE.js} +1 -1
  111. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA--MMbB0fX.js → stateDiagram-OKZ733FA-B01MvlqE.js} +1 -1
  112. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +1 -0
  113. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BXUgp2QY.js → swimlanes-SLNWSIFB-YcF12FiU.js} +2 -2
  114. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +8 -0
  115. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CsaMxki7.js → timeline-definition-Z64GVDOM-ClnRcg2s.js} +1 -1
  116. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C2IuG9iH.js → vennDiagram-T6HMQDX7-B6gr_z-q.js} +1 -1
  117. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CmEWOXCW.js → wardleyDiagram-T6FBY63Y-CvSgguGH.js} +1 -1
  118. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D_9LE0MG.js → xychartDiagram-ELKLHX3M-23OtSZCb.js} +1 -1
  119. package/dist/worker/console/static/index.html +2 -2
  120. package/dist/worker/console/static-src/app/console-types.js +68 -4
  121. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  122. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  123. package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
  124. package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
  125. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  126. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  127. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
  128. package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
  129. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  130. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  131. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  132. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  133. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
  134. package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
  135. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
  136. package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
  137. package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
  138. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  139. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  140. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  141. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  142. package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
  143. package/dist/worker/console/workspace-context.js +234 -0
  144. package/dist/worker/console/workspace-registry.js +214 -0
  145. package/dist/worker/observability/init-runtime-activity.js +26 -0
  146. package/dist/worker/observe/node-input.js +88 -2
  147. package/dist/worker/observe/routes.js +188 -0
  148. package/dist/worker/observe/server.js +4 -0
  149. package/dist/worker/observe/static/api.js +29 -9
  150. package/dist/worker/observe/static/app.js +18 -70
  151. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  152. package/dist/worker/observe/static/dag-node-purpose.js +203 -0
  153. package/dist/worker/observe/static/format.js +37 -0
  154. package/dist/worker/observe/static/index.html +6 -1
  155. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  156. package/dist/worker/observe/static/operator-chrome.css +145 -20
  157. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  158. package/dist/worker/observe/static/operator-chrome.js +330 -100
  159. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  160. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  161. package/dist/worker/observe/static/router.d.ts +46 -0
  162. package/dist/worker/observe/static/router.js +25 -1
  163. package/dist/worker/observe/static/state.d.ts +50 -0
  164. package/dist/worker/observe/static/state.js +174 -2
  165. package/dist/worker/observe/static/styles.css +357 -35
  166. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  167. package/dist/worker/observe/static/views/dag-inspector.js +533 -79
  168. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  169. package/dist/worker/observe/static/views/dag.js +6 -4
  170. package/dist/worker/observe/static/views/dashboard.js +6 -4
  171. package/dist/worker/observe/static/views/night.js +2 -2
  172. package/dist/worker/observe/static/views/pool.js +1 -0
  173. package/dist/worker/observe/static/views/run.js +3 -1
  174. package/dist/worker/observe/static/views/task.js +56 -1
  175. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +29 -1
  176. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  177. package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
  178. package/dist/workflows/dag/backend-test-writer-completeness.js +17 -17
  179. package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
  180. package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
  181. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  182. package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
  183. package/dist/workflows/dag/frontend-review-context.js +92 -10
  184. package/dist/workflows/dag/init-hybrid.js +238 -51
  185. package/dist/workflows/dag/interrupt-request.js +3 -3
  186. package/dist/workflows/dag/lifecycle.js +3 -3
  187. package/dist/workflows/dag/node-execution.js +45 -0
  188. package/dist/workflows/dag/types.js +62 -12
  189. package/docs/README.md +2 -0
  190. package/docs/architecture/evolution.md +45 -0
  191. package/docs/init-surface.manifest.json +1 -0
  192. package/docs/operations/local-development-environment.md +21 -0
  193. package/docs/templates/README.md +1 -1
  194. package/docs/templates/agent-dag.schema.json +18 -0
  195. package/docs/templates/backend-test-dag.json +265 -237
  196. package/docs/templates/frontend-implementation-contract.schema.json +681 -24
  197. package/harness.json +3 -3
  198. package/package.json +3 -1
  199. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  200. package/dist/worker/console/static/assets/channel-TY36zt-i.js +0 -1
  201. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +0 -1
  202. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +0 -1
  203. package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
  204. package/dist/worker/console/static/assets/index-Ci8ksvBm.js +0 -406
  205. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +0 -1
  206. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +0 -8
  207. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  208. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
@@ -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]");
@@ -237,13 +258,14 @@ function bounded(value) {
237
258
  ? normalized
238
259
  : `${normalized.slice(0, MAX_DIAGNOSTIC_CHARS)}\n...[truncated]`;
239
260
  }
240
- function collectionItems(stdout) {
261
+ function collectionItems(stdout, layout) {
262
+ const rootPrefix = `${(layout ?? resolveBackendTestLayout(undefined)).testRoot}/`;
241
263
  return [...new Set(stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => {
242
264
  if (!line.includes("::"))
243
265
  return false;
244
266
  if (/^(ERROR|FAILED|E\s+)/.test(line))
245
267
  return false;
246
- return /^testcase\//.test(line.replaceAll("\\", "/"));
268
+ return line.replaceAll("\\", "/").startsWith(rootPrefix);
247
269
  }).map((line) => line.replaceAll("\\", "/")))];
248
270
  }
249
271
  function escapeRegExp(value) {
@@ -308,8 +330,68 @@ function classifyFixtureResolutionFailure(output, layout) {
308
330
  repairPaths: paths,
309
331
  };
310
332
  }
311
- function classifyCollectionFailure(output) {
333
+ /**
334
+ * Generated-local bare helper module signatures found in the bound pytest
335
+ * asset inventory. A file such as `testcase/__551_manager_helpers.py` in the
336
+ * test root/script dir is generated-local: when collection reports
337
+ * `No module named '__551_manager_helpers'`, the defect can be precisely
338
+ * attributed to the generated mapped script instead of a missing third-party
339
+ * package.
340
+ */
341
+ function generatedLocalBareHelperNames(inventory) {
342
+ if (!inventory)
343
+ return [];
344
+ const names = [];
345
+ for (const file of inventory.assetFiles) {
346
+ const stem = path.posix.basename(file).replace(/\.py$/, "");
347
+ if (/^__\d+[A-Za-z0-9_]*$/.test(stem))
348
+ names.push(stem);
349
+ }
350
+ return [...new Set(names)];
351
+ }
352
+ function classifyCollectionFailure(output, evidence) {
312
353
  const normalized = output.replaceAll("\\", "/");
354
+ const helperNames = generatedLocalBareHelperNames(evidence?.inventory);
355
+ const helperByLower = new Map(helperNames.map((name) => [name.toLowerCase(), name]));
356
+ // A bare `__NNN_...` module import that collection cannot find is only a
357
+ // candidate generated-local helper signature. REPAIRABLE requires two
358
+ // deterministic pieces of evidence: (1) a helper asset of the same name in
359
+ // the bound inventory, and (2) at least one mapped generated script whose
360
+ // source truly imports that bare module. Without both — or when attribution
361
+ // is impossible — fail closed as BLOCKED so an un-attributable dependency
362
+ // error is never widened into a repair. Genuine third-party modules (no
363
+ // `__NNN_` signature) stay BLOCKED below.
364
+ const missingModule = /No module named ['\"]([^'\"]+)['\"]/i.exec(normalized)?.[1];
365
+ if (missingModule && /^__\d+[A-Za-z0-9_]*$/.test(missingModule)) {
366
+ const helperAsset = helperByLower.get(missingModule.toLowerCase());
367
+ const inventory = evidence?.inventory;
368
+ const scriptBareImports = inventory?.scriptBareImports ?? {};
369
+ const missingLower = missingModule.toLowerCase();
370
+ const importedByMappedScript = inventory
371
+ ? inventory.mappedScripts.some((script) => (scriptBareImports[script] ?? []).some((name) => name.toLowerCase() === missingLower))
372
+ : false;
373
+ if (helperAsset && importedByMappedScript && inventory) {
374
+ const repairPaths = [...new Set([
375
+ ...(helperAsset
376
+ ? inventory.assetFiles.filter((file) => path.posix.basename(file).replace(/\.py$/, "") === helperAsset)
377
+ : []),
378
+ ...inventory.mappedScripts,
379
+ ])].sort();
380
+ return {
381
+ status: "REPAIRABLE",
382
+ kind: "missing-generated-local-helper",
383
+ detail: `generated testcase asset imports missing bare generated-local helper module ${missingModule}; inline the helper or remove the generated shared dependency`,
384
+ repairPaths,
385
+ };
386
+ }
387
+ return {
388
+ status: "BLOCKED",
389
+ kind: "missing-generated-local-helper-without-evidence",
390
+ detail: helperAsset
391
+ ? `missing bare generated-local helper module ${missingModule} has a name match but no mapped generated script imports it (no precise generated-local attribution)`
392
+ : `missing bare __NNN_* module ${missingModule} has no generated-local helper asset in the bound inventory (cannot be precisely attributed)`,
393
+ };
394
+ }
313
395
  const blockedPatterns = [
314
396
  [/No module named ['\"](?!testcase(?:\.|['\"]))/i, "missing-third-party-module"],
315
397
  [/pytest(?:_|-)plugin|pluggy|unknown hook|unrecognized arguments/i, "pytest-plugin-or-config"],
@@ -335,7 +417,7 @@ function classifyCollectionFailure(output) {
335
417
  return { status: "BLOCKED", kind: "unclassified-collection-error", detail: "collection failure cannot be safely attributed to generated testcase assets" };
336
418
  }
337
419
  export function assessBackendPytestCollection(input) {
338
- const items = collectionItems(input.stdout);
420
+ const items = collectionItems(input.stdout, input.layout);
339
421
  const fixtureResolution = input.fixtureResolution ?? { exitCode: 0, stdout: "fixture resolution assumed by direct assessor", stderr: "" };
340
422
  if (input.exitCode === 0 && fixtureResolution.exitCode === 0) {
341
423
  return backendPytestCollectionFactsSchema.parse({
@@ -401,7 +483,7 @@ export function assessBackendPytestCollection(input) {
401
483
  fixtureStderrExcerpt: bounded(fixtureResolution.stderr),
402
484
  });
403
485
  }
404
- const classification = classifyCollectionFailure(`${input.stdout}\n${input.stderr}`);
486
+ const classification = classifyCollectionFailure(`${input.stdout}\n${input.stderr}`, { layout: input.layout, inventory: input.inventory });
405
487
  return backendPytestCollectionFactsSchema.parse({
406
488
  schemaId: "backend-test-pytest-collection-v3",
407
489
  phase: input.phase,
@@ -412,7 +494,9 @@ export function assessBackendPytestCollection(input) {
412
494
  fixtureResolutionAttempted: false,
413
495
  fixtureResolutionStatus: "NOT_RUN",
414
496
  fixtureResolutionExitCode: null,
415
- repairPaths: testcasePythonPaths(`${input.stdout}\n${input.stderr}`),
497
+ repairPaths: classification.repairPaths && classification.repairPaths.length > 0
498
+ ? classification.repairPaths
499
+ : testcasePythonPaths(`${input.stdout}\n${input.stderr}`),
416
500
  mappedScripts: input.inventory.mappedScripts,
417
501
  existingMappedScripts: input.inventory.mappedScripts,
418
502
  missingMappedScripts: [],
@@ -657,7 +741,7 @@ export async function materializeBackendTestExecutionReadiness(input) {
657
741
  if (input.effective.phase !== "effective" || input.effective.status !== "PASS" || input.effective.fixtureResolutionStatus !== "PASS") {
658
742
  throw new Error("backend-test execution readiness requires effective collection and fixture-resolution PASS");
659
743
  }
660
- const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.effective.mappedScripts);
744
+ const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.effective.mappedScripts, { layout: input.layout });
661
745
  assertSameInventory(input.effective, current);
662
746
  const eligibility = input.eligibility ?? { eligibleItemIds: input.effective.collectedItemIds, excludedItems: [] };
663
747
  const status = eligibility.eligibleItemIds.length === 0
@@ -756,7 +840,7 @@ async function firstUnreadableAssetPath(workspaceRoot, readiness) {
756
840
  }
757
841
  return undefined;
758
842
  }
759
- export async function assertBackendTestExecutionReadinessFresh(workspaceRoot, readiness) {
843
+ export async function assertBackendTestExecutionReadinessFresh(workspaceRoot, readiness, layout) {
760
844
  if (!["PASS", "PARTIAL"].includes(readiness.status)) {
761
845
  const counts = formatReadinessCounts(readiness);
762
846
  const reasonTally = readiness.eligibleItemIds.length === 0 && readiness.excludedItems.length > 0
@@ -779,7 +863,7 @@ export async function assertBackendTestExecutionReadinessFresh(workspaceRoot, re
779
863
  }
780
864
  let current;
781
865
  try {
782
- current = await buildBackendPytestAssetInventory(workspaceRoot, readiness.mappedScripts);
866
+ current = await buildBackendPytestAssetInventory(workspaceRoot, readiness.mappedScripts, { layout });
783
867
  }
784
868
  catch {
785
869
  const fallbackPath = [...readiness.mappedScripts].sort()[0] ?? Object.keys(readiness.assetHashes).sort()[0] ?? "unknown";
@@ -814,11 +898,11 @@ export async function assertBackendTestExecutionReadinessFresh(workspaceRoot, re
814
898
  }
815
899
  }
816
900
  }
817
- export async function assertBackendPytestCollectionFresh(workspaceRoot, effective) {
901
+ export async function assertBackendPytestCollectionFresh(workspaceRoot, effective, layout) {
818
902
  if (effective.phase !== "effective" || effective.status !== "PASS") {
819
903
  throw new Error("backend pytest execution requires effective collection PASS facts");
820
904
  }
821
- const current = await buildBackendPytestAssetInventory(workspaceRoot, effective.mappedScripts);
905
+ const current = await buildBackendPytestAssetInventory(workspaceRoot, effective.mappedScripts, { layout });
822
906
  const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
823
907
  if (priorityOnly.length > 0)
824
908
  throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
@@ -826,7 +910,7 @@ export async function assertBackendPytestCollectionFresh(workspaceRoot, effectiv
826
910
  }
827
911
  export async function materializeEffectiveBackendPytestCollection(input) {
828
912
  if (input.initial.status === "PASS") {
829
- const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.initial.mappedScripts);
913
+ const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.initial.mappedScripts, { layout: input.layout });
830
914
  const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
831
915
  if (priorityOnly.length > 0)
832
916
  throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
@@ -844,7 +928,7 @@ export async function materializeEffectiveBackendPytestCollection(input) {
844
928
  if (input.final.status !== "PASS")
845
929
  throw new Error("backend pytest collection still fails after bounded repair");
846
930
  await assertBackendPytestRepairSafety(input.workspaceRoot, input.initial, input.final);
847
- const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.final.mappedScripts);
931
+ const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.final.mappedScripts, { layout: input.layout });
848
932
  const priorityOnly = priorityOnlyBackendPytestScripts(current.assetFiles);
849
933
  if (priorityOnly.length > 0)
850
934
  throw new Error(`priority-only-pytest-module: ${priorityOnly.join(", ")}`);
@@ -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 = [];
@@ -879,28 +884,23 @@ export function isBackendTestShardChildTask(task) {
879
884
  task.id?.startsWith("generate-backend-pytest-case-"));
880
885
  }
881
886
  /**
882
- * Is this task the README-only plan node of the map-sharded md topology?
883
- * The plan node writes testcase/md/README.md alone; module case cards are
884
- * produced by the downstream map_agent children, so the Completeness Gate
885
- * must NOT require module files to exist at plan time.
887
+ * Historical classifier for the Markdown plan node. Current backend-test N2
888
+ * is read-only and persists a run-owned plan.md, so writer completeness never
889
+ * invokes the workspace README assessment below for newly generated DAGs.
886
890
  */
887
891
  export function isBackendTestMdPlanTask(task) {
888
892
  return task.id === "generate-backend-md-plan-pi";
889
893
  }
890
894
  /**
891
- * Is this task the shared-resources plan node of the map-sharded pytest
892
- * topology? The plan node writes helpers/factories only; per-module pytest
893
- * scripts are produced by the downstream map_agent children, so the Gate
894
- * must NOT require mapped pytest scripts to exist at plan time.
895
+ * Historical classifier for the former shared-resources pytest writer. Current
896
+ * N7 is static and every map child writes one self-contained pytest module.
895
897
  */
896
898
  export function isBackendTestPytestPlanTask(task) {
897
899
  return task.id === "generate-backend-pytest-plan-pi";
898
900
  }
899
901
  /**
900
- * Plan-node completeness for the md topology: verify the README itself is
901
- * structurally complete (Coverage Scope, Coverage Matrix, a non-empty module
902
- * index), WITHOUT requiring the indexed module files to exist yet — those are
903
- * written by the downstream map_agent shard children.
902
+ * Legacy workspace-plan completeness helper retained for isolated historical
903
+ * fixtures. New DAGs validate the run-owned plan artifact in N3 instead.
904
904
  */
905
905
  export async function assessBackendTestMdPlanCompleteness(workspaceRoot, layout) {
906
906
  const resolvedPlan = layout ?? resolveBackendTestLayout(undefined);
@@ -26,6 +26,7 @@ export function buildExpandedChildTask(input) {
26
26
  allowedPaths: renderDynamicPatternList(child.allowedPaths, item, index, expansion.itemName) ?? [],
27
27
  forbiddenPaths: renderDynamicPatternList(child.forbiddenPaths, item, index, expansion.itemName) ?? [],
28
28
  writeSet: renderDynamicPatternList(child.writeSet, item, index, expansion.itemName),
29
+ readSet: renderDynamicPatternList(child.readSet, item, index, expansion.itemName),
29
30
  outputContract: child.outputContract,
30
31
  writerOutcomePolicy: child.writerOutcomePolicy,
31
32
  writeGuardPolicy: child.writeGuardPolicy,
@@ -8,6 +8,7 @@ import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-sto
8
8
  import { findPackageRoot } from "../../shared/package-metadata.js";
9
9
  import { pathMatchesPattern } from "../../shared/git-progress.js";
10
10
  import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
11
+ import { isDagSourceBindingV2 } from "./types.js";
11
12
  import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, openspecCitationsRel, parseOpenspecCitationsBlock, persistRuntimeSkeleton, } from "./structured-output-repair.js";
12
13
  export const frontendNormalizationActionSchema = z.enum([
13
14
  "remove-trailing-commas",
@@ -279,8 +280,33 @@ export const frontendImplementationContractSchema = z
279
280
  requirementSha256: z.string().regex(/^[a-f0-9]{64}$/),
280
281
  referencePaths: z.array(safePath),
281
282
  requirementIds: z.array(id),
283
+ // Source binding v2 (frontend): bound ledger pointer + fragment map.
284
+ ledgerPath: safePath.optional(),
285
+ ledgerSha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
286
+ inputDigest: z.string().regex(/^[a-f0-9]{64}$/).optional(),
287
+ requirementToFragments: z
288
+ .record(z.string(), z.array(z.string()))
289
+ .optional(),
282
290
  })
283
- .strict(),
291
+ .strict()
292
+ .superRefine((value, ctx) => {
293
+ // v2 all-or-none:任一 ledger 字段出现则四项必须齐全,防止 v1+optional 冒充 v2。
294
+ const anyLedgerField = value.ledgerPath !== undefined ||
295
+ value.ledgerSha256 !== undefined ||
296
+ value.inputDigest !== undefined ||
297
+ value.requirementToFragments !== undefined;
298
+ const allLedgerFields = value.ledgerPath !== undefined &&
299
+ value.ledgerSha256 !== undefined &&
300
+ value.inputDigest !== undefined &&
301
+ value.requirementToFragments !== undefined;
302
+ if (anyLedgerField && !allLedgerFields) {
303
+ ctx.addIssue({
304
+ code: z.ZodIssueCode.custom,
305
+ path: ["sourceBinding"],
306
+ message: "source binding v2 ledger fields (ledgerPath/ledgerSha256/inputDigest/requirementToFragments) must be provided together; partial or optional v1+ledger bindings are rejected",
307
+ });
308
+ }
309
+ }),
284
310
  riskLevel: z.enum(["small", "standard", "high-risk"]),
285
311
  targets: z
286
312
  .object({
@@ -296,6 +322,10 @@ export const frontendImplementationContractSchema = z
296
322
  implementationTargets: z.array(safePath),
297
323
  verificationTargetIds: z.array(z.string().min(1)),
298
324
  evidenceGap: gap.optional(),
325
+ // Source fidelity ledger binding (AC-005/AC-006): 每个 requirement
326
+ // 携带来源 fragment 证据,prewrite 门禁校验其真实性与覆盖。
327
+ sourceFragmentIds: z.array(z.string().min(1)).optional(),
328
+ sourceRefs: z.array(safePath).optional(),
299
329
  })
300
330
  .strict()).min(1),
301
331
  uiStates: z.array(z
@@ -432,6 +462,30 @@ export const frontendImplementationContractSchema = z
432
462
  message: `unknown verification target ${targetId}`,
433
463
  path: ["requirements"],
434
464
  });
465
+ // Source fidelity ledger binding (AC-005/AC-006): 绑定携带
466
+ // requirementToFragments(ledger v2)时,每个 requirement 必须携带
467
+ // 有效 sourceFragmentIds/sourceRefs,否则 fail closed(防引用伪造/缺失)。
468
+ const boundFragments = value.sourceBinding.requirementToFragments?.[requirement.id];
469
+ if (value.sourceBinding.ledgerPath &&
470
+ (!requirement.sourceFragmentIds ||
471
+ requirement.sourceFragmentIds.length === 0 ||
472
+ !requirement.sourceRefs ||
473
+ requirement.sourceRefs.length === 0)) {
474
+ ctx.addIssue({
475
+ code: "custom",
476
+ message: `requirement ${requirement.id} must carry sourceFragmentIds/sourceRefs when a source fidelity ledger is bound`,
477
+ path: ["requirements", requirement.id, "sourceFragmentIds"],
478
+ });
479
+ }
480
+ else if (boundFragments !== undefined &&
481
+ (!requirement.sourceFragmentIds ||
482
+ requirement.sourceFragmentIds.length === 0)) {
483
+ ctx.addIssue({
484
+ code: "custom",
485
+ message: `requirement ${requirement.id} is bound to ledger fragments ${boundFragments.join(", ")} but declares none`,
486
+ path: ["requirements", requirement.id, "sourceFragmentIds"],
487
+ });
488
+ }
435
489
  }
436
490
  for (const state of value.uiStates) {
437
491
  if (state.applicable &&
@@ -568,6 +622,29 @@ export async function assertFrontendSourceBindingFresh(input) {
568
622
  throw new Error(`frontend source binding is stale: ${source.path}`);
569
623
  }
570
624
  }
625
+ // Source binding v2 ledger 新鲜度(AC-FIDELITY-REPAIR-001/002):v2 绑定携带的
626
+ // ledger 文件必须存在且 sha256 与绑定一致,否则 fail closed;v1 绑定无此路径。
627
+ if (isDagSourceBindingV2(input.binding)) {
628
+ const absolute = resolveDagTaskSourcePath({
629
+ workspaceRoot: input.workspaceRoot,
630
+ taskId: input.binding.taskId,
631
+ sourcePath: input.binding.ledgerPath,
632
+ });
633
+ let content;
634
+ try {
635
+ content = await readFile(absolute);
636
+ const info = await stat(absolute);
637
+ if (!info.isFile())
638
+ throw new Error("not a regular file");
639
+ }
640
+ catch (error) {
641
+ throw new Error(`frontend source fidelity ledger unavailable: ${input.binding.ledgerPath}: ${error instanceof Error ? error.message : String(error)}`);
642
+ }
643
+ const actual = createHash("sha256").update(content).digest("hex");
644
+ if (actual !== input.binding.ledgerSha256) {
645
+ throw new Error(`frontend source fidelity ledger is stale or tampered: ${input.binding.ledgerPath} (bound ${input.binding.ledgerSha256}, observed ${actual})`);
646
+ }
647
+ }
571
648
  }
572
649
  const SECRET_KEY = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|credential|authorization)/i;
573
650
  const SECRET_VALUE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b)/;
@@ -742,7 +819,7 @@ export function canonicalFrontendContractSourceBinding(binding) {
742
819
  const requirement = binding.sources.find((source) => source.kind === "requirement");
743
820
  if (!requirement)
744
821
  throw new Error("frontend implementation contract gate requires a requirement source in DAG sourceBinding");
745
- return {
822
+ const sourceBinding = {
746
823
  taskId: binding.taskId,
747
824
  requirementPath: requirement.path,
748
825
  requirementSha256: requirement.sha256,
@@ -752,6 +829,15 @@ export function canonicalFrontendContractSourceBinding(binding) {
752
829
  .sort(),
753
830
  requirementIds: binding.requirementIds.map(canonicalizeRequirementId),
754
831
  };
832
+ // Source binding v2(schemaVersion=2 强制字段):透传 ledger 指针与
833
+ // requirement→fragment 映射。v1 绑定不得以 optional 字段冒充 v2。
834
+ if (isDagSourceBindingV2(binding)) {
835
+ sourceBinding.ledgerPath = binding.ledgerPath;
836
+ sourceBinding.ledgerSha256 = binding.ledgerSha256;
837
+ sourceBinding.inputDigest = binding.inputDigest;
838
+ sourceBinding.requirementToFragments = binding.requirementToFragments;
839
+ }
840
+ return sourceBinding;
755
841
  }
756
842
  function asRecord(value) {
757
843
  return value && typeof value === "object" && !Array.isArray(value)
@@ -47,6 +47,27 @@ function componentSelection(contract) {
47
47
  })
48
48
  .join("\n");
49
49
  }
50
+ /**
51
+ * 有界 source fidelity ledger 指针(AC-007 / AC-FIDELITY-007):长原文不重新
52
+ * 全量内联到节点提示词;只注入紧凑 canonical contract 与 bound ledger 指针
53
+ * (路径/哈希/inputDigest),ledger coverage 证明长尾未丢失。
54
+ */
55
+ function sourceFidelityLedger(contract) {
56
+ const binding = contract.sourceBinding;
57
+ if (!binding.ledgerPath)
58
+ return "_(not bound — v1 source binding)_";
59
+ const lines = [
60
+ `- ledger: ${binding.ledgerPath}`,
61
+ `- ledgerSha256: ${binding.ledgerSha256 ?? "(missing)"}`,
62
+ `- inputDigest: ${binding.inputDigest ?? "(missing)"}`,
63
+ "- 语义权威:原始 requirement/acceptance 文档与 requirement-ledger.json;本计划只消费派生摘要与 bound ledger 指针,长原文不内联。",
64
+ ];
65
+ const boundCount = Object.keys(binding.requirementToFragments ?? {}).length;
66
+ if (boundCount > 0) {
67
+ lines.push(`- requirement→fragment bindings: ${boundCount}`);
68
+ }
69
+ return lines.join("\n");
70
+ }
50
71
  export function renderFrontendPlanMarkdown(contract) {
51
72
  return [
52
73
  "# Frontend Implementation Plan",
@@ -54,6 +75,9 @@ export function renderFrontendPlanMarkdown(contract) {
54
75
  "## Requirement Coverage",
55
76
  requirementsCoverage(contract),
56
77
  "",
78
+ "## Source Fidelity Ledger",
79
+ sourceFidelityLedger(contract),
80
+ "",
57
81
  "## Implementation Steps",
58
82
  bulletList(contract.implementationSteps) || "_(not specified)_",
59
83
  "",