@starci/eslint-grammar-offset-pop 0.15.10 → 0.15.11
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 +5 -0
- package/package.json +2 -2
- package/runtime/knowledge/hfs/canon-pins.yaml +44 -24
- package/runtime/knowledge/hfs/slots.yaml +164 -172
- package/runtime/scripts/hfs/declaration-shape.mjs +15 -18
- package/runtime/scripts/hfs/manifest-shape.mjs +4 -4
- package/runtime/scripts/hfs/rule-params-shape.mjs +4 -1
- package/runtime/scripts/hfs/slot-app-view.mjs +1 -1
- package/runtime/scripts/hfs/slots.mjs +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.11 - 2026-10-29
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
|
|
7
|
+
|
|
3
8
|
## 0.15.10 - 2026-10-29
|
|
4
9
|
|
|
5
10
|
- 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.
|
|
3
|
+
"version": "0.15.11",
|
|
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.
|
|
47
|
+
"@starci/eslint-grammar-core": "0.15.11"
|
|
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.
|
|
34
|
+
version: 0.15.11
|
|
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.
|
|
41
|
+
version: 3.7.0
|
|
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.
|
|
48
|
+
version: 9.5.0
|
|
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.11
|
|
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.11
|
|
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.
|
|
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.
|
|
109
|
+
version: 4.10.0
|
|
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
|
|
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
|
-
# ---
|
|
598
|
-
'@
|
|
599
|
-
version: 0.
|
|
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:
|
|
602
|
-
why:
|
|
603
|
-
|
|
604
|
-
version:
|
|
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:
|
|
607
|
-
why: the
|
|
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:
|
|
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.
|
|
10
|
+
version: 3.9.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`).
|
|
@@ -102,34 +105,34 @@ versioning:
|
|
|
102
105
|
# requiredInstances, why), each shaped as the same field in the slot body; every other field is the edition's.
|
|
103
106
|
# `why` under lite names the adapter behind the slot's ports (pgmq, Postgres locks, in-process socket.io).
|
|
104
107
|
# `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 (
|
|
108
|
+
# provider the hfs.json connections[].provider that enables this opt-in slot (postgres); never an app kind.
|
|
106
109
|
# since the manifest version that introduced the slot.
|
|
107
110
|
presenceValues: [required, optional, opt-in, forbidden]
|
|
108
111
|
trackedValues: [tracked, ignored, external]
|
|
109
112
|
testValues: [unit-beside, e2e, none]
|
|
110
113
|
|
|
111
114
|
# 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
|
|
115
|
+
# 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
116
|
# event bus, saga, reactors, projection, realtime, webhooks, fenced job), declared in hfs.json the same way, and the same unit
|
|
114
117
|
# 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
|
|
118
|
+
# (ruling:R212/case-10): schema authority in db/migrations, the queue port on BullMQ over the Redis hfs.json
|
|
116
119
|
# sides.be.backing declares (as in full, fed by the Postgres outbox), event-bus ports on pgmq-backed topics or in-process
|
|
117
120
|
# 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
|
|
121
|
+
# outbox, storage on S3-compatible MinIO (integrations/object-storage) and auth through integrations/passport. Realtime uses SSE or the socket.io gateway
|
|
119
122
|
# 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
123
|
# 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
|
|
122
|
-
# services sides.be.backing declares. The front end is full's (R97).
|
|
124
|
+
# .starcistacks/production/infra/swarm/ (owner ruling 2026-10-14); lite runs its PostgreSQL as the db service of its
|
|
125
|
+
# stack and only the backing services sides.be.backing declares. The front end is full's (R97).
|
|
123
126
|
# Lite is this same manifest filtered: a slot whose `editions` lacks lite does not exist for a lite app, `litePresence` replaces
|
|
124
127
|
# presence and a `lite` map replaces the fields it names. The edition is chosen once at scaffold; there is no upgrade
|
|
125
128
|
# or downgrade between lite and full. Lite contracts are allowed (owner decision 2026-10-14).
|
|
126
129
|
editions: [full, lite]
|
|
127
130
|
|
|
128
131
|
# 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
|
|
132
|
+
# may read (hfs.json sides.<side>.reads names a subset), or a tree an app-root slot owns; nothing else crosses sides.
|
|
130
133
|
sides:
|
|
131
|
-
be: {reads: [
|
|
132
|
-
fe: {reads: [be/contracts
|
|
134
|
+
be: {reads: []}
|
|
135
|
+
fe: {reads: [be/contracts/]} # be/contracts/: the committed contract snapshots, the input of the front end's codegen
|
|
133
136
|
|
|
134
137
|
# 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
138
|
# (BE_KIND_ISOLATION), every cross-kind call is an event on the bus, and only be/apps/* compose features.
|
|
@@ -192,9 +195,9 @@ crossApp: apps never import each other; shared code is a packages/<pkg> slot.
|
|
|
192
195
|
ruleParams:
|
|
193
196
|
db:
|
|
194
197
|
# R259: btree_gist supplies UUID equality for the booking interval exclusion constraint.
|
|
195
|
-
# R259: unaccent
|
|
198
|
+
# R259: unaccent ships in the supabase/postgres image of the db service, used by Vietnamese-content search functions.
|
|
196
199
|
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
|
|
200
|
+
# 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
201
|
# job enqueues only by inserting one row into this table, writing only these columns, one of which is `queue`.
|
|
199
202
|
queueOutbox: {table: public.queue, columns: [queue, payload, created_at, correlation_id]}
|
|
200
203
|
common:
|
|
@@ -218,8 +221,18 @@ ruleParams:
|
|
|
218
221
|
"node:https": [platform/http]
|
|
219
222
|
http: [platform/http]
|
|
220
223
|
https: [platform/http]
|
|
221
|
-
"@aws-sdk/client-s3": [integrations/object-storage] # the ObjectStorage port: one S3 adapter for MinIO, S3
|
|
224
|
+
"@aws-sdk/client-s3": [integrations/object-storage] # the ObjectStorage port: one S3 adapter for MinIO, S3 and Spaces (R331)
|
|
222
225
|
"@aws-sdk/s3-request-presigner": [integrations/object-storage]
|
|
226
|
+
minio: [] # storage goes through the S3 client of integrations/object-storage
|
|
227
|
+
"@supabase": [] # no Supabase client in a back end
|
|
228
|
+
"@nestjs/passport": [integrations/passport] # the identity libraries (R328)
|
|
229
|
+
passport: [integrations/passport]
|
|
230
|
+
passport-google-oauth20: [integrations/passport]
|
|
231
|
+
passport-local: [integrations/passport]
|
|
232
|
+
"@nestjs/jwt": [integrations/passport]
|
|
233
|
+
jose: [integrations/passport]
|
|
234
|
+
bcryptjs: [integrations/passport]
|
|
235
|
+
bcrypt: []
|
|
223
236
|
cache-manager: [integrations/cache, integrations/redis]
|
|
224
237
|
"@nestjs/cache-manager": [integrations/cache, integrations/redis] # the CACHE_MANAGER token: caching goes through the cache capability
|
|
225
238
|
redis: [integrations/cache, integrations/redis]
|
|
@@ -391,21 +404,41 @@ ruleParams:
|
|
|
391
404
|
# end may declare in hfs.json sides.be.backing. `clients` are the libraries that consume the service: one of them as a dependency
|
|
392
405
|
# is its consumer, and under lite none of them may be a dependency while the service is undeclared. `patterns` are the declared
|
|
393
406
|
# 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
|
|
407
|
+
# `editions` limits admission: owner:lite-vector-is-pgvector keeps vector search on pgvector in lite.
|
|
395
408
|
backing:
|
|
396
409
|
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
410
|
qdrant: {clients: ["@qdrant/js-client-rest", "@qdrant/qdrant-js"], patterns: [], images: [qdrant], editions: [full]}
|
|
398
411
|
# Edition lite (ruleParams(manifest, 'be', 'lite') merges these keys over the base; a map merges one level deep): the schema
|
|
399
|
-
# authority is the
|
|
412
|
+
# authority is the SQL migration tree (db/migrations/*.sql, forward-only), so a lite back end holds no TypeORM
|
|
400
413
|
# migration or entity under be/ (R34/R35 read schemaAuthority; be.persistence's lite overlay allows neither). The unit roles,
|
|
401
414
|
# 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
415
|
# bullmq in platform/queue and ioredis/redis in integrations/cache and integrations/redis (ruling:R212/case-10: the queue is
|
|
403
416
|
# 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
|
|
417
|
+
# HFS_BACKING_UNDECLARED checks), except Kafka, which is owned nowhere (platform/event-bus runs pgmq queues through
|
|
405
418
|
# 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
419
|
# Redis is declared, else hub postgres with @socket.io/postgres-adapter; no sticky sessions).
|
|
420
|
+
# R334: the database functions code may call through platform/database's callDbFunction, advisoryXactLock and notify helpers,
|
|
421
|
+
# 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.
|
|
422
|
+
dbFunctions:
|
|
423
|
+
- {name: pg_advisory_xact_lock, reason: "advisoryXactLock: a transaction-scoped lock TypeORM has no API for"}
|
|
424
|
+
- {name: hashtextextended, reason: "advisoryXactLock: the 64-bit key of a text lock name"}
|
|
425
|
+
- {name: pg_notify, reason: "notify: LISTEN/NOTIFY of the Postgres realtime hub"}
|
|
426
|
+
- {name: to_regproc, reason: "callDbFunction: whether an optional database function exists"}
|
|
427
|
+
- {name: job_claim, reason: "platform/jobs (add job, lite): the fenced claim upsert of jobs.sql"}
|
|
428
|
+
- {name: job_advance, reason: "platform/jobs (add job, lite): the token-guarded step write of jobs.sql"}
|
|
429
|
+
- {name: job_complete, reason: "platform/jobs (add job, lite): the token-guarded completion of jobs.sql"}
|
|
430
|
+
- {name: job_fail, reason: "platform/jobs (add job, lite): the token-guarded failure of jobs.sql"}
|
|
431
|
+
- {name: queue_write, reason: "platform/queue (add queue, lite): the outbox insert of queue-outbox.sql"}
|
|
432
|
+
- {name: queue_waiting, reason: "platform/queue (add queue, lite): the relay read FOR UPDATE SKIP LOCKED of queue-outbox.sql"}
|
|
433
|
+
- {name: queue_mark_sent, reason: "platform/queue (add queue, lite): the relay mark of queue-outbox.sql"}
|
|
434
|
+
- {name: inbox_claim_event, reason: "platform/inbox (add webhook, lite): the conflict-safe delivery claim"}
|
|
435
|
+
- {name: inbox_claim_client, reason: "platform/inbox (add webhook, lite): the client claim upsert returning its committed outcome"}
|
|
436
|
+
- {name: inbox_save_client_outcome, reason: "platform/inbox (add webhook, lite): the saved client outcome"}
|
|
437
|
+
- {name: inbox_sweep_clients, reason: "platform/inbox (add webhook, lite): the bounded retention sweep FOR UPDATE SKIP LOCKED"}
|
|
438
|
+
- {name: event_bus_write, reason: "platform/event-bus (add webhook, lite): the typed envelope insert of the outbox"}
|
|
439
|
+
|
|
407
440
|
lite:
|
|
408
|
-
schemaAuthority:
|
|
441
|
+
schemaAuthority: sql
|
|
409
442
|
infraOwners:
|
|
410
443
|
kafkajs: []
|
|
411
444
|
"@qdrant/js-client-rest": []
|
|
@@ -435,7 +468,7 @@ slots:
|
|
|
435
468
|
- id: app.package-manifest
|
|
436
469
|
since: 3.1.0
|
|
437
470
|
profiles: [app]
|
|
438
|
-
editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite manages
|
|
471
|
+
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
472
|
# The one package.json of the app, at the app root: be's dependencies and every tool, and every script; npm workspaces are
|
|
440
473
|
# exactly fe/apps/* and fe/packages/* (HFS_MONO_WORKSPACES), each with its own package.json. be has no package.json of its own.
|
|
441
474
|
path: package.json
|
|
@@ -504,7 +537,7 @@ slots:
|
|
|
504
537
|
- id: app.format-config
|
|
505
538
|
since: 3.1.0
|
|
506
539
|
profiles: [app]
|
|
507
|
-
editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite additionally ignores
|
|
540
|
+
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
541
|
# The one formatter configuration of the app (prettier walks up from every file of both sides to it): .prettierrc names
|
|
509
542
|
# @starci/prettier-config, .prettierignore is the template.
|
|
510
543
|
path: "{.prettierrc,.prettierignore}"
|
|
@@ -513,7 +546,7 @@ slots:
|
|
|
513
546
|
tier: none
|
|
514
547
|
tests: none
|
|
515
548
|
managedBy: tool-config
|
|
516
|
-
liteManagedBy: tool-config-lite # the same files
|
|
549
|
+
liteManagedBy: tool-config-lite # the same files as full
|
|
517
550
|
rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL]
|
|
518
551
|
- id: app.tool-config-optional
|
|
519
552
|
since: 3.1.0
|
|
@@ -556,7 +589,7 @@ slots:
|
|
|
556
589
|
- id: app.ci
|
|
557
590
|
since: 3.1.0
|
|
558
591
|
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
|
|
592
|
+
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
593
|
path: ".github/workflows/ci.yml"
|
|
561
594
|
presence: required
|
|
562
595
|
tracked: tracked
|
|
@@ -581,7 +614,7 @@ slots:
|
|
|
581
614
|
- id: app.ci-images
|
|
582
615
|
since: 3.1.0
|
|
583
616
|
profiles: [app]
|
|
584
|
-
editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite renders its
|
|
617
|
+
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
618
|
# Builds every image of the app (one matrix entry per declared app, path-filtered), never pushes: rendered by starci app sync.
|
|
586
619
|
path: ".github/workflows/images.yml"
|
|
587
620
|
presence: required
|
|
@@ -596,7 +629,7 @@ slots:
|
|
|
596
629
|
exemplifiedBy:
|
|
597
630
|
- packages/hfs/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml
|
|
598
631
|
- {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
|
|
632
|
+
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
633
|
# The database deploy workflow: workflow_dispatch only (like app.ci-e2e); starci app sync renders it under the managed ci group.
|
|
601
634
|
path: ".github/workflows/db-deploy.yml"
|
|
602
635
|
presence: optional
|
|
@@ -634,7 +667,7 @@ slots:
|
|
|
634
667
|
since: 3.1.0
|
|
635
668
|
profiles: [app]
|
|
636
669
|
# Production delivery tooling for the app's own processes: compose files, Caddyfile, *.env.example and ops shell scripts.
|
|
637
|
-
# Lite
|
|
670
|
+
# Lite keeps its encrypted custody in app.starcistacks; R06 scans every file here.
|
|
638
671
|
path: "deploy/"
|
|
639
672
|
presence: optional
|
|
640
673
|
tracked: tracked
|
|
@@ -718,7 +751,7 @@ slots:
|
|
|
718
751
|
profiles: [app]
|
|
719
752
|
exemplifiedBy:
|
|
720
753
|
- {path: packages/hfs/templates/app/skeleton/scripts/codegen.mjs, edition: full}
|
|
721
|
-
editionWhy: "Lite manages its
|
|
754
|
+
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
755
|
# The app's operational scripts only (`*.mjs`, `*.cjs`, `*.ps1`, `*.sh`), or nothing: an empty folder is `scripts/.gitkeep`, which
|
|
723
756
|
# `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
757
|
# 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 +836,7 @@ slots:
|
|
|
803
836
|
- id: app.starcistacks
|
|
804
837
|
since: 3.1.0
|
|
805
838
|
profiles: [app]
|
|
806
|
-
editionWhy: "owner:lite-production-swarm: lite requires declared docker-swarm production with
|
|
839
|
+
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
840
|
exemplifiedBy:
|
|
808
841
|
- {path: packages/hfs/templates/app/skeleton/.starcistacks/, edition: full}
|
|
809
842
|
- {path: packages/hfs/templates/app/skeleton-lite/.starcistacks/, edition: lite}
|
|
@@ -818,7 +851,7 @@ slots:
|
|
|
818
851
|
"<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
|
|
819
852
|
forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
|
|
820
853
|
# Owner ruling 2026-10-14: lite declares docker-swarm production, app runtime config, sealed custody and seed data.
|
|
821
|
-
#
|
|
854
|
+
# The db service supplies the database and broker (pgmq, pg_cron); production has no Compose or host cron.
|
|
822
855
|
lite:
|
|
823
856
|
requires: [application-stacks.yaml]
|
|
824
857
|
allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/swarm/**", "<env>/infra/metadata.json",
|
|
@@ -881,122 +914,78 @@ slots:
|
|
|
881
914
|
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
915
|
rules: [HFS_PLAINTEXT_SECRET]
|
|
883
916
|
|
|
884
|
-
# ----- app root: the
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
|
917
|
+
# ----- app root: the database of a lite app (ruling:lite-self-hosted-postgres-passport-minio): a self-hosted PostgreSQL in the app's
|
|
918
|
+
# stack (the `db` service, image supabase/postgres for pg_cron, pg_net, pgmq, vector, unaccent and pgcrypto), migrated by
|
|
919
|
+
# `starci app db migrate --url-env <NAME>`. A full app keeps its TypeORM migrations under be/ and has no such folder.
|
|
920
|
+
- id: app.db
|
|
901
921
|
profiles: [app]
|
|
922
|
+
editions: [lite]
|
|
902
923
|
exemplifiedBy:
|
|
903
|
-
- packages/hfs/templates/app/skeleton-lite/
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
path: "supabase/config.toml"
|
|
908
|
-
presence: opt-in
|
|
909
|
-
provider: supabase
|
|
910
|
-
litePresence: required
|
|
924
|
+
- packages/hfs/templates/app/skeleton-lite/db/
|
|
925
|
+
# The one database directory, at the app root beside be/, fe/ and .starcistacks (never under a side).
|
|
926
|
+
path: "db/"
|
|
927
|
+
presence: required
|
|
911
928
|
tracked: tracked
|
|
912
929
|
tier: none
|
|
913
930
|
tests: none
|
|
914
|
-
since:
|
|
915
|
-
- id: app.
|
|
931
|
+
since: 3.9.0
|
|
932
|
+
- id: app.db.migrations
|
|
916
933
|
profiles: [app]
|
|
934
|
+
editions: [lite]
|
|
917
935
|
exemplifiedBy:
|
|
918
|
-
- packages/hfs/templates/app/skeleton-lite/
|
|
919
|
-
- {path: packages/hfs/src/db-migration-shape.spec.mjs, edition:
|
|
920
|
-
|
|
921
|
-
#
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
provider: supabase
|
|
926
|
-
litePresence: required
|
|
927
|
-
lite: {minInstances: 1}
|
|
936
|
+
- packages/hfs/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql
|
|
937
|
+
- {path: packages/hfs/src/db-migration-shape.spec.mjs, edition: lite}
|
|
938
|
+
# The schema authority of a lite app: forward-only SQL migrations named <timestamp14>_<kebab-name>.sql, one file per change,
|
|
939
|
+
# immutable once on origin/main, applied in order by `starci app db migrate` and recorded in the history table.
|
|
940
|
+
path: "db/migrations/<stamp>_<name>.sql"
|
|
941
|
+
presence: required
|
|
942
|
+
minInstances: 1
|
|
928
943
|
tracked: tracked
|
|
929
944
|
tier: none
|
|
930
945
|
tests: none
|
|
931
|
-
since:
|
|
932
|
-
|
|
946
|
+
since: 3.9.0
|
|
947
|
+
rules: [DB_MIGRATION_SHAPE, LITE_DB_MIGRATION]
|
|
948
|
+
- id: app.db.migrations.archive
|
|
933
949
|
profiles: [app]
|
|
934
|
-
|
|
935
|
-
#
|
|
936
|
-
# older files move here.
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
presence: opt-in
|
|
940
|
-
provider: supabase
|
|
950
|
+
editions: [lite]
|
|
951
|
+
# The migrations an app retired at a baseline cutover: the live schema is dumped into ONE baseline migration, marked applied
|
|
952
|
+
# without a re-run, and the older files move here. `starci app db migrate` never applies this folder and no migration rule judges it.
|
|
953
|
+
path: "db/migrations/archive/"
|
|
954
|
+
presence: optional
|
|
941
955
|
tracked: tracked
|
|
942
956
|
tier: none
|
|
943
957
|
tests: none
|
|
944
|
-
since: 3.
|
|
945
|
-
- id: app.
|
|
958
|
+
since: 3.9.0
|
|
959
|
+
- id: app.db.seed
|
|
946
960
|
profiles: [app]
|
|
947
|
-
|
|
961
|
+
editions: [lite]
|
|
962
|
+
path: "db/seed.sql"
|
|
948
963
|
presence: optional # synthetic dev data only; a lite app seeds nothing it cannot regenerate
|
|
949
964
|
tracked: tracked
|
|
950
965
|
tier: none
|
|
951
966
|
tests: none
|
|
952
|
-
since:
|
|
953
|
-
- id: app.
|
|
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
|
|
967
|
+
since: 3.9.0
|
|
968
|
+
- id: app.db.bootstrap
|
|
970
969
|
profiles: [app]
|
|
971
|
-
|
|
972
|
-
|
|
970
|
+
editions: [lite]
|
|
971
|
+
# The managed pre-migration bootstrap (templates/app/db-bootstrap): the auth, storage and extensions schemas, auth.users with the
|
|
972
|
+
# GoTrue column set, auth.uid/role/jwt stubs, the NOLOGIN browser roles and the realtime publication, so migrations written for a
|
|
973
|
+
# hosted Supabase apply to the self-hosted db service. `starci app db migrate` applies it before the first migration of an empty database.
|
|
974
|
+
path: "db/bootstrap.sql"
|
|
975
|
+
presence: required
|
|
973
976
|
tracked: tracked
|
|
977
|
+
managedBy: db-bootstrap
|
|
974
978
|
tier: none
|
|
975
979
|
tests: none
|
|
976
|
-
since:
|
|
977
|
-
|
|
978
|
-
|
|
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
|
|
980
|
+
since: 3.9.0
|
|
981
|
+
rules: [HFS_MANAGED_FILE_DRIFT]
|
|
982
|
+
|
|
994
983
|
|
|
995
984
|
# ----- side root (both sides): what a standalone repository root holds, less the app-root files ----------
|
|
996
985
|
- id: repo.side-root-forbidden
|
|
997
986
|
since: 3.1.0
|
|
998
987
|
profiles: [be, fe]
|
|
999
|
-
path: "{package.json,package-lock.json,hfs.json,README.md,.gitignore,.gitattributes,.husky/,.github/,.starciwork/,.starcistacks/,
|
|
988
|
+
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
989
|
presence: forbidden
|
|
1001
990
|
tracked: external
|
|
1002
991
|
tier: none
|
|
@@ -1266,8 +1255,8 @@ slots:
|
|
|
1266
1255
|
- {path: packages/hfs/src/mcp-tools-snapshot.spec.mjs, editions: [lite, full]}
|
|
1267
1256
|
|
|
1268
1257
|
# ----- 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
|
|
1270
|
-
# BE_APP_COMPOSITION_BUDGET holds absolutely (not a ratchet):
|
|
1258
|
+
# The files of every app (owner ruling 2026-10-14, R245 and R246, since 3.1.0), each with a line budget (lines excluding
|
|
1259
|
+
# import declarations, multi-line imports included) that BE_APP_COMPOSITION_BUDGET holds absolutely (not a ratchet):
|
|
1271
1260
|
# main.ts the entry: builds EnvSource once, imports and calls read<App>Env from <app>.env.ts, builds options and boots.
|
|
1272
1261
|
# <app>.env.ts exports interface <App>Env and read<App>Env(env: EnvSource): <App>Env (for example ApiEnv/readApiEnv);
|
|
1273
1262
|
# fail-fast environment parsing only, via EnvSource and unchanged capability parse helpers; no localhost
|
|
@@ -1317,7 +1306,7 @@ slots:
|
|
|
1317
1306
|
- id: be.app.cli
|
|
1318
1307
|
since: 3.1.0
|
|
1319
1308
|
profiles: [be]
|
|
1320
|
-
editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4):
|
|
1309
|
+
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
1310
|
# The ONE app of every one-off action (migrate, seed, sync, backup, any operator command): be/apps/cli, on nest-commander.
|
|
1322
1311
|
# main.ts boots with CommandFactory.run (BE_CLI_BOOTSTRAP); app.module.ts composes the platform and the cli feature root
|
|
1323
1312
|
# (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 +1316,7 @@ slots:
|
|
|
1327
1316
|
path: "apps/<app>/src/"
|
|
1328
1317
|
appKind: cli
|
|
1329
1318
|
presence: opt-in
|
|
1330
|
-
litePresence: optional # lite: schema migration is
|
|
1319
|
+
litePresence: optional # lite: schema migration is `starci app db migrate`'s job; a cli app exists only when the app declares one
|
|
1331
1320
|
tracked: tracked
|
|
1332
1321
|
tier: app
|
|
1333
1322
|
owner: true
|
|
@@ -1678,7 +1667,7 @@ slots:
|
|
|
1678
1667
|
- id: be.persistence
|
|
1679
1668
|
since: 3.1.0
|
|
1680
1669
|
profiles: [be]
|
|
1681
|
-
editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4):
|
|
1670
|
+
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
1671
|
path: "src/modules/{domain,platform,projections}/<capability>/persistence/"
|
|
1683
1672
|
# 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
1673
|
# registered on several declared connections. Any other capability registers its arrays on exactly one (BE_SCHEMA_OWNER).
|
|
@@ -1689,15 +1678,17 @@ slots:
|
|
|
1689
1678
|
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
1679
|
allows: ["entities/<table>.entity.ts", "migrations/<epochMs13>-<kebab-name>.ts", "<name>.sql.ts", "<name>.rows.ts", connection.ts]
|
|
1691
1680
|
forbids: [index.ts, "<name>.repository.ts"]
|
|
1692
|
-
# Lite
|
|
1693
|
-
#
|
|
1681
|
+
# Lite maps its tables with the same TypeORM entities as full (ruling:lite-persistence-typeorm-entities): each capability's
|
|
1682
|
+
# persistence/entities/<table>.entity.ts and an entities/index.ts exporting its <c>Entities array, which platform/database
|
|
1683
|
+
# lists by name; the schema authority stays db/migrations (ruleParams.be.lite.schemaAuthority), so no be migration and no
|
|
1684
|
+
# raw SQL file (R334 BE_PERSISTENCE_NO_RAW_SQL).
|
|
1694
1685
|
lite:
|
|
1695
|
-
requires: []
|
|
1696
|
-
allows: ["
|
|
1697
|
-
forbids: [index.ts, "<name>.repository.ts",
|
|
1686
|
+
requires: []
|
|
1687
|
+
allows: ["entities/<table>.entity.ts", entities/index.ts, connection.ts]
|
|
1688
|
+
forbids: [index.ts, "<name>.repository.ts", migrations/, "<name>.sql.ts", "<name>.rows.ts"]
|
|
1698
1689
|
tests: none
|
|
1699
1690
|
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]
|
|
1691
|
+
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
1692
|
- id: be.platform
|
|
1702
1693
|
since: 3.1.0
|
|
1703
1694
|
profiles: [be]
|
|
@@ -1748,7 +1739,7 @@ slots:
|
|
|
1748
1739
|
owner: true
|
|
1749
1740
|
lite:
|
|
1750
1741
|
why: >-
|
|
1751
|
-
the same EventPublisher and EventConsumer ports on
|
|
1742
|
+
the same EventPublisher and EventConsumer ports on pgmq queues: the outbox relay sends each committed row to the
|
|
1752
1743
|
pgmq queue of every consumer group of its topic through the shared EntityManager, a consumer reads with a visibility
|
|
1753
1744
|
timeout, retries by letting it lapse and archives the message on dead letter (read_ct over the budget); a single api may
|
|
1754
1745
|
deliver in process. No Kafka (knowledge/patterns/be/event-bus.yaml liteAdapter)
|
|
@@ -1762,7 +1753,7 @@ slots:
|
|
|
1762
1753
|
- id: be.platform.queue
|
|
1763
1754
|
since: 3.1.0
|
|
1764
1755
|
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
|
|
1756
|
+
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
1757
|
path: "src/modules/platform/queue/"
|
|
1767
1758
|
presence: opt-in
|
|
1768
1759
|
pattern: queue # the ONE BullMQ owner: the outbox relay into queues (jobId = outbox id) and job schedulers (knowledge/patterns/be/queues.yaml)
|
|
@@ -1773,7 +1764,7 @@ slots:
|
|
|
1773
1764
|
why: >-
|
|
1774
1765
|
the same QueueOutbox and QueueWorkerRegistry ports and full's BullMQ adapter on the declared Redis (backing: [redis]): the
|
|
1775
1766
|
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
|
|
1767
|
+
job that exhausts its attempts as failed (the dead letter); the outbox table comes from a db migration, and a
|
|
1777
1768
|
schedule is a BullMQ job scheduler or a pg_cron tick that inserts one outbox row (knowledge/patterns/be/queues.yaml liteAdapter)
|
|
1778
1769
|
requires: [index.ts, queue.module.ts, queue.port.ts, queue.contracts.ts, queue-outbox.service.ts, queue-outbox.service.spec.ts]
|
|
1779
1770
|
tests: unit-beside
|
|
@@ -1867,7 +1858,7 @@ slots:
|
|
|
1867
1858
|
- id: be.platform.jobs
|
|
1868
1859
|
since: 3.1.0
|
|
1869
1860
|
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
|
|
1861
|
+
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
1862
|
path: "src/modules/platform/jobs/"
|
|
1872
1863
|
presence: opt-in
|
|
1873
1864
|
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 +1873,7 @@ slots:
|
|
|
1882
1873
|
- id: be.projections
|
|
1883
1874
|
since: 3.1.0
|
|
1884
1875
|
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
|
|
1876
|
+
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
1877
|
path: "src/modules/projections/<name>/"
|
|
1887
1878
|
presence: opt-in
|
|
1888
1879
|
pattern: projection # a read model: only its projection writes its table; recompute is idempotent and replayable; readers use get*
|
|
@@ -1891,7 +1882,7 @@ slots:
|
|
|
1891
1882
|
owner: true
|
|
1892
1883
|
requires: [index.ts, "<name>.module.ts", "<name>.projection.ts", "<name>.projection-entity.ts"]
|
|
1893
1884
|
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
|
|
1885
|
+
lite: # the read model's table is a db migration (lite's schema authority) mapped by its entity under persistence/entities/ (R334)
|
|
1895
1886
|
requires: [index.ts, "<name>.module.ts", "<name>.projection.ts"]
|
|
1896
1887
|
allows: [index.ts, "<name>.module.ts", "<name>.module-definition.ts", "<name>.options.ts", "<name>.projection.ts", "<name>.contracts.ts", persistence/]
|
|
1897
1888
|
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 +1915,44 @@ slots:
|
|
|
1924
1915
|
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
1916
|
rules: [BE_TIER_DIRECTION, BE_INFRA_OWNER, BE_INTEGRATION_SPEC_MISSING]
|
|
1926
1917
|
|
|
1927
|
-
- id: be.integrations.
|
|
1918
|
+
- id: be.integrations.passport
|
|
1928
1919
|
profiles: [be]
|
|
1920
|
+
editions: [lite]
|
|
1929
1921
|
exemplifiedBy:
|
|
1930
|
-
- packages/hfs/templates/be/skeleton-lite/src/modules/integrations/
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
#
|
|
1934
|
-
#
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1922
|
+
- packages/hfs/templates/be/skeleton-lite/src/modules/integrations/passport/
|
|
1923
|
+
# The one owner of the lite identity libraries (ruleParams.be.infraOwners: @nestjs/passport, passport, passport-google-oauth20,
|
|
1924
|
+
# passport-local, @nestjs/jwt, jose, bcryptjs): the Google OAuth 2.0 strategy (state and PKCE), the local email and password
|
|
1925
|
+
# strategy (bcrypt), the app's own asymmetric access tokens and their JWKS, and the rotating refresh cookie. domain/identity
|
|
1926
|
+
# verifies a token through this integration's verifier and builds the principal (contexts/code/be/api-auth.md).
|
|
1927
|
+
path: "src/modules/integrations/passport/"
|
|
1928
|
+
presence: required
|
|
1929
|
+
tracked: tracked
|
|
1930
|
+
tier: integrations
|
|
1931
|
+
owner: true
|
|
1932
|
+
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
|
|
1933
|
+
allows: [index.ts, passport.config.ts, passport.options.ts, passport.decorators.ts, passport.tokens.ts, passport.password.ts, passport.cookie.ts, strategies/, errors/]
|
|
1934
|
+
tests: unit-beside
|
|
1935
|
+
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]
|
|
1936
|
+
coverage: required
|
|
1937
|
+
since: 3.9.0
|
|
1938
|
+
- id: be.integrations.object-storage
|
|
1939
|
+
profiles: [be]
|
|
1940
|
+
exemplifiedBy:
|
|
1941
|
+
- packages/hfs/templates/be/skeleton-lite/src/modules/integrations/object-storage/
|
|
1942
|
+
# The one owner of the S3 client (@aws-sdk/client-s3 and @aws-sdk/s3-request-presigner) over S3-compatible storage (MinIO in
|
|
1943
|
+
# lite): the ObjectStorage port with presigned PUT and GET; no other module imports an S3 SDK or minio.
|
|
1944
|
+
path: "src/modules/integrations/object-storage/"
|
|
1945
|
+
presence: optional
|
|
1939
1946
|
litePresence: required
|
|
1940
1947
|
tracked: tracked
|
|
1941
1948
|
tier: integrations
|
|
1942
1949
|
owner: true
|
|
1943
|
-
requires: [index.ts,
|
|
1944
|
-
allows: [index.ts,
|
|
1950
|
+
requires: [index.ts, storage.config.ts, storage.options.ts, storage.decorators.ts, storage.client.ts]
|
|
1951
|
+
allows: [index.ts, storage.config.ts, storage.options.ts, storage.decorators.ts, storage.client.ts, errors/]
|
|
1945
1952
|
tests: unit-beside
|
|
1946
|
-
rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT]
|
|
1953
|
+
rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT, BE_INFRA_OWNER, BE_STORAGE_OWNER]
|
|
1947
1954
|
coverage: required
|
|
1948
|
-
since:
|
|
1955
|
+
since: 3.9.0
|
|
1949
1956
|
|
|
1950
1957
|
# ----- BE: tests (owner test layout 2026-09-30). Unit specs are <name>.service.spec.ts beside their service and nothing else; everything else
|
|
1951
1958
|
# lives under src/tests/, one folder per kind, and the folder and the file suffix always agree (a file whose suffix does
|
|
@@ -1953,15 +1960,15 @@ slots:
|
|
|
1953
1960
|
- id: be.tests.world
|
|
1954
1961
|
since: 3.1.0
|
|
1955
1962
|
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
|
|
1963
|
+
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
1964
|
path: "src/tests/world/"
|
|
1958
1965
|
presence: optional
|
|
1959
1966
|
tracked: tracked
|
|
1960
1967
|
tier: e2e
|
|
1961
1968
|
lite:
|
|
1962
1969
|
why: >-
|
|
1963
|
-
the world of the optional integration specs of a lite app: global-setup.ts connects to the local
|
|
1964
|
-
(`
|
|
1970
|
+
the world of the optional integration specs of a lite app: global-setup.ts connects to the local db service
|
|
1971
|
+
(`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
1972
|
own; use-test-world.ts exports useTestWorld({ modules: [...] }) -> world.db.<connection> (the shared EntityManager) and
|
|
1966
1973
|
world.fake.<provider> (a network-edge fake of an external SaaS). There is no world.apps (no e2e) and no world.infra
|
|
1967
1974
|
(no self-hosted service to cut)
|
|
@@ -2030,7 +2037,7 @@ slots:
|
|
|
2030
2037
|
- id: be.tests.integration
|
|
2031
2038
|
since: 3.1.0
|
|
2032
2039
|
profiles: [be]
|
|
2033
|
-
editionWhy: "Lite integration specs use the local
|
|
2040
|
+
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
2041
|
path: "src/tests/integration/<capability>/*.integration-spec.ts"
|
|
2035
2042
|
presence: optional
|
|
2036
2043
|
tracked: tracked
|
|
@@ -2039,8 +2046,8 @@ slots:
|
|
|
2039
2046
|
coverage: none
|
|
2040
2047
|
lite:
|
|
2041
2048
|
why: >-
|
|
2042
|
-
optional under lite: one capability module through useTestWorld({ modules }) against the local
|
|
2043
|
-
(`
|
|
2049
|
+
optional under lite: one capability module through useTestWorld({ modules }) against the local db service
|
|
2050
|
+
(`npm run db:start`), run by test:integration; no container, no toxiproxy and no outage through the world (R112 is full's)
|
|
2044
2051
|
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
2052
|
rules: [BE_TEST_TOPOLOGY, BE_INTEGRATION_SPEC_MISSING]
|
|
2046
2053
|
- id: be.tests.e2e
|
|
@@ -2053,7 +2060,7 @@ slots:
|
|
|
2053
2060
|
tracked: tracked
|
|
2054
2061
|
tier: e2e
|
|
2055
2062
|
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 `
|
|
2063
|
+
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
2064
|
coverage: none
|
|
2058
2065
|
why: flow e2e through useTestWorld({ apps }); external services are the world's network fakes; run by test:e2e
|
|
2059
2066
|
rules: [HFS_CI_E2E_AUTOMATIC, BE_TEST_TOPOLOGY, BE_E2E_FLOW]
|
|
@@ -2112,7 +2119,7 @@ slots:
|
|
|
2112
2119
|
tracked: tracked
|
|
2113
2120
|
tier: route
|
|
2114
2121
|
# 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
|
|
2122
|
+
# route handler of this slot, the OAuth callback is a back-end door, and no file holds a Server Action
|
|
2116
2123
|
# (FE_ROUTE_HANDLER_FORBIDDEN, FE_SERVER_ACTION_FORBIDDEN).
|
|
2117
2124
|
allows: ["[locale]/**/{page,layout,template,loading,error,not-found,default}.tsx", "[locale]/providers.tsx",
|
|
2118
2125
|
global-error.tsx, page.tsx, # the root page.tsx only redirects (FE_ROUTE_FILES_THIN)
|
|
@@ -2143,20 +2150,6 @@ slots:
|
|
|
2143
2150
|
tests: none
|
|
2144
2151
|
goesTo: "apps/<app>/src/proxy.ts: Next >= 16 reads proxy.ts and refuses middleware.ts"
|
|
2145
2152
|
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
2153
|
- id: fe.feature
|
|
2161
2154
|
since: 3.1.0
|
|
2162
2155
|
profiles: [fe]
|
|
@@ -2488,18 +2481,17 @@ slots:
|
|
|
2488
2481
|
- id: fe.modules.auth
|
|
2489
2482
|
profiles: [fe]
|
|
2490
2483
|
# 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.
|
|
2492
|
-
#
|
|
2484
|
+
# 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
|
|
2485
|
+
# the back end's httpOnly cookie and the access token in memory (ruling:lite-self-hosted-postgres-passport-minio).
|
|
2493
2486
|
path: "apps/<app>/src/modules/auth/"
|
|
2494
2487
|
presence: optional
|
|
2495
2488
|
tracked: tracked
|
|
2496
2489
|
tier: modules
|
|
2497
2490
|
owner: true
|
|
2498
2491
|
requires: [index.ts]
|
|
2499
|
-
entries: [server.ts] # the server-only PKCE exchange API used by the opted-in callback route
|
|
2500
2492
|
tests: none
|
|
2501
2493
|
since: 3.0.0
|
|
2502
|
-
rules: [
|
|
2494
|
+
rules: [FE_STACK_FORBIDDEN]
|
|
2503
2495
|
- id: fe.modules.toast
|
|
2504
2496
|
profiles: [fe]
|
|
2505
2497
|
# 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 `
|
|
9
|
-
const
|
|
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
|
|
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 ['
|
|
14
|
+
if (!isPlainObject(block)) return ['db must be an object'];
|
|
23
15
|
const bad = [];
|
|
24
|
-
for (const key of Object.keys(block)) if (!['
|
|
25
|
-
|
|
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', '
|
|
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(...
|
|
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
|
|
19
|
-
const CONNECTION_PROVIDERS = Object.freeze(["postgres"
|
|
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
|
|
23
|
-
export const SCHEMA_AUTHORITIES = Object.freeze(["typeorm", "
|
|
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`.
|
|
@@ -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 (
|
|
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
|
-
|
|
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
|
|
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;
|