@starci/eslint-grammar-offset-pop 0.15.10 → 0.15.12

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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.12 - 2026-10-29
4
+
5
+ - Changed: the peer moves to the 0.15.12 cohort; 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 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. No rule changed.
6
+
7
+ ## 0.15.11 - 2026-10-29
8
+
9
+ - Changed: the peer moves to the 0.15.11 cohort; the bundled slot manifest 3.9.0 replaces the lite `app.supabase*` slots with `app.db` (`db/migrations`, `db/migrations/archive`, `db/seed.sql`, the managed `db/bootstrap.sql`), `be.integrations.supabase` with `be.integrations.passport` and `be.integrations.object-storage`, removes `fe.route.callback`, sets lite `schemaAuthority: sql`, gives lite TypeORM entities in `be.persistence`, adds `ruleParams.be.dbFunctions`, and moves the S3 SDKs to `integrations/object-storage` and the identity libraries to `integrations/passport` in `infraOwners` (`@supabase/*`, `minio` and `bcrypt` nowhere); the bundled rule catalog retires R216, R217, R219, R254 and R287 and adds R328-R334; the bundled canon pins name hfs 4.10.0, eslint-canon-be 3.7.0, eslint-canon-fe 9.5.0 and the Grammar family 0.15.11, pin `@nestjs/passport` 11.0.5 (CommonJS on the Nest 11 line), `passport` 0.7.0, `passport-google-oauth20` 2.0.0, `passport-local` 1.0.0, `bcryptjs` 3.0.3 and their types (tokens are signed and verified with `jose`; `@nestjs/jwt` stays allowed in `integrations/passport` but unpinned), and drop `@supabase/ssr`, `@supabase/supabase-js` and the `supabase` CLI. No rule changed.
10
+
11
+
3
12
  ## 0.15.10 - 2026-10-29
4
13
 
5
14
  - Changed: `@starci/eslint-grammar-core` 0.15.10 is the peer dependency; the bundled slot manifest gives the model libraries their owners in `ruleParams.be.infraOwners` (`@starci/ai-router` to `integrations/ai-gateway`, `@langchain/*` to `integrations/ai`; `openai`, `langchain`, `@langchain/langgraph`, `@langchain/community` and the provider SDKs nowhere) and lists R326 and R327 on `be.integrations`; the bundled canon pins name hfs 4.9.11, eslint-canon-be 3.6.8, eslint-canon-fe 9.4.9, the Grammar family 0.15.10 (grammar, eslint-grammar-core, eslint-grammar-offset-pop), and pin `@langchain/core` 1.3.1 and `@langchain/openai` 1.8.0. No rule changed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-grammar-offset-pop",
3
- "version": "0.15.10",
3
+ "version": "0.15.12",
4
4
  "description": "Offset Pop Grammar lint: the inherited Common component contract over its own visual family.",
5
5
  "keywords": [
6
6
  "eslint",
@@ -44,7 +44,7 @@
44
44
  "peerDependencies": {
45
45
  "eslint": ">=9",
46
46
  "typescript": ">=5.9",
47
- "@starci/eslint-grammar-core": "0.15.10"
47
+ "@starci/eslint-grammar-core": "0.15.12"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@starci/eslint-canon-fe": "file:../eslint-canon-fe",
@@ -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.10
34
+ version: 0.15.12
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.6.8
41
+ version: 3.7.1
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.4.9
48
+ version: 9.5.1
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.10
55
+ version: 0.15.12
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.10
62
+ version: 0.15.12
63
63
  group: starci
64
64
  install: registry
65
65
  side: fe
@@ -85,12 +85,12 @@ pins:
85
85
  side: both
86
86
  source: packages/prettier-config/package.json
87
87
  '@starci/jest-preset':
88
- version: 2.4.1
88
+ version: 2.5.0
89
89
  group: starci
90
90
  install: registry
91
91
  side: be
92
92
  source: packages/jest-preset/package.json
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, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
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
95
  version: 1.2.3
96
96
  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.9.11
109
+ version: 4.10.1
110
110
  group: starci
111
111
  install: registry
112
112
  side: both
@@ -543,7 +543,7 @@ pins:
543
543
  version: 3.1150.0
544
544
  group: framework
545
545
  side: be
546
- why: 'the S3 transport of integrations/object-storage, the one ObjectStorage adapter of both editions (MinIO, S3, Spaces, Supabase Storage); 3.1150.0 is the release of 2026-10-12, chosen over newer ones that had not aged two weeks.'
546
+ why: 'the S3 transport of integrations/object-storage, the one ObjectStorage adapter of both editions (MinIO, S3, Spaces); 3.1150.0 is the release of 2026-10-12, chosen over newer ones that had not aged two weeks.'
547
547
  '@aws-sdk/s3-request-presigner':
548
548
  version: 3.1150.0
549
549
  group: framework
@@ -594,27 +594,47 @@ 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
- # --- Supabase lite
598
- '@supabase/ssr':
599
- version: 0.12.7
597
+ # --- lite identity (integrations/passport, ruling lite-self-hosted-postgres-passport-minio)
598
+ '@nestjs/passport':
599
+ version: 11.0.5
600
600
  group: framework
601
- side: fe
602
- why: the one server/browser client factory for a lite Next app; exact version registry-checked for the lite scaffold.
603
- '@supabase/supabase-js':
604
- version: 2.117.2
601
+ side: be
602
+ 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.'
603
+ passport:
604
+ version: 0.7.0
605
605
  group: framework
606
- side: both
607
- why: the generated Database-typed client used by the FE data owner and the BE Auth/Storage integration.
606
+ side: be
607
+ why: the strategy runtime under @nestjs/passport (its peer range ^0.5 || ^0.6 || ^0.7).
608
+ passport-google-oauth20:
609
+ version: 2.0.0
610
+ group: framework
611
+ side: be
612
+ why: Google OAuth 2.0 (authorization code with state and PKCE) of integrations/passport.
613
+ passport-local:
614
+ version: 1.0.0
615
+ group: framework
616
+ side: be
617
+ why: the email and password strategy of integrations/passport.
618
+ bcryptjs:
619
+ version: 3.0.3
620
+ group: framework
621
+ side: be
622
+ why: 'bcrypt in pure JavaScript (no native build on the alpine image); verifies the $2a$/$2b$ hashes auth.users carries over.'
623
+ '@types/passport-google-oauth20':
624
+ version: 2.0.17
625
+ group: framework
626
+ side: be
627
+ why: types of passport-google-oauth20.
628
+ '@types/passport-local':
629
+ version: 1.0.38
630
+ group: framework
631
+ side: be
632
+ why: types of passport-local.
608
633
  jose:
609
634
  version: 6.2.12
610
635
  group: framework
611
636
  side: be
612
- why: verifies Supabase access tokens against the project JWKS with issuer, audience, expiry and algorithm checks.
613
- supabase:
614
- version: 2.119.0
615
- group: tooling
616
- side: both
617
- why: the root CLI wrapper owns local schema lifecycle and generates the committed database types.
637
+ why: signs the app's own ES256 access tokens and verifies them against its own key set (createLocalJWKSet) in integrations/passport.
618
638
  class-transformer:
619
639
  version: 0.5.1
620
640
  group: framework
@@ -7,7 +7,7 @@
7
7
  # catalog READ this file through scripts/hfs/slots.mjs; none of them hardcodes a path.
8
8
  # Shape: modules/schemas/hfs-slots.schema.yaml. The repository side is hfs.json: modules/schemas/hfs-repo.schema.yaml.
9
9
  schema: starci/hfs-slots@3
10
- version: 3.7.0
10
+ version: 3.10.0
11
11
 
12
12
  versioning:
13
13
  # MAJOR.MINOR.PATCH of this manifest. A repository pins only the MAJOR in hfs.json ("hfs": 3).
@@ -38,6 +38,9 @@ versioning:
38
38
  # or environment reads (BE_APP_OPTIONS_COMPOSITION). Secrets use EnvSource.secret(): <NAME>_FILE first, plain <NAME> only
39
39
  # when the file key is absent; .starcistacks owns custody. Capability <capability>.config.ts roles stay unchanged.
40
40
  # 3.3.0 is a minor (alpha.16): it ADDS the optional app.ci-stack-deploy slot, the managed workflow of the starci app stack verbs.
41
+ # 3.9.0 (ruling:lite-self-hosted-postgres-passport-minio, 2026-10-29) is a major for lite and changes no full slot but the S3 owner's
42
+ # folder: lite leaves Supabase. The app.supabase* and be.integrations.supabase slots and fe.route.callback are removed; app.db (db/, its
43
+ # migrations, archive, seed and managed bootstrap), be.integrations.passport and be.integrations.object-storage are added; lite's schemaAuthority is sql.
41
44
  # 3.4.0 is a minor (alpha.25): it ADDS the opt-in be.contract.mcp-tools slot, the committed MCP tool-catalog snapshot of an app.
42
45
  # 3.5.0 is a minor (alpha.27): it ADDS the opt-in app.browser-config slot, the managed browser/tsconfig.json that type-checks the browser
43
46
  # journey (enabled with app.browser by `browser: true`).
@@ -45,6 +48,8 @@ versioning:
45
48
  # 2026-10-24: lite may serve SSE, and SSE is the default door for one-way push), and lets fe.modules.api hold the SSE stream operations (sse/).
46
49
  # 3.7.0 is a minor (alpha.31): it ADDS the optional app.scripts.integration slot (scripts/integration/*.mjs, the product test-support scripts that only the integration harness runs) and the optional fe.transport.stream slot, the fetch reader of server-sent-events streams (the stream half of the
47
50
  # transport client layer), and lets fe.hooks.swr hold the SWR subscription hooks (api/sse/subscriptions/useSubscribe<X>Swr.ts, one useSWRSubscription per stream operation).
51
+ # 3.10.0 is a minor (2026-10-29): it ADDS the lite be.platform.database.entities slot, the entity registry (slot fields role and importedBy), so the
52
+ # tier matrix and the public-entry rules admit exactly its <c>Entities imports and its deep importers (persistence.md BE-PERSISTENCE-3).
48
53
 
49
54
  # Field meaning, per slot.
50
55
  # id stable name, never changes meaning inside a major.
@@ -65,6 +70,10 @@ versioning:
65
70
  # outcomeHome true on a file slot: the one file that declares the repository's Outcome union. A repository holds exactly one such
66
71
  # file (modules/api/outcome.ts, the api package's outcome.ts, or modules/db/outcome.ts of a lite app),
67
72
  # and no other file declares an Outcome union (one-outcome-union, FE_TRANSPORT_OWNER).
73
+ # role on a file slot: the one part it plays that a rule reads. entity-registry = the lite PRIMARY_ENTITIES list
74
+ # (be.platform.database.entities): the one platform file that imports capability <c>Entities, imported by deep path
75
+ # only from the files importedBy names (architecture/entity-registry.mjs).
76
+ # importedBy on a slot with a role: the side-relative path patterns of the only files that import its file by deep path.
68
77
  # requires files or directories that must exist inside each instance; <var> is replaced by the bound name,
69
78
  # a leading / means repository root; a trailing / means directory.
70
79
  # requiredInstances variable -> names that must exist as instances (config, logging ... of platform).
@@ -102,34 +111,34 @@ versioning:
102
111
  # requiredInstances, why), each shaped as the same field in the slot body; every other field is the edition's.
103
112
  # `why` under lite names the adapter behind the slot's ports (pgmq, Postgres locks, in-process socket.io).
104
113
  # `tests` is never overlaid: a lite slot owes exactly the unit specs its full slot owes.
105
- # provider the hfs.json connections[].provider that enables this opt-in slot (supabase); never an app kind.
114
+ # provider the hfs.json connections[].provider that enables this opt-in slot (postgres); never an app kind.
106
115
  # since the manifest version that introduced the slot.
107
116
  presenceValues: [required, optional, opt-in, forbidden]
108
117
  trackedValues: [tracked, ignored, external]
109
118
  testValues: [unit-beside, e2e, none]
110
119
 
111
120
  # The editions a repository declares in hfs.json `edition` (absent means full). ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite IS
112
- # the full architecture with Supabase backing services - the same monorepo, slots, CQRS shape and patterns (queue,
121
+ # the full architecture on its own self-hosted PostgreSQL (ruling:lite-self-hosted-postgres-passport-minio) - the same monorepo, slots, CQRS shape and patterns (queue,
113
122
  # event bus, saga, reactors, projection, realtime, webhooks, fenced job), declared in hfs.json the same way, and the same unit
114
123
  # specs (unitRoles, logicRoles, 100 per file, @starci/jest-preset doubles, builders). What differs is ADAPTERS and DELIVERY
115
- # (ruling:R212/case-10): schema authority in supabase/migrations, the queue port on BullMQ over the Redis hfs.json
124
+ # (ruling:R212/case-10): schema authority in db/migrations, the queue port on BullMQ over the Redis hfs.json
116
125
  # sides.be.backing declares (as in full, fed by the Postgres outbox), event-bus ports on pgmq-backed topics or in-process
117
126
  # delivery, cache and locks on Postgres or the declared Redis, schedules on pg_cron (+ pg_net) enqueueing through the queue
118
- # outbox, storage and auth on Supabase. Realtime uses SSE or the socket.io gateway
127
+ # outbox, storage on S3-compatible MinIO (integrations/object-storage) and auth through integrations/passport. Realtime uses SSE or the socket.io gateway
119
128
  # in the api process, which scales horizontally and stateless (memory hub for one replica or tests; the Redis hub and @socket.io/redis-adapter when Redis is declared, else the Postgres hub and @socket.io/postgres-adapter, for N replicas; websocket only, no sticky sessions). Integration and provider sandbox contract specs are optional;
120
129
  # declared patterns owe their integration proofs (R284). Lite has no e2e world. Production uses declared docker-swarm under
121
- # .starcistacks/production/infra/swarm/ (owner ruling 2026-10-14); lite runs no self-hosted database, and runs only the backing
122
- # services sides.be.backing declares. The front end is full's (R97).
130
+ # .starcistacks/production/infra/swarm/ (owner ruling 2026-10-14); lite runs its PostgreSQL as the db service of its
131
+ # stack and only the backing services sides.be.backing declares. The front end is full's (R97).
123
132
  # Lite is this same manifest filtered: a slot whose `editions` lacks lite does not exist for a lite app, `litePresence` replaces
124
133
  # presence and a `lite` map replaces the fields it names. The edition is chosen once at scaffold; there is no upgrade
125
134
  # or downgrade between lite and full. Lite contracts are allowed (owner decision 2026-10-14).
126
135
  editions: [full, lite]
127
136
 
128
137
  # The two sides of an app: the folder of each is its profile name (be/, fe/). `reads` lists the only paths of the OTHER side a side
129
- # may read (hfs.json sides.<side>.reads names a subset), or a tree an app-root slot owns (supabase/types/); nothing else crosses sides.
138
+ # may read (hfs.json sides.<side>.reads names a subset), or a tree an app-root slot owns; nothing else crosses sides.
130
139
  sides:
131
- be: {reads: [supabase/types/]} # the generated Database types of a Supabase connection (server code types its PostgREST calls)
132
- fe: {reads: [be/contracts/, supabase/types/]} # be/contracts/: the committed contract snapshots, the input of the front end's codegen; supabase/types/: the same generated types
140
+ be: {reads: []}
141
+ fe: {reads: [be/contracts/]} # be/contracts/: the committed contract snapshots, the input of the front end's codegen
133
142
 
134
143
  # The trigger kinds of the back end's features: what starts the work of a feature. A feature slot names its kind in `trigger`; no kind imports another
135
144
  # (BE_KIND_ISOLATION), every cross-kind call is an event on the bus, and only be/apps/* compose features.
@@ -192,9 +201,9 @@ crossApp: apps never import each other; shared code is a packages/<pkg> slot.
192
201
  ruleParams:
193
202
  db:
194
203
  # R259: btree_gist supplies UUID equality for the booking interval exclusion constraint.
195
- # R259: unaccent is a first-party Supabase extension used by Vietnamese-content search functions.
204
+ # R259: unaccent ships in the supabase/postgres image of the db service, used by Vietnamese-content search functions.
196
205
  extensions: [pgmq, pg_cron, pgcrypto, pg_trgm, vector, uuid-ossp, pg_net, supabase_vault, unaccent, btree_gist]
197
- # R258 DB_CRON_OWNED: THE queue outbox of a Supabase schema (the relay hands its unsent rows to BullMQ, ruling:R212/case-10). A pg_cron
206
+ # R258 DB_CRON_OWNED: THE queue outbox of a lite schema (the relay hands its unsent rows to BullMQ, ruling:R212/case-10). A pg_cron
198
207
  # job enqueues only by inserting one row into this table, writing only these columns, one of which is `queue`.
199
208
  queueOutbox: {table: public.queue, columns: [queue, payload, created_at, correlation_id]}
200
209
  common:
@@ -218,8 +227,18 @@ ruleParams:
218
227
  "node:https": [platform/http]
219
228
  http: [platform/http]
220
229
  https: [platform/http]
221
- "@aws-sdk/client-s3": [integrations/object-storage] # the ObjectStorage port: one S3 adapter for MinIO, S3, Spaces and Supabase Storage
230
+ "@aws-sdk/client-s3": [integrations/object-storage] # the ObjectStorage port: one S3 adapter for MinIO, S3 and Spaces (R331)
222
231
  "@aws-sdk/s3-request-presigner": [integrations/object-storage]
232
+ minio: [] # storage goes through the S3 client of integrations/object-storage
233
+ "@supabase": [] # no Supabase client in a back end
234
+ "@nestjs/passport": [integrations/passport] # the identity libraries (R328)
235
+ passport: [integrations/passport]
236
+ passport-google-oauth20: [integrations/passport]
237
+ passport-local: [integrations/passport]
238
+ "@nestjs/jwt": [integrations/passport]
239
+ jose: [integrations/passport]
240
+ bcryptjs: [integrations/passport]
241
+ bcrypt: []
223
242
  cache-manager: [integrations/cache, integrations/redis]
224
243
  "@nestjs/cache-manager": [integrations/cache, integrations/redis] # the CACHE_MANAGER token: caching goes through the cache capability
225
244
  redis: [integrations/cache, integrations/redis]
@@ -391,21 +410,41 @@ ruleParams:
391
410
  # end may declare in hfs.json sides.be.backing. `clients` are the libraries that consume the service: one of them as a dependency
392
411
  # is its consumer, and under lite none of them may be a dependency while the service is undeclared. `patterns` are the declared
393
412
  # patterns that need the service. `images` are the image repositories (the last path segment) a stack runs it from.
394
- # `editions` limits admission: owner:lite-vector-is-pgvector keeps vector search on Supabase pgvector in lite.
413
+ # `editions` limits admission: owner:lite-vector-is-pgvector keeps vector search on pgvector in lite.
395
414
  backing:
396
415
  redis: {clients: [bullmq, "@nestjs/bullmq", bull, "@nestjs/bull", ioredis, redis, "@keyv/redis", cache-manager-redis-yet, cache-manager-ioredis-yet, "@socket.io/redis-adapter"], patterns: [queue, fenced-job], images: [redis, redis-stack, redis-stack-server]}
397
416
  qdrant: {clients: ["@qdrant/js-client-rest", "@qdrant/qdrant-js"], patterns: [], images: [qdrant], editions: [full]}
398
417
  # Edition lite (ruleParams(manifest, 'be', 'lite') merges these keys over the base; a map merges one level deep): the schema
399
- # authority is the Supabase migration tree (supabase/migrations/*.sql, forward-only), so a lite back end holds no TypeORM
418
+ # authority is the SQL migration tree (db/migrations/*.sql, forward-only), so a lite back end holds no TypeORM
400
419
  # migration or entity under be/ (R34/R35 read schemaAuthority; be.persistence's lite overlay allows neither). The unit roles,
401
420
  # logic roles and spec doubles are full's (owner ruling 2026-10-12 (owner:lite-production-swarm), with the Work boundary amended by ruling:R212/case-4). infraOwners: full's owners,
402
421
  # bullmq in platform/queue and ioredis/redis in integrations/cache and integrations/redis (ruling:R212/case-10: the queue is
403
422
  # Redis in both editions; a backing library is legal only when hfs.json sides.be.backing declares its service, which
404
- # HFS_BACKING_UNDECLARED checks), except Kafka, which is owned nowhere (platform/event-bus runs Supabase Queues (pgmq) through
423
+ # HFS_BACKING_UNDECLARED checks), except Kafka, which is owned nowhere (platform/event-bus runs pgmq queues through
405
424
  # the shared EntityManager); realtime is the same socket.io gateway as full, run inside the api process, scaled horizontally and stateless (hub redis when
406
425
  # Redis is declared, else hub postgres with @socket.io/postgres-adapter; no sticky sessions).
426
+ # R334: the database functions code may call through platform/database's callDbFunction, advisoryXactLock and notify helpers,
427
+ # each {name, reason}; the canon lists the pg built-ins it uses itself, and an app adds its own in hfs.json sides.be.dbFunctions.
428
+ dbFunctions:
429
+ - {name: pg_advisory_xact_lock, reason: "advisoryXactLock: a transaction-scoped lock TypeORM has no API for"}
430
+ - {name: hashtextextended, reason: "advisoryXactLock: the 64-bit key of a text lock name"}
431
+ - {name: pg_notify, reason: "notify: LISTEN/NOTIFY of the Postgres realtime hub"}
432
+ - {name: to_regproc, reason: "callDbFunction: whether an optional database function exists"}
433
+ - {name: job_claim, reason: "platform/jobs (add job, lite): the fenced claim upsert of jobs.sql"}
434
+ - {name: job_advance, reason: "platform/jobs (add job, lite): the token-guarded step write of jobs.sql"}
435
+ - {name: job_complete, reason: "platform/jobs (add job, lite): the token-guarded completion of jobs.sql"}
436
+ - {name: job_fail, reason: "platform/jobs (add job, lite): the token-guarded failure of jobs.sql"}
437
+ - {name: queue_write, reason: "platform/queue (add queue, lite): the outbox insert of queue-outbox.sql"}
438
+ - {name: queue_waiting, reason: "platform/queue (add queue, lite): the relay read FOR UPDATE SKIP LOCKED of queue-outbox.sql"}
439
+ - {name: queue_mark_sent, reason: "platform/queue (add queue, lite): the relay mark of queue-outbox.sql"}
440
+ - {name: inbox_claim_event, reason: "platform/inbox (add webhook, lite): the conflict-safe delivery claim"}
441
+ - {name: inbox_claim_client, reason: "platform/inbox (add webhook, lite): the client claim upsert returning its committed outcome"}
442
+ - {name: inbox_save_client_outcome, reason: "platform/inbox (add webhook, lite): the saved client outcome"}
443
+ - {name: inbox_sweep_clients, reason: "platform/inbox (add webhook, lite): the bounded retention sweep FOR UPDATE SKIP LOCKED"}
444
+ - {name: event_bus_write, reason: "platform/event-bus (add webhook, lite): the typed envelope insert of the outbox"}
445
+
407
446
  lite:
408
- schemaAuthority: supabase
447
+ schemaAuthority: sql
409
448
  infraOwners:
410
449
  kafkajs: []
411
450
  "@qdrant/js-client-rest": []
@@ -435,7 +474,7 @@ slots:
435
474
  - id: app.package-manifest
436
475
  since: 3.1.0
437
476
  profiles: [app]
438
- editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite manages Supabase and wiring scripts; full also manages e2e and stack test scripts. Both may run optional contracts."
477
+ editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite manages its database and wiring scripts; full also manages e2e and stack test scripts. Both may run optional contracts."
439
478
  # The one package.json of the app, at the app root: be's dependencies and every tool, and every script; npm workspaces are
440
479
  # exactly fe/apps/* and fe/packages/* (HFS_MONO_WORKSPACES), each with its own package.json. be has no package.json of its own.
441
480
  path: package.json
@@ -504,7 +543,7 @@ slots:
504
543
  - id: app.format-config
505
544
  since: 3.1.0
506
545
  profiles: [app]
507
- editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite additionally ignores Supabase-generated row types when formatting; full has its TypeORM schema authority."
546
+ editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite additionally ignores the generated database row types when formatting; full has its TypeORM schema authority."
508
547
  # The one formatter configuration of the app (prettier walks up from every file of both sides to it): .prettierrc names
509
548
  # @starci/prettier-config, .prettierignore is the template.
510
549
  path: "{.prettierrc,.prettierignore}"
@@ -513,7 +552,7 @@ slots:
513
552
  tier: none
514
553
  tests: none
515
554
  managedBy: tool-config
516
- liteManagedBy: tool-config-lite # the same files plus the generated supabase/types, which the Supabase CLI writes unformatted
555
+ liteManagedBy: tool-config-lite # the same files as full
517
556
  rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL]
518
557
  - id: app.tool-config-optional
519
558
  since: 3.1.0
@@ -556,7 +595,7 @@ slots:
556
595
  - id: app.ci
557
596
  since: 3.1.0
558
597
  profiles: [app]
559
- editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite CI proves boot through R272 wiring and checks Supabase artifacts; full also provides its e2e lane."
598
+ editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite CI proves boot through R272 wiring and checks its database artifacts; full also provides its e2e lane."
560
599
  path: ".github/workflows/ci.yml"
561
600
  presence: required
562
601
  tracked: tracked
@@ -581,7 +620,7 @@ slots:
581
620
  - id: app.ci-images
582
621
  since: 3.1.0
583
622
  profiles: [app]
584
- editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite renders its Supabase-backed image workflow; full renders images for its separately deployed services."
623
+ editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite renders its image workflow; full renders images for its separately deployed services."
585
624
  # Builds every image of the app (one matrix entry per declared app, path-filtered), never pushes: rendered by starci app sync.
586
625
  path: ".github/workflows/images.yml"
587
626
  presence: required
@@ -596,7 +635,7 @@ slots:
596
635
  exemplifiedBy:
597
636
  - packages/hfs/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml
598
637
  - {path: packages/hfs/src/hfs-release-provenance.spec.mjs, edition: full}
599
- editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite manages its Supabase database deploy workflow; full owns an optional provider-specific workflow."
638
+ editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite manages its database deploy workflow; full owns an optional provider-specific workflow."
600
639
  # The database deploy workflow: workflow_dispatch only (like app.ci-e2e); starci app sync renders it under the managed ci group.
601
640
  path: ".github/workflows/db-deploy.yml"
602
641
  presence: optional
@@ -634,7 +673,7 @@ slots:
634
673
  since: 3.1.0
635
674
  profiles: [app]
636
675
  # Production delivery tooling for the app's own processes: compose files, Caddyfile, *.env.example and ops shell scripts.
637
- # Lite still keeps its backing services on Supabase and its encrypted custody in app.starcistacks; R06 scans every file here.
676
+ # Lite keeps its encrypted custody in app.starcistacks; R06 scans every file here.
638
677
  path: "deploy/"
639
678
  presence: optional
640
679
  tracked: tracked
@@ -718,7 +757,7 @@ slots:
718
757
  profiles: [app]
719
758
  exemplifiedBy:
720
759
  - {path: packages/hfs/templates/app/skeleton/scripts/codegen.mjs, edition: full}
721
- editionWhy: "Lite manages its Supabase codegen script; full owns operational scripts through the generic path (ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4))."
760
+ editionWhy: "Lite manages its database-types codegen script; full owns operational scripts through the generic path (ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4))."
722
761
  # The app's operational scripts only (`*.mjs`, `*.cjs`, `*.ps1`, `*.sh`), or nothing: an empty folder is `scripts/.gitkeep`, which
723
762
  # `starci app scaffold` writes. A spec or a test (BE_SPEC_PLACEMENT, FE_NO_TESTS), a check or lint source (`check-*`, `eslint-local-rules*`,
724
763
  # a local eslint plugin: HFS_REPO_LOCAL_CHECK) and anything that re-implements a canon check is not an operational script and has no place here.
@@ -803,7 +842,7 @@ slots:
803
842
  - id: app.starcistacks
804
843
  since: 3.1.0
805
844
  profiles: [app]
806
- editionWhy: "owner:lite-production-swarm: lite requires declared docker-swarm production with Supabase backing; owner:dev-docker-prod-swarm keeps dependencies in Docker Linux containers in dev and the app and dependencies on Swarm in prod except hosted Supabase."
845
+ editionWhy: "owner:lite-production-swarm: lite requires declared docker-swarm production with its PostgreSQL as a stack service; owner:dev-docker-prod-swarm keeps dependencies in Docker Linux containers in dev and the app and dependencies on Swarm in prod."
807
846
  exemplifiedBy:
808
847
  - {path: packages/hfs/templates/app/skeleton/.starcistacks/, edition: full}
809
848
  - {path: packages/hfs/templates/app/skeleton-lite/.starcistacks/, edition: lite}
@@ -818,7 +857,7 @@ slots:
818
857
  "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
819
858
  forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
820
859
  # Owner ruling 2026-10-14: lite declares docker-swarm production, app runtime config, sealed custody and seed data.
821
- # Supabase supplies the database and broker; production has no Compose or host cron.
860
+ # The db service supplies the database and broker (pgmq, pg_cron); production has no Compose or host cron.
822
861
  lite:
823
862
  requires: [application-stacks.yaml]
824
863
  allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/swarm/**", "<env>/infra/metadata.json",
@@ -881,122 +920,78 @@ slots:
881
920
  goesTo: ".starcistacks/<env>/secrets/<slug>.enc (at the app root); decrypted values only in memory or under %LOCALAPPDATA%/StarCi/secrets/<project>/<env>/ via *_FILE"
882
921
  rules: [HFS_PLAINTEXT_SECRET]
883
922
 
884
- # ----- app root: the Supabase project of an app whose connection declares provider: supabase (every lite app) -------
885
- - id: app.supabase
886
- profiles: [app]
887
- exemplifiedBy:
888
- - packages/hfs/templates/app/skeleton-lite/supabase/
889
- - {path: packages/hfs/src/hfs-database-access.spec.mjs, edition: full}
890
- editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): Supabase is required in lite; full enables it only through a declared connection."
891
- # The one Supabase project directory, at the app root beside be/, fe/ and .starcistacks (never under a side).
892
- path: "supabase/"
893
- presence: opt-in # enabled by a connection with provider: supabase; required under edition lite
894
- provider: supabase
895
- litePresence: required
896
- tracked: tracked
897
- tier: none
898
- tests: none
899
- since: 2.1.0
900
- - id: app.supabase.config
923
+ # ----- app root: the database of a lite app (ruling:lite-self-hosted-postgres-passport-minio): a self-hosted PostgreSQL in the app's
924
+ # stack (the `db` service, image supabase/postgres for pg_cron, pg_net, pgmq, vector, unaccent and pgcrypto), migrated by
925
+ # `starci app db migrate --url-env <NAME>`. A full app keeps its TypeORM migrations under be/ and has no such folder.
926
+ - id: app.db
901
927
  profiles: [app]
928
+ editions: [lite]
902
929
  exemplifiedBy:
903
- - packages/hfs/templates/app/skeleton-lite/supabase/config.toml
904
- - {path: packages/hfs/src/db-config-policy.spec.mjs, edition: full}
905
- editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires Supabase configuration; full requires it only when Supabase is declared."
906
- # The project configuration: auth keys are env(NAME) references, never literals (L08 judges its policy keys).
907
- path: "supabase/config.toml"
908
- presence: opt-in
909
- provider: supabase
910
- litePresence: required
930
+ - packages/hfs/templates/app/skeleton-lite/db/
931
+ # The one database directory, at the app root beside be/, fe/ and .starcistacks (never under a side).
932
+ path: "db/"
933
+ presence: required
911
934
  tracked: tracked
912
935
  tier: none
913
936
  tests: none
914
- since: 2.1.0
915
- - id: app.supabase.migrations
937
+ since: 3.9.0
938
+ - id: app.db.migrations
916
939
  profiles: [app]
940
+ editions: [lite]
917
941
  exemplifiedBy:
918
- - packages/hfs/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql
919
- - {path: packages/hfs/src/db-migration-shape.spec.mjs, edition: full}
920
- editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite uses Supabase schema authority and requires a migration; full opts into that provider."
921
- # The schema authority of an app on Supabase: forward-only SQL migrations named <timestamp14>_<kebab-name>.sql,
922
- # one file per change, immutable once on origin/main (L02-L08 judge them).
923
- path: "supabase/migrations/<stamp>_<name>.sql"
924
- presence: opt-in
925
- provider: supabase
926
- litePresence: required
927
- lite: {minInstances: 1}
942
+ - packages/hfs/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql
943
+ - {path: packages/hfs/src/db-migration-shape.spec.mjs, edition: lite}
944
+ # The schema authority of a lite app: forward-only SQL migrations named <timestamp14>_<kebab-name>.sql, one file per change,
945
+ # immutable once on origin/main, applied in order by `starci app db migrate` and recorded in the history table.
946
+ path: "db/migrations/<stamp>_<name>.sql"
947
+ presence: required
948
+ minInstances: 1
928
949
  tracked: tracked
929
950
  tier: none
930
951
  tests: none
931
- since: 2.1.0
932
- - id: app.supabase.migrations.archive
952
+ since: 3.9.0
953
+ rules: [DB_MIGRATION_SHAPE, LITE_DB_MIGRATION]
954
+ - id: app.db.migrations.archive
933
955
  profiles: [app]
934
- # The migrations an existing app retired at its cutover (owner ruling 2026-10-13, migration baseline policy): the live schema is
935
- # dumped into ONE baseline migration written to the current standard, marked applied on production without a re-run, and the
936
- # older files move here. The Supabase CLI applies only supabase/migrations/<stamp>_<name>.sql, never this folder, and no
937
- # migration rule (L02-L08) judges it; new migrations append after the baseline.
938
- path: "supabase/migrations/_archive/"
939
- presence: opt-in
940
- provider: supabase
956
+ editions: [lite]
957
+ # The migrations an app retired at a baseline cutover: the live schema is dumped into ONE baseline migration, marked applied
958
+ # without a re-run, and the older files move here. `starci app db migrate` never applies this folder and no migration rule judges it.
959
+ path: "db/migrations/archive/"
960
+ presence: optional
941
961
  tracked: tracked
942
962
  tier: none
943
963
  tests: none
944
- since: 3.0.0
945
- - id: app.supabase.seed
964
+ since: 3.9.0
965
+ - id: app.db.seed
946
966
  profiles: [app]
947
- path: "supabase/seed.sql"
967
+ editions: [lite]
968
+ path: "db/seed.sql"
948
969
  presence: optional # synthetic dev data only; a lite app seeds nothing it cannot regenerate
949
970
  tracked: tracked
950
971
  tier: none
951
972
  tests: none
952
- since: 2.1.0
953
- - id: app.supabase.types
954
- profiles: [app]
955
- templateNa: "Database row types are generated from the app's Supabase schema by contract:emit; a hand-authored scaffold template would invent the schema contract."
956
- editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires generated Supabase row types; full enables them with the provider."
957
- # The generated Database types (database.types.ts) contract:emit writes from the migrations; both sides read it
958
- # (sides.<side>.reads supabase/types/); drift against regeneration is L09.
959
- path: "supabase/types/database.types.ts"
960
- presence: opt-in
961
- provider: supabase
962
- litePresence: required
963
- tracked: tracked
964
- tier: none
965
- tests: none
966
- since: 2.1.0
967
- exemplifiedBy:
968
- - {path: packages/hfs/src/db-types-returned-collector.spec.mjs, edition: full}
969
- - id: app.supabase.templates
973
+ since: 3.9.0
974
+ - id: app.db.bootstrap
970
975
  profiles: [app]
971
- path: "supabase/templates/*.html"
972
- presence: optional # the auth e-mail templates Supabase serves
976
+ editions: [lite]
977
+ # The managed pre-migration bootstrap (templates/app/db-bootstrap): the auth, storage and extensions schemas, auth.users with the
978
+ # GoTrue column set, auth.uid/role/jwt stubs, the NOLOGIN browser roles and the realtime publication, so migrations written for a
979
+ # hosted Supabase apply to the self-hosted db service. `starci app db migrate` applies it before the first migration of an empty database.
980
+ path: "db/bootstrap.sql"
981
+ presence: required
973
982
  tracked: tracked
983
+ managedBy: db-bootstrap
974
984
  tier: none
975
985
  tests: none
976
- since: 2.1.0
977
- - id: app.supabase.local-state
978
- profiles: [app]
979
- path: "supabase/{.temp,.branches}/"
980
- presence: optional
981
- tracked: ignored # the local CLI's working state: exists, never committed
982
- tier: none
983
- tests: none
984
- since: 2.1.0
985
- - id: app.supabase.functions
986
- profiles: [app]
987
- path: "supabase/functions/"
988
- presence: forbidden # a Deno runtime is a second language runtime outside the lint canon and the slot machine (lite v1)
989
- tracked: external
990
- tier: none
991
- tests: none
992
- goesTo: "a be feature: webhooks/<provider> for a third-party callback, api for an endpoint, a job of a queue (BullMQ on the declared Redis, its tick enqueued through the queue outbox) or a cli command on a schedule (pg_cron / pg_net)"
993
- since: 2.1.0
986
+ since: 3.9.0
987
+ rules: [HFS_MANAGED_FILE_DRIFT]
988
+
994
989
 
995
990
  # ----- side root (both sides): what a standalone repository root holds, less the app-root files ----------
996
991
  - id: repo.side-root-forbidden
997
992
  since: 3.1.0
998
993
  profiles: [be, fe]
999
- path: "{package.json,package-lock.json,hfs.json,README.md,.gitignore,.gitattributes,.husky/,.github/,.starciwork/,.starcistacks/,supabase/,.sops.yaml,sonar-project.properties,codecov.yml,.prettierrc,.prettierignore,scripts/}"
994
+ path: "{package.json,package-lock.json,hfs.json,README.md,.gitignore,.gitattributes,.husky/,.github/,.starciwork/,.starcistacks/,db/,.sops.yaml,sonar-project.properties,codecov.yml,.prettierrc,.prettierignore,scripts/}"
1000
995
  presence: forbidden
1001
996
  tracked: external
1002
997
  tier: none
@@ -1266,8 +1261,8 @@ slots:
1266
1261
  - {path: packages/hfs/src/mcp-tools-snapshot.spec.mjs, editions: [lite, full]}
1267
1262
 
1268
1263
  # ----- BE: apps (one slot per app kind; a new kind = a new slot, minor bump) ---------------------------
1269
- # The files of every app (owner ruling 2026-10-14, R245 and R246, since 3.1.0), each with a line budget that
1270
- # BE_APP_COMPOSITION_BUDGET holds absolutely (not a ratchet):
1264
+ # The files of every app (owner ruling 2026-10-14, R245 and R246, since 3.1.0), each with a line budget (lines excluding
1265
+ # import declarations, multi-line imports included) that BE_APP_COMPOSITION_BUDGET holds absolutely (not a ratchet):
1271
1266
  # main.ts the entry: builds EnvSource once, imports and calls read<App>Env from <app>.env.ts, builds options and boots.
1272
1267
  # <app>.env.ts exports interface <App>Env and read<App>Env(env: EnvSource): <App>Env (for example ApiEnv/readApiEnv);
1273
1268
  # fail-fast environment parsing only, via EnvSource and unchanged capability parse helpers; no localhost
@@ -1317,7 +1312,7 @@ slots:
1317
1312
  - id: be.app.cli
1318
1313
  since: 3.1.0
1319
1314
  profiles: [be]
1320
- editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): Supabase CLI owns lite migrations; a lite cli app is optional for declared operator commands."
1315
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): `starci app db migrate` owns lite migrations; a lite cli app is optional for declared operator commands."
1321
1316
  # The ONE app of every one-off action (migrate, seed, sync, backup, any operator command): be/apps/cli, on nest-commander.
1322
1317
  # main.ts boots with CommandFactory.run (BE_CLI_BOOTSTRAP); app.module.ts composes the platform and the cli feature root
1323
1318
  # (src/features/cli/cli.module.ts); cli.env.ts exports CliEnv and readCliEnv(env: EnvSource): CliEnv, and cli.options.ts carries the env-derived values,
@@ -1327,7 +1322,7 @@ slots:
1327
1322
  path: "apps/<app>/src/"
1328
1323
  appKind: cli
1329
1324
  presence: opt-in
1330
- litePresence: optional # lite: schema migration is the Supabase CLI's job; a cli app exists only when the app declares one
1325
+ litePresence: optional # lite: schema migration is `starci app db migrate`'s job; a cli app exists only when the app declares one
1331
1326
  tracked: tracked
1332
1327
  tier: app
1333
1328
  owner: true
@@ -1678,7 +1673,7 @@ slots:
1678
1673
  - id: be.persistence
1679
1674
  since: 3.1.0
1680
1675
  profiles: [be]
1681
- editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): Supabase migrations own lite tables, with SQL and row types here; full keeps TypeORM entities and migrations here."
1676
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): db/migrations own lite tables and both editions map them with TypeORM entities here (ruling:lite-persistence-typeorm-entities); full also keeps its TypeORM migrations here."
1682
1677
  path: "src/modules/{domain,platform,projections}/<capability>/persistence/"
1683
1678
  # R178: the platform capabilities whose tables exist on EVERY connection that uses them (the event-bus outbox, the inbox of every service, the job table); their arrays may be
1684
1679
  # registered on several declared connections. Any other capability registers its arrays on exactly one (BE_SCHEMA_OWNER).
@@ -1689,15 +1684,17 @@ slots:
1689
1684
  requires: [connection.ts] # the capability's <c>Entities / <c>Migrations arrays (the owner index re-exports the two arrays by name); no CONNECTION alias: the connection is the one the apps register the arrays on
1690
1685
  allows: ["entities/<table>.entity.ts", "migrations/<epochMs13>-<kebab-name>.ts", "<name>.sql.ts", "<name>.rows.ts", connection.ts]
1691
1686
  forbids: [index.ts, "<name>.repository.ts"]
1692
- # Lite keeps the shared-EntityManager persistence (raw SqlText + rows types over the generated Database row types) but the
1693
- # schema authority is supabase/migrations (ruleParams.be.lite.schemaAuthority): no TypeORM entity and no be migration.
1687
+ # Lite maps its tables with the same TypeORM entities as full (ruling:lite-persistence-typeorm-entities): each capability's
1688
+ # persistence/entities/<table>.entity.ts and an entities/index.ts exporting its <c>Entities array, which platform/database
1689
+ # lists by name; the schema authority stays db/migrations (ruleParams.be.lite.schemaAuthority), so no be migration and no
1690
+ # raw SQL file (R334 BE_PERSISTENCE_NO_RAW_SQL).
1694
1691
  lite:
1695
- requires: [] # no TypeORM arrays to re-export: the persistence of a lite capability is its sql and rows files
1696
- allows: ["<name>.sql.ts", "<name>.rows.ts", connection.ts]
1697
- forbids: [index.ts, "<name>.repository.ts", entities/, migrations/]
1692
+ requires: []
1693
+ allows: ["entities/<table>.entity.ts", entities/index.ts, connection.ts]
1694
+ forbids: [index.ts, "<name>.repository.ts", migrations/, "<name>.sql.ts", "<name>.rows.ts"]
1698
1695
  tests: none
1699
1696
  coverage: none
1700
- rules: [BE_SCHEMA_OWNER, BE_SCHEMA_AUTHORITY, BE_SQL_OUTSIDE_PERSISTENCE, BE_SQL_TABLE_OWNER, BE_SQL_RETURNING_SHAPE, BE_CONTEXT_COUPLING, BE_CONTEXT_PLATFORM_TABLES]
1697
+ rules: [BE_SCHEMA_OWNER, BE_SCHEMA_AUTHORITY, BE_SQL_OUTSIDE_PERSISTENCE, BE_PERSISTENCE_NO_RAW_SQL, BE_SQL_TABLE_OWNER, BE_SQL_RETURNING_SHAPE, BE_CONTEXT_COUPLING, BE_CONTEXT_PLATFORM_TABLES]
1701
1698
  - id: be.platform
1702
1699
  since: 3.1.0
1703
1700
  profiles: [be]
@@ -1720,6 +1717,21 @@ slots:
1720
1717
  coverage: required
1721
1718
  budget: {indexExports: 60}
1722
1719
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE]
1720
+ - id: be.platform.database.entities
1721
+ since: 3.10.0
1722
+ profiles: [be]
1723
+ editions: [lite]
1724
+ editionWhy: "lite registers the one PRIMARY_ENTITIES list on its single primary connection; full registers each capability's <c>Entities per connection from persistence/connection.ts."
1725
+ path: "src/modules/platform/database/entities.ts"
1726
+ presence: optional
1727
+ tracked: tracked
1728
+ tier: inherit
1729
+ role: entity-registry
1730
+ importedBy: ["apps/*/src/compose/*.ts", "apps/*/src/app.module.ts", "src/tests/world/**"]
1731
+ tests: none
1732
+ coverage: none
1733
+ why: "the entity registry (persistence.md BE-PERSISTENCE-3): it lists every capability's <c>Entities array by name, no glob, as PRIMARY_ENTITIES, the one list the primary connection maps, and exports nothing else. It is the one platform file that imports domain and platform capabilities, each import naming only that capability's <c>Entities from its index.ts (BE_TIER_DIRECTION, ARCH_OWNER_EXPORT_BYPASS); DatabaseModule and the platform/database index never import it, and only the files importedBy names (app composition and the test world) import it, by the deep path @modules/platform/database/entities (BE_PUBLIC_SURFACE), since database.module, this list and the capability indexes would otherwise form a cycle that fails at boot"
1734
+ rules: [BE_TIER_DIRECTION, BE_PUBLIC_SURFACE, BE_SCHEMA_OWNER]
1723
1735
  - id: be.platform.logging
1724
1736
  since: 3.5.0
1725
1737
  profiles: [be]
@@ -1748,7 +1760,7 @@ slots:
1748
1760
  owner: true
1749
1761
  lite:
1750
1762
  why: >-
1751
- the same EventPublisher and EventConsumer ports on Supabase Queues (pgmq): the outbox relay sends each committed row to the
1763
+ the same EventPublisher and EventConsumer ports on pgmq queues: the outbox relay sends each committed row to the
1752
1764
  pgmq queue of every consumer group of its topic through the shared EntityManager, a consumer reads with a visibility
1753
1765
  timeout, retries by letting it lapse and archives the message on dead letter (read_ct over the budget); a single api may
1754
1766
  deliver in process. No Kafka (knowledge/patterns/be/event-bus.yaml liteAdapter)
@@ -1762,7 +1774,7 @@ slots:
1762
1774
  - id: be.platform.queue
1763
1775
  since: 3.1.0
1764
1776
  profiles: [be]
1765
- editionWhy: "ruling:R212/case-10: both editions implement the queue ports with BullMQ on Redis; lite declares its Redis in hfs.json sides.be.backing and keeps the outbox table in a supabase migration."
1777
+ editionWhy: "ruling:R212/case-10: both editions implement the queue ports with BullMQ on Redis; lite declares its Redis in hfs.json sides.be.backing and keeps the outbox table in a db migration."
1766
1778
  path: "src/modules/platform/queue/"
1767
1779
  presence: opt-in
1768
1780
  pattern: queue # the ONE BullMQ owner: the outbox relay into queues (jobId = outbox id) and job schedulers (knowledge/patterns/be/queues.yaml)
@@ -1773,7 +1785,7 @@ slots:
1773
1785
  why: >-
1774
1786
  the same QueueOutbox and QueueWorkerRegistry ports and full's BullMQ adapter on the declared Redis (backing: [redis]): the
1775
1787
  relay adds each committed outbox row with the outbox id as the job id, workers retry with exponential backoff and keep a
1776
- job that exhausts its attempts as failed (the dead letter); the outbox table comes from a supabase migration, and a
1788
+ job that exhausts its attempts as failed (the dead letter); the outbox table comes from a db migration, and a
1777
1789
  schedule is a BullMQ job scheduler or a pg_cron tick that inserts one outbox row (knowledge/patterns/be/queues.yaml liteAdapter)
1778
1790
  requires: [index.ts, queue.module.ts, queue.port.ts, queue.contracts.ts, queue-outbox.service.ts, queue-outbox.service.spec.ts]
1779
1791
  tests: unit-beside
@@ -1867,7 +1879,7 @@ slots:
1867
1879
  - id: be.platform.jobs
1868
1880
  since: 3.1.0
1869
1881
  profiles: [be]
1870
- editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite persists fenced claims in Supabase migrations and dispatches through the BullMQ queue on its declared Redis (ruling:R212/case-10); full uses TypeORM schema authority and the same queue adapter."
1882
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite persists fenced claims in db migrations and dispatches through the BullMQ queue on its declared Redis (ruling:R212/case-10); full uses TypeORM schema authority and the same queue adapter."
1871
1883
  path: "src/modules/platform/jobs/"
1872
1884
  presence: opt-in
1873
1885
  pattern: fenced-job # the ONE fencing primitive: the job entity, the claim that bumps the token, guarded writes (knowledge/patterns/be/jobs.yaml)
@@ -1882,7 +1894,7 @@ slots:
1882
1894
  - id: be.projections
1883
1895
  since: 3.1.0
1884
1896
  profiles: [be]
1885
- editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite projection tables live in Supabase migrations; full keeps the TypeORM projection entity beside its projection."
1897
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite projection tables live in db migrations; full keeps the TypeORM projection entity beside its projection."
1886
1898
  path: "src/modules/projections/<name>/"
1887
1899
  presence: opt-in
1888
1900
  pattern: projection # a read model: only its projection writes its table; recompute is idempotent and replayable; readers use get*
@@ -1891,7 +1903,7 @@ slots:
1891
1903
  owner: true
1892
1904
  requires: [index.ts, "<name>.module.ts", "<name>.projection.ts", "<name>.projection-entity.ts"]
1893
1905
  allows: [index.ts, "<name>.module.ts", "<name>.module-definition.ts", "<name>.options.ts", "<name>.projection.ts", "<name>.projection-entity.ts", "<name>.contracts.ts", persistence/]
1894
- lite: # the read model's table is a supabase migration with its rows type (lite's schema authority): no TypeORM projection entity
1906
+ lite: # the read model's table is a db migration (lite's schema authority) mapped by its entity under persistence/entities/ (R334)
1895
1907
  requires: [index.ts, "<name>.module.ts", "<name>.projection.ts"]
1896
1908
  allows: [index.ts, "<name>.module.ts", "<name>.module-definition.ts", "<name>.options.ts", "<name>.projection.ts", "<name>.contracts.ts", persistence/]
1897
1909
  tests: unit-beside # a measured slot: <name>.projection.spec.ts beside its projection is legal (the file owes 100 per file; a spec of its own is optional)
@@ -1924,28 +1936,44 @@ slots:
1924
1936
  why: a self-hosted ML model client is an integration; weights and datasets are never in git, and a language model is called through the ai-router gateway by the integrations/ai LangChain client (BE-AI-1, BE-AI-3)
1925
1937
  rules: [BE_TIER_DIRECTION, BE_INFRA_OWNER, BE_INTEGRATION_SPEC_MISSING]
1926
1938
 
1927
- - id: be.integrations.supabase
1939
+ - id: be.integrations.passport
1940
+ profiles: [be]
1941
+ editions: [lite]
1942
+ exemplifiedBy:
1943
+ - packages/hfs/templates/be/skeleton-lite/src/modules/integrations/passport/
1944
+ # The one owner of the lite identity libraries (ruleParams.be.infraOwners: @nestjs/passport, passport, passport-google-oauth20,
1945
+ # passport-local, @nestjs/jwt, jose, bcryptjs): the Google OAuth 2.0 strategy (state and PKCE), the local email and password
1946
+ # strategy (bcrypt), the app's own asymmetric access tokens and their JWKS, and the rotating refresh cookie. domain/identity
1947
+ # verifies a token through this integration's verifier and builds the principal (contexts/code/be/api-auth.md).
1948
+ path: "src/modules/integrations/passport/"
1949
+ presence: required
1950
+ tracked: tracked
1951
+ tier: integrations
1952
+ owner: true
1953
+ requires: [index.ts, passport.config.ts, passport.options.ts, passport.decorators.ts, passport.tokens.ts, passport.password.ts, passport.cookie.ts] # Secrets, no defaults (R44); options and named injectors (R83); tokens and JWKS; bcrypt; the refresh cookie
1954
+ allows: [index.ts, passport.config.ts, passport.options.ts, passport.decorators.ts, passport.tokens.ts, passport.password.ts, passport.cookie.ts, strategies/, errors/]
1955
+ tests: unit-beside
1956
+ rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT, BE_INFRA_OWNER, BE_AUTH_PASSPORT_OWNER, BE_AUTH_JWT_OWN_JWKS, BE_AUTH_REFRESH_COOKIE]
1957
+ coverage: required
1958
+ since: 3.9.0
1959
+ - id: be.integrations.object-storage
1928
1960
  profiles: [be]
1929
1961
  exemplifiedBy:
1930
- - packages/hfs/templates/be/skeleton-lite/src/modules/integrations/supabase/
1931
- - {path: packages/hfs/src/be-supabase-jwt-verified.spec.mjs, edition: full}
1932
- editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires the Supabase Auth and Storage integration; full opts in through a connection."
1933
- # The only be import of @supabase/* (L10): the Auth admin and Storage admin client, plus the JWKS-based verification of
1934
- # the Supabase access token the guard chain reads (L16). The connection itself is ordinary Postgres (the shared
1935
- # EntityManager of the declared connection); what lives here are the service calls no database role covers.
1936
- path: "src/modules/integrations/supabase/"
1937
- presence: opt-in # enabled by a connection with provider: supabase; required under edition lite
1938
- provider: supabase
1962
+ - packages/hfs/templates/be/skeleton-lite/src/modules/integrations/object-storage/
1963
+ # The one owner of the S3 client (@aws-sdk/client-s3 and @aws-sdk/s3-request-presigner) over S3-compatible storage (MinIO in
1964
+ # lite): the ObjectStorage port with presigned PUT and GET; no other module imports an S3 SDK or minio.
1965
+ path: "src/modules/integrations/object-storage/"
1966
+ presence: optional
1939
1967
  litePresence: required
1940
1968
  tracked: tracked
1941
1969
  tier: integrations
1942
1970
  owner: true
1943
- requires: [index.ts, supabase.config.ts, supabase.options.ts, supabase.decorators.ts, supabase.client.ts, supabase.jwks.ts] # Secrets, no defaults (R44); the options and the named injector (R83); the admin client; the token verification
1944
- allows: [index.ts, supabase.config.ts, supabase.options.ts, supabase.decorators.ts, supabase.client.ts, supabase.jwks.ts, errors/]
1971
+ requires: [index.ts, storage.config.ts, storage.options.ts, storage.decorators.ts, storage.client.ts]
1972
+ allows: [index.ts, storage.config.ts, storage.options.ts, storage.decorators.ts, storage.client.ts, errors/]
1945
1973
  tests: unit-beside
1946
- rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT]
1974
+ rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT, BE_INFRA_OWNER, BE_STORAGE_OWNER]
1947
1975
  coverage: required
1948
- since: 2.1.0
1976
+ since: 3.9.0
1949
1977
 
1950
1978
  # ----- BE: tests (owner test layout 2026-09-30). Unit specs are <name>.service.spec.ts beside their service and nothing else; everything else
1951
1979
  # lives under src/tests/, one folder per kind, and the folder and the file suffix always agree (a file whose suffix does
@@ -1953,15 +1981,15 @@ slots:
1953
1981
  - id: be.tests.world
1954
1982
  since: 3.1.0
1955
1983
  profiles: [be]
1956
- editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite integration proofs use the local Supabase stack and network fakes; full also boots apps and self-hosted services."
1984
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite integration proofs use the local db service and network fakes; full also boots apps and self-hosted services."
1957
1985
  path: "src/tests/world/"
1958
1986
  presence: optional
1959
1987
  tracked: tracked
1960
1988
  tier: e2e
1961
1989
  lite:
1962
1990
  why: >-
1963
- the world of the optional integration specs of a lite app: global-setup.ts connects to the local Supabase CLI stack
1964
- (`starci supabase start`, with the migrations applied by the local stack) and starts no container, no toxiproxy and no stack of its
1991
+ the world of the optional integration specs of a lite app: global-setup.ts connects to the local db service
1992
+ (`npm run db:start`, with the migrations applied by `npm run db:push`) and starts no container, no toxiproxy and no stack of its
1965
1993
  own; use-test-world.ts exports useTestWorld({ modules: [...] }) -> world.db.<connection> (the shared EntityManager) and
1966
1994
  world.fake.<provider> (a network-edge fake of an external SaaS). There is no world.apps (no e2e) and no world.infra
1967
1995
  (no self-hosted service to cut)
@@ -2030,7 +2058,7 @@ slots:
2030
2058
  - id: be.tests.integration
2031
2059
  since: 3.1.0
2032
2060
  profiles: [be]
2033
- editionWhy: "Lite integration specs use the local Supabase CLI stack without containers or toxiproxy (ruling:R212/case-1, amended by ruling:R212/case-2 (owner:lite-production-swarm) and ruling:R212/case-4, with contracts allowed by owner:lite-contract-specs); R284 requires declared-pattern proofs there. Full additionally proves infrastructure outages through its world (R112)."
2061
+ editionWhy: "Lite integration specs use the local db service without containers or toxiproxy (ruling:R212/case-1, amended by ruling:R212/case-2 (owner:lite-production-swarm) and ruling:R212/case-4, with contracts allowed by owner:lite-contract-specs); R284 requires declared-pattern proofs there. Full additionally proves infrastructure outages through its world (R112)."
2034
2062
  path: "src/tests/integration/<capability>/*.integration-spec.ts"
2035
2063
  presence: optional
2036
2064
  tracked: tracked
@@ -2039,8 +2067,8 @@ slots:
2039
2067
  coverage: none
2040
2068
  lite:
2041
2069
  why: >-
2042
- optional under lite: one capability module through useTestWorld({ modules }) against the local Supabase CLI stack
2043
- (`starci supabase start`), run by test:integration; no container, no toxiproxy and no outage through the world (R112 is full's)
2070
+ optional under lite: one capability module through useTestWorld({ modules }) against the local db service
2071
+ (`npm run db:start`), run by test:integration; no container, no toxiproxy and no outage through the world (R112 is full's)
2044
2072
  why: one capability module through useTestWorld({ modules }), no HTTP door of ours (SQL, transactions, concurrency, inbox claims on the real database; and every integration client, `<capability>` being its provider folder, against our real infra, a real peer app or the library fake at the network edge, with its success, its ErrorCode refusal mapping and an outage through the world, R112); run by test:integration
2045
2073
  rules: [BE_TEST_TOPOLOGY, BE_INTEGRATION_SPEC_MISSING]
2046
2074
  - id: be.tests.e2e
@@ -2053,7 +2081,7 @@ slots:
2053
2081
  tracked: tracked
2054
2082
  tier: e2e
2055
2083
  tests: e2e
2056
- goesTo: "the full edition: a lite app has no e2e world; its logic is proven by unit specs and its database by optional integration specs on `starci supabase start`"
2084
+ goesTo: "the full edition: a lite app has no e2e world; its logic is proven by unit specs and its database by optional integration specs on `npm run db:start`"
2057
2085
  coverage: none
2058
2086
  why: flow e2e through useTestWorld({ apps }); external services are the world's network fakes; run by test:e2e
2059
2087
  rules: [HFS_CI_E2E_AUTOMATIC, BE_TEST_TOPOLOGY, BE_E2E_FLOW]
@@ -2112,7 +2140,7 @@ slots:
2112
2140
  tracked: tracked
2113
2141
  tier: route
2114
2142
  # ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4) (lite = full): routes are thin and the front end serves no API - health/live is the one
2115
- # route handler of this slot, the Supabase auth callback is fe.route.callback, and no file holds a Server Action
2143
+ # route handler of this slot, the OAuth callback is a back-end door, and no file holds a Server Action
2116
2144
  # (FE_ROUTE_HANDLER_FORBIDDEN, FE_SERVER_ACTION_FORBIDDEN).
2117
2145
  allows: ["[locale]/**/{page,layout,template,loading,error,not-found,default}.tsx", "[locale]/providers.tsx",
2118
2146
  global-error.tsx, page.tsx, # the root page.tsx only redirects (FE_ROUTE_FILES_THIN)
@@ -2143,20 +2171,6 @@ slots:
2143
2171
  tests: none
2144
2172
  goesTo: "apps/<app>/src/proxy.ts: Next >= 16 reads proxy.ts and refuses middleware.ts"
2145
2173
  rules: [FE_NEXT_CONVENTIONS]
2146
- - id: fe.route.callback
2147
- profiles: [fe]
2148
- # The auth callback of a Supabase session (the PKCE code exchange), kept only when the auth flow truly needs a server exchange
2149
- # (prefer the client-side PKCE exchange in fe.modules.auth): the one non-health route handler of an app with a Supabase
2150
- # connection; an app without one has no route handler in fe/ (L15).
2151
- path: "apps/<app>/src/app/auth/callback/route.ts"
2152
- presence: opt-in
2153
- provider: supabase
2154
- tracked: tracked
2155
- tier: route
2156
- tests: none
2157
- since: 2.1.0
2158
- rules: [FE_ROUTE_HANDLER_FORBIDDEN, FE_CALLBACK_SHAPE, FE_SUPABASE_CLIENT_OWNER, FE_SERVICE_ROLE_FORBIDDEN]
2159
-
2160
2174
  - id: fe.feature
2161
2175
  since: 3.1.0
2162
2176
  profiles: [fe]
@@ -2488,18 +2502,17 @@ slots:
2488
2502
  - id: fe.modules.auth
2489
2503
  profiles: [fe]
2490
2504
  # The identity-provider client (fe-stack auth, FE-AUTH): the session, the access token the Apollo link attaches and the
2491
- # sign-in/sign-out calls. With a Supabase connection it is the ONLY fe importer of @supabase/supabase-js and uses its auth API
2492
- # only; Supabase on the front end never reads or writes data (owner ruling 2026-10-13).
2505
+ # sign-in/sign-out calls. It talks only to the app's own back-end auth doors and holds no credential: the refresh token lives in
2506
+ # the back end's httpOnly cookie and the access token in memory (ruling:lite-self-hosted-postgres-passport-minio).
2493
2507
  path: "apps/<app>/src/modules/auth/"
2494
2508
  presence: optional
2495
2509
  tracked: tracked
2496
2510
  tier: modules
2497
2511
  owner: true
2498
2512
  requires: [index.ts]
2499
- entries: [server.ts] # the server-only PKCE exchange API used by the opted-in callback route
2500
2513
  tests: none
2501
2514
  since: 3.0.0
2502
- rules: [FE_SUPABASE_CLIENT_OWNER, FE_SERVICE_ROLE_FORBIDDEN]
2515
+ rules: [FE_STACK_FORBIDDEN]
2503
2516
  - id: fe.modules.toast
2504
2517
  profiles: [fe]
2505
2518
  # Mutation feedback (fe-stack toast, FE-TOAST): the HeroUI toast wrapper, showGraphQLToast over a GraphQLResponse and the
@@ -2,27 +2,23 @@
2
2
  // against a loaded manifest remain in slots.mjs; this module mirrors modules/schemas/hfs-repo.schema.yaml only.
3
3
  import { isPlainObject } from '../../engine/plain-object.mjs';
4
4
  import { APP_KIND, connectionShapeProblems, NAME, PACKAGE_NAME, RUNTIME_KIND, SLOT_ID } from './manifest-shape.mjs';
5
+ import { dbFunctionsOk } from './rule-params-shape.mjs';
5
6
  import { kindShapeProblems, patternShapeProblems } from './declaration-slots.mjs';
6
7
  import { byCodeUnit } from '../lib/list.mjs';
7
8
 
8
- /** Shape problems of the optional hfs.json `supabase` block whose auth posture DB_CONFIG_POLICY checks. */
9
- const isSupabaseStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === 'string' && entry.trim());
9
+ /** Shape problems of the optional hfs.json `db` block of a lite app: the environment variable naming its database, its image, forced RLS and its application role. */
10
+ const isStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === 'string' && entry.trim());
10
11
 
11
- function supabaseValueProblems(block) {
12
- const bad = [];
13
- if (block.enableSignup !== undefined && typeof block.enableSignup !== 'boolean') bad.push('supabase.enableSignup must be true or false');
14
- if (block.jwtExpiry !== undefined && !(Number.isInteger(block.jwtExpiry) && block.jwtExpiry > 0 && block.jwtExpiry <= 3600)) bad.push('supabase.jwtExpiry must be an integer of seconds, 3600 at most');
15
- if (block.siteUrl !== undefined && (typeof block.siteUrl !== 'string' || !block.siteUrl.trim())) bad.push('supabase.siteUrl must be a URL string');
16
- for (const key of ['redirectUrls', 'forceRls']) if (block[key] !== undefined && !isSupabaseStringList(block[key])) bad.push(`supabase.${key} must be a list of strings`);
17
- return bad;
18
- }
19
-
20
- function supabaseBlockProblems(block) {
12
+ function dbBlockProblems(block) {
21
13
  if (block === undefined) return [];
22
- if (!isPlainObject(block)) return ['supabase must be an object'];
14
+ if (!isPlainObject(block)) return ['db must be an object'];
23
15
  const bad = [];
24
- for (const key of Object.keys(block)) if (!['enableSignup', 'jwtExpiry', 'siteUrl', 'redirectUrls', 'forceRls'].includes(key)) bad.push(`supabase has unknown key ${key}`);
25
- bad.push(...supabaseValueProblems(block));
16
+ for (const key of Object.keys(block)) if (!['urlEnv', 'image', 'forceRls', 'forceRlsExemptions', 'appRole'].includes(key)) bad.push(`db has unknown key ${key}`);
17
+ if (block.urlEnv !== undefined && (typeof block.urlEnv !== 'string' || !/^[A-Z][A-Z0-9_]*$/.test(block.urlEnv))) bad.push('db.urlEnv must name an UPPER_CASE environment variable');
18
+ if (block.image !== undefined && (typeof block.image !== 'string' || !/@sha256:[0-9a-f]{64}$/.test(block.image))) bad.push('db.image must be a digest-pinned image reference');
19
+ if (block.forceRls !== undefined && !isStringList(block.forceRls)) bad.push('db.forceRls must be a list of strings');
20
+ if (block.appRole !== undefined && (typeof block.appRole !== 'string' || !/^[a-z][a-z0-9_]*$/.test(block.appRole))) bad.push('db.appRole must name a role');
21
+ if (block.forceRlsExemptions !== undefined && !Array.isArray(block.forceRlsExemptions)) bad.push('db.forceRlsExemptions must be a list of { table, reason }');
26
22
  return bad;
27
23
  }
28
24
 
@@ -95,7 +91,8 @@ function declarationSideProblems(side, value, names) {
95
91
  const bad = [];
96
92
  const at = `sides.${side}`;
97
93
  if (!isPlainObject(value)) return [`${at} must be an object`];
98
- for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads', 'extraPackages', 'grammar', 'backing', 'queueOutbox'].includes(key)) bad.push(`${at} has unknown key ${key}`);
94
+ for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads', 'extraPackages', 'grammar', 'backing', 'queueOutbox', 'dbFunctions'].includes(key)) bad.push(`${at} has unknown key ${key}`);
95
+ if (value.dbFunctions !== undefined && (side !== 'be' || !dbFunctionsOk(value.dbFunctions))) bad.push(`${at}.dbFunctions belongs to the be side and lists {name, reason} database functions with unique names`);
99
96
  bad.push(...appShapeProblems(value.apps, at, side, names));
100
97
  if (isBadUniqueList(value.optionalSlots, (slot) => SLOT_ID.test(String(slot)))) bad.push(`${at}.optionalSlots must be a unique list of slot ids`);
101
98
  if (value.backing !== undefined && side !== 'be') bad.push('backing belongs to the be side');
@@ -127,13 +124,13 @@ function observeBlockProblems(observe) {
127
124
  return bad;
128
125
  }
129
126
 
130
- const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'supabase', 'sides', 'browser', 'lexicon', 'observe']);
127
+ const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'db', 'sides', 'browser', 'lexicon', 'observe']);
131
128
  const RUNTIME_DECLARATION_KEYS = new Set(['hfs', 'kind', 'project']);
132
129
 
133
130
  function productDeclarationProblems(declaration, profiles) {
134
131
  const bad = [];
135
132
  for (const key of Object.keys(declaration)) if (!DECLARATION_KEYS.has(key)) bad.push(`unknown key ${key}`);
136
- bad.push(...supabaseBlockProblems(declaration.supabase), ...lexiconBlockProblems(declaration.lexicon), ...observeBlockProblems(declaration.observe));
133
+ bad.push(...dbBlockProblems(declaration.db), ...lexiconBlockProblems(declaration.lexicon), ...observeBlockProblems(declaration.observe));
137
134
  if (declaration.browser !== undefined && declaration.browser !== true) bad.push('browser is `true` when the app has a browser journey (slot app.browser), and is left out otherwise');
138
135
  if (declaration.kind !== APP_KIND) bad.push(`kind must be ${APP_KIND} (a product is one app repository with a be and an fe side) or ${RUNTIME_KIND} (the StarCi runtime repository)`);
139
136
  if (!isPlainObject(declaration.sides) || Object.keys(declaration.sides).sort(byCodeUnit).join() !== profiles.join()) { bad.push('sides must declare exactly be and fe'); return bad; }
@@ -15,12 +15,12 @@ const ENV_PREFIX =/^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/;
15
15
  const CONTEXT_ISOLATIONS = Object.freeze(["database", "schema"]);
16
16
  /** The app kinds that may own a context: the services, never the migrate or cli apps that only run migrations. */
17
17
  const CONTEXT_OWNER_KINDS = Object.freeze(["api", "worker"]);
18
- /** The database engines a connection may run on: a self-described Postgres, or the Postgres of a Supabase project. */
19
- const CONNECTION_PROVIDERS = Object.freeze(["postgres", "supabase"]);
18
+ /** The database engine a connection runs on. */
19
+ const CONNECTION_PROVIDERS = Object.freeze(["postgres"]);
20
20
  /** The editions an app repository may declare in hfs.json (`full` when absent): the filter slots.mjs applies. */
21
21
  export const EDITIONS = Object.freeze(["full", "lite"]);
22
- /** What decides where schema changes live (ruleParams.<side>.schemaAuthority): TypeORM migrations under persistence/, or the Supabase migration tree. */
23
- export const SCHEMA_AUTHORITIES = Object.freeze(["typeorm", "supabase"]);
22
+ /** What decides where schema changes live (ruleParams.<side>.schemaAuthority): TypeORM migrations under persistence/, or the SQL migration tree db/migrations. */
23
+ export const SCHEMA_AUTHORITIES = Object.freeze(["typeorm", "sql"]);
24
24
  /**
25
25
  * The shape problems of the `connections` of one side: a list of {name, envPrefix, owner, isolation}, the owner an api or worker app
26
26
  * of the same side (a connection is a bounded context and one service owns it), isolation `database` or `schema`.
@@ -87,7 +87,9 @@ export const MANIFEST_KINDS = [APP_KIND, RUNTIME_KIND];
87
87
  export const manifestKind = (m) => (isPlainObject(m) && m.kind !== undefined ? m.kind : APP_KIND);
88
88
  const strList = (v) => stringList(v);
89
89
 
90
- const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'editionWhy', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome', 'templateNa', 'exemplifiedBy'];
90
+ const APP_SLOT_KEYS = ['id', 'profiles', 'path', 'presence', 'tracked', 'tier', 'tests', 'owner', 'appKind', 'minInstances', 'requiredWhen', 'requiredInstances', 'requires', 'pattern', 'trigger', 'allows', 'forbids', 'layers', 'kinds', 'roles', 'composedBy', 'budget', 'managedBy', 'liteManagedBy', 'rules', 'goesTo', 'why', 'since', 'perConnection', 'parent', 'editions', 'editionWhy', 'litePresence', 'lite', 'provider', 'coverage', 'entries', 'outcomeHome', 'templateNa', 'exemplifiedBy', 'role', 'importedBy'];
91
+ /** The roles a file slot may play that a rule reads (`role`): the entity registry of persistence.md BE-PERSISTENCE-3 (architecture/entity-registry.mjs). */
92
+ const SLOT_ROLES = ['entity-registry'];
91
93
  /** The fields a slot's `lite` overlay may hold: the same keys it would carry in the slot body, resolved under edition lite. */
92
94
  const LITE_OVERLAY_KEYS = ['path', 'requires', 'allows', 'forbids', 'minInstances', 'requiredInstances', 'why'];
93
95
  /** A runtime slot has no app kind, side composition, layer or managed template; it may name the generator of a generated copy. */
@@ -249,6 +251,8 @@ function slotEntryProblems(slot, at) {
249
251
  else if (Array.isArray(slot.allows) && slot.entries.some((entry) => !slot.allows.includes(entry))) bad.push(`${at}: every entry must be one of the names the slot allows`);
250
252
  }
251
253
  if (slot.outcomeHome !== undefined && !(slot.outcomeHome === true && typeof slot.path === 'string' && slot.path && !slot.path.endsWith('/'))) bad.push(`${at}: outcomeHome is true and belongs to a file slot (the one file that declares the repository's Outcome union)`);
254
+ if (slot.role !== undefined && !(SLOT_ROLES.includes(slot.role) && typeof slot.path === 'string' && slot.path && !slot.path.endsWith('/'))) bad.push(`${at}: role is one of ${SLOT_ROLES.join(', ')} and belongs to a file slot`);
255
+ if (slot.importedBy !== undefined && !(slot.role !== undefined && strList(slot.importedBy) && slot.importedBy.length && new Set(slot.importedBy).size === slot.importedBy.length && slot.importedBy.every(relPath))) bad.push(`${at}: importedBy is a non-empty list of unique relative path patterns of the only files that import the slot's file by deep path, on a slot with a role`);
252
256
  return bad;
253
257
  }
254
258
 
@@ -62,6 +62,9 @@ function beSpecificProblems(be, manifest) {
62
62
  return bad;
63
63
  }
64
64
 
65
+ /** True for an absent list or a list of {name, reason} database functions, names unique. */
66
+ export const dbFunctionsOk = (list) => list === undefined || (Array.isArray(list) && list.every((entry) => isPlainObject(entry) && Object.keys(entry).sort().join() === 'name,reason' && /^[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)?$/.test(entry.name) && typeof entry.reason === 'string' && entry.reason.trim().length > 0) && new Set(list.map((entry) => entry.name)).size === list.length);
67
+
65
68
  /** The shape problems of `m.ruleParams`. */
66
69
  export function ruleParamsProblems(m) {
67
70
  const bad = [];
@@ -71,7 +74,7 @@ export function ruleParamsProblems(m) {
71
74
  // common holds the parameters both sides share; a side may restate one of them (a valid value) as an override, nothing else.
72
75
  // Edition lite: a side's optional `lite` map overrides keys of the same side (never `lite` itself) when the app declares edition lite.
73
76
  if (!sideOk(rp.common, []) || !fileLinesOk(rp.common.fileLines) || !blockOk(rp.common.duplicateBlock)) bad.push('ruleParams.common needs fileLines {soft, hardGrowth} and duplicateBlock {lines >= 2, tokens >= 1}; optional functionLines and featureFiles must be positive integers');
74
- if (!sideOk(rp.be, BE_OWN, ['schemaAuthority', 'lite']) || (rp.be.schemaAuthority !== undefined && !SCHEMA_AUTHORITIES.includes(rp.be.schemaAuthority)) || !liteOk(rp.be.lite, [...BE_OWN, ...SHARED_KEYS, 'schemaAuthority'])) bad.push('ruleParams.be needs infraOwners, eventBus, specDoubles, paramNames, suffixes, bannedSuffixes, contractShape {helper}, unitRoles, logicRoles, thinRoles, patternScenarios, kindPatterns, addKinds and backing (shared parameters live in ruleParams.common, a side may override them); optional: schemaAuthority (' + SCHEMA_AUTHORITIES.join(' | ') + ') and lite (overrides of the same keys)'); else bad.push(...unitRolesProblems(rp.be), ...roleListProblems(rp.be));
77
+ if (!sideOk(rp.be, BE_OWN, ['schemaAuthority', 'lite', 'dbFunctions']) || (rp.be.schemaAuthority !== undefined && !SCHEMA_AUTHORITIES.includes(rp.be.schemaAuthority)) || !dbFunctionsOk(rp.be.dbFunctions) || !liteOk(rp.be.lite, [...BE_OWN, ...SHARED_KEYS, 'schemaAuthority', 'dbFunctions'])) bad.push('ruleParams.be needs infraOwners, eventBus, specDoubles, paramNames, suffixes, bannedSuffixes, contractShape {helper}, unitRoles, logicRoles, thinRoles, patternScenarios, kindPatterns, addKinds and backing (shared parameters live in ruleParams.common, a side may override them); optional: schemaAuthority (' + SCHEMA_AUTHORITIES.join(' | ') + '), dbFunctions ([{name, reason}]) and lite (overrides of the same keys)'); else bad.push(...unitRolesProblems(rp.be), ...roleListProblems(rp.be));
75
78
  bad.push(...beSpecificProblems(rp.be, m));
76
79
  if (rp.fe !== undefined && (!sideOk(rp.fe, [], ['lite']) || !liteOk(rp.fe.lite, SHARED_KEYS))) bad.push('ruleParams.fe may only restate the shared parameters of ruleParams.common, and carry a lite override of them');
77
80
  if (!dbParamsOk(rp.db)) bad.push('ruleParams.db may contain only extensions (a list of unique non-empty extension names) and queueOutbox ({table: <schema>.<table>, columns: [unique column names including queue]})');
@@ -49,7 +49,7 @@ function sideCrossing(repo, fromSide, toSide, toPath) {
49
49
  function importAllowedApp({ repo, root, sides }, fromPath, toPath) {
50
50
  const from = splitSide(fromPath);
51
51
  const to = splitSide(toPath);
52
- // A side file may read an app-root path (supabase/types/) only through a declared read, exactly as it reads the other side.
52
+ // A side file may read an app-root path (db/types/) only through a declared read, exactly as it reads the other side.
53
53
  if (from && from.side !== to?.side) return sideCrossing(repo, from.side, to?.side ?? null, toPath);
54
54
  if (from && to) return sides[from.side].importAllowed(from.rest, to.rest);
55
55
  return root.importAllowed(fromPath, toPath);
@@ -120,6 +120,7 @@ function sideDeclaration(manifest, declaration, name, edition, providers) {
120
120
  connections: Object.freeze((s.connections ?? []).map(connectionView)),
121
121
  providers,
122
122
  reads: Object.freeze([...(s.reads ?? [])]),
123
+ dbFunctions: Object.freeze((s.dbFunctions ?? []).map((entry) => Object.freeze({ ...entry }))),
123
124
  ...(s.grammar === undefined ? {} : { grammar: s.grammar }),
124
125
  ...(s.extraPackages === undefined ? {} : { extraPackages: Object.freeze(s.extraPackages.map(entry => Object.freeze({ ...entry }))) }),
125
126
  manifestVersion: manifest.version,
@@ -221,7 +222,8 @@ function createScopeResolver(manifest, repo) {
221
222
  trackingOf,
222
223
  /** True only for a path a slot owns and requires to be committed. */
223
224
  isTracked: (p) => trackingOf(p) === 'tracked',
224
- ruleParams: () => ruleParams(manifest, profile, edition),
225
+ // The app's own database functions (hfs.json sides.be.dbFunctions) join the canon's list (R334).
226
+ ruleParams: () => withDeclaredDbFunctions(ruleParams(manifest, profile, edition), repo.dbFunctions),
225
227
  allowedImports: (tier) => manifest.tiers[profile]?.[tier]?.mayImport ?? null,
226
228
  });
227
229
  }
@@ -235,13 +237,17 @@ function createScopeResolver(manifest, repo) {
235
237
  */
236
238
  export function createSlotResolver(manifest, repo) {
237
239
  if (repo.profile !== APP_SCOPE) return createScopeResolver(manifest, repo);
238
- // The root declares no connection of its own, yet a provider slot of the app profile (app.supabase*) is enabled by a
240
+ // The root declares no connection of its own, yet a provider slot of the app profile is enabled by a
239
241
  // side's connection with that provider: the root scope reads the union of the sides' connections.
240
242
  const root = createScopeResolver(manifest, { ...repo, connections: SIDES.flatMap((side) => repo.sides[side].connections) });
241
243
  const sides = Object.fromEntries(PROFILES.map((side) => [side, createScopeResolver(manifest, repo.sides[side])]));
242
244
  return appResolver(repo, root, sides);
243
245
  }
244
246
 
247
+ /** The rule parameters with the app's declared database functions appended to `dbFunctions` (be only), frozen. */
248
+ const withDeclaredDbFunctions = (params, declared = []) => (params.dbFunctions === undefined && !declared.length ? params
249
+ : Object.freeze({ ...params, dbFunctions: Object.freeze([...(params.dbFunctions ?? []), ...declared]) }));
250
+
245
251
  /** The rule parameters of one profile (be: infraOwners, suffixes, bannedSuffixes and the rest over the shared ruleParams.common fileLines and duplicateBlock; fe: common alone or with its overrides; runtime: ruleParams.runtime of a runtime manifest), as a frozen deep copy. Under `edition` lite the `lite` overrides of ruleParams.<profile> merge over the base (the `lite` key itself is never returned). */
246
252
  export function ruleParams(manifest, profile, edition = 'full') {
247
253
  const profiles = manifestKind(manifest) === RUNTIME_KIND ? [RUNTIME_KIND] : PROFILES;