@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,830 @@
1
+ /**
2
+ * CI check — a module package's process singletons exist once (feature 080, T061;
3
+ * D-160.6 widened past entity classes).
4
+ *
5
+ * ## The mechanism
6
+ *
7
+ * The platform composes a module package through its **published artefact**: the
8
+ * generated composition imports `@endora-commerce/mod-<id>/backend`, which the
9
+ * package's `exports` map points at `dist`. A file that names the *same*
10
+ * package's **source** by filesystem path gets a second evaluation of that file
11
+ * and of everything on its import graph. Two copies in one process, and `tsc`
12
+ * cannot see it — the two spellings have identical types.
13
+ *
14
+ * T040c measured this for the platform's five directories: 59 runtime values,
15
+ * none shared, `instanceof HttpError` false across the boundary, every 404 from
16
+ * a packaged module a 500. D-160.6 wrote the rule down **about entity classes**,
17
+ * where the failure is a lookup miss: loud, at a known moment.
18
+ *
19
+ * **The asymmetry is the finding this check exists for.** A duplicated
20
+ * module-scope *value* fails **silently**. The second copy is simply empty, and
21
+ * every reader sees an empty registry rather than an error. It has now happened
22
+ * twice in two consecutive packaging batches:
23
+ *
24
+ * * batch one — seven files acquired a second, empty `paymentAdapterRegistry`;
25
+ * * batch two — `backend/test/integration/shipments/carrier-module-off.test.ts`
26
+ * registered its synthetic carrier into a second, empty
27
+ * `ShippingAdapterRegistry`. The adapter was never *found*, so the shipment
28
+ * path took its "no carrier configured" branch and reported success: a
29
+ * switched-off module read as **present**, which is the exact state that
30
+ * file exists to refuse (Principle XVII item 6). A silent duplication
31
+ * producing a silent fail-open, two layers down from the duplication.
32
+ *
33
+ * Neither was visible to `tsc`, to any other check, or to `test:unit:fast` —
34
+ * both live under the contract and integration trees that command skips.
35
+ *
36
+ * **Batch four is the third, and it is neither silent nor a value** (T061a;
37
+ * D-160.6.1). `invoices` moved, and seven files in the test tree acquired a
38
+ * second `Invoice` — an entity class, so D-160.6's own subject, but a
39
+ * `@TransitivelyScoped` **parent**: `KsefSubmission` names it, the platform
40
+ * resolves a tenant chain by class *name* (D-169/D-170), and
41
+ * `assertTransitiveParentsResolve` refuses an ambiguous name rather than
42
+ * guessing. So this duplication is a **hard refusal at ORM init** —
43
+ * `UnresolvableTenantParentError` raised inside `setupBackendServer`, failing
44
+ * every file in the fork rather than the one with the bad import, which makes it
45
+ * the loudest of the three and the hardest to attribute.
46
+ *
47
+ * **And not one of the seven named the entity.** They import an `invoices`
48
+ * *service*; four of those services import the entity. The duplication therefore
49
+ * sat one hop *behind* the reached binding, and conjunct 2 asks about the
50
+ * binding — which is why this file's own header used to name the shape as out of
51
+ * reach. It is the third signal below.
52
+ *
53
+ * ## The rule, and why it is not "no module package source is ever reached"
54
+ *
55
+ * Several hundred value-position reaches into a module package's source stand in
56
+ * this tree — the run prints the number as `sites=`, and it grows by a module's
57
+ * worth every time the sweep packages one — and the overwhelming majority are
58
+ * correct: a unit test that constructs its own service and hands it a stubbed
59
+ * `EntityManager` has **one** copy in its process, so there is no second copy
60
+ * for it to disagree with (D-168 says so in `test/helpers/package-entities.ts`).
61
+ * A blanket rule would therefore be a ledger of nothing but exceptions, which is
62
+ * the shape `check:diacritic-folds`' header refuses, and it would have no
63
+ * available repair: a package's `exports` map publishes no service class, so
64
+ * there is nothing for most of those reaches to be rewritten to.
65
+ *
66
+ * So the rule is the mechanism itself, and it is a **conjunction**:
67
+ *
68
+ * 1. **Both copies are in one process.** The reaching file's own value-import
69
+ * closure also loads that package's published artefact — through the
70
+ * generated composition, through a test harness that composes it, or by
71
+ * naming the bare specifier itself. This is derived, never declared: it is
72
+ * what makes the five entity reaches in unit tests *correct* rather than
73
+ * ledgered.
74
+ * 2. **The platform composed the reached object.** Identity only matters where
75
+ * something else holds the other copy — so the binding is one the package's
76
+ * own composition hands to the container (`asValue` / `asFunction(() => x)`
77
+ * / `asClass`), or a member of the `entities` array the ORM registered.
78
+ *
79
+ * Both conjuncts come off the artefacts on every run. A thirtieth package, a new
80
+ * registry and a new entity are covered by existing, and no list of them is
81
+ * written down (D-100).
82
+ *
83
+ * ## Three findings
84
+ *
85
+ * * **`composed-singleton-reach`** — the conjunction above, with the binding
86
+ * named. **No ledger, deliberately**: the composed container and
87
+ * `test/helpers/package-singletons.ts` / `package-entities.ts` are the
88
+ * supported doors and are always available, so an entry could only license
89
+ * re-opening the defect.
90
+ * * **`chain-parent-reach`** — the same conjunction with conjunct 2 asked of
91
+ * the reach's **import closure** instead of the binding it names, narrowed
92
+ * to the one class of duplication the platform *refuses* rather than
93
+ * tolerates: a `@TransitivelyScoped` parent (batch four, above). Also no
94
+ * ledger, for the same reason and one more — a second copy of one of these
95
+ * stops the whole fork booting, so an entry could not describe a site that
96
+ * is right to stand.
97
+ *
98
+ * **The narrowing is the design, not a compromise.** The general shape —
99
+ * *any* composed singleton reached transitively — is every reach this run
100
+ * prints as `sites=`, most of them correct and most with no available repair
101
+ * (D-160.6.1's "deliberately not ruled"), so it would be a ledger of nothing
102
+ * but exceptions: the shape `check:diacritic-folds`' header refuses and the
103
+ * shape that got this check's third signal deleted. The chain parents are
104
+ * **derived** from the decorators on every run — two today (`Invoice`,
105
+ * `Order`), one of them in a package — so a third is covered by existing and
106
+ * no list of them is written down (D-100).
107
+ *
108
+ * **No depth limit, and the number is measured rather than picked.** On this
109
+ * tree the reverse walk terminates at **2** hops, and it costs **2.2 ms** of
110
+ * a 10.1 s run — 8011 value edges over 3234 files, one BFS per duplicated
111
+ * class rather than one per site. A cap at one hop would already be blind to
112
+ * two live consumers (`test/unit/invoices/auto-issue-reactor.test.ts` and
113
+ * `issue-route-email-outcome.test.ts`, both correct today because they
114
+ * compose nothing), and the next service extracted between a test and an
115
+ * entity moves a site past whatever number were written here.
116
+ * * **`whole-file-reach`** — a namespace, dynamic, side-effect or `require`
117
+ * reach into a package's source under conjunct 1. It names no binding, so
118
+ * it takes the file's whole graph whatever that is, and no per-binding
119
+ * predicate can judge it (`check:platform-surface`'s reasoning verbatim).
120
+ * Two-way ledger.
121
+ *
122
+ * ## What it cannot see, stated here rather than discovered later
123
+ *
124
+ * * A **computed** specifier — `await import(pathVariable)`. A literal
125
+ * dynamic import is `whole-file-reach`; a built one is outside every static
126
+ * check in this repository.
127
+ * * A module named as a **string** that is only *read* as text
128
+ * (`readFileSync(resolve(root, 'packages/modules/audit_logs/src/…'))`, one
129
+ * live site, deliberate and documented). That loads nothing, so it
130
+ * duplicates nothing; a string that is later *executed* would need dataflow
131
+ * the expression does not carry.
132
+ * * A singleton reached **transitively** that is *not* a tenant-chain parent —
133
+ * a test that imports a service class from a package's source, where that
134
+ * class's module body closes over the package's registry. `chain-parent-reach`
135
+ * walks the closure for the classes whose duplication is a refusal;
136
+ * everything else on it is still judged per binding, so a second copy that
137
+ * fails *silently* one hop down is invisible here. That is the narrowing
138
+ * three paragraphs above, and it is a decision rather than an oversight:
139
+ * D-160.6.1 leaves the general reach deliberately unruled, because a
140
+ * package's `exports` map publishes no service class and most of those
141
+ * reaches have no repair to be pointed at.
142
+ * * **A reach whose duplication only matters in a process it cannot name.**
143
+ * Conjunct 1 asks about the reaching file's own closure, which
144
+ * under-approximates for a file nothing runs on its own: a test helper
145
+ * reaching the harness through `import type` loads no artefact, while every
146
+ * test importing it loads both. `chain-parent-reach` answers this by walking
147
+ * value edges *backwards* as well (`coLoadedArtefacts`) — measured, it adds
148
+ * batch four's seventh site and no false one — and the other two signals
149
+ * deliberately do not, because widening the conjunct that clears several
150
+ * hundred correct reaches is a separate question with a separate
151
+ * measurement.
152
+ * * **Which copy produced the object under test.** Batch one's *second*
153
+ * finding was a service class duplicated the same way, caught by
154
+ * `toBeInstanceOf(AddressService)` and by nothing else — the instance came
155
+ * from the composed container and the class from the source. An
156
+ * `identity-comparison` signal over `instanceof` / `toBeInstanceOf` against
157
+ * a source-reached binding was written, measured and **removed**: it fired
158
+ * on all three sites in the tree and all three were correct, because in
159
+ * each of them the compared *object* was built by the test from the same
160
+ * source copy (`newsletter`'s provider registry twice,
161
+ * `transactional_emails`' reconciler once). The property it needs — which
162
+ * copy produced the left operand — is not lexically decidable, so the
163
+ * signal would have been a ledger of nothing but exceptions, which is the
164
+ * shape `check:diacritic-folds`' header refuses. Conjunct 2 is deliberately
165
+ * the narrower question, asked of an artefact rather than of dataflow.
166
+ * * `admin` and `storefront`. Neither composes the backend platform, so
167
+ * conjunct 1 is false for every file in them by construction.
168
+ * * A tenant chain whose parent is **computed** — `@TransitivelyScoped(NAME,
169
+ * 'fk')`. That is not a blind spot but a **refusal**: the signal's whole
170
+ * population is those literals, so an unreadable one exits 2 rather than
171
+ * quietly shrinking it.
172
+ * * A `@TransitivelyScoped` on a class that is not `@Entity()`. Feature 050's
173
+ * classification is total over persisted entities, so a real chain's child
174
+ * is one; without the conjunct the population is 31 names on this tree, 29
175
+ * of them the tenancy suite's synthetic fixtures.
176
+ *
177
+ * Static analysis through the TypeScript compiler API, so a specifier mentioned
178
+ * in a comment or a string literal is not a finding.
179
+ *
180
+ * ## One analysis, two hosts
181
+ *
182
+ * This file is the analysis (`specs/101-endora-check/contracts/package-scope-layout.md`
183
+ * §6). `backend/scripts/check-singleton-identity.ts` hosts it over every
184
+ * consumer root in this repository and holds `WHOLE_FILE_REACHES_ALLOWED`,
185
+ * which is a ledger of *this* tree's reaches, so `allowed` is an argument and
186
+ * not a value this file reads — a ledger read from inside the analysis is a
187
+ * ledger only one host can have.
188
+ *
189
+ * `endora check`'s host lands with Phase 3 and its estate entry says why: the
190
+ * rule's subject is a reach into a module package's source from a process that
191
+ * also loads that package's *artefact*, and `specifierGraph` skips a reach whose
192
+ * target is the reaching file's **own** package — correctly, since a package's
193
+ * internal relative imports are one copy and not two. So one package in
194
+ * isolation has no second package for a reach to land in, and what the rule
195
+ * needs is the peers' sources, which is Tier B's input rather than Tier A's.
196
+ */
197
+ import { readdirSync, statSync } from 'node:fs';
198
+ import { join } from 'node:path';
199
+ import ts from 'typescript';
200
+ import { namedSpecifiers } from '../lib/specifiers.js';
201
+ const REGISTRATION_CALLS = /(?:^|\.)(asValue|asFunction|asClass)$/;
202
+ /**
203
+ * The half of a composed singleton's reason that says the **ORM** holds the
204
+ * other copy.
205
+ *
206
+ * One constant rather than the same literal in three places: the chain-parent
207
+ * signal and the `entities-registry` reconciliation both narrow the composed set
208
+ * to this derivation, and a second spelling of it is two answers waiting to
209
+ * disagree about which classes the ORM registered.
210
+ */
211
+ const ENTITIES_ARRAY_WHY = "the ORM registered this class from the package's entities array";
212
+ /** The decorator whose first argument names a tenant chain's parent by class name. */
213
+ const TRANSITIVE_SCOPE_DECORATOR = 'TransitivelyScoped';
214
+ /** The candidate spellings a specifier can reach, in the order node would try. */
215
+ function candidateKeys(base) {
216
+ return [
217
+ base.replace(/\.js$/, '.ts'),
218
+ base.replace(/\.js$/, '.tsx'),
219
+ `${base}.ts`,
220
+ `${base}.tsx`,
221
+ `${base}/index.ts`,
222
+ base,
223
+ ];
224
+ }
225
+ /** `a/b/../c` → `a/c`, over the one separator every key in this check uses. */
226
+ function normalizeKey(path) {
227
+ const out = [];
228
+ for (const part of path.split('/')) {
229
+ if (part === '' || part === '.')
230
+ continue;
231
+ if (part === '..')
232
+ out.pop();
233
+ else
234
+ out.push(part);
235
+ }
236
+ return out.join('/');
237
+ }
238
+ /**
239
+ * Which walked file a relative specifier written in `fromKey` names, or `null`.
240
+ *
241
+ * Only relative specifiers: a bare one is a package boundary and is conjunct 1's
242
+ * business, not conjunct 2's.
243
+ */
244
+ export function resolveRelative(fromKey, specifier, keys) {
245
+ if (!specifier.startsWith('.'))
246
+ return null;
247
+ const base = normalizeKey(`${fromKey.split('/').slice(0, -1).join('/')}/${specifier}`);
248
+ for (const candidate of candidateKeys(base))
249
+ if (keys.has(candidate))
250
+ return candidate;
251
+ return null;
252
+ }
253
+ function sourceFileOf(key, text) {
254
+ return ts.createSourceFile(key, text, ts.ScriptTarget.Latest, true);
255
+ }
256
+ function isExported(node) {
257
+ return (ts.canHaveModifiers(node) &&
258
+ (ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword));
259
+ }
260
+ /**
261
+ * What each name a file exports *is*, at module scope.
262
+ *
263
+ * The distinction that matters is `value` (one object, created when the module
264
+ * evaluates) against `function` / `class` (code, re-created identically by a
265
+ * second evaluation and only identity-bearing where somebody compares it —
266
+ * which is `identity-comparison`'s job). `entity` is a class the ORM keys its
267
+ * metadata on, so a second one is a lookup miss.
268
+ */
269
+ export function exportedKinds(key, text) {
270
+ const sf = sourceFileOf(key, text);
271
+ const kinds = new Map();
272
+ for (const statement of sf.statements) {
273
+ if (!isExported(statement))
274
+ continue;
275
+ if (ts.isVariableStatement(statement)) {
276
+ for (const declaration of statement.declarationList.declarations) {
277
+ if (ts.isIdentifier(declaration.name))
278
+ kinds.set(declaration.name.text, 'value');
279
+ }
280
+ }
281
+ else if (ts.isClassDeclaration(statement) && statement.name) {
282
+ const decorated = (ts.getDecorators(statement) ?? []).some((decorator) => decorator.getText(sf).startsWith('@Entity'));
283
+ kinds.set(statement.name.text, decorated ? 'entity' : 'class');
284
+ }
285
+ else if (ts.isFunctionDeclaration(statement) && statement.name) {
286
+ kinds.set(statement.name.text, 'function');
287
+ }
288
+ }
289
+ return kinds;
290
+ }
291
+ /** Where each name a file imports as a value comes from, resolved to a walked key. */
292
+ function valueImportOrigins(key, text, keys) {
293
+ const sf = sourceFileOf(key, text);
294
+ const origins = new Map();
295
+ for (const statement of sf.statements) {
296
+ if (!ts.isImportDeclaration(statement) ||
297
+ !statement.importClause ||
298
+ statement.importClause.isTypeOnly ||
299
+ !ts.isStringLiteral(statement.moduleSpecifier)) {
300
+ continue;
301
+ }
302
+ const target = resolveRelative(key, statement.moduleSpecifier.text, keys);
303
+ if (target === null)
304
+ continue;
305
+ if (statement.importClause.name)
306
+ origins.set(statement.importClause.name.text, target);
307
+ const bound = statement.importClause.namedBindings;
308
+ if (bound && ts.isNamedImports(bound)) {
309
+ for (const element of bound.elements) {
310
+ if (!element.isTypeOnly)
311
+ origins.set(element.name.text, target);
312
+ }
313
+ }
314
+ }
315
+ return origins;
316
+ }
317
+ /** `<file>#<name>` — the identity of a binding, in one namespace. */
318
+ export function bindingKey(file, name) {
319
+ return `${file}#${name}`;
320
+ }
321
+ /**
322
+ * Every binding the platform holds exactly one of, derived from the packages'
323
+ * own sources.
324
+ *
325
+ * Two derivations and no third, each naming the thing that holds the other copy:
326
+ * the **container** (`asValue` / `asFunction(() => x)` / `asClass`) and the
327
+ * **ORM** (a member of the `entities` array). Both are read as literal AST
328
+ * nodes, so a mention in a comment contributes nothing.
329
+ */
330
+ export function composedSingletons(input) {
331
+ const keys = new Set(input.sources.keys());
332
+ const composed = new Map();
333
+ for (const pkg of input.packages) {
334
+ const prefix = `${pkg.root}/`;
335
+ for (const [key, text] of input.sources) {
336
+ if (!key.startsWith(prefix))
337
+ continue;
338
+ const sf = sourceFileOf(key, text);
339
+ const origins = valueImportOrigins(key, text, keys);
340
+ const ownKinds = exportedKinds(key, text);
341
+ const record = (identifier, why, wanted) => {
342
+ const from = origins.get(identifier.text) ?? (ownKinds.has(identifier.text) ? key : null);
343
+ if (from === null)
344
+ return;
345
+ const text2 = input.sources.get(from);
346
+ if (text2 === undefined)
347
+ return;
348
+ const kind = exportedKinds(from, text2).get(identifier.text);
349
+ if (kind === undefined || !wanted.includes(kind))
350
+ return;
351
+ composed.set(bindingKey(from, identifier.text), `${pkg.moduleId}: ${why}`);
352
+ };
353
+ const visit = (node) => {
354
+ if (ts.isCallExpression(node) && REGISTRATION_CALLS.test(node.expression.getText(sf))) {
355
+ const [first] = node.arguments;
356
+ const call = node.expression.getText(sf);
357
+ if (first !== undefined) {
358
+ if (ts.isIdentifier(first)) {
359
+ record(first, `the composed container holds this object (${call})`, [
360
+ 'value',
361
+ 'class',
362
+ 'entity',
363
+ ]);
364
+ }
365
+ else if (ts.isArrowFunction(first) && ts.isIdentifier(first.body)) {
366
+ record(first.body, `the composed container holds this object (${call})`, [
367
+ 'value',
368
+ 'class',
369
+ 'entity',
370
+ ]);
371
+ }
372
+ }
373
+ }
374
+ if (ts.isVariableDeclaration(node) &&
375
+ ts.isIdentifier(node.name) &&
376
+ node.name.text === 'entities' &&
377
+ node.initializer &&
378
+ ts.isArrayLiteralExpression(node.initializer)) {
379
+ for (const element of node.initializer.elements) {
380
+ if (ts.isIdentifier(element)) {
381
+ record(element, ENTITIES_ARRAY_WHY, ['entity']);
382
+ }
383
+ }
384
+ }
385
+ node.forEachChild(visit);
386
+ };
387
+ sf.forEachChild(visit);
388
+ }
389
+ }
390
+ return composed;
391
+ }
392
+ export function transitiveParents(sources) {
393
+ const parents = new Map();
394
+ const unreadable = [];
395
+ for (const [key, text] of sources) {
396
+ if (!text.includes(TRANSITIVE_SCOPE_DECORATOR))
397
+ continue;
398
+ const sf = sourceFileOf(key, text);
399
+ const visit = (node) => {
400
+ if (ts.isClassDeclaration(node)) {
401
+ const decorators = ts.getDecorators(node) ?? [];
402
+ const persisted = decorators.some((decorator) => decorator.getText(sf).startsWith('@Entity'));
403
+ for (const decorator of persisted ? decorators : []) {
404
+ const call = decorator.expression;
405
+ if (!ts.isCallExpression(call))
406
+ continue;
407
+ if (call.expression.getText(sf) !== TRANSITIVE_SCOPE_DECORATOR)
408
+ continue;
409
+ const [first] = call.arguments;
410
+ const line = sf.getLineAndCharacterOfPosition(decorator.getStart(sf)).line + 1;
411
+ if (first === undefined || !ts.isStringLiteral(first)) {
412
+ unreadable.push(`${key}:${line}`);
413
+ continue;
414
+ }
415
+ const children = parents.get(first.text) ?? [];
416
+ children.push(node.name?.text ?? `<anonymous at ${key}:${line}>`);
417
+ parents.set(first.text, children);
418
+ }
419
+ }
420
+ node.forEachChild(visit);
421
+ };
422
+ sf.forEachChild(visit);
423
+ }
424
+ return { parents, unreadable };
425
+ }
426
+ /**
427
+ * Where each `@Entity()` class the walk read is declared, by class name.
428
+ *
429
+ * The independent half of the `tenant-chains` reconciliation: the decorator on
430
+ * the child names a parent, and this says whether the walk reached the file that
431
+ * declares it. Pre-filtered on the decorator's own text, so it parses the ~230
432
+ * files that hold an entity rather than all 3200.
433
+ */
434
+ export function entityClassLocations(sources) {
435
+ const found = new Map();
436
+ for (const [key, text] of sources) {
437
+ if (!text.includes('@Entity'))
438
+ continue;
439
+ for (const [name, kind] of exportedKinds(key, text)) {
440
+ if (kind !== 'entity')
441
+ continue;
442
+ const where = found.get(name) ?? [];
443
+ where.push(key);
444
+ found.set(name, where);
445
+ }
446
+ }
447
+ return found;
448
+ }
449
+ export function chainParentSubjects(input, scan = transitiveParents(input.sources), composed = composedSingletons(input)) {
450
+ const subjects = [];
451
+ for (const [key, why] of composed) {
452
+ if (!why.includes(ENTITIES_ARRAY_WHY))
453
+ continue;
454
+ const cut = key.lastIndexOf('#');
455
+ const file = key.slice(0, cut);
456
+ const className = key.slice(cut + 1);
457
+ const children = scan.parents.get(className);
458
+ if (children === undefined)
459
+ continue;
460
+ subjects.push({ file, className, moduleId: why.split(':')[0] ?? '', children });
461
+ }
462
+ return subjects;
463
+ }
464
+ /**
465
+ * For every walked file, the chain parents its own value-import closure loads.
466
+ *
467
+ * A **reverse** breadth-first walk from each subject rather than a forward one
468
+ * per reach: the subjects are few and the reaches are many, so this is one BFS
469
+ * per duplicated class over the whole edge set instead of one per site, and it
470
+ * yields the *shortest* path for free — which is what the message prints, and
471
+ * what makes the depth question answerable rather than assumed.
472
+ *
473
+ * **No depth limit**, and the measurement rather than the taste is the reason:
474
+ * over this repository the real hits sit at 1 and 2 hops, and the walk to
475
+ * exhaustion costs 0.1 s of a 13 s run (feature 080, T061a). A cap would be a
476
+ * number written down that the next service extraction — one more file between
477
+ * the test and the entity — would silently step past, which is the failure mode
478
+ * this check's own header spends four paragraphs on.
479
+ */
480
+ export function chainParentReaches(edges, subjects) {
481
+ const reverse = new Map();
482
+ for (const [from, targets] of edges) {
483
+ for (const target of targets) {
484
+ const callers = reverse.get(target) ?? [];
485
+ callers.push(from);
486
+ reverse.set(target, callers);
487
+ }
488
+ }
489
+ const answer = new Map();
490
+ for (const subject of subjects) {
491
+ const seen = new Map([[subject.file, [subject.file]]]);
492
+ let frontier = [subject.file];
493
+ let hops = 0;
494
+ while (frontier.length > 0) {
495
+ for (const file of frontier) {
496
+ const hits = answer.get(file) ?? [];
497
+ hits.push({ subject, hops, via: [...(seen.get(file) ?? [file])].reverse() });
498
+ answer.set(file, hits);
499
+ }
500
+ const next = [];
501
+ for (const file of frontier) {
502
+ for (const caller of reverse.get(file) ?? []) {
503
+ if (seen.has(caller))
504
+ continue;
505
+ seen.set(caller, [...(seen.get(file) ?? [file]), caller]);
506
+ next.push(caller);
507
+ }
508
+ }
509
+ frontier = next;
510
+ hops += 1;
511
+ }
512
+ }
513
+ return answer;
514
+ }
515
+ /**
516
+ * Conjunct 1, asked of the **process** rather than of the file — for the
517
+ * chain-parent signal only.
518
+ *
519
+ * The per-file closure under-approximates for a file nothing runs on its own: a
520
+ * test *helper* reaches the harness through `import type` and so loads no
521
+ * artefact of its own, while every test that imports it loads both. Batch four's
522
+ * seventh site is exactly that file (`test/helpers/orders-neighbour-ports.ts`),
523
+ * and the narrow conjunct clears it while the tree it lives in fails at ORM
524
+ * init.
525
+ *
526
+ * So a file counts as sharing a process with artefact `A` when **some** walked
527
+ * file's value-import closure holds both it and `A` — computed by walking value
528
+ * edges backwards from the file and unioning conjunct 1's answer over everything
529
+ * that reaches it. It is used **only** by the chain-parent signal, deliberately:
530
+ * widening the named-binding signal the same way changes the population that
531
+ * clears several hundred correct reaches, which is a separate question with a
532
+ * separate measurement. Measured here — on this repository it adds one true
533
+ * finding and no false one.
534
+ */
535
+ function coLoadedArtefacts(edges, loaded, files) {
536
+ const reverse = new Map();
537
+ for (const [from, targets] of edges) {
538
+ for (const target of targets) {
539
+ const callers = reverse.get(target) ?? [];
540
+ callers.push(from);
541
+ reverse.set(target, callers);
542
+ }
543
+ }
544
+ const answer = new Map();
545
+ for (const file of files) {
546
+ if (answer.has(file))
547
+ continue;
548
+ const out = new Set(loaded.get(file) ?? []);
549
+ const seen = new Set([file]);
550
+ const queue = [file];
551
+ while (queue.length > 0) {
552
+ const current = queue.shift();
553
+ for (const id of loaded.get(current) ?? [])
554
+ out.add(id);
555
+ for (const caller of reverse.get(current) ?? []) {
556
+ if (seen.has(caller))
557
+ continue;
558
+ seen.add(caller);
559
+ queue.push(caller);
560
+ }
561
+ }
562
+ answer.set(file, out);
563
+ }
564
+ return answer;
565
+ }
566
+ function packageOf(key, packages) {
567
+ for (const pkg of packages)
568
+ if (key.startsWith(`${pkg.root}/`))
569
+ return pkg;
570
+ return null;
571
+ }
572
+ function bareNameOf(specifier) {
573
+ const parts = specifier.split('/');
574
+ return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : (parts[0] ?? specifier);
575
+ }
576
+ function specifierGraph(input) {
577
+ const keys = new Set(input.sources.keys());
578
+ const direct = new Map();
579
+ const edges = new Map();
580
+ const reaches = [];
581
+ for (const [key, text] of input.sources) {
582
+ const own = packageOf(key, input.packages);
583
+ const mine = new Set();
584
+ const targets = [];
585
+ for (const specifier of namedSpecifiers(text, key)) {
586
+ if (specifier.kind === 'type-only-import' || specifier.kind === 'import-type-node')
587
+ continue;
588
+ const bare = bareNameOf(specifier.text);
589
+ const named = input.packages.find((pkg) => pkg.npmName === bare);
590
+ if (named !== undefined && (own === null || own.npmName !== bare))
591
+ mine.add(named.moduleId);
592
+ const target = resolveRelative(key, specifier.text, keys);
593
+ if (target === null)
594
+ continue;
595
+ targets.push(target);
596
+ const pkg = packageOf(target, input.packages);
597
+ if (pkg === null || (own !== null && own.root === pkg.root))
598
+ continue;
599
+ reaches.push({
600
+ file: key,
601
+ line: specifier.line,
602
+ target,
603
+ pkg,
604
+ bindings: specifier.bindings,
605
+ kind: specifier.kind,
606
+ whole: specifier.kind === 'side-effect-import' ||
607
+ specifier.kind === 'dynamic-import' ||
608
+ specifier.kind === 'require-call' ||
609
+ specifier.bindings.some((binding) => binding.startsWith('* as ')),
610
+ });
611
+ }
612
+ direct.set(key, mine);
613
+ edges.set(key, targets);
614
+ }
615
+ return { direct, edges, reaches };
616
+ }
617
+ /**
618
+ * Every module package whose **published artefact** this file's value-import
619
+ * closure loads — conjunct 1, derived rather than declared.
620
+ *
621
+ * The closure is over value edges only: a `import type` resolves at build time
622
+ * and evaluates nothing, so it puts no second copy in any process.
623
+ */
624
+ function artefactsLoaded(input, graph) {
625
+ const { direct, edges } = graph;
626
+ const memo = new Map();
627
+ const resolve = (key, stack) => {
628
+ const cached = memo.get(key);
629
+ if (cached !== undefined)
630
+ return cached;
631
+ if (stack.has(key))
632
+ return new Set();
633
+ stack.add(key);
634
+ const out = new Set(direct.get(key) ?? []);
635
+ for (const target of edges.get(key) ?? []) {
636
+ for (const id of resolve(target, stack))
637
+ out.add(id);
638
+ }
639
+ stack.delete(key);
640
+ // A cycle member is not memoised under a partial answer: the first key on
641
+ // the cycle would otherwise freeze the empty set every later visitor reads.
642
+ if (stack.size === 0)
643
+ memo.set(key, out);
644
+ return out;
645
+ };
646
+ const answer = new Map();
647
+ for (const key of input.sources.keys())
648
+ answer.set(key, resolve(key, new Set()));
649
+ return answer;
650
+ }
651
+ /** The whole analysis, pure over {@link SingletonIdentityInput}. */
652
+ export function checkSingletonIdentity(input, allowed) {
653
+ const keys = new Set(input.sources.keys());
654
+ const composed = composedSingletons(input);
655
+ const graph = specifierGraph(input);
656
+ const loaded = artefactsLoaded(input, graph);
657
+ const parentScan = transitiveParents(input.sources);
658
+ const subjects = chainParentSubjects(input, parentScan, composed);
659
+ const chainReaches = chainParentReaches(graph.edges, subjects);
660
+ const findings = [];
661
+ const reaches = graph.reaches;
662
+ const coLoaded = coLoadedArtefacts(graph.edges, loaded, reaches.filter((reach) => chainReaches.has(reach.target)).map((reach) => reach.file));
663
+ const usedAllowances = new Set();
664
+ for (const reach of reaches) {
665
+ // Conjunct 1: both copies in one process. Derived, never declared.
666
+ const sharesProcess = loaded.get(reach.file)?.has(reach.pkg.moduleId) ?? false;
667
+ // Conjunct 2 asked of the reach's **closure** rather than of the binding it
668
+ // names, and only for the classes whose duplication the platform refuses.
669
+ // It runs before the two per-binding signals below because it is the one
670
+ // that survives a `whole` reach and a reach naming an innocent binding
671
+ // alike: what makes it a finding is what the specifier *loads*.
672
+ for (const hit of chainReaches.get(reach.target) ?? []) {
673
+ const { subject } = hit;
674
+ if (!(coLoaded.get(reach.file)?.has(subject.moduleId) ?? false))
675
+ continue;
676
+ // Hop 0 naming the class itself is already `composed-singleton-reach`,
677
+ // which says the same thing with the binding named. Reporting it twice
678
+ // would make the count a property of how the message is phrased.
679
+ if (hit.hops === 0 && reach.bindings.includes(subject.className))
680
+ continue;
681
+ findings.push({
682
+ kind: 'chain-parent-reach',
683
+ file: reach.file,
684
+ line: reach.line,
685
+ moduleId: subject.moduleId,
686
+ target: subject.file,
687
+ binding: subject.className,
688
+ why: `\`${subject.className}\` is a \`@TransitivelyScoped\` parent — ` +
689
+ `${subject.children.join(', ')} name${subject.children.length === 1 ? 's' : ''} it — ` +
690
+ 'and the platform resolves a tenant chain by class **name**, so two classes of that ' +
691
+ 'name are an ambiguity `assertTransitiveParentsResolve` refuses at ORM init rather ' +
692
+ 'than guesses at: `UnresolvableTenantParentError` inside `setupBackendServer`, which ' +
693
+ 'fails every file in the process rather than this one. This specifier loads it ' +
694
+ `${hit.hops} hop(s) down, through ${hit.via.join(' -> ')}`,
695
+ });
696
+ }
697
+ if (!sharesProcess)
698
+ continue;
699
+ if (reach.whole) {
700
+ const ledgerKey = `${reach.file}:${reach.target}`;
701
+ if (allowed[ledgerKey] !== undefined) {
702
+ usedAllowances.add(ledgerKey);
703
+ continue;
704
+ }
705
+ findings.push({
706
+ kind: 'whole-file-reach',
707
+ file: reach.file,
708
+ line: reach.line,
709
+ moduleId: reach.pkg.moduleId,
710
+ target: reach.target,
711
+ binding: null,
712
+ why: `a ${reach.kind} names no binding, so it takes the file's whole import graph — ` +
713
+ `every module-scope value on it is a second copy while this process also holds ` +
714
+ `${reach.pkg.npmName}`,
715
+ });
716
+ continue;
717
+ }
718
+ for (const binding of reach.bindings) {
719
+ const why = composed.get(bindingKey(reach.target, binding));
720
+ if (why === undefined)
721
+ continue;
722
+ findings.push({
723
+ kind: 'composed-singleton-reach',
724
+ file: reach.file,
725
+ line: reach.line,
726
+ moduleId: reach.pkg.moduleId,
727
+ target: reach.target,
728
+ binding,
729
+ why,
730
+ });
731
+ }
732
+ }
733
+ for (const ledgerKey of Object.keys(allowed)) {
734
+ if (usedAllowances.has(ledgerKey))
735
+ continue;
736
+ const [file = ledgerKey, target = ''] = ledgerKey.split(':');
737
+ // Staleness is judged **in the tree that holds the file**. A ledger entry
738
+ // names a site, and a site cannot be found absent from a tree its file is
739
+ // not in — the fixture backends of `moved-module-tree.test.ts` carry no
740
+ // test tree, and reporting four stale entries there would be the check
741
+ // going red for the fixture's shape rather than for the repository's. The
742
+ // other direction of the two-way rule — an entry whose file was *deleted*
743
+ // — is held by `check-singleton-identity.test.ts`, which resolves every key
744
+ // against this checkout's disk.
745
+ if (!keys.has(file))
746
+ continue;
747
+ findings.push({
748
+ kind: 'stale-allowance',
749
+ file,
750
+ line: 0,
751
+ moduleId: '',
752
+ target,
753
+ binding: null,
754
+ why: 'the ledger allows a whole-file reach this walk did not find',
755
+ });
756
+ }
757
+ const packagesWithEntities = [
758
+ ...new Set([...composed.entries()]
759
+ .filter(([, why]) => why.includes(ENTITIES_ARRAY_WHY))
760
+ .map(([, why]) => why.split(':')[0] ?? '')),
761
+ ].sort();
762
+ const entityClasses = entityClassLocations(input.sources);
763
+ const chainParentNames = [...parentScan.parents.keys()].sort();
764
+ return {
765
+ findings,
766
+ sites: reaches.length,
767
+ singletons: composed.size,
768
+ packagesWithEntities,
769
+ chainParentNames,
770
+ resolvedChainParentNames: chainParentNames.filter((name) => entityClasses.has(name)),
771
+ unreadableChainParents: parentScan.unreadable,
772
+ chainParentSubjects: subjects,
773
+ };
774
+ }
775
+ const SKIPPED_DIRECTORIES = new Set(['node_modules', 'dist', '.git', 'build', '.next', 'coverage']);
776
+ /**
777
+ * Every TypeScript source under `dir`, with the rule's own directory prunes.
778
+ *
779
+ * Exported because the population is part of the rule: two hosts computing
780
+ * "which files this check reads" two ways is the shape that lets one of them go
781
+ * half-blind.
782
+ */
783
+ export function collectSingletonSources(dir, out = []) {
784
+ let entries;
785
+ try {
786
+ entries = readdirSync(dir);
787
+ }
788
+ catch {
789
+ return out;
790
+ }
791
+ for (const name of entries) {
792
+ const full = join(dir, name);
793
+ let directory;
794
+ try {
795
+ directory = statSync(full).isDirectory();
796
+ }
797
+ catch {
798
+ continue;
799
+ }
800
+ if (directory) {
801
+ if (SKIPPED_DIRECTORIES.has(name))
802
+ continue;
803
+ collectSingletonSources(full, out);
804
+ }
805
+ else if ((name.endsWith('.ts') || name.endsWith('.tsx')) && !name.endsWith('.d.ts')) {
806
+ out.push(full);
807
+ }
808
+ }
809
+ return out;
810
+ }
811
+ /**
812
+ * Which module packages the committed ORM registry imports an `entities` array
813
+ * from — the independent derivation the walk is reconciled against (issue #244).
814
+ *
815
+ * It is a second author's answer to the same question: the registry is generated
816
+ * from a filesystem walk of the packages' `exports` maps and names each package
817
+ * by its bare specifier, while this check derives entities from each package's
818
+ * own source. A walk that stopped reading a package's sources makes the two
819
+ * disagree in the same run.
820
+ */
821
+ export function packagesInEntitiesRegistry(source, npmNames) {
822
+ const found = new Set();
823
+ for (const match of source.matchAll(/entities as \w+\s*\}\s*from\s*'([^']+)'/g)) {
824
+ const bare = bareNameOf(match[1] ?? '');
825
+ if (npmNames.has(bare))
826
+ found.add(bare);
827
+ }
828
+ return [...found].sort();
829
+ }
830
+ //# sourceMappingURL=singleton-identity.js.map