@starci/hfs 1.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/README.md +38 -0
- package/bin/hfs.mjs +100 -0
- package/package.json +28 -0
- package/runtime/engine/runtime-root.mjs +32 -0
- package/runtime/engine/yaml.mjs +161 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +212 -0
- package/runtime/knowledge/hfs/slots.yaml +842 -0
- package/runtime/modules/kernel/failure-codes.yaml +169 -0
- package/runtime/scripts/lib/glob.mjs +23 -0
- package/runtime/scripts/lib/hfs-check.mjs +305 -0
- package/runtime/scripts/lib/hfs-slots.mjs +675 -0
- package/runtime/scripts/lib/path-key.mjs +15 -0
- package/sync/cli.mjs +15 -0
- package/sync/hygiene.mjs +92 -0
- package/sync/index.mjs +224 -0
- package/sync/skeleton.mjs +54 -0
- package/sync/sonar-key.mjs +45 -0
- package/templates/be/e2e.yml +21 -0
- package/templates/be/gitignore +2 -0
- package/templates/be/pre-commit +8 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.composition.spec.ts +31 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +7 -0
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +23 -0
- package/templates/be/skeleton/apps/__app__/src/main.ts +20 -0
- package/templates/be/skeleton/src/features/system-health/index.ts +1 -0
- package/templates/be/skeleton/src/features/system-health/system-health.module.ts +6 -0
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.spec.ts +13 -0
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +18 -0
- package/templates/be/skeleton/src/features/system-health/transport/http/system-health-http.module.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/config/env-source.spec.ts +36 -0
- package/templates/be/skeleton/src/modules/platform/config/env-source.ts +40 -0
- package/templates/be/skeleton/src/modules/platform/config/errors/config.error.ts +21 -0
- package/templates/be/skeleton/src/modules/platform/config/index.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/config/server.config.spec.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/config/server.config.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/config/server.options.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/errors/domain-error.spec.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/errors/domain-error.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.spec.ts +38 -0
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +24 -0
- package/templates/be/skeleton/src/modules/platform/errors/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.spec.ts +33 -0
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.ts +35 -0
- package/templates/be/skeleton/src/modules/platform/logging/log-id.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/logging/logger.port.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +11 -0
- package/templates/be/sonar-project.properties +10 -0
- package/templates/be/starciwork.gitignore +39 -0
- package/templates/common/ci.yml +50 -0
- package/templates/common/codecov.yml +13 -0
- package/templates/common/gitignore.base +33 -0
- package/templates/common/pre-push +5 -0
- package/templates/fe/e2e.yml +22 -0
- package/templates/fe/gitignore +3 -0
- package/templates/fe/pre-commit +7 -0
- package/templates/fe/skeleton/apps/__app__/next.config.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/error.tsx +22 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +31 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +15 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +27 -0
- package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +24 -0
- package/templates/fe/skeleton/apps/__app__/src/app/globals.css +1 -0
- package/templates/fe/skeleton/apps/__app__/src/app/health/live/route.spec.ts +10 -0
- package/templates/fe/skeleton/apps/__app__/src/app/health/live/route.ts +5 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/config.ts +8 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +19 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages.spec.ts +27 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/navigation.ts +5 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/request.ts +13 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.spec.ts +10 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.ts +9 -0
- package/templates/fe/skeleton/apps/__app__/src/proxy.ts +10 -0
- package/templates/fe/sonar-project.properties +11 -0
|
@@ -0,0 +1,842 @@
|
|
|
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
|
+
# budget size budgets (lines per file, files per slot, exports).
|
|
46
|
+
# appKind the app kind of hfs.json apps[].kind whose directory this slot describes.
|
|
47
|
+
# managedBy template id that hfs sync renders; a difference is HFS_MANAGED_FILE_DRIFT.
|
|
48
|
+
# rules rule ids (the catalog lands with knowledge/hfs/rules.yaml).
|
|
49
|
+
# since / retiredIn / successor lifecycle.
|
|
50
|
+
presenceValues: [required, optional, opt-in, forbidden]
|
|
51
|
+
trackedValues: [tracked, ignored, external]
|
|
52
|
+
testValues: [unit-beside, e2e, none]
|
|
53
|
+
|
|
54
|
+
appKinds:
|
|
55
|
+
be: [api, worker, migrate, cli]
|
|
56
|
+
fe: [next]
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
59
|
+
# Tiers and the dependency direction matrix (owner granularity; type-only imports count; no cycles anywhere)
|
|
60
|
+
# mayImport lists the tiers a file of that tier may import. Importing inside the same owner is always allowed.
|
|
61
|
+
# ---------------------------------------------------------------------------------------------------------
|
|
62
|
+
tiers:
|
|
63
|
+
be:
|
|
64
|
+
app: {mayImport: [feature, domain, platform, integrations, package]}
|
|
65
|
+
feature: {mayImport: [domain, platform, integrations, package]} # never another feature
|
|
66
|
+
domain: {mayImport: [domain, platform, integrations, package], acyclic: true}
|
|
67
|
+
integrations: {mayImport: [platform, package]}
|
|
68
|
+
platform: {mayImport: [platform, package], acyclic: true}
|
|
69
|
+
e2e: {mayImport: [e2e, app, feature, domain, platform, integrations, fixtures, package]}
|
|
70
|
+
fixtures: {mayImport: [domain, platform, integrations, package]} # public contracts only
|
|
71
|
+
package: {mayImport: [package]}
|
|
72
|
+
fe:
|
|
73
|
+
route: {mayImport: [feature, foundation, modules, transport, package]} # app/** adapters
|
|
74
|
+
feature: {mayImport: [components, hooks, foundation, modules, transport, package]}
|
|
75
|
+
# A component never reaches data: config, routes, i18n and types are foundation; every other module and the api
|
|
76
|
+
# transport are reached through a hook (owner decision 2026-09-29).
|
|
77
|
+
components: {mayImport: [components, hooks, foundation, package], lowerLayerOnly: true} # blocks, composites, branches, leaves
|
|
78
|
+
# Across domains a hook is imported only through the other domain's public index (hooks/<domain>/index.ts), never deep.
|
|
79
|
+
hooks: {mayImport: [hooks, foundation, modules, transport, package]}
|
|
80
|
+
foundation: {mayImport: [foundation, package], acyclic: true} # modules/{config,i18n,routes,types}
|
|
81
|
+
modules: {mayImport: [foundation, modules, transport, package], acyclic: true}
|
|
82
|
+
transport: {mayImport: [foundation, modules, package]} # modules/api: the one fetch
|
|
83
|
+
e2e: {mayImport: [e2e, package]} # black box: no app source
|
|
84
|
+
package: {mayImport: [package]}
|
|
85
|
+
crossOwner: every import across owners targets the owner's public entry (index.ts or index.tsx); never export *.
|
|
86
|
+
crossApp: apps never import each other; shared code is a packages/<pkg> slot.
|
|
87
|
+
|
|
88
|
+
# Parameters the lint factories and checks read per profile (through ruleParams(profile) in scripts/lib/hfs-slots.mjs).
|
|
89
|
+
ruleParams:
|
|
90
|
+
be:
|
|
91
|
+
# BE_MODULE_SHAPE: the only modules that may be @Global(); directories under src/modules/platform/.
|
|
92
|
+
globalModules: [src/modules/platform/config/, src/modules/platform/logging/, src/modules/platform/database/]
|
|
93
|
+
# HFS_SIZE_GROWTH: a file above soft may not grow against its parent commit; a new file stays within soft.
|
|
94
|
+
fileLines: {soft: 500, hardGrowth: true}
|
|
95
|
+
# HFS_DUPLICATE_BLOCK: a token-normalised block of at least this many source lines that appears in two owners.
|
|
96
|
+
duplicateBlockLines: 25
|
|
97
|
+
fe:
|
|
98
|
+
# Slot budgets (component.tsx 300, hooks 200, modules 400) are stricter than this cap where they apply.
|
|
99
|
+
fileLines: {soft: 500, hardGrowth: true}
|
|
100
|
+
# FE_TRANSPORT_OWNER: the one module per app that may call fetch.
|
|
101
|
+
clientModule: "apps/<app>/src/modules/api/client.ts"
|
|
102
|
+
duplicateBlockLines: 25
|
|
103
|
+
|
|
104
|
+
slots:
|
|
105
|
+
|
|
106
|
+
# ----- repository root (both profiles) ---------------------------------------------------------------
|
|
107
|
+
- id: repo.readme
|
|
108
|
+
profiles: [be, fe]
|
|
109
|
+
path: README.md
|
|
110
|
+
presence: required
|
|
111
|
+
tracked: tracked
|
|
112
|
+
tier: none
|
|
113
|
+
tests: none
|
|
114
|
+
rules: [HFS_README_*]
|
|
115
|
+
- id: repo.declaration
|
|
116
|
+
profiles: [be, fe]
|
|
117
|
+
path: hfs.json
|
|
118
|
+
presence: required
|
|
119
|
+
tracked: tracked
|
|
120
|
+
tier: none
|
|
121
|
+
tests: none
|
|
122
|
+
rules: [HFS_ARCH_CONFIG_UNREAD]
|
|
123
|
+
- id: repo.package-manifest
|
|
124
|
+
profiles: [be, fe]
|
|
125
|
+
path: "{package.json,package-lock.json}"
|
|
126
|
+
presence: required
|
|
127
|
+
tracked: tracked
|
|
128
|
+
tier: none
|
|
129
|
+
tests: none
|
|
130
|
+
rules: [HFS_PACKAGE_MANAGER_MIXED, HFS_DEP_VERSION_SKEW, HFS_CANON_PIN_DRIFT]
|
|
131
|
+
- id: repo.git-meta
|
|
132
|
+
profiles: [be, fe]
|
|
133
|
+
path: "{.gitignore,.gitattributes}"
|
|
134
|
+
presence: required
|
|
135
|
+
tracked: tracked
|
|
136
|
+
tier: none
|
|
137
|
+
tests: none
|
|
138
|
+
managedBy: gitignore-base
|
|
139
|
+
rules: [HFS_GITIGNORE_BLOCK_DRIFT]
|
|
140
|
+
- id: repo.tool-config
|
|
141
|
+
profiles: [be, fe]
|
|
142
|
+
path: "{tsconfig.json,eslint.config.mjs,.editorconfig,.nvmrc,.prettierignore}"
|
|
143
|
+
presence: required # each file is a thin extends of a canon package
|
|
144
|
+
tracked: tracked
|
|
145
|
+
tier: none
|
|
146
|
+
tests: none
|
|
147
|
+
managedBy: tool-config
|
|
148
|
+
rules: [HFS_TOOL_CONFIG_LOCAL, HFS_MANAGED_FILE_DRIFT, HFS_TS_STRICT]
|
|
149
|
+
- id: repo.tool-config-optional
|
|
150
|
+
profiles: [be, fe]
|
|
151
|
+
path: "{tsconfig.build.json,tsconfig.e2e.json,.npmrc,jest.config.e2e.js}"
|
|
152
|
+
presence: optional
|
|
153
|
+
tracked: tracked
|
|
154
|
+
tier: none
|
|
155
|
+
tests: none
|
|
156
|
+
managedBy: tool-config
|
|
157
|
+
rules: [HFS_TOOL_CONFIG_LOCAL, HFS_MANAGED_FILE_DRIFT]
|
|
158
|
+
- id: be.tool-config
|
|
159
|
+
profiles: [be]
|
|
160
|
+
path: "{nest-cli.json,jest.config.js}"
|
|
161
|
+
presence: required
|
|
162
|
+
tracked: tracked
|
|
163
|
+
tier: none
|
|
164
|
+
tests: none
|
|
165
|
+
managedBy: tool-config
|
|
166
|
+
rules: [HFS_TOOL_CONFIG_LOCAL, HFS_MANAGED_FILE_DRIFT]
|
|
167
|
+
- id: fe.tool-config
|
|
168
|
+
profiles: [fe]
|
|
169
|
+
path: "{vitest.config.ts,vitest.setup.ts,playwright.config.ts,turbo.json}"
|
|
170
|
+
presence: optional
|
|
171
|
+
tracked: tracked
|
|
172
|
+
tier: none
|
|
173
|
+
tests: none
|
|
174
|
+
managedBy: tool-config
|
|
175
|
+
rules: [HFS_TOOL_CONFIG_LOCAL, HFS_MANAGED_FILE_DRIFT]
|
|
176
|
+
- id: repo.quality-config
|
|
177
|
+
profiles: [be, fe]
|
|
178
|
+
path: "{sonar-project.properties,codecov.yml}"
|
|
179
|
+
presence: required
|
|
180
|
+
tracked: tracked
|
|
181
|
+
tier: none
|
|
182
|
+
tests: none
|
|
183
|
+
managedBy: quality-config
|
|
184
|
+
rules: [HFS_SONAR_CONFIG, HFS_MANAGED_FILE_DRIFT]
|
|
185
|
+
- id: repo.hooks
|
|
186
|
+
profiles: [be, fe]
|
|
187
|
+
path: ".husky/{pre-commit,pre-push}"
|
|
188
|
+
presence: required
|
|
189
|
+
tracked: tracked
|
|
190
|
+
tier: none
|
|
191
|
+
tests: none
|
|
192
|
+
managedBy: husky
|
|
193
|
+
rules: [HFS_CI_E2E_AUTOMATIC, HFS_MANAGED_FILE_DRIFT]
|
|
194
|
+
- id: repo.ci
|
|
195
|
+
profiles: [be, fe]
|
|
196
|
+
path: ".github/workflows/ci.yml"
|
|
197
|
+
presence: required
|
|
198
|
+
tracked: tracked
|
|
199
|
+
tier: none
|
|
200
|
+
tests: none
|
|
201
|
+
managedBy: ci-workflows
|
|
202
|
+
rules: [HFS_CI_E2E_AUTOMATIC, HFS_CI_MISSING_CANON, HFS_MANAGED_FILE_DRIFT]
|
|
203
|
+
- id: repo.ci-e2e
|
|
204
|
+
profiles: [be, fe]
|
|
205
|
+
path: ".github/workflows/e2e.yml"
|
|
206
|
+
presence: optional # on: workflow_dispatch only
|
|
207
|
+
tracked: tracked
|
|
208
|
+
tier: none
|
|
209
|
+
tests: none
|
|
210
|
+
managedBy: ci-workflows
|
|
211
|
+
rules: [HFS_CI_E2E_AUTOMATIC, HFS_MANAGED_FILE_DRIFT]
|
|
212
|
+
- id: repo.github-meta
|
|
213
|
+
profiles: [be, fe]
|
|
214
|
+
path: ".github/{CODEOWNERS,pull_request_template.md,ISSUE_TEMPLATE/**,dependabot.yml}"
|
|
215
|
+
presence: optional
|
|
216
|
+
tracked: tracked
|
|
217
|
+
tier: none
|
|
218
|
+
tests: none
|
|
219
|
+
- id: repo.scripts
|
|
220
|
+
profiles: [be, fe]
|
|
221
|
+
path: "scripts/<name>.mjs"
|
|
222
|
+
presence: optional
|
|
223
|
+
tracked: tracked
|
|
224
|
+
tier: none
|
|
225
|
+
tests: none
|
|
226
|
+
rules: [HFS_SCRIPT_ONE_OFF] # one-off codemods and fix-* scripts are agent output, not tooling
|
|
227
|
+
- id: repo.docs
|
|
228
|
+
profiles: [be, fe]
|
|
229
|
+
path: "docs/{adr,runbooks,guides}/**/*.md"
|
|
230
|
+
presence: opt-in
|
|
231
|
+
tracked: tracked
|
|
232
|
+
tier: none
|
|
233
|
+
tests: none
|
|
234
|
+
rules: [HFS_ROOT_MARKDOWN_FORBIDDEN]
|
|
235
|
+
- id: repo.docs-media
|
|
236
|
+
profiles: [be, fe]
|
|
237
|
+
path: "docs/**/*.{png,svg,webp}"
|
|
238
|
+
presence: opt-in
|
|
239
|
+
tracked: tracked
|
|
240
|
+
tier: none
|
|
241
|
+
tests: none
|
|
242
|
+
budget: {bytes: 512000}
|
|
243
|
+
- id: repo.packages
|
|
244
|
+
profiles: [be, fe]
|
|
245
|
+
path: "packages/<pkg>/"
|
|
246
|
+
presence: opt-in
|
|
247
|
+
tracked: tracked
|
|
248
|
+
tier: package
|
|
249
|
+
owner: true
|
|
250
|
+
requires: [package.json, src/index.ts, tsconfig.json]
|
|
251
|
+
tests: unit-beside
|
|
252
|
+
rules: [FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT]
|
|
253
|
+
- id: repo.app-image
|
|
254
|
+
profiles: [be, fe]
|
|
255
|
+
path: "apps/<app>/Dockerfile"
|
|
256
|
+
presence: optional
|
|
257
|
+
tracked: tracked
|
|
258
|
+
tier: none
|
|
259
|
+
tests: none
|
|
260
|
+
- id: repo.dockerignore
|
|
261
|
+
profiles: [be, fe]
|
|
262
|
+
path: .dockerignore
|
|
263
|
+
presence: optional
|
|
264
|
+
tracked: tracked
|
|
265
|
+
tier: none
|
|
266
|
+
tests: none
|
|
267
|
+
|
|
268
|
+
# ----- never tracked: build output the tools need in place ---------------------------------------------
|
|
269
|
+
- id: repo.build-output
|
|
270
|
+
profiles: [be, fe]
|
|
271
|
+
path: "{**/node_modules/,dist/,apps/*/dist/,packages/*/dist/,.next/,apps/*/.next/,coverage/,test-results/,playwright-report/,**/next-env.d.ts,**/*.tsbuildinfo}"
|
|
272
|
+
presence: optional
|
|
273
|
+
tracked: ignored
|
|
274
|
+
tier: none
|
|
275
|
+
tests: none
|
|
276
|
+
- id: repo.generated
|
|
277
|
+
profiles: [be, fe]
|
|
278
|
+
path: "{**/__generated__/,apps/*/src/schema.gql}"
|
|
279
|
+
presence: optional
|
|
280
|
+
tracked: ignored
|
|
281
|
+
tier: none
|
|
282
|
+
tests: none
|
|
283
|
+
why: produced by `npm run codegen` (prebuild and pretypecheck); never committed
|
|
284
|
+
|
|
285
|
+
# ----- never in the working tree: caches, scratch, agent output ----------------------------------------
|
|
286
|
+
- id: repo.tool-cache
|
|
287
|
+
profiles: [be, fe]
|
|
288
|
+
path: "{.eslintcache,.turbo/,.scannerwork/,.sonar/,.jest-cache/,.cache/,.tools/}"
|
|
289
|
+
presence: forbidden
|
|
290
|
+
tracked: external
|
|
291
|
+
tier: none
|
|
292
|
+
tests: none
|
|
293
|
+
goesTo: "${STARCI_CACHE_HOME:-%LOCALAPPDATA%/StarCi/cache}/<repo>/<tool>/ (the canon presets set eslint --cache-location, jest cacheDirectory, turbo cacheDir, sonar.working.directory)"
|
|
294
|
+
- id: repo.agent-output
|
|
295
|
+
profiles: [be, fe]
|
|
296
|
+
path: "{report*.json,*.tmp.json,%*%,nul,.qwen*,.artifacts/,.tmp-*,*-lint.json,lf.json,lt.json,*.log,design-plans/,.gitmounts/,.dat,vi-flat.txt}"
|
|
297
|
+
presence: forbidden
|
|
298
|
+
tracked: external
|
|
299
|
+
tier: none
|
|
300
|
+
tests: none
|
|
301
|
+
goesTo: "agent scratchpad or the StarCi blob store (%LOCALAPPDATA%/StarCi/blobs), cited by {name, sha256}"
|
|
302
|
+
rules: [HFS_UNTRACKED_ROOT_ENTRY, HFS_AGENT_DATA_TRACKED]
|
|
303
|
+
- id: repo.worktrees
|
|
304
|
+
profiles: [be, fe]
|
|
305
|
+
path: "{.worktrees/,worktrees/,.starciwork/worktrees/}"
|
|
306
|
+
presence: forbidden
|
|
307
|
+
tracked: external
|
|
308
|
+
tier: none
|
|
309
|
+
tests: none
|
|
310
|
+
goesTo: "D:/starci-lanes/<project>/<lane>/ (outside every repository)"
|
|
311
|
+
- id: repo.plaintext-env
|
|
312
|
+
profiles: [be, fe]
|
|
313
|
+
path: "{.env,.env.*,.secrets/,**/*.pem,**/*.key}"
|
|
314
|
+
presence: forbidden
|
|
315
|
+
tracked: external
|
|
316
|
+
tier: none
|
|
317
|
+
tests: none
|
|
318
|
+
goesTo: ".starcistacks/<env>/secrets/<slug>.enc (BE repo); decrypted values only in memory or under %LOCALAPPDATA%/StarCi/secrets/<project>/<env>/ via *_FILE"
|
|
319
|
+
rules: [HFS_PLAINTEXT_SECRET]
|
|
320
|
+
- id: be.root-e2e
|
|
321
|
+
profiles: [be]
|
|
322
|
+
path: "e2e/"
|
|
323
|
+
presence: forbidden
|
|
324
|
+
tracked: external
|
|
325
|
+
tier: none
|
|
326
|
+
tests: none
|
|
327
|
+
goesTo: "src/tests/e2e/ for specs; operational probes move to scripts/ or are deleted"
|
|
328
|
+
|
|
329
|
+
# ----- contract snapshots (the committed machine-emitted files, because they are a cross-repo interface) --
|
|
330
|
+
- id: be.contract.graphql
|
|
331
|
+
profiles: [be]
|
|
332
|
+
path: "contracts/<app>/schema.graphql"
|
|
333
|
+
presence: opt-in
|
|
334
|
+
tracked: tracked
|
|
335
|
+
tier: none
|
|
336
|
+
tests: none
|
|
337
|
+
why: emitted by `npm run contract:emit`; CI fails when the emitted file differs (a contract, not a build artefact)
|
|
338
|
+
rules: [HFS_CONTRACT_SNAPSHOT_DRIFT]
|
|
339
|
+
- id: be.contract.openapi
|
|
340
|
+
profiles: [be]
|
|
341
|
+
path: "contracts/<app>/openapi.json"
|
|
342
|
+
presence: opt-in
|
|
343
|
+
tracked: tracked
|
|
344
|
+
tier: none
|
|
345
|
+
tests: none
|
|
346
|
+
rules: [HFS_CONTRACT_SNAPSHOT_DRIFT]
|
|
347
|
+
- id: fe.contract.copy
|
|
348
|
+
profiles: [fe]
|
|
349
|
+
path: "apps/<app>/src/modules/api/contract/<be-app>.{graphql,json}"
|
|
350
|
+
presence: opt-in
|
|
351
|
+
tracked: tracked
|
|
352
|
+
tier: none
|
|
353
|
+
tests: none
|
|
354
|
+
why: byte copy of the BE snapshot, refreshed by `npm run contract:pull`; the land gate compares hashes
|
|
355
|
+
rules: [HFS_CONTRACT_SNAPSHOT_DRIFT]
|
|
356
|
+
|
|
357
|
+
# ----- BE: apps (one slot per app kind; a new kind = a new slot, minor bump) ---------------------------
|
|
358
|
+
- id: be.app.api
|
|
359
|
+
profiles: [be]
|
|
360
|
+
path: "apps/<app>/src/"
|
|
361
|
+
appKind: api
|
|
362
|
+
presence: required # at least one api app
|
|
363
|
+
tracked: tracked
|
|
364
|
+
tier: app
|
|
365
|
+
owner: true
|
|
366
|
+
minInstances: 1
|
|
367
|
+
requires: [main.ts, app.module.ts, "<app>.composition.spec.ts"]
|
|
368
|
+
allows: [main.ts, app.module.ts, "<app>.composition.spec.ts", "<app>.options.ts"]
|
|
369
|
+
tests: unit-beside # the composition spec boots the REAL AppModule with stubbed options
|
|
370
|
+
budget: {app.module.ts: 250, main.ts: 80}
|
|
371
|
+
rules: [BE_APP_COMPOSITION_ONLY, BE_APP_BUSINESS_ROLE, BE_DEFAULT_DENY, BE_ERROR_MASKED, BE_FEATURE_NOT_COMPOSED]
|
|
372
|
+
- id: be.app.worker
|
|
373
|
+
profiles: [be]
|
|
374
|
+
path: "apps/<app>/src/"
|
|
375
|
+
appKind: worker # composes transport/schedule and transport/message modules only
|
|
376
|
+
presence: opt-in
|
|
377
|
+
tracked: tracked
|
|
378
|
+
tier: app
|
|
379
|
+
owner: true
|
|
380
|
+
requires: [main.ts, app.module.ts, "<app>.composition.spec.ts"]
|
|
381
|
+
tests: unit-beside
|
|
382
|
+
rules: [BE_APP_COMPOSITION_ONLY, BE_BACKGROUND_UNOWNED]
|
|
383
|
+
- id: be.app.migrate
|
|
384
|
+
profiles: [be]
|
|
385
|
+
path: "apps/<app>/src/"
|
|
386
|
+
appKind: migrate # the ONLY process that runs migrations (one run per connection, before api/worker start)
|
|
387
|
+
presence: required
|
|
388
|
+
requiredWhen: connections # required as soon as hfs.json declares a connection
|
|
389
|
+
tracked: tracked
|
|
390
|
+
tier: app
|
|
391
|
+
owner: true
|
|
392
|
+
requires: [main.ts, "<app>.composition.spec.ts"]
|
|
393
|
+
tests: unit-beside
|
|
394
|
+
rules: [BE_SCHEMA_AUTHORITY, BE_ENTRYPOINT_ONLY_IN_APPS]
|
|
395
|
+
- id: be.app.cli
|
|
396
|
+
profiles: [be]
|
|
397
|
+
path: "apps/<app>/src/"
|
|
398
|
+
appKind: cli
|
|
399
|
+
presence: opt-in
|
|
400
|
+
tracked: tracked
|
|
401
|
+
tier: app
|
|
402
|
+
owner: true
|
|
403
|
+
requires: [main.ts, app.module.ts, "<app>.composition.spec.ts"]
|
|
404
|
+
tests: unit-beside
|
|
405
|
+
|
|
406
|
+
# ----- BE: features ------------------------------------------------------------------------------------
|
|
407
|
+
- id: be.feature
|
|
408
|
+
profiles: [be]
|
|
409
|
+
path: "src/features/<feature>/"
|
|
410
|
+
presence: required
|
|
411
|
+
tracked: tracked
|
|
412
|
+
tier: feature
|
|
413
|
+
owner: true
|
|
414
|
+
minInstances: 1
|
|
415
|
+
requires: [index.ts, "<feature>.module.ts", application/]
|
|
416
|
+
allows: [index.ts, "<feature>.module.ts", application/, transport/, messages/] # nothing else at feature root
|
|
417
|
+
tests: unit-beside
|
|
418
|
+
budget: {files: 250, indexExports: 60} # a feature above 250 source files must be split by product capability
|
|
419
|
+
rules: [BE_FEATURE_SHAPE, BE_FEATURE_IMPORTS_FEATURE, BE_PUBLIC_SURFACE, BE_FEATURE_NOT_COMPOSED]
|
|
420
|
+
- id: be.feature.application
|
|
421
|
+
profiles: [be]
|
|
422
|
+
path: "src/features/<feature>/application/"
|
|
423
|
+
presence: required
|
|
424
|
+
tracked: tracked
|
|
425
|
+
tier: feature
|
|
426
|
+
allows: ["<action>.use-case.ts", "<action>.contracts.ts", "<action>.{command,query,handler}.ts", "*.spec.ts"]
|
|
427
|
+
forbids: [graphql/, http/, message/, schedule/, transport/] # no protocol names under application/
|
|
428
|
+
tests: unit-beside
|
|
429
|
+
rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_APPLICATION_TRANSPORT_FRAMEWORK, BE_ENTITY_IN_CONTRACT]
|
|
430
|
+
- id: be.feature.application.support
|
|
431
|
+
profiles: [be]
|
|
432
|
+
path: "src/features/<feature>/application/support/"
|
|
433
|
+
presence: optional # helpers and types local to ONE feature; same tier as application, never a shared home
|
|
434
|
+
tracked: tracked
|
|
435
|
+
tier: feature # feature tier: another feature cannot import it (features never import features)
|
|
436
|
+
allows: ["<name>.ts", "<name>.types.ts", "*.spec.ts"]
|
|
437
|
+
forbids: [graphql/, http/, message/, schedule/, transport/]
|
|
438
|
+
tests: unit-beside # a <name>.spec.ts beside every <name>.ts
|
|
439
|
+
rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_APPLICATION_TRANSPORT_FRAMEWORK, BE_FEATURE_IMPORTS_FEATURE]
|
|
440
|
+
since: 1.0.0
|
|
441
|
+
- id: be.transport.http
|
|
442
|
+
profiles: [be]
|
|
443
|
+
path: "src/features/<feature>/transport/http/"
|
|
444
|
+
presence: optional
|
|
445
|
+
tracked: tracked
|
|
446
|
+
tier: feature
|
|
447
|
+
requires: ["<feature>-http.module.ts"]
|
|
448
|
+
allows: ["<action>.controller.ts", "dto/<action>.{request,response}.ts", "<action>.mapper.ts", "*.spec.ts"]
|
|
449
|
+
tests: unit-beside
|
|
450
|
+
rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED]
|
|
451
|
+
- id: be.transport.graphql
|
|
452
|
+
profiles: [be]
|
|
453
|
+
path: "src/features/<feature>/transport/graphql/"
|
|
454
|
+
presence: optional
|
|
455
|
+
tracked: tracked
|
|
456
|
+
tier: feature
|
|
457
|
+
requires: ["<feature>-graphql.module.ts"]
|
|
458
|
+
allows: ["<action>.resolver.ts", "dto/<action>.{input,type,args}.ts", "<action>.mapper.ts", "*.spec.ts"]
|
|
459
|
+
tests: unit-beside
|
|
460
|
+
rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED]
|
|
461
|
+
- id: be.transport.message
|
|
462
|
+
profiles: [be]
|
|
463
|
+
path: "src/features/<feature>/transport/message/"
|
|
464
|
+
presence: opt-in # queue worker: consumed only by a worker app
|
|
465
|
+
tracked: tracked
|
|
466
|
+
tier: feature
|
|
467
|
+
requires: ["<feature>-message.module.ts"]
|
|
468
|
+
allows: ["<event>.consumer.ts", "<event>.message.ts", "*.spec.ts"]
|
|
469
|
+
tests: unit-beside
|
|
470
|
+
rules: [BE_BACKGROUND_UNOWNED]
|
|
471
|
+
- id: be.transport.schedule
|
|
472
|
+
profiles: [be]
|
|
473
|
+
path: "src/features/<feature>/transport/schedule/"
|
|
474
|
+
presence: opt-in # cron job, sweep or outbox publisher: consumed only by a worker app
|
|
475
|
+
tracked: tracked
|
|
476
|
+
tier: feature
|
|
477
|
+
requires: ["<feature>-schedule.module.ts"]
|
|
478
|
+
allows: ["<job>.job.ts", "*.spec.ts"]
|
|
479
|
+
tests: unit-beside
|
|
480
|
+
rules: [BE_BACKGROUND_UNOWNED]
|
|
481
|
+
- id: be.transport.websocket
|
|
482
|
+
profiles: [be]
|
|
483
|
+
path: "src/features/<feature>/transport/websocket/"
|
|
484
|
+
presence: opt-in
|
|
485
|
+
tracked: tracked
|
|
486
|
+
tier: feature
|
|
487
|
+
requires: ["<feature>-websocket.module.ts"]
|
|
488
|
+
allows: ["<channel>.gateway.ts", "dto/*.ts", "*.spec.ts"]
|
|
489
|
+
tests: unit-beside
|
|
490
|
+
since: 1.0.0
|
|
491
|
+
- id: be.feature.transport.cli
|
|
492
|
+
profiles: [be]
|
|
493
|
+
path: "src/features/<feature>/transport/cli/"
|
|
494
|
+
presence: opt-in # a command line entry: calls application use cases only, composed by an app of kind cli
|
|
495
|
+
tracked: tracked
|
|
496
|
+
tier: feature
|
|
497
|
+
requires: ["<feature>-cli.module.ts"]
|
|
498
|
+
allows: ["<command>.command.ts", "dto/*.ts", "*.spec.ts"]
|
|
499
|
+
tests: unit-beside
|
|
500
|
+
rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_FEATURE_NOT_COMPOSED, BE_ENTRYPOINT_ONLY_IN_APPS]
|
|
501
|
+
since: 1.0.0
|
|
502
|
+
|
|
503
|
+
# ----- BE: modules (three tiers, closed set) -----------------------------------------------------------
|
|
504
|
+
- id: be.domain
|
|
505
|
+
profiles: [be]
|
|
506
|
+
path: "src/modules/domain/<capability>/"
|
|
507
|
+
presence: optional
|
|
508
|
+
tracked: tracked
|
|
509
|
+
tier: domain
|
|
510
|
+
owner: true
|
|
511
|
+
requires: [index.ts]
|
|
512
|
+
allows: ["<capability>.module.ts", "<capability>.config.ts", "<capability>.options.ts", errors/, messages/, persistence/, policies/, "*.service.ts", "*.contracts.ts", "*.spec.ts"]
|
|
513
|
+
forbids: [testing/, exceptions/, utils/, helpers/, shared/]
|
|
514
|
+
tests: unit-beside
|
|
515
|
+
budget: {indexExports: 60}
|
|
516
|
+
rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_PUBLIC_SURFACE, BE_ERROR_HOME, BE_CONFIG_OWNER]
|
|
517
|
+
- id: be.errors
|
|
518
|
+
profiles: [be]
|
|
519
|
+
path: "src/modules/{domain,integrations}/<capability>/errors/"
|
|
520
|
+
presence: optional
|
|
521
|
+
tracked: tracked
|
|
522
|
+
tier: inherit
|
|
523
|
+
allows: ["<capability>.error.ts", "<name>.error.ts", "*.spec.ts"] # one family per capability, extends platform/errors DomainError
|
|
524
|
+
tests: unit-beside
|
|
525
|
+
rules: [BE_ERROR_HOME]
|
|
526
|
+
- id: be.domain.messages
|
|
527
|
+
profiles: [be]
|
|
528
|
+
path: "src/modules/{domain,integrations}/<capability>/messages/"
|
|
529
|
+
presence: optional # a capability that raises no user-facing text carries none
|
|
530
|
+
tracked: tracked
|
|
531
|
+
tier: inherit
|
|
532
|
+
requires: [index.ts]
|
|
533
|
+
allows: ["<capability>.messages.ts", "*.spec.ts"] # one keyed catalog per capability, vi and en, read through the platform/i18n MessageCatalog port
|
|
534
|
+
tests: unit-beside
|
|
535
|
+
rules: [BE_USER_COPY_LITERAL]
|
|
536
|
+
since: 1.0.0
|
|
537
|
+
- id: be.feature.messages
|
|
538
|
+
profiles: [be]
|
|
539
|
+
path: "src/features/<feature>/messages/"
|
|
540
|
+
presence: optional
|
|
541
|
+
tracked: tracked
|
|
542
|
+
tier: feature
|
|
543
|
+
requires: [index.ts]
|
|
544
|
+
allows: ["<feature>.messages.ts", "*.spec.ts"] # one keyed catalog per feature, vi and en, read through the platform/i18n MessageCatalog port
|
|
545
|
+
tests: unit-beside
|
|
546
|
+
rules: [BE_USER_COPY_LITERAL]
|
|
547
|
+
since: 1.0.0
|
|
548
|
+
- id: be.persistence
|
|
549
|
+
profiles: [be]
|
|
550
|
+
path: "src/modules/{domain,platform}/<capability>/persistence/"
|
|
551
|
+
presence: optional
|
|
552
|
+
tracked: tracked
|
|
553
|
+
tier: inherit
|
|
554
|
+
requires: [connection.ts] # export const CONNECTION = "<name from hfs.json connections>"
|
|
555
|
+
allows: ["entities/<table>.entity.ts", "migrations/<yyyyMMddHHmmss>-<kebab-name>.ts", "<name>.repository.ts", index.ts, "*.spec.ts"]
|
|
556
|
+
tests: unit-beside
|
|
557
|
+
rules: [BE_SCHEMA_OWNER, BE_SCHEMA_AUTHORITY, BE_SQL_OUTSIDE_REPOSITORY]
|
|
558
|
+
- id: be.platform
|
|
559
|
+
profiles: [be]
|
|
560
|
+
path: "src/modules/platform/<capability>/"
|
|
561
|
+
presence: required
|
|
562
|
+
tracked: tracked
|
|
563
|
+
tier: platform
|
|
564
|
+
owner: true
|
|
565
|
+
# config = EnvSource reader only; logging = LoggerPort + adapter; errors = DomainError base + one filter + GraphQL
|
|
566
|
+
# formatError with masking; primitives = isRecord, hashing, ids, cookies; clock = the injected Clock port
|
|
567
|
+
# (now()), with a FakeClock test double from @starci/jest-preset; i18n = the MessageCatalog port, a keyed
|
|
568
|
+
# lookup with named interpolation over the vi/en catalogs a capability or feature carries in its messages/,
|
|
569
|
+
# resolved through the RequestLocale provider (request: stored user preference, else Accept-Language, else
|
|
570
|
+
# vi; non-request: recipient's stored preference, else vi - see knowledge/patterns/be/messages.yaml);
|
|
571
|
+
# database when a connection exists.
|
|
572
|
+
requiredInstances: {capability: [config, logging, errors, primitives, clock, i18n]}
|
|
573
|
+
requires: [index.ts]
|
|
574
|
+
forbids: [common/, utils/, shared/, testing/] # primitives live in platform/primitives
|
|
575
|
+
tests: unit-beside
|
|
576
|
+
rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE]
|
|
577
|
+
- id: be.integrations
|
|
578
|
+
profiles: [be]
|
|
579
|
+
path: "src/modules/integrations/<provider>/"
|
|
580
|
+
presence: optional
|
|
581
|
+
tracked: tracked
|
|
582
|
+
tier: integrations
|
|
583
|
+
owner: true
|
|
584
|
+
requires: [index.ts, "<provider>.config.ts"]
|
|
585
|
+
tests: unit-beside
|
|
586
|
+
rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT]
|
|
587
|
+
- id: be.integrations.model
|
|
588
|
+
profiles: [be]
|
|
589
|
+
path: "src/modules/integrations/<provider>/model/"
|
|
590
|
+
presence: opt-in
|
|
591
|
+
tracked: tracked
|
|
592
|
+
tier: integrations
|
|
593
|
+
tests: unit-beside
|
|
594
|
+
why: an ML or LLM model client is an integration; weights and datasets are never in git
|
|
595
|
+
|
|
596
|
+
# ----- BE: tests ---------------------------------------------------------------------------------------
|
|
597
|
+
- id: be.tests.e2e
|
|
598
|
+
profiles: [be]
|
|
599
|
+
path: "src/tests/e2e/<area>/*.e2e-spec.ts"
|
|
600
|
+
presence: optional
|
|
601
|
+
tracked: tracked
|
|
602
|
+
tier: e2e
|
|
603
|
+
tests: e2e
|
|
604
|
+
rules: [HFS_CI_E2E_AUTOMATIC, BE_TEST_TOPOLOGY, BE_E2E_FLOW]
|
|
605
|
+
- id: be.tests.e2e-live
|
|
606
|
+
profiles: [be]
|
|
607
|
+
path: "src/tests/e2e/live/<area>/*.e2e-spec.ts"
|
|
608
|
+
presence: opt-in
|
|
609
|
+
tracked: tracked
|
|
610
|
+
tier: e2e
|
|
611
|
+
tests: e2e
|
|
612
|
+
why: run only by test:e2e:live (E2E_LIVE=1); excluded from plain test:e2e by folder, never by file name
|
|
613
|
+
- id: be.tests.e2e-setup
|
|
614
|
+
profiles: [be]
|
|
615
|
+
path: "src/tests/e2e/setup/"
|
|
616
|
+
presence: optional
|
|
617
|
+
tracked: tracked
|
|
618
|
+
tier: e2e
|
|
619
|
+
tests: none
|
|
620
|
+
why: boots the REAL AppModule and overrides providers; no per-lane hand-assembled modules
|
|
621
|
+
- id: be.tests.fixtures
|
|
622
|
+
profiles: [be]
|
|
623
|
+
path: "src/tests/fixtures/"
|
|
624
|
+
presence: optional
|
|
625
|
+
tracked: tracked
|
|
626
|
+
tier: fixtures
|
|
627
|
+
tests: unit-beside
|
|
628
|
+
why: typed data builders and typed mock<T>() doubles; importable by unit specs and e2e; never imports a feature
|
|
629
|
+
|
|
630
|
+
# ----- BE: work and stacks -----------------------------------------------------------------------------
|
|
631
|
+
- id: be.starciwork
|
|
632
|
+
profiles: [be]
|
|
633
|
+
path: ".starciwork/"
|
|
634
|
+
presence: required
|
|
635
|
+
tracked: tracked
|
|
636
|
+
tier: none
|
|
637
|
+
tests: none
|
|
638
|
+
requires: [.gitignore, workspace.yaml, features/index.yaml]
|
|
639
|
+
allows: [workspace.yaml, "brand/**", shell/index.yaml, "features/<feature>/index.yaml",
|
|
640
|
+
"features/<feature>/<family>/<name>/index.yaml", "features/<feature>/br/<rule>/ac/<name>/index.yaml",
|
|
641
|
+
"features/<feature>/ui/<name>/assets/<approved-file>", "features/<feature>/uat/<name>/{index,accounts,fixtures}.yaml",
|
|
642
|
+
"features/<feature>/uat/<name>/{seed,cleanup}.sql", "_resources/{identities,environments,fixtures}/<slug>/resource.yaml"]
|
|
643
|
+
forbids: [evidence/, logs/, reports/, captures/, runs/, "kernel-*/", "*.sqlite*", _derived/, "work/node records"]
|
|
644
|
+
rules: [HFS_AGENT_DATA_TRACKED, HFS_WORK_NODE_RETIRED, HFS_IDENTITY_CUSTODY]
|
|
645
|
+
- id: be.starcistacks
|
|
646
|
+
profiles: [be]
|
|
647
|
+
path: ".starcistacks/"
|
|
648
|
+
presence: required
|
|
649
|
+
tracked: tracked
|
|
650
|
+
tier: none
|
|
651
|
+
tests: none
|
|
652
|
+
requires: [application-stacks.yaml]
|
|
653
|
+
allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,k8s,terraform}/**",
|
|
654
|
+
"<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/seeds/**"]
|
|
655
|
+
forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
|
|
656
|
+
rules: [HFS_STACKS_SHAPE, HFS_PLAINTEXT_SECRET, HFS_IDENTITY_CUSTODY]
|
|
657
|
+
- id: be.sops
|
|
658
|
+
profiles: [be]
|
|
659
|
+
path: .sops.yaml
|
|
660
|
+
presence: required
|
|
661
|
+
tracked: tracked
|
|
662
|
+
tier: none
|
|
663
|
+
tests: none
|
|
664
|
+
managedBy: sops-policy
|
|
665
|
+
|
|
666
|
+
# ----- FE: apps ----------------------------------------------------------------------------------------
|
|
667
|
+
- id: fe.app.next
|
|
668
|
+
profiles: [fe]
|
|
669
|
+
path: "apps/<app>/{package.json,next.config.ts,tsconfig.json,postcss.config.mjs}"
|
|
670
|
+
appKind: next
|
|
671
|
+
presence: required
|
|
672
|
+
tracked: tracked
|
|
673
|
+
tier: none
|
|
674
|
+
minInstances: 1
|
|
675
|
+
requires: [package.json, next.config.ts, tsconfig.json, postcss.config.mjs, src/app/, src/modules/i18n/, src/modules/api/,
|
|
676
|
+
"src/app/global-error.tsx", "src/app/[locale]/layout.tsx", "src/app/[locale]/error.tsx", "src/app/[locale]/not-found.tsx", "src/app/[locale]/loading.tsx"]
|
|
677
|
+
tests: none
|
|
678
|
+
rules: [FE_APP_ISOLATION, FE_ERROR_BOUNDARY_MISSING, FE_NEXT_CONVENTIONS]
|
|
679
|
+
- id: fe.app-optional
|
|
680
|
+
profiles: [fe]
|
|
681
|
+
path: "apps/<app>/{vitest.config.ts,public/}"
|
|
682
|
+
presence: optional
|
|
683
|
+
tracked: tracked
|
|
684
|
+
tier: none
|
|
685
|
+
tests: none
|
|
686
|
+
- id: fe.route
|
|
687
|
+
profiles: [fe]
|
|
688
|
+
path: "apps/<app>/src/app/"
|
|
689
|
+
presence: required
|
|
690
|
+
tracked: tracked
|
|
691
|
+
tier: route
|
|
692
|
+
allows: ["[locale]/**/{page,layout,template,loading,error,not-found}.tsx", "[locale]/**/route.ts", global-error.tsx, "page.tsx (redirect only)",
|
|
693
|
+
"health/{live,ready}/route.ts", globals.css, "icon.*", robots.ts, sitemap.ts]
|
|
694
|
+
tests: none
|
|
695
|
+
rules: [FE_ROUTE_FILES_THIN, FE_CLIENT_BOUNDARY, FE_OWNER_REACHABLE]
|
|
696
|
+
- id: fe.source-root-pinned
|
|
697
|
+
profiles: [fe]
|
|
698
|
+
path: "apps/<app>/src/{proxy.ts,instrumentation.ts,instrumentation-client.ts}"
|
|
699
|
+
presence: optional
|
|
700
|
+
tracked: tracked
|
|
701
|
+
tier: route
|
|
702
|
+
tests: none
|
|
703
|
+
rules: [FE_NEXT_CONVENTIONS] # middleware.ts is refused on Next >= 16
|
|
704
|
+
- id: fe.feature
|
|
705
|
+
profiles: [fe]
|
|
706
|
+
path: "apps/<app>/src/features/{pages,layouts,overlays}/<name>/"
|
|
707
|
+
presence: required
|
|
708
|
+
tracked: tracked
|
|
709
|
+
tier: feature
|
|
710
|
+
owner: true
|
|
711
|
+
requires: [index.tsx]
|
|
712
|
+
allows: [index.tsx, component.tsx, classNames.ts, "*.spec.tsx", "*.spec.ts"]
|
|
713
|
+
tests: unit-beside
|
|
714
|
+
budget: {component.tsx: 300, index.tsx: 200}
|
|
715
|
+
rules: [FE_SIZE_BUDGET, FE_OWNER_REACHABLE, FE_I18N_LITERAL]
|
|
716
|
+
- id: fe.components
|
|
717
|
+
profiles: [fe]
|
|
718
|
+
path: "apps/<app>/src/components/{blocks,composites,branches,leaves}/<name>/"
|
|
719
|
+
presence: optional
|
|
720
|
+
tracked: tracked
|
|
721
|
+
tier: components
|
|
722
|
+
owner: true
|
|
723
|
+
layers: [blocks, composites, branches, leaves] # a layer imports only the layers after it
|
|
724
|
+
requires: [index.tsx]
|
|
725
|
+
allows: [index.tsx, component.tsx, classNames.ts, "*.spec.tsx", "*.spec.ts"]
|
|
726
|
+
tests: unit-beside
|
|
727
|
+
budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
|
|
728
|
+
rules: [FE_CONNECTED_BLOCK_RENDER_PAIR, FE_I18N_LITERAL, FE_NATIVE_FORM_CONTROL, FE_CLIENT_BOUNDARY]
|
|
729
|
+
- id: fe.hooks
|
|
730
|
+
profiles: [fe]
|
|
731
|
+
path: "apps/<app>/src/hooks/<domain>/"
|
|
732
|
+
presence: optional
|
|
733
|
+
tracked: tracked
|
|
734
|
+
tier: hooks
|
|
735
|
+
owner: true # a domain is an owner: other domains import it through index.ts only
|
|
736
|
+
requires: [index.ts]
|
|
737
|
+
allows: [index.ts, "use<name>.ts", "<domain>.shared.ts", "*.spec.ts"] # React hooks only; <domain>.shared.ts = sanctioned non-hook helpers
|
|
738
|
+
tests: unit-beside
|
|
739
|
+
budget: {file: 200}
|
|
740
|
+
rules: [FE_HOOKS_ARE_HOOKS, FE_DATA_FRESHNESS]
|
|
741
|
+
- id: fe.modules
|
|
742
|
+
profiles: [fe]
|
|
743
|
+
path: "apps/<app>/src/modules/<capability>/"
|
|
744
|
+
presence: required
|
|
745
|
+
tracked: tracked
|
|
746
|
+
tier: modules
|
|
747
|
+
owner: true
|
|
748
|
+
requiredInstances: {capability: [api, config, i18n, routes]}
|
|
749
|
+
requires: [index.ts]
|
|
750
|
+
tests: unit-beside
|
|
751
|
+
budget: {file: 400}
|
|
752
|
+
rules: [FE_ENV_OWNER, FE_TRANSPORT_OWNER]
|
|
753
|
+
- id: fe.modules.api
|
|
754
|
+
profiles: [fe]
|
|
755
|
+
path: "apps/<app>/src/modules/api/"
|
|
756
|
+
presence: required
|
|
757
|
+
tracked: tracked
|
|
758
|
+
tier: transport
|
|
759
|
+
requires: [index.ts, client.ts, outcome.ts] # client.ts is the only fetch; outcome.ts the only result vocabulary
|
|
760
|
+
allows: [client.ts, outcome.ts, "<domain>/read-*.ts", "<domain>/*.graphql", "<domain>/<domain>.mapper.ts", contract/, __generated__/, "*.spec.ts"]
|
|
761
|
+
tests: unit-beside
|
|
762
|
+
rules: [FE_TRANSPORT_OWNER, FE_HTTP_STATUS_COLLAPSE, FE_WIRE_GENERATED]
|
|
763
|
+
- id: fe.modules.config
|
|
764
|
+
profiles: [fe]
|
|
765
|
+
path: "apps/<app>/src/modules/config/"
|
|
766
|
+
presence: required
|
|
767
|
+
tracked: tracked
|
|
768
|
+
tier: foundation
|
|
769
|
+
tests: unit-beside
|
|
770
|
+
why: the only reader of process.env and NEXT_PUBLIC_*; fails fast, no localhost fallback
|
|
771
|
+
- id: fe.modules.i18n
|
|
772
|
+
profiles: [fe]
|
|
773
|
+
path: "apps/<app>/src/modules/i18n/"
|
|
774
|
+
presence: required
|
|
775
|
+
tracked: tracked
|
|
776
|
+
tier: foundation
|
|
777
|
+
requires: [config.ts, routing.ts, navigation.ts, request.ts, messages/]
|
|
778
|
+
tests: unit-beside
|
|
779
|
+
rules: [FE_I18N_PLACEMENT, FE_I18N_CATALOG]
|
|
780
|
+
- id: fe.modules.brand
|
|
781
|
+
profiles: [fe]
|
|
782
|
+
path: "apps/<app>/src/modules/brand/brand.css"
|
|
783
|
+
presence: optional
|
|
784
|
+
tracked: tracked
|
|
785
|
+
tier: none
|
|
786
|
+
tests: none
|
|
787
|
+
why: the ONLY app file that may hold colour values; sets grammar brand tokens only, light and dark
|
|
788
|
+
rules: [FE_STYLE_TOKEN_ONLY]
|
|
789
|
+
- id: fe.modules.routes
|
|
790
|
+
profiles: [fe]
|
|
791
|
+
path: "apps/<app>/src/modules/routes/"
|
|
792
|
+
presence: required
|
|
793
|
+
tracked: tracked
|
|
794
|
+
tier: foundation
|
|
795
|
+
tests: unit-beside
|
|
796
|
+
why: every href builder; FE_HREF_RESOLVES checks each against app/
|
|
797
|
+
- id: fe.modules.types
|
|
798
|
+
profiles: [fe]
|
|
799
|
+
path: "apps/<app>/src/modules/types/"
|
|
800
|
+
presence: optional
|
|
801
|
+
tracked: tracked
|
|
802
|
+
tier: foundation
|
|
803
|
+
tests: unit-beside
|
|
804
|
+
why: shared types with no runtime behaviour; a component may import them
|
|
805
|
+
- id: fe.package.ui
|
|
806
|
+
profiles: [fe]
|
|
807
|
+
path: "packages/<family>-ui/"
|
|
808
|
+
presence: opt-in
|
|
809
|
+
tracked: tracked
|
|
810
|
+
tier: package
|
|
811
|
+
owner: true
|
|
812
|
+
requires: [package.json, src/index.ts, tsconfig.json]
|
|
813
|
+
tests: unit-beside
|
|
814
|
+
why: built to dist with explicit named exports; only composites, branches and leaves; no unused export; no colour literal
|
|
815
|
+
rules: [FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT, FE_STYLE_TOKEN_ONLY]
|
|
816
|
+
- id: fe.e2e
|
|
817
|
+
profiles: [fe]
|
|
818
|
+
path: "e2e/<area>/*.e2e-spec.ts"
|
|
819
|
+
presence: optional
|
|
820
|
+
tracked: tracked
|
|
821
|
+
tier: e2e
|
|
822
|
+
tests: e2e
|
|
823
|
+
requires: [/playwright.config.ts, /tsconfig.e2e.json]
|
|
824
|
+
rules: [FE_E2E_SHAPE, HFS_CI_E2E_AUTOMATIC]
|
|
825
|
+
- id: fe.e2e-support
|
|
826
|
+
profiles: [fe]
|
|
827
|
+
path: "e2e/{support,fixtures}/"
|
|
828
|
+
presence: optional
|
|
829
|
+
tracked: tracked
|
|
830
|
+
tier: e2e
|
|
831
|
+
tests: none
|
|
832
|
+
|
|
833
|
+
# Checks that read this manifest (all ship from .claude; none keeps its own path list)
|
|
834
|
+
consumers:
|
|
835
|
+
- scripts/lib/hfs-slots.mjs # the loader every consumer below goes through
|
|
836
|
+
- scripts/checks/hfs/slot-check.mjs # HFS_SLOT_* / tracked / external / managed drift
|
|
837
|
+
- scripts/checks/architecture.mjs # tiers, direction matrix, cycles, reachability
|
|
838
|
+
- packages/eslint/be starciBeConfig({hfs}) # file globs for rules come from slots
|
|
839
|
+
- packages/eslint/fe starciFeConfig({hfs})
|
|
840
|
+
- packages/stylelint-canon # colour and brand allowances from fe.modules.brand
|
|
841
|
+
- scripts/hfs/sync.mjs # renders managedBy templates
|
|
842
|
+
- modules/kernel/failure-codes.yaml # every rule id listed above has a Vietnamese why
|