@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,1005 @@
1
+ /**
2
+ * The deployment divergence report — the derivation, the seam classification and
3
+ * the findings (`specs/107-override-report-and-ladder/contracts/divergence-report.md`).
4
+ *
5
+ * ## Why this is in the package, and not in either application
6
+ *
7
+ * The walk is the TypeScript compiler API over a deployment's own overlay tree.
8
+ * It was `backend/scripts/lib/divergence.ts` until
9
+ * `specs/110-instance-repository/` T138a, which no client can reach: a scaffolded
10
+ * instance has an `apps/<deployment>/` tree of its very own — that is what
11
+ * `instance-tree.md` §2.2 writes it for — and no way whatever to render a report
12
+ * over it. That is `docs-artefacts.ts`' story one artefact family over, and
13
+ * R3.5's answer is the same one: *"one generator, one derivation … never a
14
+ * second implementation"*, with the **population** as the parameter.
15
+ *
16
+ * It stayed out of `backend/src/**` for a reason that has not changed:
17
+ * `typescript` is a devDependency and `backend/src/**` is compiled into the
18
+ * production image (D-165), so an analysis imported from there would make the
19
+ * compiler a runtime dependency of every instance. The package has the compiler
20
+ * as a declared `dependency` and is a `devDependency` of the instance, which is
21
+ * where an analysis of a tree belongs.
22
+ *
23
+ * ## Everything in here is pure over its input, and that is what made the move a
24
+ * relocation
25
+ *
26
+ * {@link deriveDivergence} takes the deployment's sources, the route table, the
27
+ * owner map, the declaration and `ModuleContext`'s members. Not one of the five
28
+ * is walked for here. The two hosts differ only in how each is assembled —
29
+ * `backend/scripts/generate-divergence.ts` from this repository's module layout,
30
+ * platform sources and composition roots, `generate/divergence.ts` from the
31
+ * packages an instance installed — and `divergence-artefacts.ts` is where the
32
+ * assembly the two share lives.
33
+ *
34
+ * ## The one absolute rule
35
+ *
36
+ * Every subject is read as a **literal**: a string literal, a template literal
37
+ * with no substitution, an object-literal property name, or a file-local `const`
38
+ * bound to one of those. A subject the analysis cannot resolve is a **finding**,
39
+ * never a skip (FR-016, issue #113).
40
+ *
41
+ * That rule matters more here than anywhere else in the estate, and the reason
42
+ * is measurable: the overlay population in this repository is two modules and
43
+ * one decoration. A walk that silently skipped what it could not read would
44
+ * print `entries=1` over a deployment with fifty, and nothing else in the tree
45
+ * would notice.
46
+ *
47
+ * ## What it cannot see, stated here rather than discovered later
48
+ *
49
+ * - a seam call inside a helper the overlay module imports from **outside**
50
+ * its own directory. The population is the deployment's own overlay tree
51
+ * (§3.1), so a `ctx` handed to a function in `admin/` or in a core module is
52
+ * not read. It is also not a shape a deployment can reach: an overlay module
53
+ * may not name a file in another module's directory
54
+ * (`check:module-boundary`), and a helper of the deployment's own lives
55
+ * inside the overlay module;
56
+ * - a `ModuleContext` reached through a value the analysis cannot type — the
57
+ * receiver must be an identifier the file declares with the annotation
58
+ * `ModuleContext`, which is how every `registerModule` in the tree is
59
+ * written. A receiver it cannot place contributes no site, and refusal 3
60
+ * ("no seam call of any kind read") is what stops that from becoming a clean
61
+ * report over a tree full of them;
62
+ * - a queue name a worker takes from a value built at run time. `ctx.worker`
63
+ * takes a **constructed** `Worker`, so the name is read from the
64
+ * `new Worker('<literal>', …)` the call wraps, directly or through a
65
+ * file-local binding. Anything else is `computed-subject`.
66
+ */
67
+ import { realpathSync } from 'node:fs';
68
+ import ts from 'typescript';
69
+ /**
70
+ * The rung table, keyed by `ModuleContext`'s own member spelling.
71
+ *
72
+ * **The population is not this map** — it is the members the platform's source
73
+ * declares, read by {@link moduleContextSeams}. A member this map does not name
74
+ * is the `unclassified-seam` finding, which is what stops the eleventh seam
75
+ * arriving unclassified the way `rootPlugin` did.
76
+ *
77
+ * The three `rung: null` entries are the ladder's own table's three `—` rows
78
+ * (`escalation-ladder.md` §2, `data-model.md` §2.3): a registration and a worker
79
+ * are a module contributing its own surface to the container and to the queue,
80
+ * and the ladder ranks ways of changing what **core** does. They are recorded
81
+ * because a deployment adding either is a way its tree differs from core; they
82
+ * carry no rung because there is no cost to core in them.
83
+ */
84
+ export const SEAM_CLASSIFICATION = {
85
+ 'di.register': { verdict: 'divergence', kind: 'registration', rung: null },
86
+ 'di.providePort': { verdict: 'divergence', kind: 'port-provided', rung: 3 },
87
+ 'di.decorate': { verdict: 'divergence', kind: 'decoration', rung: 4 },
88
+ subscribe: { verdict: 'divergence', kind: 'subscription', rung: 1 },
89
+ interceptors: { verdict: 'divergence', kind: 'interceptor', rung: 2 },
90
+ rootPlugin: { verdict: 'divergence', kind: 'root-plugin', rung: 4 },
91
+ worker: { verdict: 'divergence', kind: 'worker', rung: null },
92
+ routes: {
93
+ verdict: 'own-surface',
94
+ why: 'a route an overlay module registers is its own route, gated on its own module — not a change to what core serves',
95
+ },
96
+ ungatedRoutes: {
97
+ verdict: 'own-surface',
98
+ why: "the module's own route, outside the module's own gate; the reason string is required at the call and is read there, and it removes no other module's gate",
99
+ },
100
+ onBoot: {
101
+ verdict: 'own-surface',
102
+ why: "the module's own boot hook, run inside its own system scope; it contributes nothing another module resolves",
103
+ },
104
+ module: { verdict: 'not-a-seam', why: 'the module identity the context was built for' },
105
+ asClass: { verdict: 'not-a-seam', why: 'a registration builder — it constructs a registration, it does not register one' },
106
+ asFunction: { verdict: 'not-a-seam', why: 'a registration builder' },
107
+ asValue: { verdict: 'not-a-seam', why: 'a registration builder' },
108
+ cradle: {
109
+ verdict: 'not-a-seam',
110
+ why: 'the deferred resolution surface; a cross-module read through it is recorded as `port-consumed` from the `lazyPort` call that spells the name',
111
+ },
112
+ log: { verdict: 'not-a-seam', why: "the platform's logger, bound to this module" },
113
+ };
114
+ /**
115
+ * The `ModuleContext` members the platform's source declares — the population
116
+ * the table above is held against.
117
+ *
118
+ * Read from source text so a fixture enters at the top of the analysis, and read
119
+ * as the interface's own members rather than as a list: `di`'s three methods are
120
+ * returned dotted (`di.register`), because that is how {@link calleeTailOf}
121
+ * spells a call on them and a check whose two halves spell one thing differently
122
+ * cannot reconcile them.
123
+ */
124
+ export function moduleContextSeams(source) {
125
+ const sf = ts.createSourceFile('module-context.ts', source, ts.ScriptTarget.Latest, true);
126
+ const members = [];
127
+ const nameOf = (member) => member.name !== undefined && (ts.isIdentifier(member.name) || ts.isStringLiteral(member.name))
128
+ ? member.name.text
129
+ : null;
130
+ const visit = (node) => {
131
+ if (ts.isInterfaceDeclaration(node) && node.name.text === 'ModuleContext') {
132
+ for (const member of node.members) {
133
+ const name = nameOf(member);
134
+ if (name === null)
135
+ continue;
136
+ // `di` is a property whose type is an inline object literal of three
137
+ // **methods**, and its members are the seams — so it contributes them
138
+ // and never itself.
139
+ //
140
+ // The expansion is keyed on the members being callable, not on the type
141
+ // being a literal: `module` is also an inline object literal, of two
142
+ // `string` properties, and expanding it would report `ctx.module.id` and
143
+ // `ctx.module.version` as seams the ladder does not classify. A property
144
+ // is a fact the context carries; a method is something a module *does*.
145
+ const type = ts.isPropertySignature(member) ? member.type : undefined;
146
+ if (type !== undefined && ts.isTypeLiteralNode(type)) {
147
+ const callable = type.members.filter((inner) => ts.isMethodSignature(inner) && nameOf(inner) !== null);
148
+ if (callable.length > 0) {
149
+ for (const inner of callable)
150
+ members.push(`${name}.${nameOf(inner) ?? ''}`);
151
+ continue;
152
+ }
153
+ }
154
+ members.push(name);
155
+ }
156
+ }
157
+ node.forEachChild(visit);
158
+ };
159
+ sf.forEachChild(visit);
160
+ return members;
161
+ }
162
+ /** `ctx.di.register` → `di.register`; `ctx.subscribe` → `subscribe`. */
163
+ export function calleeTailOf(node) {
164
+ const expression = node.expression;
165
+ if (!ts.isPropertyAccessExpression(expression))
166
+ return '';
167
+ const inner = expression.expression;
168
+ const prefix = ts.isPropertyAccessExpression(inner) ? `${inner.name.text}.` : '';
169
+ return `${prefix}${expression.name.text}`;
170
+ }
171
+ /** The identifier a seam call is written on, or `null`. */
172
+ function receiverOf(node) {
173
+ const expression = node.expression;
174
+ if (!ts.isPropertyAccessExpression(expression))
175
+ return null;
176
+ const inner = expression.expression;
177
+ if (ts.isIdentifier(inner))
178
+ return inner;
179
+ if (ts.isPropertyAccessExpression(inner) && ts.isIdentifier(inner.expression)) {
180
+ return inner.expression;
181
+ }
182
+ return null;
183
+ }
184
+ /**
185
+ * The identifiers this file declares as a `ModuleContext`.
186
+ *
187
+ * Two ways in, and the second is not a relaxation of the first:
188
+ *
189
+ * 1. **An annotation** — a parameter or a `const` whose type node is the
190
+ * identifier `ModuleContext`. That is how every overlay module in this
191
+ * repository is written and how a test writes one.
192
+ * 2. **The first parameter of an exported `registerModule`**, whatever it is
193
+ * named and whether or not it is annotated.
194
+ *
195
+ * ## Why the second exists, and why it is a contract rather than a heuristic
196
+ *
197
+ * `walkAnalysableSources` admits `.js`, and a `.js` file cannot carry a type
198
+ * annotation. So a client who writes their overlay module in JavaScript — which
199
+ * the loader accepts, `UNIT_EXTENSIONS` being `['.js', '.ts']`, and which is
200
+ * what a compiled deployment ships — got **a clean report over a tree full of
201
+ * decorations** (`specs/124-instance-customisation-gap/` FR-009). That is
202
+ * precisely the state refusal 3 exists to refuse, and nothing in an instance
203
+ * called it until FR-008.
204
+ *
205
+ * The `[NEEDS CLARIFICATION]` FR-009 carried was between *any single-parameter
206
+ * exported `registerModule`* and *a parameter named `ctx`*, to be settled by
207
+ * measuring the overlay corpus. Measured on 2026-09-13: the corpus is
208
+ * the two `backend.ts` files under `backend/src/apps/{acceptance,example}`, plus
209
+ * this package's own fixtures, and **every one of them writes
210
+ * `registerModule(ctx: ModuleContext)`** — so the measurement does not
211
+ * discriminate, and the answer comes from the loader instead. It is not a
212
+ * naming convention that makes the first parameter a context: it is
213
+ * `overlayModuleEntriesUnder`, which refuses a `backend.js`/`backend.ts` that
214
+ * exports no `registerModule` function and calls the one it finds with a
215
+ * `ModuleContext` and nothing else. The parameter's *name* is the client's
216
+ * choice and nothing enforces it; the parameter's *identity* is the platform's.
217
+ * Reading the name would be the heuristic.
218
+ *
219
+ * The **first** parameter rather than "the only one": the loader passes the
220
+ * context as argument 0, so a second parameter is a thing the platform never
221
+ * fills and says nothing about the first.
222
+ *
223
+ * A receiver reached any other way — a local the context was handed to, a field
224
+ * on a class — is still unplaceable, and refusal 3 (`no-seam-call-read`) is what
225
+ * keeps that from reading as "this deployment uses no seam". FR-008 is the floor
226
+ * under this widening, not an alternative to it.
227
+ */
228
+ function contextBindings(sf) {
229
+ const names = new Set();
230
+ const declaresContext = (type) => type !== undefined &&
231
+ ts.isTypeReferenceNode(type) &&
232
+ ts.isIdentifier(type.typeName) &&
233
+ type.typeName.text === 'ModuleContext';
234
+ /** `export function registerModule(…)` / `export const registerModule = (…) =>`. */
235
+ const registerModuleParameter = (node) => {
236
+ const exported = (modifiers) => modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true;
237
+ if (ts.isFunctionDeclaration(node) &&
238
+ node.name?.text === 'registerModule' &&
239
+ exported(node.modifiers)) {
240
+ return node.parameters[0] ?? null;
241
+ }
242
+ if (ts.isVariableStatement(node) &&
243
+ exported(node.modifiers)) {
244
+ for (const declaration of node.declarationList.declarations) {
245
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== 'registerModule') {
246
+ continue;
247
+ }
248
+ const initializer = declaration.initializer;
249
+ if (initializer !== undefined &&
250
+ (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))) {
251
+ return initializer.parameters[0] ?? null;
252
+ }
253
+ }
254
+ }
255
+ return null;
256
+ };
257
+ const visit = (node) => {
258
+ if (ts.isParameter(node) && ts.isIdentifier(node.name) && declaresContext(node.type)) {
259
+ names.add(node.name.text);
260
+ }
261
+ if (ts.isVariableDeclaration(node) &&
262
+ ts.isIdentifier(node.name) &&
263
+ declaresContext(node.type)) {
264
+ names.add(node.name.text);
265
+ }
266
+ const parameter = registerModuleParameter(node);
267
+ if (parameter !== null && ts.isIdentifier(parameter.name)) {
268
+ names.add(parameter.name.text);
269
+ }
270
+ node.forEachChild(visit);
271
+ };
272
+ sf.forEachChild(visit);
273
+ return names;
274
+ }
275
+ /** File-local `const x = '<literal>'` bindings, for a subject written once above. */
276
+ function literalBindings(sf) {
277
+ const bindings = new Map();
278
+ const visit = (node) => {
279
+ if (ts.isVariableDeclaration(node) &&
280
+ ts.isIdentifier(node.name) &&
281
+ node.initializer !== undefined) {
282
+ const literal = plainLiteral(node.initializer);
283
+ if (literal !== null)
284
+ bindings.set(node.name.text, literal);
285
+ }
286
+ node.forEachChild(visit);
287
+ };
288
+ sf.forEachChild(visit);
289
+ return bindings;
290
+ }
291
+ /** A string literal or a template with no substitution; nothing else. */
292
+ function plainLiteral(node) {
293
+ if (ts.isStringLiteral(node))
294
+ return node.text;
295
+ if (ts.isNoSubstitutionTemplateLiteral(node))
296
+ return node.text;
297
+ return null;
298
+ }
299
+ /** A literal, or a file-local `const` that is one. Never a computation. */
300
+ function resolveSubject(node, bindings) {
301
+ if (node === undefined)
302
+ return null;
303
+ const literal = plainLiteral(node);
304
+ if (literal !== null)
305
+ return literal;
306
+ if (ts.isIdentifier(node))
307
+ return bindings.get(node.text) ?? null;
308
+ return null;
309
+ }
310
+ function propertyOf(object, name) {
311
+ for (const property of object.properties) {
312
+ if (ts.isPropertyAssignment(property) &&
313
+ (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) &&
314
+ property.name.text === name) {
315
+ return property.initializer;
316
+ }
317
+ }
318
+ return undefined;
319
+ }
320
+ /** The queue name a `new Worker('<name>', …)` names, directly or one hop back. */
321
+ function queueNameOf(argument, bindings, workers) {
322
+ if (argument === undefined)
323
+ return null;
324
+ if (ts.isNewExpression(argument)) {
325
+ return resolveSubject(argument.arguments?.[0], bindings);
326
+ }
327
+ if (ts.isIdentifier(argument))
328
+ return workers.get(argument.text) ?? null;
329
+ return null;
330
+ }
331
+ /** File-local `const w = new Worker('<name>', …)` bindings. */
332
+ function workerBindings(sf, bindings) {
333
+ const workers = new Map();
334
+ const visit = (node) => {
335
+ if (ts.isVariableDeclaration(node) &&
336
+ ts.isIdentifier(node.name) &&
337
+ node.initializer !== undefined &&
338
+ ts.isNewExpression(node.initializer)) {
339
+ const name = resolveSubject(node.initializer.arguments?.[0], bindings);
340
+ if (name !== null)
341
+ workers.set(node.name.text, name);
342
+ }
343
+ node.forEachChild(visit);
344
+ };
345
+ sf.forEachChild(visit);
346
+ return workers;
347
+ }
348
+ const CALL_TEXT_LIMIT = 120;
349
+ function callText(node, sf) {
350
+ const text = node.getText(sf).replace(/\s+/g, ' ');
351
+ return text.length > CALL_TEXT_LIMIT ? `${text.slice(0, CALL_TEXT_LIMIT)}…` : text;
352
+ }
353
+ /**
354
+ * Every seam call in one deployment's overlay tree.
355
+ *
356
+ * The fixture enters here as source text, which is the top of this analysis:
357
+ * every classification below — the receiver, the seam spelling, the literal
358
+ * resolution — runs over it.
359
+ */
360
+ export function deriveSeamSites(sources) {
361
+ const sites = [];
362
+ for (const source of sources) {
363
+ const sf = ts.createSourceFile(source.file, source.text, ts.ScriptTarget.Latest, true);
364
+ const contexts = contextBindings(sf);
365
+ const bindings = literalBindings(sf);
366
+ const workers = workerBindings(sf, bindings);
367
+ const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
368
+ const push = (node, seam, rest) => {
369
+ sites.push({
370
+ moduleId: source.moduleId,
371
+ file: source.file,
372
+ line: lineOf(node),
373
+ seam,
374
+ text: callText(node, sf),
375
+ ...rest,
376
+ });
377
+ };
378
+ const visit = (node) => {
379
+ if (ts.isCallExpression(node)) {
380
+ // `lazyPort<T>(ctx, 'name')` — a helper over the cradle rather than a
381
+ // `ModuleContext` member, and the only kind that records a module
382
+ // *reaching* rather than *contributing*.
383
+ if (ts.isIdentifier(node.expression) && node.expression.text === 'lazyPort') {
384
+ push(node, 'lazyPort', { subject: resolveSubject(node.arguments[1], bindings) });
385
+ }
386
+ const receiver = receiverOf(node);
387
+ if (receiver !== null && contexts.has(receiver.text)) {
388
+ const seam = calleeTailOf(node);
389
+ const [first, second] = node.arguments;
390
+ switch (seam) {
391
+ case 'di.register': {
392
+ if (first !== undefined && ts.isObjectLiteralExpression(first)) {
393
+ for (const property of first.properties) {
394
+ const name = property.name !== undefined &&
395
+ (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))
396
+ ? property.name.text
397
+ : null;
398
+ push(property, seam, { subject: name });
399
+ }
400
+ // An empty object literal is a call that registers nothing; it
401
+ // is still a site, so the run's `sites` count matches the calls
402
+ // it examined.
403
+ if (first.properties.length === 0)
404
+ push(node, seam, { subject: null });
405
+ }
406
+ else {
407
+ push(node, seam, { subject: null });
408
+ }
409
+ break;
410
+ }
411
+ case 'di.providePort':
412
+ case 'di.decorate':
413
+ case 'subscribe': {
414
+ push(node, seam, { subject: resolveSubject(first, bindings) });
415
+ break;
416
+ }
417
+ case 'rootPlugin': {
418
+ const reason = resolveSubject(first, bindings);
419
+ push(node, seam, {
420
+ subject: reason,
421
+ ...(reason === null ? {} : { declaredReason: reason }),
422
+ });
423
+ break;
424
+ }
425
+ case 'worker': {
426
+ push(node, seam, { subject: queueNameOf(first, bindings, workers) });
427
+ break;
428
+ }
429
+ case 'interceptors': {
430
+ if (first !== undefined && ts.isArrayLiteralExpression(first)) {
431
+ for (const element of first.elements) {
432
+ if (!ts.isObjectLiteralExpression(element)) {
433
+ push(element, seam, { subject: null });
434
+ continue;
435
+ }
436
+ const target = resolveSubject(propertyOf(element, 'target'), bindings);
437
+ const phaseText = resolveSubject(propertyOf(element, 'phase'), bindings);
438
+ const idText = resolveSubject(propertyOf(element, 'id'), bindings);
439
+ const orderNode = propertyOf(element, 'order');
440
+ const order = orderNode !== undefined && ts.isNumericLiteral(orderNode)
441
+ ? Number(orderNode.text)
442
+ : 0;
443
+ // `phase` and `id` are required by the registration type, so a
444
+ // missing one is a `tsc` error rather than this check's
445
+ // business; an unreadable one makes the whole entry a
446
+ // `computed-subject`, because its key carries the phase.
447
+ if (target === null || phaseText === null || idText === null) {
448
+ push(element, seam, { subject: null });
449
+ continue;
450
+ }
451
+ push(element, seam, {
452
+ subject: target,
453
+ interceptor: {
454
+ phase: phaseText === 'post' ? 'post' : 'pre',
455
+ order,
456
+ id: idText,
457
+ },
458
+ });
459
+ }
460
+ if (first.elements.length === 0)
461
+ push(node, seam, { subject: null });
462
+ }
463
+ else {
464
+ push(node, seam, { subject: null });
465
+ }
466
+ break;
467
+ }
468
+ default: {
469
+ // `routes`, `ungatedRoutes`, `onBoot`, the builders and `cradle`
470
+ // are classified `own-surface` / `not-a-seam` above; they are
471
+ // deliberately not sites, because a site is a divergence
472
+ // candidate and those are not.
473
+ if (second !== undefined)
474
+ break;
475
+ break;
476
+ }
477
+ }
478
+ }
479
+ }
480
+ node.forEachChild(visit);
481
+ };
482
+ sf.forEachChild(visit);
483
+ }
484
+ return sites;
485
+ }
486
+ /**
487
+ * "Has this population already got this file?" — the guard that keeps a file two
488
+ * roots both reach from entering the population twice.
489
+ *
490
+ * The environment this check builds is a **union** of several walks, and two of
491
+ * them legitimately overlap: a module walk root can sit *inside* the platform's
492
+ * source root, in which case that module's files are reached once as the
493
+ * module's and once as the platform's. `files` is then the size of a multiset
494
+ * rather than of a population, and it is the one instrument this estate has for
495
+ * spotting a walk that has gone blind — a number wrong for a reason nobody knows
496
+ * is worse than a number that is missing.
497
+ *
498
+ * Three properties, each of them the design and not a detail.
499
+ *
500
+ * - **By real path, never by the spelling.** Two roots reaching one file reach
501
+ * it under two path strings whenever either root is a symlink, and a string
502
+ * comparison would let both through — which is the case a `git worktree` and
503
+ * a linked `node_modules` both produce.
504
+ * - **First claim wins**, so the *narrower* walk's attribution survives: the
505
+ * passes run most-specific first, and a file inside a module is that module's
506
+ * however wide a root also covers it. `routeIdentities` already resolves the
507
+ * same contest the same way for a route it sees twice.
508
+ * - **It is not keyed on any package, root or name.** The next pair of roots
509
+ * that overlaps for some other reason is handled by this same guard, because
510
+ * what it knows about is a file it has already been given.
511
+ *
512
+ * A path `realPathOf` cannot resolve — a file deleted between the walk and the
513
+ * read — falls back to the spelling rather than throwing: this guard's job is to
514
+ * collapse a duplicate, and refusing a run is the caller's decision to take.
515
+ */
516
+ export function claimFileOnce(realPathOf = (file) => realpathSync.native(file)) {
517
+ const claimed = new Set();
518
+ return (file) => {
519
+ // The fallback lives here rather than inside the default resolver so that it
520
+ // holds for an injected one too: the guarantee is the guard's, and a resolver
521
+ // that throws must not take out a walk that has already read the file.
522
+ let key;
523
+ try {
524
+ key = realPathOf(file);
525
+ }
526
+ catch {
527
+ key = file;
528
+ }
529
+ if (claimed.has(key))
530
+ return false;
531
+ claimed.add(key);
532
+ return true;
533
+ };
534
+ }
535
+ /**
536
+ * Every `"<METHOD> <path>"` a route registration in the composition serves, and
537
+ * the module that owns it.
538
+ *
539
+ * `ctx.interceptors`' target is that identity, and the registry accepts one no
540
+ * route matches **silently** — the interceptor simply never runs. So the report
541
+ * reconciles the two, which is the `unmatched-interceptor-target` finding, and
542
+ * the same sweep answers the entry's `owner`: an intercepted endpoint's owner is
543
+ * the module that registered the route, not the module that registered a
544
+ * container name of the same spelling.
545
+ *
546
+ * Deliberately not `check:action-route-permissions`' `findAdminRoutes`: that one
547
+ * filters to `/api/v1/admin/**`, and an interceptor may target any endpoint any
548
+ * module owns.
549
+ */
550
+ export function routeIdentities(sources) {
551
+ const identities = new Map();
552
+ const methods = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'all']);
553
+ const claim = (identity, moduleId) => {
554
+ // First registration wins, and a named owner beats an unnamed one: a route
555
+ // registered inside a module is that module's, whatever a later file in the
556
+ // application's own tree spells.
557
+ const known = identities.get(identity);
558
+ if (known === undefined || (known === null && moduleId !== null)) {
559
+ identities.set(identity, moduleId);
560
+ }
561
+ };
562
+ for (const source of sources) {
563
+ if (!source.text.includes('/api/'))
564
+ continue;
565
+ const sf = ts.createSourceFile(source.file, source.text, ts.ScriptTarget.Latest, true);
566
+ const bindings = literalBindings(sf);
567
+ const visit = (node) => {
568
+ if (ts.isCallExpression(node) &&
569
+ ts.isPropertyAccessExpression(node.expression) &&
570
+ methods.has(node.expression.name.text)) {
571
+ const path = resolveSubject(node.arguments[0], bindings);
572
+ if (path !== null && path.startsWith('/')) {
573
+ const method = node.expression.name.text.toUpperCase();
574
+ const verbs = method === 'ALL'
575
+ ? ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']
576
+ : [method];
577
+ for (const verb of verbs)
578
+ claim(`${verb} ${path}`, source.moduleId);
579
+ }
580
+ }
581
+ node.forEachChild(visit);
582
+ };
583
+ sf.forEachChild(visit);
584
+ }
585
+ return identities;
586
+ }
587
+ /** What each finding means and what to do about it. */
588
+ export const DIVERGENCE_REMEDIES = {
589
+ 'computed-subject': 'A seam subject — a decoration name, an interceptor target, an event, a port, a queue or a\n' +
590
+ 'registration key — is not a literal, so the report cannot say what this deployment changed.\n' +
591
+ 'Reading it as "no divergence" is the direction that agrees with the defect (issue #113), so it\n' +
592
+ 'is a finding. Write the subject as a string literal, or as a `const` in the same file.',
593
+ 'unowned-subject': 'The name this deployment decorates or consumes is registered by no module in the composition.\n' +
594
+ 'Composition throws for it at boot; this is the same refusal in a better place — the merge\n' +
595
+ 'request that wrote it. Check the spelling against the owner module’s `ctx.di.register` /\n' +
596
+ '`ctx.di.providePort` call, or the port’s doc block, which names its container name.',
597
+ 'unmatched-interceptor-target': 'An interceptor names an endpoint identity no route registration matches. The registry accepts\n' +
598
+ 'it silently — the interceptor simply never runs — so nothing else in the platform would tell\n' +
599
+ 'you. The identity is `"<METHOD> <route pattern>"`, the pattern exactly as the owner registers\n' +
600
+ 'it (`/api/v1/orders/:id`, not `/api/v1/orders/123`).',
601
+ 'undeclared-divergence': 'This deployment diverges from core here and its declaration says nothing about it. Add the\n' +
602
+ 'entry’s key to `reasons` in `backend/src/apps/<deployment>/divergence.ts`, with a sentence\n' +
603
+ 'naming what core does and what this deployment does instead — "we do not need it" is not a\n' +
604
+ 'reason (D-101).',
605
+ 'stale-reason': 'A reason describes a divergence the tree no longer holds. That is how a deployment silently\n' +
606
+ 'reacquires a hazard it once declared (D-101’s own reasoning, applied to a wider subject).\n' +
607
+ 'Delete the key, or restore the divergence it describes.',
608
+ 'unclassified-seam': '`ModuleContext` declares a member the escalation ladder does not classify. Every seam has\n' +
609
+ 'exactly one rung, one `own-surface` reason or one `not-a-seam` reason\n' +
610
+ '(`contracts/escalation-ladder.md` §3.1), and a member with none would be a way to diverge that\n' +
611
+ 'the report cannot see. Classify it in `SEAM_CLASSIFICATION` and give the ladder its rung.',
612
+ 'stale-decoration-order': 'A `decorationOrder` entry names a registration that no two of this deployment’s overlay\n' +
613
+ 'modules decorate. It changes nothing today and will describe the wrong thing the next time a\n' +
614
+ 'decoration is added — which is how a deployment silently reacquires an ambiguity it resolved.',
615
+ 'foreign-order-member': 'A `decorationOrder` entry names a module that is not one of this deployment’s overlay modules.\n' +
616
+ 'Only an overlay module may decorate a name it does not own (D-156.4), so there is no ordering\n' +
617
+ 'this entry can resolve — the composer would ignore it.',
618
+ 'incomplete-order': 'A `decorationOrder` entry names fewer modules than decorate that registration. A partial order\n' +
619
+ 'refuses the composition at boot (`AmbiguousDecorationError`) rather than ordering it, so the\n' +
620
+ 'entry reads as a decision and behaves as a comment.',
621
+ };
622
+ /**
623
+ * What the report does not cover, with a reason each (FR-018).
624
+ *
625
+ * `hostNotRecorded` is how a **host** states its own narrowing, and it is the
626
+ * whole answer to the question T138a had to settle: this repository derives the
627
+ * owner map from module *sources* and from a bridging table its composition
628
+ * roots carry, and a client's instance has neither. The kinds are identical —
629
+ * all nine are derived from the deployment's own overlay tree, which is the one
630
+ * population the two hosts share exactly — but the **attribution** differs, and
631
+ * a report that was silently narrower would be worse than no report at all,
632
+ * because a deployment's divergence is exactly the thing a client is asked to
633
+ * trust. So the sentences go in the artefact, in the field FR-018 already has
634
+ * for making silence readable, rather than into a release note nobody reads
635
+ * beside the report. This repository passes none and its six committed
636
+ * artefacts are byte-identical.
637
+ */
638
+ export function divergenceBoundary(classification = SEAM_CLASSIFICATION, hostNotRecorded = []) {
639
+ const recorded = [
640
+ ...new Set(Object.values(classification)
641
+ .filter((entry) => entry.verdict === 'divergence')
642
+ .map((entry) => entry.kind)),
643
+ ];
644
+ // `port-consumed` is a kind with no `ModuleContext` member — it comes from
645
+ // `lazyPort` over the cradle — and `omission` comes from the declaration and
646
+ // from no seam at all. Both are recorded, so both are named here.
647
+ for (const kind of ['port-consumed', 'omission']) {
648
+ if (!recorded.includes(kind))
649
+ recorded.push(kind);
650
+ }
651
+ recorded.sort();
652
+ const notRecorded = Object.entries(classification)
653
+ .filter((entry) => entry[1].verdict === 'own-surface')
654
+ .map(([seam, entry]) => ({ seam: `ctx.${seam}`, why: entry.why }))
655
+ .sort((a, b) => a.seam.localeCompare(b.seam));
656
+ notRecorded.push({
657
+ seam: 'manifest.ts declarations (permissions, palette actions, i18n bundles, CLI commands)',
658
+ why: 'each is a module declaring its own surface, and each is already swept by the instrument that owns it — the permission inventory, the bundle-shape test, the action-route check',
659
+ });
660
+ // Appended rather than merged into the sort above: the seam entries are the
661
+ // classification's and are ordered by it, and a host's own narrowing is a
662
+ // different claim — it is about this *run*, not about `ModuleContext`.
663
+ notRecorded.push(...hostNotRecorded);
664
+ return {
665
+ recorded,
666
+ notRecorded,
667
+ runtimeOnly: [
668
+ {
669
+ fact: "an installed extension package's interceptors and subscriptions",
670
+ why: 'a package is discovered at run time and ships compiled output; what it registers is a fact about a process, not about this tree. An installed package cannot decorate at all (D-156.3), which is what keeps the highest-value seam inside this artefact',
671
+ },
672
+ {
673
+ fact: "the operator's activation choices",
674
+ why: 'presence is the conjunction of two orthogonal axes (Principle XVII), and activation is a Setting in the store. Whether a divergence recorded here is live is the running instance’s answer, and a build-time artefact gating on it would be the shape D-67/D-68 refuse',
675
+ },
676
+ {
677
+ fact: 'whether each decoration applied, and at what depth',
678
+ why: 'depth is a fact about a composition rather than about a tree: which wrap went innermost is what `decorationOrder` and the composer’s emission order decide together. `detail.depth` is `null` here rather than guessed',
679
+ },
680
+ ],
681
+ };
682
+ }
683
+ /** `<kind>:<module>:<subject>`, with an interceptor's `#<phase>`. */
684
+ export function divergenceKeyOf(entry) {
685
+ const phase = entry.detail.kind === 'interceptor' ? `#${entry.detail.phase}` : '';
686
+ return `${entry.kind}:${entry.module}:${entry.subject}${phase}`;
687
+ }
688
+ const SEAM_TO_KIND = (classification, seam) => {
689
+ if (seam === 'lazyPort')
690
+ return 'port-consumed';
691
+ const entry = classification[seam];
692
+ return entry !== undefined && entry.verdict === 'divergence' ? entry.kind : null;
693
+ };
694
+ const RUNG_OF = (classification, seam) => {
695
+ if (seam === 'lazyPort')
696
+ return 3;
697
+ const entry = classification[seam];
698
+ return entry !== undefined && entry.verdict === 'divergence' ? entry.rung : null;
699
+ };
700
+ /**
701
+ * The whole derivation: one report, and every finding it raised producing it.
702
+ *
703
+ * Pure over its input, so a fixture deployment enters at the top of the analysis
704
+ * (issue #130) and the acceptance instrument (SC-008) needs no tree on disk.
705
+ */
706
+ export function deriveDivergence(input) {
707
+ const classification = input.classification ?? SEAM_CLASSIFICATION;
708
+ const findings = [];
709
+ const overlayModules = [...input.overlayModules].sort();
710
+ const overlaySet = new Set(overlayModules);
711
+ const note = (kind, where, detail) => {
712
+ findings.push({ kind, deployment: input.deployment, where, detail });
713
+ };
714
+ // FR-031 — every `ModuleContext` member the platform declares has exactly one
715
+ // classification. A member with none is a way to diverge the report cannot
716
+ // see, which is what let `rootPlugin` arrive unclassified.
717
+ for (const seam of input.seams) {
718
+ if (classification[seam] === undefined) {
719
+ note('unclassified-seam', 'packages/platform/src/kernel/module-context.ts', `\`ctx.${seam}\` is a ModuleContext member the escalation ladder does not classify`);
720
+ }
721
+ }
722
+ const sites = deriveSeamSites(input.sources);
723
+ const entries = [];
724
+ for (const site of sites) {
725
+ const kind = SEAM_TO_KIND(classification, site.seam);
726
+ // A site whose seam is classified `own-surface` or `not-a-seam` produces no
727
+ // entry by construction — `deriveSeamSites` pushes none for those — so a
728
+ // `null` here is an unclassified member, already reported above.
729
+ if (kind === null)
730
+ continue;
731
+ if (site.subject === null) {
732
+ note('computed-subject', `${site.file}:${site.line}`, `\`ctx.${site.seam}\` names a subject the analysis cannot resolve to a literal: ${site.text}`);
733
+ continue;
734
+ }
735
+ const detail = detailFor(kind, site, input.routes);
736
+ if (detail.kind === 'interceptor' && !detail.targetMatched) {
737
+ note('unmatched-interceptor-target', `${site.file}:${site.line}`, `interceptor '${detail.id}' targets '${site.subject}', which no route registration matches`);
738
+ }
739
+ // Who owns the subject is a per-kind question, because the subjects are not
740
+ // one namespace: a decoration and a port name a **container registration**,
741
+ // an interceptor names an **endpoint**, and a subscription names an
742
+ // **event** — for which the platform publishes no catalogue at all
743
+ // (`escalation-ladder.md` rung 1's stated gap), so its owner is honestly
744
+ // unknown rather than absent.
745
+ let owner = null;
746
+ if (kind === 'decoration' || kind === 'port-consumed') {
747
+ const registered = input.owners.get(site.subject);
748
+ if (registered === undefined && !input.rootSupplied.has(site.subject)) {
749
+ note('unowned-subject', `${site.file}:${site.line}`, `'${site.subject}' is registered by no module in the composition`);
750
+ continue;
751
+ }
752
+ owner = registered ?? null;
753
+ }
754
+ else if (kind === 'interceptor') {
755
+ owner = input.routes.get(site.subject) ?? null;
756
+ }
757
+ else if (kind === 'subscription') {
758
+ owner = null;
759
+ }
760
+ else {
761
+ owner = input.owners.get(site.subject) ?? null;
762
+ }
763
+ const key = divergenceKeyOf({ kind, module: site.moduleId, subject: site.subject, detail });
764
+ entries.push({
765
+ key,
766
+ kind,
767
+ module: site.moduleId,
768
+ subject: site.subject,
769
+ owner,
770
+ rung: RUNG_OF(classification, site.seam),
771
+ detail,
772
+ reason: input.declaration.reasons[key] ?? '',
773
+ });
774
+ }
775
+ // The omissions, from the declaration and from no seam at all. They are the
776
+ // one kind the declaration supplies the population for, and it is D-101's
777
+ // ruling that it does: the boot refuses an omission that is not declared, so
778
+ // the declaration and the composed set are already two-way against each other
779
+ // in the place that can see the composed set.
780
+ for (const omission of input.declaration.omittedModules) {
781
+ const detail = { kind: 'omission' };
782
+ entries.push({
783
+ key: divergenceKeyOf({
784
+ kind: 'omission',
785
+ module: 'core',
786
+ subject: omission.moduleId,
787
+ detail,
788
+ }),
789
+ kind: 'omission',
790
+ module: 'core',
791
+ subject: omission.moduleId,
792
+ owner: omission.moduleId,
793
+ rung: null,
794
+ detail,
795
+ reason: omission.reason,
796
+ });
797
+ }
798
+ entries.sort((a, b) => a.key.localeCompare(b.key));
799
+ // Where the declaration was read, as the **reader's** tree spells it.
800
+ //
801
+ // A parameter since `specs/110-instance-repository/` T138a, and it had to
802
+ // become one: the default below is this repository's own layout, and a client's
803
+ // instance holds its deployment at `apps/<deployment>/` with no `backend/src`
804
+ // above it. A finding naming a directory the reader does not have is SC-001's
805
+ // second rule broken — *"no repository-relative paths; the reader's tree is not
806
+ // this one"* — and it is the finding's whole job to send someone to a file.
807
+ // Measured on a real scaffolded instance before the parameter existed: every
808
+ // `undeclared-divergence` it raised named `backend/src/apps/instance/divergence.ts`.
809
+ const declarationPath = input.declarationPath ??
810
+ (input.deployment === 'core'
811
+ ? '(no deployment)'
812
+ : `backend/src/apps/${input.deployment}/divergence.ts`);
813
+ // FR-004, both directions. An omission carries its reason inline, so it is
814
+ // never `undeclared-divergence`; every other kind reads the `reasons` map.
815
+ const derivedKeys = new Set(entries.map((entry) => entry.key));
816
+ for (const entry of entries) {
817
+ if (entry.kind === 'omission')
818
+ continue;
819
+ if (entry.reason.length === 0) {
820
+ note('undeclared-divergence', declarationPath, `no reason for \`${entry.key}\` — ${describeEntry(entry)}`);
821
+ }
822
+ }
823
+ for (const key of Object.keys(input.declaration.reasons).sort()) {
824
+ if (!derivedKeys.has(key)) {
825
+ note('stale-reason', declarationPath, `\`${key}\` describes no divergence in this tree`);
826
+ }
827
+ }
828
+ // P3's three build-time findings. The other two — an undeclared ambiguity and
829
+ // a declared order that disagrees with the composition — stay at boot, because
830
+ // an ambiguity is a correctness question and a stale entry is not
831
+ // (`deployment-declaration.md` §3.4).
832
+ const decoratorsOf = new Map();
833
+ for (const entry of entries) {
834
+ if (entry.kind !== 'decoration')
835
+ continue;
836
+ const modules = decoratorsOf.get(entry.subject) ?? new Set();
837
+ modules.add(entry.module);
838
+ decoratorsOf.set(entry.subject, modules);
839
+ }
840
+ for (const [name, declared] of Object.entries(input.declaration.decorationOrder).sort()) {
841
+ const applied = decoratorsOf.get(name) ?? new Set();
842
+ if (applied.size < 2) {
843
+ note('stale-decoration-order', declarationPath, `\`decorationOrder['${name}']\` orders ${applied.size} decorating module(s); an order resolves an ambiguity between two or more`);
844
+ }
845
+ for (const member of declared) {
846
+ if (!overlaySet.has(member)) {
847
+ note('foreign-order-member', declarationPath, `\`decorationOrder['${name}']\` names '${member}', which is not one of this deployment's overlay modules`);
848
+ }
849
+ }
850
+ const missing = [...applied].filter((module) => !declared.includes(module)).sort();
851
+ if (applied.size >= 2 && missing.length > 0) {
852
+ note('incomplete-order', declarationPath, `\`decorationOrder['${name}']\` omits ${missing.map((id) => `'${id}'`).join(', ')}, which also decorate it`);
853
+ }
854
+ }
855
+ findings.sort((a, b) => a.kind === b.kind ? a.where.localeCompare(b.where) : a.kind.localeCompare(b.kind));
856
+ return {
857
+ report: {
858
+ deployment: input.deployment,
859
+ generatedFrom: { overlayRoot: input.overlayRoot },
860
+ overlayModules,
861
+ entries,
862
+ boundary: divergenceBoundary(classification, input.hostNotRecorded ?? []),
863
+ },
864
+ findings,
865
+ sites,
866
+ };
867
+ }
868
+ function detailFor(kind, site, routes) {
869
+ switch (kind) {
870
+ case 'decoration':
871
+ return { kind: 'decoration', depth: null };
872
+ case 'interceptor': {
873
+ const facts = site.interceptor ?? { phase: 'pre', order: 0, id: '' };
874
+ return {
875
+ kind: 'interceptor',
876
+ phase: facts.phase,
877
+ order: facts.order,
878
+ id: facts.id,
879
+ targetMatched: site.subject !== null && routes.has(site.subject),
880
+ };
881
+ }
882
+ case 'root-plugin':
883
+ return { kind: 'root-plugin', declaredReason: site.declaredReason ?? '' };
884
+ case 'subscription':
885
+ return { kind: 'subscription' };
886
+ case 'port-provided':
887
+ return { kind: 'port-provided' };
888
+ case 'port-consumed':
889
+ return { kind: 'port-consumed' };
890
+ case 'registration':
891
+ return { kind: 'registration' };
892
+ case 'worker':
893
+ return { kind: 'worker' };
894
+ case 'omission':
895
+ return { kind: 'omission' };
896
+ }
897
+ }
898
+ /** One sentence naming the divergence, for a finding a reader can act on. */
899
+ export function describeEntry(entry) {
900
+ const owner = entry.owner === null ? 'a composition root' : `'${entry.owner}'`;
901
+ switch (entry.kind) {
902
+ case 'decoration':
903
+ return `'${entry.module}' wraps '${entry.subject}', which ${owner} registers`;
904
+ case 'interceptor':
905
+ return `'${entry.module}' runs ${entry.detail.kind === 'interceptor' ? entry.detail.phase : ''} on '${entry.subject}'`;
906
+ case 'subscription':
907
+ return `'${entry.module}' subscribes to '${entry.subject}'`;
908
+ case 'port-consumed':
909
+ return `'${entry.module}' resolves the port '${entry.subject}', owned by ${owner}`;
910
+ case 'port-provided':
911
+ return `'${entry.module}' publishes the port '${entry.subject}'`;
912
+ case 'registration':
913
+ return `'${entry.module}' registers '${entry.subject}'`;
914
+ case 'root-plugin':
915
+ return `'${entry.module}' mounts a plugin at the server root`;
916
+ case 'worker':
917
+ return `'${entry.module}' consumes the queue '${entry.subject}'`;
918
+ case 'omission':
919
+ return `this deployment does not ship '${entry.subject}'`;
920
+ }
921
+ }
922
+ /**
923
+ * The names one rendering makes two incompatible statements about
924
+ * (`specs/124-instance-customisation-gap/` FR-010).
925
+ *
926
+ * A8 of the instance acceptance criterion printed both in one line:
927
+ *
928
+ * > 1 entries: `registration:instance_acceptance_overlay:instanceAcceptanceOverlayService`;
929
+ * > findings: … [unowned-subject] … `'instanceAcceptanceOverlayService'` is
930
+ * > registered by no module in the composition
931
+ *
932
+ * The rendering knew the overlay module registered the name — it lists the
933
+ * registration — and then reported a decoration of that same name as owned by
934
+ * nobody, over a tree that had booted. Whatever produced that, it is a finding
935
+ * about the **run** and not about the tree, which is what makes it a refusal
936
+ * rather than a tenth finding kind: the remedy `unowned-subject` prints
937
+ * (*"Composition throws for it at boot"*) sends its reader to fix a spelling
938
+ * that is not wrong.
939
+ *
940
+ * Pure over the result, so both hosts get it from the same expression and a red
941
+ * proof enters where a real run enters.
942
+ */
943
+ export function selfContradictingSubjects(result) {
944
+ const registered = new Set(result.report.entries
945
+ .filter((entry) => entry.kind === 'registration')
946
+ .map((entry) => entry.subject));
947
+ const contradicted = new Set();
948
+ for (const finding of result.findings) {
949
+ if (finding.kind !== 'unowned-subject')
950
+ continue;
951
+ for (const name of registered) {
952
+ // The detail is the finding's own sentence — `'<name>' is registered by
953
+ // no module in the composition` — and the quotes are what stop
954
+ // `blogService` matching a finding about `blogServiceCache`.
955
+ if (finding.detail.includes(`'${name}'`))
956
+ contradicted.add(name);
957
+ }
958
+ }
959
+ return [...contradicted].sort();
960
+ }
961
+ export function divergenceRefusal(input) {
962
+ if (input.seamsClassified <= 0) {
963
+ return {
964
+ kind: 'no-seam-classified',
965
+ message: 'the rung table classifies no seam, so every ModuleContext member would pass ' +
966
+ 'unclassified and the ladder would rank nothing; refusing to report a vacuous pass',
967
+ };
968
+ }
969
+ if (input.deployments.length === 0 && input.committedDeploymentArtefacts.length > 0) {
970
+ return {
971
+ kind: 'no-deployment-with-committed-artefact',
972
+ message: `no deployment on disk under src/apps/, while ` +
973
+ `${input.committedDeploymentArtefacts.length} committed deployment report(s) name ` +
974
+ 'one; the population this run judges is gone, not clean',
975
+ };
976
+ }
977
+ if (input.ownersResolved <= 0) {
978
+ return {
979
+ kind: 'no-owner-resolved',
980
+ message: 'the port→owner map resolved no container name anywhere in the tree, so every ' +
981
+ 'decoration and every consumed port would read as owned by nobody — a finding about ' +
982
+ 'the run dressed as one about the tree; refusing to report a vacuous pass',
983
+ };
984
+ }
985
+ const contradictions = input.selfContradictingSubjects ?? [];
986
+ if (contradictions.length > 0) {
987
+ return {
988
+ kind: 'self-contradicting-attribution',
989
+ message: `one rendering lists registration(s) of ${contradictions.map((name) => `'${name}'`).join(', ')} ` +
990
+ 'and reports the same name(s) as `unowned-subject` — a derivation contradicting itself ' +
991
+ 'inside one file, whose remedy text ("Composition throws for it at boot") is untrue of a ' +
992
+ 'tree that composed; refusing to report a finding about the run as one about the tree',
993
+ };
994
+ }
995
+ if (input.sites <= 0 && input.overlaySpellsASeamCall) {
996
+ return {
997
+ kind: 'no-seam-call-read',
998
+ message: 'no seam call of any kind was read across the overlay walk, while the overlay ' +
999
+ 'sources spell one — a resolver that stopped recognising `ctx.di.decorate` prints a ' +
1000
+ 'clean report over a tree full of decorations; refusing to report a vacuous pass',
1001
+ };
1002
+ }
1003
+ return null;
1004
+ }
1005
+ //# sourceMappingURL=divergence.js.map