@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.
- package/CHANGELOG.md +19 -1
- package/README.md +1 -0
- package/client-boundary.mjs +66 -23
- package/docs/client-boundary.md +9 -9
- package/docs/hooks-folder.md +16 -4
- package/docs/hygiene.md +2 -2
- package/docs/transport.md +26 -26
- package/docs/type-safety.md +6 -6
- package/fe-stack.mjs +86 -0
- package/formatting.mjs +12 -6
- package/hooks-folder.mjs +73 -31
- package/hygiene.mjs +4 -4
- package/icon.mjs +25 -17
- package/index.mjs +2 -0
- package/lib/fe-stack.mjs +73 -0
- package/lib/scope.mjs +23 -2
- package/lib/why.mjs +23 -13
- package/next-conventions.mjs +4 -3
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +207 -15
- package/runtime/knowledge/hfs/fe-stack.yaml +218 -0
- package/runtime/knowledge/hfs/slots.yaml +319 -179
- package/runtime/knowledge/patterns/fe/folder.yaml +24 -18
- package/runtime/knowledge/sonar-gate.yaml +10 -2
- package/runtime/modules/kernel/removed-vocabulary.yaml +48 -0
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +0 -2
- package/runtime/scripts/hfs/architecture/config.mjs +3 -2
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +6 -2
- package/runtime/scripts/hfs/architecture/error-masked.mjs +10 -1
- package/runtime/scripts/hfs/architecture/frontend.mjs +10 -3
- package/runtime/scripts/hfs/architecture/hfs.mjs +6 -3
- package/runtime/scripts/hfs/architecture/index.mjs +2 -0
- package/runtime/scripts/hfs/architecture/owners.mjs +19 -16
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +4 -1
- package/runtime/scripts/hfs/architecture/server-action.mjs +75 -20
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +2 -2
- package/runtime/scripts/hfs/architecture/supabase.mjs +36 -210
- package/runtime/scripts/hfs/architecture/tiers.mjs +1 -33
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +110 -54
- package/runtime/scripts/hfs/architecture/type-context.mjs +4 -1
- package/runtime/scripts/hfs/edition-slots.mjs +13 -4
- package/runtime/scripts/hfs/manifest-shape.mjs +3 -3
- package/runtime/scripts/hfs/rule-catalog.mjs +21 -1
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +4 -5
- package/served-locale.mjs +10 -13
- package/the-split.mjs +5 -2
- package/transport.mjs +81 -67
- 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
|
|
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@
|
|
10
|
-
version:
|
|
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":
|
|
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
|
-
#
|
|
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).
|
|
89
|
-
#
|
|
90
|
-
#
|
|
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
|
|
305
|
-
# migration tree (supabase/migrations/*.sql, forward-only), so a lite back end holds no TypeORM
|
|
306
|
-
# be/ (R34/R35 read schemaAuthority; be.persistence's lite overlay allows neither).
|
|
307
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
561
|
-
# secrets;
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
1707
|
-
#
|
|
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
|
|
1776
|
-
#
|
|
1777
|
-
#
|
|
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,
|
|
1780
|
-
|
|
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/
|
|
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
|
|
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/
|
|
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
|
|
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.
|
|
2000
|
+
- id: fe.modules.types.ambient
|
|
2001
|
+
parent: fe.modules.types
|
|
1848
2002
|
profiles: [fe]
|
|
1849
|
-
|
|
1850
|
-
|
|
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:
|
|
1861
|
-
|
|
1862
|
-
allows: [
|
|
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.
|
|
1867
|
-
|
|
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
|
-
#
|
|
1870
|
-
#
|
|
1871
|
-
|
|
1872
|
-
path: "apps/<app>/src/modules/
|
|
1873
|
-
presence:
|
|
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:
|
|
1878
|
-
|
|
2020
|
+
tier: modules
|
|
2021
|
+
owner: true
|
|
2022
|
+
requires: [index.ts]
|
|
1879
2023
|
tests: none
|
|
1880
|
-
|
|
1881
|
-
rules: [
|
|
1882
|
-
|
|
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/
|
|
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/
|
|
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
|
|
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/
|
|
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
|
|
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:
|