@starci/eslint-canon-fe 5.1.2 → 6.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 (68) hide show
  1. package/CHANGELOG.md +78 -2
  2. package/README.md +17 -18
  3. package/brand-values.mjs +1 -3
  4. package/class-names.mjs +9 -9
  5. package/client-boundary.mjs +125 -37
  6. package/comments.mjs +14 -21
  7. package/docs/client-boundary.md +26 -1
  8. package/docs/grammar-boundary.md +33 -0
  9. package/docs/hygiene.md +15 -4
  10. package/docs/lint-escape-hatch.md +3 -1
  11. package/docs/next-conventions.md +26 -0
  12. package/docs/size-growth.md +29 -0
  13. package/docs/spec-quality.md +15 -7
  14. package/docs/status-colors.md +32 -0
  15. package/docs/translation.md +4 -0
  16. package/docs/transport.md +36 -9
  17. package/e2e-shape.mjs +20 -23
  18. package/env-owner.mjs +1 -1
  19. package/file-layout.mjs +105 -103
  20. package/formatting.mjs +2 -4
  21. package/grammar-boundary.mjs +85 -9
  22. package/hooks-folder.mjs +17 -16
  23. package/hygiene.mjs +370 -92
  24. package/icon.mjs +21 -20
  25. package/index.mjs +27 -85
  26. package/landmark.mjs +0 -11
  27. package/lib/bindings.mjs +144 -0
  28. package/lib/config.mjs +79 -0
  29. package/lib/hfs.mjs +48 -0
  30. package/lib/next.mjs +6 -0
  31. package/lib/platform.mjs +66 -0
  32. package/lib/react.mjs +28 -0
  33. package/lib/scope.mjs +119 -19
  34. package/lib/status-tones.generated.mjs +9 -0
  35. package/lib/types.mjs +63 -0
  36. package/lib/why.mjs +41 -11
  37. package/lint-escape-hatch.mjs +17 -15
  38. package/loading.mjs +9 -16
  39. package/naming.mjs +8 -2
  40. package/next-conventions.mjs +109 -25
  41. package/package.json +9 -4
  42. package/props-and-slots.mjs +19 -13
  43. package/runtime/engine/plain-object.mjs +5 -0
  44. package/runtime/engine/runtime-root.mjs +32 -0
  45. package/runtime/engine/yaml.mjs +161 -0
  46. package/runtime/knowledge/hfs/slots.yaml +1072 -0
  47. package/runtime/scripts/lib/git.mjs +53 -0
  48. package/runtime/scripts/lib/glob.mjs +23 -0
  49. package/runtime/scripts/lib/hfs-allows.mjs +42 -0
  50. package/runtime/scripts/lib/hfs-slots.mjs +722 -0
  51. package/runtime/scripts/lib/hfs-view.mjs +68 -0
  52. package/runtime/scripts/lib/next-contract.mjs +14 -0
  53. package/runtime/scripts/lib/path-key.mjs +17 -0
  54. package/runtime/scripts/lib/recorded-lines.mjs +27 -0
  55. package/served-locale.mjs +30 -16
  56. package/shape-slot.mjs +22 -20
  57. package/size-and-state-budget.mjs +41 -23
  58. package/size-growth.mjs +58 -0
  59. package/spec-quality.mjs +95 -16
  60. package/status-colors.mjs +139 -0
  61. package/the-split.mjs +13 -13
  62. package/tokens.mjs +26 -34
  63. package/translation.mjs +52 -19
  64. package/transport.mjs +437 -89
  65. package/type-safety.mjs +8 -17
  66. package/typography.mjs +7 -11
  67. package/vendor-boundary.mjs +5 -27
  68. package/lint-adoption.mjs +0 -79
@@ -0,0 +1,1072 @@
1
+ # HFS slot manifest (owner-approved 2026-09-29, decisions 1-10 in the owner decisions record).
2
+ # The one place that says what may exist in a StarCi product repository and where. Every tracked path of every
3
+ # repository must match exactly one slot. Checks, lint factories, the architecture machine, templates and the why
4
+ # catalog READ this file through scripts/lib/hfs-slots.mjs; none of them hardcodes a path.
5
+ # Shape: modules/schemas/hfs-slots.schema.yaml. The repository side is hfs.json: modules/schemas/hfs-repo.schema.yaml.
6
+ schema: starci/hfs-slots@1
7
+ version: 1.0.0
8
+
9
+ versioning:
10
+ # MAJOR.MINOR.PATCH of this manifest. A repository pins only the MAJOR in hfs.json ("hfs": 1).
11
+ patch: wording, why text, examples; no check result can change.
12
+ minor: >-
13
+ add a slot (always presence optional or opt-in), add an app kind or protocol slot, add a rule at severity warn,
14
+ tighten a budget that no pinned repo currently exceeds. A minor never changes an existing slot's path,
15
+ presence, imports or tests. Every repo pinned to the same MAJOR stays green without edits.
16
+ major: >-
17
+ change or remove an existing slot, make an optional slot required, promote a rule warn to error, change the
18
+ direction matrix. Needs owner approval and a migration lane per repo. There is no compatibility window: the old
19
+ MAJOR is refused the day the new one lands (owner ruling: no backward compatibility).
20
+ retire: >-
21
+ a slot is never edited in place; it gets retiredIn (a major) and a successor slot id. After that major a
22
+ path matching a retired slot is HFS_SLOT_RETIRED (error).
23
+ pins: knowledge/hfs/canon-pins.yaml carries the exact versions of every @starci/* package and framework this MAJOR supports.
24
+
25
+ # Field meaning, per slot.
26
+ # id stable name, never changes meaning inside a major.
27
+ # profiles be, fe, or both: the repository profiles the slot exists in.
28
+ # path pattern. <name> is one path segment (bound to the variable name), * and ? stay inside a segment,
29
+ # ** spans directories, {a,b} alternates. A pattern ending in / is a directory: it owns everything
30
+ # below it, except what a more specific slot owns. Otherwise it names files.
31
+ # presence required | optional | opt-in (legal only when hfs.json lists it or declares an app of its kind) | forbidden.
32
+ # A slot is optional in the sense of the requirements list when its presence is not required.
33
+ # tracked tracked (committed) | ignored (may exist, must be gitignored) | external (must not exist in the tree;
34
+ # goesTo says where it lives).
35
+ # tier node of the import direction matrix (tiers below); none = takes no part in import checks;
36
+ # inherit = the tier of the enclosing owner.
37
+ # owner true when every directory matching the slot is an owner: the unit cross-owner imports and cycles use.
38
+ # requires files or directories that must exist inside each instance; <var> is replaced by the bound name,
39
+ # a leading / means repository root; a trailing / means directory.
40
+ # requiredInstances variable -> names that must exist as instances (config, logging ... of platform).
41
+ # minInstances at least this many instances (one feature, one api app).
42
+ # requiredWhen connections: required only when hfs.json declares a database connection.
43
+ # allows / forbids names inside the instance; the loader exposes them, slot-check enforces them.
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).
46
+ # budget size budgets (lines per file, files per slot, exports).
47
+ # appKind the app kind of hfs.json apps[].kind whose directory this slot describes.
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).
52
+ # rules rule ids (the catalog lands with knowledge/hfs/rules.yaml).
53
+ # since / retiredIn / successor lifecycle.
54
+ presenceValues: [required, optional, opt-in, forbidden]
55
+ trackedValues: [tracked, ignored, external]
56
+ testValues: [unit-beside, e2e, none]
57
+
58
+ appKinds:
59
+ be: [api, worker, migrate, cli]
60
+ fe: [next]
61
+
62
+ # ---------------------------------------------------------------------------------------------------------
63
+ # Tiers and the dependency direction matrix (owner granularity; type-only imports count; no cycles anywhere)
64
+ # mayImport lists the tiers a file of that tier may import. Importing inside the same owner is always allowed.
65
+ # ---------------------------------------------------------------------------------------------------------
66
+ tiers:
67
+ be:
68
+ app: {mayImport: [feature, domain, platform, integrations, package]}
69
+ feature: {mayImport: [domain, platform, integrations, package]} # never another feature
70
+ domain: {mayImport: [domain, platform, integrations, package], acyclic: true}
71
+ integrations: {mayImport: [platform, package]}
72
+ platform: {mayImport: [platform, package], acyclic: true}
73
+ e2e: {mayImport: [e2e, app, feature, domain, platform, integrations, fixtures, package]}
74
+ fixtures: {mayImport: [domain, platform, integrations, package]} # public contracts only
75
+ package: {mayImport: [package]}
76
+ fe:
77
+ route: {mayImport: [feature, foundation, modules, transport, package]} # app/** adapters
78
+ feature: {mayImport: [components, hooks, foundation, modules, transport, package]}
79
+ # A component never reaches data: config, routes, i18n and types are foundation; every other module and the api
80
+ # transport are reached through a hook (owner decision 2026-09-29).
81
+ components: {mayImport: [components, hooks, foundation, package], lowerLayerOnly: true} # blocks, composites, branches, leaves
82
+ # Across domains a hook is imported only through the other domain's public index (hooks/<domain>/index.ts), never deep.
83
+ hooks: {mayImport: [hooks, foundation, modules, transport, package]}
84
+ foundation: {mayImport: [foundation, package], acyclic: true} # modules/{config,i18n,routes,types}
85
+ modules: {mayImport: [foundation, modules, transport, package], acyclic: true}
86
+ transport: {mayImport: [foundation, modules, package]} # modules/api: the one fetch
87
+ e2e: {mayImport: [e2e, package]} # black box: no app source
88
+ package: {mayImport: [package]}
89
+ crossOwner: every import across owners targets the owner's public entry (index.ts or index.tsx); never export *.
90
+ crossApp: apps never import each other; shared code is a packages/<pkg> slot.
91
+
92
+ # Parameters the lint factories and checks read per profile (through ruleParams(profile) in scripts/lib/hfs-slots.mjs).
93
+ ruleParams:
94
+ be:
95
+ # HFS_SIZE_GROWTH: a file above soft may not grow against its parent commit; a new file stays within soft.
96
+ fileLines: {soft: 500, hardGrowth: true}
97
+ # R21 HFS_DUPLICATE_CODE: the ONE threshold of duplicate code (the architecture machine reads it here; no other file states it):
98
+ # a token-normalised block of at least `lines` source lines and `tokens` tokens that appears twice.
99
+ duplicateBlock: {lines: 8, tokens: 60}
100
+ # R90 BE_INFRA_OWNER: raw infrastructure library (a module specifier or a global reference) -> the platform or
101
+ # integration capabilities allowed to import it; [] means nowhere in a back end. A specifier also covers its subpaths.
102
+ infraOwners:
103
+ fetch: [platform/http]
104
+ axios: [platform/http]
105
+ got: [platform/http]
106
+ undici: [platform/http]
107
+ "node:http": [platform/http]
108
+ "node:https": [platform/http]
109
+ http: [platform/http]
110
+ https: [platform/http]
111
+ cache-manager: [integrations/cache, integrations/redis]
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
+ # R89 BE_SOURCE_FORM: the closed role-suffix vocabulary of a source file name (<kebab-name>.<suffix>.ts; BE-CONVENTION 1.15)
130
+ # and the suffixes that are never a role. index.ts, main.ts, app.module.ts and the migrations of be.persistence are the
131
+ # only names outside it.
132
+ 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]
133
+ bannedSuffixes: [use-case, repository, store, worker, scheduler, cron, listener, dto, util, utils, helper, helpers, rules, types, constants, int-spec, harness-spec, test]
134
+ fe:
135
+ # Slot budgets (component.tsx 300, hooks 200, modules 400) are stricter than this cap where they apply.
136
+ fileLines: {soft: 500, hardGrowth: true}
137
+ duplicateBlock: {lines: 8, tokens: 60}
138
+
139
+ slots:
140
+
141
+ # ----- repository root (both profiles) ---------------------------------------------------------------
142
+ - id: repo.readme
143
+ profiles: [be, fe]
144
+ path: README.md
145
+ presence: required
146
+ tracked: tracked
147
+ tier: none
148
+ tests: none
149
+ rules: [HFS_README_*]
150
+ - id: repo.declaration
151
+ profiles: [be, fe]
152
+ path: hfs.json
153
+ presence: required
154
+ tracked: tracked
155
+ tier: none
156
+ tests: none
157
+ rules: [HFS_ARCH_CONFIG_UNREAD]
158
+ - id: fe.package-manifest
159
+ profiles: [fe]
160
+ path: package.json
161
+ presence: required
162
+ tracked: tracked
163
+ tier: none
164
+ tests: none
165
+ managedBy: package-scripts # the `scripts` block only; the rest of the file is the repository's
166
+ rules: [HFS_PACKAGE_MANAGER_MIXED, HFS_DEP_VERSION_SKEW, HFS_CANON_PIN_DRIFT, HFS_MANAGED_FILE_DRIFT]
167
+ - id: fe.lockfile
168
+ profiles: [fe]
169
+ path: package-lock.json
170
+ presence: required
171
+ tracked: tracked
172
+ tier: none
173
+ tests: none
174
+ rules: [HFS_PACKAGE_MANAGER_MIXED, HFS_DEP_VERSION_SKEW]
175
+ - id: be.package-manifest
176
+ profiles: [be]
177
+ path: package.json
178
+ presence: required
179
+ tracked: tracked
180
+ tier: none
181
+ tests: none
182
+ managedBy: package-scripts # the `scripts` block only; the rest of the file is the repository's
183
+ rules: [HFS_PACKAGE_MANAGER_MIXED, HFS_DEP_VERSION_SKEW, HFS_CANON_PIN_DRIFT, HFS_MANAGED_FILE_DRIFT]
184
+ - id: be.lockfile
185
+ profiles: [be]
186
+ path: package-lock.json
187
+ presence: required
188
+ tracked: tracked
189
+ tier: none
190
+ tests: none
191
+ rules: [HFS_PACKAGE_MANAGER_MIXED, HFS_DEP_VERSION_SKEW]
192
+ - id: repo.git-meta
193
+ profiles: [be, fe]
194
+ path: "{.gitignore,.gitattributes}"
195
+ presence: required
196
+ tracked: tracked
197
+ tier: none
198
+ tests: none
199
+ rules: [HFS_GITIGNORE_BLOCK_DRIFT] # the marked block of .gitignore is rendered by hfs sync (templates/<profile>/gitignore)
200
+ - id: repo.tool-config
201
+ profiles: [be, fe]
202
+ path: "{.editorconfig,.nvmrc}"
203
+ presence: required # plain dotfiles, the repository's own
204
+ tracked: tracked
205
+ tier: none
206
+ tests: none
207
+ - id: repo.tool-config-optional
208
+ profiles: [be, fe]
209
+ path: .npmrc
210
+ presence: optional
211
+ tracked: tracked
212
+ tier: none
213
+ tests: none
214
+ - id: be.tool-config
215
+ profiles: [be]
216
+ # Managed files (hfs sync renders them, hfs check compares them): the whole tool configuration of a back end. Each is
217
+ # a reference to a canon package, never a configuration: tsconfig.json extends @starci/tsconfig/be.json and adds the
218
+ # three path aliases and excludes the world, integration, e2e and contract trees (fixtures stay in the root program: unit
219
+ # specs import them); tsconfig.build.json puts an overlay preset after it and adds only the path-relative options a
220
+ # preset cannot hold; the tests' own src/tests/tsconfig.json (the nearest config of a world, integration, e2e or contract
221
+ # file, so typed lint and typecheck:tests both find it) extends it with the e2e preset; eslint.config.mjs is the
222
+ # one-line starciBeConfig call; jest.config.js calls the @starci/jest-preset factory; .prettierrc names
223
+ # @starci/prettier-config; .prettierignore is the template.
224
+ path: "{tsconfig.json,tsconfig.build.json,src/tests/tsconfig.json,eslint.config.mjs,jest.config.js,.prettierrc,.prettierignore}"
225
+ presence: required
226
+ tracked: tracked
227
+ tier: none
228
+ tests: none
229
+ managedBy: tool-config
230
+ rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL, HFS_TS_STRICT]
231
+ - id: be.nest-cli
232
+ profiles: [be]
233
+ path: nest-cli.json
234
+ presence: required
235
+ tracked: tracked
236
+ tier: none
237
+ tests: none
238
+ - id: be.tool-config-local
239
+ profiles: [be]
240
+ 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}"
241
+ presence: forbidden
242
+ tracked: external
243
+ tier: none
244
+ tests: none
245
+ 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"
246
+ rules: [HFS_TOOL_CONFIG_LOCAL]
247
+ - id: fe.tool-config
248
+ profiles: [fe]
249
+ # Managed files (hfs sync renders them, hfs check compares them): the tool configuration of a front end. Each is a
250
+ # reference to a canon package, never a configuration: tsconfig.json extends @starci/tsconfig/next.json and only
251
+ # excludes the e2e tree (each app's own tsconfig.json, in fe.app.next, carries its aliases and include);
252
+ # tsconfig.e2e.json extends the e2e preset over e2e/ and playwright.config.ts; eslint.config.mjs is the one-line
253
+ # starciFeConfig call (it lints apps, packages and e2e/ in one run, so there is no separate e2e lint); stylelint.config.mjs
254
+ # is the one-line starciStylelintConfig call; vitest.config.ts is the one-line starciVitestWorkspace call;
255
+ # .prettierrc names @starci/prettier-config; .prettierignore is the template.
256
+ path: "{tsconfig.json,tsconfig.e2e.json,eslint.config.mjs,stylelint.config.mjs,vitest.config.ts,.prettierrc,.prettierignore}"
257
+ presence: required
258
+ tracked: tracked
259
+ tier: none
260
+ tests: none
261
+ managedBy: tool-config
262
+ rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL, HFS_RULE_OFF_WITHOUT_REPLACEMENT, HFS_TS_STRICT]
263
+ - id: fe.tool-config-repo
264
+ profiles: [fe]
265
+ # The three tool files a preset cannot render because they carry the repository's own facts: vitest.setup.ts (its
266
+ # setup), playwright.config.ts (its web servers, ports and projects: @starci/playwright-preset returns the shared
267
+ # defaults and the repository adds what only it knows) and turbo.json (its task graph).
268
+ path: "{vitest.setup.ts,playwright.config.ts,turbo.json}"
269
+ roles: {playwright: playwright.config.ts}
270
+ presence: optional
271
+ tracked: tracked
272
+ tier: none
273
+ tests: none
274
+ - id: fe.tool-config-local
275
+ profiles: [fe]
276
+ 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.*,vitest.config.js,vitest.config.mjs,vitest.config.cjs,vitest.workspace.*,jest.config.*,lint-staged.config.*,.lintstagedrc*}"
277
+ presence: forbidden
278
+ tracked: external
279
+ tier: none
280
+ tests: none
281
+ 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"
282
+ rules: [HFS_TOOL_CONFIG_LOCAL]
283
+ - id: repo.quality-config
284
+ profiles: [be, fe]
285
+ path: "{sonar-project.properties,codecov.yml}"
286
+ presence: required
287
+ tracked: tracked
288
+ tier: none
289
+ tests: none
290
+ managedBy: quality-config
291
+ rules: [HFS_SONAR_CONFIG, HFS_MANAGED_FILE_DRIFT]
292
+ - id: repo.hooks
293
+ profiles: [be, fe]
294
+ path: ".husky/{pre-commit,pre-push}"
295
+ presence: required
296
+ tracked: tracked
297
+ tier: none
298
+ tests: none
299
+ managedBy: hooks
300
+ rules: [HFS_CI_E2E_AUTOMATIC, HFS_MANAGED_FILE_DRIFT]
301
+ - id: repo.ci
302
+ profiles: [be, fe]
303
+ path: ".github/workflows/ci.yml"
304
+ presence: required
305
+ tracked: tracked
306
+ tier: none
307
+ tests: none
308
+ managedBy: ci-workflows
309
+ rules: [HFS_CI_E2E_AUTOMATIC, HFS_CI_MISSING_CANON, HFS_MANAGED_FILE_DRIFT]
310
+ - id: repo.ci-e2e
311
+ profiles: [be, fe]
312
+ path: ".github/workflows/e2e.yml"
313
+ presence: optional # on: workflow_dispatch only
314
+ tracked: tracked
315
+ tier: none
316
+ tests: none
317
+ managedBy: ci-workflows
318
+ rules: [HFS_CI_E2E_AUTOMATIC, HFS_MANAGED_FILE_DRIFT]
319
+ - id: repo.github-meta
320
+ profiles: [be, fe]
321
+ path: ".github/{CODEOWNERS,pull_request_template.md,ISSUE_TEMPLATE/**,dependabot.yml}"
322
+ presence: optional
323
+ tracked: tracked
324
+ tier: none
325
+ tests: none
326
+ - id: repo.scripts
327
+ profiles: [be, fe]
328
+ path: "scripts/<name>.mjs"
329
+ presence: optional
330
+ tracked: tracked
331
+ tier: none
332
+ tests: none
333
+ rules: [HFS_SCRIPT_ONE_OFF] # one-off codemods and fix-* scripts are agent output, not tooling
334
+ - id: repo.docs
335
+ profiles: [be, fe]
336
+ path: "docs/{adr,runbooks,guides}/**/*.md"
337
+ presence: opt-in
338
+ tracked: tracked
339
+ tier: none
340
+ tests: none
341
+ rules: [HFS_ROOT_MARKDOWN_FORBIDDEN]
342
+ - id: repo.docs-media
343
+ profiles: [be, fe]
344
+ path: "docs/**/*.{png,svg,webp}"
345
+ presence: opt-in
346
+ tracked: tracked
347
+ tier: none
348
+ tests: none
349
+ budget: {bytes: 512000}
350
+ - id: repo.packages
351
+ profiles: [be, fe]
352
+ path: "packages/<pkg>/"
353
+ presence: opt-in
354
+ tracked: tracked
355
+ tier: package
356
+ owner: true
357
+ requires: [package.json, src/index.ts, tsconfig.json]
358
+ tests: unit-beside
359
+ rules: [FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT]
360
+ - id: repo.app-image
361
+ profiles: [be, fe]
362
+ path: "apps/<app>/Dockerfile"
363
+ presence: optional
364
+ tracked: tracked
365
+ tier: none
366
+ tests: none
367
+ - id: repo.dockerignore
368
+ profiles: [be, fe]
369
+ path: .dockerignore
370
+ presence: optional
371
+ tracked: tracked
372
+ tier: none
373
+ tests: none
374
+
375
+ # ----- never tracked: build output the tools need in place ---------------------------------------------
376
+ - id: repo.build-output
377
+ profiles: [be, fe]
378
+ path: "{**/node_modules/,dist/,apps/*/dist/,packages/*/dist/,.next/,apps/*/.next/,coverage/,reports/,test-results/,playwright-report/,**/next-env.d.ts,**/*.tsbuildinfo}"
379
+ presence: optional
380
+ tracked: ignored
381
+ tier: none
382
+ tests: none
383
+ - id: repo.generated
384
+ profiles: [be, fe]
385
+ path: "{**/__generated__/,apps/*/src/schema.gql}"
386
+ presence: optional
387
+ tracked: ignored
388
+ tier: none
389
+ tests: none
390
+ why: produced by `npm run codegen` (prebuild and pretypecheck); never committed
391
+
392
+ # ----- never in the working tree: caches, scratch, agent output ----------------------------------------
393
+ - id: repo.tool-cache
394
+ profiles: [be, fe]
395
+ path: "{.eslintcache,.turbo/,.scannerwork/,.sonar/,.jest-cache/,.cache/,.tools/}"
396
+ presence: forbidden
397
+ tracked: external
398
+ tier: none
399
+ tests: none
400
+ goesTo: "${STARCI_CACHE_HOME:-%LOCALAPPDATA%/StarCi/cache}/<repo>/<tool>/ (the canon presets set eslint --cache-location, jest cacheDirectory, turbo cacheDir, sonar.working.directory)"
401
+ - id: repo.agent-output
402
+ profiles: [be, fe]
403
+ path: "{report*.json,*.tmp.json,%*%,nul,.qwen*,.artifacts/,.tmp-*,*-lint.json,lf.json,lt.json,*.log,design-plans/,.gitmounts/,.dat,vi-flat.txt}"
404
+ presence: forbidden
405
+ tracked: external
406
+ tier: none
407
+ tests: none
408
+ goesTo: "agent scratchpad or the StarCi blob store (%LOCALAPPDATA%/StarCi/blobs), cited by {name, sha256}"
409
+ rules: [HFS_UNTRACKED_ROOT_ENTRY, HFS_AGENT_DATA_TRACKED]
410
+ - id: repo.worktrees
411
+ profiles: [be, fe]
412
+ path: "{.worktrees/,worktrees/,.starciwork/worktrees/}"
413
+ presence: forbidden
414
+ tracked: external
415
+ tier: none
416
+ tests: none
417
+ goesTo: "D:/starci-lanes/<project>/<lane>/ (outside every repository)"
418
+ - id: repo.plaintext-env
419
+ profiles: [be, fe]
420
+ path: "{.env,.env.*,.secrets/,**/*.pem,**/*.key}"
421
+ presence: forbidden
422
+ tracked: external
423
+ tier: none
424
+ tests: none
425
+ goesTo: ".starcistacks/<env>/secrets/<slug>.enc (BE repo); decrypted values only in memory or under %LOCALAPPDATA%/StarCi/secrets/<project>/<env>/ via *_FILE"
426
+ rules: [HFS_PLAINTEXT_SECRET]
427
+ - id: be.root-e2e
428
+ profiles: [be]
429
+ path: "e2e/"
430
+ presence: forbidden
431
+ tracked: external
432
+ tier: none
433
+ tests: none
434
+ goesTo: "src/tests/e2e/ for specs; operational probes move to scripts/ or are deleted"
435
+
436
+ # ----- contract snapshots (the committed machine-emitted files, because they are a cross-repo interface) --
437
+ - id: be.contract.graphql
438
+ profiles: [be]
439
+ path: "contracts/<app>/schema.graphql"
440
+ presence: opt-in
441
+ tracked: tracked
442
+ tier: none
443
+ tests: none
444
+ 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)
445
+ rules: [HFS_CONTRACT_SNAPSHOT_DRIFT]
446
+ - id: be.contract.openapi
447
+ profiles: [be]
448
+ path: "contracts/<app>/openapi.json"
449
+ presence: opt-in
450
+ tracked: tracked
451
+ tier: none
452
+ tests: none
453
+ rules: [HFS_CONTRACT_SNAPSHOT_DRIFT]
454
+ - id: fe.contract.copy
455
+ profiles: [fe]
456
+ path: "apps/<app>/src/modules/api/contract/<be-app>.{graphql,json}"
457
+ presence: opt-in
458
+ tracked: tracked
459
+ tier: none
460
+ tests: none
461
+ why: byte copy of the BE snapshot, refreshed by `npm run contract:pull`; the land gate compares hashes
462
+ rules: [HFS_CONTRACT_SNAPSHOT_DRIFT]
463
+
464
+ # ----- BE: apps (one slot per app kind; a new kind = a new slot, minor bump) ---------------------------
465
+ - id: be.app.api
466
+ profiles: [be]
467
+ path: "apps/<app>/src/"
468
+ appKind: api
469
+ presence: required # at least one api app
470
+ tracked: tracked
471
+ tier: app
472
+ owner: true
473
+ minInstances: 1
474
+ requires: [main.ts, app.module.ts, "<app>.composition.spec.ts"]
475
+ allows: [main.ts, app.module.ts, "<app>.composition.spec.ts", "<app>.options.ts"]
476
+ tests: unit-beside # the composition spec boots the REAL AppModule with stubbed options
477
+ budget: {app.module.ts: 250, main.ts: 80}
478
+ rules: [BE_APP_COMPOSITION_ONLY, BE_APP_BUSINESS_ROLE, BE_DEFAULT_DENY, BE_ERROR_MASKED, BE_FEATURE_NOT_COMPOSED]
479
+ - id: be.app.worker
480
+ profiles: [be]
481
+ path: "apps/<app>/src/"
482
+ appKind: worker # composes transport/schedule and transport/message modules only
483
+ presence: opt-in
484
+ tracked: tracked
485
+ tier: app
486
+ owner: true
487
+ requires: [main.ts, app.module.ts, "<app>.composition.spec.ts"]
488
+ tests: unit-beside
489
+ rules: [BE_APP_COMPOSITION_ONLY, BE_BACKGROUND_UNOWNED]
490
+ - id: be.app.migrate
491
+ profiles: [be]
492
+ path: "apps/<app>/src/"
493
+ appKind: migrate # the ONLY process that runs migrations (one run per connection, before api/worker start)
494
+ presence: required
495
+ requiredWhen: connections # required as soon as hfs.json declares a connection
496
+ tracked: tracked
497
+ tier: app
498
+ owner: true
499
+ requires: [main.ts, "<app>.composition.spec.ts"]
500
+ tests: unit-beside
501
+ rules: [BE_SCHEMA_AUTHORITY, BE_ENTRYPOINT_ONLY_IN_APPS]
502
+ - id: be.app.cli
503
+ profiles: [be]
504
+ path: "apps/<app>/src/"
505
+ appKind: cli
506
+ presence: opt-in
507
+ tracked: tracked
508
+ tier: app
509
+ owner: true
510
+ requires: [main.ts, app.module.ts, "<app>.composition.spec.ts"]
511
+ tests: unit-beside
512
+
513
+ # ----- BE: features ------------------------------------------------------------------------------------
514
+ - id: be.feature
515
+ profiles: [be]
516
+ path: "src/features/<feature>/"
517
+ presence: required
518
+ tracked: tracked
519
+ tier: feature
520
+ owner: true
521
+ minInstances: 1
522
+ requires: [index.ts, "<feature>.module.ts", application/]
523
+ allows: [index.ts, "<feature>.module.ts", application/, transport/, messages/] # nothing else at feature root
524
+ tests: unit-beside
525
+ budget: {files: 250, indexExports: 60} # a feature above 250 source files must be split by product capability
526
+ rules: [BE_FEATURE_SHAPE, BE_FEATURE_IMPORTS_FEATURE, BE_PUBLIC_SURFACE, BE_FEATURE_NOT_COMPOSED]
527
+ - id: be.feature.application
528
+ profiles: [be]
529
+ path: "src/features/<feature>/application/"
530
+ presence: required
531
+ tracked: tracked
532
+ tier: feature
533
+ allows: ["<action>.{command,query,handler,contracts}.ts", "*.spec.ts"]
534
+ forbids: [graphql/, http/, message/, schedule/, transport/] # no protocol names under application/
535
+ tests: unit-beside
536
+ rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_APPLICATION_TRANSPORT_FRAMEWORK, BE_ENTITY_IN_CONTRACT, BE_CQRS_SHAPE]
537
+ - id: be.feature.application.support
538
+ profiles: [be]
539
+ path: "src/features/<feature>/application/support/"
540
+ presence: optional # helpers and types local to ONE feature; same tier as application, never a shared home
541
+ tracked: tracked
542
+ tier: feature # feature tier: another feature cannot import it (features never import features)
543
+ allows: ["<name>.<role>.ts", "*.spec.ts"] # <role> from ruleParams.be.suffixes; no <name>.types.ts
544
+ forbids: [graphql/, http/, message/, schedule/, transport/]
545
+ tests: unit-beside # a <name>.spec.ts beside every <name>.ts
546
+ rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_APPLICATION_TRANSPORT_FRAMEWORK, BE_FEATURE_IMPORTS_FEATURE]
547
+ since: 1.0.0
548
+ - id: be.transport.http
549
+ profiles: [be]
550
+ path: "src/features/<feature>/transport/http/"
551
+ presence: optional
552
+ tracked: tracked
553
+ tier: feature
554
+ requires: ["<feature>-http.module.ts"]
555
+ allows: ["<action>.controller.ts", "dto/<action>.{request,response}.ts", "<action>.mapper.ts", "*.spec.ts"]
556
+ tests: unit-beside
557
+ composedBy: [api]
558
+ rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED, BE_TRANSPORT_SHAPE]
559
+ - id: be.transport.graphql
560
+ profiles: [be]
561
+ path: "src/features/<feature>/transport/graphql/"
562
+ presence: optional
563
+ tracked: tracked
564
+ tier: feature
565
+ requires: ["<feature>-graphql.module.ts"]
566
+ allows: ["<action>.resolver.ts", "dto/<action>.{input,type,args}.ts", "<action>.mapper.ts", "*.spec.ts"]
567
+ tests: unit-beside
568
+ composedBy: [api]
569
+ rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED, BE_TRANSPORT_SHAPE]
570
+ - id: be.transport.message
571
+ profiles: [be]
572
+ path: "src/features/<feature>/transport/message/"
573
+ presence: opt-in # queue worker: consumed only by a worker app
574
+ tracked: tracked
575
+ tier: feature
576
+ requires: ["<feature>-message.module.ts"]
577
+ allows: ["<event>.consumer.ts", "<event>.message.ts", "*.spec.ts"]
578
+ tests: unit-beside
579
+ composedBy: [worker]
580
+ rules: [BE_BACKGROUND_UNOWNED]
581
+ - id: be.transport.schedule
582
+ profiles: [be]
583
+ path: "src/features/<feature>/transport/schedule/"
584
+ presence: opt-in # cron job, sweep or outbox publisher: consumed only by a worker app
585
+ tracked: tracked
586
+ tier: feature
587
+ requires: ["<feature>-schedule.module.ts"]
588
+ allows: ["<job>.job.ts", "*.spec.ts"]
589
+ tests: unit-beside
590
+ composedBy: [worker]
591
+ rules: [BE_BACKGROUND_UNOWNED]
592
+ - id: be.transport.websocket
593
+ profiles: [be]
594
+ path: "src/features/<feature>/transport/websocket/"
595
+ presence: opt-in
596
+ tracked: tracked
597
+ tier: feature
598
+ requires: ["<feature>-websocket.module.ts"]
599
+ allows: ["<channel>.gateway.ts", "dto/*.ts", "*.spec.ts"]
600
+ tests: unit-beside
601
+ composedBy: [api]
602
+ since: 1.0.0
603
+ - id: be.feature.transport.cli
604
+ profiles: [be]
605
+ path: "src/features/<feature>/transport/cli/"
606
+ presence: opt-in # a command line entry: dispatches one command or query, composed by an app of kind cli
607
+ tracked: tracked
608
+ tier: feature
609
+ requires: ["<feature>-cli.module.ts"]
610
+ allows: ["<name>.cli.ts", "dto/*.ts", "*.spec.ts"] # .cli.ts: .command.ts is a CQRS message (HFS delta 3)
611
+ tests: unit-beside
612
+ composedBy: [cli]
613
+ rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_FEATURE_NOT_COMPOSED, BE_ENTRYPOINT_ONLY_IN_APPS]
614
+ since: 1.0.0
615
+
616
+ # ----- BE: modules (three tiers, closed set) -----------------------------------------------------------
617
+ - id: be.domain
618
+ profiles: [be]
619
+ path: "src/modules/domain/<capability>/"
620
+ presence: optional
621
+ tracked: tracked
622
+ tier: domain
623
+ owner: true
624
+ requires: [index.ts]
625
+ allows: ["<capability>.module.ts", "<capability>.config.ts", "<capability>.options.ts", errors/, messages/, persistence/, policies/, "*.service.ts", "*.contracts.ts", "*.spec.ts"]
626
+ forbids: [testing/, exceptions/, utils/, helpers/, shared/]
627
+ tests: unit-beside
628
+ budget: {indexExports: 60}
629
+ rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_PUBLIC_SURFACE, BE_ERROR_HOME, BE_CONFIG_OWNER]
630
+ - id: be.errors
631
+ profiles: [be]
632
+ path: "src/modules/{domain,integrations}/<capability>/errors/"
633
+ presence: optional
634
+ tracked: tracked
635
+ tier: inherit
636
+ allows: ["<capability>.error.ts", "<name>.error.ts", "*.spec.ts"] # one family per capability, extends platform/errors DomainError
637
+ tests: unit-beside
638
+ rules: [BE_ERROR_HOME]
639
+ - id: be.domain.messages
640
+ profiles: [be]
641
+ path: "src/modules/{domain,integrations}/<capability>/messages/"
642
+ presence: optional # a capability that raises no user-facing text carries none
643
+ tracked: tracked
644
+ tier: inherit
645
+ allows: ["<capability>.messages.ts", "*.spec.ts"] # one keyed catalog per capability, vi and en, read through the platform/i18n MessageCatalog port
646
+ tests: unit-beside
647
+ rules: [BE_USER_COPY_LITERAL]
648
+ since: 1.0.0
649
+ - id: be.feature.messages
650
+ profiles: [be]
651
+ path: "src/features/<feature>/messages/"
652
+ presence: optional
653
+ tracked: tracked
654
+ tier: feature
655
+ allows: ["<feature>.messages.ts", "*.spec.ts"] # one keyed catalog per feature, vi and en, read through the platform/i18n MessageCatalog port
656
+ tests: unit-beside
657
+ rules: [BE_USER_COPY_LITERAL]
658
+ since: 1.0.0
659
+ - id: be.persistence
660
+ profiles: [be]
661
+ path: "src/modules/{domain,platform}/<capability>/persistence/"
662
+ presence: optional
663
+ tracked: tracked
664
+ tier: inherit
665
+ requires: [connection.ts] # CONNECTION = <CONN>_CONNECTION of an hfs.json connection, and the capability's <c>Entities / <c>Migrations arrays (the owner index re-exports the two arrays by name)
666
+ allows: ["entities/<table>.entity.ts", "migrations/<epochMs13>-<kebab-name>.ts", "<name>.sql.ts", "<name>.rows.ts", connection.ts, "*.spec.ts"]
667
+ forbids: [index.ts, "<name>.repository.ts"]
668
+ tests: unit-beside
669
+ rules: [BE_SCHEMA_OWNER, BE_SCHEMA_AUTHORITY, BE_SQL_OUTSIDE_PERSISTENCE]
670
+ - id: be.platform
671
+ profiles: [be]
672
+ path: "src/modules/platform/<capability>/"
673
+ presence: required
674
+ tracked: tracked
675
+ tier: platform
676
+ owner: true
677
+ # config = EnvSource reader only; logging = LoggerPort + adapter; errors = DomainError base + one filter + GraphQL
678
+ # formatError with masking; primitives = isRecord, hashing, ids, cookies; clock = the injected Clock port
679
+ # (now()), with a FakeClock test double from @starci/jest-preset; i18n = the MessageCatalog port, a keyed
680
+ # lookup with named interpolation over the vi/en catalogs a capability or feature carries in its messages/,
681
+ # resolved through the RequestLocale provider (request: stored user preference, else Accept-Language, else
682
+ # vi; non-request: recipient's stored preference, else vi - see knowledge/patterns/be/messages.yaml);
683
+ # database when a connection exists.
684
+ requiredInstances: {capability: [config, logging, errors, primitives, clock, i18n]}
685
+ requires: [index.ts]
686
+ forbids: [common/, utils/, shared/, testing/] # primitives live in platform/primitives
687
+ tests: unit-beside
688
+ budget: {indexExports: 60}
689
+ rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE]
690
+ - id: be.integrations
691
+ profiles: [be]
692
+ path: "src/modules/integrations/<provider>/"
693
+ presence: optional
694
+ tracked: tracked
695
+ tier: integrations
696
+ owner: true
697
+ requires: [index.ts, "<provider>.config.ts"]
698
+ tests: unit-beside
699
+ budget: {indexExports: 60}
700
+ rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT]
701
+ - id: be.integrations.model
702
+ profiles: [be]
703
+ path: "src/modules/integrations/<provider>/model/"
704
+ presence: opt-in
705
+ tracked: tracked
706
+ tier: integrations
707
+ tests: unit-beside
708
+ why: an ML or LLM model client is an integration; weights and datasets are never in git
709
+
710
+ # ----- BE: tests (owner test layout 2026-09-30). Unit specs sit beside their subject (<name>.spec.ts); everything else
711
+ # lives under src/tests/, one folder per kind, and the folder and the file suffix always agree (a file whose suffix does
712
+ # not match its folder matches no slot: HFS_SLOT_UNDECLARED).
713
+ - id: be.tests.world
714
+ profiles: [be]
715
+ path: "src/tests/world/"
716
+ presence: optional
717
+ tracked: tracked
718
+ tier: e2e
719
+ allows: [global-setup.ts, global-teardown.ts, use-test-world.ts, fakes/, kit/] # jest globalSetup/globalTeardown default-export (Jest API); plus root files with a role suffix (test-world-files, R47)
720
+ tests: none
721
+ why: >-
722
+ the ONLY test infrastructure location, shared by integration and e2e: global-setup.ts starts the shared
723
+ infrastructure (containers) and runs apps/migrate's exported bootstrap once; use-test-world.ts exports
724
+ useTestWorld({ apps: { <name>: { module, listen? } } } | { modules: [...] }) -> world.apps.<name>.api,
725
+ world.db.<connection> (the shared EntityManager), world.fake.<provider> (a network-edge fake started by the world, with
726
+ 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
727
+ overridden in the DI container. It is the only test location that may import typeorm's DataSource or testcontainers,
728
+ call migrate/runMigrations/synchronize, or write process.env.
729
+ - id: be.tests.fixtures
730
+ profiles: [be]
731
+ path: "src/tests/fixtures/"
732
+ presence: optional
733
+ tracked: tracked
734
+ tier: fixtures
735
+ allows: [database.ts] # mockEntityManager() and fakeTransaction() (BE-CONVENTION 1.16); other fixtures use role suffixes
736
+ tests: unit-beside
737
+ why: typed doubles and data builders; importable by unit specs, integration and e2e; never imports a feature
738
+ - id: be.tests.integration
739
+ profiles: [be]
740
+ path: "src/tests/integration/<capability>/*.integration-spec.ts"
741
+ presence: optional
742
+ tracked: tracked
743
+ tier: e2e
744
+ tests: e2e
745
+ why: one capability module on the real database, no HTTP (SQL, transactions, concurrency, inbox claims) through useTestWorld({ modules }); run by test:integration
746
+ rules: [BE_TEST_TOPOLOGY]
747
+ - id: be.tests.e2e
748
+ profiles: [be]
749
+ path: "src/tests/e2e/<area>/*.e2e-spec.ts"
750
+ presence: optional
751
+ tracked: tracked
752
+ tier: e2e
753
+ tests: e2e
754
+ why: flow e2e through useTestWorld({ apps }); external services are the world's network fakes; run by test:e2e
755
+ rules: [HFS_CI_E2E_AUTOMATIC, BE_TEST_TOPOLOGY, BE_E2E_FLOW]
756
+ - id: be.tests.contract
757
+ profiles: [be]
758
+ path: "src/tests/contract/<provider>/*.contract-spec.ts"
759
+ presence: optional
760
+ tracked: tracked
761
+ tier: e2e
762
+ tests: e2e
763
+ why: provider sandbox contracts, skipped without sandbox config; run only by test:contract, never part of test or test:e2e
764
+ rules: [BE_TEST_TOPOLOGY]
765
+ - id: be.tests.e2e-world-retired
766
+ profiles: [be]
767
+ path: "src/tests/e2e/world/"
768
+ presence: forbidden
769
+ tracked: external
770
+ tier: none
771
+ tests: none
772
+ goesTo: "src/tests/world/ (the only test infrastructure location; e2e/<area>/ holds only *.e2e-spec.ts)"
773
+
774
+ # ----- BE: work and stacks -----------------------------------------------------------------------------
775
+ - id: be.starciwork
776
+ profiles: [be]
777
+ path: ".starciwork/"
778
+ presence: required
779
+ tracked: tracked
780
+ tier: none
781
+ tests: none
782
+ requires: [.gitignore, workspace.yaml, features/index.yaml]
783
+ allows: [workspace.yaml, "brand/**", shell/index.yaml, "features/<feature>/index.yaml",
784
+ "features/<feature>/<family>/<name>/index.yaml", "features/<feature>/br/<rule>/ac/<name>/index.yaml",
785
+ "features/<feature>/ui/<name>/assets/<approved-file>", "features/<feature>/uat/<name>/{index,accounts,fixtures}.yaml",
786
+ "features/<feature>/uat/<name>/{seed,cleanup}.sql", "_resources/{identities,environments,fixtures}/<slug>/resource.yaml"]
787
+ forbids: [evidence/, logs/, reports/, captures/, runs/, "kernel-*/", "*.sqlite*", _derived/, "work/node records"]
788
+ rules: [HFS_AGENT_DATA_TRACKED, HFS_WORK_NODE_RETIRED, HFS_IDENTITY_CUSTODY]
789
+ - id: be.starcistacks
790
+ profiles: [be]
791
+ path: ".starcistacks/"
792
+ presence: required
793
+ tracked: tracked
794
+ tier: none
795
+ tests: none
796
+ requires: [application-stacks.yaml]
797
+ allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,k8s,terraform}/**",
798
+ "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/seeds/**"]
799
+ forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
800
+ rules: [HFS_STACKS_SHAPE, HFS_PLAINTEXT_SECRET, HFS_IDENTITY_CUSTODY]
801
+ - id: be.sops
802
+ profiles: [be]
803
+ path: .sops.yaml
804
+ presence: required
805
+ tracked: tracked
806
+ tier: none
807
+ tests: none
808
+
809
+ # ----- FE: apps ----------------------------------------------------------------------------------------
810
+ - id: fe.app.next
811
+ profiles: [fe]
812
+ path: "apps/<app>/{package.json,next.config.ts,tsconfig.json,postcss.config.mjs}"
813
+ appKind: next
814
+ presence: required
815
+ tracked: tracked
816
+ tier: none
817
+ minInstances: 1
818
+ requires: [package.json, next.config.ts, tsconfig.json, postcss.config.mjs, src/app/, src/modules/i18n/,
819
+ "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"]
820
+ tests: none
821
+ rules: [FE_APP_ISOLATION, FE_ERROR_BOUNDARY_MISSING, FE_NEXT_CONVENTIONS]
822
+ - id: fe.app-optional
823
+ profiles: [fe]
824
+ path: "apps/<app>/{vitest.config.ts,playwright.config.ts,public/}"
825
+ presence: optional
826
+ tracked: tracked
827
+ tier: none
828
+ roles: {playwright: playwright.config.ts}
829
+ tests: none
830
+ - id: fe.route
831
+ profiles: [fe]
832
+ path: "apps/<app>/src/app/"
833
+ presence: required
834
+ tracked: tracked
835
+ tier: route
836
+ allows: ["[locale]/**/{page,layout,template,loading,error,not-found,default}.tsx", "[locale]/**/route.ts", "[locale]/providers.tsx",
837
+ global-error.tsx, page.tsx, # the root page.tsx only redirects (FE_ROUTE_FILES_THIN)
838
+ not-found.tsx, layout.tsx, # next-intl: a request outside every locale renders the root not-found inside a root layout
839
+ "health/{live,ready}/route.ts", "api/**/route.ts", "_*/**", providers.tsx, globals.css,
840
+ "{icon,favicon,apple-icon,opengraph-image,twitter-image}.*", robots.ts, sitemap.ts, manifest.ts, "**/*.spec.ts", "**/*.spec.tsx"]
841
+ 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}
842
+ tests: none
843
+ rules: [FE_ROUTE_FILES_THIN, FE_CLIENT_BOUNDARY, FE_OWNER_REACHABLE]
844
+ - id: fe.source-root-pinned
845
+ profiles: [fe]
846
+ path: "apps/<app>/src/{proxy.ts,instrumentation.ts,instrumentation-client.ts}"
847
+ presence: optional
848
+ tracked: tracked
849
+ tier: route
850
+ roles: {proxy: proxy.ts, instrumentation: instrumentation.ts, instrumentation-client: instrumentation-client.ts}
851
+ tests: none
852
+ rules: [FE_NEXT_CONVENTIONS] # middleware.ts is refused on Next >= 16 (fe.source-root-retired)
853
+ - id: fe.source-root-retired
854
+ profiles: [fe]
855
+ path: "apps/<app>/src/{middleware.ts,middleware.js}"
856
+ presence: forbidden
857
+ tracked: external
858
+ tier: none
859
+ tests: none
860
+ goesTo: "apps/<app>/src/proxy.ts: Next >= 16 renamed middleware to proxy and refuses the old name"
861
+ rules: [FE_NEXT_CONVENTIONS]
862
+ - id: fe.feature
863
+ profiles: [fe]
864
+ path: "apps/<app>/src/features/{pages,layouts,overlays}/<name>/"
865
+ presence: required
866
+ tracked: tracked
867
+ tier: feature
868
+ owner: true
869
+ kinds: [pages, layouts, overlays]
870
+ requires: [index.tsx]
871
+ allows: [index.tsx, component.tsx, classNames.ts, "*.spec.tsx", "*.spec.ts"]
872
+ 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
873
+ tests: unit-beside
874
+ budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
875
+ rules: [FE_SIZE_BUDGET, FE_OWNER_REACHABLE, FE_I18N_LITERAL]
876
+ - id: fe.components
877
+ profiles: [fe]
878
+ path: "apps/<app>/src/components/{blocks,composites,branches,leaves}/<name>/"
879
+ presence: optional
880
+ tracked: tracked
881
+ tier: components
882
+ owner: true
883
+ layers: [blocks, composites, branches, leaves] # a layer imports only the layers after it
884
+ requires: [index.tsx]
885
+ allows: [index.tsx, component.tsx, classNames.ts, "*.spec.tsx", "*.spec.ts"]
886
+ 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
887
+ tests: unit-beside
888
+ budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
889
+ rules: [FE_CONNECTED_BLOCK_RENDER_PAIR, FE_I18N_LITERAL, FE_NATIVE_FORM_CONTROL, FE_CLIENT_BOUNDARY]
890
+ - id: fe.hooks
891
+ profiles: [fe]
892
+ path: "apps/<app>/src/hooks/<domain>/"
893
+ presence: optional
894
+ tracked: tracked
895
+ tier: hooks
896
+ owner: true # a domain is an owner: other domains import it through index.ts only
897
+ requires: [index.ts]
898
+ allows: [index.ts, "use<name>.ts", "<domain>.shared.ts", "*.spec.ts"] # React hooks only; <domain>.shared.ts = sanctioned non-hook helpers
899
+ roles: {entry: index.ts, shared: "<domain>.shared.ts"}
900
+ tests: unit-beside
901
+ budget: {file: 200, dataHooks: 6, useState: 6}
902
+ rules: [FE_HOOKS_ARE_HOOKS, FE_DATA_FRESHNESS]
903
+ - id: fe.modules
904
+ profiles: [fe]
905
+ path: "apps/<app>/src/modules/<capability>/"
906
+ presence: required
907
+ tracked: tracked
908
+ tier: modules
909
+ owner: true
910
+ requiredInstances: {capability: [config, i18n, routes]}
911
+ requires: [index.ts]
912
+ tests: unit-beside
913
+ budget: {file: 400}
914
+ rules: [FE_ENV_OWNER, FE_TRANSPORT_OWNER]
915
+ - id: fe.modules.api
916
+ profiles: [fe]
917
+ path: "apps/<app>/src/modules/api/"
918
+ presence: optional
919
+ tracked: tracked
920
+ tier: transport
921
+ # The app's reads of the one client. A repository has exactly ONE transport client and ONE Outcome union: the app's
922
+ # client.ts/outcome.ts (fe.transport.client/outcome) in a one-app repository, or the api package's
923
+ # (fe.package.api.client/outcome) when the repository shares it (FE_TRANSPORT_OWNER, machine).
924
+ requires: [index.ts]
925
+ allows: [index.ts, client.ts, outcome.ts, "<domain>/read-*.ts", "<domain>/*.graphql", "<domain>/<domain>.mapper.ts", contract/, __generated__/, "*.spec.ts"]
926
+ roles: {entry: index.ts, reader: "read-*.ts"} # a reader is a server module (FE_CLIENT_REACHES_SERVER)
927
+ tests: unit-beside
928
+ rules: [FE_TRANSPORT_OWNER, FE_HTTP_STATUS_COLLAPSE, FE_WIRE_GENERATED]
929
+ - id: fe.transport.client
930
+ profiles: [fe]
931
+ path: "apps/<app>/src/modules/api/client.ts"
932
+ presence: optional
933
+ tracked: tracked
934
+ tier: transport
935
+ tests: unit-beside
936
+ why: the one fetch of a one-app repository (timeout, abort, 401/403 -> refused); a shared client is fe.package.api.client
937
+ rules: [FE_TRANSPORT_OWNER]
938
+ - id: fe.transport.outcome
939
+ profiles: [fe]
940
+ path: "apps/<app>/src/modules/api/outcome.ts"
941
+ presence: optional
942
+ tracked: tracked
943
+ tier: transport
944
+ tests: unit-beside
945
+ why: the one Outcome union of a one-app repository; a shared union is fe.package.api.outcome
946
+ rules: [FE_HTTP_STATUS_COLLAPSE]
947
+ - id: fe.modules.config
948
+ profiles: [fe]
949
+ path: "apps/<app>/src/modules/config/"
950
+ presence: required
951
+ tracked: tracked
952
+ tier: foundation
953
+ tests: unit-beside
954
+ why: the only reader of process.env and NEXT_PUBLIC_*; fails fast, no localhost fallback
955
+ - id: fe.modules.i18n
956
+ profiles: [fe]
957
+ path: "apps/<app>/src/modules/i18n/"
958
+ presence: required
959
+ tracked: tracked
960
+ tier: foundation
961
+ # The app's i18n module: its catalogs and the one call of the repository's i18n factory. The next-intl stack (routing,
962
+ # navigation, request config) is written once per repository: in the i18n package (fe.package.i18n, `createAppI18n`)
963
+ # when the repository has one, else in the only app's module.
964
+ requires: [index.ts]
965
+ tests: unit-beside
966
+ rules: [FE_I18N_PLACEMENT, FE_I18N_CATALOG]
967
+ - id: fe.modules.brand
968
+ profiles: [fe]
969
+ path: "apps/<app>/src/modules/brand/brand.css"
970
+ presence: optional
971
+ tracked: tracked
972
+ tier: none
973
+ tests: none
974
+ why: the ONLY app file that may hold colour values; sets grammar brand tokens only, light and dark
975
+ rules: [FE_STYLE_TOKEN_ONLY]
976
+ - id: fe.modules.routes
977
+ profiles: [fe]
978
+ path: "apps/<app>/src/modules/routes/"
979
+ presence: required
980
+ tracked: tracked
981
+ tier: foundation
982
+ tests: unit-beside
983
+ why: every href builder; FE_HREF_RESOLVES checks each against app/
984
+ - id: fe.modules.types
985
+ profiles: [fe]
986
+ path: "apps/<app>/src/modules/types/"
987
+ presence: optional
988
+ tracked: tracked
989
+ tier: foundation
990
+ tests: unit-beside
991
+ why: shared types with no runtime behaviour; a component may import them
992
+ - id: fe.package.ui
993
+ profiles: [fe]
994
+ path: "packages/<family>-ui/"
995
+ presence: opt-in
996
+ tracked: tracked
997
+ tier: package
998
+ owner: true
999
+ kinds: [composites, branches, leaves] # the layer folders under src/ (a package owns no blocks)
1000
+ roles: {entry: index.tsx, drawing: component.tsx, styles: classNames.ts}
1001
+ requires: [package.json, src/index.ts, tsconfig.json]
1002
+ tests: unit-beside
1003
+ budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
1004
+ why: built to dist with explicit named exports; only composites, branches and leaves; no unused export; no colour literal
1005
+ rules: [FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT, FE_STYLE_TOKEN_ONLY]
1006
+ - id: fe.package.api
1007
+ profiles: [fe]
1008
+ path: "packages/<family>-api/"
1009
+ presence: opt-in
1010
+ tracked: tracked
1011
+ tier: package
1012
+ owner: true
1013
+ requires: [package.json, src/index.ts, src/client.ts, src/outcome.ts, tsconfig.json]
1014
+ tests: unit-beside
1015
+ why: the one transport client and the one Outcome union shared by every app of the repository; no app keeps its own
1016
+ rules: [FE_TRANSPORT_OWNER, FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT]
1017
+ - id: fe.package.api.client
1018
+ profiles: [fe]
1019
+ path: "packages/<family>-api/src/client.ts"
1020
+ presence: optional # enabled with its package (fe.package.api is the opt-in)
1021
+ tracked: tracked
1022
+ tier: package
1023
+ tests: unit-beside
1024
+ why: the repository's one fetch (timeout, abort, 401/403 -> refused)
1025
+ rules: [FE_TRANSPORT_OWNER]
1026
+ - id: fe.package.api.outcome
1027
+ profiles: [fe]
1028
+ path: "packages/<family>-api/src/outcome.ts"
1029
+ presence: optional # enabled with its package (fe.package.api is the opt-in)
1030
+ tracked: tracked
1031
+ tier: package
1032
+ tests: unit-beside
1033
+ why: the repository's one Outcome union
1034
+ rules: [FE_HTTP_STATUS_COLLAPSE]
1035
+ - id: fe.package.i18n
1036
+ profiles: [fe]
1037
+ path: "packages/<family>-i18n/"
1038
+ presence: opt-in
1039
+ tracked: tracked
1040
+ tier: package
1041
+ owner: true
1042
+ requires: [package.json, src/index.ts, tsconfig.json]
1043
+ tests: unit-beside
1044
+ why: the next-intl stack (routing, navigation, request config) written once, exported as the createAppI18n factory every app calls
1045
+ rules: [FE_I18N_PLACEMENT, FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT]
1046
+ - id: fe.e2e
1047
+ profiles: [fe]
1048
+ path: "e2e/<area>/*.e2e-spec.ts"
1049
+ presence: optional
1050
+ tracked: tracked
1051
+ tier: e2e
1052
+ tests: e2e
1053
+ requires: [/playwright.config.ts, /tsconfig.e2e.json]
1054
+ rules: [FE_E2E_SHAPE, HFS_CI_E2E_AUTOMATIC]
1055
+ - id: fe.e2e-support
1056
+ profiles: [fe]
1057
+ path: "e2e/{support,fixtures}/"
1058
+ presence: optional
1059
+ tracked: tracked
1060
+ tier: e2e
1061
+ tests: none
1062
+
1063
+ # Checks that read this manifest (all ship from .claude; none keeps its own path list)
1064
+ consumers:
1065
+ - scripts/lib/hfs-slots.mjs # the loader every consumer below goes through
1066
+ - scripts/checks/hfs/slot-check.mjs # HFS_SLOT_* / tracked / external / managed drift
1067
+ - scripts/checks/architecture.mjs # tiers, direction matrix, cycles, reachability
1068
+ - packages/eslint/be starciBeConfig({hfs}) # file globs for rules come from slots
1069
+ - packages/eslint/fe starciFeConfig({hfs})
1070
+ - packages/stylelint-canon # colour and brand allowances from fe.modules.brand
1071
+ - packages/hfs/sync # renders managedBy templates (hfs sync) and judges them (hfs check)
1072
+ - modules/kernel/failure-codes.yaml # every rule id listed above has a Vietnamese why