@starci/hfs 4.10.1 → 4.11.8
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.
- package/CHANGELOG.md +76 -0
- package/README.md +40 -1
- package/db/drift.mjs +19 -16
- package/db/image.mjs +13 -0
- package/db/migrate.mjs +3 -3
- package/db/postgres.mjs +1 -1
- package/package.json +2 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +23 -8
- package/runtime/knowledge/hfs/rules.yaml +54 -13
- package/runtime/knowledge/hfs/slots.yaml +67 -36
- package/runtime/knowledge/patterns/be/event-bus.yaml +12 -10
- package/runtime/knowledge/patterns/be/idempotency.yaml +1 -1
- package/runtime/knowledge/patterns/be/jobs.yaml +3 -0
- package/runtime/knowledge/patterns/be/queues.yaml +14 -14
- package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +28 -18
- package/runtime/modules/schemas/application-stacks.schema.yaml +4 -2
- package/runtime/scripts/gates/starcistacks.mjs +6 -2
- package/runtime/scripts/guards/push-scan.mjs +1 -1
- package/runtime/scripts/hfs/ci-declaration.mjs +3 -2
- package/runtime/scripts/hfs/db-baseline-verb.mjs +94 -22
- package/runtime/scripts/hfs/db-errors.mjs +2 -0
- package/runtime/scripts/hfs/db-plan-verb.mjs +17 -5
- package/runtime/scripts/hfs/db-squash.mjs +514 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +3 -15
- package/runtime/scripts/hfs/example-secrets.mjs +4 -3
- package/runtime/scripts/hfs/rule-params-shape.mjs +11 -9
- package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -1
- package/runtime/scripts/hfs/rules/database-config.mjs +1 -1
- package/runtime/scripts/hfs/rules/database-constants.mjs +16 -1
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +2 -110
- package/runtime/scripts/hfs/rules/database-queues.mjs +53 -239
- package/runtime/scripts/hfs/rules/database-sql.mjs +2 -0
- package/runtime/scripts/hfs/rules/database-transaction.mjs +45 -0
- package/runtime/scripts/hfs/rules/deploy-scan.mjs +1 -1
- package/runtime/scripts/hfs/rules/edition.mjs +1 -1
- package/runtime/scripts/hfs/rules/env-parity.mjs +1 -1
- package/runtime/scripts/hfs/rules/lite-secrets.mjs +3 -3
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +1 -1
- package/runtime/scripts/hfs/rules/secret-custody-only.mjs +2 -2
- package/runtime/scripts/hfs/rules/secrets.mjs +1 -1
- package/runtime/scripts/hfs/rules/stack-tooling.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-deploy.mjs +2 -1
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +3 -4
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +2 -2
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks.mjs +3 -3
- package/runtime/scripts/hfs/sealed-members.mjs +10 -6
- package/runtime/scripts/hfs/secret-files.mjs +16 -0
- package/runtime/scripts/hfs/secret-init.mjs +120 -21
- package/runtime/scripts/hfs/secret-output.mjs +2 -1
- package/runtime/scripts/hfs/secret-prompt.mjs +128 -0
- package/runtime/scripts/hfs/secret-sops.mjs +93 -9
- package/runtime/scripts/hfs/secret-standard.mjs +315 -0
- package/runtime/scripts/hfs/secret-store.mjs +23 -9
- package/runtime/scripts/hfs/secret.mjs +110 -46
- package/runtime/scripts/hfs/slots.mjs +8 -1
- package/runtime/scripts/hfs/stack-deploy-config.mjs +20 -6
- package/runtime/scripts/hfs/stacks-gate.mjs +9 -2
- package/runtime/scripts/hfs/starcistacks-runtime.mjs +1 -1
- package/runtime/scripts/lib/sealed-member.mjs +36 -0
- package/runtime/scripts/lib/secret-patterns.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.mjs +4 -6
- package/scaffold/add-inbox-lite.mjs +2 -2
- package/scaffold/add-queue-lite.mjs +24 -5
- package/scaffold/app.mjs +2 -2
- package/src/main.mjs +5 -4
- package/stack/build.mjs +3 -1
- package/stack/config.mjs +2 -1
- package/stack/docker-transport.mjs +2 -1
- package/stack/remote.mjs +4 -3
- package/stack/sealed.mjs +9 -8
- package/stack/target-binding.mjs +2 -1
- package/sync/hygiene.mjs +1 -1
- package/sync/index.mjs +47 -12
- package/templates/app/db-bootstrap/db/bootstrap.sql +9 -14
- package/templates/app/db-image/starcistacks/images/db/Dockerfile +18 -0
- package/templates/app/gitignore +2 -1
- package/templates/app/gitignore-lite +3 -1
- package/templates/app/package-scripts/db.json +2 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +4 -2
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +1 -1
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +1 -1
- package/templates/app/skeleton-lite/README.md +2 -2
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +7 -5
- package/templates/be/inbox/events.json.tpl +1 -1
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +58 -11
- package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +13 -1
- package/templates/be/inbox/platform/event-bus/event-bus.log-events.ts.tpl +9 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +1 -1
- package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +11 -5
- package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +9 -2
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +18 -5
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +37 -16
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +33 -5
- package/templates/be/inbox/platform/event-bus/event-consumer.service.spec.ts.tpl +143 -0
- package/templates/be/inbox/platform/event-bus/event-consumer.service.ts.tpl +119 -0
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +13 -4
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +4 -4
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +4 -4
- package/templates/be/inbox/platform/migration.sql.tpl +97 -34
- package/templates/be/inbox/service.spec.ts.tpl +1 -1
- package/templates/be/inbox/webhook-door.ts.tpl +2 -2
- package/templates/be/patterns/jobs/platform/jobs.log-events.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform-lite/job-claim.service.spec.ts.tpl +4 -4
- package/templates/be/patterns/jobs/platform-lite/job-claim.service.ts.tpl +5 -5
- package/templates/be/patterns/jobs/platform-lite/job-tick.service.spec.ts.tpl +65 -0
- package/templates/be/patterns/jobs/platform-lite/job-tick.service.ts.tpl +78 -0
- package/templates/be/patterns/jobs/platform-lite/job-ticks.contracts.ts.tpl +16 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.module.ts.tpl +40 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.service.spec.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform-lite/queue-outbox.service.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +2 -2
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +2 -2
- package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +37 -2
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +36 -3
- package/templates/be/skeleton/src/modules/platform/http/http.config.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +5 -3
- package/templates/be/skeleton/src/modules/platform/http/http.options.ts +9 -2
- package/templates/be/skeleton/src/modules/platform/http/index.ts +2 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.client.spec.ts +96 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.client.ts +93 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -1
- package/runtime/scripts/hfs/rules/database-queue-outbox.mjs +0 -102
- package/templates/be/skeleton-lite/src/modules/platform/database/database.functions.ts +0 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.11.8 - 2026-10-29
|
|
4
|
+
|
|
5
|
+
- Added: `starci app secret migrate <env> <member> --to <existing> --into-existing` and `split <env> <member> --into-existing` merge into a member that already exists (never a legacy `.enc` one: migrate it first). Without the flag an existing target is refused, naming the flag. Every existing target is decrypted in memory and checked before anything is written: a key it already holds with another value refuses the whole move (`refused, nothing was written: <env>/<member>.enc.yaml already holds KEY with another value`, names only); an identical value merges. Each target is written once, and the source is removed only after every target is written (an own target is staged as `<member>-staged.enc.yaml` and moved over it). The dry run names each target's keys, which targets exist and the keys they already hold (`secret-standard.mjs` `plannedTargets`, `moveKeys`).
|
|
6
|
+
- Added: `split <env> <member> --group KEY=member` (repeatable) and `--group-file <yaml>` (a map of `KEY: member`) send a key to the owner's member instead of its first word's; a key named twice, or one the member does not hold, is refused. The dry run prints the resulting plan (`groupMap`).
|
|
7
|
+
- Changed: `audit` does not name MIXED_PROVIDERS for a member the stack groups on purpose, one whose every key the environment's `secrets:` declares per key with `member: <that member>` (`declaredGroup`); the finding names that fix beside split.
|
|
8
|
+
- Changed: `deploy.secrets.settings` of application-stacks takes one `{ member, keys }` or a list of them over several members; a key two entries name is refused (STACKS_DEPLOY_CONFIG_INVALID), and the sealer reads each listed member once (`stack-deploy-config.mjs`, `stack/sealed.mjs`).
|
|
9
|
+
- Changed: the bundled canon pins name hfs 4.11.8, eslint-canon-be 3.8.7, eslint-canon-fe 9.5.9 and the Grammar family 0.15.20.
|
|
10
|
+
- Spec: `src/hfs-secret-standard.spec.mjs` (group, merge, collision refusal, group file, grouped audit; fake values only), `src/hfs-stacks-deploy.spec.mjs` (settings list).
|
|
11
|
+
|
|
12
|
+
## 4.11.7 - 2026-10-29
|
|
13
|
+
|
|
14
|
+
- Fixed: `starci app db baseline --squash` reaches a loopback source (`127.0.0.1`, `localhost`, `[::1]`, as the app's own dev database on `127.0.0.1:54322`) on a Linux Docker engine. The scratch container reached it only as `host.docker.internal`, which a Linux engine maps to its bridge gateway, where a port published on 127.0.0.1 refuses the connection; Docker Desktop alone forwarded it. `sourceTarget` (`runtime/scripts/hfs/db-squash.mjs`) now keeps the first route psql connects on: the scratch container through `host.docker.internal`, else a one-shot container of the same image on the host's network; when neither connects the verb refuses with each route's psql failure. Every client tool runs where its target's route says (`exec`, `targetRunner`).
|
|
15
|
+
- Changed: a failed history read of the squash source names the psql failure (password and URL withheld) instead of "database command failed; inspect psql privately".
|
|
16
|
+
- Changed: `starci app db plan` refuses a dynamic migration block naming every `EXECUTE` of every active migration as `<file>:<line>` under "dynamic migration blocks cannot prove database inventory", instead of the bare heading at the first one (`repositoryState`, `runtime/scripts/hfs/db-plan-verb.mjs`; the line is the DO body's opening quote plus the statement's body line).
|
|
17
|
+
- Changed: `starci app db baseline` passes the plan's refusal through (`cleanPlan`, `db-baseline-verb.mjs`) after "baseline requires a clean database plan with every migration already applied; the plan refused: ", and on drift names each inventory that differs with its missing, unexpected and changed counts, instead of the bare heading with the plan's output discarded.
|
|
18
|
+
- Changed: the bundled canon pins name hfs 4.11.7, eslint-canon-be 3.8.6, eslint-canon-fe 9.5.8 and the Grammar family 0.15.19.
|
|
19
|
+
- Spec: `src/hfs-db-squash.spec.mjs`: its source, published on 127.0.0.1 only, is the loopback route CI's Linux engine proves; a source no route reaches is refused. `src/hfs-database-baseline.spec.mjs`: two EXECUTEs in one block after a multi-byte comment and one in a second migration are named by file and line, with no database query; baseline's pass-through of a plan refusal and of a drift.
|
|
20
|
+
|
|
21
|
+
## 4.11.6 - 2026-10-29
|
|
22
|
+
|
|
23
|
+
- Added: the skeleton's `platform/http` takes `HttpOptions.trustedCa`, the PEM of one more certificate authority the outbound client trusts beside Node's bundled roots (never instead of them; verification stays on). `FetchHttpClient` builds one undici `Agent({ connect: { ca: [...rootCertificates, trustedCa] } })` for it and hands it to the global `fetch` as `dispatcher` on every request, closing it at module destroy; without the option it builds no pool and every request is sent exactly as before. `HTTP_OPTIONS` and `InjectHttpOptions` carry the options (`http.decorators.ts`), `parseHttpConfig` (`http.config.ts`) reads them from the optional `HTTP_TRUSTED_CA` (or `HTTP_TRUSTED_CA_FILE`), and the full skeleton's api app reads `http: parseHttpConfig(env)` into its options and registers `HttpModule.register({ isGlobal: true, ...options.http })`, so a test world passes its per-run fake CA (`w.fake.<name>.ca`) as `HTTP_TRUSTED_CA` in the environment it builds the app's options from.
|
|
24
|
+
- Added: `undici` 6.29.0 joins the starter dependencies of both editions (its one owner is `platform/http`, R90). Specs: `src/platform-http-trusted-ca.spec.mjs` (the skeleton client against a real HTTPS server signed by a per-run CA from test-world's `issueFakeTls`: refused without `trustedCa`, answered with it, refused under an unrelated CA, and the CA joins the bundled roots), and the skeleton's `fetch-http-client.client.spec.ts` (no dispatcher by default; one shared pool with the CA, closed at destroy).
|
|
25
|
+
- Changed: the bundled canon pins name hfs 4.11.6, @starci/test-world 1.3.1, eslint-canon-be 3.8.5, eslint-canon-fe 9.5.7 and the Grammar family 0.15.18, and pin `undici` 6.29.0 (owner `platform/http`).
|
|
26
|
+
|
|
27
|
+
## 4.11.5 - 2026-10-29
|
|
28
|
+
|
|
29
|
+
- Changed: `starci app secret set <env> <member> <KEY> [KEY ...]` seals every named key in one sops write, so no member is left half filled. On a terminal it prompts `<KEY>: ` for each key with masked input (`runtime/scripts/hfs/secret-prompt.mjs` `promptHidden`: one `*` per character, a paste in one chunk, backspace, Enter; Ctrl+C or end of input cancels with nothing written; an empty value is refused) instead of refusing "set reads a value from redirected stdin"; redirected stdin gives one key its whole text (a multi-line value kept) or several one `KEY=value` line each, every key once and no other; `--generate <bytes>` makes one value per key. `secretMain` is async and takes `prompt` and `hostIdentity` ports, which `main` passes through.
|
|
30
|
+
- Changed: the age identity is found by itself (`localAgeIdentity`, `secret-init.mjs`): SOPS_AGE_KEY or SOPS_AGE_KEY_FILE of the environment, else of the app's `.claude/secret.env`, else of its main checkout's when the app is a git worktree (`mainCheckout`, `secret-files.mjs`), else the `hostIdentity` the harness CLI passes (its secret.env's SOPS_AGE_KEY), else the sops `keys.txt`. `defaultSops` takes that port and exposes `identity` and `requireIdentity`; a `set` that adds to an existing member, and any decrypt, with no identity is refused before a value is asked for, naming the recipient, every place looked and the one-line fix. A decrypt whose identity is not the member's recipient is refused naming where the identity came from and the recipient, instead of `SOPS selected identity [recipient-mismatch]`. Re-sealing a member whose every key the set replaces decrypts nothing (`secret-store.mjs`). The success line of a set that creates a member names its recipient and where this machine holds the identity, or the fix.
|
|
31
|
+
- Added (ruling app-secrets-one-standard): a member is one provider's `.starcistacks/<env>/secrets/<provider>.enc.yaml`, sops YAML whose values come back byte for byte (decrypted to JSON, `secret-sops.mjs`); a legacy dotenv `.enc` is read, and added to in its own format, until `migrate` moves it. Every reader of the secrets folder resolves members through `runtime/scripts/lib/sealed-member.mjs` (`memberFile`, `isSealedName`; a member sealed in both forms is refused), and the gates accept `.enc.yaml` where they accepted `.enc` (stacks shape, custody-only, lite secrets, env parity, R06, hygiene, the push scan, the stacks gate's `where`, deploy and script scans); the `.starcistacks` gate's custody check admits `<env>/secrets/<member>.enc.yaml` while runtime custody keeps `<name>.<fmt>.enc`.
|
|
32
|
+
- Added: `set --from-file KEY=path` reads a key byte for byte (a PEM, a trailing newline); a member is sealed to its environment's rule of the app's `.sops.yaml`, and sealing runs with no identity selected, so a rule may name several recipients. `recipients <env> [--add|--remove <age1...|owner>]` prints or rewrites one rule per environment before the app-wide one (`environmentRecipients`), `owner` being the found identity's recipient; `init` takes that identity (the harness secret.env's included) as the app's recipient instead of generating one. The app-wide rule matches `.enc.yaml`, and an earlier `.enc`-only rule still matches a `.enc.yaml` member.
|
|
33
|
+
- Added: `audit [<env>]` names LEGACY_MEMBER, MIXED_PROVIDERS (by the first word of the keys), RECIPIENT_DRIFT, UNDECLARED_KEY, MISSING and UNTRACKED, exit 1 while any stands; `migrate <env> <member> [--to <provider>] [--execute]` and `split <env> <member> [--execute]` move members, dry runs naming keys only, decrypting in memory and never writing plaintext (`secret-standard.mjs`). `set` declares each key of a compose environment in its stack `secrets:` (`name`, `member`, `key`, `where`, comments kept, the edit proved by a re-parse); a swarm environment's keys are named to wire by hand. The application-stacks schema admits `member` and `key` on a secret entry, and the stacks gate checks the member holds the key.
|
|
34
|
+
- Changed: the managed `.gitignore` block re-includes `.starcistacks/**/secrets/*.enc.yaml` beside the legacy `*.enc`.
|
|
35
|
+
- Spec: `src/hfs-secret-standard.spec.mjs`.
|
|
36
|
+
- Changed: the bundled canon pins name hfs 4.11.5, eslint-canon-be 3.8.4, eslint-canon-fe 9.5.6 and the Grammar family 0.15.17.
|
|
37
|
+
- Spec: `src/hfs-secret-set.spec.mjs` (fake values only; a child process with a forced-TTY stdin drives the real prompt).
|
|
38
|
+
|
|
39
|
+
## 4.11.4 - 2026-10-29
|
|
40
|
+
|
|
41
|
+
- Changed: the bundled canon pins name @starci/test-world 1.3.0 (HTTPS fakes over the run's ephemeral fake CA), hfs 4.11.4, eslint-canon-be 3.8.3, eslint-canon-fe 9.5.5 and the Grammar family 0.15.16. No rule changed.
|
|
42
|
+
|
|
43
|
+
## 4.11.3 - 2026-10-29
|
|
44
|
+
|
|
45
|
+
Breaking for lite apps (rulings lite-pgmq-bus-bullmq-jobs and lite-db-image-in-stacks): `npx starci app sync --write` moves the db image and removes `db/Dockerfile`; an app whose hfs.json declares `sides.be.queueOutbox` drops it, and a pg_cron job left in an active migration is rewritten as a `platform/jobs` tick.
|
|
46
|
+
|
|
47
|
+
- Changed: the managed db image installs pgmq, the lite event bus: `ADD --checksum=sha256:67221f28…27f7` of the pgmq 1.13.1 release source (sync `PGMQ`), whose control and SQL files are copied into the image (pgmq is SQL objects only; it installs into its own schema `pgmq`). Proved by building the image and running create, send_topic, read, archive, bury and requeue as the app role.
|
|
48
|
+
- Changed: the db image's Dockerfile is `.starcistacks/images/db/Dockerfile` (slot `app.db.image`, `db/image.mjs` `DB_DOCKERFILE` and `DB_IMAGE_CONTEXT`), built with that folder as its context by `stack build`, the managed `images.yml` and `stack-deploy.yml`, `npm run db:image`, `app db drift` (`DB_DRIFT_NO_IMAGE` names the new path) and `app db baseline --squash`; `app sync` reports a leftover `db/Dockerfile` as retired (`--check`) and removes it (`--write`). The lite .gitignore block re-includes the file, and the stacks gates treat `.starcistacks/images/` as no environment and no custody.
|
|
49
|
+
- Changed: the lite event-bus templates (templates/be/inbox/platform/event-bus, contexts/code/be/event-bus.md BE-EVENT-BUS-6) publish once with `public.event_bus_send` (`pgmq.send_topic` of the typed envelope to the event name) in the publisher's transaction, with no outbox table or relay, and pgmq routes it to every consumer queue `<topic>__<consumer>` bound to the name. `event-consumer.service.ts` is the `EventConsumerRegistry` (`subscribe(QUEUE, consumer, { maxDeliveries })`) and the read loop of each consumer queue (started at application bootstrap, stopped at shutdown, no scheduler): `public.event_bus_read` with a visibility timeout, `public.event_bus_archive` on success, a failure left to reappear, `public.event_bus_bury` into `<queue>__dead` (original archived, logged with event and consumer) past `maxDeliveries` (default 5) or when malformed. `BaseEvent.payload` is an `EventPayload` carrying `dedupeKey`, and each delivery hands the consumer `consumer` and `dedupeKey` for its inbox claim; `EventBus` gains `deadLetters(queue, limit)` and `requeue(queue, id)`. The receipt event and its contract carry `dedupeKey` (the receipt id). The inbox migration creates the extension and the security-definer bus functions (the app role holds no pgmq grant); the `event_bus` table and `event_bus_write` are retired. Proved on the built image: a rolled-back send leaves nothing, a committed send reaches two bound consumers once each, bury, dead-letter read and requeue.
|
|
50
|
+
- Changed: R258 `DB_PGMQ_DECLARED` (`rules/database-queues.mjs`) stays the event-bus law, requires each topic queue's `<queue>__dead` queue (which it owns), and also refuses a `pgmq.bind_topic` that is not literal, not in an active migration, or binds a queue no earlier `pgmq.create` declared; `DB_CRON_OWNED`, the queue-outbox enqueue check, `rules/database-queue-outbox.mjs` and the scheduler inventory of `rules/database-queue-sources.mjs` are retired (pg_cron stays refused by R259). `databaseQueueDeclarations` still lists the pg_cron jobs an archived chain schedules for `db baseline`.
|
|
51
|
+
- Changed: the bundled slot manifest 3.13.0 admits `pgmq` in `ruleParams.db.extensions` (the lite event bus, ruling lite-pgmq-bus-bullmq-jobs), retires `ruleParams.db.queueOutbox` and `sides.be.queueOutbox` with the pg_cron half of R258, lists `public.event_bus_send`, `public.event_bus_read`, `public.event_bus_archive`, `public.event_bus_bury`, `public.event_bus_dead_letters` and `public.event_bus_requeue` in place of `public.event_bus_write` in `ruleParams.be.dbFunctions`, and moves slot `app.db.image` to `.starcistacks/images/db/Dockerfile` (ruling lite-db-image-in-stacks, admitted by the lite `app.starcistacks` allows); the bundled rule catalog keeps R258 `DB_PGMQ_DECLARED` as the event-bus law, retires `DB_CRON_OWNED` and adds R336 `BE_EVENT_BUS_LITE_SHAPE`; the bundled canon pins name hfs 4.11.3, eslint-canon-be 3.8.2, eslint-canon-fe 9.5.4 and the Grammar family 0.15.15.
|
|
52
|
+
|
|
53
|
+
## 4.11.2 - 2026-10-29
|
|
54
|
+
|
|
55
|
+
- Changed: the lite skeleton's database helpers live in `platform/database/database.client.ts` (a `client`, a logic role of the closed suffix list, so the file is measured and its colocated `database.client.spec.ts` is a legal unit spec), replacing `database.functions.ts`, whose suffix is not on the list. `callDbFunction` takes `schema.name` or a bare pg built-in of the canon's list (called in pg_catalog), refuses anything else with `DatabaseError` `DATABASE_IDENTIFIER_REJECTED` before building SQL, double-quotes each checked part and builds the parameter list from the argument count; it is no longer `async` (R73). The call statement parses for R268's recurring-work scan, so it raises no `HFS_SCHEDULE_UNDECLARED`.
|
|
56
|
+
- Changed: the lite jobs (`job_tick_claim` of R335 among them), queue, inbox and event-bus templates call their database functions schema-qualified (`public.job_claim`, `public.queue_write`, ...), and their specs expect the quoted statement. Spec: `src/db-functions-call.spec.mjs`.
|
|
57
|
+
- Changed: the bundled slot manifest 3.12.0 lists the canon's database functions of R334 schema-qualified (`public.job_claim`, `public.job_tick_claim`) beside the pg built-ins it names bare (`gen_random_uuid` joins them), and its shape (`rule-params-shape.mjs`, `declaration-shape.mjs`) with the manifest check of `slots.mjs` refuses in hfs.json `sides.be.dbFunctions` a `pg_catalog` name and any bare name that is not a built-in the canon lists; the manifest bump is minor although that refusal is stricter, since R334 is new on manifest major 3 and its one app (nivo-lite) lists only names it admits (its one bare name, `gen_random_uuid`, is now a canon built-in); the bundled rule catalog's R334 and R68 laws name `platform/database/database.client.ts` as the one composer of the call statement; the bundled canon pins name hfs 4.11.2, eslint-canon-be 3.8.1, eslint-canon-fe 9.5.3 and the Grammar family 0.15.14.
|
|
58
|
+
|
|
59
|
+
## 4.11.1 - 2026-10-29
|
|
60
|
+
|
|
61
|
+
- Added: `starci app db baseline --squash --url-env <NAME>` (`runtime/scripts/hfs/db-squash.mjs`) retires the whole active chain into one baseline read from a database whose history records exactly that chain, for a cutover whose production starts on an empty database; the source is only read. It keeps the app's schemas with their privileges (`pg_dump --schema-only --no-owner`, no `-x`) and object references and function bodies byte-identical, and leaves out, listing each, the extensions of `--drop-extension` (default `pg_net,pg_cron`; pgmq stays as the lite event bus), the pgmq queues of `--drop-pgmq-queue` (names or `*` patterns, no match drops nothing), every function or view reaching a dropped extension's schema, an empty publication (`supabase_realtime`), event triggers on platform functions, platform schemas, roles and the grants naming them, and grants to the migrating role that owns every replayed object; a kept object naming a left-out one is refused with both names. `--apply` writes the baseline (kept pgmq queues and grants outside the app schemas in its tail), a marked section of `db/bootstrap.sql` (app roles with their attributes and no password, memberships, allowed role settings such as `search_path`, grants on bootstrap schemas, database-wide default privileges), `db/seed.sql` from `--seed-tables` (`on conflict do nothing`, foreign-key order, sequences set) and archives the chain. Each run first replays bootstrap, baseline and seed (twice) on a scratch container of the app's db image (`db/Dockerfile`, or `--image`) and refuses unless the replay's projection equals the source's; every PostgreSQL client runs in that container. Spec: `src/hfs-db-squash.spec.mjs`.
|
|
62
|
+
- Changed: `db/bootstrap.sql` creates the `extensions` schema again, with USAGE for its roles: allowed extensions are created `with schema extensions` and the app role's search_path is `public, extensions`, so migrations that name `extensions.vector`, `extensions.unaccent` or `extensions.gen_random_bytes`, or pin a function's search_path to `''`, apply unchanged (removing the schema in 4.11.0 broke them). `starci app sync` judges the file except the one section the squash writes (sync mode `section`; the markers live in `rules/database-constants.mjs` with `BOOTSTRAP_FILE`, which `db/migrate.mjs` and `db/drift.mjs` re-export).
|
|
63
|
+
- Added: R213 (`DB_MIGRATION_SHAPE`) refuses transaction control in a migration, named by file:line (`rules/database-transaction.mjs`): a top-level BEGIN, START TRANSACTION, COMMIT, END, ROLLBACK, ABORT, SAVEPOINT, RELEASE or PREPARE TRANSACTION, or a COMMIT or ROLLBACK inside a DO block, since `db migrate` and the squash replay apply each file in one transaction with its history row; function and procedure bodies are not judged. Spec: `src/db-migration-transaction.spec.mjs`.
|
|
64
|
+
- Changed: the bundled canon pins name hfs 4.11.1.
|
|
65
|
+
|
|
66
|
+
## 4.11.0 - 2026-10-29
|
|
67
|
+
|
|
68
|
+
Breaking for lite apps (rulings lite-official-postgres-image and lite-queue-bullmq; the move is contexts/project/lite.md "Moving an app off Supabase").
|
|
69
|
+
|
|
70
|
+
- Changed: the lite db service runs the Docker Official `postgres` image. `starci app sync --write` writes the managed `db/Dockerfile` (slot `app.db.image`, templates/app/db-image): `FROM postgres:17.11@sha256:31ba9c94…b08f` (sync `DB_BASE_IMAGE`) plus `postgresql-17-pgvector=0.8.7-1.pgdg13+1` from PGDG (`PGVECTOR_PACKAGE`), nothing else. The managed db scripts gain `db:image` (`docker build --tag <project>-db:local db`) and `db:create` builds it first; the lite skeleton's stack runs `{{project}}/db` for its db component and service, deploys it as `deploy.images.db: {app: db}` (`starci app stack build` and the managed `images.yml` and `stack-deploy.yml` build `db/Dockerfile` for it, `db/image.mjs`); `starci app db drift` builds `db/Dockerfile` for its scratch database (`DB_DRIFT_NO_IMAGE` without one) instead of reading `components.db.image`. `DB_IMAGE` (supabase/postgres) is removed.
|
|
71
|
+
- Changed: `db/bootstrap.sql` keeps the NOLOGIN browser roles and the auth and storage stubs and no longer creates the `extensions` schema or the `supabase_realtime` publication; it applies green, twice, on the official image.
|
|
72
|
+
- Changed: R276 refuses every PostgreSQL or Mongo image in a lite stack (`isLiteDatabaseImage` is removed): the db component runs the app's own db image; `DB_EXTENSION_ALLOWED` (R259) defaults to pgcrypto, pg_trgm, vector, uuid-ossp, unaccent and btree_gist.
|
|
73
|
+
- Added: the lite tick scheduler (R335, BE-JOBS-6). `starci app add job` in lite writes `platform/jobs/job-ticks.contracts.ts` (`JOB_TICKS`, empty), `job-tick.service.ts` (`JobTickService`: one `CronJob` per tick in `SchedulerRegistry`; each firing takes `advisoryXactLock(tx, "tick:<name>")`, claims the tick's UTC minute through `job_tick_claim` and only then writes the queue outbox) and its spec, a lite `jobs.module.ts` composing `ScheduleModule.forRoot()`, the `job_ticks` table and `job_tick_claim` function in the jobs migration, and pins `@nestjs/schedule` and `cron` in the app's package.json; `JobsLogEvent.TickFailed`; `job_tick_claim` joins the canon `ruleParams.be.dbFunctions`. The bundled lite pattern knowledge (queues, jobs, idempotency, realtime) names the tick instead of pg_cron, and the failure-code catalog carries `BE_JOBS_SCHEDULER_OWNER`.
|
|
74
|
+
- Changed: the bundled slot manifest 3.11.0 adds the managed lite slot `app.db.image` (`db/Dockerfile`), narrows `ruleParams.db.extensions` to what the official postgres image plus pgvector carries (pgcrypto, pg_trgm, vector, uuid-ossp, unaccent, btree_gist; no pgmq, pg_cron, pg_net or supabase_vault) and gives `@nestjs/schedule` and `cron` one owner, `platform/jobs`, in `infraOwners`; the bundled rule catalog adds R335 `BE_JOBS_SCHEDULER_OWNER` and its R250 and R276 laws name the platform/jobs tick and the db image; the bundled canon pins name hfs 4.11.0, eslint-canon-be 3.8.0, eslint-canon-fe 9.5.2 and the Grammar family 0.15.13 and pin `@nestjs/schedule` 6.1.3 (CommonJS on the Nest 11 line) and `cron` 4.4.0.
|
|
75
|
+
- Fixed: `starci app db baseline` judges the archive (`db/migrations/archive/`) as frozen history (R257): an archived file needs only its leading 14-digit stamp and an underscore, so a retired chain's snake_case names, impossible times (`20261014300000`) and shared stamps no longer refuse the cutover. The active chain keeps R213 in full (`<ts14>_<kebab>.sql` with a valid UTC stamp); R213 and R332 set no upper bound, so an active stamp after now is no breach. A file both active and archived is refused by name. The new version sorts after every archived and active file, and the baseline header names the last valid stamp, the boundary R257 judges.
|
|
76
|
+
- Fixed: a refusal over the migration chain lists every offender as `<file>: <reason> (<rule>)`, all at once: a misnamed or invalid-time active file (`db baseline`, `db restamp`, `db plan`), and for `db baseline` the `--stamp` boundary file, the committed-history findings (R257), the queue and cron findings (R258) and each active migration a schema-only dump cannot rebuild. Spec: `src/hfs-database-baseline.spec.mjs`.
|
|
77
|
+
|
|
78
|
+
|
|
3
79
|
## 4.10.1 - 2026-10-29
|
|
4
80
|
|
|
5
81
|
- Changed: the bundled slot manifest 3.10.0 adds the lite slot `be.platform.database.entities`, the entity registry (slot fields `role: entity-registry` and `importedBy`, validated by the manifest shape); the bundled architecture machine reads it (`architecture/entity-registry.mjs`): `tier-direction` (R26 `BE_TIER_DIRECTION`) admits the registry's import of a domain or platform capability's `index.ts` that binds only that capability's `<c>Entities`, and `owner-export-bypass` (R30 `ARCH_OWNER_EXPORT_BYPASS`) admits a direct import of the registry from the files `importedBy` names (`apps/*/src/compose/*.ts`, `apps/*/src/app.module.ts`, `src/tests/world/**`); every other import of either shape stays refused, and full has no registry; the bundled canon pins name hfs 4.10.1, eslint-canon-be 3.7.1, eslint-canon-fe 9.5.1 and the Grammar family 0.15.12; the lite skeleton's `platform/database/entities.ts` names its importers (app composition and the test world).
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ starci app db drift [--url-env <NAME>] [--json] [--cwd <dir>]
|
|
|
24
24
|
starci app new service <dir> <name> [--inject <binding>...] [--cwd <dir>]
|
|
25
25
|
starci app new spec <file>.service.ts [--cwd <dir>]
|
|
26
26
|
starci app new image [--cwd <dir>]
|
|
27
|
-
starci app secret list|
|
|
27
|
+
starci app secret init|set|get|unset|rename|remove|materialize|list|verify|recipients|migrate|split|audit ... [--cwd <dir>]
|
|
28
28
|
starci app hygiene [--cwd <dir>]
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -92,6 +92,25 @@ and 2 is no verdict: a host without Docker or without the app's packages gets `D
|
|
|
92
92
|
green. An app with an entity list runs it as the `db-drift` row of `starci app release gate` and in the database step of its managed CI's
|
|
93
93
|
build job.
|
|
94
94
|
|
|
95
|
+
`starci app db baseline --squash --url-env <NAME> --stamp <ts14> [--drop-extension a,b] [--drop-pgmq-queue q1,job_*] [--seed-tables t1,t2]
|
|
96
|
+
[--image <ref>] [--apply]` retires the whole active chain into one baseline read from a database it is applied to, for a cutover whose
|
|
97
|
+
production starts on an empty database (`runtime/scripts/hfs/db-squash.mjs`). The source must record exactly the active chain in its
|
|
98
|
+
history, and it is only read. The verb dumps the source with its privileges (`pg_dump --schema-only --no-owner`, no `-x`) and keeps the
|
|
99
|
+
app's schemas: it leaves out the extensions `--drop-extension` names (default `pg_net,pg_cron`; pgmq stays, the lite event bus), the
|
|
100
|
+
pgmq queues `--drop-pgmq-queue` names (a name or a `*` pattern; no match drops nothing), every function or view whose body reaches a
|
|
101
|
+
dropped extension's schema, an empty publication, an event trigger on a platform function, the platform's schemas and roles and the
|
|
102
|
+
grants naming them, and grants to the migrating role that owns every replayed object; each is listed, and a kept object naming a left-out
|
|
103
|
+
one is refused with both names. Object references and function bodies stay byte-identical. It writes `db/migrations/<stamp>_baseline.sql`
|
|
104
|
+
(the `-- starci:baseline <last archived stamp>` header, the kept extensions created where the source has them, the schema and grants, then
|
|
105
|
+
the kept pgmq queues and the grants on objects outside the app schemas), a marked section of `db/bootstrap.sql` (the source's app roles
|
|
106
|
+
with their attributes and no password, their memberships, their allowed role settings such as `search_path`, the grants on schemas the
|
|
107
|
+
bootstrap creates and the database-wide default privileges, none of which a per-database dump keeps; `starci app sync` judges and keeps
|
|
108
|
+
the rest of the file), `db/seed.sql` (the rows of `--seed-tables`, `insert ... on conflict do nothing` in foreign-key order with their
|
|
109
|
+
sequences set; a table without a primary key or unique constraint is refused) and moves the chain to `db/migrations/archive/`. Before
|
|
110
|
+
writing, a scratch container of the app's db image (built from `.starcistacks/images/db/Dockerfile`, or `--image`) replays bootstrap, baseline and seed (the
|
|
111
|
+
seed twice); the same projection read from the replay must equal the source's, or the verb refuses with the differences. Docker runs every
|
|
112
|
+
PostgreSQL client, so the host needs none. Without `--apply` it writes nothing.
|
|
113
|
+
|
|
95
114
|
Input bounds stay with the backend DTO: `MaxLength`, `ArrayMaxSize`, `Min` and `Max` become GraphQL `@inputBounds`
|
|
96
115
|
input-field directives or JSON Schema constraints on the REST operation table's DTO properties. Codegen derives
|
|
97
116
|
`INPUT_BOUNDS` beside GraphQL wire types and `REST_INPUT_BOUNDS` under the REST API owner's `__generated__/` directory.
|
|
@@ -198,6 +217,26 @@ pass.
|
|
|
198
217
|
Exit codes: 0 clean, 1 an error finding, 2 a tool could not run or a refusal (not a Git work tree, bad flag, `--fast` with no merge-base). `starci app check` and `starci app lint` never write to the app
|
|
199
218
|
(`starci app lint --fix` lets ESLint and stylelint fix in place).
|
|
200
219
|
|
|
220
|
+
## Secrets
|
|
221
|
+
|
|
222
|
+
`starci app secret` keeps an app's sealed members (the standard is the harness's PROJECT-STACKS-5): one per provider,
|
|
223
|
+
`.starcistacks/<env>/secrets/<provider>.enc.yaml`, sops YAML whose values keep every byte, a legacy dotenv `<member>.enc`
|
|
224
|
+
read until `migrate` moves it (`runtime/scripts/lib/sealed-member.mjs` names both). `set <env> <provider> <KEY> [KEY ...]`
|
|
225
|
+
seals every named key in one sops write (`runtime/scripts/hfs/secret.mjs`): on a terminal it prompts `<KEY>: ` for each with
|
|
226
|
+
masked input (one `*` per character; a paste, backspace, Enter, Ctrl+C to cancel with nothing written), redirected stdin gives
|
|
227
|
+
one key its whole text or several one `KEY=value` line each, `--from-file KEY=path` reads a key byte for byte
|
|
228
|
+
(`secret-prompt.mjs`), and `--generate <bytes>` makes a random value per key. It then declares the keys in the environment's
|
|
229
|
+
`secrets:` of a compose environment (`declareKeys`, `secret-standard.mjs`) and names a swarm environment's to wire by hand.
|
|
230
|
+
A member is sealed to its environment's rule of the managed `.sops.yaml`; `recipients <env> [--add|--remove <age1...|owner>]`
|
|
231
|
+
rewrites that rule (`environmentRecipients`, `secret-init.mjs`), and `init` takes the owner's identity as the app's recipient.
|
|
232
|
+
Re-sealing a member whose every key the set replaces needs no age identity; adding a key to one does, and the identity is found
|
|
233
|
+
by itself (`localAgeIdentity`): SOPS_AGE_KEY or SOPS_AGE_KEY_FILE of the environment, else of the app's git-ignored
|
|
234
|
+
`.claude/secret.env`, else of its main checkout's when the app is a git worktree, else the `hostIdentity` the harness CLI passes
|
|
235
|
+
(its own secret.env), else the sops `keys.txt`; with none, the verb refuses before asking for a value, naming every place it
|
|
236
|
+
looked and the one line that fixes it. `audit [<env>]` names every deviation and exits 1; `migrate <env> <member> [--to
|
|
237
|
+
<provider>]` and `split <env> <member>` repair them, dry runs until `--execute` (`secret-standard.mjs`). No output carries a
|
|
238
|
+
value except `get --reveal`.
|
|
239
|
+
|
|
201
240
|
## Sonar
|
|
202
241
|
|
|
203
242
|
One mechanism for both sides: every finding of the canon is imported into Sonar, and the quality gate
|
package/db/drift.mjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// The migrations (db/migrations/*.sql, after db/bootstrap.sql when present) are the only DDL and `synchronize` is false, so an entity only
|
|
3
3
|
// maps tables that already exist and can drift from them. The verb builds TypeORM's metadata from the app's entity list
|
|
4
4
|
// (be/src/modules/platform/database/entities.ts, compiled in-process with the app's own typescript and loaded with the app's own typeorm) through a
|
|
5
|
-
// DataSource that never connects, applies the migrations to a scratch database (a throwaway container of the image the app's
|
|
6
|
-
// .starcistacks
|
|
5
|
+
// DataSource that never connects, applies the migrations to a scratch database (a throwaway container of the image the app's managed
|
|
6
|
+
// .starcistacks/images/db/Dockerfile builds, or the empty database named by --url-env), reads that schema from
|
|
7
7
|
// information_schema.columns and pg_catalog, and reports every entity column whose table or column is missing, whose type differs (both
|
|
8
8
|
// normalised to PostgreSQL's format_type names) or whose nullability differs, under BE_ENTITY_SCHEMA_DRIFT. Database columns no entity maps
|
|
9
9
|
// are fine. Exit 0 is green, 1 is drift (or a migration that does not apply to an empty database), 2 is a refusal: no verdict, which is
|
|
@@ -12,14 +12,17 @@ import fs from 'node:fs';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import Module, { createRequire } from 'node:module';
|
|
14
14
|
import { randomBytes } from 'node:crypto';
|
|
15
|
-
import { runVerb
|
|
15
|
+
import { runVerb } from '../release/index.mjs';
|
|
16
16
|
import { failure, refusal } from '../stack/errors.mjs';
|
|
17
17
|
import { firstLine, tool } from '../stack/context.mjs';
|
|
18
18
|
import { ENTITY_LIST, hasEntityList } from './entity-list.mjs';
|
|
19
|
+
import { DB_DOCKERFILE, DB_IMAGE_CONTEXT } from './image.mjs';
|
|
20
|
+
import { BOOTSTRAP_FILE } from '../runtime/scripts/hfs/rules/database-constants.mjs';
|
|
21
|
+
|
|
22
|
+
export { BOOTSTRAP_FILE };
|
|
19
23
|
|
|
20
24
|
export const DRIFT_CODE = 'BE_ENTITY_SCHEMA_DRIFT';
|
|
21
25
|
export const MIGRATIONS_DIR = 'db/migrations';
|
|
22
|
-
export const BOOTSTRAP_FILE = 'db/bootstrap.sql';
|
|
23
26
|
const NEEDS_DATABASE = 'DB_DRIFT_NEEDS_DATABASE';
|
|
24
27
|
const KEY = /^[A-Z][A-Z0-9_]*$/u;
|
|
25
28
|
const MIGRATION = /^\d{14}_[a-z0-9][a-z0-9_-]*\.sql$/u;
|
|
@@ -141,27 +144,27 @@ export function migrationFiles(root) {
|
|
|
141
144
|
return [...(fs.existsSync(path.join(root, BOOTSTRAP_FILE)) ? [BOOTSTRAP_FILE] : []), ...migrations];
|
|
142
145
|
}
|
|
143
146
|
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Starts a throwaway container of `image` on a loopback port; returns { connection, stop }. Docker missing or failing is no verdict. */
|
|
152
|
-
function startScratch(ctx, image) {
|
|
147
|
+
/**
|
|
148
|
+
* Builds the app's db image from its managed Dockerfile (DB_DOCKERFILE) and starts a throwaway container of it on a loopback port;
|
|
149
|
+
* returns { connection, stop }. Docker missing or failing is no verdict; an app without that Dockerfile is refused (sync writes it).
|
|
150
|
+
*/
|
|
151
|
+
function startScratch(ctx, root) {
|
|
152
|
+
if (!fs.existsSync(path.join(root, DB_DOCKERFILE))) throw refusal('DB_DRIFT_NO_IMAGE', `${DB_DOCKERFILE} is missing: run "npx starci app sync --write" (the scratch database runs the app's own db image) or pass --url-env`);
|
|
153
153
|
const docker = (args, env) => tool(ctx, 'docker', args, { env: { ...ctx.env, ...env } });
|
|
154
154
|
const probe = docker(['version', '--format', '{{.Server.Version}}']);
|
|
155
155
|
if (probe.error || probe.status !== 0) throw needsDatabase('Docker is not available on this host; start it, or pass --url-env naming an empty database');
|
|
156
|
+
const build = docker(['build', '--quiet', '--file', path.join(root, DB_DOCKERFILE), path.join(root, DB_IMAGE_CONTEXT)]);
|
|
157
|
+
const image = /sha256:[a-f0-9]{64}/u.exec(String(build.stdout ?? ''))?.[0];
|
|
158
|
+
if (build.status !== 0 || !image) throw needsDatabase(`docker could not build ${DB_DOCKERFILE}: ${firstLine(build.stderr)}`);
|
|
156
159
|
const password = randomBytes(18).toString('hex');
|
|
157
160
|
const name = `starci-db-drift-${randomBytes(6).toString('hex')}`;
|
|
158
161
|
const run = docker(['run', '-d', '--rm', '--name', name, '--label', 'starci.db-drift=1', '-e', 'POSTGRES_PASSWORD', '-p', '127.0.0.1::5432', image], { POSTGRES_PASSWORD: password });
|
|
159
162
|
const stop = () => { docker(['rm', '-f', name]); };
|
|
160
|
-
if (run.status !== 0) { stop(); throw needsDatabase(`docker could not start ${
|
|
163
|
+
if (run.status !== 0) { stop(); throw needsDatabase(`docker could not start the image of ${DB_DOCKERFILE}: ${firstLine(run.stderr)}`); }
|
|
161
164
|
const port = docker(['port', name, '5432/tcp']);
|
|
162
165
|
const match = /:(\d+)\s*$/mu.exec(String(port.stdout ?? '').split('\n')[0]);
|
|
163
166
|
if (port.status !== 0 || !match) { stop(); throw needsDatabase('docker published no port for the scratch database'); }
|
|
164
|
-
return { connection: { host: '127.0.0.1', port: Number(match[1]), user: 'postgres', password, database: 'postgres' }, stop, how: `scratch container of ${
|
|
167
|
+
return { connection: { host: '127.0.0.1', port: Number(match[1]), user: 'postgres', password, database: 'postgres' }, stop, how: `scratch container of the image ${DB_DOCKERFILE} builds` };
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
/** Connects with the app's pg, retrying while a fresh container is still initialising; gives up after READY_TIMEOUT_MS. */
|
|
@@ -200,7 +203,7 @@ export async function migratedSchema(ctx, appRoot, { urlEnv } = {}) {
|
|
|
200
203
|
if (typeof value !== 'string' || !/^postgres(?:ql)?:\/\//u.test(value)) throw needsDatabase(`${urlEnv} holds no postgres connection URL`);
|
|
201
204
|
secrets = [value];
|
|
202
205
|
scratch = { connection: { connectionString: value }, stop: () => {}, how: `the database ${urlEnv} names` };
|
|
203
|
-
} else scratch = startScratch(ctx,
|
|
206
|
+
} else scratch = startScratch(ctx, root);
|
|
204
207
|
const scrub = (text) => secrets.reduce((out, secret) => out.replaceAll(secret, '[value]'), String(text));
|
|
205
208
|
let client;
|
|
206
209
|
try {
|
package/db/image.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// image.mjs - the one name of a lite app's db image (slot app.db.image, rulings lite-official-postgres-image and lite-db-image-in-stacks):
|
|
2
|
+
// the managed .starcistacks/images/db/Dockerfile builds it, `deploy.images.db` of .starcistacks/application-stacks.yaml names it as app
|
|
3
|
+
// `db`, and the stack build, the images and stack-deploy workflows, the local db container, the drift scratch database and the squash
|
|
4
|
+
// replay all build that one file. db/ keeps only schema and data (migrations, bootstrap, seed).
|
|
5
|
+
|
|
6
|
+
/** The image app name a deploy block gives the db service. */
|
|
7
|
+
export const DB_IMAGE_APP = 'db';
|
|
8
|
+
/** The build context of the db image, app-relative: the folder of its Dockerfile, which copies nothing from it. */
|
|
9
|
+
export const DB_IMAGE_CONTEXT = '.starcistacks/images/db';
|
|
10
|
+
/** The managed Dockerfile of the db image, app-relative. */
|
|
11
|
+
export const DB_DOCKERFILE = `${DB_IMAGE_CONTEXT}/Dockerfile`;
|
|
12
|
+
/** Where the db image's Dockerfile lived before it moved into the stacks; `starci app sync --write` removes it. */
|
|
13
|
+
export const RETIRED_DB_DOCKERFILE = 'db/Dockerfile';
|
package/db/migrate.mjs
CHANGED
|
@@ -12,14 +12,14 @@ import fs from 'node:fs';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { isPlainObject as plain } from '../runtime/engine/plain-object.mjs';
|
|
14
14
|
import { byCodeUnit } from '../runtime/engine/by-code-unit.mjs';
|
|
15
|
-
import { MIGRATIONS_DIR } from '../runtime/scripts/hfs/rules/database-constants.mjs';
|
|
15
|
+
import { BOOTSTRAP_FILE, MIGRATIONS_DIR } from '../runtime/scripts/hfs/rules/database-constants.mjs';
|
|
16
|
+
|
|
17
|
+
export { BOOTSTRAP_FILE };
|
|
16
18
|
import { approvalMissing, failure, refusal } from '../stack/errors.mjs';
|
|
17
19
|
import { readDeclaration, runVerb } from '../release/index.mjs';
|
|
18
20
|
import { DEFAULT_DB_URL_ENV, HISTORY_DDL, dbTarget, historyVersions, markApplied, markReverted, psqlTransaction } from './postgres.mjs';
|
|
19
21
|
|
|
20
22
|
const DATABASE_CODE = 'DB_CONFIG_INVALID';
|
|
21
|
-
/** The managed pre-migration bootstrap (slot app.db.bootstrap), applied before the first migration of an empty database. */
|
|
22
|
-
export const BOOTSTRAP_FILE = 'db/bootstrap.sql';
|
|
23
23
|
|
|
24
24
|
const SEGMENT = /^[A-Za-z0-9._-]+$/u;
|
|
25
25
|
const NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/u;
|
package/db/postgres.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { firstLine, tool } from '../stack/context.mjs';
|
|
|
6
6
|
|
|
7
7
|
/** The default environment variable that names the app's database. */
|
|
8
8
|
export const DEFAULT_DB_URL_ENV = 'DATABASE_URL';
|
|
9
|
-
/** The history table `starci app db migrate` records applied migrations in (
|
|
9
|
+
/** The history table `starci app db migrate` records applied migrations in (Supabase's layout, so a history restored from a hosted Supabase carries over; migrate creates it on an empty database). */
|
|
10
10
|
export const HISTORY_TABLE = 'supabase_migrations.schema_migrations';
|
|
11
11
|
|
|
12
12
|
const KEY = /^[A-Z][A-Z0-9_]*$/u;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/hfs",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.11.8",
|
|
4
4
|
"description": "The StarCi app command implementation for scaffolding, linting, checking and synchronizing one product repository.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"rxjs": "7.8.2",
|
|
44
44
|
"typeorm": "0.3.31",
|
|
45
45
|
"typescript": "5.9.3",
|
|
46
|
+
"undici": "6.29.0",
|
|
46
47
|
"zod": "4.6.7"
|
|
47
48
|
},
|
|
48
49
|
"files": [
|
|
@@ -31,35 +31,35 @@ pins:
|
|
|
31
31
|
source: packages/event-stream/package.json
|
|
32
32
|
why: 'the one server-sent-events client of every front end (fe-stack event-stream, owner:event-stream-package): the transport client layer (modules/api/sse/clients) creates one instance for the app with the bearer token source and the stream operations follow on it; it carries Last-Event-ID resume, the capped doubling backoff, the heartbeat timeout, the single 401 renewal and the useSWRSubscription adapter, so no app keeps a hand-written reader.'
|
|
33
33
|
'@starci/grammar':
|
|
34
|
-
version: 0.15.
|
|
34
|
+
version: 0.15.20
|
|
35
35
|
group: starci
|
|
36
36
|
install: registry
|
|
37
37
|
side: fe
|
|
38
38
|
source: packages/grammar/package.json
|
|
39
39
|
why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
|
|
40
40
|
'@starci/eslint-canon-be':
|
|
41
|
-
version: 3.7
|
|
41
|
+
version: 3.8.7
|
|
42
42
|
group: starci
|
|
43
43
|
install: registry
|
|
44
44
|
side: be
|
|
45
45
|
source: packages/eslint-canon-be/package.json
|
|
46
46
|
why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
|
|
47
47
|
'@starci/eslint-canon-fe':
|
|
48
|
-
version: 9.5.
|
|
48
|
+
version: 9.5.9
|
|
49
49
|
group: starci
|
|
50
50
|
install: registry
|
|
51
51
|
side: fe
|
|
52
52
|
source: packages/eslint-canon-fe/package.json
|
|
53
53
|
why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
|
|
54
54
|
'@starci/eslint-grammar-core':
|
|
55
|
-
version: 0.15.
|
|
55
|
+
version: 0.15.20
|
|
56
56
|
group: starci
|
|
57
57
|
install: registry
|
|
58
58
|
side: fe
|
|
59
59
|
source: packages/eslint-grammar-core/package.json
|
|
60
60
|
why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
|
|
61
61
|
'@starci/eslint-grammar-offset-pop':
|
|
62
|
-
version: 0.15.
|
|
62
|
+
version: 0.15.20
|
|
63
63
|
group: starci
|
|
64
64
|
install: registry
|
|
65
65
|
side: fe
|
|
@@ -92,12 +92,12 @@ pins:
|
|
|
92
92
|
source: packages/jest-preset/package.json
|
|
93
93
|
why: 'the Jest preset of a back end: the world runner runs up to min(--maxWorkers, slots) world files at once, each in its own process bound to one data slot of the test world (STARCI_TEST_WORLD_SLOT), and reads test-world state protocol 2 only: it pairs exactly with @starci/test-world, and any other state file is JEST_PRESET_WORLD_PAIR_MISMATCH. The unit run writes the lcov Sonar and Codecov import (services only); the integration, e2e and contract projects run every spec file in a worker process of its own (world-runner.cjs), so a process-global framework registry (@nestjs/graphql type metadata) never leaks between e2e files; it ships the unit kit (mockEntityManager, fakeTransaction, fakeQueryBuilder, queueQueryBuilders, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
|
|
94
94
|
'@starci/test-world':
|
|
95
|
-
version: 1.
|
|
95
|
+
version: 1.3.1
|
|
96
96
|
group: starci
|
|
97
97
|
install: registry
|
|
98
98
|
side: be
|
|
99
99
|
source: packages/test-world/package.json
|
|
100
|
-
why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
|
|
100
|
+
why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. A fake declared https (defineHttpFake) serves TLS with the run''s ephemeral in-memory fake CA and publishes its PEM as w.fake.<name>.ca for the app''s own client to trust; verification is never turned off. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
|
|
101
101
|
'@starci/ai-router':
|
|
102
102
|
version: 0.3.0
|
|
103
103
|
group: starci
|
|
@@ -106,7 +106,7 @@ pins:
|
|
|
106
106
|
source: packages/ai-router/package.json
|
|
107
107
|
why: 'the AI pool router of a back end: a NestJS module that routes OpenAI-compatible chat, stream and embeddings calls across keys, models and providers, with @starci/ai-router/testing for its specs; @starci/jest-preset compiles it as an ESM-only dependency.'
|
|
108
108
|
'@starci/hfs':
|
|
109
|
-
version: 4.
|
|
109
|
+
version: 4.11.8
|
|
110
110
|
group: starci
|
|
111
111
|
install: registry
|
|
112
112
|
side: both
|
|
@@ -594,12 +594,27 @@ pins:
|
|
|
594
594
|
group: framework
|
|
595
595
|
side: be
|
|
596
596
|
why: 'the Express 5 integration @nestjs/apollo loads at boot (R111 nestjs-apollo-express5, knowledge/hfs/peer-integrations.yaml); every api app that serves GraphQL installs it; the latest registry version on 2026-10-13.'
|
|
597
|
+
undici:
|
|
598
|
+
version: 6.29.0
|
|
599
|
+
group: framework
|
|
600
|
+
side: be
|
|
601
|
+
why: 'the Agent of platform/http (its one owner, R90): FetchHttpClient hands fetch a pool that trusts HttpOptions.trustedCa beside the bundled roots (a test world''s per-run fake CA); the 6 line is the undici the Node 22 runtime of the app images bundles, and its Agent also serves the fetch of Node 24; 6.29.0 is the release of 2026-09-25, chosen over newer ones that had not aged two weeks.'
|
|
597
602
|
# --- lite identity (integrations/passport, ruling lite-self-hosted-postgres-passport-minio)
|
|
598
603
|
'@nestjs/passport':
|
|
599
604
|
version: 11.0.5
|
|
600
605
|
group: framework
|
|
601
606
|
side: be
|
|
602
607
|
why: 'PassportStrategy of integrations/passport; 11.0.5 is CommonJS on the Nest 11 line (12.0.0 is ESM-only and breaks Nest 11 jest under the CommonJS transform of @starci/jest-preset), as nivo-lite runs it.'
|
|
608
|
+
'@nestjs/schedule':
|
|
609
|
+
version: 6.1.3
|
|
610
|
+
group: framework
|
|
611
|
+
side: be
|
|
612
|
+
why: 'the tick scheduler of platform/jobs, the one owner that imports it (R335, ruling lite-official-postgres-image); 6.1.3 is CommonJS on the Nest 11 line (12.x is ESM-only and breaks Nest 11 jest under the CommonJS transform of @starci/jest-preset).'
|
|
613
|
+
cron:
|
|
614
|
+
version: 4.4.0
|
|
615
|
+
group: framework
|
|
616
|
+
side: be
|
|
617
|
+
why: 'CronJob of the platform/jobs tick scheduler; the exact version @nestjs/schedule 6.1.3 depends on, so one cron is installed.'
|
|
603
618
|
passport:
|
|
604
619
|
version: 0.7.0
|
|
605
620
|
group: framework
|
|
@@ -1407,7 +1407,7 @@ rules:
|
|
|
1407
1407
|
witness:
|
|
1408
1408
|
- {"path":"packages/eslint/be/legacy-sql-http-log-proof.spec.mjs","edition":"lite","codes":["BE_SQL_INTERPOLATED"]}
|
|
1409
1409
|
- {"path":"packages/eslint/be/legacy-sql-http-log-proof.spec.mjs","edition":"full","codes":["BE_SQL_INTERPOLATED"]}
|
|
1410
|
-
law: "SQL text carries no runtime substitution; values are numbered parameters."
|
|
1410
|
+
law: "SQL text carries no runtime substitution; values are numbered parameters. The one exception is R334's lite call statement in platform/database/database.client.ts, which splices only the name parts and parameter list its own validator checked."
|
|
1411
1411
|
kinds: [lint, codemod]
|
|
1412
1412
|
gates: [pre-commit, settle, land, ci]
|
|
1413
1413
|
failureCodes: ["BE_SQL_INTERPOLATED"]
|
|
@@ -3695,7 +3695,7 @@ rules:
|
|
|
3695
3695
|
editions: [lite, full]
|
|
3696
3696
|
docs: [docs/architecture.md#r213]
|
|
3697
3697
|
templateNa: "Full apps keep TypeORM migrations under be/; the managed db/migrations scaffold is lite's."
|
|
3698
|
-
law: "A migration is `db/migrations/<ts14>_<kebab>.sql`; the stamp is a valid UTC instant, active stamps are unique, and a new migration sorts strictly after the committed active migrations in HEAD. Repository history includes the archive: moving an active migration to the archive retires it from ordering, so a replacement baseline may predate archived stamps and later migrations append after the baseline. Wall-clock time, commit time and remote-branch stamps do not set the ordering boundary. A migration already on the base is immutable (a diff or an unarchived removal is a finding); a file that does not parse as PostgreSQL is a shape failure."
|
|
3698
|
+
law: "A migration is `db/migrations/<ts14>_<kebab>.sql`; the stamp is a valid UTC instant, active stamps are unique, and a new migration sorts strictly after the committed active migrations in HEAD. Repository history includes the archive: moving an active migration to the archive retires it from ordering, so a replacement baseline may predate archived stamps and later migrations append after the baseline. Wall-clock time, commit time and remote-branch stamps do not set the ordering boundary. A migration already on the base is immutable (a diff or an unarchived removal is a finding); a file that does not parse as PostgreSQL is a shape failure. `starci app db migrate` applies each migration in one transaction with its history row, so a top-level BEGIN, START TRANSACTION, COMMIT, END, ROLLBACK, ABORT, SAVEPOINT, RELEASE or PREPARE TRANSACTION, or a COMMIT or ROLLBACK inside a DO block, is a shape failure named by file:line; a function or procedure body is never judged."
|
|
3699
3699
|
kinds: [check]
|
|
3700
3700
|
gates: [settle, land, ci]
|
|
3701
3701
|
failureCodes: ["DB_MIGRATION_SHAPE"]
|
|
@@ -4176,7 +4176,7 @@ rules:
|
|
|
4176
4176
|
editions: [lite, full]
|
|
4177
4177
|
docs:
|
|
4178
4178
|
- {path: docs/application-stacks.md#r250-deploy-scan, editions: [lite, full]}
|
|
4179
|
-
law: "In lite and full apps, scheduled work belongs to the back-end queue scheduler, a
|
|
4179
|
+
law: "In lite and full apps, scheduled work belongs to the back-end queue scheduler, a platform/jobs tick (R335) that enqueues through the queue outbox or a declared replicated-job. Deployment files, scripts, workflows, compose files and Dockerfiles install no host crontab, cron.d entry, systemd timer, @reboot job or at job; a workflow does not schedule app work. Docker compose up, down and pull run only against an explicitly dev environment. Deploy shell scripts delegate to docker, starci or the app CLI, and contain no node -e, psql, inline SQL or downloaded shell logic."
|
|
4180
4180
|
kinds: [check]
|
|
4181
4181
|
gates: [settle, land, ci]
|
|
4182
4182
|
failureCodes: ["HFS_HOST_CRON", "HFS_DEPLOY_NO_LOGIC", "HFS_DEPLOY_COMPOSE_ON_VPS", "HFS_DEPLOY_SCHEDULER"]
|
|
@@ -4310,22 +4310,21 @@ rules:
|
|
|
4310
4310
|
- id: R258
|
|
4311
4311
|
code: "DB_PGMQ_DECLARED"
|
|
4312
4312
|
editions: [lite]
|
|
4313
|
-
editionWhy: "
|
|
4313
|
+
editionWhy: "ruling:lite-pgmq-bus-bullmq-jobs makes pgmq in the db service the lite event bus while jobs run on BullMQ over the declared Redis; full's bus is Kafka."
|
|
4314
4314
|
docs: [docs/microservices.md#r258]
|
|
4315
|
-
templateNa: "
|
|
4316
|
-
law: "
|
|
4315
|
+
templateNa: "Topic queues and their bindings depend on product-owned consumer groups and are authored in its db migrations."
|
|
4316
|
+
law: "DB_PGMQ_DECLARED: pgmq is the event bus and jobs run on BullMQ (ruling:lite-pgmq-bus-bullmq-jobs), so the active migrations leave no pgmq queue named by a queue constant under modules/queues; a pgmq queue a migration created for one is dropped by a later migration. Every pgmq queue the active migrations leave is an event-bus topic queue named by a literal *_TOPIC_QUEUE constant under platform/event-bus, every such constant has its pgmq queue, a topic binding (pgmq.bind_topic) names a literal pattern and a queue an earlier pgmq.create declared, no queue or binding is created by dynamic SQL or inside a function, and pgmq objects are granted only to the application role while they exist."
|
|
4317
4317
|
kinds: [check]
|
|
4318
4318
|
gates: [settle, land, ci]
|
|
4319
|
-
failureCodes: ["DB_PGMQ_DECLARED"
|
|
4319
|
+
failureCodes: ["DB_PGMQ_DECLARED"]
|
|
4320
4320
|
enforcers:
|
|
4321
4321
|
- {kind: hfs, id: db-queue-cron, at: scripts/hfs/rules/database-queues.mjs}
|
|
4322
4322
|
proves:
|
|
4323
|
-
- {path: packages/hfs/src/hfs-database-queues.spec.mjs, edition: lite, codes: [DB_PGMQ_DECLARED
|
|
4324
|
-
- {path: packages/hfs/src/hfs-queue-outbox-declaration.spec.mjs, edition: lite, codes: [DB_CRON_OWNED]}
|
|
4323
|
+
- {path: packages/hfs/src/hfs-database-queues.spec.mjs, edition: lite, codes: [DB_PGMQ_DECLARED]}
|
|
4325
4324
|
exemplifiedBy:
|
|
4326
|
-
- {path: packages/hfs/src/hfs-database-queues.spec.mjs, edition: lite, codes: [DB_PGMQ_DECLARED
|
|
4325
|
+
- {path: packages/hfs/src/hfs-database-queues.spec.mjs, edition: lite, codes: [DB_PGMQ_DECLARED]}
|
|
4327
4326
|
witness:
|
|
4328
|
-
- {path: packages/hfs/src/hfs-database-queues.spec.mjs, edition: lite, codes: [DB_PGMQ_DECLARED
|
|
4327
|
+
- {path: packages/hfs/src/hfs-database-queues.spec.mjs, edition: lite, codes: [DB_PGMQ_DECLARED]}
|
|
4329
4328
|
- id: R259
|
|
4330
4329
|
code: "DB_EXTENSION_ALLOWED"
|
|
4331
4330
|
editions: [lite]
|
|
@@ -4701,7 +4700,7 @@ rules:
|
|
|
4701
4700
|
- {path: packages/hfs/src/hfs-stacks-full-fixtures.spec.mjs, edition: full}
|
|
4702
4701
|
- id: R276
|
|
4703
4702
|
code: "STACKS_LITE_SELFHOSTED_DB"
|
|
4704
|
-
law: "Lite runs exactly one database, its db component of image
|
|
4703
|
+
law: "Lite runs exactly one database, its db component of the image its managed .starcistacks/images/db/Dockerfile builds (the digest-pinned official postgres image plus pgvector, ruling:lite-official-postgres-image) for vector search too, and no other PostgreSQL, MongoDB or Qdrant backing image, even when declared (owner:lite-vector-is-pgvector, ruling:lite-self-hosted-postgres-passport-minio). A Redis image runs only when hfs.json sides.be.backing declares redis (ruling:R212/case-10), as one minimal instance; a declared Redis runs as exactly one stack service, with AOF persistence (`appendonly yes`, `appendfsync everysec` or `always`, never `no`) on a writable named volume so queue jobs survive a restart and with no maxmemory-policy but noeviction so none is evicted. A queue job Redis loses inside the at most one second `everysec` leaves unwritten is not lost work: every job is born from a Postgres outbox row, and the relay marks that row sent only after Redis confirms the job is in its append-only file (WAITAOF) and still holds it, so a job Redis did not persist leaves its row waiting and the relay adds it again (examples/lite-app queue-relay.service.spec.ts and bullmq-queue-transport.client.spec.ts). A sidecar keeps its own SQLite on a named persistent volume declared by its component and mounted in the rendered model, and never uses the app's declared Redis, which serves the back-end app services only; a sidecar requiring another database, that Redis included, is refused. Full retains its declared backing topology."
|
|
4705
4704
|
kinds: [check]
|
|
4706
4705
|
gates: ["settle","land","ci"]
|
|
4707
4706
|
failureCodes: ["STACKS_LITE_SELFHOSTED_DB", "STACKS_SIDECAR_VOLUME_UNDECLARED", "STACKS_SIDECAR_DB_UNSUPPORTED", "STACKS_LITE_REDIS_PERSISTENCE"]
|
|
@@ -5829,7 +5828,7 @@ rules:
|
|
|
5829
5828
|
ruling: "Lite entities only map tables the migrations create, so each entity column is checked against the migrated schema by starci app db drift, which CI runs in the build job and the release gate runs as its db-drift row."
|
|
5830
5829
|
- id: R334
|
|
5831
5830
|
code: "BE_PERSISTENCE_NO_RAW_SQL"
|
|
5832
|
-
law: "Lite domain and feature code reads and writes through TypeORM entities on the injected EntityManager (find, findOne, findBy, count, exists, insert, save, update, delete, upsert, increment, and createQueryBuilder(Entity, alias) with named parameters for joins, aggregates, FOR UPDATE / SKIP LOCKED and jsonb or vector operators; never a table-name builder, a builder with no entity or a Repository); no .query( call and no sql template outside platform/database; a database function TypeORM cannot express is called through the callDbFunction, advisoryXactLock and notify helpers of platform/database, and callDbFunction names only a function ruleParams.be.dbFunctions or hfs.json sides.be.dbFunctions lists with a reason. Entities only map tables db/migrations created (synchronize stays false); entity-versus-schema drift is BE_ENTITY_SCHEMA_DRIFT of starci app db drift."
|
|
5831
|
+
law: "Lite domain and feature code reads and writes through TypeORM entities on the injected EntityManager (find, findOne, findBy, count, exists, insert, save, update, delete, upsert, increment, and createQueryBuilder(Entity, alias) with named parameters for joins, aggregates, FOR UPDATE / SKIP LOCKED and jsonb or vector operators; never a table-name builder, a builder with no entity or a Repository); no .query( call and no sql template outside platform/database; a database function TypeORM cannot express is called through the callDbFunction, advisoryXactLock and notify helpers of platform/database/database.client.ts, and callDbFunction names only a function ruleParams.be.dbFunctions or hfs.json sides.be.dbFunctions lists with a reason: a function of a migration schema-qualified (public.job_claim), a pg built-in bare (pg_notify, called in pg_catalog). That file alone composes the call statement, from the name parts its validator checked and double-quotes and a numbered parameter list. Entities only map tables db/migrations created (synchronize stays false); entity-versus-schema drift is BE_ENTITY_SCHEMA_DRIFT of starci app db drift."
|
|
5833
5832
|
kinds: [lint]
|
|
5834
5833
|
gates: [pre-commit, settle, land, ci]
|
|
5835
5834
|
failureCodes: ["BE_PERSISTENCE_NO_RAW_SQL"]
|
|
@@ -5849,3 +5848,45 @@ rules:
|
|
|
5849
5848
|
source: owner
|
|
5850
5849
|
quote: "big refactor đi … không viết raw sql như thế theo starci-academy-be"
|
|
5851
5850
|
ruling: "Lite persistence moves to TypeORM entities on the EntityManager, the shape of full and starci-academy-be (ruling lite-persistence-typeorm-entities)."
|
|
5851
|
+
- id: R335
|
|
5852
|
+
code: "BE_JOBS_SCHEDULER_OWNER"
|
|
5853
|
+
law: "Recurring work is a tick of platform/jobs (ruling lite-official-postgres-image): JOB_TICKS of job-ticks.contracts.ts lists the ticks as data, and only job-tick.service.ts (JobTickService, which registers one CronJob per tick in SchedulerRegistry) and jobs.module.ts (which composes ScheduleModule.forRoot()) import @nestjs/schedule or cron; no file uses the Cron, Interval or Timeout decorator. JobTickService takes advisoryXactLock(tx, \"tick:<name>\") and claims the tick's UTC minute through callDbFunction(tx, \"job_tick_claim\", ...) in the transaction that writes the queue outbox row, so of the replicas that fire a tick exactly one enqueues it once per minute."
|
|
5854
|
+
kinds: [lint]
|
|
5855
|
+
gates: [pre-commit, settle, land, ci]
|
|
5856
|
+
failureCodes: ["BE_JOBS_SCHEDULER_OWNER"]
|
|
5857
|
+
enforcers:
|
|
5858
|
+
- {kind: eslint-be, id: jobs-scheduler-owner}
|
|
5859
|
+
editions: [lite, full]
|
|
5860
|
+
docs: [contexts/code/be/jobs.md]
|
|
5861
|
+
proves:
|
|
5862
|
+
- packages/eslint-canon-be/src/jobs-scheduler.spec.ts
|
|
5863
|
+
exemplifiedBy:
|
|
5864
|
+
- {path: packages/hfs/templates/be/patterns/jobs/platform-lite/job-tick.service.ts.tpl, edition: lite}
|
|
5865
|
+
- {path: packages/eslint-canon-be/src/jobs-scheduler.spec.ts, editions: [lite, full], codes: [BE_JOBS_SCHEDULER_OWNER]}
|
|
5866
|
+
witness:
|
|
5867
|
+
- {path: packages/eslint-canon-be/src/jobs-scheduler.spec.ts, edition: lite, codes: [BE_JOBS_SCHEDULER_OWNER]}
|
|
5868
|
+
- {path: packages/eslint-canon-be/src/jobs-scheduler.spec.ts, edition: full, codes: [BE_JOBS_SCHEDULER_OWNER]}
|
|
5869
|
+
cases:
|
|
5870
|
+
- id: case-1
|
|
5871
|
+
date: "2026-10-29"
|
|
5872
|
+
source: owner
|
|
5873
|
+
quote: "chạy postgresql thuần image chính chủ nhé ko phải supebase"
|
|
5874
|
+
ruling: "The lite db image carries no pg_cron, so recurring work moves to a Nest schedule tick of platform/jobs under an advisory lock and a minute claim (ruling lite-official-postgres-image)."
|
|
5875
|
+
- id: R336
|
|
5876
|
+
code: "BE_EVENT_BUS_LITE_SHAPE"
|
|
5877
|
+
law: "On the lite event bus (pgmq, delivery at least once and unordered), every event payload type declares dedupeKey, the business key a consumer inbox claims as (consumer, dedupeKey), and a reactor subscribes with a *_TOPIC_QUEUE constant imported from platform/event-bus and literal options, so every subscription is static data. The payload type is a literal, EventPayload, or an interface or type alias of the event file that declares dedupeKey or extends EventPayload (ruling lite-pgmq-bus-bullmq-jobs)."
|
|
5878
|
+
kinds: [lint]
|
|
5879
|
+
gates: [pre-commit, settle, land, ci]
|
|
5880
|
+
failureCodes: ["BE_EVENT_BUS_LITE_SHAPE"]
|
|
5881
|
+
enforcers:
|
|
5882
|
+
- {kind: eslint-be, id: event-dedupe-key}
|
|
5883
|
+
- {kind: eslint-be, id: event-subscription-static}
|
|
5884
|
+
editions: [lite]
|
|
5885
|
+
docs: [contexts/code/be/event-bus.md]
|
|
5886
|
+
proves:
|
|
5887
|
+
- packages/eslint-canon-be/src/event-bus-lite.spec.ts
|
|
5888
|
+
exemplifiedBy:
|
|
5889
|
+
- {path: packages/hfs/templates/be/inbox/delivery-recorded.event.ts.tpl, edition: lite}
|
|
5890
|
+
- {path: packages/eslint-canon-be/src/event-bus-lite.spec.ts, edition: lite, codes: [BE_EVENT_BUS_LITE_SHAPE]}
|
|
5891
|
+
witness:
|
|
5892
|
+
- {path: packages/eslint-canon-be/src/event-bus-lite.spec.ts, edition: lite, codes: [BE_EVENT_BUS_LITE_SHAPE]}
|