@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,1374 @@
1
+ /**
2
+ * A `catch` around a gated-port call may not swallow the module's presence
3
+ * answer (issue #84; the deferred half of feature 072's D-43).
4
+ *
5
+ * `lazyPort` resolves the name inside the *forwarded call*, so a switched-off
6
+ * owner surfaces as `ModuleDisabledError` at the call site rather than at wiring
7
+ * time. A `try { … } catch { return null }` around one therefore converts
8
+ * fail-closed into fail-open, and does it silently: the caller answers "no data"
9
+ * where the truthful answer is "this capability is off", and the operator reads a
10
+ * working screen that is lying to them.
11
+ *
12
+ * **The whole of the reasoning is in `backend/scripts/check-port-catches.ts`**,
13
+ * the repository-scope host, whose header it has always been: the three shapes
14
+ * the measured 51 sites turned out to be, the two spellings one predicate judges,
15
+ * the `async`-boundary bound, the alias fixpoint and its exclusions, the one hop
16
+ * backwards through a class, the scoping correction of issue #278, what counts as
17
+ * handling it, and `OWNER LOCKED`. None of it is restated here.
18
+ *
19
+ * `specs/101-endora-check/contracts/package-scope-layout.md` §6 — one analysis,
20
+ * two hosts.
21
+ *
22
+ * ## Two things changed on the way across, and both are load-bearing
23
+ *
24
+ * **1. `portOwners` can be seeded from outside, and it had to be.** The owner map
25
+ * is built from `di.providePort` in a file that declares `registerModule` — the
26
+ * **owner's** file. `isProxyCall` then admits `lazyPort(ctx, '<name>')` only when
27
+ * `<name>` is already in that map. So over one package in isolation a consumer's
28
+ * resolutions seed nothing and every `catch` around one reads clean.
29
+ *
30
+ * Measured over this repository's module packages before the seam existed: the
31
+ * whole-tree run attributes **226** sites to 41 packages, the same analysis over
32
+ * each package alone finds **19** in 10 packages, and **31 of the 41 lose every
33
+ * site** — including all four PIM connectors and `product_feeds`, which is to say
34
+ * the integration-heavy modules the package host exists for: the ones whose ports
35
+ * are almost all somebody else's. {@link PortCatchInput.peerOwners} is what a
36
+ * package-scope host supplies instead, out of its installed and workspace peers.
37
+ *
38
+ * **2. {@link resolvedPortNames} is new, and it is the denominator.** A rule that
39
+ * reads an owner map has to say how much of it it managed to resolve, or a short
40
+ * map is indistinguishable from a clean tree — and the estate's idiom for *"I
41
+ * read n of the m things I needed"* is the `read:` line's `sources=` token, not a
42
+ * sentence. This collects every `lazyPort(ctx, '<literal>')` name the sources
43
+ * write **whether or not an owner is known**, so a host can print
44
+ * `sources=owners:<attributed>/<written>` and refuse when the numerator is zero.
45
+ * Nothing else in the analysis can supply it: everywhere else, an unowned name is
46
+ * already gone.
47
+ *
48
+ * **3. `checkPortCatches`'s ledger argument loses its default.** A host states
49
+ * which exemptions it is judging against; `PORT_CATCHES_TO_DRAIN` is this
50
+ * repository's and stays in `backend/scripts/`, where `check:lock-claims`'
51
+ * population can also still see it.
52
+ */
53
+ import { existsSync, readdirSync, statSync } from 'node:fs';
54
+ import { join, posix } from 'node:path';
55
+ import ts from 'typescript';
56
+ import { NO_HOST_RESIDENT_MODULES, } from '../lib/module-population.js';
57
+ import { declaresRegisterModule } from '../lib/module-roots.js';
58
+ import { moduleOf, providedPortNames } from '../lib/port-registrations.js';
59
+ import { lockedOwners, } from '../lib/switchable-modules.js';
60
+ /** A file that belongs to no module — a composition root, `http/`, `db/`. */
61
+ const ROOT = '(root)';
62
+ /**
63
+ * The bindings a lexical scope declares **directly**, by the name a bare
64
+ * identifier inside it would resolve to (issue #278).
65
+ *
66
+ * Only the shapes the check can *judge* are collected: a `const`/`let`/`var`
67
+ * with an identifier name, and a function or constructor parameter with one.
68
+ * A destructuring binding (`const { promotion } = deps`), an import binding and
69
+ * a `catch` variable are deliberately absent — the check cannot tell what they
70
+ * hold, and treating one as a shadow would silence a real finding, which is the
71
+ * one direction a narrowing may not fail in. A nested `function` declaration is
72
+ * absent for the same reason: it binds a name, but not to anything this analysis
73
+ * has an opinion about.
74
+ */
75
+ const SCOPE_BINDINGS = new WeakMap();
76
+ function directBindings(scope) {
77
+ const cached = SCOPE_BINDINGS.get(scope);
78
+ if (cached !== undefined)
79
+ return cached;
80
+ const found = new Map();
81
+ const addList = (list) => {
82
+ for (const declaration of list.declarations) {
83
+ if (ts.isIdentifier(declaration.name))
84
+ found.set(declaration.name.text, declaration);
85
+ }
86
+ };
87
+ const addStatements = (statements) => {
88
+ for (const statement of statements) {
89
+ if (ts.isVariableStatement(statement))
90
+ addList(statement.declarationList);
91
+ }
92
+ };
93
+ if (ts.isSourceFile(scope) || ts.isBlock(scope) || ts.isModuleBlock(scope)) {
94
+ addStatements(scope.statements);
95
+ }
96
+ else if (ts.isCaseBlock(scope)) {
97
+ for (const clause of scope.clauses)
98
+ addStatements(clause.statements);
99
+ }
100
+ else if (ts.isForStatement(scope) || ts.isForInStatement(scope) || ts.isForOfStatement(scope)) {
101
+ const { initializer } = scope;
102
+ if (initializer !== undefined && ts.isVariableDeclarationList(initializer))
103
+ addList(initializer);
104
+ }
105
+ else if (ts.isFunctionLike(scope)) {
106
+ for (const parameter of scope.parameters) {
107
+ if (ts.isIdentifier(parameter.name))
108
+ found.set(parameter.name.text, parameter);
109
+ }
110
+ }
111
+ SCOPE_BINDINGS.set(scope, found);
112
+ return found;
113
+ }
114
+ /**
115
+ * The declaration a bare identifier `name`, written at `at`, resolves to — or
116
+ * `null` when nothing in scope binds it and the alias table therefore answers.
117
+ *
118
+ * Walks the parent chain, so an inner block wins over an outer one and a
119
+ * parameter wins over a file-level `const` of the same spelling. That ordering
120
+ * is the whole point: `catalog`'s `queue` case, which the file-scoping rule
121
+ * already handles across files, is the same collision one level down.
122
+ */
123
+ function nearestBinding(at, name) {
124
+ for (let node = at.parent; node !== undefined; node = node.parent) {
125
+ const binding = directBindings(node).get(name);
126
+ if (binding !== undefined)
127
+ return binding;
128
+ }
129
+ return null;
130
+ }
131
+ /**
132
+ * Can the check say **positively** that this expression is not a port?
133
+ *
134
+ * The distinction this function exists to keep is the one a narrowing gets
135
+ * wrong: {@link Analysis.readsAsPort}'s carriage analysis deliberately
136
+ * under-approximates — a call's *result* is data, so `carries` answers "no" both
137
+ * for a value that is genuinely not a port and for one it simply cannot follow.
138
+ * "Cannot tell" is not "not a port", and using the first as a shadow silences a
139
+ * real finding. It was measured: `catalog` binds
140
+ * `const customFields = this.#requireCustomFields()` — a call the analysis
141
+ * cannot follow, holding `custom_fields`' gated definition port — and treating
142
+ * that binding as a shadow took six `catch` sites in `attribute-commands.ts` out
143
+ * of the population.
144
+ *
145
+ * So this is a small, syntactic allow-list of values that cannot be a port
146
+ * however the analysis is extended: a literal, an object or array of
147
+ * non-ports, a `new` whose every argument is one of those, a primitive-valued
148
+ * operator. A **call**, an **identifier**, a **property access**, a **closure**
149
+ * and a parameter with no default are all "cannot tell", and none of them
150
+ * shadows anything. `catalog`'s `const queue = new Queue('catalog-bulk', {…})`,
151
+ * the collision the file-scoping rule was originally written for, is on the
152
+ * allow-list; `const transitionService = requireTransitionService()` is not, and
153
+ * does not need to be — issue #278's own case is fixed by scoping the parameter
154
+ * to its declaring file, which is where the binding actually is.
155
+ */
156
+ function manifestlyNotAPort(node) {
157
+ if (ts.isStringLiteralLike(node) ||
158
+ ts.isNumericLiteral(node) ||
159
+ ts.isBigIntLiteral(node) ||
160
+ ts.isRegularExpressionLiteral(node) ||
161
+ ts.isTemplateExpression(node) ||
162
+ node.kind === ts.SyntaxKind.TrueKeyword ||
163
+ node.kind === ts.SyntaxKind.FalseKeyword ||
164
+ node.kind === ts.SyntaxKind.NullKeyword ||
165
+ ts.isObjectLiteralExpression(node) ||
166
+ ts.isTypeOfExpression(node) ||
167
+ ts.isVoidExpression(node) ||
168
+ ts.isPrefixUnaryExpression(node)) {
169
+ return true;
170
+ }
171
+ if (ts.isArrayLiteralExpression(node))
172
+ return node.elements.every(manifestlyNotAPort);
173
+ if (ts.isNewExpression(node))
174
+ return (node.arguments ?? []).every(manifestlyNotAPort);
175
+ if (ts.isParenthesizedExpression(node) ||
176
+ ts.isAwaitExpression(node) ||
177
+ ts.isNonNullExpression(node) ||
178
+ ts.isAsExpression(node) ||
179
+ ts.isSatisfiesExpression(node)) {
180
+ return manifestlyNotAPort(node.expression);
181
+ }
182
+ if (ts.isConditionalExpression(node)) {
183
+ return manifestlyNotAPort(node.whenTrue) && manifestlyNotAPort(node.whenFalse);
184
+ }
185
+ if (ts.isBinaryExpression(node)) {
186
+ const operator = node.operatorToken.kind;
187
+ // `??`, `||` and `&&` yield one of their operands; every other operator
188
+ // yields a primitive, whatever its operands were.
189
+ if (operator !== ts.SyntaxKind.QuestionQuestionToken &&
190
+ operator !== ts.SyntaxKind.BarBarToken &&
191
+ operator !== ts.SyntaxKind.AmpersandAmpersandToken) {
192
+ return true;
193
+ }
194
+ return manifestlyNotAPort(node.left) && manifestlyNotAPort(node.right);
195
+ }
196
+ return false;
197
+ }
198
+ /**
199
+ * Does the binding `name` resolves to here hide a wider alias of that spelling?
200
+ *
201
+ * Three answers, and only the third is a shadow: no binding at all (the alias
202
+ * table answers), a binding the alias table itself introduced (it **is** the
203
+ * alias), or a binding whose value is manifestly not a port.
204
+ */
205
+ function shadowsAlias(at, name, carriers) {
206
+ const binding = nearestBinding(at, name);
207
+ if (binding === null || carriers.has(binding))
208
+ return false;
209
+ // A parameter with no default is "cannot tell": a caller this analysis did
210
+ // not walk may hand it a port, and the alias table is how that is found out.
211
+ if (ts.isParameter(binding)) {
212
+ return binding.initializer !== undefined && manifestlyNotAPort(binding.initializer);
213
+ }
214
+ if (!ts.isVariableDeclaration(binding) || binding.initializer === undefined)
215
+ return false;
216
+ return manifestlyNotAPort(binding.initializer);
217
+ }
218
+ /** Every alias is visible here: a gated port's own name, outside its owner. */
219
+ const EVERYWHERE = '*';
220
+ /**
221
+ * `<file>:<port>` — the ledger key, and the identity of a site, with `#promise`
222
+ * appended for the promise form.
223
+ *
224
+ * The key is deliberately not line-based: moving code inside a file must not
225
+ * invalidate an entry, and re-opening the hole must not silently inherit one.
226
+ * The discriminator is the same rule one granularity across. A file can hold
227
+ * both spellings over one alias — `product_feeds` does — and a single key would
228
+ * let an entry written for the `try` absorb a `.catch` added later, which is
229
+ * the inheritance the line-independence was chosen to avoid. It is a **suffix**
230
+ * so that every entry standing when the promise form landed keeps its key.
231
+ */
232
+ export function keyOf(found) {
233
+ const base = `${found.file}:${found.port}`;
234
+ return found.form === 'promise' ? `${base}#promise` : base;
235
+ }
236
+ export { lockedOwners };
237
+ /**
238
+ * The awilix resolver-builder chain. `ctx.asFunction(f).singleton()` is the
239
+ * factory `f` in another wrapper, so whatever `f` carries the registration
240
+ * carries — unlike an ordinary method call on a value, whose result is data.
241
+ */
242
+ const RESOLVER_BUILDERS = new Set(['singleton', 'scoped', 'transient', 'inject', 'disposer']);
243
+ /** Where such a chain starts — the registration wrapping a factory. */
244
+ const RESOLVER_ENTRIES = new Set(['asFunction', 'asValue', 'asClass']);
245
+ /**
246
+ * A call that claims a **container** name — `registerValues(container, …)`,
247
+ * `composedModules.contribute(…)`, `ctx.di.register(…)`,
248
+ * `ctx.di.providePort(…)`.
249
+ *
250
+ * `contribute` is the same claim as `registerValues` made inside D-45's window
251
+ * (issue #52), and it has to be read here for the same reason: a root moving 72
252
+ * contributions onto the method would otherwise take every one of those names
253
+ * out of this analysis, and the check would go quiet without anything changing
254
+ * about the tree.
255
+ *
256
+ * Deliberately narrow. Matching a bare `.register(` would also match Fastify's
257
+ * `app.register(plugin, options)` and turn every option key in the tree into an
258
+ * alias. The caller narrows it further: only a **root's** registration key is
259
+ * published everywhere, because a root belongs to no module and a module's own
260
+ * internal names (`mailer`, `client`, `settings`) collide with half the tree.
261
+ */
262
+ function isContainerRegistration(node) {
263
+ if (ts.isIdentifier(node.expression))
264
+ return node.expression.text === 'registerValues';
265
+ if (!ts.isPropertyAccessExpression(node.expression))
266
+ return false;
267
+ const method = node.expression.name.text;
268
+ if (method === 'contribute')
269
+ return true;
270
+ if (method !== 'register' && method !== 'providePort')
271
+ return false;
272
+ return tailName(node.expression.expression) === 'di';
273
+ }
274
+ /**
275
+ * Which names stand for a gated port, and where each one may be read.
276
+ *
277
+ * A **fixpoint**, because the shapes chain without bound: a `lazyPort` is bound
278
+ * to a local, the local passed to a constructor, the constructed holder given a
279
+ * deps-object key, that key registered on the container by a root, and the
280
+ * container name resolved by a module three files away. Each round can only add
281
+ * aliases, and a round that adds none is the last.
282
+ */
283
+ function analyze(sources, hostResident = NO_HOST_RESIDENT_MODULES, peerOwners = new Map()) {
284
+ const parsed = new Map();
285
+ for (const [file, text] of sources) {
286
+ parsed.set(file, ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true));
287
+ }
288
+ const portOwners = new Map();
289
+ // Peers first, so the walk's own registrations overwrite them: a package that
290
+ // provides a port is its owner whatever a peer's artefact also claims.
291
+ for (const [name, owner] of peerOwners)
292
+ portOwners.set(name, owner);
293
+ for (const [file, sf] of parsed) {
294
+ // A module's composition entry point, by the **marker** rather than by a
295
+ // filename (feature 080, T040b).
296
+ //
297
+ // This read `file.endsWith('backend.ts')`, which a module package's entry
298
+ // point is not: it lives wherever the `exports` map's `./backend` subpath
299
+ // points, and for every package in this repository that is
300
+ // `src/backend/index.ts`. So a packaged owner provided no port as far as
301
+ // this analysis was concerned, and every `catch` around one of its gates
302
+ // read clean — fail-open, and the same defect !920 found in
303
+ // `check-port-dependencies` under the same assumption. `webhooks` is where
304
+ // it surfaced when its module became a package: the `LEDGER-PERMANENT`
305
+ // self-edge below went *stale*, which is the loud half of a blindness whose
306
+ // other half is silent.
307
+ //
308
+ // `declaresRegisterModule` is `generate-composer.ts`'s own marker, so the
309
+ // composer and this check cannot disagree about which file composes a
310
+ // module.
311
+ if (!declaresRegisterModule(sf.getFullText()))
312
+ continue;
313
+ const owner = moduleOf(`/src/${file}`, hostResident);
314
+ if (owner === null)
315
+ continue;
316
+ for (const name of providedPortNames(sf.getFullText(), file))
317
+ portOwners.set(name, owner);
318
+ }
319
+ const aliases = new Map();
320
+ /** Alias → scope → the gated port names it carries there; see {@link Analysis.gatesAt}. */
321
+ const gatesOf = new Map();
322
+ /**
323
+ * The declarations the alias table itself introduced — a `const` bound to a
324
+ * carrying value, a parameter the port was passed as (issue #278).
325
+ *
326
+ * A binding in here is the alias rather than a shadow of one, which is what
327
+ * keeps `const cartService = lazyPort(…)` readable as a port in the very file
328
+ * that binds it while an unrelated `const transitionService = …` two modules
329
+ * away is not.
330
+ */
331
+ const carrierBindings = new Set();
332
+ /** True when the alias is new — which is what keeps the fixpoint running. */
333
+ let grew = false;
334
+ const markCarrier = (binding) => {
335
+ if (carrierBindings.has(binding))
336
+ return;
337
+ carrierBindings.add(binding);
338
+ // A lifted shadow is as much a change as a new alias: the round that lifts
339
+ // it may add nothing else, and the next round is where the reads it unblocks
340
+ // are seen.
341
+ grew = true;
342
+ };
343
+ const addAlias = (name, scope, where, gates) => {
344
+ const scopes = aliases.get(name) ?? new Set();
345
+ aliases.set(name, scopes);
346
+ if (!scopes.has(scope)) {
347
+ scopes.add(scope);
348
+ grew = true;
349
+ if (process.env.PORT_CATCH_WHY)
350
+ console.error(`ALIAS ${name} @${scope} <- ${where ?? '-'}`);
351
+ }
352
+ if (gates === undefined)
353
+ return;
354
+ // Gates keep the fixpoint running on their own: a holder can be bound
355
+ // before the round that discovers what it was built from.
356
+ const byScope = gatesOf.get(name) ?? new Map();
357
+ gatesOf.set(name, byScope);
358
+ const carried = byScope.get(scope) ?? new Set();
359
+ byScope.set(scope, carried);
360
+ for (const gate of gates) {
361
+ if (carried.has(gate))
362
+ continue;
363
+ carried.add(gate);
364
+ grew = true;
365
+ if (process.env.PORT_CATCH_WHY)
366
+ console.error(`GATE ${name} <- ${gate} @${where ?? '-'}`);
367
+ }
368
+ };
369
+ // A gated port's own name reads as one everywhere except inside its owner,
370
+ // where the identical identifier is normally the module's own instance.
371
+ for (const name of portOwners.keys())
372
+ addAlias(name, EVERYWHERE, undefined, new Set([name]));
373
+ const readsAsPort = (name, moduleId, file, at) => {
374
+ const scopes = aliases.get(name);
375
+ if (scopes === undefined)
376
+ return false;
377
+ // Lexical resolution first (issue #278): a nearer binding that manifestly
378
+ // holds no port is what the identifier denotes here, whatever a wider alias
379
+ // of the same spelling says.
380
+ if (at !== undefined && shadowsAlias(at, name, carrierBindings))
381
+ return false;
382
+ if (scopes.has(moduleId) || scopes.has(file))
383
+ return true;
384
+ return scopes.has(EVERYWHERE) && portOwners.get(name) !== moduleId;
385
+ };
386
+ const gatesAt = (name, moduleId, file) => {
387
+ const found = new Set();
388
+ for (const [scope, gates] of gatesOf.get(name) ?? []) {
389
+ const visible = scope === moduleId ||
390
+ scope === file ||
391
+ (scope === EVERYWHERE && portOwners.get(name) !== moduleId);
392
+ if (visible)
393
+ for (const gate of gates)
394
+ found.add(gate);
395
+ }
396
+ return found;
397
+ };
398
+ /** `lazyPort<T>(ctx, 'gatedName')`, and only a gated one. */
399
+ const isProxyCall = (node) => {
400
+ if (!ts.isCallExpression(node) ||
401
+ !ts.isIdentifier(node.expression) ||
402
+ node.expression.text !== 'lazyPort') {
403
+ return false;
404
+ }
405
+ const [, nameArgument] = node.arguments;
406
+ return (nameArgument !== undefined &&
407
+ ts.isStringLiteralLike(nameArgument) &&
408
+ portOwners.has(nameArgument.text));
409
+ };
410
+ const declarations = collectDeclarations(parsed);
411
+ const resolveClass = (file, name) => resolveDeclaration(declarations, (perFile) => perFile.classes, file, name);
412
+ const resolveFunction = (file, name) => resolveDeclaration(declarations, (perFile) => perFile.functions, file, name);
413
+ /**
414
+ * Every spelling some file declares a function under — what the factory
415
+ * clause in `carries` asks. Deliberately still by name: see that clause.
416
+ */
417
+ const functionNames = new Set();
418
+ for (const perFile of declarations.values()) {
419
+ for (const name of perFile.functions.keys())
420
+ functionNames.add(name);
421
+ }
422
+ /**
423
+ * One round over every file. Reads the alias table as it stands and adds to
424
+ * it; `grew` says whether another round can find anything new.
425
+ */
426
+ const round = () => {
427
+ for (const [file, sf] of parsed) {
428
+ const scope = moduleOf(`/src/${file}`, hostResident) ?? ROOT;
429
+ const reads = (name, at) => readsAsPort(name, scope, file, at);
430
+ const at = (node) => `${file}:${sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1}`;
431
+ /**
432
+ * Does evaluating this expression yield a value that still reaches the
433
+ * gate?
434
+ *
435
+ * Calls are the deliberate exception: `proxy.applyToCart(…)` *is* the
436
+ * gated call, but its **result** is data, so carriage does not survive a
437
+ * call. What does survive is construction — a holder given the proxy
438
+ * keeps forwarding to it — and a closure, which has not run yet.
439
+ */
440
+ const carries = (node) => {
441
+ if (isProxyCall(node))
442
+ return true;
443
+ // A bare identifier resolves lexically first (issue #278); a property
444
+ // name below does not, having no lexical binding to resolve against.
445
+ if (ts.isIdentifier(node))
446
+ return reads(node.text, node);
447
+ // `cradle().promotionService`, `this.deps.promotion` — the **trailing**
448
+ // name is what the gate answers for. A field read *off* a port
449
+ // (`proxy.rows`, `p.attributeValues[k]`) is data, and treating it as a
450
+ // carrier cascaded through every local it was ever assigned to.
451
+ if (ts.isPropertyAccessExpression(node))
452
+ return reads(node.name.text);
453
+ if (ts.isParenthesizedExpression(node) ||
454
+ ts.isAwaitExpression(node) ||
455
+ ts.isNonNullExpression(node) ||
456
+ ts.isAsExpression(node) ||
457
+ ts.isSatisfiesExpression(node)) {
458
+ return carries(node.expression);
459
+ }
460
+ if (ts.isConditionalExpression(node)) {
461
+ return carries(node.whenTrue) || carries(node.whenFalse);
462
+ }
463
+ // `port ?? fallback`, `flag && port` — a comparison yields a boolean.
464
+ if (ts.isBinaryExpression(node)) {
465
+ const operator = node.operatorToken.kind;
466
+ if (operator !== ts.SyntaxKind.QuestionQuestionToken &&
467
+ operator !== ts.SyntaxKind.BarBarToken &&
468
+ operator !== ts.SyntaxKind.AmpersandAmpersandToken) {
469
+ return false;
470
+ }
471
+ return carries(node.left) || carries(node.right);
472
+ }
473
+ // An object literal deliberately does **not** carry, however many ports
474
+ // it holds: a deps bag is a record, and tainting the bag makes every
475
+ // `this.deps.anythingAtAll()` in the receiving class read as a port
476
+ // call. Its carrying keys become aliases one by one instead, which is
477
+ // where the port is actually reached.
478
+ //
479
+ // So a holder **built from** a bag does not carry either, and that
480
+ // blind spot is known and kept (feature 134 D18 §2, half 2 withdrawn).
481
+ // Letting `new C({ k: <carrier> })` carry was measured: it made the
482
+ // `product_feeds` inline-delivery chain visible, and it also added
483
+ // fourteen unhandled sites, among them `catch` blocks around holder
484
+ // methods that never touch the port — `connections.getOrFail()` reads a
485
+ // row, yet the holder carries `credentials` as a whole, because gates
486
+ // are attributed per value rather than per method. A site the analysis
487
+ // cannot tell apart from a real swallow has no honest classification,
488
+ // so the widening waits for per-method attribution.
489
+ if (ts.isArrayLiteralExpression(node))
490
+ return node.elements.some(carries);
491
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node))
492
+ return bodyReads(node, reads);
493
+ if (ts.isNewExpression(node))
494
+ return (node.arguments ?? []).some(carries);
495
+ if (ts.isCallExpression(node)) {
496
+ // A **factory** hands the port on, and `ctx.asFunction(f).singleton()`
497
+ // is `f` in a wrapper. Every other call returns data — including
498
+ // `proxy.applyToCart(…)`, which *is* the gated call but whose result
499
+ // is a discount, and `JSON.stringify(payloadFromAPort)`, which is a
500
+ // string. Carriage through an arbitrary callee turned every local
501
+ // downstream of a port into an alias.
502
+ const callee = node.expression;
503
+ const argumentsCarry = () => (node.arguments ?? []).some(carries);
504
+ if (ts.isPropertyAccessExpression(callee)) {
505
+ if (RESOLVER_BUILDERS.has(callee.name.text))
506
+ return carries(callee.expression);
507
+ return RESOLVER_ENTRIES.has(callee.name.text) && argumentsCarry();
508
+ }
509
+ if (!ts.isIdentifier(callee))
510
+ return false;
511
+ return ((RESOLVER_ENTRIES.has(callee.text) || functionNames.has(callee.text)) && argumentsCarry());
512
+ }
513
+ return false;
514
+ };
515
+ /**
516
+ * **Which** gates a carrying expression reaches — the input to D-63's
517
+ * `OWNER LOCKED` derivation.
518
+ *
519
+ * A blunt walk of the whole expression rather than a mirror of
520
+ * {@link carries}: it is only ever asked about a value that already
521
+ * carries, and over-collecting can only add owners, which can only make
522
+ * the "every owner is locked" test harder to pass. Under-collecting is
523
+ * the error that would matter, because it retires a site whose gate an
524
+ * operator can still close.
525
+ *
526
+ * Which is why the lexical narrowing of issue #278 stops here: `reads` is
527
+ * called **without** a node, so a shadowed local still contributes its
528
+ * gates. Shadowing decides whether a site *exists*; it may not decide
529
+ * which owners a site that does exist rests on.
530
+ */
531
+ const gatesIn = (node) => {
532
+ const found = new Set();
533
+ const record = (name) => {
534
+ if (portOwners.has(name))
535
+ found.add(name);
536
+ for (const gate of gatesAt(name, scope, file))
537
+ found.add(gate);
538
+ };
539
+ const scan = (inner) => {
540
+ if (ts.isTypeNode(inner))
541
+ return;
542
+ if (isProxyCall(inner) && ts.isCallExpression(inner)) {
543
+ const [, nameArgument] = inner.arguments;
544
+ if (nameArgument !== undefined && ts.isStringLiteralLike(nameArgument)) {
545
+ found.add(nameArgument.text);
546
+ }
547
+ return;
548
+ }
549
+ if (ts.isPropertyAccessExpression(inner)) {
550
+ if (reads(inner.name.text))
551
+ record(inner.name.text);
552
+ scan(inner.expression);
553
+ return;
554
+ }
555
+ if (ts.isIdentifier(inner) && reads(inner.text))
556
+ record(inner.text);
557
+ inner.forEachChild(scan);
558
+ };
559
+ scan(node);
560
+ return found;
561
+ };
562
+ const visit = (node) => {
563
+ // `const cartService = lazyPort(…)`, `const recompute = new X(port)`.
564
+ // Scoped to the **file**, because a `const` is: `catalog` renames its
565
+ // bulk-operation service to `queue` in one file and holds a BullMQ
566
+ // queue under the same spelling in another, and a module-wide binding
567
+ // read the second as a port.
568
+ if (ts.isVariableDeclaration(node) &&
569
+ ts.isIdentifier(node.name) &&
570
+ node.initializer &&
571
+ carries(node.initializer)) {
572
+ addAlias(node.name.text, file, at(node), gatesIn(node.initializer));
573
+ // This binding *is* the alias, so it must not read as a shadow of one.
574
+ markCarrier(node);
575
+ }
576
+ // `{ promotion: lazyPort(ctx, 'promotionService') }` — a deps-object key.
577
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
578
+ if (carries(node.initializer)) {
579
+ addAlias(node.name.text, scope, at(node), gatesIn(node.initializer));
580
+ }
581
+ }
582
+ if (ts.isShorthandPropertyAssignment(node) && reads(node.name.text)) {
583
+ addAlias(node.name.text, scope, at(node), gatesIn(node.name));
584
+ }
585
+ if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
586
+ // `new CartService(emFactory, lazyPort(…))` — the parameter it lands on.
587
+ const callee = ts.isIdentifier(node.expression) ? node.expression.text : null;
588
+ if (callee !== null && node.arguments) {
589
+ node.arguments.forEach((argument, index) => {
590
+ if (!carries(argument))
591
+ return;
592
+ // The two lookups are kept apart rather than merged into one
593
+ // ternary: `classes` and `functions` hold different declaration
594
+ // shapes, and merging them costs two casts for no gain.
595
+ let declared;
596
+ if (ts.isNewExpression(node)) {
597
+ const owner = resolveClass(file, callee);
598
+ const constructor = owner?.declaration.members.find(ts.isConstructorDeclaration);
599
+ if (owner !== undefined && constructor !== undefined) {
600
+ declared = { file: owner.file, declaration: constructor };
601
+ }
602
+ }
603
+ else {
604
+ declared = resolveFunction(file, callee);
605
+ }
606
+ if (declared === undefined)
607
+ return;
608
+ const parameter = declared.declaration.parameters[index];
609
+ if (parameter && ts.isIdentifier(parameter.name)) {
610
+ // Issue #278 — the **declaring** file, which is where the
611
+ // parameter is in scope, rather than the module the `new`
612
+ // happens to sit in, where it is not in scope at all.
613
+ addAlias(parameter.name.text, declared.file, at(node), gatesIn(argument));
614
+ markCarrier(parameter);
615
+ }
616
+ });
617
+ }
618
+ // A name a **root** contributes is global: it belongs to no module,
619
+ // and whichever module resolves it gets the port behind it. That is
620
+ // the shape the five e-mail notifiers arrived by (issue #113). A
621
+ // module's own `ctx.di.register` key stays module-scoped — it is
622
+ // either a port, and already global by the rule above, or an internal
623
+ // name whose spelling (`mailer`, `client`, `settings`) collides with
624
+ // half the tree.
625
+ if (scope === ROOT && ts.isCallExpression(node) && isContainerRegistration(node)) {
626
+ for (const argument of node.arguments) {
627
+ if (!ts.isObjectLiteralExpression(argument))
628
+ continue;
629
+ for (const property of argument.properties) {
630
+ if (ts.isPropertyAssignment(property) &&
631
+ ts.isIdentifier(property.name) &&
632
+ carries(property.initializer)) {
633
+ addAlias(property.name.text, EVERYWHERE, at(property), gatesIn(property.initializer));
634
+ }
635
+ if (ts.isShorthandPropertyAssignment(property) && reads(property.name.text)) {
636
+ addAlias(property.name.text, EVERYWHERE, at(property), gatesIn(property.name));
637
+ }
638
+ }
639
+ }
640
+ }
641
+ }
642
+ node.forEachChild(visit);
643
+ };
644
+ sf.forEachChild(visit);
645
+ }
646
+ };
647
+ // Bounded so a pathological tree cannot spin: each round can only add
648
+ // aliases, and the deepest chain the tree holds today is five hops.
649
+ for (let pass = 0; pass < 24; pass += 1) {
650
+ grew = false;
651
+ round();
652
+ if (!grew)
653
+ break;
654
+ }
655
+ return { portOwners, aliases, gatesAt, readsAsPort, parsed };
656
+ }
657
+ /**
658
+ * The file a relative specifier names, among the files read — or `null` for a
659
+ * bare specifier, which names a package and is never another module's private
660
+ * source (D18), or for a path the walk does not hold.
661
+ */
662
+ function resolveRelative(fromFile, specifier, parsed) {
663
+ if (!specifier.startsWith('./') && !specifier.startsWith('../'))
664
+ return null;
665
+ const base = posix.normalize(posix.join(posix.dirname(fromFile), specifier));
666
+ const stem = base.replace(/\.(?:js|ts|mjs|mts)$/, '');
667
+ for (const candidate of [`${stem}.ts`, `${stem}.tsx`, `${stem}.mts`, `${stem}/index.ts`]) {
668
+ if (parsed.has(candidate))
669
+ return candidate;
670
+ }
671
+ return null;
672
+ }
673
+ function collectDeclarations(parsed) {
674
+ const all = new Map();
675
+ for (const [file, sf] of parsed) {
676
+ const own = {
677
+ classes: new Map(),
678
+ functions: new Map(),
679
+ imports: new Map(),
680
+ reExports: [],
681
+ };
682
+ const collect = (node) => {
683
+ if (ts.isClassDeclaration(node) && node.name)
684
+ own.classes.set(node.name.text, node);
685
+ if (ts.isFunctionDeclaration(node) && node.name)
686
+ own.functions.set(node.name.text, node);
687
+ if (ts.isVariableDeclaration(node) &&
688
+ ts.isIdentifier(node.name) &&
689
+ node.initializer &&
690
+ ts.isArrowFunction(node.initializer)) {
691
+ own.functions.set(node.name.text, node.initializer);
692
+ }
693
+ node.forEachChild(collect);
694
+ };
695
+ sf.forEachChild(collect);
696
+ for (const statement of sf.statements) {
697
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
698
+ const from = resolveRelative(file, statement.moduleSpecifier.text, parsed);
699
+ const bindings = statement.importClause?.namedBindings;
700
+ if (from === null || bindings === undefined || !ts.isNamedImports(bindings))
701
+ continue;
702
+ for (const element of bindings.elements) {
703
+ own.imports.set(element.name.text, {
704
+ from,
705
+ name: (element.propertyName ?? element.name).text,
706
+ });
707
+ }
708
+ }
709
+ if (ts.isExportDeclaration(statement) &&
710
+ statement.moduleSpecifier !== undefined &&
711
+ ts.isStringLiteral(statement.moduleSpecifier)) {
712
+ const from = resolveRelative(file, statement.moduleSpecifier.text, parsed);
713
+ if (from === null)
714
+ continue;
715
+ const clause = statement.exportClause;
716
+ if (clause === undefined) {
717
+ own.reExports.push({ from, names: null });
718
+ }
719
+ else if (ts.isNamedExports(clause)) {
720
+ const names = new Map();
721
+ for (const element of clause.elements) {
722
+ names.set(element.name.text, (element.propertyName ?? element.name).text);
723
+ }
724
+ own.reExports.push({ from, names });
725
+ }
726
+ }
727
+ }
728
+ all.set(file, own);
729
+ }
730
+ return all;
731
+ }
732
+ /**
733
+ * The declaration `name`, written in `file`, resolves to: the file's own, or
734
+ * the one a relative import names, followed through relative re-exports.
735
+ * Never a declaration some unrelated file happens to spell the same way.
736
+ */
737
+ function resolveDeclaration(declarations, pick, file, name) {
738
+ const own = (at, exported) => {
739
+ const perFile = declarations.get(at);
740
+ const declaration = perFile === undefined ? undefined : pick(perFile).get(exported);
741
+ return declaration === undefined ? undefined : { file: at, declaration };
742
+ };
743
+ const seen = new Set();
744
+ const exportedFrom = (at, exported) => {
745
+ const visit = `${at}#${exported}`;
746
+ if (seen.has(visit))
747
+ return undefined;
748
+ seen.add(visit);
749
+ const direct = own(at, exported);
750
+ if (direct !== undefined)
751
+ return direct;
752
+ const perFile = declarations.get(at);
753
+ if (perFile === undefined)
754
+ return undefined;
755
+ // `import { x } from './y.js'; export { x };` is not followed: no file in
756
+ // the tree re-exports that way, and following it would be one more shape
757
+ // nothing exercises.
758
+ for (const reExport of perFile.reExports) {
759
+ if (reExport.names === null) {
760
+ const found = exportedFrom(reExport.from, exported);
761
+ if (found !== undefined)
762
+ return found;
763
+ }
764
+ else {
765
+ const original = reExport.names.get(exported);
766
+ if (original !== undefined) {
767
+ const found = exportedFrom(reExport.from, original);
768
+ if (found !== undefined)
769
+ return found;
770
+ }
771
+ }
772
+ }
773
+ return undefined;
774
+ };
775
+ const local = own(file, name);
776
+ if (local !== undefined)
777
+ return local;
778
+ const imported = declarations.get(file)?.imports.get(name);
779
+ return imported === undefined ? undefined : exportedFrom(imported.from, imported.name);
780
+ }
781
+ /**
782
+ * Does a closure's body reach the gate? A `() => cradle().portName()` has not
783
+ * resolved anything yet, so the closure itself carries and every name it is
784
+ * bound to is an alias.
785
+ *
786
+ * Property *names* and parameter *names* are skipped: `{ promotion: 1 }`
787
+ * mentions an alias without reading one.
788
+ */
789
+ function bodyReads(fn, reads) {
790
+ let hit = false;
791
+ const scan = (node) => {
792
+ if (hit || ts.isTypeNode(node))
793
+ return;
794
+ if (ts.isPropertyAccessExpression(node)) {
795
+ if (reads(node.name.text)) {
796
+ hit = true;
797
+ return;
798
+ }
799
+ scan(node.expression);
800
+ return;
801
+ }
802
+ if (ts.isPropertyAssignment(node)) {
803
+ scan(node.initializer);
804
+ return;
805
+ }
806
+ if (ts.isParameter(node)) {
807
+ if (node.initializer)
808
+ scan(node.initializer);
809
+ return;
810
+ }
811
+ // Issue #278 — a bare identifier inside the closure resolves lexically, so
812
+ // a `const` the closure binds itself does not make the closure a carrier.
813
+ if (ts.isIdentifier(node) && reads(node.text, node)) {
814
+ hit = true;
815
+ return;
816
+ }
817
+ node.forEachChild(scan);
818
+ };
819
+ scan(fn.body);
820
+ return hit;
821
+ }
822
+ /** The trailing identifier of a receiver: `this.deps.x` → `x`, `y` → `y`. */
823
+ function tailName(node) {
824
+ if (ts.isIdentifier(node))
825
+ return node.text;
826
+ if (ts.isPropertyAccessExpression(node))
827
+ return node.name.text;
828
+ if (ts.isNonNullExpression(node) || ts.isParenthesizedExpression(node)) {
829
+ return tailName(node.expression);
830
+ }
831
+ return null;
832
+ }
833
+ /**
834
+ * Functions the `catch` may hand the error to and still be handling it.
835
+ *
836
+ * Two shapes exist in the tree, and both are correct code the literal rule read
837
+ * as a violation: `toCatalogHttpError(err, key): never`, whose last statement is
838
+ * `throw err`, and `ReturnEmailNotifier#contained(rc, kind, error)`, which calls
839
+ * `rethrowIfModuleDisabled` on the caller's behalf. The requirement is narrow on
840
+ * purpose — the delegate must **end** by re-throwing its own parameter, or name
841
+ * the kernel's narrowing. A helper that ends by throwing something it built
842
+ * itself converts the presence answer and does not qualify.
843
+ */
844
+ function collectRethrowDelegates(parsed) {
845
+ const delegates = new Set();
846
+ const qualifies = (parameters, body) => {
847
+ if (body === undefined || !ts.isBlock(body))
848
+ return false;
849
+ let names = false;
850
+ const scan = (node) => {
851
+ if (ts.isIdentifier(node) &&
852
+ (node.text === 'rethrowIfModuleDisabled' || node.text === 'ModuleDisabledError')) {
853
+ names = true;
854
+ }
855
+ node.forEachChild(scan);
856
+ };
857
+ body.forEachChild(scan);
858
+ if (names)
859
+ return true;
860
+ const last = body.statements.at(-1);
861
+ if (!last || !ts.isThrowStatement(last) || !last.expression)
862
+ return false;
863
+ if (!ts.isIdentifier(last.expression))
864
+ return false;
865
+ const thrown = last.expression.text;
866
+ return parameters.some((parameter) => ts.isIdentifier(parameter.name) && parameter.name.text === thrown);
867
+ };
868
+ for (const sf of parsed) {
869
+ const collect = (node) => {
870
+ if ((ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) &&
871
+ node.name &&
872
+ ts.isIdentifier(node.name) &&
873
+ qualifies(node.parameters, node.body)) {
874
+ delegates.add(node.name.text);
875
+ }
876
+ if (ts.isVariableDeclaration(node) &&
877
+ ts.isIdentifier(node.name) &&
878
+ node.initializer &&
879
+ (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) &&
880
+ qualifies(node.initializer.parameters, node.initializer.body)) {
881
+ delegates.add(node.name.text);
882
+ }
883
+ node.forEachChild(collect);
884
+ };
885
+ sf.forEachChild(collect);
886
+ }
887
+ return delegates;
888
+ }
889
+ /** The kernel's one-line narrowing, by the name every handler spells it. */
890
+ const NARROWING = 'rethrowIfModuleDisabled';
891
+ /**
892
+ * Does a handler body let `ModuleDisabledError` through?
893
+ *
894
+ * One predicate for both forms, deliberately: a `catch` block and a
895
+ * `.catch(handler)` body answer the same question, and two implementations of
896
+ * it are two answers waiting to disagree — which is how the promise form came
897
+ * to be unjudged in the first place.
898
+ *
899
+ * A **block** whose last statement is a `throw` re-throws unconditionally. An
900
+ * **expression** body (a concise arrow) cannot throw at all, so it qualifies
901
+ * only by naming the narrowing or a delegate. `catch (e) { if (rare) throw e }`
902
+ * and `.catch((e) => { if (rare) throw e })` both fail the last-statement test
903
+ * and both are violations: `ModuleDisabledError` is an `HttpError`, so a
904
+ * status-code test lets it through by accident rather than by decision.
905
+ */
906
+ function bodyHandles(body, delegates) {
907
+ if (ts.isBlock(body)) {
908
+ const last = body.statements.at(-1);
909
+ if (last && ts.isThrowStatement(last))
910
+ return true;
911
+ }
912
+ let named = false;
913
+ const scan = (node) => {
914
+ if (ts.isIdentifier(node) && node.text === NARROWING)
915
+ named = true;
916
+ if (ts.isIdentifier(node) && node.text === 'ModuleDisabledError')
917
+ named = true;
918
+ if (ts.isCallExpression(node)) {
919
+ const callee = ts.isPropertyAccessExpression(node.expression)
920
+ ? node.expression.name.text
921
+ : ts.isIdentifier(node.expression)
922
+ ? node.expression.text
923
+ : null;
924
+ if (callee !== null && delegates.has(callee))
925
+ named = true;
926
+ }
927
+ node.forEachChild(scan);
928
+ };
929
+ scan(body);
930
+ return named;
931
+ }
932
+ /** Does this `catch` let `ModuleDisabledError` through? */
933
+ function handles(clause, delegates) {
934
+ return bodyHandles(clause.block, delegates);
935
+ }
936
+ function rejectionHandlerOf(node) {
937
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression))
938
+ return null;
939
+ const method = node.expression.name.text;
940
+ const guarded = node.expression.expression;
941
+ if (method === 'catch') {
942
+ const [handler] = node.arguments;
943
+ // `.catch()` with no argument consumes nothing; it is not a site.
944
+ return handler === undefined ? null : { guarded, handler };
945
+ }
946
+ if (method === 'then') {
947
+ const handler = node.arguments[1];
948
+ return handler === undefined ? null : { guarded, handler };
949
+ }
950
+ return null;
951
+ }
952
+ /**
953
+ * Does a promise-form handler let `ModuleDisabledError` through?
954
+ *
955
+ * Three ways, and the first two have no counterpart in the statement form
956
+ * because a `catch` block cannot be written as a reference: the narrowing
957
+ * handed over directly (`.catch(rethrowIfModuleDisabled)`), a re-throwing
958
+ * delegate handed over directly (`.catch(toCatalogHttpError)` — the same
959
+ * delegates the statement form accepts being *called*), and a function
960
+ * expression whose body qualifies under {@link bodyHandles}.
961
+ *
962
+ * Anything else — a bare identifier the analysis cannot see through, a logger,
963
+ * `() => undefined` — is a violation, in the direction the doubt has to fail.
964
+ */
965
+ function handlerHandles(handler, delegates) {
966
+ if (ts.isArrowFunction(handler) || ts.isFunctionExpression(handler)) {
967
+ return bodyHandles(handler.body, delegates);
968
+ }
969
+ const named = tailName(handler);
970
+ if (named === null)
971
+ return false;
972
+ return named === NARROWING || delegates.has(named);
973
+ }
974
+ /**
975
+ * How many rounds the method fixpoint runs before it gives up.
976
+ *
977
+ * The same cap the alias table uses, for the same reason: a chain of private
978
+ * methods is bounded in practice, and a cap makes "did not converge" a bug
979
+ * report rather than a hang.
980
+ */
981
+ const METHOD_HOP_ROUNDS = 12;
982
+ /** The name of a class member as `this.<name>` would spell it, or `null`. */
983
+ function memberName(name) {
984
+ if (name === undefined)
985
+ return null;
986
+ if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name))
987
+ return name.text;
988
+ if (ts.isStringLiteral(name))
989
+ return name.text;
990
+ return null;
991
+ }
992
+ /** `this.<name>` / `this.#name`, as the receiver of a call — the name, or `null`. */
993
+ function thisMethodCalled(node) {
994
+ if (!ts.isPropertyAccessExpression(node.expression))
995
+ return null;
996
+ if (node.expression.expression.kind !== ts.SyntaxKind.ThisKeyword)
997
+ return null;
998
+ const name = node.expression.name;
999
+ return ts.isIdentifier(name) || ts.isPrivateIdentifier(name) ? name.text : null;
1000
+ }
1001
+ /** The class a node sits inside, by name — `null` for a class expression. */
1002
+ function enclosingClassName(node) {
1003
+ for (let at = node; at !== undefined; at = at.parent) {
1004
+ if (ts.isClassDeclaration(at))
1005
+ return at.name?.text ?? null;
1006
+ }
1007
+ return null;
1008
+ }
1009
+ /** `<file>#<class>#<method>` — a method's identity for the hop. */
1010
+ function methodKey(file, className, method) {
1011
+ return `${file}#${className}#${method}`;
1012
+ }
1013
+ /**
1014
+ * Which methods carry a gate into a `catch` around a call to them (D-88).
1015
+ *
1016
+ * Two passes and then a fixpoint. The first records, per class member, the port
1017
+ * or alias names its body reaches directly — the same three call shapes the
1018
+ * `try` scan reads, so a method and a `try` block agree about what a port call
1019
+ * looks like by construction. The second records `this.<name>(…)` edges inside
1020
+ * the class. The fixpoint then walks the edges backwards until nothing new
1021
+ * appears, which is what makes a private method calling a private method that
1022
+ * reaches a gate carry as well.
1023
+ *
1024
+ * Nothing crosses a class boundary here, and nothing crosses a file boundary:
1025
+ * that is the whole limit, and it is stated in the header because one hop is a
1026
+ * limit too.
1027
+ */
1028
+ function collectCarryingMethods(parsed, readsAsPortIn, declaresMember) {
1029
+ /** method key → the port/alias names its body reaches. */
1030
+ const carries = new Map();
1031
+ /** method key → the method keys it calls through `this`. */
1032
+ const edges = new Map();
1033
+ for (const [file, sf] of parsed) {
1034
+ const reads = (name, at) => readsAsPortIn(file, name, at);
1035
+ const record = (className, method, body) => {
1036
+ const key = methodKey(file, className, method);
1037
+ const ports = carries.get(key) ?? new Set();
1038
+ const calls = edges.get(key) ?? new Set();
1039
+ const scan = (inner) => {
1040
+ if (ts.isCallExpression(inner)) {
1041
+ const hop = thisMethodCalled(inner);
1042
+ if (hop !== null && declaresMember(file, className, hop)) {
1043
+ calls.add(methodKey(file, className, hop));
1044
+ }
1045
+ else if (ts.isPropertyAccessExpression(inner.expression)) {
1046
+ const receiver = tailName(inner.expression.expression);
1047
+ // Issue #278 — the receiver resolves lexically when it is a bare
1048
+ // identifier (`transitionService.apply(…)`); `this.deps.promotion`
1049
+ // is a property chain and has no lexical binding to resolve.
1050
+ const receiverNode = ts.isIdentifier(inner.expression.expression)
1051
+ ? inner.expression.expression
1052
+ : undefined;
1053
+ if (receiver !== null && reads(receiver, receiverNode))
1054
+ ports.add(receiver);
1055
+ else if (reads(inner.expression.name.text))
1056
+ ports.add(inner.expression.name.text);
1057
+ }
1058
+ if (ts.isIdentifier(inner.expression) &&
1059
+ inner.expression.text !== 'lazyPort' &&
1060
+ reads(inner.expression.text, inner.expression)) {
1061
+ ports.add(inner.expression.text);
1062
+ }
1063
+ if (ts.isIdentifier(inner.expression) && inner.expression.text === 'requireModuleEnabled') {
1064
+ ports.add('requireModuleEnabled');
1065
+ }
1066
+ }
1067
+ inner.forEachChild(scan);
1068
+ };
1069
+ body.forEachChild(scan);
1070
+ carries.set(key, ports);
1071
+ edges.set(key, calls);
1072
+ };
1073
+ for (const [className, members] of classesIn(sf)) {
1074
+ for (const [name, body] of members)
1075
+ record(className, name, body);
1076
+ }
1077
+ }
1078
+ for (let round = 0; round < METHOD_HOP_ROUNDS; round += 1) {
1079
+ let grew = false;
1080
+ for (const [key, calls] of edges) {
1081
+ const ports = carries.get(key);
1082
+ if (ports === undefined)
1083
+ continue;
1084
+ for (const callee of calls) {
1085
+ for (const port of carries.get(callee) ?? []) {
1086
+ if (!ports.has(port)) {
1087
+ ports.add(port);
1088
+ grew = true;
1089
+ }
1090
+ }
1091
+ }
1092
+ }
1093
+ if (!grew)
1094
+ break;
1095
+ }
1096
+ // A method that reaches nothing is not a carrier; dropping it here keeps the
1097
+ // lookup at the `try` site a presence test rather than a size test.
1098
+ for (const [key, ports] of [...carries])
1099
+ if (ports.size === 0)
1100
+ carries.delete(key);
1101
+ return carries;
1102
+ }
1103
+ /** Class name → its method-shaped members, by the name `this.<x>` spells. */
1104
+ function classesIn(sf) {
1105
+ const classes = new Map();
1106
+ const visit = (node) => {
1107
+ if (ts.isClassDeclaration(node) && node.name) {
1108
+ const members = classes.get(node.name.text) ?? new Map();
1109
+ for (const member of node.members) {
1110
+ const name = memberName(member.name);
1111
+ if (name === null)
1112
+ continue;
1113
+ if (ts.isMethodDeclaration(member) && member.body)
1114
+ members.set(name, member.body);
1115
+ else if (ts.isPropertyDeclaration(member) &&
1116
+ member.initializer &&
1117
+ (ts.isArrowFunction(member.initializer) || ts.isFunctionExpression(member.initializer))) {
1118
+ members.set(name, member.initializer.body);
1119
+ }
1120
+ }
1121
+ classes.set(node.name.text, members);
1122
+ }
1123
+ node.forEachChild(visit);
1124
+ };
1125
+ sf.forEachChild(visit);
1126
+ return classes;
1127
+ }
1128
+ /** Every `try` and every promise-form rejection handler that reaches a gated port. */
1129
+ export function findPortCatches(input) {
1130
+ return scanSources(input).found;
1131
+ }
1132
+ function scanSources(input) {
1133
+ const hostResident = input.hostResidentModules ?? NO_HOST_RESIDENT_MODULES;
1134
+ const analysis = analyze(input.sources, hostResident, input.peerOwners ?? new Map());
1135
+ const locked = lockedOwners(input.manifests ?? []);
1136
+ /**
1137
+ * D-63 — every gate this alias carries is owned by a module the platform
1138
+ * refuses to switch off, so the `catch` has no reachable presence answer to
1139
+ * swallow. An alias carrying no gate at all (`requireModuleEnabled`) is never
1140
+ * locked: nothing is known about what it answers.
1141
+ */
1142
+ const isOwnerLocked = (gates) => gates.length > 0 &&
1143
+ gates.every((gate) => {
1144
+ const owner = analysis.portOwners.get(gate);
1145
+ return owner !== undefined && locked.has(owner);
1146
+ });
1147
+ const found = [];
1148
+ /** Every `.catch(h)` / `.then(ok, onErr)` the walk read — see {@link SiteScan}. */
1149
+ let rejectionHandlerSites = 0;
1150
+ // The analysis's own ASTs, not a second parse of the same text: the shadowing
1151
+ // rule (issue #278) records **declaration nodes** as carriers, and node
1152
+ // identity only holds across one parse.
1153
+ const { parsed } = analysis;
1154
+ const delegates = collectRethrowDelegates(parsed.values());
1155
+ /**
1156
+ * The classes each file declares, by member name.
1157
+ *
1158
+ * It decides a **shadowing** rule the pre-D-88 check got wrong in one
1159
+ * direction: `this.close(…)` where `close` is a method of the enclosing class
1160
+ * is that method, never a module-scoped alias that happens to share the
1161
+ * spelling. `product_feeds` holds both — a plugin closure bound to `close` and
1162
+ * a `TaxonomyRefreshService#close` that writes a check row — and without this
1163
+ * the hop reported the second as though it reached the first's twenty gates.
1164
+ */
1165
+ const declaredMembers = new Map();
1166
+ for (const [file, sf] of parsed) {
1167
+ declaredMembers.set(file, new Map([...classesIn(sf)].map(([name, members]) => [name, new Set(members.keys())])));
1168
+ }
1169
+ const declaresMember = (file, className, name) => declaredMembers.get(file)?.get(className)?.has(name) === true;
1170
+ const carryingMethods = collectCarryingMethods(parsed, (file, name, at) => analysis.readsAsPort(name, moduleOf(`/src/${file}`, hostResident) ?? ROOT, file, at), declaresMember);
1171
+ for (const [file] of input.sources) {
1172
+ const moduleId = moduleOf(`/src/${file}`, hostResident) ?? ROOT;
1173
+ const sf = parsed.get(file);
1174
+ const readsAsPort = (name, at) => analysis.readsAsPort(name, moduleId, file, at);
1175
+ /**
1176
+ * One site, whichever spelling it is written in.
1177
+ *
1178
+ * `guardedRoots` are the expressions the handler would absorb a throw from
1179
+ * — the `try` block's statements, or the receiver chain a `.catch` hangs
1180
+ * off. Everything below that point is identical for the two forms by
1181
+ * construction, which is what stops the promise form drifting into a
1182
+ * second, weaker rule.
1183
+ */
1184
+ const record = (guardedRoots, at, handled, form) => {
1185
+ /**
1186
+ * The name the site is reported under → the port/alias names it stands
1187
+ * for. Identity for a direct reach; for a D-88 method hop the key is the
1188
+ * method and the value is what its body reaches, so `gates` — and with
1189
+ * it the `OWNER LOCKED` derivation — comes out unchanged.
1190
+ */
1191
+ const ports = new Map();
1192
+ const add = (name, via) => {
1193
+ const carried = ports.get(name) ?? new Set();
1194
+ for (const one of via)
1195
+ carried.add(one);
1196
+ ports.set(name, carried);
1197
+ };
1198
+ const className = enclosingClassName(at);
1199
+ /** D-88's shadowing rule — see {@link declaredMembers}. */
1200
+ const ownMethod = (inner) => {
1201
+ const method = className === null ? null : thisMethodCalled(inner);
1202
+ return method !== null && className !== null && declaresMember(file, className, method)
1203
+ ? method
1204
+ : null;
1205
+ };
1206
+ const scan = (inner) => {
1207
+ if (ts.isCallExpression(inner) &&
1208
+ ts.isPropertyAccessExpression(inner.expression) &&
1209
+ ownMethod(inner) === null) {
1210
+ const receiver = tailName(inner.expression.expression);
1211
+ // Issue #278 — see the twin in `collectCarryingMethods`: a bare
1212
+ // identifier receiver resolves lexically, a property chain cannot.
1213
+ const receiverNode = ts.isIdentifier(inner.expression.expression)
1214
+ ? inner.expression.expression
1215
+ : undefined;
1216
+ if (receiver !== null && readsAsPort(receiver, receiverNode)) {
1217
+ add(receiver, [receiver]);
1218
+ }
1219
+ else if (readsAsPort(inner.expression.name.text)) {
1220
+ // `this.deps.getTransactionalEmailSender()` — the alias is the
1221
+ // thing being called, not the object it hangs off.
1222
+ add(inner.expression.name.text, [inner.expression.name.text]);
1223
+ }
1224
+ }
1225
+ if (ts.isCallExpression(inner) &&
1226
+ ts.isIdentifier(inner.expression) &&
1227
+ inner.expression.text !== 'lazyPort' &&
1228
+ readsAsPort(inner.expression.text, inner.expression)) {
1229
+ add(inner.expression.text, [inner.expression.text]);
1230
+ }
1231
+ // `requireModuleEnabled('x')` throws the same error, on purpose.
1232
+ if (ts.isCallExpression(inner) &&
1233
+ ts.isIdentifier(inner.expression) &&
1234
+ inner.expression.text === 'requireModuleEnabled') {
1235
+ add('requireModuleEnabled', ['requireModuleEnabled']);
1236
+ }
1237
+ // D-88 — one hop backwards, through `this` and nothing else.
1238
+ if (ts.isCallExpression(inner) && className !== null) {
1239
+ const method = ownMethod(inner);
1240
+ const carried = method === null ? undefined : carryingMethods.get(methodKey(file, className, method));
1241
+ if (method !== null && carried !== undefined)
1242
+ add(method, carried);
1243
+ }
1244
+ inner.forEachChild(scan);
1245
+ };
1246
+ for (const root of guardedRoots)
1247
+ scan(root);
1248
+ if (ports.size > 0) {
1249
+ const line = sf.getLineAndCharacterOfPosition(at.getStart(sf)).line + 1;
1250
+ for (const [port, via] of ports) {
1251
+ const gates = [
1252
+ ...new Set([...via].flatMap((one) => [
1253
+ ...(analysis.portOwners.has(one) ? [one] : []),
1254
+ ...analysis.gatesAt(one, moduleId, file),
1255
+ ])),
1256
+ ].sort();
1257
+ found.push({
1258
+ file,
1259
+ line,
1260
+ moduleId,
1261
+ port,
1262
+ form,
1263
+ handled,
1264
+ gates,
1265
+ gateOwners: [
1266
+ ...new Set(gates
1267
+ .map((gate) => analysis.portOwners.get(gate))
1268
+ .filter((owner) => owner !== undefined)),
1269
+ ].sort(),
1270
+ ownerLocked: isOwnerLocked(gates),
1271
+ });
1272
+ }
1273
+ }
1274
+ };
1275
+ const visit = (node) => {
1276
+ if (ts.isTryStatement(node) && node.catchClause) {
1277
+ record([...node.tryBlock.statements], node, handles(node.catchClause, delegates), 'statement');
1278
+ }
1279
+ const rejection = rejectionHandlerOf(node);
1280
+ if (rejection !== null) {
1281
+ // Counted before the port test, and over every rejection handler in the
1282
+ // tree: it is the population floor, and a floor derived from the
1283
+ // findings would be satisfied by a recogniser that had stopped working.
1284
+ rejectionHandlerSites += 1;
1285
+ record([rejection.guarded], node, handlerHandles(rejection.handler, delegates), 'promise');
1286
+ }
1287
+ node.forEachChild(visit);
1288
+ };
1289
+ sf.forEachChild(visit);
1290
+ }
1291
+ found.sort((a, b) => (a.file === b.file ? a.line - b.line : a.file.localeCompare(b.file)));
1292
+ return { found, rejectionHandlerSites };
1293
+ }
1294
+ export function checkPortCatches(input, ledger) {
1295
+ const { found: all, rejectionHandlerSites } = scanSources(input);
1296
+ const unhandled = all.filter((entry) => !entry.handled);
1297
+ const ownerLocked = unhandled.filter((entry) => entry.ownerLocked);
1298
+ const open = unhandled.filter((entry) => !entry.ownerLocked);
1299
+ // An `OWNER LOCKED` site is deliberately **not** in the key set: a ledger
1300
+ // entry over one therefore reads stale and has to go, which is what makes the
1301
+ // classification re-red the site if the lock is ever withdrawn (D-63).
1302
+ const keys = new Set(open.map(keyOf));
1303
+ return {
1304
+ total: all.length,
1305
+ violations: open.filter((entry) => ledger[keyOf(entry)] === undefined),
1306
+ ledgered: open.filter((entry) => ledger[keyOf(entry)] !== undefined),
1307
+ ownerLocked,
1308
+ stale: Object.keys(ledger).filter((key) => !keys.has(key)),
1309
+ rejectionHandlerSites,
1310
+ };
1311
+ }
1312
+ /**
1313
+ * Every source this rule reads under `roots`.
1314
+ *
1315
+ * One spelling, both hosts: the repository host supplies its source roots and the
1316
+ * package host supplies one package's, and neither gets to differ about which
1317
+ * files are in the population.
1318
+ */
1319
+ export function collectPortCatchFiles(roots, out = []) {
1320
+ for (const root of roots) {
1321
+ if (!existsSync(root))
1322
+ continue;
1323
+ for (const name of readdirSync(root)) {
1324
+ const full = join(root, name);
1325
+ if (statSync(full).isDirectory()) {
1326
+ if (name === 'node_modules' || name === 'dist')
1327
+ continue;
1328
+ collectPortCatchFiles([full], out);
1329
+ }
1330
+ else if (name.endsWith('.ts') && !name.endsWith('.test.ts') && !name.endsWith('.d.ts')) {
1331
+ out.push(full);
1332
+ }
1333
+ }
1334
+ }
1335
+ return out;
1336
+ }
1337
+ /**
1338
+ * Every gated-port name the sources **write**, owned or not — the denominator.
1339
+ *
1340
+ * This is the one thing the rest of the analysis structurally cannot report. A
1341
+ * `lazyPort(ctx, '<name>')` whose `<name>` is in no owner map never becomes a
1342
+ * site, never becomes an alias and never becomes a finding: it disappears, and
1343
+ * the disappearance is indistinguishable from a package that resolves nothing.
1344
+ * So the count is taken here, before the owner map is consulted at all, and a
1345
+ * host prints `sources=owners:<attributed>/<written>` from it.
1346
+ *
1347
+ * Deliberately **not** gated on `portOwners`, and deliberately blind to whether
1348
+ * the name is really a port: what a host needs is *how many names this package
1349
+ * asked me to attribute*, and every `lazyPort` literal is one of those. A
1350
+ * non-literal name is not counted — it cannot be attributed by anyone, so
1351
+ * counting it would make the denominator permanently unreachable.
1352
+ */
1353
+ export function resolvedPortNames(sources) {
1354
+ const names = new Set();
1355
+ for (const [file, text] of sources) {
1356
+ if (!text.includes('lazyPort'))
1357
+ continue;
1358
+ const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
1359
+ const visit = (node) => {
1360
+ if (ts.isCallExpression(node) &&
1361
+ ts.isIdentifier(node.expression) &&
1362
+ node.expression.text === 'lazyPort') {
1363
+ const [, nameArgument] = node.arguments;
1364
+ if (nameArgument !== undefined && ts.isStringLiteralLike(nameArgument)) {
1365
+ names.add(nameArgument.text);
1366
+ }
1367
+ }
1368
+ node.forEachChild(visit);
1369
+ };
1370
+ sf.forEachChild(visit);
1371
+ }
1372
+ return names;
1373
+ }
1374
+ //# sourceMappingURL=port-catches.js.map