@starci/hfs 4.0.7 → 4.0.9

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 (704) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +86 -43
  3. package/emit/contracts.mjs +56 -9
  4. package/emit/db-types.mjs +169 -0
  5. package/emit/events.mjs +41 -0
  6. package/emit/operations-worker.mjs +1 -1
  7. package/emit/operations.mjs +2 -2
  8. package/emit/schema-worker.mjs +1 -1
  9. package/lint/bound-sys.cjs +50 -0
  10. package/lint/run.mjs +54 -20
  11. package/package.json +14 -9
  12. package/report/sonar.mjs +8 -8
  13. package/runtime/engine/config.mjs +494 -0
  14. package/runtime/engine/invalid-config.mjs +15 -0
  15. package/runtime/engine/orca-config.mjs +27 -0
  16. package/runtime/engine/runtime-root.mjs +5 -0
  17. package/runtime/engine/yaml.mjs +3 -3
  18. package/runtime/knowledge/hfs/canon-pins.yaml +163 -32
  19. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -2
  20. package/runtime/knowledge/hfs/rules.yaml +2059 -0
  21. package/runtime/knowledge/hfs/slots.yaml +915 -123
  22. package/runtime/knowledge/patterns/be/api.yaml +369 -0
  23. package/runtime/knowledge/patterns/be/cli.yaml +297 -0
  24. package/runtime/knowledge/patterns/be/domain.yaml +113 -0
  25. package/runtime/knowledge/patterns/be/event-bus.yaml +285 -0
  26. package/runtime/knowledge/patterns/be/jobs.yaml +261 -0
  27. package/runtime/knowledge/patterns/be/projections.yaml +178 -0
  28. package/runtime/knowledge/patterns/be/queues.yaml +236 -0
  29. package/runtime/knowledge/patterns/be/reactors.yaml +140 -0
  30. package/runtime/knowledge/patterns/be/realtime.yaml +222 -0
  31. package/runtime/knowledge/patterns/be/saga.yaml +248 -0
  32. package/runtime/knowledge/patterns/be/webhooks.yaml +221 -0
  33. package/runtime/knowledge/patterns/fe/folder.yaml +133 -27
  34. package/runtime/knowledge/sonar-gate.yaml +53 -19
  35. package/runtime/modules/kernel/failure-codes.yaml +580 -50
  36. package/runtime/modules/models/registry.yaml +591 -0
  37. package/runtime/modules/models/runtimes.yaml +701 -0
  38. package/runtime/scripts/api/fs/forbidden-root.mjs +51 -0
  39. package/runtime/scripts/api/fs/is-link-like.mjs +25 -0
  40. package/runtime/scripts/api/fs/lib.mjs +14 -0
  41. package/runtime/scripts/api/fs/safe-remove.mjs +104 -0
  42. package/runtime/scripts/api/git/config-get.mjs +10 -0
  43. package/runtime/scripts/api/git/diff.mjs +5 -0
  44. package/runtime/scripts/api/git/lib.mjs +17 -27
  45. package/runtime/scripts/api/git/log.mjs +5 -0
  46. package/runtime/scripts/api/git/ls-files.mjs +6 -0
  47. package/runtime/scripts/api/git/ls-tree.mjs +5 -0
  48. package/runtime/scripts/api/git/merge-base.mjs +9 -0
  49. package/runtime/scripts/api/git/remote.mjs +5 -0
  50. package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
  51. package/runtime/scripts/api/git/show.mjs +6 -0
  52. package/runtime/scripts/api/sops/decrypt.mjs +10 -6
  53. package/runtime/scripts/api/sops/lib.mjs +35 -0
  54. package/runtime/scripts/api/sops/seal.mjs +18 -0
  55. package/runtime/scripts/{lib/hfs-allows.mjs → hfs/allows.mjs} +3 -3
  56. package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
  57. package/runtime/scripts/{checks → hfs}/architecture/backend.mjs +17 -24
  58. package/runtime/scripts/{checks → hfs}/architecture/background-unowned.mjs +16 -34
  59. package/runtime/scripts/{checks → hfs}/architecture/client-reaches-server.mjs +3 -1
  60. package/runtime/scripts/{checks → hfs}/architecture/clones.mjs +4 -4
  61. package/runtime/scripts/{checks → hfs}/architecture/config.mjs +11 -8
  62. package/runtime/scripts/{checks → hfs}/architecture/connection-map.mjs +21 -9
  63. package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
  64. package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
  65. package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
  66. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
  67. package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
  68. package/runtime/scripts/{checks → hfs}/architecture/contract-fixture-guard.mjs +5 -11
  69. package/runtime/scripts/{checks → hfs}/architecture/contracts.mjs +33 -162
  70. package/runtime/scripts/{checks → hfs}/architecture/dead-exports.mjs +58 -29
  71. package/runtime/scripts/{checks → hfs}/architecture/doc-language.mjs +1 -1
  72. package/runtime/scripts/{checks → hfs}/architecture/entrypoint.mjs +2 -1
  73. package/runtime/scripts/{checks → hfs}/architecture/fe-slot-allows.mjs +4 -4
  74. package/runtime/scripts/{checks → hfs}/architecture/feature-shape.mjs +6 -3
  75. package/runtime/scripts/{checks → hfs}/architecture/framework-pinned.mjs +1 -1
  76. package/runtime/scripts/{checks → hfs}/architecture/frontend.mjs +17 -43
  77. package/runtime/scripts/{checks → hfs}/architecture/hfs-graph.mjs +1 -1
  78. package/runtime/scripts/{checks → hfs}/architecture/hfs.mjs +27 -42
  79. package/runtime/scripts/{checks → hfs}/architecture/i18n-keys.mjs +1 -1
  80. package/runtime/scripts/{checks → hfs}/architecture/index.mjs +17 -4
  81. package/runtime/scripts/{checks → hfs}/architecture/machine-ast.mjs +44 -11
  82. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
  83. package/runtime/scripts/{checks → hfs}/architecture/module-per-transport.mjs +30 -6
  84. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +96 -0
  85. package/runtime/scripts/{checks → hfs}/architecture/next-data.mjs +40 -168
  86. package/runtime/scripts/{checks → hfs}/architecture/owners.mjs +35 -15
  87. package/runtime/scripts/{checks → hfs}/architecture/register-once.mjs +2 -1
  88. package/runtime/scripts/{checks → hfs}/architecture/registration.mjs +23 -81
  89. package/runtime/scripts/{checks → hfs}/architecture/required-files.mjs +21 -13
  90. package/runtime/scripts/{checks → hfs}/architecture/schema-owner.mjs +17 -114
  91. package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
  92. package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
  93. package/runtime/scripts/{checks → hfs}/architecture/source-names.mjs +27 -147
  94. package/runtime/scripts/{checks → hfs}/architecture/sql-owner.mjs +59 -18
  95. package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
  96. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
  97. package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
  98. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
  99. package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
  100. package/runtime/scripts/{checks → hfs}/architecture/surface.mjs +10 -4
  101. package/runtime/scripts/{checks → hfs}/architecture/test-world-files.mjs +11 -21
  102. package/runtime/scripts/{checks → hfs}/architecture/tiers.mjs +47 -5
  103. package/runtime/scripts/{checks → hfs}/architecture/transport-owner.mjs +3 -3
  104. package/runtime/scripts/{checks → hfs}/architecture/typescript.mjs +13 -27
  105. package/runtime/scripts/{checks → hfs}/architecture.mjs +5 -5
  106. package/runtime/scripts/{lib/hfs-check.mjs → hfs/check.mjs} +95 -84
  107. package/runtime/scripts/hfs/coverage-scope.mjs +162 -0
  108. package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
  109. package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
  110. package/runtime/scripts/hfs/edition-slots.mjs +66 -0
  111. package/runtime/scripts/hfs/manifest-shape.mjs +247 -0
  112. package/runtime/scripts/hfs/param-names.mjs +18 -0
  113. package/runtime/scripts/{lib/hfs-path-findings.mjs → hfs/path-findings.mjs} +7 -7
  114. package/runtime/scripts/{lib → hfs}/repo-identity.mjs +9 -6
  115. package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
  116. package/runtime/scripts/hfs/rules/app-root.mjs +51 -0
  117. package/runtime/scripts/hfs/rules/cli.mjs +37 -0
  118. package/runtime/scripts/hfs/rules/contract-compat.mjs +71 -0
  119. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/contract.mjs +11 -9
  120. package/runtime/scripts/hfs/rules/database-config.mjs +102 -0
  121. package/runtime/scripts/hfs/rules/database-constants.mjs +13 -0
  122. package/runtime/scripts/hfs/rules/database-migrations.mjs +111 -0
  123. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +233 -0
  124. package/runtime/scripts/hfs/rules/database-sql.mjs +336 -0
  125. package/runtime/scripts/hfs/rules/database.mjs +68 -0
  126. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/deps.mjs +1 -1
  127. package/runtime/scripts/hfs/rules/docker.mjs +259 -0
  128. package/runtime/scripts/hfs/rules/edition.mjs +111 -0
  129. package/runtime/scripts/hfs/rules/event-bus.mjs +153 -0
  130. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +66 -0
  131. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/fe-no-tests.mjs +6 -5
  132. package/runtime/scripts/{lib/hfs-rules/frontend.mjs → hfs/rules/frontend-tree.mjs} +9 -9
  133. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/integration-specs.mjs +3 -6
  134. package/runtime/scripts/hfs/rules/kinds.mjs +84 -0
  135. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/lint-suppression.mjs +1 -1
  136. package/runtime/scripts/hfs/rules/monorepo.mjs +179 -0
  137. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/peer-integrations.mjs +3 -3
  138. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/pipeline.mjs +11 -21
  139. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/proof-commands.mjs +2 -2
  140. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/read.mjs +1 -1
  141. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/repo-local-checks.mjs +2 -2
  142. package/runtime/scripts/hfs/rules/saga.mjs +170 -0
  143. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/secrets.mjs +4 -4
  144. package/runtime/scripts/hfs/rules/services.mjs +220 -0
  145. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/spec-placement.mjs +1 -1
  146. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/stacks.mjs +5 -5
  147. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +89 -0
  148. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/test-topology.mjs +2 -2
  149. package/runtime/scripts/hfs/secret.mjs +175 -0
  150. package/runtime/scripts/{lib/hfs-slots.mjs → hfs/slots.mjs} +114 -150
  151. package/runtime/scripts/hfs/sql/pg-parse.mjs +46 -0
  152. package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
  153. package/runtime/scripts/{lib/hfs-tree.mjs → hfs/tree.mjs} +6 -5
  154. package/runtime/scripts/{checks → hfs}/typescript-programs.mjs +0 -5
  155. package/runtime/scripts/{lib/hfs-view.mjs → hfs/view.mjs} +6 -5
  156. package/runtime/scripts/lib/dockerfile.mjs +124 -0
  157. package/runtime/scripts/lib/env.mjs +10 -0
  158. package/runtime/scripts/lib/event-contract.mjs +103 -0
  159. package/runtime/scripts/lib/git.mjs +36 -0
  160. package/runtime/scripts/lib/graphql-contract.mjs +429 -0
  161. package/runtime/scripts/lib/i18n.mjs +86 -0
  162. package/runtime/scripts/lib/is-main.mjs +13 -0
  163. package/runtime/scripts/lib/json.mjs +13 -3
  164. package/runtime/scripts/lib/language.mjs +26 -7
  165. package/runtime/scripts/lib/list.mjs +20 -0
  166. package/runtime/scripts/lib/package-at.mjs +37 -0
  167. package/runtime/scripts/lib/path-key.mjs +59 -2
  168. package/runtime/scripts/lib/read-text.mjs +34 -0
  169. package/runtime/scripts/lib/regex.mjs +4 -0
  170. package/runtime/scripts/lib/same-text.mjs +5 -0
  171. package/runtime/scripts/lib/secret-patterns.mjs +4 -3
  172. package/runtime/scripts/lib/sleep-sync.mjs +2 -1
  173. package/runtime/scripts/lib/sops-envelope.mjs +58 -0
  174. package/runtime/scripts/lib/stack-declaration.mjs +2 -2
  175. package/runtime/scripts/lib/stack-services.mjs +7 -31
  176. package/runtime/scripts/lib/ts-ast.mjs +35 -0
  177. package/runtime/scripts/{checks/common.mjs → lib/walk.mjs} +5 -15
  178. package/scaffold/add-app.mjs +174 -0
  179. package/scaffold/add-cli-lite.mjs +257 -0
  180. package/scaffold/add-inbox-lite.mjs +314 -0
  181. package/scaffold/add-table.mjs +344 -0
  182. package/scaffold/add.mjs +299 -0
  183. package/scaffold/app.mjs +359 -70
  184. package/scaffold/edition-gate.mjs +25 -0
  185. package/scaffold/image.mjs +19 -0
  186. package/scaffold/lite-exports.mjs +186 -0
  187. package/scaffold/service.mjs +33 -27
  188. package/scaffold/supabase-ports.mjs +26 -0
  189. package/src/main.mjs +327 -0
  190. package/sync/cli.mjs +6 -9
  191. package/sync/format.mjs +1 -1
  192. package/sync/hygiene.mjs +17 -17
  193. package/sync/index.mjs +171 -71
  194. package/sync/managed.mjs +19 -11
  195. package/sync/sonar-key.mjs +5 -0
  196. package/templates/app/ci-workflows/github/workflows/ci.yml +14 -14
  197. package/templates/app/ci-workflows/github/workflows/images.yml +37 -0
  198. package/templates/app/ci-workflows/sonar-steps.yml +9 -0
  199. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +66 -0
  200. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +31 -0
  201. package/templates/app/ci-workflows-lite/github/workflows/images.yml +37 -0
  202. package/templates/app/codegen-lite/scripts/codegen.mjs +32 -0
  203. package/templates/app/docker-ignore/dockerignore +50 -0
  204. package/templates/app/gitignore-lite +35 -0
  205. package/templates/app/hooks/husky/pre-commit +3 -8
  206. package/templates/app/hooks/husky/pre-push +45 -6
  207. package/templates/app/package-scripts/core.json +8 -0
  208. package/templates/app/package-scripts/package.json +3 -10
  209. package/templates/app/package-scripts-lite/package.json +15 -0
  210. package/templates/app/quality-config/codecov.yml +15 -3
  211. package/templates/app/quality-config/sonar-base.properties +4 -0
  212. package/templates/app/quality-config/sonar-project.properties +2 -5
  213. package/templates/app/quality-config-lite/sonar-project.properties +5 -0
  214. package/templates/app/skeleton/.starcistacks/dev/seeds/primary-notes.sql +5 -0
  215. package/templates/app/skeleton/.starciwork/features/system-health/index.yaml +4 -0
  216. package/templates/app/skeleton/.starciwork/{features/index.yaml → index.yaml} +2 -2
  217. package/templates/app/skeleton/README.md +7 -5
  218. package/templates/app/skeleton-lite/.sops.yaml +3 -0
  219. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +5 -0
  220. package/templates/app/skeleton-lite/.starcistacks/dev/secrets/.gitkeep +1 -0
  221. package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +55 -0
  222. package/templates/app/skeleton-lite/README.md +40 -0
  223. package/templates/app/skeleton-lite/supabase/config.toml +41 -0
  224. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +56 -0
  225. package/templates/app/skeleton-lite/supabase/seed.sql +0 -0
  226. package/templates/app/task-graph/turbo.json +23 -0
  227. package/templates/app/tool-config-lite/prettierignore +2 -0
  228. package/templates/app/tool-config-lite/prettierrc +1 -0
  229. package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +38 -0
  230. package/templates/app/upgrade-full/.starcistacks/dev/README.md +10 -0
  231. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +13 -0
  232. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +3 -0
  233. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +12 -0
  234. package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +7 -0
  235. package/templates/be/common/main-listen.ts.partial +14 -0
  236. package/templates/be/common/sql-ident.ts.partial +7 -0
  237. package/templates/be/common/sql-tag.ts.partial +5 -0
  238. package/templates/be/image/api/Dockerfile +38 -0
  239. package/templates/be/image/cli/Dockerfile +38 -0
  240. package/templates/be/image/worker/Dockerfile +36 -0
  241. package/templates/be/inbox/database-client.ts.tpl +50 -0
  242. package/templates/be/inbox/index.ts.tpl +2 -0
  243. package/templates/be/inbox/migration.sql.tpl +29 -0
  244. package/templates/be/inbox/module-definition.ts.tpl +6 -0
  245. package/templates/be/inbox/module.ts.tpl +14 -0
  246. package/templates/be/inbox/options.ts.tpl +2 -0
  247. package/templates/be/inbox/service.ts.tpl +33 -0
  248. package/templates/be/inbox/sql.ts.tpl +16 -0
  249. package/templates/be/inbox/webhook-door.ts.tpl +36 -0
  250. package/templates/be/patterns/api/application.command.http.ts.tpl +10 -0
  251. package/templates/be/patterns/api/application.command.ts.tpl +10 -0
  252. package/templates/be/patterns/api/application.contracts.ts.tpl +11 -0
  253. package/templates/be/patterns/api/application.handler.http.ts.tpl +22 -0
  254. package/templates/be/patterns/api/application.handler.ts.tpl +22 -0
  255. package/templates/be/patterns/api/feature.index.http.ts.tpl +1 -0
  256. package/templates/be/patterns/api/feature.index.ts.tpl +1 -0
  257. package/templates/be/patterns/api/feature.module.ts.tpl +6 -0
  258. package/templates/be/patterns/api/graphql.input.ts.tpl +13 -0
  259. package/templates/be/patterns/api/graphql.mapper.ts.tpl +9 -0
  260. package/templates/be/patterns/api/graphql.module.ts.tpl +7 -0
  261. package/templates/be/patterns/api/graphql.resolver.ts.tpl +27 -0
  262. package/templates/be/patterns/api/graphql.type.ts.tpl +9 -0
  263. package/templates/be/patterns/api/http.controller.ts.tpl +27 -0
  264. package/templates/be/patterns/api/http.mapper.ts.tpl +9 -0
  265. package/templates/be/patterns/api/http.module.ts.tpl +7 -0
  266. package/templates/be/patterns/api/http.request.ts.tpl +10 -0
  267. package/templates/be/patterns/api/http.response.ts.tpl +5 -0
  268. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +15 -0
  269. package/templates/be/patterns/cli/group.cli.ts.tpl +11 -0
  270. package/templates/be/patterns/cli/group.module.ts.tpl +7 -0
  271. package/templates/be/patterns/cli/sub.run.cli.spec.ts.tpl +17 -0
  272. package/templates/be/patterns/cli/sub.run.cli.ts.tpl +15 -0
  273. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +22 -0
  274. package/templates/be/patterns/event-bus/platform/event-bus.config.ts.tpl +19 -0
  275. package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +58 -0
  276. package/templates/be/patterns/event-bus/platform/event-bus.decorators.ts.tpl +46 -0
  277. package/templates/be/patterns/event-bus/platform/event-bus.log-events.ts.tpl +12 -0
  278. package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +10 -0
  279. package/templates/be/patterns/event-bus/platform/event-bus.module.ts.tpl +52 -0
  280. package/templates/be/patterns/event-bus/platform/event-bus.options.ts.tpl +19 -0
  281. package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +28 -0
  282. package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +165 -0
  283. package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +93 -0
  284. package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +90 -0
  285. package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +58 -0
  286. package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +136 -0
  287. package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +56 -0
  288. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +272 -0
  289. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +128 -0
  290. package/templates/be/patterns/event-bus/platform/event-transport.port.ts.tpl +180 -0
  291. package/templates/be/patterns/event-bus/platform/event.policy.spec.ts.tpl +67 -0
  292. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +38 -0
  293. package/templates/be/patterns/event-bus/platform/index.ts.tpl +11 -0
  294. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.spec.ts.tpl +348 -0
  295. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +241 -0
  296. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +15 -0
  297. package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +8 -0
  298. package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +37 -0
  299. package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +11 -0
  300. package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +12 -0
  301. package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +26 -0
  302. package/templates/be/patterns/jobs/job.index.ts.tpl +1 -0
  303. package/templates/be/patterns/jobs/job.module.ts.tpl +6 -0
  304. package/templates/be/patterns/jobs/job.processor.ts.tpl +20 -0
  305. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +20 -0
  306. package/templates/be/patterns/jobs/job.step.ts.tpl +19 -0
  307. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +31 -0
  308. package/templates/be/patterns/jobs/platform/fenced.processor.ts.tpl +13 -0
  309. package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +150 -0
  310. package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +100 -0
  311. package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +138 -0
  312. package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +69 -0
  313. package/templates/be/patterns/jobs/platform/jobs.config.ts.tpl +11 -0
  314. package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +62 -0
  315. package/templates/be/patterns/jobs/platform/jobs.decorators.ts.tpl +31 -0
  316. package/templates/be/patterns/jobs/platform/jobs.error.ts.tpl +22 -0
  317. package/templates/be/patterns/jobs/platform/jobs.index.ts.tpl +11 -0
  318. package/templates/be/patterns/jobs/platform/jobs.log-events.ts.tpl +9 -0
  319. package/templates/be/patterns/jobs/platform/jobs.messages.ts.tpl +15 -0
  320. package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +10 -0
  321. package/templates/be/patterns/jobs/platform/jobs.module.ts.tpl +32 -0
  322. package/templates/be/patterns/jobs/platform/jobs.options.ts.tpl +11 -0
  323. package/templates/be/patterns/jobs/platform/jobs.port.ts.tpl +36 -0
  324. package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +8 -0
  325. package/templates/be/patterns/jobs/platform/persistence.job.entity.ts.tpl +53 -0
  326. package/templates/be/patterns/jobs/platform/persistence.jobs.rows.ts.tpl +13 -0
  327. package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +33 -0
  328. package/templates/be/patterns/jobs/platform/persistence.migration.ts.tpl +29 -0
  329. package/templates/be/patterns/outbox/platform/index.ts.tpl +2 -0
  330. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +96 -0
  331. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +78 -0
  332. package/templates/be/patterns/outbox/platform/outbox.log-events.ts.tpl +5 -0
  333. package/templates/be/patterns/projections/migration.ts.tpl +18 -0
  334. package/templates/be/patterns/projections/persistence.connection.ts.tpl +8 -0
  335. package/templates/be/patterns/projections/projection.entity.ts.tpl +9 -0
  336. package/templates/be/patterns/projections/projection.index.ts.tpl +3 -0
  337. package/templates/be/patterns/projections/projection.module-definition.ts.tpl +7 -0
  338. package/templates/be/patterns/projections/projection.module.ts.tpl +18 -0
  339. package/templates/be/patterns/projections/projection.options.ts.tpl +2 -0
  340. package/templates/be/patterns/projections/projection.projection.spec.ts.tpl +55 -0
  341. package/templates/be/patterns/projections/projection.projection.ts.tpl +21 -0
  342. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +142 -0
  343. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +81 -0
  344. package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +8 -0
  345. package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +25 -0
  346. package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +25 -0
  347. package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +9 -0
  348. package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +12 -0
  349. package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +48 -0
  350. package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +17 -0
  351. package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +128 -0
  352. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +48 -0
  353. package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +115 -0
  354. package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +91 -0
  355. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +45 -0
  356. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +15 -0
  357. package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +31 -0
  358. package/templates/be/patterns/queues/platform/queue.decorators.ts.tpl +45 -0
  359. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +7 -0
  360. package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +5 -0
  361. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +10 -0
  362. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +62 -0
  363. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +22 -0
  364. package/templates/be/patterns/queues/platform/queue.policy.spec.ts.tpl +19 -0
  365. package/templates/be/patterns/queues/platform/queue.policy.ts.tpl +11 -0
  366. package/templates/be/patterns/queues/platform/queue.port.ts.tpl +14 -0
  367. package/templates/be/patterns/queues/queue.index.ts.tpl +3 -0
  368. package/templates/be/patterns/queues/queue.module.ts.tpl +6 -0
  369. package/templates/be/patterns/queues/queue.queue.ts.tpl +23 -0
  370. package/templates/be/patterns/reactors/application.command.ts.tpl +10 -0
  371. package/templates/be/patterns/reactors/application.contracts.ts.tpl +12 -0
  372. package/templates/be/patterns/reactors/application.handler.ts.tpl +22 -0
  373. package/templates/be/patterns/reactors/consumer.ts.tpl +19 -0
  374. package/templates/be/patterns/reactors/message.module.ts.tpl +20 -0
  375. package/templates/be/patterns/reactors/reactor.index.ts.tpl +1 -0
  376. package/templates/be/patterns/reactors/reactor.module.ts.tpl +6 -0
  377. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +6 -0
  378. package/templates/be/patterns/realtime/channel.index.ts.tpl +1 -0
  379. package/templates/be/patterns/realtime/channel.input.ts.tpl +12 -0
  380. package/templates/be/patterns/realtime/channel.subscription.spec.ts.tpl +51 -0
  381. package/templates/be/patterns/realtime/channel.subscription.ts.tpl +32 -0
  382. package/templates/be/patterns/realtime/channel.type.ts.tpl +9 -0
  383. package/templates/be/patterns/saga/saga.compensate.command.ts.tpl +11 -0
  384. package/templates/be/patterns/saga/saga.compensate.contracts.ts.tpl +7 -0
  385. package/templates/be/patterns/saga/saga.compensate.handler.ts.tpl +22 -0
  386. package/templates/be/patterns/saga/saga.compensation.ts.tpl +21 -0
  387. package/templates/be/patterns/saga/saga.complete.command.ts.tpl +11 -0
  388. package/templates/be/patterns/saga/saga.complete.contracts.ts.tpl +7 -0
  389. package/templates/be/patterns/saga/saga.complete.handler.ts.tpl +22 -0
  390. package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +21 -0
  391. package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +21 -0
  392. package/templates/be/patterns/saga/saga.index.ts.tpl +1 -0
  393. package/templates/be/patterns/saga/saga.message.module.ts.tpl +23 -0
  394. package/templates/be/patterns/saga/saga.module.ts.tpl +20 -0
  395. package/templates/be/patterns/saga/saga.service.spec.ts.tpl +70 -0
  396. package/templates/be/patterns/saga/saga.service.ts.tpl +44 -0
  397. package/templates/be/patterns/saga/saga.state.ts.tpl +14 -0
  398. package/templates/be/patterns/saga/saga.step.ts.tpl +11 -0
  399. package/templates/be/patterns/saga/saga.undo.command.ts.tpl +10 -0
  400. package/templates/be/patterns/saga/saga.undo.contracts.ts.tpl +5 -0
  401. package/templates/be/patterns/saga/saga.undo.handler.ts.tpl +21 -0
  402. package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +72 -0
  403. package/templates/be/patterns/webhooks/webhook.door.ts.tpl +36 -0
  404. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +6 -0
  405. package/templates/be/patterns/webhooks/webhook.index.ts.tpl +1 -0
  406. package/templates/be/patterns/webhooks/webhook.request.ts.tpl +9 -0
  407. package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
  408. package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -2
  409. package/templates/be/skeleton/apps/__app__/src/main.ts +7 -15
  410. package/templates/be/skeleton/apps/cli/src/app.module.ts +30 -0
  411. package/templates/be/skeleton/apps/cli/src/cli.options.ts +22 -0
  412. package/templates/be/skeleton/apps/cli/src/main.ts +24 -0
  413. package/templates/be/skeleton/src/features/cli/cli.module.ts +7 -0
  414. package/templates/be/skeleton/src/features/cli/index.ts +1 -0
  415. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +17 -0
  416. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +15 -0
  417. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +7 -0
  418. package/templates/be/skeleton/src/features/cli/migrate/subs/run.cli.spec.ts +17 -0
  419. package/templates/be/skeleton/src/features/cli/migrate/subs/run.cli.ts +18 -0
  420. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +17 -0
  421. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +15 -0
  422. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +7 -0
  423. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +33 -0
  424. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.ts +34 -0
  425. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +13 -0
  426. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +40 -0
  427. package/templates/be/skeleton/src/modules/domain/identity/messages/identity.messages.ts +1 -1
  428. package/templates/be/skeleton/src/modules/domain/note/errors/note.error.ts +16 -0
  429. package/templates/be/skeleton/src/modules/domain/note/index.ts +3 -0
  430. package/templates/be/skeleton/src/modules/domain/note/note.contracts.ts +19 -0
  431. package/templates/be/skeleton/src/modules/domain/note/note.decorators.ts +9 -0
  432. package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +7 -0
  433. package/templates/be/skeleton/src/modules/domain/note/note.module.ts +19 -0
  434. package/templates/be/skeleton/src/modules/domain/note/note.options.ts +2 -0
  435. package/templates/be/skeleton/src/modules/domain/note/note.service.spec.ts +76 -0
  436. package/templates/be/skeleton/src/modules/domain/note/note.service.ts +37 -0
  437. package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +8 -0
  438. package/templates/be/skeleton/src/modules/domain/note/persistence/entities/note.entity.ts +17 -0
  439. package/templates/be/skeleton/src/modules/domain/note/persistence/migrations/1790000000000-init-note.ts +22 -0
  440. package/templates/be/skeleton/src/modules/domain/note/persistence/note.rows.ts +17 -0
  441. package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +4 -0
  442. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +20 -0
  443. package/templates/be/skeleton/src/modules/platform/config/index.ts +1 -1
  444. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +18 -0
  445. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +41 -0
  446. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.ts +17 -0
  447. package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +13 -0
  448. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +77 -0
  449. package/templates/be/skeleton/src/modules/platform/database/database.options.ts +30 -0
  450. package/templates/be/skeleton/src/modules/platform/database/database.port.ts +65 -0
  451. package/templates/be/skeleton/src/modules/platform/database/database.sql.ts +47 -0
  452. package/templates/be/skeleton/src/modules/platform/database/errors/database.error.ts +16 -0
  453. package/templates/be/skeleton/src/modules/platform/database/index.ts +9 -0
  454. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +30 -0
  455. package/templates/be/skeleton/src/modules/platform/database/migration-runner.service.spec.ts +84 -0
  456. package/templates/be/skeleton/src/modules/platform/database/migration-runner.service.ts +23 -0
  457. package/templates/be/skeleton/src/modules/platform/database/primary.config.ts +9 -0
  458. package/templates/be/skeleton/src/modules/platform/database/primary.connection.ts +2 -0
  459. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +10 -0
  460. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +108 -0
  461. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +75 -0
  462. package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.spec.ts +88 -0
  463. package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.ts +29 -0
  464. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +48 -0
  465. package/templates/be/skeleton/src/modules/platform/errors/messages/errors.messages.ts +1 -1
  466. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +12 -0
  467. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -0
  468. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +8 -1
  469. package/templates/be/skeleton/src/modules/platform/http-security/http-security.decorators.ts +8 -0
  470. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +14 -1
  471. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +12 -0
  472. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +10 -2
  473. package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.spec.ts +63 -0
  474. package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +2 -2
  475. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +108 -0
  476. package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.spec.ts +39 -0
  477. package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.ts +25 -0
  478. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +172 -0
  479. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.ts +66 -0
  480. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +3 -1
  481. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +52 -0
  482. package/templates/be/skeleton-lite/apps/api/Dockerfile +37 -0
  483. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +16 -0
  484. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +81 -0
  485. package/templates/be/skeleton-lite/apps/api/src/main.ts +34 -0
  486. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +14 -0
  487. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +50 -0
  488. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +26 -0
  489. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +19 -0
  490. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +26 -0
  491. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +7 -0
  492. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +6 -0
  493. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +19 -0
  494. package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +10 -0
  495. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +24 -0
  496. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +8 -0
  497. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +17 -0
  498. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +32 -0
  499. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +24 -0
  500. package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +4 -0
  501. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +54 -0
  502. package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +23 -0
  503. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +10 -0
  504. package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +49 -0
  505. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +4 -0
  506. package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +21 -0
  507. package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +11 -0
  508. package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +28 -0
  509. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +25 -0
  510. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +9 -0
  511. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +4 -0
  512. package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +5 -0
  513. package/templates/be/table/error.ts.tpl +16 -0
  514. package/templates/be/table/index.ts.tpl +2 -0
  515. package/templates/be/table/migration.sql.tpl +51 -0
  516. package/templates/be/table/module-definition.ts.tpl +7 -0
  517. package/templates/be/table/module.ts.tpl +14 -0
  518. package/templates/be/table/options.ts.tpl +2 -0
  519. package/templates/be/table/rows.ts.tpl +4 -0
  520. package/templates/be/table/service.ts.tpl +23 -0
  521. package/templates/be/table/sql.ts.tpl +4 -0
  522. package/templates/be/tool-config/jest.config.js +3 -1
  523. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +14 -0
  524. package/templates/be/upgrade-full/apps/cli/src/main.ts +26 -0
  525. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +6 -0
  526. package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +23 -0
  527. package/templates/common/gitignore.base +1 -2
  528. package/templates/fe/app/package.json.tpl +1 -0
  529. package/templates/fe/app/tsconfig.json +12 -0
  530. package/templates/fe/common/app-layout.tsx.partial +8 -0
  531. package/templates/fe/common/site-shell.tsx.partial +9 -0
  532. package/templates/fe/image/next/Dockerfile +42 -0
  533. package/templates/fe/skeleton/apps/app/next.config.ts +23 -0
  534. package/templates/fe/skeleton/apps/app/postcss.config.mjs +7 -0
  535. package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/error.tsx +3 -3
  536. package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +22 -0
  537. package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/loading.tsx +2 -2
  538. package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/not-found.tsx +2 -2
  539. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +9 -0
  540. package/templates/fe/skeleton/apps/{__app__ → app}/src/app/global-error.tsx +2 -2
  541. package/templates/fe/skeleton/apps/app/src/app/globals.css +10 -0
  542. package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +18 -0
  543. package/templates/fe/skeleton/apps/app/src/features/pages/AppErrorPage/component.tsx +19 -0
  544. package/templates/fe/skeleton/apps/app/src/features/pages/AppErrorPage/index.tsx +11 -0
  545. package/templates/fe/skeleton/apps/app/src/features/pages/AppGlobalErrorPage/index.tsx +16 -0
  546. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/component.tsx +10 -0
  547. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +15 -0
  548. package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +8 -0
  549. package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +9 -0
  550. package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +4 -0
  551. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +28 -0
  552. package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -0
  553. package/templates/fe/skeleton/apps/app/src/modules/routes/index.ts +4 -0
  554. package/templates/fe/skeleton/apps/app/src/proxy.ts +1 -0
  555. package/templates/fe/skeleton/apps/landing/next.config.ts +21 -0
  556. package/templates/fe/skeleton/apps/landing/postcss.config.mjs +7 -0
  557. package/templates/fe/skeleton/apps/landing/src/app/[locale]/error.tsx +10 -0
  558. package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +22 -0
  559. package/templates/fe/skeleton/apps/landing/src/app/[locale]/loading.tsx +6 -0
  560. package/templates/fe/skeleton/apps/landing/src/app/[locale]/not-found.tsx +6 -0
  561. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +9 -0
  562. package/templates/fe/skeleton/apps/landing/src/app/global-error.tsx +10 -0
  563. package/templates/fe/skeleton/apps/landing/src/app/globals.css +10 -0
  564. package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +24 -0
  565. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingErrorPage/component.tsx +18 -0
  566. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingErrorPage/index.tsx +11 -0
  567. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingGlobalErrorPage/index.tsx +16 -0
  568. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +8 -0
  569. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +9 -0
  570. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/component.tsx +24 -0
  571. package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +16 -0
  572. package/templates/fe/skeleton/apps/landing/src/modules/config/index.ts +12 -0
  573. package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +7 -0
  574. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/vi.json +31 -0
  575. package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -0
  576. package/templates/fe/skeleton/apps/landing/src/modules/routes/index.ts +4 -0
  577. package/templates/fe/skeleton/apps/landing/src/proxy.ts +1 -0
  578. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +49 -0
  579. package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +24 -0
  580. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +20 -0
  581. package/templates/fe/skeleton/packages/__project__-ui/src/branches/Providers/index.tsx +23 -0
  582. package/templates/fe/skeleton/packages/__project__-ui/src/composites/ErrorNotice/index.tsx +14 -0
  583. package/templates/fe/skeleton/packages/__project__-ui/src/composites/GlobalErrorNotice/index.tsx +25 -0
  584. package/templates/fe/skeleton/packages/__project__-ui/src/composites/LoadingNotice/index.tsx +9 -0
  585. package/templates/fe/skeleton/packages/__project__-ui/src/composites/LocaleShell/index.tsx +21 -0
  586. package/templates/fe/skeleton/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +19 -0
  587. package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/classNames.ts +4 -0
  588. package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/index.tsx +19 -0
  589. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +6 -0
  590. package/templates/fe/skeleton-lite/apps/web/next.config.ts +22 -0
  591. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/error.tsx +10 -0
  592. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +37 -0
  593. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/loading.tsx +6 -0
  594. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/not-found.tsx +6 -0
  595. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +9 -0
  596. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/providers.tsx +26 -0
  597. package/templates/fe/skeleton-lite/apps/web/src/app/auth/callback/route.ts +24 -0
  598. package/templates/fe/skeleton-lite/apps/web/src/app/global-error.tsx +10 -0
  599. package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/src/app/globals.css +1 -1
  600. package/templates/fe/skeleton-lite/apps/web/src/app/health/live/route.ts +5 -0
  601. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/component.tsx +29 -0
  602. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +41 -0
  603. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/component.tsx +17 -0
  604. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/index.tsx +38 -0
  605. package/templates/fe/skeleton-lite/apps/web/src/components/composites/ErrorNotice/index.tsx +13 -0
  606. package/templates/fe/skeleton-lite/apps/web/src/components/composites/GlobalErrorNotice/index.tsx +24 -0
  607. package/templates/fe/skeleton-lite/apps/web/src/components/composites/LoadingNotice/index.tsx +7 -0
  608. package/templates/fe/skeleton-lite/apps/web/src/components/composites/NotFoundNotice/index.tsx +18 -0
  609. package/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/classNames.ts +4 -0
  610. package/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/index.tsx +13 -0
  611. package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +31 -0
  612. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppErrorPage/component.tsx +12 -0
  613. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppErrorPage/index.tsx +10 -0
  614. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppGlobalErrorPage/index.tsx +15 -0
  615. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +14 -0
  616. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +30 -0
  617. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +8 -0
  618. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +9 -0
  619. package/templates/fe/skeleton-lite/apps/web/src/modules/brand/brand.css +20 -0
  620. package/templates/fe/skeleton-lite/apps/web/src/modules/brand/index.ts +2 -0
  621. package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +20 -0
  622. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/read-session.ts +25 -0
  623. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-in.ts +25 -0
  624. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +17 -0
  625. package/templates/fe/skeleton-lite/apps/web/src/modules/db/browser.ts +14 -0
  626. package/templates/fe/skeleton-lite/apps/web/src/modules/db/index.ts +3 -0
  627. package/templates/fe/skeleton-lite/apps/web/src/modules/db/outcome.ts +59 -0
  628. package/templates/fe/skeleton-lite/apps/web/src/modules/db/principal.ts +29 -0
  629. package/templates/fe/skeleton-lite/apps/web/src/modules/db/server.ts +70 -0
  630. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +50 -0
  631. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +8 -0
  632. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +48 -0
  633. package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/src/modules/i18n/request.ts +4 -2
  634. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +14 -0
  635. package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +7 -0
  636. package/templates/fe/skeleton-lite/apps/web/src/modules/routes/safe-next-path.ts +26 -0
  637. package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +43 -0
  638. package/templates/fe/table/read.ts.tpl +16 -0
  639. package/templates/fe/table/write.ts.tpl +16 -0
  640. package/upgrade/check.mjs +137 -0
  641. package/upgrade/index.mjs +524 -0
  642. package/bin/hfs.mjs +0 -233
  643. package/runtime/engine/admission.mjs +0 -284
  644. package/runtime/engine/digest.mjs +0 -10
  645. package/runtime/engine/ledger-db.mjs +0 -1245
  646. package/runtime/engine/machine-db.mjs +0 -1565
  647. package/runtime/engine/migrations/machine/0001-init.sql +0 -887
  648. package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +0 -13
  649. package/runtime/engine/migrations/machine/0003-worktrees-workflow-orca.sql +0 -21
  650. package/runtime/engine/migrations/runtime/0001-init.sql +0 -1072
  651. package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +0 -13
  652. package/runtime/engine/migrations/runtime/0004-attempt-why.sql +0 -43
  653. package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +0 -93
  654. package/runtime/scripts/api/fs/rmdir-link.mjs +0 -12
  655. package/runtime/scripts/lib/artifact-hold.mjs +0 -89
  656. package/runtime/scripts/lib/artifact-store.mjs +0 -103
  657. package/runtime/scripts/lib/redact.mjs +0 -148
  658. package/runtime/scripts/lib/safe-remove.mjs +0 -275
  659. package/runtime/scripts/lib/test-secrets.mjs +0 -116
  660. package/templates/fe/skeleton/apps/__app__/next.config.ts +0 -11
  661. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +0 -20
  662. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +0 -9
  663. package/templates/fe/skeleton/apps/__app__/src/components/composites/FailureScreen/index.tsx +0 -28
  664. package/templates/fe/skeleton/apps/__app__/src/features/layouts/LocaleShell/index.tsx +0 -35
  665. package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +0 -21
  666. package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +0 -17
  667. package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +0 -26
  668. package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +0 -15
  669. package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +0 -25
  670. package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +0 -15
  671. package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +0 -27
  672. package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +0 -8
  673. package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +0 -32
  674. package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +0 -8
  675. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/config.ts +0 -8
  676. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/index.ts +0 -2
  677. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +0 -25
  678. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.ts +0 -9
  679. package/templates/fe/skeleton/apps/__app__/src/modules/routes/index.ts +0 -4
  680. package/templates/fe/skeleton/apps/__app__/src/proxy.ts +0 -10
  681. /package/runtime/scripts/{checks → hfs}/architecture/config-unread.mjs +0 -0
  682. /package/runtime/scripts/{checks → hfs}/architecture/constructor-deps.mjs +0 -0
  683. /package/runtime/scripts/{checks → hfs}/architecture/cross-app-duplicate.mjs +0 -0
  684. /package/runtime/scripts/{checks → hfs}/architecture/default-deny.mjs +0 -0
  685. /package/runtime/scripts/{checks → hfs}/architecture/error-codes.mjs +0 -0
  686. /package/runtime/scripts/{checks → hfs}/architecture/error-masked.mjs +0 -0
  687. /package/runtime/scripts/{checks → hfs}/architecture/hooks-are-hooks.mjs +0 -0
  688. /package/runtime/scripts/{checks → hfs}/architecture/injection-token-exported.mjs +0 -0
  689. /package/runtime/scripts/{checks → hfs}/architecture/package-shape.mjs +0 -0
  690. /package/runtime/scripts/{checks → hfs}/architecture/reachability.mjs +0 -0
  691. /package/runtime/scripts/{checks → hfs}/architecture/route-files-thin.mjs +0 -0
  692. /package/runtime/scripts/{checks → hfs}/architecture/sql-tokens.mjs +0 -0
  693. /package/runtime/scripts/{checks → hfs}/architecture/symbols.mjs +0 -0
  694. /package/runtime/scripts/{checks → hfs}/architecture/unit-spec-providers.mjs +0 -0
  695. /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.contracts.ts +0 -0
  696. /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.handler.ts +0 -0
  697. /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.query.ts +0 -0
  698. /package/templates/be/skeleton/src/features/{system-health → api/system-health}/index.ts +0 -0
  699. /package/templates/be/skeleton/src/features/{system-health → api/system-health}/system-health.module.ts +0 -0
  700. /package/templates/be/skeleton/src/features/{system-health → api/system-health}/transport/http/live.controller.ts +0 -0
  701. /package/templates/be/skeleton/src/features/{system-health → api/system-health}/transport/http/system-health-http.module.ts +0 -0
  702. /package/templates/fe/skeleton/apps/{__app__ → app}/src/app/health/live/route.ts +0 -0
  703. /package/templates/fe/skeleton/apps/{__app__ → app}/src/modules/config/index.ts +0 -0
  704. /package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/postcss.config.mjs +0 -0
@@ -0,0 +1,170 @@
1
+ // saga.mjs - the saga pattern of a product that declares `patterns: ["saga"]` in hfs.json (R169 to R173). A saga is a feature of the saga kind
2
+ // be/src/features/saga/<saga>/
3
+ // <saga>.saga.service.ts the orchestrator: the list of its steps and compensations (a service, unit-tested beside it)
4
+ // <saga>.saga-state.ts the persisted state of a run, typed with its `status` and its `version` fence
5
+ // steps/<step>.saga-step.ts one step, dispatching one command, naming the event it puts on the wire
6
+ // compensations/<step>.compensation.ts the compensation of that step, naming the failure event that triggers it
7
+ // be/src/features/saga/<saga>/transport/message/<event>.consumer.ts the consumers that advance it, through the inbox
8
+ // be/contracts/<service>/events.json every event, a compensating one declaring `compensates: "<step event>"`
9
+ // Five tree checks (the slot manifest owns the paths: an undeclared saga folder is HFS_SLOT_NOT_ENABLED):
10
+ // R169 BE_SAGA_STEP_COMPENSATION every step has a compensation of the same name and the other way round, and the orchestrator imports them all;
11
+ // R170 BE_SAGA_STATE_VERSIONED every orchestrator has its `.saga-state.ts` whose exported state type carries `status` and a numeric `version`,
12
+ // and imports the fenced store of `platform/saga`;
13
+ // R171 BE_SAGA_EVENT_CONTRACT a step and a compensation each name their event (`readonly event = "<name>"`), both are in a contract, and the
14
+ // contract of the compensation's event says it compensates the step's event;
15
+ // R172 BE_SAGA_CONSUMER_DEDUPE a consumer of a saga feature passes the id of the delivery (`message.eventId`) on to the command, so the inbox can dedupe it;
16
+ // R173 BE_SAGA_E2E_MISSING every compensation path has an e2e spec through the world that names its event and injects a failure
17
+ // (an outage of an infra service, an app or a fake: `during`, `cut`, `latency`, `failNext`, `interruptDatabase`).
18
+ import path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { loadTypescript } from '../../lib/package-at.mjs';
21
+ import { found, readJson, readText } from './read.mjs';
22
+
23
+ const SAGA_STEP_COMPENSATION = 'BE_SAGA_STEP_COMPENSATION';
24
+ const SAGA_STATE_VERSIONED = 'BE_SAGA_STATE_VERSIONED';
25
+ const SAGA_EVENT_CONTRACT = 'BE_SAGA_EVENT_CONTRACT';
26
+ const SAGA_CONSUMER_DEDUPE = 'BE_SAGA_CONSUMER_DEDUPE';
27
+ const SAGA_E2E_MISSING = 'BE_SAGA_E2E_MISSING';
28
+
29
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
30
+ const SAGA_FILE = /^be\/src\/features\/saga\/([^/]+)\/(?:(?:steps|compensations)\/)?[^/]+\.(?:saga\.service|saga-state|saga-step|compensation)\.ts$/;
31
+ const ORCHESTRATOR = /^be\/src\/features\/saga\/([^/]+)\/([^/]+)\.saga\.service\.ts$/;
32
+ const STATE = /^be\/src\/features\/saga\/([^/]+)\/([^/]+)\.saga-state\.ts$/;
33
+ const STEP = /^be\/src\/features\/saga\/([^/]+)\/steps\/([^/]+)\.saga-step\.ts$/;
34
+ const COMPENSATION = /^be\/src\/features\/saga\/([^/]+)\/compensations\/([^/]+)\.compensation\.ts$/;
35
+ const CONSUMER = /^be\/src\/features\/saga\/([^/]+)\/transport\/message\/[^/]+\.consumer\.ts$/;
36
+ const E2E_SPEC = /^be\/src\/tests\/e2e\/[^/]+\/[^/]+\.e2e-spec\.ts$/;
37
+ const CONTRACT = /^be\/contracts\/[^/]+\/events\.json$/;
38
+ const USE_TEST_WORLD = /\buseTestWorld\s*\(/;
39
+ const INJECTION = /\.(?:during|cut|latency|failNext|interruptDatabase)\s*\(/;
40
+ const STORE_MODULE = /(?:^|\/)platform\/saga$/;
41
+
42
+ /** The TypeScript compiler of the app (else of the runtime), or null. */
43
+ const typescriptFor = (repoRoot) => loadTypescript(repoRoot, HERE);
44
+
45
+ const parse = (ts, text) => ts.createSourceFile('saga.ts', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
46
+
47
+ /** The module specifiers a source file imports or re-exports. */
48
+ function specifiersOf(ts, sourceFile) {
49
+ const out = [];
50
+ for (const statement of sourceFile.statements) {
51
+ if ((ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) out.push(statement.moduleSpecifier.text);
52
+ }
53
+ return out;
54
+ }
55
+
56
+ /** The string literal of the class property `event` (`readonly event = "order.placed"`), or null. */
57
+ function eventOf(ts, sourceFile) {
58
+ let name = null;
59
+ const visit = (node) => {
60
+ if (ts.isPropertyDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === 'event' && node.initializer && ts.isStringLiteralLike(node.initializer)) name = node.initializer.text;
61
+ ts.forEachChild(node, visit);
62
+ };
63
+ visit(sourceFile);
64
+ return name;
65
+ }
66
+
67
+ /** Whether an exported interface or type alias of the file declares `status` and a `version: number` member. */
68
+ function statesVersion(ts, sourceFile) {
69
+ let versioned = false;
70
+ const memberNames = (members) => new Map(members.filter((member) => ts.isPropertySignature(member) && ts.isIdentifier(member.name)).map((member) => [member.name.text, member.type]));
71
+ for (const statement of sourceFile.statements) {
72
+ if (!statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue;
73
+ const members = ts.isInterfaceDeclaration(statement) ? statement.members : ts.isTypeAliasDeclaration(statement) && ts.isTypeLiteralNode(statement.type) ? statement.type.members : null;
74
+ if (members === null) continue;
75
+ const named = memberNames(members);
76
+ if (named.has('status') && named.get('version')?.kind === ts.SyntaxKind.NumberKeyword) versioned = true;
77
+ }
78
+ return versioned;
79
+ }
80
+
81
+ /** Whether a source text reads `.eventId` of something (the id of the delivery travels on). */
82
+ function readsEventId(ts, sourceFile) {
83
+ let read = false;
84
+ const visit = (node) => {
85
+ if (ts.isPropertyAccessExpression(node) && node.name.text === 'eventId') read = true;
86
+ ts.forEachChild(node, visit);
87
+ };
88
+ visit(sourceFile);
89
+ return read;
90
+ }
91
+
92
+ /** The sagas of the app: every folder of the saga kind (`be/src/features/saga/<saga>/`). */
93
+ const featuresOf = (files) => [...new Set(files.map((file) => SAGA_FILE.exec(file)?.[1]).filter((feature) => feature !== undefined))].sort();
94
+
95
+ /** Findings of R169 to R173 over the tracked paths `files` of the app at `repoRoot`. */
96
+ export function sagaFindings({ repoRoot, files }) {
97
+ const features = featuresOf(files);
98
+ if (features.length === 0) return [];
99
+ const ts = typescriptFor(repoRoot);
100
+ if (ts === null) return [];
101
+ const findings = [];
102
+ const events = new Map();
103
+ for (const contract of files.filter((file) => CONTRACT.test(file))) {
104
+ for (const [name, event] of Object.entries(readJson(repoRoot, contract)?.events ?? {})) events.set(name, { contract, ...event });
105
+ }
106
+ const specs = files.filter((file) => E2E_SPEC.test(file)).map((file) => ({ file, text: readText(repoRoot, file) ?? '' })).filter(({ text }) => USE_TEST_WORLD.test(text));
107
+ const compensationPaths = [];
108
+ for (const feature of features) {
109
+ const under = (pattern) => files.filter((file) => pattern.test(file) && pattern.exec(file)[1] === feature);
110
+ const steps = new Map(under(STEP).map((file) => [STEP.exec(file)[2], file]));
111
+ const compensations = new Map(under(COMPENSATION).map((file) => [COMPENSATION.exec(file)[2], file]));
112
+ const orchestrators = under(ORCHESTRATOR);
113
+ const states = new Map(under(STATE).map((file) => [STATE.exec(file)[2], file]));
114
+ const folder = `be/src/features/saga/${feature}/`;
115
+ // R169
116
+ for (const [stem, file] of steps) if (!compensations.has(stem)) findings.push(found(SAGA_STEP_COMPENSATION, file, `${file} is a saga step with no compensation: add ${folder}compensations/${stem}.compensation.ts, the command that undoes it; every forward step of a saga has one.`, { step: stem }));
117
+ for (const [stem, file] of compensations) if (!steps.has(stem)) findings.push(found(SAGA_STEP_COMPENSATION, file, `${file} is a compensation of no step: ${folder}steps/${stem}.saga-step.ts does not exist; a compensation undoes the step of the same name.`, { step: stem }));
118
+ if (orchestrators.length === 0 && (steps.size > 0 || compensations.size > 0)) findings.push(found(SAGA_STEP_COMPENSATION, folder, `${folder} holds steps or compensations but no orchestrator: add <saga>.saga.service.ts, the list of the steps and their compensations.`));
119
+ for (const orchestrator of orchestrators) {
120
+ const text = readText(repoRoot, orchestrator);
121
+ if (text === null) continue;
122
+ const sourceFile = parse(ts, text);
123
+ const imported = specifiersOf(ts, sourceFile).map((specifier) => path.posix.normalize(path.posix.join(path.posix.dirname(orchestrator), specifier)));
124
+ for (const file of [...steps.values(), ...compensations.values()]) {
125
+ if (!imported.includes(file.replace(/\.ts$/, ''))) findings.push(found(SAGA_STEP_COMPENSATION, orchestrator, `${orchestrator} does not list ${file}: the orchestrator imports every step and every compensation of its saga, so a step nobody runs cannot hide.`, { step: file }));
126
+ }
127
+ // R170
128
+ const stem = ORCHESTRATOR.exec(orchestrator)[2];
129
+ const stateFile = states.get(stem);
130
+ if (stateFile === undefined) findings.push(found(SAGA_STATE_VERSIONED, orchestrator, `${orchestrator} has no ${folder}${stem}.saga-state.ts: the persisted state of a run is typed there, with its status and its version fence.`, { saga: stem }));
131
+ if (!specifiersOf(ts, sourceFile).some((specifier) => STORE_MODULE.test(specifier))) findings.push(found(SAGA_STATE_VERSIONED, orchestrator, `${orchestrator} does not use the fenced store of platform/saga: a run is persisted and moved only through it (every transition names the version it read), never by a write of its own.`, { saga: stem }));
132
+ }
133
+ for (const [stem, file] of states) {
134
+ const text = readText(repoRoot, file);
135
+ if (text !== null && !statesVersion(ts, parse(ts, text))) findings.push(found(SAGA_STATE_VERSIONED, file, `${file} exports no state type with a \`status\` and a \`version: number\`; a saga run is persisted with a version, the fence that stops a zombie delivery from moving it twice.`, { saga: stem }));
136
+ }
137
+ // R171
138
+ const eventOfFile = new Map();
139
+ for (const [kind, map] of [['step', steps], ['compensation', compensations]]) {
140
+ for (const [stem, file] of map) {
141
+ const text = readText(repoRoot, file);
142
+ const name = text === null ? null : eventOf(ts, parse(ts, text));
143
+ eventOfFile.set(`${kind}:${stem}`, name);
144
+ if (name === null) findings.push(found(SAGA_EVENT_CONTRACT, file, `${file} names no event: a ${kind} of a saga declares \`readonly event = "<name>"\`, the event of the contract it ${kind === 'step' ? 'puts on the wire' : 'answers'}.`, { step: stem }));
145
+ else if (!events.has(name)) findings.push(found(SAGA_EVENT_CONTRACT, file, `${file} names the event "${name}", which no be/contracts/<service>/events.json declares; every event of a saga is in a vendored contract.`, { step: stem, event: name }));
146
+ }
147
+ }
148
+ for (const [stem, file] of compensations) {
149
+ const compensationEvent = eventOfFile.get(`compensation:${stem}`);
150
+ const stepEvent = eventOfFile.get(`step:${stem}`);
151
+ if (compensationEvent && stepEvent && events.has(compensationEvent) && events.get(compensationEvent).compensates !== stepEvent) {
152
+ findings.push(found(SAGA_EVENT_CONTRACT, file, `${file}: the contract of "${compensationEvent}" (${events.get(compensationEvent).contract}) does not declare \`compensates: "${stepEvent}"\`; the failure event of a compensation says which step's event it undoes.`, { step: stem, event: compensationEvent }));
153
+ }
154
+ if (compensationEvent) compensationPaths.push({ file, stem, event: compensationEvent });
155
+ }
156
+ // R172
157
+ for (const file of under(CONSUMER)) {
158
+ const text = readText(repoRoot, file);
159
+ if (text !== null && !readsEventId(ts, parse(ts, text))) findings.push(found(SAGA_CONSUMER_DEDUPE, file, `${file} is a consumer of the saga ${feature} but never passes the id of the delivery on (\`message.eventId\`); the saga takes every event through the inbox, so the consumer hands the id to its command.`, { feature }));
160
+ }
161
+ }
162
+ // R173
163
+ for (const { file, stem, event } of compensationPaths) {
164
+ const named = specs.filter(({ text }) => text.includes(event));
165
+ const expected = `be/src/tests/e2e/<area>/<name>.e2e-spec.ts`;
166
+ if (named.length === 0) findings.push(found(SAGA_E2E_MISSING, file, `${file}: the compensation path of "${event}" has no e2e spec; add ${expected}, which boots the apps with useTestWorld, drives the failure and reads the compensated state back.`, { step: stem, event }));
167
+ else if (!named.some(({ text }) => INJECTION.test(text))) findings.push(found(SAGA_E2E_MISSING, named[0].file, `${named[0].file} names "${event}" but injects no failure; the compensation path is proven with a fault through the world (an outage of an infra service or an app with \`during\`, \`cut\`, \`latency\`, a fake's \`failNext\`, \`interruptDatabase\`), not only the happy flow.`, { step: stem, event }));
168
+ }
169
+ return findings;
170
+ }
@@ -5,11 +5,11 @@
5
5
  // - a tracked `.enc` that is not a sops envelope (an encrypted-looking file that holds plaintext);
6
6
  // - a line of any other tracked text file that matches a secret pattern.
7
7
  // A finding names the file, the line and the pattern, never the value.
8
- import { FORBIDDEN_FILES, secretHits } from '../secret-patterns.mjs';
9
- import { isSopsEnvelope } from '../test-secrets.mjs';
8
+ import { FORBIDDEN_FILES, secretHits } from '../../lib/secret-patterns.mjs';
9
+ import { isSopsEnvelope } from '../../lib/sops-envelope.mjs';
10
10
  import { found, readText } from './read.mjs';
11
11
 
12
- export const PLAINTEXT_SECRET = 'HFS_PLAINTEXT_SECRET';
12
+ const PLAINTEXT_SECRET = 'HFS_PLAINTEXT_SECRET';
13
13
  /** Files whose lines are not secrets: a lockfile carries integrity hashes, never a credential. */
14
14
  const NOT_SCANNED = /(^|\/)package-lock\.json$/;
15
15
 
@@ -19,7 +19,7 @@ export const slotOwnsSecrets = (slot) => slot?.rules?.includes(PLAINTEXT_SECRET)
19
19
  /**
20
20
  * The findings of R06 for one tracked file that no forbidden slot claims: a secret by being (an env file, a key file), an `.enc` that is
21
21
  * no sops envelope, or a line that matches a secret pattern. `text` is the file's text, or null when it cannot be read (binary, too
22
- * large, absent). The whole-tree check reads the work tree and the commit hook (`hfs work-hygiene`) reads the staged blob: one judgement.
22
+ * large, absent). The whole-tree check reads the work tree and the commit hook (`starci app hygiene`) reads the staged blob: one judgement.
23
23
  */
24
24
  export function secretFileFindings({ file, text }) {
25
25
  const being = FORBIDDEN_FILES.find((rule) => rule.test(file));
@@ -0,0 +1,220 @@
1
+ // services.mjs - the microservice policy of a product that runs more than one service (R163 to R167).
2
+ // A product keeps every back-end service as a Nest app at `be/apps/<service>/` of the one repository, sharing the `be/src` libraries and
3
+ // the one package.json; the services it does not own stay pinned images in the stack. Five tree checks:
4
+ // R163 HFS_SERVICE_PLACEMENT a service root (a Dockerfile, or a package.json other than the app's own and a workspace package's)
5
+ // exists only at be/apps/<service>/ (fe/apps/<app>/Dockerfile is a front end's);
6
+ // R164 HFS_IMAGE_UNPINNED in a multi-service product every component image of application-stacks.yaml is a digest or an exact
7
+ // version (x.y.z), never a missing tag, `latest`, a branch word or a major/minor-only tag;
8
+ // R165 HFS_SERVICE_STACK_DECLARATION each api or worker app of a multi-service product is a `role: service` component of the stack;
9
+ // R166 HFS_EVENT_CONTRACT the async contract is vendored: `be/contracts/<service>/events.json` equals the provider's
10
+ // `apps/<service>/src/events.ts` table (an event may declare `compensates: "<event>"`), every event a
11
+ // consumer's `apps/<app>/src/consumes.ts` names exists in that snapshot at the same version;
12
+ // R167 BE_ASYNC_SPEC_MISSING every consumed event (a consumes.ts entry) is named by an e2e spec through `useTestWorld`; the spec of a
13
+ // saga step (an event whose contract says `compensates`) also names the compensated event, so the whole
14
+ // flow is driven, not the undo alone.
15
+ // "Multi-service" = two or more be apps of kind api or worker. A single-service product is judged by R163 and R166-R167 only.
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { findStackDeclaration } from '../../lib/stack-declaration.mjs';
19
+ import { loadTypescript } from '../../lib/package-at.mjs';
20
+ import { propertyText } from '../../lib/ts-ast.mjs';
21
+ import { folded, readEventClasses, snapshotText } from '../../lib/event-contract.mjs';
22
+ import { found, readText } from './read.mjs';
23
+
24
+ const SERVICE_PLACEMENT = 'HFS_SERVICE_PLACEMENT';
25
+ const IMAGE_UNPINNED = 'HFS_IMAGE_UNPINNED';
26
+ const SERVICE_STACK_DECLARATION = 'HFS_SERVICE_STACK_DECLARATION';
27
+ const EVENT_CONTRACT = 'HFS_EVENT_CONTRACT';
28
+ const ASYNC_SPEC_MISSING = 'BE_ASYNC_SPEC_MISSING';
29
+
30
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
31
+ const SERVICE_KINDS = new Set(['api', 'worker']);
32
+ const BE_SERVICE_FILE = /^be\/apps\/([^/]+)\/(.+)$/;
33
+ const DOCKERFILE = /(?:^|\/)Dockerfile(?:\.[^/]+)?$/;
34
+ const WORKSPACE_PACKAGE_JSON = /^fe\/(?:packages|apps)\/[^/]+\/package\.json$/;
35
+ const SNAPSHOT = /^be\/contracts\/([^/]+)\/events\.json$/;
36
+ const DIGEST = /@sha256:[0-9a-f]{64}$/;
37
+ const EXACT_TAG = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
38
+ const EVENT_CLASS_FILE = /^be\/src\/modules\/events\/([^/]+)\/[^/]+\.event\.ts$/;
39
+ const CONSUMER_FILE = /^be\/src\/features\/(?:[^/]+\/)+transport\/message\/[^/]+\.consumer\.ts$/;
40
+ const E2E_SPEC = /^be\/src\/tests\/e2e\/[^/]+\/[^/]+\.e2e-spec\.ts$/;
41
+ const USE_TEST_WORLD = /\buseTestWorld\s*\(/;
42
+
43
+ /** The be apps of kind api or worker. */
44
+ const servicesOf = (repo) => (repo.sides?.be?.apps ?? []).filter((app) => SERVICE_KINDS.has(app.kind));
45
+
46
+ /** The TypeScript compiler of the app (else of the runtime), or null. */
47
+ const typescriptFor = (repoRoot) => loadTypescript(repoRoot, HERE);
48
+
49
+ // ------------------------------------------------------------------------------------------------ R163 placement
50
+
51
+ /** Findings of R163: a service root outside `be/apps/<service>/`. */
52
+ function servicePlacementFindings({ files }) {
53
+ const findings = [];
54
+ for (const file of files) {
55
+ if (file.startsWith('.starciwork/') || file.startsWith('.starcistacks/') || file.includes('node_modules/')) continue;
56
+ const dockerfile = DOCKERFILE.test(file) && !BE_SERVICE_FILE.test(file) && !/^fe\/apps\/[^/]+\/Dockerfile(?:\.[^/]+)?$/.test(file) && file !== 'Dockerfile';
57
+ const manifest = path.posix.basename(file) === 'package.json' && file !== 'package.json' && !WORKSPACE_PACKAGE_JSON.test(file);
58
+ const rootDockerfile = file === 'Dockerfile';
59
+ if (!dockerfile && !manifest && !rootDockerfile) continue;
60
+ const what = manifest ? 'a package.json of its own' : 'a Dockerfile';
61
+ findings.push(found(SERVICE_PLACEMENT, file, `${file} gives a folder ${what}, which makes it a service root outside be/apps/<service>/; a back-end service is a Nest app at be/apps/<service>/ (own Dockerfile there) sharing the be/src libraries and the one package.json, never a folder of its own.`));
62
+ }
63
+ return findings;
64
+ }
65
+
66
+ // ------------------------------------------------------------------------------------------------ R164 pinned images
67
+
68
+ /** The image reference with `${VAR:-default}` resolved to its default, or null when it cannot be (a bare `${VAR}`). */
69
+ function resolvedImage(image) {
70
+ let unresolved = false;
71
+ const resolved = image.replace(/\$\{[^}:]+(?::-([^}]*))?\}/g, (_all, fallback) => { if (fallback === undefined) unresolved = true; return fallback ?? ''; });
72
+ return unresolved ? null : resolved;
73
+ }
74
+
75
+ /** Why an image is not pinned, or null when it is: a digest, or an exact version tag. */
76
+ function unpinnedReason(image) {
77
+ const resolved = resolvedImage(image);
78
+ if (resolved === null) return 'its reference is an environment variable with no default';
79
+ if (DIGEST.test(resolved)) return null;
80
+ const name = resolved.slice(resolved.lastIndexOf('/') + 1);
81
+ const colon = name.indexOf(':');
82
+ if (colon < 0) return 'it has no tag';
83
+ const tag = name.slice(colon + 1);
84
+ return EXACT_TAG.test(tag) ? null : `its tag \`${tag}\` is not an exact version (x.y.z) or a digest, so it moves`;
85
+ }
86
+
87
+ /** Findings of R164 over a multi-service product's stack declaration. */
88
+ function imagePinFindings({ repoRoot, repo }) {
89
+ if (servicesOf(repo).length < 2) return [];
90
+ const declaration = findStackDeclaration(repoRoot);
91
+ const components = declaration.doc?.components;
92
+ if (components === null || typeof components !== 'object' || components === undefined) return [];
93
+ const file = '.starcistacks/application-stacks.yaml';
94
+ const findings = [];
95
+ for (const [name, component] of Object.entries(components)) {
96
+ const image = component?.image;
97
+ if (typeof image !== 'string') continue;
98
+ const reason = unpinnedReason(image);
99
+ if (reason !== null) findings.push(found(IMAGE_UNPINNED, file, `${file} components.${name}.image ${image}: ${reason}. A product with more than one service pins every image: an exact version (\`<repo>/<service>:1.2.3\`, \`postgres:16.4\`) or a digest (\`@sha256:...\`).`, { component: name, image }));
100
+ }
101
+ return findings;
102
+ }
103
+
104
+ // ------------------------------------------------------------------------------------------------ R165 stack declaration
105
+
106
+ /** Findings of R165: each service app of a multi-service product is a `role: service` component. */
107
+ function serviceStackFindings({ repoRoot, repo }) {
108
+ const services = servicesOf(repo);
109
+ if (services.length < 2) return [];
110
+ const declaration = findStackDeclaration(repoRoot);
111
+ if (declaration.doc === undefined) return [];
112
+ const components = declaration.doc.components ?? {};
113
+ const file = '.starcistacks/application-stacks.yaml';
114
+ const findings = [];
115
+ for (const app of services) {
116
+ const component = components[app.name];
117
+ if (component === null || typeof component !== 'object') findings.push(found(SERVICE_STACK_DECLARATION, file, `${file} declares no component \`${app.name}\` for the ${app.kind} app be/apps/${app.name}; every service of a multi-service product is a \`role: service\` component (own image \`<repo>/${app.name}:<exact version>\`, built from be/apps/${app.name}/Dockerfile).`, { app: app.name }));
118
+ else if (component.role !== 'service') findings.push(found(SERVICE_STACK_DECLARATION, file, `${file} components.${app.name} is the stack's face of be/apps/${app.name} and must be \`role: service\`, not \`${String(component.role)}\`.`, { app: app.name }));
119
+ }
120
+ return findings;
121
+ }
122
+
123
+ // ------------------------------------------------------------------------------------------------ R166 event contract
124
+
125
+ /** The typed event classes of every service: `[{ service, file, className, name, version, compensates, payload }]` and the problems of the ones that cannot be read. */
126
+ function eventClassesOf({ repoRoot, files, ts }) {
127
+ const classes = [];
128
+ const problems = [];
129
+ for (const file of files.filter((candidate) => EVENT_CLASS_FILE.test(candidate)).sort()) {
130
+ const service = EVENT_CLASS_FILE.exec(file)[1];
131
+ const read = readEventClasses(ts, readText(repoRoot, file) ?? '', file);
132
+ for (const event of read.events) classes.push({ service, file, ...event });
133
+ for (const problem of read.problems) problems.push({ service, file, problem });
134
+ }
135
+ return { classes, problems };
136
+ }
137
+
138
+ /** Findings of R166: each service's event classes are the one source of its vendored `events.json`, and a compensating event names a declared event. */
139
+ function eventContractFindings({ repoRoot, files }) {
140
+ const classFiles = files.filter((file) => EVENT_CLASS_FILE.test(file));
141
+ const snapshots = files.filter((file) => SNAPSHOT.test(file));
142
+ if (classFiles.length === 0 && snapshots.length === 0) return [];
143
+ const ts = typescriptFor(repoRoot);
144
+ if (ts === null) return [];
145
+ const tracked = new Set(files);
146
+ const { classes, problems } = eventClassesOf({ repoRoot, files, ts });
147
+ const findings = problems.map(({ service, file, problem }) => found(EVENT_CONTRACT, file, `${file} is not an event class the contract can be emitted from: ${problem}.`, { service }));
148
+ const services = [...new Set(classes.map((event) => event.service))].sort();
149
+ const declared = new Set(classes.map((event) => event.name));
150
+ for (const service of services) {
151
+ const snapshot = `be/contracts/${service}/events.json`;
152
+ const events = classes.filter((event) => event.service === service);
153
+ if (!tracked.has(snapshot)) findings.push(found(EVENT_CONTRACT, snapshot, `${service} declares event classes (be/src/modules/events/${service}/) but ${snapshot} is not committed; run \`npm run contract:emit\` and commit the snapshot, the contract its consumers rely on.`, { service }));
154
+ else if (folded(readText(repoRoot, snapshot) ?? '') !== snapshotText(service, events)) findings.push(found(EVENT_CONTRACT, snapshot, `${snapshot} differs from what the event classes of ${service} emit now; run \`npm run contract:emit\` and commit the result.`, { service, drift: 'stale' }));
155
+ }
156
+ for (const snapshot of snapshots) {
157
+ const service = SNAPSHOT.exec(snapshot)[1];
158
+ if (!services.includes(service) && !problems.some((entry) => entry.service === service)) findings.push(found(EVENT_CONTRACT, snapshot, `${snapshot} is committed but be/src/modules/events/${service}/ declares no event class; delete the snapshot or restore the classes.`, { service, drift: 'left-behind' }));
159
+ }
160
+ for (const event of classes) {
161
+ if (event.compensates !== undefined && !declared.has(event.compensates)) findings.push(found(EVENT_CONTRACT, event.file, `${event.file}: event "${event.name}" compensates "${event.compensates}", which no event class declares; name the event whose step it undoes.`, { service: event.service, event: event.name }));
162
+ }
163
+ return findings;
164
+ }
165
+
166
+ // ------------------------------------------------------------------------------------------------ R167 async specs
167
+
168
+ /** The class an expression names, from `readonly event = <Class>` of a consumer: the identifier, or null. */
169
+ function consumedClassOf(ts, text) {
170
+ return propertyText(ts, text, { file: 'consumer.ts', key: 'event', declaration: true, kind: 'identifier' });
171
+ }
172
+
173
+ /** The events the consumers consume: `[{ file, className, service, event }]` from `readonly event = <EventClass>` of every `transport/message/*.consumer.ts`. */
174
+ function consumedEvents({ repoRoot, files, ts }) {
175
+ const { classes } = eventClassesOf({ repoRoot, files, ts });
176
+ const consumed = [];
177
+ for (const file of files.filter((candidate) => CONSUMER_FILE.test(candidate))) {
178
+ const className = consumedClassOf(ts, readText(repoRoot, file) ?? '');
179
+ const event = classes.find((candidate) => candidate.className === className);
180
+ if (event !== undefined) consumed.push({ file, className, service: event.service, event: event.name, compensates: event.compensates });
181
+ }
182
+ return consumed;
183
+ }
184
+
185
+ /** Findings of R167: every consumed event has an e2e spec through the world; a saga step's spec also names the event it compensates. */
186
+ function asyncSpecFindings({ repoRoot, files }) {
187
+ const ts = typescriptFor(repoRoot);
188
+ if (ts === null) return [];
189
+ const consumed = consumedEvents({ repoRoot, files, ts });
190
+ if (consumed.length === 0) return [];
191
+ const specs = files
192
+ .filter((file) => E2E_SPEC.test(file))
193
+ .map((file) => ({ file, text: readText(repoRoot, file) ?? '' }))
194
+ .filter(({ text }) => USE_TEST_WORLD.test(text));
195
+ const findings = [];
196
+ for (const { file, className, service, event, compensates } of consumed) {
197
+ const named = specs.filter(({ text }) => text.includes(event));
198
+ if (named.length === 0) {
199
+ findings.push(found(ASYNC_SPEC_MISSING, file, `${file}: the consumer of ${service} event "${event}" (${className}) has no e2e spec; add be/src/tests/e2e/<area>/<name>.e2e-spec.ts, which boots the apps with useTestWorld and names it: publish the event, read the persisted effect back and redeliver it.`, { service, event }));
200
+ continue;
201
+ }
202
+ if (typeof compensates === 'string' && !named.some(({ text }) => text.includes(compensates))) {
203
+ findings.push(found(ASYNC_SPEC_MISSING, named[0].file, `${named[0].file} is the spec of the saga step "${event}", which undoes "${compensates}", but never names it; the spec drives the whole flow: the step, the failure, then the compensated state.`, { service, event, compensates }));
204
+ }
205
+ }
206
+ return findings;
207
+ }
208
+
209
+ // ------------------------------------------------------------------------------------------------ entry
210
+
211
+ /** Every finding of the microservice policy over the app at `repoRoot`; `files` are its tracked app-relative paths. */
212
+ export function serviceFindings({ repoRoot, files, repo }) {
213
+ return [
214
+ ...servicePlacementFindings({ files }),
215
+ ...imagePinFindings({ repoRoot, repo }),
216
+ ...serviceStackFindings({ repoRoot, repo }),
217
+ ...eventContractFindings({ repoRoot, files }),
218
+ ...asyncSpecFindings({ repoRoot, files }),
219
+ ];
220
+ }
@@ -7,7 +7,7 @@
7
7
  // finding names the rule rather than only the missing slot. Which folder is a layer is read from the slot manifest, never spelled here.
8
8
  import { found } from './read.mjs';
9
9
 
10
- export const SPEC_PLACEMENT = 'BE_SPEC_PLACEMENT';
10
+ const SPEC_PLACEMENT = 'BE_SPEC_PLACEMENT';
11
11
  const SPEC_FILE = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$/;
12
12
  const TEST_SLOT_TESTS = new Set(['unit-beside', 'e2e']);
13
13
 
@@ -6,13 +6,13 @@
6
6
  // - the declaration (read by scripts/lib/stack-declaration.mjs, the one reader of it) states a sonar service, a local
7
7
  // Sonar is owned by the host (`stack.owner: host`, root `.claude/ext/sonar`), and no service still points at `.stacks`.
8
8
  // The declaration's services contract (custody, CI wiring, project keys) stays check-starcistacks's; this file judges shape only.
9
- import { braceVariants, globExpression } from '../glob.mjs';
10
- import { declaredStack, findStackDeclaration, STACK_ROOT, text } from '../stack-declaration.mjs';
9
+ import { braceVariants, globExpression } from '../../lib/glob.mjs';
10
+ import { declaredStack, findStackDeclaration, STACK_ROOT, text } from '../../lib/stack-declaration.mjs';
11
11
  import { found } from './read.mjs';
12
12
 
13
- export const STACKS_SHAPE = 'HFS_STACKS_SHAPE';
14
- export const STACKS_SLOT = 'app.starcistacks';
15
- export const HOST_SONAR_ROOT = '.claude/ext/sonar';
13
+ const STACKS_SHAPE = 'HFS_STACKS_SHAPE';
14
+ const STACKS_SLOT = 'app.starcistacks';
15
+ const HOST_SONAR_ROOT = '.claude/ext/sonar';
16
16
  const SEALED = /\.enc$/;
17
17
  const INSIDE_SECRETS = /^[^/]+\/secrets\/[^/]+\.enc$/;
18
18
  const RETIRED_ROOT = /^\.?stacks(?:\/|$)/;
@@ -0,0 +1,89 @@
1
+ // supabase-secrets.mjs - L17 extends the one R06 secret scan for edition lite. It keeps the ordinary
2
+ // HFS_PLAINTEXT_SECRET judgement, then adds the reduced lite custody tree and provider-shaped literals
3
+ // which are meaningful only in the Supabase/FE surface. Findings name shapes, never captured values.
4
+ import { Buffer } from 'node:buffer';
5
+ import { secretFindings } from './secrets.mjs';
6
+ import { found, readText } from './read.mjs';
7
+
8
+ const LITE_SECRET_CUSTODY = 'HFS_LITE_SECRET_CUSTODY';
9
+
10
+ const LOCKFILE = /(^|\/)package-lock\.json$/u;
11
+ const ROOT_ENV = /^\.env(?:\..*)?$/iu;
12
+ const SECRETS_ENV = /^secrets\.env$/iu;
13
+ const SECRETS_DIRECTORY = /^\.secrets(?:\/|$)/iu;
14
+ const SEALED_HOME = /^\.starcistacks\/[^/]+\/secrets\/[^/]+\.enc$/iu;
15
+
16
+ const LITE_SECRET_PATTERNS = Object.freeze([
17
+ { name: 'supabase-personal-access-token', re: /\bsbp_[A-Za-z0-9_-]{20,}\b/u },
18
+ { name: 'supabase-publishable-key', re: /\bsb_publishable_[A-Za-z0-9_-]{20,}\b/u },
19
+ { name: 'supabase-secret-token', re: /\bsb_secret_[A-Za-z0-9_-]{20,}\b/u },
20
+ { name: 'password-connection-string', re: /\b(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/@]+:[^@\s/${}]{8,}@/iu },
21
+ ]);
22
+ const JWT_CANDIDATE = /(?<![A-Za-z0-9_-])eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9_-])/gu;
23
+
24
+ const jsonObject = segment => {
25
+ try {
26
+ const value = JSON.parse(Buffer.from(segment, 'base64url').toString('utf8'));
27
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ };
32
+
33
+ const validJwt = candidate => {
34
+ const [header, payload, signature, extra] = candidate.split('.');
35
+ if (extra !== undefined || !signature) return false;
36
+ const decodedHeader = jsonObject(header);
37
+ const decodedPayload = jsonObject(payload);
38
+ return typeof decodedHeader?.alg === 'string' && decodedHeader.alg.toLowerCase() !== 'none' && decodedPayload !== null;
39
+ };
40
+
41
+ const liteSecretPatterns = line => {
42
+ const names = LITE_SECRET_PATTERNS.filter(pattern => pattern.re.test(line)).map(pattern => pattern.name);
43
+ if ([...line.matchAll(JWT_CANDIDATE)].some(match => validJwt(match[0]))) names.push('supabase-eyj-service-token');
44
+ return names;
45
+ };
46
+
47
+ const inSecretFreeTree = (repo, file) => repo.profile === 'fe' || file.startsWith('fe/') || file.startsWith('supabase/');
48
+
49
+ /** Raw L17 findings. The catalog-driven findings filter decides which editions judge them. */
50
+ export function liteSecretCustodyFindings({ repoRoot, files, repo }) {
51
+ const findings = [];
52
+ for (const file of files) {
53
+ if (repo.profile === 'app') {
54
+ if (ROOT_ENV.test(file) || SECRETS_ENV.test(file) || SECRETS_DIRECTORY.test(file)) {
55
+ findings.push(found(LITE_SECRET_CUSTODY, file, `${file} is forbidden at the lite app root; lite keeps no .env*, secrets.env or .secrets/ custody path.`, { pattern: 'lite-forbidden-entry' }));
56
+ continue;
57
+ }
58
+ if (file.endsWith('.enc') && !SEALED_HOME.test(file)) {
59
+ findings.push(found(LITE_SECRET_CUSTODY, file, `${file} is a sealed file outside .starcistacks/<env>/secrets/; lite has exactly one encrypted custody path.`, { pattern: 'sealed-file-home' }));
60
+ }
61
+ } else if (file.endsWith('.enc')) {
62
+ findings.push(found(LITE_SECRET_CUSTODY, file, `${file} is a sealed file inside a side tree; lite keeps *.enc only at the app root under .starcistacks/<env>/secrets/.`, { pattern: 'sealed-file-home' }));
63
+ }
64
+ if (!inSecretFreeTree(repo, file) || LOCKFILE.test(file)) continue;
65
+ const text = readText(repoRoot, file);
66
+ if (text === null) continue;
67
+ const seen = new Set();
68
+ const lines = text.split(/\r?\n/u);
69
+ for (let index = 0; index < lines.length; index += 1) {
70
+ for (const pattern of liteSecretPatterns(lines[index])) {
71
+ if (seen.has(pattern)) continue;
72
+ seen.add(pattern);
73
+ findings.push(found(LITE_SECRET_CUSTODY, file, `${file}:${index + 1} holds a provider-shaped secret (${pattern}); lite keeps no secret literal under fe/ or supabase/.`, { line: index + 1, pattern }));
74
+ }
75
+ }
76
+ }
77
+ return findings;
78
+ }
79
+
80
+ /**
81
+ * Drop-in extension of secretFindings for scripts/hfs/check.mjs. When an L17 finding identifies the same file/line as
82
+ * the generic R06 scanner (a complete JWT, for example), the specific lite code wins so one physical issue is emitted once.
83
+ */
84
+ export function supabaseSecretFindings(input) {
85
+ const ordinary = secretFindings(input);
86
+ const lite = liteSecretCustodyFindings(input);
87
+ const liteJwtAuthority = (input.repo.edition ?? 'full') === 'lite';
88
+ return [...ordinary.filter(item => !(liteJwtAuthority && item.pattern === 'jwt' && inSecretFreeTree(input.repo, item.path))), ...lite];
89
+ }
@@ -4,11 +4,11 @@
4
4
  // - one `jest.config.js` at the root: a second jest config (`jest.config.<x>.js`, `jest.<x>.config.*`, one per app) and a
5
5
  // `jest` key in a package.json are refused.
6
6
  // `int-spec`, `harness-spec`, the retired test folders and the per-lane configs under src/tests are the architecture machine's
7
- // (HFS_TEST_KIND_RETIRED, a sub-code of this rule); that jest.config.js is the rendered one (projects `unit`, `integration`, `e2e` and
7
+ // (reported under this same code, BE_TEST_TOPOLOGY); that jest.config.js is the rendered one (projects `unit`, `integration`, `e2e` and
8
8
  // `contract`, diagnostics off) is the managed-file check's (HFS_MANAGED_FILE_DRIFT); a spec whose suffix disagrees with its src/tests folder has no slot (HFS_SLOT_UNDECLARED).
9
9
  import { found, readJson } from './read.mjs';
10
10
 
11
- export const TEST_TOPOLOGY = 'BE_TEST_TOPOLOGY';
11
+ const TEST_TOPOLOGY = 'BE_TEST_TOPOLOGY';
12
12
  const DOT_TEST = /\.test\.[cm]?[jt]sx?$/;
13
13
  const JEST_CONFIG = /(?:^|\/)jest(?:\.[^/]+)*\.config(?:\.[^/]+)*\.(?:[cm]?[jt]s|json)$|(?:^|\/)jest\.config\.[^/]+$/;
14
14
  const ROOT_JEST_CONFIG = 'jest.config.js';