@starci/eslint-canon-fe 8.3.0 → 8.4.1

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 (80) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/callback-shape.mjs +344 -0
  3. package/client-boundary.mjs +1 -1
  4. package/docs/hooks-folder.md +1 -1
  5. package/env-owner.mjs +120 -9
  6. package/function-budget.mjs +49 -0
  7. package/hooks-folder.mjs +149 -18
  8. package/icon.mjs +67 -0
  9. package/index.mjs +47 -0
  10. package/lib/own-hook-origin.mjs +53 -0
  11. package/lib/program-for.mjs +22 -0
  12. package/lib/pure-owner-helper.mjs +148 -0
  13. package/lib/redux-hook-origin.mjs +44 -0
  14. package/next-conventions.mjs +38 -35
  15. package/package.json +1 -1
  16. package/runtime/config.example.yaml +7 -6
  17. package/runtime/engine/config.mjs +10 -18
  18. package/runtime/engine/removed-vocabulary.mjs +0 -2
  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 +333 -62
  22. package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
  23. package/runtime/knowledge/sonar-gate.yaml +1 -1
  24. package/runtime/modules/kernel/removed-vocabulary.yaml +30 -0
  25. package/runtime/modules/models/runtimes.yaml +5 -7
  26. package/runtime/packages/hfs/templates/app/package-scripts/package.json +1 -0
  27. package/runtime/scripts/api/git/lib.mjs +4 -1
  28. package/runtime/scripts/api/git/merge-base.mjs +9 -0
  29. package/runtime/scripts/api/git/recorded-lines.mjs +54 -8
  30. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
  31. package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
  32. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  33. package/runtime/scripts/hfs/architecture/config.mjs +1 -1
  34. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  35. package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
  36. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  37. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  38. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  39. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  40. package/runtime/scripts/hfs/architecture/index.mjs +1 -1
  41. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
  42. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  43. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  44. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  45. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  46. package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
  47. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  48. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
  49. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
  50. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  51. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  52. package/runtime/scripts/hfs/architecture/tiers.mjs +4 -4
  53. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
  54. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  55. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  56. package/runtime/scripts/hfs/manifest-shape.mjs +69 -9
  57. package/runtime/scripts/hfs/param-names.mjs +1 -1
  58. package/runtime/scripts/hfs/project-rule.mjs +3 -1
  59. package/runtime/scripts/hfs/rule-catalog.mjs +158 -11
  60. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  61. package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
  62. package/runtime/scripts/hfs/slot-imports.mjs +18 -5
  63. package/runtime/scripts/hfs/slots.mjs +3 -2
  64. package/runtime/scripts/hfs/typescript-programs.mjs +0 -8
  65. package/runtime/scripts/lib/env.mjs +20 -0
  66. package/runtime/scripts/lib/event-contract.mjs +107 -0
  67. package/runtime/scripts/lib/git.mjs +1 -1
  68. package/runtime/scripts/lib/i18n.mjs +0 -2
  69. package/runtime/scripts/lib/json-clone.mjs +61 -0
  70. package/runtime/scripts/lib/language.mjs +1 -1
  71. package/runtime/scripts/lib/list.mjs +4 -0
  72. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  73. package/runtime/scripts/lib/stack-services.mjs +1 -1
  74. package/runtime/scripts/lib/ts-analysis.mjs +5 -2
  75. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  76. package/served-locale.mjs +80 -9
  77. package/size-growth.mjs +10 -6
  78. package/swr-family.mjs +197 -0
  79. package/translation.mjs +135 -7
  80. 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.1.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,8 +25,8 @@ 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
@@ -37,6 +37,7 @@ versioning:
37
37
  # for fail-fast env parsing only, and `<app>.options.ts` holds env-derived options and port bindings only, with no parsing
38
38
  # or environment reads (BE_APP_OPTIONS_COMPOSITION). Secrets use EnvSource.secret(): <NAME>_FILE first, plain <NAME> only
39
39
  # when the file key is absent; .starcistacks owns custody. Capability <capability>.config.ts roles stay unchanged.
40
+ # 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.
40
41
 
41
42
  # Field meaning, per slot.
42
43
  # id stable name, never changes meaning inside a major.
@@ -83,16 +84,16 @@ versioning:
83
84
  # liteManagedBy is managed only in lite; its effective lite.path still lists literal files.
84
85
  # the managed file list lives here and nowhere in code. A difference is HFS_MANAGED_FILE_DRIFT (a
85
86
  # back end's tsconfig.json: HFS_TS_STRICT when a flag or an option was added).
86
- # rules rule ids (the catalog lands with knowledge/hfs/rules.yaml).
87
+ # rules rule codes from knowledge/hfs/rules.yaml.
87
88
  # editions the editions (below) the slot exists in; default all of them. A slot outside the declared edition owns
88
89
  # no path - under lite, what full keeps absent is removed, and what lite forbids is a finding.
89
90
  # litePresence the presence under edition lite (default: presence): a bare value, or {be, fe} for a both-sides slot.
90
91
  # A slot forbidden in lite is external there and must name its goesTo. Only infrastructure is forbidden in lite:
91
- # the self-hosted stack (.starcistacks infra/, runtime/, seeds/), the e2e and contract test layers and their
92
- # 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.
93
94
  # lite the fields the slot carries under lite only (path, requires, allows, forbids, minInstances,
94
95
  # requiredInstances, why), each shaped as the same field in the slot body; every other field is the edition's.
95
- # `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).
96
97
  # `tests` is never overlaid: a lite slot owes exactly the unit specs its full slot owes.
97
98
  # provider the hfs.json connections[].provider that enables this opt-in slot (supabase); never an app kind.
98
99
  # since the manifest version that introduced the slot.
@@ -100,17 +101,18 @@ presenceValues: [required, optional, opt-in, forbidden]
100
101
  trackedValues: [tracked, ignored, external]
101
102
  testValues: [unit-beside, e2e, none]
102
103
 
103
- # The editions a repository declares in hfs.json `edition` (absent means full). Owner ruling 2026-10-12 (R212 case-1): lite IS
104
- # 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,
105
106
  # event bus, saga, reactors, projection, realtime, webhooks, fenced job), declared in hfs.json the same way, and the same unit
106
107
  # specs (unitRoles, logicRoles, 100 per file, @starci/jest-preset doubles, builders). What differs is ADAPTERS and DELIVERY, all
107
- # on Supabase: schema authority in supabase/migrations, the queue and event-bus ports on Supabase Queues (pgmq), realtime on
108
- # Supabase Realtime, cache and locks on Postgres, schedules on pg_cron (+ pg_net), storage and auth on Supabase. Integration specs
109
- # are optional and run only against the local Supabase CLI stack (`starci supabase start`); there is no e2e or contract world and no
110
- # 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).
111
113
  # Lite is this same manifest filtered: a slot whose `editions` lacks lite does not exist for a lite app, `litePresence` replaces
112
114
  # presence and a `lite` map replaces the fields it names. Upgrade lite -> full adds only (swap the adapters, add the e2e and
113
- # 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).
114
116
  editions: [full, lite]
115
117
 
116
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
@@ -162,12 +164,12 @@ tiers:
162
164
  package: {mayImport: [package]}
163
165
  fe:
164
166
  route: {mayImport: [feature, foundation, modules, transport, package]} # app/** adapters
165
- feature: {mayImport: [feature, components, hooks, foundation, modules, transport, package], mayImportKinds: {feature: {from: [pages, layouts], to: [overlays]}}} # pages and layouts compose overlays and blocks; other feature-to-feature edges stay forbidden
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
166
168
  # A component never reaches data: config, routes, i18n and types are foundation; every other module and the api
167
169
  # transport are reached through a hook (owner decision 2026-09-29).
168
170
  components: {mayImport: [components, hooks, foundation, package], lowerLayerOnly: true} # blocks, composites, branches, leaves
169
171
  # Across domains a hook is imported only through the other domain's public index (hooks/<domain>/index.ts), never deep.
170
- 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
171
173
  foundation: {mayImport: [foundation, package], acyclic: true} # modules/{config,i18n,routes,types}
172
174
  modules: {mayImport: [foundation, modules, transport, package], acyclic: true}
173
175
  transport: {mayImport: [foundation, modules, package]} # modules/api: the one fetch
@@ -178,10 +180,16 @@ crossApp: apps never import each other; shared code is a packages/<pkg> slot.
178
180
  # Parameters the lint factories and checks read per profile (through ruleParams(profile) in scripts/hfs/slots.mjs, which
179
181
  # merges common under the profile: a side restates a common key only to override it).
180
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]
181
187
  common:
182
188
  # HFS_SIZE_GROWTH: a file above soft may not grow against its parent commit; a new file stays within soft. Slot
183
189
  # budgets (component.tsx 300, hooks 200, modules 400) are stricter than this cap where they apply.
184
190
  fileLines: {soft: 500, hardGrowth: true}
191
+ functionLines: 80
192
+ featureFiles: 250
185
193
  # R21 HFS_DUPLICATE_CODE: the one threshold of duplicate code of both sides (the architecture machine reads it here;
186
194
  # no other file states it): a token-normalised block of at least `lines` source lines and `tokens` tokens that appears twice.
187
195
  duplicateBlock: {lines: 8, tokens: 60}
@@ -328,7 +336,7 @@ ruleParams:
328
336
  # Edition lite (ruleParams(manifest, 'be', 'lite') merges these keys over the base; a map merges one level deep): the schema
329
337
  # authority is the Supabase migration tree (supabase/migrations/*.sql, forward-only), so a lite back end holds no TypeORM
330
338
  # migration or entity under be/ (R34/R35 read schemaAuthority; be.persistence's lite overlay allows neither). The unit roles,
331
- # 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 -
332
340
  # platform/queue and platform/event-bus run Supabase Queues (pgmq) through the shared EntityManager, cache and locks are
333
341
  # Postgres (advisory locks or an unlogged table), and realtime is the same socket.io gateway as full, run inside the api
334
342
  # process with no Redis adapter (owner ruling 2026-10-13).
@@ -347,6 +355,7 @@ slots:
347
355
 
348
356
  # ----- app root (profile app: the one repository of a product) --------------------------------------------
349
357
  - id: app.declaration
358
+ since: 3.1.0
350
359
  profiles: [app]
351
360
  path: hfs.json
352
361
  presence: required
@@ -355,6 +364,7 @@ slots:
355
364
  tests: none
356
365
  rules: [HFS_ARCH_CONFIG_UNREAD]
357
366
  - id: app.readme
367
+ since: 3.1.0
358
368
  profiles: [app]
359
369
  path: README.md
360
370
  presence: required
@@ -363,7 +373,9 @@ slots:
363
373
  tests: none
364
374
  rules: [HFS_README_*]
365
375
  - id: app.package-manifest
376
+ since: 3.1.0
366
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."
367
379
  # The one package.json of the app, at the app root: be's dependencies and every tool, and every script; npm workspaces are
368
380
  # exactly fe/apps/* and fe/packages/* (HFS_MONO_WORKSPACES), each with its own package.json. be has no package.json of its own.
369
381
  path: package.json
@@ -375,6 +387,7 @@ slots:
375
387
  liteManagedBy: package-scripts-lite
376
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]
377
389
  - id: app.task-graph
390
+ since: 3.1.0
378
391
  profiles: [app]
379
392
  # The turbo task graph of the app (build, dev, lint, typecheck over the fe workspaces; build depends on ^build and caches
380
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
@@ -387,6 +400,7 @@ slots:
387
400
  managedBy: task-graph
388
401
  rules: [HFS_MANAGED_FILE_DRIFT, HFS_MONO_WORKSPACES]
389
402
  - id: app.lockfile
403
+ since: 3.1.0
390
404
  profiles: [app]
391
405
  path: package-lock.json
392
406
  presence: required
@@ -395,22 +409,42 @@ slots:
395
409
  tests: none
396
410
  rules: [HFS_PACKAGE_MANAGER_MIXED, HFS_DEP_VERSION_SKEW]
397
411
  - id: app.git-meta
412
+ since: 3.1.0
398
413
  profiles: [app]
399
- path: "{.gitignore,.gitattributes}"
414
+ path: .gitignore
400
415
  presence: required
401
416
  tracked: tracked
402
417
  tier: none
403
418
  tests: none
404
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]
405
432
  - id: app.tool-config
433
+ since: 3.1.0
406
434
  profiles: [app]
407
435
  path: "{.editorconfig,.nvmrc}"
408
- presence: required # plain dotfiles, the repository's own
436
+ presence: required
409
437
  tracked: tracked
410
438
  tier: none
411
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]
412
444
  - id: app.format-config
445
+ since: 3.1.0
413
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."
414
448
  # The one formatter configuration of the app (prettier walks up from every file of both sides to it): .prettierrc names
415
449
  # @starci/prettier-config, .prettierignore is the template.
416
450
  path: "{.prettierrc,.prettierignore}"
@@ -422,6 +456,7 @@ slots:
422
456
  liteManagedBy: tool-config-lite # the same files plus the generated supabase/types, which the Supabase CLI writes unformatted
423
457
  rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL]
424
458
  - id: app.tool-config-optional
459
+ since: 3.1.0
425
460
  profiles: [app]
426
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
427
462
  presence: optional
@@ -429,6 +464,7 @@ slots:
429
464
  tier: none
430
465
  tests: none
431
466
  - id: app.tool-config-local
467
+ since: 3.1.0
432
468
  profiles: [app]
433
469
  path: "{tsconfig.json,.eslintrc,.eslintrc.*,.eslintignore,eslint.config.*,.stylelintrc,.stylelintrc.*,.stylelintignore,stylelint.config.*,.prettierrc.*,prettier.config.*,jest.config.*,lint-staged.config.*,.lintstagedrc*}"
434
470
  presence: forbidden
@@ -438,6 +474,7 @@ slots:
438
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)"
439
475
  rules: [HFS_TOOL_CONFIG_LOCAL]
440
476
  - id: app.quality-config
477
+ since: 3.1.0
441
478
  profiles: [app]
442
479
  path: "{sonar-project.properties,codecov.yml}"
443
480
  presence: required
@@ -447,6 +484,7 @@ slots:
447
484
  managedBy: quality-config # lite renders the same files: the same coverage scope, Sonar import and Codecov flags as full
448
485
  rules: [HFS_SONAR_CONFIG, HFS_MANAGED_FILE_DRIFT]
449
486
  - id: app.hooks
487
+ since: 3.1.0
450
488
  profiles: [app]
451
489
  path: ".husky/{pre-commit,pre-push}"
452
490
  presence: required
@@ -456,7 +494,9 @@ slots:
456
494
  managedBy: hooks
457
495
  rules: [HFS_CI_E2E_AUTOMATIC, HFS_MANAGED_FILE_DRIFT]
458
496
  - id: app.ci
497
+ since: 3.1.0
459
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."
460
500
  path: ".github/workflows/ci.yml"
461
501
  presence: required
462
502
  tracked: tracked
@@ -466,18 +506,22 @@ slots:
466
506
  liteManagedBy: ci-workflows-lite
467
507
  rules: [HFS_CI_E2E_AUTOMATIC, HFS_CI_MISSING_CANON, HFS_MANAGED_FILE_DRIFT]
468
508
  - id: app.ci-e2e
509
+ since: 3.1.0
469
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."
470
512
  path: ".github/workflows/e2e.yml"
471
513
  presence: optional # on: workflow_dispatch only
472
- 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
473
515
  tracked: tracked
474
516
  tier: none
475
517
  tests: none
476
- 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"
477
519
  managedBy: ci-workflows
478
520
  rules: [HFS_CI_E2E_AUTOMATIC, HFS_MANAGED_FILE_DRIFT]
479
521
  - id: app.ci-images
522
+ since: 3.1.0
480
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."
481
525
  # Builds every image of the app (one matrix entry per declared app, path-filtered), never pushes: rendered by starci app sync.
482
526
  path: ".github/workflows/images.yml"
483
527
  presence: required
@@ -489,6 +533,9 @@ slots:
489
533
  rules: [HFS_MANAGED_FILE_DRIFT]
490
534
  - id: app.ci-db
491
535
  profiles: [app]
536
+ exemplifiedBy:
537
+ - packages/hfs/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml
538
+ 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."
492
539
  # The database deploy workflow: workflow_dispatch only (like app.ci-e2e); starci app sync renders it under the managed ci group.
493
540
  path: ".github/workflows/db-deploy.yml"
494
541
  presence: optional
@@ -498,7 +545,22 @@ slots:
498
545
  liteManagedBy: ci-workflows-lite
499
546
  rules: [HFS_MANAGED_FILE_DRIFT]
500
547
  since: 2.1.0
548
+ exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
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]
501
562
  - id: app.dockerignore
563
+ since: 3.1.0
502
564
  profiles: [app]
503
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.
504
566
  path: .dockerignore
@@ -509,6 +571,7 @@ slots:
509
571
  managedBy: docker-ignore
510
572
  rules: [HFS_MANAGED_FILE_DRIFT]
511
573
  - id: app.deploy
574
+ since: 3.1.0
512
575
  profiles: [app]
513
576
  # Production delivery tooling for the app's own processes: compose files, Caddyfile, *.env.example and ops shell scripts.
514
577
  # Lite still keeps its backing services on Supabase and its encrypted custody in app.starcistacks; R06 scans every file here.
@@ -519,6 +582,7 @@ slots:
519
582
  tests: none
520
583
  rules: [HFS_PLAINTEXT_SECRET]
521
584
  - id: app.ci-deploy
585
+ since: 3.1.0
522
586
  profiles: [app]
523
587
  # Product-owned delivery workflows; the literal app.ci-db path remains the managed database workflow's more specific slot.
524
588
  path: ".github/workflows/<app>-deploy.yml"
@@ -528,6 +592,7 @@ slots:
528
592
  tests: none
529
593
  rules: [HFS_PLAINTEXT_SECRET]
530
594
  - id: app.ci-authorize-deploy-key
595
+ since: 3.1.0
531
596
  profiles: [app]
532
597
  path: ".github/workflows/authorize-deploy-key.yml"
533
598
  presence: optional
@@ -536,7 +601,9 @@ slots:
536
601
  tests: none
537
602
  rules: [HFS_PLAINTEXT_SECRET]
538
603
  - id: app.browser
604
+ since: 3.1.0
539
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."
540
607
  # The browser journey of the product (Playwright, owner ruling: one minimal journey per example app, run manually): the config, its
541
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
542
609
  # refusing any test or test tooling inside fe/). Declared by `browser: true` in hfs.json; the managed script `test:browser` exists only then.
@@ -550,14 +617,31 @@ slots:
550
617
  requires: [playwright.config.ts, journeys/]
551
618
  allows: [playwright.config.ts, stack.mjs, "journeys/*.ts"]
552
619
  - id: app.github-meta
620
+ since: 3.1.0
553
621
  profiles: [app]
554
- path: ".github/{CODEOWNERS,pull_request_template.md,ISSUE_TEMPLATE/**,dependabot.yml}"
622
+ path: ".github/{CODEOWNERS,pull_request_template.md,ISSUE_TEMPLATE/**}"
555
623
  presence: optional
556
624
  tracked: tracked
557
625
  tier: none
558
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]
559
639
  - id: app.scripts
640
+ since: 3.1.0
560
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))."
561
645
  # The app's operational scripts only (`*.mjs`, `*.cjs`, `*.ps1`, `*.sh`), or nothing: an empty folder is `scripts/.gitkeep`, which
562
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*`,
563
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.
@@ -571,7 +655,9 @@ slots:
571
655
  liteManagedBy: codegen-lite
572
656
  rules: [HFS_SCRIPT_ONE_OFF, BE_SPEC_PLACEMENT, HFS_REPO_LOCAL_CHECK] # one-off codemods and fix-* scripts are agent output, not tooling
573
657
  - id: app.scripts.delivery
658
+ since: 3.1.0
574
659
  profiles: [app]
660
+ 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))."
575
661
  editions: [lite]
576
662
  # Product-owned environment, secret and build tooling, kept apart from lite's managed codegen script.
577
663
  # Full already owns these files through app.scripts; its generic *.mjs pattern must not compete with build-*.mjs.
@@ -581,7 +667,9 @@ slots:
581
667
  tier: none
582
668
  tests: none
583
669
  rules: [HFS_PLAINTEXT_SECRET, HFS_SCRIPT_ONE_OFF, BE_SPEC_PLACEMENT, HFS_REPO_LOCAL_CHECK]
670
+ exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
584
671
  - id: app.sides
672
+ since: 3.1.0
585
673
  profiles: [app]
586
674
  # 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;
587
675
  # this slot answers only for the side folders themselves.
@@ -591,7 +679,12 @@ slots:
591
679
  tier: none
592
680
  tests: none
593
681
  - id: app.starciwork
682
+ since: 3.1.0
594
683
  profiles: [app]
684
+ exemplifiedBy:
685
+ - {path: packages/hfs/templates/app/skeleton/.starciwork/, edition: full}
686
+ - {path: packages/hfs/templates/app/skeleton-lite/.starciwork/, edition: lite}
687
+ editionWhy: "ruling:R212/case-4: lite optionally keeps .gitignore, workspace.yaml and brand records; full keeps the Work catalog and product summaries."
595
688
  path: ".starciwork/"
596
689
  presence: required
597
690
  tracked: tracked
@@ -601,9 +694,10 @@ slots:
601
694
  allows: [workspace.yaml, index.yaml, "brand/**", shell/index.yaml, "features/<feature>/index.yaml",
602
695
  "features/<feature>/<family>/<name>/index.yaml", "features/<feature>/br/<rule>/ac/<name>/index.yaml",
603
696
  "features/<feature>/ui/<name>/assets/<approved-file>", "features/<feature>/uat/<name>/{index,accounts,fixtures}.yaml",
604
- "features/<feature>/uat/<name>/{seed,cleanup}.sql", "_resources/{identities,environments,fixtures}/<slug>/resource.yaml"]
605
- forbids: [evidence/, logs/, reports/, captures/, runs/, "kernel-*/", "*.sqlite*", _derived/]
606
- # Owner amendment 2026-10-14: lite keeps workspace.yaml for project identity and repo roles, plus brand drawing records,
697
+ "features/<feature>/uat/<name>/{seed,cleanup}.sql", "_resources/{identities,environments,fixtures}/<slug>/resource.yaml",
698
+ "_derived/{index.yaml,frontier.md,critique.yaml,critique.md}"]
699
+ forbids: [evidence/, logs/, reports/, captures/, runs/, "kernel-*/", "*.sqlite*"]
700
+ # ruling:R212/case-4: lite keeps workspace.yaml for project identity and repo roles, plus brand drawing records,
607
701
  # references and assets. No brand means no Work tree is needed; workspace.yaml is legal but optional.
608
702
  litePresence: optional
609
703
  lite:
@@ -611,7 +705,12 @@ slots:
611
705
  allows: [workspace.yaml, "brand/**"]
612
706
  rules: [HFS_AGENT_DATA_TRACKED, HFS_IDENTITY_CUSTODY]
613
707
  - id: app.starcistacks
708
+ since: 3.1.0
614
709
  profiles: [app]
710
+ editionWhy: "owner:lite-production-swarm: lite requires declared docker-swarm production with Supabase backing; full also admits Compose development, Kubernetes and Terraform."
711
+ exemplifiedBy:
712
+ - {path: packages/hfs/templates/app/skeleton/.starcistacks/, edition: full}
713
+ - {path: packages/hfs/templates/app/skeleton-lite/.starcistacks/, edition: lite}
615
714
  # The stack declarations and the sealed custody of the app, at the app root beside be/, fe/ and .starciwork (never under a side).
616
715
  path: ".starcistacks/"
617
716
  presence: required
@@ -619,18 +718,19 @@ slots:
619
718
  tier: none
620
719
  tests: none
621
720
  requires: [application-stacks.yaml]
622
- allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,k8s,terraform,swarm}/**", "<env>/infra/metadata.json",
721
+ allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,swarm,k8s,terraform}/**", "<env>/infra/metadata.json",
623
722
  "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
624
723
  forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
625
- # Lite keeps full's code structure with backing infrastructure replaced by Supabase (owner ruling 2026-10-13). The api and
626
- # sidecars run on docker-swarm; its value-free stack belongs in <env>/infra/swarm/. The checker also refuses components whose
627
- # images duplicate Supabase services: postgres, redis, kafka, keycloak, minio, rabbitmq, mysql and mongo.
724
+ # Owner ruling 2026-10-14: lite declares docker-swarm production, app runtime config, sealed custody and seed data.
725
+ # Supabase supplies the database and broker; production has no Compose or host cron.
628
726
  lite:
629
727
  requires: [application-stacks.yaml]
630
- allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/swarm/**", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep"]
631
- forbids: ["<env>/infra/{compose,k8s,terraform}/**", "<env>/runtime/**", "<env>/seeds/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
728
+ allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/swarm/**", "<env>/infra/metadata.json",
729
+ "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
730
+ forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
632
731
  rules: [HFS_STACKS_SHAPE, HFS_STACKS_IN_SIDE, HFS_PLAINTEXT_SECRET, HFS_IDENTITY_CUSTODY]
633
732
  - id: app.sops
733
+ since: 3.1.0
634
734
  profiles: [app]
635
735
  # The sops custody rule of .starcistacks/<env>/secrets/*.enc, at the app root beside the tree it seals.
636
736
  path: .sops.yaml
@@ -639,6 +739,7 @@ slots:
639
739
  tier: none
640
740
  tests: none
641
741
  - id: app.build-output
742
+ since: 3.1.0
642
743
  profiles: [app]
643
744
  path: "{**/node_modules/,coverage/,reports/,test-results/,**/*.tsbuildinfo}"
644
745
  presence: optional
@@ -646,6 +747,7 @@ slots:
646
747
  tier: none
647
748
  tests: none
648
749
  - id: app.tool-cache
750
+ since: 3.1.0
649
751
  profiles: [app]
650
752
  path: "{.eslintcache,.turbo/,.scannerwork/,.sonar/,.jest-cache/,.cache/,.tools/}"
651
753
  presence: forbidden
@@ -654,6 +756,7 @@ slots:
654
756
  tests: none
655
757
  goesTo: "${STARCI_CACHE_HOME:-%LOCALAPPDATA%/StarCi/cache}/<repo>/<tool>/ (the canon presets set eslint --cache-location, jest cacheDirectory, turbo cacheDir, sonar.working.directory)"
656
758
  - id: app.agent-output
759
+ since: 3.1.0
657
760
  profiles: [app]
658
761
  path: "{report*.json,*.tmp.json,%*%,nul,.artifacts/,.tmp-*,*-lint.json,lf.json,lt.json,*.log,design-plans/,.gitmounts/,.dat,vi-flat.txt}"
659
762
  presence: forbidden
@@ -663,6 +766,7 @@ slots:
663
766
  goesTo: "agent scratchpad or the StarCi blob store (%LOCALAPPDATA%/StarCi/blobs), cited by {name, sha256}"
664
767
  rules: [HFS_UNTRACKED_ROOT_ENTRY, HFS_AGENT_DATA_TRACKED]
665
768
  - id: app.worktrees
769
+ since: 3.1.0
666
770
  profiles: [app]
667
771
  path: "{.worktrees/,worktrees/,.starciwork/worktrees/}"
668
772
  presence: forbidden
@@ -671,6 +775,7 @@ slots:
671
775
  tests: none
672
776
  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"
673
777
  - id: app.plaintext-env
778
+ since: 3.1.0
674
779
  profiles: [app]
675
780
  path: "{.env,.env.*,secrets.env,.secrets/,**/*.pem,**/*.key}"
676
781
  presence: forbidden
@@ -683,6 +788,9 @@ slots:
683
788
  # ----- app root: the Supabase project of an app whose connection declares provider: supabase (every lite app) -------
684
789
  - id: app.supabase
685
790
  profiles: [app]
791
+ exemplifiedBy:
792
+ - packages/hfs/templates/app/skeleton-lite/supabase/
793
+ 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."
686
794
  # The one Supabase project directory, at the app root beside be/, fe/ and .starcistacks (never under a side).
687
795
  path: "supabase/"
688
796
  presence: opt-in # enabled by a connection with provider: supabase; required under edition lite
@@ -692,8 +800,12 @@ slots:
692
800
  tier: none
693
801
  tests: none
694
802
  since: 2.1.0
803
+ exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
695
804
  - id: app.supabase.config
696
805
  profiles: [app]
806
+ exemplifiedBy:
807
+ - packages/hfs/templates/app/skeleton-lite/supabase/config.toml
808
+ 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."
697
809
  # The project configuration: auth keys are env(NAME) references, never literals (L08 judges its policy keys).
698
810
  path: "supabase/config.toml"
699
811
  presence: opt-in
@@ -703,8 +815,12 @@ slots:
703
815
  tier: none
704
816
  tests: none
705
817
  since: 2.1.0
818
+ exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
706
819
  - id: app.supabase.migrations
707
820
  profiles: [app]
821
+ exemplifiedBy:
822
+ - packages/hfs/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql
823
+ 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."
708
824
  # The schema authority of an app on Supabase: forward-only SQL migrations named <timestamp14>_<kebab-name>.sql,
709
825
  # one file per change, immutable once on origin/main (L02-L08 judge them).
710
826
  path: "supabase/migrations/<stamp>_<name>.sql"
@@ -716,6 +832,7 @@ slots:
716
832
  tier: none
717
833
  tests: none
718
834
  since: 2.1.0
835
+ exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
719
836
  - id: app.supabase.migrations.archive
720
837
  profiles: [app]
721
838
  # The migrations an existing app retired at its cutover (owner ruling 2026-10-13, migration baseline policy): the live schema is
@@ -739,6 +856,8 @@ slots:
739
856
  since: 2.1.0
740
857
  - id: app.supabase.types
741
858
  profiles: [app]
859
+ 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."
860
+ 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."
742
861
  # The generated Database types (database.types.ts) contract:emit writes from the migrations; both sides read it
743
862
  # (sides.<side>.reads supabase/types/); drift against regeneration is L09.
744
863
  path: "supabase/types/database.types.ts"
@@ -749,6 +868,7 @@ slots:
749
868
  tier: none
750
869
  tests: none
751
870
  since: 2.1.0
871
+ exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
752
872
  - id: app.supabase.templates
753
873
  profiles: [app]
754
874
  path: "supabase/templates/*.html"
@@ -777,6 +897,7 @@ slots:
777
897
 
778
898
  # ----- side root (both sides): what a standalone repository root holds, less the app-root files ----------
779
899
  - id: repo.side-root-forbidden
900
+ since: 3.1.0
780
901
  profiles: [be, fe]
781
902
  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/}"
782
903
  presence: forbidden
@@ -785,6 +906,7 @@ slots:
785
906
  tests: none
786
907
  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"
787
908
  - id: be.tool-config
909
+ since: 3.1.0
788
910
  profiles: [be]
789
911
  # Managed files (starci app sync renders them, starci app check compares them): the whole tool configuration of a back end. Each is
790
912
  # a reference to a canon package, never a configuration: tsconfig.json extends @starci/tsconfig/be.json and adds the
@@ -800,11 +922,12 @@ slots:
800
922
  tracked: tracked
801
923
  tier: none
802
924
  tests: none
803
- # Lite renders the same files (owner ruling 2026-10-12): the unit run and its coverage are full's.
925
+ # 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.
804
926
  coverage: none
805
927
  managedBy: tool-config
806
928
  rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL, HFS_TS_STRICT]
807
929
  - id: be.nest-cli
930
+ since: 3.1.0
808
931
  profiles: [be]
809
932
  # The Nest monorepo of the back end: `monorepo: true` and one project per declared be app (apps/<app>, even for one service).
810
933
  path: nest-cli.json
@@ -815,6 +938,7 @@ slots:
815
938
  coverage: none
816
939
  rules: [HFS_MONO_NEST_PROJECTS]
817
940
  - id: be.tool-config-local
941
+ since: 3.1.0
818
942
  profiles: [be]
819
943
  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}"
820
944
  presence: forbidden
@@ -824,6 +948,7 @@ slots:
824
948
  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"
825
949
  rules: [HFS_TOOL_CONFIG_LOCAL]
826
950
  - id: fe.tool-config
951
+ since: 3.1.0
827
952
  profiles: [fe]
828
953
  # Managed files (starci app sync renders them, starci app check compares them): the tool configuration of a front end. Each is a
829
954
  # reference to a canon package, never a configuration: tsconfig.json extends @starci/tsconfig/next.json and only
@@ -839,6 +964,7 @@ slots:
839
964
  managedBy: tool-config
840
965
  rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL, HFS_RULE_OFF_WITHOUT_REPLACEMENT, HFS_TS_STRICT]
841
966
  - id: fe.tool-config-local
967
+ since: 3.1.0
842
968
  profiles: [fe]
843
969
  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}"
844
970
  presence: forbidden
@@ -848,6 +974,7 @@ slots:
848
974
  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"
849
975
  rules: [HFS_TOOL_CONFIG_LOCAL]
850
976
  - id: repo.docs
977
+ since: 3.1.0
851
978
  profiles: [be, fe]
852
979
  path: "docs/{adr,runbooks,guides}/**/*.md"
853
980
  presence: opt-in
@@ -857,6 +984,7 @@ slots:
857
984
  coverage: none
858
985
  rules: [HFS_ROOT_MARKDOWN_FORBIDDEN]
859
986
  - id: repo.docs-media
987
+ since: 3.1.0
860
988
  profiles: [be, fe]
861
989
  path: "docs/**/*.{png,svg,webp}"
862
990
  presence: opt-in
@@ -865,7 +993,21 @@ slots:
865
993
  tests: none
866
994
  coverage: none
867
995
  budget: {bytes: 512000}
996
+ - id: repo.i18n-dynamic-keys
997
+ since: 3.3.0
998
+ profiles: [be, fe]
999
+ # The side's authored ledger of catalog keys the R280 detector cannot see (fe/i18n-dynamic-keys.yaml, be/i18n-dynamic-keys.yaml:
1000
+ # 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.
1001
+ path: i18n-dynamic-keys.yaml
1002
+ presence: opt-in
1003
+ tracked: tracked
1004
+ tier: none
1005
+ tests: none
1006
+ coverage: none
1007
+ rules: [HFS_UNUSED_SYMBOL]
1008
+ exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
868
1009
  - id: repo.packages
1010
+ since: 3.1.0
869
1011
  profiles: [be, fe]
870
1012
  path: "packages/<pkg>/"
871
1013
  presence: opt-in
@@ -877,7 +1019,14 @@ slots:
877
1019
  coverage: none
878
1020
  rules: [FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT]
879
1021
  - id: repo.app-image
1022
+ since: 3.1.0
880
1023
  profiles: [be, fe]
1024
+ exemplifiedBy:
1025
+ - packages/hfs/templates/be/image/api/Dockerfile
1026
+ - packages/hfs/templates/be/image/cli/Dockerfile
1027
+ - {path: packages/hfs/templates/be/image/worker/Dockerfile, edition: full}
1028
+ - packages/hfs/templates/fe/image/next/Dockerfile
1029
+ 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."
881
1030
  # One image per app: every declared app has its own Dockerfile (apps/<app>/Dockerfile), built from the APP ROOT (the folder with
882
1031
  # the one package.json). It is app-owned (starci app scaffold writes it once from templates/<side>/image) and judged by the docker rules.
883
1032
  # A Dockerfile anywhere else is owned by no slot.
@@ -892,6 +1041,7 @@ slots:
892
1041
 
893
1042
  # ----- never tracked: build output the tools need in place ---------------------------------------------
894
1043
  - id: repo.build-output
1044
+ since: 3.1.0
895
1045
  profiles: [be, fe]
896
1046
  path: "{**/node_modules/,dist/,apps/*/dist/,packages/*/dist/,.next/,apps/*/.next/,coverage/,reports/,test-results/,**/next-env.d.ts,**/*.tsbuildinfo}"
897
1047
  presence: optional
@@ -899,6 +1049,7 @@ slots:
899
1049
  tier: none
900
1050
  tests: none
901
1051
  - id: repo.generated
1052
+ since: 3.1.0
902
1053
  profiles: [be, fe]
903
1054
  path: "{**/__generated__/,apps/*/src/schema.gql}"
904
1055
  presence: optional
@@ -909,6 +1060,7 @@ slots:
909
1060
 
910
1061
  # ----- never in the working tree: caches, scratch, agent output ----------------------------------------
911
1062
  - id: repo.tool-cache
1063
+ since: 3.1.0
912
1064
  profiles: [be, fe]
913
1065
  path: "{.eslintcache,.turbo/,.scannerwork/,.sonar/,.jest-cache/,.cache/,.tools/}"
914
1066
  presence: forbidden
@@ -917,6 +1069,7 @@ slots:
917
1069
  tests: none
918
1070
  goesTo: "${STARCI_CACHE_HOME:-%LOCALAPPDATA%/StarCi/cache}/<repo>/<tool>/ (the canon presets set eslint --cache-location, jest cacheDirectory, turbo cacheDir, sonar.working.directory)"
919
1071
  - id: repo.agent-output
1072
+ since: 3.1.0
920
1073
  profiles: [be, fe]
921
1074
  path: "{report*.json,*.tmp.json,%*%,nul,.artifacts/,.tmp-*,*-lint.json,lf.json,lt.json,*.log,design-plans/,.gitmounts/,.dat,vi-flat.txt}"
922
1075
  presence: forbidden
@@ -926,6 +1079,7 @@ slots:
926
1079
  goesTo: "agent scratchpad or the StarCi blob store (%LOCALAPPDATA%/StarCi/blobs), cited by {name, sha256}"
927
1080
  rules: [HFS_UNTRACKED_ROOT_ENTRY, HFS_AGENT_DATA_TRACKED]
928
1081
  - id: repo.worktrees
1082
+ since: 3.1.0
929
1083
  profiles: [be, fe]
930
1084
  path: "{.worktrees/,worktrees/,.starciwork/worktrees/}"
931
1085
  presence: forbidden
@@ -934,6 +1088,7 @@ slots:
934
1088
  tests: none
935
1089
  goesTo: "<lanes root>/<project>/<lane>/ (outside every repository)"
936
1090
  - id: repo.plaintext-env
1091
+ since: 3.1.0
937
1092
  profiles: [be, fe]
938
1093
  path: "{.env,.env.*,.secrets/,**/*.pem,**/*.key}"
939
1094
  presence: forbidden
@@ -943,6 +1098,7 @@ slots:
943
1098
  goesTo: ".starcistacks/<env>/secrets/<slug>.enc (at the app root); decrypted values only in memory or under %LOCALAPPDATA%/StarCi/secrets/<project>/<env>/ via *_FILE"
944
1099
  rules: [HFS_PLAINTEXT_SECRET]
945
1100
  - id: be.root-e2e
1101
+ since: 3.1.0
946
1102
  profiles: [be]
947
1103
  path: "e2e/"
948
1104
  presence: forbidden
@@ -953,6 +1109,7 @@ slots:
953
1109
 
954
1110
  # ----- contract snapshots (the committed machine-emitted files, because they are a cross-repo interface) --
955
1111
  - id: be.contract.graphql
1112
+ since: 3.1.0
956
1113
  profiles: [be]
957
1114
  path: "contracts/<app>/schema.graphql"
958
1115
  presence: opt-in
@@ -963,6 +1120,7 @@ slots:
963
1120
  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)
964
1121
  rules: [HFS_CONTRACT_SNAPSHOT_DRIFT]
965
1122
  - id: be.contract.openapi
1123
+ since: 3.1.0
966
1124
  profiles: [be]
967
1125
  path: "contracts/<app>/openapi.json"
968
1126
  presence: opt-in
@@ -973,6 +1131,7 @@ slots:
973
1131
  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
974
1132
  rules: [HFS_CONTRACT_SNAPSHOT_DRIFT]
975
1133
  - id: be.contract.events
1134
+ since: 3.1.0
976
1135
  profiles: [be]
977
1136
  path: "contracts/<app>/events.json"
978
1137
  presence: opt-in
@@ -983,6 +1142,7 @@ slots:
983
1142
  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)
984
1143
  rules: [HFS_EVENT_CONTRACT]
985
1144
  - id: be.contract.events.pin
1145
+ since: 3.1.0
986
1146
  profiles: [be]
987
1147
  path: "contracts/<app>/events.pin.json"
988
1148
  presence: opt-in
@@ -1012,6 +1172,7 @@ slots:
1012
1172
  # area's global ports when it has any); composition only, no business logic or process.env/EnvSource read.
1013
1173
  # app.module.ts the composition root: imports the groups and spreads them, binds the app-wide guards and filter.
1014
1174
  - id: be.app.api
1175
+ since: 3.1.0
1015
1176
  profiles: [be]
1016
1177
  path: "apps/<app>/src/"
1017
1178
  appKind: api
@@ -1020,13 +1181,14 @@ slots:
1020
1181
  tier: app
1021
1182
  owner: true
1022
1183
  minInstances: 1
1023
- requires: [main.ts, app.module.ts]
1184
+ requires: [main.ts, app.module.ts, "<app>.env.ts"]
1024
1185
  allows: [main.ts, app.module.ts, "<app>.env.ts", "<app>.options.ts", "compose/<area>.ts", operations.ts]
1025
- tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
1186
+ tests: none # full app boot is proven by useTestWorld({ apps }); lite by starci app check --wiring (R272)
1026
1187
  coverage: none
1027
1188
  budget: {app.module.ts: 250, main.ts: 80, "<app>.env.ts": 150, "<app>.options.ts": 200, "compose/<area>.ts": 120}
1028
- 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]
1189
+ 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]
1029
1190
  - id: be.app.worker
1191
+ since: 3.1.0
1030
1192
  profiles: [be]
1031
1193
  path: "apps/<app>/src/"
1032
1194
  appKind: worker # composes transport/message modules and job processors only (an api app may compose them too: a worker app is for separate scaling)
@@ -1034,14 +1196,16 @@ slots:
1034
1196
  tracked: tracked
1035
1197
  tier: app
1036
1198
  owner: true
1037
- requires: [main.ts, app.module.ts]
1199
+ requires: [main.ts, app.module.ts, "<app>.env.ts"]
1038
1200
  allows: [main.ts, app.module.ts, "<app>.env.ts", "<app>.options.ts", "compose/<area>.ts"]
1039
- tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
1201
+ tests: none # full app boot is proven by useTestWorld({ apps }); lite by starci app check --wiring (R272)
1040
1202
  coverage: none
1041
1203
  budget: {app.module.ts: 250, main.ts: 80, "<app>.env.ts": 150, "<app>.options.ts": 200, "compose/<area>.ts": 120}
1042
- rules: [BE_APP_COMPOSITION_ONLY, BE_APP_OPTIONS_COMPOSITION, BE_APP_COMPOSITION_BUDGET, BE_BACKGROUND_UNOWNED, BE_CONTEXT_OWNER]
1204
+ rules: [BE_APP_COMPOSITION_ONLY, BE_APP_OPTIONS_COMPOSITION, BE_APP_COMPOSITION_BUDGET, BE_BACKGROUND_UNOWNED, BE_CONTEXT_OWNER, BE_APP_ENV_REQUIRED]
1043
1205
  - id: be.app.cli
1206
+ since: 3.1.0
1044
1207
  profiles: [be]
1208
+ 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."
1045
1209
  # The ONE app of every one-off action (migrate, seed, sync, backup, any operator command): be/apps/cli, on nest-commander.
1046
1210
  # main.ts boots with CommandFactory.run (BE_CLI_BOOTSTRAP); app.module.ts composes the platform and the cli feature root
1047
1211
  # (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,
@@ -1055,18 +1219,19 @@ slots:
1055
1219
  tracked: tracked
1056
1220
  tier: app
1057
1221
  owner: true
1058
- requires: [main.ts, app.module.ts]
1222
+ requires: [main.ts, app.module.ts, "<app>.env.ts"]
1059
1223
  allows: [main.ts, app.module.ts, "<app>.env.ts", "<app>.options.ts", "compose/<area>.ts"]
1060
- tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
1224
+ tests: none # full app boot is proven by useTestWorld({ apps }); lite by starci app check --wiring (R272)
1061
1225
  coverage: none
1062
1226
  budget: {app.module.ts: 250, main.ts: 80, "<app>.env.ts": 150, "<app>.options.ts": 200, "compose/<area>.ts": 120}
1063
- rules: [BE_CLI_REQUIRED, BE_CLI_BOOTSTRAP, BE_CLI_OWNER, BE_SCHEMA_AUTHORITY, BE_APP_OPTIONS_COMPOSITION, BE_APP_COMPOSITION_BUDGET]
1227
+ 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]
1064
1228
 
1065
1229
  # ----- BE: features ------------------------------------------------------------------------------------
1066
1230
  # src/features/ is grouped by TRIGGER KIND (owner decision 2026-10-02): features/api/<feature>/ (a request: application/ and
1067
1231
  # transport/<protocol>/, composed by the api apps), features/cli/ (a one-off action: the cli feature root, compiled only into
1068
1232
  # apps/cli). Every kind imports only downward into modules/*; no kind imports another.
1069
1233
  - id: be.feature
1234
+ since: 3.1.0
1070
1235
  profiles: [be]
1071
1236
  path: "src/features/api/<feature>/"
1072
1237
  presence: required
@@ -1076,12 +1241,13 @@ slots:
1076
1241
  owner: true
1077
1242
  minInstances: 1
1078
1243
  requires: [index.ts, "<feature>.module.ts", application/]
1079
- allows: [index.ts, "<feature>.module.ts", application/, transport/, messages/] # nothing else at feature root
1244
+ allows: [index.ts, "<feature>.module.ts", "<feature>.module-definition.ts", "<feature>.options.ts", application/, transport/, messages/] # nothing else at feature root
1080
1245
  tests: none
1081
1246
  coverage: none
1082
1247
  budget: {files: 250, indexExports: 60} # a feature above 250 source files must be split by product capability
1083
1248
  rules: [BE_FEATURE_SHAPE, BE_FEATURE_IMPORTS_FEATURE, BE_PUBLIC_SURFACE, BE_FEATURE_NOT_COMPOSED]
1084
1249
  - id: be.feature.application
1250
+ since: 3.1.0
1085
1251
  profiles: [be]
1086
1252
  path: "src/features/{api/,jobs/,reactors/,saga/}<feature>/application/" # inside a trigger kind (jobs, reactors, saga) the feature folder is one level down
1087
1253
  presence: required
@@ -1093,6 +1259,7 @@ slots:
1093
1259
  coverage: none
1094
1260
  rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_APPLICATION_TRANSPORT_FRAMEWORK, BE_ENTITY_IN_CONTRACT, BE_CQRS_SHAPE]
1095
1261
  - id: be.feature.saga
1262
+ since: 3.1.0
1096
1263
  profiles: [be]
1097
1264
  path: "src/features/saga/<saga>/"
1098
1265
  pattern: saga # enabled by `patterns: ["saga"]` in hfs.json sides.be (R169 to R173); an undeclared saga folder is HFS_SLOT_NOT_ENABLED
@@ -1102,11 +1269,12 @@ slots:
1102
1269
  tier: feature
1103
1270
  owner: true
1104
1271
  requires: [index.ts, "<saga>.module.ts", "<saga>.saga.service.ts", "<saga>.saga-state.ts", steps/, compensations/, transport/]
1105
- 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
1272
+ 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
1106
1273
  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
1107
1274
  coverage: none
1108
1275
  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]
1109
1276
  - id: be.feature.saga.steps
1277
+ since: 3.1.0
1110
1278
  profiles: [be]
1111
1279
  path: "src/features/saga/<saga>/steps/"
1112
1280
  pattern: saga
@@ -1118,6 +1286,7 @@ slots:
1118
1286
  coverage: none
1119
1287
  rules: [BE_SAGA_STEP_COMPENSATION, BE_SAGA_EVENT_CONTRACT]
1120
1288
  - id: be.feature.saga.compensations
1289
+ since: 3.1.0
1121
1290
  profiles: [be]
1122
1291
  path: "src/features/saga/<saga>/compensations/"
1123
1292
  pattern: saga
@@ -1141,30 +1310,33 @@ slots:
1141
1310
  rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_APPLICATION_TRANSPORT_FRAMEWORK, BE_FEATURE_IMPORTS_FEATURE]
1142
1311
  since: 1.0.0
1143
1312
  - id: be.transport.http
1313
+ since: 3.1.0
1144
1314
  profiles: [be]
1145
1315
  path: "src/features/api/<feature>/transport/http/"
1146
1316
  presence: optional
1147
1317
  tracked: tracked
1148
1318
  tier: feature
1149
1319
  requires: ["<feature>-http.module.ts"]
1150
- allows: ["<action>.controller.ts", "dto/<action>.{request,response}.ts", "<action>.mapper.ts"]
1320
+ allows: ["<action>.controller.ts", "dto/<action>.{request,response}.ts", "<action>.mapper.ts", "<feature>-http.module-definition.ts", "<feature>-http.options.ts"]
1151
1321
  tests: none
1152
1322
  coverage: none
1153
1323
  composedBy: [api]
1154
1324
  rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED, BE_TRANSPORT_SHAPE]
1155
1325
  - id: be.transport.graphql
1326
+ since: 3.1.0
1156
1327
  profiles: [be]
1157
1328
  path: "src/features/api/<feature>/transport/graphql/"
1158
1329
  presence: optional
1159
1330
  tracked: tracked
1160
1331
  tier: feature
1161
1332
  requires: ["<feature>-graphql.module.ts"]
1162
- allows: ["<action>.resolver.ts", "dto/<action>.{input,type,args}.ts", "<action>.mapper.ts"]
1333
+ allows: ["<action>.resolver.ts", "dto/<action>.{input,type,args}.ts", "<action>.mapper.ts", "<feature>-graphql.module-definition.ts", "<feature>-graphql.options.ts"]
1163
1334
  tests: none
1164
1335
  coverage: none
1165
1336
  composedBy: [api]
1166
1337
  rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED, BE_TRANSPORT_SHAPE]
1167
1338
  - id: be.transport.message
1339
+ since: 3.1.0
1168
1340
  profiles: [be]
1169
1341
  path: "src/features/{api/,reactors/,saga/}<feature>/transport/message/"
1170
1342
  presence: opt-in # queue consumer: composed by the service that owns it (an api app) or by a worker app scaled apart
@@ -1172,7 +1344,7 @@ slots:
1172
1344
  tracked: tracked
1173
1345
  tier: feature
1174
1346
  requires: ["<feature>-message.module.ts"]
1175
- allows: ["<event>.consumer.ts"] # BE-CONVENTION 1.6: transport/message holds only consumers; a .message.ts is outside the closed suffix list
1347
+ 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
1176
1348
  tests: none
1177
1349
  coverage: none
1178
1350
  composedBy: [api, worker]
@@ -1204,7 +1376,7 @@ slots:
1204
1376
  tracked: tracked
1205
1377
  tier: feature
1206
1378
  requires: ["<provider>-http.module.ts", "<provider>.webhook.ts", "<provider>.webhook.spec.ts"]
1207
- allows: ["<provider>-http.module.ts", "<provider>.webhook.ts", "<provider>.webhook.spec.ts", dto/]
1379
+ allows: ["<provider>-http.module.ts", "<provider>-http.module-definition.ts", "<provider>-http.options.ts", "<provider>.webhook.ts", "<provider>.webhook.spec.ts", dto/]
1208
1380
  tests: unit-beside # the door's own spec (<provider>.webhook.spec.ts) beside the door: the one non-service unit spec of this kind
1209
1381
  coverage: none
1210
1382
  composedBy: [api]
@@ -1225,6 +1397,7 @@ slots:
1225
1397
  since: 1.0.0
1226
1398
  - id: be.feature.realtime
1227
1399
  profiles: [be]
1400
+ 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."
1228
1401
  path: "src/features/realtime/<channel>/"
1229
1402
  presence: opt-in # a push channel: reads and pushes only (a socket gateway or a GraphQL subscription), no domain write
1230
1403
  pattern: realtime # declaring the pattern (hfs.json sides.be.patterns) enables the kind (knowledge/patterns/be/realtime.yaml)
@@ -1243,17 +1416,19 @@ slots:
1243
1416
  coverage: none
1244
1417
  rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED]
1245
1418
  since: 1.0.0
1419
+ exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1246
1420
  - id: be.feature.realtime.graphql
1247
1421
  profiles: [be]
1422
+ 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."
1248
1423
  path: "src/features/realtime/<channel>/transport/graphql/"
1249
1424
  presence: opt-in # a GraphQL subscription over graphql-ws
1250
- litePresence: forbidden # a graphql-ws server is self-hosted push infrastructure: lite pushes through Supabase Realtime
1425
+ 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
1251
1426
  pattern: realtime
1252
1427
  tracked: tracked
1253
1428
  tier: feature
1254
1429
  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"
1255
1430
  requires: ["<channel>-graphql.module.ts", "<channel>.subscription.ts", "<channel>.subscription.spec.ts"]
1256
- allows: ["<channel>-graphql.module.ts", "<channel>.subscription.ts", "<channel>.subscription.spec.ts", dto/]
1431
+ allows: ["<channel>-graphql.module.ts", "<channel>-graphql.module-definition.ts", "<channel>-graphql.options.ts", "<channel>.subscription.ts", "<channel>.subscription.spec.ts", dto/]
1257
1432
  tests: unit-beside # the door's own spec beside the door (a subscription has no service of its own)
1258
1433
  coverage: none
1259
1434
  composedBy: [api]
@@ -1261,6 +1436,7 @@ slots:
1261
1436
  since: 1.0.0
1262
1437
  - id: be.feature.realtime.dto
1263
1438
  profiles: [be]
1439
+ 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."
1264
1440
  path: "src/features/realtime/<channel>/transport/graphql/dto/"
1265
1441
  presence: opt-in
1266
1442
  litePresence: forbidden # the dto of the graphql door, which lite does not serve
@@ -1282,20 +1458,21 @@ slots:
1282
1458
  tracked: tracked
1283
1459
  tier: feature
1284
1460
  requires: ["<channel>-websocket.module.ts", "<channel>.gateway.ts", "<channel>.gateway.spec.ts"]
1285
- allows: ["<channel>-websocket.module.ts", "<channel>.gateway.ts", "<channel>.gateway.spec.ts"]
1461
+ allows: ["<channel>-websocket.module.ts", "<channel>-websocket.module-definition.ts", "<channel>-websocket.options.ts", "<channel>.gateway.ts", "<channel>.gateway.spec.ts"]
1286
1462
  tests: unit-beside # the door's own spec beside the door (a gateway has no service of its own)
1287
1463
  coverage: none
1288
1464
  composedBy: [api]
1289
1465
  rules: [BE_REALTIME_SHAPE, BE_REALTIME_WRITES, BE_REALTIME_TOPIC_SCOPE, BE_DEFAULT_DENY]
1290
1466
  since: 1.0.0
1291
1467
  - id: be.cli
1468
+ since: 3.1.0
1292
1469
  profiles: [be]
1293
1470
  # The cli feature root: every one-off action of the back end, compiled only into apps/cli. A group is a nest-commander
1294
1471
  # @Command (<group>/<group>.cli.ts) whose sub-commands are @SubCommand classes extending CommandRunner
1295
1472
  # (<group>/subs/<name>.cli.ts), each with its unit spec beside it (<name>.cli.spec.ts): a command is an action runner, not a
1296
1473
  # thin transport (BE_CLI_COMMAND_SHAPE). `.cli.ts` because `.command.ts` is the CQRS message (HFS delta 3). Every module of
1297
- # the root is a static @Module (a feature holds no module definition): cli.module.ts imports each group module, a group
1298
- # module provides its commands, and a command takes its inputs from a platform capability (the connections and their opener
1474
+ # the root has a module definition beside it (ruling:module-definition-everywhere) and no register override: cli.module.ts imports each
1475
+ # 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
1299
1476
  # from platform/database) or from a command-line @Option.
1300
1477
  path: "src/features/cli/"
1301
1478
  presence: optional
@@ -1306,7 +1483,7 @@ slots:
1306
1483
  requires: [index.ts, cli.module.ts]
1307
1484
  # A composed root: an app of kind cli lists cli.module.ts, never a group module (BE_MODULE_SHAPE).
1308
1485
  composedBy: [cli]
1309
- 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"]
1486
+ 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"]
1310
1487
  tests: unit-beside
1311
1488
  coverage: none
1312
1489
  rules: [BE_CLI_COMMAND_SHAPE, BE_CLI_OWNER, BE_CLI_REQUIRED, BE_MODULE_SHAPE, BE_SCHEMA_AUTHORITY]
@@ -1323,8 +1500,11 @@ slots:
1323
1500
  tests: none
1324
1501
  coverage: none
1325
1502
  since: 3.0.0
1503
+ rules: [HFS_ASSET_OVERSIZE, HFS_ASSET_KIND]
1504
+
1326
1505
  # ----- BE: modules (three tiers, closed set) -----------------------------------------------------------
1327
1506
  - id: be.domain
1507
+ since: 3.1.0
1328
1508
  profiles: [be]
1329
1509
  path: "src/modules/domain/<capability>/"
1330
1510
  presence: optional
@@ -1334,13 +1514,14 @@ slots:
1334
1514
  requires: [index.ts]
1335
1515
  # <name>.templates.ts holds the capability's static templates, copy and presets as TS constants (owner ruling 2026-10-13);
1336
1516
  # user-editable ones are seed rows, large or user media live in object storage.
1337
- allows: ["<capability>.module.ts", "<capability>.config.ts", "<capability>.options.ts", errors/, messages/, persistence/, policies/, "*.service.ts", "*.service.spec.ts", "*.contracts.ts", "*.templates.ts"]
1517
+ 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"]
1338
1518
  forbids: [testing/, exceptions/, utils/, helpers/, shared/]
1339
1519
  tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts, nothing else
1340
1520
  coverage: required
1341
1521
  budget: {indexExports: 60}
1342
1522
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_PUBLIC_SURFACE, BE_ERROR_HOME, BE_CONFIG_OWNER, BE_CONTEXT_COUPLING, BE_CONTEXT_TRANSACTION]
1343
1523
  - id: be.errors
1524
+ since: 3.1.0
1344
1525
  profiles: [be]
1345
1526
  path: "src/modules/{domain,integrations}/<capability>/errors/"
1346
1527
  presence: optional
@@ -1373,7 +1554,9 @@ slots:
1373
1554
  rules: [BE_USER_COPY_LITERAL]
1374
1555
  since: 1.0.0
1375
1556
  - id: be.persistence
1557
+ since: 3.1.0
1376
1558
  profiles: [be]
1559
+ 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."
1377
1560
  path: "src/modules/{domain,platform,projections}/<capability>/persistence/"
1378
1561
  # 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
1379
1562
  # registered on several declared connections. Any other capability registers its arrays on exactly one (BE_SCHEMA_OWNER).
@@ -1394,6 +1577,7 @@ slots:
1394
1577
  coverage: none
1395
1578
  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]
1396
1579
  - id: be.platform
1580
+ since: 3.1.0
1397
1581
  profiles: [be]
1398
1582
  path: "src/modules/platform/<capability>/"
1399
1583
  presence: required
@@ -1416,7 +1600,9 @@ slots:
1416
1600
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE]
1417
1601
  # ----- 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.
1418
1602
  - id: be.platform.event-bus
1603
+ since: 3.1.0
1419
1604
  profiles: [be]
1605
+ 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."
1420
1606
  path: "src/modules/platform/event-bus/"
1421
1607
  presence: opt-in
1422
1608
  pattern: event-bus # the ONE durable event transport: transactional outbox, relay, Kafka, inbox, retry and dead letter (knowledge/patterns/be/event-bus.yaml)
@@ -1434,8 +1620,11 @@ slots:
1434
1620
  coverage: required
1435
1621
  budget: {indexExports: 60}
1436
1622
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE, BE_INFRA_OWNER]
1623
+ exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1437
1624
  - id: be.platform.queue
1625
+ since: 3.1.0
1438
1626
  profiles: [be]
1627
+ 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."
1439
1628
  path: "src/modules/platform/queue/"
1440
1629
  presence: opt-in
1441
1630
  pattern: queue # the ONE BullMQ owner: the outbox relay into queues (jobId = outbox id) and job schedulers (knowledge/patterns/be/queues.yaml)
@@ -1454,6 +1643,7 @@ slots:
1454
1643
  budget: {indexExports: 60}
1455
1644
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE, BE_INFRA_OWNER]
1456
1645
  - id: be.events
1646
+ since: 3.1.0
1457
1647
  profiles: [be]
1458
1648
  path: "src/modules/events/<service>/"
1459
1649
  presence: opt-in
@@ -1467,6 +1657,7 @@ slots:
1467
1657
  coverage: none
1468
1658
  rules: [BE_EVENT_CLASS_CONTRACT, BE_TIER_DIRECTION]
1469
1659
  - id: be.queues
1660
+ since: 3.1.0
1470
1661
  profiles: [be]
1471
1662
  path: "src/modules/queues/<queue>/"
1472
1663
  presence: opt-in
@@ -1475,11 +1666,12 @@ slots:
1475
1666
  tier: queues
1476
1667
  owner: true
1477
1668
  requires: [index.ts, "<queue>.queue.ts"] # the module exists when a domain module imports the producer; a scheduler-only queue has none
1478
- allows: ["<queue>.queue.ts", "<queue>.module.ts", "<queue>.contracts.ts", index.ts]
1669
+ allows: ["<queue>.queue.ts", "<queue>.module.ts", "<queue>.module-definition.ts", "<queue>.options.ts", "<queue>.contracts.ts", index.ts]
1479
1670
  tests: none
1480
1671
  coverage: none
1481
1672
  rules: [BE_OUTBOX_WRITE_TX, BE_TIER_DIRECTION]
1482
1673
  - id: be.feature.reactors
1674
+ since: 3.1.0
1483
1675
  profiles: [be]
1484
1676
  path: "src/features/reactors/<reactor>/"
1485
1677
  presence: opt-in
@@ -1489,11 +1681,12 @@ slots:
1489
1681
  tier: feature
1490
1682
  owner: true
1491
1683
  requires: [index.ts, "<reactor>.module.ts", transport/]
1492
- allows: [index.ts, "<reactor>.module.ts", application/, transport/]
1684
+ allows: [index.ts, "<reactor>.module.ts", "<reactor>.module-definition.ts", "<reactor>.options.ts", application/, transport/]
1493
1685
  tests: none
1494
1686
  coverage: none
1495
1687
  rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED]
1496
1688
  - id: be.feature.jobs
1689
+ since: 3.1.0
1497
1690
  profiles: [be]
1498
1691
  path: "src/features/jobs/<job>/"
1499
1692
  presence: opt-in
@@ -1503,11 +1696,12 @@ slots:
1503
1696
  tier: feature
1504
1697
  owner: true
1505
1698
  requires: [index.ts, "<job>.module.ts", steps/, transport/]
1506
- allows: [index.ts, "<job>.module.ts", application/, steps/, transport/]
1699
+ allows: [index.ts, "<job>.module.ts", "<job>.module-definition.ts", "<job>.options.ts", application/, steps/, transport/]
1507
1700
  tests: none
1508
1701
  coverage: none
1509
1702
  rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED]
1510
1703
  - id: be.feature.jobs.queue
1704
+ since: 3.1.0
1511
1705
  profiles: [be]
1512
1706
  path: "src/features/jobs/<job>/transport/queue/"
1513
1707
  presence: opt-in # the queue transport of the job: its module and the processor the job runner hands the fenced claim to
@@ -1515,12 +1709,13 @@ slots:
1515
1709
  tracked: tracked
1516
1710
  tier: feature
1517
1711
  requires: ["<job>-queue.module.ts", "<job>.processor.ts"]
1518
- allows: ["<job>-queue.module.ts", "<job>.processor.ts"]
1712
+ allows: ["<job>-queue.module.ts", "<job>-queue.module-definition.ts", "<job>-queue.options.ts", "<job>.processor.ts"]
1519
1713
  tests: none
1520
1714
  coverage: none
1521
1715
  composedBy: [api, worker]
1522
1716
  rules: [BE_BACKGROUND_UNOWNED]
1523
1717
  - id: be.feature.jobs.steps
1718
+ since: 3.1.0
1524
1719
  profiles: [be]
1525
1720
  path: "src/features/jobs/<job>/steps/"
1526
1721
  presence: opt-in
@@ -1532,7 +1727,9 @@ slots:
1532
1727
  coverage: none
1533
1728
  rules: [BE_JOB_SHAPE, BE_JOB_RUN_KEY]
1534
1729
  - id: be.platform.jobs
1730
+ since: 3.1.0
1535
1731
  profiles: [be]
1732
+ 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."
1536
1733
  path: "src/modules/platform/jobs/"
1537
1734
  presence: opt-in
1538
1735
  pattern: fenced-job # the ONE fencing primitive: the job entity, the claim that bumps the token, guarded writes (knowledge/patterns/be/jobs.yaml)
@@ -1545,7 +1742,9 @@ slots:
1545
1742
  budget: {indexExports: 60}
1546
1743
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE, BE_JOB_WRITE_OUTSIDE_OWNER, BE_JOB_FENCE_REQUIRED]
1547
1744
  - id: be.projections
1745
+ since: 3.1.0
1548
1746
  profiles: [be]
1747
+ 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."
1549
1748
  path: "src/modules/projections/<name>/"
1550
1749
  presence: opt-in
1551
1750
  pattern: projection # a read model: only its projection writes its table; recompute is idempotent and replayable; readers use get*
@@ -1560,7 +1759,9 @@ slots:
1560
1759
  tests: unit-beside # a measured slot: <name>.projection.spec.ts beside its projection is legal (the file owes 100 per file; a spec of its own is optional)
1561
1760
  coverage: required
1562
1761
  rules: [BE_PROJECTION_WRITE_OWNER, BE_PROJECTION_SHAPE, BE_TIER_DIRECTION, BE_CONTEXT_COUPLING]
1762
+ exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1563
1763
  - id: be.integrations
1764
+ since: 3.1.0
1564
1765
  profiles: [be]
1565
1766
  path: "src/modules/integrations/<provider>/"
1566
1767
  presence: optional
@@ -1573,6 +1774,7 @@ slots:
1573
1774
  budget: {indexExports: 80} # a provider's port surface includes clients, config, options, tokens and errors
1574
1775
  rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT, BE_INTEGRATION_SPEC_MISSING]
1575
1776
  - id: be.integrations.model
1777
+ since: 3.1.0
1576
1778
  profiles: [be]
1577
1779
  path: "src/modules/integrations/<provider>/model/"
1578
1780
  presence: opt-in
@@ -1581,8 +1783,13 @@ slots:
1581
1783
  tests: none
1582
1784
  coverage: required
1583
1785
  why: an ML or LLM model client is an integration; weights and datasets are never in git
1786
+ rules: [BE_TIER_DIRECTION, BE_INFRA_OWNER, BE_INTEGRATION_SPEC_MISSING]
1787
+
1584
1788
  - id: be.integrations.supabase
1585
1789
  profiles: [be]
1790
+ exemplifiedBy:
1791
+ - packages/hfs/templates/be/skeleton-lite/src/modules/integrations/supabase/
1792
+ 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."
1586
1793
  # The only be import of @supabase/* (L10): the Auth admin and Storage admin client, plus the JWKS-based verification of
1587
1794
  # the Supabase access token the guard chain reads (L16). The connection itself is ordinary Postgres (the shared
1588
1795
  # EntityManager of the declared connection); what lives here are the service calls no database role covers.
@@ -1599,12 +1806,15 @@ slots:
1599
1806
  rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT]
1600
1807
  coverage: required
1601
1808
  since: 2.1.0
1809
+ exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1602
1810
 
1603
1811
  # ----- BE: tests (owner test layout 2026-09-30). Unit specs are <name>.service.spec.ts beside their service and nothing else; everything else
1604
1812
  # lives under src/tests/, one folder per kind, and the folder and the file suffix always agree (a file whose suffix does
1605
1813
  # not match its folder matches no slot: HFS_SLOT_UNDECLARED).
1606
1814
  - id: be.tests.world
1815
+ since: 3.1.0
1607
1816
  profiles: [be]
1817
+ 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."
1608
1818
  path: "src/tests/world/"
1609
1819
  presence: optional
1610
1820
  tracked: tracked
@@ -1628,7 +1838,11 @@ slots:
1628
1838
  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
1629
1839
  overridden in the DI container. It is the only test location that may import typeorm's DataSource or testcontainers,
1630
1840
  call migrate/runMigrations/synchronize, or write process.env.
1841
+ rules: [BE_TEST_TOPOLOGY, BE_TEST_CONSTRAINTS_ON]
1842
+ exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1843
+
1631
1844
  - id: be.tests.world.kit
1845
+ since: 3.1.0
1632
1846
  profiles: [be]
1633
1847
  path: "src/tests/world/kit/"
1634
1848
  presence: optional
@@ -1639,6 +1853,7 @@ slots:
1639
1853
  coverage: none
1640
1854
  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
1641
1855
  - id: be.tests.fixtures
1856
+ since: 3.1.0
1642
1857
  profiles: [be]
1643
1858
  path: "src/tests/fixtures/"
1644
1859
  presence: optional
@@ -1649,6 +1864,7 @@ slots:
1649
1864
  coverage: none
1650
1865
  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
1651
1866
  - id: be.tests.fixtures.builders
1867
+ since: 3.1.0
1652
1868
  profiles: [be]
1653
1869
  path: "src/tests/fixtures/builders/*.builder.ts"
1654
1870
  presence: optional
@@ -1663,6 +1879,7 @@ slots:
1663
1879
  *.fixture.ts or *.factory.ts anywhere, is a BE_SOURCE_FORM finding
1664
1880
  rules: [BE_SOURCE_FORM]
1665
1881
  - id: be.tests.fixtures.i18n
1882
+ since: 3.1.0
1666
1883
  profiles: [be]
1667
1884
  path: "src/tests/fixtures/i18n/"
1668
1885
  presence: optional
@@ -1673,7 +1890,9 @@ slots:
1673
1890
  coverage: none
1674
1891
  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
1675
1892
  - id: be.tests.integration
1893
+ since: 3.1.0
1676
1894
  profiles: [be]
1895
+ 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)."
1677
1896
  path: "src/tests/integration/<capability>/*.integration-spec.ts"
1678
1897
  presence: optional
1679
1898
  tracked: tracked
@@ -1687,10 +1906,12 @@ slots:
1687
1906
  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
1688
1907
  rules: [BE_TEST_TOPOLOGY, BE_INTEGRATION_SPEC_MISSING]
1689
1908
  - id: be.tests.e2e
1909
+ since: 3.1.0
1690
1910
  profiles: [be]
1911
+ 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."
1691
1912
  path: "src/tests/e2e/<area>/*.e2e-spec.ts"
1692
1913
  presence: optional
1693
- litePresence: forbidden # owner ruling 2026-10-12: a lite app has no e2e world
1914
+ 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
1694
1915
  tracked: tracked
1695
1916
  tier: e2e
1696
1917
  tests: e2e
@@ -1699,18 +1920,20 @@ slots:
1699
1920
  why: flow e2e through useTestWorld({ apps }); external services are the world's network fakes; run by test:e2e
1700
1921
  rules: [HFS_CI_E2E_AUTOMATIC, BE_TEST_TOPOLOGY, BE_E2E_FLOW]
1701
1922
  - id: be.tests.contract
1923
+ since: 3.1.0
1702
1924
  profiles: [be]
1703
1925
  path: "src/tests/contract/<provider>/*.contract-spec.ts"
1704
1926
  presence: optional
1705
- litePresence: forbidden # owner ruling 2026-10-12: a lite app has no contract world
1927
+ litePresence: optional # owner decision 2026-10-14: lite may run provider sandbox contracts
1706
1928
  tracked: tracked
1707
1929
  tier: e2e
1708
1930
  tests: e2e
1709
- goesTo: "the full edition: a lite app has no contract world (provider sandbox specs)"
1931
+ goesTo: "src/tests/contract/<provider>/*.contract-spec.ts in either edition: optional provider sandbox specs via test:contract; e2e stays full-only"
1710
1932
  coverage: none
1711
1933
  why: provider sandbox contracts, skipped without sandbox config; run only by test:contract, never part of test or test:e2e
1712
1934
  rules: [BE_TEST_TOPOLOGY]
1713
1935
  - id: be.tests.e2e-world-dir
1936
+ since: 3.1.0
1714
1937
  profiles: [be]
1715
1938
  path: "src/tests/e2e/world/"
1716
1939
  presence: forbidden
@@ -1721,6 +1944,7 @@ slots:
1721
1944
 
1722
1945
  # ----- FE: apps ----------------------------------------------------------------------------------------
1723
1946
  - id: fe.app.next
1947
+ since: 3.1.0
1724
1948
  profiles: [fe]
1725
1949
  # Each fe app is an npm workspace: its package.json is @<project>/<app> with its own runtime dependencies and the fixed
1726
1950
  # workspace scripts (HFS_MONO_FE_WORKSPACE); turbo runs them.
@@ -1735,6 +1959,7 @@ slots:
1735
1959
  tests: none
1736
1960
  rules: [FE_APP_ISOLATION, FE_ERROR_BOUNDARY_MISSING, FE_NEXT_CONVENTIONS, HFS_MONO_FE_WORKSPACE, HFS_MONO_WORKSPACE_DEP]
1737
1961
  - id: fe.app-optional
1962
+ since: 3.1.0
1738
1963
  profiles: [fe]
1739
1964
  path: "apps/<app>/public/"
1740
1965
  presence: optional
@@ -1742,12 +1967,13 @@ slots:
1742
1967
  tier: none
1743
1968
  tests: none
1744
1969
  - id: fe.route
1970
+ since: 3.1.0
1745
1971
  profiles: [fe]
1746
1972
  path: "apps/<app>/src/app/"
1747
1973
  presence: required
1748
1974
  tracked: tracked
1749
1975
  tier: route
1750
- # 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
1976
+ # 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
1751
1977
  # route handler of this slot, the Supabase auth callback is fe.route.callback, and no file holds a Server Action
1752
1978
  # (FE_ROUTE_HANDLER_FORBIDDEN, FE_SERVER_ACTION_FORBIDDEN).
1753
1979
  allows: ["[locale]/**/{page,layout,template,loading,error,not-found,default}.tsx", "[locale]/providers.tsx",
@@ -1760,6 +1986,7 @@ slots:
1760
1986
  tests: none
1761
1987
  rules: [FE_ROUTE_FILES_THIN, FE_CLIENT_BOUNDARY, FE_OWNER_REACHABLE, FE_ROUTE_HANDLER_FORBIDDEN, FE_SERVER_ACTION_FORBIDDEN]
1762
1988
  - id: fe.source-root-pinned
1989
+ since: 3.1.0
1763
1990
  profiles: [fe]
1764
1991
  path: "apps/<app>/src/{proxy.ts,instrumentation.ts,instrumentation-client.ts,sentry.server.config.ts,sentry.edge.config.ts}"
1765
1992
  presence: optional
@@ -1769,6 +1996,7 @@ slots:
1769
1996
  tests: none
1770
1997
  rules: [FE_NEXT_CONVENTIONS] # middleware.ts is refused on Next >= 16 (fe.source-root-middleware)
1771
1998
  - id: fe.source-root-middleware
1999
+ since: 3.1.0
1772
2000
  profiles: [fe]
1773
2001
  path: "apps/<app>/src/{middleware.ts,middleware.js}"
1774
2002
  presence: forbidden
@@ -1789,7 +2017,10 @@ slots:
1789
2017
  tier: route
1790
2018
  tests: none
1791
2019
  since: 2.1.0
2020
+ rules: [FE_ROUTE_HANDLER_FORBIDDEN, FE_CALLBACK_SHAPE, FE_SUPABASE_CLIENT_OWNER, FE_SERVICE_ROLE_FORBIDDEN]
2021
+
1792
2022
  - id: fe.feature
2023
+ since: 3.1.0
1793
2024
  profiles: [fe]
1794
2025
  path: "apps/<app>/src/features/{pages,layouts,overlays}/<name>/"
1795
2026
  presence: required
@@ -1804,6 +2035,7 @@ slots:
1804
2035
  budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
1805
2036
  rules: [FE_SIZE_BUDGET, FE_OWNER_REACHABLE, FE_I18N_LITERAL]
1806
2037
  - id: fe.components
2038
+ since: 3.1.0
1807
2039
  profiles: [fe]
1808
2040
  path: "apps/<app>/src/components/{blocks,composites,branches,leaves}/<name>/"
1809
2041
  presence: optional
@@ -1834,6 +2066,7 @@ slots:
1834
2066
  since: 3.0.0
1835
2067
  rules: [FE_CLIENT_BOUNDARY]
1836
2068
  - id: fe.hooks
2069
+ since: 3.1.0
1837
2070
  profiles: [fe]
1838
2071
  path: "apps/<app>/src/hooks/<domain>/"
1839
2072
  presence: optional
@@ -1956,7 +2189,10 @@ slots:
1956
2189
  tests: none
1957
2190
  budget: {file: 400}
1958
2191
  since: 3.0.0
2192
+ rules: [FE_SOURCE_FORM, FE_HOOKS_ARE_HOOKS, FE_OWNER_REACHABLE]
2193
+
1959
2194
  - id: fe.modules
2195
+ since: 3.1.0
1960
2196
  profiles: [fe]
1961
2197
  path: "apps/<app>/src/modules/<capability>/"
1962
2198
  presence: required
@@ -1970,6 +2206,7 @@ slots:
1970
2206
  budget: {file: 400}
1971
2207
  rules: [FE_ENV_OWNER, FE_TRANSPORT_OWNER]
1972
2208
  - id: fe.modules.api
2209
+ since: 3.1.0
1973
2210
  profiles: [fe]
1974
2211
  path: "apps/<app>/src/modules/api/"
1975
2212
  presence: optional
@@ -1989,7 +2226,19 @@ slots:
1989
2226
  entries: ["graphql/queries/query-*.ts", "graphql/queries/types/*.ts", "graphql/mutations/mutation-*.ts", "graphql/mutations/types/*.ts"]
1990
2227
  tests: none
1991
2228
  rules: [FE_TRANSPORT_OWNER, FE_HTTP_STATUS_COLLAPSE, FE_WIRE_GENERATED]
2229
+ - id: fe.transport.generated
2230
+ since: 3.2.0
2231
+ profiles: [fe]
2232
+ path: "apps/<app>/src/modules/api/graphql/__generated__/"
2233
+ presence: optional
2234
+ tracked: ignored
2235
+ tier: transport
2236
+ tests: none
2237
+ why: the canonical GraphQL client output of codegen is local and never committed; this specific slot overrides the tracked API parent
2238
+ rules: [FE_WIRE_GENERATED]
2239
+ exampleNa: "owner:no-runtime-specs (2026-10-17): the only example of this slot in an edition was a deleted runtime fixture; the scaffold e2e exercises the scaffolded slot"
1992
2240
  - id: fe.transport.client
2241
+ since: 3.1.0
1993
2242
  profiles: [fe]
1994
2243
  path: "apps/<app>/src/modules/api/graphql/clients/"
1995
2244
  presence: optional
@@ -1999,6 +2248,7 @@ slots:
1999
2248
  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
2000
2249
  rules: [FE_TRANSPORT_OWNER]
2001
2250
  - id: fe.transport.outcome
2251
+ since: 3.1.0
2002
2252
  profiles: [fe]
2003
2253
  path: "apps/<app>/src/modules/api/graphql/types.ts"
2004
2254
  presence: optional
@@ -2009,6 +2259,7 @@ slots:
2009
2259
  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
2010
2260
  rules: [FE_HTTP_STATUS_COLLAPSE]
2011
2261
  - id: fe.modules.config
2262
+ since: 3.1.0
2012
2263
  profiles: [fe]
2013
2264
  path: "apps/<app>/src/modules/config/"
2014
2265
  presence: required
@@ -2016,7 +2267,10 @@ slots:
2016
2267
  tier: foundation
2017
2268
  tests: none
2018
2269
  why: the only reader of process.env and NEXT_PUBLIC_*; fails fast, no localhost fallback
2270
+ rules: [FE_ENV_OWNER]
2271
+
2019
2272
  - id: fe.modules.i18n
2273
+ since: 3.1.0
2020
2274
  profiles: [fe]
2021
2275
  path: "apps/<app>/src/modules/i18n/"
2022
2276
  presence: required
@@ -2029,6 +2283,7 @@ slots:
2029
2283
  tests: none
2030
2284
  rules: [FE_I18N_PLACEMENT, FE_I18N_CATALOG]
2031
2285
  - id: fe.modules.brand
2286
+ since: 3.1.0
2032
2287
  profiles: [fe]
2033
2288
  path: "apps/<app>/src/modules/brand/brand.css"
2034
2289
  presence: optional
@@ -2038,6 +2293,7 @@ slots:
2038
2293
  why: the ONLY app file that may hold colour values; sets grammar brand tokens only, light and dark
2039
2294
  rules: [FE_STYLE_TOKEN_ONLY]
2040
2295
  - id: fe.modules.routes
2296
+ since: 3.1.0
2041
2297
  profiles: [fe]
2042
2298
  path: "apps/<app>/src/modules/routes/"
2043
2299
  presence: required
@@ -2045,7 +2301,10 @@ slots:
2045
2301
  tier: foundation
2046
2302
  tests: none
2047
2303
  why: every href builder; FE_HREF_RESOLVES checks each against app/
2304
+ rules: [FE_HREF_RESOLVES, FE_ROUTE_HARDCODED]
2305
+
2048
2306
  - id: fe.modules.types
2307
+ since: 3.1.0
2049
2308
  profiles: [fe]
2050
2309
  path: "apps/<app>/src/modules/types/"
2051
2310
  presence: optional
@@ -2053,6 +2312,8 @@ slots:
2053
2312
  tier: foundation
2054
2313
  tests: none
2055
2314
  why: shared types with no runtime behaviour; a component may import them
2315
+ rules: [FE_TYPE_ESCAPE, HFS_TS_STRICT]
2316
+
2056
2317
  - id: fe.modules.types.ambient
2057
2318
  parent: fe.modules.types
2058
2319
  profiles: [fe]
@@ -2065,6 +2326,8 @@ slots:
2065
2326
  tests: none
2066
2327
  since: 2.2.0
2067
2328
  why: global ambient declarations loaded by the app compiler; this owner exposes no runtime API
2329
+ rules: [FE_TYPE_ESCAPE, HFS_TS_STRICT]
2330
+
2068
2331
  - id: fe.modules.auth
2069
2332
  profiles: [fe]
2070
2333
  # The identity-provider client (fe-stack auth, FE-AUTH): the session, the access token the Apollo link attaches and the
@@ -2076,6 +2339,7 @@ slots:
2076
2339
  tier: modules
2077
2340
  owner: true
2078
2341
  requires: [index.ts]
2342
+ entries: [server.ts] # the server-only PKCE exchange API used by the opted-in callback route
2079
2343
  tests: none
2080
2344
  since: 3.0.0
2081
2345
  rules: [FE_SUPABASE_CLIENT_OWNER, FE_SERVICE_ROLE_FORBIDDEN]
@@ -2093,7 +2357,10 @@ slots:
2093
2357
  roles: {entry: index.ts, hooks: hooks.ts} # hooks.ts: the use*WithToast hooks of the toast module
2094
2358
  tests: none
2095
2359
  since: 3.0.0
2360
+ rules: [FE_I18N_LITERAL, FE_HOOKS_ARE_HOOKS, FE_HTTP_STATUS_COLLAPSE]
2361
+
2096
2362
  - id: fe.package.ui
2363
+ since: 3.1.0
2097
2364
  profiles: [fe]
2098
2365
  path: "packages/<family>-ui/"
2099
2366
  presence: opt-in
@@ -2108,6 +2375,7 @@ slots:
2108
2375
  why: built to dist with explicit named exports; only composites, branches and leaves; no unused export; no colour literal
2109
2376
  rules: [FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT, FE_STYLE_TOKEN_ONLY]
2110
2377
  - id: fe.package.api
2378
+ since: 3.1.0
2111
2379
  profiles: [fe]
2112
2380
  path: "packages/<family>-api/"
2113
2381
  presence: opt-in
@@ -2119,6 +2387,7 @@ slots:
2119
2387
  why: the one transport client and the one Outcome union shared by every app of the repository; no app keeps its own
2120
2388
  rules: [FE_TRANSPORT_OWNER, FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT]
2121
2389
  - id: fe.package.api.client
2390
+ since: 3.1.0
2122
2391
  profiles: [fe]
2123
2392
  path: "packages/<family>-api/src/graphql/clients/"
2124
2393
  presence: optional # enabled with its package (fe.package.api is the opt-in)
@@ -2128,6 +2397,7 @@ slots:
2128
2397
  why: the repository's one pair of Apollo clients and their link chain
2129
2398
  rules: [FE_TRANSPORT_OWNER]
2130
2399
  - id: fe.package.api.outcome
2400
+ since: 3.1.0
2131
2401
  profiles: [fe]
2132
2402
  path: "packages/<family>-api/src/graphql/types.ts"
2133
2403
  presence: optional # enabled with its package (fe.package.api is the opt-in)
@@ -2138,6 +2408,7 @@ slots:
2138
2408
  why: the repository's one GraphQLResponse union
2139
2409
  rules: [FE_HTTP_STATUS_COLLAPSE]
2140
2410
  - id: fe.package.i18n
2411
+ since: 3.1.0
2141
2412
  profiles: [fe]
2142
2413
  path: "packages/<family>-i18n/"
2143
2414
  presence: opt-in