@starci/hfs 1.0.0 → 2.0.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 (246) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +116 -12
  3. package/bin/hfs.mjs +119 -15
  4. package/emit/compiler.mjs +35 -0
  5. package/emit/contracts.mjs +97 -0
  6. package/emit/operations-worker.mjs +24 -0
  7. package/emit/operations.mjs +126 -0
  8. package/emit/schema-worker.mjs +117 -0
  9. package/emit/static-graph.mjs +670 -0
  10. package/emit/type-schema.mjs +145 -0
  11. package/package.json +10 -2
  12. package/report/sonar.mjs +180 -0
  13. package/runtime/engine/admission.mjs +284 -0
  14. package/runtime/engine/digest.mjs +10 -0
  15. package/runtime/engine/ledger-db.mjs +1245 -0
  16. package/runtime/engine/machine-db.mjs +1484 -0
  17. package/runtime/engine/migrations/machine/0001-init.sql +887 -0
  18. package/runtime/engine/migrations/runtime/0001-init.sql +1072 -0
  19. package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +13 -0
  20. package/runtime/engine/migrations/runtime/0004-attempt-why.sql +43 -0
  21. package/runtime/engine/plain-object.mjs +5 -0
  22. package/runtime/knowledge/hfs/canon-pins.yaml +16 -58
  23. package/runtime/knowledge/hfs/slots.yaml +409 -140
  24. package/runtime/knowledge/patterns/fe/folder.yaml +309 -0
  25. package/runtime/knowledge/sonar-gate.yaml +85 -0
  26. package/runtime/modules/kernel/failure-codes.yaml +1480 -16
  27. package/runtime/scripts/checks/architecture/backend.mjs +350 -0
  28. package/runtime/scripts/checks/architecture/background-unowned.mjs +107 -0
  29. package/runtime/scripts/checks/architecture/client-reaches-server.mjs +94 -0
  30. package/runtime/scripts/checks/architecture/clones.mjs +200 -0
  31. package/runtime/scripts/checks/architecture/config-unread.mjs +35 -0
  32. package/runtime/scripts/checks/architecture/config.mjs +310 -0
  33. package/runtime/scripts/checks/architecture/connection-map.mjs +208 -0
  34. package/runtime/scripts/checks/architecture/constructor-deps.mjs +100 -0
  35. package/runtime/scripts/checks/architecture/contract-fixture-guard.mjs +128 -0
  36. package/runtime/scripts/checks/architecture/contracts.mjs +792 -0
  37. package/runtime/scripts/checks/architecture/cross-app-duplicate.mjs +73 -0
  38. package/runtime/scripts/checks/architecture/dead-exports.mjs +265 -0
  39. package/runtime/scripts/checks/architecture/default-deny.mjs +129 -0
  40. package/runtime/scripts/checks/architecture/doc-language.mjs +39 -0
  41. package/runtime/scripts/checks/architecture/entrypoint.mjs +57 -0
  42. package/runtime/scripts/checks/architecture/error-codes.mjs +45 -0
  43. package/runtime/scripts/checks/architecture/error-masked.mjs +60 -0
  44. package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +38 -0
  45. package/runtime/scripts/checks/architecture/feature-shape.mjs +49 -0
  46. package/runtime/scripts/checks/architecture/framework-pinned.mjs +83 -0
  47. package/runtime/scripts/checks/architecture/frontend.mjs +995 -0
  48. package/runtime/scripts/checks/architecture/hfs-graph.mjs +61 -0
  49. package/runtime/scripts/checks/architecture/hfs.mjs +521 -0
  50. package/runtime/scripts/checks/architecture/hooks-are-hooks.mjs +88 -0
  51. package/runtime/scripts/checks/architecture/i18n-keys.mjs +146 -0
  52. package/runtime/scripts/checks/architecture/index.mjs +316 -0
  53. package/runtime/scripts/checks/architecture/injection-token-exported.mjs +62 -0
  54. package/runtime/scripts/checks/architecture/machine-ast.mjs +138 -0
  55. package/runtime/scripts/checks/architecture/module-per-transport.mjs +130 -0
  56. package/runtime/scripts/checks/architecture/next-data.mjs +775 -0
  57. package/runtime/scripts/checks/architecture/owners.mjs +89 -0
  58. package/runtime/scripts/checks/architecture/package-shape.mjs +63 -0
  59. package/runtime/scripts/checks/architecture/reachability.mjs +233 -0
  60. package/runtime/scripts/checks/architecture/register-once.mjs +141 -0
  61. package/runtime/scripts/checks/architecture/registration.mjs +319 -0
  62. package/runtime/scripts/checks/architecture/required-files.mjs +172 -0
  63. package/runtime/scripts/checks/architecture/route-files-thin.mjs +97 -0
  64. package/runtime/scripts/checks/architecture/schema-owner.mjs +261 -0
  65. package/runtime/scripts/checks/architecture/size-growth.mjs +73 -0
  66. package/runtime/scripts/checks/architecture/source-names.mjs +607 -0
  67. package/runtime/scripts/checks/architecture/sql-owner.mjs +142 -0
  68. package/runtime/scripts/checks/architecture/sql-tokens.mjs +327 -0
  69. package/runtime/scripts/checks/architecture/symbols.mjs +193 -0
  70. package/runtime/scripts/checks/architecture/test-world-files.mjs +163 -0
  71. package/runtime/scripts/checks/architecture/tiers.mjs +130 -0
  72. package/runtime/scripts/checks/architecture/transport-owner.mjs +112 -0
  73. package/runtime/scripts/checks/architecture/typescript.mjs +500 -0
  74. package/runtime/scripts/checks/architecture/unit-spec-providers.mjs +122 -0
  75. package/runtime/scripts/checks/architecture.mjs +41 -0
  76. package/runtime/scripts/checks/common.mjs +37 -0
  77. package/runtime/scripts/checks/typescript-programs.mjs +82 -0
  78. package/runtime/scripts/lib/artifact-hold.mjs +89 -0
  79. package/runtime/scripts/lib/artifact-store.mjs +103 -0
  80. package/runtime/scripts/lib/fs-kind.mjs +10 -0
  81. package/runtime/scripts/lib/git.mjs +53 -0
  82. package/runtime/scripts/lib/hfs-allows.mjs +57 -0
  83. package/runtime/scripts/lib/hfs-check.mjs +254 -28
  84. package/runtime/scripts/lib/hfs-rules/contract.mjs +126 -0
  85. package/runtime/scripts/lib/hfs-rules/deps.mjs +63 -0
  86. package/runtime/scripts/lib/hfs-rules/fe-no-tests.mjs +47 -0
  87. package/runtime/scripts/lib/hfs-rules/frontend.mjs +124 -0
  88. package/runtime/scripts/lib/hfs-rules/lint-suppression.mjs +34 -0
  89. package/runtime/scripts/lib/hfs-rules/pipeline.mjs +51 -0
  90. package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +64 -0
  91. package/runtime/scripts/lib/hfs-rules/read.mjs +28 -0
  92. package/runtime/scripts/lib/hfs-rules/repo-local-checks.mjs +32 -0
  93. package/runtime/scripts/lib/hfs-rules/secrets.mjs +54 -0
  94. package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +31 -0
  95. package/runtime/scripts/lib/hfs-rules/stacks.mjs +54 -0
  96. package/runtime/scripts/lib/hfs-rules/test-topology.mjs +31 -0
  97. package/runtime/scripts/lib/hfs-slots.mjs +95 -41
  98. package/runtime/scripts/lib/hfs-tree.mjs +80 -0
  99. package/runtime/scripts/lib/hfs-view.mjs +68 -0
  100. package/runtime/scripts/lib/json.mjs +22 -0
  101. package/runtime/scripts/lib/language.mjs +107 -0
  102. package/runtime/scripts/lib/path-key.mjs +2 -0
  103. package/runtime/scripts/lib/redact.mjs +148 -0
  104. package/runtime/scripts/lib/repo-identity.mjs +50 -0
  105. package/runtime/scripts/lib/safe-remove.mjs +179 -0
  106. package/runtime/scripts/lib/secret-patterns.mjs +44 -0
  107. package/runtime/scripts/lib/sleep-sync.mjs +17 -0
  108. package/runtime/scripts/lib/stack-declaration.mjs +52 -0
  109. package/runtime/scripts/lib/stack-services.mjs +217 -0
  110. package/runtime/scripts/lib/test-secrets.mjs +120 -0
  111. package/scaffold/service.mjs +333 -0
  112. package/sync/format.mjs +46 -0
  113. package/sync/hygiene.mjs +56 -24
  114. package/sync/index.mjs +126 -41
  115. package/sync/managed.mjs +170 -0
  116. package/sync/skeleton.mjs +32 -10
  117. package/sync/sonar-key.mjs +13 -0
  118. package/sync/ts-strict.mjs +48 -0
  119. package/templates/{common → be/ci-workflows/github/workflows}/ci.yml +13 -13
  120. package/templates/be/{e2e.yml → ci-workflows/github/workflows/e2e.yml} +1 -0
  121. package/templates/be/hooks/husky/pre-commit +13 -0
  122. package/templates/be/hooks/husky/pre-push +7 -0
  123. package/templates/be/package-scripts/package.json +21 -0
  124. package/templates/be/{sonar-project.properties → quality-config/sonar-project.properties} +2 -3
  125. package/templates/be/skeleton/apps/__app__/src/app.module.ts +14 -4
  126. package/templates/be/skeleton/apps/__app__/src/main.ts +9 -6
  127. package/templates/be/skeleton/scripts/.gitkeep +0 -0
  128. package/templates/be/skeleton/src/features/system-health/application/check-liveness.contracts.ts +2 -0
  129. package/templates/be/skeleton/src/features/system-health/application/check-liveness.handler.ts +22 -0
  130. package/templates/be/skeleton/src/features/system-health/application/check-liveness.query.ts +11 -0
  131. package/templates/be/skeleton/src/features/system-health/index.ts +1 -1
  132. package/templates/be/skeleton/src/features/system-health/system-health.module.ts +3 -3
  133. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +8 -9
  134. package/templates/be/skeleton/src/features/system-health/transport/http/system-health-http.module.ts +3 -6
  135. package/templates/be/skeleton/src/modules/domain/liveness/index.ts +3 -0
  136. package/templates/be/skeleton/src/modules/domain/liveness/liveness.contracts.ts +9 -0
  137. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +7 -0
  138. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module.ts +14 -0
  139. package/templates/be/skeleton/src/modules/domain/liveness/liveness.options.ts +2 -0
  140. package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.spec.ts +40 -0
  141. package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.ts +26 -0
  142. package/templates/be/skeleton/src/modules/platform/clock/clock.decorators.ts +9 -0
  143. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +7 -0
  144. package/templates/be/skeleton/src/modules/platform/clock/clock.module.ts +19 -0
  145. package/templates/be/skeleton/src/modules/platform/clock/clock.options.ts +2 -0
  146. package/templates/be/skeleton/src/modules/platform/clock/clock.port.ts +5 -0
  147. package/templates/be/skeleton/src/modules/platform/clock/index.ts +4 -0
  148. package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.spec.ts +20 -0
  149. package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.ts +9 -0
  150. package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -0
  151. package/templates/be/skeleton/src/modules/platform/composition/index.ts +2 -0
  152. package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +9 -0
  153. package/templates/be/skeleton/src/modules/platform/config/index.ts +2 -3
  154. package/templates/be/skeleton/src/modules/platform/config/server.config.ts +1 -1
  155. package/templates/be/skeleton/src/modules/platform/config/server.options.ts +0 -3
  156. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +5 -0
  157. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.decorators.ts +9 -0
  158. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.handler.ts +27 -0
  159. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.log-events.ts +5 -0
  160. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +7 -0
  161. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module.ts +18 -0
  162. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.options.ts +2 -0
  163. package/templates/be/skeleton/src/modules/platform/cqrs/index.ts +4 -0
  164. package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +8 -12
  165. package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +5 -0
  166. package/templates/be/skeleton/src/modules/platform/errors/index.ts +1 -1
  167. package/templates/be/skeleton/src/modules/platform/logging/index.ts +4 -4
  168. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +99 -0
  169. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +38 -0
  170. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +9 -0
  171. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +7 -0
  172. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +7 -0
  173. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +23 -10
  174. package/templates/be/skeleton/src/modules/platform/logging/logging.options.ts +2 -0
  175. package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +15 -0
  176. package/templates/be/tool-config/eslint.config.mjs +3 -0
  177. package/templates/be/tool-config/jest.config.js +1 -0
  178. package/templates/be/tool-config/prettierignore +8 -0
  179. package/templates/be/tool-config/prettierrc +1 -0
  180. package/templates/be/tool-config/src/tests/tsconfig.json +5 -0
  181. package/templates/be/tool-config/tsconfig.build.json +5 -0
  182. package/templates/be/tool-config/tsconfig.json +11 -0
  183. package/templates/common/gitignore.base +1 -1
  184. package/templates/fe/ci-workflows/github/workflows/ci.yml +54 -0
  185. package/templates/fe/hooks/husky/pre-commit +16 -0
  186. package/templates/fe/hooks/husky/pre-push +6 -0
  187. package/templates/fe/package-scripts/package.json +17 -0
  188. package/templates/fe/parts/api-client.ts +44 -0
  189. package/templates/fe/parts/api-outcome.ts +7 -0
  190. package/templates/fe/quality-config/sonar-project.properties +8 -0
  191. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +1 -1
  192. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +1 -1
  193. package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +1 -1
  194. package/templates/fe/skeleton/scripts/.gitkeep +0 -0
  195. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +1 -0
  196. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +3 -0
  197. package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +1 -0
  198. package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +4 -0
  199. package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +12 -0
  200. package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +2 -0
  201. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +9 -0
  202. package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +5 -0
  203. package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +5 -0
  204. package/templates/fe/skeleton-shared/packages/__family__-api/package.json +12 -0
  205. package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +1 -0
  206. package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +3 -0
  207. package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +1 -0
  208. package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +5 -0
  209. package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +18 -0
  210. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +19 -0
  211. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +2 -0
  212. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +12 -0
  213. package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +15 -0
  214. package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +5 -0
  215. package/templates/fe/tool-config/eslint.config.mjs +3 -0
  216. package/templates/fe/tool-config/prettierignore +10 -0
  217. package/templates/fe/tool-config/prettierrc +1 -0
  218. package/templates/fe/tool-config/stylelint.config.mjs +3 -0
  219. package/templates/fe/tool-config/tsconfig.json +4 -0
  220. package/templates/be/pre-commit +0 -8
  221. package/templates/be/skeleton/apps/__app__/src/__app__.composition.spec.ts +0 -31
  222. package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.spec.ts +0 -13
  223. package/templates/be/skeleton/src/modules/platform/config/env-source.spec.ts +0 -36
  224. package/templates/be/skeleton/src/modules/platform/config/server.config.spec.ts +0 -19
  225. package/templates/be/skeleton/src/modules/platform/errors/domain-error.spec.ts +0 -15
  226. package/templates/be/skeleton/src/modules/platform/errors/error.filter.spec.ts +0 -38
  227. package/templates/be/skeleton/src/modules/platform/logging/json-logger.spec.ts +0 -33
  228. package/templates/be/skeleton/src/modules/platform/logging/json-logger.ts +0 -35
  229. package/templates/be/skeleton/src/modules/platform/logging/log-id.ts +0 -9
  230. package/templates/be/skeleton/src/modules/platform/logging/logger.port.ts +0 -19
  231. package/templates/common/codecov.yml +0 -13
  232. package/templates/common/pre-push +0 -5
  233. package/templates/fe/e2e.yml +0 -22
  234. package/templates/fe/pre-commit +0 -7
  235. package/templates/fe/skeleton/apps/__app__/src/app/health/live/route.spec.ts +0 -10
  236. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages.spec.ts +0 -27
  237. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.spec.ts +0 -10
  238. package/templates/fe/sonar-project.properties +0 -11
  239. /package/templates/be/skeleton/src/modules/platform/config/{env-source.ts → env-source.config.ts} +0 -0
  240. /package/templates/be/skeleton/src/modules/platform/errors/{domain-error.ts → domain.error.ts} +0 -0
  241. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/next.config.ts +0 -0
  242. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/config.ts +0 -0
  243. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/navigation.ts +0 -0
  244. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/request.ts +0 -0
  245. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/routing.ts +0 -0
  246. /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/proxy.ts +0 -0
@@ -42,9 +42,13 @@ versioning:
42
42
  # requiredWhen connections: required only when hfs.json declares a database connection.
43
43
  # allows / forbids names inside the instance; the loader exposes them, slot-check enforces them.
44
44
  # tests unit-beside | e2e | none.
45
+ # composedBy the app kinds allowed to compose a transport slot (an app of another kind imports none of its modules).
45
46
  # budget size budgets (lines per file, files per slot, exports).
46
47
  # appKind the app kind of hfs.json apps[].kind whose directory this slot describes.
47
- # managedBy template id that hfs sync renders; a difference is HFS_MANAGED_FILE_DRIFT.
48
+ # managedBy the template directory hfs sync renders this slot's files from. The slot path then lists literal files
49
+ # only, and each is rendered from templates/<profile or common>/<managedBy>/<path, leading dots dropped>;
50
+ # the managed file list lives here and nowhere in code. A difference is HFS_MANAGED_FILE_DRIFT (a
51
+ # back end's tsconfig.json: HFS_TS_STRICT when a flag or an option was added).
48
52
  # rules rule ids (the catalog lands with knowledge/hfs/rules.yaml).
49
53
  # since / retiredIn / successor lifecycle.
50
54
  presenceValues: [required, optional, opt-in, forbidden]
@@ -80,7 +84,6 @@ tiers:
80
84
  foundation: {mayImport: [foundation, package], acyclic: true} # modules/{config,i18n,routes,types}
81
85
  modules: {mayImport: [foundation, modules, transport, package], acyclic: true}
82
86
  transport: {mayImport: [foundation, modules, package]} # modules/api: the one fetch
83
- e2e: {mayImport: [e2e, package]} # black box: no app source
84
87
  package: {mayImport: [package]}
85
88
  crossOwner: every import across owners targets the owner's public entry (index.ts or index.tsx); never export *.
86
89
  crossApp: apps never import each other; shared code is a packages/<pkg> slot.
@@ -88,18 +91,70 @@ crossApp: apps never import each other; shared code is a packages/<pkg> slot.
88
91
  # Parameters the lint factories and checks read per profile (through ruleParams(profile) in scripts/lib/hfs-slots.mjs).
89
92
  ruleParams:
90
93
  be:
91
- # BE_MODULE_SHAPE: the only modules that may be @Global(); directories under src/modules/platform/.
92
- globalModules: [src/modules/platform/config/, src/modules/platform/logging/, src/modules/platform/database/]
93
94
  # HFS_SIZE_GROWTH: a file above soft may not grow against its parent commit; a new file stays within soft.
94
95
  fileLines: {soft: 500, hardGrowth: true}
95
- # HFS_DUPLICATE_BLOCK: a token-normalised block of at least this many source lines that appears in two owners.
96
- duplicateBlockLines: 25
96
+ # R21 HFS_DUPLICATE_CODE: the ONE threshold of duplicate code (the architecture machine reads it here; no other file states it):
97
+ # a token-normalised block of at least `lines` source lines and `tokens` tokens that appears twice.
98
+ duplicateBlock: {lines: 8, tokens: 60}
99
+ # R90 BE_INFRA_OWNER: raw infrastructure library (a module specifier or a global reference) -> the platform or
100
+ # integration capabilities allowed to import it; [] means nowhere in a back end. A specifier also covers its subpaths.
101
+ infraOwners:
102
+ fetch: [platform/http]
103
+ axios: [platform/http]
104
+ got: [platform/http]
105
+ undici: [platform/http]
106
+ "node:http": [platform/http]
107
+ "node:https": [platform/http]
108
+ http: [platform/http]
109
+ https: [platform/http]
110
+ cache-manager: [integrations/cache, integrations/redis]
111
+ "@nestjs/cache-manager": [integrations/cache, integrations/redis] # the CACHE_MANAGER token: caching goes through the cache capability (retired local rule must-use-cache-service)
112
+ redis: [integrations/cache, integrations/redis]
113
+ ioredis: [integrations/cache, integrations/redis]
114
+ bullmq: [platform/messaging]
115
+ "@nestjs/bullmq": [platform/messaging]
116
+ kafkajs: [platform/messaging]
117
+ "@nestjs/schedule": [platform/scheduling, platform/retry, platform/messaging, platform/http]
118
+ setInterval: [platform/scheduling, platform/retry, platform/messaging, platform/http]
119
+ setTimeout: [platform/scheduling, platform/retry, platform/messaging, platform/http]
120
+ "node:timers": [platform/scheduling, platform/retry, platform/messaging, platform/http]
121
+ "node:timers/promises": [platform/scheduling, platform/retry, platform/messaging, platform/http]
122
+ winston: [platform/logging]
123
+ dayjs: [platform/clock, platform/primitives]
124
+ moment: [platform/clock, platform/primitives]
125
+ date-fns-tz: [platform/clock, platform/primitives]
126
+ "@nestjs/config": []
127
+ dotenv: []
128
+ "@nestjs/event-emitter": []
129
+ # R48 BE_SPEC_QUALITY spec-infra-double-from-kit: in a `<name>.service.spec.ts` a provider `{ provide: TOKEN, useValue: X }` takes X from
130
+ # the kit named here (the package root of `kit`). The token name is normalised to UPPER_SNAKE (`EntityManager` -> ENTITY_MANAGER), the
131
+ # FIRST entry whose `token` regex matches decides, and `fallback` decides for every other token. `forms` say what X may be:
132
+ # call = `double(...)`, new = `new Double(...)`, curried = `double(...)(...)`, object = a plain object literal (REAL values),
133
+ # primitive = a string/number/boolean literal, array = an array literal (multi-providers of doubles). A const bound to such a value counts.
134
+ specDoubles:
135
+ kit: "@starci/jest-preset"
136
+ doubles:
137
+ - {token: "(?:^|_)OPTIONS$", double: builder, forms: [curried, object]}
138
+ - {token: "(?:^|_)ENTITY_MANAGER$", double: mockEntityManager, forms: [call]}
139
+ - {token: "(?:^|_)TRANSACTION(?:_RUNNER)?$", double: fakeTransaction, forms: [call]}
140
+ - {token: "(?:^|_)CLOCK$", double: FakeClock, forms: [new]}
141
+ - {token: "(?:^|_)OUTBOX(?:_|$)", double: recordingOutbox, forms: [call]}
142
+ - {token: "(?:^|_)CACHE(?:_|$)", double: fakeCache, forms: [call]}
143
+ - {token: "(?:^|_)(?:LOCK|LEASE|FENCE|HOLD)(?:_|$)", double: fakeLock, forms: [call]}
144
+ - {token: "(?:^|_)(?:IDS|ID_GENERATOR|ID_FACTORY)$", double: fakeIds, forms: [call]}
145
+ fallback: {double: mock, forms: [call, primitive, array]}
146
+ # R89 BE_SOURCE_FORM: the closed role-suffix vocabulary of a source file name (<kebab-name>.<suffix>.ts; BE-CONVENTION 1.15)
147
+ # and the suffixes that are never a role. index.ts, main.ts, app.module.ts and the migrations of be.persistence are the
148
+ # only names outside it.
149
+ suffixes: [module, module-definition, decorators, options, config, connection, service, command, query, handler, contracts, port, resolver, controller, gateway, consumer, job, cli, input, type, args, request, response, mapper, policy, entity, sql, rows, error, messages, log-events, cache-keys, guard, interceptor, filter, client, spec, integration-spec, e2e-spec, contract-spec, global-setup, builder]
150
+ # R47 BE_CONTRACT_UNGUARDED: the world's shape vocabulary. `helper` names the function a contract spec imports to erase the values of a JSON
151
+ # payload (`shapeOf`); a contract spec proves a fake's fixtures by asserting helper(real) equals helper(fixture).
152
+ contractShape: {helper: shapeOf}
153
+ bannedSuffixes: [use-case, repository, fixture, factory, store, worker, scheduler, cron, listener, dto, util, utils, helper, helpers, rules, types, constants, int-spec, harness-spec, test]
97
154
  fe:
98
155
  # Slot budgets (component.tsx 300, hooks 200, modules 400) are stricter than this cap where they apply.
99
156
  fileLines: {soft: 500, hardGrowth: true}
100
- # FE_TRANSPORT_OWNER: the one module per app that may call fetch.
101
- clientModule: "apps/<app>/src/modules/api/client.ts"
102
- duplicateBlockLines: 25
157
+ duplicateBlock: {lines: 8, tokens: 60}
103
158
 
104
159
  slots:
105
160
 
@@ -120,14 +175,40 @@ slots:
120
175
  tier: none
121
176
  tests: none
122
177
  rules: [HFS_ARCH_CONFIG_UNREAD]
123
- - id: repo.package-manifest
124
- profiles: [be, fe]
125
- path: "{package.json,package-lock.json}"
178
+ - id: fe.package-manifest
179
+ profiles: [fe]
180
+ path: package.json
181
+ presence: required
182
+ tracked: tracked
183
+ tier: none
184
+ tests: none
185
+ managedBy: package-scripts # the `scripts` block only; the rest of the file is the repository's
186
+ rules: [HFS_PACKAGE_MANAGER_MIXED, HFS_DEP_VERSION_SKEW, HFS_CANON_PIN_DRIFT, HFS_MANAGED_FILE_DRIFT]
187
+ - id: fe.lockfile
188
+ profiles: [fe]
189
+ path: package-lock.json
190
+ presence: required
191
+ tracked: tracked
192
+ tier: none
193
+ tests: none
194
+ rules: [HFS_PACKAGE_MANAGER_MIXED, HFS_DEP_VERSION_SKEW]
195
+ - id: be.package-manifest
196
+ profiles: [be]
197
+ path: package.json
126
198
  presence: required
127
199
  tracked: tracked
128
200
  tier: none
129
201
  tests: none
130
- rules: [HFS_PACKAGE_MANAGER_MIXED, HFS_DEP_VERSION_SKEW, HFS_CANON_PIN_DRIFT]
202
+ managedBy: package-scripts # the `scripts` block only; the rest of the file is the repository's
203
+ rules: [HFS_PACKAGE_MANAGER_MIXED, HFS_DEP_VERSION_SKEW, HFS_CANON_PIN_DRIFT, HFS_MANAGED_FILE_DRIFT]
204
+ - id: be.lockfile
205
+ profiles: [be]
206
+ path: package-lock.json
207
+ presence: required
208
+ tracked: tracked
209
+ tier: none
210
+ tests: none
211
+ rules: [HFS_PACKAGE_MANAGER_MIXED, HFS_DEP_VERSION_SKEW]
131
212
  - id: repo.git-meta
132
213
  profiles: [be, fe]
133
214
  path: "{.gitignore,.gitattributes}"
@@ -135,47 +216,89 @@ slots:
135
216
  tracked: tracked
136
217
  tier: none
137
218
  tests: none
138
- managedBy: gitignore-base
139
- rules: [HFS_GITIGNORE_BLOCK_DRIFT]
219
+ rules: [HFS_GITIGNORE_BLOCK_DRIFT] # the marked block of .gitignore is rendered by hfs sync (templates/<profile>/gitignore)
140
220
  - id: repo.tool-config
141
221
  profiles: [be, fe]
142
- path: "{tsconfig.json,eslint.config.mjs,.editorconfig,.nvmrc,.prettierignore}"
143
- presence: required # each file is a thin extends of a canon package
222
+ path: "{.editorconfig,.nvmrc}"
223
+ presence: required # plain dotfiles, the repository's own
144
224
  tracked: tracked
145
225
  tier: none
146
226
  tests: none
147
- managedBy: tool-config
148
- rules: [HFS_TOOL_CONFIG_LOCAL, HFS_MANAGED_FILE_DRIFT, HFS_TS_STRICT]
149
227
  - id: repo.tool-config-optional
150
228
  profiles: [be, fe]
151
- path: "{tsconfig.build.json,tsconfig.e2e.json,.npmrc,jest.config.e2e.js}"
229
+ path: .npmrc
152
230
  presence: optional
153
231
  tracked: tracked
154
232
  tier: none
155
233
  tests: none
156
- managedBy: tool-config
157
- rules: [HFS_TOOL_CONFIG_LOCAL, HFS_MANAGED_FILE_DRIFT]
158
234
  - id: be.tool-config
159
235
  profiles: [be]
160
- path: "{nest-cli.json,jest.config.js}"
236
+ # Managed files (hfs sync renders them, hfs check compares them): the whole tool configuration of a back end. Each is
237
+ # a reference to a canon package, never a configuration: tsconfig.json extends @starci/tsconfig/be.json and adds the
238
+ # three path aliases and excludes the world, integration, e2e and contract trees (fixtures stay in the root program: unit
239
+ # specs import them); tsconfig.build.json puts an overlay preset after it and adds only the path-relative options a
240
+ # preset cannot hold; the tests' own src/tests/tsconfig.json (the nearest config of a world, integration, e2e or contract
241
+ # file, so typed lint and typecheck:tests both find it) extends it with the e2e preset; eslint.config.mjs is the
242
+ # one-line starciBeConfig call; jest.config.js calls the @starci/jest-preset factory; .prettierrc names
243
+ # @starci/prettier-config; .prettierignore is the template.
244
+ path: "{tsconfig.json,tsconfig.build.json,src/tests/tsconfig.json,eslint.config.mjs,jest.config.js,.prettierrc,.prettierignore}"
161
245
  presence: required
162
246
  tracked: tracked
163
247
  tier: none
164
248
  tests: none
165
249
  managedBy: tool-config
166
- rules: [HFS_TOOL_CONFIG_LOCAL, HFS_MANAGED_FILE_DRIFT]
250
+ rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL, HFS_TS_STRICT]
251
+ - id: be.nest-cli
252
+ profiles: [be]
253
+ path: nest-cli.json
254
+ presence: required
255
+ tracked: tracked
256
+ tier: none
257
+ tests: none
258
+ - id: be.tool-config-local
259
+ profiles: [be]
260
+ 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}"
261
+ presence: forbidden
262
+ tracked: external
263
+ tier: none
264
+ tests: none
265
+ 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"
266
+ rules: [HFS_TOOL_CONFIG_LOCAL]
167
267
  - id: fe.tool-config
168
268
  profiles: [fe]
169
- path: "{vitest.config.ts,vitest.setup.ts,playwright.config.ts,turbo.json}"
170
- presence: optional
269
+ # Managed files (hfs sync renders them, hfs check compares them): the tool configuration of a front end. Each is a
270
+ # reference to a canon package, never a configuration: tsconfig.json extends @starci/tsconfig/next.json and only
271
+ # names the preset and nothing else (each app's own tsconfig.json, in fe.app.next, carries its aliases and include; there is
272
+ # no test tsconfig: a front end has no tests, FE_NO_TESTS); eslint.config.mjs is the one-line starciFeConfig call;
273
+ # stylelint.config.mjs is the one-line starciStylelintConfig call; .prettierrc names @starci/prettier-config;
274
+ # .prettierignore is the template. A front end has no test configuration at all (FE_NO_TESTS).
275
+ path: "{tsconfig.json,eslint.config.mjs,stylelint.config.mjs,.prettierrc,.prettierignore}"
276
+ presence: required
171
277
  tracked: tracked
172
278
  tier: none
173
279
  tests: none
174
280
  managedBy: tool-config
175
- rules: [HFS_TOOL_CONFIG_LOCAL, HFS_MANAGED_FILE_DRIFT]
281
+ rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL, HFS_RULE_OFF_WITHOUT_REPLACEMENT, HFS_TS_STRICT]
282
+ - id: fe.tool-config-repo
283
+ profiles: [fe]
284
+ # The one tool file a preset cannot render because it carries the repository's own facts: turbo.json (its task graph).
285
+ path: turbo.json
286
+ presence: optional
287
+ tracked: tracked
288
+ tier: none
289
+ tests: none
290
+ - id: fe.tool-config-local
291
+ profiles: [fe]
292
+ 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*}"
293
+ presence: forbidden
294
+ tracked: external
295
+ tier: none
296
+ tests: none
297
+ goesTo: "nowhere: a front end has exactly the managed tool configuration (fe.tool-config); a change to a rule or a flag is proposed in the .claude runtime"
298
+ rules: [HFS_TOOL_CONFIG_LOCAL]
176
299
  - id: repo.quality-config
177
300
  profiles: [be, fe]
178
- path: "{sonar-project.properties,codecov.yml}"
301
+ path: sonar-project.properties
179
302
  presence: required
180
303
  tracked: tracked
181
304
  tier: none
@@ -189,7 +312,7 @@ slots:
189
312
  tracked: tracked
190
313
  tier: none
191
314
  tests: none
192
- managedBy: husky
315
+ managedBy: hooks
193
316
  rules: [HFS_CI_E2E_AUTOMATIC, HFS_MANAGED_FILE_DRIFT]
194
317
  - id: repo.ci
195
318
  profiles: [be, fe]
@@ -201,7 +324,7 @@ slots:
201
324
  managedBy: ci-workflows
202
325
  rules: [HFS_CI_E2E_AUTOMATIC, HFS_CI_MISSING_CANON, HFS_MANAGED_FILE_DRIFT]
203
326
  - id: repo.ci-e2e
204
- profiles: [be, fe]
327
+ profiles: [be]
205
328
  path: ".github/workflows/e2e.yml"
206
329
  presence: optional # on: workflow_dispatch only
207
330
  tracked: tracked
@@ -218,12 +341,15 @@ slots:
218
341
  tests: none
219
342
  - id: repo.scripts
220
343
  profiles: [be, fe]
221
- path: "scripts/<name>.mjs"
344
+ # The repository's operational scripts only (`*.mjs`, `*.cjs`, `*.ps1`, `*.sh`), or nothing: an empty folder is `scripts/.gitkeep`, which hfs sync --init writes
345
+ # when the folder is missing. A spec or a test (BE_SPEC_PLACEMENT, FE_NO_TESTS), a check or lint source (`check-*`, `eslint-local-rules*`, a local eslint
346
+ # plugin: HFS_REPO_LOCAL_CHECK) and anything that re-implements a canon check is not an operational script and has no place here.
347
+ path: "scripts/{.gitkeep,*.mjs,*.cjs,*.ps1,*.sh}"
222
348
  presence: optional
223
349
  tracked: tracked
224
350
  tier: none
225
351
  tests: none
226
- rules: [HFS_SCRIPT_ONE_OFF] # one-off codemods and fix-* scripts are agent output, not tooling
352
+ rules: [HFS_SCRIPT_ONE_OFF, BE_SPEC_PLACEMENT, HFS_REPO_LOCAL_CHECK] # one-off codemods and fix-* scripts are agent output, not tooling
227
353
  - id: repo.docs
228
354
  profiles: [be, fe]
229
355
  path: "docs/{adr,runbooks,guides}/**/*.md"
@@ -268,7 +394,7 @@ slots:
268
394
  # ----- never tracked: build output the tools need in place ---------------------------------------------
269
395
  - id: repo.build-output
270
396
  profiles: [be, fe]
271
- path: "{**/node_modules/,dist/,apps/*/dist/,packages/*/dist/,.next/,apps/*/.next/,coverage/,test-results/,playwright-report/,**/next-env.d.ts,**/*.tsbuildinfo}"
397
+ path: "{**/node_modules/,dist/,apps/*/dist/,packages/*/dist/,.next/,apps/*/.next/,coverage/,reports/,test-results/,**/next-env.d.ts,**/*.tsbuildinfo}"
272
398
  presence: optional
273
399
  tracked: ignored
274
400
  tier: none
@@ -334,7 +460,7 @@ slots:
334
460
  tracked: tracked
335
461
  tier: none
336
462
  tests: none
337
- why: emitted by `npm run contract:emit`; CI fails when the emitted file differs (a contract, not a build artefact)
463
+ why: emitted by `npm run contract:emit` (`hfs emit-contracts`, never written by hand); CI fails when the emitted file differs (a contract, not a build artefact)
338
464
  rules: [HFS_CONTRACT_SNAPSHOT_DRIFT]
339
465
  - id: be.contract.openapi
340
466
  profiles: [be]
@@ -343,6 +469,7 @@ slots:
343
469
  tracked: tracked
344
470
  tier: none
345
471
  tests: none
472
+ why: emitted by `npm run contract:emit` (`hfs emit-contracts`, 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
346
473
  rules: [HFS_CONTRACT_SNAPSHOT_DRIFT]
347
474
  - id: fe.contract.copy
348
475
  profiles: [fe]
@@ -364,9 +491,9 @@ slots:
364
491
  tier: app
365
492
  owner: true
366
493
  minInstances: 1
367
- requires: [main.ts, app.module.ts, "<app>.composition.spec.ts"]
368
- allows: [main.ts, app.module.ts, "<app>.composition.spec.ts", "<app>.options.ts"]
369
- tests: unit-beside # the composition spec boots the REAL AppModule with stubbed options
494
+ requires: [main.ts, app.module.ts]
495
+ allows: [main.ts, app.module.ts, "<app>.options.ts", operations.ts]
496
+ tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
370
497
  budget: {app.module.ts: 250, main.ts: 80}
371
498
  rules: [BE_APP_COMPOSITION_ONLY, BE_APP_BUSINESS_ROLE, BE_DEFAULT_DENY, BE_ERROR_MASKED, BE_FEATURE_NOT_COMPOSED]
372
499
  - id: be.app.worker
@@ -377,8 +504,9 @@ slots:
377
504
  tracked: tracked
378
505
  tier: app
379
506
  owner: true
380
- requires: [main.ts, app.module.ts, "<app>.composition.spec.ts"]
381
- tests: unit-beside
507
+ requires: [main.ts, app.module.ts]
508
+ allows: [main.ts, app.module.ts, "<app>.options.ts"]
509
+ tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
382
510
  rules: [BE_APP_COMPOSITION_ONLY, BE_BACKGROUND_UNOWNED]
383
511
  - id: be.app.migrate
384
512
  profiles: [be]
@@ -389,8 +517,9 @@ slots:
389
517
  tracked: tracked
390
518
  tier: app
391
519
  owner: true
392
- requires: [main.ts, "<app>.composition.spec.ts"]
393
- tests: unit-beside
520
+ requires: [main.ts]
521
+ allows: [main.ts, "<app>.options.ts"]
522
+ tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
394
523
  rules: [BE_SCHEMA_AUTHORITY, BE_ENTRYPOINT_ONLY_IN_APPS]
395
524
  - id: be.app.cli
396
525
  profiles: [be]
@@ -400,8 +529,9 @@ slots:
400
529
  tracked: tracked
401
530
  tier: app
402
531
  owner: true
403
- requires: [main.ts, app.module.ts, "<app>.composition.spec.ts"]
404
- tests: unit-beside
532
+ requires: [main.ts, app.module.ts]
533
+ allows: [main.ts, app.module.ts, "<app>.options.ts"]
534
+ tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
405
535
 
406
536
  # ----- BE: features ------------------------------------------------------------------------------------
407
537
  - id: be.feature
@@ -414,7 +544,7 @@ slots:
414
544
  minInstances: 1
415
545
  requires: [index.ts, "<feature>.module.ts", application/]
416
546
  allows: [index.ts, "<feature>.module.ts", application/, transport/, messages/] # nothing else at feature root
417
- tests: unit-beside
547
+ tests: none
418
548
  budget: {files: 250, indexExports: 60} # a feature above 250 source files must be split by product capability
419
549
  rules: [BE_FEATURE_SHAPE, BE_FEATURE_IMPORTS_FEATURE, BE_PUBLIC_SURFACE, BE_FEATURE_NOT_COMPOSED]
420
550
  - id: be.feature.application
@@ -423,19 +553,19 @@ slots:
423
553
  presence: required
424
554
  tracked: tracked
425
555
  tier: feature
426
- allows: ["<action>.use-case.ts", "<action>.contracts.ts", "<action>.{command,query,handler}.ts", "*.spec.ts"]
556
+ allows: ["<action>.{command,query,handler,contracts}.ts"]
427
557
  forbids: [graphql/, http/, message/, schedule/, transport/] # no protocol names under application/
428
- tests: unit-beside
429
- rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_APPLICATION_TRANSPORT_FRAMEWORK, BE_ENTITY_IN_CONTRACT]
558
+ tests: none
559
+ rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_APPLICATION_TRANSPORT_FRAMEWORK, BE_ENTITY_IN_CONTRACT, BE_CQRS_SHAPE]
430
560
  - id: be.feature.application.support
431
561
  profiles: [be]
432
562
  path: "src/features/<feature>/application/support/"
433
563
  presence: optional # helpers and types local to ONE feature; same tier as application, never a shared home
434
564
  tracked: tracked
435
565
  tier: feature # feature tier: another feature cannot import it (features never import features)
436
- allows: ["<name>.ts", "<name>.types.ts", "*.spec.ts"]
566
+ allows: ["<name>.<role>.ts"] # <role> from ruleParams.be.suffixes; no <name>.types.ts
437
567
  forbids: [graphql/, http/, message/, schedule/, transport/]
438
- tests: unit-beside # a <name>.spec.ts beside every <name>.ts
568
+ tests: none
439
569
  rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_APPLICATION_TRANSPORT_FRAMEWORK, BE_FEATURE_IMPORTS_FEATURE]
440
570
  since: 1.0.0
441
571
  - id: be.transport.http
@@ -445,9 +575,10 @@ slots:
445
575
  tracked: tracked
446
576
  tier: feature
447
577
  requires: ["<feature>-http.module.ts"]
448
- allows: ["<action>.controller.ts", "dto/<action>.{request,response}.ts", "<action>.mapper.ts", "*.spec.ts"]
449
- tests: unit-beside
450
- rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED]
578
+ allows: ["<action>.controller.ts", "dto/<action>.{request,response}.ts", "<action>.mapper.ts"]
579
+ tests: none
580
+ composedBy: [api]
581
+ rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED, BE_TRANSPORT_SHAPE]
451
582
  - id: be.transport.graphql
452
583
  profiles: [be]
453
584
  path: "src/features/<feature>/transport/graphql/"
@@ -455,9 +586,10 @@ slots:
455
586
  tracked: tracked
456
587
  tier: feature
457
588
  requires: ["<feature>-graphql.module.ts"]
458
- allows: ["<action>.resolver.ts", "dto/<action>.{input,type,args}.ts", "<action>.mapper.ts", "*.spec.ts"]
459
- tests: unit-beside
460
- rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED]
589
+ allows: ["<action>.resolver.ts", "dto/<action>.{input,type,args}.ts", "<action>.mapper.ts"]
590
+ tests: none
591
+ composedBy: [api]
592
+ rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED, BE_TRANSPORT_SHAPE]
461
593
  - id: be.transport.message
462
594
  profiles: [be]
463
595
  path: "src/features/<feature>/transport/message/"
@@ -465,8 +597,9 @@ slots:
465
597
  tracked: tracked
466
598
  tier: feature
467
599
  requires: ["<feature>-message.module.ts"]
468
- allows: ["<event>.consumer.ts", "<event>.message.ts", "*.spec.ts"]
469
- tests: unit-beside
600
+ allows: ["<event>.consumer.ts"] # BE-CONVENTION 1.6: transport/message holds only consumers; a .message.ts is outside the closed suffix list
601
+ tests: none
602
+ composedBy: [worker]
470
603
  rules: [BE_BACKGROUND_UNOWNED]
471
604
  - id: be.transport.schedule
472
605
  profiles: [be]
@@ -475,8 +608,9 @@ slots:
475
608
  tracked: tracked
476
609
  tier: feature
477
610
  requires: ["<feature>-schedule.module.ts"]
478
- allows: ["<job>.job.ts", "*.spec.ts"]
479
- tests: unit-beside
611
+ allows: ["<job>.job.ts"]
612
+ tests: none
613
+ composedBy: [worker]
480
614
  rules: [BE_BACKGROUND_UNOWNED]
481
615
  - id: be.transport.websocket
482
616
  profiles: [be]
@@ -485,18 +619,20 @@ slots:
485
619
  tracked: tracked
486
620
  tier: feature
487
621
  requires: ["<feature>-websocket.module.ts"]
488
- allows: ["<channel>.gateway.ts", "dto/*.ts", "*.spec.ts"]
489
- tests: unit-beside
622
+ allows: ["<channel>.gateway.ts", "dto/*.ts"]
623
+ tests: none
624
+ composedBy: [api]
490
625
  since: 1.0.0
491
626
  - id: be.feature.transport.cli
492
627
  profiles: [be]
493
628
  path: "src/features/<feature>/transport/cli/"
494
- presence: opt-in # a command line entry: calls application use cases only, composed by an app of kind cli
629
+ presence: opt-in # a command line entry: dispatches one command or query, composed by an app of kind cli
495
630
  tracked: tracked
496
631
  tier: feature
497
632
  requires: ["<feature>-cli.module.ts"]
498
- allows: ["<command>.command.ts", "dto/*.ts", "*.spec.ts"]
499
- tests: unit-beside
633
+ allows: ["<name>.cli.ts", "dto/*.ts"] # .cli.ts: .command.ts is a CQRS message (HFS delta 3)
634
+ tests: none
635
+ composedBy: [cli]
500
636
  rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_FEATURE_NOT_COMPOSED, BE_ENTRYPOINT_ONLY_IN_APPS]
501
637
  since: 1.0.0
502
638
 
@@ -509,9 +645,9 @@ slots:
509
645
  tier: domain
510
646
  owner: true
511
647
  requires: [index.ts]
512
- allows: ["<capability>.module.ts", "<capability>.config.ts", "<capability>.options.ts", errors/, messages/, persistence/, policies/, "*.service.ts", "*.contracts.ts", "*.spec.ts"]
648
+ allows: ["<capability>.module.ts", "<capability>.config.ts", "<capability>.options.ts", errors/, messages/, persistence/, policies/, "*.service.ts", "*.service.spec.ts", "*.contracts.ts"]
513
649
  forbids: [testing/, exceptions/, utils/, helpers/, shared/]
514
- tests: unit-beside
650
+ tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts, nothing else
515
651
  budget: {indexExports: 60}
516
652
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_PUBLIC_SURFACE, BE_ERROR_HOME, BE_CONFIG_OWNER]
517
653
  - id: be.errors
@@ -520,18 +656,17 @@ slots:
520
656
  presence: optional
521
657
  tracked: tracked
522
658
  tier: inherit
523
- allows: ["<capability>.error.ts", "<name>.error.ts", "*.spec.ts"] # one family per capability, extends platform/errors DomainError
524
- tests: unit-beside
659
+ allows: ["<capability>.error.ts", "<name>.error.ts"] # one family per capability, extends platform/errors DomainError
660
+ tests: none
525
661
  rules: [BE_ERROR_HOME]
526
662
  - id: be.domain.messages
527
663
  profiles: [be]
528
- path: "src/modules/{domain,integrations}/<capability>/messages/"
529
- presence: optional # a capability that raises no user-facing text carries none
664
+ path: "src/modules/{domain,platform,integrations}/<capability>/messages/"
665
+ presence: optional # a capability (any tier) that raises no user-facing text carries none
530
666
  tracked: tracked
531
667
  tier: inherit
532
- requires: [index.ts]
533
- allows: ["<capability>.messages.ts", "*.spec.ts"] # one keyed catalog per capability, vi and en, read through the platform/i18n MessageCatalog port
534
- tests: unit-beside
668
+ allows: ["<capability>.messages.ts"] # one keyed catalog per capability, vi and en, read through the platform/i18n MessageCatalog port
669
+ tests: none
535
670
  rules: [BE_USER_COPY_LITERAL]
536
671
  since: 1.0.0
537
672
  - id: be.feature.messages
@@ -540,9 +675,8 @@ slots:
540
675
  presence: optional
541
676
  tracked: tracked
542
677
  tier: feature
543
- requires: [index.ts]
544
- allows: ["<feature>.messages.ts", "*.spec.ts"] # one keyed catalog per feature, vi and en, read through the platform/i18n MessageCatalog port
545
- tests: unit-beside
678
+ allows: ["<feature>.messages.ts"] # one keyed catalog per feature, vi and en, read through the platform/i18n MessageCatalog port
679
+ tests: none
546
680
  rules: [BE_USER_COPY_LITERAL]
547
681
  since: 1.0.0
548
682
  - id: be.persistence
@@ -551,10 +685,11 @@ slots:
551
685
  presence: optional
552
686
  tracked: tracked
553
687
  tier: inherit
554
- requires: [connection.ts] # export const CONNECTION = "<name from hfs.json connections>"
555
- allows: ["entities/<table>.entity.ts", "migrations/<yyyyMMddHHmmss>-<kebab-name>.ts", "<name>.repository.ts", index.ts, "*.spec.ts"]
556
- tests: unit-beside
557
- rules: [BE_SCHEMA_OWNER, BE_SCHEMA_AUTHORITY, BE_SQL_OUTSIDE_REPOSITORY]
688
+ requires: [connection.ts] # the capability's <c>Entities / <c>Migrations arrays (the owner index re-exports the two arrays by name); no CONNECTION alias: the connection is the one the apps register the arrays on
689
+ allows: ["entities/<table>.entity.ts", "migrations/<epochMs13>-<kebab-name>.ts", "<name>.sql.ts", "<name>.rows.ts", connection.ts]
690
+ forbids: [index.ts, "<name>.repository.ts"]
691
+ tests: none
692
+ rules: [BE_SCHEMA_OWNER, BE_SCHEMA_AUTHORITY, BE_SQL_OUTSIDE_PERSISTENCE]
558
693
  - id: be.platform
559
694
  profiles: [be]
560
695
  path: "src/modules/platform/<capability>/"
@@ -572,7 +707,8 @@ slots:
572
707
  requiredInstances: {capability: [config, logging, errors, primitives, clock, i18n]}
573
708
  requires: [index.ts]
574
709
  forbids: [common/, utils/, shared/, testing/] # primitives live in platform/primitives
575
- tests: unit-beside
710
+ tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts, nothing else
711
+ budget: {indexExports: 60}
576
712
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE]
577
713
  - id: be.integrations
578
714
  profiles: [be]
@@ -582,7 +718,8 @@ slots:
582
718
  tier: integrations
583
719
  owner: true
584
720
  requires: [index.ts, "<provider>.config.ts"]
585
- tests: unit-beside
721
+ tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts, nothing else
722
+ budget: {indexExports: 60}
586
723
  rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT]
587
724
  - id: be.integrations.model
588
725
  profiles: [be]
@@ -590,42 +727,104 @@ slots:
590
727
  presence: opt-in
591
728
  tracked: tracked
592
729
  tier: integrations
593
- tests: unit-beside
730
+ tests: none
594
731
  why: an ML or LLM model client is an integration; weights and datasets are never in git
595
732
 
596
- # ----- BE: tests ---------------------------------------------------------------------------------------
597
- - id: be.tests.e2e
733
+ # ----- BE: tests (owner test layout 2026-09-30). Unit specs are <name>.service.spec.ts beside their service and nothing else; everything else
734
+ # lives under src/tests/, one folder per kind, and the folder and the file suffix always agree (a file whose suffix does
735
+ # not match its folder matches no slot: HFS_SLOT_UNDECLARED).
736
+ - id: be.tests.world
598
737
  profiles: [be]
599
- path: "src/tests/e2e/<area>/*.e2e-spec.ts"
738
+ path: "src/tests/world/"
600
739
  presence: optional
601
740
  tracked: tracked
602
741
  tier: e2e
603
- tests: e2e
604
- rules: [HFS_CI_E2E_AUTOMATIC, BE_TEST_TOPOLOGY, BE_E2E_FLOW]
605
- - id: be.tests.e2e-live
606
- profiles: [be]
607
- path: "src/tests/e2e/live/<area>/*.e2e-spec.ts"
608
- presence: opt-in
609
- tracked: tracked
610
- tier: e2e
611
- tests: e2e
612
- why: run only by test:e2e:live (E2E_LIVE=1); excluded from plain test:e2e by folder, never by file name
613
- - id: be.tests.e2e-setup
742
+ allows: [global-setup.ts, global-teardown.ts, use-test-world.ts, fakes/] # jest globalSetup/globalTeardown default-export (Jest API); plus root files with a role suffix (test-world-files, R47)
743
+ tests: none
744
+ why: >-
745
+ the ONLY test infrastructure location, shared by integration and e2e: global-setup.ts starts the shared
746
+ infrastructure (containers) and runs apps/migrate's exported bootstrap once; use-test-world.ts exports
747
+ useTestWorld({ apps: { <name>: { module, listen? } } } | { modules: [...] }) -> world.apps.<name>.api,
748
+ world.db.<connection> (the shared EntityManager), world.infra.<service> (latency/cut/restore on a REAL service of the repository's
749
+ own stack, `.starcistacks/<env>`, each behind toxiproxy), world.fake.<provider> (a network-edge fake of an external SaaS started by the world, with
750
+ 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
751
+ overridden in the DI container. It is the only test location that may import typeorm's DataSource or testcontainers,
752
+ call migrate/runMigrations/synchronize, or write process.env.
753
+ - id: be.tests.world.kit
614
754
  profiles: [be]
615
- path: "src/tests/e2e/setup/"
755
+ path: "src/tests/world/kit/"
616
756
  presence: optional
617
757
  tracked: tracked
618
758
  tier: e2e
759
+ allows: ["<name>.ts"] # plain kebab names, like platform/primitives (BE-CONVENTION 1.11 names primitives/time.ts): the world's inlined test helpers (poll, free-ports)
619
760
  tests: none
620
- why: boots the REAL AppModule and overrides providers; no per-lane hand-assembled modules
761
+ 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
621
762
  - id: be.tests.fixtures
622
763
  profiles: [be]
623
764
  path: "src/tests/fixtures/"
624
765
  presence: optional
625
766
  tracked: tracked
626
767
  tier: fixtures
627
- tests: unit-beside
628
- why: typed data builders and typed mock<T>() doubles; importable by unit specs and e2e; never imports a feature
768
+ allows: [i18n/] # i18n/ is the one folder that may carry localized text
769
+ tests: none
770
+ 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
771
+ - id: be.tests.fixtures.builders
772
+ profiles: [be]
773
+ path: "src/tests/fixtures/builders/*.builder.ts"
774
+ presence: optional
775
+ tracked: tracked
776
+ tier: fixtures
777
+ tests: none
778
+ why: >-
779
+ the ONLY home of test data builders, one <area>.builder.ts per area, shared by every spec of that area: pure object builders
780
+ (commissionRow(overrides), accrueInput(overrides)) for unit specs and persisting builders (orderBuilder(world.db.primary).pending().build(overrides))
781
+ for integration and e2e; arrange only, constraints on, deterministic defaults. A .builder.ts anywhere else, and a *.repository.ts,
782
+ *.fixture.ts or *.factory.ts anywhere, is a BE_SOURCE_FORM finding
783
+ rules: [BE_SOURCE_FORM]
784
+ - id: be.tests.fixtures.i18n
785
+ profiles: [be]
786
+ path: "src/tests/fixtures/i18n/"
787
+ presence: optional
788
+ tracked: tracked
789
+ tier: fixtures
790
+ allows: ["<name>.<role>.ts"] # data files that deliberately carry localized text (a real Vietnamese string a parser or formatter must accept); no specs here
791
+ tests: none
792
+ 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
793
+ - id: be.tests.integration
794
+ profiles: [be]
795
+ path: "src/tests/integration/<capability>/*.integration-spec.ts"
796
+ presence: optional
797
+ tracked: tracked
798
+ tier: e2e
799
+ tests: e2e
800
+ why: one capability module on the real database, no HTTP (SQL, transactions, concurrency, inbox claims) through useTestWorld({ modules }); run by test:integration
801
+ rules: [BE_TEST_TOPOLOGY]
802
+ - id: be.tests.e2e
803
+ profiles: [be]
804
+ path: "src/tests/e2e/<area>/*.e2e-spec.ts"
805
+ presence: optional
806
+ tracked: tracked
807
+ tier: e2e
808
+ tests: e2e
809
+ why: flow e2e through useTestWorld({ apps }); external services are the world's network fakes; run by test:e2e
810
+ rules: [HFS_CI_E2E_AUTOMATIC, BE_TEST_TOPOLOGY, BE_E2E_FLOW]
811
+ - id: be.tests.contract
812
+ profiles: [be]
813
+ path: "src/tests/contract/<provider>/*.contract-spec.ts"
814
+ presence: optional
815
+ tracked: tracked
816
+ tier: e2e
817
+ tests: e2e
818
+ why: provider sandbox contracts, skipped without sandbox config; run only by test:contract, never part of test or test:e2e
819
+ rules: [BE_TEST_TOPOLOGY]
820
+ - id: be.tests.e2e-world-retired
821
+ profiles: [be]
822
+ path: "src/tests/e2e/world/"
823
+ presence: forbidden
824
+ tracked: external
825
+ tier: none
826
+ tests: none
827
+ goesTo: "src/tests/world/ (the only test infrastructure location; e2e/<area>/ holds only *.e2e-spec.ts)"
629
828
 
630
829
  # ----- BE: work and stacks -----------------------------------------------------------------------------
631
830
  - id: be.starciwork
@@ -650,7 +849,7 @@ slots:
650
849
  tier: none
651
850
  tests: none
652
851
  requires: [application-stacks.yaml]
653
- allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,k8s,terraform}/**",
852
+ allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,k8s,terraform}/**", "<env>/infra/metadata.json",
654
853
  "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/seeds/**"]
655
854
  forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
656
855
  rules: [HFS_STACKS_SHAPE, HFS_PLAINTEXT_SECRET, HFS_IDENTITY_CUSTODY]
@@ -661,7 +860,6 @@ slots:
661
860
  tracked: tracked
662
861
  tier: none
663
862
  tests: none
664
- managedBy: sops-policy
665
863
 
666
864
  # ----- FE: apps ----------------------------------------------------------------------------------------
667
865
  - id: fe.app.next
@@ -672,13 +870,13 @@ slots:
672
870
  tracked: tracked
673
871
  tier: none
674
872
  minInstances: 1
675
- requires: [package.json, next.config.ts, tsconfig.json, postcss.config.mjs, src/app/, src/modules/i18n/, src/modules/api/,
873
+ requires: [package.json, next.config.ts, tsconfig.json, postcss.config.mjs, src/app/, src/modules/i18n/,
676
874
  "src/app/global-error.tsx", "src/app/[locale]/layout.tsx", "src/app/[locale]/error.tsx", "src/app/[locale]/not-found.tsx", "src/app/[locale]/loading.tsx"]
677
875
  tests: none
678
876
  rules: [FE_APP_ISOLATION, FE_ERROR_BOUNDARY_MISSING, FE_NEXT_CONVENTIONS]
679
877
  - id: fe.app-optional
680
878
  profiles: [fe]
681
- path: "apps/<app>/{vitest.config.ts,public/}"
879
+ path: "apps/<app>/public/"
682
880
  presence: optional
683
881
  tracked: tracked
684
882
  tier: none
@@ -689,8 +887,13 @@ slots:
689
887
  presence: required
690
888
  tracked: tracked
691
889
  tier: route
692
- allows: ["[locale]/**/{page,layout,template,loading,error,not-found}.tsx", "[locale]/**/route.ts", global-error.tsx, "page.tsx (redirect only)",
693
- "health/{live,ready}/route.ts", globals.css, "icon.*", robots.ts, sitemap.ts]
890
+ allows: ["[locale]/**/{page,layout,template,loading,error,not-found,default}.tsx", "[locale]/**/route.ts", "[locale]/providers.tsx",
891
+ global-error.tsx, page.tsx, # the root page.tsx only redirects (FE_ROUTE_FILES_THIN)
892
+ not-found.tsx, layout.tsx, # next-intl: a request outside every locale renders the root not-found inside a root layout
893
+ "health/{live,ready}/route.ts", "api/**/route.ts", "_*/**", providers.tsx, globals.css,
894
+ "{icon,favicon,apple-icon,opengraph-image,twitter-image}.*", robots.ts, sitemap.ts, manifest.ts]
895
+ # providers.tsx (root or [locale]) is the client wrapper the server layout renders around its children to host context providers.
896
+ roles: {page: page.tsx, layout: layout.tsx, template: template.tsx, loading: loading.tsx, error: error.tsx, not-found: not-found.tsx, default: default.tsx, route: route.ts, global-error: global-error.tsx, providers: providers.tsx}
694
897
  tests: none
695
898
  rules: [FE_ROUTE_FILES_THIN, FE_CLIENT_BOUNDARY, FE_OWNER_REACHABLE]
696
899
  - id: fe.source-root-pinned
@@ -699,8 +902,18 @@ slots:
699
902
  presence: optional
700
903
  tracked: tracked
701
904
  tier: route
905
+ roles: {proxy: proxy.ts, instrumentation: instrumentation.ts, instrumentation-client: instrumentation-client.ts}
906
+ tests: none
907
+ rules: [FE_NEXT_CONVENTIONS] # middleware.ts is refused on Next >= 16 (fe.source-root-retired)
908
+ - id: fe.source-root-retired
909
+ profiles: [fe]
910
+ path: "apps/<app>/src/{middleware.ts,middleware.js}"
911
+ presence: forbidden
912
+ tracked: external
913
+ tier: none
702
914
  tests: none
703
- rules: [FE_NEXT_CONVENTIONS] # middleware.ts is refused on Next >= 16
915
+ goesTo: "apps/<app>/src/proxy.ts: Next >= 16 renamed middleware to proxy and refuses the old name"
916
+ rules: [FE_NEXT_CONVENTIONS]
704
917
  - id: fe.feature
705
918
  profiles: [fe]
706
919
  path: "apps/<app>/src/features/{pages,layouts,overlays}/<name>/"
@@ -708,10 +921,12 @@ slots:
708
921
  tracked: tracked
709
922
  tier: feature
710
923
  owner: true
924
+ kinds: [pages, layouts, overlays]
711
925
  requires: [index.tsx]
712
- allows: [index.tsx, component.tsx, classNames.ts, "*.spec.tsx", "*.spec.ts"]
713
- tests: unit-beside
714
- budget: {component.tsx: 300, index.tsx: 200}
926
+ allows: [index.tsx, component.tsx, classNames.ts]
927
+ roles: {entry: index.tsx, drawing: component.tsx, styles: classNames.ts} # the connected entry, the pure drawing half and the styling declarations of a component owner
928
+ tests: none
929
+ budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
715
930
  rules: [FE_SIZE_BUDGET, FE_OWNER_REACHABLE, FE_I18N_LITERAL]
716
931
  - id: fe.components
717
932
  profiles: [fe]
@@ -722,8 +937,9 @@ slots:
722
937
  owner: true
723
938
  layers: [blocks, composites, branches, leaves] # a layer imports only the layers after it
724
939
  requires: [index.tsx]
725
- allows: [index.tsx, component.tsx, classNames.ts, "*.spec.tsx", "*.spec.ts"]
726
- tests: unit-beside
940
+ allows: [index.tsx, component.tsx, classNames.ts]
941
+ roles: {entry: index.tsx, drawing: component.tsx, styles: classNames.ts} # the connected entry, the pure drawing half and the styling declarations of a component owner
942
+ tests: none
727
943
  budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
728
944
  rules: [FE_CONNECTED_BLOCK_RENDER_PAIR, FE_I18N_LITERAL, FE_NATIVE_FORM_CONTROL, FE_CLIENT_BOUNDARY]
729
945
  - id: fe.hooks
@@ -734,9 +950,10 @@ slots:
734
950
  tier: hooks
735
951
  owner: true # a domain is an owner: other domains import it through index.ts only
736
952
  requires: [index.ts]
737
- allows: [index.ts, "use<name>.ts", "<domain>.shared.ts", "*.spec.ts"] # React hooks only; <domain>.shared.ts = sanctioned non-hook helpers
738
- tests: unit-beside
739
- budget: {file: 200}
953
+ allows: [index.ts, "use<name>.ts", "<domain>.shared.ts"] # React hooks only; <domain>.shared.ts = sanctioned non-hook helpers
954
+ roles: {entry: index.ts, shared: "<domain>.shared.ts"}
955
+ tests: none
956
+ budget: {file: 200, dataHooks: 6, useState: 6}
740
957
  rules: [FE_HOOKS_ARE_HOOKS, FE_DATA_FRESHNESS]
741
958
  - id: fe.modules
742
959
  profiles: [fe]
@@ -745,28 +962,50 @@ slots:
745
962
  tracked: tracked
746
963
  tier: modules
747
964
  owner: true
748
- requiredInstances: {capability: [api, config, i18n, routes]}
965
+ requiredInstances: {capability: [config, i18n, routes]}
749
966
  requires: [index.ts]
750
- tests: unit-beside
967
+ tests: none
751
968
  budget: {file: 400}
752
969
  rules: [FE_ENV_OWNER, FE_TRANSPORT_OWNER]
753
970
  - id: fe.modules.api
754
971
  profiles: [fe]
755
972
  path: "apps/<app>/src/modules/api/"
756
- presence: required
973
+ presence: optional
757
974
  tracked: tracked
758
975
  tier: transport
759
- requires: [index.ts, client.ts, outcome.ts] # client.ts is the only fetch; outcome.ts the only result vocabulary
760
- allows: [client.ts, outcome.ts, "<domain>/read-*.ts", "<domain>/*.graphql", "<domain>/<domain>.mapper.ts", contract/, __generated__/, "*.spec.ts"]
761
- tests: unit-beside
976
+ # The app's reads of the one client. A repository has exactly ONE transport client and ONE Outcome union: the app's
977
+ # client.ts/outcome.ts (fe.transport.client/outcome) in a one-app repository, or the api package's
978
+ # (fe.package.api.client/outcome) when the repository shares it (FE_TRANSPORT_OWNER, machine).
979
+ requires: [index.ts]
980
+ allows: [index.ts, client.ts, outcome.ts, "<domain>/read-*.ts", "<domain>/*.graphql", "<domain>/<domain>.mapper.ts", contract/, __generated__/]
981
+ roles: {entry: index.ts, reader: "read-*.ts"} # a reader is a server module (FE_CLIENT_REACHES_SERVER)
982
+ tests: none
762
983
  rules: [FE_TRANSPORT_OWNER, FE_HTTP_STATUS_COLLAPSE, FE_WIRE_GENERATED]
984
+ - id: fe.transport.client
985
+ profiles: [fe]
986
+ path: "apps/<app>/src/modules/api/client.ts"
987
+ presence: optional
988
+ tracked: tracked
989
+ tier: transport
990
+ tests: none
991
+ why: the one fetch of a one-app repository (timeout, abort, 401/403 -> refused); a shared client is fe.package.api.client
992
+ rules: [FE_TRANSPORT_OWNER]
993
+ - id: fe.transport.outcome
994
+ profiles: [fe]
995
+ path: "apps/<app>/src/modules/api/outcome.ts"
996
+ presence: optional
997
+ tracked: tracked
998
+ tier: transport
999
+ tests: none
1000
+ why: the one Outcome union of a one-app repository; a shared union is fe.package.api.outcome
1001
+ rules: [FE_HTTP_STATUS_COLLAPSE]
763
1002
  - id: fe.modules.config
764
1003
  profiles: [fe]
765
1004
  path: "apps/<app>/src/modules/config/"
766
1005
  presence: required
767
1006
  tracked: tracked
768
1007
  tier: foundation
769
- tests: unit-beside
1008
+ tests: none
770
1009
  why: the only reader of process.env and NEXT_PUBLIC_*; fails fast, no localhost fallback
771
1010
  - id: fe.modules.i18n
772
1011
  profiles: [fe]
@@ -774,8 +1013,11 @@ slots:
774
1013
  presence: required
775
1014
  tracked: tracked
776
1015
  tier: foundation
777
- requires: [config.ts, routing.ts, navigation.ts, request.ts, messages/]
778
- tests: unit-beside
1016
+ # The app's i18n module: its catalogs and the one call of the repository's i18n factory. The next-intl stack (routing,
1017
+ # navigation, request config) is written once per repository: in the i18n package (fe.package.i18n, `createAppI18n`)
1018
+ # when the repository has one, else in the only app's module.
1019
+ requires: [index.ts]
1020
+ tests: none
779
1021
  rules: [FE_I18N_PLACEMENT, FE_I18N_CATALOG]
780
1022
  - id: fe.modules.brand
781
1023
  profiles: [fe]
@@ -792,7 +1034,7 @@ slots:
792
1034
  presence: required
793
1035
  tracked: tracked
794
1036
  tier: foundation
795
- tests: unit-beside
1037
+ tests: none
796
1038
  why: every href builder; FE_HREF_RESOLVES checks each against app/
797
1039
  - id: fe.modules.types
798
1040
  profiles: [fe]
@@ -800,7 +1042,7 @@ slots:
800
1042
  presence: optional
801
1043
  tracked: tracked
802
1044
  tier: foundation
803
- tests: unit-beside
1045
+ tests: none
804
1046
  why: shared types with no runtime behaviour; a component may import them
805
1047
  - id: fe.package.ui
806
1048
  profiles: [fe]
@@ -809,26 +1051,53 @@ slots:
809
1051
  tracked: tracked
810
1052
  tier: package
811
1053
  owner: true
1054
+ kinds: [composites, branches, leaves] # the layer folders under src/ (a package owns no blocks)
1055
+ roles: {entry: index.tsx, drawing: component.tsx, styles: classNames.ts}
812
1056
  requires: [package.json, src/index.ts, tsconfig.json]
813
- tests: unit-beside
1057
+ tests: none
1058
+ budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
814
1059
  why: built to dist with explicit named exports; only composites, branches and leaves; no unused export; no colour literal
815
1060
  rules: [FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT, FE_STYLE_TOKEN_ONLY]
816
- - id: fe.e2e
1061
+ - id: fe.package.api
817
1062
  profiles: [fe]
818
- path: "e2e/<area>/*.e2e-spec.ts"
819
- presence: optional
1063
+ path: "packages/<family>-api/"
1064
+ presence: opt-in
820
1065
  tracked: tracked
821
- tier: e2e
822
- tests: e2e
823
- requires: [/playwright.config.ts, /tsconfig.e2e.json]
824
- rules: [FE_E2E_SHAPE, HFS_CI_E2E_AUTOMATIC]
825
- - id: fe.e2e-support
1066
+ tier: package
1067
+ owner: true
1068
+ requires: [package.json, src/index.ts, src/client.ts, src/outcome.ts, tsconfig.json]
1069
+ tests: none
1070
+ why: the one transport client and the one Outcome union shared by every app of the repository; no app keeps its own
1071
+ rules: [FE_TRANSPORT_OWNER, FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT]
1072
+ - id: fe.package.api.client
826
1073
  profiles: [fe]
827
- path: "e2e/{support,fixtures}/"
828
- presence: optional
1074
+ path: "packages/<family>-api/src/client.ts"
1075
+ presence: optional # enabled with its package (fe.package.api is the opt-in)
829
1076
  tracked: tracked
830
- tier: e2e
1077
+ tier: package
1078
+ tests: none
1079
+ why: the repository's one fetch (timeout, abort, 401/403 -> refused)
1080
+ rules: [FE_TRANSPORT_OWNER]
1081
+ - id: fe.package.api.outcome
1082
+ profiles: [fe]
1083
+ path: "packages/<family>-api/src/outcome.ts"
1084
+ presence: optional # enabled with its package (fe.package.api is the opt-in)
1085
+ tracked: tracked
1086
+ tier: package
1087
+ tests: none
1088
+ why: the repository's one Outcome union
1089
+ rules: [FE_HTTP_STATUS_COLLAPSE]
1090
+ - id: fe.package.i18n
1091
+ profiles: [fe]
1092
+ path: "packages/<family>-i18n/"
1093
+ presence: opt-in
1094
+ tracked: tracked
1095
+ tier: package
1096
+ owner: true
1097
+ requires: [package.json, src/index.ts, tsconfig.json]
831
1098
  tests: none
1099
+ why: the next-intl stack (routing, navigation, request config) written once, exported as the createAppI18n factory every app calls
1100
+ rules: [FE_I18N_PLACEMENT, FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT]
832
1101
 
833
1102
  # Checks that read this manifest (all ship from .claude; none keeps its own path list)
834
1103
  consumers:
@@ -838,5 +1107,5 @@ consumers:
838
1107
  - packages/eslint/be starciBeConfig({hfs}) # file globs for rules come from slots
839
1108
  - packages/eslint/fe starciFeConfig({hfs})
840
1109
  - packages/stylelint-canon # colour and brand allowances from fe.modules.brand
841
- - scripts/hfs/sync.mjs # renders managedBy templates
1110
+ - packages/hfs/sync # renders managedBy templates (hfs sync) and judges them (hfs check)
842
1111
  - modules/kernel/failure-codes.yaml # every rule id listed above has a Vietnamese why