@starci/hfs 4.3.0 → 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 (515) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/db/migrate.mjs +361 -0
  3. package/db/post-cutover.mjs +136 -0
  4. package/emit/db-types.mjs +10 -1
  5. package/lint/run.mjs +1 -1
  6. package/package.json +4 -1
  7. package/release/gate.mjs +298 -0
  8. package/release/index.mjs +103 -0
  9. package/release/push.mjs +110 -0
  10. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  11. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  12. package/runtime/knowledge/hfs/rules.yaml +3401 -152
  13. package/runtime/knowledge/hfs/slots.yaml +344 -62
  14. package/runtime/knowledge/patterns/be/api.yaml +32 -5
  15. package/runtime/knowledge/patterns/be/cli.yaml +14 -4
  16. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  17. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  18. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  19. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  20. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  21. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  22. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  23. package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
  24. package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
  25. package/runtime/knowledge/sonar-gate.yaml +1 -1
  26. package/runtime/modules/kernel/failure-codes.yaml +3113 -43
  27. package/runtime/modules/models/runtimes.yaml +1 -1
  28. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  29. package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
  30. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  31. package/runtime/scripts/api/git/add.mjs +5 -0
  32. package/runtime/scripts/api/git/init.mjs +7 -0
  33. package/runtime/scripts/api/git/lib.mjs +3 -0
  34. package/runtime/scripts/api/git/tag.mjs +5 -0
  35. package/runtime/scripts/api/node/lib.mjs +20 -0
  36. package/runtime/scripts/api/node/run-node.mjs +6 -0
  37. package/runtime/scripts/api/sops/lib.mjs +41 -0
  38. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  39. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  40. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  41. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  42. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  43. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  44. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
  45. package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  47. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  48. package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
  49. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  50. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  51. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  52. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  53. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
  54. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  55. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  56. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  57. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  58. package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
  59. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  60. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
  61. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
  62. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  63. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/tiers.mjs +3 -3
  65. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
  66. package/runtime/scripts/hfs/check-results.mjs +28 -0
  67. package/runtime/scripts/hfs/check.mjs +46 -47
  68. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  69. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  70. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  71. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  72. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  73. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  74. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  75. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  76. package/runtime/scripts/hfs/manifest-shape.mjs +67 -7
  77. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  78. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  79. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  80. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  81. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  82. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  83. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  84. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  85. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  86. package/runtime/scripts/hfs/rules/contract.mjs +66 -5
  87. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  88. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  89. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  90. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  91. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  92. package/runtime/scripts/hfs/rules/database-migrations.mjs +286 -7
  93. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  94. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  95. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  96. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  97. package/runtime/scripts/hfs/rules/database.mjs +9 -0
  98. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  99. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  100. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  101. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  102. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  103. package/runtime/scripts/hfs/rules/edition.mjs +9 -8
  104. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  105. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  106. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  107. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  108. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  109. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  110. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  111. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  112. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  113. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  114. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  115. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  116. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  117. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  118. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  119. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  120. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  121. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  122. package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
  123. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  124. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  125. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  126. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  127. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  128. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  129. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
  130. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  131. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  132. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  133. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  134. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  135. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  136. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  137. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  138. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  139. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  140. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  141. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  142. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  143. package/runtime/scripts/hfs/secret-init.mjs +127 -19
  144. package/runtime/scripts/hfs/secret-sops.mjs +38 -6
  145. package/runtime/scripts/hfs/secret-store.mjs +56 -54
  146. package/runtime/scripts/hfs/secret.mjs +34 -5
  147. package/runtime/scripts/hfs/slot-imports.mjs +18 -5
  148. package/runtime/scripts/hfs/slots.mjs +1 -0
  149. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  150. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  151. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  152. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  153. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  154. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  155. package/runtime/scripts/lib/compose-values.mjs +11 -0
  156. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  157. package/runtime/scripts/lib/json-clone.mjs +61 -0
  158. package/runtime/scripts/lib/list.mjs +4 -0
  159. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  160. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  161. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  162. package/runtime/scripts/lib/stack-services.mjs +1 -1
  163. package/runtime/scripts/lib/swarm-stack.mjs +5 -18
  164. package/runtime/scripts/lib/trim-end.mjs +8 -0
  165. package/runtime/scripts/lib/ts-analysis.mjs +5 -2
  166. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  167. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  168. package/scaffold/add-app.mjs +77 -59
  169. package/scaffold/add-cli-lite.mjs +14 -6
  170. package/scaffold/add-compose.mjs +79 -0
  171. package/scaffold/add-database-lite.mjs +14 -0
  172. package/scaffold/add-fe-overlay.mjs +122 -0
  173. package/scaffold/add-fe-page.mjs +269 -0
  174. package/scaffold/add-inbox-lite.mjs +74 -42
  175. package/scaffold/add-table.mjs +110 -87
  176. package/scaffold/add.mjs +159 -44
  177. package/scaffold/app.mjs +124 -39
  178. package/scaffold/edition-gate.mjs +3 -3
  179. package/scaffold/lite-exports.mjs +2 -1
  180. package/scaffold/service.mjs +2 -2
  181. package/src/main.mjs +229 -116
  182. package/stack/age-install.mjs +37 -0
  183. package/stack/backup.mjs +100 -0
  184. package/stack/build-args.mjs +42 -0
  185. package/stack/build.mjs +173 -0
  186. package/stack/cli.mjs +130 -0
  187. package/stack/config.mjs +57 -0
  188. package/stack/context.mjs +57 -0
  189. package/stack/converge.mjs +107 -0
  190. package/stack/deploy.mjs +301 -0
  191. package/stack/digest.mjs +16 -0
  192. package/stack/docker-transport.mjs +53 -0
  193. package/stack/edge-alias.mjs +118 -0
  194. package/stack/errors.mjs +21 -0
  195. package/stack/image-scan.mjs +152 -0
  196. package/stack/ingress-create.mjs +114 -0
  197. package/stack/ingress-publish.mjs +273 -0
  198. package/stack/ingress.mjs +706 -0
  199. package/stack/jobs.mjs +51 -0
  200. package/stack/live-restore.mjs +177 -0
  201. package/stack/prepare.mjs +266 -0
  202. package/stack/private-file.mjs +22 -0
  203. package/stack/provision.mjs +90 -0
  204. package/stack/prune.mjs +46 -0
  205. package/stack/release-store.mjs +87 -0
  206. package/stack/remote.mjs +205 -0
  207. package/stack/render.mjs +149 -0
  208. package/stack/retire.mjs +47 -0
  209. package/stack/sealed.mjs +149 -0
  210. package/stack/task.mjs +42 -0
  211. package/stack/verify.mjs +99 -0
  212. package/sync/ci-commands.mjs +13 -0
  213. package/sync/index.mjs +108 -52
  214. package/sync/stack-images.mjs +127 -0
  215. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  216. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  217. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  218. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  219. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  220. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  221. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  222. package/templates/app/codegen-graphql.mjs.partial +13 -2
  223. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  224. package/templates/app/codegen-prelude.mjs.partial +8 -0
  225. package/templates/app/gitignore +3 -0
  226. package/templates/app/gitignore-lite +3 -0
  227. package/templates/app/package-scripts/package.json +1 -0
  228. package/templates/app/package-scripts/supabase.json +7 -0
  229. package/templates/app/package-scripts-lite/package.json +1 -7
  230. package/templates/app/skeleton/.editorconfig +1 -0
  231. package/templates/app/skeleton/.gitattributes +7 -1
  232. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  233. package/templates/app/skeleton/.nvmrc +1 -1
  234. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  235. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  236. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  237. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  238. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  239. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  240. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  241. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  242. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  243. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  244. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  245. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  246. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  247. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  248. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  249. package/templates/app/skeleton/README.md +6 -6
  250. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  251. package/templates/app/skeleton-lite/.editorconfig +16 -0
  252. package/templates/app/skeleton-lite/.gitattributes +8 -0
  253. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  254. package/templates/app/skeleton-lite/.nvmrc +1 -0
  255. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  256. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
  257. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  258. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  259. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  260. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  261. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  262. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  263. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  264. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  265. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  266. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  267. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  268. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  269. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  270. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  271. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  272. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  273. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  274. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  275. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  276. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  277. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  278. package/templates/app/skeleton-lite/README.md +5 -3
  279. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  280. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  281. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  282. package/templates/be/image/worker/Dockerfile +1 -1
  283. package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
  284. package/templates/be/inbox/database-client.ts.tpl +12 -11
  285. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  286. package/templates/be/inbox/event-index.ts.tpl +1 -0
  287. package/templates/be/inbox/events.json.tpl +10 -0
  288. package/templates/be/inbox/migration.sql.tpl +1 -0
  289. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  290. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  291. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  292. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  293. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  294. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  295. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  296. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  297. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  298. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  299. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  300. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  301. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  302. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  303. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  304. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  305. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  306. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  307. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  308. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  309. package/templates/be/inbox/service.spec.ts.tpl +42 -9
  310. package/templates/be/inbox/service.ts.tpl +23 -8
  311. package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
  312. package/templates/be/inbox/webhook-door.ts.tpl +2 -2
  313. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  314. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  315. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  316. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  317. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  318. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  319. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  320. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  321. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  323. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  324. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  325. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  326. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  327. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  328. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  329. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  330. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  331. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  332. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  333. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  334. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  335. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  336. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  337. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  338. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  339. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  340. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  341. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  342. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  343. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  344. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  345. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  346. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  347. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  348. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  349. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  350. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  351. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  352. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  353. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  354. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  355. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  356. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  357. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  358. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  359. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  360. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  362. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  363. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  364. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  365. package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
  366. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  367. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  368. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
  369. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  370. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  371. package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
  372. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  373. package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
  374. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  375. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  376. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  377. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  378. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  379. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  380. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  381. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  382. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  383. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  384. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  385. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  386. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  387. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  388. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  389. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  390. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  391. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  392. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  393. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  394. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  395. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  396. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  397. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  398. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  399. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  400. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  401. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  402. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  403. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  404. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  405. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  406. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  407. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  408. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  409. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  410. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  411. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  412. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  413. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  414. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  415. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  416. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  417. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  418. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  419. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  420. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  421. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  422. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  423. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  424. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  425. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  426. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  427. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  428. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  429. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  430. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  431. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  432. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  433. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  434. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  435. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  436. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  437. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  438. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  439. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  440. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  441. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  442. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  443. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  444. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  445. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  446. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
  447. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  448. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  449. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  450. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
  451. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  452. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  453. package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
  454. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  455. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  456. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  457. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  458. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  459. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  460. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  461. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  462. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  463. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  464. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  465. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  466. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  467. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  468. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  469. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  470. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  471. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  472. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  473. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  474. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  475. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  476. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  477. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  478. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  479. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  480. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  481. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  482. package/templates/be/table/migration.sql.tpl +1 -0
  483. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  484. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  485. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  486. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  487. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  488. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  489. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  490. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  491. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  492. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  493. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  494. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  495. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  496. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  497. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  498. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  499. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  500. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  501. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  502. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  503. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  504. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  505. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  506. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  507. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  508. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  509. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  510. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  511. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  512. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  513. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  514. package/upgrade/check.mjs +3 -1
  515. package/upgrade/index.mjs +53 -40
@@ -0,0 +1,286 @@
1
+ // R270: product documentation cites paths, commands, environment keys and rules that still exist.
2
+ // Paths in links are document-relative; code/prose paths may be repository-relative. Only tracked
3
+ // names are inspected: no environment file, sealed member or secret value is ever read.
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { CATALOG } from '../../../packages/cli/src/catalog.generated.mjs';
7
+ import { checkCommand, extractCommands } from '../doc-commands.mjs';
8
+ import { globExpression } from '../../lib/glob.mjs';
9
+ import { loadTypescript } from '../../lib/package-at.mjs';
10
+ import { loadRuleCatalog } from '../rule-catalog.mjs';
11
+ import { found, readJson, readText } from './read.mjs';
12
+
13
+ const PATH_MISSING = 'HFS_DOC_PATH_MISSING';
14
+ const COMMAND_UNKNOWN = 'HFS_DOC_COMMAND_UNKNOWN';
15
+ const ENV_UNKNOWN = 'HFS_DOC_ENV_UNKNOWN';
16
+ const RULE_UNKNOWN = 'HFS_DOC_RULE_UNKNOWN';
17
+ const DOCUMENT = /(?:^|\/)README\.md$|(?:^|\/)docs\/.+\.mdx?$/i;
18
+ const KEY = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/;
19
+ const EXTERNAL = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i;
20
+ const PLACEHOLDER = /[<>{}*$]/;
21
+ const PATH_TOKEN = /^(?:\.\.?\/)?[\w@.-]+(?:\/[\w@.()[\]-]+)*\/?$/;
22
+ let runtimeCatalogs;
23
+
24
+ function catalogs() {
25
+ runtimeCatalogs ??= {
26
+ commands: CATALOG,
27
+ rules: loadRuleCatalog(),
28
+ };
29
+ return runtimeCatalogs;
30
+ }
31
+
32
+ const trackedPath = (target, tracked) => tracked.has(target) || [...tracked].some((file) => file.startsWith(`${target.replace(/\/$/, '')}/`));
33
+
34
+ function cleanPath(raw) {
35
+ if (EXTERNAL.test(raw) || PLACEHOLDER.test(raw)) return null;
36
+ let target;
37
+ try { target = decodeURIComponent(raw.split(/[?#]/)[0]); } catch { return null; }
38
+ if (!PATH_TOKEN.test(target) || !target || target === '.' || target === '..') return null;
39
+ return target.replace(/\/$/, '');
40
+ }
41
+
42
+ function codePathRoots(file, tracked) {
43
+ const side = /^(be|fe)\//.exec(file)?.[1] ?? '';
44
+ return [...new Set(['', side, packageDirectory(file, tracked)])];
45
+ }
46
+
47
+ function resolvedPath(raw, file, tracked, link, generated, roots) {
48
+ const target = cleanPath(raw);
49
+ if (target === null) return true;
50
+ const relative = path.posix.normalize(path.posix.join(path.posix.dirname(file), target));
51
+ const candidates = link ? [relative] : [relative, ...roots.map((root) => path.posix.join(root, target))];
52
+ return candidates.some((candidate) => !candidate.startsWith('../') && (trackedPath(candidate, tracked) || generated.has(candidate)));
53
+ }
54
+
55
+ function pathReferences(line) {
56
+ const refs = [];
57
+ // Markdown inline and reference links, including image links. URLs and anchors are excluded later.
58
+ for (const match of line.matchAll(/\]\(\s*(?:<([^>]+)>|([^\s)]+))(?:\s+['"][^)]*)?\)|^\s*\[[^\]]+\]:\s*(?:<([^>]+)>|(\S+))/g)) {
59
+ refs.push({ target: match[1] ?? match[2] ?? match[3] ?? match[4], link: true });
60
+ }
61
+ const masked = line.replace(/\]\([^)]*\)/g, ']');
62
+ for (const match of masked.matchAll(/`([^`]+)`/g)) {
63
+ const target = match[1];
64
+ if (target.includes('/') || /\.(?:md|mdx|json|ya?ml|[cm]?[jt]sx?|sh|env|toml|sql|graphql|enc)$/.test(target)) refs.push({ target, link: false });
65
+ }
66
+ // Unquoted prose paths need a slash; dotted words and API routes are not file citations.
67
+ const prose = masked.replace(/`[^`]*`|\b[a-z][a-z0-9+.-]*:\/\/\S+/gi, '');
68
+ for (const match of prose.matchAll(/(?<![\w@./:-])((?:\.\.?\/)?[\w@.-]+\/(?:[\w@.()[\]-]+\/)*[\w@.()[\]-]*)(?![\w/])/g)) {
69
+ refs.push({ target: match[1].replace(/[.,;:]$/, ''), link: false });
70
+ }
71
+ return refs;
72
+ }
73
+
74
+ /** A cited host location (the untracked custody file `.claude/secret.env`, `~/...`) or a prose pair (`expand/contract`: no extension and no tracked top-level folder of that name) is no repository path. */
75
+ function hostOrProse(target, tracked, link, roots) {
76
+ if (!link && (target === 'process.env' || target === 'import.meta.env')) return true;
77
+ if (target === '.claude/secret.env' || /^(?:~\/|\/)/.test(target)) return true;
78
+ if (link || /\.[A-Za-z0-9]+$/.test(target) || target.endsWith('/') || target.startsWith('.')) return false;
79
+ const top = target.split('/')[0];
80
+ return !roots.some((root) => trackedPath(path.posix.join(root, top), tracked));
81
+ }
82
+
83
+ function pathFindings(file, lines, tracked, generated) {
84
+ const findings = [];
85
+ const roots = codePathRoots(file, tracked);
86
+ for (const [index, line] of lines.entries()) {
87
+ for (const { target, link } of pathReferences(line)) {
88
+ if (hostOrProse(target, tracked, link, roots)) continue;
89
+ if (!resolvedPath(target, file, tracked, link, generated, roots)) findings.push(found(PATH_MISSING, file, `${file} cites a missing repository path ${target}; update the documentation or restore the path`, { line: index + 1, reference: target }));
90
+ }
91
+ }
92
+ return findings;
93
+ }
94
+
95
+ /** Tool reports are generated, never tracked; both their consumer and the slot policy must declare them. */
96
+ function generatedReports(repoRoot, tracked, resolver) {
97
+ const reports = new Set();
98
+ if (!tracked.has('sonar-project.properties') || !resolver) return reports;
99
+ const text = readText(repoRoot, 'sonar-project.properties') ?? '';
100
+ for (const match of text.matchAll(/^sonar\.javascript\.lcov\.reportPaths\s*=\s*([^\r\n]*)/gm)) {
101
+ for (const report of match[1].split(',').map((entry) => entry.trim())) {
102
+ const target = cleanPath(report);
103
+ if (target && resolver.trackingOf(target) === 'ignored') reports.add(target);
104
+ }
105
+ }
106
+ return reports;
107
+ }
108
+
109
+ function packageDirectory(file, tracked) {
110
+ let dir = path.posix.dirname(file);
111
+ while (true) {
112
+ if (tracked.has(path.posix.join(dir, 'package.json'))) return dir === '.' ? '' : dir;
113
+ if (dir === '.') return '';
114
+ dir = path.posix.dirname(dir);
115
+ }
116
+ }
117
+
118
+ function workspaceManifests(repoRoot, directory, tracked) {
119
+ const manifest = readJson(repoRoot, path.posix.join(directory, 'package.json'));
120
+ const workspaces = Array.isArray(manifest?.workspaces) ? manifest.workspaces : manifest?.workspaces?.packages ?? [];
121
+ const patterns = workspaces.map((entry) => globExpression(path.posix.join(directory, entry)));
122
+ return [...tracked].filter((entry) => path.posix.basename(entry) === 'package.json' && patterns.some((pattern) => pattern.test(path.posix.dirname(entry))));
123
+ }
124
+
125
+ function missingScripts(repoRoot, directory, tracked, name, workspaces) {
126
+ const manifests = workspaces ? workspaceManifests(repoRoot, directory, tracked) : [path.posix.join(directory, 'package.json')];
127
+ if (!manifests.length) return [path.posix.join(directory, 'package.json')];
128
+ return manifests.filter((file) => !Object.hasOwn(readJson(repoRoot, file)?.scripts ?? {}, name));
129
+ }
130
+
131
+ function scriptFindings(repoRoot, file, lines, tracked) {
132
+ const findings = [];
133
+ const initial = packageDirectory(file, tracked);
134
+ let directory = initial;
135
+ for (const [index, line] of lines.entries()) {
136
+ if (/^\s*```/.test(line)) directory = initial;
137
+ const changed = /(?:^|[;&]\s*|\$\s*)cd\s+([\w./-]+)/.exec(line);
138
+ if (changed) directory = path.posix.normalize(path.posix.join(directory, changed[1]));
139
+ for (const match of line.matchAll(/\bnpm\s+(?:--prefix\s+([\w./-]+)\s+)?run\s+(?:--silent\s+)?([\w:.-]+)/g)) {
140
+ const dir = match[1] ? path.posix.join(directory, match[1]) : directory;
141
+ const name = match[2];
142
+ const tail = line.slice(match.index + match[0].length).split(/[`;&|]/)[0];
143
+ const workspaces = /(?:^|\s)--workspaces(?:\s|$)/.test(tail);
144
+ const missing = missingScripts(repoRoot, dir, tracked, name, workspaces);
145
+ for (const manifest of missing) findings.push(found(COMMAND_UNKNOWN, file, `${file} cites npm run ${name}, absent from ${manifest}; update the command or declare the script`, { line: index + 1, reference: `npm run ${name}` }));
146
+ }
147
+ }
148
+ return findings;
149
+ }
150
+
151
+ function commandFindings(repoRoot, file, text, tracked, commands) {
152
+ const findings = scriptFindings(repoRoot, file, text.split(/\r?\n/), tracked);
153
+ for (const occurrence of extractCommands(text)) {
154
+ if (!/^\s*app\s/.test(occurrence.text)) continue;
155
+ for (const issue of checkCommand(occurrence, commands, { strictProse: true }).filter((reason) => /^unknown verb\b/.test(reason))) {
156
+ findings.push(found(COMMAND_UNKNOWN, file, `${file} cites a starci app command that does not resolve: ${issue}; use a verb from the CLI catalog`, { line: occurrence.line }));
157
+ }
158
+ }
159
+ return findings;
160
+ }
161
+
162
+ function importedFiles(file, text, tracked) {
163
+ const prefix = /^(be|fe)\//.exec(file)?.[1] ?? '';
164
+ const imports = [...text.matchAll(/\b(?:from\s*|import\s*|export\s*\*\s*from\s*)['"]([^'"]+)['"]/g)].map((match) => match[1]);
165
+ return imports.flatMap((specifier) => {
166
+ let target;
167
+ if (specifier.startsWith('.')) target = path.posix.normalize(path.posix.join(path.posix.dirname(file), specifier));
168
+ else if (specifier.startsWith('@modules/')) target = path.posix.join(prefix, 'src/modules', specifier.slice('@modules/'.length));
169
+ else return [];
170
+ return [target, `${target}.ts`, `${target}/index.ts`].filter((candidate) => tracked.has(candidate));
171
+ });
172
+ }
173
+
174
+ function addReaderKeys(text, keys) {
175
+ const readers = new Set(['env', 'source', ...[...text.matchAll(/\b(\w+)\s*:\s*EnvSource\b/g)].map((match) => match[1])]);
176
+ for (const match of text.matchAll(/\b(\w+)\s*\.\s*\w+\s*\(\s*['"]([A-Z][A-Z0-9_]*)['"]|\bprocess\.env\.([A-Z][A-Z0-9_]*)|\bprocess\.env\[['"]([A-Z][A-Z0-9_]*)['"]\]/g)) {
177
+ if (match[3] || match[4]) keys.add(match[3] ?? match[4]);
178
+ else if (readers.has(match[1])) keys.add(match[2]);
179
+ }
180
+ }
181
+
182
+ function addDocumentedKeys(text, keys) {
183
+ for (const line of text.split(/\r?\n/)) {
184
+ const cell = /^\s*\|\s*`?([A-Z][A-Z0-9_]*)`?\s*\|/.exec(line);
185
+ if (cell) keys.add(cell[1]);
186
+ const bullet = /^\s*[-*]\s*`([A-Z][A-Z0-9_]*)`/.exec(line);
187
+ if (bullet) keys.add(bullet[1]);
188
+ }
189
+ }
190
+
191
+ function environmentKeys(repoRoot, tracked) {
192
+ const keys = new Set();
193
+ const pending = [...tracked].filter((file) => /(?:^|\/)[^/]+\.env\.ts$/.test(file));
194
+ const seen = new Set();
195
+ while (pending.length) {
196
+ const file = pending.pop();
197
+ if (seen.has(file)) continue;
198
+ seen.add(file);
199
+ const text = readText(repoRoot, file) ?? '';
200
+ // Follow the typed reader's local capability parsers and barrel exports, never execute them.
201
+ pending.push(...importedFiles(file, text, tracked));
202
+ addReaderKeys(text, keys);
203
+ }
204
+ for (const file of [...tracked].filter((entry) => /(?:^|\/)KEYS\.md$/.test(entry))) {
205
+ const text = readText(repoRoot, file) ?? '';
206
+ addDocumentedKeys(text, keys);
207
+ }
208
+ return keys;
209
+ }
210
+
211
+ function symbolCall(ts, node) {
212
+ if (!node || !ts.isCallExpression(node)) return false;
213
+ const callee = node.expression;
214
+ if (ts.isIdentifier(callee)) return callee.text === 'Symbol';
215
+ return ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression) && callee.expression.text === 'Symbol' && callee.name.text === 'for';
216
+ }
217
+
218
+ function declaredTokens(ts, source, tokens) {
219
+ const visit = (node) => {
220
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && KEY.test(node.name.text) && symbolCall(ts, node.initializer)) tokens.add(node.name.text);
221
+ ts.forEachChild(node, visit);
222
+ };
223
+ visit(source);
224
+ }
225
+
226
+ function injectionTokens(repoRoot, tracked) {
227
+ const tokens = new Set();
228
+ const ts = loadTypescript(repoRoot, path.dirname(fileURLToPath(import.meta.url)));
229
+ if (!ts) return tokens;
230
+ for (const file of tracked) {
231
+ if (!/\.[cm]?[jt]sx?$/.test(file) || /(?:^|\/)(?:tests?|__tests__)\/|(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$/.test(file)) continue;
232
+ const text = readText(repoRoot, file) ?? '';
233
+ declaredTokens(ts, ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true), tokens);
234
+ }
235
+ return tokens;
236
+ }
237
+
238
+ function unknownEnvironmentName(name, line, keys, codes, tokens, explicit) {
239
+ if (!KEY.test(name) || /^R\d+$/.test(name) || codes.has(name)) return false;
240
+ if (!explicit && tokens.has(name) && !/\benv(?:ironment)?\b|\bvariable\b/i.test(line)) return false;
241
+ if (!name.includes('_') && !keys.has(name) && !/\benv(?:ironment)?\b|\b(?:key|variable)s?\b/i.test(line)) return false;
242
+ return !keys.has(name) && !(name.endsWith('_FILE') && keys.has(name.slice(0, -5)));
243
+ }
244
+
245
+ function nameFindings(file, lines, keys, rules, tokens) {
246
+ const findings = [];
247
+ const codes = new Set(rules.rules.flatMap((rule) => rule.failureCodes));
248
+ for (const [index, line] of lines.entries()) {
249
+ for (const match of line.matchAll(/\bR\d+\b/g)) {
250
+ if (!rules.rule(match[0])) findings.push(found(RULE_UNKNOWN, file, `${file} cites unknown rule ${match[0]}; use an id from the HFS rule catalog`, { line: index + 1, reference: match[0] }));
251
+ }
252
+ for (const match of line.matchAll(/`([A-Z][A-Z0-9_]*)`|\b(?:process\.env\.|\$\{?)([A-Z][A-Z0-9_]*)|\b([A-Z][A-Z0-9_]*)\s*=/g)) {
253
+ const name = match[1] ?? match[2] ?? match[3];
254
+ if (!unknownEnvironmentName(name, line, keys, codes, tokens, !match[1])) continue;
255
+ findings.push(found(ENV_UNKNOWN, file, `${file} cites unknown environment key ${name}; declare it in a typed environment reader or KEYS.md, or update the documentation`, { line: index + 1, reference: name }));
256
+ }
257
+ }
258
+ return findings;
259
+ }
260
+
261
+ /** Findings of R270 over product READMEs and docs, in lite and full; runtime docs retain their own checks. */
262
+ export function docFreshFindings({ repoRoot, files = [], all = files, repo, resolver, appSide = false, inScope = () => true }) {
263
+ if (appSide || repo?.profile === 'runtime' || !['lite', 'full'].includes(repo?.edition)) return [];
264
+ const inventory = repo.profile === 'app' ? all : files;
265
+ const docs = inventory.filter((file) => DOCUMENT.test(file) && path.posix.basename(file) !== 'KEYS.md' && inScope(file));
266
+ if (!docs.length) return [];
267
+ const tracked = new Set(all);
268
+ const { commands, rules } = catalogs();
269
+ const keys = environmentKeys(repoRoot, tracked);
270
+ const tokens = injectionTokens(repoRoot, tracked);
271
+ const generated = generatedReports(repoRoot, tracked, resolver);
272
+ const findings = [];
273
+ for (const file of docs) {
274
+ const text = readText(repoRoot, file);
275
+ if (text === null) continue;
276
+ const lines = text.split(/\r?\n/);
277
+ findings.push(...pathFindings(file, lines, tracked, generated), ...commandFindings(repoRoot, file, text, tracked, commands), ...nameFindings(file, lines, keys, rules, tokens));
278
+ }
279
+ const seen = new Set();
280
+ return findings.filter((finding) => {
281
+ const key = `${finding.code}:${finding.path}:${finding.line}:${finding.reference ?? finding.message}`;
282
+ if (seen.has(key)) return false;
283
+ seen.add(key);
284
+ return true;
285
+ });
286
+ }
@@ -0,0 +1,249 @@
1
+ // Docker API liveness reachability through the app module graph and its health probe.
2
+ import path from 'node:path';
3
+ import { literalText as tsLiteral, variableInitializerGroups } from '../../lib/ts-ast.mjs';
4
+ import { execForm, shellCommands } from '../../lib/dockerfile.mjs';
5
+ import { readText } from './read.mjs';
6
+
7
+ /** Resolve only app-owned source imports; health routes must be reachable from this app's module composition. */
8
+ function healthSourceBase(filename, source) {
9
+ if (source.startsWith('.')) return path.posix.normalize(path.posix.join(path.posix.dirname(filename), source));
10
+ if (source.startsWith('@features/')) return `be/src/features/${source.slice(10)}`;
11
+ if (source.startsWith('@modules/')) return `be/src/modules/${source.slice(9)}`;
12
+ return null;
13
+ }
14
+
15
+ function healthSourcePath(repoRoot, filename, source) {
16
+ const base = healthSourceBase(filename, source);
17
+ if (!base || base.startsWith('../')) return null;
18
+ return [base, `${base}.ts`, `${base}/index.ts`].find((candidate) => readText(repoRoot, candidate) !== null) ?? null;
19
+ }
20
+
21
+ const tsDecorators = (ts, node) => ts.canHaveDecorators(node) ? ts.getDecorators(node) ?? [] : [];
22
+ const tsDecoratorCall = (ts, node, name) => tsDecorators(ts, node).map((entry) => entry.expression).find((entry) => ts.isCallExpression(entry) && ts.isIdentifier(entry.expression) && entry.expression.text === name);
23
+
24
+ function collectHealthUsage(ts, node, state, seen = new Set()) {
25
+ if (ts.isIdentifier(node)) {
26
+ state.used.add(node.text);
27
+ if (state.bindings.has(node.text) && !seen.has(node.text)) collectHealthUsage(ts, state.bindings.get(node.text), state, new Set(seen).add(node.text));
28
+ }
29
+ ts.forEachChild(node, (child) => collectHealthUsage(ts, child, state, seen));
30
+ }
31
+
32
+ function collectHealthModuleObject(ts, node, state) {
33
+ if (!node || !ts.isObjectLiteralExpression(node)) return;
34
+ for (const property of node.properties) {
35
+ if (!ts.isPropertyAssignment(property)) continue;
36
+ const name = property.name.getText(state.source).replace(/^['"]|['"]$/g, '');
37
+ if (['imports', 'controllers'].includes(name)) collectHealthUsage(ts, property.initializer, state);
38
+ }
39
+ }
40
+
41
+ function collectHealthReturns(ts, node, state) {
42
+ const expression = node.expression;
43
+ if (ts.isReturnStatement(node) && expression && ts.isObjectLiteralExpression(expression)
44
+ && expression.properties.some((property) => property.name?.getText(state.source) === 'module')) collectHealthModuleObject(ts, expression, state);
45
+ ts.forEachChild(node, (child) => collectHealthReturns(ts, child, state));
46
+ }
47
+
48
+ function collectHealthClassWiring(ts, node, state) {
49
+ collectHealthModuleObject(ts, tsDecoratorCall(ts, node, 'Module')?.arguments[0], state);
50
+ for (const method of node.members) {
51
+ if (method.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)) collectHealthReturns(ts, method, state);
52
+ }
53
+ }
54
+
55
+ function collectHealthExportNames(ts, node, state) {
56
+ if (!ts.isExportDeclaration(node) || node.moduleSpecifier || !node.exportClause || !ts.isNamedExports(node.exportClause)) return;
57
+ for (const entry of node.exportClause.elements) {
58
+ if (!state.wanted || state.wanted.includes(entry.name.text)) state.used.add((entry.propertyName ?? entry.name).text);
59
+ }
60
+ }
61
+
62
+ function collectHealthWiring(ts, node, state) {
63
+ const { wanted } = state;
64
+ if (ts.isClassDeclaration(node) && (!wanted || wanted.includes(node.name?.text))) collectHealthClassWiring(ts, node, state);
65
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && wanted?.includes(node.name.text) && node.initializer) collectHealthUsage(ts, node.initializer, state);
66
+ if (ts.isFunctionDeclaration(node) && wanted?.includes(node.name?.text) && node.body) collectHealthUsage(ts, node.body, state);
67
+ collectHealthExportNames(ts, node, state);
68
+ ts.forEachChild(node, (child) => collectHealthWiring(ts, child, state));
69
+ }
70
+
71
+ function healthWiringNames(ts, source, wanted) {
72
+ const bindings = new Map([...variableInitializerGroups(ts, source)].map(([name, initializers]) => [name, initializers.at(-1)]));
73
+ const state = { source, wanted, used: new Set(), bindings };
74
+ collectHealthWiring(ts, source, state);
75
+ return state.used;
76
+ }
77
+
78
+ function sourceHasLiveRoute(ts, source, wanted, used) {
79
+ const isLiveController = (node) => {
80
+ if (!ts.isClassDeclaration(node)) return false;
81
+ if (wanted && !wanted.includes(node.name?.text) && !used.has(node.name?.text)) return false;
82
+ if (tsLiteral(ts, tsDecoratorCall(ts, node, 'Controller')?.arguments[0]) !== 'health') return false;
83
+ return node.members.some((member) => tsLiteral(ts, tsDecoratorCall(ts, member, 'Get')?.arguments[0]) === 'live');
84
+ };
85
+ return source.statements.filter((node) => !ts.isImportDeclaration(node)).some((node) => tsSome(ts, node, isLiveController));
86
+ }
87
+
88
+ function healthImportNames(ts, clause, used) {
89
+ const bindings = clause?.namedBindings;
90
+ if (bindings && ts.isNamedImports(bindings)) {
91
+ return bindings.elements.filter((entry) => !entry.isTypeOnly && used.has(entry.name.text)).map((entry) => (entry.propertyName ?? entry.name).text);
92
+ }
93
+ return [clause?.name?.text].filter((name) => used.has(name));
94
+ }
95
+
96
+ function healthImportTargets({ repoRoot, filename, ts, source, used }) {
97
+ const targets = [];
98
+ for (const statement of source.statements.filter((node) => ts.isImportDeclaration(node))) {
99
+ const clause = statement.importClause;
100
+ if (clause?.isTypeOnly) continue;
101
+ const imported = healthImportNames(ts, clause, used);
102
+ if (!imported.length) continue;
103
+ const target = healthSourcePath(repoRoot, filename, statement.moduleSpecifier.text);
104
+ if (target) targets.push({ filename: target, wanted: imported });
105
+ }
106
+ return targets;
107
+ }
108
+
109
+ function healthExportTargets({ repoRoot, filename, ts, source, wanted }) {
110
+ const targets = [];
111
+ for (const statement of source.statements.filter((node) => ts.isExportDeclaration(node) && node.moduleSpecifier && !node.isTypeOnly)) {
112
+ const target = healthSourcePath(repoRoot, filename, statement.moduleSpecifier.text);
113
+ let exported = wanted;
114
+ if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
115
+ exported = statement.exportClause.elements.filter((entry) => !wanted || wanted.includes(entry.name.text)).map((entry) => (entry.propertyName ?? entry.name).text);
116
+ }
117
+ if (target && (!exported || exported.length)) targets.push({ filename: target, wanted: exported });
118
+ }
119
+ return targets;
120
+ }
121
+
122
+ /** Find GET /health/live in the local import closure, excluding unused imports and type-only imports. */
123
+ function apiLiveRoute({ repoRoot, app, ts }) {
124
+ const pending = [{ filename: `be/apps/${app}/src/app.module.ts`, wanted: ['AppModule'] }];
125
+ const visited = new Set();
126
+ while (pending.length) {
127
+ const { filename, wanted } = pending.pop();
128
+ const identity = `${filename}:${wanted?.join(',') ?? '*'}`;
129
+ if (visited.has(identity)) continue;
130
+ visited.add(identity);
131
+ const text = readText(repoRoot, filename);
132
+ if (text === null) continue;
133
+ const source = ts.createSourceFile(filename, text, ts.ScriptTarget.Latest, true);
134
+ const used = healthWiringNames(ts, source, wanted);
135
+ if (sourceHasLiveRoute(ts, source, wanted, used)) return true;
136
+ pending.push(
137
+ ...healthImportTargets({ repoRoot, filename, ts, source, used }),
138
+ ...healthExportTargets({ repoRoot, filename, ts, source, wanted }),
139
+ );
140
+ }
141
+ return false;
142
+ }
143
+
144
+ /** Keep literal URL pieces while substituting the runtime's declared port for a dynamic expression. */
145
+ function probeUrlText(ts, node, port) {
146
+ if (!node) return null;
147
+ const literal = tsLiteral(ts, node);
148
+ if (literal !== null) return literal;
149
+ if (ts.isParenthesizedExpression(node)) return probeUrlText(ts, node.expression, port);
150
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
151
+ const left = probeUrlText(ts, node.left, port);
152
+ const right = probeUrlText(ts, node.right, port);
153
+ return left === null && right === null ? null : `${left ?? port}${right ?? port}`;
154
+ }
155
+ if (ts.isTemplateExpression(node)) return node.head.text + node.templateSpans.map((entry) => `${port}${entry.literal.text}`).join('');
156
+ return null;
157
+ }
158
+
159
+ function localLiveUrl(value, port) {
160
+ try {
161
+ const url = new URL(value.replace(/\$\{?PORT\}?/g, port));
162
+ return url.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)
163
+ && (url.port || '80') === port && url.pathname === '/health/live';
164
+ } catch { return false; }
165
+ }
166
+
167
+ function tsSome(ts, node, predicate) {
168
+ if (!node) return false;
169
+ if (predicate(node)) return true;
170
+ let foundNode = false;
171
+ ts.forEachChild(node, (child) => { if (tsSome(ts, child, predicate)) foundNode = true; });
172
+ return foundNode;
173
+ }
174
+
175
+ const failedExit = (ts, node) => ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)
176
+ && node.expression.expression.getText() === 'process' && node.expression.name.text === 'exit'
177
+ && node.arguments[0] && ts.isNumericLiteral(node.arguments[0]) && Number(node.arguments[0].text) > 0;
178
+ const responseStatus = (ts, node, response) => tsSome(ts, node, (entry) => ts.isPropertyAccessExpression(entry)
179
+ && ts.isIdentifier(entry.expression) && entry.expression.text === response && ['ok', 'status', 'statusCode'].includes(entry.name.text));
180
+
181
+ /** The failure exit must belong to the callback consuming this response, and depend on that response's status. */
182
+ function responseFails(ts, callback) {
183
+ if (!callback || !(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) return false;
184
+ const response = callback.parameters[0]?.name;
185
+ if (!response || !ts.isIdentifier(response)) return false;
186
+ return tsSome(ts, callback.body, (node) => {
187
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.expression.getText() === 'process' && node.expression.name.text === 'exit') {
188
+ const arg = node.arguments[0];
189
+ return arg && ts.isConditionalExpression(arg) && responseStatus(ts, arg.condition, response.text)
190
+ && [arg.whenTrue, arg.whenFalse].some((entry) => ts.isNumericLiteral(entry) && Number(entry.text) > 0);
191
+ }
192
+ return ts.isIfStatement(node) && responseStatus(ts, node.expression, response.text)
193
+ && tsSome(ts, node.thenStatement, (entry) => failedExit(ts, entry) || ts.isThrowStatement(entry));
194
+ });
195
+ }
196
+
197
+ function chainedCall(ts, node, name) {
198
+ const member = node.parent;
199
+ return member && ts.isPropertyAccessExpression(member) && member.expression === node && member.name.text === name
200
+ && member.parent && ts.isCallExpression(member.parent) ? member.parent : null;
201
+ }
202
+
203
+ function nodeProbeReachesLive(ts, code, port) {
204
+ const source = ts.createSourceFile('healthcheck.js', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
205
+ return tsSome(ts, source, (node) => {
206
+ if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression) || node.expression.text !== 'fetch') return false;
207
+ const url = probeUrlText(ts, node.arguments[0], port);
208
+ if (!url || !localLiveUrl(url, port)) return false;
209
+ for (let parent = node.parent; parent && parent !== source; parent = parent.parent) {
210
+ if (ts.isFunctionDeclaration(parent) || ts.isFunctionExpression(parent) || ts.isArrowFunction(parent) || ts.isIfStatement(parent) || ts.isConditionalExpression(parent)) return false;
211
+ }
212
+ const consumed = chainedCall(ts, node, 'then');
213
+ if (!consumed || !responseFails(ts, consumed.arguments[0])) return false;
214
+ const rejected = chainedCall(ts, consumed, 'catch')?.arguments[0] ?? consumed.arguments[1];
215
+ return rejected && (ts.isArrowFunction(rejected) || ts.isFunctionExpression(rejected)) && tsSome(ts, rejected.body, (entry) => failedExit(ts, entry));
216
+ });
217
+ }
218
+
219
+ /** A shell health probe cannot erase a failure through a pipeline, fallback, or later independent command. */
220
+ function masksProbeFailure(body) {
221
+ let quote = null;
222
+ for (let index = 0; index < body.length; index += 1) {
223
+ const char = body[index];
224
+ if (char === '\\') { index += 1; continue; }
225
+ if (quote) { if (char === quote) quote = null; continue; }
226
+ if (char === '"' || char === "'") { quote = char; continue; }
227
+ if (char === ';' || char === '|') return true;
228
+ }
229
+ return false;
230
+ }
231
+
232
+ function healthCommandReachesLive(ts, body, port) {
233
+ const exec = execForm(body);
234
+ if (!exec && masksProbeFailure(body)) return false;
235
+ const commands = exec ? [exec] : shellCommands(body);
236
+ if (commands.at(-1)?.[0] === 'true' || commands.at(-1)?.join(' ') === 'exit 0') return false;
237
+ return commands.some((parts) => {
238
+ const tool = parts[0];
239
+ if (['sh', '/bin/sh', 'bash', '/bin/bash'].includes(tool) && parts.includes('-c')) return healthCommandReachesLive(ts, parts[parts.indexOf('-c') + 1] ?? '', port);
240
+ if (['curl', 'wget'].includes(tool)) return (tool === 'wget' || parts.some((arg) => arg === '--fail' || /^-[^-]*f/.test(arg))) && parts.slice(1).some((arg) => localLiveUrl(arg, port));
241
+ if (tool !== 'node' || !parts.includes('-e')) return false;
242
+ return nodeProbeReachesLive(ts, parts[parts.indexOf('-e') + 1] ?? '', port);
243
+ });
244
+ }
245
+
246
+ /** Whether the API image probes a registered local liveness route and propagates probe failure. */
247
+ export function healthProbeReachesLive({ repoRoot, app, ts, body, port }) {
248
+ return healthCommandReachesLive(ts, body, port) && apiLiveRoute({ repoRoot, app, ts });
249
+ }
@@ -2,7 +2,7 @@
2
2
  // app; a Dockerfile anywhere else is owned by no slot) built from the APP ROOT, which holds the one package.json and the managed .dockerignore.
3
3
  // The Dockerfile is app-owned (starci app scaffold writes it once from templates/<side>/image) and judged here, structurally, from its parsed
4
4
  // instructions (scripts/lib/dockerfile.mjs), never by matching the text:
5
- // HFS_DOCKER_BUILD_CONTEXT the header comment states the app's own build command (`docker build -f <side>/apps/<app>/Dockerfile ... .`, the
5
+ // HFS_DOCKER_BUILD_CONTEXT the header comment states the app's own build command (`starci docker build <app>` or `docker build -f <side>/apps/<app>/Dockerfile ... .`, the
6
6
  // context the app root) and no COPY or ADD source leaves the context (`..`, an absolute path)
7
7
  // HFS_DOCKER_STAGES a `build` stage and a last `runtime` stage; the runtime is unprivileged (`USER node`) and runs no build; every install
8
8
  // is `npm ci` (never `npm install`); a back-end runtime installs `--omit=dev --ignore-scripts`, a front-end runtime installs nothing
@@ -18,12 +18,14 @@ import { loadTypescript } from '../../lib/package-at.mjs';
18
18
  import { propertyText } from '../../lib/ts-ast.mjs';
19
19
  import { execForm, parseDockerfile, shellCommands, words } from '../../lib/dockerfile.mjs';
20
20
  import { found, readText } from './read.mjs';
21
+ import { healthProbeReachesLive } from './docker-health.mjs';
21
22
 
22
23
  const DOCKER_BUILD_CONTEXT = 'HFS_DOCKER_BUILD_CONTEXT';
23
24
  const DOCKER_STAGES = 'HFS_DOCKER_STAGES';
24
25
  const DOCKER_ENTRY = 'HFS_DOCKER_ENTRY';
25
26
  const DOCKER_BASE_PIN = 'HFS_DOCKER_BASE_PIN';
26
27
  const DOCKER_SECRETS = 'HFS_DOCKER_SECRETS';
28
+ const DOCKER_HEALTHCHECK_FAKE = 'HFS_DOCKER_HEALTHCHECK_FAKE';
27
29
 
28
30
  /** The one node base image of every Dockerfile of the canon: an exact node version on an exact alpine release. */
29
31
  export const NODE_IMAGE = 'node:22.14.0-alpine3.21';
@@ -78,11 +80,12 @@ function envOf(stage) {
78
80
  return env;
79
81
  }
80
82
 
81
- /** True when a header comment states `docker build -f <wanted> ... .`. */
82
- const statesBuildCommand = (parsed, wanted) => parsed.comments.some((comment) => {
83
+ /** Accept the app command or the equivalent explicit Dockerfile and root context. */
84
+ const statesBuildCommand = (parsed, wanted, app) => parsed.comments.some((comment) => {
83
85
  const parts = words(comment.text);
84
86
  const build = parts.findIndex((part, index) => part === 'docker' && parts[index + 1] === 'build');
85
87
  if (build < 0) return false;
88
+ if (parts[build - 1] === 'starci') return parts[build + 2] === app;
86
89
  const flag = parts.indexOf('-f', build);
87
90
  return flag > 0 && parts[flag + 1] === wanted && parts.at(-1) === '.';
88
91
  });
@@ -105,7 +108,7 @@ function copySourceFindings(file, item) {
105
108
  function contextFindings(file, parsed, side, app) {
106
109
  const findings = [];
107
110
  const wanted = dockerfilePath(side, app);
108
- if (!statesBuildCommand(parsed, wanted)) findings.push(found(DOCKER_BUILD_CONTEXT, file, `${file} does not state its build command in a header comment: \`docker build -f ${wanted} -t <image> .\` (the context is the app root, the folder with the one package.json and lockfile).`, { expected: wanted }));
111
+ if (!statesBuildCommand(parsed, wanted, app)) findings.push(found(DOCKER_BUILD_CONTEXT, file, `${file} does not state its build command in a header comment: \`starci docker build ${app}\` or \`docker build -f ${wanted} -t <image> .\` (the context is the app root, the folder with the one package.json and lockfile).`, { expected: wanted }));
109
112
  for (const item of parsed.instructions.filter((entry) => entry.keyword === 'COPY' || entry.keyword === 'ADD')) findings.push(...copySourceFindings(file, item));
110
113
  return findings;
111
114
  }
@@ -116,7 +119,7 @@ function stageInstallFindings(file, stages) {
116
119
  for (const stage of stages) {
117
120
  for (const command of commandsOf(stage)) {
118
121
  const call = npmCall(command.words);
119
- if (call && (call.verb === 'install' || call.verb === 'i') && !call.args.some((arg) => arg === '-g' || arg === '--global')) findings.push(found(DOCKER_STAGES, file, `${file}:${command.line} runs \`npm ${call.verb}\`; an image installs with \`npm ci\` from the lockfile.`, { line: command.line }));
122
+ if (call && (call.verb === 'install' || call.verb === 'i')) findings.push(found(DOCKER_STAGES, file, `${file}:${command.line} runs \`npm ${call.verb}\`; an image installs with \`npm ci\` from the lockfile.`, { line: command.line }));
120
123
  }
121
124
  }
122
125
  return findings;
@@ -183,6 +186,19 @@ function entryHealthFindings(file, runtime, side, kind, exposes, health, none) {
183
186
  return processHealthFindings(file, kind, exposes, health, none);
184
187
  }
185
188
 
189
+ /** R262 strengthens only API images: a constant success or an absent/local-unreachable live route is refused. */
190
+ function healthReachabilityFindings({ repoRoot, file, parsed, side, app, kind, ts }) {
191
+ if (side !== 'be' || kind !== 'api' || !ts) return [];
192
+ const runtime = parsed.stages.at(-1);
193
+ if (runtime?.name !== RUNTIME_STAGE) return [];
194
+ const health = instructionsOf(runtime, 'HEALTHCHECK').at(-1);
195
+ if (!health || /^NONE\b/i.test(health.text)) return [];
196
+ const body = health.text.replace(/^(?:--\S+\s+)*CMD\s+/i, '');
197
+ const port = envOf(runtime).get('PORT') ?? DEFAULT_PORT;
198
+ if (healthProbeReachesLive({ repoRoot, app, ts, body, port })) return [];
199
+ return [found(DOCKER_HEALTHCHECK_FAKE, file, `${file}:${health.line} the API HEALTHCHECK must request its reachable /health/live route on the runtime's local port and fail when the probe fails; a constant-success command or a URL without a registered live route does not probe this app.`, { line: health.line })];
200
+ }
201
+
186
202
  function nextEntryFindings({ repoRoot, file, parsed, app, project, ts }) {
187
203
  const findings = [];
188
204
  const build = parsed.stages.find((stage) => stage.name === BUILD_STAGE);
@@ -244,7 +260,7 @@ const isCredentialName = (name) => !name.startsWith('NEXT_PUBLIC_') && name.spli
244
260
  const isSecretPath = (source) => {
245
261
  const segments = source.split('/').filter((part) => part && part !== '.');
246
262
  const base = segments.at(-1) ?? '';
247
- const env = base === '.env' || (base.startsWith('.env.') && base !== '.env.example');
263
+ const env = base.startsWith('.env') && base !== '.env.example';
248
264
  return env || segments.some((segment) => SECRET_FOLDERS.has(segment)) || SECRET_SUFFIXES.some((suffix) => base.endsWith(suffix)) || base.startsWith('kubeconfig');
249
265
  };
250
266
 
@@ -289,7 +305,7 @@ export function dockerFindings({ repoRoot, files, repo }) {
289
305
  ...repo.sides.be.apps.map((entry) => ({ side: 'be', app: entry.name, kind: entry.kind })),
290
306
  ...repo.sides.fe.apps.map((entry) => ({ side: 'fe', app: entry.name, kind: entry.kind })),
291
307
  ];
292
- const ts = apps.some((entry) => entry.side === 'fe') ? typescriptFor(repoRoot) : null;
308
+ const ts = apps.some((entry) => entry.side === 'fe' || entry.kind === 'api') ? typescriptFor(repoRoot) : null;
293
309
  return apps.flatMap(({ side, app, kind }) => {
294
310
  const file = dockerfilePath(side, app);
295
311
  if (!tracked.has(file)) return [];
@@ -300,6 +316,7 @@ export function dockerFindings({ repoRoot, files, repo }) {
300
316
  ...contextFindings(file, parsed, side, app),
301
317
  ...stageFindings(file, parsed, side, kind),
302
318
  ...entryFindings({ repoRoot, file, parsed, side, app, kind, project: repo.project, ts }),
319
+ ...healthReachabilityFindings({ repoRoot, file, parsed, side, app, kind, ts }),
303
320
  ...pinFindings(file, parsed),
304
321
  ...secretFindings(file, parsed),
305
322
  ];