@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,1347 @@
1
+ /**
2
+ * The package-scope hosts — one per rule whose `EstateEntry.host` is `'built'`
3
+ * (`specs/101-endora-check/contracts/package-scope-layout.md` §6).
4
+ *
5
+ * A host does three things and no more: it derives the rule's population from
6
+ * the {@link PackageLayout}, it calls the **relocated analysis** — the same
7
+ * function `backend/scripts/check-<name>.ts` calls — and it turns the result
8
+ * into a {@link RuleResult}. It re-implements no predicate. A second
9
+ * implementation of an analysis is the defect this repository is built against,
10
+ * in its purest form.
11
+ *
12
+ * The order the verdicts are decided in is `exit-reduction.md` §2's, and it is
13
+ * the same three lines in every host:
14
+ *
15
+ * 1. no declaration for the subject → `not-applicable`, naming the absent
16
+ * declaration;
17
+ * 2. a declaration and a walk that came back short → `unreadable`, exit 2;
18
+ * 3. otherwise `ran`, with the estate's own `read:` line.
19
+ */
20
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
21
+ import { dirname, join, relative, resolve, sep } from 'node:path';
22
+ import ts from 'typescript';
23
+ import { SUPPORTED_LANGUAGES } from '@endora-commerce/contracts';
24
+ import { barrelKeyOf, publishedSurface, PUBLISHED_SUBPATHS, } from '../lib/platform-surface.js';
25
+ import { declaresRegisterModule } from '../lib/module-roots.js';
26
+ import { providedPortNames } from '../lib/port-registrations.js';
27
+ import { readSizeRefusal } from '../lib/read-size.js';
28
+ import { checkBundlePairing } from '../rules/bundle-pairing.js';
29
+ import { analyzeSource as commandCoverageAnalyse, collectScannedFiles, isScannedPath as commandCoverageOpens, } from '../rules/command-coverage.js';
30
+ import { analyzeSource as containerAnalyse, collectModuleFiles, } from '../rules/container-imports.js';
31
+ import { checkNulBytes, isScannablePath, SKIPPED_DIRECTORIES, } from '../rules/nul-bytes.js';
32
+ import { analyzeSource as channelAnalyse, collectChannelSources, } from '../rules/channel-resolution.js';
33
+ import { checkDefaultLanguageProse, collectProseSources, DETECTED_LANGUAGES, isScannedPath as defaultLanguageProseOpens, ledgerKey as proseLedgerKey, } from '../rules/default-language-prose.js';
34
+ import { checkDiacriticFolds, collectFoldSources, isScannablePath as foldsScannablePath, } from '../rules/diacritic-folds.js';
35
+ import { collectEntryScopeSources, declaredProgramEntryPoints, findEntrySites, keyOf as entryScopeKeyOf, violationsOf as entryScopeViolations, } from '../rules/entry-scope.js';
36
+ import { analyzeSource as relationAnalyse, collectSources as collectRelationSources, findingKey as relationKey, isViolation as isRelationViolation, RELATION_DECORATOR_HINT, } from '../rules/kernel-boundary.js';
37
+ import { checkPlatformSurface, collectPlatformSurfaceSources, keyOf as platformSurfaceKeyOf, remedyOf as platformSurfaceRemedy, } from '../rules/platform-surface.js';
38
+ import { checkPortShape, collectPortShapeSources, } from '../rules/port-shape.js';
39
+ import { checkTransactionContext, collectTransactionSources, keyOf as transactionKeyOf, } from '../rules/transaction-context.js';
40
+ import { checkSubscribeSeam, checkWorkerSeam, collectSeamFiles, keyOf as subscriptionKeyOf, workerKeyOf, } from '../rules/subscribe-seam.js';
41
+ import { checkQueueNames, collectQueueNameFiles, keyOf as queueNameKeyOf, remedyFor as queueNameRemedy, } from '../rules/queue-names.js';
42
+ import { checkEntryPresence, collectPresenceFiles, keyOf as presenceKeyOf, remedyFor as presenceRemedy, } from '../rules/entry-presence.js';
43
+ import { checkPortCatches, collectPortCatchFiles, keyOf as portCatchKeyOf, resolvedPortNames, } from '../rules/port-catches.js';
44
+ import { readPeerOwners } from './peer-owners.js';
45
+ import { analyzeEmittedFiles, classifyFindings, declaredEntityClasses, PREFIX as TENANT_PREFIX, remedyFor as tenantRemedy, walkEmitted, } from '../rules/entity-tenant-classification.js';
46
+ import { estateEntry } from './estate.js';
47
+ import { isFile, layerExpectation, } from './layout.js';
48
+ /** The estate's `sources=` token for a package: its own `exports` map. */
49
+ const PACKAGE_EXPORTS = 'package-exports';
50
+ function entryOf(id) {
51
+ const entry = estateEntry(id);
52
+ if (entry === undefined) {
53
+ // Unreachable through `runCheck`, which iterates the estate; a host called
54
+ // directly with an unknown id must not answer with a default.
55
+ throw new Error(`[endora check] no estate entry for \`${id}\``);
56
+ }
57
+ return entry;
58
+ }
59
+ function notApplicable(id, explanation) {
60
+ return {
61
+ id,
62
+ verdict: 'not-applicable',
63
+ findings: [],
64
+ acknowledged: [],
65
+ readSize: null,
66
+ explanation,
67
+ unevaluatedSignals: entryOf(id).partial ?? [],
68
+ };
69
+ }
70
+ function unreadable(id, explanation, readSize) {
71
+ return {
72
+ id,
73
+ verdict: 'unreadable',
74
+ findings: [],
75
+ acknowledged: [],
76
+ readSize,
77
+ explanation,
78
+ unevaluatedSignals: entryOf(id).partial ?? [],
79
+ };
80
+ }
81
+ function ran(id, readSize, findings) {
82
+ return {
83
+ id,
84
+ verdict: 'ran',
85
+ findings,
86
+ acknowledged: [],
87
+ readSize,
88
+ explanation: '',
89
+ unevaluatedSignals: entryOf(id).partial ?? [],
90
+ };
91
+ }
92
+ /**
93
+ * The one sentence a `not-applicable`-from-a-declaration verdict prints.
94
+ *
95
+ * It names the declaration the rule looked for and did not find. That sentence
96
+ * is what makes the verdict auditable by its reader, and it is the whole
97
+ * difference between this verdict and a skip.
98
+ */
99
+ function absentDeclaration(entry) {
100
+ const declaration = entry.subjectDeclaration?.declaration ?? 'subject';
101
+ return `no ${declaration}: this rule has no subject in this package`;
102
+ }
103
+ /**
104
+ * Turn a walk plus its declared expectation into either `ran`'s read line or the
105
+ * short-walk refusal, in one place.
106
+ *
107
+ * `read-size.ts`'s three refusal kinds are already the estate's, and a
108
+ * `no-expectation` verdict is caught by the caller before it gets here — it is
109
+ * `not-applicable`, not a refusal (§3).
110
+ */
111
+ function readSizeOrShortWalk(id, input) {
112
+ const refusal = readSizeRefusal(input);
113
+ if (refusal === null)
114
+ return { ok: true };
115
+ return {
116
+ ok: false,
117
+ result: unreadable(id, `${refusal.message}. Supply the sources the package's \`exports\` map declares, or ` +
118
+ `withdraw the declaration.`, input),
119
+ };
120
+ }
121
+ /**
122
+ * The coverage token for a rule's walk.
123
+ *
124
+ * Three answers, and they are `exit-reduction.md` §2's three states: a
125
+ * refusal the layout could not derive at all, `null` for *no expectation* —
126
+ * which is `not-applicable`, never `0/0` — and the token.
127
+ */
128
+ function coverageOf(layout, walked, opens) {
129
+ if (layout.layerRefusal !== null)
130
+ return { refusal: layout.layerRefusal };
131
+ const expectation = layerExpectation(layout, walked, opens);
132
+ if (expectation.expected.length === 0)
133
+ return null;
134
+ return {
135
+ source: PACKAGE_EXPORTS,
136
+ expected: expectation.expected.length,
137
+ covered: expectation.covered.length,
138
+ };
139
+ }
140
+ /** `true` when {@link coverageOf} answered with a refusal rather than a token. */
141
+ function isRefusal(value) {
142
+ return value !== null && 'refusal' in value;
143
+ }
144
+ const opensTypeScript = (path) => path.endsWith('.ts') && !path.endsWith('.d.ts') && !path.endsWith('.test.ts');
145
+ /* ---------------------------------------------------------------- nul-bytes */
146
+ /**
147
+ * The one rule with no repository input at all: its population is every file the
148
+ * package holds, minus the same declared exclusions the repository host uses.
149
+ *
150
+ * `self-reported`, exactly as in this repository — nothing else derives "every
151
+ * file that is not binary", and inventing a `package-exports` expectation for it
152
+ * would be a second author that agrees with the walk by construction.
153
+ */
154
+ const nulBytes = (layout) => {
155
+ const id = 'check:nul-bytes';
156
+ const files = [];
157
+ for (const absolute of walkEverything(layout.packageRoot)) {
158
+ const path = relative(layout.packageRoot, absolute).split(sep).join('/');
159
+ if (!isScannablePath(path))
160
+ continue;
161
+ files.push({ path, bytes: readFileSync(absolute) });
162
+ }
163
+ const result = checkNulBytes(files);
164
+ const readSize = { prefix: '[nul-bytes]', files: result.scanned };
165
+ const short = readSizeOrShortWalk(id, readSize);
166
+ if (!short.ok)
167
+ return short.result;
168
+ return ran(id, readSize, result.violations.map((finding) => ({
169
+ rule: id,
170
+ key: finding.path,
171
+ location: `${finding.path}:${finding.line}:${finding.column}`,
172
+ message: `${finding.count} raw NUL byte(s). Git classifies the file as binary, so every diff ` +
173
+ `of it reads "Binary files differ" and the file stops being reviewable. Spell the ` +
174
+ `byte as \`\\0\` — the same byte at runtime, and the diff comes back.`,
175
+ })));
176
+ };
177
+ /* ----------------------------------------------------------- bundle-pairing */
178
+ /**
179
+ * The two-language floor, and the single most likely thing a stranger gets
180
+ * wrong.
181
+ *
182
+ * The subject is the manifest's `i18n.bundlesDir`, read from the **emitted**
183
+ * manifest where the package emits — the platform composes a package through its
184
+ * published artefact, so that is the value the platform will read. A package
185
+ * declaring no bundles directory owes no translation and is `not-applicable`.
186
+ *
187
+ * `undeclared-bundle-dir` is declared unevaluated on the rule's line: the
188
+ * directory names that signal probes come from *other* modules' manifests
189
+ * (`declaredBundleDirectories`), and a lone package supplies none.
190
+ */
191
+ const bundlePairing = (layout) => {
192
+ const id = 'check:bundle-pairing';
193
+ const entry = entryOf(id);
194
+ const manifest = readEmittedManifest(layout);
195
+ if (manifest === null) {
196
+ return unreadable(id, `the package's manifest could not be read at its \`exports\` root target. Build the ` +
197
+ `package (\`pnpm run build\` in its directory) — the platform composes a module ` +
198
+ `through its published artefact, so answering from source would make this verdict ` +
199
+ `differ from the platform's.`, null);
200
+ }
201
+ const bundlesDir = manifest.bundlesDir;
202
+ if (bundlesDir === null || bundlesDir.length === 0) {
203
+ return notApplicable(id, absentDeclaration(entry));
204
+ }
205
+ const languages = [...SUPPORTED_LANGUAGES];
206
+ if (languages.length === 0) {
207
+ return unreadable(id, `the platform's shipped-language set is empty, so every module is vacuously paired ` +
208
+ `and the predicate has nothing to compare. Install \`@endora-commerce/contracts\`.`, null);
209
+ }
210
+ const module = {
211
+ moduleId: layout.moduleId,
212
+ directory: layout.packageRoot,
213
+ bundlesDir,
214
+ };
215
+ const result = checkBundlePairing({ modules: [module], languages });
216
+ const readSize = {
217
+ prefix: '[bundle-pairing]',
218
+ files: result.filesRead.length,
219
+ sites: result.classified.length,
220
+ coverage: [
221
+ { source: 'shipped-languages', expected: languages.length, covered: result.languagesProbed },
222
+ ],
223
+ };
224
+ const short = readSizeOrShortWalk(id, readSize);
225
+ if (!short.ok)
226
+ return short.result;
227
+ return ran(id, readSize, result.findings.map((finding) => ({
228
+ rule: id,
229
+ key: `${finding.kind}|${finding.language ?? '-'}|${layout.keyOf(finding.path)}`,
230
+ location: layout.keyOf(finding.path),
231
+ message: `${finding.kind}: ${finding.detail}`,
232
+ })));
233
+ };
234
+ /* -------------------------------------------------------- container-imports */
235
+ const containerImports = (layout) => {
236
+ const id = 'check:container-imports';
237
+ const entry = entryOf(id);
238
+ const files = collectModuleFiles([layout.sourceRoot]);
239
+ const coverage = coverageOf(layout, files, opensTypeScript);
240
+ if (isRefusal(coverage))
241
+ return unreadable(id, coverage.refusal, null);
242
+ if (coverage === null)
243
+ return notApplicable(id, absentDeclaration(entry));
244
+ const readSize = {
245
+ prefix: '[container-imports]',
246
+ files: files.length,
247
+ coverage: [coverage],
248
+ };
249
+ const short = readSizeOrShortWalk(id, readSize);
250
+ if (!short.ok)
251
+ return short.result;
252
+ // Attribution keys on the declared `endora.id`, never on a `modules/<id>/`
253
+ // path segment: D-141 is this repository's convention and a stranger's
254
+ // checkout has no reason to hold it.
255
+ const hostResident = new Map([[layout.keyOf(layout.sourceRoot), layout.moduleId]]);
256
+ const findings = files.flatMap((file) => containerAnalyse(readFileSync(file, 'utf8'), layout.keyOf(file), hostResident));
257
+ return ran(id, readSize, findings.map((finding) => ({
258
+ rule: id,
259
+ key: `${finding.file}|${finding.specifier}`,
260
+ location: `${finding.file}:${finding.line}`,
261
+ message: `imports '${finding.specifier}'. A module sees exactly one kernel surface — ` +
262
+ `\`ModuleContext\` — and spells the container's vocabulary through \`ctx.asClass\` / ` +
263
+ `\`ctx.asFunction\` / \`ctx.asValue\`. A module that reaches the container directly ` +
264
+ `can register a route, a worker or a subscriber that never passes through its ` +
265
+ `gating seam, and nothing would notice.`,
266
+ })));
267
+ };
268
+ /* ----------------------------------------------------------- subscribe-seam */
269
+ const subscribeSeam = (layout) => {
270
+ const id = 'check:subscribe-seam';
271
+ const entry = entryOf(id);
272
+ const files = collectSeamFiles([layout.sourceRoot]);
273
+ const coverage = coverageOf(layout, files, opensTypeScript);
274
+ if (isRefusal(coverage))
275
+ return unreadable(id, coverage.refusal, null);
276
+ if (coverage === null)
277
+ return notApplicable(id, absentDeclaration(entry));
278
+ const sources = new Map(files.map((file) => [layout.keyOf(file), readFileSync(file, 'utf8')]));
279
+ const readSize = {
280
+ prefix: '[subscribe-seam]',
281
+ files: sources.size,
282
+ coverage: [coverage],
283
+ };
284
+ const short = readSizeOrShortWalk(id, readSize);
285
+ if (!short.ok)
286
+ return short.result;
287
+ const hostResidentModules = new Map([[layout.keyOf(layout.sourceRoot), layout.moduleId]]);
288
+ const input = { sources, hostResidentModules };
289
+ const subscriptions = checkSubscribeSeam(input, {});
290
+ const workers = checkWorkerSeam(input, {});
291
+ const findings = [
292
+ ...subscriptions.violations.map((site) => ({
293
+ rule: id,
294
+ key: subscriptionKeyOf(site),
295
+ location: `${site.file}:${site.line}`,
296
+ message: `${site.receiver}.on('${site.event}') is an EventBus subscription outside the ` +
297
+ `module's gating seam, so the handler keeps running with the module switched off ` +
298
+ `(Constitution XVII). Register it from \`backend.ts\` with ` +
299
+ `\`ctx.subscribe(event, handler)\`.`,
300
+ })),
301
+ ...workers.violations.map((site) => ({
302
+ rule: id,
303
+ key: workerKeyOf(site),
304
+ location: `${site.file}:${site.line}`,
305
+ message: `${site.spelling} is a BullMQ queue consumer outside the module's gating seam, so ` +
306
+ `the platform cannot stop it: it is in no per-module registry and the presence ` +
307
+ `reconcile has nothing to reconcile. Hand it to \`ctx.worker(worker)\`.`,
308
+ })),
309
+ ];
310
+ const result = ran(id, readSize, findings);
311
+ // The repository-scope host refuses a run that read no worker site at all,
312
+ // because *that* tree is known to hold queue consumers. One package holding
313
+ // none is the ordinary case, so the floor is stated rather than enforced.
314
+ if (workers.sites.length > 0) {
315
+ return { ...result, unevaluatedSignals: [] };
316
+ }
317
+ return result;
318
+ };
319
+ /* -------------------------------------------------------------- queue-names */
320
+ const queueNames = (layout) => {
321
+ const id = 'check:queue-names';
322
+ const entry = entryOf(id);
323
+ const files = collectQueueNameFiles([layout.sourceRoot]);
324
+ const coverage = coverageOf(layout, files, opensTypeScript);
325
+ if (isRefusal(coverage))
326
+ return unreadable(id, coverage.refusal, null);
327
+ if (coverage === null)
328
+ return notApplicable(id, absentDeclaration(entry));
329
+ const sources = new Map(files.map((file) => [layout.keyOf(file), readFileSync(file, 'utf8')]));
330
+ const readSize = {
331
+ prefix: '[queue-names]',
332
+ files: sources.size,
333
+ coverage: [coverage],
334
+ };
335
+ const short = readSizeOrShortWalk(id, readSize);
336
+ if (!short.ok)
337
+ return short.result;
338
+ const result = checkQueueNames({
339
+ sources,
340
+ hostResidentModules: new Map([[layout.keyOf(layout.sourceRoot), layout.moduleId]]),
341
+ });
342
+ const findings = result.findings.map((finding) => ({
343
+ rule: id,
344
+ key: queueNameKeyOf(finding.site),
345
+ location: `${finding.site.file}:${finding.site.line}`,
346
+ message: queueNameRemedy(finding),
347
+ }));
348
+ const outcome = ran(id, { ...readSize, sites: result.sites.length }, findings);
349
+ // The repository-scope host refuses a run that resolved no queue name at all,
350
+ // because *that* tree is known to hold queues. One package holding none is
351
+ // the ordinary case — most modules ship no worker — so the floor is stated
352
+ // rather than enforced.
353
+ if (result.resolved.length > 0) {
354
+ return { ...outcome, unevaluatedSignals: [] };
355
+ }
356
+ return outcome;
357
+ };
358
+ /* --------------------------------------------------------- command-coverage */
359
+ /**
360
+ * Principle XIII, and the one rule here whose repository host stages its
361
+ * findings.
362
+ *
363
+ * That staging is `MIGRATED_MODULES`, a rollout ledger of *these* modules, and a
364
+ * third-party package is in no rollout. So every finding is blocking, which is
365
+ * what `--strict` already means in this repository and what CI already runs.
366
+ */
367
+ const commandCoverage = (layout) => {
368
+ const id = 'check:command-coverage';
369
+ const entry = entryOf(id);
370
+ const files = collectScannedFiles(layout.sourceRoot);
371
+ // The rule's own membership predicate, not a `.ts` test: this walk prunes
372
+ // `migrations` by design, so a package that publishes a `./migrations` layer
373
+ // must not be reported short for a layer the rule excludes.
374
+ const coverage = coverageOf(layout, files, (path) => commandCoverageOpens(path, layout.sourceRoot));
375
+ if (isRefusal(coverage))
376
+ return unreadable(id, coverage.refusal, null);
377
+ if (coverage === null)
378
+ return notApplicable(id, absentDeclaration(entry));
379
+ const readSize = {
380
+ prefix: '[command-coverage]',
381
+ files: files.length,
382
+ coverage: [coverage],
383
+ };
384
+ const short = readSizeOrShortWalk(id, readSize);
385
+ if (!short.ok)
386
+ return short.result;
387
+ const findings = files.flatMap((file) => {
388
+ const key = layout.keyOf(file);
389
+ return commandCoverageAnalyse(key, readFileSync(file, 'utf8'));
390
+ });
391
+ return ran(id, readSize, findings.map((finding) => ({
392
+ rule: id,
393
+ key: `${finding.kind}|${finding.filePath}|${finding.method}`,
394
+ location: `${finding.filePath}:${finding.line ?? '?'}`,
395
+ message: `${finding.kind}: ${finding.message}`,
396
+ })));
397
+ };
398
+ /* ------------------------------------------------------- channel:resolution */
399
+ /**
400
+ * Constitution XII — the request's sales channel is resolved once, by the
401
+ * canonical resolver, and no module re-derives it.
402
+ *
403
+ * Unconditional: every module's backend sources are the subject, and there is
404
+ * no declaration a package can decline to make. This repository's rollout
405
+ * allow-list does not travel — a stranger's package is in no rollout of ours —
406
+ * so every violation is blocking.
407
+ */
408
+ const channelResolution = (layout) => {
409
+ const id = 'channel:resolution';
410
+ const entry = entryOf(id);
411
+ const files = collectChannelSources(layout.sourceRoot);
412
+ const coverage = coverageOf(layout, files, opensTypeScript);
413
+ if (isRefusal(coverage))
414
+ return unreadable(id, coverage.refusal, null);
415
+ if (coverage === null)
416
+ return notApplicable(id, absentDeclaration(entry));
417
+ const readSize = {
418
+ prefix: '[channel-resolution]',
419
+ files: files.length,
420
+ coverage: [coverage],
421
+ };
422
+ const short = readSizeOrShortWalk(id, readSize);
423
+ if (!short.ok)
424
+ return short.result;
425
+ const findings = files.flatMap((file) => channelAnalyse(readFileSync(file, 'utf8'), layout.keyOf(file)));
426
+ return ran(id, readSize, findings.map((violation) => ({
427
+ rule: id,
428
+ key: `${violation.file}|${violation.kind}|${violation.line}`,
429
+ location: `${violation.file}:${violation.line}`,
430
+ message: `${violation.kind}: ${violation.detail}. The current sales channel is resolved once, ` +
431
+ `by the platform's own resolver, and read through \`getResolvedChannel()\` — a module ` +
432
+ `that re-derives it answers a different question from the one the request asked ` +
433
+ `(Constitution XII).`,
434
+ })));
435
+ };
436
+ /* -------------------------------------------------- default-language-prose */
437
+ /**
438
+ * The owner ruling of 2026-09-01, clause 1: a module's own prose is English by
439
+ * default.
440
+ *
441
+ * Unconditional, and the ledger is the **package's** — this repository's shards
442
+ * are a statement about its own 46 sites and a stranger's package predates none
443
+ * of them, so the analysis is handed no shard and the author's own
444
+ * acknowledgements are applied by `ledger.ts` over every rule at once.
445
+ *
446
+ * Detection is Polish only, which is the rule's declared bound rather than this
447
+ * host's: `DETECTED_LANGUAGES` is reconciled against the platform's shipped
448
+ * languages on the `read:` line, so a third shipped language is a short walk
449
+ * here exactly as it is in this repository.
450
+ */
451
+ const defaultLanguageProse = (layout) => {
452
+ const id = 'check:default-language-prose';
453
+ const entry = entryOf(id);
454
+ const files = collectProseSources(layout.sourceRoot);
455
+ // The rule's own membership predicate, not a `.ts` test: this walk prunes
456
+ // `migrations` by design (an applied migration cannot be edited, so a ledger
457
+ // entry over a literal in one would never drain), so a package that publishes
458
+ // a `./migrations` layer must not be reported short for a layer the rule
459
+ // excludes.
460
+ const coverage = coverageOf(layout, files, (path) => defaultLanguageProseOpens(path, layout.sourceRoot));
461
+ if (isRefusal(coverage))
462
+ return unreadable(id, coverage.refusal, null);
463
+ if (coverage === null)
464
+ return notApplicable(id, absentDeclaration(entry));
465
+ const languages = [...SUPPORTED_LANGUAGES];
466
+ const detectable = languages.filter((language) => language === 'en' || DETECTED_LANGUAGES.includes(language.split('-')[0] ?? ''));
467
+ const sources = new Map(files.map((file) => [layout.keyOf(file), readFileSync(file, 'utf8')]));
468
+ const result = checkDefaultLanguageProse({ sources, languages }, [], () => layout.moduleId);
469
+ const readSize = {
470
+ prefix: '[default-language-prose]',
471
+ files: sources.size,
472
+ sites: result.classified,
473
+ coverage: [
474
+ coverage,
475
+ { source: 'detected-languages', expected: languages.length, covered: detectable.length },
476
+ ],
477
+ };
478
+ const short = readSizeOrShortWalk(id, readSize);
479
+ if (!short.ok)
480
+ return short.result;
481
+ return ran(id, readSize, result.violations.map((site) => ({
482
+ rule: id,
483
+ key: proseLedgerKey(site),
484
+ location: `${site.file}:${site.line}`,
485
+ message: `${site.kind}: ${site.language} prose in a ${site.placement} — ${site.text}`,
486
+ })));
487
+ };
488
+ /* ------------------------------------------------------- diacritic-folds */
489
+ /**
490
+ * Issues #240 and #245 — the fold and the slug builder have one owner.
491
+ *
492
+ * The package is one population root, and `SHARED_FOLD_HELPER` is *right* to
493
+ * match nothing here: a package has no exempt file. It imports `foldDiacritics`
494
+ * and `slugify` from `@endora-commerce/contracts` like every other consumer,
495
+ * which is exactly what the exemption exists to make true — so both ledgers are
496
+ * empty and every finding is blocking.
497
+ */
498
+ const diacriticFolds = (layout) => {
499
+ const id = 'check:diacritic-folds';
500
+ const entry = entryOf(id);
501
+ // One root, named after the package's own source directory, so the rule's
502
+ // population predicate answers for a package-relative key exactly as it does
503
+ // for a repo-relative one.
504
+ const rootName = layout.keyOf(layout.sourceRoot);
505
+ const roots = { [rootName]: 'the package under check — its whole source tree.' };
506
+ const scanned = [];
507
+ for (const file of collectFoldSources(layout.sourceRoot)) {
508
+ const path = layout.keyOf(file);
509
+ if (!foldsScannablePath(path, roots))
510
+ continue;
511
+ scanned.push({ path, source: readFileSync(file, 'utf8') });
512
+ }
513
+ const coverage = coverageOf(layout, scanned.map((file) => join(layout.packageRoot, file.path)),
514
+ // `layerExpectation` hands an **absolute** layer entry; the rule's predicate
515
+ // reads the key namespace the walk keyed with, so it is asked in that one.
516
+ (absolute) => foldsScannablePath(layout.keyOf(absolute), roots));
517
+ if (isRefusal(coverage))
518
+ return unreadable(id, coverage.refusal, null);
519
+ if (coverage === null)
520
+ return notApplicable(id, absentDeclaration(entry));
521
+ const result = checkDiacriticFolds(scanned, {}, {}, roots);
522
+ const readSize = {
523
+ prefix: '[diacritic-folds]',
524
+ files: result.scanned,
525
+ sites: result.replaceSites,
526
+ coverage: [coverage],
527
+ };
528
+ const short = readSizeOrShortWalk(id, readSize);
529
+ if (!short.ok)
530
+ return short.result;
531
+ return ran(id, readSize, result.violations.map((finding) => ({
532
+ rule: id,
533
+ key: `${finding.kind}|${finding.path}|${finding.literal}`,
534
+ location: `${finding.path}:${finding.line}:${finding.column}`,
535
+ message: `${finding.kind}: ${finding.literal}. Import \`foldDiacritics\` or \`slugify\` from ` +
536
+ `\`@endora-commerce/contracts\` — the obvious one-liner reads as complete and is ` +
537
+ `not: \`ł\` has no canonical decomposition, so NFD leaves it alone and the strip ` +
538
+ `has nothing to remove.`,
539
+ })));
540
+ };
541
+ /* ------------------------------------------------------------- entry-scope */
542
+ /**
543
+ * Feature 072 FR-020 — a non-HTTP entry point establishes its own scope.
544
+ *
545
+ * The subject is a `package.json` script running a source path, a worker, a
546
+ * repeating timer or a `process.on` handler. A package with none of those has
547
+ * nothing to scope, and the rule says so rather than reporting clean over a
548
+ * population it never had (`exit-reduction.md` §2).
549
+ */
550
+ const entryScope = (layout) => {
551
+ const id = 'check:entry-scope';
552
+ const entry = entryOf(id);
553
+ const files = collectEntryScopeSources(layout.sourceRoot);
554
+ const coverage = coverageOf(layout, files, (path) => path.endsWith('.ts') && !path.endsWith('.d.ts'));
555
+ if (isRefusal(coverage))
556
+ return unreadable(id, coverage.refusal, null);
557
+ if (coverage === null)
558
+ return notApplicable(id, absentDeclaration(entry));
559
+ const declared = declaredProgramsOf(layout);
560
+ const sites = files.flatMap((file) => findEntrySites(file, readFileSync(file, 'utf8'), declared, layout.keyOf));
561
+ if (sites.length === 0)
562
+ return notApplicable(id, absentDeclaration(entry));
563
+ const readSize = {
564
+ prefix: '[entry-scope]',
565
+ files: files.length,
566
+ sites: sites.length,
567
+ coverage: [coverage],
568
+ };
569
+ const short = readSizeOrShortWalk(id, readSize);
570
+ if (!short.ok)
571
+ return short.result;
572
+ return ran(id, readSize, entryScopeViolations(sites, {}).map((site) => ({
573
+ rule: id,
574
+ key: entryScopeKeyOf(site),
575
+ location: `${site.file}:${site.line}`,
576
+ message: `${site.kind}/${site.construct} in \`${site.scheduler}\` establishes no scope. A ` +
577
+ `non-HTTP entry point has no caller to answer, so it opens its own: call ` +
578
+ `\`enterPlatformScope\` or \`enterSystemScope\` in the callback itself.`,
579
+ })));
580
+ };
581
+ /* ---------------------------------------------------------- kernel-boundary */
582
+ /**
583
+ * Feature 072 D-32, rule A — an ORM relation stays inside its own module or
584
+ * points at the platform.
585
+ *
586
+ * Rules B and C are the platform roots' and a package holds none; they are
587
+ * declared unevaluated on this rule's line rather than counted zero.
588
+ */
589
+ const kernelBoundary = (layout) => {
590
+ const id = 'check:kernel-boundary';
591
+ const entry = entryOf(id);
592
+ const files = collectRelationSources(layout.sourceRoot);
593
+ const coverage = coverageOf(layout, files, opensTypeScript);
594
+ if (isRefusal(coverage))
595
+ return unreadable(id, coverage.refusal, null);
596
+ if (coverage === null)
597
+ return notApplicable(id, absentDeclaration(entry));
598
+ const hostResident = hostResidentOf(layout);
599
+ const relationFiles = files.filter((file) => RELATION_DECORATOR_HINT.test(readFileSync(file, 'utf8')));
600
+ const readSize = {
601
+ prefix: '[kernel-boundary]',
602
+ files: files.length,
603
+ sites: relationFiles.length,
604
+ coverage: [coverage],
605
+ };
606
+ const short = readSizeOrShortWalk(id, readSize);
607
+ if (!short.ok)
608
+ return short.result;
609
+ const findings = relationFiles.flatMap((file) => relationAnalyse(readFileSync(file, 'utf8'), file, hostResident));
610
+ return ran(id, readSize, findings.filter(isRelationViolation).map((finding) => ({
611
+ rule: id,
612
+ key: relationKey(finding),
613
+ location: layout.keyOf(finding.file),
614
+ message: `${finding.className}.${finding.property} (@${finding.decorator}) relates ` +
615
+ `${finding.sourceOwner} -> ${finding.targetOwner}. A relation across a module ` +
616
+ `boundary is a foreign key the ORM will create, in a schema neither module can be ` +
617
+ `detached from (Constitution I). Reach the other module through its port instead.`,
618
+ })));
619
+ };
620
+ /* --------------------------------------------------------- platform-surface */
621
+ /**
622
+ * D-160.8 — a module reaches only the platform surface the host publishes.
623
+ *
624
+ * The relative-specifier half is vacuous here and is declared vacuous rather
625
+ * than counted zero: a module in a package reaches the host by **bare**
626
+ * specifier only. The host package is the installed `@endora-commerce/platform`,
627
+ * whose barrels this run reads; without it every host reach is `unreadable` and
628
+ * no rule is reported clean on that basis.
629
+ */
630
+ const platformSurface = (layout) => {
631
+ const id = 'check:platform-surface';
632
+ const entry = entryOf(id);
633
+ const files = collectPlatformSurfaceSources(layout.sourceRoot);
634
+ const coverage = coverageOf(layout, files, opensTypeScript);
635
+ if (isRefusal(coverage))
636
+ return unreadable(id, coverage.refusal, null);
637
+ if (coverage === null)
638
+ return notApplicable(id, absentDeclaration(entry));
639
+ const host = installedPlatform(layout);
640
+ if (host === null) {
641
+ return unreadable(id, `\`@endora-commerce/platform\` is not installed beside this package, so the surface ` +
642
+ `it publishes cannot be read and a reach into it cannot be judged. Install the ` +
643
+ `platform — a shorter published set reports *fewer* findings, which is why this is ` +
644
+ `a refusal rather than a clean run.`, null);
645
+ }
646
+ const sources = new Map(files.map((file) => [layout.keyOf(file), readFileSync(file, 'utf8')]));
647
+ const readSize = {
648
+ prefix: '[platform-surface]',
649
+ files: sources.size,
650
+ coverage: [
651
+ coverage,
652
+ { source: 'platform-barrels', expected: host.barrels, covered: host.barrels },
653
+ ],
654
+ };
655
+ const short = readSizeOrShortWalk(id, readSize);
656
+ if (!short.ok)
657
+ return short.result;
658
+ const result = checkPlatformSurface({
659
+ sources,
660
+ files: new Set(sources.keys()),
661
+ surface: host.surface,
662
+ moduleIdOf: () => layout.moduleId,
663
+ canonicalTargetOf: (target) => target,
664
+ host: host.package,
665
+ platformSourceRoot: host.sourceRoot,
666
+ }, {});
667
+ return ran(id, readSize, result.violations.map((finding) => ({
668
+ rule: id,
669
+ key: platformSurfaceKeyOf(finding),
670
+ location: `${finding.file}:${finding.line}`,
671
+ message: `${finding.kind}: ${platformSurfaceRemedy(finding)}`,
672
+ })));
673
+ };
674
+ /* --------------------------------------------------------------- port-shape */
675
+ /**
676
+ * D-97.3 and issue #192 — signals 1 and 2 on a published port.
677
+ *
678
+ * Signal 3 asks whether a module resolves a container name **no contract
679
+ * publishes**, which needs the published surface of every installed peer; it is
680
+ * declared unevaluated on the rule's own line. Both ledgers and both
681
+ * platform-name sets are empty, which is what makes the two signals that do run
682
+ * blocking.
683
+ */
684
+ const portShape = (layout) => {
685
+ const id = 'check:port-shape';
686
+ const entry = entryOf(id);
687
+ const ports = layout.layers.find((layer) => layer.subpath === './ports');
688
+ if (ports === undefined)
689
+ return notApplicable(id, absentDeclaration(entry));
690
+ const portFiles = collectPortShapeSources(ports.directory);
691
+ const moduleFiles = collectPortShapeSources(layout.sourceRoot);
692
+ const coverage = coverageOf(layout, moduleFiles, opensTypeScript);
693
+ if (isRefusal(coverage))
694
+ return unreadable(id, coverage.refusal, null);
695
+ if (coverage === null)
696
+ return notApplicable(id, absentDeclaration(entry));
697
+ const readSize = {
698
+ prefix: '[port-shape]',
699
+ files: moduleFiles.length,
700
+ sites: portFiles.length,
701
+ coverage: [
702
+ coverage,
703
+ { source: 'ports-subpaths', expected: 1, covered: portFiles.length > 0 ? 1 : 0 },
704
+ ],
705
+ };
706
+ const short = readSizeOrShortWalk(id, readSize);
707
+ if (!short.ok)
708
+ return short.result;
709
+ const keyed = (list) => new Map(list.map((file) => [layout.keyOf(file), readFileSync(file, 'utf8')]));
710
+ const result = checkPortShape({
711
+ contracts: new Map(),
712
+ modules: keyed(moduleFiles),
713
+ modulePorts: keyed(portFiles),
714
+ hostResidentModules: hostResidentOf(layout),
715
+ });
716
+ // Signal 1 only. Signal 2 compares a doc block's container name to the name
717
+ // the port is **registered** under, and a port's provider is routinely another
718
+ // module — `orders` publishes `PaymentPlacementApplyPort` and `payments`
719
+ // registers it — so over one package every such port reads
720
+ // `container-name-unregistered`. That is the state §5 of
721
+ // `contracts/package-scope-layout.md` rules on: a port whose owner is not
722
+ // installed is `unreadable` **for that edge**, never unowned, because the
723
+ // wiring may be right and the map short. Both signals are declared on the
724
+ // rule's own line rather than counted zero.
725
+ return ran(id, readSize, result.findings.map((finding) => ({
726
+ rule: id,
727
+ key: `${finding.kind}|${finding.portName}|${finding.member}`,
728
+ location: `${finding.file}:${finding.line}`,
729
+ message: `${finding.kind}: ${finding.portName}.${finding.member} — feature detection through a ` +
730
+ `port is impossible by construction. \`lazyPort\`'s proxy answers every property ` +
731
+ `with a function, so \`if (port.maybe)\` is always true and the forward throws when ` +
732
+ `the provider has none.`,
733
+ })));
734
+ };
735
+ /* ------------------------------------------------------ transaction-context */
736
+ /**
737
+ * Issue #200 — SQL written inside a transaction that does not run inside it.
738
+ *
739
+ * The subject is the package's backend sources, which is what an `exports`
740
+ * subpath publishing them declares. The ledger argument is the **package's**,
741
+ * never this repository's: `CONNECTION_LEVEL_SQL_IN_TRANSACTIONS` is a statement
742
+ * about this tree's debt and a stranger's package predates none of it, so the
743
+ * analysis is handed an empty map and the author's own acknowledgements are
744
+ * applied later, by `ledger.ts`, over every rule at once.
745
+ */
746
+ const transactionContext = (layout) => {
747
+ const id = 'check:transaction-context';
748
+ const entry = entryOf(id);
749
+ const files = collectTransactionSources(layout.sourceRoot);
750
+ const coverage = coverageOf(layout, files, opensTypeScript);
751
+ if (isRefusal(coverage))
752
+ return unreadable(id, coverage.refusal, null);
753
+ if (coverage === null)
754
+ return notApplicable(id, absentDeclaration(entry));
755
+ const sources = new Map(files.map((file) => [layout.keyOf(file), readFileSync(file, 'utf8')]));
756
+ const readSize = {
757
+ prefix: '[transaction-context]',
758
+ files: sources.size,
759
+ coverage: [coverage],
760
+ };
761
+ const short = readSizeOrShortWalk(id, readSize);
762
+ if (!short.ok)
763
+ return short.result;
764
+ const result = checkTransactionContext({ sources }, {});
765
+ return ran(id, readSize, result.violations.map((escape) => ({
766
+ rule: id,
767
+ key: transactionKeyOf(escape),
768
+ location: `${escape.file}:${escape.line}`,
769
+ message: `[${escape.scope}/${escape.shape}/${escape.direction}] ${escape.statement} — a ` +
770
+ `connection-level handle takes its own pooled connection, so this statement ` +
771
+ `commits the moment it runs: the enclosing rollback cannot reach it and a read ` +
772
+ `cannot see what the transaction has written. Use the EntityManager's own ` +
773
+ `\`em.execute(sql, params)\`, which passes the transaction context and is ` +
774
+ `identical outside a transaction.`,
775
+ })));
776
+ };
777
+ /** `[<package-relative source root>] -> <module id>`, the attribution a package declares. */
778
+ function hostResidentOf(layout) {
779
+ return new Map([[layout.keyOf(layout.sourceRoot), layout.moduleId]]);
780
+ }
781
+ /**
782
+ * The `package.json` scripts that run a source path — `check:entry-scope`'s
783
+ * second population source (issue #228), spelled the same way its keys are.
784
+ */
785
+ function declaredProgramsOf(layout) {
786
+ try {
787
+ const manifest = JSON.parse(readFileSync(join(layout.packageRoot, 'package.json'), 'utf8'));
788
+ return new Set(declaredProgramEntryPoints(JSON.stringify({ scripts: manifest.scripts ?? {} })));
789
+ }
790
+ catch {
791
+ return new Set();
792
+ }
793
+ }
794
+ /**
795
+ * The installed `@endora-commerce/platform`, its published barrels and the
796
+ * surface they declare — or `null` when it is not installed.
797
+ *
798
+ * `check:platform-surface` refuses rather than degrades on its absence, because
799
+ * a shorter published set reports *fewer* findings: the obvious repair for a
800
+ * finding is to widen the barrel, and the whole of D-160.8 is that the barrel is
801
+ * not widened quietly.
802
+ */
803
+ function installedPlatform(layout) {
804
+ const dir = join(layout.packageRoot, 'node_modules', '@endora-commerce', 'platform');
805
+ if (!isFile(join(dir, 'package.json')))
806
+ return null;
807
+ let manifest;
808
+ try {
809
+ manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
810
+ }
811
+ catch {
812
+ return null;
813
+ }
814
+ const name = manifest.name;
815
+ if (typeof name !== 'string')
816
+ return null;
817
+ const barrelSources = new Map();
818
+ const barrelTargets = new Map();
819
+ for (const subpath of PUBLISHED_SUBPATHS) {
820
+ const source = join(dir, 'src', subpath, 'index.ts');
821
+ if (!isFile(source))
822
+ continue;
823
+ barrelSources.set(barrelKeyOf(subpath), readFileSync(source, 'utf8'));
824
+ barrelTargets.set(subpath, barrelKeyOf(subpath));
825
+ }
826
+ if (barrelSources.size === 0)
827
+ return null;
828
+ // The installed host's own `exports` map, which declares more than the five
829
+ // barrels: `./composition` is host composition surface no module may name
830
+ // (D-160.14), and telling its author that the map "refuses the path at
831
+ // resolution time" would be false — it resolves.
832
+ const declaredSubpaths = new Set(Object.keys(manifest.exports ?? {})
833
+ .filter((key) => key.startsWith('./') && key !== './package.json')
834
+ .map((key) => key.slice(2)));
835
+ const surface = publishedSurface(barrelSources, (fromKey, specifier) => resolvePlatformTarget(fromKey, specifier, dir));
836
+ return {
837
+ package: { name, subpathTargets: barrelTargets, declaredSubpaths },
838
+ surface,
839
+ sourceRoot: 'src',
840
+ barrels: barrelSources.size,
841
+ };
842
+ }
843
+ /** A barrel's relative specifier, resolved against the installed platform's sources. */
844
+ function resolvePlatformTarget(fromKey, specifier, dir) {
845
+ const from = join(dir, 'src', fromKey.replace(/^src\//, ''));
846
+ for (const candidate of [
847
+ resolve(dirname(from), `${specifier.replace(/\.js$/, '')}.ts`),
848
+ resolve(dirname(from), specifier.replace(/\.js$/, ''), 'index.ts'),
849
+ ]) {
850
+ if (isFile(candidate))
851
+ return posixKey(relative(dir, candidate));
852
+ }
853
+ return null;
854
+ }
855
+ function posixKey(path) {
856
+ return path.split(sep).join('/');
857
+ }
858
+ /**
859
+ * The module manifest, read where the **platform** reads it.
860
+ *
861
+ * A rule whose subject is something the platform *loads* reads the emitted file
862
+ * — that is not a shortcut, it is the only thing a published package has, and
863
+ * answering from source would make this command's verdict differ from the
864
+ * platform's, which is the one thing a conformance command must not do
865
+ * (`contracts/package-scope-layout.md` §4). The artefact's currency is decided
866
+ * first: a source strictly newer than its emitted target is `stale-artefact`,
867
+ * and is never answered from source as a convenience.
868
+ *
869
+ * It is read as **text through the compiler API** rather than `await import`ed,
870
+ * for two reasons and not one. A host is synchronous, which is Phase 1's shape
871
+ * and the reason the whole estate can be iterated in one pass; and an import
872
+ * evaluates whatever the artefact's own import graph reaches, which for a module
873
+ * is the platform an author may not have installed. Reading literal AST nodes
874
+ * costs the same discipline every analysis in this estate already keeps.
875
+ *
876
+ * **One reader, and it fails closed.** A field it cannot read as a literal is
877
+ * `null` / absent rather than guessed at, and the rule that needs it reports
878
+ * `unreadable` — never a clean run over a manifest this run could not see.
879
+ */
880
+ function readEmittedManifest(layout) {
881
+ const root = layout.layers.find((layer) => layer.subpath === '.');
882
+ if (root === undefined)
883
+ return null;
884
+ const artefact = join(layout.packageRoot, ...root.target.replace(/^\.\//, '').split('/'));
885
+ try {
886
+ const artefactStat = statSync(artefact);
887
+ const sourceStat = statSync(root.entry);
888
+ if (sourceStat.mtimeMs > artefactStat.mtimeMs)
889
+ return null;
890
+ }
891
+ catch {
892
+ return null;
893
+ }
894
+ const sf = ts.createSourceFile(artefact, readFileSync(artefact, 'utf8'), ts.ScriptTarget.Latest, true);
895
+ let bundlesDir = null;
896
+ let nonDeactivatable = false;
897
+ const actions = [];
898
+ const literal = (node) => node !== undefined && ts.isStringLiteralLike(node) ? node.text : null;
899
+ const property = (object, name) => object.properties.find((member) => ts.isPropertyAssignment(member) &&
900
+ (ts.isIdentifier(member.name) || ts.isStringLiteral(member.name)) &&
901
+ member.name.text === name)?.initializer;
902
+ const visit = (node) => {
903
+ if (ts.isObjectLiteralExpression(node)) {
904
+ const dir = literal(property(node, 'bundlesDir'));
905
+ if (dir !== null)
906
+ bundlesDir = dir;
907
+ const activation = property(node, 'activation');
908
+ if (activation !== undefined && ts.isObjectLiteralExpression(activation)) {
909
+ const locked = property(activation, 'nonDeactivatable');
910
+ if (locked !== undefined && locked.kind === ts.SyntaxKind.TrueKeyword) {
911
+ nonDeactivatable = true;
912
+ }
913
+ }
914
+ const declared = property(node, 'actions');
915
+ if (declared !== undefined && ts.isArrayLiteralExpression(declared)) {
916
+ for (const element of declared.elements) {
917
+ if (!ts.isObjectLiteralExpression(element))
918
+ continue;
919
+ const actionId = literal(property(element, 'id'));
920
+ const targetRoute = literal(property(element, 'targetRoute'));
921
+ if (actionId === null || targetRoute === null)
922
+ continue;
923
+ const required = literal(property(element, 'requiredPermission'));
924
+ actions.push({
925
+ id: actionId,
926
+ targetRoute,
927
+ ...(required === null ? {} : { requiredPermission: required }),
928
+ });
929
+ }
930
+ }
931
+ }
932
+ node.forEachChild(visit);
933
+ };
934
+ sf.forEachChild(visit);
935
+ return { bundlesDir, actions, nonDeactivatable };
936
+ }
937
+ /**
938
+ * Every file under `dir`, with the rule's own declared directory prunes applied.
939
+ *
940
+ * The prune is for speed and the population is {@link isScannablePath}'s, which
941
+ * is the rule — one decision, taken in one place, exactly as the repository
942
+ * host's walk takes it.
943
+ */
944
+ function walkEverything(dir, out = []) {
945
+ let entries;
946
+ try {
947
+ entries = readdirSync(dir);
948
+ }
949
+ catch {
950
+ return out;
951
+ }
952
+ for (const name of entries) {
953
+ if (SKIPPED_DIRECTORIES[name] !== undefined)
954
+ continue;
955
+ const full = join(dir, name);
956
+ let stat;
957
+ try {
958
+ stat = statSync(full);
959
+ }
960
+ catch {
961
+ continue;
962
+ }
963
+ if (stat.isDirectory())
964
+ walkEverything(full, out);
965
+ else if (stat.isFile())
966
+ out.push(full);
967
+ }
968
+ return out;
969
+ }
970
+ /**
971
+ * Every declared layer's artefact, with its currency decided.
972
+ *
973
+ * Decided per layer rather than for the package as a whole, because the two
974
+ * refusals name different remedies: `absent` says *build the package*, `stale`
975
+ * says *rebuild it*, and answering either from source would make this command's
976
+ * verdict differ from the platform's (§4).
977
+ */
978
+ function artefactStates(layout) {
979
+ return layout.layers.map((layer) => {
980
+ const artefact = join(layout.packageRoot, ...layer.target.replace(/^\.\//, '').split('/'));
981
+ let state;
982
+ try {
983
+ const artefactStat = statSync(artefact);
984
+ const sourceStat = statSync(layer.entry);
985
+ state = sourceStat.mtimeMs > artefactStat.mtimeMs ? 'stale' : 'current';
986
+ }
987
+ catch {
988
+ state = 'absent';
989
+ }
990
+ return { layer, artefact, state };
991
+ });
992
+ }
993
+ /**
994
+ * Principle XI's out-of-tree instrument, and the reason Phase 3 exists.
995
+ *
996
+ * The subject is *the entity classes this package hands the ORM*, so the read is
997
+ * the **artefact** (§4) — the platform composes a module package through its
998
+ * published output, and a rule that answered from source would pass a build that
999
+ * dropped the decorators, which is precisely the silence this rule exists to
1000
+ * remove.
1001
+ *
1002
+ * ## Which subpath, derived rather than spelled
1003
+ *
1004
+ * Nothing here spells `./backend`. The subpath that publishes entity classes is
1005
+ * *the one whose artefact declares an `entities` array* — the package's own
1006
+ * statement about itself, and the estate's own words for the declaration
1007
+ * (`subjectDeclaration`). A package whose artefacts declare none publishes no
1008
+ * entity class and is `not-applicable`; a package with an artefact this run
1009
+ * could not read is `unreadable`, because the layer it could not read is a layer
1010
+ * that might have been the one.
1011
+ *
1012
+ * ## The floor is the composition's own list
1013
+ *
1014
+ * `entities` is a finer and more independent author than the layer count: the
1015
+ * walk deliberately reaches exactly one layer, so a per-layer expectation would
1016
+ * read `1/1` by construction — the tautology §3 refuses. `declared-entities`
1017
+ * compares the classes the walk classified against the classes the composition
1018
+ * hands the ORM, and a short answer is a refusal. That is also the guard on the
1019
+ * emit shape itself: if TypeScript's decorator lowering changed, this walk would
1020
+ * classify nothing and the token would say so, rather than the run reporting
1021
+ * every entity classified over an artefact it could not read.
1022
+ */
1023
+ const entityTenantClassification = (layout) => {
1024
+ const id = 'check-entity-tenant-classification';
1025
+ const entry = entryOf(id);
1026
+ if (layout.layerRefusal !== null)
1027
+ return unreadable(id, layout.layerRefusal, null);
1028
+ const states = artefactStates(layout);
1029
+ const stale = states.filter((state) => state.state === 'stale');
1030
+ if (stale.length > 0) {
1031
+ return unreadable(id, `the source behind ${stale
1032
+ .map((state) => state.layer.subpath)
1033
+ .join(', ')} is newer than the artefact the platform would load, so what this run read ` +
1034
+ `is not what the platform reads. Rebuild the package; this rule is never answered from ` +
1035
+ `source as a convenience.`, null);
1036
+ }
1037
+ const absent = states.filter((state) => state.state === 'absent');
1038
+ if (absent.length > 0) {
1039
+ return unreadable(id, `${absent
1040
+ .map((state) => state.layer.subpath)
1041
+ .join(', ')} is declared in the \`exports\` map and its target is not on disk, so the ` +
1042
+ `subpath that publishes this package's entity classes may be one this run could not ` +
1043
+ `open. Build the package (\`pnpm run build\` in its directory).`, null);
1044
+ }
1045
+ const publishing = states
1046
+ .map((state) => ({
1047
+ state,
1048
+ declared: declaredEntityClasses(readFileSync(state.artefact, 'utf8'), state.artefact),
1049
+ }))
1050
+ .find((candidate) => candidate.declared.length > 0);
1051
+ if (publishing === undefined)
1052
+ return notApplicable(id, absentDeclaration(entry));
1053
+ const files = walkEmitted(dirname(publishing.state.artefact));
1054
+ const findings = analyzeEmittedFiles(files);
1055
+ const classified = new Set(findings.map((finding) => finding.className));
1056
+ const readSize = {
1057
+ prefix: TENANT_PREFIX,
1058
+ files: files.length,
1059
+ sites: findings.length,
1060
+ coverage: [
1061
+ {
1062
+ source: 'declared-entities',
1063
+ expected: publishing.declared.length,
1064
+ covered: publishing.declared.filter((name) => classified.has(name)).length,
1065
+ },
1066
+ ],
1067
+ };
1068
+ const short = readSizeRefusal(readSize);
1069
+ if (short !== null) {
1070
+ return unreadable(id, `${short.message}. The classes this package's composition hands the ORM are the ` +
1071
+ `independent second author here: ${publishing.declared
1072
+ .filter((name) => !classified.has(name))
1073
+ .join(', ')} could not be read out of the artefact.`, readSize);
1074
+ }
1075
+ const outcome = classifyFindings(findings);
1076
+ return ran(id, readSize, [...outcome.unclassified, ...outcome.multiple].map((finding) => ({
1077
+ rule: id,
1078
+ key: `${layout.moduleId}|${finding.className}`,
1079
+ location: layout.keyOf(finding.file),
1080
+ message: tenantRemedy(finding),
1081
+ })));
1082
+ };
1083
+ /* --------------------------------------------- entry-presence (Phase 3) */
1084
+ /**
1085
+ * Constitution XVII's hardest seam: an entry point nothing can catch a throw
1086
+ * from decides presence *before* it works.
1087
+ *
1088
+ * ## Unconditional, and that is a correction
1089
+ *
1090
+ * The estate filed this rule's subject as *"module manifest declaring an
1091
+ * activation control"*, and a host built on that reading would have answered
1092
+ * `not-applicable` for a `nonDeactivatable` package. It is wrong, and the rule's
1093
+ * own input says so: `lockedModules` takes a module's **boot hooks** out of the
1094
+ * population and leaves its **timers** in — `_lifecycle`'s lease heartbeat is
1095
+ * ledgered rather than exempted, which is only meaningful because a locked
1096
+ * module's timers are still judged. So the declaration is corrected to `null`
1097
+ * and the manifest decides an *exemption*, not applicability.
1098
+ *
1099
+ * ## The manifest is read where it can be, and its absence over-reports loudly
1100
+ *
1101
+ * `lockedModules` defaults to empty on purpose: forgetting it widens the
1102
+ * population rather than narrowing it, so a caller that cannot read the manifest
1103
+ * over-reports instead of going quietly blind. A package whose artefact is not
1104
+ * current therefore still gets a run — the rule's subject is source text — and
1105
+ * the `locked-owner-exemption` signal is printed on its own line, because a
1106
+ * finding an author cannot reproduce is one they learn to ignore (§5.1). Where
1107
+ * the manifest *is* read the signal is dropped, which is what makes its presence
1108
+ * mean something.
1109
+ *
1110
+ * ## The ledger
1111
+ *
1112
+ * The repository's two ledgers are not consulted: they are entries about *these*
1113
+ * modules and a third-party package is in none of them. The package's own ledger
1114
+ * is applied by the frame, one layer up, exactly as for every other rule.
1115
+ */
1116
+ const entryPresence = (layout) => {
1117
+ const id = 'check:entry-presence';
1118
+ const files = collectPresenceFiles([layout.sourceRoot]);
1119
+ const coverage = coverageOf(layout, files, opensTypeScript);
1120
+ if (isRefusal(coverage))
1121
+ return unreadable(id, coverage.refusal, null);
1122
+ if (coverage === null)
1123
+ return notApplicable(id, absentDeclaration(entryOf(id)));
1124
+ // Keyed **relative to the source root**, which is what the repository host's
1125
+ // keys are relative to (`modules/<id>/…` under `src/`). The analysis attributes
1126
+ // a file to a module through `moduleOf`, whose second source matches on the
1127
+ // tail after the last `/src/` — so a package-relative key would hand it
1128
+ // `/src/src/backend/…` and it would attribute nothing, and the rule would
1129
+ // report `violations=0` over every package for ever.
1130
+ const sources = new Map(files.map((file) => [posixKey(relative(layout.sourceRoot, file)), readFileSync(file, 'utf8')]));
1131
+ const readSize = {
1132
+ prefix: '[entry-presence]',
1133
+ files: sources.size,
1134
+ coverage: [coverage],
1135
+ };
1136
+ const short = readSizeOrShortWalk(id, readSize);
1137
+ if (!short.ok)
1138
+ return short.result;
1139
+ // Every top-level name under the source root belongs to this one module — the
1140
+ // package half of `module-roots`' own derivation (D-142: identity is
1141
+ // `endora.id`, never a directory name), taken off the walk rather than spelled.
1142
+ const hostResidentModules = new Map();
1143
+ for (const key of sources.keys()) {
1144
+ const [head] = key.split('/');
1145
+ if (head !== undefined && head.length > 0)
1146
+ hostResidentModules.set(head, layout.moduleId);
1147
+ }
1148
+ const manifest = readEmittedManifest(layout);
1149
+ const result = checkEntryPresence({
1150
+ sources,
1151
+ lockedModules: manifest !== null && manifest.nonDeactivatable ? new Set([layout.moduleId]) : new Set(),
1152
+ hostResidentModules,
1153
+ }, {});
1154
+ const outcome = ran(id, readSize, result.violations.map((finding) => ({
1155
+ rule: id,
1156
+ key: presenceKeyOf(finding),
1157
+ location: `${layout.keyOf(join(layout.sourceRoot, finding.file))}:${finding.line}`,
1158
+ message: presenceRemedy(finding),
1159
+ })));
1160
+ // The exemption was derived, so the degradation does not apply to this run.
1161
+ if (manifest !== null)
1162
+ return { ...outcome, unevaluatedSignals: [] };
1163
+ return outcome;
1164
+ };
1165
+ /* ------------------------------------------------ port-catches (Phase 3) */
1166
+ /**
1167
+ * A `catch` around a gated-port call may not swallow the module's presence
1168
+ * answer — Constitution XVII at the one seam where the throw has somewhere to go
1169
+ * and gets eaten anyway.
1170
+ *
1171
+ * ## The peer owner map is not optional here, and the measurement says why
1172
+ *
1173
+ * The analysis admits `lazyPort(ctx, '<name>')` as a port only when `<name>` is
1174
+ * in its owner map, and that map is built from `di.providePort` in the **owner's**
1175
+ * composing file. So a consuming package seeds nothing on its own. Over this
1176
+ * repository's module packages: 226 sites attributed to 41 packages by the
1177
+ * whole-tree run, **19** in 10 packages when each is analysed alone, and **31 of
1178
+ * the 41 lose every site** — the four PIM connectors and `product_feeds` among
1179
+ * them. {@link readPeerOwners} is the input that closes it, out of the installed
1180
+ * and workspace peers, synchronously and with no new dependency.
1181
+ *
1182
+ * ## `sources=owners:<n>/<m>`, and the floor that comes with it
1183
+ *
1184
+ * The estate's idiom for *"I read n of the m things I needed"* is the `read:`
1185
+ * line's `sources=` token, and this rule's `m` is the number of gated-port names
1186
+ * the package **writes** ({@link resolvedPortNames}) — a denominator the package
1187
+ * itself authors, independent of any owner map. `n` is how many of those an owner
1188
+ * could be found for. A reader, and a later ratchet, can then see a partially
1189
+ * resolved run for what it is instead of taking a caveat in prose on trust.
1190
+ *
1191
+ * The floor falls out of the same idiom rather than being bolted on: **`n === 0`
1192
+ * with `m > 0` is `unreadable`**, because a run that resolved none of the names
1193
+ * it was asked to resolve has judged nothing — which is `read-size.ts`'
1194
+ * `read-nothing` refusal one granularity in, and the same refusal the repository
1195
+ * host makes about an owner map that resolved zero. A package that writes no
1196
+ * `lazyPort` name at all has nothing to resolve and is not short: `m === 0` is
1197
+ * `ran`, and the token is omitted rather than printed `0/0`.
1198
+ *
1199
+ * ## `OWNER LOCKED` still over-reports, and still says so
1200
+ *
1201
+ * The estate's `owner-locked-merge` signal is unchanged and is printed whenever
1202
+ * the peers' **manifests** were not read — which is every package-scope run,
1203
+ * because `readPeerOwners` reads registrations out of artefacts and locks out of
1204
+ * manifests are a separate question. Absent, a locked owner's site reads as a
1205
+ * violation rather than as retired: over-reporting, the safe direction, declared.
1206
+ */
1207
+ const portCatches = (layout) => {
1208
+ const id = 'check:port-catches';
1209
+ const files = collectPortCatchFiles([layout.sourceRoot]);
1210
+ const layerCoverage = coverageOf(layout, files, opensTypeScript);
1211
+ if (isRefusal(layerCoverage))
1212
+ return unreadable(id, layerCoverage.refusal, null);
1213
+ if (layerCoverage === null)
1214
+ return notApplicable(id, absentDeclaration(entryOf(id)));
1215
+ // Keyed relative to the source root, for the same reason `check:entry-presence`
1216
+ // is: `moduleOf` matches on the tail after the last `/src/`, and a
1217
+ // package-relative key would attribute every file to no module.
1218
+ const sources = new Map(files.map((file) => [posixKey(relative(layout.sourceRoot, file)), readFileSync(file, 'utf8')]));
1219
+ const hostResidentModules = new Map();
1220
+ for (const key of sources.keys()) {
1221
+ const [head] = key.split('/');
1222
+ if (head !== undefined && head.length > 0)
1223
+ hostResidentModules.set(head, layout.moduleId);
1224
+ }
1225
+ const peers = readPeerOwners(layout);
1226
+ const written = resolvedPortNames(sources);
1227
+ const attributed = [...written].filter((name) => peers.portOwners.has(name) || ownProvidedNames(sources).has(name));
1228
+ const coverage = [layerCoverage];
1229
+ if (written.size > 0) {
1230
+ coverage.push({ source: 'owners', expected: written.size, covered: attributed.length });
1231
+ }
1232
+ const readSize = {
1233
+ prefix: '[port-catches]',
1234
+ files: sources.size + peers.filesRead,
1235
+ coverage,
1236
+ };
1237
+ // **The `owners` token is printed but is deliberately not put through the
1238
+ // short-walk refusal**, and the distinction is the whole of the owner-map
1239
+ // ruling. `read-size.ts` refuses any `covered < expected`, which would make a
1240
+ // package with one unresolved peer lose the rule entirely — fail-closed in the
1241
+ // wrong dimension, trading a small honest gap for a total blind spot, and for a
1242
+ // package reaching another paid module that is the common case. So the layer
1243
+ // walk keeps the standard refusal, and `owners` carries its own floor:
1244
+ //
1245
+ // * `covered > 0` → `ran`, with the fraction stating its own incompleteness
1246
+ // in a form something can ratchet on later;
1247
+ // * `covered === 0` with `expected > 0` → `unreadable`, because a run that
1248
+ // attributed none of the names it was asked to attribute has judged
1249
+ // nothing. That is `read-nothing` one granularity in, and the same refusal
1250
+ // the repository host makes about an owner map that resolved zero;
1251
+ // * `expected === 0` → nothing to resolve, token omitted, never `0/0`.
1252
+ const short = readSizeOrShortWalk(id, { ...readSize, coverage: [layerCoverage] });
1253
+ if (!short.ok)
1254
+ return short.result;
1255
+ const unattributed = [...written].filter((name) => !attributed.includes(name));
1256
+ // **The floor is "no owner map was read", not "these names did not resolve",
1257
+ // and the difference is five false refusals.** Built the second way first, and
1258
+ // measured: `audit_logs`, `google_tag_manager`, `linkedin_ads`, `meta_ads` and
1259
+ // `prompt_actions` each resolve exactly one gated name, `settingsReadPort` —
1260
+ // which the **platform** owns (`compose-app.ts` contributes it), not an
1261
+ // uninstalled module. All five refused, and the remedy they printed, *install
1262
+ // the owning module*, named work an author cannot do and does not need to.
1263
+ //
1264
+ // **The wrong remedy was worse than the wrong verdict.** A refusal telling an
1265
+ // author to install a module they do not need and cannot identify is a worse
1266
+ // outcome than a `ran` line that states what it could not attribute — the first
1267
+ // sends them looking for something that is not missing, the second tells them
1268
+ // exactly what this run did and did not judge. That is §5.1's failure, where a
1269
+ // finding an author cannot reproduce is one they learn to ignore, and it is the
1270
+ // reason the floor moved rather than the verdict being softened.
1271
+ //
1272
+ // So the refusal is the honest analogue of `read-nothing`: the run consulted an
1273
+ // owner map and the map was **empty**. That keeps the fixture case — a package
1274
+ // resolving a peer's port with no peer installed anywhere — and drops all five
1275
+ // false ones, because those runs read 174 owners and simply do not own this
1276
+ // name between them.
1277
+ //
1278
+ // **What this leaves, stated rather than discovered:** a package whose every
1279
+ // resolved name belongs to an uninstalled *module*, in a tree that has other
1280
+ // peers, reads `owners:0/1` on a `ran` line rather than refusing. It is printed
1281
+ // and named, not silent. Closing it properly means deriving the platform's own
1282
+ // registrations — `PLATFORM_OWNED_NAMES` is today a curated list because
1283
+ // `compose-app.ts` registers them through `contribute({ … })`, which neither
1284
+ // `registeredNames` nor `providedPortNames` recognises — and that recogniser is
1285
+ // the next thing to build here, not a weakening to accept for ever.
1286
+ if (written.size > 0 && attributed.length === 0 && peers.portOwners.size === 0) {
1287
+ return unreadable(id, `this package resolves ${written.size} gated port name(s) — ${unattributed.join(', ')} — ` +
1288
+ `and no owner map could be read at all: no module package was found installed beside ` +
1289
+ `it or in a workspace with it. The analysis admits none of those calls as a port, so ` +
1290
+ `every \`catch\` around one is unjudged. That is not clean: install the modules that ` +
1291
+ `own them, or read this rule as not having run.`, readSize);
1292
+ }
1293
+ const result = checkPortCatches({ sources, hostResidentModules, peerOwners: peers.portOwners }, {});
1294
+ const findings = result.violations.map((finding) => ({
1295
+ rule: id,
1296
+ key: portCatchKeyOf(finding),
1297
+ location: `${layout.keyOf(join(layout.sourceRoot, finding.file))}:${finding.line}`,
1298
+ message: `a \`catch\` around \`${finding.port}\` (${finding.form}) swallows ` +
1299
+ `\`ModuleDisabledError\`, so a switched-off owner reads as "no data" instead of "this ` +
1300
+ `capability is off" (Constitution XVII). Delete it if it was only defensive, move the ` +
1301
+ `degrade into the port's return type, or keep it and add ` +
1302
+ `\`rethrowIfModuleDisabled(error)\`.`,
1303
+ }));
1304
+ const outcome = ran(id, { ...readSize, sites: result.total }, findings);
1305
+ if (unattributed.length === 0)
1306
+ return outcome;
1307
+ // Partially resolved. The fraction is already on the `read:` line; this names
1308
+ // the ones, because "owners:3/5" does not tell an author which two to install.
1309
+ return {
1310
+ ...outcome,
1311
+ explanation: `${unattributed.length} of the ${written.size} gated port name(s) this package resolves ` +
1312
+ `could not be attributed to an owner — ${unattributed.join(', ')}. Each is either a ` +
1313
+ `module you have not installed or a name the platform itself registers, and this run ` +
1314
+ `cannot tell those apart; a \`catch\` around one of them is unjudged rather than clean.`,
1315
+ };
1316
+ };
1317
+ /** The gated port names the package's **own** composing files provide. */
1318
+ function ownProvidedNames(sources) {
1319
+ const names = new Set();
1320
+ for (const [file, text] of sources) {
1321
+ if (!declaresRegisterModule(text))
1322
+ continue;
1323
+ for (const name of providedPortNames(text, file))
1324
+ names.add(name);
1325
+ }
1326
+ return names;
1327
+ }
1328
+ export const PACKAGE_HOSTS = new Map([
1329
+ ['channel:resolution', channelResolution],
1330
+ ['check-entity-tenant-classification', entityTenantClassification],
1331
+ ['check:bundle-pairing', bundlePairing],
1332
+ ['check:command-coverage', commandCoverage],
1333
+ ['check:container-imports', containerImports],
1334
+ ['check:default-language-prose', defaultLanguageProse],
1335
+ ['check:entry-presence', entryPresence],
1336
+ ['check:diacritic-folds', diacriticFolds],
1337
+ ['check:entry-scope', entryScope],
1338
+ ['check:kernel-boundary', kernelBoundary],
1339
+ ['check:nul-bytes', nulBytes],
1340
+ ['check:platform-surface', platformSurface],
1341
+ ['check:port-catches', portCatches],
1342
+ ['check:port-shape', portShape],
1343
+ ['check:queue-names', queueNames],
1344
+ ['check:subscribe-seam', subscribeSeam],
1345
+ ['check:transaction-context', transactionContext],
1346
+ ]);
1347
+ //# sourceMappingURL=hosts.js.map