@starci/hfs 4.7.32 → 4.7.33

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 (473) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +13 -6
  3. package/emit/contracts.mjs +4 -3
  4. package/emit/form-bounds-reader.ts.partial +58 -0
  5. package/emit/form-bounds.mjs +155 -0
  6. package/emit/graphql-input-bounds.mjs +97 -0
  7. package/emit/input-bounds.mjs +44 -0
  8. package/emit/operation-schemas.mjs +137 -0
  9. package/emit/operations-worker.mjs +2 -2
  10. package/emit/operations.mjs +52 -16
  11. package/emit/schema-worker.mjs +17 -4
  12. package/emit/type-schema.mjs +17 -5
  13. package/package.json +9 -2
  14. package/release/gate.mjs +18 -23
  15. package/release/index.mjs +1 -1
  16. package/release/push.mjs +4 -5
  17. package/runtime/engine/admission.mjs +331 -0
  18. package/runtime/engine/canonical-json.mjs +14 -0
  19. package/runtime/engine/db/blob.mjs +380 -0
  20. package/runtime/engine/db/event-compact.mjs +73 -0
  21. package/runtime/engine/db/ledger-open.mjs +38 -0
  22. package/runtime/engine/db/ledger-paths.mjs +87 -0
  23. package/runtime/engine/db/ledger.mjs +1068 -0
  24. package/runtime/engine/db/machine-connection.mjs +150 -0
  25. package/runtime/engine/db/machine-open.mjs +60 -0
  26. package/runtime/engine/db/machine-schema.mjs +105 -0
  27. package/runtime/engine/db/machine.mjs +1317 -0
  28. package/runtime/engine/db/provider-reservations.mjs +127 -0
  29. package/runtime/engine/db/provision-authority.mjs +76 -0
  30. package/runtime/engine/db/provision-lock.mjs +29 -0
  31. package/runtime/engine/db/provision-operations.mjs +146 -0
  32. package/runtime/engine/db/ref-value.mjs +67 -0
  33. package/runtime/engine/db/schema/machine.sql +920 -0
  34. package/runtime/engine/db/schema/runtime.sql +1113 -0
  35. package/runtime/engine/refuse.mjs +11 -0
  36. package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
  37. package/runtime/knowledge/hfs/design-truths.yaml +149 -0
  38. package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
  39. package/runtime/knowledge/hfs/rules.yaml +269 -48
  40. package/runtime/knowledge/hfs/slots.yaml +113 -53
  41. package/runtime/knowledge/patterns/be/api.yaml +46 -20
  42. package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
  43. package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
  44. package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
  45. package/runtime/knowledge/patterns/be/projections.yaml +2 -2
  46. package/runtime/knowledge/patterns/be/queues.yaml +57 -22
  47. package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
  48. package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
  49. package/runtime/knowledge/patterns/be/saga.yaml +2 -2
  50. package/runtime/knowledge/sonar-gate.yaml +1 -1
  51. package/runtime/modules/kernel/command-policy.yaml +249 -0
  52. package/runtime/modules/kernel/failure-codes.yaml +211 -52
  53. package/runtime/modules/kernel/hook-input.yaml +6 -0
  54. package/runtime/modules/kernel/protected-zone.yaml +81 -0
  55. package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
  56. package/runtime/modules/kernel/roles.yaml +405 -0
  57. package/runtime/modules/models/registry.yaml +3 -1
  58. package/runtime/modules/models/runtimes.yaml +2 -0
  59. package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
  60. package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
  61. package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
  62. package/runtime/scripts/agent/codex-app-server.mjs +79 -0
  63. package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
  64. package/runtime/scripts/agent/host-agents.mjs +80 -0
  65. package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
  66. package/runtime/scripts/agent/trust-launch.mjs +191 -0
  67. package/runtime/scripts/agent/trust.mjs +523 -0
  68. package/runtime/scripts/api/fs/rename-over.mjs +21 -0
  69. package/runtime/scripts/api/fs/temp-path.mjs +8 -0
  70. package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
  71. package/runtime/scripts/api/git/diff-names.mjs +8 -0
  72. package/runtime/scripts/api/git/head-time.mjs +10 -0
  73. package/runtime/scripts/api/git/index-lock.mjs +78 -0
  74. package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
  75. package/runtime/scripts/api/git/log.mjs +5 -0
  76. package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
  77. package/runtime/scripts/api/git/merge-tree.mjs +5 -0
  78. package/runtime/scripts/api/git/rev-list.mjs +5 -0
  79. package/runtime/scripts/api/git/rev-parse.mjs +13 -0
  80. package/runtime/scripts/api/git/status-query.mjs +5 -0
  81. package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
  82. package/runtime/scripts/api/node/spawn-node.mjs +6 -0
  83. package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
  84. package/runtime/scripts/api/npm/lib.mjs +32 -0
  85. package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
  86. package/runtime/scripts/api/npm/run-npm.mjs +6 -0
  87. package/runtime/scripts/api/npm/view.mjs +30 -0
  88. package/runtime/scripts/api/npm/whoami.mjs +8 -0
  89. package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
  90. package/runtime/scripts/api/process/kill-tree.mjs +22 -0
  91. package/runtime/scripts/api/process/lib.mjs +125 -0
  92. package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
  93. package/runtime/scripts/api/process/process-list.mjs +28 -0
  94. package/runtime/scripts/api/process/system-tool.mjs +37 -0
  95. package/runtime/scripts/gates/canon-digest.mjs +81 -0
  96. package/runtime/scripts/gates/canon-pins.mjs +16 -0
  97. package/runtime/scripts/gates/commit-proof.mjs +53 -0
  98. package/runtime/scripts/gates/gate-input.mjs +98 -0
  99. package/runtime/scripts/gates/gate.mjs +669 -0
  100. package/runtime/scripts/gates/grammar-dist.mjs +206 -0
  101. package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
  102. package/runtime/scripts/gates/test-world-run.mjs +254 -0
  103. package/runtime/scripts/gates/type-impact.mjs +277 -0
  104. package/runtime/scripts/gates/type-program.mjs +113 -0
  105. package/runtime/scripts/guards/assigned-command.mjs +12 -0
  106. package/runtime/scripts/guards/call-rights.mjs +47 -0
  107. package/runtime/scripts/guards/command-guard.mjs +468 -0
  108. package/runtime/scripts/guards/command-policy.mjs +342 -0
  109. package/runtime/scripts/guards/critic-reach.mjs +82 -0
  110. package/runtime/scripts/guards/deps-guard.mjs +150 -0
  111. package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
  112. package/runtime/scripts/guards/git-pathspec.mjs +225 -0
  113. package/runtime/scripts/guards/git-policy.mjs +355 -0
  114. package/runtime/scripts/guards/guards-root.mjs +27 -0
  115. package/runtime/scripts/guards/hook-install.mjs +343 -0
  116. package/runtime/scripts/guards/hook-io.mjs +76 -0
  117. package/runtime/scripts/guards/hook-verdict.mjs +12 -0
  118. package/runtime/scripts/guards/install-verdict.mjs +41 -0
  119. package/runtime/scripts/guards/kernel-seat.mjs +63 -0
  120. package/runtime/scripts/guards/launch-verdict.mjs +74 -0
  121. package/runtime/scripts/guards/op-starci.mjs +16 -0
  122. package/runtime/scripts/guards/op-write-scope.mjs +27 -0
  123. package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
  124. package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
  125. package/runtime/scripts/guards/protected-zone.mjs +56 -0
  126. package/runtime/scripts/guards/refusals.mjs +27 -0
  127. package/runtime/scripts/guards/release-definition.mjs +185 -0
  128. package/runtime/scripts/guards/release-push-gate.mjs +72 -0
  129. package/runtime/scripts/guards/release-record.mjs +58 -0
  130. package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
  131. package/runtime/scripts/guards/rights.mjs +268 -0
  132. package/runtime/scripts/guards/seat-tools.mjs +47 -0
  133. package/runtime/scripts/guards/shell-commands.mjs +328 -0
  134. package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
  135. package/runtime/scripts/guards/verdict-process.mjs +81 -0
  136. package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
  137. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
  138. package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
  139. package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
  140. package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
  141. package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
  142. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
  143. package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
  144. package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
  145. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
  146. package/runtime/scripts/hfs/check.mjs +6 -4
  147. package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
  148. package/runtime/scripts/hfs/db-restamp-verb.mjs +54 -14
  149. package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
  150. package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
  151. package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
  152. package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
  153. package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
  154. package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
  155. package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
  156. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
  157. package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
  158. package/runtime/scripts/hfs/rules/edition.mjs +56 -12
  159. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
  160. package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
  161. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
  162. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
  163. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
  164. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
  165. package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
  166. package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
  167. package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
  168. package/runtime/scripts/hfs/sealed-members.mjs +3 -3
  169. package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
  170. package/runtime/scripts/hfs/slots.mjs +2 -1
  171. package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
  172. package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
  173. package/runtime/scripts/hfs/view.mjs +2 -1
  174. package/runtime/scripts/lib/app-data.mjs +11 -0
  175. package/runtime/scripts/lib/bypass-markers.mjs +272 -75
  176. package/runtime/scripts/lib/check-format.mjs +7 -0
  177. package/runtime/scripts/lib/cli-arg.mjs +120 -0
  178. package/runtime/scripts/lib/css-rules.mjs +220 -0
  179. package/runtime/scripts/lib/dom-tree.mjs +4 -0
  180. package/runtime/scripts/lib/escape.mjs +16 -0
  181. package/runtime/scripts/lib/event-items.mjs +22 -0
  182. package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
  183. package/runtime/scripts/lib/git-dir.mjs +120 -0
  184. package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
  185. package/runtime/scripts/lib/guard-command.mjs +24 -0
  186. package/runtime/scripts/lib/hook-limits.mjs +5 -0
  187. package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
  188. package/runtime/scripts/lib/js-lex.mjs +200 -0
  189. package/runtime/scripts/lib/json-formats.mjs +0 -1
  190. package/runtime/scripts/lib/language.mjs +94 -1
  191. package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
  192. package/runtime/scripts/lib/loosening-config.mjs +361 -0
  193. package/runtime/scripts/lib/loosening-state.mjs +223 -0
  194. package/runtime/scripts/lib/menu-parts.mjs +11 -0
  195. package/runtime/scripts/lib/normalize.mjs +25 -0
  196. package/runtime/scripts/lib/op-gate.mjs +15 -0
  197. package/runtime/scripts/lib/redact.mjs +208 -0
  198. package/runtime/scripts/lib/runtime-location.mjs +21 -0
  199. package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
  200. package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
  201. package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
  202. package/runtime/scripts/lib/seat-env.mjs +43 -0
  203. package/runtime/scripts/lib/secret-patterns.mjs +58 -9
  204. package/runtime/scripts/lib/shell-source.mjs +4 -0
  205. package/runtime/scripts/lib/source-phrases.mjs +34 -0
  206. package/runtime/scripts/lib/sqlite.mjs +21 -0
  207. package/runtime/scripts/lib/swarm-stack.mjs +2 -1
  208. package/runtime/scripts/lib/tar-files.mjs +39 -0
  209. package/runtime/scripts/lib/ts-analysis.mjs +10 -0
  210. package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
  211. package/runtime/scripts/lib/worktree-record.mjs +14 -0
  212. package/runtime/scripts/machine/artifact-hold.mjs +52 -0
  213. package/runtime/scripts/machine/ask-receipts.mjs +113 -0
  214. package/runtime/scripts/machine/evidence-store.mjs +223 -0
  215. package/runtime/scripts/machine/home.mjs +216 -0
  216. package/runtime/scripts/machine/host-lock.mjs +184 -0
  217. package/runtime/scripts/machine/lock-recovery.mjs +55 -0
  218. package/runtime/scripts/machine/roles-contract.mjs +90 -0
  219. package/runtime/scripts/machine/runtime-change.mjs +20 -0
  220. package/runtime/scripts/machine/workflow-tree.mjs +122 -0
  221. package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
  222. package/runtime/scripts/reconciler/guard-row.mjs +25 -0
  223. package/runtime/scripts/work/asset-slot.mjs +284 -0
  224. package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
  225. package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
  226. package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
  227. package/runtime/scripts/work/brand/brand.mjs +892 -0
  228. package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
  229. package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
  230. package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
  231. package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
  232. package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
  233. package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
  234. package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
  235. package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
  236. package/runtime/scripts/work/draw/draw-source.mjs +412 -0
  237. package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
  238. package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
  239. package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
  240. package/runtime/scripts/work/draw-grammar.mjs +175 -0
  241. package/runtime/scripts/work/draw-render-page.mjs +188 -0
  242. package/runtime/scripts/work/draw-render.mjs +626 -0
  243. package/runtime/scripts/work/grammar-proposal.mjs +279 -0
  244. package/runtime/scripts/work/png.mjs +237 -0
  245. package/runtime/scripts/work/render-tools.mjs +64 -0
  246. package/runtime/scripts/work/work-io.mjs +119 -0
  247. package/scaffold/add-inbox-lite.mjs +2 -2
  248. package/scaffold/add-plan.mjs +40 -13
  249. package/scaffold/add-queue-lite.mjs +372 -0
  250. package/scaffold/add.mjs +5 -2
  251. package/scaffold/app.mjs +1 -2
  252. package/scaffold/edition-gate.mjs +11 -7
  253. package/scaffold/lite-exports.mjs +10 -0
  254. package/scaffold/pattern-file-slots.mjs +33 -0
  255. package/scaffold/test-world.mjs +1 -1
  256. package/scaffold/yaml-block-edit.mjs +117 -0
  257. package/src/main.mjs +5 -4
  258. package/stack/deploy.mjs +7 -3
  259. package/stack/post-deploy.mjs +48 -0
  260. package/sync/index.mjs +10 -0
  261. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
  262. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
  263. package/templates/app/codegen-graphql.mjs.partial +59 -12
  264. package/templates/app/hooks/husky/pre-commit +2 -2
  265. package/templates/app/hooks/husky/pre-push +10 -3
  266. package/templates/app/package-scripts/redis-dev.json +3 -0
  267. package/templates/app/package-scripts-lite/package.json +1 -0
  268. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
  269. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  270. package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
  271. package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
  272. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
  273. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
  275. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
  276. package/templates/app/skeleton/README.md +2 -0
  277. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
  278. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
  279. package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
  280. package/templates/app/skeleton-lite/README.md +1 -1
  281. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
  282. package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
  283. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
  284. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
  285. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
  286. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
  287. package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
  288. package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
  289. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
  290. package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  291. package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
  292. package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  293. package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  294. package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
  295. package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  296. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
  297. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
  298. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
  299. package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
  300. package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  301. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  302. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
  303. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  304. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  305. package/templates/be/inbox/platform/migration.sql.tpl +1 -0
  306. package/templates/be/inbox/service.spec.ts.tpl +4 -4
  307. package/templates/be/inbox/service.ts.tpl +3 -1
  308. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
  309. package/templates/be/inbox/webhook-door.ts.tpl +3 -2
  310. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
  311. package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
  312. package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
  313. package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
  314. package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
  315. package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
  316. package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
  317. package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
  318. package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
  319. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
  320. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
  321. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
  323. package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
  324. package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
  325. package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
  326. package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  327. package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
  328. package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  329. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  330. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
  331. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
  332. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
  333. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
  334. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
  335. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
  336. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
  337. package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
  338. package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  339. package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
  340. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
  341. package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
  342. package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
  343. package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
  344. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
  345. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
  346. package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
  347. package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  348. package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
  349. package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
  350. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  351. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
  352. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
  353. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
  354. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  355. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  356. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
  357. package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
  358. package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
  359. package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
  360. package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
  361. package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
  362. package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
  363. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
  364. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
  365. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
  366. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
  367. package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
  368. package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
  369. package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
  370. package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
  371. package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
  372. package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
  373. package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
  374. package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
  375. package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
  376. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
  377. package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
  378. package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
  379. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
  380. package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
  381. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
  382. package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
  383. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
  384. package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
  385. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
  386. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
  387. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
  388. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
  389. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
  390. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
  391. package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
  392. package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
  393. package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
  394. package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
  395. package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
  396. package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
  397. package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
  398. package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
  399. package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
  400. package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
  401. package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
  402. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
  403. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
  404. package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
  405. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
  406. package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
  407. package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
  408. package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
  409. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
  410. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
  411. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
  412. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
  413. package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
  414. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
  415. package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
  416. package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
  417. package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
  418. package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
  419. package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
  420. package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
  421. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
  422. package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
  423. package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
  424. package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
  425. package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
  426. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
  427. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
  428. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
  429. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
  430. package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
  431. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
  432. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
  433. package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
  434. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
  435. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
  436. package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
  437. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
  438. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
  439. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
  440. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
  441. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
  442. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
  443. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
  444. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  445. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
  446. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
  447. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
  448. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
  449. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  450. package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
  451. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
  452. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  453. package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
  454. package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
  455. package/upgrade/check.mjs +12 -135
  456. package/upgrade/file-state.mjs +1 -16
  457. package/upgrade/index.mjs +34 -361
  458. package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
  459. package/runtime/scripts/lib/gate-loosening.mjs +0 -292
  460. package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
  461. package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
  462. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
  463. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
  464. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
  465. package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
  466. package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
  467. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
  468. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
  469. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
  470. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
  471. package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
  472. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
  473. package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +0 -23
@@ -7,16 +7,22 @@
7
7
  // - inside a package.json: an e2e, stack or browser script (`test:e2e`, `test:stack`, `test:browser`,
8
8
  // with a pre/post prefix too) or a dependency of the infrastructure lite does not run - a test-world container or proxy
9
9
  // (testcontainers, toxiproxy) or a browser runner (playwright, cypress), and every library the lite infraOwners own nowhere
10
- // while full owns them (BullMQ, Kafka, Redis: lite runs pgmq and Postgres locks instead);
10
+ // while full owns them (Kafka: the lite event bus runs pgmq);
11
11
  // - a declaration of what lite does not have: an app kind, pattern or trigger kind every slot of which is forbidden under
12
12
  // lite. The forbidden names are DERIVED from the manifest, so adding or restoring a kind is a manifest edit, not an edit here
13
- // (under the 2026-10-13 ruling no pattern, trigger or app kind is gone).
13
+ // (under the 2026-10-13 ruling no pattern, trigger or app kind is gone);
14
+ // - a backing service that is not declared or not used (ruling:R212/case-10): a client library of a service of
15
+ // ruleParams.be.backing (bullmq for redis) or a declared pattern that needs one (queue) while hfs.json sides.be.backing does
16
+ // not declare it is HFS_BACKING_UNDECLARED, as is a service the catalog's editions excludes (owner:lite-vector-is-pgvector),
17
+ // and a declared service no dependency of the app consumes is HFS_BACKING_UNUSED.
14
18
  // A full app answers []: the rule tests the edition once and a full view forbids none of these names.
15
19
  import { found, readJson } from './read.mjs';
16
20
  import { DEPENDENCY_SECTIONS } from './fe-no-tests.mjs';
17
- import { editionRuleParams } from '../edition-slots.mjs';
21
+ import { editionRuleParams, judgedInEdition } from '../edition-slots.mjs';
18
22
 
19
23
  export const HFS_EDITION_FORBIDDEN_PRESENT = 'HFS_EDITION_FORBIDDEN_PRESENT';
24
+ export const HFS_BACKING_UNDECLARED = 'HFS_BACKING_UNDECLARED';
25
+ export const HFS_BACKING_UNUSED = 'HFS_BACKING_UNUSED';
20
26
 
21
27
  const PACKAGE_JSON = /(?:^|\/)package\.json$/;
22
28
  /** The scripts of a world lite does not have: the e2e layer, the self-hosted stack and the browser journey. */
@@ -54,8 +60,8 @@ function editionGone(resolver) {
54
60
  const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
55
61
 
56
62
  /**
57
- * The specifiers a lite back end may import nowhere although full gives them an owner: the self-hosted infrastructure libraries
58
- * the lite infraOwners override removes (bullmq, kafkajs, ioredis, ...). Read from the two rule-parameter views, never listed here.
63
+ * The specifiers a lite back end may import nowhere although full gives them an owner: the infrastructure libraries the lite
64
+ * infraOwners override removes (kafkajs). Read from the two rule-parameter views, never listed here.
59
65
  */
60
66
  function liteRemovedLibraries(manifest) {
61
67
  const be = manifest?.ruleParams?.be;
@@ -72,24 +78,60 @@ function scriptFindings(rel, pkg) {
72
78
  .map((name) => found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} has script ${JSON.stringify(name)}: a lite app has unit specs, optional integration specs on the local Supabase stack and optional contract specs, never an e2e, stack or browser run`, { name }));
73
79
  }
74
80
 
75
- function dependencyFindings(rel, pkg, removed) {
81
+ /** The backing services of the manifest (ruleParams.be.backing): name -> {clients, patterns, images, editions?}. */
82
+ const backingCatalog = (manifest) => manifest?.ruleParams?.be?.backing ?? {};
83
+
84
+ /** The backing service `name` is a client library of, or null. */
85
+ const serviceOfClient = (catalog, name) => Object.keys(catalog).find((service) => catalog[service].clients.includes(name)) ?? null;
86
+
87
+ /** The dependency names of a package.json, every section. */
88
+ const dependencyNames = (pkg) => DEPENDENCY_SECTIONS.flatMap((section) => (isPlainObject(pkg?.[section]) ? Object.keys(pkg[section]) : []));
89
+
90
+ function dependencyFindings(rel, pkg, { removed, catalog, declared }) {
76
91
  const findings = [];
77
92
  for (const section of DEPENDENCY_SECTIONS) {
78
93
  const deps = pkg[section];
79
94
  if (!isPlainObject(deps)) continue;
80
95
  for (const name of Object.keys(deps)) {
96
+ const service = serviceOfClient(catalog, name);
81
97
  if (LITE_FORBIDDEN_TEST_INFRA.test(name)) findings.push(found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} ${section} has ${name}: a lite app runs no test containers, fault proxy or browser journey - its integration specs run against \`supabase start\``, { section, name }));
82
- else if (removed.has(name)) findings.push(found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} ${section} has ${name}: lite replaces backing infrastructure with Supabase - the pattern's port is implemented on Supabase (pgmq, Postgres locks)`, { section, name }));
98
+ else if (removed.has(name)) findings.push(found(HFS_EDITION_FORBIDDEN_PRESENT, rel, `${rel} ${section} has ${name}: lite runs no self-hosted Kafka - the event bus port is implemented on Supabase (pgmq) or in process`, { section, name }));
99
+ else if (service !== null && !judgedInEdition(catalog[service], 'lite')) findings.push(found(HFS_BACKING_UNDECLARED, rel, `${rel} ${section} has ${name}, a client of ${service}, which the lite edition does not admit: remove the dependency`, { section, name, service }));
100
+ else if (service !== null && !declared.has(service)) findings.push(found(HFS_BACKING_UNDECLARED, rel, `${rel} ${section} has ${name}, a client of ${service}, which hfs.json sides.be.backing does not declare: declare ${service} for the capability that needs it, or remove the dependency`, { section, name, service }));
83
101
  }
84
102
  }
85
103
  return findings;
86
104
  }
87
105
 
88
106
  /** The findings one package.json's scripts and dependencies produce under lite. */
89
- function packageFindings(repoRoot, rel, removed) {
107
+ function packageFindings(repoRoot, rel, context) {
90
108
  const pkg = readJson(repoRoot, rel);
91
109
  if (!isPlainObject(pkg)) return [];
92
- return [...scriptFindings(rel, pkg), ...dependencyFindings(rel, pkg, removed)];
110
+ return [...scriptFindings(rel, pkg), ...dependencyFindings(rel, pkg, context)];
111
+ }
112
+
113
+ /**
114
+ * The backing findings of the be declaration: a declared pattern whose service is undeclared, and (at the app root or a
115
+ * standalone back end, whose package.json carries the back end's dependencies) a declared service no dependency consumes.
116
+ */
117
+ function backingFindings({ repoRoot, beView, catalog, ownsPackage }) {
118
+ const declared = new Set(beView.backing ?? []);
119
+ const findings = [];
120
+ for (const service of declared) {
121
+ if (catalog[service] && !judgedInEdition(catalog[service], 'lite')) findings.push(found(HFS_BACKING_UNDECLARED, 'hfs.json', `hfs.json sides.be.backing declares ${service}, which the lite edition does not admit: remove the declaration`, { service }));
122
+ }
123
+ for (const [service, entry] of Object.entries(catalog)) {
124
+ const needing = (beView.patterns ?? []).filter((pattern) => entry.patterns.includes(pattern));
125
+ if (needing.length && !declared.has(service)) findings.push(found(HFS_BACKING_UNDECLARED, 'hfs.json', `hfs.json declares the ${needing.join(', ')} pattern, which runs on ${service}: declare ${service} in sides.be.backing`, { service, patterns: needing }));
126
+ }
127
+ if (!ownsPackage) return findings;
128
+ const dependencies = new Set(dependencyNames(readJson(repoRoot, 'package.json')));
129
+ for (const service of declared) {
130
+ if (!judgedInEdition(catalog[service], 'lite')) continue;
131
+ const clients = catalog[service]?.clients ?? [];
132
+ if (!clients.some((name) => dependencies.has(name))) findings.push(found(HFS_BACKING_UNUSED, 'hfs.json', `hfs.json sides.be.backing declares ${service}, but no dependency consumes it (${clients.join(', ')}): remove the declaration, or add the capability that needs it`, { service }));
133
+ }
134
+ return findings;
93
135
  }
94
136
 
95
137
  const goneAppFindings = (repo, gone) => (repo.apps ?? [])
@@ -121,11 +163,13 @@ export function editionFindings({ repoRoot, files, repo, resolver, manifest, wit
121
163
  let beView = null;
122
164
  if (withDeclaration && repo.profile === 'app') beView = repo.sides?.be;
123
165
  else if (withDeclaration && repo.profile === 'be') beView = repo;
124
- const findings = beView ? declarationFindings(beView, gone) : [];
125
- const removed = liteRemovedLibraries(manifest);
166
+ const catalog = backingCatalog(manifest);
167
+ const findings = beView ? [...declarationFindings(beView, gone), ...backingFindings({ repoRoot, beView, catalog, ownsPackage: true })] : [];
168
+ const declared = new Set((repo.profile === 'app' ? repo.sides?.be?.backing : repo.backing) ?? []);
169
+ const context = { removed: liteRemovedLibraries(manifest), catalog, declared };
126
170
  for (const file of files) {
127
171
  if (file.includes('node_modules/') || !PACKAGE_JSON.test(file)) continue;
128
- findings.push(...packageFindings(repoRoot, file, removed));
172
+ findings.push(...packageFindings(repoRoot, file, context));
129
173
  }
130
174
  return findings;
131
175
  }
@@ -11,6 +11,7 @@ import { byCodeUnit } from '../../lib/list.mjs';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { loadTypescript } from '../../lib/package-at.mjs';
13
13
  import { isConstVariable, sourceLocation } from '../../lib/ts-ast.mjs';
14
+ import { lexicalChecker } from '../../lib/ts-analysis.mjs';
14
15
 
15
16
  export const FE_GRAPHQL_CONTRACT = 'FE_GRAPHQL_CONTRACT';
16
17
 
@@ -61,16 +62,6 @@ function templateText(ts, node, checker, names, visiting = new Set()) {
61
62
  return text;
62
63
  }
63
64
 
64
- /** Bind this file in isolation: the compiler resolves lexical names, but imports and library files are never loaded. */
65
- function lexicalChecker(ts, source) {
66
- const options = { noLib: true, noResolve: true, allowJs: true };
67
- const host = ts.createCompilerHost(options);
68
- host.getSourceFile = (file) => file === source.fileName ? source : undefined;
69
- host.fileExists = () => false;
70
- host.readFile = () => undefined;
71
- return ts.createProgram([source.fileName], options, host).getTypeChecker();
72
- }
73
-
74
65
  /** Actual tagged-template AST nodes: comments, quoted examples and other template tags are not operations. */
75
66
  function gqlDocuments(ts, file, text) {
76
67
  const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
@@ -11,6 +11,7 @@ import { parseYaml } from '../../../engine/yaml.mjs';
11
11
  import { found, readJson } from './read.mjs';
12
12
  import { DEPENDENCY_SECTIONS } from './fe-no-tests.mjs';
13
13
  import { editionRuleParams } from '../edition-slots.mjs';
14
+ import { globExpression } from '../../lib/glob.mjs';
14
15
 
15
16
  export const FE_STACK_FORBIDDEN = 'FE_STACK_FORBIDDEN';
16
17
  export const FE_STACK_UNLISTED = 'FE_STACK_UNLISTED';
@@ -21,18 +22,23 @@ const APP_ROOT_MANIFEST = 'package.json';
21
22
 
22
23
  let forbidden = null;
23
24
 
24
- /** name -> {concern, canonical} for every package a concern of the catalog forbids, read once. */
25
+ /** Package-name patterns and their concern, canonical packages and guidance, read once. */
25
26
  function forbiddenPackages() {
26
27
  if (forbidden) return forbidden;
27
28
  const catalog = parseYaml(fs.readFileSync(CATALOG_FILE, 'utf8'));
28
29
  if (catalog?.schema !== 'starci/fe-stack@1' || !Array.isArray(catalog.concerns)) throw new Error('knowledge/hfs/fe-stack.yaml is not a starci/fe-stack@1 catalog');
29
- forbidden = new Map();
30
+ forbidden = [];
30
31
  for (const concern of catalog.concerns) {
31
- for (const name of concern.forbids ?? []) forbidden.set(name, { concern: concern.id, canonical: concern.packages ?? [] });
32
+ for (const name of concern.forbids ?? []) forbidden.push({ matches: globExpression(name), concern: concern.id, canonical: concern.packages ?? [], why: concern.why });
32
33
  }
33
34
  return forbidden;
34
35
  }
35
36
 
37
+ /** A forbidden package or npm scope is judged before the closed dependency catalog. */
38
+ function forbiddingPackage(name) {
39
+ return forbiddenPackages().find(entry => entry.matches.test(name));
40
+ }
41
+
36
42
  /**
37
43
  * The packages the back end owns (be infraOwners with at least one owner, full's view: a library lite removes is the edition rule's
38
44
  * finding, not a front-end one): a root dependency of the back end.
@@ -47,14 +53,13 @@ function backendOwned(manifest) {
47
53
  function manifestFindings(repoRoot, rel, skip) {
48
54
  const pkg = readJson(repoRoot, rel);
49
55
  if (!pkg || typeof pkg !== 'object') return [];
50
- const catalog = forbiddenPackages();
51
56
  const findings = [];
52
57
  for (const section of DEPENDENCY_SECTIONS) {
53
58
  for (const name of Object.keys(pkg[section] ?? {})) {
54
- const entry = catalog.get(name);
59
+ const entry = forbiddingPackage(name);
55
60
  if (!entry || skip.has(name)) continue;
56
61
  const canonical = entry.canonical.length ? entry.canonical.join(', ') : 'the canonical library of the concern';
57
- findings.push(found(FE_STACK_FORBIDDEN, rel, `${rel} ${section} declares ${name}, which the front-end stack forbids for the ${entry.concern} concern (knowledge/hfs/fe-stack.yaml); use ${canonical} and delete ${name}`, { section, dependency: name, concern: entry.concern }));
62
+ findings.push(found(FE_STACK_FORBIDDEN, rel, `${rel} ${section} declares ${name}, which the front-end stack forbids for the ${entry.concern} concern (knowledge/hfs/fe-stack.yaml); use ${canonical} and delete ${name}. ${entry.why}`, { section, dependency: name, concern: entry.concern }));
58
63
  }
59
64
  }
60
65
  return findings;
@@ -98,7 +103,7 @@ function extraPackages(declaration) {
98
103
  function unlistedFindings(rel, pkg, allowed, extras, workspaces) {
99
104
  const findings = [];
100
105
  for (const section of DEPENDENCY_SECTIONS) for (const name of Object.keys(pkg?.[section] ?? {})) {
101
- if (allowed.has(name) || extras.has(name) || workspaces.has(name) || forbiddenPackages().has(name)) continue;
106
+ if (allowed.has(name) || extras.has(name) || workspaces.has(name) || forbiddingPackage(name)) continue;
102
107
  findings.push(found(FE_STACK_UNLISTED, rel, `${rel} ${section} declares ${name}, absent from knowledge/hfs/fe-stack.yaml; add a canonical concern or declare sides.fe.extraPackages with its name and a nonempty reason in hfs.json`, { section, dependency: name }));
103
108
  }
104
109
  return findings;
@@ -11,7 +11,7 @@ const LITE_PROOF_LANE_MISSING = 'BE_LITE_PROOF_LANE_MISSING';
11
11
  const PATTERN_SPEC_MISSING = 'BE_PATTERN_SPEC_MISSING';
12
12
  const HERE = path.dirname(fileURLToPath(import.meta.url));
13
13
  const INTEGRATION_SPEC = /^be\/src\/tests\/integration\/.+\.integration-spec\.ts$/;
14
- const PATTERNS = new Set(['event-bus', 'queue', 'webhooks', 'realtime', 'fenced-job', 'projection', 'saga']);
14
+ const PATTERNS = new Set(['logging', 'event-bus', 'queue', 'webhooks', 'realtime', 'fenced-job', 'projection', 'saga', 'idempotency']);
15
15
  // Fallback saga scenarios until the shared manifest provides its scenario table.
16
16
  const SAGA_SCENARIOS = ['compensation-on-failure', 'step-order', 'state-version-fences', 'consumer-dedupes'];
17
17
 
@@ -1,10 +1,20 @@
1
- // R276: lite uses managed Supabase backing; sidecar SQLite persists on declared named volumes.
1
+ // R276: lite keeps Postgres and pgvector on managed Supabase, admits only the backing services available to its edition,
2
+ // and runs a declared Redis as one service with AOF fsynced at least every second on a named volume and no eviction
3
+ // (ruling:R212/case-10, owner:lite-vector-is-pgvector); sidecar SQLite persists on declared named volumes.
2
4
  import { isPlainObject as plain } from '../../../engine/plain-object.mjs';
3
5
  import { composeEntries } from '../../lib/compose-values.mjs';
4
6
  import { repositoryOf } from '../../lib/stack-services.mjs';
7
+ import { loadSlotManifest } from '../slots.mjs';
8
+ import { judgedInEdition } from '../edition-slots.mjs';
9
+ import { readJson } from './read.mjs';
5
10
  import { stacksDeploymentFindings } from './stacks-runtime.mjs';
6
11
 
7
- const BACKING_IMAGE = /^(?:postgres(?:ql)?|postgresql-repmgr|redis(?:-stack(?:-server)?)?|mongo(?:db)?|mongodb-community-server)$/i;
12
+ const DATABASE_IMAGE = /^(?:postgres(?:ql)?|postgresql-repmgr|mongo(?:db)?|mongodb-community-server)$/i;
13
+ const APPEND_ONLY = /(?:^|\s)--appendonly\s+yes(?:\s|$)/;
14
+ const EVICTION_POLICY = /(?:^|\s)--maxmemory-policy\s+(\S+)/;
15
+ const FSYNC_POLICY = /(?:^|\s)--appendfsync\s+(\S+)/;
16
+ const BOUNDED_FSYNC = new Set(['everysec', 'always']);
17
+ const REDIS_DATA_DIR = '/data';
8
18
  const DATABASE_ADAPTER_KEYS = new Set(['db_type', 'database_type', 'db_provider', 'database_provider', 'db_client', 'database_client', 'db_adapter', 'database_adapter']);
9
19
  const SQLITE_ADAPTERS = new Set(['sqlite', 'sqlite3', 'better-sqlite3', 'sql.js']);
10
20
  const DATABASE_URL_KEY = /DB|DATABASE|POSTGRES|REDIS|MONGO/i;
@@ -14,18 +24,72 @@ const NAMED_VOLUME = /^[a-z0-9][a-z0-9_.-]*$/i;
14
24
  const mapping = value => plain(value) ? value : {};
15
25
  const present = value => typeof value === 'string' && Boolean(value.trim());
16
26
 
17
- function imageIssues(components, services) {
27
+ /** The backing service (a key of ruleParams.be.backing) whose image repository `image` names, or null. */
28
+ function serviceOfImage(catalog, image) {
29
+ if (!present(image)) return null;
30
+ const repository = repositoryOf(image).toLowerCase();
31
+ return Object.keys(catalog).find(service => catalog[service].images.includes(repository)) ?? null;
32
+ }
33
+
34
+ /** The services hfs.json sides.be.backing declares: from the resolved declaration, else from hfs.json at the app root. */
35
+ function declaredBacking(repo, repoRoot) {
36
+ const declared = repo?.sides?.be?.backing ?? repo?.backing ?? (present(repoRoot) ? readJson(repoRoot, 'hfs.json')?.sides?.be?.backing : undefined);
37
+ return new Set(Array.isArray(declared) ? declared : []);
38
+ }
39
+
40
+ function imageIssues(components, services, { catalog, declared }) {
18
41
  const errors = [];
19
42
  for (const [prefix, inventory] of [['components', components], ['services', services]]) {
20
43
  for (const [id, entry] of Object.entries(inventory)) {
21
- if (present(entry?.image) && BACKING_IMAGE.test(repositoryOf(entry.image)))
44
+ if (!present(entry?.image)) continue;
45
+ const service = serviceOfImage(catalog, entry.image);
46
+ if (DATABASE_IMAGE.test(repositoryOf(entry.image)))
22
47
  errors.push({ code: 'STACKS_LITE_SELFHOSTED_DB', at: `${prefix}.${id}.image`,
23
- message: 'lite must not run a self-hosted Postgres, Redis or Mongo database image; use managed Supabase backing' });
48
+ message: 'lite must not run a self-hosted Postgres or Mongo database image; use managed Supabase backing' });
49
+ else if (service !== null && !judgedInEdition(catalog[service], 'lite'))
50
+ errors.push({ code: 'STACKS_LITE_SELFHOSTED_DB', at: `${prefix}.${id}.image`,
51
+ message: `lite must not run ${service}; vector search uses Supabase pgvector (owner:lite-vector-is-pgvector)` });
52
+ else if (service !== null && !declared.has(service))
53
+ errors.push({ code: 'STACKS_LITE_SELFHOSTED_DB', at: `${prefix}.${id}.image`,
54
+ message: `lite runs ${service} only when hfs.json sides.be.backing declares it for the capability that needs it (ruling:R212/case-10)` });
24
55
  }
25
56
  }
26
57
  return errors;
27
58
  }
28
59
 
60
+ const commandText = service => (Array.isArray(service?.command) ? service.command.map(String).join(' ') : String(service?.command ?? ''));
61
+
62
+ function persistentDataMount(entry, volumes) {
63
+ if (typeof entry === 'string') {
64
+ const [name, target, options = ''] = entry.split(':');
65
+ return NAMED_VOLUME.test(name) && Object.hasOwn(volumes, name) && target === REDIS_DATA_DIR && !options.split(',').includes('ro');
66
+ }
67
+ return plain(entry) && entry.type === 'volume' && Object.hasOwn(volumes, entry.source) && entry.target === REDIS_DATA_DIR && entry.read_only !== true;
68
+ }
69
+
70
+ /**
71
+ * AOF on a writable named volume at the data directory, fsynced at least every second (everysec, the Redis default, or always),
72
+ * and no eviction policy but noeviction (the Redis default).
73
+ */
74
+ function persisted(service, volumes) {
75
+ const command = commandText(service);
76
+ const policy = EVICTION_POLICY.exec(command)?.[1].replaceAll(/["']/g, '');
77
+ const fsync = FSYNC_POLICY.exec(command)?.[1].replaceAll(/["']/g, '');
78
+ return APPEND_ONLY.test(command) && (policy === undefined || policy === 'noeviction') && (fsync === undefined || BOUNDED_FSYNC.has(fsync))
79
+ && (Array.isArray(service.volumes) ? service.volumes : []).some(entry => persistentDataMount(entry, volumes));
80
+ }
81
+
82
+ /** Queue jobs live in Redis, so a declared Redis runs as exactly one stack service that persists every job and evicts none. */
83
+ function redisPersistenceIssues(services, volumes, { catalog, declared }) {
84
+ if (!declared.has('redis')) return [];
85
+ const redis = Object.entries(services).filter(([, service]) => serviceOfImage(catalog, service?.image) === 'redis');
86
+ if (redis.length !== 1) return [{ code: 'STACKS_LITE_REDIS_PERSISTENCE', at: 'services',
87
+ message: `hfs.json sides.be.backing declares redis, so the lite stack runs exactly one Redis service; it runs ${redis.length}` }];
88
+ return redis.filter(([, service]) => !persisted(service, volumes))
89
+ .map(([id]) => ({ code: 'STACKS_LITE_REDIS_PERSISTENCE', at: `services.${id}`,
90
+ message: `the lite Redis runs redis-server --appendonly yes with appendfsync everysec (the default) or always, a writable named volume mounted at ${REDIS_DATA_DIR} and maxmemory-policy noeviction (the default), so queue jobs survive a restart and are never evicted` }));
91
+ }
92
+
29
93
  function unsupportedAdapter(service) {
30
94
  for (const [key, value] of composeEntries(service?.environment, { stringsOnly: true })) {
31
95
  const adapterKey = DATABASE_ADAPTER_KEYS.has(key.toLowerCase());
@@ -94,13 +158,15 @@ function sidecarIssues({ components, services, bindings, volumes }) {
94
158
  }
95
159
 
96
160
  /** The lite backing contract; full keeps its existing backing choices. */
97
- export function liteBackingIssues({ doc, environment, spec = doc?.environments?.[environment], model, repo, edition = repo?.edition }) {
161
+ export function liteBackingIssues({ doc, environment, spec = doc?.environments?.[environment], model, repo, repoRoot, edition = repo?.edition }) {
98
162
  if (edition !== 'lite' || spec?.runtime !== 'docker-swarm') return [];
99
- const components = mapping(doc?.components), services = mapping(model?.services);
163
+ const components = mapping(doc?.components), services = mapping(model?.services), volumes = mapping(model?.volumes);
164
+ const backing = { catalog: mapping(loadSlotManifest().ruleParams?.be?.backing), declared: declaredBacking(repo, repoRoot) };
100
165
  return [
101
- ...imageIssues(components, services),
166
+ ...imageIssues(components, services, backing),
167
+ ...redisPersistenceIssues(services, volumes, backing),
102
168
  ...renderedAdapterIssues(components, services),
103
- ...sidecarIssues({ components, services, bindings: mapping(spec.components), volumes: mapping(model?.volumes) }),
169
+ ...sidecarIssues({ components, services, bindings: mapping(spec.components), volumes }),
104
170
  ];
105
171
  }
106
172
 
@@ -23,7 +23,7 @@ export function hostCrontabFindings({ repoRoot, files }) {
23
23
  if (text === null) continue;
24
24
  for (const [index, line] of text.split(/\r?\n/).entries()) {
25
25
  if (HOST_CRON.test(line)) findings.push(found(CODE, file,
26
- `${file}:${index + 1}: host crontab installation is forbidden; recurring work must use an app scheduler, pgmq/pg_cron, or a declared Swarm scheduler and replicated-job`, { line: index + 1 }));
26
+ `${file}:${index + 1}: host crontab installation is forbidden; recurring work must use an app queue scheduler, a pg_cron tick enqueueing through the queue outbox, or a declared Swarm scheduler and replicated-job`, { line: index + 1 }));
27
27
  }
28
28
  }
29
29
  return findings;
@@ -381,14 +381,25 @@ function appComposesRealtime(repoRoot, app, files) {
381
381
  return false;
382
382
  }
383
383
 
384
+ const SHARED_HUBS = new Set(['postgres', 'redis']);
385
+
386
+ /** The hub a service selects with REALTIME_HUB, or null when it declares none. */
387
+ function realtimeHub(service) {
388
+ const entry = composeEntries(service.environment, { missingValue: null }).find(([key]) => key === 'REALTIME_HUB');
389
+ return entry && present(entry[1]) ? String(entry[1]).trim() : null;
390
+ }
391
+
392
+ /** A service runs on several nodes: more than one replica, or the global mode (one task per node). */
393
+ const scaledOut = service => service.deploy?.mode === 'global' || Number(service.deploy?.replicas ?? 1) > 1;
394
+
395
+ /** Realtime scale (knowledge/patterns/be/realtime.yaml BE-REALTIME-7): an API with several replicas selects a shared hub. */
384
396
  function realtimeIssues(input, add) {
385
- if (input.repo?.edition !== 'lite') return;
386
397
  const apis = new Set(list(input.repo?.sides?.be?.apps).filter(app => app.kind === 'api').map(app => app.name));
387
398
  for (const [id, service] of Object.entries(mapping(input.model.services))) {
388
399
  const component = mapping(input.doc.components)[id], app = component?.app ?? id;
389
- if (!apis.has(app) || service.deploy?.replicas === 1) continue;
390
- if (appComposesRealtime(input.repoRoot, app, list(input.files)))
391
- add('STACKS_REALTIME_SCALED', `services.${id}.deploy.replicas`, 'A lite API composing realtime must pin deploy.replicas: 1 because its realtime hub and gateway live in one API process');
400
+ if (!apis.has(app) || !scaledOut(service) || !appComposesRealtime(input.repoRoot, app, list(input.files))) continue;
401
+ if (!SHARED_HUBS.has(realtimeHub(service) ?? ''))
402
+ add('STACKS_REALTIME_SCALED', `services.${id}.environment.REALTIME_HUB`, 'A realtime API with more than one replica must select a shared hub (REALTIME_HUB postgres or redis): the memory hub serves one process only');
392
403
  }
393
404
  }
394
405
 
@@ -0,0 +1,73 @@
1
+ // R311: the Vietnamese copy of an app reaches users as written. A message catalog (`<owner>.messages.ts` or a `vi.json` locale file)
2
+ // is valid UTF-8, and no `vi` string carries the mark of a lossy re-encode: a diacritic letter replaced by a literal `?`
3
+ // (so a word reads as letters around a question mark) or by the U+FFFD replacement character.
4
+ import { found, readText } from './read.mjs';
5
+
6
+ export const HFS_TEXT_CORRUPT = 'HFS_TEXT_CORRUPT';
7
+ const CATALOG_TS_SUFFIX = '.messages.ts';
8
+ const CATALOG_JSON = /(?:^|\/)vi\.json$/;
9
+ const OUTSIDE = /(?:^|\/)(?:node_modules|\.git|dist|build)\//;
10
+ const REPLACEMENT = '\uFFFD';
11
+ // A `?` between letters, doubled, or closing a word before punctuation is a lost diacritic; a sentence-final "?" and a lone "?" are not.
12
+ const LOST_LETTER = /\p{L}\?(?=[\p{L}?])|\?\?|\p{L}\?(?=[.,;:)])|(?:^|[\s("])\?\p{L}/u;
13
+ const STRING_LITERAL = /"((?:[^"\\\n]|\\.)*)"/g;
14
+
15
+ function lineOf(text, index) {
16
+ return text.slice(0, index).split('\n').length;
17
+ }
18
+
19
+ // The text of the `vi:` map of a catalog bundle: from `vi:` to the first closing brace at the bundle's own indent.
20
+ function viBlocks(text) {
21
+ const blocks = [];
22
+ for (const start of text.matchAll(/\bvi:\s*\{/g)) {
23
+ const from = start.index + start[0].length;
24
+ const end = text.indexOf('\n }', from);
25
+ blocks.push({ from, to: end < 0 ? text.length : end });
26
+ }
27
+ return blocks;
28
+ }
29
+
30
+ function jsonStrings(node, out) {
31
+ if (typeof node === 'string') out.push(node);
32
+ else if (node && typeof node === 'object') for (const value of Object.values(node)) jsonStrings(value, out);
33
+ }
34
+
35
+ function corruptLine(file, line, value) {
36
+ return found(HFS_TEXT_CORRUPT, file, `${file}:${line} Vietnamese text ${JSON.stringify(value)} lost its diacritics (a "?" or U+FFFD stands for a letter); rewrite the file as UTF-8 with an editor or Node, never a shell pipe`, { line });
37
+ }
38
+
39
+ function tsFindings(file, text) {
40
+ const findings = [];
41
+ for (const { from, to } of viBlocks(text)) {
42
+ for (const literal of text.slice(from, to).matchAll(STRING_LITERAL)) {
43
+ if (LOST_LETTER.test(literal[1])) findings.push(corruptLine(file, lineOf(text, from + literal.index), literal[1]));
44
+ }
45
+ }
46
+ return findings;
47
+ }
48
+
49
+ function jsonFindings(file, text) {
50
+ let doc;
51
+ try { doc = JSON.parse(text); } catch { return []; }
52
+ const strings = [];
53
+ jsonStrings(doc, strings);
54
+ return strings.filter((value) => LOST_LETTER.test(value)).map((value) => corruptLine(file, lineOf(text, text.indexOf(value)), value));
55
+ }
56
+
57
+ /** Corrupted Vietnamese copy in the message catalogs of this root/side scope. */
58
+ export function textIntegrityFindings({ repoRoot, files, repo, inScope = () => true }) {
59
+ if (repo.kind === 'runtime' || !['lite', 'full'].includes(repo.edition ?? 'full')) return [];
60
+ const findings = [];
61
+ for (const file of files) {
62
+ const ts = file.endsWith(CATALOG_TS_SUFFIX);
63
+ if (!inScope(file) || OUTSIDE.test(file) || !(ts || CATALOG_JSON.test(file))) continue;
64
+ const text = readText(repoRoot, file);
65
+ if (text === null) continue;
66
+ if (text.includes(REPLACEMENT)) {
67
+ findings.push(found(HFS_TEXT_CORRUPT, file, `${file}:${lineOf(text, text.indexOf(REPLACEMENT))} is not valid UTF-8 (it holds U+FFFD); rewrite the file as UTF-8 with an editor or Node`, { line: lineOf(text, text.indexOf(REPLACEMENT)) }));
68
+ continue;
69
+ }
70
+ findings.push(...(ts ? tsFindings(file, text) : jsonFindings(file, text)));
71
+ }
72
+ return findings;
73
+ }
@@ -0,0 +1,74 @@
1
+ // hook-shape.mjs - RT_HOOK_SHAPE (knowledge/hfs/rules.yaml, gate runtime): the app hook templates keep the gate model.
2
+ // pre-commit L0 only: no typecheck and no test run (`test:` scripts, npm test, jest, vitest, a spec). Types and the targeted specs
3
+ // belong to the op gate.
4
+ // pre-push a release gate check, never a build or test run: no npm test, jest, vitest, typecheck or lint, and it carries the
5
+ // release gate markers: `starci-release` (the L4 record directory), `refs/backup/` (the backup namespace) and `v[0-9]`
6
+ // (the release tag pattern). A push of anything but the release cut and refs/backup/* is refused there.
7
+ // Guard hooks keep their host timeout above the runtime's own input, verdict, cleanup and output budgets.
8
+ // Comments are not judged (the templates explain what they do not run). Pure apart from ctx.read.
9
+ import { shellCommandSource } from '../../lib/shell-source.mjs';
10
+ import { HOOK_INPUT_LIMITS } from '../../lib/hook-limits.mjs';
11
+
12
+ export const CODE = 'RT_HOOK_SHAPE';
13
+ export const TEMPLATE_DIR = 'packages/hfs/templates/app/hooks/husky';
14
+ const PRE_COMMIT = `${TEMPLATE_DIR}/pre-commit`;
15
+ const PRE_PUSH = `${TEMPLATE_DIR}/pre-push`;
16
+ const HOST_SETTINGS = '.claude/settings.json';
17
+ const GUARD_LIMITS = 'modules/kernel/hook-input.yaml';
18
+
19
+ const PRE_COMMIT_FORBIDDEN = [
20
+ [/\btypecheck\b/, 'a typecheck'],
21
+ [/\btest:|\bnpm\s+(?:run\s+)?test\b|\bjest\b|\bvitest\b|\bspecs?\b/, 'a test run'],
22
+ ];
23
+ const PRE_PUSH_FORBIDDEN = [
24
+ [/\bnpm\s+(?:run\s+)?test\b|\btest:|\bjest\b|\bvitest\b/, 'a test run'],
25
+ [/\btypecheck\b/, 'a typecheck'],
26
+ [/\blint\b/, 'a lint run'],
27
+ ];
28
+ const PRE_PUSH_MARKERS = ['starci-release', 'refs/backup/', 'v[0-9]', 'RIGHTS_PUSH_NOT_RELEASE'];
29
+
30
+ const finding = (file, message) => ({ code: CODE, level: 'error', path: file, message: `${CODE} ${file}: ${message}` });
31
+
32
+ /** The RT_HOOK_SHAPE findings of one hook template: `name` is 'pre-commit' or 'pre-push', `text` its content. */
33
+ function fileHookShapeFindings({ path: file, name, text }) {
34
+ const body = shellCommandSource(text);
35
+ const found = [];
36
+ const forbidden = name === 'pre-commit' ? PRE_COMMIT_FORBIDDEN : PRE_PUSH_FORBIDDEN;
37
+ for (const [pattern, what] of forbidden) {
38
+ if (pattern.test(body)) found.push(finding(file, name === 'pre-commit' ? `the commit gate is L0 only and runs ${what}; types and targeted specs belong to the op gate` : `the push gate checks the release gate and runs ${what}; only the release cut tests`));
39
+ }
40
+ if (name === 'pre-push') {
41
+ const missing = PRE_PUSH_MARKERS.filter((marker) => !body.includes(marker));
42
+ if (missing.length) {
43
+ const markers = missing.map((m) => `\`${m}\``).join(', ');
44
+ found.push(finding(file, `the push gate lacks the release gate marker${missing.length === 1 ? '' : 's'} ${markers}; it must check the L4 record of HEAD and allow only the release cut and refs/backup/*`));
45
+ }
46
+ }
47
+ return found;
48
+ }
49
+
50
+ /** Guard hooks finish inside the host timeout even when input, verdict, tree cleanup and both writes exhaust their budgets. */
51
+ function guardTimeoutFindings(ctx) {
52
+ if (!ctx.fileSet.has(HOST_SETTINGS)) return [];
53
+ const limits = HOOK_INPUT_LIMITS;
54
+ const budget = limits.minInputWindowMs + limits.verdictTimeoutMs + limits.teardownTimeoutMs + 2 * limits.outputTimeoutMs;
55
+ const found = budget < limits.hookTimeoutSeconds * 1000 ? [] : [finding(GUARD_LIMITS, 'the complete guard lifetime must be shorter than the host hook timeout')];
56
+ let settings;
57
+ try { settings = JSON.parse(ctx.read(HOST_SETTINGS)); }
58
+ catch { return [...found, finding(HOST_SETTINGS, 'the guard hook settings must be readable JSON')]; }
59
+ const hooks = (settings?.hooks?.PreToolUse ?? []).flatMap((entry) => entry.hooks ?? []);
60
+ for (const hook of hooks) {
61
+ if (!/\bguard\s+(?:command|seat-tools)\b/.test(String(hook.command ?? ''))) continue;
62
+ if (hook.timeout !== limits.hookTimeoutSeconds) found.push(finding(HOST_SETTINGS, `the guard hook timeout must match ${GUARD_LIMITS} hookTimeoutSeconds`));
63
+ }
64
+ return found;
65
+ }
66
+
67
+ /** RT_HOOK_SHAPE over the app hook templates and tracked host guard settings (ctx of scripts/hfs/runtime-check.mjs). */
68
+ export function hookShapeFindings(ctx) {
69
+ return [...guardTimeoutFindings(ctx), ...[['pre-commit', PRE_COMMIT], ['pre-push', PRE_PUSH]].flatMap(([name, file]) => {
70
+ if (!ctx.fileSet.has(file)) return [];
71
+ const text = ctx.read(file);
72
+ return text === null || text === undefined ? [] : fileHookShapeFindings({ path: file, name, text });
73
+ })];
74
+ }
@@ -0,0 +1,44 @@
1
+ // release-notes.mjs - RELEASE_NOTES (knowledge/hfs/rules.yaml, gate runtime): a release tag `v<version>` on HEAD has its CHANGELOG.md section
2
+ // `## [<version>]` with no unfinished markers left in it and no `in preparation` mark. The tag message and the GitHub Release are that section
3
+ // (docs/git-governance.md), so a tag cut over an unfinished section would publish a half-written release. Only the tags that point at HEAD are
4
+ // judged: the release being cut. Pure apart from ctx.read and the git read (ctx.tagsAtHead overrides it).
5
+ import { tag } from '../../api/git/tag.mjs';
6
+
7
+ export const CODE = 'RELEASE_NOTES';
8
+ const RELEASE_TAG = /^v\d[\w.+-]*$/;
9
+ const UNFINISHED = /\b(?:TODO|PENDING|TBD)\b|in preparation/i;
10
+
11
+ /** The CHANGELOG section of `version`: {found, body}. Pure. */
12
+ export function changelogSection(changelog, version) {
13
+ const lines = String(changelog ?? '').split(/\r?\n/);
14
+ const start = lines.findIndex((line) => { const m = /^## \[([^\]]+)\]/.exec(line); return m && m[1] === version; });
15
+ if (start < 0) return { found: false, body: '' };
16
+ const end = lines.findIndex((line, i) => i > start && line.startsWith('## ['));
17
+ return { found: true, body: lines.slice(start, end < 0 ? undefined : end).join('\n') };
18
+ }
19
+
20
+ /** The RELEASE_NOTES findings of the release tags `tags` against `changelog` text. Pure. */
21
+ export function releaseNotesFindings({ tags, changelog, file = 'CHANGELOG.md' }) {
22
+ const found = [];
23
+ for (const name of tags.filter((t) => RELEASE_TAG.test(t))) {
24
+ const section = changelogSection(changelog, name.slice(1));
25
+ if (!section.found) { found.push({ code: CODE, level: 'error', path: file, message: `${CODE} ${file}: the release tag ${name} has no \`## [${name.slice(1)}]\` section: write the release notes before the tag` }); continue; }
26
+ const unfinished = new RegExp(UNFINISHED.source, 'gi');
27
+ const marks = [...new Set([...section.body.matchAll(unfinished)].map(([mark]) => mark))];
28
+ if (marks.length) {
29
+ const listedMarks = marks.map((m) => `\`${m}\``).join(', ');
30
+ found.push({ code: CODE, level: 'error', path: file, message: `${CODE} ${file}: the section of ${name} still holds ${listedMarks}: finish it before the tag` });
31
+ }
32
+ }
33
+ return found;
34
+ }
35
+
36
+ /** RELEASE_NOTES over the checkout (ctx of scripts/hfs/runtime-check.mjs): the release tags at HEAD against the CHANGELOG at HEAD. */
37
+ export function releaseNotesRepoFindings(ctx) {
38
+ let tags = ctx.tagsAtHead;
39
+ if (!tags) {
40
+ try { const r = tag(['--points-at', 'HEAD'], { dir: ctx.repoRoot }); tags = r.status === 0 ? String(r.stdout).split(/\r?\n/).map((t) => t.trim()).filter(Boolean) : []; } catch { tags = []; }
41
+ }
42
+ const changelog = ctx.read('CHANGELOG.md');
43
+ return tags.some((t) => RELEASE_TAG.test(t)) ? releaseNotesFindings({ tags, changelog }) : [];
44
+ }
@@ -80,10 +80,10 @@ function placeholderValue(key) {
80
80
  return generated;
81
81
  }
82
82
 
83
- /** Create missing members for this app's recipient, preserving owner-provisioned members. */
84
- export function materializeDeclaredPlaceholders(repoRoot, { recipient, recipientsForFile, processEnv = process.env, sops, write } = {}) {
83
+ /** Create missing members (those `only` selects) for this app's recipient, preserving owner-provisioned members. */
84
+ export function materializeDeclaredPlaceholders(repoRoot, { recipient, recipientsForFile, processEnv = process.env, sops, write, only = () => true } = {}) {
85
85
  const members = [];
86
- for (const member of declaredSealedMembers(repoRoot)) {
86
+ for (const member of declaredSealedMembers(repoRoot).filter((declared) => only(declared))) {
87
87
  const relative = `.starcistacks/${member.env}/secrets/${member.slug}.enc`;
88
88
  if (fs.existsSync(path.join(repoRoot, relative))) continue;
89
89
  const age = recipientsForFile?.(path.join(repoRoot, relative)) ?? [recipient];
@@ -1,5 +1,5 @@
1
1
  // slot-side-problems.mjs - declaration checks for one app side against the HFS manifest.
2
- import { optionalSlotProblems } from './declaration-slots.mjs';
2
+ import { backingProblems, optionalSlotProblems } from './declaration-slots.mjs';
3
3
 
4
4
  function sideAppKindProblems(manifest, side, apps) {
5
5
  const bad = [];
@@ -19,9 +19,9 @@ function requiredAppKindProblems(manifest, side, declaration) {
19
19
  }
20
20
 
21
21
  /** Problems for one side declaration, preserving the manifest's checks and their order. */
22
- export function sideProblems(manifest, side, declaration) {
22
+ export function sideProblems(manifest, side, declaration, edition = 'full') {
23
23
  const bad = sideAppKindProblems(manifest, side, declaration.apps);
24
- bad.push(...optionalSlotProblems(manifest, side, declaration));
24
+ bad.push(...optionalSlotProblems(manifest, side, declaration), ...backingProblems(manifest, side, declaration, edition));
25
25
  for (const read of declaration.reads ?? []) if (!manifest.sides[side].reads.includes(read)) bad.push(`sides.${side}.reads names ${read}; ${side} may read only ${manifest.sides[side].reads.join(', ') || 'nothing of the other side'}`);
26
26
  bad.push(...requiredAppKindProblems(manifest, side, declaration));
27
27
  return bad;
@@ -116,6 +116,7 @@ function sideDeclaration(manifest, declaration, name, edition, providers) {
116
116
  apps: Object.freeze(s.apps.map((a) => Object.freeze({ name: a.name, kind: a.kind }))),
117
117
  optionalSlots: Object.freeze([...(s.optionalSlots ?? [])]),
118
118
  patterns: Object.freeze([...(s.patterns ?? [])]), kinds: Object.freeze([...(s.kinds ?? [])]),
119
+ backing: Object.freeze([...(s.backing ?? [])]),
119
120
  connections: Object.freeze((s.connections ?? []).map(connectionView)),
120
121
  providers,
121
122
  reads: Object.freeze([...(s.reads ?? [])]),
@@ -139,7 +140,7 @@ export function resolveRepoDeclaration(manifest, declaration, { file = HFS_DECLA
139
140
  if (declaration.kind === RUNTIME_KIND) return runtimeDeclaration(manifest, declaration, file, side);
140
141
  const { edition, known, valid } = declarationEdition(manifest, declaration);
141
142
  if (!valid) fail('HFS_EDITION_INVALID', `hfs.json edition is ${JSON.stringify(declaration.edition)}; the editions this manifest knows are ${known.join(', ')} (absent means full)`, { file, edition: declaration.edition });
142
- const bad = PROFILES.flatMap((name) => sideProblems(manifest, name, declaration.sides[name]));
143
+ const bad = PROFILES.flatMap((name) => sideProblems(manifest, name, declaration.sides[name], edition));
143
144
  if (bad.length) declarationInvalid(bad, file);
144
145
  if (side !== null && !PROFILES.includes(side)) fail('HFS_DECLARATION_INVALID', `${side} is not a side of an app (be, fe)`, { file, side });
145
146
  // A provider declared on any connection enables the provider slots of every profile (a fe side declares none): each view carries the union.