@starci/hfs 4.2.17 → 4.4.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 (564) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +7 -0
  3. package/db/migrate.mjs +361 -0
  4. package/db/post-cutover.mjs +136 -0
  5. package/emit/db-types.mjs +10 -1
  6. package/lint/run.mjs +1 -1
  7. package/package.json +4 -1
  8. package/release/gate.mjs +298 -0
  9. package/release/index.mjs +103 -0
  10. package/release/push.mjs +110 -0
  11. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  12. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  13. package/runtime/knowledge/hfs/rules.yaml +3437 -160
  14. package/runtime/knowledge/hfs/slots.yaml +430 -92
  15. package/runtime/knowledge/patterns/be/api.yaml +92 -2
  16. package/runtime/knowledge/patterns/be/cli.yaml +37 -9
  17. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  18. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  19. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  20. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  21. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  22. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  23. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  24. package/runtime/knowledge/patterns/be/webhooks.yaml +210 -36
  25. package/runtime/knowledge/patterns/fe/folder.yaml +4 -0
  26. package/runtime/knowledge/sonar-gate.yaml +1 -1
  27. package/runtime/modules/kernel/failure-codes.yaml +3128 -47
  28. package/runtime/modules/models/runtimes.yaml +1 -1
  29. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  30. package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
  31. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  32. package/runtime/scripts/api/git/add.mjs +5 -0
  33. package/runtime/scripts/api/git/cat-file.mjs +5 -0
  34. package/runtime/scripts/api/git/init.mjs +7 -0
  35. package/runtime/scripts/api/git/lib.mjs +3 -0
  36. package/runtime/scripts/api/git/tag.mjs +5 -0
  37. package/runtime/scripts/api/node/lib.mjs +20 -0
  38. package/runtime/scripts/api/node/run-node.mjs +6 -0
  39. package/runtime/scripts/api/sops/lib.mjs +41 -0
  40. package/runtime/scripts/api/sops/resolve-sops.mjs +7 -0
  41. package/runtime/scripts/api/sops/seal.mjs +1 -1
  42. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  43. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  44. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  45. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  46. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  47. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  48. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +103 -0
  49. package/runtime/scripts/hfs/architecture/backend.mjs +22 -10
  50. package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
  51. package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
  52. package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
  53. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  54. package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
  55. package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
  56. package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
  57. package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
  58. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  59. package/runtime/scripts/hfs/architecture/dead-exports.mjs +23 -50
  60. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  61. package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
  62. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
  63. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  64. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  65. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  66. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
  67. package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
  68. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +49 -25
  69. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  70. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  71. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  72. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  73. package/runtime/scripts/hfs/architecture/register-once.mjs +15 -7
  74. package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
  75. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  76. package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
  77. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +214 -17
  78. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
  79. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  80. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  81. package/runtime/scripts/hfs/architecture/tiers.mjs +11 -4
  82. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
  83. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +9 -13
  84. package/runtime/scripts/hfs/check-results.mjs +28 -0
  85. package/runtime/scripts/hfs/check.mjs +63 -72
  86. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  87. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  88. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  89. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  90. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  91. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  92. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  93. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  94. package/runtime/scripts/hfs/manifest-shape.mjs +91 -6
  95. package/runtime/scripts/hfs/path-findings.mjs +32 -2
  96. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  97. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  98. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  99. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  100. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  101. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  102. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  103. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  104. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  105. package/runtime/scripts/hfs/rules/contract.mjs +66 -5
  106. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  107. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  108. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  109. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  110. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  111. package/runtime/scripts/hfs/rules/database-migrations.mjs +317 -37
  112. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  113. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  114. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  115. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  116. package/runtime/scripts/hfs/rules/database.mjs +11 -2
  117. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  118. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  119. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  120. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  121. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  122. package/runtime/scripts/hfs/rules/edition.mjs +11 -10
  123. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  124. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  125. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  126. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  127. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  128. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  129. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  130. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  131. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  132. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  133. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  134. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  135. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  136. package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -0
  137. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  138. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  139. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  140. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  141. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  142. package/runtime/scripts/hfs/rules/secrets.mjs +26 -0
  143. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  144. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  145. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  146. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  147. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  148. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  149. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +168 -0
  150. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  151. package/runtime/scripts/hfs/rules/stacks.mjs +13 -8
  152. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  153. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  154. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  155. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  156. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  157. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  158. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  159. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  160. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  161. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  162. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  163. package/runtime/scripts/hfs/secret-document.mjs +97 -0
  164. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  165. package/runtime/scripts/hfs/secret-errors.mjs +20 -0
  166. package/runtime/scripts/hfs/secret-files.mjs +124 -0
  167. package/runtime/scripts/hfs/secret-init.mjs +300 -0
  168. package/runtime/scripts/hfs/secret-output.mjs +100 -0
  169. package/runtime/scripts/hfs/secret-sops.mjs +130 -0
  170. package/runtime/scripts/hfs/secret-store.mjs +163 -0
  171. package/runtime/scripts/hfs/secret.mjs +162 -193
  172. package/runtime/scripts/hfs/slot-imports.mjs +18 -3
  173. package/runtime/scripts/hfs/slot-required.mjs +13 -0
  174. package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
  175. package/runtime/scripts/hfs/slots.mjs +1 -0
  176. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  177. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  178. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  179. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  180. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  181. package/runtime/scripts/hfs/tree-findings.mjs +22 -0
  182. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  183. package/runtime/scripts/lib/compose-values.mjs +11 -0
  184. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  185. package/runtime/scripts/lib/json-clone.mjs +61 -0
  186. package/runtime/scripts/lib/list.mjs +4 -0
  187. package/runtime/scripts/lib/roots.mjs +59 -0
  188. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  189. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  190. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  191. package/runtime/scripts/lib/stack-services.mjs +1 -1
  192. package/runtime/scripts/lib/swarm-stack.mjs +126 -0
  193. package/runtime/scripts/lib/trim-end.mjs +8 -0
  194. package/runtime/scripts/lib/ts-analysis.mjs +23 -0
  195. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  196. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  197. package/scaffold/add-app.mjs +77 -59
  198. package/scaffold/add-cli-lite.mjs +24 -17
  199. package/scaffold/add-compose.mjs +79 -0
  200. package/scaffold/add-database-lite.mjs +14 -0
  201. package/scaffold/add-fe-overlay.mjs +122 -0
  202. package/scaffold/add-fe-page.mjs +269 -0
  203. package/scaffold/add-inbox-lite.mjs +87 -124
  204. package/scaffold/add-plan.mjs +183 -0
  205. package/scaffold/add-table.mjs +132 -70
  206. package/scaffold/add.mjs +186 -204
  207. package/scaffold/app.mjs +128 -43
  208. package/scaffold/edition-gate.mjs +3 -3
  209. package/scaffold/image.mjs +2 -2
  210. package/scaffold/lite-exports.mjs +2 -1
  211. package/scaffold/service.mjs +2 -2
  212. package/src/main.mjs +238 -119
  213. package/stack/age-install.mjs +37 -0
  214. package/stack/backup.mjs +100 -0
  215. package/stack/build-args.mjs +42 -0
  216. package/stack/build.mjs +173 -0
  217. package/stack/cli.mjs +130 -0
  218. package/stack/config.mjs +57 -0
  219. package/stack/context.mjs +57 -0
  220. package/stack/converge.mjs +107 -0
  221. package/stack/deploy.mjs +301 -0
  222. package/stack/digest.mjs +16 -0
  223. package/stack/docker-transport.mjs +53 -0
  224. package/stack/edge-alias.mjs +118 -0
  225. package/stack/errors.mjs +21 -0
  226. package/stack/image-scan.mjs +152 -0
  227. package/stack/ingress-create.mjs +114 -0
  228. package/stack/ingress-publish.mjs +273 -0
  229. package/stack/ingress.mjs +706 -0
  230. package/stack/jobs.mjs +51 -0
  231. package/stack/live-restore.mjs +177 -0
  232. package/stack/prepare.mjs +266 -0
  233. package/stack/private-file.mjs +22 -0
  234. package/stack/provision.mjs +90 -0
  235. package/stack/prune.mjs +46 -0
  236. package/stack/release-store.mjs +87 -0
  237. package/stack/remote.mjs +205 -0
  238. package/stack/render.mjs +149 -0
  239. package/stack/retire.mjs +47 -0
  240. package/stack/sealed.mjs +149 -0
  241. package/stack/task.mjs +42 -0
  242. package/stack/verify.mjs +99 -0
  243. package/sync/ci-commands.mjs +13 -0
  244. package/sync/index.mjs +127 -64
  245. package/sync/stack-images.mjs +127 -0
  246. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  247. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  248. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  249. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  250. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  251. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  252. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  253. package/templates/app/codegen-graphql.mjs.partial +13 -2
  254. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  255. package/templates/app/codegen-prelude.mjs.partial +8 -0
  256. package/templates/app/gitignore +4 -0
  257. package/templates/app/gitignore-lite +4 -0
  258. package/templates/app/hooks/husky/pre-commit +7 -12
  259. package/templates/app/package-scripts/package.json +1 -0
  260. package/templates/app/package-scripts/supabase.json +7 -0
  261. package/templates/app/package-scripts-lite/package.json +1 -7
  262. package/templates/app/skeleton/.editorconfig +1 -0
  263. package/templates/app/skeleton/.gitattributes +7 -1
  264. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  265. package/templates/app/skeleton/.nvmrc +1 -1
  266. package/templates/app/skeleton/.sops.yaml +1 -1
  267. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  268. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  269. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  270. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  271. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  272. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  273. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  275. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  276. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  277. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  278. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  279. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  280. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  281. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  282. package/templates/app/skeleton/README.md +6 -6
  283. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  284. package/templates/app/skeleton-lite/.editorconfig +16 -0
  285. package/templates/app/skeleton-lite/.gitattributes +8 -0
  286. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  287. package/templates/app/skeleton-lite/.nvmrc +1 -0
  288. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  289. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +193 -0
  290. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  291. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  292. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  293. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  294. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  295. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  296. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  297. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  298. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  299. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  300. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  301. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  302. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  303. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  304. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  305. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  306. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  307. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  308. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  309. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  310. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  311. package/templates/app/skeleton-lite/.starciwork/workspace.yaml +9 -0
  312. package/templates/app/skeleton-lite/README.md +8 -4
  313. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  314. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  315. package/templates/app/starciwork.gitignore-lite +6 -0
  316. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  317. package/templates/be/image/worker/Dockerfile +1 -1
  318. package/templates/be/inbox/database-client.spec.ts.tpl +22 -28
  319. package/templates/be/inbox/database-client.ts.tpl +14 -28
  320. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  321. package/templates/be/inbox/event-index.ts.tpl +1 -0
  322. package/templates/be/inbox/events.json.tpl +10 -0
  323. package/templates/be/inbox/migration.sql.tpl +1 -0
  324. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  325. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  326. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  327. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  328. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  329. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  330. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  331. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  332. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  333. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  334. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  335. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  336. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  337. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  338. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  339. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  340. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  341. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  342. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  343. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  344. package/templates/be/inbox/service.spec.ts.tpl +49 -20
  345. package/templates/be/inbox/service.ts.tpl +27 -16
  346. package/templates/be/inbox/sql.ts.tpl +2 -2
  347. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -3
  348. package/templates/be/inbox/webhook-door.ts.tpl +4 -3
  349. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  350. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  351. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  352. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  353. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  354. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  355. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  356. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  357. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  358. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  359. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  360. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  362. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  363. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  364. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  365. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  366. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  367. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  368. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  369. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  370. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  371. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  372. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  373. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  374. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  375. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  376. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  377. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  378. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  379. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  380. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  381. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  382. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  383. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  384. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  385. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  386. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  387. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  388. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  389. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  390. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  391. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  392. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  393. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  394. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  395. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  396. package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +3 -3
  397. package/templates/be/patterns/webhooks/webhook.door.ts.tpl +3 -3
  398. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  399. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  400. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  401. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  402. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  403. package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +21 -0
  404. package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +1 -1
  405. package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -45
  406. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  407. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  408. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +44 -0
  409. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  410. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  411. package/templates/be/skeleton/apps/__app__/src/main.ts +18 -10
  412. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  413. package/templates/be/skeleton/apps/cli/src/cli.env.ts +13 -0
  414. package/templates/be/skeleton/apps/cli/src/cli.options.ts +7 -17
  415. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  416. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  417. package/templates/be/skeleton/apps/cli/src/main.ts +3 -2
  418. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  419. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  420. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  421. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  422. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  423. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  424. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  425. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  426. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  427. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  428. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  429. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  430. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  431. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  432. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  433. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  434. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  435. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  436. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  437. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  438. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  439. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  440. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  441. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  442. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  443. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  444. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  445. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  446. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  447. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  448. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  449. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  450. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +6 -6
  451. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  452. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  453. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  454. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  455. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  456. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  457. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  458. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  459. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  460. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  461. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  462. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  463. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  464. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  465. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  466. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  467. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  468. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  469. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  470. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  471. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  472. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  473. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  474. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  475. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  476. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  477. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  478. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  479. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  480. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  481. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  482. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  483. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  484. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  485. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  486. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  487. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  488. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  489. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +25 -0
  490. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +1 -1
  491. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +18 -58
  492. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  493. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  494. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  495. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +47 -0
  496. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  497. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  498. package/templates/be/skeleton-lite/apps/api/src/main.ts +5 -13
  499. package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +17 -0
  500. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  501. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  502. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  503. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  504. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  505. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  506. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  507. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  508. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  509. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  510. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  511. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  512. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  513. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  514. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  515. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  516. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  517. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  518. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  519. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  520. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  521. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  522. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  523. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  524. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  525. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  526. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  527. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  528. package/templates/be/table/migration.sql.tpl +1 -0
  529. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  530. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  531. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  532. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  533. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  534. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  535. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  536. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  537. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  538. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  539. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  540. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  541. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  542. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  543. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  544. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  545. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  546. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  547. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  548. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  549. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  550. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  551. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  552. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  553. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  554. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  555. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  556. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  557. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  558. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  559. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  560. package/upgrade/check.mjs +3 -1
  561. package/upgrade/file-state.mjs +68 -0
  562. package/upgrade/index.mjs +58 -94
  563. package/runtime/scripts/api/git/log.mjs +0 -5
  564. package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +0 -55
@@ -0,0 +1,320 @@
1
+ // R280: declarations are not consumers. Error-kind maps, message catalogs, imports,
2
+ // re-exports and tests do not keep an otherwise unused value alive.
3
+ import path from 'node:path';
4
+ import { createRequire } from 'node:module';
5
+ import { found, readText } from './read.mjs';
6
+ import { nameText } from '../../lib/ts-ast.mjs';
7
+ import { unwrapCatalogExpression as unwrap } from '../../lib/ts-analysis.mjs';
8
+ import { argumentBindings, callable, jsonCatalogBindings, jsonCatalogRead, staticStrings } from './unused-symbol-values.mjs';
9
+ import { catalogNamespace, translationBindings } from './unused-symbol-translators.mjs';
10
+ import { applyDynamicKeysLedger } from './unused-symbol-ledger.mjs';
11
+ import { returnedExpressions, valuesOf } from './unused-symbol-flow.mjs';
12
+
13
+ export const HFS_UNUSED_SYMBOL = 'HFS_UNUSED_SYMBOL';
14
+ const SOURCE = /\.[cm]?[jt]sx?$/u;
15
+ const EXCLUDED = /(?:^|\/)(?:node_modules|__generated__|_generated|dist|tests?|__tests__|test-support)\/|(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$|\.d\.[cm]?ts$/u;
16
+ const CATALOG = /(?:\.messages|\.catalog)\.[cm]?[jt]s$/u;
17
+ const JSON_CATALOG = /(?:^|\/)(?:messages|locales)\/[^/]+\.json$|\.catalog\.json$/u;
18
+ const QUEUE_NAME = /(?:^QUEUE(?:_NAME)?$|_QUEUE(?:_NAME)?$)/u;
19
+ const ERROR_NAME = /(?:^ERROR_CODE$|_ERROR_CODE$)/u;
20
+ const slash = value => value.replaceAll('\\', '/');
21
+
22
+ function compilerAt(repoRoot, supplied) {
23
+ if (supplied) return supplied;
24
+ // TypeScript belongs to the checked product, just as in the architecture machine.
25
+ // Its missing-compiler refusal is reported there, rather than mislabeled as dead code.
26
+ try { return createRequire(path.join(repoRoot, 'package.json'))('typescript'); } catch { return null; }
27
+ }
28
+
29
+ function sourceFiles(ts, repoRoot, files) {
30
+ const sources = new Map();
31
+ for (const file of files) {
32
+ if (!SOURCE.test(file) || EXCLUDED.test(file) || !/(?:^|\/)src\//u.test(file)) continue;
33
+ const text = readText(repoRoot, file);
34
+ if (text !== null) sources.set(slash(path.resolve(repoRoot, file)), ts.createSourceFile(path.resolve(repoRoot, file), text, ts.ScriptTarget.Latest, true));
35
+ }
36
+ return sources;
37
+ }
38
+
39
+ function projectOptions(ts, repoRoot, files) {
40
+ const projects = [];
41
+ for (const file of files.filter(file => /(?:^|\/)tsconfig\.json$/u.test(file))) {
42
+ const text = readText(repoRoot, file);
43
+ if (text === null) continue;
44
+ const absolute = path.resolve(repoRoot, file);
45
+ const config = ts.parseConfigFileTextToJson(absolute, text);
46
+ if (config.error) continue;
47
+ const host = { ...ts.sys, readFile: target => {
48
+ const rel = slash(path.relative(repoRoot, target));
49
+ return rel.startsWith('../') ? undefined : readText(repoRoot, rel) ?? undefined;
50
+ } };
51
+ const parsed = ts.parseJsonConfigFileContent(config.config, host, path.dirname(absolute), {}, absolute);
52
+ projects.push({ directory: `${slash(path.dirname(absolute))}/`, options: parsed.options });
53
+ }
54
+ return projects.sort((a, b) => b.directory.length - a.directory.length);
55
+ }
56
+
57
+ function trackedProgram(ts, repoRoot, files, sources) {
58
+ const options = { noLib: true, types: [], allowJs: true, resolveJsonModule: true, jsx: ts.JsxEmit.Preserve, target: ts.ScriptTarget.Latest, module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler };
59
+ const projects = projectOptions(ts, repoRoot, files);
60
+ const catalogs = new Set(files.filter(file => JSON_CATALOG.test(file)).map(file => slash(path.resolve(repoRoot, file))));
61
+ const host = {
62
+ getSourceFile: file => sources.get(slash(path.resolve(file))),
63
+ getDefaultLibFileName: () => '', writeFile: () => {},
64
+ getCurrentDirectory: () => repoRoot, getDirectories: () => [],
65
+ fileExists: file => sources.has(slash(path.resolve(file))) || catalogs.has(slash(path.resolve(file))),
66
+ readFile: file => sources.get(slash(path.resolve(file)))?.text,
67
+ getCanonicalFileName: slash, useCaseSensitiveFileNames: () => true,
68
+ getNewLine: () => '\n',
69
+ };
70
+ const resolveModule = (name, containing) => {
71
+ const project = projects.find(project => slash(containing).startsWith(project.directory));
72
+ return ts.resolveModuleName(name, containing, { ...options, ...project?.options }, host).resolvedModule;
73
+ };
74
+ host.resolveModuleNames = (names, containing) => names.map(name => resolveModule(name, containing));
75
+ return { program: ts.createProgram([...sources.keys()], options, host), resolveModule };
76
+ }
77
+
78
+ const symbolOf = (ts, checker, node) => {
79
+ const shorthand = ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node;
80
+ const symbol = shorthand ? checker.getShorthandAssignmentValueSymbol(node.parent) : checker.getSymbolAtLocation(node);
81
+ return symbol?.flags === ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
82
+ };
83
+
84
+ function addCandidate(state, source, node, kind, name, value, symbol) {
85
+ const at = source.getLineAndCharacterOfPosition(node.getStart(source));
86
+ const candidate = { path: slash(path.relative(state.repoRoot, source.fileName)), line: at.line + 1, column: at.character + 1, kind, name, value, used: false };
87
+ state.candidates.push(candidate);
88
+ if (symbol) state.symbols.set(symbol, candidate);
89
+ if (symbol?.valueDeclaration) state.decls.set(symbol.valueDeclaration, candidate);
90
+ return candidate;
91
+ }
92
+
93
+ function enumCandidates(state, source, declaration) {
94
+ const members = [];
95
+ for (const member of declaration.members) {
96
+ const name = nameText(state.ts, member.name, { numbers: true });
97
+ if (name === null) continue;
98
+ const kind = declaration.name.text.endsWith('ErrorCode') ? 'error code' : 'enum member';
99
+ const value = state.checker.getConstantValue(member) ?? null;
100
+ members.push(addCandidate(state, source, member, kind, `${declaration.name.text}.${name}`, value, state.checker.getSymbolAtLocation(member.name)));
101
+ }
102
+ state.containers.set(state.checker.getSymbolAtLocation(declaration.name), members);
103
+ }
104
+
105
+ function catalogProperties(state, source, object, prefix = '') {
106
+ for (const property of object.properties) {
107
+ if (!state.ts.isPropertyAssignment(property)) continue;
108
+ const name = nameText(state.ts, property.name, { numbers: true });
109
+ if (name === null) continue;
110
+ const child = unwrap(state.ts, property.initializer);
111
+ if (state.ts.isObjectLiteralExpression(child) && !child.properties.some(item => ['vi', 'en'].includes(nameText(state.ts, item.name, { numbers: true })))) {
112
+ catalogProperties(state, source, child, ['vi', 'en'].includes(name) ? prefix : `${prefix}${name}.`);
113
+ } else {
114
+ const key = `${prefix}${name}`;
115
+ addCandidate(state, source, property, 'catalog key', key, key, state.checker.getSymbolAtLocation(property.name));
116
+ }
117
+ }
118
+ }
119
+
120
+ function variableCandidates(state, source, declaration) {
121
+ const { ts, checker } = state;
122
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) return;
123
+ const value = unwrap(ts, declaration.initializer);
124
+ const name = declaration.name.text;
125
+ if (CATALOG.test(source.fileName) && ts.isObjectLiteralExpression(value)) {
126
+ catalogProperties(state, source, value);
127
+ return;
128
+ }
129
+ if (ts.isStringLiteralLike(value) && (QUEUE_NAME.test(name) || ERROR_NAME.test(name))) {
130
+ addCandidate(state, source, declaration, QUEUE_NAME.test(name) ? 'queue constant' : 'error code', name, value.text, checker.getSymbolAtLocation(declaration.name));
131
+ } else if (/(?:ErrorCodes|ERROR_CODES)$/u.test(name) && ts.isObjectLiteralExpression(value)) {
132
+ const members = value.properties.filter(property => ts.isPropertyAssignment(property) && ts.isStringLiteralLike(unwrap(ts, property.initializer))).map(property =>
133
+ addCandidate(state, source, property, 'error code', `${name}.${nameText(ts, property.name, { numbers: true })}`, unwrap(ts, property.initializer).text, checker.getSymbolAtLocation(property.name)));
134
+ state.containers.set(checker.getSymbolAtLocation(declaration.name), members);
135
+ }
136
+ }
137
+
138
+ function collectCandidates(state, sources) {
139
+ for (const source of sources.values()) {
140
+ const visit = node => {
141
+ if (state.ts.isEnumDeclaration(node)) enumCandidates(state, source, node);
142
+ else if (state.ts.isVariableDeclaration(node)) variableCandidates(state, source, node);
143
+ state.ts.forEachChild(node, visit);
144
+ };
145
+ visit(source);
146
+ }
147
+ }
148
+
149
+ function metadataDeclaration(ts, node, source) {
150
+ if (!ts.isVariableDeclaration(node)) return false;
151
+ if (CATALOG.test(source.fileName)) return true;
152
+ const name = nameText(ts, node.name, { numbers: true }) ?? '';
153
+ return /(?:ERROR_KINDS|ERROR_MESSAGES|ERROR_CODES|errorKinds|errorMessages)$/u.test(name) && ts.isObjectLiteralExpression(unwrap(ts, node.initializer) ?? node);
154
+ }
155
+
156
+ function declarationName(ts, node) {
157
+ const parent = node.parent;
158
+ return parent?.name === node && (ts.isVariableDeclaration(parent) || ts.isParameter(parent) || ts.isBindingElement(parent) || ts.isPropertyAssignment(parent)
159
+ || ts.isPropertyDeclaration(parent) || ts.isMethodDeclaration(parent) || ts.isFunctionDeclaration(parent) || ts.isClassDeclaration(parent));
160
+ }
161
+
162
+ const matchesMember = (member, key) => key === null || member.name.endsWith(`.${key}`) || typeof member.value === 'number' && String(member.value) === key;
163
+ const markMembers = (members, key) => { for (const member of members) if (matchesMember(member, key)) member.used = true; };
164
+
165
+ function markContainerRead(ts, members, node) {
166
+ const receiver = ts.isPropertyAccessExpression(node.parent) && node.parent.name === node ? node.parent : node;
167
+ const parent = receiver.parent;
168
+ if (ts.isPropertyAccessExpression(parent) && parent.expression === receiver) {
169
+ markMembers(members, parent.name.text);
170
+ return;
171
+ }
172
+ if (ts.isElementAccessExpression(parent) && parent.expression === receiver) {
173
+ const key = nameText(ts, parent.argumentExpression, { identifiers: false, numbers: true });
174
+ markMembers(members, key);
175
+ return;
176
+ }
177
+ // An enum handed to a consumer or enumerated dynamically can read any member.
178
+ for (const member of members) member.used = true;
179
+ }
180
+
181
+ function markReference(state, node) {
182
+ const { ts, checker } = state;
183
+ if (declarationName(ts, node)) return;
184
+ const symbol = symbolOf(ts, checker, node);
185
+ // A property read through an imported object resolves to a transient widened symbol; its declaration is the candidate's.
186
+ const candidate = state.symbols.get(symbol) ?? state.decls.get(symbol?.valueDeclaration);
187
+ if (candidate) candidate.used = true;
188
+ const members = state.containers.get(symbol);
189
+ if (members) markContainerRead(ts, members, node);
190
+ }
191
+
192
+ function ignoredRead(state, node, source) {
193
+ const { ts } = state;
194
+ if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node) || ts.isEnumDeclaration(node) || ts.isTypeNode(node) || metadataDeclaration(ts, node, source)) return true;
195
+ return ts.isVariableDeclaration(node) && (state.symbols.has(state.checker.getSymbolAtLocation(node.name)) || state.containers.has(state.checker.getSymbolAtLocation(node.name)));
196
+ }
197
+
198
+ const readKey = (state, prefix, value) => (prefix === null ? state.looseReads : state.catalogReads).add(prefix === null ? value : `${prefix}${value}`);
199
+
200
+ const hasLiteral = template => Boolean(template.head.text) || template.templateSpans.some(span => span.literal.text);
201
+
202
+ /** The function a call runs, when it is a local or imported arrow or declaration. */
203
+ function calledFunction(state, call) {
204
+ const target = callable(state, call.expression);
205
+ return target && (state.ts.isArrowFunction(target) || state.ts.isFunctionExpression(target) || state.ts.isFunctionDeclaration(target)) ? target : null;
206
+ }
207
+
208
+ /** What an argument that is no literal can still spell: resolved values, wrapper arguments, and what a key builder returns. */
209
+ function indirectArguments(state, argument, prefix, seen) {
210
+ const { ts } = state;
211
+ for (const value of staticStrings(state, argument)) readKey(state, prefix, value);
212
+ for (const leaf of valuesOf(state, argument)) if (leaf !== argument) catalogArguments(state, leaf, prefix, seen);
213
+ const builder = ts.isCallExpression(argument) ? calledFunction(state, argument) : null;
214
+ for (const returned of builder ? returnedExpressions(ts, builder) : []) catalogArguments(state, returned, prefix, seen);
215
+ }
216
+
217
+ function templateArguments(state, template, prefix) {
218
+ // A literal head or tail makes the template a key pattern; a template of holes only proves nothing.
219
+ if (prefix !== null && hasLiteral(template)) state.templates.push([`${prefix}${template.head.text}`, ...template.templateSpans.map(span => span.literal.text)]);
220
+ }
221
+
222
+ function catalogArguments(state, node, prefix, seen = new Set()) {
223
+ const { ts } = state;
224
+ const argument = unwrap(ts, node);
225
+ if (!argument || seen.has(argument)) return;
226
+ seen.add(argument);
227
+ if (ts.isConditionalExpression(argument)) {
228
+ catalogArguments(state, argument.whenTrue, prefix, seen);
229
+ catalogArguments(state, argument.whenFalse, prefix, seen);
230
+ } else if (ts.isStringLiteralLike(argument)) readKey(state, prefix, argument.text);
231
+ else if (ts.isTemplateExpression(argument)) templateArguments(state, argument, prefix);
232
+ else indirectArguments(state, argument, prefix, seen);
233
+ }
234
+
235
+ function namespacePrefix(namespace) {
236
+ if (namespace === null) return null;
237
+ return namespace ? `${namespace}.` : '';
238
+ }
239
+
240
+ function collectLiteralReads(state, node) {
241
+ const { ts } = state;
242
+ if (ts.isStringLiteralLike(node) && !declarationName(ts, node)) state.reads.add(node.text);
243
+ jsonCatalogRead(state, node);
244
+ if (!ts.isCallExpression(node)) return;
245
+ for (const namespace of catalogNamespace(state, node.expression)) catalogArguments(state, node.arguments[0], namespacePrefix(namespace));
246
+ }
247
+
248
+ function sourceReads(state, sources) {
249
+ for (const source of sources.values()) {
250
+ const visit = node => {
251
+ const { ts } = state;
252
+ if (ignoredRead(state, node, source)) return;
253
+ if (ts.isIdentifier(node)) markReference(state, node);
254
+ collectLiteralReads(state, node);
255
+ ts.forEachChild(node, visit);
256
+ };
257
+ visit(source);
258
+ }
259
+ }
260
+
261
+ function jsonCandidates(state, files) {
262
+ for (const file of files.filter(file => JSON_CATALOG.test(file) && !EXCLUDED.test(file))) {
263
+ let json;
264
+ try { json = JSON.parse(readText(state.repoRoot, file)); } catch { continue; }
265
+ const leaves = (value, prefix = '') => {
266
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
267
+ for (const [key, child] of Object.entries(value)) leaves(child, prefix ? `${prefix}.${key}` : key);
268
+ } else if (prefix) state.candidates.push({ path: file, line: 1, column: 1, kind: 'catalog key', name: prefix, value: prefix, used: false });
269
+ };
270
+ leaves(json);
271
+ }
272
+ }
273
+
274
+ /** R106's rule for a translator handed in without its namespace: a key is read when a string equals any tail of its dotted path. */
275
+ function looseRead(state, name) {
276
+ const segments = name.split('.');
277
+ return segments.some((_, index) => state.looseReads.has(segments.slice(index).join('.')));
278
+ }
279
+
280
+ function catalogRead(state, candidate, liveCodes) {
281
+ if (candidate.name.startsWith('errors.') && liveCodes.has(candidate.name.slice(7))) return true;
282
+ if (state.catalogReads.has(candidate.name) || looseRead(state, candidate.name)) return true;
283
+ return state.templates.some(parts => parts.some(Boolean) && (() => {
284
+ const tail = candidate.name;
285
+ let offset = 0;
286
+ for (const [index, part] of parts.entries()) {
287
+ const next = tail.indexOf(part, offset);
288
+ if (next < 0 || index === 0 && next !== 0) return false;
289
+ offset = next + part.length;
290
+ }
291
+ return tail.endsWith(parts.at(-1));
292
+ })());
293
+ }
294
+
295
+ /** The same rule applies to lite and full. Analyze all tracked consumers before filtering findings to a fast-check scope. */
296
+ export function unusedSymbolFindings({ repoRoot, files, repo = {}, isRoot = false, inScope = () => true, ts: supplied }) {
297
+ const edition = repo.edition ?? 'full';
298
+ if (isRoot || repo.kind === 'runtime' || !['lite', 'full'].includes(edition)) return [];
299
+ const ts = compilerAt(repoRoot, supplied);
300
+ if (!ts) return [];
301
+ const sources = sourceFiles(ts, repoRoot, files);
302
+ const { program, resolveModule } = trackedProgram(ts, repoRoot, files, sources);
303
+ const state = { ts, checker: program.getTypeChecker(), resolveModule, repoRoot, candidates: [], symbols: new Map(), containers: new Map(), reads: new Set(), catalogReads: new Set(), translators: new Map(), templates: [], arguments: new Map(), catalogObjects: new Map(), decls: new Map(), looseReads: new Set() };
304
+ collectCandidates(state, sources);
305
+ argumentBindings(state, sources);
306
+ jsonCatalogBindings(state, sources, files);
307
+ translationBindings(state, sources);
308
+ sourceReads(state, sources);
309
+ jsonCandidates(state, files);
310
+ const ledgerFindings = applyDynamicKeysLedger(state, repo.profile);
311
+ for (const candidate of state.candidates) if (candidate.kind === 'error code' && state.reads.has(candidate.value)) candidate.used = true;
312
+ const liveCodes = new Set(state.candidates.filter(candidate => candidate.kind === 'error code' && candidate.used).map(candidate => candidate.value));
313
+ const findings = new Map();
314
+ for (const candidate of state.candidates) {
315
+ if (candidate.used || candidate.kind === 'catalog key' && catalogRead(state, candidate, liveCodes) || !inScope(candidate.path)) continue;
316
+ const { used: _used, ...extra } = candidate;
317
+ findings.set(`${candidate.path}:${candidate.name}`, found(HFS_UNUSED_SYMBOL, candidate.path, `${candidate.path} declares unused ${candidate.kind} ${candidate.name}; remove it or read it in production source (declarations, metadata maps and tests do not count).`, extra));
318
+ }
319
+ return [...findings.values(), ...ledgerFindings.filter(finding => inScope(finding.path))].sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.name.localeCompare(b.name));
320
+ }
@@ -0,0 +1,185 @@
1
+ // source-ast.mjs - the syntax-tree reading the runtime rules share (scripts/hfs/runtime-check.mjs): one TypeScript parse
2
+ // per runtime source file, never a text grep. Pure: every function takes a source text and returns facts about it.
3
+ // parseSource(text, file) the TypeScript SourceFile (JS script kind for .mjs/.cjs/.js)
4
+ // moduleRefs(source) every import/export-from/require()/import() of a string specifier: [{module, line}]
5
+ // relativeImports(source) the relative specifiers among them: [{specifier, line, column}]
6
+ // localBindings(source) every name the file declares (variables, functions, classes, parameters, imports)
7
+ // exportedNames(source) the names the module exports (export function/const/class, export {a as b}, default)
8
+ import { createRequire } from 'node:module';
9
+ import path from 'node:path';
10
+
11
+ let typescript = null;
12
+ /** The TypeScript compiler, loaded once from the runtime's own dependencies. */
13
+ export const ts = () => (typescript ??= createRequire(import.meta.url)('typescript'));
14
+
15
+ const FS_MODULES = new Set(['fs', 'node:fs', 'fs/promises', 'node:fs/promises']);
16
+
17
+ /** `node` is a string literal naming an fs module specifier ('fs', 'node:fs', 'fs/promises', 'node:fs/promises'). */
18
+ const fsSpecifier = (t, node) => Boolean(node && t.isStringLiteralLike(node) && FS_MODULES.has(node.text));
19
+
20
+ /** `node` is a `require('fs' | 'node:fs' | 'fs/promises' | 'node:fs/promises')` call. */
21
+ const fsRequireCall = (t, node) => node && t.isCallExpression(node) && t.isIdentifier(node.expression) && node.expression.text === 'require' && fsSpecifier(t, node.arguments[0]);
22
+
23
+ function bindImport(t, node, wanted, namespaces, members) {
24
+ if (!t.isImportDeclaration(node) || !fsSpecifier(t, node.moduleSpecifier)) return;
25
+ const clause = node.importClause;
26
+ if (clause?.name) namespaces.add(clause.name.text);
27
+ const bindings = clause?.namedBindings;
28
+ if (bindings && t.isNamespaceImport(bindings)) namespaces.add(bindings.name.text);
29
+ if (bindings && t.isNamedImports(bindings)) for (const element of bindings.elements) {
30
+ const imported = (element.propertyName ?? element.name).text;
31
+ if (imported === 'promises') namespaces.add(element.name.text);
32
+ else if (wanted(imported)) members.set(element.name.text, imported);
33
+ }
34
+ }
35
+
36
+ function requiredName(t, element) {
37
+ if (element.propertyName && t.isIdentifier(element.propertyName)) return element.propertyName.text;
38
+ if (t.isIdentifier(element.name)) return element.name.text;
39
+ return null;
40
+ }
41
+
42
+ function bindRequire(t, node, wanted, namespaces, members, destructuredRequires) {
43
+ if (!t.isVariableDeclaration(node) || !node.initializer || !fsRequireCall(t, node.initializer)) return;
44
+ if (t.isIdentifier(node.name)) namespaces.add(node.name.text);
45
+ else if (destructuredRequires && t.isObjectBindingPattern(node.name)) for (const element of node.name.elements) {
46
+ const imported = requiredName(t, element);
47
+ if (imported && wanted(imported) && t.isIdentifier(element.name)) members.set(element.name.text, imported);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * The fs bindings a parsed source declares: `{ namespaces, members }`. `namespaces` holds the local names bound to
53
+ * a whole fs module - a default or namespace import, a `promises` named import, `const fs = require('fs')`.
54
+ * `members` is a Map(local name -> imported name) of named imports `wanted` accepts, plus the names of a
55
+ * `const {x} = require('fs')` destructure unless `destructuredRequires` is false.
56
+ */
57
+ export function fsBindings(source, wanted, { destructuredRequires = true } = {}) {
58
+ const t = ts();
59
+ const namespaces = new Set();
60
+ const members = new Map();
61
+ const bind = (node) => {
62
+ bindImport(t, node, wanted, namespaces, members);
63
+ bindRequire(t, node, wanted, namespaces, members, destructuredRequires);
64
+ t.forEachChild(node, bind);
65
+ };
66
+ bind(source);
67
+ return { namespaces, members };
68
+ }
69
+
70
+ /** The fs member `callee` (a property access: `fs.X`, `fs.promises.X`) reaches through `namespaces`, or null. */
71
+ export const fsMemberAccess = (t, callee, namespaces, wanted) => {
72
+ if (!t.isPropertyAccessExpression(callee) || !wanted(callee.name.text)) return null;
73
+ const target = callee.expression;
74
+ if (t.isIdentifier(target) && namespaces.has(target.text)) return callee.name.text;
75
+ if (t.isPropertyAccessExpression(target) && target.name.text === 'promises' && t.isIdentifier(target.expression) && namespaces.has(target.expression.text)) return callee.name.text;
76
+ return null;
77
+ };
78
+
79
+ /** The SourceFile of `text`; `.ts` files parse as TypeScript, everything else as JavaScript. */
80
+ export function parseSource(text, file = 'x.mjs') {
81
+ const t = ts();
82
+ return t.createSourceFile(file, String(text), t.ScriptTarget.Latest, true, file.endsWith('.ts') ? t.ScriptKind.TS : t.ScriptKind.JS);
83
+ }
84
+
85
+ /** The 1-based line of `node` in `source`. */
86
+ export const lineOf = (source, node) => source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
87
+
88
+ /** Every module reference of a parsed file: static imports, re-exports, require('x') and import('x') with a string specifier. */
89
+ export function moduleRefs(source) {
90
+ const t = ts();
91
+ const out = [];
92
+ const visit = (node) => {
93
+ if ((t.isImportDeclaration(node) || t.isExportDeclaration(node)) && node.moduleSpecifier && t.isStringLiteralLike(node.moduleSpecifier)) {
94
+ out.push({ module: node.moduleSpecifier.text, line: lineOf(source, node), column: source.getLineAndCharacterOfPosition(node.getStart(source)).character + 1 });
95
+ } else if (t.isCallExpression(node) && node.arguments.length && t.isStringLiteralLike(node.arguments[0])) {
96
+ const callee = node.expression;
97
+ if ((t.isIdentifier(callee) && callee.text === 'require') || callee.kind === t.SyntaxKind.ImportKeyword) {
98
+ out.push({ module: node.arguments[0].text, line: lineOf(source, node), column: source.getLineAndCharacterOfPosition(node.getStart(source)).character + 1 });
99
+ }
100
+ }
101
+ t.forEachChild(node, visit);
102
+ };
103
+ visit(source);
104
+ return out;
105
+ }
106
+
107
+ /** The relative module references (`./x.mjs`, `../y.mjs`) of a parsed file. */
108
+ const relativeImports = (source) => moduleRefs(source).filter((ref) => ref.module.startsWith('.')).map((ref) => ({ specifier: ref.module, line: ref.line, column: ref.column }));
109
+
110
+ /**
111
+ * Exact relative runtime module targets from the shared AST. Tracked targets participate in the owner graph;
112
+ * an existing ignored generated target remains outside it. An absent target cannot silently remove an edge.
113
+ */
114
+ export function relativeImportTargets(ctx, file) {
115
+ return relativeImports(ctx.parsed(file)).map((ref) => {
116
+ const to = path.posix.normalize(path.posix.join(path.posix.dirname(file), ref.specifier));
117
+ const tracked = ctx.fileSet.has(to);
118
+ const missing = ctx.read(to) == null;
119
+ return { ...ref, to, tracked, missing };
120
+ });
121
+ }
122
+
123
+ /** Adds the names `name` binds (an identifier, or the elements of a destructuring pattern) to `names`. */
124
+ function addBoundNames(t, name, names) {
125
+ if (!name) return;
126
+ if (t.isIdentifier(name)) names.add(name.text);
127
+ else if (t.isObjectBindingPattern(name) || t.isArrayBindingPattern(name)) for (const el of name.elements) if (!t.isOmittedExpression(el)) addBoundNames(t, el.name, names);
128
+ }
129
+
130
+ const isNamedDeclaration = (t, node) => (t.isFunctionDeclaration(node) || t.isClassDeclaration(node) || t.isFunctionExpression(node) || t.isClassExpression(node)) && node.name;
131
+
132
+ /** Adds the local name `node` binds, when it binds one, to `names`. */
133
+ function addNodeBindings(t, node, names) {
134
+ if (t.isVariableDeclaration(node) || t.isParameter(node)) addBoundNames(t, node.name, names);
135
+ else if (isNamedDeclaration(t, node)) names.add(node.name.text);
136
+ else if (t.isImportClause(node) && node.name) names.add(node.name.text);
137
+ else if (t.isNamespaceImport(node) || t.isImportSpecifier(node)) names.add(node.name.text);
138
+ }
139
+
140
+ /** Every local name a parsed file binds: variable and parameter names (destructuring included), functions, classes, imports. */
141
+ export function localBindings(source) {
142
+ const t = ts();
143
+ const names = new Set();
144
+ const visit = (node) => {
145
+ addNodeBindings(t, node, names);
146
+ t.forEachChild(node, visit);
147
+ };
148
+ visit(source);
149
+ return names;
150
+ }
151
+
152
+ function variableExports(t, node, source) {
153
+ const out = [];
154
+ for (const declaration of node.declarationList.declarations) {
155
+ if (!t.isIdentifier(declaration.name)) { out.push({ name: '<pattern>', fn: false, line: lineOf(source, declaration) }); continue; }
156
+ const initializer = declaration.initializer;
157
+ out.push({ name: declaration.name.text, fn: Boolean(initializer && (t.isArrowFunction(initializer) || t.isFunctionExpression(initializer))), line: lineOf(source, declaration) });
158
+ }
159
+ return out;
160
+ }
161
+
162
+ function exportClauseNames(t, node, source) {
163
+ if (!node.exportClause) return [{ name: '*', fn: false, line: lineOf(source, node) }];
164
+ if (t.isNamedExports(node.exportClause)) return node.exportClause.elements.map((element) => ({ name: element.name.text, fn: false, line: lineOf(source, element), reexport: true }));
165
+ return [{ name: node.exportClause.name.text, fn: false, line: lineOf(source, node) }];
166
+ }
167
+
168
+ function statementExports(t, node, source, exported, isDefault) {
169
+ if (t.isFunctionDeclaration(node) && exported(node)) return [{ name: isDefault(node) ? 'default' : node.name?.text ?? 'default', fn: true, line: lineOf(source, node) }];
170
+ if (t.isClassDeclaration(node) && exported(node)) return [{ name: isDefault(node) ? 'default' : node.name?.text ?? 'default', fn: false, line: lineOf(source, node) }];
171
+ if (t.isVariableStatement(node) && exported(node)) return variableExports(t, node, source);
172
+ if (t.isExportDeclaration(node)) return exportClauseNames(t, node, source);
173
+ if (t.isExportAssignment(node)) return [{ name: 'default', fn: false, line: lineOf(source, node) }];
174
+ return [];
175
+ }
176
+
177
+ /** The names a parsed module exports, each with whether it is a function (declaration or an arrow/function initializer). */
178
+ export function exportedNames(source) {
179
+ const t = ts();
180
+ const out = [];
181
+ const exported = (node) => (t.canHaveModifiers?.(node) ? t.getModifiers(node) : node.modifiers)?.some((m) => m.kind === t.SyntaxKind.ExportKeyword);
182
+ const isDefault = (node) => (t.canHaveModifiers?.(node) ? t.getModifiers(node) : node.modifiers)?.some((m) => m.kind === t.SyntaxKind.DefaultKeyword);
183
+ for (const node of source.statements) out.push(...statementExports(t, node, source, exported, isDefault));
184
+ return out;
185
+ }
@@ -0,0 +1,19 @@
1
+ // workflow-source.mjs - what the workflow rules (ci-upload.mjs, git-triggers.mjs) share: which tracked files are CI workflows, and a workflow's
2
+ // YAML document. A template's `{{placeholder}}` lines are blanked before the text is parsed.
3
+ import { parseYaml } from '../../../engine/yaml.mjs';
4
+
5
+ const WORKFLOW = /(?:^|\/)\.?(?:github\/)?workflows\/[^/]+\.ya?ml$/;
6
+
7
+ /** The tracked workflow files of the runtime, its examples and the hfs templates (never a spec fixture under tests/). */
8
+ const workflowFiles = (files) => files.filter((file) => WORKFLOW.test(file) && !file.startsWith('tests/'));
9
+
10
+ /** The parsed workflow, or null when the text is not YAML (the other checks name that). */
11
+ export function parseWorkflow(text) {
12
+ try { return parseYaml(text.replace(/^\{\{\w+\}\}\s*$/gm, '').replace(/\{\{\w+\}\}/g, 'x')); } catch { return null; }
13
+ }
14
+
15
+ /** The findings of `perFile({path, text})` over every tracked workflow `ctx` holds (ctx of scripts/hfs/runtime-check.mjs). */
16
+ export const workflowFindings = (ctx, perFile) => workflowFiles(ctx.files).flatMap((file) => {
17
+ const text = ctx.read(file);
18
+ return text === null || text === undefined ? [] : perFile({ path: file, text });
19
+ });
@@ -0,0 +1,101 @@
1
+ // Names-only declarations become genuine sealed placeholders when an app or example is materialized.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { parseYaml } from '../../engine/yaml.mjs';
6
+ import { byCodeUnit } from '../lib/list.mjs';
7
+ import { setSecretValues } from './secret-store.mjs';
8
+ import { sealBootstrapValues } from './secret-bootstrap-crypto.mjs';
9
+
10
+ const ENVIRONMENT = /^[a-z0-9][a-z0-9-]*$/u;
11
+ const MEMBER = /^secrets\/([a-z0-9][a-z0-9-]*)(?:\.enc)?$/u;
12
+ const KEY = /^[A-Z][A-Z0-9_]*$/u;
13
+
14
+ function readDocument(file, format) {
15
+ if (!fs.existsSync(file)) return {};
16
+ const text = fs.readFileSync(file, 'utf8');
17
+ return format === 'json' ? JSON.parse(text) : parseYaml(text);
18
+ }
19
+
20
+ function memberOf(environment, declaration) {
21
+ const entry = typeof declaration === 'string' ? { where: declaration } : declaration;
22
+ const location = entry?.where ?? (entry?.name && `secrets/${entry.name}`);
23
+ const match = MEMBER.exec(location ?? '');
24
+ if (!ENVIRONMENT.test(environment) || !match)
25
+ throw new Error('sealed member declarations must use .starcistacks/<environment>/secrets/<slug>');
26
+ return { env: environment, slug: match[1], keys: entry.keys };
27
+ }
28
+
29
+ function rosterKeys(repoRoot, member) {
30
+ const file = path.join(repoRoot, '.starcistacks', member.env, 'runtime', 'env', 'KEYS.md');
31
+ if (!fs.existsSync(file)) return [];
32
+ const keys = [];
33
+ for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/u)) {
34
+ const key = /^\|\s*`([A-Z][A-Z0-9_]*)`\s*\|/u.exec(line)?.[1];
35
+ if (!key) continue;
36
+ const locations = [...line.matchAll(/(?:secrets\/|\/run\/secrets\/)([a-z0-9][a-z0-9-]*)(?:\.enc)?/gu)];
37
+ if (locations.some((match) => match[1] === member.slug)) keys.push(key);
38
+ }
39
+ return keys;
40
+ }
41
+
42
+ function keysOf(repoRoot, member) {
43
+ const declared = Array.isArray(member.keys) ? member.keys : rosterKeys(repoRoot, member);
44
+ const keys = declared.length ? declared : [member.slug.replaceAll('-', '_').toUpperCase()];
45
+ if (keys.some((key) => !KEY.test(key))) throw new Error('sealed member keys must be environment key names');
46
+ return [...new Set(keys)].sort(byCodeUnit);
47
+ }
48
+
49
+ function descriptorMembers(repoRoot) {
50
+ const stacks = path.join(repoRoot, '.starcistacks');
51
+ if (!fs.existsSync(stacks)) return [];
52
+ const members = [];
53
+ for (const entry of fs.readdirSync(stacks, { withFileTypes: true })) {
54
+ if (!entry.isDirectory() || !ENVIRONMENT.test(entry.name)) continue;
55
+ const descriptor = readDocument(path.join(stacks, entry.name, 'environment.json'), 'json');
56
+ for (const declaration of descriptor.secrets ?? []) members.push(memberOf(entry.name, declaration));
57
+ }
58
+ return members;
59
+ }
60
+
61
+ /** Resolve public member/key names without opening any existing envelope or private identity. */
62
+ export function declaredSealedMembers(repoRoot) {
63
+ const manifest = readDocument(path.join(repoRoot, '.starcistacks', 'application-stacks.yaml'), 'yaml');
64
+ const declarations = descriptorMembers(repoRoot);
65
+ for (const [environment, model] of Object.entries(manifest?.environments ?? {})) {
66
+ for (const declaration of model?.secrets ?? []) declarations.push(memberOf(environment, declaration));
67
+ }
68
+ const members = new Map();
69
+ for (const member of declarations) {
70
+ const id = `${member.env}/${member.slug}`;
71
+ members.set(id, { ...member, keys: keysOf(repoRoot, member) });
72
+ }
73
+ return [...members.values()].sort((left, right) => `${left.env}/${left.slug}`.localeCompare(`${right.env}/${right.slug}`));
74
+ }
75
+
76
+ function placeholderValue(key) {
77
+ const generated = randomUUID();
78
+ if (/DB_URL$/u.test(key)) return `postgresql://app_be:${generated}@db.invalid:5432/app`;
79
+ if (key.endsWith('_URL')) return `https://example.invalid/${generated}`;
80
+ return generated;
81
+ }
82
+
83
+ /** Create missing members for this app's recipient, preserving owner-provisioned members. */
84
+ export function materializeDeclaredPlaceholders(repoRoot, { recipient, recipientsForFile, processEnv = process.env, sops, write } = {}) {
85
+ const members = [];
86
+ for (const member of declaredSealedMembers(repoRoot)) {
87
+ const relative = `.starcistacks/${member.env}/secrets/${member.slug}.enc`;
88
+ if (fs.existsSync(path.join(repoRoot, relative))) continue;
89
+ const age = recipientsForFile?.(path.join(repoRoot, relative)) ?? [recipient];
90
+ if (!age.length || age.some((value) => !/^age1[a-z0-9]+$/u.test(value ?? '')))
91
+ throw new Error('sealed placeholders require the app public age recipient');
92
+ if (!sops && age.length !== 1) throw new Error('multiple recipients require the standard SOPS adapter');
93
+ const values = Object.fromEntries(member.keys.map((key) => [key, placeholderValue(key)]));
94
+ const adapter = sops ?? { seal: () => sealBootstrapValues({ values, recipient: age[0] }) };
95
+ setSecretValues(repoRoot, { slug: member.slug, values, env: member.env }, {
96
+ age, processEnv, sops: adapter, write,
97
+ });
98
+ members.push(relative);
99
+ }
100
+ return { members };
101
+ }