elanous 0.2.14 → 0.2.15

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 (293) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-33ffcc49ab45123b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-74383f890f075511.js → page-0e2c57fe6add4b2a.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-e05a77df7e005b04.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-b8c49f6b4fa2df91.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-976813c91d7394ea.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/{page-fb260b9283dbf646.js → page-28b9448091776c06.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-60c7a8e1a4fe62ce.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-2a52b316cb143d9c.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-c3ce483c8d5db370.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-96f074f6538e8a2c.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-e97be5334d83a9f5.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-955ec2da3be4c560.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-c1343f9499d86f3f.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-d23b12bb92242558.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-04250f0e2c0d03df.js → page-e2ee2896811e98d0.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-5efbf81124c474ae.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-85f1a1fc0a82738c.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-8ac8d71c2adf9c8a.js → page-da6c08aae0f92768.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-153c2b3bcc89e8de.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-94dc383d3d54db1a.js → page-87a11e6a05f15199.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-b3759cc14da80908.js → page-7d4182964407c09c.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-0d6d23b40263a7ea.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-f5597e894c02cd33.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/{page-35bdeae2e2c17147.js → page-73735b64bfe1ae67.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-8fcec4c9ea2e7f70.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-9817c7f9007466cc.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-134c1bc762aee62c.js → page-97f9f43172a3a90e.js} +1 -1
  53. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +1 -0
  54. package/apps/pwa/out/approvals/index.html +1 -1
  55. package/apps/pwa/out/approvals/index.txt +14 -14
  56. package/apps/pwa/out/autopilot/index.html +1 -1
  57. package/apps/pwa/out/autopilot/index.txt +14 -14
  58. package/apps/pwa/out/board/index.html +1 -1
  59. package/apps/pwa/out/board/index.txt +14 -14
  60. package/apps/pwa/out/botlab/index.html +1 -1
  61. package/apps/pwa/out/botlab/index.txt +14 -14
  62. package/apps/pwa/out/bots/index.html +1 -1
  63. package/apps/pwa/out/bots/index.txt +14 -14
  64. package/apps/pwa/out/ceo/index.html +1 -0
  65. package/apps/pwa/out/ceo/index.txt +33 -0
  66. package/apps/pwa/out/chat/index.html +1 -1
  67. package/apps/pwa/out/chat/index.txt +14 -14
  68. package/apps/pwa/out/consult/index.html +1 -1
  69. package/apps/pwa/out/consult/index.txt +14 -14
  70. package/apps/pwa/out/control/index.html +1 -1
  71. package/apps/pwa/out/control/index.txt +14 -14
  72. package/apps/pwa/out/design-check/index.html +1 -1
  73. package/apps/pwa/out/design-check/index.txt +14 -14
  74. package/apps/pwa/out/editor/index.html +1 -1
  75. package/apps/pwa/out/editor/index.txt +14 -14
  76. package/apps/pwa/out/exec/index.html +1 -1
  77. package/apps/pwa/out/exec/index.txt +14 -14
  78. package/apps/pwa/out/field/index.html +1 -1
  79. package/apps/pwa/out/field/index.txt +14 -14
  80. package/apps/pwa/out/healthz +1 -1
  81. package/apps/pwa/out/index.html +1 -1
  82. package/apps/pwa/out/index.txt +14 -14
  83. package/apps/pwa/out/inside/index.html +1 -1
  84. package/apps/pwa/out/inside/index.txt +14 -14
  85. package/apps/pwa/out/intake/index.html +1 -1
  86. package/apps/pwa/out/intake/index.txt +14 -14
  87. package/apps/pwa/out/live/index.html +1 -1
  88. package/apps/pwa/out/live/index.txt +14 -14
  89. package/apps/pwa/out/loops/index.html +1 -1
  90. package/apps/pwa/out/loops/index.txt +14 -14
  91. package/apps/pwa/out/market/index.html +1 -1
  92. package/apps/pwa/out/market/index.txt +14 -14
  93. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  94. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  95. package/apps/pwa/out/morning/index.html +1 -1
  96. package/apps/pwa/out/morning/index.txt +14 -14
  97. package/apps/pwa/out/observatory/index.html +1 -1
  98. package/apps/pwa/out/observatory/index.txt +14 -14
  99. package/apps/pwa/out/ops/checklist/index.html +1 -1
  100. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  101. package/apps/pwa/out/ops/release/index.html +1 -1
  102. package/apps/pwa/out/ops/release/index.txt +14 -14
  103. package/apps/pwa/out/ops/seats/index.html +1 -1
  104. package/apps/pwa/out/ops/seats/index.txt +14 -14
  105. package/apps/pwa/out/outputs/index.html +1 -1
  106. package/apps/pwa/out/outputs/index.txt +14 -14
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +15 -15
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +14 -14
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +14 -14
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +14 -14
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +14 -14
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +14 -14
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +14 -14
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +14 -14
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +14 -14
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +14 -14
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +14 -14
  129. package/apps/pwa/out/today/index.html +1 -1
  130. package/apps/pwa/out/today/index.txt +14 -14
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +14 -14
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +14 -14
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +14 -14
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +14 -14
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +14 -14
  143. package/graphs/heal/heal-loop.yaml +1 -1
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/graphs/release/recipes.yaml +3 -0
  146. package/graphs/release/release-loop.yaml +4 -2
  147. package/package.json +1 -1
  148. package/scripts/brand/check.ts +7 -5
  149. package/scripts/ci-typecheck-changed.ts +50 -18
  150. package/scripts/docs/generate-graph-anatomy.ts +315 -0
  151. package/scripts/expose-rubric-suggest.ts +18 -12
  152. package/scripts/lecture-note/node.ts +31 -11
  153. package/scripts/lessons/handbook-page.ts +30 -3
  154. package/scripts/marketing/generate-ax-brief.ts +140 -0
  155. package/scripts/marketing/generate-feature-map.ts +118 -0
  156. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  157. package/scripts/release-loop/gate-node.ts +80 -14
  158. package/scripts/release-loop/vault-note-node.ts +240 -8
  159. package/scripts/release-story/draft.ts +57 -9
  160. package/scripts/rhythm/daily.ts +263 -76
  161. package/scripts/rhythm/weekly.ts +30 -9
  162. package/scripts/smart-merge/resync-shadow.ts +147 -0
  163. package/src/agent-env/profile.ts +161 -0
  164. package/src/agent-substrate/pr-reviewer.ts +2 -0
  165. package/src/ask-user-question/pending-questions.ts +5 -0
  166. package/src/auth/github-app-token.ts +112 -13
  167. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  168. package/src/briefing/brief-items.ts +121 -0
  169. package/src/claims/claims-render.ts +14 -5
  170. package/src/cli/agent-env-cli.ts +21 -0
  171. package/src/cli/decisions-cli.ts +9 -0
  172. package/src/cli/doctor-cli.ts +23 -1
  173. package/src/cli/doctor-node-modules.ts +147 -0
  174. package/src/cli/intake-cli.ts +46 -17
  175. package/src/cli/pod-cli.ts +2 -0
  176. package/src/cli/release-cli.ts +61 -27
  177. package/src/cli/seat-requests-cli.ts +52 -0
  178. package/src/context-bus/context-now-public.ts +16 -6
  179. package/src/context-bus/context-now.ts +3 -1
  180. package/src/context-bus/coord-events.ts +6 -4
  181. package/src/decisions/decision-cards.ts +33 -7
  182. package/src/decisions/proact-meter.ts +286 -0
  183. package/src/decisions/proact1-lite.ts +86 -0
  184. package/src/decisions/web-push-decision-cards.ts +1 -1
  185. package/src/domains/alert-audit.ts +271 -0
  186. package/src/domains/core-tools.ts +4 -3
  187. package/src/domains/ops-facts-tool.ts +31 -1
  188. package/src/harness/harness-cli-command.ts +111 -15
  189. package/src/harness/harness-incidents.ts +2 -0
  190. package/src/harness/harness-pod-dispatch.ts +100 -11
  191. package/src/harness/harness-queue.ts +380 -50
  192. package/src/harness/helper-repair.ts +87 -0
  193. package/src/harness/helper-scan.ts +16 -4
  194. package/src/harness/launch-stamp.ts +236 -0
  195. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  196. package/src/harness/ledger-write-guard.ts +35 -0
  197. package/src/harness/pod-memory-policy.ts +15 -0
  198. package/src/harness/repository-name.ts +55 -0
  199. package/src/hq/fence-audit.ts +90 -0
  200. package/src/hq/hq.ts +62 -4
  201. package/src/hq/lease.ts +4 -4
  202. package/src/hq/move-plan.ts +126 -0
  203. package/src/hq/promote.ts +31 -0
  204. package/src/index.ts +77 -7
  205. package/src/intake-plane/check.ts +63 -0
  206. package/src/intake-plane/digest.ts +37 -15
  207. package/src/intake-plane/front-classifier.ts +6 -0
  208. package/src/intake-plane/items.ts +3 -2
  209. package/src/intake-plane/lens.ts +177 -0
  210. package/src/intake-plane/route.ts +145 -1
  211. package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
  212. package/src/launch-head/seat-goal-queue.ts +198 -0
  213. package/src/loops/budget.ts +36 -5
  214. package/src/loops/checker.ts +2 -2
  215. package/src/loops/orchestrator/author-depth.ts +65 -9
  216. package/src/loops/orchestrator/author-input-check.ts +8 -3
  217. package/src/loops/orchestrator/author-ledger.ts +3 -1
  218. package/src/loops/orchestrator/seat-share.ts +56 -0
  219. package/src/loops/orchestrator/tick.ts +11 -4
  220. package/src/loops/orchestrator/traffic.ts +117 -11
  221. package/src/loops/registry.ts +142 -61
  222. package/src/maturity/feature-maturity.ts +2 -1
  223. package/src/maturity/tui-slash-availability.ts +31 -0
  224. package/src/mss/logging/log-store.ts +35 -4
  225. package/src/nexus/api/http-server.ts +28 -14
  226. package/src/nexus/api/loop-edges.ts +173 -0
  227. package/src/nexus/api/schedules-read.ts +13 -0
  228. package/src/nexus/api/seat-requests.ts +79 -56
  229. package/src/prompt-enhance/enhance.ts +6 -1
  230. package/src/release-loop/auto-start.ts +6 -3
  231. package/src/release-loop/checklist.ts +13 -4
  232. package/src/release-loop/feature-store.ts +58 -31
  233. package/src/release-loop/merged-pr-checklist.ts +125 -0
  234. package/src/release-loop/placement.ts +86 -26
  235. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  236. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  237. package/src/seat-loop/seat-loop.ts +126 -10
  238. package/src/self-dev/dev-pipeline.ts +8 -4
  239. package/src/self-dev/draft-sweep.ts +90 -10
  240. package/src/self-dev/draft-triage-rules.ts +25 -8
  241. package/src/self-dev/orchestrate.ts +15 -1
  242. package/src/self-dev/review-provider-fallback.ts +6 -1
  243. package/src/self-dev/run-supervisor.ts +49 -2
  244. package/src/self-implement/frozen-merges.ts +3 -2
  245. package/src/self-implement/goal-author.ts +43 -5
  246. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  247. package/src/self-implement/heal-intake.ts +120 -2
  248. package/src/self-implement/host-regate.ts +9 -2
  249. package/src/self-implement/l8-merge-queue.ts +132 -0
  250. package/src/self-implement/orchestrator.ts +187 -40
  251. package/src/self-implement/pipeline-shape.ts +1 -1
  252. package/src/self-implement/revert-guard.ts +42 -7
  253. package/src/self-implement/run-ledger.ts +2 -0
  254. package/src/self-implement/run-status-mapping.ts +2 -0
  255. package/src/self-implement/seams.ts +1 -1
  256. package/src/self-implement/self-implement-cli.ts +1 -0
  257. package/src/self-implement/speed-claims.ts +338 -0
  258. package/src/steward/triage.ts +74 -3
  259. package/src/steward/working-backwards.ts +4 -1
  260. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  261. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  262. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  263. package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
  264. package/src/task-orchestrator/surfaces/self-implement-pod.ts +147 -36
  265. package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
  266. package/src/telegram.ts +25 -4
  267. package/src/user-config.ts +29 -5
  268. package/src/version/packed-revision.json +1 -1
  269. package/src/workflow-runtime/nodes/bash.ts +9 -1
  270. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +0 -1
  291. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  292. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  293. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
@@ -53,6 +53,9 @@ export type ChangedTsFiles = {
53
53
  files: Set<string>;
54
54
  base: string;
55
55
  baseSource: string;
56
+ head?: string;
57
+ diffFiles?: Set<string>;
58
+ warnings?: string[];
56
59
  failures?: { command: string; why: string }[];
57
60
  };
58
61
 
@@ -78,23 +81,41 @@ export function changedTsFiles(run = sh, env: NodeJS.ProcessEnv = process.env):
78
81
  }
79
82
  }
80
83
 
81
- const files = new Set<string>();
82
- const add = (out: string) => {
83
- for (const line of out.split('\n')) {
84
- const file = line.trim().replace(/^\.\//, '');
85
- if (/\.tsx?$/.test(file) && existsSync(file)) files.add(file);
86
- }
87
- };
88
84
  const failures: { command: string; why: string }[] = [];
89
- const addCommand = (command: string) => {
90
- try { add(run(command)); } catch (cause) {
85
+ // Revision reads are observational: an unreadable HEAD/base must not turn into a collection failure.
86
+ const quietRevision = (command: string): string => { try { return run(command).trim(); } catch { return ''; } };
87
+ const head = quietRevision('git rev-parse HEAD') || undefined;
88
+ const warnings: string[] = [];
89
+ if (head && quietRevision(`git rev-parse ${base}`) === head) {
90
+ const main = quietRevision('git rev-parse origin/main');
91
+ const merged = main && main !== head ? quietRevision('git merge-base HEAD origin/main') : '';
92
+ if (merged && merged !== head) {
93
+ warnings.push(`base=${base} 가 HEAD 와 같은 커밋 — origin/main (${main})과 달라 merge-base 로 재계산`);
94
+ base = merged;
95
+ baseSource = 'recomputed merge-base(origin/main)';
96
+ } else if (main && main !== head) {
97
+ failures.push({ command: `git rev-parse ${base}`, why: `base 가 HEAD(${head}) 와 같아 변경을 셀 수 없다 — TSC_BASE_REF 로 base 를 주라 (못 셌음 ≠ 0)` });
98
+ }
99
+ }
100
+
101
+ const files = new Set<string>();
102
+ const diffFiles = new Set<string>();
103
+ const commands = [`git diff --name-only ${base}...HEAD`, 'git diff --name-only HEAD', 'git ls-files --others --exclude-standard'];
104
+ for (const command of commands) {
105
+ try {
106
+ for (const line of run(command).split('\n')) {
107
+ const file = line.trim().replace(/^\.\//, '');
108
+ if (!file) continue;
109
+ const ts = /\.tsx?$/.test(file);
110
+ // A deleted .ts is not a compile candidate, so it must not count as «should have been collected».
111
+ if (!ts || existsSync(file)) diffFiles.add(file);
112
+ if (ts && existsSync(file)) files.add(file);
113
+ }
114
+ } catch (cause) {
91
115
  failures.push({ command, why: cause instanceof Error ? cause.message : String(cause) });
92
116
  }
93
- };
94
- addCommand(`git diff --name-only ${base}...HEAD`);
95
- addCommand('git diff --name-only HEAD');
96
- addCommand('git ls-files --others --exclude-standard');
97
- return { files, base, baseSource, failures };
117
+ }
118
+ return { files, base, baseSource, head, diffFiles, warnings, failures };
98
119
  }
99
120
 
100
121
  type RequiredExportField = { typeName: string; fieldName: string };
@@ -850,10 +871,13 @@ export function runGate(io: Partial<GateIo> = {}): void {
850
871
  ? { additions: [] }
851
872
  : exportedLiteralUnionMembersInChanges(changed, base));
852
873
  const changedResult = getChanged();
853
- const { files: changed, base, baseSource, failures = [] } = changedResult instanceof Set
854
- ? { files: changedResult, base: 'injected test seam', baseSource: 'injected test seam', failures: [] }
874
+ const { files: changed, base, baseSource, head, diffFiles, warnings = [], failures = [] } = changedResult instanceof Set
875
+ ? { files: changedResult, base: 'injected test seam', baseSource: 'injected test seam', head: 'injected test seam', diffFiles: new Set<string>(), warnings: [], failures: [] }
855
876
  : changedResult;
856
- log(`[tsc-gate] base=${base} (${baseSource}); 변경 .ts ${changed.size}개 관측.`);
877
+ for (const message of warnings) warn(`[tsc-gate] ⚠ ${message}`);
878
+ const diffCount = diffFiles?.size;
879
+ const diffTsCount = diffFiles && [...diffFiles].filter((file) => /\.tsx?$/.test(file)).length;
880
+ log(`[tsc-gate] base=${base} (${baseSource}); 변경 .ts ${changed.size}개 관측. (head=${head ?? 'unknown'} · 전체 diff 파일 ${diffCount ?? 'unknown'}개 · diff .ts ${diffTsCount ?? 'unknown'}개)`);
857
881
  if (failures.length > 0) {
858
882
  error('[tsc-gate] ⛔ 변경 파일 수집 실패:');
859
883
  for (const { command, why } of failures) error(` ${command}: ${why}`);
@@ -861,7 +885,15 @@ export function runGate(io: Partial<GateIo> = {}): void {
861
885
  exit(1);
862
886
  return;
863
887
  }
864
- if (changed.size === 0) { log('[tsc-gate] 변경된 .ts 파일 없음 — 위 base 기준 0개이며 tsc 실행 없이 통과.'); return; }
888
+ if (changed.size === 0) {
889
+ if (!(changedResult instanceof Set) && (!head || diffCount === undefined || diffTsCount === undefined || diffTsCount > 0)) {
890
+ error(`[tsc-gate] FAIL — 변경 파일 수집 불일치: 전체 diff 파일 ${diffCount ?? 'unknown'}개 · diff .ts ${diffTsCount ?? 'unknown'}개 · 수집 .ts 0개 (못 셌음 ≠ 0).`);
891
+ exit(1);
892
+ return;
893
+ }
894
+ log(`[tsc-gate] PASS — 변경된 .ts 파일 없음 · base=${base} · head=${head} · 전체 diff 파일 ${diffCount}개 · diff .ts ${diffTsCount}개 · 수집 .ts 0개; tsc 실행 없이 통과.`);
895
+ return;
896
+ }
865
897
  const fieldCheck = requiredExportFields(changed, base);
866
898
  const requiredFields = Array.isArray(fieldCheck) ? fieldCheck : fieldCheck.fields;
867
899
  if (!Array.isArray(fieldCheck) && fieldCheck.failure) {
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * DOC-GEN① — 그래프 아나토미를 코드에서 생성한다.
4
+ *
5
+ * 원천은 둘뿐이다.
6
+ * 노드 종류 = src/graph-kinds/registry.ts (HARNESS_CORE_KINDS · WORKFLOW_CORE_KINDS · listNodeKinds)
7
+ * 그래프 = graphs/ YAML 을 src/self-implement/graph-yaml.ts 의 loadGraphTemplates · edgeMapOf 가 읽는다
8
+ *
9
+ * renderGraphAnatomy 는 순수하다 — 파일을 쓰지 않고 process.cwd() 를 읽지 않는다.
10
+ * 뿌리·커밋·시각은 인자로만 받는다. main 만 내부 문서 `graph-anatomy` 에 쓴다.
11
+ */
12
+
13
+ import { execFileSync } from 'node:child_process';
14
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
15
+ import { dirname, join, relative } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ import { listNodeKinds, type GraphKind, type NodeKindEntry } from '../../src/graph-kinds/registry.js';
19
+ import {
20
+ edgeMapOf,
21
+ loadGraphTemplates,
22
+ parseGraphTemplateYaml,
23
+ type GraphLoadResult,
24
+ type GraphParseIssue,
25
+ type GraphTemplateSpec,
26
+ } from '../../src/self-implement/graph-yaml.js';
27
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
28
+
29
+ /** drafts/ · overlays/ 는 스캔에서 뺀다(실측 10-05). 그 이름인 디렉터리와 그 아래는 안 읽는다. */
30
+ const EXCLUDED_DIR_NAMES = new Set(['drafts', 'overlays']);
31
+
32
+ export interface RenderGraphAnatomyInput {
33
+ readonly graphsRoot: string;
34
+ readonly commit: string;
35
+ readonly generatedAt: string;
36
+ }
37
+
38
+ export interface ScannedGraphDir {
39
+ /** graphsRoot 기준 상대 경로. 뿌리 자신은 '.' */
40
+ readonly dir: string;
41
+ readonly result: GraphLoadResult;
42
+ }
43
+
44
+ function cell(value: string): string {
45
+ return value.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
46
+ }
47
+
48
+ function edgeCountOf(template: GraphTemplateSpec): number {
49
+ return Object.values(edgeMapOf(template)).reduce((sum, targets) => sum + targets.length, 0);
50
+ }
51
+
52
+ function edgeLinesOf(template: GraphTemplateSpec): string[] {
53
+ const lines: string[] = [];
54
+ for (const edge of template.edges) {
55
+ if (edge.to !== undefined) lines.push(`${edge.from} → ${edge.to}`);
56
+ for (const [key, target] of Object.entries(edge.map ?? {})) lines.push(`${edge.from} → ${target} (${key})`);
57
+ for (const fallback of edge.fallback ?? []) lines.push(`${edge.from} → ${fallback.node} (fallback)`);
58
+ }
59
+ return lines;
60
+ }
61
+
62
+ /**
63
+ * A directory with recipes.yaml holds workflow graphs run by `elanous graph run` (src/graph-runner/runner.ts), not
64
+ * harness templates. The runner gives a recipe-less node (done · failed) `recipe: 'none'` before the shared parser
65
+ * (runner.ts «command-less node … represented internally as `none`»); read those graphs the same way, and do not
66
+ * read recipes.yaml itself as a graph.
67
+ */
68
+ function isRunnerDir(abs: string): boolean { return existsSync(join(abs, 'recipes.yaml')); }
69
+
70
+ function parseLikeRunner(source: string, label: string): ReturnType<typeof parseGraphTemplateYaml> {
71
+ let raw: unknown;
72
+ try { raw = parseYaml(source); } catch { return parseGraphTemplateYaml(source, label); }
73
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return parseGraphTemplateYaml(source, label);
74
+ const document = raw as Record<string, unknown>;
75
+ if (Array.isArray(document.nodes)) {
76
+ document.nodes = document.nodes.map((node: unknown) => node && typeof node === 'object' && !Array.isArray(node) && !('recipe' in node)
77
+ ? { ...node, recipe: 'none' } : node);
78
+ }
79
+ return parseGraphTemplateYaml(stringifyYaml(document), label);
80
+ }
81
+
82
+ function graphFileNames(abs: string): string[] {
83
+ const runner = isRunnerDir(abs);
84
+ return readdirSync(abs).filter((name) => (name.endsWith('.yaml') || name.endsWith('.yml')) && !(runner && name === 'recipes.yaml')).sort();
85
+ }
86
+
87
+ /** loadGraphTemplates for harness dirs; the runner's reading for workflow-graph dirs (same result shape). */
88
+ function loadDir(abs: string): GraphLoadResult {
89
+ if (!isRunnerDir(abs)) return loadGraphTemplates(abs);
90
+ const errors: GraphParseIssue[] = [];
91
+ const warnings: GraphParseIssue[] = [];
92
+ const templates: Record<string, GraphTemplateSpec> = {};
93
+ let files: string[];
94
+ try { files = graphFileNames(abs); }
95
+ catch (error) { return { templates, errors: [{ path: abs, message: `그래프 디렉토리를 못 읽었다: ${String(error)}` }], warnings, scannedFiles: 0 }; }
96
+ for (const file of files) {
97
+ let source: string;
98
+ try { source = readFileSync(join(abs, file), 'utf8'); }
99
+ catch (error) { errors.push({ path: file, message: `읽기 실패: ${String(error)}` }); continue; }
100
+ const result = parseLikeRunner(source, file);
101
+ errors.push(...result.errors);
102
+ warnings.push(...result.warnings);
103
+ if (!result.template) continue;
104
+ if (templates[result.template.graphId]) { errors.push({ path: file, message: `graph_id '${result.template.graphId}' 가 중복이다` }); continue; }
105
+ templates[result.template.graphId] = result.template;
106
+ }
107
+ return { templates, errors, warnings, scannedFiles: files.length };
108
+ }
109
+
110
+ /** 뿌리와 하위 디렉터리를 돈다. drafts/ · overlays/ 와 그 아래는 제외. 디렉터리마다 loadGraphTemplates 를 그대로 부른다. */
111
+ export function scanGraphDirs(graphsRoot: string): ScannedGraphDir[] {
112
+ const scanned: ScannedGraphDir[] = [];
113
+ const visit = (dir: string): void => {
114
+ scanned.push({ dir: relative(graphsRoot, dir) || '.', result: loadDir(dir) });
115
+ let names: string[];
116
+ try {
117
+ names = readdirSync(dir, { withFileTypes: true })
118
+ .filter((entry) => entry.isDirectory() && !EXCLUDED_DIR_NAMES.has(entry.name))
119
+ .map((entry) => entry.name)
120
+ .sort();
121
+ } catch {
122
+ return;
123
+ }
124
+ for (const name of names) visit(join(dir, name));
125
+ };
126
+ visit(graphsRoot);
127
+ return scanned;
128
+ }
129
+
130
+ /**
131
+ * loadGraphTemplates 는 파일명을 템플릿에 안 남긴다.
132
+ * 같은 디렉터리 yaml 을 parseGraphTemplateYaml 로 한 번씩 읽어 graph_id → 상대 경로를 붙인다.
133
+ * 파싱에 실패한 파일은 표에 안 들어가므로 맵에도 안 넣는다.
134
+ */
135
+ /** 그 디렉터리에서 loadGraphTemplates 가 센 yaml 이름. 못 읽으면 빈 목록. */
136
+ function yamlNames(graphsRoot: string, dir: string): readonly string[] {
137
+ const abs = dir === '.' ? graphsRoot : join(graphsRoot, dir);
138
+ try {
139
+ return graphFileNames(abs);
140
+ } catch {
141
+ return [];
142
+ }
143
+ }
144
+
145
+ function graphIdPaths(graphsRoot: string, dir: string): ReadonlyMap<string, string> {
146
+ const abs = dir === '.' ? graphsRoot : join(graphsRoot, dir);
147
+ const prefix = dir === '.' ? '' : `${dir}/`;
148
+ const paths = new Map<string, string>();
149
+ let names: string[];
150
+ try {
151
+ names = graphFileNames(abs);
152
+ } catch {
153
+ return paths;
154
+ }
155
+ for (const name of names) {
156
+ let source: string;
157
+ try { source = readFileSync(join(abs, name), 'utf8'); }
158
+ catch { continue; }
159
+ const parsed = isRunnerDir(abs) ? parseLikeRunner(source, name) : parseGraphTemplateYaml(source, name);
160
+ if (!parsed.template) continue;
161
+ const rel = `${prefix}${name}`;
162
+ const prev = paths.get(parsed.template.graphId);
163
+ paths.set(parsed.template.graphId, prev === undefined ? rel : `${prev}, ${rel}`);
164
+ }
165
+ return paths;
166
+ }
167
+
168
+ function kindRows(entries: readonly NodeKindEntry[]): string[] {
169
+ return [...entries]
170
+ .sort((a, b) => (a.graph < b.graph ? -1 : a.graph > b.graph ? 1 : a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0))
171
+ .map((entry) => `| ${cell(entry.graph)} | ${cell(entry.kind)} | ${entry.core ? 'core' : 'plugin'} |`);
172
+ }
173
+
174
+ function issueLines(label: string, issues: readonly GraphParseIssue[]): string[] {
175
+ if (issues.length === 0) return [`- ${label}: 0`];
176
+ return [`- ${label}: ${issues.length}`, ...issues.map((issue) => ` - ${cell(issue.path)} — ${issue.message}`)];
177
+ }
178
+
179
+ /**
180
+ * 마크다운 한 장. 파일을 쓰지 않는다. graphsRoot · commit · generatedAt 만 읽는다.
181
+ */
182
+ export function renderGraphAnatomy(input: RenderGraphAnatomyInput): string {
183
+ const scanned = scanGraphDirs(input.graphsRoot);
184
+ const templates: { dir: string; fileLabel: string; template: GraphTemplateSpec }[] = [];
185
+ for (const { dir, result } of scanned) {
186
+ for (const template of Object.values(result.templates)) {
187
+ templates.push({ dir, fileLabel: dir === '.' ? `${template.graphId}` : `${dir}/${template.graphId}`, template });
188
+ }
189
+ }
190
+ templates.sort((a, b) => (a.template.graphId < b.template.graphId ? -1 : a.template.graphId > b.template.graphId ? 1 : a.dir < b.dir ? -1 : 1));
191
+
192
+ const scannedFiles = scanned.reduce((sum, entry) => sum + entry.result.scannedFiles, 0);
193
+ const missingDirs = scanned.filter((entry) =>
194
+ entry.result.scannedFiles === 0
195
+ && entry.result.errors.some((issue) => issue.message.startsWith('그래프 디렉토리를 못 읽었다')),
196
+ );
197
+
198
+ const lines: string[] = [
199
+ '# Graph anatomy',
200
+ '',
201
+ '> 이 문서는 생성물이다 — 손으로 고치지 마라',
202
+ '',
203
+ `- generatedAt: ${input.generatedAt}`,
204
+ `- commit: ${input.commit}`,
205
+ `- graphsRoot: (인자)`,
206
+ `- scannedFiles: ${scannedFiles}`,
207
+ `- scannedDirs: ${scanned.length}`,
208
+ `- templates: ${templates.length}`,
209
+ '',
210
+ '## 표 1 노드 종류',
211
+ '',
212
+ '원천: `src/graph-kinds/registry.ts` — `HARNESS_CORE_KINDS` · `WORKFLOW_CORE_KINDS` · `listNodeKinds()`.',
213
+ '',
214
+ '| graph | kind | core/plugin |',
215
+ '| --- | --- | --- |',
216
+ ...kindRows(listNodeKinds()),
217
+ '',
218
+ '## 표 2 그래프',
219
+ '',
220
+ '원천: `loadGraphTemplates(dir)` ⊕ `edgeMapOf(template)`. 간선 수는 edgeMapOf 합(to · map · fallback, 중복 목적지는 한 번). `drafts/` · `overlays/` 는 제외.',
221
+ '',
222
+ '| 파일 경로 | graph_id | 노드 수 | 간선 수 | entryNode | terminalNodes |',
223
+ '| --- | --- | --- | --- | --- | --- |',
224
+ ];
225
+
226
+ if (templates.length === 0) {
227
+ lines.push('| (없음) | — | 0 | 0 | — | — |');
228
+ }
229
+ const pathCache = new Map<string, ReadonlyMap<string, string>>();
230
+ for (const { dir, template } of templates) {
231
+ let paths = pathCache.get(dir);
232
+ if (paths === undefined) {
233
+ paths = graphIdPaths(input.graphsRoot, dir);
234
+ pathCache.set(dir, paths);
235
+ }
236
+ const where = paths.get(template.graphId) ?? (dir === '.' ? `${template.graphId}.yaml` : `${dir}/`);
237
+ lines.push(`| ${cell(where)} | ${cell(template.graphId)} | ${template.nodes.length} | ${edgeCountOf(template)} | ${cell(template.entryNode)} | ${cell(template.terminalNodes.join(', '))} |`);
238
+ }
239
+
240
+ lines.push('', '## 그래프별 노드 · 간선', '');
241
+ if (templates.length === 0) lines.push('(읽힌 그래프 없음)', '');
242
+ for (const { dir, template } of templates) {
243
+ const where = dir === '.' ? '(graphs root)' : dir;
244
+ lines.push(`### ${template.graphId}`, '', `- path: ${where}`, '', '#### 노드', '', '| nodeId | kind |', '| --- | --- |');
245
+ for (const node of template.nodes) lines.push(`| ${cell(node.nodeId)} | ${cell(node.kind)} |`);
246
+ lines.push('', '#### 간선', '');
247
+ const edges = edgeLinesOf(template);
248
+ if (edges.length === 0) lines.push('(간선 없음)');
249
+ else for (const edge of edges) lines.push(`- ${edge}`);
250
+ lines.push('');
251
+ }
252
+
253
+ lines.push('## 스캔한 파일', '');
254
+ lines.push('loadGraphTemplates 가 센 yaml. 파싱에 실패한 파일은 표 2에 없고 여기와 오류 절에만 있다.', '');
255
+ lines.push('| 디렉터리 | 파일 |', '| --- | --- |');
256
+ for (const { dir } of scanned) {
257
+ const names = yamlNames(input.graphsRoot, dir);
258
+ const prefix = dir === '.' ? '' : `${dir}/`;
259
+ if (names.length === 0) {
260
+ lines.push(`| ${cell(dir === '.' ? '(graphs root)' : dir)} | (yaml 0) |`);
261
+ continue;
262
+ }
263
+ for (const name of names) lines.push(`| ${cell(dir === '.' ? '(graphs root)' : dir)} | ${cell(`${prefix}${name}`)} |`);
264
+ }
265
+ lines.push('', '## 로드 오류 · 경고 · scannedFiles', '');
266
+ lines.push(`scannedFiles 합: ${scannedFiles}`, '');
267
+ if (missingDirs.length > 0) {
268
+ lines.push('디렉토리 없음 (0개 읽음과 구분):', '');
269
+ for (const entry of missingDirs) {
270
+ for (const issue of entry.result.errors) lines.push(`- ${cell(issue.path)} — ${issue.message}`);
271
+ }
272
+ lines.push('');
273
+ }
274
+ for (const { dir, result } of scanned) {
275
+ const label = dir === '.' ? '(graphs root)' : dir;
276
+ const unreadable = result.scannedFiles === 0 && result.errors.some((issue) => issue.message.startsWith('그래프 디렉토리를 못 읽었다'));
277
+ lines.push(`### ${label}`, '');
278
+ lines.push(`- scannedFiles: ${result.scannedFiles}${unreadable ? ' (디렉토리 없음)' : result.scannedFiles === 0 ? ' (0개 읽음)' : ''}`);
279
+ lines.push(...issueLines('errors', result.errors));
280
+ lines.push(...issueLines('warnings', result.warnings));
281
+ lines.push('');
282
+ }
283
+
284
+ const axes: GraphKind[] = ['harness', 'workflow'];
285
+ lines.push('## 축별 종류 수', '');
286
+ for (const axis of axes) lines.push(`- ${axis}: ${listNodeKinds(axis).length}`);
287
+ lines.push('');
288
+ return `${lines.join('\n').replace(/\n{3,}/g, '\n\n')}\n`;
289
+ }
290
+
291
+ function repoRootFromHere(): string {
292
+ return join(dirname(fileURLToPath(import.meta.url)), '..', '..');
293
+ }
294
+
295
+ function readCommit(repoRoot: string): string {
296
+ return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' }).trim();
297
+ }
298
+
299
+ export function defaultOutputPath(repoRoot: string): string {
300
+ return join(repoRoot, 'docs', 'generated', 'graph-anatomy.md');
301
+ }
302
+
303
+ function main(): void {
304
+ const repoRoot = repoRootFromHere();
305
+ const graphsRoot = join(repoRoot, 'graphs');
306
+ const generatedAt = new Date().toISOString();
307
+ const commit = readCommit(repoRoot);
308
+ const markdown = renderGraphAnatomy({ graphsRoot, commit, generatedAt });
309
+ const out = defaultOutputPath(repoRoot);
310
+ mkdirSync(dirname(out), { recursive: true });
311
+ writeFileSync(out, markdown);
312
+ process.stdout.write(`${out}\n`);
313
+ }
314
+
315
+ if (import.meta.main) main();
@@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process';
5
5
  import { join, relative, resolve, sep } from 'node:path';
6
6
  import { stringify } from 'yaml';
7
7
  import { debug } from '../src/debug/log.js';
8
- import { checkBrand } from './brand/check.js';
8
+ import { BRAND_RULES_UNMEASURED, checkBrand } from './brand/check.js';
9
9
  import { checkCommands, extractElanousCommands, type HelpRunner } from './docs-cli-check.js';
10
10
  import { scanLeaks } from './public-export.js';
11
11
 
@@ -25,7 +25,7 @@ function measure(run: () => Criterion): Criterion {
25
25
  }
26
26
 
27
27
  /** Files are repository-relative paths (or absolute paths within root); the ledger is never read or written. */
28
- export function suggestExposeRubric(root: string, files?: readonly string[], help?: HelpRunner): Suggestion[] {
28
+ export function suggestExposeRubric(root: string, files?: readonly string[], help?: HelpRunner, rulesPath?: string): Suggestion[] {
29
29
  const names = files ?? readdirSync(join(root, DOCS)).filter((name) => name.endsWith('.md')).map((name) => `${DOCS}/${name}`);
30
30
  const paths = [...new Set(names.map((name) => relative(root, resolve(root, name)).split(sep).join('/')))];
31
31
  if (paths.some((path) => !/^release\/public\/docs\/[^/]+\.md$/.test(path))) throw new Error('문서 경로는 release/public/docs/*.md 이어야 합니다');
@@ -77,6 +77,15 @@ export function suggestExposeRubric(root: string, files?: readonly string[], hel
77
77
  const failed = unmeasured(error);
78
78
  return { path, criteria: { reproducible: failed, confidential: failed, brand: failed } };
79
79
  }
80
+ const brand = measure(() => {
81
+ const result = checkBrand('public-docs', [absolute], rulesPath);
82
+ if (result.missing) return { status: 'n/a', reason: result.message ?? BRAND_RULES_UNMEASURED };
83
+ if (result.files !== 1 || result.rules === 0) return unmeasured('브랜드 규칙 없음 또는 파일 검사 실패');
84
+ const ids = [...new Set(result.findings.map((finding) => finding.id))].slice(0, 3);
85
+ return ids.length
86
+ ? { status: 'fail', reason: `브랜드 규칙: ${ids.join(', ')}` }
87
+ : { status: 'pass', reason: '브랜드 규칙 위반 없음' };
88
+ });
80
89
  const reproducible = measure(() => {
81
90
  const refs = extractElanousCommands(path, text);
82
91
  if (!refs.length) return { status: 'n/a', reason: '문서에 elanous 명령 없음' };
@@ -95,14 +104,7 @@ export function suggestExposeRubric(root: string, files?: readonly string[], hel
95
104
  ? { status: 'fail', reason: `누출 표식: ${markers.join(', ')}` }
96
105
  : { status: 'pass', reason: '누출 표식 없음' };
97
106
  });
98
- const brand = measure(() => {
99
- const result = checkBrand('public-docs', [absolute]);
100
- if (result.missing || result.files !== 1 || result.rules === 0) return unmeasured('브랜드 규칙 없음 또는 파일 검사 실패');
101
- const ids = [...new Set(result.findings.map((finding) => finding.id))].slice(0, 3);
102
- return ids.length
103
- ? { status: 'fail', reason: `브랜드 규칙: ${ids.join(', ')}` }
104
- : { status: 'pass', reason: '브랜드 규칙 위반 없음' };
105
- });
107
+ // 브랜드 규칙이 없으면 브랜드만 «측정 불가» — 따로 잰 재현성·누출 결과는 덮어쓰지 않는다.
106
108
  return { path, criteria: { reproducible, confidential, brand } };
107
109
  });
108
110
  const statuses = results.flatMap((item) => KEYS.map((key) => item.criteria[key].status));
@@ -119,19 +121,23 @@ export function run(argv: readonly string[], root = process.cwd(), help?: HelpRu
119
121
  const json = argv.includes('--json');
120
122
  let files: string[] | undefined;
121
123
  let out: string | undefined;
124
+ let rulesPath: string | undefined;
122
125
  for (let i = 0; i < argv.length; i++) {
123
126
  const arg = argv[i]!;
124
127
  if (arg === '--json') continue;
125
128
  if (arg === '--yaml-patch') {
126
129
  out = argv[++i];
127
130
  if (!out || out.startsWith('--')) throw new Error('--yaml-patch 에 출력 경로가 필요합니다');
131
+ } else if (arg === '--rules') {
132
+ rulesPath = argv[++i];
133
+ if (!rulesPath || rulesPath.startsWith('--')) throw new Error('--rules 에 규칙 경로가 필요합니다');
128
134
  } else if (arg === '--files') {
129
135
  files = [];
130
136
  while (argv[i + 1] && !argv[i + 1]!.startsWith('--')) files.push(argv[++i]!);
131
137
  if (!files.length) throw new Error('--files 에 문서 경로가 필요합니다');
132
138
  } else throw new Error(`알 수 없는 옵션: ${arg}`);
133
139
  }
134
- const results = suggestExposeRubric(root, files, help);
140
+ const results = suggestExposeRubric(root, files, help, rulesPath);
135
141
  if (out) {
136
142
  const target = resolve(root, out);
137
143
  const ledger = resolve(root, LEDGER);
@@ -140,7 +146,7 @@ export function run(argv: readonly string[], root = process.cwd(), help?: HelpRu
140
146
  }
141
147
  if (json) console.log(JSON.stringify(results));
142
148
  else {
143
- for (const { path, criteria } of results) console.log(`${path} · ${KEYS.map((key) => `${key} ${criteria[key].status}${criteria[key].status === 'fail' ? `(${criteria[key].reason.split(': ').slice(1).join(': ') || criteria[key].reason})` : ''}`).join(' · ')}`);
149
+ for (const { path, criteria } of results) console.log(`${path} · ${KEYS.map((key) => `${key} ${criteria[key].status}${criteria[key].status === 'fail' ? `(${criteria[key].reason.split(': ').slice(1).join(': ') || criteria[key].reason})` : criteria[key].reason === BRAND_RULES_UNMEASURED ? `(${criteria[key].reason})` : ''}`).join(' · ')}`);
144
150
  const statuses = results.flatMap((item) => KEYS.map((key) => item.criteria[key].status));
145
151
  console.log(`합계 문서 ${results.length} · pass ${statuses.filter((status) => status === 'pass').length} · fail ${statuses.filter((status) => status === 'fail').length} · n/a ${statuses.filter((status) => status === 'n/a').length} · maturity/value/evidence/support 사람 판정 (MK)`);
146
152
  }
@@ -5,6 +5,7 @@ import { dirname, extname, isAbsolute, join, resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { homedir } from 'node:os';
7
7
  import { debug } from '../../src/debug/log.js';
8
+ import { runOcr } from '../../skills/omni-digest/src/ocr.js';
8
9
  import { streamLLM } from '../../src/llm.js';
9
10
  import { unified } from 'unified';
10
11
  import remarkParse from 'remark-parse';
@@ -19,6 +20,7 @@ export interface Counts { inputs: number; diagrams: number; rendered: number; ht
19
20
  export interface StageResult extends Counts { outcome: 'ok' | 'skipped' }
20
21
  export interface StageOptions {
21
22
  llm?: (prompt: string) => Promise<string>;
23
+ ocr?: (filePath: string) => Promise<string>;
22
24
  toolsPath?: string;
23
25
  referenceRoot?: string;
24
26
  }
@@ -62,9 +64,9 @@ function binary(name: string, path = process.env.PATH ?? ''): string | undefined
62
64
  return undefined;
63
65
  }
64
66
 
65
- function run(bin: string, args: string[], dir: string, timeout: number): ReturnType<typeof spawnSync> {
67
+ function run(bin: string, args: string[], dir: string, timeout: number, cacheDir = dir): ReturnType<typeof spawnSync> {
66
68
  return spawnSync(bin, args, { cwd: dir, encoding: 'utf8', timeout, maxBuffer: 20 * 1024 * 1024,
67
- env: { ...process.env, HOME: dir, XDG_CACHE_HOME: dir, XDG_CONFIG_HOME: dir, UV_CACHE_DIR: dir, PYTHONDONTWRITEBYTECODE: '1' } });
69
+ env: { ...process.env, HOME: dir, XDG_CACHE_HOME: dir, XDG_CONFIG_HOME: dir, UV_CACHE_DIR: cacheDir, PYTHONDONTWRITEBYTECODE: '1' } });
68
70
  }
69
71
 
70
72
  function readInputs(dir: string): Input[] {
@@ -83,7 +85,7 @@ function counts(dir: string): Counts {
83
85
  return { inputs, diagrams, rendered, html: existsSync(join(dir, 'notes.html')) ? 'yes' : 'no', pdf: existsSync(join(dir, 'notes.pdf')) ? 'yes' : 'skipped' };
84
86
  }
85
87
 
86
- export function collect(folder: string, options: StageOptions = {}): StageResult {
88
+ export async function collect(folder: string, options: StageOptions = {}): Promise<StageResult> {
87
89
  const dir = outputDir(folder);
88
90
  const files: Input[] = [];
89
91
  const pdfText = binary('pdftotext', options.toolsPath);
@@ -94,11 +96,22 @@ export function collect(folder: string, options: StageOptions = {}): StageResult
94
96
  const kind = kinds[extname(entry.name).toLowerCase()];
95
97
  if (!entry.isFile() || !kind) continue;
96
98
  const at = join(base, entry.name);
97
- const text = kind === 'markdown' ? readFileSync(at, 'utf8') : kind === 'image' ? '이미지 — 시각 자료 (텍스트 추출 없음)' :
98
- pdfText ? (() => {
99
- const result = run(pdfText, ['-layout', at, '-'], dir, 60000);
100
- return result.status === 0 ? String(result.stdout) : 'PDF 텍스트 없음 — pdftotext 추출 실패';
101
- })() : 'PDF 텍스트 없음 — pdftotext 없음';
99
+ let text: string;
100
+ if (kind === 'markdown') text = readFileSync(at, 'utf8');
101
+ else if (kind === 'image') {
102
+ try {
103
+ const extracted = await (options.ocr ?? runOcr)(at);
104
+ if (!extracted.trim()) throw new Error('Upstage/OCR.space 폴백 결과가 비었다');
105
+ text = extracted;
106
+ } catch (error) {
107
+ const reason = error instanceof Error ? error.message : String(error);
108
+ text = `이미지 텍스트 못 읽음 · ${reason}`;
109
+ debug.log('lecture-note.collect', 'ocr-failed', { name: prefix + entry.name, reason });
110
+ }
111
+ } else if (pdfText) {
112
+ const result = run(pdfText, ['-layout', at, '-'], dir, 60000);
113
+ text = result.status === 0 ? String(result.stdout) : 'PDF 텍스트 없음 — pdftotext 추출 실패';
114
+ } else text = 'PDF 텍스트 없음 — pdftotext 없음';
102
115
  files.push({ name: prefix + entry.name, kind, text });
103
116
  }
104
117
  writeFileSync(artifact(dir, 'inputs.json'), JSON.stringify(files, null, 2) + '\n');
@@ -131,19 +144,26 @@ export function render(folder: string, options: StageOptions = {}): StageResult
131
144
  const renderer = join(options.referenceRoot ?? ownRoot, 'diagram-master/references/engines/mermaid/render_mermaid.py');
132
145
  const uv = binary('uv', options.toolsPath);
133
146
  let unavailable = 0;
147
+ let failed = 0;
134
148
  for (const [i, match] of [...notes.matchAll(mermaid)].entries()) {
135
149
  const png = artifact(dir, `diagram-${i + 1}.png`);
136
150
  if (existsSync(png)) rmSync(png);
137
151
  if (!uv || !existsSync(renderer)) { unavailable++; continue; }
138
152
  const mmd = artifact(dir, `diagram-${i + 1}.mmd`);
139
153
  writeFileSync(mmd, match[1]!);
140
- const result = run(uv, ['run', 'python', renderer, mmd, '--output', png], dir, 120000);
154
+ const cacheDir = join(process.env.ELANOUS_STATE_DIR ?? join(homedir(), '.elanous'), 'lecture-note', 'uv-cache');
155
+ const result = run(uv, ['run', 'python', renderer, mmd, '--output', png], dir, 120000, cacheDir);
141
156
  if (result.status !== 0 || !existsSync(png)) {
142
157
  if (existsSync(png)) rmSync(png);
143
- throw new Error(`Mermaid 렌더 실패 (diagram-${i + 1}, exit=${result.status}): ${result.error?.message ?? result.stderr ?? result.stdout ?? 'PNG 없음'}`);
158
+ failed++;
159
+ const detail = String(result.stderr ?? '').split(/\r?\n/)[0]?.trim() || result.error?.message ||
160
+ String(result.stdout ?? '').split(/\r?\n/)[0]?.trim() || 'PNG 없음';
161
+ const reason = `Mermaid 렌더 실패 (diagram-${i + 1}, exit=${result.status}): ${detail}`;
162
+ debug.log('lecture-note.render', 'diagram-failed', { name: `diagram-${i + 1}`, reason });
144
163
  }
145
164
  }
146
165
  if (unavailable) console.log(`렌더러 없음: ${unavailable}`);
166
+ if (failed) console.log(`못 그림: ${failed}`);
147
167
  return { ...counts(dir), outcome: 'ok' };
148
168
  }
149
169
 
@@ -216,7 +236,7 @@ export function pdf(folder: string, options: StageOptions = {}): StageResult {
216
236
  }
217
237
 
218
238
  export async function runStage(stage: Stage, folder: string, options: StageOptions = {}): Promise<StageResult> {
219
- const result = stage === 'collect' ? collect(folder, options) : stage === 'draft' ? await draft(folder, options) :
239
+ const result = stage === 'collect' ? await collect(folder, options) : stage === 'draft' ? await draft(folder, options) :
220
240
  stage === 'render' ? render(folder, options) : stage === 'html' ? html(folder, options) : pdf(folder, options);
221
241
  debug.log('lecture.note', stage, { inputs: result.inputs, diagrams: result.diagrams, rendered: result.rendered, pdf: result.pdf });
222
242
  return result;
@@ -29,14 +29,41 @@ function inlineText(value: string): string {
29
29
  www ? 'w\u200bww.' : `${scheme ?? at}\u200b${slashes ?? ''}`);
30
30
  }
31
31
 
32
+ function displayId(id: string): string {
33
+ return id.replace(/(-(?:\d{4}-\d{2}-\d{2}|undated))-[a-f\d]{64}$/i, '$1');
34
+ }
35
+
36
+ function summaryText(value: string): string {
37
+ // 원장 적재 때 줄바꿈이 사라져 «| a | b | |---|---| | 1 | 2 |» 처럼 한 줄로 뭉개진 표는 행 경계 «| |»에서 다시 줄로 편다.
38
+ const lines = value.split(/\r?\n/)
39
+ .flatMap(line => /\|\s*:?-{3,}:?\s*\|/.test(line) && /\|\s+\|/.test(line) ? line.replace(/\|\s+\|/g, '|\n|').split('\n') : [line])
40
+ .map(line => line.trim()).filter(Boolean);
41
+ const cells = (line: string) => line.replace(/^\||\|$/g, '').split('|').map(cell => cell.trim());
42
+ const findTable = () => lines.findIndex((line, index) => {
43
+ if (!line.includes('|') || !lines[index + 1]) return false;
44
+ const separator = cells(lines[index + 1]);
45
+ return separator.length > 1 && separator.every(cell => /^:?-{3,}:?$/.test(cell));
46
+ });
47
+ // 표가 여럿이면 모두 «첫 데이터 행 한 줄»로 접는다 — 뒤 표의 구분선·이스케이프가 남지 않게.
48
+ for (let tableAt = findTable(); tableAt >= 0; tableAt = findTable()) {
49
+ const row = lines[tableAt + 2]?.includes('|') ? lines[tableAt + 2] : lines[tableAt];
50
+ const summary = cells(row).join(' · ');
51
+ let after = tableAt + 2;
52
+ while (after < lines.length && lines[after].includes('|')) after++;
53
+ lines.splice(tableAt, after - tableAt, summary);
54
+ }
55
+ const text = lines.join(' ').replace(/\*\*|__/g, '').replace(/\s+/g, ' ').trim();
56
+ return inlineText(text.length > 200 ? `${text.slice(0, 200)}…` : text);
57
+ }
58
+
32
59
  function section(title: string, rows: LessonRow[], ledger: LessonLedger, kind: LessonStatus): string[] {
33
60
  const lines = [`## ${title}`, ''];
34
61
  if (!rows.length) return [...lines, '없음', ''];
35
62
  for (const row of rows) {
36
63
  const last = ledger.get(row.id).occurrences.at(-1);
37
- lines.push(`### ${inlineText(row.id)} — ${inlineText(row.incident)}`,
38
- `- 원인: ${inlineText(row.cause)}`,
39
- `- 처방: ${inlineText(row.remedy)}`,
64
+ lines.push(`### ${inlineText(displayId(row.id))} — ${inlineText(row.incident)}`,
65
+ `- 원인: ${summaryText(row.cause)}`,
66
+ `- 처방: ${summaryText(row.remedy)}`,
40
67
  `- 재발: ${row.occurrence_count}회`,
41
68
  `- 마지막 발생: ${inlineText(last?.at ?? '없음')} · 출처: ${inlineText(last?.source ?? '없음')}`);
42
69
  if (kind === 'candidate') lines.push(`- 반증: ${row.disproof?.trim() ? inlineText(row.disproof) : '반증 없음 — 승격 불가'}`);