@starci/eslint-grammar-core 0.15.8 → 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.
@@ -168,7 +168,7 @@ rules:
168
168
  code: "HFS_STACKS_SHAPE"
169
169
  editions: [lite, full]
170
170
  docs: [docs/legacy-tree-rule-coverage.md#r10]
171
- law: "`.starcistacks` has the edition-aware standard shape and the host Sonar owner. STACK-DEV keeps every dependency in Docker Linux containers in development (local Supabase through its CLI); application processes follow their declared development profile. Production runs the app and dependencies on Docker Swarm except hosted Supabase, the only admitted managed backing provider (owner:dev-docker-prod-swarm). Swarm stacks have digest-pinned images, healthchecks, rollout, rollback and restart policies, versioned external secrets, ingress-only published ports and declared schedulers for replicated jobs. Recurring work is app-owned; host crontab installation is forbidden. Lite declares Supabase as managed backing and runs no duplicate backing-service component."
171
+ law: "`.starcistacks` has the edition-aware standard shape and the host Sonar owner. STACK-DEV keeps every dependency in Docker Linux containers in development (the db service among them); application processes follow their declared development profile. Production runs the app and dependencies, its db service included, on Docker Swarm (owner:dev-docker-prod-swarm). Swarm stacks have digest-pinned images, healthchecks, rollout, rollback and restart policies, versioned external secrets, ingress-only published ports and declared schedulers for replicated jobs. Recurring work is app-owned; host crontab installation is forbidden. Lite declares its db service as backing (backing.db) and runs no other backing-service component."
172
172
  kinds: [check, codemod]
173
173
  gates: [settle, land, ci]
174
174
  failureCodes: ["HFS_STACKS_SHAPE"]
@@ -640,7 +640,7 @@ rules:
640
640
  - {path: packages/eslint/be/a16-r34-schema-authority.spec.mjs, editions: [lite, full], codes: [BE_SCHEMA_AUTHORITY]}
641
641
  - id: R35
642
642
  code: "BE_SCHEMA_OWNER"
643
- law: "Entities and migrations live in the owning capability's `persistence/`; its `<c>Entities` and `<c>Migrations` are registered under exactly one declared connection, which every `Inject<Conn>EntityManager` of the capability names (no `CONNECTION` alias); only a platform capability the manifest lists as `perConnection` (the event-bus outbox, the job table) registers on several."
643
+ law: "Entities and migrations live in the owning capability's `persistence/`; its `<c>Entities` and `<c>Migrations` are registered under exactly one declared connection, which every `Inject<Conn>EntityManager` of the capability names (no `CONNECTION` alias); only a platform capability the manifest lists as `perConnection` (the event-bus outbox, the job table) registers on several. In lite a capability's `persistence/entities/index.ts` declares its `<c>Entities` and `platform/database/entities.ts` names it in `PRIMARY_ENTITIES`, the one list the app composition registers."
644
644
  kinds: [lint, codemod, design]
645
645
  gates: [settle, land, ci]
646
646
  failureCodes: ["BE_SCHEMA_OWNER"]
@@ -665,7 +665,7 @@ rules:
665
665
  enforcers:
666
666
  - {kind: eslint-be, id: sql-text-only}
667
667
  - {kind: eslint-be, id: no-query-builder}
668
- editions: [lite, full]
668
+ editions: [full]
669
669
  docs: ["docs/architecture.md#r36"]
670
670
  proves:
671
671
  - path: packages/eslint/be/r36-sql-boundary.spec.mjs
@@ -722,7 +722,7 @@ rules:
722
722
  proves:
723
723
  - {path: packages/eslint/be/a16-error-home.spec.mjs, editions: [lite, full]}
724
724
  exemplifiedBy:
725
- - {path: packages/hfs/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts, edition: lite}
725
+ - {path: packages/hfs/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts, edition: lite}
726
726
  - {path: packages/hfs/templates/be/skeleton/src/modules/domain/identity/errors/identity.error.ts, edition: full}
727
727
  - {path: packages/eslint/be/a16-error-home.spec.mjs, editions: [lite, full]}
728
728
  witness:
@@ -1820,7 +1820,7 @@ rules:
1820
1820
  - {path: packages/eslint/be/be-r89-source-form-coverage.spec.mjs, edition: full, codes: [BE_SOURCE_FORM, BE_PUBLIC_CONTRACT_FORM]}
1821
1821
  - id: R90
1822
1822
  code: "BE_INFRA_OWNER"
1823
- law: "Each raw infrastructure library (HTTP, cache, queue, scheduler, logger, date, config, events) is imported or referenced only by its one owning platform or integration capability, and the `HttpClient` port of `platform/http` is called only by an `integrations` capability."
1823
+ law: "Each raw infrastructure library (HTTP, cache, queue, scheduler, logger, date, config, events, model SDKs) is imported or referenced only by its one owning platform or integration capability, and the `HttpClient` port of `platform/http` is called only by an `integrations` capability."
1824
1824
  kinds: [lint, codemod]
1825
1825
  failureCodes: ["BE_INFRA_OWNER"]
1826
1826
  enforcers:
@@ -3619,7 +3619,7 @@ rules:
3619
3619
  docs: [docs/lite-edition.md#edition-validation]
3620
3620
  templateNa: "The hfs.json edition declaration is generated programmatically by packages/hfs/scaffold/app.mjs; no managed declaration template exists. Forbidden paths are rejected rather than scaffolded."
3621
3621
  code: "HFS_EDITION_INVALID"
3622
- law: "hfs.json names an edition the manifest knows (full, or lite; absent means full). Lite is a minimal production (case-10): the same monorepo, code structure, slots, patterns and back-end unit specs, one stateless instance type that scales horizontally, managed Supabase for Postgres, Auth and Storage, and one minimal instance of each backing service a declared capability needs (hfs.json sides.be.backing: Redis for the queue, with AOF persistence); vector search uses Supabase pgvector (owner:lite-vector-is-pgvector, which excludes Qdrant from lite); a backing service is declared or absent, never on by default. The queue is BullMQ on Redis in both editions, fed by the Postgres outbox; pg_cron ticks enqueue through that outbox; the event bus keeps its lite adapter (pgmq-backed topics or in-process delivery) because full's event bus is Kafka, not Redis; cache and locks are Postgres unless Redis is declared; the api and sidecars run on docker-swarm on a VPS. The realtime pattern serves SSE (the default for one-way push), the same socket.io gateway or the restricted GraphQL subscription inside the be api process (scales horizontally and stateless: the memory hub is for a single replica or tests only, and replicas > 1 use the Redis hub and the sharded @socket.io/redis-adapter when Redis is declared, else the Postgres hub and @socket.io/postgres-adapter; both shared socket adapters force websocket-only connections with no sticky sessions; R212 case-9, which amends the realtime clauses of case-2, case-3 and case-5; the GraphQL subscription door is R212 case-7 and owner:lite-graphql-subscription-door), and both editions share knowledge/hfs/fe-stack.yaml. Lite requires application-stacks.yaml and permits environment metadata, runbooks, infra/swarm and encrypted-secret custody, while refusing infra/compose, infra/k8s, infra/terraform and components duplicating Supabase backing services (runtime/{config,env}, seeds and the sealed secrets of its environments are allowed). Contract specs and `test:contract` are allowed (owner 2026-10-14); it has no e2e spec, `test:e2e`, `test:stack` or `test:browser` script, e2e workflow, browser journey, Work catalog, shell, features, resources, UAT or br records, test container, toxiproxy, browser-runner or Kafka dependency, client library of an undeclared backing service (HFS_BACKING_UNDECLARED), declared backing service without a consumer (HFS_BACKING_UNUSED), or supabase/functions. Its .starciwork is optional and admits only .gitignore, workspace.yaml and brand/**; workspace.yaml is optional and carries project identity and repo roles for the Work tools."
3622
+ law: "hfs.json names an edition the manifest knows (full, or lite; absent means full). Lite is a minimal production (case-10): the same monorepo, code structure, slots, patterns and back-end unit specs, one stateless instance type that scales horizontally, its own PostgreSQL db service, Passport identity and S3-compatible storage (ruling:lite-self-hosted-postgres-passport-minio), and one minimal instance of each backing service a declared capability needs (hfs.json sides.be.backing: Redis for the queue, with AOF persistence); vector search uses pgvector in the db service (owner:lite-vector-is-pgvector, which excludes Qdrant from lite); a backing service is declared or absent, never on by default. The queue is BullMQ on Redis in both editions, fed by the Postgres outbox; pg_cron ticks enqueue through that outbox; the event bus keeps its lite adapter (pgmq-backed topics or in-process delivery) because full's event bus is Kafka, not Redis; cache and locks are Postgres unless Redis is declared; the api and sidecars run on docker-swarm on a VPS. The realtime pattern serves SSE (the default for one-way push), the same socket.io gateway or the restricted GraphQL subscription inside the be api process (scales horizontally and stateless: the memory hub is for a single replica or tests only, and replicas > 1 use the Redis hub and the sharded @socket.io/redis-adapter when Redis is declared, else the Postgres hub and @socket.io/postgres-adapter; both shared socket adapters force websocket-only connections with no sticky sessions; R212 case-9, which amends the realtime clauses of case-2, case-3 and case-5; the GraphQL subscription door is R212 case-7 and owner:lite-graphql-subscription-door), and both editions share knowledge/hfs/fe-stack.yaml. Lite requires application-stacks.yaml and permits environment metadata, runbooks, infra/swarm and encrypted-secret custody, while refusing infra/compose, infra/k8s, infra/terraform and backing components other than its db service (runtime/{config,env}, seeds and the sealed secrets of its environments are allowed). Contract specs and `test:contract` are allowed (owner 2026-10-14); it has no e2e spec, `test:e2e`, `test:stack` or `test:browser` script, e2e workflow, browser journey, Work catalog, shell, features, resources, UAT or br records, test container, toxiproxy, browser-runner or Kafka dependency, client library of an undeclared backing service (HFS_BACKING_UNDECLARED), declared backing service without a consumer (HFS_BACKING_UNUSED), or a supabase/ tree (LITE_DB_MIGRATION). Its .starciwork is optional and admits only .gitignore, workspace.yaml and brand/**; workspace.yaml is optional and carries project identity and repo roles for the Work tools."
3623
3623
  kinds: [check]
3624
3624
  gates: [settle, land, ci]
3625
3625
  failureCodes: ["HFS_EDITION_INVALID", "HFS_EDITION_FORBIDDEN_PRESENT", "HFS_STARCIWORK_LITE_FORBIDDEN", "HFS_BACKING_UNDECLARED", "HFS_BACKING_UNUSED"]
@@ -3678,7 +3678,7 @@ rules:
3678
3678
  date: "2026-10-24"
3679
3679
  source: owner
3680
3680
  quote: "Lite still has Redis, Qdrant and the like!!! That is, it deploys a minimal production. Lite is one instance that scales horizontally and is stateless! Full's infrastructure is more complex. Lite cannot go up to full!!! Lite is lite, full is full. The queue is Redis!!! (translated from Vietnamese; the owner approved the ruling table with \"ok, fix all of it, that is right\")"
3681
- ruling: "This case replaces the clauses of case-1, case-2 and case-3 that ban backing services in lite and the clause of case-7 that keeps Redis out of the realtime hub of lite (no BullMQ or Redis, one instance with no Redis adapter, the full architecture minus self-hosted infrastructure, the queue on pgmq). Lite is a minimal production: one instance type (one api, an optional worker) that is stateless and scales horizontally, managed Supabase for Postgres, Auth and Storage, and one minimal instance of each backing service a declared capability needs, deployed compactly on docker-swarm; full is several services each with its own database plus Redis, Kafka/CDC, its own IdP, object storage and observability on a multi-node stack. A backing service is declared or absent: hfs.json sides.be.backing names redis in lite (redis or qdrant in full), nothing is on by default; owner:lite-vector-is-pgvector narrows the lite vector store to Supabase pgvector and refuses Qdrant even when declared, the lite stack renders only declared services (STACKS_LITE_SELFHOSTED_DB), a client library of an undeclared service is refused (HFS_BACKING_UNDECLARED) and a declared service without a consumer is refused (HFS_BACKING_UNUSED). The queue is BullMQ on Redis in both editions behind the same platform/queue ports (outbox in Postgres, relay to BullMQ, processors, retry with exponential backoff, failed jobs kept as the dead letter); pgmq is not a queue transport, a declared queue or fenced-job pattern requires Redis, and a pg_cron tick enqueues through the queue outbox. The lite Redis runs with AOF persistence (appendonly yes) on a named volume because queue jobs must not be lost (STACKS_LITE_REDIS_PERSISTENCE); cache, rate limit and realtime pub/sub may share that instance, which keeps maxmemory-policy noeviction so no queue job is evicted. Durable data and the outbox stay in Postgres. MongoDB stays out of lite. Editions are fixed at scaffold (case-6)."
3681
+ ruling: "This case replaces the clauses of case-1, case-2 and case-3 that ban backing services in lite and the clause of case-7 that keeps Redis out of the realtime hub of lite (no BullMQ or Redis, one instance with no Redis adapter, the full architecture minus self-hosted infrastructure, the queue on pgmq). Lite is a minimal production: one instance type (one api, an optional worker) that is stateless and scales horizontally, its own PostgreSQL db service, Passport identity and S3-compatible storage (ruling:lite-self-hosted-postgres-passport-minio), and one minimal instance of each backing service a declared capability needs, deployed compactly on docker-swarm; full is several services each with its own database plus Redis, Kafka/CDC, its own IdP, object storage and observability on a multi-node stack. A backing service is declared or absent: hfs.json sides.be.backing names redis in lite (redis or qdrant in full), nothing is on by default; owner:lite-vector-is-pgvector narrows the lite vector store to Supabase pgvector and refuses Qdrant even when declared, the lite stack renders only declared services (STACKS_LITE_SELFHOSTED_DB), a client library of an undeclared service is refused (HFS_BACKING_UNDECLARED) and a declared service without a consumer is refused (HFS_BACKING_UNUSED). The queue is BullMQ on Redis in both editions behind the same platform/queue ports (outbox in Postgres, relay to BullMQ, processors, retry with exponential backoff, failed jobs kept as the dead letter); pgmq is not a queue transport, a declared queue or fenced-job pattern requires Redis, and a pg_cron tick enqueues through the queue outbox. The lite Redis runs with AOF persistence (appendonly yes) on a named volume because queue jobs must not be lost (STACKS_LITE_REDIS_PERSISTENCE); cache, rate limit and realtime pub/sub may share that instance, which keeps maxmemory-policy noeviction so no queue job is evicted. Durable data and the outbox stay in Postgres. MongoDB stays out of lite. Editions are fixed at scaffold (case-6)."
3682
3682
  proves: [packages/hfs/src/hfs-edition-invalid.spec.mjs, packages/hfs/src/hfs-edition-forbidden-present.spec.mjs, packages/hfs/src/hfs-starciwork-lite-forbidden.spec.mjs, packages/hfs/src/hfs-backing-declared.spec.mjs]
3683
3683
  exemplifiedBy:
3684
3684
  - {path: packages/hfs/src/hfs-edition-invalid.spec.mjs, editions: [lite, full]}
@@ -3694,8 +3694,8 @@ rules:
3694
3694
  code: "DB_MIGRATION_SHAPE"
3695
3695
  editions: [lite, full]
3696
3696
  docs: [docs/architecture.md#r213]
3697
- templateNa: "Full apps opt into Supabase without a managed Supabase migration scaffold; their migration sequence and Git history are product-authored."
3698
- law: "A migration is `supabase/migrations/<ts14>_<kebab>.sql`; the stamp is a valid UTC instant, active stamps are unique, and a new migration sorts strictly after the committed active migrations in HEAD. Repository history includes the archive: moving an active migration to the archive retires it from ordering, so a replacement baseline may predate archived stamps and later migrations append after the baseline. Wall-clock time, commit time and remote-branch stamps do not set the ordering boundary. A migration already on the base is immutable (a diff or an unarchived removal is a finding); a file that does not parse as PostgreSQL is a shape failure."
3697
+ templateNa: "Full apps keep TypeORM migrations under be/; the managed db/migrations scaffold is lite's."
3698
+ law: "A migration is `db/migrations/<ts14>_<kebab>.sql`; the stamp is a valid UTC instant, active stamps are unique, and a new migration sorts strictly after the committed active migrations in HEAD. Repository history includes the archive: moving an active migration to the archive retires it from ordering, so a replacement baseline may predate archived stamps and later migrations append after the baseline. Wall-clock time, commit time and remote-branch stamps do not set the ordering boundary. A migration already on the base is immutable (a diff or an unarchived removal is a finding); a file that does not parse as PostgreSQL is a shape failure."
3699
3699
  kinds: [check]
3700
3700
  gates: [settle, land, ci]
3701
3701
  failureCodes: ["DB_MIGRATION_SHAPE"]
@@ -3704,7 +3704,7 @@ rules:
3704
3704
  proves:
3705
3705
  - {path: packages/hfs/src/db-migration-shape.spec.mjs, editions: [lite, full]}
3706
3706
  exemplifiedBy:
3707
- - {path: packages/hfs/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql, edition: lite}
3707
+ - {path: packages/hfs/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql, edition: lite}
3708
3708
  - {path: packages/hfs/src/db-migration-shape.spec.mjs, editions: [lite, full]}
3709
3709
  witness:
3710
3710
  - {path: packages/hfs/src/db-migration-shape.spec.mjs, editions: [lite, full], codes: [DB_MIGRATION_SHAPE]}
@@ -3712,7 +3712,7 @@ rules:
3712
3712
  code: "DB_RLS_REQUIRED"
3713
3713
  editions: [lite, full]
3714
3714
  docs: [docs/lite-edition.md#r214]
3715
- law: "Row level security is code a parser can read: every `create table` (or `create table as`) in an exposed schema (`public` plus the config.toml `[api] schemas`) is followed, in the same migration, by `alter table <t> enable row level security` (and `force` for a table hfs.json `supabase.forceRls` declares); no `DO` block or function runs dynamic SQL that builds DDL or grants; a write or `for all` policy never uses `(true)`, `to anon`/`to public` is legal only on a `for select` policy named `*_public_read`, every policy is named `<table>_<role>_<action>`, and `service_role` is never granted."
3715
+ law: "Row level security is code a parser can read: every `create table` (or `create table as`) in the app schema `public` is followed, in the same migration, by `alter table <t> enable row level security` (and `force` for a table hfs.json `db.forceRls` declares); no `DO` block or function runs dynamic SQL that builds DDL or grants; a write or `for all` policy never uses `(true)`, `to anon`/`to public` is legal only on a `for select` policy named `*_public_read`, every policy is named `<table>_<role>_<action>`, and `service_role` is never granted."
3716
3716
  kinds: [check, codemod]
3717
3717
  gates: [settle, land, ci]
3718
3718
  failureCodes: ["DB_RLS_REQUIRED", "DB_DYNAMIC_DDL", "DB_POLICY_SHAPE"]
@@ -3756,98 +3756,24 @@ rules:
3756
3756
  - {path: packages/hfs/src/db-storage-policy.spec.mjs, edition: lite, codes: [DB_STORAGE_POLICY]}
3757
3757
  - {path: packages/hfs/src/db-definer-safe.spec.mjs, edition: full, codes: [DB_DEFINER_SAFE]}
3758
3758
  - {path: packages/hfs/src/db-storage-policy.spec.mjs, edition: full, codes: [DB_STORAGE_POLICY]}
3759
- - id: R216
3760
- code: "DB_CONFIG_POLICY"
3761
- editions: [lite, full]
3762
- docs: [docs/lite-edition.md#r216]
3763
- templateNa: "Database types are generated from each app migration set, not static templates; full-edition Supabase configuration is app-owned."
3764
- law: "The database configuration and its types are checked facts: `supabase/config.toml` carries no literal value under a credential key (the value is `env(NAME)`), `auth.jwt_expiry` is at most 3600, and signup, site url and redirect urls equal the Supabase block of hfs.json where it declares them; `supabase/types/database.types.ts` is committed and equals the text `contract:emit` regenerates from the migrations. Type emission refuses HFS_EMIT_DB_TYPES_STALE unless the complete applied migration version set equals the SQL filename stamps; lint and check report drift without writing types."
3765
- kinds: [check]
3766
- gates: [land, ci]
3767
- failureCodes: ["DB_CONFIG_POLICY", "DB_TYPES_DRIFT", "HFS_EMIT_DB_TYPES_STALE"]
3768
- enforcers:
3769
- - {kind: hfs, id: db-config-types, at: scripts/hfs/rules/database.mjs}
3770
- - {kind: hfs, id: db-types-emit, at: packages/hfs/emit/db-types.mjs}
3771
- proves:
3772
- - {path: packages/hfs/src/db-types-returned-collector.spec.mjs, editions: [lite, full], codes: [DB_TYPES_DRIFT]}
3773
- - {path: packages/hfs/src/db-config-policy.spec.mjs, editions: [lite, full], codes: [DB_CONFIG_POLICY]}
3774
- - {path: packages/hfs/src/db-types-emit-stale.spec.mjs, editions: [lite, full], codes: [HFS_EMIT_DB_TYPES_STALE]}
3775
- exemplifiedBy:
3776
- - {path: packages/hfs/templates/app/skeleton-lite/supabase/config.toml, edition: lite}
3777
- - {path: packages/hfs/src/db-types-returned-collector.spec.mjs, editions: [lite, full]}
3778
- - {path: packages/hfs/src/db-config-policy.spec.mjs, editions: [lite, full]}
3779
- - {path: packages/hfs/src/db-types-emit-stale.spec.mjs, editions: [lite, full], codes: [HFS_EMIT_DB_TYPES_STALE]}
3780
- witness:
3781
- - {path: packages/hfs/src/db-types-returned-collector.spec.mjs, edition: lite, codes: [DB_TYPES_DRIFT]}
3782
- - {path: packages/hfs/src/db-types-returned-collector.spec.mjs, edition: full, codes: [DB_TYPES_DRIFT]}
3783
- - {path: packages/hfs/src/db-config-policy.spec.mjs, edition: lite, codes: [DB_CONFIG_POLICY]}
3784
- - {path: packages/hfs/src/db-config-policy.spec.mjs, edition: full, codes: [DB_CONFIG_POLICY]}
3785
- - {path: packages/hfs/src/db-types-emit-stale.spec.mjs, editions: [lite, full], codes: [HFS_EMIT_DB_TYPES_STALE]}
3786
- - id: R217
3787
- code: "FE_SUPABASE_CLIENT_OWNER"
3788
- editions: [lite, full]
3789
- docs: [docs/lite-edition.md#r217]
3790
- law: "Supabase has one door per side: on the front end `@supabase/supabase-js` and `@supabase/ssr` are imported only by `fe.modules.auth` and only their auth API is called (sign-in, sign-out, session, access token; never `.from`, `.rpc`, `.storage` or `.channel`, so the front end reads and writes no data through Supabase), and on the back end only `be.integrations.supabase` imports them, every back-end client carrying the committed `Database` type; the front end holds no credential: `fe/` names no service-role or `*_SECRET`, `*_PRIVATE`, `*_TOKEN`, `*_PASSWORD` value (public configuration is read once by `modules/config`)."
3791
- kinds: [check, design]
3792
- gates: [settle, land, ci]
3793
- failureCodes: ["FE_SUPABASE_CLIENT_OWNER", "BE_SUPABASE_CLIENT_OWNER", "FE_SERVICE_ROLE_FORBIDDEN"]
3794
- enforcers:
3795
- - {kind: machine, id: supabase-client-owner, at: scripts/hfs/architecture/supabase.mjs}
3796
- - {kind: machine, id: supabase-service-role, at: scripts/hfs/architecture/supabase.mjs}
3797
- proves:
3798
- - {path: packages/hfs/src/fe-supabase-client-owner.spec.mjs, editions: [lite, full], codes: [FE_SUPABASE_CLIENT_OWNER]}
3799
- - {path: packages/hfs/src/r217-backend-client-owner.spec.mjs, editions: [lite, full], codes: [BE_SUPABASE_CLIENT_OWNER]}
3800
- - {path: packages/hfs/src/fe-service-role-forbidden.spec.mjs, editions: [lite, full], codes: [FE_SERVICE_ROLE_FORBIDDEN]}
3801
- exemplifiedBy:
3802
- - {path: packages/hfs/templates/fe/auth/supabase-client.ts.tpl, editions: [lite, full]}
3803
- - {path: packages/hfs/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts, edition: lite}
3804
- - {path: packages/hfs/src/fe-supabase-client-owner.spec.mjs, editions: [lite, full]}
3805
- - {path: packages/hfs/src/r217-backend-client-owner.spec.mjs, editions: [lite, full]}
3806
- - {path: packages/hfs/src/fe-service-role-forbidden.spec.mjs, editions: [lite, full]}
3807
- witness:
3808
- - {path: packages/hfs/src/fe-supabase-client-owner.spec.mjs, edition: lite, codes: [FE_SUPABASE_CLIENT_OWNER]}
3809
- - {path: packages/hfs/src/r217-backend-client-owner.spec.mjs, edition: lite, codes: [BE_SUPABASE_CLIENT_OWNER]}
3810
- - {path: packages/hfs/src/fe-service-role-forbidden.spec.mjs, edition: lite, codes: [FE_SERVICE_ROLE_FORBIDDEN]}
3811
- - {path: packages/hfs/src/fe-supabase-client-owner.spec.mjs, edition: full, codes: [FE_SUPABASE_CLIENT_OWNER]}
3812
- - {path: packages/hfs/src/r217-backend-client-owner.spec.mjs, edition: full, codes: [BE_SUPABASE_CLIENT_OWNER]}
3813
- - {path: packages/hfs/src/fe-service-role-forbidden.spec.mjs, edition: full, codes: [FE_SERVICE_ROLE_FORBIDDEN]}
3814
- - id: R219
3815
- code: "BE_SUPABASE_JWT_VERIFIED"
3816
- editions: [lite, full]
3817
- docs: [docs/lite-edition.md#r219]
3818
- templateNa: "Full-edition Supabase verification is an optional integration with no managed verifier scaffold; lite has the referenced integration template."
3819
- law: "The principal is verified, never trusted: a back-end principal comes only from a Supabase access token verified against the project JWKS (signature, issuer, audience, expiry, pinned algorithm), never decoded unverified and never from the request body."
3820
- kinds: [check, design]
3821
- gates: [settle, land, ci]
3822
- failureCodes: ["BE_SUPABASE_JWT_VERIFIED"]
3823
- enforcers:
3824
- - {kind: machine, id: supabase-principal, at: scripts/hfs/architecture/supabase.mjs}
3825
- proves:
3826
- - {path: packages/hfs/src/be-supabase-jwt-verified.spec.mjs, editions: [lite, full]}
3827
- exemplifiedBy:
3828
- - {path: packages/hfs/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts, edition: lite}
3829
- - {path: packages/hfs/src/be-supabase-jwt-verified.spec.mjs, editions: [lite, full]}
3830
- witness:
3831
- - {path: packages/hfs/src/be-supabase-jwt-verified.spec.mjs, edition: lite, codes: [BE_SUPABASE_JWT_VERIFIED]}
3832
- - {path: packages/hfs/src/be-supabase-jwt-verified.spec.mjs, edition: full, codes: [BE_SUPABASE_JWT_VERIFIED]}
3833
3759
  - id: R220
3834
3760
  code: "HFS_LITE_SECRET_CUSTODY"
3835
3761
  editions: [lite]
3836
- editionWhy: "ruling:lite-backing-services assigns Supabase backing to lite; R220 adds lite's reduced custody tree and Supabase/FE literal scan, while R06 governs plaintext secret material in both editions."
3762
+ editionWhy: "ruling:lite-self-hosted-postgres-passport-minio gives lite its own database and identity; R220 adds lite's reduced custody tree and the fe/ and db/ literal scan, while R06 governs plaintext secret material in both editions."
3837
3763
  docs: [docs/host-secrets.md#application-secret-rules]
3838
- law: "A lite app holds no secret material in the tree: root `.env*`, `secrets.env`, `.secrets/` and an `.enc` file outside `.starcistacks/<env>/secrets` are forbidden, and `fe/` and `supabase/` hold no Supabase token, JWT or connection string with a password; the one custody is the sealed `.starcistacks/<env>/secrets/<slug>.enc`."
3764
+ law: "A lite app holds no secret material in the tree: root `.env*`, `secrets.env`, `.secrets/` and an `.enc` file outside `.starcistacks/<env>/secrets` are forbidden, and `fe/` and `db/` hold no provider token, JWT or connection string with a password; the one custody is the sealed `.starcistacks/<env>/secrets/<slug>.enc`."
3839
3765
  kinds: [check]
3840
3766
  gates: [settle, land, ci]
3841
3767
  failureCodes: ["HFS_LITE_SECRET_CUSTODY"]
3842
3768
  enforcers:
3843
- - {kind: hfs, id: lite-secret-custody, at: scripts/hfs/rules/supabase-secrets.mjs}
3769
+ - {kind: hfs, id: lite-secret-custody, at: scripts/hfs/rules/lite-secrets.mjs}
3844
3770
  proves:
3845
- - {path: packages/hfs/src/hfs-supabase-secrets.spec.mjs, edition: lite}
3771
+ - {path: packages/hfs/src/hfs-lite-secrets.spec.mjs, edition: lite}
3846
3772
  exemplifiedBy:
3847
3773
  - {path: packages/hfs/templates/app/skeleton-lite/.sops.yaml, edition: lite}
3848
- - {path: packages/hfs/src/hfs-supabase-secrets.spec.mjs, edition: lite}
3774
+ - {path: packages/hfs/src/hfs-lite-secrets.spec.mjs, edition: lite}
3849
3775
  witness:
3850
- - {path: packages/hfs/src/hfs-supabase-secrets.spec.mjs, edition: lite}
3776
+ - {path: packages/hfs/src/hfs-lite-secrets.spec.mjs, edition: lite}
3851
3777
  - id: R221
3852
3778
  code: "CI_TRIGGERS_RELEASE_ONLY"
3853
3779
  law: "Workflow triggers are push filtered to v* tags or release/candidate-* branches, and workflow_dispatch. Runtime workflows and app ci.yml also admit main. App ci.yml includes the candidate branch filter for starci app release push. Other branches, events, path filters and ignore filters are refused."
@@ -4124,7 +4050,7 @@ rules:
4124
4050
  code: "FE_SERVER_ACTION_FORBIDDEN"
4125
4051
  editions: [lite, full]
4126
4052
  docs: [docs/code-pattern-enforcement.md#r243]
4127
- law: "The front end serves no API, in every app and edition (R212 case-2 (owner:lite-production-swarm), with the Work boundary amended by ruling:R212/case-4): no file under `fe/` carries a `\"use server\"` directive (module or function level), and the only route handlers are `app/health/live/route.ts` and the auth callback of the `fe.route.callback` slot; reads and writes reach the back end through `modules/api` and the SWR hooks, and a webhook, cron, public API or third-party callback is a back-end feature."
4053
+ law: "The front end serves no API, in every app and edition (R212 case-2 (owner:lite-production-swarm), with the Work boundary amended by ruling:R212/case-4): no file under `fe/` carries a `\"use server\"` directive (module or function level), and the only route handler is `app/health/live/route.ts`; reads and writes reach the back end through `modules/api` and the SWR hooks, and a webhook, cron, public API or third-party callback is a back-end feature."
4128
4054
  kinds: [check, lint]
4129
4055
  gates: [pre-commit, settle, land, ci]
4130
4056
  failureCodes: ["FE_SERVER_ACTION_FORBIDDEN", "FE_ROUTE_HANDLER_FORBIDDEN"]
@@ -4191,7 +4117,7 @@ rules:
4191
4117
  witness:
4192
4118
  - {path: packages/eslint/be/app-composition-coverage.spec.mjs, edition: lite, codes: [BE_APP_COMPOSITION_BUDGET]}
4193
4119
  - {path: packages/eslint/be/app-composition-coverage.spec.mjs, edition: full, codes: [BE_APP_COMPOSITION_BUDGET]}
4194
- law: "Each app file stays within the line budget its `be.app.*` slot names (`budget`, keyed by the slot entry the file matches): `main.ts` 80, `<app>.env.ts` 150, `<app>.options.ts` 200, `compose/<area>.ts` 120, `app.module.ts` 250. The budget is absolute, not a ratchet: an entrypoint that outgrows it moves typed, fail-fast parsing into `<app>.env.ts`; an app root that outgrows it moves an area into a new `compose/<area>.ts` group, never into the env or options file."
4120
+ law: "Each app file stays within the line budget its `be.app.*` slot names (`budget`, keyed by the slot entry the file matches): `main.ts` 80, `<app>.env.ts` 150, `<app>.options.ts` 200, `compose/<area>.ts` 120, `app.module.ts` 250, counted in lines excluding import declarations (a multi-line import counts none of its lines). The budget is absolute, not a ratchet: an entrypoint that outgrows it moves typed, fail-fast parsing into `<app>.env.ts`; an app root that outgrows it moves an area into a new `compose/<area>.ts` group, never into the env or options file."
4195
4121
  kinds: [lint, codemod]
4196
4122
  gates: [pre-commit, settle, land, ci]
4197
4123
  failureCodes: ["BE_APP_COMPOSITION_BUDGET"]
@@ -4289,10 +4215,10 @@ rules:
4289
4215
  editions: [lite, full]
4290
4216
  docs:
4291
4217
  - {path: docs/application-stacks.md#r252-script-hygiene, editions: [lite, full]}
4292
- law: "In lite and full apps, executable scripts and workflow commands never load plaintext secrets.env, secret.env, .env secret files or a home-directory credential file, enable shell xtrace or request --reveal. Service-role references belong only to the back-end Supabase integration; deployment scripts and source outside that integration never use the service role. Scanners inspect source references and report paths without reading secret files or printing their values."
4218
+ law: "In lite and full apps, executable scripts and workflow commands never load plaintext secrets.env, secret.env, .env secret files or a home-directory credential file, enable shell xtrace or request --reveal. Scanners inspect source references and report paths without reading secret files or printing their values."
4293
4219
  kinds: [check]
4294
4220
  gates: [settle, land, ci]
4295
- failureCodes: ["HFS_SCRIPT_PLAINTEXT_LOAD", "HFS_SCRIPT_XTRACE", "HFS_SERVICE_ROLE_SCOPE"]
4221
+ failureCodes: ["HFS_SCRIPT_PLAINTEXT_LOAD", "HFS_SCRIPT_XTRACE"]
4296
4222
  enforcers:
4297
4223
  - {kind: hfs, id: script-hygiene, at: scripts/hfs/rules/script-hygiene.mjs}
4298
4224
  proves:
@@ -4323,31 +4249,13 @@ rules:
4323
4249
  - {path: packages/hfs/src/hfs-secret-custody.spec.mjs, editions: [lite, full]}
4324
4250
  witness:
4325
4251
  - {path: packages/hfs/src/hfs-secret-custody.spec.mjs, editions: [lite, full]}
4326
- - id: R254
4327
- code: "DB_BE_ONLY_ACCESS"
4328
- editions: [lite]
4329
- editionWhy: "owner:lite-backing-services makes Supabase lite's database backing; these SQL access requirements apply to that backing."
4330
- docs: [docs/microservices.md#r254]
4331
- templateNa: "Browser grants, policies and publication membership belong to product-authored Supabase migrations, rather than a scaffolded SQL surface."
4332
- law: "In lite and full, browser data access stays closed on exposed schemas: authenticated receives neither table or sequence privileges nor data policies, and anon reads only through a select policy named *_public_read. A table listed in hfs.json supabase.directAccess is an explicit exception. A table joins supabase_realtime only when that direct access is declared. The front end uses Supabase for Auth and reaches product data through the back end."
4333
- kinds: [check]
4334
- gates: [settle, land, ci]
4335
- failureCodes: ["DB_BE_ONLY_ACCESS", "DB_REALTIME_UNLISTED"]
4336
- enforcers:
4337
- - {kind: hfs, id: db-be-only-access, at: scripts/hfs/rules/database-access.mjs}
4338
- proves:
4339
- - {path: packages/hfs/src/hfs-database-access.spec.mjs, edition: lite, codes: [DB_BE_ONLY_ACCESS, DB_REALTIME_UNLISTED]}
4340
- exemplifiedBy:
4341
- - {path: packages/hfs/src/hfs-database-access.spec.mjs, edition: lite, codes: [DB_BE_ONLY_ACCESS, DB_REALTIME_UNLISTED]}
4342
- witness:
4343
- - {path: packages/hfs/src/hfs-database-access.spec.mjs, edition: lite, codes: [DB_BE_ONLY_ACCESS, DB_REALTIME_UNLISTED]}
4344
4252
  - id: R255
4345
4253
  code: "DB_APP_ROLE"
4346
4254
  editions: [lite]
4347
- editionWhy: "owner:lite-backing-services makes Supabase lite's database backing; application-role and forced-RLS requirements apply to that backing."
4255
+ editionWhy: "owner:lite-backing-services makes the db service lite's database; application-role and forced-RLS requirements apply to that backing."
4348
4256
  docs: [docs/microservices.md#r255]
4349
- templateNa: "Application roles and table ownership are product-authored Supabase migration content, rather than a scaffolded SQL surface."
4350
- law: "The declared back-end database role (hfs.json supabase.appRole, app_be by default) has only the privileges it needs: role declarations refuse superuser and bypassrls, require noinherit and nobypassrls, and never grant all schema or table privileges. Tables owned by that role enable and force row level security by default; a forceRls exemption identifies the table and carries a nonempty reason. This applies to every edition that declares Supabase."
4257
+ templateNa: "Application roles and table ownership are product-authored db migration content, rather than a scaffolded SQL surface."
4258
+ law: "The declared back-end database role (app_be by default) has only the privileges it needs: role declarations refuse superuser and bypassrls, require noinherit and nobypassrls, and never grant all schema or table privileges. Tables owned by that role enable and force row level security by default; a forceRls exemption identifies the table and carries a nonempty reason. This applies to every edition that declares Supabase."
4351
4259
  kinds: [check]
4352
4260
  gates: [settle, land, ci]
4353
4261
  failureCodes: ["DB_APP_ROLE", "DB_FORCE_RLS_DEFAULT"]
@@ -4362,9 +4270,9 @@ rules:
4362
4270
  - id: R256
4363
4271
  code: "DB_POLICY_TAUTOLOGY"
4364
4272
  editions: [lite]
4365
- editionWhy: "owner:lite-backing-services makes Supabase lite's database backing; access-policy, view, definer and storage SQL requirements apply to that backing."
4273
+ editionWhy: "owner:lite-backing-services makes the db service lite's database; access-policy, view, definer and storage SQL requirements apply to that backing."
4366
4274
  docs: [docs/microservices.md#r256]
4367
- templateNa: "Access policies, views, definers and storage policies belong to product-authored Supabase migrations, rather than a scaffolded SQL surface."
4275
+ templateNa: "Access policies, views, definers and storage policies belong to product-authored db migrations, rather than a scaffolded SQL surface."
4368
4276
  law: "Exposed database objects keep their access boundaries in lite and full. Policies refuse tautological true, is-not-null and self-equality predicates; policies for browser roles prove a principal predicate. An exposed view or materialized view uses security_invoker or the migration set ends with access revoked from anon and authenticated. Disabling RLS or moving a table into an exposed schema preserves the R214 RLS obligation. A security definer declaration, including alter function, preserves R215 safety checks and the migration set ends with execute revoked from authenticated when exposed, in addition to R215's public and anon revocation. A storage.objects policy scopes an ownership prefix to the principal unless its bucket id ends _public."
4369
4277
  kinds: [check]
4370
4278
  gates: [settle, land, ci]
@@ -4384,9 +4292,9 @@ rules:
4384
4292
  - id: R257
4385
4293
  code: "DB_BASELINE_SHAPE"
4386
4294
  editions: [lite]
4387
- editionWhy: "owner:lite-backing-services makes Supabase lite's database backing; baseline and archive requirements apply to its migration history."
4295
+ editionWhy: "owner:lite-backing-services makes the db service lite's database; baseline and archive requirements apply to its migration history."
4388
4296
  docs: [docs/microservices.md#r257]
4389
- templateNa: "A baseline is derived from a product's existing Supabase migration history and cannot be supplied by a scaffold template."
4297
+ templateNa: "A baseline is derived from a product's existing migration history and cannot be supplied by a scaffold template."
4390
4298
  law: "An archived migration chain has exactly one active baseline marked -- starci:baseline <archived-last-stamp>, with a later valid stamp or an explicit restamp. Archived files only need the digits-stamp_name.sql shape, whatever the stamp value (they are immutable history and never applied); the baseline and active migrations keep the real-instant stamp check. Archive contents are immutable against repository history; an active migration enters the archive unchanged only in the same diff that adds the replacement baseline. The app db baseline, restamp and plan verbs are read-only unless --apply is explicit."
4391
4299
  kinds: [check]
4392
4300
  gates: [settle, land, ci]
@@ -4402,9 +4310,9 @@ rules:
4402
4310
  - id: R258
4403
4311
  code: "DB_PGMQ_DECLARED"
4404
4312
  editions: [lite]
4405
- editionWhy: "owner:lite-backing-services makes Supabase lite's database backing and ruling:R212/case-10 moves the lite queue to Redis; the pgmq retirement and pg_cron ownership requirements apply to that backing."
4313
+ editionWhy: "owner:lite-backing-services makes the db service lite's database and ruling:R212/case-10 moves the lite queue to Redis; the pgmq retirement and pg_cron ownership requirements apply to that backing."
4406
4314
  docs: [docs/microservices.md#r258]
4407
- templateNa: "Queue declarations and enqueue-only cron commands depend on product-owned work and are authored in its Supabase migrations."
4315
+ templateNa: "Queue declarations and enqueue-only cron commands depend on product-owned work and are authored in its db migrations."
4408
4316
  law: "DB_QUEUE_CRON: the queue is BullMQ on Redis (ruling:R212/case-10), so the active migrations leave no pgmq queue named by a queue constant under modules/queues; a pgmq queue a migration created for one is dropped by a later migration. Every pgmq queue the active migrations leave is an event-bus topic queue named by a literal *_TOPIC_QUEUE constant under platform/event-bus, every such constant has its pgmq queue, and pgmq objects are granted only to the application role while they exist. A pg_cron job belongs to a migration and only enqueues; its name is scheduled once while it is active, so a later migration schedules it again only after its own cron.unschedule of that name, and one tick function has one live job at a time. Its command, directly or through one enqueue-only *_tick function, inserts one row of a literal queue constant into the queue outbox table of ruleParams.db.queueOutbox, writing only its declared columns (the relay hands it to BullMQ). An app whose hfs.json sides.be.backing declares redis may declare its own outbox in sides.be.queueOutbox ({table, enqueue, queueParameter}); the command, or the tick, then calls the declared enqueue function with a literal queue constant for the declared parameter and pure other arguments, while the active definition of that function only inserts one row into the declared table with its queue column bound to that parameter (an ON CONFLICT DO NOTHING, a read of the declared table and pure expressions are the only other work). A direct insert into the app's outbox table, or any other function, stays refused. The job executes no business DML or HTTP request and shares no tick with an app scheduler. A pgmq.send validates only in the original body of a job an older migration scheduled while that queue existed; the live job and its current tick never enqueue that way. Removing a tick unschedules its job."
4409
4317
  kinds: [check]
4410
4318
  gates: [settle, land, ci]
@@ -4421,41 +4329,40 @@ rules:
4421
4329
  - id: R259
4422
4330
  code: "DB_EXTENSION_ALLOWED"
4423
4331
  editions: [lite]
4424
- editionWhy: "owner:lite-backing-services makes Supabase lite's database backing; database configuration, extension, migration and seed requirements apply to that backing."
4332
+ editionWhy: "owner:lite-backing-services makes the db service lite's database; database configuration, extension, migration and seed requirements apply to that backing."
4425
4333
  docs: [docs/microservices.md#r259]
4426
- templateNa: "Extension use, contract migrations, vault operations and seed literals are product-authored Supabase content, rather than a scaffolded SQL surface."
4427
- law: "DB_CONFIG_EXT: extensions belong to the database allowlist, vault writes and migration role passwords are refused, pgmq schemas are never exposed through the API, and lite API access is closed except for explicit public-read policies. R216 DB_CONFIG_POLICY also checks API schema allowlisting, existing seed paths, anonymous sign-ins, disabled edge runtime, bounded storage uploads and the database major-version pin. Destructive DDL belongs only to *_contract_* migrations whose targets are no longer referenced by back-end SQL or entities. Seeds contain no real email or phone literals."
4334
+ templateNa: "Extension use, contract migrations, vault operations and seed literals are product-authored db content, rather than a scaffolded SQL surface."
4335
+ law: "DB_CONFIG_EXT: extensions belong to the database allowlist, vault writes and migration role passwords are refused, and seed data holds no real email or phone. Destructive DDL belongs only to *_contract_* migrations whose targets are no longer referenced by back-end SQL or entities. Seeds contain no real email or phone literals."
4428
4336
  kinds: [check]
4429
4337
  gates: [settle, land, ci]
4430
- failureCodes: ["DB_EXTENSION_ALLOWED", "DB_API_EXPOSED", "DB_MIGRATION_DESTRUCTIVE", "HFS_SEED_PII", "DB_VAULT_WRITE"]
4338
+ failureCodes: ["DB_EXTENSION_ALLOWED", "DB_MIGRATION_DESTRUCTIVE", "HFS_SEED_PII", "DB_VAULT_WRITE"]
4431
4339
  enforcers:
4432
4340
  - {kind: hfs, id: db-config-ext, at: scripts/hfs/rules/database-config.mjs}
4433
4341
  - {kind: hfs, id: db-migration-destructive, at: scripts/hfs/rules/database-migrations.mjs}
4434
4342
  proves:
4435
- - {path: packages/hfs/src/hfs-database-config.spec.mjs, editions: [lite], codes: [DB_EXTENSION_ALLOWED, DB_API_EXPOSED, DB_MIGRATION_DESTRUCTIVE, HFS_SEED_PII, DB_VAULT_WRITE]}
4343
+ - {path: packages/hfs/src/hfs-database-config.spec.mjs, editions: [lite], codes: [DB_EXTENSION_ALLOWED, DB_MIGRATION_DESTRUCTIVE, HFS_SEED_PII, DB_VAULT_WRITE]}
4436
4344
  exemplifiedBy:
4437
- - {path: packages/hfs/src/hfs-database-config.spec.mjs, editions: [lite], codes: [DB_EXTENSION_ALLOWED, DB_API_EXPOSED, DB_MIGRATION_DESTRUCTIVE, HFS_SEED_PII, DB_VAULT_WRITE]}
4345
+ - {path: packages/hfs/src/hfs-database-config.spec.mjs, editions: [lite], codes: [DB_EXTENSION_ALLOWED, DB_MIGRATION_DESTRUCTIVE, HFS_SEED_PII, DB_VAULT_WRITE]}
4438
4346
  witness:
4439
- - {path: packages/hfs/src/hfs-database-config.spec.mjs, editions: [lite], codes: [DB_EXTENSION_ALLOWED, DB_API_EXPOSED, DB_MIGRATION_DESTRUCTIVE, HFS_SEED_PII, DB_VAULT_WRITE]}
4347
+ - {path: packages/hfs/src/hfs-database-config.spec.mjs, editions: [lite], codes: [DB_EXTENSION_ALLOWED, DB_MIGRATION_DESTRUCTIVE, HFS_SEED_PII, DB_VAULT_WRITE]}
4440
4348
  - id: R260
4441
4349
  code: "BE_JWT_ALG_PINNED"
4442
- law: "A back-end JWT verifier pins a nonempty literal subset of ES256, RS256 and EdDSA and obtains its issuer from configuration. A Supabase principal is built only after verification and rejection of every role other than authenticated, in lite and full."
4350
+ law: "A back-end JWT verifier pins a nonempty literal subset of ES256, RS256 and EdDSA and obtains its issuer from configuration. A principal is built only after verification and rejection of every role other than authenticated, in lite and full."
4443
4351
  kinds: [check, lint]
4444
4352
  gates: ["pre-commit","settle","land","ci"]
4445
4353
  failureCodes: ["BE_JWT_ALG_PINNED"]
4446
4354
  enforcers:
4447
4355
  - {kind: eslint-be, id: jwt-alg-pinned}
4448
- - {kind: machine, id: jwt-policy, at: scripts/hfs/architecture/supabase.mjs}
4356
+ - {kind: machine, id: jwt-policy, at: scripts/hfs/architecture/identity.mjs}
4449
4357
  editions: ["lite","full"]
4450
- templateNa: "Full identity providers are product-owned integrations with no scaffolded JWT verifier; the lite Supabase verifier is templated and both editions are exercised by the verifier specs."
4358
+ templateNa: "Full identity providers are product-owned integrations with no scaffolded JWT verifier; the lite own-token verifier is templated and both editions are exercised by the verifier specs."
4451
4359
  docs:
4452
4360
  - "docs/microservices.md#r260"
4453
4361
  proves:
4454
4362
  - "packages/eslint/be/jwt-alg.spec.mjs"
4455
4363
  - packages/hfs/src/architecture-jwt-policy.spec.mjs
4456
4364
  exemplifiedBy:
4457
- - {"path":"packages/hfs/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts","edition":"lite"}
4458
- - {"path":"examples/lite-app/be/src/modules/integrations/supabase/supabase.jwks.ts","edition":"lite"}
4365
+ - {"path":"packages/hfs/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts","edition":"lite"}
4459
4366
  - {"path":"packages/eslint/be/jwt-alg.spec.mjs","edition":"full","codes":["BE_JWT_ALG_PINNED"]}
4460
4367
  witness:
4461
4368
  - {"path":"packages/eslint/be/jwt-alg.spec.mjs","editions":["lite","full"],"codes":["BE_JWT_ALG_PINNED"]}
@@ -4794,14 +4701,14 @@ rules:
4794
4701
  - {path: packages/hfs/src/hfs-stacks-full-fixtures.spec.mjs, edition: full}
4795
4702
  - id: R276
4796
4703
  code: "STACKS_LITE_SELFHOSTED_DB"
4797
- law: "Lite keeps PostgreSQL and vector search on managed Supabase pgvector and runs no PostgreSQL, MongoDB or Qdrant backing image, even when declared (owner:lite-vector-is-pgvector). A Redis image runs only when hfs.json sides.be.backing declares redis (ruling:R212/case-10), as one minimal instance; a declared Redis runs as exactly one stack service, with AOF persistence (`appendonly yes`, `appendfsync everysec` or `always`, never `no`) on a writable named volume so queue jobs survive a restart and with no maxmemory-policy but noeviction so none is evicted. A queue job Redis loses inside the at most one second `everysec` leaves unwritten is not lost work: every job is born from a Postgres outbox row, and the relay marks that row sent only after Redis confirms the job is in its append-only file (WAITAOF) and still holds it, so a job Redis did not persist leaves its row waiting and the relay adds it again (examples/lite-app queue-relay.service.spec.ts and bullmq-queue-transport.client.spec.ts). A sidecar keeps its own SQLite on a named persistent volume declared by its component and mounted in the rendered model, and never uses the app's declared Redis, which serves the back-end app services only; a sidecar requiring another database, that Redis included, is refused. Full retains its declared backing topology."
4704
+ law: "Lite runs exactly one database, its db component of image supabase/postgres (pinned by digest) with pgvector for vector search, and no other PostgreSQL, MongoDB or Qdrant backing image, even when declared (owner:lite-vector-is-pgvector, ruling:lite-self-hosted-postgres-passport-minio). A Redis image runs only when hfs.json sides.be.backing declares redis (ruling:R212/case-10), as one minimal instance; a declared Redis runs as exactly one stack service, with AOF persistence (`appendonly yes`, `appendfsync everysec` or `always`, never `no`) on a writable named volume so queue jobs survive a restart and with no maxmemory-policy but noeviction so none is evicted. A queue job Redis loses inside the at most one second `everysec` leaves unwritten is not lost work: every job is born from a Postgres outbox row, and the relay marks that row sent only after Redis confirms the job is in its append-only file (WAITAOF) and still holds it, so a job Redis did not persist leaves its row waiting and the relay adds it again (examples/lite-app queue-relay.service.spec.ts and bullmq-queue-transport.client.spec.ts). A sidecar keeps its own SQLite on a named persistent volume declared by its component and mounted in the rendered model, and never uses the app's declared Redis, which serves the back-end app services only; a sidecar requiring another database, that Redis included, is refused. Full retains its declared backing topology."
4798
4705
  kinds: [check]
4799
4706
  gates: ["settle","land","ci"]
4800
4707
  failureCodes: ["STACKS_LITE_SELFHOSTED_DB", "STACKS_SIDECAR_VOLUME_UNDECLARED", "STACKS_SIDECAR_DB_UNSUPPORTED", "STACKS_LITE_REDIS_PERSISTENCE"]
4801
4708
  enforcers:
4802
4709
  - {kind: hfs, id: stacks-lite-backing, at: scripts/hfs/rules/stacks-lite-backing.mjs}
4803
4710
  editions: ["lite"]
4804
- editionWhy: "ruling:R276 records owner ruling 4, amended by ruling:R212/case-10: lite keeps Postgres on managed Supabase, uses Supabase pgvector for vectors, refuses Qdrant even when declared (owner:lite-vector-is-pgvector), runs a Redis instance only when declared, runs a declared Redis as one service persisted with AOF fsynced at least every second on a named volume and never evicting (the outbox relay re-adds a job Redis did not persist), and permits database-dependent sidecars only with SQLite on declared persistent volumes, never on the app's Redis; full keeps its declared backing topology."
4711
+ editionWhy: "ruling:R276 records owner ruling 4, amended by ruling:R212/case-10 and ruling:lite-self-hosted-postgres-passport-minio: lite runs its own Postgres as the db service, uses pgvector for vectors, refuses Qdrant even when declared (owner:lite-vector-is-pgvector), runs a Redis instance only when declared, runs a declared Redis as one service persisted with AOF fsynced at least every second on a named volume and never evicting (the outbox relay re-adds a job Redis did not persist), and permits database-dependent sidecars only with SQLite on declared persistent volumes, never on the app's Redis; full keeps its declared backing topology."
4805
4712
  docs:
4806
4713
  - "docs/application-stacks.md#r276"
4807
4714
  proves: [packages/hfs/src/hfs-stacks-lite-backing.spec.mjs]
@@ -5021,25 +4928,6 @@ rules:
5021
4928
  - {path: packages/eslint/fe/translation.spec.mjs, editions: [lite, full], codes: [FE_COPY_IN_CALL, FE_NO_ALERT]}
5022
4929
  - {path: packages/eslint/grammar-core/icon.spec.mjs, editions: [lite, full], codes: [FE_ICON_INLINE_SVG]}
5023
4930
 
5024
- - id: R287
5025
- code: "FE_CALLBACK_SHAPE"
5026
- amendedBy: [owner:auth-callback-email-link, owner:auth-callback-pure-helpers]
5027
- law: "The fe.route.callback handler performs only the auth code exchange and redirect: read the callback URL, obtain the auth client, exchange the code and redirect to the declared route. An email confirmation or recovery link (owner:auth-callback-email-link) arrives as the query values token_hash and type; the handler then awaits one verifyOtp({ token_hash, type }) of the auth owner (the auth client, or verifyOtp from modules/auth/server), built from exactly those two query values, in place of or beside the one code exchange. The handler may read the exchange outcome's success or ok flag, and may call pure text helpers exported by the app's modules/routes or modules/auth (owner:auth-callback-pure-helpers): a synchronous function with no await, no network, storage, DOM, process or console access and no import beyond other such helpers, found by TypeScript symbol resolution, so the public origin and the same-origin validation of next stay in the owner modules. It carries no product data operation, business logic, additional request handler or JSON response."
5028
- kinds: [lint]
5029
- gates: [pre-commit, settle, land, ci]
5030
- failureCodes: ["FE_CALLBACK_SHAPE"]
5031
- enforcers:
5032
- - {kind: eslint-fe, id: callback-shape}
5033
- editions: [lite, full]
5034
- docs: [docs/quality-bar.md#r287]
5035
- exemplifiedBy:
5036
- - {path: packages/eslint/fe/callback-shape.spec.mjs, editions: [lite, full], codes: [FE_CALLBACK_SHAPE]}
5037
- - {path: packages/hfs/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts, editions: [lite, full]}
5038
- proves:
5039
- - {path: packages/eslint/fe/callback-shape.spec.mjs, editions: [lite, full], codes: [FE_CALLBACK_SHAPE]}
5040
- witness:
5041
- - {path: packages/eslint/fe/callback-shape.spec.mjs, editions: [lite, full], codes: [FE_CALLBACK_SHAPE]}
5042
-
5043
4931
  - id: R288
5044
4932
  code: "FE_STACK_UNLISTED"
5045
4933
  law: "Every direct FE workspace dependency belongs to knowledge/hfs/fe-stack.yaml or is declared in hfs.json fe.extraPackages with its name and a nonempty reason. Installed runtime dependencies carried by a listed Grammar package are not app declarations and raise no FE_STACK_UNLISTED; direct forbidden dependencies still raise FE_STACK_FORBIDDEN. Every product Next app declares the catalog's core runtime packages; codegen, lint, TypeScript and Tailwind tools may be installed once at the app root. The marketing landing app is exempt (lite and full) from core presence. A front end that selects sides.fe.grammar and declares @starci/eslint-canon-fe 9 or later also declares that family's lint package from knowledge/grammars/index.yaml, which carries its Grammar component rules (FE_STACK_FAMILY_LINT_MISSING). GraphQL documents, including gql templates, match the back-end contracts, and SWR keys carry fetcher identities without an opt-in declaration."
@@ -5756,3 +5644,208 @@ rules:
5756
5644
  source: owner
5757
5645
  quote: "ok trò"
5758
5646
  ruling: "A route page is one Grammar Page (owner ruling page-frame): the single owner of the page inset and rhythm; the lint refuses a sibling beside the Page and a bare PageHeader or PageContainer in a page owner."
5647
+ - id: R326
5648
+ code: "BE_AI_KEY_CUSTODY"
5649
+ law: "An upstream model provider key variable (OPENROUTER_API_KEY, OPENAI_API_KEY and the rest of the canon's one list) is named only inside the ai-router gateway's custody owner integrations/ai-gateway, in a literal, a member or a destructured property; every other file reaches a model through the gateway with a virtual key."
5650
+ kinds: [lint]
5651
+ gates: [pre-commit, settle, land, ci]
5652
+ failureCodes: ["BE_AI_KEY_CUSTODY"]
5653
+ enforcers:
5654
+ - {kind: eslint-be, id: provider-key-custody}
5655
+ editions: [lite, full]
5656
+ docs: [contexts/code/be/ai.md]
5657
+ templateNa: "The skeleton scaffolds no model call; an app adds the ai and ai-gateway integrations when it calls a model."
5658
+ proves:
5659
+ - packages/eslint-canon-be/src/ai-gateway.spec.ts
5660
+ exemplifiedBy:
5661
+ - {path: packages/eslint-canon-be/src/ai-gateway.spec.ts, editions: [lite, full], codes: [BE_AI_KEY_CUSTODY]}
5662
+ witness:
5663
+ - {path: packages/eslint-canon-be/src/ai-gateway.spec.ts, editions: [lite, full], codes: [BE_AI_KEY_CUSTODY]}
5664
+ cases:
5665
+ - id: case-1
5666
+ date: "2026-10-29"
5667
+ source: owner
5668
+ quote: "nhớ làm langchain nhé update .claude lun là call thẳng từ code phải dùng langchain với embeded context"
5669
+ ruling: "A model call made straight from code uses LangChain with retrieved context, through the ai-router gateway (owner ruling be-ai-langchain-gateway): only the gateway's custody owner reads a provider key."
5670
+ - id: R327
5671
+ code: "BE_AI_GATEWAY_CLIENT"
5672
+ law: "Code builds only the ChatOpenAI and OpenAIEmbeddings clients of @langchain/openai, each from an inline options object with no spread, its configuration.baseURL read from a <c>.options.ts type and its apiKey the reveal() of a platform/config Secret, so every model call goes through the ai-router gateway with a virtual key."
5673
+ kinds: [lint]
5674
+ gates: [pre-commit, settle, land, ci]
5675
+ failureCodes: ["BE_AI_GATEWAY_CLIENT"]
5676
+ enforcers:
5677
+ - {kind: eslint-be, id: ai-client-through-gateway}
5678
+ editions: [lite, full]
5679
+ docs: [contexts/code/be/ai.md]
5680
+ templateNa: "The skeleton scaffolds no model call; an app adds the ai and ai-gateway integrations when it calls a model."
5681
+ proves:
5682
+ - packages/eslint-canon-be/src/ai-gateway.spec.ts
5683
+ exemplifiedBy:
5684
+ - {path: packages/eslint-canon-be/src/ai-gateway.spec.ts, editions: [lite, full], codes: [BE_AI_GATEWAY_CLIENT]}
5685
+ witness:
5686
+ - {path: packages/eslint-canon-be/src/ai-gateway.spec.ts, editions: [lite, full], codes: [BE_AI_GATEWAY_CLIENT]}
5687
+ cases:
5688
+ - id: case-1
5689
+ date: "2026-10-29"
5690
+ source: owner
5691
+ quote: "nhớ làm langchain nhé update .claude lun là call thẳng từ code phải dùng langchain với embeded context"
5692
+ ruling: "A model call made straight from code uses LangChain with retrieved context, through the ai-router gateway (owner ruling be-ai-langchain-gateway): the LangChain clients point at the gateway's base URL with its virtual key."
5693
+ - id: R328
5694
+ code: "BE_AUTH_PASSPORT_OWNER"
5695
+ law: "The identity libraries (@nestjs/passport, passport, passport-google-oauth20, passport-local, @nestjs/jwt, jose and bcryptjs) are imported only by integrations/passport, the one owner of the Google OAuth 2.0 strategy (state and PKCE), the local email and password strategy (bcrypt) and the app's own access tokens; bcrypt and every Supabase client are owned nowhere."
5696
+ kinds: [lint]
5697
+ gates: [pre-commit, settle, land, ci]
5698
+ failureCodes: ["BE_AUTH_PASSPORT_OWNER"]
5699
+ enforcers:
5700
+ - {kind: eslint-be, id: infra-import-owner}
5701
+ editions: [lite]
5702
+ docs: [contexts/code/be/api-auth.md]
5703
+ proves:
5704
+ - packages/eslint-canon-be/src/auth-passport.spec.ts
5705
+ exemplifiedBy:
5706
+ - {path: packages/hfs/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts, edition: lite}
5707
+ witness:
5708
+ - {path: packages/eslint-canon-be/src/auth-passport.spec.ts, edition: lite, codes: [BE_AUTH_PASSPORT_OWNER]}
5709
+ cases:
5710
+ - id: case-1
5711
+ date: "2026-10-29"
5712
+ source: owner
5713
+ quote: "self host postgresql + auth thì trò tự code gg auth (lite, full dùng keycloak); storage minio trên tayson có r!!!!; auth dùng passport nestjs; cập nhật lại luôn cả .claude nhé cho lite app"
5714
+ ruling: "Lite leaves Supabase: its identity is the app own Passport integration (Google OAuth and email/password) on its self-hosted PostgreSQL; full keeps Keycloak."
5715
+ - id: R329
5716
+ code: "BE_AUTH_JWT_OWN_JWKS"
5717
+ law: "A lite back-end principal comes only from the app's own access token verified by integrations/passport against its own key set (jose jwtVerify over createLocalJWKSet, issuer, audience and algorithms pinned), never from a remote JWKS, an unverified decode or a request body claim; domain/identity builds the principal from that verification."
5718
+ kinds: [check, lint]
5719
+ gates: [pre-commit, settle, land, ci]
5720
+ failureCodes: ["BE_AUTH_JWT_OWN_JWKS"]
5721
+ enforcers:
5722
+ - {kind: machine, id: own-jwks-principal, at: scripts/hfs/architecture/identity.mjs}
5723
+ - {kind: eslint-be, id: own-jwks-only}
5724
+ editions: [lite]
5725
+ docs: [contexts/code/be/api-auth.md]
5726
+ proves:
5727
+ - packages/hfs/src/be-auth-jwt-own-jwks.spec.mjs
5728
+ - packages/eslint-canon-be/src/auth-passport.spec.ts
5729
+ exemplifiedBy:
5730
+ - {path: packages/hfs/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts, edition: lite}
5731
+ witness:
5732
+ - {path: packages/hfs/src/be-auth-jwt-own-jwks.spec.mjs, edition: lite, codes: [BE_AUTH_JWT_OWN_JWKS]}
5733
+ cases:
5734
+ - id: case-1
5735
+ date: "2026-10-29"
5736
+ source: owner
5737
+ quote: "self host postgresql + auth thì trò tự code gg auth (lite, full dùng keycloak); storage minio trên tayson có r!!!!; auth dùng passport nestjs; cập nhật lại luôn cả .claude nhé cho lite app"
5738
+ ruling: "The lite back end verifies its own asymmetric access tokens against its own JWKS; it trusts no remote identity provider."
5739
+ - id: R330
5740
+ code: "BE_AUTH_REFRESH_COOKIE"
5741
+ law: "A refresh token travels only in an httpOnly, Secure, SameSite=Strict cookie scoped to the refresh door (set and cleared only through integrations/passport), is rotated on every use with reuse detection, and never appears as a field of a request or response body."
5742
+ kinds: [lint]
5743
+ gates: [pre-commit, settle, land, ci]
5744
+ failureCodes: ["BE_AUTH_REFRESH_COOKIE"]
5745
+ enforcers:
5746
+ - {kind: eslint-be, id: refresh-token-cookie-only}
5747
+ editions: [lite]
5748
+ docs: [contexts/code/be/api-auth.md]
5749
+ proves:
5750
+ - packages/eslint-canon-be/src/auth-passport.spec.ts
5751
+ exemplifiedBy:
5752
+ - {path: packages/hfs/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts, edition: lite}
5753
+ witness:
5754
+ - {path: packages/eslint-canon-be/src/auth-passport.spec.ts, edition: lite, codes: [BE_AUTH_REFRESH_COOKIE]}
5755
+ cases:
5756
+ - id: case-1
5757
+ date: "2026-10-29"
5758
+ source: owner
5759
+ quote: "self host postgresql + auth thì trò tự code gg auth (lite, full dùng keycloak); storage minio trên tayson có r!!!!; auth dùng passport nestjs; cập nhật lại luôn cả .claude nhé cho lite app"
5760
+ ruling: "The refresh token of the lite identity rotates and lives only in an httpOnly cookie; the browser holds no credential."
5761
+ - id: R331
5762
+ code: "BE_STORAGE_OWNER"
5763
+ law: "The S3 client (@aws-sdk/client-s3 and @aws-sdk/s3-request-presigner) is imported only by integrations/object-storage, the ObjectStorage port over S3-compatible storage (MinIO in lite) with presigned PUT and GET; no module imports minio."
5764
+ kinds: [lint]
5765
+ gates: [pre-commit, settle, land, ci]
5766
+ failureCodes: ["BE_STORAGE_OWNER"]
5767
+ enforcers:
5768
+ - {kind: eslint-be, id: infra-import-owner}
5769
+ editions: [lite, full]
5770
+ docs: [contexts/code/be/infra.md]
5771
+ proves:
5772
+ - packages/eslint-canon-be/src/auth-passport.spec.ts
5773
+ exemplifiedBy:
5774
+ - {path: packages/hfs/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts, edition: lite}
5775
+ witness:
5776
+ - {path: packages/eslint-canon-be/src/auth-passport.spec.ts, editions: [lite, full], codes: [BE_STORAGE_OWNER]}
5777
+ cases:
5778
+ - id: case-1
5779
+ date: "2026-10-29"
5780
+ source: owner
5781
+ quote: "self host postgresql + auth thì trò tự code gg auth (lite, full dùng keycloak); storage minio trên tayson có r!!!!; auth dùng passport nestjs; cập nhật lại luôn cả .claude nhé cho lite app"
5782
+ ruling: "Lite storage is the MinIO already running on the Tay Son host, reached through the one S3 integration."
5783
+ - id: R332
5784
+ code: "LITE_DB_MIGRATION"
5785
+ law: "A lite app's schema lives only in db/: every schema change is one db/migrations/<ts14>_<kebab>.sql (archive db/migrations/archive/, seed db/seed.sql), and the tree holds no supabase/ folder and no SQL file outside db/ but a .starcistacks seed."
5786
+ kinds: [check]
5787
+ gates: [settle, land, ci]
5788
+ failureCodes: ["LITE_DB_MIGRATION"]
5789
+ enforcers:
5790
+ - {kind: hfs, id: lite-db-path, at: scripts/hfs/rules/database.mjs}
5791
+ editions: [lite]
5792
+ docs: [contexts/code/be/persistence.md]
5793
+ proves:
5794
+ - packages/hfs/src/db-migration-shape.spec.mjs
5795
+ exemplifiedBy:
5796
+ - {path: packages/hfs/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql, edition: lite}
5797
+ witness:
5798
+ - {path: packages/hfs/src/db-migration-shape.spec.mjs, edition: lite, codes: [LITE_DB_MIGRATION]}
5799
+ cases:
5800
+ - id: case-1
5801
+ date: "2026-10-29"
5802
+ source: owner
5803
+ quote: "self host postgresql + auth thì trò tự code gg auth (lite, full dùng keycloak); storage minio trên tayson có r!!!!; auth dùng passport nestjs; cập nhật lại luôn cả .claude nhé cho lite app"
5804
+ ruling: "The lite database is self-hosted PostgreSQL in the app stack; its migrations live in db/migrations and starci app db migrate applies them."
5805
+ - id: R333
5806
+ code: "BE_ENTITY_SCHEMA_DRIFT"
5807
+ law: "Every TypeORM entity column of an app's entity list (be/src/modules/platform/database/entities.ts) matches the schema its SQL migrations build: `starci app db drift` applies db/bootstrap.sql and every active db/migrations/*.sql to a scratch database (a container of the app's db image, or the empty database --url-env names), reads information_schema.columns and pg_catalog, and refuses a missing table, a missing column, a type that differs once both are normalised to PostgreSQL's format_type names (an enum by its type name), and a nullability that differs. A database column no entity maps is fine. A host that cannot provide the scratch database gets no verdict (exit 2, needs a database), never a green."
5808
+ kinds: [check]
5809
+ gates: [ci]
5810
+ failureCodes: ["BE_ENTITY_SCHEMA_DRIFT"]
5811
+ enforcers:
5812
+ - {kind: hfs, id: db-drift, at: packages/hfs/db/drift.mjs}
5813
+ editions: [lite]
5814
+ editionWhy: "The owner ruling of 2026-10-29 maps lite tables with TypeORM entities while db/migrations/*.sql stays the only DDL and synchronize is false, so only lite entities can drift from a schema they do not create; full entities ship with their own TypeORM migrations."
5815
+ docs: [contexts/code/be/persistence.md]
5816
+ templateNa: "The entities and the migrations are the product's tables; the check judges whichever an app writes, and a template scaffolds no table."
5817
+ proves:
5818
+ - {path: packages/hfs/src/db-drift.spec.mjs, edition: lite, codes: [BE_ENTITY_SCHEMA_DRIFT]}
5819
+ - {path: packages/hfs/src/db-drift-docker.spec.mjs, edition: lite, codes: [BE_ENTITY_SCHEMA_DRIFT]}
5820
+ exemplifiedBy:
5821
+ - {path: packages/hfs/src/db-drift.spec.mjs, edition: lite, codes: [BE_ENTITY_SCHEMA_DRIFT]}
5822
+ witness:
5823
+ - {path: packages/hfs/src/db-drift.spec.mjs, edition: lite, codes: [BE_ENTITY_SCHEMA_DRIFT]}
5824
+ cases:
5825
+ - id: case-1
5826
+ date: "2026-10-29"
5827
+ source: owner
5828
+ quote: "lite persistence moves to TypeORM entities while db/migrations/*.sql stays the only DDL and synchronize is false"
5829
+ ruling: "Lite entities only map tables the migrations create, so each entity column is checked against the migrated schema by starci app db drift, which CI runs in the build job and the release gate runs as its db-drift row."
5830
+ - id: R334
5831
+ code: "BE_PERSISTENCE_NO_RAW_SQL"
5832
+ law: "Lite domain and feature code reads and writes through TypeORM entities on the injected EntityManager (find, findOne, findBy, count, exists, insert, save, update, delete, upsert, increment, and createQueryBuilder(Entity, alias) with named parameters for joins, aggregates, FOR UPDATE / SKIP LOCKED and jsonb or vector operators; never a table-name builder, a builder with no entity or a Repository); no .query( call and no sql template outside platform/database; a database function TypeORM cannot express is called through the callDbFunction, advisoryXactLock and notify helpers of platform/database, and callDbFunction names only a function ruleParams.be.dbFunctions or hfs.json sides.be.dbFunctions lists with a reason. Entities only map tables db/migrations created (synchronize stays false); entity-versus-schema drift is BE_ENTITY_SCHEMA_DRIFT of starci app db drift."
5833
+ kinds: [lint]
5834
+ gates: [pre-commit, settle, land, ci]
5835
+ failureCodes: ["BE_PERSISTENCE_NO_RAW_SQL"]
5836
+ enforcers:
5837
+ - {kind: eslint-be, id: no-raw-sql}
5838
+ editions: [lite]
5839
+ docs: [contexts/code/be/persistence.md]
5840
+ proves:
5841
+ - packages/eslint-canon-be/src/no-raw-sql.spec.ts
5842
+ exemplifiedBy:
5843
+ - {path: packages/eslint-canon-be/src/no-raw-sql.spec.ts, edition: lite, codes: [BE_PERSISTENCE_NO_RAW_SQL]}
5844
+ witness:
5845
+ - {path: packages/eslint-canon-be/src/no-raw-sql.spec.ts, edition: lite, codes: [BE_PERSISTENCE_NO_RAW_SQL]}
5846
+ cases:
5847
+ - id: case-1
5848
+ date: "2026-10-29"
5849
+ source: owner
5850
+ quote: "big refactor đi … không viết raw sql như thế theo starci-academy-be"
5851
+ ruling: "Lite persistence moves to TypeORM entities on the EntityManager, the shape of full and starci-academy-be (ruling lite-persistence-typeorm-entities)."