@endora-commerce/cli 0.100.0

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 (356) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +37 -0
  3. package/dist/bin/endora.d.ts +28 -0
  4. package/dist/bin/endora.d.ts.map +1 -0
  5. package/dist/bin/endora.js +926 -0
  6. package/dist/bin/endora.js.map +1 -0
  7. package/dist/check/estate.d.ts +189 -0
  8. package/dist/check/estate.d.ts.map +1 -0
  9. package/dist/check/estate.js +1037 -0
  10. package/dist/check/estate.js.map +1 -0
  11. package/dist/check/hosts.d.ts +25 -0
  12. package/dist/check/hosts.d.ts.map +1 -0
  13. package/dist/check/hosts.js +1347 -0
  14. package/dist/check/hosts.js.map +1 -0
  15. package/dist/check/index.d.ts +60 -0
  16. package/dist/check/index.d.ts.map +1 -0
  17. package/dist/check/index.js +111 -0
  18. package/dist/check/index.js.map +1 -0
  19. package/dist/check/layout.d.ts +136 -0
  20. package/dist/check/layout.d.ts.map +1 -0
  21. package/dist/check/layout.js +262 -0
  22. package/dist/check/layout.js.map +1 -0
  23. package/dist/check/ledger.d.ts +98 -0
  24. package/dist/check/ledger.d.ts.map +1 -0
  25. package/dist/check/ledger.js +173 -0
  26. package/dist/check/ledger.js.map +1 -0
  27. package/dist/check/peer-owners.d.ts +116 -0
  28. package/dist/check/peer-owners.d.ts.map +1 -0
  29. package/dist/check/peer-owners.js +225 -0
  30. package/dist/check/peer-owners.js.map +1 -0
  31. package/dist/check/report.d.ts +33 -0
  32. package/dist/check/report.d.ts.map +1 -0
  33. package/dist/check/report.js +107 -0
  34. package/dist/check/report.js.map +1 -0
  35. package/dist/check/run.d.ts +147 -0
  36. package/dist/check/run.d.ts.map +1 -0
  37. package/dist/check/run.js +111 -0
  38. package/dist/check/run.js.map +1 -0
  39. package/dist/checks.d.ts +17 -0
  40. package/dist/checks.d.ts.map +1 -0
  41. package/dist/checks.js +17 -0
  42. package/dist/checks.js.map +1 -0
  43. package/dist/dev/index.d.ts +83 -0
  44. package/dist/dev/index.d.ts.map +1 -0
  45. package/dist/dev/index.js +298 -0
  46. package/dist/dev/index.js.map +1 -0
  47. package/dist/generate/divergence.d.ts +38 -0
  48. package/dist/generate/divergence.d.ts.map +1 -0
  49. package/dist/generate/divergence.js +237 -0
  50. package/dist/generate/divergence.js.map +1 -0
  51. package/dist/generate/index.d.ts +90 -0
  52. package/dist/generate/index.d.ts.map +1 -0
  53. package/dist/generate/index.js +369 -0
  54. package/dist/generate/index.js.map +1 -0
  55. package/dist/index.d.ts +23 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +35 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/inputs/declaration.d.ts +18 -0
  60. package/dist/inputs/declaration.d.ts.map +1 -0
  61. package/dist/inputs/declaration.js +64 -0
  62. package/dist/inputs/declaration.js.map +1 -0
  63. package/dist/inputs/env-file.d.ts +73 -0
  64. package/dist/inputs/env-file.d.ts.map +1 -0
  65. package/dist/inputs/env-file.js +134 -0
  66. package/dist/inputs/env-file.js.map +1 -0
  67. package/dist/inputs/prompt.d.ts +21 -0
  68. package/dist/inputs/prompt.d.ts.map +1 -0
  69. package/dist/inputs/prompt.js +59 -0
  70. package/dist/inputs/prompt.js.map +1 -0
  71. package/dist/inputs/resolve.d.ts +163 -0
  72. package/dist/inputs/resolve.d.ts.map +1 -0
  73. package/dist/inputs/resolve.js +290 -0
  74. package/dist/inputs/resolve.js.map +1 -0
  75. package/dist/install/host.d.ts +27 -0
  76. package/dist/install/host.d.ts.map +1 -0
  77. package/dist/install/host.js +90 -0
  78. package/dist/install/host.js.map +1 -0
  79. package/dist/install/index.d.ts +173 -0
  80. package/dist/install/index.d.ts.map +1 -0
  81. package/dist/install/index.js +793 -0
  82. package/dist/install/index.js.map +1 -0
  83. package/dist/install/wizard.d.ts +144 -0
  84. package/dist/install/wizard.d.ts.map +1 -0
  85. package/dist/install/wizard.js +362 -0
  86. package/dist/install/wizard.js.map +1 -0
  87. package/dist/lib/admin-artefacts.d.ts +70 -0
  88. package/dist/lib/admin-artefacts.d.ts.map +1 -0
  89. package/dist/lib/admin-artefacts.js +354 -0
  90. package/dist/lib/admin-artefacts.js.map +1 -0
  91. package/dist/lib/admin-surfaces.d.ts +298 -0
  92. package/dist/lib/admin-surfaces.d.ts.map +1 -0
  93. package/dist/lib/admin-surfaces.js +669 -0
  94. package/dist/lib/admin-surfaces.js.map +1 -0
  95. package/dist/lib/delegated-composer.d.ts +85 -0
  96. package/dist/lib/delegated-composer.d.ts.map +1 -0
  97. package/dist/lib/delegated-composer.js +241 -0
  98. package/dist/lib/delegated-composer.js.map +1 -0
  99. package/dist/lib/divergence-artefacts.d.ts +305 -0
  100. package/dist/lib/divergence-artefacts.d.ts.map +1 -0
  101. package/dist/lib/divergence-artefacts.js +828 -0
  102. package/dist/lib/divergence-artefacts.js.map +1 -0
  103. package/dist/lib/divergence.d.ts +337 -0
  104. package/dist/lib/divergence.d.ts.map +1 -0
  105. package/dist/lib/divergence.js +1005 -0
  106. package/dist/lib/divergence.js.map +1 -0
  107. package/dist/lib/docs-artefacts.d.ts +395 -0
  108. package/dist/lib/docs-artefacts.d.ts.map +1 -0
  109. package/dist/lib/docs-artefacts.js +781 -0
  110. package/dist/lib/docs-artefacts.js.map +1 -0
  111. package/dist/lib/emitted-exports.d.ts +21 -0
  112. package/dist/lib/emitted-exports.d.ts.map +1 -0
  113. package/dist/lib/emitted-exports.js +96 -0
  114. package/dist/lib/emitted-exports.js.map +1 -0
  115. package/dist/lib/emitted-freshness.d.ts +112 -0
  116. package/dist/lib/emitted-freshness.d.ts.map +1 -0
  117. package/dist/lib/emitted-freshness.js +288 -0
  118. package/dist/lib/emitted-freshness.js.map +1 -0
  119. package/dist/lib/entity-index-artefact.d.ts +95 -0
  120. package/dist/lib/entity-index-artefact.d.ts.map +1 -0
  121. package/dist/lib/entity-index-artefact.js +210 -0
  122. package/dist/lib/entity-index-artefact.js.map +1 -0
  123. package/dist/lib/instance-build-inputs.d.ts +108 -0
  124. package/dist/lib/instance-build-inputs.d.ts.map +1 -0
  125. package/dist/lib/instance-build-inputs.js +165 -0
  126. package/dist/lib/instance-build-inputs.js.map +1 -0
  127. package/dist/lib/module-docs.d.ts +473 -0
  128. package/dist/lib/module-docs.d.ts.map +1 -0
  129. package/dist/lib/module-docs.js +711 -0
  130. package/dist/lib/module-docs.js.map +1 -0
  131. package/dist/lib/module-package-subpaths.d.ts +56 -0
  132. package/dist/lib/module-package-subpaths.d.ts.map +1 -0
  133. package/dist/lib/module-package-subpaths.js +223 -0
  134. package/dist/lib/module-package-subpaths.js.map +1 -0
  135. package/dist/lib/module-packages.d.ts +200 -0
  136. package/dist/lib/module-packages.d.ts.map +1 -0
  137. package/dist/lib/module-packages.js +580 -0
  138. package/dist/lib/module-packages.js.map +1 -0
  139. package/dist/lib/module-population.d.ts +129 -0
  140. package/dist/lib/module-population.d.ts.map +1 -0
  141. package/dist/lib/module-population.js +172 -0
  142. package/dist/lib/module-population.js.map +1 -0
  143. package/dist/lib/module-roots.d.ts +337 -0
  144. package/dist/lib/module-roots.d.ts.map +1 -0
  145. package/dist/lib/module-roots.js +586 -0
  146. package/dist/lib/module-roots.js.map +1 -0
  147. package/dist/lib/nested-checkouts.d.ts +33 -0
  148. package/dist/lib/nested-checkouts.d.ts.map +1 -0
  149. package/dist/lib/nested-checkouts.js +160 -0
  150. package/dist/lib/nested-checkouts.js.map +1 -0
  151. package/dist/lib/platform-root.d.ts +43 -0
  152. package/dist/lib/platform-root.d.ts.map +1 -0
  153. package/dist/lib/platform-root.js +134 -0
  154. package/dist/lib/platform-root.js.map +1 -0
  155. package/dist/lib/platform-surface.d.ts +235 -0
  156. package/dist/lib/platform-surface.d.ts.map +1 -0
  157. package/dist/lib/platform-surface.js +393 -0
  158. package/dist/lib/platform-surface.js.map +1 -0
  159. package/dist/lib/port-registrations.d.ts +223 -0
  160. package/dist/lib/port-registrations.d.ts.map +1 -0
  161. package/dist/lib/port-registrations.js +532 -0
  162. package/dist/lib/port-registrations.js.map +1 -0
  163. package/dist/lib/read-size.d.ts +154 -0
  164. package/dist/lib/read-size.d.ts.map +1 -0
  165. package/dist/lib/read-size.js +182 -0
  166. package/dist/lib/read-size.js.map +1 -0
  167. package/dist/lib/registration-owners.d.ts +79 -0
  168. package/dist/lib/registration-owners.d.ts.map +1 -0
  169. package/dist/lib/registration-owners.js +77 -0
  170. package/dist/lib/registration-owners.js.map +1 -0
  171. package/dist/lib/release-index.d.ts +53 -0
  172. package/dist/lib/release-index.d.ts.map +1 -0
  173. package/dist/lib/release-index.js +162 -0
  174. package/dist/lib/release-index.js.map +1 -0
  175. package/dist/lib/repeating-timers.d.ts +79 -0
  176. package/dist/lib/repeating-timers.d.ts.map +1 -0
  177. package/dist/lib/repeating-timers.js +189 -0
  178. package/dist/lib/repeating-timers.js.map +1 -0
  179. package/dist/lib/source-text.d.ts +34 -0
  180. package/dist/lib/source-text.d.ts.map +1 -0
  181. package/dist/lib/source-text.js +80 -0
  182. package/dist/lib/source-text.js.map +1 -0
  183. package/dist/lib/specifiers.d.ts +20 -0
  184. package/dist/lib/specifiers.d.ts.map +1 -0
  185. package/dist/lib/specifiers.js +130 -0
  186. package/dist/lib/specifiers.js.map +1 -0
  187. package/dist/lib/sql-tables.d.ts +166 -0
  188. package/dist/lib/sql-tables.d.ts.map +1 -0
  189. package/dist/lib/sql-tables.js +464 -0
  190. package/dist/lib/sql-tables.js.map +1 -0
  191. package/dist/lib/switchable-modules.d.ts +54 -0
  192. package/dist/lib/switchable-modules.d.ts.map +1 -0
  193. package/dist/lib/switchable-modules.js +104 -0
  194. package/dist/lib/switchable-modules.js.map +1 -0
  195. package/dist/lib/tailwind-sources.d.ts +136 -0
  196. package/dist/lib/tailwind-sources.d.ts.map +1 -0
  197. package/dist/lib/tailwind-sources.js +307 -0
  198. package/dist/lib/tailwind-sources.js.map +1 -0
  199. package/dist/lib/ui-layer.d.ts +54 -0
  200. package/dist/lib/ui-layer.d.ts.map +1 -0
  201. package/dist/lib/ui-layer.js +57 -0
  202. package/dist/lib/ui-layer.js.map +1 -0
  203. package/dist/lib/workspace-packages.d.ts +186 -0
  204. package/dist/lib/workspace-packages.d.ts.map +1 -0
  205. package/dist/lib/workspace-packages.js +351 -0
  206. package/dist/lib/workspace-packages.js.map +1 -0
  207. package/dist/new-instance/deploy.d.ts +211 -0
  208. package/dist/new-instance/deploy.d.ts.map +1 -0
  209. package/dist/new-instance/deploy.js +1381 -0
  210. package/dist/new-instance/deploy.js.map +1 -0
  211. package/dist/new-instance/docs-toolchain.d.ts +66 -0
  212. package/dist/new-instance/docs-toolchain.d.ts.map +1 -0
  213. package/dist/new-instance/docs-toolchain.js +69 -0
  214. package/dist/new-instance/docs-toolchain.js.map +1 -0
  215. package/dist/new-instance/host.d.ts +124 -0
  216. package/dist/new-instance/host.d.ts.map +1 -0
  217. package/dist/new-instance/host.js +276 -0
  218. package/dist/new-instance/host.js.map +1 -0
  219. package/dist/new-instance/index.d.ts +118 -0
  220. package/dist/new-instance/index.d.ts.map +1 -0
  221. package/dist/new-instance/index.js +567 -0
  222. package/dist/new-instance/index.js.map +1 -0
  223. package/dist/new-instance/modules.d.ts +188 -0
  224. package/dist/new-instance/modules.d.ts.map +1 -0
  225. package/dist/new-instance/modules.js +392 -0
  226. package/dist/new-instance/modules.js.map +1 -0
  227. package/dist/new-instance/template.d.ts +505 -0
  228. package/dist/new-instance/template.d.ts.map +1 -0
  229. package/dist/new-instance/template.js +1886 -0
  230. package/dist/new-instance/template.js.map +1 -0
  231. package/dist/new-module/emit.d.ts +67 -0
  232. package/dist/new-module/emit.d.ts.map +1 -0
  233. package/dist/new-module/emit.js +1393 -0
  234. package/dist/new-module/emit.js.map +1 -0
  235. package/dist/new-module/host.d.ts +67 -0
  236. package/dist/new-module/host.d.ts.map +1 -0
  237. package/dist/new-module/host.js +224 -0
  238. package/dist/new-module/host.js.map +1 -0
  239. package/dist/new-module/index.d.ts +32 -0
  240. package/dist/new-module/index.d.ts.map +1 -0
  241. package/dist/new-module/index.js +193 -0
  242. package/dist/new-module/index.js.map +1 -0
  243. package/dist/new-module/spec.d.ts +188 -0
  244. package/dist/new-module/spec.d.ts.map +1 -0
  245. package/dist/new-module/spec.js +404 -0
  246. package/dist/new-module/spec.js.map +1 -0
  247. package/dist/new-module/text.d.ts +15 -0
  248. package/dist/new-module/text.d.ts.map +1 -0
  249. package/dist/new-module/text.js +22 -0
  250. package/dist/new-module/text.js.map +1 -0
  251. package/dist/new-storefront/dockerfile.d.ts +20 -0
  252. package/dist/new-storefront/dockerfile.d.ts.map +1 -0
  253. package/dist/new-storefront/dockerfile.js +131 -0
  254. package/dist/new-storefront/dockerfile.js.map +1 -0
  255. package/dist/new-storefront/gitignore.d.ts +23 -0
  256. package/dist/new-storefront/gitignore.d.ts.map +1 -0
  257. package/dist/new-storefront/gitignore.js +39 -0
  258. package/dist/new-storefront/gitignore.js.map +1 -0
  259. package/dist/new-storefront/index.d.ts +93 -0
  260. package/dist/new-storefront/index.d.ts.map +1 -0
  261. package/dist/new-storefront/index.js +329 -0
  262. package/dist/new-storefront/index.js.map +1 -0
  263. package/dist/new-storefront/npmrc.d.ts +98 -0
  264. package/dist/new-storefront/npmrc.d.ts.map +1 -0
  265. package/dist/new-storefront/npmrc.js +217 -0
  266. package/dist/new-storefront/npmrc.js.map +1 -0
  267. package/dist/new-storefront/reference.d.ts +189 -0
  268. package/dist/new-storefront/reference.d.ts.map +1 -0
  269. package/dist/new-storefront/reference.js +430 -0
  270. package/dist/new-storefront/reference.js.map +1 -0
  271. package/dist/new-storefront/rewrite.d.ts +171 -0
  272. package/dist/new-storefront/rewrite.d.ts.map +1 -0
  273. package/dist/new-storefront/rewrite.js +701 -0
  274. package/dist/new-storefront/rewrite.js.map +1 -0
  275. package/dist/release-index.json +284 -0
  276. package/dist/rules/action-route-permissions.d.ts +141 -0
  277. package/dist/rules/action-route-permissions.d.ts.map +1 -0
  278. package/dist/rules/action-route-permissions.js +556 -0
  279. package/dist/rules/action-route-permissions.js.map +1 -0
  280. package/dist/rules/bundle-pairing.d.ts +74 -0
  281. package/dist/rules/bundle-pairing.d.ts.map +1 -0
  282. package/dist/rules/bundle-pairing.js +281 -0
  283. package/dist/rules/bundle-pairing.js.map +1 -0
  284. package/dist/rules/channel-resolution.d.ts +17 -0
  285. package/dist/rules/channel-resolution.d.ts.map +1 -0
  286. package/dist/rules/channel-resolution.js +382 -0
  287. package/dist/rules/channel-resolution.js.map +1 -0
  288. package/dist/rules/command-coverage.d.ts +210 -0
  289. package/dist/rules/command-coverage.d.ts.map +1 -0
  290. package/dist/rules/command-coverage.js +714 -0
  291. package/dist/rules/command-coverage.js.map +1 -0
  292. package/dist/rules/container-imports.d.ts +60 -0
  293. package/dist/rules/container-imports.d.ts.map +1 -0
  294. package/dist/rules/container-imports.js +158 -0
  295. package/dist/rules/container-imports.js.map +1 -0
  296. package/dist/rules/default-language-prose.d.ts +212 -0
  297. package/dist/rules/default-language-prose.d.ts.map +1 -0
  298. package/dist/rules/default-language-prose.js +710 -0
  299. package/dist/rules/default-language-prose.js.map +1 -0
  300. package/dist/rules/diacritic-folds.d.ts +238 -0
  301. package/dist/rules/diacritic-folds.d.ts.map +1 -0
  302. package/dist/rules/diacritic-folds.js +681 -0
  303. package/dist/rules/diacritic-folds.js.map +1 -0
  304. package/dist/rules/entity-tenant-classification.d.ts +171 -0
  305. package/dist/rules/entity-tenant-classification.d.ts.map +1 -0
  306. package/dist/rules/entity-tenant-classification.js +323 -0
  307. package/dist/rules/entity-tenant-classification.js.map +1 -0
  308. package/dist/rules/entry-presence.d.ts +142 -0
  309. package/dist/rules/entry-presence.d.ts.map +1 -0
  310. package/dist/rules/entry-presence.js +339 -0
  311. package/dist/rules/entry-presence.js.map +1 -0
  312. package/dist/rules/entry-scope.d.ts +91 -0
  313. package/dist/rules/entry-scope.d.ts.map +1 -0
  314. package/dist/rules/entry-scope.js +404 -0
  315. package/dist/rules/entry-scope.js.map +1 -0
  316. package/dist/rules/env-inputs.d.ts +222 -0
  317. package/dist/rules/env-inputs.d.ts.map +1 -0
  318. package/dist/rules/env-inputs.js +951 -0
  319. package/dist/rules/env-inputs.js.map +1 -0
  320. package/dist/rules/kernel-boundary.d.ts +37 -0
  321. package/dist/rules/kernel-boundary.d.ts.map +1 -0
  322. package/dist/rules/kernel-boundary.js +195 -0
  323. package/dist/rules/kernel-boundary.js.map +1 -0
  324. package/dist/rules/nul-bytes.d.ts +233 -0
  325. package/dist/rules/nul-bytes.d.ts.map +1 -0
  326. package/dist/rules/nul-bytes.js +332 -0
  327. package/dist/rules/nul-bytes.js.map +1 -0
  328. package/dist/rules/platform-surface.d.ts +479 -0
  329. package/dist/rules/platform-surface.d.ts.map +1 -0
  330. package/dist/rules/platform-surface.js +749 -0
  331. package/dist/rules/platform-surface.js.map +1 -0
  332. package/dist/rules/port-catches.d.ts +225 -0
  333. package/dist/rules/port-catches.d.ts.map +1 -0
  334. package/dist/rules/port-catches.js +1374 -0
  335. package/dist/rules/port-catches.js.map +1 -0
  336. package/dist/rules/port-shape.d.ts +213 -0
  337. package/dist/rules/port-shape.d.ts.map +1 -0
  338. package/dist/rules/port-shape.js +670 -0
  339. package/dist/rules/port-shape.js.map +1 -0
  340. package/dist/rules/queue-names.d.ts +108 -0
  341. package/dist/rules/queue-names.d.ts.map +1 -0
  342. package/dist/rules/queue-names.js +395 -0
  343. package/dist/rules/queue-names.js.map +1 -0
  344. package/dist/rules/singleton-identity.d.ts +205 -0
  345. package/dist/rules/singleton-identity.d.ts.map +1 -0
  346. package/dist/rules/singleton-identity.js +830 -0
  347. package/dist/rules/singleton-identity.js.map +1 -0
  348. package/dist/rules/subscribe-seam.d.ts +121 -0
  349. package/dist/rules/subscribe-seam.d.ts.map +1 -0
  350. package/dist/rules/subscribe-seam.js +594 -0
  351. package/dist/rules/subscribe-seam.js.map +1 -0
  352. package/dist/rules/transaction-context.d.ts +40 -0
  353. package/dist/rules/transaction-context.d.ts.map +1 -0
  354. package/dist/rules/transaction-context.js +294 -0
  355. package/dist/rules/transaction-context.js.map +1 -0
  356. package/package.json +59 -0
@@ -0,0 +1,714 @@
1
+ /**
2
+ * Command-coverage check — feature 054 (FR-009 / FR-010, Constitution Principle XIII).
3
+ *
4
+ * `pnpm --filter backend run check:command-coverage -- [--strict] [--module <name> ...]`
5
+ *
6
+ * Flags, **per method/function/route handler** in every file a module owns:
7
+ * 1. an **unaudited sensitive write** — a method that performs a mutation call
8
+ * (`persist*`, `nativeUpdate`, `nativeDelete`, `remove*`, `flush`) but
9
+ * neither runs a Command (`commandBus.run(...)`) nor writes audit
10
+ * (`.record*(...)`) in the same method;
11
+ * 2. a **double-audit** — a method that BOTH runs a Command AND writes audit by
12
+ * hand (a converted write must remove its manual audit call, FR-010).
13
+ *
14
+ * Method-level (not file-level) so a partially-migrated file is judged per
15
+ * method: a converted `adjust` no longer masks an unaudited `grant` in the same
16
+ * file, and a Command in one method is not mistaken for a double-audit against a
17
+ * legacy `record()` in another.
18
+ *
19
+ * Escape hatch: a genuinely non-sensitive write (bookkeeping rows — progress
20
+ * counters, cache, queue state) can be exempted by putting a
21
+ * `command-coverage-ignore: <reason>` comment anywhere in the method. This keeps
22
+ * "build-breaking per module" honest without forcing audit onto non-domain writes,
23
+ * mirroring the audited `withSystemScope` escape hatch for tenancy.
24
+ *
25
+ * ## The escape hatch is a two-way ratchet (issue #116)
26
+ *
27
+ * 185 methods carry that comment, and until now nothing ever re-read one. An
28
+ * ignore written for a write that has since moved elsewhere — into a Command, or
29
+ * into another module's audited service — went on reading as a considered
30
+ * decision about a write that is no longer there, and the next person to add a
31
+ * write to that method inherited the exemption silently.
32
+ *
33
+ * So a marker on a method that **no longer writes at all** is reported as
34
+ * `stale-ignore`, in the idiom of `PORT_CATCHES_TO_DRAIN` and
35
+ * `BARE_SUBSCRIPTIONS_TO_DRAIN`: an unledgered violation fails the build, and an
36
+ * entry that no longer describes one fails it too. MR !532's marker in
37
+ * `orders/order-completion-reactor.ts` — added because a refactor made an
38
+ * *existing* write visible to this check — is exactly the entry that has to be
39
+ * re-verified rather than trusted, and now it is, on every run.
40
+ *
41
+ * **The staleness half looks for writes more widely than the flagging half**,
42
+ * and the asymmetry is deliberate: both errors then fall on the safe side. The
43
+ * flagging half only flags an ORM mutation call it is sure about; the staleness
44
+ * half additionally counts a raw SQL write statement (`conn.execute` with an
45
+ * `update`/`insert into`/`delete from`) and any write reached transitively
46
+ * through `this.<name>(…)` in the same file — so a marker guarding a real write
47
+ * this check cannot itself see is left alone, and only a marker guarding nothing
48
+ * is reported.
49
+ *
50
+ * **With one subtraction (D-89c): a downstream write that itself carries a
51
+ * marker does not keep the caller's marker alive.** The transitive rule exists
52
+ * so a marker over a write this check cannot see is not called stale; when the
53
+ * write it reaches is *already exempted where it happens*, the caller's marker
54
+ * is provably guarding nothing and the ratchet has to say so. The worked
55
+ * example carries the failure and its correction in one file:
56
+ * `payments/services/payment-reference-port.ts` marked `stampExternalReference`
57
+ * and `stampExternalReferenceIfAbsent`, neither of which writes, and later
58
+ * marked the private `stamp` that does the `flush` — with a comment explaining
59
+ * that the marker had to be repeated there "because the check reads the
60
+ * function that writes". Both redundant caller markers survived, and nothing
61
+ * could see them.
62
+ *
63
+ * ## What "every service" was allowed to mean (issue #122)
64
+ *
65
+ * The walk matched `**​/services/<file>.ts` — **one level, nothing else**. Over
66
+ * the tree that is **472 of 1152** module files: 680 were never opened, 78 of
67
+ * them containing a write signal. Invisible were `pim_ergonode/services/import/`,
68
+ * `product_feeds/services/delivery/` and `services/queues/` (nested a level too
69
+ * deep), every `workers/`, `queues/` and `jobs/` file, every `commands/` file,
70
+ * every `routes*.ts`, every `backend.ts` boot hook, every `scripts/` entry point
71
+ * and every `seeds/` reconciler. Queue consumers and admin route handlers are
72
+ * exactly where writes live, so the check read clean over the two categories
73
+ * Principle XIII is most about.
74
+ *
75
+ * The boundary is now **every `.ts` file under `src/modules/` and `src/apps/`**.
76
+ * Four exclusions remain, and each is an argument rather than an omission:
77
+ *
78
+ * - `migrations/` — DDL applied by the migrator with no request, no actor and
79
+ * no undo; an audit entry for one would have nobody to attribute it to. The
80
+ * migration registry and `db:fresh` are its gate.
81
+ * - `*.test.ts` / `*.d.ts` — not shipped code.
82
+ * - `audit_logs/` — the audit writer itself. Its writes *are* the audit
83
+ * entries; requiring one for each is circular.
84
+ *
85
+ * Everything else is judged, including `seeds/` and `scripts/`: a CLI entry
86
+ * point creating an administrator and a boot reconciler writing predefined rows
87
+ * are operator-visible writes that happen to run outside a request, and a
88
+ * category-wide exemption for them is the same mistake one folder over.
89
+ *
90
+ * ## Two narrowings the widening forced
91
+ *
92
+ * A wider walk meets shapes a `services/` file rarely has, and a check that
93
+ * answers them with exemptions is lying about the tree rather than reading it:
94
+ *
95
+ * 1. **`remove` is only an ORM mutation off an EntityManager.** 30 of the 36
96
+ * first-pass findings were `deps.<x>Service.remove(id)` in a route handler
97
+ * — a call into an audited service. Every other name in the vocabulary
98
+ * (`persist`, `persistAndFlush`, `nativeUpdate`, `nativeDelete`,
99
+ * `removeAndFlush`, `flush`) is MikroORM's alone and still counts off any
100
+ * receiver. The **staleness half keeps counting `remove` everywhere**, so
101
+ * the asymmetry above survives: the narrowing can only add a report, never
102
+ * silence one.
103
+ * 2. **A route file is judged per handler.** `registerXAdminRoutes` is not a
104
+ * unit of work; each handler is. Read as one unit, a single
105
+ * `commandBus.run` anywhere in the file clears every other handler in it —
106
+ * the masking the per-method rule exists to prevent, one level up. It also
107
+ * manufactured two `double-audit` reports across handlers that never met.
108
+ *
109
+ * ## What this check cannot see, and will not pretend to (D-89b)
110
+ *
111
+ * This check reads **call** shapes. A field assignment on a managed entity —
112
+ * `order.status = ref`, `refund.settlementState = outcome.state` — is a write
113
+ * the unit of work will flush and this check cannot see it. The unit is judged
114
+ * by the calls it makes, so an assignment is caught only when the same unit also
115
+ * calls one of the vocabulary. A unit that assigns and lets its caller flush is
116
+ * outside the population, **by construction and not by exemption**.
117
+ *
118
+ * That is refused deliberately rather than deferred. The shapes are not
119
+ * separable by a static name test: `x.y = z` is the most common statement form
120
+ * in the language, `x` is a managed entity only when the type checker says so,
121
+ * and this check does not build a program. Reading every assignment as a
122
+ * candidate write would put a finding on most methods in the tree and teach
123
+ * people to write exemptions; reading none of them keeps the check's green
124
+ * honest — provided the green is read as what it is. So: a green
125
+ * `check:command-coverage --strict` means *"no unaudited sensitive write of a
126
+ * shape this check can see"*, and it does not mean *"every sensitive write is
127
+ * audited"*. It cannot be made to mean the second at anything like this cost.
128
+ *
129
+ * Scope & staging: build-breaks (exit 1) for **migrated modules**
130
+ * (`MIGRATED_MODULES`, in the repository-scope host, or `--module`); any other
131
+ * module would be report-only. The platform-wide rollout is COMPLETE and CI runs
132
+ * with `--strict`, so a finding in ANY module, including a brand-new one not yet
133
+ * in the list, fails the build.
134
+ *
135
+ * ## One analysis, two hosts
136
+ *
137
+ * This file is the analysis. `backend/scripts/check-command-coverage.ts` hosts it
138
+ * over this repository's module tree and owns the rollout ledger, which is a
139
+ * fact about *these* modules; `endora check` hosts it over one module package
140
+ * (`specs/101-endora-check/contracts/package-scope-layout.md` §6). A third-party
141
+ * package is in no rollout, so the package-scope host judges it strictly — which
142
+ * is what `--strict` already means here.
143
+ */
144
+ import { readdirSync, statSync } from 'node:fs';
145
+ import { join } from 'node:path';
146
+ import ts from 'typescript';
147
+ /**
148
+ * Mutation names that belong to MikroORM and to nothing else in this tree, so
149
+ * they count off any receiver.
150
+ */
151
+ const ORM_MUTATIONS = new Set([
152
+ 'persist',
153
+ 'persistAndFlush',
154
+ 'nativeUpdate',
155
+ 'nativeDelete',
156
+ 'removeAndFlush',
157
+ 'flush',
158
+ ]);
159
+ /**
160
+ * Mutation names that are also ordinary vocabulary. `remove` is MikroORM's and
161
+ * also every service's, every queue backend's and every transport client's —
162
+ * `deps.countryService.remove(code)`, `scheduler.remove(id)`,
163
+ * `ftpClient.remove(path)`. `create` is worse on the same axis: it is the name
164
+ * of nearly every service method in this tree. Both count for the flagging half
165
+ * only off an EntityManager; the staleness half counts them everywhere (see the
166
+ * header).
167
+ *
168
+ * `create` joined in D-89. `em.create(Entity, …)` puts a managed entity into the
169
+ * unit of work — the insert is queued from that moment and the next `flush`
170
+ * writes it, whoever calls it — so a unit whose only mutation call is an
171
+ * `em.create` was invisible to this check while being every bit as much a write
172
+ * as the `persist` next door. That is the hole that let a CSV import rewrite
173
+ * catalogue and stock unaudited for a year.
174
+ */
175
+ const AMBIGUOUS_MUTATIONS = new Set(['remove', 'create']);
176
+ const MUTATION_METHODS = new Set([...ORM_MUTATIONS, ...AMBIGUOUS_MUTATIONS]);
177
+ /**
178
+ * An identifier that names a MikroORM EntityManager, as this tree spells it:
179
+ * `em`, the transactional `tx` / `trx`, the short forked forms `tem` / `cem`,
180
+ * any `<word>Em` (`txEm`, `targetEm`), and the factories `em()` / `emFactory()`.
181
+ *
182
+ * Deliberately *not* "anything ending in em" — that matches `item` and `system`,
183
+ * and a check that flags `item.remove(x)` teaches people to write exemptions.
184
+ */
185
+ const ENTITY_MANAGER_NAME = /^(?:em|tem|cem|tx|trx|entityManager|emFactory|[A-Za-z]+Em)$/;
186
+ /**
187
+ * Whether an expression is (or yields) an EntityManager, by name.
188
+ *
189
+ * Unwraps calls and the usual wrappers, so `this.em()`, `this.deps.em()` and
190
+ * `em.fork()` all resolve; recurses through a chained mutation so
191
+ * `em.remove(a).remove(b)` stays an EntityManager at every link.
192
+ */
193
+ function isEntityManagerReceiver(node) {
194
+ let current = node;
195
+ while (ts.isCallExpression(current) ||
196
+ ts.isNonNullExpression(current) ||
197
+ ts.isParenthesizedExpression(current) ||
198
+ ts.isAwaitExpression(current)) {
199
+ current = current.expression;
200
+ }
201
+ const name = ts.isPropertyAccessExpression(current)
202
+ ? current.name.text
203
+ : ts.isIdentifier(current)
204
+ ? current.text
205
+ : null;
206
+ if (name === null)
207
+ return false;
208
+ if (ENTITY_MANAGER_NAME.test(name))
209
+ return true;
210
+ if (ts.isPropertyAccessExpression(current) &&
211
+ (MUTATION_METHODS.has(name) || name === 'fork' || name === 'transactional')) {
212
+ return isEntityManagerReceiver(current.expression);
213
+ }
214
+ return false;
215
+ }
216
+ /**
217
+ * Fastify route registrations. Each function argument is its own unit of work.
218
+ *
219
+ * A call only counts when its first argument is a string literal path, which is
220
+ * what tells `app.get('/api/…', handler)` apart from `cache.get(key)` and
221
+ * `set.delete(key)`. `.route({ … })` is not in the list because the tree does
222
+ * not use it; a first use would be invisible here, so it is named in the
223
+ * companion test rather than left to be discovered.
224
+ */
225
+ const ROUTE_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'all']);
226
+ /** Option-object properties whose value is a handler in its own right. */
227
+ const HANDLER_PROPERTIES = new Set(['handler', 'preHandler', 'onRequest', 'preValidation']);
228
+ /**
229
+ * A SQL statement that writes, as it appears in a string or template literal
230
+ * handed to `conn.execute` / `em.execute`.
231
+ *
232
+ * Only the staleness half reads this. Matching prose in a literal
233
+ * (`'update the row'`) marks the unit as writing, which suppresses a staleness
234
+ * report — the safe direction, since the cost is a marker left standing rather
235
+ * than a marker deleted off a live write.
236
+ */
237
+ const SQL_WRITE = /\b(insert\s+into|update\s+["`']?[a-z_]|delete\s+from|truncate\s+table)/i;
238
+ /**
239
+ * Writes that leave the database entirely — BullMQ schedulers and Redis keys.
240
+ *
241
+ * Only the staleness half reads this, for the same reason it reads `SQL_WRITE`.
242
+ * `product_feeds/workers/taxonomy-refresh-worker.ts` documents its
243
+ * `queue.removeJobScheduler(…)` with a marker that says, correctly, "Redis-only";
244
+ * a sweep that knew only ORM and SQL called that marker dead the moment
245
+ * `workers/` came into scope. The flagging half deliberately does not read it —
246
+ * a queue write is not a Command Bus write.
247
+ */
248
+ const NON_SQL_WRITE_METHODS = new Set([
249
+ 'removeJobScheduler',
250
+ 'upsertJobScheduler',
251
+ 'removeRepeatable',
252
+ 'removeRepeatableByKey',
253
+ 'obliterate',
254
+ 'drain',
255
+ 'clean',
256
+ 'del',
257
+ 'unlink',
258
+ 'hset',
259
+ 'hdel',
260
+ 'expire',
261
+ 'setex',
262
+ ]);
263
+ const AUDIT_RECEIVER = /(auditLog|auditLogService|auditService|AuditLogService|cartAuditService|\.audit)$/;
264
+ const SUPPRESS_TOKEN = 'command-coverage-ignore';
265
+ /** Whether an object literal is a Command definition (`action` + `run` props). */
266
+ function isCommandLiteral(node) {
267
+ const names = new Set(node.properties
268
+ .map((p) => (p.name && ts.isIdentifier(p.name) ? p.name.text : null))
269
+ .filter((n) => n !== null));
270
+ return names.has('action') && names.has('run');
271
+ }
272
+ /**
273
+ * Scan a single method/function subtree for mutation / audit / command calls.
274
+ *
275
+ * `nested` holds the route handlers that are units of their own; their subtrees
276
+ * belong to them, not to the registration function that contains them.
277
+ */
278
+ function scanUnit(node, sf, nested) {
279
+ const scan = {
280
+ hasMutation: false,
281
+ mutationLine: null,
282
+ hasAuditWrite: false,
283
+ runsCommand: false,
284
+ definesCommand: false,
285
+ calls: new Set(),
286
+ writesAnything: false,
287
+ };
288
+ const visit = (n) => {
289
+ if (nested.has(n))
290
+ return;
291
+ if (ts.isObjectLiteralExpression(n) && isCommandLiteral(n)) {
292
+ scan.definesCommand = true;
293
+ }
294
+ if (ts.isStringLiteralLike(n) && SQL_WRITE.test(n.text)) {
295
+ scan.writesAnything = true;
296
+ }
297
+ // The sanctioned free-function audit primitive (commands/audit-from-context):
298
+ // `recordAuditFromContext(auditLog, em, …)` writes a co-transactional entry.
299
+ if (ts.isCallExpression(n) &&
300
+ ts.isIdentifier(n.expression) &&
301
+ n.expression.text === 'recordAuditFromContext') {
302
+ scan.hasAuditWrite = true;
303
+ }
304
+ // A bare `helper(...)` — delegation to a module-level function.
305
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
306
+ scan.calls.add(n.expression.text);
307
+ }
308
+ if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
309
+ const method = n.expression.name.text;
310
+ const receiver = n.expression.expression;
311
+ const receiverText = receiver.getText(sf);
312
+ if (NON_SQL_WRITE_METHODS.has(method)) {
313
+ scan.writesAnything = true;
314
+ }
315
+ if (MUTATION_METHODS.has(method)) {
316
+ // The staleness half reads the widest possible answer; the flagging half
317
+ // asks an ambiguous name to prove its receiver is an EntityManager.
318
+ scan.writesAnything = true;
319
+ if (!AMBIGUOUS_MUTATIONS.has(method) || isEntityManagerReceiver(receiver)) {
320
+ scan.hasMutation = true;
321
+ if (scan.mutationLine === null) {
322
+ scan.mutationLine = sf.getLineAndCharacterOfPosition(n.getStart(sf)).line + 1;
323
+ }
324
+ }
325
+ }
326
+ if ((method === 'record' || method === 'recordWithin') && AUDIT_RECEIVER.test(receiverText)) {
327
+ scan.hasAuditWrite = true;
328
+ }
329
+ if (method === 'run' && /commandBus$/.test(receiverText)) {
330
+ scan.runsCommand = true;
331
+ }
332
+ // `this.<name>(...)` — a candidate delegation to an audited runner.
333
+ if (receiver.kind === ts.SyntaxKind.ThisKeyword) {
334
+ scan.calls.add(method);
335
+ }
336
+ }
337
+ ts.forEachChild(n, visit);
338
+ };
339
+ ts.forEachChild(node, visit);
340
+ return scan;
341
+ }
342
+ function unitName(node) {
343
+ if ((ts.isMethodDeclaration(node) ||
344
+ ts.isFunctionDeclaration(node) ||
345
+ ts.isGetAccessorDeclaration(node) ||
346
+ ts.isSetAccessorDeclaration(node)) &&
347
+ node.name) {
348
+ return node.name.getText();
349
+ }
350
+ if (ts.isConstructorDeclaration(node))
351
+ return 'constructor';
352
+ if (ts.isPropertyDeclaration(node) && node.name)
353
+ return node.name.getText();
354
+ if (ts.isVariableDeclaration(node) && node.name)
355
+ return node.name.getText();
356
+ return '<anonymous>';
357
+ }
358
+ function isArrowOrFn(node) {
359
+ return !!node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node));
360
+ }
361
+ /**
362
+ * Route handlers, as units of their own.
363
+ *
364
+ * Fastify registers them as function arguments to `app.<method>('/path', …)`,
365
+ * so a registration function contains every handler in the file. Judged as one
366
+ * unit it hides them from each other; each handler gets its own unit here, named
367
+ * `POST /api/v1/admin/…` so a finding points at the route an operator calls.
368
+ */
369
+ function collectRouteHandlers(sf) {
370
+ const handlers = [];
371
+ const visit = (node) => {
372
+ if (ts.isCallExpression(node) &&
373
+ ts.isPropertyAccessExpression(node.expression) &&
374
+ ROUTE_METHODS.has(node.expression.name.text) &&
375
+ node.arguments.length > 1 &&
376
+ node.arguments[0] !== undefined &&
377
+ ts.isStringLiteralLike(node.arguments[0])) {
378
+ const name = `${node.expression.name.text.toUpperCase()} ${node.arguments[0].text}`;
379
+ for (const arg of node.arguments.slice(1)) {
380
+ if (isArrowOrFn(arg)) {
381
+ handlers.push({ name, node: arg, suppressed: false, suppressedLine: null });
382
+ }
383
+ else if (ts.isObjectLiteralExpression(arg)) {
384
+ for (const prop of arg.properties) {
385
+ if (ts.isPropertyAssignment(prop) &&
386
+ ts.isIdentifier(prop.name) &&
387
+ HANDLER_PROPERTIES.has(prop.name.text) &&
388
+ isArrowOrFn(prop.initializer)) {
389
+ handlers.push({
390
+ name: `${name} (${prop.name.text})`,
391
+ node: prop.initializer,
392
+ suppressed: false,
393
+ suppressedLine: null,
394
+ });
395
+ }
396
+ }
397
+ }
398
+ }
399
+ }
400
+ ts.forEachChild(node, visit);
401
+ };
402
+ ts.forEachChild(sf, visit);
403
+ return handlers;
404
+ }
405
+ /**
406
+ * Function-valued locals — `const audit = async (…) => …` declared *inside* a
407
+ * registration function or a method.
408
+ *
409
+ * They are **not** judged: a closure's write belongs to the unit that runs it,
410
+ * which is why `collectUnits` only takes a top-level `const fn = () => …`. But
411
+ * they are legitimate delegation targets, and `organizations/routes.admin.ts`
412
+ * is built entirely on one — every audited handler in it calls the file's local
413
+ * `audit(…)`. Without them, six handlers that audit correctly were reported as
414
+ * unaudited, which is the wrong direction for a check nobody may exempt away.
415
+ */
416
+ function collectHelperClosures(sf) {
417
+ const helpers = [];
418
+ const visit = (node) => {
419
+ if (ts.isVariableDeclaration(node) &&
420
+ isArrowOrFn(node.initializer) &&
421
+ ts.isIdentifier(node.name) &&
422
+ // top-level ones are already units in their own right
423
+ !(node.parent?.parent?.parent !== undefined && ts.isSourceFile(node.parent.parent.parent))) {
424
+ helpers.push({ name: node.name.text, node });
425
+ }
426
+ ts.forEachChild(node, visit);
427
+ };
428
+ ts.forEachChild(sf, visit);
429
+ return helpers;
430
+ }
431
+ /**
432
+ * Collect top-level method/function units plus every route handler, and the set
433
+ * of handler nodes their enclosing registration function must not absorb.
434
+ */
435
+ function collectUnits(sf) {
436
+ const handlers = collectRouteHandlers(sf);
437
+ const nested = new Set(handlers.map((h) => h.node));
438
+ const units = [];
439
+ const walk = (node) => {
440
+ let root = null;
441
+ if (ts.isMethodDeclaration(node) ||
442
+ ts.isFunctionDeclaration(node) ||
443
+ ts.isConstructorDeclaration(node) ||
444
+ ts.isGetAccessorDeclaration(node) ||
445
+ ts.isSetAccessorDeclaration(node)) {
446
+ root = node;
447
+ }
448
+ else if (ts.isPropertyDeclaration(node) && isArrowOrFn(node.initializer)) {
449
+ root = node; // class field arrow method
450
+ }
451
+ else if (ts.isVariableDeclaration(node) &&
452
+ isArrowOrFn(node.initializer) &&
453
+ // only top-level `const x = () => …`, not locals inside a method
454
+ node.parent?.parent?.parent !== undefined &&
455
+ ts.isSourceFile(node.parent.parent.parent)) {
456
+ root = node;
457
+ }
458
+ if (root) {
459
+ const suppressedLine = suppressionLine(root, sf, nested);
460
+ units.push({
461
+ name: unitName(root),
462
+ node: root,
463
+ suppressed: suppressedLine !== null,
464
+ suppressedLine,
465
+ });
466
+ return; // do NOT recurse — inline callbacks belong to this unit
467
+ }
468
+ ts.forEachChild(node, walk);
469
+ };
470
+ walk(sf);
471
+ for (const handler of handlers) {
472
+ const suppressedLine = suppressionLine(handler.node, sf, nested);
473
+ units.push({ ...handler, suppressed: suppressedLine !== null, suppressedLine });
474
+ }
475
+ return { units, nested };
476
+ }
477
+ /**
478
+ * The 1-based line of this unit's `command-coverage-ignore`, or `null`.
479
+ *
480
+ * Two things it deliberately does not count (issue #122):
481
+ *
482
+ * - a token inside a **nested** unit — a marker on one route handler is not an
483
+ * exemption for the registration function that holds it, nor for the handler
484
+ * next to it;
485
+ * - a token in trivia that is not the unit's **own** doc comment. Four command
486
+ * files describe their policy in a file header that quotes the token; read
487
+ * from `getFullStart()`, that header exempted whichever declaration happened
488
+ * to come first — and once the staleness sweep landed, reported it as a dead
489
+ * marker nobody had written. Only the **last leading comment block, adjacent
490
+ * to the declaration**, counts: a blank line between the two makes it a file
491
+ * header rather than a doc comment. That is what still lets
492
+ * `_lifecycle/services/presence-load.ts` spell its rationale out in JSDoc
493
+ * above the function it exempts.
494
+ */
495
+ function suppressionLine(node, sf, nested) {
496
+ const full = sf.getFullText();
497
+ const inNested = (offset) => [...nested].some((n) => n !== node && offset >= n.getStart(sf) && offset < n.getEnd());
498
+ for (let at = full.indexOf(SUPPRESS_TOKEN, node.getStart(sf)); at !== -1 && at < node.getEnd(); at = full.indexOf(SUPPRESS_TOKEN, at + SUPPRESS_TOKEN.length)) {
499
+ if (!inNested(at))
500
+ return sf.getLineAndCharacterOfPosition(at).line + 1;
501
+ }
502
+ const leading = ts.getLeadingCommentRanges(full, node.getFullStart()) ?? [];
503
+ const own = leading.at(-1);
504
+ // Adjacent means "nothing but one line break between the comment and the
505
+ // declaration" — a blank line makes it a file header, not this unit's doc.
506
+ if (own && !/\n\s*\n/.test(full.slice(own.end, node.getStart(sf)))) {
507
+ const at = full.indexOf(SUPPRESS_TOKEN, own.pos);
508
+ if (at !== -1 && at < own.end)
509
+ return sf.getLineAndCharacterOfPosition(at).line + 1;
510
+ }
511
+ return null;
512
+ }
513
+ /** Static, dependency-free per-method analysis of a single source file. */
514
+ export function analyzeSource(filePath, source) {
515
+ const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
516
+ const collected = collectUnits(sf);
517
+ const units = collected.units.map((u) => ({ ...u, scan: scanUnit(u.node, sf, collected.nested) }));
518
+ // Delegation targets that are not themselves judged (see collectHelperClosures).
519
+ const helpers = collectHelperClosures(sf).map((h) => ({
520
+ ...h,
521
+ scan: scanUnit(h.node, sf, collected.nested),
522
+ }));
523
+ const delegates = [...units, ...helpers];
524
+ // Pass 1 — a "runner" method executes writes through the Command Bus (calls
525
+ // commandBus.run, or defines a Command literal the bus will run). A method that
526
+ // delegates to a runner (`this.<runner>(...)`) is therefore audited too.
527
+ const runnerNames = new Set(delegates.filter((u) => u.scan.runsCommand || u.scan.definesCommand).map((u) => u.name));
528
+ // …and a "recorder" method writes audit by hand (`auditLog.record(...)` /
529
+ // `.recordWithin(...)`). A very common shape is a public write that mutates and
530
+ // then calls a private `this.writeAudit()` helper which records — the mutation
531
+ // and the audit call live in different methods. Recognizing delegation to a
532
+ // recorder (symmetric with runner delegation) clears that legitimate pattern
533
+ // instead of flagging an already-audited write as unaudited.
534
+ const recorderNames = new Set(delegates.filter((u) => u.scan.hasAuditWrite).map((u) => u.name));
535
+ // A unit is directly covered if it runs a Command, defines one, records audit,
536
+ // or forward-delegates to a runner/recorder.
537
+ const isDirectlyCovered = (u) => u.scan.runsCommand ||
538
+ u.scan.definesCommand ||
539
+ u.scan.hasAuditWrite ||
540
+ [...u.scan.calls].some((n) => runnerNames.has(n) || recorderNames.has(n));
541
+ // Reverse delegation — a mutating PRIVATE helper (`applyGlobal`, `applyProduct`)
542
+ // that is invoked by a covered public method is part of that method's audited
543
+ // unit of work: its `em.persist` only flushes when the covered caller flushes,
544
+ // co-transactionally with the caller's audit/Command. Collect every method name
545
+ // called via `this.<name>()` from a covered unit and treat those as covered too.
546
+ const coveredCallees = new Set();
547
+ for (const u of delegates) {
548
+ if (isDirectlyCovered(u))
549
+ for (const n of u.scan.calls)
550
+ coveredCallees.add(n);
551
+ }
552
+ // The staleness half. A unit still writes if it writes itself, or if anything
553
+ // it calls as `this.<name>(…)` in this file does — `reserve()` delegating to
554
+ // `#reserveFlat()`, a reaper delegating to `release()`. Memoised over the
555
+ // recursion so a cycle terminates.
556
+ const byName = new Map(delegates.map((u) => [u.name, u]));
557
+ /** Does this delegate carry a marker of its own? Helpers never do. */
558
+ const carriesMarker = (unit) => 'suppressed' in unit && unit.suppressed === true;
559
+ const reachesWrite = (name, seen = new Set()) => {
560
+ if (seen.has(name))
561
+ return false;
562
+ seen.add(name);
563
+ const unit = byName.get(name);
564
+ if (!unit)
565
+ return false;
566
+ if (unit.scan.writesAnything)
567
+ return true;
568
+ return [...unit.scan.calls].some((callee) => {
569
+ // D-89(c) — a downstream that carries its own marker is already exempted
570
+ // where it writes, so it does not keep this caller's marker alive. Without
571
+ // this the ratchet cannot see the shape it was written for: a marker on
572
+ // two public callers of a private body that carries a third.
573
+ const target = byName.get(callee);
574
+ if (target !== undefined && carriesMarker(target))
575
+ return false;
576
+ return reachesWrite(callee, seen);
577
+ });
578
+ };
579
+ // A route handler is named `POST /api/…`, a method `rename` — only the latter
580
+ // reads as a call site.
581
+ const label = (name) => (name.includes(' ') ? name : `${name}()`);
582
+ const findings = [];
583
+ for (const u of units) {
584
+ if (u.suppressed) {
585
+ if (!reachesWrite(u.name)) {
586
+ findings.push({
587
+ filePath,
588
+ line: u.suppressedLine,
589
+ method: u.name,
590
+ kind: 'stale-ignore',
591
+ message: `${label(u.name)} carries a command-coverage-ignore but writes nothing — ` +
592
+ 'the write it exempted has moved or gone. Delete the marker; keep the ' +
593
+ 'sentence as an ordinary comment if it still explains something',
594
+ });
595
+ }
596
+ continue;
597
+ }
598
+ const s = u.scan;
599
+ const covered = isDirectlyCovered(u) || coveredCallees.has(u.name);
600
+ if (s.hasMutation && !covered) {
601
+ findings.push({
602
+ filePath,
603
+ line: s.mutationLine,
604
+ method: u.name,
605
+ kind: 'unaudited-sensitive-write',
606
+ message: `${label(u.name)} mutates without a Command or an audit entry`,
607
+ });
608
+ }
609
+ if (s.runsCommand && s.hasAuditWrite) {
610
+ findings.push({
611
+ filePath,
612
+ line: s.mutationLine,
613
+ method: u.name,
614
+ kind: 'double-audit',
615
+ message: `${label(u.name)} runs a Command AND records audit by hand (remove the manual record() — FR-010)`,
616
+ });
617
+ }
618
+ }
619
+ return findings;
620
+ }
621
+ /** Whether a repo-relative module path belongs to a build-breaking (migrated) module. */
622
+ export function isMigratedModulePath(relPath, migrated) {
623
+ // Not anchored on `/services/` any more: the walk reaches `routes.admin.ts`,
624
+ // `workers/` and `commands/` too, and anchoring there would have made every
625
+ // newly visible file report-only in a module that is fully migrated.
626
+ const m = /modules\/([^/]+)\//.exec(relPath.replaceAll('\\', '/'));
627
+ return m !== null && migrated.includes(m[1]);
628
+ }
629
+ /**
630
+ * Registered modules {@link collectScannedFiles} excludes wholesale, so the
631
+ * population floor asks for the tree the check actually reads.
632
+ *
633
+ * One entry, and it is the argument in the header rather than a convenience:
634
+ * the audit writer's writes *are* the audit entries. Named here so the walk and
635
+ * the floor cannot disagree about it.
636
+ */
637
+ export const EXCLUDED_MODULES = ['audit_logs'];
638
+ /**
639
+ * Directory names this rule's walk prunes wherever they occur under its root.
640
+ *
641
+ * `migrations` is the one that is a *rule* rather than a convenience: a
642
+ * migration is schema, not a service write, and judging one would put a finding
643
+ * on every table this platform creates.
644
+ *
645
+ * `test-support` is the second of that kind, and it arrived measured
646
+ * (`specs/134-paid-module-extraction/` T015). A module's test doubles and
647
+ * fixtures moved out of `backend/test/helpers/` into `src/test-support/`, which
648
+ * is inside this walk where the old location was outside it — so
649
+ * `seedXlInstallation()`, a fixture writer that has always existed, became an
650
+ * `unaudited-sensitive-write` on the day it moved and took `master` red in
651
+ * `moved-module-tree.test.ts` rather than in the check anybody had run. A
652
+ * fixture is not a service write for the same reason a migration is not: nothing
653
+ * an operator did is being recorded, and requiring a Command of one would put a
654
+ * finding on every seed in the tree. The layer is excluded exactly as
655
+ * `*.test.ts` is below, which is what it is — the test tree, one directory over.
656
+ */
657
+ export const PRUNED_DIRECTORIES = [
658
+ 'node_modules',
659
+ 'dist',
660
+ 'migrations',
661
+ 'test-support',
662
+ ];
663
+ /**
664
+ * Whether a file is in this rule's population — the **one** membership decision,
665
+ * asked by the walk and by anything that needs to know what the walk would open.
666
+ *
667
+ * It is one function called from two places on purpose, in the idiom
668
+ * `check-nul-bytes.ts` argues for in its own header: while the prune the walk
669
+ * does for speed and the rule the population states were two expressions of one
670
+ * predicate, one could go missing without the other noticing. The second caller
671
+ * is `endora check`'s package-scope floor, which has to know that a declared
672
+ * `./migrations` layer is **not** expected of this rule — otherwise a package
673
+ * that publishes migrations reports a short walk for a layer the rule excludes
674
+ * by design.
675
+ */
676
+ export function isScannedPath(absolutePath, root) {
677
+ const posix = absolutePath.replaceAll('\\', '/');
678
+ const within = posix.slice(root.replaceAll('\\', '/').length).split('/');
679
+ if (within.slice(0, -1).some((segment) => PRUNED_DIRECTORIES.includes(segment)))
680
+ return false;
681
+ if (!posix.endsWith('.ts') || posix.endsWith('.d.ts') || posix.endsWith('.test.ts'))
682
+ return false;
683
+ return !EXCLUDED_MODULES.some((id) => posix.includes(`/${id}/`));
684
+ }
685
+ /**
686
+ * Every file the check judges, under `root` (`src/modules` or `src/apps`).
687
+ *
688
+ * Exported so the check's own test can assert the **real** tree is clean rather
689
+ * than only that the analyzer can go red on a fixture — the scan scope then has
690
+ * one definition, shared by the CLI and the test.
691
+ *
692
+ * The exclusions are argued in the header; each is a claim that the category
693
+ * cannot hold an operator-visible write, not a convenience.
694
+ */
695
+ export function collectScannedFiles(root) {
696
+ const files = [];
697
+ const walk = (dir) => {
698
+ for (const name of readdirSync(dir)) {
699
+ const full = join(dir, name);
700
+ const st = statSync(full);
701
+ if (st.isDirectory()) {
702
+ if (PRUNED_DIRECTORIES.includes(name))
703
+ continue;
704
+ walk(full);
705
+ }
706
+ else if (isScannedPath(full, root)) {
707
+ files.push(full);
708
+ }
709
+ }
710
+ };
711
+ walk(root);
712
+ return files;
713
+ }
714
+ //# sourceMappingURL=command-coverage.js.map