@starci/eslint-canon-fe 8.3.0 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +6 -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/knowledge/hfs/canon-pins.yaml +11 -11
  17. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  18. package/runtime/knowledge/hfs/slots.yaml +344 -62
  19. package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
  20. package/runtime/knowledge/sonar-gate.yaml +1 -1
  21. package/runtime/modules/models/runtimes.yaml +1 -1
  22. package/runtime/packages/hfs/templates/app/package-scripts/package.json +1 -0
  23. package/runtime/scripts/api/git/lib.mjs +3 -0
  24. package/runtime/scripts/api/git/merge-base.mjs +9 -0
  25. package/runtime/scripts/api/git/recorded-lines.mjs +54 -8
  26. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
  27. package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
  28. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  29. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  30. package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
  31. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  32. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  33. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  34. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  35. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
  36. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  37. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  38. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  39. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  40. package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
  41. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  42. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
  43. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
  44. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  45. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  46. package/runtime/scripts/hfs/architecture/tiers.mjs +3 -3
  47. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
  48. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  49. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  50. package/runtime/scripts/hfs/manifest-shape.mjs +67 -7
  51. package/runtime/scripts/hfs/project-rule.mjs +3 -1
  52. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  53. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  54. package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
  55. package/runtime/scripts/hfs/slot-imports.mjs +18 -5
  56. package/runtime/scripts/hfs/slots.mjs +1 -0
  57. package/runtime/scripts/lib/env.mjs +20 -0
  58. package/runtime/scripts/lib/event-contract.mjs +107 -0
  59. package/runtime/scripts/lib/json-clone.mjs +61 -0
  60. package/runtime/scripts/lib/list.mjs +4 -0
  61. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  62. package/runtime/scripts/lib/stack-services.mjs +1 -1
  63. package/runtime/scripts/lib/ts-analysis.mjs +5 -2
  64. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  65. package/served-locale.mjs +80 -9
  66. package/size-growth.mjs +10 -6
  67. package/swr-family.mjs +197 -0
  68. package/translation.mjs +135 -7
  69. 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,10 @@ 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
+ - {path: tests/fixtures/hfs-callback-full/.github/workflows/db-deploy.yml, edition: full}
539
+ editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): lite manages its Supabase database deploy workflow; full owns an optional provider-specific workflow."
492
540
  # The database deploy workflow: workflow_dispatch only (like app.ci-e2e); starci app sync renders it under the managed ci group.
493
541
  path: ".github/workflows/db-deploy.yml"
494
542
  presence: optional
@@ -498,7 +546,21 @@ slots:
498
546
  liteManagedBy: ci-workflows-lite
499
547
  rules: [HFS_MANAGED_FILE_DRIFT]
500
548
  since: 2.1.0
549
+ - id: app.ci-stack-deploy
550
+ since: 3.3.0
551
+ profiles: [app]
552
+ editionWhy: "ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4): both editions deploy their images to a Docker Swarm stack through the same starci stack verbs; the workflow lists the images the app declares."
553
+ # The Swarm stack deploy workflow: workflow_dispatch only (like app.ci-db); a thin file that calls `starci app stack`, rendered by starci app sync.
554
+ path: ".github/workflows/stack-deploy.yml"
555
+ presence: optional
556
+ tracked: tracked
557
+ tier: none
558
+ tests: none
559
+ managedBy: ci-workflows
560
+ liteManagedBy: ci-workflows-lite
561
+ rules: [HFS_MANAGED_FILE_DRIFT]
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,11 @@ 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
+ exemplifiedBy:
661
+ - {path: tests/fixtures/hfs-delivery-lite/scripts/build-frontend.mjs, edition: lite}
662
+ editionWhy: "Lite splits product delivery tooling from managed codegen; full owns the same files through app.scripts (ruling:R212/case-2 (with the Work boundary amended by ruling:R212/case-4))."
575
663
  editions: [lite]
576
664
  # Product-owned environment, secret and build tooling, kept apart from lite's managed codegen script.
577
665
  # Full already owns these files through app.scripts; its generic *.mjs pattern must not compete with build-*.mjs.
@@ -582,6 +670,7 @@ slots:
582
670
  tests: none
583
671
  rules: [HFS_PLAINTEXT_SECRET, HFS_SCRIPT_ONE_OFF, BE_SPEC_PLACEMENT, HFS_REPO_LOCAL_CHECK]
584
672
  - id: app.sides
673
+ since: 3.1.0
585
674
  profiles: [app]
586
675
  # The two sides. Everything below be/ and fe/ is judged by the side's own slots (profiles be and fe) with the side folder as its root;
587
676
  # this slot answers only for the side folders themselves.
@@ -591,7 +680,12 @@ slots:
591
680
  tier: none
592
681
  tests: none
593
682
  - id: app.starciwork
683
+ since: 3.1.0
594
684
  profiles: [app]
685
+ exemplifiedBy:
686
+ - {path: packages/hfs/templates/app/skeleton/.starciwork/, edition: full}
687
+ - {path: packages/hfs/templates/app/skeleton-lite/.starciwork/, edition: lite}
688
+ editionWhy: "ruling:R212/case-4: lite optionally keeps .gitignore, workspace.yaml and brand records; full keeps the Work catalog and product summaries."
595
689
  path: ".starciwork/"
596
690
  presence: required
597
691
  tracked: tracked
@@ -601,9 +695,10 @@ slots:
601
695
  allows: [workspace.yaml, index.yaml, "brand/**", shell/index.yaml, "features/<feature>/index.yaml",
602
696
  "features/<feature>/<family>/<name>/index.yaml", "features/<feature>/br/<rule>/ac/<name>/index.yaml",
603
697
  "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,
698
+ "features/<feature>/uat/<name>/{seed,cleanup}.sql", "_resources/{identities,environments,fixtures}/<slug>/resource.yaml",
699
+ "_derived/{index.yaml,frontier.md,critique.yaml,critique.md}"]
700
+ forbids: [evidence/, logs/, reports/, captures/, runs/, "kernel-*/", "*.sqlite*"]
701
+ # ruling:R212/case-4: lite keeps workspace.yaml for project identity and repo roles, plus brand drawing records,
607
702
  # references and assets. No brand means no Work tree is needed; workspace.yaml is legal but optional.
608
703
  litePresence: optional
609
704
  lite:
@@ -611,7 +706,12 @@ slots:
611
706
  allows: [workspace.yaml, "brand/**"]
612
707
  rules: [HFS_AGENT_DATA_TRACKED, HFS_IDENTITY_CUSTODY]
613
708
  - id: app.starcistacks
709
+ since: 3.1.0
614
710
  profiles: [app]
711
+ editionWhy: "owner:lite-production-swarm: lite requires declared docker-swarm production with Supabase backing; full also admits Compose development, Kubernetes and Terraform."
712
+ exemplifiedBy:
713
+ - {path: packages/hfs/templates/app/skeleton/.starcistacks/, edition: full}
714
+ - {path: packages/hfs/templates/app/skeleton-lite/.starcistacks/, edition: lite}
615
715
  # The stack declarations and the sealed custody of the app, at the app root beside be/, fe/ and .starciwork (never under a side).
616
716
  path: ".starcistacks/"
617
717
  presence: required
@@ -619,18 +719,19 @@ slots:
619
719
  tier: none
620
720
  tests: none
621
721
  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",
722
+ allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,swarm,k8s,terraform}/**", "<env>/infra/metadata.json",
623
723
  "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
624
724
  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.
725
+ # Owner ruling 2026-10-14: lite declares docker-swarm production, app runtime config, sealed custody and seed data.
726
+ # Supabase supplies the database and broker; production has no Compose or host cron.
628
727
  lite:
629
728
  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"]
729
+ allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/swarm/**", "<env>/infra/metadata.json",
730
+ "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
731
+ forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
632
732
  rules: [HFS_STACKS_SHAPE, HFS_STACKS_IN_SIDE, HFS_PLAINTEXT_SECRET, HFS_IDENTITY_CUSTODY]
633
733
  - id: app.sops
734
+ since: 3.1.0
634
735
  profiles: [app]
635
736
  # The sops custody rule of .starcistacks/<env>/secrets/*.enc, at the app root beside the tree it seals.
636
737
  path: .sops.yaml
@@ -639,6 +740,7 @@ slots:
639
740
  tier: none
640
741
  tests: none
641
742
  - id: app.build-output
743
+ since: 3.1.0
642
744
  profiles: [app]
643
745
  path: "{**/node_modules/,coverage/,reports/,test-results/,**/*.tsbuildinfo}"
644
746
  presence: optional
@@ -646,6 +748,7 @@ slots:
646
748
  tier: none
647
749
  tests: none
648
750
  - id: app.tool-cache
751
+ since: 3.1.0
649
752
  profiles: [app]
650
753
  path: "{.eslintcache,.turbo/,.scannerwork/,.sonar/,.jest-cache/,.cache/,.tools/}"
651
754
  presence: forbidden
@@ -654,6 +757,7 @@ slots:
654
757
  tests: none
655
758
  goesTo: "${STARCI_CACHE_HOME:-%LOCALAPPDATA%/StarCi/cache}/<repo>/<tool>/ (the canon presets set eslint --cache-location, jest cacheDirectory, turbo cacheDir, sonar.working.directory)"
656
759
  - id: app.agent-output
760
+ since: 3.1.0
657
761
  profiles: [app]
658
762
  path: "{report*.json,*.tmp.json,%*%,nul,.artifacts/,.tmp-*,*-lint.json,lf.json,lt.json,*.log,design-plans/,.gitmounts/,.dat,vi-flat.txt}"
659
763
  presence: forbidden
@@ -663,6 +767,7 @@ slots:
663
767
  goesTo: "agent scratchpad or the StarCi blob store (%LOCALAPPDATA%/StarCi/blobs), cited by {name, sha256}"
664
768
  rules: [HFS_UNTRACKED_ROOT_ENTRY, HFS_AGENT_DATA_TRACKED]
665
769
  - id: app.worktrees
770
+ since: 3.1.0
666
771
  profiles: [app]
667
772
  path: "{.worktrees/,worktrees/,.starciwork/worktrees/}"
668
773
  presence: forbidden
@@ -671,6 +776,7 @@ slots:
671
776
  tests: none
672
777
  goesTo: "<lanes root>/<project>/<lane>/ (outside every repository) for a lane; a Kernel workflow's worktree is created and owned by Orca outside the app checkout, never tracked, and removed by the runtime's host-side controller once the workflow's finish marked it release-pending"
673
778
  - id: app.plaintext-env
779
+ since: 3.1.0
674
780
  profiles: [app]
675
781
  path: "{.env,.env.*,secrets.env,.secrets/,**/*.pem,**/*.key}"
676
782
  presence: forbidden
@@ -683,6 +789,10 @@ slots:
683
789
  # ----- app root: the Supabase project of an app whose connection declares provider: supabase (every lite app) -------
684
790
  - id: app.supabase
685
791
  profiles: [app]
792
+ exemplifiedBy:
793
+ - packages/hfs/templates/app/skeleton-lite/supabase/
794
+ - {path: tests/fixtures/hfs-callback-full/supabase/, edition: full}
795
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): Supabase is required in lite; full enables it only through a declared connection."
686
796
  # The one Supabase project directory, at the app root beside be/, fe/ and .starcistacks (never under a side).
687
797
  path: "supabase/"
688
798
  presence: opt-in # enabled by a connection with provider: supabase; required under edition lite
@@ -694,6 +804,10 @@ slots:
694
804
  since: 2.1.0
695
805
  - id: app.supabase.config
696
806
  profiles: [app]
807
+ exemplifiedBy:
808
+ - packages/hfs/templates/app/skeleton-lite/supabase/config.toml
809
+ - {path: tests/fixtures/hfs-callback-full/supabase/config.toml, edition: full}
810
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires Supabase configuration; full requires it only when Supabase is declared."
697
811
  # The project configuration: auth keys are env(NAME) references, never literals (L08 judges its policy keys).
698
812
  path: "supabase/config.toml"
699
813
  presence: opt-in
@@ -705,6 +819,10 @@ slots:
705
819
  since: 2.1.0
706
820
  - id: app.supabase.migrations
707
821
  profiles: [app]
822
+ exemplifiedBy:
823
+ - packages/hfs/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql
824
+ - {path: tests/fixtures/hfs-callback-full/supabase/migrations/20261014100000_baseline.sql, edition: full}
825
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite uses Supabase schema authority and requires a migration; full opts into that provider."
708
826
  # The schema authority of an app on Supabase: forward-only SQL migrations named <timestamp14>_<kebab-name>.sql,
709
827
  # one file per change, immutable once on origin/main (L02-L08 judge them).
710
828
  path: "supabase/migrations/<stamp>_<name>.sql"
@@ -739,6 +857,10 @@ slots:
739
857
  since: 2.1.0
740
858
  - id: app.supabase.types
741
859
  profiles: [app]
860
+ templateNa: "Database row types are generated from the app's Supabase schema by contract:emit; a hand-authored scaffold template would invent the schema contract."
861
+ exemplifiedBy:
862
+ - {path: tests/fixtures/hfs-callback-full/supabase/types/database.types.ts, edition: full}
863
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires generated Supabase row types; full enables them with the provider."
742
864
  # The generated Database types (database.types.ts) contract:emit writes from the migrations; both sides read it
743
865
  # (sides.<side>.reads supabase/types/); drift against regeneration is L09.
744
866
  path: "supabase/types/database.types.ts"
@@ -777,6 +899,7 @@ slots:
777
899
 
778
900
  # ----- side root (both sides): what a standalone repository root holds, less the app-root files ----------
779
901
  - id: repo.side-root-forbidden
902
+ since: 3.1.0
780
903
  profiles: [be, fe]
781
904
  path: "{package.json,package-lock.json,hfs.json,README.md,.gitignore,.gitattributes,.husky/,.github/,.starciwork/,.starcistacks/,supabase/,.sops.yaml,sonar-project.properties,codecov.yml,.prettierrc,.prettierignore,scripts/}"
782
905
  presence: forbidden
@@ -785,6 +908,7 @@ slots:
785
908
  tests: none
786
909
  goesTo: "the app root: one package.json, lockfile, hfs.json, README, git and CI files, hooks, formatter, Sonar and Codecov configuration, scripts/, .starciwork, .starcistacks and .sops.yaml per app"
787
910
  - id: be.tool-config
911
+ since: 3.1.0
788
912
  profiles: [be]
789
913
  # Managed files (starci app sync renders them, starci app check compares them): the whole tool configuration of a back end. Each is
790
914
  # a reference to a canon package, never a configuration: tsconfig.json extends @starci/tsconfig/be.json and adds the
@@ -800,11 +924,12 @@ slots:
800
924
  tracked: tracked
801
925
  tier: none
802
926
  tests: none
803
- # Lite renders the same files (owner ruling 2026-10-12): the unit run and its coverage are full's.
927
+ # Lite renders the same files (owner ruling 2026-10-12 (owner:lite-production-swarm), with the Work boundary amended by ruling:R212/case-4): the unit run and its coverage are full's.
804
928
  coverage: none
805
929
  managedBy: tool-config
806
930
  rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL, HFS_TS_STRICT]
807
931
  - id: be.nest-cli
932
+ since: 3.1.0
808
933
  profiles: [be]
809
934
  # The Nest monorepo of the back end: `monorepo: true` and one project per declared be app (apps/<app>, even for one service).
810
935
  path: nest-cli.json
@@ -815,6 +940,7 @@ slots:
815
940
  coverage: none
816
941
  rules: [HFS_MONO_NEST_PROJECTS]
817
942
  - id: be.tool-config-local
943
+ since: 3.1.0
818
944
  profiles: [be]
819
945
  path: "{.eslintrc,.eslintrc.*,.eslintignore,eslint.config.js,eslint.config.cjs,eslint.config.ts,eslint.config.mts,eslint.config.cts,.prettierrc.*,prettier.config.*,jest.config.ts,jest.config.mjs,jest.config.cjs,jest.config.json,jest.config.e2e.js}"
820
946
  presence: forbidden
@@ -824,6 +950,7 @@ slots:
824
950
  goesTo: "nowhere: a back end has exactly the managed tool configuration (be.tool-config); a change to a rule or a flag is proposed in the .claude runtime"
825
951
  rules: [HFS_TOOL_CONFIG_LOCAL]
826
952
  - id: fe.tool-config
953
+ since: 3.1.0
827
954
  profiles: [fe]
828
955
  # Managed files (starci app sync renders them, starci app check compares them): the tool configuration of a front end. Each is a
829
956
  # reference to a canon package, never a configuration: tsconfig.json extends @starci/tsconfig/next.json and only
@@ -839,6 +966,7 @@ slots:
839
966
  managedBy: tool-config
840
967
  rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL, HFS_RULE_OFF_WITHOUT_REPLACEMENT, HFS_TS_STRICT]
841
968
  - id: fe.tool-config-local
969
+ since: 3.1.0
842
970
  profiles: [fe]
843
971
  path: "{.eslintrc,.eslintrc.*,.eslintignore,eslint.config.js,eslint.config.cjs,eslint.config.ts,eslint.config.mts,eslint.config.cts,.stylelintrc,.stylelintrc.*,.stylelintignore,stylelint.config.js,stylelint.config.cjs,stylelint.config.ts,stylelint.config.json,prettier.config.*,.prettierrc.*,lint-staged.config.*,.lintstagedrc*,turbo.json}"
844
972
  presence: forbidden
@@ -848,6 +976,7 @@ slots:
848
976
  goesTo: "nowhere: a front end has exactly the managed tool configuration (fe.tool-config); the task graph is the app root's turbo.json (app.task-graph); a change to a rule or a flag is proposed in the .claude runtime"
849
977
  rules: [HFS_TOOL_CONFIG_LOCAL]
850
978
  - id: repo.docs
979
+ since: 3.1.0
851
980
  profiles: [be, fe]
852
981
  path: "docs/{adr,runbooks,guides}/**/*.md"
853
982
  presence: opt-in
@@ -857,6 +986,7 @@ slots:
857
986
  coverage: none
858
987
  rules: [HFS_ROOT_MARKDOWN_FORBIDDEN]
859
988
  - id: repo.docs-media
989
+ since: 3.1.0
860
990
  profiles: [be, fe]
861
991
  path: "docs/**/*.{png,svg,webp}"
862
992
  presence: opt-in
@@ -865,7 +995,23 @@ slots:
865
995
  tests: none
866
996
  coverage: none
867
997
  budget: {bytes: 512000}
998
+ - id: repo.i18n-dynamic-keys
999
+ since: 3.3.0
1000
+ profiles: [be, fe]
1001
+ # The side's authored ledger of catalog keys the R280 detector cannot see (fe/i18n-dynamic-keys.yaml, be/i18n-dynamic-keys.yaml:
1002
+ # each side owns its own catalogs). A key it names counts as read; an entry matching no catalog key, or a malformed file, is itself an R280 finding.
1003
+ path: i18n-dynamic-keys.yaml
1004
+ presence: opt-in
1005
+ tracked: tracked
1006
+ tier: none
1007
+ tests: none
1008
+ coverage: none
1009
+ exemplifiedBy:
1010
+ - {path: tests/fixtures/hfs-i18n-ledger/fe/i18n-dynamic-keys.yaml, editions: [lite, full]}
1011
+ - {path: tests/fixtures/hfs-i18n-ledger/be/i18n-dynamic-keys.yaml, editions: [lite, full]}
1012
+ rules: [HFS_UNUSED_SYMBOL]
868
1013
  - id: repo.packages
1014
+ since: 3.1.0
869
1015
  profiles: [be, fe]
870
1016
  path: "packages/<pkg>/"
871
1017
  presence: opt-in
@@ -877,7 +1023,14 @@ slots:
877
1023
  coverage: none
878
1024
  rules: [FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT]
879
1025
  - id: repo.app-image
1026
+ since: 3.1.0
880
1027
  profiles: [be, fe]
1028
+ exemplifiedBy:
1029
+ - packages/hfs/templates/be/image/api/Dockerfile
1030
+ - packages/hfs/templates/be/image/cli/Dockerfile
1031
+ - {path: packages/hfs/templates/be/image/worker/Dockerfile, edition: full}
1032
+ - packages/hfs/templates/fe/image/next/Dockerfile
1033
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite may host Next without its own image; back-end images stay required in both editions."
881
1034
  # One image per app: every declared app has its own Dockerfile (apps/<app>/Dockerfile), built from the APP ROOT (the folder with
882
1035
  # the one package.json). It is app-owned (starci app scaffold writes it once from templates/<side>/image) and judged by the docker rules.
883
1036
  # A Dockerfile anywhere else is owned by no slot.
@@ -892,6 +1045,7 @@ slots:
892
1045
 
893
1046
  # ----- never tracked: build output the tools need in place ---------------------------------------------
894
1047
  - id: repo.build-output
1048
+ since: 3.1.0
895
1049
  profiles: [be, fe]
896
1050
  path: "{**/node_modules/,dist/,apps/*/dist/,packages/*/dist/,.next/,apps/*/.next/,coverage/,reports/,test-results/,**/next-env.d.ts,**/*.tsbuildinfo}"
897
1051
  presence: optional
@@ -899,6 +1053,7 @@ slots:
899
1053
  tier: none
900
1054
  tests: none
901
1055
  - id: repo.generated
1056
+ since: 3.1.0
902
1057
  profiles: [be, fe]
903
1058
  path: "{**/__generated__/,apps/*/src/schema.gql}"
904
1059
  presence: optional
@@ -909,6 +1064,7 @@ slots:
909
1064
 
910
1065
  # ----- never in the working tree: caches, scratch, agent output ----------------------------------------
911
1066
  - id: repo.tool-cache
1067
+ since: 3.1.0
912
1068
  profiles: [be, fe]
913
1069
  path: "{.eslintcache,.turbo/,.scannerwork/,.sonar/,.jest-cache/,.cache/,.tools/}"
914
1070
  presence: forbidden
@@ -917,6 +1073,7 @@ slots:
917
1073
  tests: none
918
1074
  goesTo: "${STARCI_CACHE_HOME:-%LOCALAPPDATA%/StarCi/cache}/<repo>/<tool>/ (the canon presets set eslint --cache-location, jest cacheDirectory, turbo cacheDir, sonar.working.directory)"
919
1075
  - id: repo.agent-output
1076
+ since: 3.1.0
920
1077
  profiles: [be, fe]
921
1078
  path: "{report*.json,*.tmp.json,%*%,nul,.artifacts/,.tmp-*,*-lint.json,lf.json,lt.json,*.log,design-plans/,.gitmounts/,.dat,vi-flat.txt}"
922
1079
  presence: forbidden
@@ -926,6 +1083,7 @@ slots:
926
1083
  goesTo: "agent scratchpad or the StarCi blob store (%LOCALAPPDATA%/StarCi/blobs), cited by {name, sha256}"
927
1084
  rules: [HFS_UNTRACKED_ROOT_ENTRY, HFS_AGENT_DATA_TRACKED]
928
1085
  - id: repo.worktrees
1086
+ since: 3.1.0
929
1087
  profiles: [be, fe]
930
1088
  path: "{.worktrees/,worktrees/,.starciwork/worktrees/}"
931
1089
  presence: forbidden
@@ -934,6 +1092,7 @@ slots:
934
1092
  tests: none
935
1093
  goesTo: "<lanes root>/<project>/<lane>/ (outside every repository)"
936
1094
  - id: repo.plaintext-env
1095
+ since: 3.1.0
937
1096
  profiles: [be, fe]
938
1097
  path: "{.env,.env.*,.secrets/,**/*.pem,**/*.key}"
939
1098
  presence: forbidden
@@ -943,6 +1102,7 @@ slots:
943
1102
  goesTo: ".starcistacks/<env>/secrets/<slug>.enc (at the app root); decrypted values only in memory or under %LOCALAPPDATA%/StarCi/secrets/<project>/<env>/ via *_FILE"
944
1103
  rules: [HFS_PLAINTEXT_SECRET]
945
1104
  - id: be.root-e2e
1105
+ since: 3.1.0
946
1106
  profiles: [be]
947
1107
  path: "e2e/"
948
1108
  presence: forbidden
@@ -953,6 +1113,7 @@ slots:
953
1113
 
954
1114
  # ----- contract snapshots (the committed machine-emitted files, because they are a cross-repo interface) --
955
1115
  - id: be.contract.graphql
1116
+ since: 3.1.0
956
1117
  profiles: [be]
957
1118
  path: "contracts/<app>/schema.graphql"
958
1119
  presence: opt-in
@@ -963,6 +1124,7 @@ slots:
963
1124
  why: emitted by `npm run contract:emit` (`starci app emit`, never written by hand); CI fails when the emitted file differs (a contract, not a build artefact)
964
1125
  rules: [HFS_CONTRACT_SNAPSHOT_DRIFT]
965
1126
  - id: be.contract.openapi
1127
+ since: 3.1.0
966
1128
  profiles: [be]
967
1129
  path: "contracts/<app>/openapi.json"
968
1130
  presence: opt-in
@@ -973,6 +1135,7 @@ slots:
973
1135
  why: emitted by `npm run contract:emit` (`starci app emit`, never written by hand) from the app's typed operation table `apps/<app>/src/operations.ts` (canon BE-OPERATIONS-1); CI fails when the emitted file differs
974
1136
  rules: [HFS_CONTRACT_SNAPSHOT_DRIFT]
975
1137
  - id: be.contract.events
1138
+ since: 3.1.0
976
1139
  profiles: [be]
977
1140
  path: "contracts/<app>/events.json"
978
1141
  presence: opt-in
@@ -983,6 +1146,7 @@ slots:
983
1146
  why: the async contract of a service (`starci/event-contract@1`), emitted by `npm run contract:emit` (`starci app emit`, never written by hand) from the typed event classes of `src/modules/events/<service>/`; the consumers of its events rely on it (R166 HFS_EVENT_CONTRACT)
984
1147
  rules: [HFS_EVENT_CONTRACT]
985
1148
  - id: be.contract.events.pin
1149
+ since: 3.1.0
986
1150
  profiles: [be]
987
1151
  path: "contracts/<app>/events.pin.json"
988
1152
  presence: opt-in
@@ -1012,6 +1176,7 @@ slots:
1012
1176
  # area's global ports when it has any); composition only, no business logic or process.env/EnvSource read.
1013
1177
  # app.module.ts the composition root: imports the groups and spreads them, binds the app-wide guards and filter.
1014
1178
  - id: be.app.api
1179
+ since: 3.1.0
1015
1180
  profiles: [be]
1016
1181
  path: "apps/<app>/src/"
1017
1182
  appKind: api
@@ -1020,13 +1185,14 @@ slots:
1020
1185
  tier: app
1021
1186
  owner: true
1022
1187
  minInstances: 1
1023
- requires: [main.ts, app.module.ts]
1188
+ requires: [main.ts, app.module.ts, "<app>.env.ts"]
1024
1189
  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
1190
+ tests: none # full app boot is proven by useTestWorld({ apps }); lite by starci app check --wiring (R272)
1026
1191
  coverage: none
1027
1192
  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]
1193
+ rules: [BE_APP_COMPOSITION_ONLY, BE_APP_BUSINESS_ROLE, BE_APP_OPTIONS_COMPOSITION, BE_APP_COMPOSITION_BUDGET, BE_DEFAULT_DENY, BE_ERROR_MASKED, BE_FEATURE_NOT_COMPOSED, BE_CONTEXT_OWNER, BE_APP_ENV_REQUIRED]
1029
1194
  - id: be.app.worker
1195
+ since: 3.1.0
1030
1196
  profiles: [be]
1031
1197
  path: "apps/<app>/src/"
1032
1198
  appKind: worker # composes transport/message modules and job processors only (an api app may compose them too: a worker app is for separate scaling)
@@ -1034,14 +1200,16 @@ slots:
1034
1200
  tracked: tracked
1035
1201
  tier: app
1036
1202
  owner: true
1037
- requires: [main.ts, app.module.ts]
1203
+ requires: [main.ts, app.module.ts, "<app>.env.ts"]
1038
1204
  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
1205
+ tests: none # full app boot is proven by useTestWorld({ apps }); lite by starci app check --wiring (R272)
1040
1206
  coverage: none
1041
1207
  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]
1208
+ rules: [BE_APP_COMPOSITION_ONLY, BE_APP_OPTIONS_COMPOSITION, BE_APP_COMPOSITION_BUDGET, BE_BACKGROUND_UNOWNED, BE_CONTEXT_OWNER, BE_APP_ENV_REQUIRED]
1043
1209
  - id: be.app.cli
1210
+ since: 3.1.0
1044
1211
  profiles: [be]
1212
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): Supabase CLI owns lite migrations; a lite cli app is optional for declared operator commands."
1045
1213
  # The ONE app of every one-off action (migrate, seed, sync, backup, any operator command): be/apps/cli, on nest-commander.
1046
1214
  # main.ts boots with CommandFactory.run (BE_CLI_BOOTSTRAP); app.module.ts composes the platform and the cli feature root
1047
1215
  # (src/features/cli/cli.module.ts); cli.env.ts exports CliEnv and readCliEnv(env: EnvSource): CliEnv, and cli.options.ts carries the env-derived values,
@@ -1055,18 +1223,19 @@ slots:
1055
1223
  tracked: tracked
1056
1224
  tier: app
1057
1225
  owner: true
1058
- requires: [main.ts, app.module.ts]
1226
+ requires: [main.ts, app.module.ts, "<app>.env.ts"]
1059
1227
  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
1228
+ tests: none # full app boot is proven by useTestWorld({ apps }); lite by starci app check --wiring (R272)
1061
1229
  coverage: none
1062
1230
  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]
1231
+ rules: [BE_CLI_REQUIRED, BE_CLI_BOOTSTRAP, BE_CLI_OWNER, BE_SCHEMA_AUTHORITY, BE_APP_OPTIONS_COMPOSITION, BE_APP_COMPOSITION_BUDGET, BE_APP_ENV_REQUIRED]
1064
1232
 
1065
1233
  # ----- BE: features ------------------------------------------------------------------------------------
1066
1234
  # src/features/ is grouped by TRIGGER KIND (owner decision 2026-10-02): features/api/<feature>/ (a request: application/ and
1067
1235
  # transport/<protocol>/, composed by the api apps), features/cli/ (a one-off action: the cli feature root, compiled only into
1068
1236
  # apps/cli). Every kind imports only downward into modules/*; no kind imports another.
1069
1237
  - id: be.feature
1238
+ since: 3.1.0
1070
1239
  profiles: [be]
1071
1240
  path: "src/features/api/<feature>/"
1072
1241
  presence: required
@@ -1076,12 +1245,13 @@ slots:
1076
1245
  owner: true
1077
1246
  minInstances: 1
1078
1247
  requires: [index.ts, "<feature>.module.ts", application/]
1079
- allows: [index.ts, "<feature>.module.ts", application/, transport/, messages/] # nothing else at feature root
1248
+ allows: [index.ts, "<feature>.module.ts", "<feature>.module-definition.ts", "<feature>.options.ts", application/, transport/, messages/] # nothing else at feature root
1080
1249
  tests: none
1081
1250
  coverage: none
1082
1251
  budget: {files: 250, indexExports: 60} # a feature above 250 source files must be split by product capability
1083
1252
  rules: [BE_FEATURE_SHAPE, BE_FEATURE_IMPORTS_FEATURE, BE_PUBLIC_SURFACE, BE_FEATURE_NOT_COMPOSED]
1084
1253
  - id: be.feature.application
1254
+ since: 3.1.0
1085
1255
  profiles: [be]
1086
1256
  path: "src/features/{api/,jobs/,reactors/,saga/}<feature>/application/" # inside a trigger kind (jobs, reactors, saga) the feature folder is one level down
1087
1257
  presence: required
@@ -1093,6 +1263,7 @@ slots:
1093
1263
  coverage: none
1094
1264
  rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_APPLICATION_TRANSPORT_FRAMEWORK, BE_ENTITY_IN_CONTRACT, BE_CQRS_SHAPE]
1095
1265
  - id: be.feature.saga
1266
+ since: 3.1.0
1096
1267
  profiles: [be]
1097
1268
  path: "src/features/saga/<saga>/"
1098
1269
  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 +1273,12 @@ slots:
1102
1273
  tier: feature
1103
1274
  owner: true
1104
1275
  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
1276
+ allows: [index.ts, "<saga>.module.ts", "<saga>.module-definition.ts", "<saga>.options.ts", "<saga>.saga.service.ts", "<saga>.saga.service.spec.ts", "<saga>.saga-state.ts", "<saga>.saga.log-events.ts", application/, steps/, compensations/, transport/] # the orchestrator is a `*.service.ts` because a handler injects only services; unit-tested beside it
1106
1277
  tests: unit-beside # <saga>.saga.service.spec.ts beside the orchestrator; the state machine and its fence are platform/saga's service; each compensation path has an e2e spec
1107
1278
  coverage: none
1108
1279
  rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED, BE_SAGA_STEP_COMPENSATION, BE_SAGA_STATE_VERSIONED, BE_SAGA_EVENT_CONTRACT, BE_SAGA_E2E_MISSING]
1109
1280
  - id: be.feature.saga.steps
1281
+ since: 3.1.0
1110
1282
  profiles: [be]
1111
1283
  path: "src/features/saga/<saga>/steps/"
1112
1284
  pattern: saga
@@ -1118,6 +1290,7 @@ slots:
1118
1290
  coverage: none
1119
1291
  rules: [BE_SAGA_STEP_COMPENSATION, BE_SAGA_EVENT_CONTRACT]
1120
1292
  - id: be.feature.saga.compensations
1293
+ since: 3.1.0
1121
1294
  profiles: [be]
1122
1295
  path: "src/features/saga/<saga>/compensations/"
1123
1296
  pattern: saga
@@ -1141,30 +1314,33 @@ slots:
1141
1314
  rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_APPLICATION_TRANSPORT_FRAMEWORK, BE_FEATURE_IMPORTS_FEATURE]
1142
1315
  since: 1.0.0
1143
1316
  - id: be.transport.http
1317
+ since: 3.1.0
1144
1318
  profiles: [be]
1145
1319
  path: "src/features/api/<feature>/transport/http/"
1146
1320
  presence: optional
1147
1321
  tracked: tracked
1148
1322
  tier: feature
1149
1323
  requires: ["<feature>-http.module.ts"]
1150
- allows: ["<action>.controller.ts", "dto/<action>.{request,response}.ts", "<action>.mapper.ts"]
1324
+ allows: ["<action>.controller.ts", "dto/<action>.{request,response}.ts", "<action>.mapper.ts", "<feature>-http.module-definition.ts", "<feature>-http.options.ts"]
1151
1325
  tests: none
1152
1326
  coverage: none
1153
1327
  composedBy: [api]
1154
1328
  rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED, BE_TRANSPORT_SHAPE]
1155
1329
  - id: be.transport.graphql
1330
+ since: 3.1.0
1156
1331
  profiles: [be]
1157
1332
  path: "src/features/api/<feature>/transport/graphql/"
1158
1333
  presence: optional
1159
1334
  tracked: tracked
1160
1335
  tier: feature
1161
1336
  requires: ["<feature>-graphql.module.ts"]
1162
- allows: ["<action>.resolver.ts", "dto/<action>.{input,type,args}.ts", "<action>.mapper.ts"]
1337
+ allows: ["<action>.resolver.ts", "dto/<action>.{input,type,args}.ts", "<action>.mapper.ts", "<feature>-graphql.module-definition.ts", "<feature>-graphql.options.ts"]
1163
1338
  tests: none
1164
1339
  coverage: none
1165
1340
  composedBy: [api]
1166
1341
  rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED, BE_TRANSPORT_SHAPE]
1167
1342
  - id: be.transport.message
1343
+ since: 3.1.0
1168
1344
  profiles: [be]
1169
1345
  path: "src/features/{api/,reactors/,saga/}<feature>/transport/message/"
1170
1346
  presence: opt-in # queue consumer: composed by the service that owns it (an api app) or by a worker app scaled apart
@@ -1172,7 +1348,7 @@ slots:
1172
1348
  tracked: tracked
1173
1349
  tier: feature
1174
1350
  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
1351
+ allows: ["<event>.consumer.ts", "<feature>-message.module-definition.ts", "<feature>-message.options.ts"] # BE-CONVENTION 1.6: transport/message holds only consumers; a .message.ts is outside the closed suffix list
1176
1352
  tests: none
1177
1353
  coverage: none
1178
1354
  composedBy: [api, worker]
@@ -1204,7 +1380,7 @@ slots:
1204
1380
  tracked: tracked
1205
1381
  tier: feature
1206
1382
  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/]
1383
+ allows: ["<provider>-http.module.ts", "<provider>-http.module-definition.ts", "<provider>-http.options.ts", "<provider>.webhook.ts", "<provider>.webhook.spec.ts", dto/]
1208
1384
  tests: unit-beside # the door's own spec (<provider>.webhook.spec.ts) beside the door: the one non-service unit spec of this kind
1209
1385
  coverage: none
1210
1386
  composedBy: [api]
@@ -1225,6 +1401,9 @@ slots:
1225
1401
  since: 1.0.0
1226
1402
  - id: be.feature.realtime
1227
1403
  profiles: [be]
1404
+ exemplifiedBy:
1405
+ - {path: tests/fixtures/hfs-lite-slots/be/src/features/realtime/order-status/, edition: lite}
1406
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite runs socket.io in the api process, one instance without Redis; full may scale its gateway."
1228
1407
  path: "src/features/realtime/<channel>/"
1229
1408
  presence: opt-in # a push channel: reads and pushes only (a socket gateway or a GraphQL subscription), no domain write
1230
1409
  pattern: realtime # declaring the pattern (hfs.json sides.be.patterns) enables the kind (knowledge/patterns/be/realtime.yaml)
@@ -1245,15 +1424,16 @@ slots:
1245
1424
  since: 1.0.0
1246
1425
  - id: be.feature.realtime.graphql
1247
1426
  profiles: [be]
1427
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite uses only the in-process socket.io gateway; the GraphQL subscription door remains full-only."
1248
1428
  path: "src/features/realtime/<channel>/transport/graphql/"
1249
1429
  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
1430
+ litePresence: forbidden # ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite pushes through the api's in-process socket.io gateway
1251
1431
  pattern: realtime
1252
1432
  tracked: tracked
1253
1433
  tier: feature
1254
1434
  goesTo: "src/features/realtime/<channel>/transport/websocket/ (be.feature.realtime.websocket): both editions push through the socket.io gateway; lite runs it in the api process"
1255
1435
  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/]
1436
+ allows: ["<channel>-graphql.module.ts", "<channel>-graphql.module-definition.ts", "<channel>-graphql.options.ts", "<channel>.subscription.ts", "<channel>.subscription.spec.ts", dto/]
1257
1437
  tests: unit-beside # the door's own spec beside the door (a subscription has no service of its own)
1258
1438
  coverage: none
1259
1439
  composedBy: [api]
@@ -1261,6 +1441,7 @@ slots:
1261
1441
  since: 1.0.0
1262
1442
  - id: be.feature.realtime.dto
1263
1443
  profiles: [be]
1444
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): GraphQL subscription DTOs exist only with the full subscription door; lite uses socket.io contracts."
1264
1445
  path: "src/features/realtime/<channel>/transport/graphql/dto/"
1265
1446
  presence: opt-in
1266
1447
  litePresence: forbidden # the dto of the graphql door, which lite does not serve
@@ -1282,20 +1463,21 @@ slots:
1282
1463
  tracked: tracked
1283
1464
  tier: feature
1284
1465
  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"]
1466
+ allows: ["<channel>-websocket.module.ts", "<channel>-websocket.module-definition.ts", "<channel>-websocket.options.ts", "<channel>.gateway.ts", "<channel>.gateway.spec.ts"]
1286
1467
  tests: unit-beside # the door's own spec beside the door (a gateway has no service of its own)
1287
1468
  coverage: none
1288
1469
  composedBy: [api]
1289
1470
  rules: [BE_REALTIME_SHAPE, BE_REALTIME_WRITES, BE_REALTIME_TOPIC_SCOPE, BE_DEFAULT_DENY]
1290
1471
  since: 1.0.0
1291
1472
  - id: be.cli
1473
+ since: 3.1.0
1292
1474
  profiles: [be]
1293
1475
  # The cli feature root: every one-off action of the back end, compiled only into apps/cli. A group is a nest-commander
1294
1476
  # @Command (<group>/<group>.cli.ts) whose sub-commands are @SubCommand classes extending CommandRunner
1295
1477
  # (<group>/subs/<name>.cli.ts), each with its unit spec beside it (<name>.cli.spec.ts): a command is an action runner, not a
1296
1478
  # 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
1479
+ # the root has a module definition beside it (ruling:module-definition-everywhere) and no register override: cli.module.ts imports each
1480
+ # group module as <Group>Module.register({}), a group module provides its commands, and a command takes its inputs from a platform capability (the connections and their opener
1299
1481
  # from platform/database) or from a command-line @Option.
1300
1482
  path: "src/features/cli/"
1301
1483
  presence: optional
@@ -1306,7 +1488,7 @@ slots:
1306
1488
  requires: [index.ts, cli.module.ts]
1307
1489
  # A composed root: an app of kind cli lists cli.module.ts, never a group module (BE_MODULE_SHAPE).
1308
1490
  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"]
1491
+ allows: [index.ts, cli.module.ts, "cli.module-definition.ts", "cli.options.ts", "<group>/<group>.cli.ts", "<group>/<group>.cli.spec.ts", "<group>/<group>.module.ts", "<group>/<group>.module-definition.ts", "<group>/<group>.options.ts", "<group>/subs/<name>.cli.ts", "<group>/subs/<name>.cli.spec.ts"]
1310
1492
  tests: unit-beside
1311
1493
  coverage: none
1312
1494
  rules: [BE_CLI_COMMAND_SHAPE, BE_CLI_OWNER, BE_CLI_REQUIRED, BE_MODULE_SHAPE, BE_SCHEMA_AUTHORITY]
@@ -1323,8 +1505,11 @@ slots:
1323
1505
  tests: none
1324
1506
  coverage: none
1325
1507
  since: 3.0.0
1508
+ rules: [HFS_ASSET_OVERSIZE, HFS_ASSET_KIND]
1509
+
1326
1510
  # ----- BE: modules (three tiers, closed set) -----------------------------------------------------------
1327
1511
  - id: be.domain
1512
+ since: 3.1.0
1328
1513
  profiles: [be]
1329
1514
  path: "src/modules/domain/<capability>/"
1330
1515
  presence: optional
@@ -1334,13 +1519,14 @@ slots:
1334
1519
  requires: [index.ts]
1335
1520
  # <name>.templates.ts holds the capability's static templates, copy and presets as TS constants (owner ruling 2026-10-13);
1336
1521
  # 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"]
1522
+ allows: ["<capability>.module.ts", "<capability>.module-definition.ts", "<capability>.config.ts", "<capability>.options.ts", errors/, messages/, persistence/, policies/, "*.service.ts", "*.service.spec.ts", "*.contracts.ts", "*.templates.ts"]
1338
1523
  forbids: [testing/, exceptions/, utils/, helpers/, shared/]
1339
1524
  tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts, nothing else
1340
1525
  coverage: required
1341
1526
  budget: {indexExports: 60}
1342
1527
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_PUBLIC_SURFACE, BE_ERROR_HOME, BE_CONFIG_OWNER, BE_CONTEXT_COUPLING, BE_CONTEXT_TRANSACTION]
1343
1528
  - id: be.errors
1529
+ since: 3.1.0
1344
1530
  profiles: [be]
1345
1531
  path: "src/modules/{domain,integrations}/<capability>/errors/"
1346
1532
  presence: optional
@@ -1373,7 +1559,9 @@ slots:
1373
1559
  rules: [BE_USER_COPY_LITERAL]
1374
1560
  since: 1.0.0
1375
1561
  - id: be.persistence
1562
+ since: 3.1.0
1376
1563
  profiles: [be]
1564
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): Supabase migrations own lite tables, with SQL and row types here; full keeps TypeORM entities and migrations here."
1377
1565
  path: "src/modules/{domain,platform,projections}/<capability>/persistence/"
1378
1566
  # R178: the platform capabilities whose tables exist on EVERY connection that uses them (the event-bus outbox, the inbox of every service, the job table); their arrays may be
1379
1567
  # registered on several declared connections. Any other capability registers its arrays on exactly one (BE_SCHEMA_OWNER).
@@ -1394,6 +1582,7 @@ slots:
1394
1582
  coverage: none
1395
1583
  rules: [BE_SCHEMA_OWNER, BE_SCHEMA_AUTHORITY, BE_SQL_OUTSIDE_PERSISTENCE, BE_SQL_TABLE_OWNER, BE_SQL_RETURNING_SHAPE, BE_CONTEXT_COUPLING, BE_CONTEXT_PLATFORM_TABLES]
1396
1584
  - id: be.platform
1585
+ since: 3.1.0
1397
1586
  profiles: [be]
1398
1587
  path: "src/modules/platform/<capability>/"
1399
1588
  presence: required
@@ -1416,7 +1605,11 @@ slots:
1416
1605
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE]
1417
1606
  # ----- BE: pattern slots (opt-in; enabled by hfs.json sides.be.patterns). A path under one is HFS_SLOT_NOT_ENABLED unless its pattern is declared.
1418
1607
  - id: be.platform.event-bus
1608
+ since: 3.1.0
1419
1609
  profiles: [be]
1610
+ exemplifiedBy:
1611
+ - {path: tests/fixtures/hfs-lite-slots/be/src/modules/platform/event-bus/, edition: lite}
1612
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite implements the event ports with outbox and pgmq; full uses its Kafka adapter."
1420
1613
  path: "src/modules/platform/event-bus/"
1421
1614
  presence: opt-in
1422
1615
  pattern: event-bus # the ONE durable event transport: transactional outbox, relay, Kafka, inbox, retry and dead letter (knowledge/patterns/be/event-bus.yaml)
@@ -1435,7 +1628,9 @@ slots:
1435
1628
  budget: {indexExports: 60}
1436
1629
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE, BE_INFRA_OWNER]
1437
1630
  - id: be.platform.queue
1631
+ since: 3.1.0
1438
1632
  profiles: [be]
1633
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite implements queue ports with pgmq and pg_cron; full uses BullMQ and Redis."
1439
1634
  path: "src/modules/platform/queue/"
1440
1635
  presence: opt-in
1441
1636
  pattern: queue # the ONE BullMQ owner: the outbox relay into queues (jobId = outbox id) and job schedulers (knowledge/patterns/be/queues.yaml)
@@ -1454,6 +1649,7 @@ slots:
1454
1649
  budget: {indexExports: 60}
1455
1650
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE, BE_INFRA_OWNER]
1456
1651
  - id: be.events
1652
+ since: 3.1.0
1457
1653
  profiles: [be]
1458
1654
  path: "src/modules/events/<service>/"
1459
1655
  presence: opt-in
@@ -1467,6 +1663,7 @@ slots:
1467
1663
  coverage: none
1468
1664
  rules: [BE_EVENT_CLASS_CONTRACT, BE_TIER_DIRECTION]
1469
1665
  - id: be.queues
1666
+ since: 3.1.0
1470
1667
  profiles: [be]
1471
1668
  path: "src/modules/queues/<queue>/"
1472
1669
  presence: opt-in
@@ -1475,11 +1672,12 @@ slots:
1475
1672
  tier: queues
1476
1673
  owner: true
1477
1674
  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]
1675
+ allows: ["<queue>.queue.ts", "<queue>.module.ts", "<queue>.module-definition.ts", "<queue>.options.ts", "<queue>.contracts.ts", index.ts]
1479
1676
  tests: none
1480
1677
  coverage: none
1481
1678
  rules: [BE_OUTBOX_WRITE_TX, BE_TIER_DIRECTION]
1482
1679
  - id: be.feature.reactors
1680
+ since: 3.1.0
1483
1681
  profiles: [be]
1484
1682
  path: "src/features/reactors/<reactor>/"
1485
1683
  presence: opt-in
@@ -1489,11 +1687,12 @@ slots:
1489
1687
  tier: feature
1490
1688
  owner: true
1491
1689
  requires: [index.ts, "<reactor>.module.ts", transport/]
1492
- allows: [index.ts, "<reactor>.module.ts", application/, transport/]
1690
+ allows: [index.ts, "<reactor>.module.ts", "<reactor>.module-definition.ts", "<reactor>.options.ts", application/, transport/]
1493
1691
  tests: none
1494
1692
  coverage: none
1495
1693
  rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED]
1496
1694
  - id: be.feature.jobs
1695
+ since: 3.1.0
1497
1696
  profiles: [be]
1498
1697
  path: "src/features/jobs/<job>/"
1499
1698
  presence: opt-in
@@ -1503,11 +1702,12 @@ slots:
1503
1702
  tier: feature
1504
1703
  owner: true
1505
1704
  requires: [index.ts, "<job>.module.ts", steps/, transport/]
1506
- allows: [index.ts, "<job>.module.ts", application/, steps/, transport/]
1705
+ allows: [index.ts, "<job>.module.ts", "<job>.module-definition.ts", "<job>.options.ts", application/, steps/, transport/]
1507
1706
  tests: none
1508
1707
  coverage: none
1509
1708
  rules: [BE_KIND_ISOLATION, BE_FEATURE_IMPORTS_FEATURE, BE_FEATURE_NOT_COMPOSED]
1510
1709
  - id: be.feature.jobs.queue
1710
+ since: 3.1.0
1511
1711
  profiles: [be]
1512
1712
  path: "src/features/jobs/<job>/transport/queue/"
1513
1713
  presence: opt-in # the queue transport of the job: its module and the processor the job runner hands the fenced claim to
@@ -1515,12 +1715,13 @@ slots:
1515
1715
  tracked: tracked
1516
1716
  tier: feature
1517
1717
  requires: ["<job>-queue.module.ts", "<job>.processor.ts"]
1518
- allows: ["<job>-queue.module.ts", "<job>.processor.ts"]
1718
+ allows: ["<job>-queue.module.ts", "<job>-queue.module-definition.ts", "<job>-queue.options.ts", "<job>.processor.ts"]
1519
1719
  tests: none
1520
1720
  coverage: none
1521
1721
  composedBy: [api, worker]
1522
1722
  rules: [BE_BACKGROUND_UNOWNED]
1523
1723
  - id: be.feature.jobs.steps
1724
+ since: 3.1.0
1524
1725
  profiles: [be]
1525
1726
  path: "src/features/jobs/<job>/steps/"
1526
1727
  presence: opt-in
@@ -1532,7 +1733,9 @@ slots:
1532
1733
  coverage: none
1533
1734
  rules: [BE_JOB_SHAPE, BE_JOB_RUN_KEY]
1534
1735
  - id: be.platform.jobs
1736
+ since: 3.1.0
1535
1737
  profiles: [be]
1738
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite persists fenced claims in Supabase migrations and dispatches through pgmq; full uses TypeORM schema authority and its queue adapter."
1536
1739
  path: "src/modules/platform/jobs/"
1537
1740
  presence: opt-in
1538
1741
  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 +1748,11 @@ slots:
1545
1748
  budget: {indexExports: 60}
1546
1749
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE, BE_JOB_WRITE_OUTSIDE_OWNER, BE_JOB_FENCE_REQUIRED]
1547
1750
  - id: be.projections
1751
+ since: 3.1.0
1548
1752
  profiles: [be]
1753
+ exemplifiedBy:
1754
+ - {path: tests/fixtures/hfs-lite-slots/be/src/modules/projections/order-summary/, edition: lite}
1755
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite projection tables live in Supabase migrations; full keeps the TypeORM projection entity beside its projection."
1549
1756
  path: "src/modules/projections/<name>/"
1550
1757
  presence: opt-in
1551
1758
  pattern: projection # a read model: only its projection writes its table; recompute is idempotent and replayable; readers use get*
@@ -1561,6 +1768,7 @@ slots:
1561
1768
  coverage: required
1562
1769
  rules: [BE_PROJECTION_WRITE_OWNER, BE_PROJECTION_SHAPE, BE_TIER_DIRECTION, BE_CONTEXT_COUPLING]
1563
1770
  - id: be.integrations
1771
+ since: 3.1.0
1564
1772
  profiles: [be]
1565
1773
  path: "src/modules/integrations/<provider>/"
1566
1774
  presence: optional
@@ -1573,6 +1781,7 @@ slots:
1573
1781
  budget: {indexExports: 80} # a provider's port surface includes clients, config, options, tokens and errors
1574
1782
  rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT, BE_INTEGRATION_SPEC_MISSING]
1575
1783
  - id: be.integrations.model
1784
+ since: 3.1.0
1576
1785
  profiles: [be]
1577
1786
  path: "src/modules/integrations/<provider>/model/"
1578
1787
  presence: opt-in
@@ -1581,8 +1790,14 @@ slots:
1581
1790
  tests: none
1582
1791
  coverage: required
1583
1792
  why: an ML or LLM model client is an integration; weights and datasets are never in git
1793
+ rules: [BE_TIER_DIRECTION, BE_INFRA_OWNER, BE_INTEGRATION_SPEC_MISSING]
1794
+
1584
1795
  - id: be.integrations.supabase
1585
1796
  profiles: [be]
1797
+ exemplifiedBy:
1798
+ - packages/hfs/templates/be/skeleton-lite/src/modules/integrations/supabase/
1799
+ - {path: tests/fixtures/hfs-callback-full/be/src/modules/integrations/supabase/, edition: full}
1800
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite requires the Supabase Auth and Storage integration; full opts in through a connection."
1586
1801
  # The only be import of @supabase/* (L10): the Auth admin and Storage admin client, plus the JWKS-based verification of
1587
1802
  # the Supabase access token the guard chain reads (L16). The connection itself is ordinary Postgres (the shared
1588
1803
  # EntityManager of the declared connection); what lives here are the service calls no database role covers.
@@ -1604,7 +1819,11 @@ slots:
1604
1819
  # lives under src/tests/, one folder per kind, and the folder and the file suffix always agree (a file whose suffix does
1605
1820
  # not match its folder matches no slot: HFS_SLOT_UNDECLARED).
1606
1821
  - id: be.tests.world
1822
+ since: 3.1.0
1607
1823
  profiles: [be]
1824
+ exemplifiedBy:
1825
+ - {path: tests/fixtures/hfs-lite-slots/be/src/tests/world/, edition: lite}
1826
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite integration proofs use the local Supabase stack and network fakes; full also boots apps and self-hosted services."
1608
1827
  path: "src/tests/world/"
1609
1828
  presence: optional
1610
1829
  tracked: tracked
@@ -1628,7 +1847,10 @@ slots:
1628
1847
  failNext/replayWebhook/delay), world.waitFor; fakes/<provider>/ holds the fake servers and payload fixtures, kit/ the inlined test helpers, and the world root may hold role-suffixed helpers (<name>.client.ts, <name>.contracts.ts, ...). Nothing is
1629
1848
  overridden in the DI container. It is the only test location that may import typeorm's DataSource or testcontainers,
1630
1849
  call migrate/runMigrations/synchronize, or write process.env.
1850
+ rules: [BE_TEST_TOPOLOGY, BE_TEST_CONSTRAINTS_ON]
1851
+
1631
1852
  - id: be.tests.world.kit
1853
+ since: 3.1.0
1632
1854
  profiles: [be]
1633
1855
  path: "src/tests/world/kit/"
1634
1856
  presence: optional
@@ -1639,6 +1861,7 @@ slots:
1639
1861
  coverage: none
1640
1862
  why: the helpers the test world needs (polling, free ports, transports), inlined so the world imports no other repository; a bare <name>.ts entry in allows is what admits a plain name in BE_SOURCE_FORM
1641
1863
  - id: be.tests.fixtures
1864
+ since: 3.1.0
1642
1865
  profiles: [be]
1643
1866
  path: "src/tests/fixtures/"
1644
1867
  presence: optional
@@ -1649,6 +1872,7 @@ slots:
1649
1872
  coverage: none
1650
1873
  why: payload fixtures with a role suffix (the doubles come from @starci/jest-preset, not from here); the test data builders live in its builders/ slot; importable by unit specs, integration and e2e; never imports a feature
1651
1874
  - id: be.tests.fixtures.builders
1875
+ since: 3.1.0
1652
1876
  profiles: [be]
1653
1877
  path: "src/tests/fixtures/builders/*.builder.ts"
1654
1878
  presence: optional
@@ -1663,6 +1887,7 @@ slots:
1663
1887
  *.fixture.ts or *.factory.ts anywhere, is a BE_SOURCE_FORM finding
1664
1888
  rules: [BE_SOURCE_FORM]
1665
1889
  - id: be.tests.fixtures.i18n
1890
+ since: 3.1.0
1666
1891
  profiles: [be]
1667
1892
  path: "src/tests/fixtures/i18n/"
1668
1893
  presence: optional
@@ -1673,7 +1898,9 @@ slots:
1673
1898
  coverage: none
1674
1899
  why: the ONLY test-fixture location where Vietnamese or another language may appear in source (LANG_NOT_ENGLISH); placement is the marker, there is no comment pragma
1675
1900
  - id: be.tests.integration
1901
+ since: 3.1.0
1676
1902
  profiles: [be]
1903
+ editionWhy: "Lite integration specs use the local Supabase CLI stack without containers or toxiproxy (ruling:R212/case-1, amended by ruling:R212/case-2 (owner:lite-production-swarm) and ruling:R212/case-4, with contracts allowed by owner:lite-contract-specs); R284 requires declared-pattern proofs there. Full additionally proves infrastructure outages through its world (R112)."
1677
1904
  path: "src/tests/integration/<capability>/*.integration-spec.ts"
1678
1905
  presence: optional
1679
1906
  tracked: tracked
@@ -1687,10 +1914,12 @@ slots:
1687
1914
  why: one capability module through useTestWorld({ modules }), no HTTP door of ours (SQL, transactions, concurrency, inbox claims on the real database; and every integration client, `<capability>` being its provider folder, against our real infra, a real peer app or the library fake at the network edge, with its success, its ErrorCode refusal mapping and an outage through the world, R112); run by test:integration
1688
1915
  rules: [BE_TEST_TOPOLOGY, BE_INTEGRATION_SPEC_MISSING]
1689
1916
  - id: be.tests.e2e
1917
+ since: 3.1.0
1690
1918
  profiles: [be]
1919
+ editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): full proves app flows through its e2e world; lite uses the R272 wiring proof and unit/integration specs."
1691
1920
  path: "src/tests/e2e/<area>/*.e2e-spec.ts"
1692
1921
  presence: optional
1693
- litePresence: forbidden # owner ruling 2026-10-12: a lite app has no e2e world
1922
+ litePresence: forbidden # ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite has no e2e world
1694
1923
  tracked: tracked
1695
1924
  tier: e2e
1696
1925
  tests: e2e
@@ -1699,18 +1928,20 @@ slots:
1699
1928
  why: flow e2e through useTestWorld({ apps }); external services are the world's network fakes; run by test:e2e
1700
1929
  rules: [HFS_CI_E2E_AUTOMATIC, BE_TEST_TOPOLOGY, BE_E2E_FLOW]
1701
1930
  - id: be.tests.contract
1931
+ since: 3.1.0
1702
1932
  profiles: [be]
1703
1933
  path: "src/tests/contract/<provider>/*.contract-spec.ts"
1704
1934
  presence: optional
1705
- litePresence: forbidden # owner ruling 2026-10-12: a lite app has no contract world
1935
+ litePresence: optional # owner decision 2026-10-14: lite may run provider sandbox contracts
1706
1936
  tracked: tracked
1707
1937
  tier: e2e
1708
1938
  tests: e2e
1709
- goesTo: "the full edition: a lite app has no contract world (provider sandbox specs)"
1939
+ goesTo: "src/tests/contract/<provider>/*.contract-spec.ts in either edition: optional provider sandbox specs via test:contract; e2e stays full-only"
1710
1940
  coverage: none
1711
1941
  why: provider sandbox contracts, skipped without sandbox config; run only by test:contract, never part of test or test:e2e
1712
1942
  rules: [BE_TEST_TOPOLOGY]
1713
1943
  - id: be.tests.e2e-world-dir
1944
+ since: 3.1.0
1714
1945
  profiles: [be]
1715
1946
  path: "src/tests/e2e/world/"
1716
1947
  presence: forbidden
@@ -1721,6 +1952,7 @@ slots:
1721
1952
 
1722
1953
  # ----- FE: apps ----------------------------------------------------------------------------------------
1723
1954
  - id: fe.app.next
1955
+ since: 3.1.0
1724
1956
  profiles: [fe]
1725
1957
  # Each fe app is an npm workspace: its package.json is @<project>/<app> with its own runtime dependencies and the fixed
1726
1958
  # workspace scripts (HFS_MONO_FE_WORKSPACE); turbo runs them.
@@ -1735,6 +1967,7 @@ slots:
1735
1967
  tests: none
1736
1968
  rules: [FE_APP_ISOLATION, FE_ERROR_BOUNDARY_MISSING, FE_NEXT_CONVENTIONS, HFS_MONO_FE_WORKSPACE, HFS_MONO_WORKSPACE_DEP]
1737
1969
  - id: fe.app-optional
1970
+ since: 3.1.0
1738
1971
  profiles: [fe]
1739
1972
  path: "apps/<app>/public/"
1740
1973
  presence: optional
@@ -1742,12 +1975,13 @@ slots:
1742
1975
  tier: none
1743
1976
  tests: none
1744
1977
  - id: fe.route
1978
+ since: 3.1.0
1745
1979
  profiles: [fe]
1746
1980
  path: "apps/<app>/src/app/"
1747
1981
  presence: required
1748
1982
  tracked: tracked
1749
1983
  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
1984
+ # ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4) (lite = full): routes are thin and the front end serves no API - health/live is the one
1751
1985
  # route handler of this slot, the Supabase auth callback is fe.route.callback, and no file holds a Server Action
1752
1986
  # (FE_ROUTE_HANDLER_FORBIDDEN, FE_SERVER_ACTION_FORBIDDEN).
1753
1987
  allows: ["[locale]/**/{page,layout,template,loading,error,not-found,default}.tsx", "[locale]/providers.tsx",
@@ -1760,6 +1994,7 @@ slots:
1760
1994
  tests: none
1761
1995
  rules: [FE_ROUTE_FILES_THIN, FE_CLIENT_BOUNDARY, FE_OWNER_REACHABLE, FE_ROUTE_HANDLER_FORBIDDEN, FE_SERVER_ACTION_FORBIDDEN]
1762
1996
  - id: fe.source-root-pinned
1997
+ since: 3.1.0
1763
1998
  profiles: [fe]
1764
1999
  path: "apps/<app>/src/{proxy.ts,instrumentation.ts,instrumentation-client.ts,sentry.server.config.ts,sentry.edge.config.ts}"
1765
2000
  presence: optional
@@ -1769,6 +2004,7 @@ slots:
1769
2004
  tests: none
1770
2005
  rules: [FE_NEXT_CONVENTIONS] # middleware.ts is refused on Next >= 16 (fe.source-root-middleware)
1771
2006
  - id: fe.source-root-middleware
2007
+ since: 3.1.0
1772
2008
  profiles: [fe]
1773
2009
  path: "apps/<app>/src/{middleware.ts,middleware.js}"
1774
2010
  presence: forbidden
@@ -1779,6 +2015,8 @@ slots:
1779
2015
  rules: [FE_NEXT_CONVENTIONS]
1780
2016
  - id: fe.route.callback
1781
2017
  profiles: [fe]
2018
+ exemplifiedBy:
2019
+ - {path: tests/fixtures/hfs-callback-full/fe/apps/web/src/app/auth/callback/route.ts, edition: full}
1782
2020
  # The auth callback of a Supabase session (the PKCE code exchange), kept only when the auth flow truly needs a server exchange
1783
2021
  # (prefer the client-side PKCE exchange in fe.modules.auth): the one non-health route handler of an app with a Supabase
1784
2022
  # connection; an app without one has no route handler in fe/ (L15).
@@ -1789,7 +2027,10 @@ slots:
1789
2027
  tier: route
1790
2028
  tests: none
1791
2029
  since: 2.1.0
2030
+ rules: [FE_ROUTE_HANDLER_FORBIDDEN, FE_CALLBACK_SHAPE, FE_SUPABASE_CLIENT_OWNER, FE_SERVICE_ROLE_FORBIDDEN]
2031
+
1792
2032
  - id: fe.feature
2033
+ since: 3.1.0
1793
2034
  profiles: [fe]
1794
2035
  path: "apps/<app>/src/features/{pages,layouts,overlays}/<name>/"
1795
2036
  presence: required
@@ -1804,6 +2045,7 @@ slots:
1804
2045
  budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
1805
2046
  rules: [FE_SIZE_BUDGET, FE_OWNER_REACHABLE, FE_I18N_LITERAL]
1806
2047
  - id: fe.components
2048
+ since: 3.1.0
1807
2049
  profiles: [fe]
1808
2050
  path: "apps/<app>/src/components/{blocks,composites,branches,leaves}/<name>/"
1809
2051
  presence: optional
@@ -1834,6 +2076,7 @@ slots:
1834
2076
  since: 3.0.0
1835
2077
  rules: [FE_CLIENT_BOUNDARY]
1836
2078
  - id: fe.hooks
2079
+ since: 3.1.0
1837
2080
  profiles: [fe]
1838
2081
  path: "apps/<app>/src/hooks/<domain>/"
1839
2082
  presence: optional
@@ -1956,7 +2199,10 @@ slots:
1956
2199
  tests: none
1957
2200
  budget: {file: 400}
1958
2201
  since: 3.0.0
2202
+ rules: [FE_SOURCE_FORM, FE_HOOKS_ARE_HOOKS, FE_OWNER_REACHABLE]
2203
+
1959
2204
  - id: fe.modules
2205
+ since: 3.1.0
1960
2206
  profiles: [fe]
1961
2207
  path: "apps/<app>/src/modules/<capability>/"
1962
2208
  presence: required
@@ -1970,6 +2216,7 @@ slots:
1970
2216
  budget: {file: 400}
1971
2217
  rules: [FE_ENV_OWNER, FE_TRANSPORT_OWNER]
1972
2218
  - id: fe.modules.api
2219
+ since: 3.1.0
1973
2220
  profiles: [fe]
1974
2221
  path: "apps/<app>/src/modules/api/"
1975
2222
  presence: optional
@@ -1989,7 +2236,20 @@ slots:
1989
2236
  entries: ["graphql/queries/query-*.ts", "graphql/queries/types/*.ts", "graphql/mutations/mutation-*.ts", "graphql/mutations/types/*.ts"]
1990
2237
  tests: none
1991
2238
  rules: [FE_TRANSPORT_OWNER, FE_HTTP_STATUS_COLLAPSE, FE_WIRE_GENERATED]
2239
+ - id: fe.transport.generated
2240
+ since: 3.2.0
2241
+ profiles: [fe]
2242
+ path: "apps/<app>/src/modules/api/graphql/__generated__/"
2243
+ presence: optional
2244
+ tracked: ignored
2245
+ tier: transport
2246
+ tests: none
2247
+ why: the canonical GraphQL client output of codegen is local and never committed; this specific slot overrides the tracked API parent
2248
+ rules: [FE_WIRE_GENERATED]
2249
+ exemplifiedBy:
2250
+ - {path: tests/fixtures/hfs-generated-wire/fe/apps/web/src/modules/api/graphql/__generated__/graphql.ts, editions: [lite, full]}
1992
2251
  - id: fe.transport.client
2252
+ since: 3.1.0
1993
2253
  profiles: [fe]
1994
2254
  path: "apps/<app>/src/modules/api/graphql/clients/"
1995
2255
  presence: optional
@@ -1999,6 +2259,7 @@ slots:
1999
2259
  why: the auth and no-auth Apollo clients and their link chain (retry, error, timeout, token refresh, attach access token, http) of the one app that calls the back end; when several apps do, the shared client is fe.package.api.client
2000
2260
  rules: [FE_TRANSPORT_OWNER]
2001
2261
  - id: fe.transport.outcome
2262
+ since: 3.1.0
2002
2263
  profiles: [fe]
2003
2264
  path: "apps/<app>/src/modules/api/graphql/types.ts"
2004
2265
  presence: optional
@@ -2009,6 +2270,7 @@ slots:
2009
2270
  why: the one GraphQLResponse union ({success, message, error, data}) of the one app that calls the back end; when several apps do, the shared union is fe.package.api.outcome
2010
2271
  rules: [FE_HTTP_STATUS_COLLAPSE]
2011
2272
  - id: fe.modules.config
2273
+ since: 3.1.0
2012
2274
  profiles: [fe]
2013
2275
  path: "apps/<app>/src/modules/config/"
2014
2276
  presence: required
@@ -2016,7 +2278,10 @@ slots:
2016
2278
  tier: foundation
2017
2279
  tests: none
2018
2280
  why: the only reader of process.env and NEXT_PUBLIC_*; fails fast, no localhost fallback
2281
+ rules: [FE_ENV_OWNER]
2282
+
2019
2283
  - id: fe.modules.i18n
2284
+ since: 3.1.0
2020
2285
  profiles: [fe]
2021
2286
  path: "apps/<app>/src/modules/i18n/"
2022
2287
  presence: required
@@ -2029,6 +2294,7 @@ slots:
2029
2294
  tests: none
2030
2295
  rules: [FE_I18N_PLACEMENT, FE_I18N_CATALOG]
2031
2296
  - id: fe.modules.brand
2297
+ since: 3.1.0
2032
2298
  profiles: [fe]
2033
2299
  path: "apps/<app>/src/modules/brand/brand.css"
2034
2300
  presence: optional
@@ -2038,6 +2304,7 @@ slots:
2038
2304
  why: the ONLY app file that may hold colour values; sets grammar brand tokens only, light and dark
2039
2305
  rules: [FE_STYLE_TOKEN_ONLY]
2040
2306
  - id: fe.modules.routes
2307
+ since: 3.1.0
2041
2308
  profiles: [fe]
2042
2309
  path: "apps/<app>/src/modules/routes/"
2043
2310
  presence: required
@@ -2045,7 +2312,10 @@ slots:
2045
2312
  tier: foundation
2046
2313
  tests: none
2047
2314
  why: every href builder; FE_HREF_RESOLVES checks each against app/
2315
+ rules: [FE_HREF_RESOLVES, FE_ROUTE_HARDCODED]
2316
+
2048
2317
  - id: fe.modules.types
2318
+ since: 3.1.0
2049
2319
  profiles: [fe]
2050
2320
  path: "apps/<app>/src/modules/types/"
2051
2321
  presence: optional
@@ -2053,6 +2323,8 @@ slots:
2053
2323
  tier: foundation
2054
2324
  tests: none
2055
2325
  why: shared types with no runtime behaviour; a component may import them
2326
+ rules: [FE_TYPE_ESCAPE, HFS_TS_STRICT]
2327
+
2056
2328
  - id: fe.modules.types.ambient
2057
2329
  parent: fe.modules.types
2058
2330
  profiles: [fe]
@@ -2065,6 +2337,8 @@ slots:
2065
2337
  tests: none
2066
2338
  since: 2.2.0
2067
2339
  why: global ambient declarations loaded by the app compiler; this owner exposes no runtime API
2340
+ rules: [FE_TYPE_ESCAPE, HFS_TS_STRICT]
2341
+
2068
2342
  - id: fe.modules.auth
2069
2343
  profiles: [fe]
2070
2344
  # The identity-provider client (fe-stack auth, FE-AUTH): the session, the access token the Apollo link attaches and the
@@ -2076,6 +2350,7 @@ slots:
2076
2350
  tier: modules
2077
2351
  owner: true
2078
2352
  requires: [index.ts]
2353
+ entries: [server.ts] # the server-only PKCE exchange API used by the opted-in callback route
2079
2354
  tests: none
2080
2355
  since: 3.0.0
2081
2356
  rules: [FE_SUPABASE_CLIENT_OWNER, FE_SERVICE_ROLE_FORBIDDEN]
@@ -2093,7 +2368,10 @@ slots:
2093
2368
  roles: {entry: index.ts, hooks: hooks.ts} # hooks.ts: the use*WithToast hooks of the toast module
2094
2369
  tests: none
2095
2370
  since: 3.0.0
2371
+ rules: [FE_I18N_LITERAL, FE_HOOKS_ARE_HOOKS, FE_HTTP_STATUS_COLLAPSE]
2372
+
2096
2373
  - id: fe.package.ui
2374
+ since: 3.1.0
2097
2375
  profiles: [fe]
2098
2376
  path: "packages/<family>-ui/"
2099
2377
  presence: opt-in
@@ -2108,6 +2386,7 @@ slots:
2108
2386
  why: built to dist with explicit named exports; only composites, branches and leaves; no unused export; no colour literal
2109
2387
  rules: [FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT, FE_STYLE_TOKEN_ONLY]
2110
2388
  - id: fe.package.api
2389
+ since: 3.1.0
2111
2390
  profiles: [fe]
2112
2391
  path: "packages/<family>-api/"
2113
2392
  presence: opt-in
@@ -2119,6 +2398,7 @@ slots:
2119
2398
  why: the one transport client and the one Outcome union shared by every app of the repository; no app keeps its own
2120
2399
  rules: [FE_TRANSPORT_OWNER, FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT]
2121
2400
  - id: fe.package.api.client
2401
+ since: 3.1.0
2122
2402
  profiles: [fe]
2123
2403
  path: "packages/<family>-api/src/graphql/clients/"
2124
2404
  presence: optional # enabled with its package (fe.package.api is the opt-in)
@@ -2128,6 +2408,7 @@ slots:
2128
2408
  why: the repository's one pair of Apollo clients and their link chain
2129
2409
  rules: [FE_TRANSPORT_OWNER]
2130
2410
  - id: fe.package.api.outcome
2411
+ since: 3.1.0
2131
2412
  profiles: [fe]
2132
2413
  path: "packages/<family>-api/src/graphql/types.ts"
2133
2414
  presence: optional # enabled with its package (fe.package.api is the opt-in)
@@ -2138,6 +2419,7 @@ slots:
2138
2419
  why: the repository's one GraphQLResponse union
2139
2420
  rules: [FE_HTTP_STATUS_COLLAPSE]
2140
2421
  - id: fe.package.i18n
2422
+ since: 3.1.0
2141
2423
  profiles: [fe]
2142
2424
  path: "packages/<family>-i18n/"
2143
2425
  presence: opt-in