elanous 0.2.14 → 0.2.16

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 (350) 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/chunks/{1307-6e7e0377c1db0bdc.js → 1307-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-482c5dbde699947c.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-8c2d9526164f5d09.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/control/{page-e6b84ab07d3567d5.js → page-a0e2a6d1bfb52fc5.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c53598cfce6156f2.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-13a63953f53c6a23.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-5d3c8429d04f3831.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-3193bdc0bbab047a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-6107ca8ddd5e3bdd.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-dce686e582c23937.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/{page-160089123a40dd91.js → page-1a61c7ca1ceb3ae7.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-51f811fa119308b6.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-4167348049166852.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-de52e8fdf9095f7a.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-ca3952cd78a83ca2.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-e7e10cd3386222e5.js → page-5b042736aa113756.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-1ebfe049c640e171.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-57924393e3e4e539.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-0f5cd57ca928f42b.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-d28573d6792b4275.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-58043db78ba519ae.js → page-59f03fda76c03d22.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-357750dfb91bf960.js → page-d751de5f8062ff5e.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-fe1759f94003b32b.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-9afdee66b820c76e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-f8da5dc6f3752276.js → page-1ea64acadd704ace.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-3544f154c87b43ee.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  53. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  54. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  55. package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +1 -0
  56. package/apps/pwa/out/approvals/index.html +1 -1
  57. package/apps/pwa/out/approvals/index.txt +15 -15
  58. package/apps/pwa/out/autopilot/index.html +1 -1
  59. package/apps/pwa/out/autopilot/index.txt +15 -15
  60. package/apps/pwa/out/board/index.html +1 -1
  61. package/apps/pwa/out/board/index.txt +15 -15
  62. package/apps/pwa/out/botlab/index.html +1 -1
  63. package/apps/pwa/out/botlab/index.txt +15 -15
  64. package/apps/pwa/out/bots/index.html +1 -1
  65. package/apps/pwa/out/bots/index.txt +15 -15
  66. package/apps/pwa/out/ceo/index.html +1 -0
  67. package/apps/pwa/out/ceo/index.txt +33 -0
  68. package/apps/pwa/out/chat/index.html +1 -1
  69. package/apps/pwa/out/chat/index.txt +15 -15
  70. package/apps/pwa/out/consult/index.html +1 -1
  71. package/apps/pwa/out/consult/index.txt +15 -15
  72. package/apps/pwa/out/control/index.html +1 -1
  73. package/apps/pwa/out/control/index.txt +15 -15
  74. package/apps/pwa/out/design-check/index.html +1 -1
  75. package/apps/pwa/out/design-check/index.txt +15 -15
  76. package/apps/pwa/out/editor/index.html +1 -1
  77. package/apps/pwa/out/editor/index.txt +15 -15
  78. package/apps/pwa/out/exec/index.html +1 -1
  79. package/apps/pwa/out/exec/index.txt +15 -15
  80. package/apps/pwa/out/field/index.html +1 -1
  81. package/apps/pwa/out/field/index.txt +15 -15
  82. package/apps/pwa/out/healthz +1 -1
  83. package/apps/pwa/out/index.html +1 -1
  84. package/apps/pwa/out/index.txt +15 -15
  85. package/apps/pwa/out/inside/index.html +1 -1
  86. package/apps/pwa/out/inside/index.txt +15 -15
  87. package/apps/pwa/out/intake/index.html +1 -1
  88. package/apps/pwa/out/intake/index.txt +15 -15
  89. package/apps/pwa/out/live/index.html +1 -1
  90. package/apps/pwa/out/live/index.txt +15 -15
  91. package/apps/pwa/out/loops/index.html +1 -1
  92. package/apps/pwa/out/loops/index.txt +15 -15
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +15 -15
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +15 -15
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +15 -15
  101. package/apps/pwa/out/ops/checklist/index.html +1 -1
  102. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  103. package/apps/pwa/out/ops/release/index.html +1 -1
  104. package/apps/pwa/out/ops/release/index.txt +15 -15
  105. package/apps/pwa/out/ops/seats/index.html +1 -1
  106. package/apps/pwa/out/ops/seats/index.txt +15 -15
  107. package/apps/pwa/out/outputs/index.html +1 -1
  108. package/apps/pwa/out/outputs/index.txt +15 -15
  109. package/apps/pwa/out/reflection/index.html +1 -1
  110. package/apps/pwa/out/reflection/index.txt +16 -16
  111. package/apps/pwa/out/scheduler/index.html +1 -1
  112. package/apps/pwa/out/scheduler/index.txt +15 -15
  113. package/apps/pwa/out/sessions/index.html +1 -1
  114. package/apps/pwa/out/sessions/index.txt +15 -15
  115. package/apps/pwa/out/settings/devices/index.html +1 -1
  116. package/apps/pwa/out/settings/devices/index.txt +15 -15
  117. package/apps/pwa/out/settings/index.html +1 -1
  118. package/apps/pwa/out/settings/index.txt +15 -15
  119. package/apps/pwa/out/setup/done/index.html +1 -1
  120. package/apps/pwa/out/setup/done/index.txt +15 -15
  121. package/apps/pwa/out/setup/index.html +1 -1
  122. package/apps/pwa/out/setup/index.txt +15 -15
  123. package/apps/pwa/out/share/index.html +1 -1
  124. package/apps/pwa/out/share/index.txt +15 -15
  125. package/apps/pwa/out/showroom/index.html +1 -1
  126. package/apps/pwa/out/showroom/index.txt +15 -15
  127. package/apps/pwa/out/tasks/index.html +1 -1
  128. package/apps/pwa/out/tasks/index.txt +15 -15
  129. package/apps/pwa/out/term/index.html +1 -1
  130. package/apps/pwa/out/term/index.txt +15 -15
  131. package/apps/pwa/out/today/index.html +1 -1
  132. package/apps/pwa/out/today/index.txt +15 -15
  133. package/apps/pwa/out/trace/index.html +1 -1
  134. package/apps/pwa/out/trace/index.txt +15 -15
  135. package/apps/pwa/out/vault/index.html +1 -1
  136. package/apps/pwa/out/vault/index.txt +15 -15
  137. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  138. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  139. package/apps/pwa/out/workflows/index.html +1 -1
  140. package/apps/pwa/out/workflows/index.txt +15 -15
  141. package/apps/pwa/out/workspace/index.html +1 -1
  142. package/apps/pwa/out/workspace/index.txt +15 -15
  143. package/apps/pwa/out/worktrees/index.html +1 -1
  144. package/apps/pwa/out/worktrees/index.txt +15 -15
  145. package/graphs/doc-gen/doc-gen.yaml +24 -0
  146. package/graphs/doc-gen/recipes.yaml +3 -0
  147. package/graphs/heal/heal-loop.yaml +1 -1
  148. package/graphs/implement-loop.yaml +1 -1
  149. package/graphs/orchestrator/orchestrator.yaml +7 -0
  150. package/graphs/orchestrator/recipes.yaml +3 -0
  151. package/graphs/release/recipes.yaml +3 -0
  152. package/graphs/release/release-loop.yaml +4 -2
  153. package/graphs/steward/recipes.yaml +2 -1
  154. package/package.json +1 -1
  155. package/scripts/brand/check.ts +7 -5
  156. package/scripts/ci-public-export-import-gate.ts +154 -0
  157. package/scripts/ci-test-interference-gate.ts +5 -1
  158. package/scripts/ci-typecheck-changed.ts +82 -31
  159. package/scripts/docs/doc-rot.ts +164 -0
  160. package/scripts/docs/generate-graph-anatomy.ts +437 -0
  161. package/scripts/docs-cli-check.ts +4 -4
  162. package/scripts/expose-rubric-suggest.ts +18 -12
  163. package/scripts/goals/goal-census.ts +343 -0
  164. package/scripts/lecture-note/node.ts +31 -11
  165. package/scripts/lessons/handbook-page.ts +30 -3
  166. package/scripts/marketing/generate-ax-brief.ts +140 -0
  167. package/scripts/marketing/generate-feature-map.ts +118 -0
  168. package/scripts/measure/author-section-contribution.ts +84 -0
  169. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  170. package/scripts/release-loop/cut-branch.ts +43 -13
  171. package/scripts/release-loop/gate-node.ts +82 -14
  172. package/scripts/release-loop/vault-note-node.ts +257 -8
  173. package/scripts/release-story/draft.ts +57 -9
  174. package/scripts/rhythm/daily.ts +263 -76
  175. package/scripts/rhythm/monthly.ts +97 -0
  176. package/scripts/rhythm/weekly.ts +30 -9
  177. package/scripts/smart-merge/resync-shadow.ts +147 -0
  178. package/src/acp/client.ts +17 -2
  179. package/src/agent-env/profile.ts +161 -0
  180. package/src/agent-mission/driver.ts +51 -1
  181. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  182. package/src/agent-mission/supervisor-metrics.ts +218 -0
  183. package/src/agent-substrate/pr-reviewer.ts +2 -0
  184. package/src/ask-user-question/pending-questions.ts +5 -0
  185. package/src/auth/github-app-token.ts +112 -13
  186. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  187. package/src/autopilot/track-agent-shadow.ts +2 -1
  188. package/src/briefing/brief-items.ts +276 -0
  189. package/src/budget/usage-outcomes.ts +204 -0
  190. package/src/claims/claims-render.ts +14 -5
  191. package/src/cli/agent-env-cli.ts +21 -0
  192. package/src/cli/brief-cli.ts +73 -0
  193. package/src/cli/decisions-cli.ts +9 -0
  194. package/src/cli/doctor-cli.ts +23 -1
  195. package/src/cli/doctor-node-modules.ts +147 -0
  196. package/src/cli/intake-cli.ts +46 -17
  197. package/src/cli/know-cli.ts +20 -0
  198. package/src/cli/logs-cli.ts +43 -1
  199. package/src/cli/pod-cli.ts +2 -0
  200. package/src/cli/pr-cli.ts +80 -12
  201. package/src/cli/release-cli.ts +66 -29
  202. package/src/cli/seat-requests-cli.ts +52 -0
  203. package/src/cli/self-update.ts +200 -1
  204. package/src/cli/usage-cli.ts +26 -0
  205. package/src/context-bus/context-now-public.ts +16 -6
  206. package/src/context-bus/context-now.ts +3 -1
  207. package/src/context-bus/coord-events.ts +6 -4
  208. package/src/decisions/decision-cards.ts +33 -7
  209. package/src/decisions/proact-meter.ts +286 -0
  210. package/src/decisions/proact1-lite.ts +86 -0
  211. package/src/decisions/web-push-decision-cards.ts +1 -1
  212. package/src/discord-self-message.ts +9 -0
  213. package/src/domains/alert-audit.ts +271 -0
  214. package/src/domains/core-tools.ts +4 -3
  215. package/src/domains/ops-facts-tool.ts +31 -1
  216. package/src/flow-loop/split.ts +24 -0
  217. package/src/harness/harness-cli-command.ts +147 -17
  218. package/src/harness/harness-incidents.ts +2 -0
  219. package/src/harness/harness-pod-dispatch.ts +67 -8
  220. package/src/harness/harness-queue.ts +380 -50
  221. package/src/harness/harness-seams.ts +13 -3
  222. package/src/harness/helper-repair.ts +218 -0
  223. package/src/harness/helper-scan.ts +27 -4
  224. package/src/harness/launch-stamp.ts +236 -0
  225. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  226. package/src/harness/ledger-write-guard.ts +35 -0
  227. package/src/harness/nested-elanous-policy.ts +114 -0
  228. package/src/harness/pod-memory-policy.ts +15 -0
  229. package/src/harness/repository-name.ts +55 -0
  230. package/src/hq/fence-audit.ts +90 -0
  231. package/src/hq/hq.ts +70 -6
  232. package/src/hq/lease.ts +9 -7
  233. package/src/hq/move-plan.ts +126 -0
  234. package/src/hq/promote.ts +31 -0
  235. package/src/hq/transfer.ts +185 -0
  236. package/src/index.ts +189 -13
  237. package/src/intake-plane/check.ts +63 -0
  238. package/src/intake-plane/digest.ts +65 -21
  239. package/src/intake-plane/front-classifier.ts +6 -0
  240. package/src/intake-plane/items.ts +3 -2
  241. package/src/intake-plane/lens.ts +177 -0
  242. package/src/intake-plane/route.ts +145 -1
  243. package/src/knowledge/know-find.ts +105 -0
  244. package/src/llm.ts +75 -19
  245. package/src/loops/budget.ts +36 -5
  246. package/src/loops/checker.ts +2 -2
  247. package/src/loops/orchestrator/author-depth.ts +65 -9
  248. package/src/loops/orchestrator/author-input-check.ts +8 -3
  249. package/src/loops/orchestrator/author-ledger.ts +3 -1
  250. package/src/loops/orchestrator/finish-rate.ts +14 -3
  251. package/src/loops/orchestrator/seat-share.ts +56 -0
  252. package/src/loops/orchestrator/tick.ts +107 -29
  253. package/src/loops/orchestrator/traffic.ts +117 -11
  254. package/src/loops/registry.ts +142 -61
  255. package/src/maturity/bot-slash-catalog.ts +11 -2
  256. package/src/maturity/feature-maturity.ts +4 -3
  257. package/src/maturity/tui-slash-availability.ts +40 -0
  258. package/src/mss/logging/log-store.ts +35 -4
  259. package/src/nexus/api/http-server.ts +28 -14
  260. package/src/nexus/api/loop-edges.ts +173 -0
  261. package/src/nexus/api/schedules-read.ts +13 -0
  262. package/src/nexus/api/seat-requests.ts +79 -56
  263. package/src/oauth/overload-failover-launch.ts +75 -0
  264. package/src/oauth/overload-failover.ts +137 -0
  265. package/src/prompt-enhance/enhance.ts +6 -1
  266. package/src/release-loop/auto-start.ts +6 -3
  267. package/src/release-loop/checklist.ts +19 -5
  268. package/src/release-loop/feature-store.ts +59 -31
  269. package/src/release-loop/merged-pr-checklist.ts +125 -0
  270. package/src/release-loop/placement.ts +98 -29
  271. package/src/seat-dispatch/seat-ask.ts +33 -6
  272. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  273. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  274. package/src/seat-loop/seat-loop.ts +215 -12
  275. package/src/self-dev/ask-launch-flow.ts +22 -1
  276. package/src/self-dev/authored-goal-trace.ts +64 -0
  277. package/src/self-dev/dev-cli.ts +19 -1
  278. package/src/self-dev/dev-pipeline.ts +14 -9
  279. package/src/self-dev/draft-sweep.ts +90 -10
  280. package/src/self-dev/draft-triage-rules.ts +25 -8
  281. package/src/self-dev/entrance-registry.ts +10 -6
  282. package/src/self-dev/old-door.ts +73 -0
  283. package/src/self-dev/orchestrate.ts +15 -1
  284. package/src/self-dev/review-provider-fallback.ts +6 -1
  285. package/src/self-dev/run-supervisor.ts +49 -2
  286. package/src/self-implement/app-api-budget.ts +166 -0
  287. package/src/self-implement/budget-gate.ts +7 -5
  288. package/src/self-implement/follow-up-goals.ts +289 -0
  289. package/src/self-implement/frozen-merges.ts +3 -2
  290. package/src/self-implement/goal-author.ts +50 -6
  291. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  292. package/src/self-implement/heal-intake.ts +120 -2
  293. package/src/self-implement/heal-triage.ts +44 -0
  294. package/src/self-implement/host-regate.ts +9 -2
  295. package/src/self-implement/l8-merge-queue.ts +132 -0
  296. package/src/self-implement/orchestrator.ts +325 -47
  297. package/src/self-implement/pipeline-shape.ts +1 -1
  298. package/src/self-implement/revert-guard.ts +42 -7
  299. package/src/self-implement/run-ledger.ts +6 -0
  300. package/src/self-implement/run-status-mapping.ts +2 -0
  301. package/src/self-implement/seams.ts +45 -16
  302. package/src/self-implement/self-implement-cli.ts +1 -0
  303. package/src/self-implement/speed-claims.ts +338 -0
  304. package/src/session-runtime/index.ts +2 -0
  305. package/src/session-runtime/retry-api.ts +87 -0
  306. package/src/session-runtime/retry-policy.ts +107 -6
  307. package/src/steward/triage.ts +74 -3
  308. package/src/steward/working-backwards.ts +20 -2
  309. package/src/task-cards/card-cli.ts +25 -0
  310. package/src/task-cards/card-store.ts +5 -3
  311. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  312. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  313. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  314. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  315. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  316. package/src/task-orchestrator/surfaces/pod-pool.ts +70 -9
  317. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  318. package/src/task-orchestrator/surfaces/self-implement-pod.ts +166 -46
  319. package/src/task-orchestrator/surfaces/self-implement.ts +7 -1
  320. package/src/telegram-commands.ts +5 -0
  321. package/src/telegram.ts +72 -4
  322. package/src/user-config.ts +64 -7
  323. package/src/version/packed-revision.json +1 -1
  324. package/src/workflow-runtime/nodes/bash.ts +9 -1
  325. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  349. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  350. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Public export import guard — a file the public export includes must not statically import
3
+ * a module the export excludes. 0.2.15 shipped that break: src/decisions/proact-meter.ts
4
+ * imported src/directives/directive-index (exclude `src/directives/directive-index*`) and the
5
+ * export-check PWA build failed.
6
+ *
7
+ * bun scripts/ci-public-export-import-gate.ts --changed-files a b … # exit 1 on a hit · 0 when clean
8
+ *
9
+ * Only string-literal specifiers are resolved. Non-literal specifiers (createRequire with a
10
+ * concatenated string, and the like) are counted as unseen — this check cannot see them.
11
+ * The public PWA build itself stays on the release export-check run; it is not run here.
12
+ */
13
+ import { spawnSync } from 'node:child_process';
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+ import { join, posix, resolve } from 'node:path';
16
+ type ExportSelectors = {
17
+ loadExportConfig: (root: string, file?: string) => { readonly include: readonly string[]; readonly exclude: readonly string[]; readonly replace: Readonly<Record<string, string>> };
18
+ selectExportFiles: (tracked: readonly string[], config: { readonly include: readonly string[]; readonly exclude: readonly string[]; readonly replace: Readonly<Record<string, string>> }) => string[];
19
+ };
20
+
21
+ /**
22
+ * scripts/public-export.ts `loadExportConfig` (line 123) and `selectExportFiles` (line 140).
23
+ * Imported on call, not at load: that module imports typescript, and a tree that has not
24
+ * installed it must still be able to load this gate and report why it could not measure.
25
+ */
26
+ async function exportSelectors(): Promise<ExportSelectors> {
27
+ return import('./public-export.js');
28
+ }
29
+
30
+ export interface ExportImportHit {
31
+ readonly file: string;
32
+ readonly line: number;
33
+ readonly target: string;
34
+ }
35
+
36
+ export interface ExportImportCheck {
37
+ readonly measured: boolean;
38
+ readonly hits: readonly ExportImportHit[];
39
+ readonly unseen: number;
40
+ readonly detail?: string;
41
+ }
42
+
43
+ const CODE_FILE = /\.(?:[cm]?[jt]sx?)$/;
44
+ const FROM_CLAUSE = /\b(?:import|export)\s+(?:type\s+)?(?:[^'"\n;]*?\s+from\s+)?(['"])([^'"\n]+)\1/g;
45
+ const DYNAMIC_IMPORT = /\bimport\s*\(\s*(['"])([^'"\n]+)\1/g;
46
+ const NON_LITERAL_IMPORT = /\b(?:import|require|createRequire)\s*\(\s*(?!['"`])/g;
47
+
48
+ function trackedFiles(root: string): string[] | undefined {
49
+ const run = spawnSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf-8', timeout: 60_000, maxBuffer: 64 * 1024 * 1024 });
50
+ if (run.status !== 0) return undefined;
51
+ return String(run.stdout).split('\0').filter(Boolean);
52
+ }
53
+
54
+ /** Specifier resolved against the importer's directory, then mapped onto a repo-relative path. */
55
+ function resolveSpecifier(importer: string, specifier: string): string | undefined {
56
+ if (!specifier.startsWith('.')) return undefined;
57
+ const raw = posix.normalize(posix.join(posix.dirname(importer), specifier));
58
+ return raw.startsWith('../') ? undefined : raw;
59
+ }
60
+
61
+ function candidatePaths(resolved: string): string[] {
62
+ const ext = posix.extname(resolved);
63
+ const bases = ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs'
64
+ ? [resolved.slice(0, -ext.length) + '.ts', resolved.slice(0, -ext.length) + '.tsx', resolved]
65
+ : [resolved];
66
+ const out: string[] = [];
67
+ for (const base of bases) {
68
+ out.push(base);
69
+ if (!posix.extname(base)) {
70
+ out.push(`${base}.ts`, `${base}.tsx`, `${base}.js`, `${base}.mjs`);
71
+ out.push(posix.join(base, 'index.ts'), posix.join(base, 'index.tsx'), posix.join(base, 'index.js'));
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+
77
+ function lineOf(text: string, index: number): number {
78
+ let line = 1;
79
+ for (let i = 0; i < index && i < text.length; i++) if (text[i] === '\n') line++;
80
+ return line;
81
+ }
82
+
83
+ /**
84
+ * Changed files that the public manifest includes, scanned for a literal import of a tracked
85
+ * module that the same manifest leaves out. `unseen` counts non-literal import/require calls.
86
+ */
87
+ export async function exportImportCheck(changedFiles: readonly string[], root: string): Promise<ExportImportCheck> {
88
+ const tracked = trackedFiles(root);
89
+ if (!tracked) return { measured: false, hits: [], unseen: 0, detail: 'git ls-files 실패' };
90
+ let included: Set<string>;
91
+ try {
92
+ const { loadExportConfig, selectExportFiles } = await exportSelectors();
93
+ included = new Set(selectExportFiles(tracked, loadExportConfig(root)));
94
+ } catch (error) {
95
+ return { measured: false, hits: [], unseen: 0, detail: error instanceof Error ? error.message : '매니페스트를 못 읽었다' };
96
+ }
97
+ const trackedSet = new Set(tracked);
98
+ const hits: ExportImportHit[] = [];
99
+ let unseen = 0;
100
+ for (const file of changedFiles) {
101
+ if (!included.has(file) || !CODE_FILE.test(file)) continue;
102
+ const abs = join(root, file);
103
+ if (!existsSync(abs)) continue;
104
+ let text: string;
105
+ try { text = readFileSync(abs, 'utf8'); } catch { continue; }
106
+ const seen = new Set<string>();
107
+ const consider = (index: number, specifier: string) => {
108
+ const resolvedSpec = resolveSpecifier(file, specifier);
109
+ if (!resolvedSpec) return;
110
+ const target = candidatePaths(resolvedSpec).find((candidate) => trackedSet.has(candidate));
111
+ if (!target || included.has(target)) return;
112
+ const key = `${index}\0${target}`;
113
+ if (seen.has(key)) return;
114
+ seen.add(key);
115
+ hits.push({ file, line: lineOf(text, index), target });
116
+ };
117
+ for (const pattern of [FROM_CLAUSE, DYNAMIC_IMPORT]) {
118
+ pattern.lastIndex = 0;
119
+ for (const match of text.matchAll(pattern)) consider(match.index ?? 0, match[2] ?? '');
120
+ }
121
+ NON_LITERAL_IMPORT.lastIndex = 0;
122
+ unseen += [...text.matchAll(NON_LITERAL_IMPORT)].length;
123
+ }
124
+ return { measured: true, hits, unseen };
125
+ }
126
+
127
+ /** Gate form: 1 on a hit, 0 when clean. An unmeasured check blocks only when a changed file is included. */
128
+ export async function runPublicExportImportGate(io: { args: readonly string[]; cwd: string; log: (line: string) => void; error: (line: string) => void }): Promise<number> {
129
+ const at = io.args.indexOf('--changed-files');
130
+ const files = at >= 0 ? io.args.slice(at + 1).filter((arg) => !arg.startsWith('--')) : [];
131
+ if (files.length === 0) return 0;
132
+ let result: ExportImportCheck;
133
+ try { result = await exportImportCheck(files, io.cwd); }
134
+ catch { result = { measured: false, hits: [], unseen: 0, detail: '검사 실행 실패' }; }
135
+ if (!result.measured) {
136
+ let publicFiles = files;
137
+ try {
138
+ const { loadExportConfig, selectExportFiles } = await exportSelectors();
139
+ publicFiles = selectExportFiles(files, loadExportConfig(io.cwd));
140
+ } catch { publicFiles = files; }
141
+ if (publicFiles.length === 0) { io.log('⚠ public-export-import: 못 쟀다 — 공개 대상 변경 없음'); return 0; }
142
+ io.error(`✗ public-export-import: 못 쟀다 — 막는다(${result.detail ?? '?'})`);
143
+ return 1;
144
+ }
145
+ if (result.unseen > 0) io.log(`⚠ public-export-import: 비리터럴 지정자 ${result.unseen}곳은 못 봄`);
146
+ if (result.hits.length === 0) return 0;
147
+ io.error(`✗ public-export-import: 공개 파일이 공개 내보내기에서 제외된 모듈을 import 한다 (${result.hits.length}곳) — 공개본 빌드가 여기서 깨진다:`);
148
+ for (const hit of result.hits) io.error(` ${hit.file}:${hit.line} → ${hit.target}`);
149
+ return 1;
150
+ }
151
+
152
+ if (import.meta.main) {
153
+ process.exit(await runPublicExportImportGate({ args: process.argv.slice(2), cwd: resolve(import.meta.dir, '..'), log: console.log, error: console.error }));
154
+ }
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  detectTestInterference,
4
4
  formatIsolatedFailureSummary,
5
+ runBunTest,
5
6
  type InterferenceReport,
6
7
  type TestRunner,
7
8
  } from './detect-test-interference.js';
@@ -11,6 +12,8 @@ const TEST_FILE = /\.test\.tsx?$/;
11
12
 
12
13
  export type TestInterferenceGateIo = {
13
14
  args?: readonly string[];
15
+ /** Landing tree. `bun test` of the changed files runs here, not in process.cwd(). */
16
+ cwd?: string;
14
17
  log?: (message: string) => void;
15
18
  detect?: (files: readonly string[], runner?: TestRunner) => Promise<InterferenceReport>;
16
19
  runner?: TestRunner;
@@ -36,6 +39,7 @@ export function selectedTestFiles(files: readonly string[]): string[] {
36
39
  export async function runTestInterferenceGate(io: TestInterferenceGateIo = {}): Promise<number> {
37
40
  const args = io.args ?? process.argv.slice(2);
38
41
  const log = io.log ?? console.log;
42
+ const cwd = io.cwd ?? process.cwd();
39
43
  const changed = parseChangedFiles(args);
40
44
  const tests = selectedTestFiles(changed ?? []);
41
45
  if (tests.length < 2) {
@@ -48,7 +52,7 @@ export async function runTestInterferenceGate(io: TestInterferenceGateIo = {}):
48
52
  const capNotice = omitted > 0 ? ` · 상한 ${MAX_INSPECTED_TEST_FILES}개 적용, ${omitted}개 미검사.` : '';
49
53
  let report: InterferenceReport;
50
54
  try {
51
- report = await (io.detect ?? detectTestInterference)(inspected, io.runner);
55
+ report = await (io.detect ?? detectTestInterference)(inspected, io.runner ?? ((files) => runBunTest(files, undefined, cwd)));
52
56
  } catch {
53
57
  log(`[test-interference-gate] 경고: 측정 불가 — 판정기 실행 실패, 간섭 없음으로 처리하지 않음.${capNotice}`);
54
58
  return 0;
@@ -15,9 +15,18 @@ import {
15
15
  import { debug } from '../src/debug/log.js';
16
16
  import { tscEnv } from '../src/typecheck-ratchet.js';
17
17
 
18
+ /** Inherited GIT_DIR / GIT_WORK_TREE would make `--cwd <other tree>` still read the launcher's repo. */
19
+ function gitEnv(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
20
+ const env = { ...base };
21
+ delete env.GIT_DIR;
22
+ delete env.GIT_WORK_TREE;
23
+ delete env.GIT_COMMON_DIR;
24
+ return env;
25
+ }
26
+
18
27
  /** git 조회용 — 명령 실패는 호출자가 빈 출력과 구분할 수 있게 전파한다. */
19
- function sh(cmd: string): string {
20
- return execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
28
+ function sh(cmd: string, cwd = process.cwd()): string {
29
+ return execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], cwd, env: gitEnv() });
21
30
  }
22
31
 
23
32
  export type TscRun = { out: string; ran: boolean; why?: string };
@@ -53,16 +62,21 @@ export type ChangedTsFiles = {
53
62
  files: Set<string>;
54
63
  base: string;
55
64
  baseSource: string;
65
+ head?: string;
66
+ diffFiles?: Set<string>;
67
+ warnings?: string[];
56
68
  failures?: { command: string; why: string }[];
57
69
  };
58
70
 
59
- /** 선택한 base와 파일 수를 함께 보존한다. 0개도 base 기준 관측값이다. */
60
- export function changedTsFiles(run = sh, env: NodeJS.ProcessEnv = process.env): ChangedTsFiles {
71
+ /** 선택한 base와 파일 수를 함께 보존한다. 0개도 base 기준 관측값이다.
72
+ * `cwd` 는 착지 트리다 — `pr land --cwd` 가 프로세스 cwd 가 아니라 그 트리를 재게 한다. */
73
+ export function changedTsFiles(run: (cmd: string) => string = sh, env: NodeJS.ProcessEnv = process.env, cwd = process.cwd(), baseRef?: string): ChangedTsFiles {
61
74
  let base: string;
62
75
  let baseSource: string;
63
- if (env.TSC_BASE_REF) {
64
- base = env.TSC_BASE_REF;
65
- baseSource = 'TSC_BASE_REF';
76
+ const pinned = baseRef ?? env.TSC_BASE_REF;
77
+ if (pinned) {
78
+ base = pinned;
79
+ baseSource = baseRef ? 'baseRef' : 'TSC_BASE_REF';
66
80
  } else if (env.GITHUB_BASE_REF) {
67
81
  base = `origin/${env.GITHUB_BASE_REF}`;
68
82
  baseSource = 'GITHUB_BASE_REF';
@@ -78,23 +92,43 @@ export function changedTsFiles(run = sh, env: NodeJS.ProcessEnv = process.env):
78
92
  }
79
93
  }
80
94
 
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
95
  const failures: { command: string; why: string }[] = [];
89
- const addCommand = (command: string) => {
90
- try { add(run(command)); } catch (cause) {
96
+ // Revision reads are observational: an unreadable HEAD/base must not turn into a collection failure.
97
+ const quietRevision = (command: string): string => { try { return run(command).trim(); } catch { return ''; } };
98
+ const head = quietRevision('git rev-parse HEAD') || undefined;
99
+ const warnings: string[] = [];
100
+ if (head && quietRevision(`git rev-parse ${base}`) === head) {
101
+ const main = quietRevision('git rev-parse origin/main');
102
+ const merged = main && main !== head ? quietRevision('git merge-base HEAD origin/main') : '';
103
+ if (merged && merged !== head) {
104
+ warnings.push(`base=${base} 가 HEAD 와 같은 커밋 — origin/main (${main})과 달라 merge-base 로 재계산`);
105
+ base = merged;
106
+ baseSource = 'recomputed merge-base(origin/main)';
107
+ } else if (main && main !== head) {
108
+ failures.push({ command: `git rev-parse ${base}`, why: `base 가 HEAD(${head}) 와 같아 변경을 셀 수 없다 — TSC_BASE_REF 로 base 를 주라 (못 셌음 ≠ 0)` });
109
+ }
110
+ }
111
+
112
+ const files = new Set<string>();
113
+ const diffFiles = new Set<string>();
114
+ const commands = [`git diff --name-only ${base}...HEAD`, 'git diff --name-only HEAD', 'git ls-files --others --exclude-standard'];
115
+ for (const command of commands) {
116
+ try {
117
+ for (const line of run(command).split('\n')) {
118
+ const file = line.trim().replace(/^\.\//, '');
119
+ if (!file) continue;
120
+ const ts = /\.tsx?$/.test(file);
121
+ // A deleted .ts is not a compile candidate, so it must not count as «should have been collected».
122
+ // existsSync is bound to the landing tree — process.cwd() would count the launcher's files.
123
+ const present = existsSync(join(cwd, file));
124
+ if (!ts || present) diffFiles.add(file);
125
+ if (ts && present) files.add(file);
126
+ }
127
+ } catch (cause) {
91
128
  failures.push({ command, why: cause instanceof Error ? cause.message : String(cause) });
92
129
  }
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 };
130
+ }
131
+ return { files, base, baseSource, head, diffFiles, warnings, failures };
98
132
  }
99
133
 
100
134
  type RequiredExportField = { typeName: string; fieldName: string };
@@ -745,7 +779,7 @@ export function readMergeBaseDiagnostics(base: string, command: string, cwd = pr
745
779
  const workspace = mkdtempSync(join(tmpdir(), 'ci-typecheck-baseline-'));
746
780
  const worktree = join(workspace, 'base');
747
781
  try {
748
- execFileSync('git', ['worktree', 'add', '--detach', worktree, base], { cwd, stdio: 'pipe' });
782
+ execFileSync('git', ['worktree', 'add', '--detach', worktree, base], { cwd, stdio: 'pipe', env: gitEnv() });
749
783
  const modules = join(cwd, 'node_modules');
750
784
  if (!existsSync(modules)) return { unavailable: `기준 worktree 의존성을 재사용할 수 없다(${modules} 없음)` };
751
785
  symlinkSync(modules, join(worktree, 'node_modules'));
@@ -759,7 +793,7 @@ export function readMergeBaseDiagnostics(base: string, command: string, cwd = pr
759
793
  } catch (error) {
760
794
  return { unavailable: error instanceof Error ? error.message : String(error) };
761
795
  } finally {
762
- try { execFileSync('git', ['worktree', 'remove', '--force', worktree], { cwd, stdio: 'pipe' }); }
796
+ try { execFileSync('git', ['worktree', 'remove', '--force', worktree], { cwd, stdio: 'pipe', env: gitEnv() }); }
763
797
  catch { /* acquisition failure is rendered by the caller */ }
764
798
  rmSync(workspace, { recursive: true, force: true });
765
799
  }
@@ -771,6 +805,10 @@ export type GateIo = {
771
805
  literalUnionMembers?: (changed: ReadonlySet<string>, base: string) => LiteralUnionMember[] | LiteralUnionMemberCheck;
772
806
  readBaselineDiagnostics?: (base: string, command: string) => BaselineDiagnostics;
773
807
  runTscCmd: (cmd: string) => TscRun;
808
+ /** Landing tree. Git, existsSync, tsc, and the test baseline are measured here — not in process.cwd(). */
809
+ cwd?: string;
810
+ /** Explicit base revision. Same rule as TSC_BASE_REF, without reading the process environment. */
811
+ baseRef?: string;
774
812
  log: (message: string) => void;
775
813
  warn: (message: string) => void;
776
814
  error: (message: string) => void;
@@ -840,8 +878,10 @@ function inspectTscResult(tsc: TscRun, changed: ReadonlySet<string>, baseline: R
840
878
  }
841
879
 
842
880
  export function runGate(io: Partial<GateIo> = {}): void {
843
- const { changedTsFiles: getChanged = changedTsFiles, runTscCmd = (cmd: string) => runTsc(cmd), log = (message: string) => console.log(message), warn = (message: string) => console.warn(message), error = (message: string) => console.error(message), exit = ((code: number) => process.exit(code)) as GateIo['exit'] } = io;
844
- const readBaselineDiagnostics = io.readBaselineDiagnostics ?? ((base: string, command: string) => readMergeBaseDiagnostics(base, command));
881
+ const cwd = io.cwd ?? process.cwd();
882
+ const defaultChanged = () => changedTsFiles((cmd) => sh(cmd, cwd), process.env, cwd, io.baseRef);
883
+ const { changedTsFiles: getChanged = defaultChanged, runTscCmd = (cmd: string) => runTsc(cmd, ((command, options) => execSync(command, { ...options, cwd, env: gitEnv() })) as typeof execSync), log = (message: string) => console.log(message), warn = (message: string) => console.warn(message), error = (message: string) => console.error(message), exit = ((code: number) => process.exit(code)) as GateIo['exit'] } = io;
884
+ const readBaselineDiagnostics = io.readBaselineDiagnostics ?? ((base: string, command: string) => readMergeBaseDiagnostics(base, command, cwd));
845
885
  // Set 주입은 기존 테스트 심이며 기준 revision이 없다. 실제 ChangedTsFiles 경로만 Git 관측을 요구한다.
846
886
  const requiredExportFields = io.requiredExportFields ?? ((changed: ReadonlySet<string>, base: string) => base === 'injected test seam'
847
887
  ? { fields: [], removedSymbols: [], parameterIncreases: [] }
@@ -850,10 +890,13 @@ export function runGate(io: Partial<GateIo> = {}): void {
850
890
  ? { additions: [] }
851
891
  : exportedLiteralUnionMembersInChanges(changed, base));
852
892
  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: [] }
893
+ const { files: changed, base, baseSource, head, diffFiles, warnings = [], failures = [] } = changedResult instanceof Set
894
+ ? { files: changedResult, base: 'injected test seam', baseSource: 'injected test seam', head: 'injected test seam', diffFiles: new Set<string>(), warnings: [], failures: [] }
855
895
  : changedResult;
856
- log(`[tsc-gate] base=${base} (${baseSource}); 변경 .ts ${changed.size}개 관측.`);
896
+ for (const message of warnings) warn(`[tsc-gate] ⚠ ${message}`);
897
+ const diffCount = diffFiles?.size;
898
+ const diffTsCount = diffFiles && [...diffFiles].filter((file) => /\.tsx?$/.test(file)).length;
899
+ log(`[tsc-gate] base=${base} (${baseSource}); 변경 .ts ${changed.size}개 관측. (head=${head ?? 'unknown'} · 전체 diff 파일 ${diffCount ?? 'unknown'}개 · diff .ts ${diffTsCount ?? 'unknown'}개)`);
857
900
  if (failures.length > 0) {
858
901
  error('[tsc-gate] ⛔ 변경 파일 수집 실패:');
859
902
  for (const { command, why } of failures) error(` ${command}: ${why}`);
@@ -861,7 +904,15 @@ export function runGate(io: Partial<GateIo> = {}): void {
861
904
  exit(1);
862
905
  return;
863
906
  }
864
- if (changed.size === 0) { log('[tsc-gate] 변경된 .ts 파일 없음 — 위 base 기준 0개이며 tsc 실행 없이 통과.'); return; }
907
+ if (changed.size === 0) {
908
+ if (!(changedResult instanceof Set) && (!head || diffCount === undefined || diffTsCount === undefined || diffTsCount > 0)) {
909
+ error(`[tsc-gate] FAIL — 변경 파일 수집 불일치: 전체 diff 파일 ${diffCount ?? 'unknown'}개 · diff .ts ${diffTsCount ?? 'unknown'}개 · 수집 .ts 0개 (못 셌음 ≠ 0).`);
910
+ exit(1);
911
+ return;
912
+ }
913
+ log(`[tsc-gate] PASS — 변경된 .ts 파일 없음 · base=${base} · head=${head} · 전체 diff 파일 ${diffCount}개 · diff .ts ${diffTsCount}개 · 수집 .ts 0개; tsc 실행 없이 통과.`);
914
+ return;
915
+ }
865
916
  const fieldCheck = requiredExportFields(changed, base);
866
917
  const requiredFields = Array.isArray(fieldCheck) ? fieldCheck : fieldCheck.fields;
867
918
  if (!Array.isArray(fieldCheck) && fieldCheck.failure) {
@@ -934,8 +985,8 @@ export function runGate(io: Partial<GateIo> = {}): void {
934
985
  }, { level: 'info' });
935
986
  log(`[tsc-gate] root 설정 범위 ${rootScope.size}개 관측 (apps/pwa/** 제외).`);
936
987
  if (runsPwa) log(`[tsc-gate] PWA 설정 범위 ${pwaScope.size}개 관측.`);
937
- const tscBin = existsSync('node_modules/.bin/tsc') ? 'node_modules/.bin/tsc' : 'npx tsc';
938
- const baseline = readTestTypecheckBaseline(process.cwd());
988
+ const tscBin = existsSync(join(cwd, 'node_modules/.bin/tsc')) ? 'node_modules/.bin/tsc' : 'npx tsc';
989
+ const baseline = readTestTypecheckBaseline(cwd);
939
990
  const rootCommand = `${tscBin} --noEmit -p ${TYPECHECK_GATE_CONFIG}`;
940
991
  const pwaCommand = `${tscBin} --noEmit -p apps/pwa/tsconfig.json`;
941
992
  const pwaEnvCommand = `${tscBin} --noEmit -p tsconfig.pwa-env.json`;
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env bun
2
+ // DOC-ROT① — 내부 매뉴얼(내부 문서 `*`)의 명령·저장소 경로를 실제와 대조한다.
3
+ // bun scripts/docs/doc-rot.ts [--json] [files…] (기본 = 내부 문서 `*`)
4
+ // 명령 판정은 scripts/docs-cli-check.ts 의 checkCommands 만 쓴다(같은 판정을 다시 짜지 않는다).
5
+ // 경로 판정은 인라인 코드 안의 src/ · scripts/ · docs/ · graphs/ · test/ 가 repoRoot 에 있는지다.
6
+ // ⛔ --help 를 못 잰 명령은 unmeasured 로 따로 센다 — «없다»로 세지 않는다.
7
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
8
+ import { spawnSync } from 'node:child_process';
9
+ import { join, relative, resolve } from 'node:path';
10
+ import {
11
+ checkCommands,
12
+ extractElanousCommands,
13
+ type Finding,
14
+ type HelpRunner,
15
+ } from '../docs-cli-check.js';
16
+
17
+ const REPO = resolve(import.meta.dir, '../..');
18
+ const PATH_ROOTS = ['src/', 'scripts/', 'docs/', 'graphs/', 'test/'] as const;
19
+ const PASSTHROUGH_COMMANDS = new Set(['gh', 'git']);
20
+
21
+ export interface MissingPath { line: number; path: string }
22
+ export interface DocRotFinding extends Omit<Finding, 'kind'> {
23
+ kind: Finding['kind'] | 'passthrough' | 'globalFlag' | 'intentional';
24
+ }
25
+ export interface DocRotFile {
26
+ file: string;
27
+ commands: DocRotFinding[];
28
+ missingPaths: MissingPath[];
29
+ }
30
+ export interface DocRotTotals {
31
+ files: number;
32
+ staleFiles: number;
33
+ missingPaths: number;
34
+ badCommands: number;
35
+ unmeasured: number;
36
+ passthrough: number;
37
+ globalFlag: number;
38
+ intentional: number;
39
+ }
40
+ export interface DocRotReport {
41
+ files: DocRotFile[];
42
+ totals: DocRotTotals;
43
+ }
44
+
45
+ export interface ScanDocRotInput {
46
+ files: readonly string[];
47
+ repoRoot: string;
48
+ help?: HelpRunner;
49
+ read?: (file: string) => string;
50
+ }
51
+
52
+ /** 인라인 코드에서 저장소 경로를 뽑는다. 줄번호 꼬리(`:12`)는 떼고, glob(`*`)은 건너뛴다. */
53
+ export function extractRepoPaths(text: string): MissingPath[] {
54
+ const out: MissingPath[] = [];
55
+ const seen = new Set<string>();
56
+ for (const m of text.matchAll(/`([^`\n]+)`/g)) {
57
+ const raw = m[1]!;
58
+ if (raw.includes('*')) continue;
59
+ if (!PATH_ROOTS.some((root) => raw.startsWith(root))) continue;
60
+ const path = raw.replace(/:\d+$/, '');
61
+ if (!PATH_ROOTS.some((root) => path.startsWith(root)) || path.endsWith('/')) continue;
62
+ const line = text.slice(0, m.index ?? 0).split('\n').length;
63
+ const key = `${line}\0${path}`;
64
+ if (seen.has(key)) continue;
65
+ seen.add(key);
66
+ out.push({ line, path });
67
+ }
68
+ return out;
69
+ }
70
+
71
+ /** 파일마다 명령 findings(checkCommands 재사용)와 없는 경로를 센다. */
72
+ export function scanDocRot(input: ScanDocRotInput): DocRotReport {
73
+ const read = input.read ?? ((file: string) => readFileSync(file, 'utf8'));
74
+ const texts = input.files.map((file) => ({ file, text: read(file) }));
75
+ const help: HelpRunner = input.help ?? ((args) => {
76
+ const result = spawnSync('bun', ['bin/elanous.mjs', '--test', ...args, '--help'], {
77
+ cwd: input.repoRoot, encoding: 'utf8', timeout: 60_000, env: { ...process.env, NO_COLOR: '1' },
78
+ });
79
+ return { ok: result.status === 0, out: `${result.stdout ?? ''}\n${result.stderr ?? ''}` };
80
+ });
81
+ let commandRoot: ReturnType<HelpRunner> | undefined;
82
+ const cachedHelp: HelpRunner = (args) => {
83
+ if (args.length === 0) return commandRoot ??= help(args);
84
+ return help(args);
85
+ };
86
+ const checked = texts.map(({ file, text }) => {
87
+ const label = relative(input.repoRoot, file) || file;
88
+ return { file: label, text, commands: checkCommands(extractElanousCommands(label, text), cachedHelp) };
89
+ });
90
+ const hasUnknownFlag = checked.some(({ commands }) => commands.some((finding) => finding.kind === 'unknown-flag'));
91
+ // A separate lookup measures root Options; a failure must not mean "no global flags".
92
+ const rootOptionsHelp = hasUnknownFlag ? help([]) : commandRoot;
93
+ const rootOptions = rootOptionsHelp?.ok ? (rootOptionsHelp.out.split(/\nOptions:\s*\n/)[1] ?? '').split(/\n\S[^\n]*:\s*\n/)[0]! : '';
94
+ const globalFlags = new Set([...rootOptions.matchAll(/^\s+(?:-\w,\s*)?(--[a-z][a-z0-9-]*)\b/gm)].map((match) => match[1]!));
95
+ const files: DocRotFile[] = checked.map(({ file, text, commands }) => {
96
+ const lines = text.split('\n');
97
+ const classified: DocRotFinding[] = commands.map((finding) => {
98
+ if (finding.kind !== 'unmeasured' && (lines[finding.ref.line - 1] ?? '').includes('⛔')) return { ...finding, kind: 'intentional' };
99
+ if (finding.kind !== 'unknown-flag') return finding;
100
+ if (PASSTHROUGH_COMMANDS.has(finding.ref.cmd)) return { ...finding, kind: 'passthrough' };
101
+ if (!rootOptionsHelp?.ok) return { ...finding, kind: 'unmeasured' };
102
+ const flag = /^--[a-z][a-z0-9-]*/.exec(finding.detail)?.[0];
103
+ if (flag && globalFlags.has(flag)) return { ...finding, kind: 'globalFlag' };
104
+ return finding;
105
+ });
106
+ const missingPaths = extractRepoPaths(text).filter((entry) => !existsSync(join(input.repoRoot, entry.path)));
107
+ return { file, commands: classified, missingPaths };
108
+ });
109
+ const isBad = (finding: DocRotFinding) => finding.kind === 'unknown-command' || finding.kind === 'unknown-subcommand' || finding.kind === 'unknown-flag';
110
+ const count = (kind: DocRotFinding['kind']) => files.reduce((sum, file) => sum + file.commands.filter((finding) => finding.kind === kind).length, 0);
111
+ const totals: DocRotTotals = {
112
+ files: files.length,
113
+ staleFiles: files.filter((file) => file.missingPaths.length > 0 || file.commands.some(isBad)).length,
114
+ missingPaths: files.reduce((sum, file) => sum + file.missingPaths.length, 0),
115
+ badCommands: files.reduce((sum, file) => sum + file.commands.filter(isBad).length, 0),
116
+ unmeasured: count('unmeasured'),
117
+ passthrough: count('passthrough'),
118
+ globalFlag: count('globalFlag'),
119
+ intentional: count('intentional'),
120
+ };
121
+ return { files, totals };
122
+ }
123
+
124
+ export function defaultManualFiles(repoRoot: string = REPO): string[] {
125
+ return readdirSync(join(repoRoot, 'docs', 'manual'))
126
+ .filter((name) => name.endsWith('.md'))
127
+ .sort()
128
+ .map((name) => join(repoRoot, 'docs', 'manual', name));
129
+ }
130
+
131
+ function renderTable(report: DocRotReport): string {
132
+ const lines = [
133
+ `doc-rot — 매뉴얼 ${report.totals.files} · 낡은 파일 ${report.totals.staleFiles} · 없는 경로 ${report.totals.missingPaths} · 깨진 명령 ${report.totals.badCommands} · 못 잰 명령 ${report.totals.unmeasured} · 통과 래퍼 ${report.totals.passthrough} · 전역 옵션 ${report.totals.globalFlag} · 의도적 예시 ${report.totals.intentional}`,
134
+ ];
135
+ for (const file of report.files) {
136
+ for (const finding of file.commands) lines.push(` ${finding.kind} ${file.file}:${finding.ref.line} ${finding.detail}`);
137
+ for (const missing of file.missingPaths) lines.push(` missing-path ${file.file}:${missing.line} ${missing.path}`);
138
+ }
139
+ return lines.join('\n');
140
+ }
141
+
142
+ if (import.meta.main) {
143
+ try {
144
+ const { registerStandaloneLogSink } = await import('../../src/domains/standalone-log-sink.js');
145
+ await registerStandaloneLogSink('doc-rot');
146
+ } catch { /* fail-open — 관측 배선 실패가 측정을 막지 않는다 */ }
147
+ const { debug } = await import('../../src/debug/log.js');
148
+ const json = process.argv.includes('--json');
149
+ const args = process.argv.slice(2).filter((arg) => arg !== '--json');
150
+ const files = args.length ? args.map((arg) => resolve(arg)) : defaultManualFiles();
151
+ const report = scanDocRot({ files, repoRoot: REPO });
152
+ debug.log('docs.rot', 'scanned', {
153
+ files: report.totals.files,
154
+ staleFiles: report.totals.staleFiles,
155
+ missingPaths: report.totals.missingPaths,
156
+ badCommands: report.totals.badCommands,
157
+ unmeasured: report.totals.unmeasured,
158
+ passthrough: report.totals.passthrough,
159
+ globalFlag: report.totals.globalFlag,
160
+ intentional: report.totals.intentional,
161
+ });
162
+ console.log(json ? JSON.stringify(report) : renderTable(report));
163
+ process.exit(report.totals.badCommands + report.totals.missingPaths > 0 ? 1 : 0);
164
+ }