@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.
- package/CHANGELOG.md +78 -2
- package/README.md +17 -18
- package/brand-values.mjs +1 -3
- package/class-names.mjs +9 -9
- package/client-boundary.mjs +125 -37
- package/comments.mjs +14 -21
- package/docs/client-boundary.md +26 -1
- package/docs/grammar-boundary.md +33 -0
- package/docs/hygiene.md +15 -4
- package/docs/lint-escape-hatch.md +3 -1
- package/docs/next-conventions.md +26 -0
- package/docs/size-growth.md +29 -0
- package/docs/spec-quality.md +15 -7
- package/docs/status-colors.md +32 -0
- package/docs/translation.md +4 -0
- package/docs/transport.md +36 -9
- package/e2e-shape.mjs +20 -23
- package/env-owner.mjs +1 -1
- package/file-layout.mjs +105 -103
- package/formatting.mjs +2 -4
- package/grammar-boundary.mjs +85 -9
- package/hooks-folder.mjs +17 -16
- package/hygiene.mjs +370 -92
- package/icon.mjs +21 -20
- package/index.mjs +27 -85
- package/landmark.mjs +0 -11
- package/lib/bindings.mjs +144 -0
- package/lib/config.mjs +79 -0
- package/lib/hfs.mjs +48 -0
- package/lib/next.mjs +6 -0
- package/lib/platform.mjs +66 -0
- package/lib/react.mjs +28 -0
- package/lib/scope.mjs +119 -19
- package/lib/status-tones.generated.mjs +9 -0
- package/lib/types.mjs +63 -0
- package/lib/why.mjs +41 -11
- package/lint-escape-hatch.mjs +17 -15
- package/loading.mjs +9 -16
- package/naming.mjs +8 -2
- package/next-conventions.mjs +109 -25
- package/package.json +9 -4
- package/props-and-slots.mjs +19 -13
- package/runtime/engine/plain-object.mjs +5 -0
- package/runtime/engine/runtime-root.mjs +32 -0
- package/runtime/engine/yaml.mjs +161 -0
- package/runtime/knowledge/hfs/slots.yaml +1072 -0
- package/runtime/scripts/lib/git.mjs +53 -0
- package/runtime/scripts/lib/glob.mjs +23 -0
- package/runtime/scripts/lib/hfs-allows.mjs +42 -0
- package/runtime/scripts/lib/hfs-slots.mjs +722 -0
- package/runtime/scripts/lib/hfs-view.mjs +68 -0
- package/runtime/scripts/lib/next-contract.mjs +14 -0
- package/runtime/scripts/lib/path-key.mjs +17 -0
- package/runtime/scripts/lib/recorded-lines.mjs +27 -0
- package/served-locale.mjs +30 -16
- package/shape-slot.mjs +22 -20
- package/size-and-state-budget.mjs +41 -23
- package/size-growth.mjs +58 -0
- package/spec-quality.mjs +95 -16
- package/status-colors.mjs +139 -0
- package/the-split.mjs +13 -13
- package/tokens.mjs +26 -34
- package/translation.mjs +52 -19
- package/transport.mjs +437 -89
- package/type-safety.mjs +8 -17
- package/typography.mjs +7 -11
- package/vendor-boundary.mjs +5 -27
- 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
|