@starci/eslint-canon-fe 8.2.18 → 8.4.0

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 (92) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +19 -1
  3. package/callback-shape.mjs +344 -0
  4. package/client-boundary.mjs +1 -1
  5. package/docs/hooks-folder.md +1 -1
  6. package/docs/size-and-state-budget.md +6 -6
  7. package/drawing.mjs +65 -28
  8. package/env-owner.mjs +120 -9
  9. package/function-budget.mjs +49 -0
  10. package/hooks-folder.mjs +149 -18
  11. package/icon.mjs +67 -0
  12. package/index.mjs +47 -0
  13. package/lib/own-hook-origin.mjs +53 -0
  14. package/lib/program-for.mjs +22 -0
  15. package/lib/pure-owner-helper.mjs +148 -0
  16. package/lib/redux-hook-origin.mjs +44 -0
  17. package/next-conventions.mjs +38 -35
  18. package/package.json +1 -1
  19. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  20. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  21. package/runtime/knowledge/hfs/slots.yaml +430 -92
  22. package/runtime/knowledge/patterns/fe/folder.yaml +4 -0
  23. package/runtime/knowledge/sonar-gate.yaml +1 -1
  24. package/runtime/modules/models/runtimes.yaml +1 -1
  25. package/runtime/packages/hfs/templates/app/package-scripts/package.json +1 -0
  26. package/runtime/scripts/api/git/cat-file.mjs +5 -0
  27. package/runtime/scripts/api/git/diff.mjs +5 -0
  28. package/runtime/scripts/api/git/lib.mjs +3 -0
  29. package/runtime/scripts/api/git/merge-base.mjs +9 -0
  30. package/runtime/scripts/api/git/recorded-lines.mjs +54 -8
  31. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +103 -0
  32. package/runtime/scripts/hfs/architecture/backend.mjs +22 -10
  33. package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
  34. package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
  35. package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
  36. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  37. package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
  38. package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
  39. package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
  40. package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
  41. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  42. package/runtime/scripts/hfs/architecture/dead-exports.mjs +23 -50
  43. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  44. package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
  45. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
  46. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  47. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  48. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  49. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
  50. package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
  51. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +49 -25
  52. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  53. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  54. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  55. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  56. package/runtime/scripts/hfs/architecture/register-once.mjs +15 -7
  57. package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
  58. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  59. package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
  60. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +214 -17
  61. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
  62. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  63. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/tiers.mjs +11 -4
  65. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
  66. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +9 -13
  67. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  68. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  69. package/runtime/scripts/hfs/manifest-shape.mjs +91 -6
  70. package/runtime/scripts/hfs/path-findings.mjs +32 -2
  71. package/runtime/scripts/hfs/project-rule.mjs +3 -1
  72. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  73. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  74. package/runtime/scripts/hfs/rules/secrets.mjs +26 -0
  75. package/runtime/scripts/hfs/slot-imports.mjs +18 -3
  76. package/runtime/scripts/hfs/slot-required.mjs +13 -0
  77. package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
  78. package/runtime/scripts/hfs/slots.mjs +1 -0
  79. package/runtime/scripts/lib/env.mjs +20 -0
  80. package/runtime/scripts/lib/event-contract.mjs +107 -0
  81. package/runtime/scripts/lib/json-clone.mjs +61 -0
  82. package/runtime/scripts/lib/list.mjs +4 -0
  83. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  84. package/runtime/scripts/lib/stack-services.mjs +1 -1
  85. package/runtime/scripts/lib/ts-analysis.mjs +23 -0
  86. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  87. package/served-locale.mjs +80 -9
  88. package/size-and-state-budget.mjs +11 -8
  89. package/size-growth.mjs +10 -6
  90. package/swr-family.mjs +197 -0
  91. package/translation.mjs +135 -7
  92. package/transport.mjs +126 -59
@@ -7,7 +7,7 @@
7
7
  # catalog READ this file through scripts/hfs/slots.mjs; none of them hardcodes a path.
8
8
  # Shape: modules/schemas/hfs-slots.schema.yaml. The repository side is hfs.json: modules/schemas/hfs-repo.schema.yaml.
9
9
  schema: starci/hfs-slots@3
10
- version: 3.0.0
10
+ version: 3.3.0
11
11
 
12
12
  versioning:
13
13
  # MAJOR.MINOR.PATCH of this manifest. A repository pins only the MAJOR in hfs.json ("hfs": 3).
@@ -25,12 +25,19 @@ versioning:
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
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
28
+ # 3.0.0 is a major (ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4)): lite is the full code structure deployed with Supabase backing services
29
+ # and without e2e, and the front end is one standard for both editions (knowledge/hfs/fe-stack.yaml). The
30
30
  # front-end database slots and the Supabase realtime feature slot are removed (Supabase on the front end is auth only, realtime
31
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
32
  # marked `since: 3.0.0` are added: the Apollo, SWR, RHF, zustand and socket.io hooks, redux, modules/auth, modules/toast, the
33
33
  # providers, be.assets and the migration archive.
34
+ # 3.1.0 is a minor (owner ruling 2026-10-14, R245 and R246): every be.app.* slot allows `<app>.env.ts` and the composition groups
35
+ # `compose/<area>.ts` and budgets its app files (`app.module.ts` 250, `<app>.options.ts` 200, `<app>.env.ts` 150,
36
+ # `compose/<area>.ts` 120, `main.ts` 80); `<app>.env.ts` exports interface <App>Env and read<App>Env(env: EnvSource): <App>Env
37
+ # for fail-fast env parsing only, and `<app>.options.ts` holds env-derived options and port bindings only, with no parsing
38
+ # or environment reads (BE_APP_OPTIONS_COMPOSITION). Secrets use EnvSource.secret(): <NAME>_FILE first, plain <NAME> only
39
+ # when the file key is absent; .starcistacks owns custody. Capability <capability>.config.ts roles stay unchanged.
40
+ # 3.3.0 is a minor (alpha.16): it ADDS the optional app.ci-stack-deploy slot, the managed workflow of the starci app stack verbs.
34
41
 
35
42
  # Field meaning, per slot.
36
43
  # id stable name, never changes meaning inside a major.
@@ -77,16 +84,16 @@ versioning:
77
84
  # liteManagedBy is managed only in lite; its effective lite.path still lists literal files.
78
85
  # the managed file list lives here and nowhere in code. A difference is HFS_MANAGED_FILE_DRIFT (a
79
86
  # back end's tsconfig.json: HFS_TS_STRICT when a flag or an option was added).
80
- # rules rule ids (the catalog lands with knowledge/hfs/rules.yaml).
87
+ # rules rule codes from knowledge/hfs/rules.yaml.
81
88
  # editions the editions (below) the slot exists in; default all of them. A slot outside the declared edition owns
82
89
  # no path - under lite, what full keeps absent is removed, and what lite forbids is a finding.
83
90
  # litePresence the presence under edition lite (default: presence): a bare value, or {be, fe} for a both-sides slot.
84
91
  # 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.
92
+ # database/broker services, the e2e layer and its workflow, and the browser journey. Lite may have contract specs.
93
+ # editionWhy why the slot's presence, shape or adapter differs between lite and full, with the owning ruling citation.
87
94
  # lite the fields the slot carries under lite only (path, requires, allows, forbids, minInstances,
88
95
  # 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).
96
+ # `why` under lite names the adapter behind the slot's ports (pgmq, Postgres locks, in-process socket.io).
90
97
  # `tests` is never overlaid: a lite slot owes exactly the unit specs its full slot owes.
91
98
  # provider the hfs.json connections[].provider that enables this opt-in slot (supabase); never an app kind.
92
99
  # since the manifest version that introduced the slot.
@@ -94,17 +101,18 @@ presenceValues: [required, optional, opt-in, forbidden]
94
101
  trackedValues: [tracked, ignored, external]
95
102
  testValues: [unit-beside, e2e, none]
96
103
 
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,
104
+ # The editions a repository declares in hfs.json `edition` (absent means full). ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite IS
105
+ # the full architecture with Supabase backing services - the same monorepo, slots, CQRS shape and patterns (queue,
99
106
  # event bus, saga, reactors, projection, realtime, webhooks, fenced job), declared in hfs.json the same way, and the same unit
100
107
  # 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).
108
+ # on Supabase except realtime: schema authority in supabase/migrations, queue and event-bus ports on Supabase Queues (pgmq),
109
+ # cache and locks on Postgres, schedules on pg_cron (+ pg_net), storage and auth on Supabase. Realtime uses the socket.io gateway
110
+ # in the api process, one instance and no Redis adapter. Integration and provider sandbox contract specs are optional;
111
+ # declared patterns owe their integration proofs (R284). Lite has no e2e world. Production uses declared docker-swarm under
112
+ # .starcistacks/production/infra/swarm/ (owner ruling 2026-10-14); no self-hosted database or broker. The front end is full's (R97).
105
113
  # Lite is this same manifest filtered: a slot whose `editions` lacks lite does not exist for a lite app, `litePresence` replaces
106
114
  # 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.
115
+ # self-hosted backing services); downgrade is not supported. Lite contracts are allowed (owner decision 2026-10-14).
108
116
  editions: [full, lite]
109
117
 
110
118
  # 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
@@ -139,6 +147,7 @@ naming:
139
147
  # ---------------------------------------------------------------------------------------------------------
140
148
  # Tiers and the dependency direction matrix (owner granularity; type-only imports count; no cycles anywhere)
141
149
  # mayImport lists the tiers a file of that tier may import. Importing inside the same owner is always allowed.
150
+ # mayImportKinds narrows a permitted tier edge to its named from/to folder kinds; other kind pairs remain forbidden.
142
151
  # ---------------------------------------------------------------------------------------------------------
143
152
  tiers:
144
153
  be:
@@ -155,12 +164,12 @@ tiers:
155
164
  package: {mayImport: [package]}
156
165
  fe:
157
166
  route: {mayImport: [feature, foundation, modules, transport, package]} # app/** adapters
158
- feature: {mayImport: [components, hooks, foundation, modules, transport, package]}
167
+ feature: {mayImport: [feature, components, hooks, foundation, modules, package], mayImportKinds: {feature: {from: [pages, layouts], to: [overlays]}}} # pages and layouts compose overlays and blocks; data goes through hooks/swr
159
168
  # A component never reaches data: config, routes, i18n and types are foundation; every other module and the api
160
169
  # transport are reached through a hook (owner decision 2026-09-29).
161
170
  components: {mayImport: [components, hooks, foundation, package], lowerLayerOnly: true} # blocks, composites, branches, leaves
162
171
  # Across domains a hook is imported only through the other domain's public index (hooks/<domain>/index.ts), never deep.
163
- hooks: {mayImport: [hooks, foundation, modules, transport, package]}
172
+ hooks: {mayImport: [hooks, foundation, modules, transport, package], mayImportSlots: {transport: {from: [fe.hooks.swr]}}} # only hooks/swr imports an operation
164
173
  foundation: {mayImport: [foundation, package], acyclic: true} # modules/{config,i18n,routes,types}
165
174
  modules: {mayImport: [foundation, modules, transport, package], acyclic: true}
166
175
  transport: {mayImport: [foundation, modules, package]} # modules/api: the one fetch
@@ -171,10 +180,16 @@ crossApp: apps never import each other; shared code is a packages/<pkg> slot.
171
180
  # Parameters the lint factories and checks read per profile (through ruleParams(profile) in scripts/hfs/slots.mjs, which
172
181
  # merges common under the profile: a side restates a common key only to override it).
173
182
  ruleParams:
183
+ db:
184
+ # R259: btree_gist supplies UUID equality for the booking interval exclusion constraint.
185
+ # R259: unaccent is a first-party Supabase extension used by Vietnamese-content search functions.
186
+ extensions: [pgmq, pg_cron, pgcrypto, pg_trgm, vector, uuid-ossp, pg_net, supabase_vault, unaccent, btree_gist]
174
187
  common:
175
188
  # HFS_SIZE_GROWTH: a file above soft may not grow against its parent commit; a new file stays within soft. Slot
176
189
  # budgets (component.tsx 300, hooks 200, modules 400) are stricter than this cap where they apply.
177
190
  fileLines: {soft: 500, hardGrowth: true}
191
+ functionLines: 80
192
+ featureFiles: 250
178
193
  # R21 HFS_DUPLICATE_CODE: the one threshold of duplicate code of both sides (the architecture machine reads it here;
179
194
  # no other file states it): a token-normalised block of at least `lines` source lines and `tokens` tokens that appears twice.
180
195
  duplicateBlock: {lines: 8, tokens: 60}
@@ -321,7 +336,7 @@ ruleParams:
321
336
  # Edition lite (ruleParams(manifest, 'be', 'lite') merges these keys over the base; a map merges one level deep): the schema
322
337
  # authority is the Supabase migration tree (supabase/migrations/*.sql, forward-only), so a lite back end holds no TypeORM
323
338
  # 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 -
339
+ # logic roles and spec doubles are full's (owner ruling 2026-10-12 (owner:lite-production-swarm), with the Work boundary amended by ruling:R212/case-4). infraOwners: the self-hosted libraries are owned nowhere -
325
340
  # platform/queue and platform/event-bus run Supabase Queues (pgmq) through the shared EntityManager, cache and locks are
326
341
  # Postgres (advisory locks or an unlogged table), and realtime is the same socket.io gateway as full, run inside the api
327
342
  # process with no Redis adapter (owner ruling 2026-10-13).
@@ -340,6 +355,7 @@ slots:
340
355
 
341
356
  # ----- app root (profile app: the one repository of a product) --------------------------------------------
342
357
  - id: app.declaration
358
+ since: 3.1.0
343
359
  profiles: [app]
344
360
  path: hfs.json
345
361
  presence: required
@@ -348,6 +364,7 @@ slots:
348
364
  tests: none
349
365
  rules: [HFS_ARCH_CONFIG_UNREAD]
350
366
  - id: app.readme
367
+ since: 3.1.0
351
368
  profiles: [app]
352
369
  path: README.md
353
370
  presence: required
@@ -356,7 +373,9 @@ slots:
356
373
  tests: none
357
374
  rules: [HFS_README_*]
358
375
  - id: app.package-manifest
376
+ since: 3.1.0
359
377
  profiles: [app]
378
+ editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite manages Supabase and wiring scripts; full also manages e2e and stack test scripts. Both may run optional contracts."
360
379
  # The one package.json of the app, at the app root: be's dependencies and every tool, and every script; npm workspaces are
361
380
  # exactly fe/apps/* and fe/packages/* (HFS_MONO_WORKSPACES), each with its own package.json. be has no package.json of its own.
362
381
  path: package.json
@@ -368,6 +387,7 @@ slots:
368
387
  liteManagedBy: package-scripts-lite
369
388
  rules: [HFS_PACKAGE_MANAGER_MIXED, HFS_DEP_VERSION_SKEW, HFS_CANON_PIN_DRIFT, HFS_MANAGED_FILE_DRIFT, HFS_MONO_WORKSPACES, HFS_MONO_WORKSPACE_DEP]
370
389
  - id: app.task-graph
390
+ since: 3.1.0
371
391
  profiles: [app]
372
392
  # The turbo task graph of the app (build, dev, lint, typecheck over the fe workspaces; build depends on ^build and caches
373
393
  # .next and dist): rendered by starci app sync from templates/app/task-graph/turbo.json, judged for drift by starci app check. turbo needs
@@ -380,6 +400,7 @@ slots:
380
400
  managedBy: task-graph
381
401
  rules: [HFS_MANAGED_FILE_DRIFT, HFS_MONO_WORKSPACES]
382
402
  - id: app.lockfile
403
+ since: 3.1.0
383
404
  profiles: [app]
384
405
  path: package-lock.json
385
406
  presence: required
@@ -388,22 +409,42 @@ slots:
388
409
  tests: none
389
410
  rules: [HFS_PACKAGE_MANAGER_MIXED, HFS_DEP_VERSION_SKEW]
390
411
  - id: app.git-meta
412
+ since: 3.1.0
391
413
  profiles: [app]
392
- path: "{.gitignore,.gitattributes}"
414
+ path: .gitignore
393
415
  presence: required
394
416
  tracked: tracked
395
417
  tier: none
396
418
  tests: none
397
419
  rules: [HFS_GITIGNORE_BLOCK_DRIFT] # the marked block of .gitignore is rendered by starci app sync (templates/app/gitignore)
420
+ - id: app.gitattributes
421
+ since: 3.2.0
422
+ profiles: [app]
423
+ path: .gitattributes
424
+ presence: required
425
+ tracked: tracked
426
+ tier: none
427
+ tests: none
428
+ managedBy: skeleton
429
+ liteManagedBy: skeleton-lite
430
+ editionWhy: "Lite renders the same managed file from skeleton-lite because the editions share one code structure while their adapters differ (ruling:R212/case-2, with Work amended by ruling:R212/case-4 and hosting by owner:lite-production-swarm)."
431
+ rules: [HFS_MANAGED_FILE_DRIFT]
398
432
  - id: app.tool-config
433
+ since: 3.1.0
399
434
  profiles: [app]
400
435
  path: "{.editorconfig,.nvmrc}"
401
- presence: required # plain dotfiles, the repository's own
436
+ presence: required
402
437
  tracked: tracked
403
438
  tier: none
404
439
  tests: none
440
+ managedBy: skeleton
441
+ liteManagedBy: skeleton-lite
442
+ editionWhy: "Lite renders the same managed files from skeleton-lite because the editions share one code structure while their adapters differ (ruling:R212/case-2, with Work amended by ruling:R212/case-4 and hosting by owner:lite-production-swarm)."
443
+ rules: [HFS_MANAGED_FILE_DRIFT]
405
444
  - id: app.format-config
445
+ since: 3.1.0
406
446
  profiles: [app]
447
+ editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite additionally ignores Supabase-generated row types when formatting; full has its TypeORM schema authority."
407
448
  # The one formatter configuration of the app (prettier walks up from every file of both sides to it): .prettierrc names
408
449
  # @starci/prettier-config, .prettierignore is the template.
409
450
  path: "{.prettierrc,.prettierignore}"
@@ -415,6 +456,7 @@ slots:
415
456
  liteManagedBy: tool-config-lite # the same files plus the generated supabase/types, which the Supabase CLI writes unformatted
416
457
  rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL]
417
458
  - id: app.tool-config-optional
459
+ since: 3.1.0
418
460
  profiles: [app]
419
461
  path: "{.npmrc,.npmignore}" # .npmignore: npm reads the nearest ignore file instead of the root files negations, so an app that ships inside another package keeps its sealed credentials out of the tarball here
420
462
  presence: optional
@@ -422,6 +464,7 @@ slots:
422
464
  tier: none
423
465
  tests: none
424
466
  - id: app.tool-config-local
467
+ since: 3.1.0
425
468
  profiles: [app]
426
469
  path: "{tsconfig.json,.eslintrc,.eslintrc.*,.eslintignore,eslint.config.*,.stylelintrc,.stylelintrc.*,.stylelintignore,stylelint.config.*,.prettierrc.*,prettier.config.*,jest.config.*,lint-staged.config.*,.lintstagedrc*}"
427
470
  presence: forbidden
@@ -431,6 +474,7 @@ slots:
431
474
  goesTo: "nowhere at the app root: each side holds exactly its managed tool configuration (be.tool-config, fe.tool-config) and the root only the formatter (app.format-config)"
432
475
  rules: [HFS_TOOL_CONFIG_LOCAL]
433
476
  - id: app.quality-config
477
+ since: 3.1.0
434
478
  profiles: [app]
435
479
  path: "{sonar-project.properties,codecov.yml}"
436
480
  presence: required
@@ -440,6 +484,7 @@ slots:
440
484
  managedBy: quality-config # lite renders the same files: the same coverage scope, Sonar import and Codecov flags as full
441
485
  rules: [HFS_SONAR_CONFIG, HFS_MANAGED_FILE_DRIFT]
442
486
  - id: app.hooks
487
+ since: 3.1.0
443
488
  profiles: [app]
444
489
  path: ".husky/{pre-commit,pre-push}"
445
490
  presence: required
@@ -449,7 +494,9 @@ slots:
449
494
  managedBy: hooks
450
495
  rules: [HFS_CI_E2E_AUTOMATIC, HFS_MANAGED_FILE_DRIFT]
451
496
  - id: app.ci
497
+ since: 3.1.0
452
498
  profiles: [app]
499
+ editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite CI proves boot through R272 wiring and checks Supabase artifacts; full also provides its e2e lane."
453
500
  path: ".github/workflows/ci.yml"
454
501
  presence: required
455
502
  tracked: tracked
@@ -459,18 +506,22 @@ slots:
459
506
  liteManagedBy: ci-workflows-lite
460
507
  rules: [HFS_CI_E2E_AUTOMATIC, HFS_CI_MISSING_CANON, HFS_MANAGED_FILE_DRIFT]
461
508
  - id: app.ci-e2e
509
+ since: 3.1.0
462
510
  profiles: [app]
511
+ editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite has no e2e world; R272 supplies its wiring proof, while optional contracts run separately."
463
512
  path: ".github/workflows/e2e.yml"
464
513
  presence: optional # on: workflow_dispatch only
465
- litePresence: forbidden # a lite app has no e2e world (owner ruling 2026-10-12): unit specs run in ci.yml
514
+ litePresence: forbidden # ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite has no e2e world; unit specs and R272 wiring run in ci.yml
466
515
  tracked: tracked
467
516
  tier: none
468
517
  tests: none
469
- goesTo: "the full edition: a lite app has no e2e or contract world; its unit specs run in .github/workflows/ci.yml"
518
+ goesTo: "the full edition: a lite app has no e2e world; its unit specs and wiring proof run in .github/workflows/ci.yml; contract specs use test:contract"
470
519
  managedBy: ci-workflows
471
520
  rules: [HFS_CI_E2E_AUTOMATIC, HFS_MANAGED_FILE_DRIFT]
472
521
  - id: app.ci-images
522
+ since: 3.1.0
473
523
  profiles: [app]
524
+ editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite renders its Supabase-backed image workflow; full renders images for its separately deployed services."
474
525
  # Builds every image of the app (one matrix entry per declared app, path-filtered), never pushes: rendered by starci app sync.
475
526
  path: ".github/workflows/images.yml"
476
527
  presence: required
@@ -482,6 +533,10 @@ slots:
482
533
  rules: [HFS_MANAGED_FILE_DRIFT]
483
534
  - id: app.ci-db
484
535
  profiles: [app]
536
+ exemplifiedBy:
537
+ - packages/hfs/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml
538
+ - {path: tests/fixtures/hfs-callback-full/.github/workflows/db-deploy.yml, edition: full}
539
+ editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite manages its Supabase database deploy workflow; full owns an optional provider-specific workflow."
485
540
  # The database deploy workflow: workflow_dispatch only (like app.ci-e2e); starci app sync renders it under the managed ci group.
486
541
  path: ".github/workflows/db-deploy.yml"
487
542
  presence: optional
@@ -491,7 +546,21 @@ slots:
491
546
  liteManagedBy: ci-workflows-lite
492
547
  rules: [HFS_MANAGED_FILE_DRIFT]
493
548
  since: 2.1.0
549
+ - id: app.ci-stack-deploy
550
+ since: 3.3.0
551
+ profiles: [app]
552
+ editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): both editions deploy their images to a Docker Swarm stack through the same starci stack verbs; the workflow lists the images the app declares."
553
+ # The Swarm stack deploy workflow: workflow_dispatch only (like app.ci-db); a thin file that calls `starci app stack`, rendered by starci app sync.
554
+ path: ".github/workflows/stack-deploy.yml"
555
+ presence: optional
556
+ tracked: tracked
557
+ tier: none
558
+ tests: none
559
+ managedBy: ci-workflows
560
+ liteManagedBy: ci-workflows-lite
561
+ rules: [HFS_MANAGED_FILE_DRIFT]
494
562
  - id: app.dockerignore
563
+ since: 3.1.0
495
564
  profiles: [app]
496
565
  # The one build-context filter of every image (the context is the app root): rendered by starci app sync; it keeps secrets and build output out.
497
566
  path: .dockerignore
@@ -501,8 +570,40 @@ slots:
501
570
  tests: none
502
571
  managedBy: docker-ignore
503
572
  rules: [HFS_MANAGED_FILE_DRIFT]
573
+ - id: app.deploy
574
+ since: 3.1.0
575
+ profiles: [app]
576
+ # Production delivery tooling for the app's own processes: compose files, Caddyfile, *.env.example and ops shell scripts.
577
+ # Lite still keeps its backing services on Supabase and its encrypted custody in app.starcistacks; R06 scans every file here.
578
+ path: "deploy/"
579
+ presence: optional
580
+ tracked: tracked
581
+ tier: none
582
+ tests: none
583
+ rules: [HFS_PLAINTEXT_SECRET]
584
+ - id: app.ci-deploy
585
+ since: 3.1.0
586
+ profiles: [app]
587
+ # Product-owned delivery workflows; the literal app.ci-db path remains the managed database workflow's more specific slot.
588
+ path: ".github/workflows/<app>-deploy.yml"
589
+ presence: optional
590
+ tracked: tracked
591
+ tier: none
592
+ tests: none
593
+ rules: [HFS_PLAINTEXT_SECRET]
594
+ - id: app.ci-authorize-deploy-key
595
+ since: 3.1.0
596
+ profiles: [app]
597
+ path: ".github/workflows/authorize-deploy-key.yml"
598
+ presence: optional
599
+ tracked: tracked
600
+ tier: none
601
+ tests: none
602
+ rules: [HFS_PLAINTEXT_SECRET]
504
603
  - id: app.browser
604
+ since: 3.1.0
505
605
  profiles: [app]
606
+ editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): browser journeys belong to full; lite proves app boot through R272 wiring."
506
607
  # The browser journey of the product (Playwright, owner ruling: one minimal journey per example app, run manually): the config, its
507
608
  # journeys and the script that provisions the stack it runs against. It is the product's, never the front end's (FE_NO_TESTS keeps
508
609
  # refusing any test or test tooling inside fe/). Declared by `browser: true` in hfs.json; the managed script `test:browser` exists only then.
@@ -516,14 +617,31 @@ slots:
516
617
  requires: [playwright.config.ts, journeys/]
517
618
  allows: [playwright.config.ts, stack.mjs, "journeys/*.ts"]
518
619
  - id: app.github-meta
620
+ since: 3.1.0
519
621
  profiles: [app]
520
- path: ".github/{CODEOWNERS,pull_request_template.md,ISSUE_TEMPLATE/**,dependabot.yml}"
622
+ path: ".github/{CODEOWNERS,pull_request_template.md,ISSUE_TEMPLATE/**}"
521
623
  presence: optional
522
624
  tracked: tracked
523
625
  tier: none
524
626
  tests: none
627
+ - id: app.dependabot
628
+ since: 3.2.0
629
+ profiles: [app]
630
+ path: .github/dependabot.yml
631
+ presence: required
632
+ tracked: tracked
633
+ tier: none
634
+ tests: none
635
+ managedBy: skeleton
636
+ liteManagedBy: skeleton-lite
637
+ editionWhy: "Lite renders the same managed file from skeleton-lite because the editions share one code structure while their adapters differ (ruling:R212/case-2, with Work amended by ruling:R212/case-4 and hosting by owner:lite-production-swarm)."
638
+ rules: [HFS_MANAGED_FILE_DRIFT, HFS_DEPENDABOT_MISSING]
525
639
  - id: app.scripts
640
+ since: 3.1.0
526
641
  profiles: [app]
642
+ exemplifiedBy:
643
+ - {path: packages/hfs/templates/app/skeleton/scripts/codegen.mjs, edition: full}
644
+ editionWhy: "Lite manages its Supabase codegen script; full owns operational scripts through the generic path (ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4))."
527
645
  # The app's operational scripts only (`*.mjs`, `*.cjs`, `*.ps1`, `*.sh`), or nothing: an empty folder is `scripts/.gitkeep`, which
528
646
  # `starci app scaffold` writes. A spec or a test (BE_SPEC_PLACEMENT, FE_NO_TESTS), a check or lint source (`check-*`, `eslint-local-rules*`,
529
647
  # a local eslint plugin: HFS_REPO_LOCAL_CHECK) and anything that re-implements a canon check is not an operational script and has no place here.
@@ -536,7 +654,23 @@ slots:
536
654
  path: scripts/codegen.mjs
537
655
  liteManagedBy: codegen-lite
538
656
  rules: [HFS_SCRIPT_ONE_OFF, BE_SPEC_PLACEMENT, HFS_REPO_LOCAL_CHECK] # one-off codemods and fix-* scripts are agent output, not tooling
657
+ - id: app.scripts.delivery
658
+ since: 3.1.0
659
+ profiles: [app]
660
+ exemplifiedBy:
661
+ - {path: tests/fixtures/hfs-delivery-lite/scripts/build-frontend.mjs, edition: lite}
662
+ editionWhy: "Lite splits product delivery tooling from managed codegen; full owns the same files through app.scripts (ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4))."
663
+ editions: [lite]
664
+ # Product-owned environment, secret and build tooling, kept apart from lite's managed codegen script.
665
+ # Full already owns these files through app.scripts; its generic *.mjs pattern must not compete with build-*.mjs.
666
+ path: "scripts/{env-sync.mjs,with-secrets.mjs,set-secrets.ps1,build-*.mjs}"
667
+ presence: optional
668
+ tracked: tracked
669
+ tier: none
670
+ tests: none
671
+ rules: [HFS_PLAINTEXT_SECRET, HFS_SCRIPT_ONE_OFF, BE_SPEC_PLACEMENT, HFS_REPO_LOCAL_CHECK]
539
672
  - id: app.sides
673
+ since: 3.1.0
540
674
  profiles: [app]
541
675
  # The two sides. Everything below be/ and fe/ is judged by the side's own slots (profiles be and fe) with the side folder as its root;
542
676
  # this slot answers only for the side folders themselves.
@@ -546,7 +680,12 @@ slots:
546
680
  tier: none
547
681
  tests: none
548
682
  - id: app.starciwork
683
+ since: 3.1.0
549
684
  profiles: [app]
685
+ exemplifiedBy:
686
+ - {path: packages/hfs/templates/app/skeleton/.starciwork/, edition: full}
687
+ - {path: packages/hfs/templates/app/skeleton-lite/.starciwork/, edition: lite}
688
+ editionWhy: "ruling:R212/case-4: lite optionally keeps .gitignore, workspace.yaml and brand records; full keeps the Work catalog and product summaries."
550
689
  path: ".starciwork/"
551
690
  presence: required
552
691
  tracked: tracked
@@ -556,27 +695,23 @@ slots:
556
695
  allows: [workspace.yaml, index.yaml, "brand/**", shell/index.yaml, "features/<feature>/index.yaml",
557
696
  "features/<feature>/<family>/<name>/index.yaml", "features/<feature>/br/<rule>/ac/<name>/index.yaml",
558
697
  "features/<feature>/ui/<name>/assets/<approved-file>", "features/<feature>/uat/<name>/{index,accounts,fixtures}.yaml",
559
- "features/<feature>/uat/<name>/{seed,cleanup}.sql", "_resources/{identities,environments,fixtures}/<slug>/resource.yaml"]
560
- forbids: [evidence/, logs/, reports/, captures/, runs/, "kernel-*/", "*.sqlite*", _derived/]
561
- # Lite keeps the product catalog, feature overviews, grammar and brand drawing records; UAT records belong to the forbidden slot below.
698
+ "features/<feature>/uat/<name>/{seed,cleanup}.sql", "_resources/{identities,environments,fixtures}/<slug>/resource.yaml",
699
+ "_derived/{index.yaml,frontier.md,critique.yaml,critique.md}"]
700
+ forbids: [evidence/, logs/, reports/, captures/, runs/, "kernel-*/", "*.sqlite*"]
701
+ # ruling:R212/case-4: lite keeps workspace.yaml for project identity and repo roles, plus brand drawing records,
702
+ # references and assets. No brand means no Work tree is needed; workspace.yaml is legal but optional.
703
+ litePresence: optional
562
704
  lite:
563
- requires: [.gitignore, workspace.yaml, index.yaml]
564
- allows: [workspace.yaml, index.yaml, "brand/**", shell/index.yaml, "features/<feature>/index.yaml", "features/<feature>/ui/<name>/index.yaml", "features/<feature>/ui/<name>/assets/<approved-file>"]
705
+ requires: [.gitignore]
706
+ allows: [workspace.yaml, "brand/**"]
565
707
  rules: [HFS_AGENT_DATA_TRACKED, HFS_IDENTITY_CUSTODY]
566
- - id: app.starciwork.uat
567
- profiles: [app]
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.
570
- editions: [lite]
571
- path: ".starciwork/{_resources/identities,_resources/fixtures,features/<feature>/uat,features/<feature>/br}/"
572
- presence: forbidden
573
- tracked: external
574
- tier: none
575
- tests: none
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)"
577
- since: 2.1.0
578
708
  - id: app.starcistacks
709
+ since: 3.1.0
579
710
  profiles: [app]
711
+ editionWhy: "owner:lite-production-swarm: lite requires declared docker-swarm production with Supabase backing; full also admits Compose development, Kubernetes and Terraform."
712
+ exemplifiedBy:
713
+ - {path: packages/hfs/templates/app/skeleton/.starcistacks/, edition: full}
714
+ - {path: packages/hfs/templates/app/skeleton-lite/.starcistacks/, edition: lite}
580
715
  # The stack declarations and the sealed custody of the app, at the app root beside be/, fe/ and .starciwork (never under a side).
581
716
  path: ".starcistacks/"
582
717
  presence: required
@@ -584,18 +719,19 @@ slots:
584
719
  tier: none
585
720
  tests: none
586
721
  requires: [application-stacks.yaml]
587
- allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,k8s,terraform}/**", "<env>/infra/metadata.json",
722
+ allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,swarm,k8s,terraform}/**", "<env>/infra/metadata.json",
588
723
  "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
589
724
  forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
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.
725
+ # Owner ruling 2026-10-14: lite declares docker-swarm production, app runtime config, sealed custody and seed data.
726
+ # Supabase supplies the database and broker; production has no Compose or host cron.
593
727
  lite:
594
- requires: []
595
- allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep"]
728
+ requires: [application-stacks.yaml]
729
+ allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/swarm/**", "<env>/infra/metadata.json",
730
+ "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
596
731
  forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
597
732
  rules: [HFS_STACKS_SHAPE, HFS_STACKS_IN_SIDE, HFS_PLAINTEXT_SECRET, HFS_IDENTITY_CUSTODY]
598
733
  - id: app.sops
734
+ since: 3.1.0
599
735
  profiles: [app]
600
736
  # The sops custody rule of .starcistacks/<env>/secrets/*.enc, at the app root beside the tree it seals.
601
737
  path: .sops.yaml
@@ -604,6 +740,7 @@ slots:
604
740
  tier: none
605
741
  tests: none
606
742
  - id: app.build-output
743
+ since: 3.1.0
607
744
  profiles: [app]
608
745
  path: "{**/node_modules/,coverage/,reports/,test-results/,**/*.tsbuildinfo}"
609
746
  presence: optional
@@ -611,6 +748,7 @@ slots:
611
748
  tier: none
612
749
  tests: none
613
750
  - id: app.tool-cache
751
+ since: 3.1.0
614
752
  profiles: [app]
615
753
  path: "{.eslintcache,.turbo/,.scannerwork/,.sonar/,.jest-cache/,.cache/,.tools/}"
616
754
  presence: forbidden
@@ -619,6 +757,7 @@ slots:
619
757
  tests: none
620
758
  goesTo: "${STARCI_CACHE_HOME:-%LOCALAPPDATA%/StarCi/cache}/<repo>/<tool>/ (the canon presets set eslint --cache-location, jest cacheDirectory, turbo cacheDir, sonar.working.directory)"
621
759
  - id: app.agent-output
760
+ since: 3.1.0
622
761
  profiles: [app]
623
762
  path: "{report*.json,*.tmp.json,%*%,nul,.artifacts/,.tmp-*,*-lint.json,lf.json,lt.json,*.log,design-plans/,.gitmounts/,.dat,vi-flat.txt}"
624
763
  presence: forbidden
@@ -628,6 +767,7 @@ slots:
628
767
  goesTo: "agent scratchpad or the StarCi blob store (%LOCALAPPDATA%/StarCi/blobs), cited by {name, sha256}"
629
768
  rules: [HFS_UNTRACKED_ROOT_ENTRY, HFS_AGENT_DATA_TRACKED]
630
769
  - id: app.worktrees
770
+ since: 3.1.0
631
771
  profiles: [app]
632
772
  path: "{.worktrees/,worktrees/,.starciwork/worktrees/}"
633
773
  presence: forbidden
@@ -636,8 +776,9 @@ slots:
636
776
  tests: none
637
777
  goesTo: "<lanes root>/<project>/<lane>/ (outside every repository) for a lane; a Kernel workflow's worktree is created and owned by Orca outside the app checkout, never tracked, and removed by the runtime's host-side controller once the workflow's finish marked it release-pending"
638
778
  - id: app.plaintext-env
779
+ since: 3.1.0
639
780
  profiles: [app]
640
- path: "{.env,.env.*,.secrets/,**/*.pem,**/*.key}"
781
+ path: "{.env,.env.*,secrets.env,.secrets/,**/*.pem,**/*.key}"
641
782
  presence: forbidden
642
783
  tracked: external
643
784
  tier: none
@@ -648,6 +789,10 @@ slots:
648
789
  # ----- app root: the Supabase project of an app whose connection declares provider: supabase (every lite app) -------
649
790
  - id: app.supabase
650
791
  profiles: [app]
792
+ exemplifiedBy:
793
+ - packages/hfs/templates/app/skeleton-lite/supabase/
794
+ - {path: tests/fixtures/hfs-callback-full/supabase/, edition: full}
795
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): Supabase is required in lite; full enables it only through a declared connection."
651
796
  # The one Supabase project directory, at the app root beside be/, fe/ and .starcistacks (never under a side).
652
797
  path: "supabase/"
653
798
  presence: opt-in # enabled by a connection with provider: supabase; required under edition lite
@@ -659,6 +804,10 @@ slots:
659
804
  since: 2.1.0
660
805
  - id: app.supabase.config
661
806
  profiles: [app]
807
+ exemplifiedBy:
808
+ - packages/hfs/templates/app/skeleton-lite/supabase/config.toml
809
+ - {path: tests/fixtures/hfs-callback-full/supabase/config.toml, edition: full}
810
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires Supabase configuration; full requires it only when Supabase is declared."
662
811
  # The project configuration: auth keys are env(NAME) references, never literals (L08 judges its policy keys).
663
812
  path: "supabase/config.toml"
664
813
  presence: opt-in
@@ -670,6 +819,10 @@ slots:
670
819
  since: 2.1.0
671
820
  - id: app.supabase.migrations
672
821
  profiles: [app]
822
+ exemplifiedBy:
823
+ - packages/hfs/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql
824
+ - {path: tests/fixtures/hfs-callback-full/supabase/migrations/20261014100000_baseline.sql, edition: full}
825
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite uses Supabase schema authority and requires a migration; full opts into that provider."
673
826
  # The schema authority of an app on Supabase: forward-only SQL migrations named <timestamp14>_<kebab-name>.sql,
674
827
  # one file per change, immutable once on origin/main (L02-L08 judge them).
675
828
  path: "supabase/migrations/<stamp>_<name>.sql"
@@ -704,6 +857,10 @@ slots:
704
857
  since: 2.1.0
705
858
  - id: app.supabase.types
706
859
  profiles: [app]
860
+ templateNa: "Database row types are generated from the app's Supabase schema by contract:emit; a hand-authored scaffold template would invent the schema contract."
861
+ exemplifiedBy:
862
+ - {path: tests/fixtures/hfs-callback-full/supabase/types/database.types.ts, edition: full}
863
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires generated Supabase row types; full enables them with the provider."
707
864
  # The generated Database types (database.types.ts) contract:emit writes from the migrations; both sides read it
708
865
  # (sides.<side>.reads supabase/types/); drift against regeneration is L09.
709
866
  path: "supabase/types/database.types.ts"
@@ -742,6 +899,7 @@ slots:
742
899
 
743
900
  # ----- side root (both sides): what a standalone repository root holds, less the app-root files ----------
744
901
  - id: repo.side-root-forbidden
902
+ since: 3.1.0
745
903
  profiles: [be, fe]
746
904
  path: "{package.json,package-lock.json,hfs.json,README.md,.gitignore,.gitattributes,.husky/,.github/,.starciwork/,.starcistacks/,supabase/,.sops.yaml,sonar-project.properties,codecov.yml,.prettierrc,.prettierignore,scripts/}"
747
905
  presence: forbidden
@@ -750,6 +908,7 @@ slots:
750
908
  tests: none
751
909
  goesTo: "the app root: one package.json, lockfile, hfs.json, README, git and CI files, hooks, formatter, Sonar and Codecov configuration, scripts/, .starciwork, .starcistacks and .sops.yaml per app"
752
910
  - id: be.tool-config
911
+ since: 3.1.0
753
912
  profiles: [be]
754
913
  # Managed files (starci app sync renders them, starci app check compares them): the whole tool configuration of a back end. Each is
755
914
  # a reference to a canon package, never a configuration: tsconfig.json extends @starci/tsconfig/be.json and adds the
@@ -765,11 +924,12 @@ slots:
765
924
  tracked: tracked
766
925
  tier: none
767
926
  tests: none
768
- # Lite renders the same files (owner ruling 2026-10-12): the unit run and its coverage are full's.
927
+ # Lite renders the same files (owner ruling 2026-10-12 (owner:lite-production-swarm), with the Work boundary amended by ruling:R212/case-4): the unit run and its coverage are full's.
769
928
  coverage: none
770
929
  managedBy: tool-config
771
930
  rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL, HFS_TS_STRICT]
772
931
  - id: be.nest-cli
932
+ since: 3.1.0
773
933
  profiles: [be]
774
934
  # The Nest monorepo of the back end: `monorepo: true` and one project per declared be app (apps/<app>, even for one service).
775
935
  path: nest-cli.json
@@ -780,6 +940,7 @@ slots:
780
940
  coverage: none
781
941
  rules: [HFS_MONO_NEST_PROJECTS]
782
942
  - id: be.tool-config-local
943
+ since: 3.1.0
783
944
  profiles: [be]
784
945
  path: "{.eslintrc,.eslintrc.*,.eslintignore,eslint.config.js,eslint.config.cjs,eslint.config.ts,eslint.config.mts,eslint.config.cts,.prettierrc.*,prettier.config.*,jest.config.ts,jest.config.mjs,jest.config.cjs,jest.config.json,jest.config.e2e.js}"
785
946
  presence: forbidden
@@ -789,6 +950,7 @@ slots:
789
950
  goesTo: "nowhere: a back end has exactly the managed tool configuration (be.tool-config); a change to a rule or a flag is proposed in the .claude runtime"
790
951
  rules: [HFS_TOOL_CONFIG_LOCAL]
791
952
  - id: fe.tool-config
953
+ since: 3.1.0
792
954
  profiles: [fe]
793
955
  # Managed files (starci app sync renders them, starci app check compares them): the tool configuration of a front end. Each is a
794
956
  # reference to a canon package, never a configuration: tsconfig.json extends @starci/tsconfig/next.json and only
@@ -804,6 +966,7 @@ slots:
804
966
  managedBy: tool-config
805
967
  rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL, HFS_RULE_OFF_WITHOUT_REPLACEMENT, HFS_TS_STRICT]
806
968
  - id: fe.tool-config-local
969
+ since: 3.1.0
807
970
  profiles: [fe]
808
971
  path: "{.eslintrc,.eslintrc.*,.eslintignore,eslint.config.js,eslint.config.cjs,eslint.config.ts,eslint.config.mts,eslint.config.cts,.stylelintrc,.stylelintrc.*,.stylelintignore,stylelint.config.js,stylelint.config.cjs,stylelint.config.ts,stylelint.config.json,prettier.config.*,.prettierrc.*,lint-staged.config.*,.lintstagedrc*,turbo.json}"
809
972
  presence: forbidden
@@ -813,6 +976,7 @@ slots:
813
976
  goesTo: "nowhere: a front end has exactly the managed tool configuration (fe.tool-config); the task graph is the app root's turbo.json (app.task-graph); a change to a rule or a flag is proposed in the .claude runtime"
814
977
  rules: [HFS_TOOL_CONFIG_LOCAL]
815
978
  - id: repo.docs
979
+ since: 3.1.0
816
980
  profiles: [be, fe]
817
981
  path: "docs/{adr,runbooks,guides}/**/*.md"
818
982
  presence: opt-in
@@ -822,6 +986,7 @@ slots:
822
986
  coverage: none
823
987
  rules: [HFS_ROOT_MARKDOWN_FORBIDDEN]
824
988
  - id: repo.docs-media
989
+ since: 3.1.0
825
990
  profiles: [be, fe]
826
991
  path: "docs/**/*.{png,svg,webp}"
827
992
  presence: opt-in
@@ -830,7 +995,23 @@ slots:
830
995
  tests: none
831
996
  coverage: none
832
997
  budget: {bytes: 512000}
998
+ - id: repo.i18n-dynamic-keys
999
+ since: 3.3.0
1000
+ profiles: [be, fe]
1001
+ # The side's authored ledger of catalog keys the R280 detector cannot see (fe/i18n-dynamic-keys.yaml, be/i18n-dynamic-keys.yaml:
1002
+ # each side owns its own catalogs). A key it names counts as read; an entry matching no catalog key, or a malformed file, is itself an R280 finding.
1003
+ path: i18n-dynamic-keys.yaml
1004
+ presence: opt-in
1005
+ tracked: tracked
1006
+ tier: none
1007
+ tests: none
1008
+ coverage: none
1009
+ exemplifiedBy:
1010
+ - {path: tests/fixtures/hfs-i18n-ledger/fe/i18n-dynamic-keys.yaml, editions: [lite, full]}
1011
+ - {path: tests/fixtures/hfs-i18n-ledger/be/i18n-dynamic-keys.yaml, editions: [lite, full]}
1012
+ rules: [HFS_UNUSED_SYMBOL]
833
1013
  - id: repo.packages
1014
+ since: 3.1.0
834
1015
  profiles: [be, fe]
835
1016
  path: "packages/<pkg>/"
836
1017
  presence: opt-in
@@ -842,7 +1023,14 @@ slots:
842
1023
  coverage: none
843
1024
  rules: [FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT]
844
1025
  - id: repo.app-image
1026
+ since: 3.1.0
845
1027
  profiles: [be, fe]
1028
+ exemplifiedBy:
1029
+ - packages/hfs/templates/be/image/api/Dockerfile
1030
+ - packages/hfs/templates/be/image/cli/Dockerfile
1031
+ - {path: packages/hfs/templates/be/image/worker/Dockerfile, edition: full}
1032
+ - packages/hfs/templates/fe/image/next/Dockerfile
1033
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite may host Next without its own image; back-end images stay required in both editions."
846
1034
  # One image per app: every declared app has its own Dockerfile (apps/<app>/Dockerfile), built from the APP ROOT (the folder with
847
1035
  # the one package.json). It is app-owned (starci app scaffold writes it once from templates/<side>/image) and judged by the docker rules.
848
1036
  # A Dockerfile anywhere else is owned by no slot.
@@ -857,6 +1045,7 @@ slots:
857
1045
 
858
1046
  # ----- never tracked: build output the tools need in place ---------------------------------------------
859
1047
  - id: repo.build-output
1048
+ since: 3.1.0
860
1049
  profiles: [be, fe]
861
1050
  path: "{**/node_modules/,dist/,apps/*/dist/,packages/*/dist/,.next/,apps/*/.next/,coverage/,reports/,test-results/,**/next-env.d.ts,**/*.tsbuildinfo}"
862
1051
  presence: optional
@@ -864,6 +1053,7 @@ slots:
864
1053
  tier: none
865
1054
  tests: none
866
1055
  - id: repo.generated
1056
+ since: 3.1.0
867
1057
  profiles: [be, fe]
868
1058
  path: "{**/__generated__/,apps/*/src/schema.gql}"
869
1059
  presence: optional
@@ -874,6 +1064,7 @@ slots:
874
1064
 
875
1065
  # ----- never in the working tree: caches, scratch, agent output ----------------------------------------
876
1066
  - id: repo.tool-cache
1067
+ since: 3.1.0
877
1068
  profiles: [be, fe]
878
1069
  path: "{.eslintcache,.turbo/,.scannerwork/,.sonar/,.jest-cache/,.cache/,.tools/}"
879
1070
  presence: forbidden
@@ -882,6 +1073,7 @@ slots:
882
1073
  tests: none
883
1074
  goesTo: "${STARCI_CACHE_HOME:-%LOCALAPPDATA%/StarCi/cache}/<repo>/<tool>/ (the canon presets set eslint --cache-location, jest cacheDirectory, turbo cacheDir, sonar.working.directory)"
884
1075
  - id: repo.agent-output
1076
+ since: 3.1.0
885
1077
  profiles: [be, fe]
886
1078
  path: "{report*.json,*.tmp.json,%*%,nul,.artifacts/,.tmp-*,*-lint.json,lf.json,lt.json,*.log,design-plans/,.gitmounts/,.dat,vi-flat.txt}"
887
1079
  presence: forbidden
@@ -891,6 +1083,7 @@ slots:
891
1083
  goesTo: "agent scratchpad or the StarCi blob store (%LOCALAPPDATA%/StarCi/blobs), cited by {name, sha256}"
892
1084
  rules: [HFS_UNTRACKED_ROOT_ENTRY, HFS_AGENT_DATA_TRACKED]
893
1085
  - id: repo.worktrees
1086
+ since: 3.1.0
894
1087
  profiles: [be, fe]
895
1088
  path: "{.worktrees/,worktrees/,.starciwork/worktrees/}"
896
1089
  presence: forbidden
@@ -899,6 +1092,7 @@ slots:
899
1092
  tests: none
900
1093
  goesTo: "<lanes root>/<project>/<lane>/ (outside every repository)"
901
1094
  - id: repo.plaintext-env
1095
+ since: 3.1.0
902
1096
  profiles: [be, fe]
903
1097
  path: "{.env,.env.*,.secrets/,**/*.pem,**/*.key}"
904
1098
  presence: forbidden
@@ -908,6 +1102,7 @@ slots:
908
1102
  goesTo: ".starcistacks/<env>/secrets/<slug>.enc (at the app root); decrypted values only in memory or under %LOCALAPPDATA%/StarCi/secrets/<project>/<env>/ via *_FILE"
909
1103
  rules: [HFS_PLAINTEXT_SECRET]
910
1104
  - id: be.root-e2e
1105
+ since: 3.1.0
911
1106
  profiles: [be]
912
1107
  path: "e2e/"
913
1108
  presence: forbidden
@@ -918,6 +1113,7 @@ slots:
918
1113
 
919
1114
  # ----- contract snapshots (the committed machine-emitted files, because they are a cross-repo interface) --
920
1115
  - id: be.contract.graphql
1116
+ since: 3.1.0
921
1117
  profiles: [be]
922
1118
  path: "contracts/<app>/schema.graphql"
923
1119
  presence: opt-in
@@ -928,6 +1124,7 @@ slots:
928
1124
  why: emitted by `npm run contract:emit` (`starci app emit`, never written by hand); CI fails when the emitted file differs (a contract, not a build artefact)
929
1125
  rules: [HFS_CONTRACT_SNAPSHOT_DRIFT]
930
1126
  - id: be.contract.openapi
1127
+ since: 3.1.0
931
1128
  profiles: [be]
932
1129
  path: "contracts/<app>/openapi.json"
933
1130
  presence: opt-in
@@ -938,6 +1135,7 @@ slots:
938
1135
  why: emitted by `npm run contract:emit` (`starci app emit`, never written by hand) from the app's typed operation table `apps/<app>/src/operations.ts` (canon BE-OPERATIONS-1); CI fails when the emitted file differs
939
1136
  rules: [HFS_CONTRACT_SNAPSHOT_DRIFT]
940
1137
  - id: be.contract.events
1138
+ since: 3.1.0
941
1139
  profiles: [be]
942
1140
  path: "contracts/<app>/events.json"
943
1141
  presence: opt-in
@@ -948,6 +1146,7 @@ slots:
948
1146
  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)
949
1147
  rules: [HFS_EVENT_CONTRACT]
950
1148
  - id: be.contract.events.pin
1149
+ since: 3.1.0
951
1150
  profiles: [be]
952
1151
  path: "contracts/<app>/events.pin.json"
953
1152
  presence: opt-in
@@ -959,7 +1158,25 @@ slots:
959
1158
  rules: [BE_CONTRACT_BREAKING]
960
1159
 
961
1160
  # ----- BE: apps (one slot per app kind; a new kind = a new slot, minor bump) ---------------------------
1161
+ # The files of every app (owner ruling 2026-10-14, R245 and R246, since 3.1.0), each with a line budget that
1162
+ # BE_APP_COMPOSITION_BUDGET holds absolutely (not a ratchet):
1163
+ # main.ts the entry: builds EnvSource once, imports and calls read<App>Env from <app>.env.ts, builds options and boots.
1164
+ # <app>.env.ts exports interface <App>Env and read<App>Env(env: EnvSource): <App>Env (for example ApiEnv/readApiEnv);
1165
+ # fail-fast environment parsing only, via EnvSource and unchanged capability parse helpers; no localhost
1166
+ # or default host fallback, logic, module composition, business code or feature imports. Only main.ts
1167
+ # and this app file may read process.env or EnvSource. The interface declares its own readonly parsed
1168
+ # members with parser-derived types, structurally compatible with the app's options;
1169
+ # the role name stays <app>.env.ts, never env.ts. Secrets use EnvSource.secret(): <NAME>_FILE first
1170
+ # for .starcistacks custody, plain <NAME> only when the file key is absent. A declared file supplies
1171
+ # exact contents; an empty or unreadable path stops boot without fallback.
1172
+ # <app>.options.ts the options: env-derived options plus port bindings (provider maps such as { approvalGate: X }) only;
1173
+ # no environment parsing or process.env/EnvSource reads; module, queue or scheduler composition here or in <app>.env.ts is
1174
+ # BE_APP_OPTIONS_COMPOSITION.
1175
+ # compose/<area>.ts a composition group: exports the module list of one area (an imports array, and the providers of the
1176
+ # area's global ports when it has any); composition only, no business logic or process.env/EnvSource read.
1177
+ # app.module.ts the composition root: imports the groups and spreads them, binds the app-wide guards and filter.
962
1178
  - id: be.app.api
1179
+ since: 3.1.0
963
1180
  profiles: [be]
964
1181
  path: "apps/<app>/src/"
965
1182
  appKind: api
@@ -968,13 +1185,14 @@ slots:
968
1185
  tier: app
969
1186
  owner: true
970
1187
  minInstances: 1
971
- requires: [main.ts, app.module.ts]
972
- allows: [main.ts, app.module.ts, "<app>.options.ts", operations.ts]
973
- tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
1188
+ requires: [main.ts, app.module.ts, "<app>.env.ts"]
1189
+ allows: [main.ts, app.module.ts, "<app>.env.ts", "<app>.options.ts", "compose/<area>.ts", operations.ts]
1190
+ tests: none # full app boot is proven by useTestWorld({ apps }); lite by starci app check --wiring (R272)
974
1191
  coverage: none
975
- budget: {app.module.ts: 250, main.ts: 80}
976
- rules: [BE_APP_COMPOSITION_ONLY, BE_APP_BUSINESS_ROLE, BE_DEFAULT_DENY, BE_ERROR_MASKED, BE_FEATURE_NOT_COMPOSED, BE_CONTEXT_OWNER]
1192
+ budget: {app.module.ts: 250, main.ts: 80, "<app>.env.ts": 150, "<app>.options.ts": 200, "compose/<area>.ts": 120}
1193
+ rules: [BE_APP_COMPOSITION_ONLY, BE_APP_BUSINESS_ROLE, BE_APP_OPTIONS_COMPOSITION, BE_APP_COMPOSITION_BUDGET, BE_DEFAULT_DENY, BE_ERROR_MASKED, BE_FEATURE_NOT_COMPOSED, BE_CONTEXT_OWNER, BE_APP_ENV_REQUIRED]
977
1194
  - id: be.app.worker
1195
+ since: 3.1.0
978
1196
  profiles: [be]
979
1197
  path: "apps/<app>/src/"
980
1198
  appKind: worker # composes transport/message modules and job processors only (an api app may compose them too: a worker app is for separate scaling)
@@ -982,18 +1200,22 @@ slots:
982
1200
  tracked: tracked
983
1201
  tier: app
984
1202
  owner: true
985
- requires: [main.ts, app.module.ts]
986
- allows: [main.ts, app.module.ts, "<app>.options.ts"]
987
- tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
1203
+ requires: [main.ts, app.module.ts, "<app>.env.ts"]
1204
+ allows: [main.ts, app.module.ts, "<app>.env.ts", "<app>.options.ts", "compose/<area>.ts"]
1205
+ tests: none # full app boot is proven by useTestWorld({ apps }); lite by starci app check --wiring (R272)
988
1206
  coverage: none
989
- rules: [BE_APP_COMPOSITION_ONLY, BE_BACKGROUND_UNOWNED, BE_CONTEXT_OWNER]
1207
+ budget: {app.module.ts: 250, main.ts: 80, "<app>.env.ts": 150, "<app>.options.ts": 200, "compose/<area>.ts": 120}
1208
+ rules: [BE_APP_COMPOSITION_ONLY, BE_APP_OPTIONS_COMPOSITION, BE_APP_COMPOSITION_BUDGET, BE_BACKGROUND_UNOWNED, BE_CONTEXT_OWNER, BE_APP_ENV_REQUIRED]
990
1209
  - id: be.app.cli
1210
+ since: 3.1.0
991
1211
  profiles: [be]
1212
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): Supabase CLI owns lite migrations; a lite cli app is optional for declared operator commands."
992
1213
  # The ONE app of every one-off action (migrate, seed, sync, backup, any operator command): be/apps/cli, on nest-commander.
993
1214
  # main.ts boots with CommandFactory.run (BE_CLI_BOOTSTRAP); app.module.ts composes the platform and the cli feature root
994
- # (src/features/cli/cli.module.ts); cli.options.ts carries what it needs from the environment, the connections it migrates
995
- # among them (the test world migrates through the same list). It is required as soon as the back end declares a connection
996
- # or tracks a command, with its image apps/cli/Dockerfile (BE_CLI_REQUIRED): one image, run as `cli <group> <command>`.
1215
+ # (src/features/cli/cli.module.ts); cli.env.ts exports CliEnv and readCliEnv(env: EnvSource): CliEnv, and cli.options.ts carries the env-derived values,
1216
+ # the connections it migrates among them (the test world migrates through the same list). It is required as soon as the
1217
+ # back end declares a connection or tracks a command, with its image apps/cli/Dockerfile (BE_CLI_REQUIRED): one image,
1218
+ # run as `cli <group> <command>`.
997
1219
  path: "apps/<app>/src/"
998
1220
  appKind: cli
999
1221
  presence: opt-in
@@ -1001,17 +1223,19 @@ slots:
1001
1223
  tracked: tracked
1002
1224
  tier: app
1003
1225
  owner: true
1004
- requires: [main.ts, app.module.ts]
1005
- allows: [main.ts, app.module.ts, "<app>.options.ts"]
1006
- tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
1226
+ requires: [main.ts, app.module.ts, "<app>.env.ts"]
1227
+ allows: [main.ts, app.module.ts, "<app>.env.ts", "<app>.options.ts", "compose/<area>.ts"]
1228
+ tests: none # full app boot is proven by useTestWorld({ apps }); lite by starci app check --wiring (R272)
1007
1229
  coverage: none
1008
- rules: [BE_CLI_REQUIRED, BE_CLI_BOOTSTRAP, BE_CLI_OWNER, BE_SCHEMA_AUTHORITY]
1230
+ budget: {app.module.ts: 250, main.ts: 80, "<app>.env.ts": 150, "<app>.options.ts": 200, "compose/<area>.ts": 120}
1231
+ rules: [BE_CLI_REQUIRED, BE_CLI_BOOTSTRAP, BE_CLI_OWNER, BE_SCHEMA_AUTHORITY, BE_APP_OPTIONS_COMPOSITION, BE_APP_COMPOSITION_BUDGET, BE_APP_ENV_REQUIRED]
1009
1232
 
1010
1233
  # ----- BE: features ------------------------------------------------------------------------------------
1011
1234
  # src/features/ is grouped by TRIGGER KIND (owner decision 2026-10-02): features/api/<feature>/ (a request: application/ and
1012
1235
  # transport/<protocol>/, composed by the api apps), features/cli/ (a one-off action: the cli feature root, compiled only into
1013
1236
  # apps/cli). Every kind imports only downward into modules/*; no kind imports another.
1014
1237
  - id: be.feature
1238
+ since: 3.1.0
1015
1239
  profiles: [be]
1016
1240
  path: "src/features/api/<feature>/"
1017
1241
  presence: required
@@ -1021,12 +1245,13 @@ slots:
1021
1245
  owner: true
1022
1246
  minInstances: 1
1023
1247
  requires: [index.ts, "<feature>.module.ts", application/]
1024
- allows: [index.ts, "<feature>.module.ts", application/, transport/, messages/] # nothing else at feature root
1248
+ allows: [index.ts, "<feature>.module.ts", "<feature>.module-definition.ts", "<feature>.options.ts", application/, transport/, messages/] # nothing else at feature root
1025
1249
  tests: none
1026
1250
  coverage: none
1027
1251
  budget: {files: 250, indexExports: 60} # a feature above 250 source files must be split by product capability
1028
1252
  rules: [BE_FEATURE_SHAPE, BE_FEATURE_IMPORTS_FEATURE, BE_PUBLIC_SURFACE, BE_FEATURE_NOT_COMPOSED]
1029
1253
  - id: be.feature.application
1254
+ since: 3.1.0
1030
1255
  profiles: [be]
1031
1256
  path: "src/features/{api/,jobs/,reactors/,saga/}<feature>/application/" # inside a trigger kind (jobs, reactors, saga) the feature folder is one level down
1032
1257
  presence: required
@@ -1038,6 +1263,7 @@ slots:
1038
1263
  coverage: none
1039
1264
  rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_APPLICATION_TRANSPORT_FRAMEWORK, BE_ENTITY_IN_CONTRACT, BE_CQRS_SHAPE]
1040
1265
  - id: be.feature.saga
1266
+ since: 3.1.0
1041
1267
  profiles: [be]
1042
1268
  path: "src/features/saga/<saga>/"
1043
1269
  pattern: saga # enabled by `patterns: ["saga"]` in hfs.json sides.be (R169 to R173); an undeclared saga folder is HFS_SLOT_NOT_ENABLED
@@ -1047,11 +1273,12 @@ slots:
1047
1273
  tier: feature
1048
1274
  owner: true
1049
1275
  requires: [index.ts, "<saga>.module.ts", "<saga>.saga.service.ts", "<saga>.saga-state.ts", steps/, compensations/, transport/]
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
1276
+ allows: [index.ts, "<saga>.module.ts", "<saga>.module-definition.ts", "<saga>.options.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
1051
1277
  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
1052
1278
  coverage: none
1053
1279
  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]
1054
1280
  - id: be.feature.saga.steps
1281
+ since: 3.1.0
1055
1282
  profiles: [be]
1056
1283
  path: "src/features/saga/<saga>/steps/"
1057
1284
  pattern: saga
@@ -1063,6 +1290,7 @@ slots:
1063
1290
  coverage: none
1064
1291
  rules: [BE_SAGA_STEP_COMPENSATION, BE_SAGA_EVENT_CONTRACT]
1065
1292
  - id: be.feature.saga.compensations
1293
+ since: 3.1.0
1066
1294
  profiles: [be]
1067
1295
  path: "src/features/saga/<saga>/compensations/"
1068
1296
  pattern: saga
@@ -1086,30 +1314,33 @@ slots:
1086
1314
  rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_APPLICATION_TRANSPORT_FRAMEWORK, BE_FEATURE_IMPORTS_FEATURE]
1087
1315
  since: 1.0.0
1088
1316
  - id: be.transport.http
1317
+ since: 3.1.0
1089
1318
  profiles: [be]
1090
1319
  path: "src/features/api/<feature>/transport/http/"
1091
1320
  presence: optional
1092
1321
  tracked: tracked
1093
1322
  tier: feature
1094
1323
  requires: ["<feature>-http.module.ts"]
1095
- allows: ["<action>.controller.ts", "dto/<action>.{request,response}.ts", "<action>.mapper.ts"]
1324
+ allows: ["<action>.controller.ts", "dto/<action>.{request,response}.ts", "<action>.mapper.ts", "<feature>-http.module-definition.ts", "<feature>-http.options.ts"]
1096
1325
  tests: none
1097
1326
  coverage: none
1098
1327
  composedBy: [api]
1099
1328
  rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED, BE_TRANSPORT_SHAPE]
1100
1329
  - id: be.transport.graphql
1330
+ since: 3.1.0
1101
1331
  profiles: [be]
1102
1332
  path: "src/features/api/<feature>/transport/graphql/"
1103
1333
  presence: optional
1104
1334
  tracked: tracked
1105
1335
  tier: feature
1106
1336
  requires: ["<feature>-graphql.module.ts"]
1107
- allows: ["<action>.resolver.ts", "dto/<action>.{input,type,args}.ts", "<action>.mapper.ts"]
1337
+ allows: ["<action>.resolver.ts", "dto/<action>.{input,type,args}.ts", "<action>.mapper.ts", "<feature>-graphql.module-definition.ts", "<feature>-graphql.options.ts"]
1108
1338
  tests: none
1109
1339
  coverage: none
1110
1340
  composedBy: [api]
1111
1341
  rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED, BE_TRANSPORT_SHAPE]
1112
1342
  - id: be.transport.message
1343
+ since: 3.1.0
1113
1344
  profiles: [be]
1114
1345
  path: "src/features/{api/,reactors/,saga/}<feature>/transport/message/"
1115
1346
  presence: opt-in # queue consumer: composed by the service that owns it (an api app) or by a worker app scaled apart
@@ -1117,7 +1348,7 @@ slots:
1117
1348
  tracked: tracked
1118
1349
  tier: feature
1119
1350
  requires: ["<feature>-message.module.ts"]
1120
- allows: ["<event>.consumer.ts"] # BE-CONVENTION 1.6: transport/message holds only consumers; a .message.ts is outside the closed suffix list
1351
+ allows: ["<event>.consumer.ts", "<feature>-message.module-definition.ts", "<feature>-message.options.ts"] # BE-CONVENTION 1.6: transport/message holds only consumers; a .message.ts is outside the closed suffix list
1121
1352
  tests: none
1122
1353
  coverage: none
1123
1354
  composedBy: [api, worker]
@@ -1149,7 +1380,7 @@ slots:
1149
1380
  tracked: tracked
1150
1381
  tier: feature
1151
1382
  requires: ["<provider>-http.module.ts", "<provider>.webhook.ts", "<provider>.webhook.spec.ts"]
1152
- allows: ["<provider>-http.module.ts", "<provider>.webhook.ts", "<provider>.webhook.spec.ts", dto/]
1383
+ allows: ["<provider>-http.module.ts", "<provider>-http.module-definition.ts", "<provider>-http.options.ts", "<provider>.webhook.ts", "<provider>.webhook.spec.ts", dto/]
1153
1384
  tests: unit-beside # the door's own spec (<provider>.webhook.spec.ts) beside the door: the one non-service unit spec of this kind
1154
1385
  coverage: none
1155
1386
  composedBy: [api]
@@ -1170,6 +1401,9 @@ slots:
1170
1401
  since: 1.0.0
1171
1402
  - id: be.feature.realtime
1172
1403
  profiles: [be]
1404
+ exemplifiedBy:
1405
+ - {path: tests/fixtures/hfs-lite-slots/be/src/features/realtime/order-status/, edition: lite}
1406
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite runs socket.io in the api process, one instance without Redis; full may scale its gateway."
1173
1407
  path: "src/features/realtime/<channel>/"
1174
1408
  presence: opt-in # a push channel: reads and pushes only (a socket gateway or a GraphQL subscription), no domain write
1175
1409
  pattern: realtime # declaring the pattern (hfs.json sides.be.patterns) enables the kind (knowledge/patterns/be/realtime.yaml)
@@ -1190,15 +1424,16 @@ slots:
1190
1424
  since: 1.0.0
1191
1425
  - id: be.feature.realtime.graphql
1192
1426
  profiles: [be]
1427
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite uses only the in-process socket.io gateway; the GraphQL subscription door remains full-only."
1193
1428
  path: "src/features/realtime/<channel>/transport/graphql/"
1194
1429
  presence: opt-in # a GraphQL subscription over graphql-ws
1195
- litePresence: forbidden # a graphql-ws server is self-hosted push infrastructure: lite pushes through Supabase Realtime
1430
+ litePresence: forbidden # ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite pushes through the api's in-process socket.io gateway
1196
1431
  pattern: realtime
1197
1432
  tracked: tracked
1198
1433
  tier: feature
1199
1434
  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"
1200
1435
  requires: ["<channel>-graphql.module.ts", "<channel>.subscription.ts", "<channel>.subscription.spec.ts"]
1201
- allows: ["<channel>-graphql.module.ts", "<channel>.subscription.ts", "<channel>.subscription.spec.ts", dto/]
1436
+ allows: ["<channel>-graphql.module.ts", "<channel>-graphql.module-definition.ts", "<channel>-graphql.options.ts", "<channel>.subscription.ts", "<channel>.subscription.spec.ts", dto/]
1202
1437
  tests: unit-beside # the door's own spec beside the door (a subscription has no service of its own)
1203
1438
  coverage: none
1204
1439
  composedBy: [api]
@@ -1206,6 +1441,7 @@ slots:
1206
1441
  since: 1.0.0
1207
1442
  - id: be.feature.realtime.dto
1208
1443
  profiles: [be]
1444
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): GraphQL subscription DTOs exist only with the full subscription door; lite uses socket.io contracts."
1209
1445
  path: "src/features/realtime/<channel>/transport/graphql/dto/"
1210
1446
  presence: opt-in
1211
1447
  litePresence: forbidden # the dto of the graphql door, which lite does not serve
@@ -1227,20 +1463,21 @@ slots:
1227
1463
  tracked: tracked
1228
1464
  tier: feature
1229
1465
  requires: ["<channel>-websocket.module.ts", "<channel>.gateway.ts", "<channel>.gateway.spec.ts"]
1230
- allows: ["<channel>-websocket.module.ts", "<channel>.gateway.ts", "<channel>.gateway.spec.ts"]
1466
+ allows: ["<channel>-websocket.module.ts", "<channel>-websocket.module-definition.ts", "<channel>-websocket.options.ts", "<channel>.gateway.ts", "<channel>.gateway.spec.ts"]
1231
1467
  tests: unit-beside # the door's own spec beside the door (a gateway has no service of its own)
1232
1468
  coverage: none
1233
1469
  composedBy: [api]
1234
1470
  rules: [BE_REALTIME_SHAPE, BE_REALTIME_WRITES, BE_REALTIME_TOPIC_SCOPE, BE_DEFAULT_DENY]
1235
1471
  since: 1.0.0
1236
1472
  - id: be.cli
1473
+ since: 3.1.0
1237
1474
  profiles: [be]
1238
1475
  # The cli feature root: every one-off action of the back end, compiled only into apps/cli. A group is a nest-commander
1239
1476
  # @Command (<group>/<group>.cli.ts) whose sub-commands are @SubCommand classes extending CommandRunner
1240
1477
  # (<group>/subs/<name>.cli.ts), each with its unit spec beside it (<name>.cli.spec.ts): a command is an action runner, not a
1241
1478
  # thin transport (BE_CLI_COMMAND_SHAPE). `.cli.ts` because `.command.ts` is the CQRS message (HFS delta 3). Every module of
1242
- # the root is a static @Module (a feature holds no module definition): cli.module.ts imports each group module, a group
1243
- # module provides its commands, and a command takes its inputs from a platform capability (the connections and their opener
1479
+ # the root has a module definition beside it (ruling:module-definition-everywhere) and no register override: cli.module.ts imports each
1480
+ # group module as <Group>Module.register({}), a group module provides its commands, and a command takes its inputs from a platform capability (the connections and their opener
1244
1481
  # from platform/database) or from a command-line @Option.
1245
1482
  path: "src/features/cli/"
1246
1483
  presence: optional
@@ -1251,7 +1488,7 @@ slots:
1251
1488
  requires: [index.ts, cli.module.ts]
1252
1489
  # A composed root: an app of kind cli lists cli.module.ts, never a group module (BE_MODULE_SHAPE).
1253
1490
  composedBy: [cli]
1254
- allows: [index.ts, cli.module.ts, "<group>/<group>.cli.ts", "<group>/<group>.cli.spec.ts", "<group>/<group>.module.ts", "<group>/subs/<name>.cli.ts", "<group>/subs/<name>.cli.spec.ts"]
1491
+ allows: [index.ts, cli.module.ts, "cli.module-definition.ts", "cli.options.ts", "<group>/<group>.cli.ts", "<group>/<group>.cli.spec.ts", "<group>/<group>.module.ts", "<group>/<group>.module-definition.ts", "<group>/<group>.options.ts", "<group>/subs/<name>.cli.ts", "<group>/subs/<name>.cli.spec.ts"]
1255
1492
  tests: unit-beside
1256
1493
  coverage: none
1257
1494
  rules: [BE_CLI_COMMAND_SHAPE, BE_CLI_OWNER, BE_CLI_REQUIRED, BE_MODULE_SHAPE, BE_SCHEMA_AUTHORITY]
@@ -1268,8 +1505,11 @@ slots:
1268
1505
  tests: none
1269
1506
  coverage: none
1270
1507
  since: 3.0.0
1508
+ rules: [HFS_ASSET_OVERSIZE, HFS_ASSET_KIND]
1509
+
1271
1510
  # ----- BE: modules (three tiers, closed set) -----------------------------------------------------------
1272
1511
  - id: be.domain
1512
+ since: 3.1.0
1273
1513
  profiles: [be]
1274
1514
  path: "src/modules/domain/<capability>/"
1275
1515
  presence: optional
@@ -1279,13 +1519,14 @@ slots:
1279
1519
  requires: [index.ts]
1280
1520
  # <name>.templates.ts holds the capability's static templates, copy and presets as TS constants (owner ruling 2026-10-13);
1281
1521
  # 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"]
1522
+ allows: ["<capability>.module.ts", "<capability>.module-definition.ts", "<capability>.config.ts", "<capability>.options.ts", errors/, messages/, persistence/, policies/, "*.service.ts", "*.service.spec.ts", "*.contracts.ts", "*.templates.ts"]
1283
1523
  forbids: [testing/, exceptions/, utils/, helpers/, shared/]
1284
1524
  tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts, nothing else
1285
1525
  coverage: required
1286
1526
  budget: {indexExports: 60}
1287
1527
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_PUBLIC_SURFACE, BE_ERROR_HOME, BE_CONFIG_OWNER, BE_CONTEXT_COUPLING, BE_CONTEXT_TRANSACTION]
1288
1528
  - id: be.errors
1529
+ since: 3.1.0
1289
1530
  profiles: [be]
1290
1531
  path: "src/modules/{domain,integrations}/<capability>/errors/"
1291
1532
  presence: optional
@@ -1318,7 +1559,9 @@ slots:
1318
1559
  rules: [BE_USER_COPY_LITERAL]
1319
1560
  since: 1.0.0
1320
1561
  - id: be.persistence
1562
+ since: 3.1.0
1321
1563
  profiles: [be]
1564
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): Supabase migrations own lite tables, with SQL and row types here; full keeps TypeORM entities and migrations here."
1322
1565
  path: "src/modules/{domain,platform,projections}/<capability>/persistence/"
1323
1566
  # R178: the platform capabilities whose tables exist on EVERY connection that uses them (the event-bus outbox, the inbox of every service, the job table); their arrays may be
1324
1567
  # registered on several declared connections. Any other capability registers its arrays on exactly one (BE_SCHEMA_OWNER).
@@ -1339,6 +1582,7 @@ slots:
1339
1582
  coverage: none
1340
1583
  rules: [BE_SCHEMA_OWNER, BE_SCHEMA_AUTHORITY, BE_SQL_OUTSIDE_PERSISTENCE, BE_SQL_TABLE_OWNER, BE_SQL_RETURNING_SHAPE, BE_CONTEXT_COUPLING, BE_CONTEXT_PLATFORM_TABLES]
1341
1584
  - id: be.platform
1585
+ since: 3.1.0
1342
1586
  profiles: [be]
1343
1587
  path: "src/modules/platform/<capability>/"
1344
1588
  presence: required
@@ -1361,7 +1605,11 @@ slots:
1361
1605
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE]
1362
1606
  # ----- BE: pattern slots (opt-in; enabled by hfs.json sides.be.patterns). A path under one is HFS_SLOT_NOT_ENABLED unless its pattern is declared.
1363
1607
  - id: be.platform.event-bus
1608
+ since: 3.1.0
1364
1609
  profiles: [be]
1610
+ exemplifiedBy:
1611
+ - {path: tests/fixtures/hfs-lite-slots/be/src/modules/platform/event-bus/, edition: lite}
1612
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite implements the event ports with outbox and pgmq; full uses its Kafka adapter."
1365
1613
  path: "src/modules/platform/event-bus/"
1366
1614
  presence: opt-in
1367
1615
  pattern: event-bus # the ONE durable event transport: transactional outbox, relay, Kafka, inbox, retry and dead letter (knowledge/patterns/be/event-bus.yaml)
@@ -1380,7 +1628,9 @@ slots:
1380
1628
  budget: {indexExports: 60}
1381
1629
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE, BE_INFRA_OWNER]
1382
1630
  - id: be.platform.queue
1631
+ since: 3.1.0
1383
1632
  profiles: [be]
1633
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite implements queue ports with pgmq and pg_cron; full uses BullMQ and Redis."
1384
1634
  path: "src/modules/platform/queue/"
1385
1635
  presence: opt-in
1386
1636
  pattern: queue # the ONE BullMQ owner: the outbox relay into queues (jobId = outbox id) and job schedulers (knowledge/patterns/be/queues.yaml)
@@ -1399,6 +1649,7 @@ slots:
1399
1649
  budget: {indexExports: 60}
1400
1650
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE, BE_INFRA_OWNER]
1401
1651
  - id: be.events
1652
+ since: 3.1.0
1402
1653
  profiles: [be]
1403
1654
  path: "src/modules/events/<service>/"
1404
1655
  presence: opt-in
@@ -1412,6 +1663,7 @@ slots:
1412
1663
  coverage: none
1413
1664
  rules: [BE_EVENT_CLASS_CONTRACT, BE_TIER_DIRECTION]
1414
1665
  - id: be.queues
1666
+ since: 3.1.0
1415
1667
  profiles: [be]
1416
1668
  path: "src/modules/queues/<queue>/"
1417
1669
  presence: opt-in
@@ -1420,11 +1672,12 @@ slots:
1420
1672
  tier: queues
1421
1673
  owner: true
1422
1674
  requires: [index.ts, "<queue>.queue.ts"] # the module exists when a domain module imports the producer; a scheduler-only queue has none
1423
- allows: ["<queue>.queue.ts", "<queue>.module.ts", "<queue>.contracts.ts", index.ts]
1675
+ allows: ["<queue>.queue.ts", "<queue>.module.ts", "<queue>.module-definition.ts", "<queue>.options.ts", "<queue>.contracts.ts", index.ts]
1424
1676
  tests: none
1425
1677
  coverage: none
1426
1678
  rules: [BE_OUTBOX_WRITE_TX, BE_TIER_DIRECTION]
1427
1679
  - id: be.feature.reactors
1680
+ since: 3.1.0
1428
1681
  profiles: [be]
1429
1682
  path: "src/features/reactors/<reactor>/"
1430
1683
  presence: opt-in
@@ -1434,11 +1687,12 @@ slots:
1434
1687
  tier: feature
1435
1688
  owner: true
1436
1689
  requires: [index.ts, "<reactor>.module.ts", transport/]
1437
- allows: [index.ts, "<reactor>.module.ts", application/, transport/]
1690
+ allows: [index.ts, "<reactor>.module.ts", "<reactor>.module-definition.ts", "<reactor>.options.ts", application/, transport/]
1438
1691
  tests: none
1439
1692
  coverage: none
1440
1693
  rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED]
1441
1694
  - id: be.feature.jobs
1695
+ since: 3.1.0
1442
1696
  profiles: [be]
1443
1697
  path: "src/features/jobs/<job>/"
1444
1698
  presence: opt-in
@@ -1448,11 +1702,12 @@ slots:
1448
1702
  tier: feature
1449
1703
  owner: true
1450
1704
  requires: [index.ts, "<job>.module.ts", steps/, transport/]
1451
- allows: [index.ts, "<job>.module.ts", application/, steps/, transport/]
1705
+ allows: [index.ts, "<job>.module.ts", "<job>.module-definition.ts", "<job>.options.ts", application/, steps/, transport/]
1452
1706
  tests: none
1453
1707
  coverage: none
1454
1708
  rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED]
1455
1709
  - id: be.feature.jobs.queue
1710
+ since: 3.1.0
1456
1711
  profiles: [be]
1457
1712
  path: "src/features/jobs/<job>/transport/queue/"
1458
1713
  presence: opt-in # the queue transport of the job: its module and the processor the job runner hands the fenced claim to
@@ -1460,12 +1715,13 @@ slots:
1460
1715
  tracked: tracked
1461
1716
  tier: feature
1462
1717
  requires: ["<job>-queue.module.ts", "<job>.processor.ts"]
1463
- allows: ["<job>-queue.module.ts", "<job>.processor.ts"]
1718
+ allows: ["<job>-queue.module.ts", "<job>-queue.module-definition.ts", "<job>-queue.options.ts", "<job>.processor.ts"]
1464
1719
  tests: none
1465
1720
  coverage: none
1466
1721
  composedBy: [api, worker]
1467
1722
  rules: [BE_BACKGROUND_UNOWNED]
1468
1723
  - id: be.feature.jobs.steps
1724
+ since: 3.1.0
1469
1725
  profiles: [be]
1470
1726
  path: "src/features/jobs/<job>/steps/"
1471
1727
  presence: opt-in
@@ -1477,7 +1733,9 @@ slots:
1477
1733
  coverage: none
1478
1734
  rules: [BE_JOB_SHAPE, BE_JOB_RUN_KEY]
1479
1735
  - id: be.platform.jobs
1736
+ since: 3.1.0
1480
1737
  profiles: [be]
1738
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite persists fenced claims in Supabase migrations and dispatches through pgmq; full uses TypeORM schema authority and its queue adapter."
1481
1739
  path: "src/modules/platform/jobs/"
1482
1740
  presence: opt-in
1483
1741
  pattern: fenced-job # the ONE fencing primitive: the job entity, the claim that bumps the token, guarded writes (knowledge/patterns/be/jobs.yaml)
@@ -1490,7 +1748,11 @@ slots:
1490
1748
  budget: {indexExports: 60}
1491
1749
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE, BE_JOB_WRITE_OUTSIDE_OWNER, BE_JOB_FENCE_REQUIRED]
1492
1750
  - id: be.projections
1751
+ since: 3.1.0
1493
1752
  profiles: [be]
1753
+ exemplifiedBy:
1754
+ - {path: tests/fixtures/hfs-lite-slots/be/src/modules/projections/order-summary/, edition: lite}
1755
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite projection tables live in Supabase migrations; full keeps the TypeORM projection entity beside its projection."
1494
1756
  path: "src/modules/projections/<name>/"
1495
1757
  presence: opt-in
1496
1758
  pattern: projection # a read model: only its projection writes its table; recompute is idempotent and replayable; readers use get*
@@ -1506,6 +1768,7 @@ slots:
1506
1768
  coverage: required
1507
1769
  rules: [BE_PROJECTION_WRITE_OWNER, BE_PROJECTION_SHAPE, BE_TIER_DIRECTION, BE_CONTEXT_COUPLING]
1508
1770
  - id: be.integrations
1771
+ since: 3.1.0
1509
1772
  profiles: [be]
1510
1773
  path: "src/modules/integrations/<provider>/"
1511
1774
  presence: optional
@@ -1515,9 +1778,10 @@ slots:
1515
1778
  requires: [index.ts, "<provider>.config.ts"]
1516
1779
  tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts; plus src/tests/integration/<provider>/ (R112)
1517
1780
  coverage: required
1518
- budget: {indexExports: 60}
1781
+ budget: {indexExports: 80} # a provider's port surface includes clients, config, options, tokens and errors
1519
1782
  rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT, BE_INTEGRATION_SPEC_MISSING]
1520
1783
  - id: be.integrations.model
1784
+ since: 3.1.0
1521
1785
  profiles: [be]
1522
1786
  path: "src/modules/integrations/<provider>/model/"
1523
1787
  presence: opt-in
@@ -1526,8 +1790,14 @@ slots:
1526
1790
  tests: none
1527
1791
  coverage: required
1528
1792
  why: an ML or LLM model client is an integration; weights and datasets are never in git
1793
+ rules: [BE_TIER_DIRECTION, BE_INFRA_OWNER, BE_INTEGRATION_SPEC_MISSING]
1794
+
1529
1795
  - id: be.integrations.supabase
1530
1796
  profiles: [be]
1797
+ exemplifiedBy:
1798
+ - packages/hfs/templates/be/skeleton-lite/src/modules/integrations/supabase/
1799
+ - {path: tests/fixtures/hfs-callback-full/be/src/modules/integrations/supabase/, edition: full}
1800
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires the Supabase Auth and Storage integration; full opts in through a connection."
1531
1801
  # The only be import of @supabase/* (L10): the Auth admin and Storage admin client, plus the JWKS-based verification of
1532
1802
  # the Supabase access token the guard chain reads (L16). The connection itself is ordinary Postgres (the shared
1533
1803
  # EntityManager of the declared connection); what lives here are the service calls no database role covers.
@@ -1549,7 +1819,11 @@ slots:
1549
1819
  # lives under src/tests/, one folder per kind, and the folder and the file suffix always agree (a file whose suffix does
1550
1820
  # not match its folder matches no slot: HFS_SLOT_UNDECLARED).
1551
1821
  - id: be.tests.world
1822
+ since: 3.1.0
1552
1823
  profiles: [be]
1824
+ exemplifiedBy:
1825
+ - {path: tests/fixtures/hfs-lite-slots/be/src/tests/world/, edition: lite}
1826
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite integration proofs use the local Supabase stack and network fakes; full also boots apps and self-hosted services."
1553
1827
  path: "src/tests/world/"
1554
1828
  presence: optional
1555
1829
  tracked: tracked
@@ -1573,7 +1847,10 @@ slots:
1573
1847
  failNext/replayWebhook/delay), world.waitFor; fakes/<provider>/ holds the fake servers and payload fixtures, kit/ the inlined test helpers, and the world root may hold role-suffixed helpers (<name>.client.ts, <name>.contracts.ts, ...). Nothing is
1574
1848
  overridden in the DI container. It is the only test location that may import typeorm's DataSource or testcontainers,
1575
1849
  call migrate/runMigrations/synchronize, or write process.env.
1850
+ rules: [BE_TEST_TOPOLOGY, BE_TEST_CONSTRAINTS_ON]
1851
+
1576
1852
  - id: be.tests.world.kit
1853
+ since: 3.1.0
1577
1854
  profiles: [be]
1578
1855
  path: "src/tests/world/kit/"
1579
1856
  presence: optional
@@ -1584,6 +1861,7 @@ slots:
1584
1861
  coverage: none
1585
1862
  why: the helpers the test world needs (polling, free ports, transports), inlined so the world imports no other repository; a bare <name>.ts entry in allows is what admits a plain name in BE_SOURCE_FORM
1586
1863
  - id: be.tests.fixtures
1864
+ since: 3.1.0
1587
1865
  profiles: [be]
1588
1866
  path: "src/tests/fixtures/"
1589
1867
  presence: optional
@@ -1594,6 +1872,7 @@ slots:
1594
1872
  coverage: none
1595
1873
  why: payload fixtures with a role suffix (the doubles come from @starci/jest-preset, not from here); the test data builders live in its builders/ slot; importable by unit specs, integration and e2e; never imports a feature
1596
1874
  - id: be.tests.fixtures.builders
1875
+ since: 3.1.0
1597
1876
  profiles: [be]
1598
1877
  path: "src/tests/fixtures/builders/*.builder.ts"
1599
1878
  presence: optional
@@ -1608,6 +1887,7 @@ slots:
1608
1887
  *.fixture.ts or *.factory.ts anywhere, is a BE_SOURCE_FORM finding
1609
1888
  rules: [BE_SOURCE_FORM]
1610
1889
  - id: be.tests.fixtures.i18n
1890
+ since: 3.1.0
1611
1891
  profiles: [be]
1612
1892
  path: "src/tests/fixtures/i18n/"
1613
1893
  presence: optional
@@ -1618,7 +1898,9 @@ slots:
1618
1898
  coverage: none
1619
1899
  why: the ONLY test-fixture location where Vietnamese or another language may appear in source (LANG_NOT_ENGLISH); placement is the marker, there is no comment pragma
1620
1900
  - id: be.tests.integration
1901
+ since: 3.1.0
1621
1902
  profiles: [be]
1903
+ editionWhy: "Lite integration specs use the local Supabase CLI stack without containers or toxiproxy (ruling:R212/case-1, amended by ruling:R212/case-2 (owner:lite-production-swarm) and ruling:R212/case-4, with contracts allowed by owner:lite-contract-specs); R284 requires declared-pattern proofs there. Full additionally proves infrastructure outages through its world (R112)."
1622
1904
  path: "src/tests/integration/<capability>/*.integration-spec.ts"
1623
1905
  presence: optional
1624
1906
  tracked: tracked
@@ -1632,10 +1914,12 @@ slots:
1632
1914
  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
1633
1915
  rules: [BE_TEST_TOPOLOGY, BE_INTEGRATION_SPEC_MISSING]
1634
1916
  - id: be.tests.e2e
1917
+ since: 3.1.0
1635
1918
  profiles: [be]
1919
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): full proves app flows through its e2e world; lite uses the R272 wiring proof and unit/integration specs."
1636
1920
  path: "src/tests/e2e/<area>/*.e2e-spec.ts"
1637
1921
  presence: optional
1638
- litePresence: forbidden # owner ruling 2026-10-12: a lite app has no e2e world
1922
+ litePresence: forbidden # ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite has no e2e world
1639
1923
  tracked: tracked
1640
1924
  tier: e2e
1641
1925
  tests: e2e
@@ -1644,18 +1928,20 @@ slots:
1644
1928
  why: flow e2e through useTestWorld({ apps }); external services are the world's network fakes; run by test:e2e
1645
1929
  rules: [HFS_CI_E2E_AUTOMATIC, BE_TEST_TOPOLOGY, BE_E2E_FLOW]
1646
1930
  - id: be.tests.contract
1931
+ since: 3.1.0
1647
1932
  profiles: [be]
1648
1933
  path: "src/tests/contract/<provider>/*.contract-spec.ts"
1649
1934
  presence: optional
1650
- litePresence: forbidden # owner ruling 2026-10-12: a lite app has no contract world
1935
+ litePresence: optional # owner decision 2026-10-14: lite may run provider sandbox contracts
1651
1936
  tracked: tracked
1652
1937
  tier: e2e
1653
1938
  tests: e2e
1654
- goesTo: "the full edition: a lite app has no contract world (provider sandbox specs)"
1939
+ goesTo: "src/tests/contract/<provider>/*.contract-spec.ts in either edition: optional provider sandbox specs via test:contract; e2e stays full-only"
1655
1940
  coverage: none
1656
1941
  why: provider sandbox contracts, skipped without sandbox config; run only by test:contract, never part of test or test:e2e
1657
1942
  rules: [BE_TEST_TOPOLOGY]
1658
1943
  - id: be.tests.e2e-world-dir
1944
+ since: 3.1.0
1659
1945
  profiles: [be]
1660
1946
  path: "src/tests/e2e/world/"
1661
1947
  presence: forbidden
@@ -1666,6 +1952,7 @@ slots:
1666
1952
 
1667
1953
  # ----- FE: apps ----------------------------------------------------------------------------------------
1668
1954
  - id: fe.app.next
1955
+ since: 3.1.0
1669
1956
  profiles: [fe]
1670
1957
  # Each fe app is an npm workspace: its package.json is @<project>/<app> with its own runtime dependencies and the fixed
1671
1958
  # workspace scripts (HFS_MONO_FE_WORKSPACE); turbo runs them.
@@ -1680,6 +1967,7 @@ slots:
1680
1967
  tests: none
1681
1968
  rules: [FE_APP_ISOLATION, FE_ERROR_BOUNDARY_MISSING, FE_NEXT_CONVENTIONS, HFS_MONO_FE_WORKSPACE, HFS_MONO_WORKSPACE_DEP]
1682
1969
  - id: fe.app-optional
1970
+ since: 3.1.0
1683
1971
  profiles: [fe]
1684
1972
  path: "apps/<app>/public/"
1685
1973
  presence: optional
@@ -1687,12 +1975,13 @@ slots:
1687
1975
  tier: none
1688
1976
  tests: none
1689
1977
  - id: fe.route
1978
+ since: 3.1.0
1690
1979
  profiles: [fe]
1691
1980
  path: "apps/<app>/src/app/"
1692
1981
  presence: required
1693
1982
  tracked: tracked
1694
1983
  tier: route
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
1984
+ # ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4) (lite = full): routes are thin and the front end serves no API - health/live is the one
1696
1985
  # route handler of this slot, the Supabase auth callback is fe.route.callback, and no file holds a Server Action
1697
1986
  # (FE_ROUTE_HANDLER_FORBIDDEN, FE_SERVER_ACTION_FORBIDDEN).
1698
1987
  allows: ["[locale]/**/{page,layout,template,loading,error,not-found,default}.tsx", "[locale]/providers.tsx",
@@ -1705,6 +1994,7 @@ slots:
1705
1994
  tests: none
1706
1995
  rules: [FE_ROUTE_FILES_THIN, FE_CLIENT_BOUNDARY, FE_OWNER_REACHABLE, FE_ROUTE_HANDLER_FORBIDDEN, FE_SERVER_ACTION_FORBIDDEN]
1707
1996
  - id: fe.source-root-pinned
1997
+ since: 3.1.0
1708
1998
  profiles: [fe]
1709
1999
  path: "apps/<app>/src/{proxy.ts,instrumentation.ts,instrumentation-client.ts,sentry.server.config.ts,sentry.edge.config.ts}"
1710
2000
  presence: optional
@@ -1714,6 +2004,7 @@ slots:
1714
2004
  tests: none
1715
2005
  rules: [FE_NEXT_CONVENTIONS] # middleware.ts is refused on Next >= 16 (fe.source-root-middleware)
1716
2006
  - id: fe.source-root-middleware
2007
+ since: 3.1.0
1717
2008
  profiles: [fe]
1718
2009
  path: "apps/<app>/src/{middleware.ts,middleware.js}"
1719
2010
  presence: forbidden
@@ -1724,6 +2015,8 @@ slots:
1724
2015
  rules: [FE_NEXT_CONVENTIONS]
1725
2016
  - id: fe.route.callback
1726
2017
  profiles: [fe]
2018
+ exemplifiedBy:
2019
+ - {path: tests/fixtures/hfs-callback-full/fe/apps/web/src/app/auth/callback/route.ts, edition: full}
1727
2020
  # The auth callback of a Supabase session (the PKCE code exchange), kept only when the auth flow truly needs a server exchange
1728
2021
  # (prefer the client-side PKCE exchange in fe.modules.auth): the one non-health route handler of an app with a Supabase
1729
2022
  # connection; an app without one has no route handler in fe/ (L15).
@@ -1734,7 +2027,10 @@ slots:
1734
2027
  tier: route
1735
2028
  tests: none
1736
2029
  since: 2.1.0
2030
+ rules: [FE_ROUTE_HANDLER_FORBIDDEN, FE_CALLBACK_SHAPE, FE_SUPABASE_CLIENT_OWNER, FE_SERVICE_ROLE_FORBIDDEN]
2031
+
1737
2032
  - id: fe.feature
2033
+ since: 3.1.0
1738
2034
  profiles: [fe]
1739
2035
  path: "apps/<app>/src/features/{pages,layouts,overlays}/<name>/"
1740
2036
  presence: required
@@ -1749,6 +2045,7 @@ slots:
1749
2045
  budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
1750
2046
  rules: [FE_SIZE_BUDGET, FE_OWNER_REACHABLE, FE_I18N_LITERAL]
1751
2047
  - id: fe.components
2048
+ since: 3.1.0
1752
2049
  profiles: [fe]
1753
2050
  path: "apps/<app>/src/components/{blocks,composites,branches,leaves}/<name>/"
1754
2051
  presence: optional
@@ -1779,6 +2076,7 @@ slots:
1779
2076
  since: 3.0.0
1780
2077
  rules: [FE_CLIENT_BOUNDARY]
1781
2078
  - id: fe.hooks
2079
+ since: 3.1.0
1782
2080
  profiles: [fe]
1783
2081
  path: "apps/<app>/src/hooks/<domain>/"
1784
2082
  presence: optional
@@ -1789,7 +2087,7 @@ slots:
1789
2087
  allows: [index.ts, "use<name>.ts", "<domain>.shared.ts"] # React hooks only; <domain>.shared.ts = sanctioned non-hook helpers
1790
2088
  roles: {entry: index.ts, shared: "<domain>.shared.ts"}
1791
2089
  tests: none
1792
- budget: {file: 200, dataHooks: 6, useState: 6}
2090
+ budget: {file: 200, barrel: 400, dataHooks: 6, useState: 6} # barrel: index.ts containing only export ... from statements
1793
2091
  rules: [FE_HOOKS_ARE_HOOKS, FE_DATA_FRESHNESS]
1794
2092
  - id: fe.hooks.swr
1795
2093
  profiles: [fe]
@@ -1806,7 +2104,7 @@ slots:
1806
2104
  "api/rest/queries/useGet*Swr.ts", "api/rest/mutations/use{Post,Put,Patch,Delete}*Swr.ts"]
1807
2105
  roles: {entry: index.ts, effects: SwrSideEffects.tsx}
1808
2106
  tests: none
1809
- budget: {file: 200}
2107
+ budget: {file: 200, barrel: 400}
1810
2108
  since: 3.0.0
1811
2109
  rules: [FE_HOOKS_ARE_HOOKS, FE_DATA_FRESHNESS]
1812
2110
  - id: fe.hooks.rhf
@@ -1822,7 +2120,7 @@ slots:
1822
2120
  allows: [index.ts, "use*Form.ts"]
1823
2121
  roles: {entry: index.ts}
1824
2122
  tests: none
1825
- budget: {file: 200}
2123
+ budget: {file: 200, barrel: 400}
1826
2124
  since: 3.0.0
1827
2125
  rules: [FE_HOOKS_ARE_HOOKS]
1828
2126
  - id: fe.hooks.zustand
@@ -1837,7 +2135,7 @@ slots:
1837
2135
  allows: [index.ts, store.ts, "use*.ts"]
1838
2136
  roles: {entry: index.ts, store: store.ts}
1839
2137
  tests: none
1840
- budget: {file: 200}
2138
+ budget: {file: 200, barrel: 400}
1841
2139
  since: 3.0.0
1842
2140
  rules: [FE_HOOKS_ARE_HOOKS]
1843
2141
  - id: fe.hooks.socketio
@@ -1853,7 +2151,7 @@ slots:
1853
2151
  allows: [index.ts, sockets.ts, SocketIoSideEffects.tsx, connectionStore.ts, "enums/*.ts", "types/*.ts", "use*.ts"]
1854
2152
  roles: {entry: index.ts, effects: SocketIoSideEffects.tsx}
1855
2153
  tests: none
1856
- budget: {file: 200}
2154
+ budget: {file: 200, barrel: 400}
1857
2155
  since: 3.0.0
1858
2156
  rules: [FE_HOOKS_ARE_HOOKS]
1859
2157
  - id: fe.hooks.effects
@@ -1869,7 +2167,7 @@ slots:
1869
2167
  allows: [index.ts, "use*.ts", "*SideEffect.tsx", "*SideEffects.tsx"]
1870
2168
  roles: {entry: index.ts, effects: "*SideEffect.tsx"}
1871
2169
  tests: none
1872
- budget: {file: 200}
2170
+ budget: {file: 200, barrel: 400}
1873
2171
  since: 3.0.0
1874
2172
  rules: [FE_HOOKS_ARE_HOOKS]
1875
2173
  - id: fe.hooks.reuseables
@@ -1884,7 +2182,7 @@ slots:
1884
2182
  allows: [index.ts, "use*.ts"]
1885
2183
  roles: {entry: index.ts}
1886
2184
  tests: none
1887
- budget: {file: 200}
2185
+ budget: {file: 200, barrel: 400}
1888
2186
  since: 3.0.0
1889
2187
  rules: [FE_HOOKS_ARE_HOOKS]
1890
2188
  - id: fe.redux
@@ -1901,7 +2199,10 @@ slots:
1901
2199
  tests: none
1902
2200
  budget: {file: 400}
1903
2201
  since: 3.0.0
2202
+ rules: [FE_SOURCE_FORM, FE_HOOKS_ARE_HOOKS, FE_OWNER_REACHABLE]
2203
+
1904
2204
  - id: fe.modules
2205
+ since: 3.1.0
1905
2206
  profiles: [fe]
1906
2207
  path: "apps/<app>/src/modules/<capability>/"
1907
2208
  presence: required
@@ -1909,11 +2210,13 @@ slots:
1909
2210
  tier: modules
1910
2211
  owner: true
1911
2212
  requiredInstances: {capability: [config, i18n, routes]}
2213
+ # A populated fe.modules instance with no .ts or .tsx files (CSS/assets only) needs no index.ts.
1912
2214
  requires: [index.ts]
1913
2215
  tests: none
1914
2216
  budget: {file: 400}
1915
2217
  rules: [FE_ENV_OWNER, FE_TRANSPORT_OWNER]
1916
2218
  - id: fe.modules.api
2219
+ since: 3.1.0
1917
2220
  profiles: [fe]
1918
2221
  path: "apps/<app>/src/modules/api/"
1919
2222
  presence: optional
@@ -1933,7 +2236,20 @@ slots:
1933
2236
  entries: ["graphql/queries/query-*.ts", "graphql/queries/types/*.ts", "graphql/mutations/mutation-*.ts", "graphql/mutations/types/*.ts"]
1934
2237
  tests: none
1935
2238
  rules: [FE_TRANSPORT_OWNER, FE_HTTP_STATUS_COLLAPSE, FE_WIRE_GENERATED]
2239
+ - id: fe.transport.generated
2240
+ since: 3.2.0
2241
+ profiles: [fe]
2242
+ path: "apps/<app>/src/modules/api/graphql/__generated__/"
2243
+ presence: optional
2244
+ tracked: ignored
2245
+ tier: transport
2246
+ tests: none
2247
+ why: the canonical GraphQL client output of codegen is local and never committed; this specific slot overrides the tracked API parent
2248
+ rules: [FE_WIRE_GENERATED]
2249
+ exemplifiedBy:
2250
+ - {path: tests/fixtures/hfs-generated-wire/fe/apps/web/src/modules/api/graphql/__generated__/graphql.ts, editions: [lite, full]}
1936
2251
  - id: fe.transport.client
2252
+ since: 3.1.0
1937
2253
  profiles: [fe]
1938
2254
  path: "apps/<app>/src/modules/api/graphql/clients/"
1939
2255
  presence: optional
@@ -1943,6 +2259,7 @@ slots:
1943
2259
  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
1944
2260
  rules: [FE_TRANSPORT_OWNER]
1945
2261
  - id: fe.transport.outcome
2262
+ since: 3.1.0
1946
2263
  profiles: [fe]
1947
2264
  path: "apps/<app>/src/modules/api/graphql/types.ts"
1948
2265
  presence: optional
@@ -1953,6 +2270,7 @@ slots:
1953
2270
  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
1954
2271
  rules: [FE_HTTP_STATUS_COLLAPSE]
1955
2272
  - id: fe.modules.config
2273
+ since: 3.1.0
1956
2274
  profiles: [fe]
1957
2275
  path: "apps/<app>/src/modules/config/"
1958
2276
  presence: required
@@ -1960,7 +2278,10 @@ slots:
1960
2278
  tier: foundation
1961
2279
  tests: none
1962
2280
  why: the only reader of process.env and NEXT_PUBLIC_*; fails fast, no localhost fallback
2281
+ rules: [FE_ENV_OWNER]
2282
+
1963
2283
  - id: fe.modules.i18n
2284
+ since: 3.1.0
1964
2285
  profiles: [fe]
1965
2286
  path: "apps/<app>/src/modules/i18n/"
1966
2287
  presence: required
@@ -1973,6 +2294,7 @@ slots:
1973
2294
  tests: none
1974
2295
  rules: [FE_I18N_PLACEMENT, FE_I18N_CATALOG]
1975
2296
  - id: fe.modules.brand
2297
+ since: 3.1.0
1976
2298
  profiles: [fe]
1977
2299
  path: "apps/<app>/src/modules/brand/brand.css"
1978
2300
  presence: optional
@@ -1982,6 +2304,7 @@ slots:
1982
2304
  why: the ONLY app file that may hold colour values; sets grammar brand tokens only, light and dark
1983
2305
  rules: [FE_STYLE_TOKEN_ONLY]
1984
2306
  - id: fe.modules.routes
2307
+ since: 3.1.0
1985
2308
  profiles: [fe]
1986
2309
  path: "apps/<app>/src/modules/routes/"
1987
2310
  presence: required
@@ -1989,7 +2312,10 @@ slots:
1989
2312
  tier: foundation
1990
2313
  tests: none
1991
2314
  why: every href builder; FE_HREF_RESOLVES checks each against app/
2315
+ rules: [FE_HREF_RESOLVES, FE_ROUTE_HARDCODED]
2316
+
1992
2317
  - id: fe.modules.types
2318
+ since: 3.1.0
1993
2319
  profiles: [fe]
1994
2320
  path: "apps/<app>/src/modules/types/"
1995
2321
  presence: optional
@@ -1997,6 +2323,8 @@ slots:
1997
2323
  tier: foundation
1998
2324
  tests: none
1999
2325
  why: shared types with no runtime behaviour; a component may import them
2326
+ rules: [FE_TYPE_ESCAPE, HFS_TS_STRICT]
2327
+
2000
2328
  - id: fe.modules.types.ambient
2001
2329
  parent: fe.modules.types
2002
2330
  profiles: [fe]
@@ -2009,6 +2337,8 @@ slots:
2009
2337
  tests: none
2010
2338
  since: 2.2.0
2011
2339
  why: global ambient declarations loaded by the app compiler; this owner exposes no runtime API
2340
+ rules: [FE_TYPE_ESCAPE, HFS_TS_STRICT]
2341
+
2012
2342
  - id: fe.modules.auth
2013
2343
  profiles: [fe]
2014
2344
  # The identity-provider client (fe-stack auth, FE-AUTH): the session, the access token the Apollo link attaches and the
@@ -2020,6 +2350,7 @@ slots:
2020
2350
  tier: modules
2021
2351
  owner: true
2022
2352
  requires: [index.ts]
2353
+ entries: [server.ts] # the server-only PKCE exchange API used by the opted-in callback route
2023
2354
  tests: none
2024
2355
  since: 3.0.0
2025
2356
  rules: [FE_SUPABASE_CLIENT_OWNER, FE_SERVICE_ROLE_FORBIDDEN]
@@ -2037,7 +2368,10 @@ slots:
2037
2368
  roles: {entry: index.ts, hooks: hooks.ts} # hooks.ts: the use*WithToast hooks of the toast module
2038
2369
  tests: none
2039
2370
  since: 3.0.0
2371
+ rules: [FE_I18N_LITERAL, FE_HOOKS_ARE_HOOKS, FE_HTTP_STATUS_COLLAPSE]
2372
+
2040
2373
  - id: fe.package.ui
2374
+ since: 3.1.0
2041
2375
  profiles: [fe]
2042
2376
  path: "packages/<family>-ui/"
2043
2377
  presence: opt-in
@@ -2052,6 +2386,7 @@ slots:
2052
2386
  why: built to dist with explicit named exports; only composites, branches and leaves; no unused export; no colour literal
2053
2387
  rules: [FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT, FE_STYLE_TOKEN_ONLY]
2054
2388
  - id: fe.package.api
2389
+ since: 3.1.0
2055
2390
  profiles: [fe]
2056
2391
  path: "packages/<family>-api/"
2057
2392
  presence: opt-in
@@ -2063,6 +2398,7 @@ slots:
2063
2398
  why: the one transport client and the one Outcome union shared by every app of the repository; no app keeps its own
2064
2399
  rules: [FE_TRANSPORT_OWNER, FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT]
2065
2400
  - id: fe.package.api.client
2401
+ since: 3.1.0
2066
2402
  profiles: [fe]
2067
2403
  path: "packages/<family>-api/src/graphql/clients/"
2068
2404
  presence: optional # enabled with its package (fe.package.api is the opt-in)
@@ -2072,6 +2408,7 @@ slots:
2072
2408
  why: the repository's one pair of Apollo clients and their link chain
2073
2409
  rules: [FE_TRANSPORT_OWNER]
2074
2410
  - id: fe.package.api.outcome
2411
+ since: 3.1.0
2075
2412
  profiles: [fe]
2076
2413
  path: "packages/<family>-api/src/graphql/types.ts"
2077
2414
  presence: optional # enabled with its package (fe.package.api is the opt-in)
@@ -2082,6 +2419,7 @@ slots:
2082
2419
  why: the repository's one GraphQLResponse union
2083
2420
  rules: [FE_HTTP_STATUS_COLLAPSE]
2084
2421
  - id: fe.package.i18n
2422
+ since: 3.1.0
2085
2423
  profiles: [fe]
2086
2424
  path: "packages/<family>-i18n/"
2087
2425
  presence: opt-in