@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,951 @@
1
+ /**
2
+ * CI check — **every environment value a running Endora reads is declared, and
3
+ * every declared input is read** (`specs/117-instance-bring-up/` FR-003;
4
+ * `contracts/environment-inputs.md` §3).
5
+ *
6
+ * ## Why a check of its own
7
+ *
8
+ * No existing check's population contains a `process.env` read.
9
+ * `check:module-boundary` reads import specifiers and SQL tables,
10
+ * `check:admin-zones` reads zone names and `useTranslation` scopes,
11
+ * `i18n:hardcoded` reads JSX in `.tsx`, `check:default-language-prose` reads
12
+ * prose literals, `check:entry-scope` reads entry sites. Widening one of them
13
+ * would be issue #244's shape arriving through the repair: a rule's stated
14
+ * subject enlarged past what its walk can see.
15
+ *
16
+ * ## Both directions, and the second one is the one that would rot
17
+ *
18
+ * `undeclared-input` is a variable a client's `.env` will be short of — loud,
19
+ * eventually, as a failed boot nobody can attribute. `unread-input` is an
20
+ * operator being asked for a value that changes nothing, and **nothing else in
21
+ * the tree would ever notice it**: the read went away, the declaration stayed,
22
+ * and the prompt goes on asking. A one-way check would leave the declaration
23
+ * accumulating variables the platform stopped reading three features ago,
24
+ * which is how a contract stops being believed.
25
+ *
26
+ * ## The population is roots, never "the trees that happen to declare"
27
+ *
28
+ * §R3.4. Inferring the population from the presence of declarations is issue
29
+ * #244's shape exactly: a tree that declares nothing and reads five variables
30
+ * would be perfectly clean. So the roots are handed in by the host, one per
31
+ * member of `ENVIRONMENT_CONSUMERS` — the contract package's own enum, which is
32
+ * authored nowhere near this check and does not move when a declaration does —
33
+ * and a consumer that contributed no file is a refusal rather than a consumer
34
+ * with nothing wrong in it.
35
+ *
36
+ * ## Two read dialects, because the tree has two
37
+ *
38
+ * `process.env.X` and `process.env['X']` for anything Node runs, and
39
+ * `import.meta.env.X` / `import.meta.env['X']` for the admin, which is a Vite
40
+ * SPA and reads **no** `process.env` at all — measured: 0 sites over 294 files.
41
+ * A check that knew only the Node dialect would report the admin's single input
42
+ * as `unread-input` and its actual read as nothing at all, which is a finding
43
+ * about the check dressed as one about the tree.
44
+ *
45
+ * **In the Vite dialect only a prefixed name comes from the environment.**
46
+ * `import.meta.env.DEV`, `PROD`, `MODE`, `SSR` and `BASE_URL` are Vite's own
47
+ * constants, substituted from the build mode: no `.env` can supply one and no
48
+ * operator can be asked for one, so they are not inputs and are not read as
49
+ * such. The discriminator is the {@link VITE_ENVIRONMENT_PREFIX} Vite itself
50
+ * uses to decide the same question, not a list of the built-ins that exist
51
+ * today. The Node dialect has no equivalent exclusion and needs none —
52
+ * `NEXT_RUNTIME` is a real process variable that Next sets, so it is declared
53
+ * like any other, with a sentence saying an operator has nothing to choose.
54
+ *
55
+ * ## A named constant is not a computed name
56
+ *
57
+ * `unresolvable-input-name` is a finding and never a skip (issue #113): read as
58
+ * "declared" it agrees with everything. But the tree's own **correct** idiom
59
+ * writes `process.env[PUBLIC_API_BASE_URL_VAR]`, where the constant is a string
60
+ * literal exported by `storefront/lib/env.mjs` — one spelling of the variable
61
+ * in one place, which is the shape this repository wants and which a naive
62
+ * "computed ⇒ unresolvable" rule would punish on the day it landed. Two of the
63
+ * storefront's own reads are written that way. So an identifier is resolved
64
+ * two hops: a `const` in the same file whose initialiser is a string literal,
65
+ * and an imported binding whose source file — within the walk — exports one.
66
+ * Anything further is `unresolvable-input-name`, which is the fail-closed
67
+ * direction.
68
+ *
69
+ * ## What `foreign-input` covers, and why it is two shapes under one kind
70
+ *
71
+ * A declaration is shipped by an **author** — the platform, one application,
72
+ * or one module — and both shapes are that author declaring something that is
73
+ * not theirs, with one remedy: delete it and let the owner declare it.
74
+ *
75
+ * * `owner` disagreeing with the author that ships the file. The storefront
76
+ * declaring `{ kind: 'platform' }` puts a fact about the platform in a tree
77
+ * a client owns and edits.
78
+ * * one name declared twice **within one author's** declaration. Two entries,
79
+ * two `describes`, and whichever the reader reaches first wins.
80
+ *
81
+ * **Across authors a shared name is expected and is not a finding**, and that
82
+ * distinction is load-bearing. `REVALIDATE_SECRET` is declared by the platform
83
+ * *and* by the storefront, because under D-195 those are two repositories that
84
+ * ship independently and each has to be able to say what it needs. That the two
85
+ * declarations name each other in `consumers` is precisely what §5's cross-tree
86
+ * agreement is derived from; collapsing them into one declaration would leave a
87
+ * scaffolded storefront unable to state its own requirements.
88
+ *
89
+ * ## What it does not judge, stated rather than discovered later
90
+ *
91
+ * * **Whether a value is correct.** That is `endora doctor`'s question and it
92
+ * needs a running environment.
93
+ * * **A module's inputs.** They are declared in `manifest.ts` and land with
94
+ * this feature's Phase 3; until then the module tree is outside this
95
+ * population and the host **prints** that rather than passing over it in
96
+ * silence.
97
+ * * **A read reached through a helper in another file** —
98
+ * `readEnv('DATABASE_URL')`. The walk sees member expressions on
99
+ * `process.env` and `import.meta.env`, not every function that might
100
+ * forward one.
101
+ * * **A `.env.example`.** It is prose, it is stale, and reconciling against
102
+ * it would make a stale file authoritative.
103
+ */
104
+ import { existsSync, readdirSync } from 'node:fs';
105
+ import { join } from 'node:path';
106
+ import { pathToFileURL } from 'node:url';
107
+ import ts from 'typescript';
108
+ import { ENVIRONMENT_CONSUMERS, } from '@endora-commerce/contracts';
109
+ /** The log prefix both hosts print under — one grammar, one spelling. */
110
+ export const PREFIX = '[env-inputs]';
111
+ /** What to do about each kind, printed above the findings of that kind. */
112
+ export const REMEDIES = {
113
+ 'undeclared-input': 'This code reads an environment variable nothing declares, so no `.env` written by ' +
114
+ '`endora new instance` or `endora new storefront` will carry it and `endora doctor` ' +
115
+ 'cannot tell an operator it is missing. Declare it where the code that reads it lives: ' +
116
+ 'the platform in `packages/platform/src/env/index.ts`, an application in its own ' +
117
+ '`environment-inputs.mjs`, a module in its `manifest.ts`.',
118
+ 'undeclared-module-input': 'A module reads an environment variable that neither the platform, nor the application ' +
119
+ 'tree it runs in, nor this module’s own `manifest.ts` declares — so no `.env` a ' +
120
+ 'client writes will carry it and `endora doctor` cannot tell them it is missing. That ' +
121
+ 'matters more for a module than for anything else here: a module package ships `dist`, ' +
122
+ '`i18n` and `docs`, and `.env.example` is a file in the platform’s own repository. ' +
123
+ 'Declare it in the module’s `env` array, with `owner` naming this module. **Another ' +
124
+ 'module’s declaration does not satisfy this read**, deliberately: a client may install ' +
125
+ 'this module and not that one, and an input that arrives only with a sibling is an input ' +
126
+ 'that is missing exactly when the sibling is.',
127
+ 'unread-input': 'This input is declared and nothing reads it, so an operator is asked for a value that ' +
128
+ 'changes nothing. Delete the declaration, or restore the read it was written for. For a ' +
129
+ 'module the question is asked over that module’s own sources: a sibling reading the ' +
130
+ 'name does not make this declaration earned.',
131
+ 'module-declares-a-platform-input': 'The platform already declares this variable, in `packages/platform/src/env/index.ts`, ' +
132
+ 'and a module’s read of it is satisfied by that declaration. Delete the entry. Seven ' +
133
+ 'names — `NODE_ENV`, `BACKEND_ROLE`, `STOREFRONT_BASE_URL`, `PUBLIC_API_BASE_URL`, ' +
134
+ '`BACKEND_PUBLIC_URL`, `REVALIDATE_SECRET`, `SETTINGS_SECRET_ENCRYPTION_KEY` — are read ' +
135
+ 'by thirty modules between them, and a module that declares what it *reads* rather than ' +
136
+ 'what it *owns* puts one fact in thirty manifests with thirty descriptions. Whichever a ' +
137
+ 'reader reaches first wins and the rest drift (D-100).',
138
+ 'foreign-input': 'This declaration is not the shipping author’s to make — either its `owner` names ' +
139
+ 'somebody else, or the same author declares the name twice. Delete it and let the owner ' +
140
+ 'declare it. A name declared by two *different* authors is not this finding: two trees ' +
141
+ 'that ship independently each state what they need, and `consumers` is what joins them.',
142
+ 'module-input-without-a-settings-verdict': 'A module-owned environment input is debt against the Settings module until somebody has ' +
143
+ 'said why it is not one (`contracts/environment-inputs.md` §4). Add an entry to the ' +
144
+ 'module’s shard under `backend/scripts/ledgers/module-environment-inputs/`, ' +
145
+ 'classified `bootstrap` or `configuration`, with the reason. The legitimate answer is ' +
146
+ 'that the value is needed **before the settings store can be read** — ' +
147
+ '`SETTINGS_SECRET_ENCRYPTION_KEY` and `MFA_SECRET_ENCRYPTION_KEY` are the standing ' +
148
+ 'examples, being the keys the store’s own secrets are decrypted with. "Convenience" ' +
149
+ 'and "historical" are `configuration`, which is a finding waiting to be repaired rather ' +
150
+ 'than an exemption — and an entry whose reason says nothing is not a verdict, so it is ' +
151
+ 'reported here rather than counted as one.',
152
+ 'stale-settings-verdict': 'A ledger entry describes an environment input this module no longer declares. The ' +
153
+ 'ledger is two-way: it drains as a module moves its configuration into Settings, and an ' +
154
+ 'entry left behind is a claim about a variable that is gone. Delete it — and delete the ' +
155
+ 'shard when its last entry goes, because an empty file is a done signal that says ' +
156
+ 'nothing.',
157
+ 'unresolvable-input-name': 'The variable is named by an expression this analysis cannot resolve to a literal, so it ' +
158
+ 'can be reconciled against no declaration. Write the name as a string literal, or as a ' +
159
+ '`const` initialised to one — either in this file or exported by a file in the same ' +
160
+ 'tree, which is the idiom `storefront/lib/env.mjs` already uses.',
161
+ 'generable-without-secret': '`generable` is permitted only on a secret whose two correct values are interchangeable ' +
162
+ '(`input-resolution.md` R4.5). A URL, a name, a channel code, a locale or a hostname ' +
163
+ 'carries a human judgement and may never be generated: the tool would be inventing the ' +
164
+ 'value the provenance line promises it does not.',
165
+ 'requirement-without-a-consequence': 'An `optional` input has to say **what is lost** without it, in both shipped languages — ' +
166
+ 'that sentence is what an operator decides on, and the word “optional” is not it. A ' +
167
+ '`requiredWhen` has to name an input some declaration carries, or the condition can ' +
168
+ 'never be evaluated and the input is required by nobody.',
169
+ };
170
+ const authorKey = (owner) => owner.kind === 'platform'
171
+ ? 'platform'
172
+ : owner.kind === 'application'
173
+ ? `application:${owner.application}`
174
+ : `module:${owner.moduleId}`;
175
+ /**
176
+ * The `(name, consumer)` key both reconciliations are asked against.
177
+ *
178
+ * One function rather than two template literals, because the two have to agree
179
+ * exactly and the separator is invisible. `\0` is the separator for the usual
180
+ * reason — it is the one byte neither a variable name nor a consumer can carry —
181
+ * and it is spelled as an **escape**: a raw NUL in the source would make git
182
+ * classify this file as binary and render every diff of it as
183
+ * `Binary files differ`, which is `check:nul-bytes`' whole subject. The compiled
184
+ * string is identical, so nothing about the key changes.
185
+ */
186
+ const consumerKey = (name, consumer) => `${name}\0${consumer}`;
187
+ /**
188
+ * The same key, narrowed to one module — what `unread-input` is asked against
189
+ * for a module author.
190
+ *
191
+ * A module declaring an input its **own** sources do not read is the finding;
192
+ * a sibling reading the name does not earn the declaration, and asking the
193
+ * question over the whole backend tree would say it did. That is not
194
+ * hypothetical here: `MEILISEARCH_URL` is legitimately declared by two modules,
195
+ * so the tree-wide question answers "read" for both the moment either one reads
196
+ * it, and the direction this check exists to protect — an operator asked for
197
+ * a value that changes nothing — would stop working for the module half on
198
+ * the day it landed.
199
+ */
200
+ const moduleKey = (module, name, consumer) => `${module}\0${name}\0${consumer}`;
201
+ /**
202
+ * A sentence, rather than a string.
203
+ *
204
+ * `''` is refused by the schema; this is the next failure along — a `without`
205
+ * reading `optional`, `n/a` or `-`, which parses and says nothing. Held to a
206
+ * length rather than to a blocklist, in the idiom the host-internal subpath
207
+ * reasons are held to.
208
+ */
209
+ const SAYS_SOMETHING = 12;
210
+ /** The whole rule, pure over the record, so a proof enters where a run does. */
211
+ export function checkEnvironmentInputs(input) {
212
+ const findings = [];
213
+ // Every declared name, whoever declared it — the vocabulary a `requiredWhen`
214
+ // predicate and an `undeclared-input` verdict are both answered against.
215
+ const declaredNames = new Set();
216
+ // (name, consumer) → the declarations that claim it, for the read reconciliation.
217
+ const byNameAndConsumer = new Map();
218
+ // Every name the **platform** declares — what `module-declares-a-platform-input`
219
+ // is asked against, and deliberately not "every name a non-module author
220
+ // declares". The applications' names are excluded on purpose: the backend
221
+ // genuinely reading a name the storefront also declares is `REVALIDATE_SECRET`'s
222
+ // shape, two trees that ship independently each stating what they need, and
223
+ // widening the predicate to cover it would turn a correct declaration into a
224
+ // finding.
225
+ const platformNames = new Set();
226
+ const modulesDeclaring = new Set();
227
+ let declared = 0;
228
+ let declaredByModules = 0;
229
+ for (const source of input.declarations) {
230
+ const seen = new Set();
231
+ for (const entry of source.inputs) {
232
+ declared += 1;
233
+ declaredNames.add(entry.name);
234
+ if (source.author.kind === 'platform')
235
+ platformNames.add(entry.name);
236
+ if (source.author.kind === 'module') {
237
+ declaredByModules += 1;
238
+ modulesDeclaring.add(source.author.moduleId);
239
+ }
240
+ // — `foreign-input`, shape one: the owner is not the shipping author.
241
+ if (authorKey(entry.owner) !== authorKey(source.author)) {
242
+ findings.push({
243
+ kind: 'foreign-input',
244
+ name: entry.name,
245
+ where: source.file,
246
+ detail: `declared with owner \`${authorKey(entry.owner)}\` by ` +
247
+ `\`${authorKey(source.author)}\`, which is not the author of this file`,
248
+ });
249
+ }
250
+ // — `foreign-input`, shape two: declared twice by one author.
251
+ if (seen.has(entry.name)) {
252
+ findings.push({
253
+ kind: 'foreign-input',
254
+ name: entry.name,
255
+ where: source.file,
256
+ detail: 'declared twice in one declaration — two entries, two `describes`, and whichever ' +
257
+ 'a reader reaches first wins',
258
+ });
259
+ }
260
+ seen.add(entry.name);
261
+ // — `generable-without-secret`.
262
+ if (entry.generable && !entry.secret) {
263
+ findings.push({
264
+ kind: 'generable-without-secret',
265
+ name: entry.name,
266
+ where: source.file,
267
+ detail: '`generable` is set on an input that is not a secret',
268
+ });
269
+ }
270
+ // — `requirement-without-a-consequence`, the `optional` half.
271
+ if (entry.requirement.kind === 'optional' &&
272
+ (entry.requirement.without.en.trim().length < SAYS_SOMETHING ||
273
+ entry.requirement.without.pl.trim().length < SAYS_SOMETHING)) {
274
+ findings.push({
275
+ kind: 'requirement-without-a-consequence',
276
+ name: entry.name,
277
+ where: source.file,
278
+ detail: 'its `optional` sentence does not say what is lost without the input',
279
+ });
280
+ }
281
+ for (const consumer of entry.consumers) {
282
+ const key = consumerKey(entry.name, consumer);
283
+ const claims = byNameAndConsumer.get(key);
284
+ if (claims === undefined)
285
+ byNameAndConsumer.set(key, [source]);
286
+ else
287
+ claims.push(source);
288
+ }
289
+ }
290
+ }
291
+ // — `module-declares-a-platform-input`. A **second pass**, for the same reason
292
+ // the `requiredWhen` one below is: the question is asked against another
293
+ // author's declaration, so asking it inside the first loop would make the
294
+ // verdict depend on the order the host happened to hand the declarations in
295
+ // — green when the platform's came first, red when it came second, over one
296
+ // tree.
297
+ //
298
+ // Not a shape of `foreign-input`: there the `owner` field is wrong and the
299
+ // remedy is "let the owner declare it"; here the `owner` is honest — the
300
+ // author really did mean this module — and the remedy is "delete it, the
301
+ // platform already said this". `defineModuleManifest` cannot see it, having
302
+ // no sight of the platform's declaration, so this is the only layer that can.
303
+ for (const source of input.declarations) {
304
+ if (source.author.kind !== 'module')
305
+ continue;
306
+ for (const entry of source.inputs) {
307
+ if (!platformNames.has(entry.name))
308
+ continue;
309
+ findings.push({
310
+ kind: 'module-declares-a-platform-input',
311
+ name: entry.name,
312
+ where: source.file,
313
+ detail: `declared by the module \`${source.author.moduleId}\`, and the platform declares it ` +
314
+ 'too — one variable, two descriptions, and whichever a reader reaches first wins',
315
+ });
316
+ }
317
+ }
318
+ // — `requirement-without-a-consequence`, the `requiredWhen` half. Second
319
+ // pass, because the vocabulary a predicate is answered against is every
320
+ // author's and not the declaring author's alone: a condition may
321
+ // legitimately read an input the platform owns.
322
+ for (const source of input.declarations) {
323
+ for (const entry of source.inputs) {
324
+ if (entry.requirement.kind !== 'requiredWhen')
325
+ continue;
326
+ if (declaredNames.has(entry.requirement.input))
327
+ continue;
328
+ findings.push({
329
+ kind: 'requirement-without-a-consequence',
330
+ name: entry.name,
331
+ where: source.file,
332
+ detail: `its condition reads \`${entry.requirement.input}\`, which no declaration carries, ` +
333
+ 'so the condition can never be true and the input is required by nobody',
334
+ });
335
+ }
336
+ }
337
+ // — `unresolvable-input-name`, `undeclared-input` and
338
+ // `undeclared-module-input`, over the reads.
339
+ const readNamesByConsumer = new Map();
340
+ const readNamesByModule = new Set();
341
+ for (const read of input.reads) {
342
+ if (read.name === null) {
343
+ findings.push({
344
+ kind: 'unresolvable-input-name',
345
+ name: '(computed)',
346
+ where: `${read.file}:${read.line}`,
347
+ detail: 'the variable is named by an expression that is neither a string literal nor a ' +
348
+ 'constant initialised to one',
349
+ });
350
+ continue;
351
+ }
352
+ const names = readNamesByConsumer.get(read.consumer) ?? new Set();
353
+ names.add(read.name);
354
+ readNamesByConsumer.set(read.consumer, names);
355
+ if (read.module !== undefined) {
356
+ readNamesByModule.add(moduleKey(read.module, read.name, read.consumer));
357
+ }
358
+ // **The resolution rule, and it is one predicate for both populations**
359
+ // (T3-B): a read resolves against the platform's declaration, the
360
+ // application tree's, and — where the file is a module's — that module's
361
+ // own. Never another module's.
362
+ //
363
+ // A module's declaration must not cover an *application's* read either, and
364
+ // the same line says so: `read.module` is `undefined` there, so the
365
+ // `moduleId` comparison fails for every module author. One rule, two
366
+ // populations, no second predicate to keep in step.
367
+ const claims = byNameAndConsumer.get(consumerKey(read.name, read.consumer)) ?? [];
368
+ const satisfied = claims.some((claim) => claim.author.kind !== 'module' || claim.author.moduleId === read.module);
369
+ if (satisfied)
370
+ continue;
371
+ if (read.module === undefined) {
372
+ findings.push({
373
+ kind: 'undeclared-input',
374
+ name: read.name,
375
+ where: `${read.file}:${read.line}`,
376
+ detail: `read in the ${read.consumer} tree, which declares no input of that name`,
377
+ });
378
+ continue;
379
+ }
380
+ // A sibling declaring it is worth saying, because it is the shape an author
381
+ // is most likely to think already correct — and the answer is that a client
382
+ // may install this module and not that one.
383
+ const bySibling = claims.filter((claim) => claim.author.kind === 'module');
384
+ const siblings = bySibling.length === 0
385
+ ? ''
386
+ : ` — ${bySibling
387
+ .map((claim) => claim.author.kind === 'module' ? `\`${claim.author.moduleId}\`` : '')
388
+ .join(', ')} declares it, and a sibling's declaration does not travel with this module`;
389
+ findings.push({
390
+ kind: 'undeclared-module-input',
391
+ name: read.name,
392
+ where: `${read.file}:${read.line}`,
393
+ detail: `read by the module \`${read.module}\` in the ${read.consumer} tree, and neither the ` +
394
+ `platform, the ${read.consumer} application nor \`${read.module}\`'s own manifest ` +
395
+ `declares it${siblings}`,
396
+ });
397
+ }
398
+ // — `unread-input`. Asked per (name, consumer): an input declared as read by
399
+ // the storefront and read only by the backend is a declaration that is
400
+ // wrong about which tree needs it, which is exactly the fact `consumers`
401
+ // exists to carry and the fact member scoping runs on.
402
+ for (const source of input.declarations) {
403
+ const author = source.author;
404
+ // For a module the question is asked over **that module's own** sources.
405
+ // See {@link moduleKey}: `MEILISEARCH_URL` is legitimately declared by two
406
+ // modules, so a tree-wide question answers "read" for both the moment
407
+ // either reads it.
408
+ const isRead = (name, consumer) => author.kind === 'module'
409
+ ? readNamesByModule.has(moduleKey(author.moduleId, name, consumer))
410
+ : (readNamesByConsumer.get(consumer)?.has(name) ?? false);
411
+ for (const entry of source.inputs) {
412
+ const unread = entry.consumers.filter((consumer) => !isRead(entry.name, consumer));
413
+ if (unread.length === 0)
414
+ continue;
415
+ findings.push({
416
+ kind: 'unread-input',
417
+ name: entry.name,
418
+ where: source.file,
419
+ detail: author.kind === 'module'
420
+ ? `declared as read by ${unread.join(', ')}, and no source of the module ` +
421
+ `\`${author.moduleId}\` reads it there`
422
+ : `declared as read by ${unread.join(', ')}, and no source in ` +
423
+ `${unread.length === 1 ? 'that tree' : 'those trees'} reads it`,
424
+ });
425
+ }
426
+ }
427
+ // — The Settings-debt ledger, both ways (`environment-inputs.md` §4).
428
+ //
429
+ // `undefined` is a caller that is not asking; `[]` is a ledger that was read
430
+ // and holds nothing, under which every module-owned declaration is
431
+ // unjudged. The second is the state a deleted ledger directory would
432
+ // produce, which is why the host refuses that rather than reaching here.
433
+ if (input.settingsVerdicts !== undefined) {
434
+ const verdictsOf = new Map(input.settingsVerdicts.map((shard) => [shard.moduleId, shard.entries]));
435
+ const declaredByModule = new Map();
436
+ for (const source of input.declarations) {
437
+ if (source.author.kind !== 'module')
438
+ continue;
439
+ const names = declaredByModule.get(source.author.moduleId) ?? new Set();
440
+ for (const entry of source.inputs)
441
+ names.add(entry.name);
442
+ declaredByModule.set(source.author.moduleId, names);
443
+ for (const entry of source.inputs) {
444
+ const verdict = verdictsOf.get(source.author.moduleId)?.[entry.name];
445
+ // A reason that says nothing is not a verdict. Held to a length rather
446
+ // than to a blocklist, in the idiom the `optional` sentence is held to
447
+ // twenty lines up — "historical" and "convenience" are the sentences an
448
+ // entry written to make this pass would carry, and §4.3 calls them debt
449
+ // rather than exemptions.
450
+ if (verdict !== undefined && verdict.reason.trim().length >= SAYS_SOMETHING)
451
+ continue;
452
+ findings.push({
453
+ kind: 'module-input-without-a-settings-verdict',
454
+ name: entry.name,
455
+ where: source.file,
456
+ detail: verdict === undefined
457
+ ? `declared by the module \`${source.author.moduleId}\` and judged by nobody — ` +
458
+ 'no entry in its shard of the Settings-debt ledger'
459
+ : `its ledger entry does not say why it is ${verdict.classification} rather ` +
460
+ 'than a Setting',
461
+ });
462
+ }
463
+ }
464
+ for (const shard of input.settingsVerdicts) {
465
+ const declared = declaredByModule.get(shard.moduleId) ?? new Set();
466
+ for (const name of Object.keys(shard.entries)) {
467
+ if (declared.has(name))
468
+ continue;
469
+ findings.push({
470
+ kind: 'stale-settings-verdict',
471
+ name,
472
+ where: `backend/scripts/ledgers/module-environment-inputs/${shard.moduleId}.ts`,
473
+ detail: `the module \`${shard.moduleId}\` declares no environment input of that name`,
474
+ });
475
+ }
476
+ }
477
+ }
478
+ const declaringConsumers = new Set();
479
+ for (const source of input.declarations) {
480
+ for (const entry of source.inputs) {
481
+ for (const consumer of entry.consumers)
482
+ declaringConsumers.add(consumer);
483
+ }
484
+ }
485
+ const consumersCovered = ENVIRONMENT_CONSUMERS.filter((consumer) => declaringConsumers.has(consumer) && readNamesByConsumer.has(consumer));
486
+ return {
487
+ findings,
488
+ declared,
489
+ consumersCovered,
490
+ declaredByModules,
491
+ modulesDeclaring: [...modulesDeclaring].sort(),
492
+ };
493
+ }
494
+ const isProcessEnv = (node) => ts.isPropertyAccessExpression(node) &&
495
+ node.name.text === 'env' &&
496
+ ts.isIdentifier(node.expression) &&
497
+ node.expression.text === 'process';
498
+ /**
499
+ * The prefix Vite requires on a variable it takes from the environment.
500
+ *
501
+ * Vite's own `envPrefix`, at its default. A name without it is one of Vite's
502
+ * build-mode constants and is not an operator input — see the header.
503
+ */
504
+ export const VITE_ENVIRONMENT_PREFIX = 'VITE_';
505
+ const isImportMetaEnv = (node) => ts.isPropertyAccessExpression(node) &&
506
+ node.name.text === 'env' &&
507
+ ts.isMetaProperty(node.expression);
508
+ const scriptKindOf = (path) => path.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
509
+ /**
510
+ * Every string constant a file exports or declares at module scope.
511
+ *
512
+ * Only a bare `const X = '…'`, deliberately. A `let`, a computed initialiser
513
+ * and a member of an object literal are all outside it: each is a value that
514
+ * can differ from what the reader sees, and the whole point of resolving a
515
+ * constant here is that its value is decidable from the text.
516
+ */
517
+ export function stringConstants(source) {
518
+ const constants = new Map();
519
+ for (const statement of source.statements) {
520
+ if (!ts.isVariableStatement(statement))
521
+ continue;
522
+ if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
523
+ continue;
524
+ for (const declaration of statement.declarationList.declarations) {
525
+ if (!ts.isIdentifier(declaration.name))
526
+ continue;
527
+ const initialiser = declaration.initializer;
528
+ if (initialiser === undefined)
529
+ continue;
530
+ // `as const` and a plain literal both land here; a template literal with
531
+ // no substitution is the same value written differently.
532
+ const unwrapped = ts.isAsExpression(initialiser) ? initialiser.expression : initialiser;
533
+ if (ts.isStringLiteral(unwrapped) || ts.isNoSubstitutionTemplateLiteral(unwrapped)) {
534
+ constants.set(declaration.name.text, unwrapped.text);
535
+ }
536
+ }
537
+ }
538
+ return constants;
539
+ }
540
+ /**
541
+ * Where an imported identifier came from, as `(local name) → (specifier, exported name)`.
542
+ *
543
+ * Both spellings the tree writes — `import { X }` and `import { X as Y }`.
544
+ * A namespace import is deliberately absent: `env.PUBLIC_API_BASE_URL_VAR` is a
545
+ * property access, not an identifier, and resolving it would need the module
546
+ * graph rather than one file's text.
547
+ */
548
+ function importedBindings(source) {
549
+ const bindings = new Map();
550
+ for (const statement of source.statements) {
551
+ if (!ts.isImportDeclaration(statement))
552
+ continue;
553
+ if (!ts.isStringLiteral(statement.moduleSpecifier))
554
+ continue;
555
+ const clause = statement.importClause;
556
+ if (clause?.namedBindings === undefined)
557
+ continue;
558
+ if (!ts.isNamedImports(clause.namedBindings))
559
+ continue;
560
+ for (const element of clause.namedBindings.elements) {
561
+ bindings.set(element.name.text, {
562
+ specifier: statement.moduleSpecifier.text,
563
+ exported: (element.propertyName ?? element.name).text,
564
+ });
565
+ }
566
+ }
567
+ return bindings;
568
+ }
569
+ /**
570
+ * Resolve a relative import specifier against the importing file's directory,
571
+ * in the walk's own key space.
572
+ *
573
+ * POSIX-only and extension-tolerant: the tree writes `./env.mjs` from a `.ts`
574
+ * file (ESM's own rule) and `./env.js` where the target is `env.ts`. A bare
575
+ * specifier resolves to nothing here, which is right — a constant published by
576
+ * a package is not text this walk read.
577
+ */
578
+ export function resolveRelative(fromFile, specifier) {
579
+ if (!specifier.startsWith('.'))
580
+ return null;
581
+ const fromDirectory = fromFile.slice(0, Math.max(0, fromFile.lastIndexOf('/')));
582
+ const parts = `${fromDirectory}/${specifier}`.split('/');
583
+ const stack = [];
584
+ for (const part of parts) {
585
+ if (part === '.' || part.length === 0)
586
+ continue;
587
+ if (part === '..')
588
+ stack.pop();
589
+ else
590
+ stack.push(part);
591
+ }
592
+ return stack.join('/');
593
+ }
594
+ /** The extensions a resolved specifier may really be, in the order tried. */
595
+ const CANDIDATE_EXTENSIONS = ['', '.ts', '.tsx', '.mts', '.mjs', '.js'];
596
+ /**
597
+ * Every environment read in the walk, with its name resolved where it can be.
598
+ *
599
+ * The file text is the entry point (issue #130), so a red proof drives exactly
600
+ * what a real run drives.
601
+ */
602
+ export function collectEnvironmentReads(files) {
603
+ const parsed = new Map();
604
+ for (const file of files) {
605
+ parsed.set(file.path, ts.createSourceFile(file.path, file.text, ts.ScriptTarget.Latest, true, scriptKindOf(file.path)));
606
+ }
607
+ const constantsByFile = new Map();
608
+ const constantsOf = (path) => {
609
+ const cached = constantsByFile.get(path);
610
+ if (cached !== undefined)
611
+ return cached;
612
+ const source = parsed.get(path);
613
+ const computed = source === undefined ? new Map() : stringConstants(source);
614
+ constantsByFile.set(path, computed);
615
+ return computed;
616
+ };
617
+ /** The second hop: an imported constant, resolved in the walk's own files. */
618
+ const importedConstant = (file, identifier) => {
619
+ const source = parsed.get(file.path);
620
+ if (source === undefined)
621
+ return undefined;
622
+ const binding = importedBindings(source).get(identifier);
623
+ if (binding === undefined)
624
+ return undefined;
625
+ const base = resolveRelative(file.path, binding.specifier);
626
+ if (base === null)
627
+ return undefined;
628
+ // ESM specifiers name the emitted file; the walk holds the source. Try the
629
+ // specifier as written first, then the source extensions it could be.
630
+ const stem = base.replace(/\.(js|mjs|cjs)$/, '');
631
+ for (const extension of CANDIDATE_EXTENSIONS) {
632
+ const candidate = `${stem}${extension}`;
633
+ if (!parsed.has(candidate))
634
+ continue;
635
+ const value = constantsOf(candidate).get(binding.exported);
636
+ if (value !== undefined)
637
+ return value;
638
+ }
639
+ return undefined;
640
+ };
641
+ const reads = [];
642
+ for (const file of files) {
643
+ if (!file.text.includes('process.env') && !file.text.includes('import.meta.env'))
644
+ continue;
645
+ const source = parsed.get(file.path);
646
+ if (source === undefined)
647
+ continue;
648
+ const visit = (node) => {
649
+ if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
650
+ const container = node.expression;
651
+ const viteDialect = isImportMetaEnv(container);
652
+ if (isProcessEnv(container) || viteDialect) {
653
+ const line = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
654
+ // Vite's build-mode constants are not environment inputs: no `.env`
655
+ // supplies one and no operator can be asked for one. The prefix is
656
+ // Vite's own discriminator for the same question.
657
+ const fromEnvironment = (name) => !viteDialect || name.startsWith(VITE_ENVIRONMENT_PREFIX);
658
+ if (ts.isPropertyAccessExpression(node)) {
659
+ if (fromEnvironment(node.name.text)) {
660
+ reads.push({
661
+ name: node.name.text,
662
+ file: file.path,
663
+ line,
664
+ consumer: file.consumer,
665
+ module: file.module,
666
+ shape: 'literal',
667
+ });
668
+ }
669
+ }
670
+ else {
671
+ const argument = node.argumentExpression;
672
+ if (ts.isStringLiteralLike(argument)) {
673
+ if (fromEnvironment(argument.text)) {
674
+ reads.push({
675
+ name: argument.text,
676
+ file: file.path,
677
+ line,
678
+ consumer: file.consumer,
679
+ module: file.module,
680
+ shape: 'literal',
681
+ });
682
+ }
683
+ }
684
+ else if (ts.isIdentifier(argument)) {
685
+ const local = constantsOf(file.path).get(argument.text);
686
+ const resolved = local ?? importedConstant(file, argument.text);
687
+ if (resolved === undefined || fromEnvironment(resolved)) {
688
+ reads.push({
689
+ name: resolved ?? null,
690
+ file: file.path,
691
+ line,
692
+ consumer: file.consumer,
693
+ module: file.module,
694
+ shape: local !== undefined
695
+ ? 'const'
696
+ : resolved !== undefined
697
+ ? 'imported-const'
698
+ : 'computed',
699
+ });
700
+ }
701
+ }
702
+ else {
703
+ reads.push({
704
+ name: null,
705
+ file: file.path,
706
+ line,
707
+ consumer: file.consumer,
708
+ module: file.module,
709
+ shape: 'computed',
710
+ });
711
+ }
712
+ }
713
+ }
714
+ }
715
+ ts.forEachChild(node, visit);
716
+ };
717
+ visit(source);
718
+ }
719
+ return reads;
720
+ }
721
+ /** Raised when a declaration file cannot be read as one; a caller turns it into exit 2. */
722
+ export class DeclarationUnreadableError extends Error {
723
+ name = 'DeclarationUnreadableError';
724
+ }
725
+ /**
726
+ * A declaration read out of its own **source text**, never out of a build.
727
+ *
728
+ * ## Why not `import()` the emitted module
729
+ *
730
+ * A module package resolves through its `exports` map at its build output
731
+ * (D-164), so a check that imported one would answer about the previous build —
732
+ * the `stale-artefact` class `check:action-route-permissions` had to grow a
733
+ * refusal for, measured three times as a **false green** on that check. Reading
734
+ * the text removes the question rather than guarding it: the bytes this run
735
+ * judged are the bytes on disk, and the check needs no build to run at all.
736
+ * The `exports` subpath is still the path the *runtime* readers take, and
737
+ * `test/unit/packages/platform-env-subpath.test.ts` is what holds it open.
738
+ *
739
+ * ## What it evaluates, and what it refuses
740
+ *
741
+ * A declaration is data: an array of object literals over strings, booleans,
742
+ * arrays and nested objects. Nothing else is accepted — no identifier, no
743
+ * spread, no call, no concatenation — and an expression outside that grammar is
744
+ * a refusal rather than a value guessed at, because a declaration this analysis
745
+ * cannot read in full is one it must not report on (issue #113).
746
+ *
747
+ * A string is taken literally, so `'\n'` and a template literal with no
748
+ * substitution both arrive as the text they denote. `as const` is unwrapped:
749
+ * it changes a type and no value.
750
+ */
751
+ export function evaluateDeclarationArray(text, fileName, exportName) {
752
+ const source = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true, scriptKindOf(fileName));
753
+ const literal = literalReader(source, fileName);
754
+ for (const statement of source.statements) {
755
+ if (!ts.isVariableStatement(statement))
756
+ continue;
757
+ for (const declaration of statement.declarationList.declarations) {
758
+ if (!ts.isIdentifier(declaration.name))
759
+ continue;
760
+ if (declaration.name.text !== exportName)
761
+ continue;
762
+ if (declaration.initializer === undefined) {
763
+ throw new DeclarationUnreadableError(`${fileName}: \`${exportName}\` has no value`);
764
+ }
765
+ const value = literal(declaration.initializer);
766
+ if (!Array.isArray(value)) {
767
+ throw new DeclarationUnreadableError(`${fileName}: \`${exportName}\` is not an array`);
768
+ }
769
+ return value;
770
+ }
771
+ }
772
+ throw new DeclarationUnreadableError(`${fileName}: no \`${exportName}\` declaration — the file is not a declaration this ` +
773
+ 'run can read, and a run that could not read its input has said nothing about the tree');
774
+ }
775
+ /**
776
+ * The literal evaluator, shared by the two declaration readers above and below.
777
+ *
778
+ * One function rather than two copies: a module's `env` entries and the
779
+ * platform's are the same data in two files, and two evaluators that drifted
780
+ * would accept a declaration in one place and refuse it in the other for
781
+ * reasons neither author could see.
782
+ */
783
+ function literalReader(source, fileName) {
784
+ const literal = (node) => {
785
+ const unwrapped = ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node)
786
+ ? node.expression
787
+ : node;
788
+ if (ts.isStringLiteral(unwrapped) || ts.isNoSubstitutionTemplateLiteral(unwrapped)) {
789
+ return unwrapped.text;
790
+ }
791
+ if (unwrapped.kind === ts.SyntaxKind.TrueKeyword)
792
+ return true;
793
+ if (unwrapped.kind === ts.SyntaxKind.FalseKeyword)
794
+ return false;
795
+ if (unwrapped.kind === ts.SyntaxKind.NullKeyword)
796
+ return null;
797
+ if (ts.isNumericLiteral(unwrapped))
798
+ return Number(unwrapped.text);
799
+ if (ts.isArrayLiteralExpression(unwrapped))
800
+ return unwrapped.elements.map(literal);
801
+ if (ts.isObjectLiteralExpression(unwrapped)) {
802
+ const value = {};
803
+ for (const property of unwrapped.properties) {
804
+ if (!ts.isPropertyAssignment(property)) {
805
+ throw new DeclarationUnreadableError(`${fileName}: a property that is not a plain \`key: value\` assignment — a ` +
806
+ 'declaration is data and this analysis will not guess at it');
807
+ }
808
+ const key = ts.isIdentifier(property.name)
809
+ ? property.name.text
810
+ : ts.isStringLiteral(property.name)
811
+ ? property.name.text
812
+ : null;
813
+ if (key === null) {
814
+ throw new DeclarationUnreadableError(`${fileName}: a computed property name, which no declaration may carry`);
815
+ }
816
+ value[key] = literal(property.initializer);
817
+ }
818
+ return value;
819
+ }
820
+ throw new DeclarationUnreadableError(`${fileName}: \`${unwrapped.getText(source).slice(0, 60)}\` is not a literal — a ` +
821
+ 'declaration may hold only strings, booleans, numbers, arrays and objects');
822
+ };
823
+ return literal;
824
+ }
825
+ /** The helper every module's manifest is defined through. */
826
+ const MANIFEST_FACTORY = 'defineModuleManifest';
827
+ /**
828
+ * A module's `env` declaration, read out of its `manifest.ts` **source text**.
829
+ *
830
+ * ## Why text and not the built manifest
831
+ *
832
+ * The same reason {@link evaluateDeclarationArray} gives, with more force: a
833
+ * module package resolves through its own `exports` map at its build output
834
+ * (D-164), so a reader that imported one would answer about the previous build.
835
+ * That is the `stale-artefact` class `check:action-route-permissions` had to
836
+ * grow a refusal for after three measured false greens — an `env` entry edited
837
+ * and not rebuilt would be judged as though it were not there, which for *this*
838
+ * check is a false green in the direction that matters: a declaration the author
839
+ * just wrote, reported as missing, or one they just deleted, reported as
840
+ * present. Reading the text removes the question instead of guarding it, and the
841
+ * run needs no build at all.
842
+ *
843
+ * ## What it reads, and what it refuses
844
+ *
845
+ * Only the `env` property of the object literal handed to `defineModuleManifest`,
846
+ * and only as data. Everything else in the file is ignored, which is not a
847
+ * convenience: `cms`' manifest reads `process.env['CMS_PB_BREAKPOINT_TABLET_MIN']`
848
+ * in its own settings defaults, so a manifest is **not** a literal file and a
849
+ * reader that demanded one would refuse the very module whose declaration this
850
+ * feature exists to collect.
851
+ *
852
+ * `null` is "this module declares no environment input", which is true of most
853
+ * modules and is not a finding. A manifest with **no** `defineModuleManifest`
854
+ * call is a {@link DeclarationUnreadableError}: a module whose manifest this run
855
+ * cannot find is one it cannot report on, and reading that as "declares nothing"
856
+ * is the silent skip the estate is against (issue #113).
857
+ */
858
+ export function evaluateManifestEnvDeclaration(text, fileName) {
859
+ const source = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true, scriptKindOf(fileName));
860
+ const literal = literalReader(source, fileName);
861
+ let argument = null;
862
+ let sawFactory = false;
863
+ const visit = (node) => {
864
+ if (argument === null &&
865
+ ts.isCallExpression(node) &&
866
+ ts.isIdentifier(node.expression) &&
867
+ node.expression.text === MANIFEST_FACTORY) {
868
+ sawFactory = true;
869
+ const first = node.arguments[0];
870
+ if (first !== undefined && ts.isObjectLiteralExpression(first))
871
+ argument = first;
872
+ }
873
+ ts.forEachChild(node, visit);
874
+ };
875
+ visit(source);
876
+ if (argument === null) {
877
+ throw new DeclarationUnreadableError(sawFactory
878
+ ? `${fileName}: \`${MANIFEST_FACTORY}\` is called with something other than an object ` +
879
+ 'literal, so this run cannot see what the module declares'
880
+ : `${fileName}: no \`${MANIFEST_FACTORY}\` call — a module manifest this run cannot ` +
881
+ 'read is one it cannot report on, and "declares nothing" is not the same answer');
882
+ }
883
+ for (const property of argument.properties) {
884
+ if (!ts.isPropertyAssignment(property))
885
+ continue;
886
+ const key = ts.isIdentifier(property.name)
887
+ ? property.name.text
888
+ : ts.isStringLiteral(property.name)
889
+ ? property.name.text
890
+ : null;
891
+ if (key !== 'env')
892
+ continue;
893
+ const value = literal(property.initializer);
894
+ if (!Array.isArray(value)) {
895
+ throw new DeclarationUnreadableError(`${fileName}: \`env\` is not an array`);
896
+ }
897
+ return value;
898
+ }
899
+ return null;
900
+ }
901
+ /**
902
+ * The Settings-debt ledger, read off disk one shard per module
903
+ * (`contracts/environment-inputs.md` §4.2).
904
+ *
905
+ * Sharded because the debt is the module's: a repair that moves
906
+ * `INVENTORY_LOW_STOCK_RECIPIENT` into Settings touches `inventory`'s manifest
907
+ * and `inventory`'s shard, and nothing else. It is `check:module-boundary`'s
908
+ * shape, one population over.
909
+ *
910
+ * Throws rather than returning a partial answer, and every throw is a caller's
911
+ * exit 2: a ledger this run could not read in full is one it must not report
912
+ * on, because the missing half reads as *"nobody has judged this"* for one
913
+ * direction and as *"this entry describes nothing"* for the other — two
914
+ * confident findings out of one silence.
915
+ */
916
+ export async function loadModuleVerdictShards(directory) {
917
+ if (!existsSync(directory)) {
918
+ throw new DeclarationUnreadableError(`${directory} is not there — the Settings-debt ledger is what says why a module-owned ` +
919
+ 'environment input is not a Setting, and a run that could not read it would report ' +
920
+ 'every module declaration as unjudged');
921
+ }
922
+ const shards = [];
923
+ for (const name of readdirSync(directory).sort()) {
924
+ if (!name.endsWith('.ts'))
925
+ continue;
926
+ const moduleId = name.replace(/\.ts$/, '');
927
+ let loaded;
928
+ try {
929
+ loaded = (await import(pathToFileURL(join(directory, name)).href));
930
+ }
931
+ catch (error) {
932
+ throw new DeclarationUnreadableError(`ledger shard '${moduleId}' failed to load: ${String(error)}`);
933
+ }
934
+ const entries = loaded.entries;
935
+ if (typeof entries !== 'object' || entries === null) {
936
+ throw new DeclarationUnreadableError(`ledger shard '${moduleId}' exports no \`entries\` record`);
937
+ }
938
+ if (Object.keys(entries).length === 0) {
939
+ // An empty shard is a done signal that says nothing: it satisfies every
940
+ // presence test while judging no input, and it is the file left behind
941
+ // when a module's last entry drains. Delete the file instead.
942
+ throw new DeclarationUnreadableError(`ledger shard '${moduleId}' declares no entry — delete the file instead`);
943
+ }
944
+ shards.push({
945
+ moduleId,
946
+ entries: entries,
947
+ });
948
+ }
949
+ return shards;
950
+ }
951
+ //# sourceMappingURL=env-inputs.js.map