@starci/eslint-canon-fe 8.2.13 → 8.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +1 -0
  3. package/client-boundary.mjs +66 -23
  4. package/docs/client-boundary.md +9 -9
  5. package/docs/hooks-folder.md +16 -4
  6. package/docs/hygiene.md +2 -2
  7. package/docs/transport.md +26 -26
  8. package/docs/type-safety.md +6 -6
  9. package/fe-stack.mjs +86 -0
  10. package/formatting.mjs +12 -6
  11. package/hooks-folder.mjs +73 -31
  12. package/hygiene.mjs +4 -4
  13. package/icon.mjs +25 -17
  14. package/index.mjs +2 -0
  15. package/lib/fe-stack.mjs +73 -0
  16. package/lib/scope.mjs +23 -2
  17. package/lib/why.mjs +23 -13
  18. package/next-conventions.mjs +4 -3
  19. package/package.json +1 -1
  20. package/runtime/knowledge/hfs/canon-pins.yaml +207 -15
  21. package/runtime/knowledge/hfs/fe-stack.yaml +218 -0
  22. package/runtime/knowledge/hfs/slots.yaml +319 -179
  23. package/runtime/knowledge/patterns/fe/folder.yaml +24 -18
  24. package/runtime/knowledge/sonar-gate.yaml +10 -2
  25. package/runtime/modules/kernel/removed-vocabulary.yaml +48 -0
  26. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +0 -2
  27. package/runtime/scripts/hfs/architecture/config.mjs +3 -2
  28. package/runtime/scripts/hfs/architecture/dead-exports.mjs +6 -2
  29. package/runtime/scripts/hfs/architecture/error-masked.mjs +10 -1
  30. package/runtime/scripts/hfs/architecture/frontend.mjs +10 -3
  31. package/runtime/scripts/hfs/architecture/hfs.mjs +6 -3
  32. package/runtime/scripts/hfs/architecture/index.mjs +2 -0
  33. package/runtime/scripts/hfs/architecture/owners.mjs +19 -16
  34. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +4 -1
  35. package/runtime/scripts/hfs/architecture/server-action.mjs +75 -20
  36. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +2 -2
  37. package/runtime/scripts/hfs/architecture/supabase.mjs +36 -210
  38. package/runtime/scripts/hfs/architecture/tiers.mjs +1 -33
  39. package/runtime/scripts/hfs/architecture/transport-owner.mjs +110 -54
  40. package/runtime/scripts/hfs/architecture/type-context.mjs +4 -1
  41. package/runtime/scripts/hfs/edition-slots.mjs +13 -4
  42. package/runtime/scripts/hfs/manifest-shape.mjs +3 -3
  43. package/runtime/scripts/hfs/rule-catalog.mjs +21 -1
  44. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +4 -5
  45. package/served-locale.mjs +10 -13
  46. package/the-split.mjs +5 -2
  47. package/transport.mjs +81 -67
  48. package/runtime/scripts/hfs/architecture/supabase-results.mjs +0 -182
@@ -1,16 +1,16 @@
1
1
  # HFS slot manifest (owner-approved 2026-09-29, decisions 1-10 in the owner decisions record).
2
2
  # The one place that says what may exist in a StarCi product repository and where. A product is ONE app repository
3
- # (hfs 4, manifest major 2): the app root holds the one package.json, lockfile and node_modules, the hfs.json of kind app,
3
+ # (hfs 4, manifest major 3): the app root holds the one package.json, lockfile and node_modules, the hfs.json of kind app,
4
4
  # the CI, the hooks and .starciwork (slots of profile `app`); `be/` and `fe/` are its two sides, each laid out as a
5
5
  # standalone repository root (slots of profile be or fe, paths relative to the side folder). Every tracked path of every
6
6
  # repository must match exactly one slot. Checks, lint factories, the architecture machine, templates and the why
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
- schema: starci/hfs-slots@2
10
- version: 2.1.0
9
+ schema: starci/hfs-slots@3
10
+ version: 3.0.0
11
11
 
12
12
  versioning:
13
- # MAJOR.MINOR.PATCH of this manifest. A repository pins only the MAJOR in hfs.json ("hfs": 2).
13
+ # MAJOR.MINOR.PATCH of this manifest. A repository pins only the MAJOR in hfs.json ("hfs": 3).
14
14
  patch: wording, why text, examples; no check result can change.
15
15
  minor: >-
16
16
  add a slot (always presence optional or opt-in), add an app kind or protocol slot, add a rule at severity warn,
@@ -24,6 +24,13 @@ versioning:
24
24
  # 2.1.0 is such a minor: it ADDS the edition mechanism (the `editions` list, the per-slot `editions`, `litePresence`, `lite`
25
25
  # overlays and `provider`, and `sides.<side>.reads` of an app-root tree) and the lite slots; an app without `edition` in its
26
26
  # hfs.json is edition full and every answer for it is unchanged.
27
+ # 2.2.0 adds the declaration-only fe.modules.types.ambient owner.
28
+ # 3.0.0 is a major (owner decision 2026-10-13, R212 cases 1-2): lite is the full code structure deployed without self-hosted
29
+ # infrastructure and without e2e, and the front end is one standard for both editions (knowledge/hfs/fe-stack.yaml). The
30
+ # front-end database slots and the Supabase realtime feature slot are removed (Supabase on the front end is auth only, realtime
31
+ # is the socket.io gateway in both editions), fe.route holds pages, layouts and the one health/live route handler, and the slots
32
+ # marked `since: 3.0.0` are added: the Apollo, SWR, RHF, zustand and socket.io hooks, redux, modules/auth, modules/toast, the
33
+ # providers, be.assets and the migration archive.
27
34
 
28
35
  # Field meaning, per slot.
29
36
  # id stable name, never changes meaning inside a major.
@@ -38,7 +45,6 @@ versioning:
38
45
  # tier node of the import direction matrix (tiers below); none = takes no part in import checks;
39
46
  # inherit = the tier of the enclosing owner.
40
47
  # owner true when every directory matching the slot is an owner: the unit cross-owner imports and cycles use.
41
- # anonymousActions on a directory slot: the Server Action files (relative to it) that authenticate or end a session and so run for an anonymous principal (FE_AUTH_SESSION_TRUST exempts them from refusing it; they still parse their input).
42
48
  # entries on a directory slot that sits inside an owner (not an owner itself): the files, besides the owner's index.ts, an importer
43
49
  # outside the owner may enter through (a client-safe entry of a module whose barrel is server-only). Each is a relative
44
50
  # path the slot allows. Everything else of the owner is entered through index.ts only (ARCH_OWNER_EXPORT_BYPASS).
@@ -68,26 +74,37 @@ versioning:
68
74
  # managedBy the template directory starci app sync renders this slot's files from. The slot path then lists literal files
69
75
  # only, and each is rendered from templates/<profile or common>/<managedBy>/<path, leading dots dropped>;
70
76
  # liteManagedBy the template directory starci app sync renders under edition lite (default: managedBy). A slot with only
71
- # liteManagedBy is managed only in lite; its effective lite.path still lists literal files;
77
+ # liteManagedBy is managed only in lite; its effective lite.path still lists literal files.
72
78
  # the managed file list lives here and nowhere in code. A difference is HFS_MANAGED_FILE_DRIFT (a
73
79
  # back end's tsconfig.json: HFS_TS_STRICT when a flag or an option was added).
74
80
  # rules rule ids (the catalog lands with knowledge/hfs/rules.yaml).
75
81
  # editions the editions (below) the slot exists in; default all of them. A slot outside the declared edition owns
76
82
  # no path - under lite, what full keeps absent is removed, and what lite forbids is a finding.
77
83
  # litePresence the presence under edition lite (default: presence): a bare value, or {be, fe} for a both-sides slot.
78
- # A slot forbidden in lite is external there and must name its goesTo.
84
+ # A slot forbidden in lite is external there and must name its goesTo. Only infrastructure is forbidden in lite:
85
+ # the self-hosted stack (.starcistacks infra/, runtime/, seeds/), the e2e and contract test layers and their
86
+ # workflow, and the browser journey.
79
87
  # lite the fields the slot carries under lite only (path, requires, allows, forbids, minInstances,
80
- # requiredInstances), each shaped as the same field in the slot body; every other field is the edition's.
81
- # Under lite `tests` is none on every slot: a lite app has no test world.
88
+ # requiredInstances, why), each shaped as the same field in the slot body; every other field is the edition's.
89
+ # `why` under lite names the Supabase adapter behind the slot's ports (pgmq, Postgres locks, Supabase Realtime).
90
+ # `tests` is never overlaid: a lite slot owes exactly the unit specs its full slot owes.
82
91
  # provider the hfs.json connections[].provider that enables this opt-in slot (supabase); never an app kind.
83
92
  # since the manifest version that introduced the slot.
84
93
  presenceValues: [required, optional, opt-in, forbidden]
85
94
  trackedValues: [tracked, ignored, external]
86
95
  testValues: [unit-beside, e2e, none]
87
96
 
88
- # The editions a repository declares in hfs.json `edition` (absent means full). Lite is this same manifest filtered: a slot
89
- # whose `editions` lacks lite does not exist for a lite app, `litePresence` replaces presence and a `lite` map replaces the
90
- # fields it names. Upgrade lite -> full adds only (the forbidden test world becomes legal again); downgrade is not supported.
97
+ # The editions a repository declares in hfs.json `edition` (absent means full). Owner ruling 2026-10-12 (R212 case-1): lite IS
98
+ # the full architecture deployed without self-hosted infrastructure - the same monorepo, slots, CQRS shape and patterns (queue,
99
+ # event bus, saga, reactors, projection, realtime, webhooks, fenced job), declared in hfs.json the same way, and the same unit
100
+ # specs (unitRoles, logicRoles, 100 per file, @starci/jest-preset doubles, builders). What differs is ADAPTERS and DELIVERY, all
101
+ # on Supabase: schema authority in supabase/migrations, the queue and event-bus ports on Supabase Queues (pgmq), realtime on
102
+ # Supabase Realtime, cache and locks on Postgres, schedules on pg_cron (+ pg_net), storage and auth on Supabase. Integration specs
103
+ # are optional and run only against the local Supabase CLI stack (`starci supabase start`); there is no e2e or contract world and no
104
+ # self-hosted stack (.starcistacks holds env and secret custody only). The front end is the full front end (no tests, R97).
105
+ # Lite is this same manifest filtered: a slot whose `editions` lacks lite does not exist for a lite app, `litePresence` replaces
106
+ # presence and a `lite` map replaces the fields it names. Upgrade lite -> full adds only (swap the adapters, add the e2e and
107
+ # contract world and the stack); downgrade is not supported.
91
108
  editions: [full, lite]
92
109
 
93
110
  # 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
@@ -244,7 +261,7 @@ ruleParams:
244
261
  # R89 BE_SOURCE_FORM: the closed role-suffix vocabulary of a source file name (<kebab-name>.<suffix>.ts; BE-CONVENTION 1.15)
245
262
  # and the suffixes that are never a role. index.ts, main.ts, app.module.ts and the migrations of be.persistence are the
246
263
  # only names outside it.
247
- suffixes: [module, module-definition, decorators, options, config, connection, service, command, query, handler, compensation, saga-state, saga-step, saga, contracts, port, resolver, controller, gateway, webhook, subscription, consumer, cli, event, queue, projection, projection-entity, processor, step, input, type, args, request, response, mapper, policy, entity, sql, rows, error, messages, log-events, cache-keys, guard, interceptor, filter, client, spec, integration-spec, e2e-spec, contract-spec, global-setup, builder]
264
+ suffixes: [module, module-definition, decorators, options, config, connection, service, command, query, handler, templates, compensation, saga-state, saga-step, saga, contracts, port, resolver, controller, gateway, webhook, subscription, consumer, cli, event, queue, projection, projection-entity, processor, step, input, type, args, request, response, mapper, policy, entity, sql, rows, error, messages, log-events, cache-keys, guard, interceptor, filter, client, spec, integration-spec, e2e-spec, contract-spec, global-setup, builder]
248
265
  # R47 BE_CONTRACT_UNGUARDED: the world's shape vocabulary. `helper` names the function a contract spec imports to erase the values of a JSON
249
266
  # payload (`shapeOf`); a contract spec proves a fake's fixtures by asserting helper(real) equals helper(fixture).
250
267
  contractShape: {helper: shapeOf}
@@ -301,10 +318,23 @@ ruleParams:
301
318
  api: {topic: api, variable: feature, trigger: api, patterns: [], needs: [service], defaults: {action: "@name"}}
302
319
  cli: {topic: cli, variable: group, trigger: cli, patterns: [], needs: [service], wire: {file: src/features/cli/cli.module.ts, import: "./@@group@@/@@group@@.module", symbol: "@@Group@@Module"}}
303
320
  bannedSuffixes: [use-case, repository, fixture, factory, store, worker, scheduler, cron, job, listener, dto, util, utils, helper, helpers, rules, types, constants, int-spec, harness-spec, test]
304
- # Edition lite (ruleParams(manifest, 'be', 'lite') merges these keys over the base): the schema authority is the Supabase
305
- # migration tree (supabase/migrations/*.sql, forward-only), so a lite back end holds no TypeORM migration or entity under
306
- # be/ (R34/R35 read schemaAuthority; be.persistence's lite overlay allows neither).
307
- lite: {schemaAuthority: supabase, unitRoles: []} # lite has no test world: no service or cli command owes a unit spec
321
+ # Edition lite (ruleParams(manifest, 'be', 'lite') merges these keys over the base; a map merges one level deep): the schema
322
+ # authority is the Supabase migration tree (supabase/migrations/*.sql, forward-only), so a lite back end holds no TypeORM
323
+ # migration or entity under be/ (R34/R35 read schemaAuthority; be.persistence's lite overlay allows neither). The unit roles,
324
+ # logic roles and spec doubles are full's (owner ruling 2026-10-12). infraOwners: the self-hosted libraries are owned nowhere -
325
+ # platform/queue and platform/event-bus run Supabase Queues (pgmq) through the shared EntityManager, cache and locks are
326
+ # Postgres (advisory locks or an unlogged table), and realtime is the same socket.io gateway as full, run inside the api
327
+ # process with no Redis adapter (owner ruling 2026-10-13).
328
+ lite:
329
+ schemaAuthority: supabase
330
+ infraOwners:
331
+ bullmq: []
332
+ "@nestjs/bullmq": []
333
+ kafkajs: []
334
+ redis: []
335
+ ioredis: []
336
+ graphql-subscriptions: []
337
+ graphql-ws: []
308
338
 
309
339
  slots:
310
340
 
@@ -407,9 +437,7 @@ slots:
407
437
  tracked: tracked
408
438
  tier: none
409
439
  tests: none
410
- lite: {path: sonar-project.properties}
411
- managedBy: quality-config
412
- liteManagedBy: quality-config-lite
440
+ managedBy: quality-config # lite renders the same files: the same coverage scope, Sonar import and Codecov flags as full
413
441
  rules: [HFS_SONAR_CONFIG, HFS_MANAGED_FILE_DRIFT]
414
442
  - id: app.hooks
415
443
  profiles: [app]
@@ -434,11 +462,11 @@ slots:
434
462
  profiles: [app]
435
463
  path: ".github/workflows/e2e.yml"
436
464
  presence: optional # on: workflow_dispatch only
437
- litePresence: forbidden # a lite app has no e2e run
465
+ litePresence: forbidden # a lite app has no e2e world (owner ruling 2026-10-12): unit specs run in ci.yml
438
466
  tracked: tracked
439
467
  tier: none
440
468
  tests: none
441
- goesTo: "the full edition: a lite app has no e2e workflow (no tests, nothing to run)"
469
+ goesTo: "the full edition: a lite app has no e2e or contract world; its unit specs run in .github/workflows/ci.yml"
442
470
  managedBy: ci-workflows
443
471
  rules: [HFS_CI_E2E_AUTOMATIC, HFS_MANAGED_FILE_DRIFT]
444
472
  - id: app.ci-images
@@ -480,6 +508,8 @@ slots:
480
508
  # refusing any test or test tooling inside fe/). Declared by `browser: true` in hfs.json; the managed script `test:browser` exists only then.
481
509
  path: "browser/"
482
510
  presence: opt-in
511
+ litePresence: forbidden # a browser journey runs against a provisioned stack: e2e, which lite does not have
512
+ goesTo: "the full edition: a lite app has no e2e world, so no browser journey"
483
513
  tracked: tracked
484
514
  tier: none
485
515
  tests: none
@@ -535,15 +565,15 @@ slots:
535
565
  rules: [HFS_AGENT_DATA_TRACKED, HFS_IDENTITY_CUSTODY]
536
566
  - id: app.starciwork.uat
537
567
  profiles: [app]
538
- # The UAT records of the full edition: a lite app has no UAT (no tests, no record of one), so the trees are forbidden paths
539
- # under lite only. In full this slot does not exist and .starciwork answers for them.
568
+ # The UAT records of the full edition: they are proven on the e2e world (identities, fixtures, br/ac evidence) that lite does
569
+ # not have, so the trees are forbidden paths under lite only. In full this slot does not exist and .starciwork answers for them.
540
570
  editions: [lite]
541
571
  path: ".starciwork/{_resources/identities,_resources/fixtures,features/<feature>/uat,features/<feature>/br}/"
542
572
  presence: forbidden
543
573
  tracked: external
544
574
  tier: none
545
575
  tests: none
546
- goesTo: "the full edition: a lite app has no UAT records (no identities or fixtures resources, no br/ac evidence, no uat directories)"
576
+ goesTo: "the full edition: a lite app has no e2e world, so no UAT records (no identities or fixtures resources, no br/ac evidence, no uat directories)"
547
577
  since: 2.1.0
548
578
  - id: app.starcistacks
549
579
  profiles: [app]
@@ -557,8 +587,9 @@ slots:
557
587
  allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,k8s,terraform}/**", "<env>/infra/metadata.json",
558
588
  "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
559
589
  forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
560
- # Lite runs one managed host: the reduced shape keeps the stack declaration, the environment descriptor and the sealed
561
- # secrets; infra/, runtime/ and seeds/ are the full edition's.
590
+ # Lite has no self-hosted infrastructure (owner ruling 2026-10-12): .starcistacks holds env and secret custody only - the stack
591
+ # declaration, the environment descriptor and the sealed secrets; the stack's one backing service is the Supabase project (the
592
+ # local CLI stack or the cloud project). infra/ (compose, k8s, terraform), runtime/ and seeds/ are the full edition's.
562
593
  lite:
563
594
  requires: []
564
595
  allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep"]
@@ -650,6 +681,19 @@ slots:
650
681
  tier: none
651
682
  tests: none
652
683
  since: 2.1.0
684
+ - id: app.supabase.migrations.archive
685
+ profiles: [app]
686
+ # The migrations an existing app retired at its cutover (owner ruling 2026-10-13, migration baseline policy): the live schema is
687
+ # dumped into ONE baseline migration written to the current standard, marked applied on production without a re-run, and the
688
+ # older files move here. The Supabase CLI applies only supabase/migrations/<stamp>_<name>.sql, never this folder, and no
689
+ # migration rule (L02-L08) judges it; new migrations append after the baseline.
690
+ path: "supabase/migrations/_archive/"
691
+ presence: opt-in
692
+ provider: supabase
693
+ tracked: tracked
694
+ tier: none
695
+ tests: none
696
+ since: 3.0.0
653
697
  - id: app.supabase.seed
654
698
  profiles: [app]
655
699
  path: "supabase/seed.sql"
@@ -693,7 +737,7 @@ slots:
693
737
  tracked: external
694
738
  tier: none
695
739
  tests: none
696
- goesTo: "a be feature: webhooks/<provider> for a third-party callback, api for an endpoint, a cli command on a schedule (pg_cron / pg_net)"
740
+ goesTo: "a be feature: webhooks/<provider> for a third-party callback, api for an endpoint, a job of a pgmq-backed queue or a cli command on a schedule (pg_cron / pg_net)"
697
741
  since: 2.1.0
698
742
 
699
743
  # ----- side root (both sides): what a standalone repository root holds, less the app-root files ----------
@@ -721,8 +765,7 @@ slots:
721
765
  tracked: tracked
722
766
  tier: none
723
767
  tests: none
724
- # Lite: no test tsconfig (no test tree) and no jest configuration; both are forbidden paths there (repo.tests-forbidden).
725
- lite: {path: "{tsconfig.json,tsconfig.build.json,eslint.config.mjs}"}
768
+ # Lite renders the same files (owner ruling 2026-10-12): the unit run and its coverage are full's.
726
769
  coverage: none
727
770
  managedBy: tool-config
728
771
  rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL, HFS_TS_STRICT]
@@ -864,21 +907,6 @@ slots:
864
907
  tests: none
865
908
  goesTo: ".starcistacks/<env>/secrets/<slug>.enc (at the app root); decrypted values only in memory or under %LOCALAPPDATA%/StarCi/secrets/<project>/<env>/ via *_FILE"
866
909
  rules: [HFS_PLAINTEXT_SECRET]
867
- - id: repo.tests-forbidden
868
- profiles: [be, fe]
869
- # Edition lite has no test world, on either side: a spec file, a test directory or a test-tool configuration anywhere in
870
- # a side is a forbidden path (a finding, not an ignored file). In full this slot does not exist and each test file is
871
- # judged by its own slot (the be tests tree, FE_NO_TESTS on the fe side).
872
- editions: [lite]
873
- # No nested braces: braceVariants expands each inner group per outer option. `**/e2e/` also matches a root e2e/ but
874
- # be.root-e2e wins that on specificity; both are forbidden.
875
- path: "{**/*.spec.*,**/*.test.*,**/*-spec.*,src/tests/,**/__tests__/,**/e2e/,**/test-support/,**/__mocks__/,**/jest.*,**/vitest.*,**/playwright.*,**/cypress.config.*,**/tsconfig.e2e.json,**/e2e.yml,**/e2e.yaml}"
876
- presence: forbidden
877
- tracked: external
878
- tier: none
879
- tests: none
880
- goesTo: "the full edition: a lite repository holds no spec, test directory or jest/vitest/playwright configuration"
881
- since: 2.1.0
882
910
  - id: be.root-e2e
883
911
  profiles: [be]
884
912
  path: "e2e/"
@@ -913,25 +941,21 @@ slots:
913
941
  profiles: [be]
914
942
  path: "contracts/<app>/events.json"
915
943
  presence: opt-in
916
- litePresence: forbidden # lite has no event bus, so no async contract to emit
917
944
  tracked: tracked
918
945
  tier: none
919
946
  tests: none
920
947
  coverage: none
921
948
  why: the async contract of a service (`starci/event-contract@1`), emitted by `npm run contract:emit` (`starci app emit`, never written by hand) from the typed event classes of `src/modules/events/<service>/`; the consumers of its events rely on it (R166 HFS_EVENT_CONTRACT)
922
- goesTo: "the full edition: a lite app has no event bus (no Kafka, no outbox), so no async contract exists to emit"
923
949
  rules: [HFS_EVENT_CONTRACT]
924
950
  - id: be.contract.events.pin
925
951
  profiles: [be]
926
952
  path: "contracts/<app>/events.pin.json"
927
953
  presence: opt-in
928
- litePresence: forbidden
929
954
  tracked: tracked
930
955
  tier: none
931
956
  tests: none
932
957
  coverage: none
933
958
  why: the previously published event contract of a service, the same format as its events.json (copied over when the contract is published, never edited); the current events.json may only extend it additively, a breaking change is a new `<event>.v2` key (R177 BE_CONTRACT_BREAKING)
934
- goesTo: "the full edition: a lite app has no event bus, so no published event contract to pin"
935
959
  rules: [BE_CONTRACT_BREAKING]
936
960
 
937
961
  # ----- BE: apps (one slot per app kind; a new kind = a new slot, minor bump) ---------------------------
@@ -955,14 +979,12 @@ slots:
955
979
  path: "apps/<app>/src/"
956
980
  appKind: worker # composes transport/message modules and job processors only (an api app may compose them too: a worker app is for separate scaling)
957
981
  presence: opt-in
958
- litePresence: forbidden # lite has no queue or event bus to consume; background work is a cli command on a schedule
959
982
  tracked: tracked
960
983
  tier: app
961
984
  owner: true
962
985
  requires: [main.ts, app.module.ts]
963
986
  allows: [main.ts, app.module.ts, "<app>.options.ts"]
964
987
  tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
965
- goesTo: "the full edition: a lite app has no worker (scheduled work is a cli command invoked by pg_cron / pg_net or a platform cron)"
966
988
  coverage: none
967
989
  rules: [BE_APP_COMPOSITION_ONLY, BE_BACKGROUND_UNOWNED, BE_CONTEXT_OWNER]
968
990
  - id: be.app.cli
@@ -1007,7 +1029,6 @@ slots:
1007
1029
  - id: be.feature.application
1008
1030
  profiles: [be]
1009
1031
  path: "src/features/{api/,jobs/,reactors/,saga/}<feature>/application/" # inside a trigger kind (jobs, reactors, saga) the feature folder is one level down
1010
- lite: {path: "src/features/api/<feature>/application/"} # lite has only the api kind; the other roots are forbidden slots
1011
1032
  presence: required
1012
1033
  tracked: tracked
1013
1034
  tier: feature
@@ -1021,7 +1042,6 @@ slots:
1021
1042
  path: "src/features/saga/<saga>/"
1022
1043
  pattern: saga # enabled by `patterns: ["saga"]` in hfs.json sides.be (R169 to R173); an undeclared saga folder is HFS_SLOT_NOT_ENABLED
1023
1044
  presence: opt-in # a saga: the orchestrator of a business flow that crosses services, started and advanced only by events (knowledge/patterns/be/saga.yaml)
1024
- litePresence: forbidden
1025
1045
  trigger: saga
1026
1046
  tracked: tracked
1027
1047
  tier: feature
@@ -1029,7 +1049,6 @@ slots:
1029
1049
  requires: [index.ts, "<saga>.module.ts", "<saga>.saga.service.ts", "<saga>.saga-state.ts", steps/, compensations/, transport/]
1030
1050
  allows: [index.ts, "<saga>.module.ts", "<saga>.saga.service.ts", "<saga>.saga.service.spec.ts", "<saga>.saga-state.ts", "<saga>.saga.log-events.ts", application/, steps/, compensations/, transport/] # the orchestrator is a `*.service.ts` because a handler injects only services; unit-tested beside it
1031
1051
  tests: unit-beside # <saga>.saga.service.spec.ts beside the orchestrator; the state machine and its fence are platform/saga's service; each compensation path has an e2e spec
1032
- goesTo: "the full edition: a lite app has no saga (no event bus to advance it)"
1033
1052
  coverage: none
1034
1053
  rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED, BE_SAGA_STEP_COMPENSATION, BE_SAGA_STATE_VERSIONED, BE_SAGA_EVENT_CONTRACT, BE_SAGA_E2E_MISSING]
1035
1054
  - id: be.feature.saga.steps
@@ -1037,12 +1056,10 @@ slots:
1037
1056
  path: "src/features/saga/<saga>/steps/"
1038
1057
  pattern: saga
1039
1058
  presence: opt-in
1040
- litePresence: forbidden
1041
1059
  tracked: tracked
1042
1060
  tier: feature
1043
1061
  allows: ["<step>.saga-step.ts"] # one step of the flow: its name and the event it puts on the wire (the contract); its work is done by the domain, not by the step
1044
1062
  tests: none
1045
- goesTo: "the full edition: a lite app has no saga"
1046
1063
  coverage: none
1047
1064
  rules: [BE_SAGA_STEP_COMPENSATION, BE_SAGA_EVENT_CONTRACT]
1048
1065
  - id: be.feature.saga.compensations
@@ -1050,18 +1067,15 @@ slots:
1050
1067
  path: "src/features/saga/<saga>/compensations/"
1051
1068
  pattern: saga
1052
1069
  presence: opt-in
1053
- litePresence: forbidden
1054
1070
  tracked: tracked
1055
1071
  tier: feature
1056
1072
  allows: ["<step>.compensation.ts"] # REQUIRED for every step: the command that undoes it, naming the failure event that triggers it
1057
1073
  tests: none
1058
- goesTo: "the full edition: a lite app has no saga"
1059
1074
  coverage: none
1060
1075
  rules: [BE_SAGA_STEP_COMPENSATION, BE_SAGA_EVENT_CONTRACT, BE_SAGA_E2E_MISSING]
1061
1076
  - id: be.feature.application.support
1062
1077
  profiles: [be]
1063
1078
  path: "src/features/{api/,jobs/,reactors/,saga/}<feature>/application/support/"
1064
- lite: {path: "src/features/api/<feature>/application/support/"}
1065
1079
  presence: optional # helpers and types local to ONE feature; same tier as application, never a shared home
1066
1080
  tracked: tracked
1067
1081
  tier: feature # feature tier: another feature cannot import it (features never import features)
@@ -1099,7 +1113,6 @@ slots:
1099
1113
  profiles: [be]
1100
1114
  path: "src/features/{api/,reactors/,saga/}<feature>/transport/message/"
1101
1115
  presence: opt-in # queue consumer: composed by the service that owns it (an api app) or by a worker app scaled apart
1102
- litePresence: forbidden # a consumer reads the event bus, which lite does not have
1103
1116
  pattern: event-bus # a consumer reads the event bus; declaring the pattern enables the slot
1104
1117
  tracked: tracked
1105
1118
  tier: feature
@@ -1108,7 +1121,6 @@ slots:
1108
1121
  tests: none
1109
1122
  coverage: none
1110
1123
  composedBy: [api, worker]
1111
- goesTo: "the full edition: a lite app has no event bus, so no message transport (a third-party callback is a webhooks feature)"
1112
1124
  rules: [BE_BACKGROUND_UNOWNED, BE_SAGA_CONSUMER_DEDUPE]
1113
1125
  # ----- BE: feature kinds that are not an api: a webhook door and a realtime door. Each kind is a folder of src/features/ named by
1114
1126
  # the kind and holds one folder per provider or channel; a kind imports only modules/* (never another kind) and only an
@@ -1139,9 +1151,6 @@ slots:
1139
1151
  requires: ["<provider>-http.module.ts", "<provider>.webhook.ts", "<provider>.webhook.spec.ts"]
1140
1152
  allows: ["<provider>-http.module.ts", "<provider>.webhook.ts", "<provider>.webhook.spec.ts", dto/]
1141
1153
  tests: unit-beside # the door's own spec (<provider>.webhook.spec.ts) beside the door: the one non-service unit spec of this kind
1142
- lite: # a lite app has no test world: the door and its module only
1143
- requires: ["<provider>-http.module.ts", "<provider>.webhook.ts"]
1144
- allows: ["<provider>-http.module.ts", "<provider>.webhook.ts", dto/]
1145
1154
  coverage: none
1146
1155
  composedBy: [api]
1147
1156
  rules: [BE_WEBHOOK_SHAPE, BE_WEBHOOK_UNVERIFIED, BE_DEFAULT_DENY, BE_INPUT_BOUNDED]
@@ -1163,7 +1172,6 @@ slots:
1163
1172
  profiles: [be]
1164
1173
  path: "src/features/realtime/<channel>/"
1165
1174
  presence: opt-in # a push channel: reads and pushes only (a socket gateway or a GraphQL subscription), no domain write
1166
- litePresence: forbidden # under lite realtime is Supabase Realtime (the client subscribes to channels; nothing serves them in be/)
1167
1175
  pattern: realtime # declaring the pattern (hfs.json sides.be.patterns) enables the kind (knowledge/patterns/be/realtime.yaml)
1168
1176
  trigger: realtime
1169
1177
  tracked: tracked
@@ -1172,7 +1180,11 @@ slots:
1172
1180
  requires: [index.ts, transport/]
1173
1181
  allows: [index.ts, transport/]
1174
1182
  tests: none
1175
- goesTo: "the full edition: under lite realtime is Supabase Realtime (RLS-governed), not a be kind"
1183
+ lite:
1184
+ why: >-
1185
+ a push channel on the same socket.io gateway as full (owner ruling 2026-10-13): lite runs the gateway inside the be api
1186
+ process, one instance and no Redis adapter; the front end listens through hooks/socketio
1187
+ (knowledge/patterns/be/realtime.yaml, liteAdapter)
1176
1188
  coverage: none
1177
1189
  rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED]
1178
1190
  since: 1.0.0
@@ -1180,11 +1192,11 @@ slots:
1180
1192
  profiles: [be]
1181
1193
  path: "src/features/realtime/<channel>/transport/graphql/"
1182
1194
  presence: opt-in # a GraphQL subscription over graphql-ws
1183
- litePresence: forbidden
1195
+ litePresence: forbidden # a graphql-ws server is self-hosted push infrastructure: lite pushes through Supabase Realtime
1184
1196
  pattern: realtime
1185
1197
  tracked: tracked
1186
1198
  tier: feature
1187
- goesTo: "the full edition: under lite realtime is Supabase Realtime, not a be kind"
1199
+ goesTo: "src/features/realtime/<channel>/transport/websocket/ (be.feature.realtime.websocket): both editions push through the socket.io gateway; lite runs it in the api process"
1188
1200
  requires: ["<channel>-graphql.module.ts", "<channel>.subscription.ts", "<channel>.subscription.spec.ts"]
1189
1201
  allows: ["<channel>-graphql.module.ts", "<channel>.subscription.ts", "<channel>.subscription.spec.ts", dto/]
1190
1202
  tests: unit-beside # the door's own spec beside the door (a subscription has no service of its own)
@@ -1196,11 +1208,11 @@ slots:
1196
1208
  profiles: [be]
1197
1209
  path: "src/features/realtime/<channel>/transport/graphql/dto/"
1198
1210
  presence: opt-in
1199
- litePresence: forbidden
1211
+ litePresence: forbidden # the dto of the graphql door, which lite does not serve
1200
1212
  pattern: realtime
1201
1213
  tracked: tracked
1202
1214
  tier: feature
1203
- goesTo: "the full edition: under lite realtime is Supabase Realtime, not a be kind"
1215
+ goesTo: "src/features/realtime/<channel>/transport/websocket/ (be.feature.realtime.websocket): both editions push through the socket.io gateway; lite runs it in the api process"
1204
1216
  allows: ["<name>.{input,type,args}.ts"]
1205
1217
  tests: none
1206
1218
  coverage: none
@@ -1210,12 +1222,10 @@ slots:
1210
1222
  - id: be.feature.realtime.websocket
1211
1223
  profiles: [be]
1212
1224
  path: "src/features/realtime/<channel>/transport/websocket/"
1213
- presence: opt-in # a socket gateway
1214
- litePresence: forbidden
1225
+ presence: opt-in # a socket.io gateway; lite runs it in the api process (owner ruling 2026-10-13)
1215
1226
  pattern: realtime
1216
1227
  tracked: tracked
1217
1228
  tier: feature
1218
- goesTo: "the full edition: under lite realtime is Supabase Realtime, not a be kind"
1219
1229
  requires: ["<channel>-websocket.module.ts", "<channel>.gateway.ts", "<channel>.gateway.spec.ts"]
1220
1230
  allows: ["<channel>-websocket.module.ts", "<channel>.gateway.ts", "<channel>.gateway.spec.ts"]
1221
1231
  tests: unit-beside # the door's own spec beside the door (a gateway has no service of its own)
@@ -1246,6 +1256,18 @@ slots:
1246
1256
  coverage: none
1247
1257
  rules: [BE_CLI_COMMAND_SHAPE, BE_CLI_OWNER, BE_CLI_REQUIRED, BE_MODULE_SHAPE, BE_SCHEMA_AUTHORITY]
1248
1258
 
1259
+ - id: be.assets
1260
+ profiles: [be]
1261
+ # Small binary assets bundled with the build (fonts a renderer embeds, a watermark): one folder per kind (owner ruling
1262
+ # 2026-10-13, HFS assets policy). Large or user media (music, video, uploads) live in object storage and are fetched at runtime;
1263
+ # templates and copy are TS constants in the owning capability (*.templates.ts). There is no be/resources/.
1264
+ path: "src/assets/<kind>/"
1265
+ presence: optional
1266
+ tracked: tracked
1267
+ tier: none
1268
+ tests: none
1269
+ coverage: none
1270
+ since: 3.0.0
1249
1271
  # ----- BE: modules (three tiers, closed set) -----------------------------------------------------------
1250
1272
  - id: be.domain
1251
1273
  profiles: [be]
@@ -1255,7 +1277,9 @@ slots:
1255
1277
  tier: domain
1256
1278
  owner: true
1257
1279
  requires: [index.ts]
1258
- allows: ["<capability>.module.ts", "<capability>.config.ts", "<capability>.options.ts", errors/, messages/, persistence/, policies/, "*.service.ts", "*.service.spec.ts", "*.contracts.ts"]
1280
+ # <name>.templates.ts holds the capability's static templates, copy and presets as TS constants (owner ruling 2026-10-13);
1281
+ # user-editable ones are seed rows, large or user media live in object storage.
1282
+ allows: ["<capability>.module.ts", "<capability>.config.ts", "<capability>.options.ts", errors/, messages/, persistence/, policies/, "*.service.ts", "*.service.spec.ts", "*.contracts.ts", "*.templates.ts"]
1259
1283
  forbids: [testing/, exceptions/, utils/, helpers/, shared/]
1260
1284
  tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts, nothing else
1261
1285
  coverage: required
@@ -1340,12 +1364,16 @@ slots:
1340
1364
  profiles: [be]
1341
1365
  path: "src/modules/platform/event-bus/"
1342
1366
  presence: opt-in
1343
- litePresence: forbidden # Kafka is exactly the footprint lite removes
1344
1367
  pattern: event-bus # the ONE durable event transport: transactional outbox, relay, Kafka, inbox, retry and dead letter (knowledge/patterns/be/event-bus.yaml)
1345
1368
  tracked: tracked
1346
1369
  tier: platform
1347
1370
  owner: true
1348
- goesTo: "the full edition: a lite app has no event bus (no Kafka, no outbox)"
1371
+ lite:
1372
+ why: >-
1373
+ the same EventPublisher and EventConsumer ports on Supabase Queues (pgmq): the outbox relay sends each committed row to the
1374
+ pgmq queue of every consumer group of its topic through the shared EntityManager, a consumer reads with a visibility
1375
+ timeout, retries by letting it lapse and archives the message on dead letter (read_ct over the budget); a single api may
1376
+ deliver in process. No Kafka (knowledge/patterns/be/supabase-queues.yaml)
1349
1377
  requires: [index.ts, event-bus.module.ts, event-bus.port.ts, event-bus.contracts.ts, event-bus.service.ts, event-bus.service.spec.ts]
1350
1378
  tests: unit-beside
1351
1379
  coverage: required
@@ -1355,12 +1383,16 @@ slots:
1355
1383
  profiles: [be]
1356
1384
  path: "src/modules/platform/queue/"
1357
1385
  presence: opt-in
1358
- litePresence: forbidden # BullMQ/Redis is exactly the footprint lite removes
1359
1386
  pattern: queue # the ONE BullMQ owner: the outbox relay into queues (jobId = outbox id) and job schedulers (knowledge/patterns/be/queues.yaml)
1360
1387
  tracked: tracked
1361
1388
  tier: platform
1362
1389
  owner: true
1363
- goesTo: "the full edition: a lite app has no job queue (a scheduled action is a cli command; pg_cron / pg_net fires it)"
1390
+ lite:
1391
+ why: >-
1392
+ the same QueueOutbox and QueueWorkerRegistry ports on Supabase Queues (pgmq): the relay sends each committed outbox row with
1393
+ pgmq.send (the outbox id travels in the message, so a second send is a no-op at the processor's inbox), processors read
1394
+ with a visibility timeout, back off by extending it, and pgmq.archive a message whose read_ct exceeds the budget (the dead
1395
+ letter); a schedule is a pg_cron job that enqueues. No BullMQ or Redis (knowledge/patterns/be/supabase-queues.yaml)
1364
1396
  requires: [index.ts, queue.module.ts, queue.port.ts, queue.contracts.ts, queue-outbox.service.ts, queue-outbox.service.spec.ts]
1365
1397
  tests: unit-beside
1366
1398
  coverage: required
@@ -1370,12 +1402,10 @@ slots:
1370
1402
  profiles: [be]
1371
1403
  path: "src/modules/events/<service>/"
1372
1404
  presence: opt-in
1373
- litePresence: forbidden # no event bus, so no typed events
1374
1405
  pattern: event-bus # typed event classes of one service; each equals an entry of be/contracts/<service>/events.json
1375
1406
  tracked: tracked
1376
1407
  tier: events
1377
1408
  owner: true
1378
- goesTo: "the full edition: a lite app has no event bus, so no modules/events"
1379
1409
  requires: [index.ts]
1380
1410
  allows: ["<event>.event.ts", index.ts]
1381
1411
  tests: none
@@ -1385,12 +1415,10 @@ slots:
1385
1415
  profiles: [be]
1386
1416
  path: "src/modules/queues/<queue>/"
1387
1417
  presence: opt-in
1388
- litePresence: forbidden # no queue, so no typed producers
1389
1418
  pattern: queue # a typed producer of one queue: enqueueX(payload, tx) writes the outbox; its schedulers live beside it
1390
1419
  tracked: tracked
1391
1420
  tier: queues
1392
1421
  owner: true
1393
- goesTo: "the full edition: a lite app has no job queue, so no modules/queues"
1394
1422
  requires: [index.ts, "<queue>.queue.ts"] # the module exists when a domain module imports the producer; a scheduler-only queue has none
1395
1423
  allows: ["<queue>.queue.ts", "<queue>.module.ts", "<queue>.contracts.ts", index.ts]
1396
1424
  tests: none
@@ -1400,7 +1428,6 @@ slots:
1400
1428
  profiles: [be]
1401
1429
  path: "src/features/reactors/<reactor>/"
1402
1430
  presence: opt-in
1403
- litePresence: forbidden # no event bus, so nothing reacts to a domain event
1404
1431
  pattern: event-bus # a reactor consumes events of another context and keeps a local copy or triggers a projection
1405
1432
  trigger: reactors
1406
1433
  tracked: tracked
@@ -1409,14 +1436,12 @@ slots:
1409
1436
  requires: [index.ts, "<reactor>.module.ts", transport/]
1410
1437
  allows: [index.ts, "<reactor>.module.ts", application/, transport/]
1411
1438
  tests: none
1412
- goesTo: "the full edition: a lite app has no event bus, so no reactors (a third-party callback is a webhooks feature)"
1413
1439
  coverage: none
1414
1440
  rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED]
1415
1441
  - id: be.feature.jobs
1416
1442
  profiles: [be]
1417
1443
  path: "src/features/jobs/<job>/"
1418
1444
  presence: opt-in
1419
- litePresence: forbidden # no fenced-job platform, so no job tree
1420
1445
  pattern: fenced-job # a background job: a processor with a fenced claim, steps and the application it dispatches to
1421
1446
  trigger: jobs
1422
1447
  tracked: tracked
@@ -1425,14 +1450,12 @@ slots:
1425
1450
  requires: [index.ts, "<job>.module.ts", steps/, transport/]
1426
1451
  allows: [index.ts, "<job>.module.ts", application/, steps/, transport/]
1427
1452
  tests: none
1428
- goesTo: "the full edition: a lite app has no job queue (a scheduled action is a cli command; pg_cron / pg_net fires it)"
1429
1453
  coverage: none
1430
1454
  rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED]
1431
1455
  - id: be.feature.jobs.queue
1432
1456
  profiles: [be]
1433
1457
  path: "src/features/jobs/<job>/transport/queue/"
1434
1458
  presence: opt-in # the queue transport of the job: its module and the processor the job runner hands the fenced claim to
1435
- litePresence: forbidden
1436
1459
  pattern: fenced-job
1437
1460
  tracked: tracked
1438
1461
  tier: feature
@@ -1441,31 +1464,26 @@ slots:
1441
1464
  tests: none
1442
1465
  coverage: none
1443
1466
  composedBy: [api, worker]
1444
- goesTo: "the full edition: a lite app has no job queue"
1445
1467
  rules: [BE_BACKGROUND_UNOWNED]
1446
1468
  - id: be.feature.jobs.steps
1447
1469
  profiles: [be]
1448
1470
  path: "src/features/jobs/<job>/steps/"
1449
1471
  presence: opt-in
1450
- litePresence: forbidden
1451
1472
  pattern: fenced-job # one external effect per step; it takes the claimed job and writes through the guarded methods
1452
1473
  tracked: tracked
1453
1474
  tier: feature
1454
1475
  allows: ["<step>.step.ts"]
1455
1476
  tests: none
1456
- goesTo: "the full edition: a lite app has no job queue"
1457
1477
  coverage: none
1458
1478
  rules: [BE_JOB_SHAPE, BE_JOB_RUN_KEY]
1459
1479
  - id: be.platform.jobs
1460
1480
  profiles: [be]
1461
1481
  path: "src/modules/platform/jobs/"
1462
1482
  presence: opt-in
1463
- litePresence: forbidden # no fenced-job primitive without a queue
1464
1483
  pattern: fenced-job # the ONE fencing primitive: the job entity, the claim that bumps the token, guarded writes (knowledge/patterns/be/jobs.yaml)
1465
1484
  tracked: tracked
1466
1485
  tier: platform
1467
1486
  owner: true
1468
- goesTo: "the full edition: a lite app has no job queue (a scheduled action is a cli command)"
1469
1487
  requires: [index.ts, jobs.module.ts, jobs.port.ts, jobs.contracts.ts, job-claim.service.ts, job-claim.service.spec.ts, fenced.processor.ts]
1470
1488
  tests: unit-beside
1471
1489
  coverage: required
@@ -1475,14 +1493,15 @@ slots:
1475
1493
  profiles: [be]
1476
1494
  path: "src/modules/projections/<name>/"
1477
1495
  presence: opt-in
1478
- litePresence: forbidden # a read model replayed off an event stream needs the event bus lite does not have
1479
1496
  pattern: projection # a read model: only its projection writes its table; recompute is idempotent and replayable; readers use get*
1480
1497
  tracked: tracked
1481
1498
  tier: projections
1482
1499
  owner: true
1483
- goesTo: "the full edition: a lite app has no event stream to project; a read shape is a view or a read-* in fe modules/db"
1484
1500
  requires: [index.ts, "<name>.module.ts", "<name>.projection.ts", "<name>.projection-entity.ts"]
1485
1501
  allows: [index.ts, "<name>.module.ts", "<name>.module-definition.ts", "<name>.options.ts", "<name>.projection.ts", "<name>.projection-entity.ts", "<name>.contracts.ts", persistence/]
1502
+ lite: # the read model's table is a supabase migration with its rows type (lite's schema authority): no TypeORM projection entity
1503
+ requires: [index.ts, "<name>.module.ts", "<name>.projection.ts"]
1504
+ allows: [index.ts, "<name>.module.ts", "<name>.module-definition.ts", "<name>.options.ts", "<name>.projection.ts", "<name>.contracts.ts", persistence/]
1486
1505
  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)
1487
1506
  coverage: required
1488
1507
  rules: [BE_PROJECTION_WRITE_OWNER, BE_PROJECTION_SHAPE, BE_TIER_DIRECTION, BE_CONTEXT_COUPLING]
@@ -1533,10 +1552,15 @@ slots:
1533
1552
  profiles: [be]
1534
1553
  path: "src/tests/world/"
1535
1554
  presence: optional
1536
- litePresence: forbidden # a lite app has no test world
1537
1555
  tracked: tracked
1538
1556
  tier: e2e
1539
- goesTo: "the full edition: a lite app has no test world (no containers, fakes or useTestWorld)"
1557
+ lite:
1558
+ why: >-
1559
+ the world of the optional integration specs of a lite app: global-setup.ts connects to the local Supabase CLI stack
1560
+ (`starci supabase start`, with the migrations applied by the local stack) and starts no container, no toxiproxy and no stack of its
1561
+ own; use-test-world.ts exports useTestWorld({ modules: [...] }) -> world.db.<connection> (the shared EntityManager) and
1562
+ world.fake.<provider> (a network-edge fake of an external SaaS). There is no world.apps (no e2e) and no world.infra
1563
+ (no self-hosted service to cut)
1540
1564
  allows: [global-setup.ts, global-teardown.ts, use-test-world.ts, "fakes/<provider>/server.ts", fakes/] # jest globalSetup/globalTeardown default-export (Jest API); fakes/<provider>/server.ts is the fake server of a provider, a literal file name BE_SOURCE_FORM reads as its role; plus root files with a role suffix (test-world-files, R47)
1541
1565
  tests: none
1542
1566
  coverage: none
@@ -1553,10 +1577,8 @@ slots:
1553
1577
  profiles: [be]
1554
1578
  path: "src/tests/world/kit/"
1555
1579
  presence: optional
1556
- litePresence: forbidden
1557
1580
  tracked: tracked
1558
1581
  tier: e2e
1559
- goesTo: "the full edition: a lite app has no test world"
1560
1582
  allows: ["<name>.ts"] # plain kebab names, like platform/primitives (BE-CONVENTION 1.11 names primitives/time.ts): the world's inlined test helpers (poll, free-ports)
1561
1583
  tests: none
1562
1584
  coverage: none
@@ -1565,10 +1587,8 @@ slots:
1565
1587
  profiles: [be]
1566
1588
  path: "src/tests/fixtures/"
1567
1589
  presence: optional
1568
- litePresence: forbidden
1569
1590
  tracked: tracked
1570
1591
  tier: fixtures
1571
- goesTo: "the full edition: a lite app has no test fixtures"
1572
1592
  allows: [i18n/] # i18n/ is the one folder that may carry localized text
1573
1593
  tests: none
1574
1594
  coverage: none
@@ -1577,10 +1597,8 @@ slots:
1577
1597
  profiles: [be]
1578
1598
  path: "src/tests/fixtures/builders/*.builder.ts"
1579
1599
  presence: optional
1580
- litePresence: forbidden
1581
1600
  tracked: tracked
1582
1601
  tier: fixtures
1583
- goesTo: "the full edition: a lite app has no test fixtures"
1584
1602
  tests: none
1585
1603
  coverage: none
1586
1604
  why: >-
@@ -1593,10 +1611,8 @@ slots:
1593
1611
  profiles: [be]
1594
1612
  path: "src/tests/fixtures/i18n/"
1595
1613
  presence: optional
1596
- litePresence: forbidden
1597
1614
  tracked: tracked
1598
1615
  tier: fixtures
1599
- goesTo: "the full edition: a lite app has no test fixtures"
1600
1616
  allows: ["<name>.<role>.ts"] # data files that deliberately carry localized text (a real Vietnamese string a parser or formatter must accept); no specs here
1601
1617
  tests: none
1602
1618
  coverage: none
@@ -1605,23 +1621,25 @@ slots:
1605
1621
  profiles: [be]
1606
1622
  path: "src/tests/integration/<capability>/*.integration-spec.ts"
1607
1623
  presence: optional
1608
- litePresence: forbidden
1609
1624
  tracked: tracked
1610
1625
  tier: e2e
1611
1626
  tests: e2e
1612
- goesTo: "the full edition: a lite app has no test tree"
1613
1627
  coverage: none
1628
+ lite:
1629
+ why: >-
1630
+ optional under lite: one capability module through useTestWorld({ modules }) against the local Supabase CLI stack
1631
+ (`starci supabase start`), run by test:integration; no container, no toxiproxy and no outage through the world (R112 is full's)
1614
1632
  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
1615
1633
  rules: [BE_TEST_TOPOLOGY, BE_INTEGRATION_SPEC_MISSING]
1616
1634
  - id: be.tests.e2e
1617
1635
  profiles: [be]
1618
1636
  path: "src/tests/e2e/<area>/*.e2e-spec.ts"
1619
1637
  presence: optional
1620
- litePresence: forbidden
1638
+ litePresence: forbidden # owner ruling 2026-10-12: a lite app has no e2e world
1621
1639
  tracked: tracked
1622
1640
  tier: e2e
1623
1641
  tests: e2e
1624
- goesTo: "the full edition: a lite app has no test tree"
1642
+ goesTo: "the full edition: a lite app has no e2e world; its logic is proven by unit specs and its database by optional integration specs on `starci supabase start`"
1625
1643
  coverage: none
1626
1644
  why: flow e2e through useTestWorld({ apps }); external services are the world's network fakes; run by test:e2e
1627
1645
  rules: [HFS_CI_E2E_AUTOMATIC, BE_TEST_TOPOLOGY, BE_E2E_FLOW]
@@ -1629,11 +1647,11 @@ slots:
1629
1647
  profiles: [be]
1630
1648
  path: "src/tests/contract/<provider>/*.contract-spec.ts"
1631
1649
  presence: optional
1632
- litePresence: forbidden
1650
+ litePresence: forbidden # owner ruling 2026-10-12: a lite app has no contract world
1633
1651
  tracked: tracked
1634
1652
  tier: e2e
1635
1653
  tests: e2e
1636
- goesTo: "the full edition: a lite app has no test tree"
1654
+ goesTo: "the full edition: a lite app has no contract world (provider sandbox specs)"
1637
1655
  coverage: none
1638
1656
  why: provider sandbox contracts, skipped without sandbox config; run only by test:contract, never part of test or test:e2e
1639
1657
  rules: [BE_TEST_TOPOLOGY]
@@ -1674,22 +1692,25 @@ slots:
1674
1692
  presence: required
1675
1693
  tracked: tracked
1676
1694
  tier: route
1677
- allows: ["[locale]/**/{page,layout,template,loading,error,not-found,default}.tsx", "[locale]/**/route.ts", "[locale]/providers.tsx",
1695
+ # Owner ruling 2026-10-13 (R212 case-2, lite = full): routes are thin and the front end serves no API - health/live is the one
1696
+ # route handler of this slot, the Supabase auth callback is fe.route.callback, and no file holds a Server Action
1697
+ # (FE_ROUTE_HANDLER_FORBIDDEN, FE_SERVER_ACTION_FORBIDDEN).
1698
+ allows: ["[locale]/**/{page,layout,template,loading,error,not-found,default}.tsx", "[locale]/providers.tsx",
1678
1699
  global-error.tsx, page.tsx, # the root page.tsx only redirects (FE_ROUTE_FILES_THIN)
1679
1700
  not-found.tsx, layout.tsx, # next-intl: a request outside every locale renders the root not-found inside a root layout
1680
- "health/{live,ready}/route.ts", "api/**/route.ts", "_*/**", providers.tsx, globals.css,
1701
+ health/live/route.ts, "_*/**", providers.tsx, globals.css,
1681
1702
  "{icon,favicon,apple-icon,opengraph-image,twitter-image}.*", robots.ts, sitemap.ts, manifest.ts]
1682
1703
  # providers.tsx (root or [locale]) is the client wrapper the server layout renders around its children to host context providers.
1683
1704
  roles: {page: page.tsx, layout: layout.tsx, template: template.tsx, loading: loading.tsx, error: error.tsx, not-found: not-found.tsx, default: default.tsx, route: route.ts, global-error: global-error.tsx, providers: providers.tsx}
1684
1705
  tests: none
1685
- rules: [FE_ROUTE_FILES_THIN, FE_CLIENT_BOUNDARY, FE_OWNER_REACHABLE]
1706
+ rules: [FE_ROUTE_FILES_THIN, FE_CLIENT_BOUNDARY, FE_OWNER_REACHABLE, FE_ROUTE_HANDLER_FORBIDDEN, FE_SERVER_ACTION_FORBIDDEN]
1686
1707
  - id: fe.source-root-pinned
1687
1708
  profiles: [fe]
1688
- path: "apps/<app>/src/{proxy.ts,instrumentation.ts,instrumentation-client.ts}"
1709
+ path: "apps/<app>/src/{proxy.ts,instrumentation.ts,instrumentation-client.ts,sentry.server.config.ts,sentry.edge.config.ts}"
1689
1710
  presence: optional
1690
1711
  tracked: tracked
1691
1712
  tier: route
1692
- roles: {proxy: proxy.ts, instrumentation: instrumentation.ts, instrumentation-client: instrumentation-client.ts}
1713
+ roles: {proxy: proxy.ts, instrumentation: instrumentation.ts, instrumentation-client: instrumentation-client.ts, sentry: "sentry.*.config.ts"}
1693
1714
  tests: none
1694
1715
  rules: [FE_NEXT_CONVENTIONS] # middleware.ts is refused on Next >= 16 (fe.source-root-middleware)
1695
1716
  - id: fe.source-root-middleware
@@ -1703,9 +1724,9 @@ slots:
1703
1724
  rules: [FE_NEXT_CONVENTIONS]
1704
1725
  - id: fe.route.callback
1705
1726
  profiles: [fe]
1706
- # The auth callback of a Supabase session (the PKCE code exchange): the one non-health route handler of an app with a Supabase
1707
- # connection, in either edition (an upgrade lite -> full leaves it as it is, fe/** is never touched); an app without one has no
1708
- # route handler in fe/ (L15).
1727
+ # The auth callback of a Supabase session (the PKCE code exchange), kept only when the auth flow truly needs a server exchange
1728
+ # (prefer the client-side PKCE exchange in fe.modules.auth): the one non-health route handler of an app with a Supabase
1729
+ # connection; an app without one has no route handler in fe/ (L15).
1709
1730
  path: "apps/<app>/src/app/auth/callback/route.ts"
1710
1731
  presence: opt-in
1711
1732
  provider: supabase
@@ -1741,6 +1762,22 @@ slots:
1741
1762
  tests: none
1742
1763
  budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
1743
1764
  rules: [FE_CONNECTED_BLOCK_RENDER_PAIR, FE_I18N_LITERAL, FE_NATIVE_FORM_CONTROL, FE_CLIENT_BOUNDARY]
1765
+ - id: fe.components.providers
1766
+ profiles: [fe]
1767
+ # The client providers the root layout mounts once (owner ruling 2026-10-13, fe-stack theme/state-global/data): the redux store,
1768
+ # the SWR configuration, the theme (next-themes), the HeroUI toast region and the side-effect components (SwrSideEffects,
1769
+ # SocketIoSideEffects). A provider wires state, so it sits at the feature tier, above components.
1770
+ path: "apps/<app>/src/components/providers/<name>/"
1771
+ presence: optional
1772
+ tracked: tracked
1773
+ tier: feature
1774
+ owner: true
1775
+ requires: [index.tsx]
1776
+ allows: [index.tsx]
1777
+ roles: {entry: index.tsx}
1778
+ tests: none
1779
+ since: 3.0.0
1780
+ rules: [FE_CLIENT_BOUNDARY]
1744
1781
  - id: fe.hooks
1745
1782
  profiles: [fe]
1746
1783
  path: "apps/<app>/src/hooks/<domain>/"
@@ -1754,6 +1791,116 @@ slots:
1754
1791
  tests: none
1755
1792
  budget: {file: 200, dataHooks: 6, useState: 6}
1756
1793
  rules: [FE_HOOKS_ARE_HOOKS, FE_DATA_FRESHNESS]
1794
+ - id: fe.hooks.swr
1795
+ profiles: [fe]
1796
+ # The data hooks (fe-stack data, FE-DATA-SWR): one useSWR / useSWRMutation wrapper per modules/api operation, keyed
1797
+ # QUERY_<X>_SWR_KEY / MUTATE_<X>_SWR_KEY plus the identity of the request; SwrSideEffects.tsx runs the app-wide hooks once.
1798
+ path: "apps/<app>/src/hooks/swr/"
1799
+ presence: optional
1800
+ tracked: tracked
1801
+ tier: hooks
1802
+ owner: true
1803
+ requires: [index.ts]
1804
+ allows: [index.ts, SwrSideEffects.tsx, api/index.ts, "api/{graphql,rest}/index.ts", "api/{graphql,rest}/{queries,mutations}/index.ts",
1805
+ "api/graphql/queries/useQuery*Swr.ts", "api/graphql/mutations/useMutate*Swr.ts",
1806
+ "api/rest/queries/useGet*Swr.ts", "api/rest/mutations/use{Post,Put,Patch,Delete}*Swr.ts"]
1807
+ roles: {entry: index.ts, effects: SwrSideEffects.tsx}
1808
+ tests: none
1809
+ budget: {file: 200}
1810
+ since: 3.0.0
1811
+ rules: [FE_HOOKS_ARE_HOOKS, FE_DATA_FRESHNESS]
1812
+ - id: fe.hooks.rhf
1813
+ profiles: [fe]
1814
+ # The forms (fe-stack forms, FE-FORM): react-hook-form with the zod resolver; the schema carries next-intl messages and the
1815
+ # submit runs an SWR mutation through modules/toast.
1816
+ path: "apps/<app>/src/hooks/rhf/"
1817
+ presence: optional
1818
+ tracked: tracked
1819
+ tier: hooks
1820
+ owner: true
1821
+ requires: [index.ts]
1822
+ allows: [index.ts, "use*Form.ts"]
1823
+ roles: {entry: index.ts}
1824
+ tests: none
1825
+ budget: {file: 200}
1826
+ since: 3.0.0
1827
+ rules: [FE_HOOKS_ARE_HOOKS]
1828
+ - id: fe.hooks.zustand
1829
+ profiles: [fe]
1830
+ # Feature-local client state (fe-stack state-local, FE-STATE-ZUSTAND): one store per folder.
1831
+ path: "apps/<app>/src/hooks/zustand/<store>/"
1832
+ presence: optional
1833
+ tracked: tracked
1834
+ tier: hooks
1835
+ owner: true
1836
+ requires: [index.ts, store.ts]
1837
+ allows: [index.ts, store.ts, "use*.ts"]
1838
+ roles: {entry: index.ts, store: store.ts}
1839
+ tests: none
1840
+ budget: {file: 200}
1841
+ since: 3.0.0
1842
+ rules: [FE_HOOKS_ARE_HOOKS]
1843
+ - id: fe.hooks.socketio
1844
+ profiles: [fe]
1845
+ # Realtime (fe-stack realtime, FE-REALTIME): one socket.io-client singleton per namespace in sockets.ts and one side-effect
1846
+ # component that wires the listeners to redux and SWR.
1847
+ path: "apps/<app>/src/hooks/socketio/"
1848
+ presence: optional
1849
+ tracked: tracked
1850
+ tier: hooks
1851
+ owner: true
1852
+ requires: [index.ts, sockets.ts, SocketIoSideEffects.tsx]
1853
+ allows: [index.ts, sockets.ts, SocketIoSideEffects.tsx, connectionStore.ts, "enums/*.ts", "types/*.ts", "use*.ts"]
1854
+ roles: {entry: index.ts, effects: SocketIoSideEffects.tsx}
1855
+ tests: none
1856
+ budget: {file: 200}
1857
+ since: 3.0.0
1858
+ rules: [FE_HOOKS_ARE_HOOKS]
1859
+ - id: fe.hooks.effects
1860
+ profiles: [fe]
1861
+ # App-wide side effects (route params synced into redux, the auth bootstrap): use<X>.ts hooks and the <X>SideEffect(s).tsx
1862
+ # components that run them once; a provider in components/providers mounts those components (a route never imports hooks).
1863
+ path: "apps/<app>/src/hooks/effects/"
1864
+ presence: optional
1865
+ tracked: tracked
1866
+ tier: hooks
1867
+ owner: true
1868
+ requires: [index.ts]
1869
+ allows: [index.ts, "use*.ts", "*SideEffect.tsx", "*SideEffects.tsx"]
1870
+ roles: {entry: index.ts, effects: "*SideEffect.tsx"}
1871
+ tests: none
1872
+ budget: {file: 200}
1873
+ since: 3.0.0
1874
+ rules: [FE_HOOKS_ARE_HOOKS]
1875
+ - id: fe.hooks.reuseables
1876
+ profiles: [fe]
1877
+ # Reusable generic hooks (fe-stack utils): no domain, no data.
1878
+ path: "apps/<app>/src/hooks/reuseables/"
1879
+ presence: optional
1880
+ tracked: tracked
1881
+ tier: hooks
1882
+ owner: true
1883
+ requires: [index.ts]
1884
+ allows: [index.ts, "use*.ts"]
1885
+ roles: {entry: index.ts}
1886
+ tests: none
1887
+ budget: {file: 200}
1888
+ since: 3.0.0
1889
+ rules: [FE_HOOKS_ARE_HOOKS]
1890
+ - id: fe.redux
1891
+ profiles: [fe]
1892
+ # App-wide shared state (fe-stack state-global, FE-STATE-REDUX): the store, its typed hooks, the provider and one file per slice.
1893
+ path: "apps/<app>/src/redux/"
1894
+ presence: optional
1895
+ tracked: tracked
1896
+ tier: modules
1897
+ owner: true
1898
+ requires: [index.ts, store.ts, hooks.ts]
1899
+ allows: [index.ts, store.ts, hooks.ts, ReduxProvider.tsx, "slices/*.ts"]
1900
+ roles: {entry: index.ts, store: store.ts, hooks: hooks.ts} # hooks.ts: the typed useAppDispatch/useAppSelector of the store
1901
+ tests: none
1902
+ budget: {file: 400}
1903
+ since: 3.0.0
1757
1904
  - id: fe.modules
1758
1905
  profiles: [fe]
1759
1906
  path: "apps/<app>/src/modules/<capability>/"
@@ -1772,32 +1919,38 @@ slots:
1772
1919
  presence: optional
1773
1920
  tracked: tracked
1774
1921
  tier: transport
1775
- # The app's reads of the one client. A repository has exactly ONE transport client and ONE Outcome union: the app's
1776
- # client.ts/outcome.ts (fe.transport.client/outcome) in a one-app repository, or the api package's
1777
- # (fe.package.api.client/outcome) when the repository shares it (FE_TRANSPORT_OWNER, machine).
1922
+ # The browser's calls to the back end (owner ruling 2026-10-13, fe-stack graphql/rest/codegen, FE-API-GRAPHQL): the auth and
1923
+ # no-auth Apollo clients with their link chain (graphql/clients), one file per operation (graphql/queries/query-<x>.ts,
1924
+ # graphql/mutations/mutation-<x>.ts with its gql document map and typed request/response in types/), the generated wire types
1925
+ # (graphql/__generated__) and the axios calls (rest/<area>/*.ts) for uploads, presigned URLs and auth endpoints. A repository has
1926
+ # exactly ONE client folder and ONE GraphQLResponse union: the app's (fe.transport.client/outcome) when one app calls the back end,
1927
+ # or the api package's (fe.package.api.client/outcome) when several do (FE_TRANSPORT_OWNER, machine).
1778
1928
  requires: [index.ts]
1779
- allows: [index.ts, client.ts, outcome.ts, "<domain>/read-*.ts", "<domain>/*.graphql", "<domain>/<domain>.mapper.ts", __generated__/]
1780
- roles: {entry: index.ts, reader: "read-*.ts"} # a reader is a server module (FE_CLIENT_REACHES_SERVER)
1929
+ allows: [index.ts, graphql/index.ts, graphql/types.ts, "graphql/clients/**", "graphql/queries/query-*.ts", "graphql/queries/types/*.ts",
1930
+ "graphql/mutations/mutation-*.ts", "graphql/mutations/types/*.ts", "graphql/__generated__/**", "rest/index.ts", "rest/<area>/*.ts"]
1931
+ roles: {entry: index.ts, query: "query-*.ts", mutation: "mutation-*.ts"}
1932
+ # A hook imports its one operation file directly (one file per operation, FE-API-GRAPHQL), never through a barrel of every operation.
1933
+ entries: ["graphql/queries/query-*.ts", "graphql/queries/types/*.ts", "graphql/mutations/mutation-*.ts", "graphql/mutations/types/*.ts"]
1781
1934
  tests: none
1782
1935
  rules: [FE_TRANSPORT_OWNER, FE_HTTP_STATUS_COLLAPSE, FE_WIRE_GENERATED]
1783
1936
  - id: fe.transport.client
1784
1937
  profiles: [fe]
1785
- path: "apps/<app>/src/modules/api/client.ts"
1938
+ path: "apps/<app>/src/modules/api/graphql/clients/"
1786
1939
  presence: optional
1787
1940
  tracked: tracked
1788
1941
  tier: transport
1789
1942
  tests: none
1790
- why: the one fetch of a one-app repository (timeout, abort, 401/403 -> refused); a shared client is fe.package.api.client
1943
+ why: the auth and no-auth Apollo clients and their link chain (retry, error, timeout, token refresh, attach access token, http) of the one app that calls the back end; when several apps do, the shared client is fe.package.api.client
1791
1944
  rules: [FE_TRANSPORT_OWNER]
1792
1945
  - id: fe.transport.outcome
1793
1946
  profiles: [fe]
1794
- path: "apps/<app>/src/modules/api/outcome.ts"
1947
+ path: "apps/<app>/src/modules/api/graphql/types.ts"
1795
1948
  presence: optional
1796
1949
  tracked: tracked
1797
1950
  tier: transport
1798
1951
  outcomeHome: true
1799
1952
  tests: none
1800
- why: the one Outcome union of a one-app repository; a shared union is fe.package.api.outcome
1953
+ why: the one GraphQLResponse union ({success, message, error, data}) of the one app that calls the back end; when several apps do, the shared union is fe.package.api.outcome
1801
1954
  rules: [FE_HTTP_STATUS_COLLAPSE]
1802
1955
  - id: fe.modules.config
1803
1956
  profiles: [fe]
@@ -1844,47 +1997,50 @@ slots:
1844
1997
  tier: foundation
1845
1998
  tests: none
1846
1999
  why: shared types with no runtime behaviour; a component may import them
1847
- - id: fe.modules.db
2000
+ - id: fe.modules.types.ambient
2001
+ parent: fe.modules.types
1848
2002
  profiles: [fe]
1849
- # The db transport of an app on Supabase (the ONLY place that imports @supabase/ssr or @supabase/supabase-js, L10): the
1850
- # server client factory (cookies, anon key, RLS applies), the browser client factory, getPrincipal() (React cache,
1851
- # auth.getClaims()) and toOutcome() mapping {data, error} into the same Outcome vocabulary modules/api uses. Readers
1852
- # live in <domain>/read-*.ts, Server Actions (schema-parsed input) in <domain>/write-*.ts; the row types come from
1853
- # supabase/types/database.types.ts (sides.fe.reads). The barrel index.ts is server-only (it re-exports server code), so
1854
- # browser.ts, the browser client factory a client hook imports, is a second public entry of the owner (`entries`).
1855
- path: "apps/<app>/src/modules/db/"
1856
- presence: opt-in # enabled by a connection with provider: supabase; required under edition lite
1857
- provider: supabase
1858
- litePresence: required
2003
+ path: "apps/<app>/src/modules/types/ambient/"
2004
+ presence: optional
1859
2005
  tracked: tracked
1860
- tier: transport
1861
- requires: [index.ts, server.ts, browser.ts, principal.ts]
1862
- allows: [index.ts, server.ts, browser.ts, principal.ts, outcome.ts, "<domain>/read-*.ts", "<domain>/write-*.ts", "<domain>/<domain>.mapper.ts"]
1863
- entries: [browser.ts]
1864
- anonymousActions: [auth/write-sign-in.ts, auth/write-sign-out.ts] # the Server Actions that run for an anonymous principal by design (FE_AUTH_SESSION_TRUST)
2006
+ tier: foundation
2007
+ owner: true
2008
+ allows: ["*.d.ts"]
1865
2009
  tests: none
1866
- since: 2.1.0
1867
- - id: fe.modules.db.outcome
2010
+ since: 2.2.0
2011
+ why: global ambient declarations loaded by the app compiler; this owner exposes no runtime API
2012
+ - id: fe.modules.auth
1868
2013
  profiles: [fe]
1869
- # A lite app has no modules/api: its db transport is the transport, so the one Outcome union lives here (toOutcome maps
1870
- # {data, error} into it). Under full the union stays fe.transport.outcome / fe.package.api.outcome.
1871
- editions: [lite]
1872
- path: "apps/<app>/src/modules/db/outcome.ts"
1873
- presence: opt-in
1874
- provider: supabase
1875
- litePresence: optional
2014
+ # The identity-provider client (fe-stack auth, FE-AUTH): the session, the access token the Apollo link attaches and the
2015
+ # sign-in/sign-out calls. With a Supabase connection it is the ONLY fe importer of @supabase/supabase-js and uses its auth API
2016
+ # only; Supabase on the front end never reads or writes data (owner ruling 2026-10-13).
2017
+ path: "apps/<app>/src/modules/auth/"
2018
+ presence: optional
1876
2019
  tracked: tracked
1877
- tier: transport
1878
- outcomeHome: true
2020
+ tier: modules
2021
+ owner: true
2022
+ requires: [index.ts]
1879
2023
  tests: none
1880
- why: the one Outcome union of a lite app; its db owner is the transport
1881
- rules: [FE_HTTP_STATUS_COLLAPSE]
1882
- since: 2.1.0
2024
+ since: 3.0.0
2025
+ rules: [FE_SUPABASE_CLIENT_OWNER, FE_SERVICE_ROLE_FORBIDDEN]
2026
+ - id: fe.modules.toast
2027
+ profiles: [fe]
2028
+ # Mutation feedback (fe-stack toast, FE-TOAST): the HeroUI toast wrapper, showGraphQLToast over a GraphQLResponse and the
2029
+ # use*WithToast hooks with next-intl labels.
2030
+ path: "apps/<app>/src/modules/toast/"
2031
+ presence: optional
2032
+ tracked: tracked
2033
+ tier: modules
2034
+ owner: true
2035
+ requires: [index.ts, toast.tsx, api.ts, hooks.ts]
2036
+ allows: [index.ts, toast.tsx, api.ts, hooks.ts]
2037
+ roles: {entry: index.ts, hooks: hooks.ts} # hooks.ts: the use*WithToast hooks of the toast module
2038
+ tests: none
2039
+ since: 3.0.0
1883
2040
  - id: fe.package.ui
1884
2041
  profiles: [fe]
1885
2042
  path: "packages/<family>-ui/"
1886
2043
  presence: opt-in
1887
- litePresence: optional # a lite app scaffolds no packages; one appears by need, without an optionalSlots entry
1888
2044
  tracked: tracked
1889
2045
  tier: package
1890
2046
  owner: true
@@ -1899,38 +2055,36 @@ slots:
1899
2055
  profiles: [fe]
1900
2056
  path: "packages/<family>-api/"
1901
2057
  presence: opt-in
1902
- litePresence: optional
1903
2058
  tracked: tracked
1904
2059
  tier: package
1905
2060
  owner: true
1906
- requires: [package.json, src/index.ts, src/client.ts, src/outcome.ts, tsconfig.json]
2061
+ requires: [package.json, src/index.ts, src/graphql/clients/index.ts, src/graphql/types.ts, tsconfig.json]
1907
2062
  tests: none
1908
2063
  why: the one transport client and the one Outcome union shared by every app of the repository; no app keeps its own
1909
2064
  rules: [FE_TRANSPORT_OWNER, FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT]
1910
2065
  - id: fe.package.api.client
1911
2066
  profiles: [fe]
1912
- path: "packages/<family>-api/src/client.ts"
2067
+ path: "packages/<family>-api/src/graphql/clients/"
1913
2068
  presence: optional # enabled with its package (fe.package.api is the opt-in)
1914
2069
  tracked: tracked
1915
2070
  tier: package
1916
2071
  tests: none
1917
- why: the repository's one fetch (timeout, abort, 401/403 -> refused)
2072
+ why: the repository's one pair of Apollo clients and their link chain
1918
2073
  rules: [FE_TRANSPORT_OWNER]
1919
2074
  - id: fe.package.api.outcome
1920
2075
  profiles: [fe]
1921
- path: "packages/<family>-api/src/outcome.ts"
2076
+ path: "packages/<family>-api/src/graphql/types.ts"
1922
2077
  presence: optional # enabled with its package (fe.package.api is the opt-in)
1923
2078
  tracked: tracked
1924
2079
  tier: package
1925
2080
  outcomeHome: true
1926
2081
  tests: none
1927
- why: the repository's one Outcome union
2082
+ why: the repository's one GraphQLResponse union
1928
2083
  rules: [FE_HTTP_STATUS_COLLAPSE]
1929
2084
  - id: fe.package.i18n
1930
2085
  profiles: [fe]
1931
2086
  path: "packages/<family>-i18n/"
1932
2087
  presence: opt-in
1933
- litePresence: optional
1934
2088
  tracked: tracked
1935
2089
  tier: package
1936
2090
  owner: true
@@ -1938,20 +2092,6 @@ slots:
1938
2092
  tests: none
1939
2093
  why: the next-intl stack (routing, navigation, request config) written once, exported as the createAppI18n factory every app calls
1940
2094
  rules: [FE_I18N_PLACEMENT, FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT]
1941
- - id: fe.package.db
1942
- profiles: [fe]
1943
- # The shared Supabase client factories once a second fe app needs them (server, browser, principal, toOutcome) -
1944
- # mirroring fe.transport.client vs fe.package.api.client; the one Outcome union is re-exported from
1945
- # fe.package.api.outcome when the api package exists.
1946
- path: "packages/<family>-db/"
1947
- presence: opt-in
1948
- provider: supabase
1949
- tracked: tracked
1950
- tier: package
1951
- owner: true
1952
- requires: [package.json, src/index.ts, tsconfig.json]
1953
- tests: none
1954
- since: 2.1.0
1955
2095
 
1956
2096
  # Checks that read this manifest (all ship from .claude; none keeps its own path list)
1957
2097
  consumers: