@starci/eslint-canon-fe 6.0.1 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +3 -5
  3. package/client-boundary.mjs +6 -8
  4. package/comments.mjs +38 -15
  5. package/docs/brand-values.md +2 -2
  6. package/docs/client-boundary.md +10 -10
  7. package/docs/env-owner.md +4 -4
  8. package/docs/formatting.md +2 -2
  9. package/docs/grammar-boundary.md +2 -2
  10. package/docs/hooks-folder.md +2 -2
  11. package/docs/hygiene.md +8 -8
  12. package/docs/lint-escape-hatch.md +2 -2
  13. package/docs/lists.md +6 -6
  14. package/docs/native-controls.md +6 -6
  15. package/docs/next-conventions.md +20 -20
  16. package/docs/size-and-state-budget.md +6 -6
  17. package/docs/size-growth.md +2 -2
  18. package/docs/status-colors.md +2 -2
  19. package/docs/translation.md +2 -2
  20. package/docs/transport.md +16 -16
  21. package/docs/type-safety.md +8 -8
  22. package/env-owner.mjs +2 -3
  23. package/file-layout.mjs +3 -43
  24. package/formatting.mjs +2 -2
  25. package/grammar-boundary.mjs +1 -2
  26. package/hooks-folder.mjs +6 -7
  27. package/index.mjs +3 -27
  28. package/lib/config.mjs +6 -22
  29. package/lib/scope.mjs +3 -12
  30. package/lib/why.mjs +0 -65
  31. package/lint-escape-hatch.mjs +3 -3
  32. package/lists.mjs +0 -4
  33. package/loading.mjs +2 -2
  34. package/native-controls.mjs +0 -4
  35. package/next-conventions.mjs +3 -7
  36. package/package.json +1 -1
  37. package/props-and-slots.mjs +4 -5
  38. package/runtime/knowledge/hfs/slots.yaml +148 -109
  39. package/runtime/scripts/lib/hfs-allows.mjs +15 -0
  40. package/runtime/scripts/lib/hfs-slots.mjs +13 -6
  41. package/runtime/scripts/lib/language.mjs +107 -0
  42. package/served-locale.mjs +2 -2
  43. package/shape-slot.mjs +2 -3
  44. package/size-and-state-budget.mjs +1 -4
  45. package/size-growth.mjs +1 -2
  46. package/status-colors.mjs +0 -2
  47. package/tokens.mjs +1 -1
  48. package/translation.mjs +1 -29
  49. package/transport.mjs +4 -8
  50. package/docs/e2e-shape.md +0 -161
  51. package/docs/spec-quality.md +0 -147
  52. package/e2e-shape.mjs +0 -345
  53. package/spec-quality.mjs +0 -318
@@ -84,7 +84,6 @@ tiers:
84
84
  foundation: {mayImport: [foundation, package], acyclic: true} # modules/{config,i18n,routes,types}
85
85
  modules: {mayImport: [foundation, modules, transport, package], acyclic: true}
86
86
  transport: {mayImport: [foundation, modules, package]} # modules/api: the one fetch
87
- e2e: {mayImport: [e2e, package]} # black box: no app source
88
87
  package: {mayImport: [package]}
89
88
  crossOwner: every import across owners targets the owner's public entry (index.ts or index.tsx); never export *.
90
89
  crossApp: apps never import each other; shared code is a packages/<pkg> slot.
@@ -109,6 +108,7 @@ ruleParams:
109
108
  http: [platform/http]
110
109
  https: [platform/http]
111
110
  cache-manager: [integrations/cache, integrations/redis]
111
+ "@nestjs/cache-manager": [integrations/cache, integrations/redis] # the CACHE_MANAGER token: caching goes through the cache capability (retired local rule must-use-cache-service)
112
112
  redis: [integrations/cache, integrations/redis]
113
113
  ioredis: [integrations/cache, integrations/redis]
114
114
  bullmq: [platform/messaging]
@@ -126,11 +126,31 @@ ruleParams:
126
126
  "@nestjs/config": []
127
127
  dotenv: []
128
128
  "@nestjs/event-emitter": []
129
+ # R48 BE_SPEC_QUALITY spec-infra-double-from-kit: in a `<name>.service.spec.ts` a provider `{ provide: TOKEN, useValue: X }` takes X from
130
+ # the kit named here (the package root of `kit`). The token name is normalised to UPPER_SNAKE (`EntityManager` -> ENTITY_MANAGER), the
131
+ # FIRST entry whose `token` regex matches decides, and `fallback` decides for every other token. `forms` say what X may be:
132
+ # call = `double(...)`, new = `new Double(...)`, curried = `double(...)(...)`, object = a plain object literal (REAL values),
133
+ # primitive = a string/number/boolean literal, array = an array literal (multi-providers of doubles). A const bound to such a value counts.
134
+ specDoubles:
135
+ kit: "@starci/jest-preset"
136
+ doubles:
137
+ - {token: "(?:^|_)OPTIONS$", double: builder, forms: [curried, object]}
138
+ - {token: "(?:^|_)ENTITY_MANAGER$", double: mockEntityManager, forms: [call]}
139
+ - {token: "(?:^|_)TRANSACTION(?:_RUNNER)?$", double: fakeTransaction, forms: [call]}
140
+ - {token: "(?:^|_)CLOCK$", double: FakeClock, forms: [new]}
141
+ - {token: "(?:^|_)OUTBOX(?:_|$)", double: recordingOutbox, forms: [call]}
142
+ - {token: "(?:^|_)CACHE(?:_|$)", double: fakeCache, forms: [call]}
143
+ - {token: "(?:^|_)(?:LOCK|LEASE|FENCE|HOLD)(?:_|$)", double: fakeLock, forms: [call]}
144
+ - {token: "(?:^|_)(?:IDS|ID_GENERATOR|ID_FACTORY)$", double: fakeIds, forms: [call]}
145
+ fallback: {double: mock, forms: [call, primitive, array]}
129
146
  # R89 BE_SOURCE_FORM: the closed role-suffix vocabulary of a source file name (<kebab-name>.<suffix>.ts; BE-CONVENTION 1.15)
130
147
  # and the suffixes that are never a role. index.ts, main.ts, app.module.ts and the migrations of be.persistence are the
131
148
  # 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]
149
+ suffixes: [module, module-definition, decorators, options, config, connection, service, command, query, handler, contracts, port, resolver, controller, gateway, consumer, job, cli, input, type, args, request, response, mapper, policy, entity, sql, rows, error, messages, log-events, cache-keys, guard, interceptor, filter, client, spec, integration-spec, e2e-spec, contract-spec, global-setup, builder]
150
+ # R47 BE_CONTRACT_UNGUARDED: the world's shape vocabulary. `helper` names the function a contract spec imports to erase the values of a JSON
151
+ # payload (`shapeOf`); a contract spec proves a fake's fixtures by asserting helper(real) equals helper(fixture).
152
+ contractShape: {helper: shapeOf}
153
+ bannedSuffixes: [use-case, repository, fixture, factory, store, worker, scheduler, cron, listener, dto, util, utils, helper, helpers, rules, types, constants, int-spec, harness-spec, test]
134
154
  fe:
135
155
  # Slot budgets (component.tsx 300, hooks 200, modules 400) are stricter than this cap where they apply.
136
156
  fileLines: {soft: 500, hardGrowth: true}
@@ -248,12 +268,11 @@ slots:
248
268
  profiles: [fe]
249
269
  # Managed files (hfs sync renders them, hfs check compares them): the tool configuration of a front end. Each is a
250
270
  # 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}"
271
+ # names the preset and nothing else (each app's own tsconfig.json, in fe.app.next, carries its aliases and include; there is
272
+ # no test tsconfig: a front end has no tests, FE_NO_TESTS); eslint.config.mjs is the one-line starciFeConfig call;
273
+ # stylelint.config.mjs is the one-line starciStylelintConfig call; .prettierrc names @starci/prettier-config;
274
+ # .prettierignore is the template. A front end has no test configuration at all (FE_NO_TESTS).
275
+ path: "{tsconfig.json,eslint.config.mjs,stylelint.config.mjs,.prettierrc,.prettierignore}"
257
276
  presence: required
258
277
  tracked: tracked
259
278
  tier: none
@@ -262,18 +281,15 @@ slots:
262
281
  rules: [HFS_MANAGED_FILE_DRIFT, HFS_TOOL_CONFIG_LOCAL, HFS_RULE_OFF_WITHOUT_REPLACEMENT, HFS_TS_STRICT]
263
282
  - id: fe.tool-config-repo
264
283
  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}
284
+ # The one tool file a preset cannot render because it carries the repository's own facts: turbo.json (its task graph).
285
+ path: turbo.json
270
286
  presence: optional
271
287
  tracked: tracked
272
288
  tier: none
273
289
  tests: none
274
290
  - id: fe.tool-config-local
275
291
  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*}"
292
+ path: "{.eslintrc,.eslintrc.*,.eslintignore,eslint.config.js,eslint.config.cjs,eslint.config.ts,eslint.config.mts,eslint.config.cts,.stylelintrc,.stylelintrc.*,.stylelintignore,stylelint.config.js,stylelint.config.cjs,stylelint.config.ts,stylelint.config.json,prettier.config.*,.prettierrc.*,lint-staged.config.*,.lintstagedrc*}"
277
293
  presence: forbidden
278
294
  tracked: external
279
295
  tier: none
@@ -282,7 +298,7 @@ slots:
282
298
  rules: [HFS_TOOL_CONFIG_LOCAL]
283
299
  - id: repo.quality-config
284
300
  profiles: [be, fe]
285
- path: "{sonar-project.properties,codecov.yml}"
301
+ path: sonar-project.properties
286
302
  presence: required
287
303
  tracked: tracked
288
304
  tier: none
@@ -308,7 +324,7 @@ slots:
308
324
  managedBy: ci-workflows
309
325
  rules: [HFS_CI_E2E_AUTOMATIC, HFS_CI_MISSING_CANON, HFS_MANAGED_FILE_DRIFT]
310
326
  - id: repo.ci-e2e
311
- profiles: [be, fe]
327
+ profiles: [be]
312
328
  path: ".github/workflows/e2e.yml"
313
329
  presence: optional # on: workflow_dispatch only
314
330
  tracked: tracked
@@ -325,12 +341,15 @@ slots:
325
341
  tests: none
326
342
  - id: repo.scripts
327
343
  profiles: [be, fe]
328
- path: "scripts/<name>.mjs"
344
+ # The repository's operational scripts only (`*.mjs`, `*.cjs`, `*.ps1`, `*.sh`), or nothing: an empty folder is `scripts/.gitkeep`, which hfs sync --init writes
345
+ # when the folder is missing. A spec or a test (BE_SPEC_PLACEMENT, FE_NO_TESTS), a check or lint source (`check-*`, `eslint-local-rules*`, a local eslint
346
+ # plugin: HFS_REPO_LOCAL_CHECK) and anything that re-implements a canon check is not an operational script and has no place here.
347
+ path: "scripts/{.gitkeep,*.mjs,*.cjs,*.ps1,*.sh}"
329
348
  presence: optional
330
349
  tracked: tracked
331
350
  tier: none
332
351
  tests: none
333
- rules: [HFS_SCRIPT_ONE_OFF] # one-off codemods and fix-* scripts are agent output, not tooling
352
+ rules: [HFS_SCRIPT_ONE_OFF, BE_SPEC_PLACEMENT, HFS_REPO_LOCAL_CHECK] # one-off codemods and fix-* scripts are agent output, not tooling
334
353
  - id: repo.docs
335
354
  profiles: [be, fe]
336
355
  path: "docs/{adr,runbooks,guides}/**/*.md"
@@ -375,7 +394,7 @@ slots:
375
394
  # ----- never tracked: build output the tools need in place ---------------------------------------------
376
395
  - id: repo.build-output
377
396
  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}"
397
+ path: "{**/node_modules/,dist/,apps/*/dist/,packages/*/dist/,.next/,apps/*/.next/,coverage/,reports/,test-results/,**/next-env.d.ts,**/*.tsbuildinfo}"
379
398
  presence: optional
380
399
  tracked: ignored
381
400
  tier: none
@@ -450,6 +469,7 @@ slots:
450
469
  tracked: tracked
451
470
  tier: none
452
471
  tests: none
472
+ why: emitted by `npm run contract:emit` (`hfs emit-contracts`, never written by hand) from the app's typed operation table `apps/<app>/src/operations.ts` (canon BE-OPERATIONS-1); CI fails when the emitted file differs
453
473
  rules: [HFS_CONTRACT_SNAPSHOT_DRIFT]
454
474
  - id: fe.contract.copy
455
475
  profiles: [fe]
@@ -471,9 +491,9 @@ slots:
471
491
  tier: app
472
492
  owner: true
473
493
  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
494
+ requires: [main.ts, app.module.ts]
495
+ allows: [main.ts, app.module.ts, "<app>.options.ts", operations.ts]
496
+ tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
477
497
  budget: {app.module.ts: 250, main.ts: 80}
478
498
  rules: [BE_APP_COMPOSITION_ONLY, BE_APP_BUSINESS_ROLE, BE_DEFAULT_DENY, BE_ERROR_MASKED, BE_FEATURE_NOT_COMPOSED]
479
499
  - id: be.app.worker
@@ -484,8 +504,9 @@ slots:
484
504
  tracked: tracked
485
505
  tier: app
486
506
  owner: true
487
- requires: [main.ts, app.module.ts, "<app>.composition.spec.ts"]
488
- tests: unit-beside
507
+ requires: [main.ts, app.module.ts]
508
+ allows: [main.ts, app.module.ts, "<app>.options.ts"]
509
+ tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
489
510
  rules: [BE_APP_COMPOSITION_ONLY, BE_BACKGROUND_UNOWNED]
490
511
  - id: be.app.migrate
491
512
  profiles: [be]
@@ -496,8 +517,9 @@ slots:
496
517
  tracked: tracked
497
518
  tier: app
498
519
  owner: true
499
- requires: [main.ts, "<app>.composition.spec.ts"]
500
- tests: unit-beside
520
+ requires: [main.ts]
521
+ allows: [main.ts, "<app>.options.ts"]
522
+ tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
501
523
  rules: [BE_SCHEMA_AUTHORITY, BE_ENTRYPOINT_ONLY_IN_APPS]
502
524
  - id: be.app.cli
503
525
  profiles: [be]
@@ -507,8 +529,9 @@ slots:
507
529
  tracked: tracked
508
530
  tier: app
509
531
  owner: true
510
- requires: [main.ts, app.module.ts, "<app>.composition.spec.ts"]
511
- tests: unit-beside
532
+ requires: [main.ts, app.module.ts]
533
+ allows: [main.ts, app.module.ts, "<app>.options.ts"]
534
+ tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
512
535
 
513
536
  # ----- BE: features ------------------------------------------------------------------------------------
514
537
  - id: be.feature
@@ -521,7 +544,7 @@ slots:
521
544
  minInstances: 1
522
545
  requires: [index.ts, "<feature>.module.ts", application/]
523
546
  allows: [index.ts, "<feature>.module.ts", application/, transport/, messages/] # nothing else at feature root
524
- tests: unit-beside
547
+ tests: none
525
548
  budget: {files: 250, indexExports: 60} # a feature above 250 source files must be split by product capability
526
549
  rules: [BE_FEATURE_SHAPE, BE_FEATURE_IMPORTS_FEATURE, BE_PUBLIC_SURFACE, BE_FEATURE_NOT_COMPOSED]
527
550
  - id: be.feature.application
@@ -530,9 +553,9 @@ slots:
530
553
  presence: required
531
554
  tracked: tracked
532
555
  tier: feature
533
- allows: ["<action>.{command,query,handler,contracts}.ts", "*.spec.ts"]
556
+ allows: ["<action>.{command,query,handler,contracts}.ts"]
534
557
  forbids: [graphql/, http/, message/, schedule/, transport/] # no protocol names under application/
535
- tests: unit-beside
558
+ tests: none
536
559
  rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_APPLICATION_TRANSPORT_FRAMEWORK, BE_ENTITY_IN_CONTRACT, BE_CQRS_SHAPE]
537
560
  - id: be.feature.application.support
538
561
  profiles: [be]
@@ -540,9 +563,9 @@ slots:
540
563
  presence: optional # helpers and types local to ONE feature; same tier as application, never a shared home
541
564
  tracked: tracked
542
565
  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
566
+ allows: ["<name>.<role>.ts"] # <role> from ruleParams.be.suffixes; no <name>.types.ts
544
567
  forbids: [graphql/, http/, message/, schedule/, transport/]
545
- tests: unit-beside # a <name>.spec.ts beside every <name>.ts
568
+ tests: none
546
569
  rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_APPLICATION_TRANSPORT_FRAMEWORK, BE_FEATURE_IMPORTS_FEATURE]
547
570
  since: 1.0.0
548
571
  - id: be.transport.http
@@ -552,8 +575,8 @@ slots:
552
575
  tracked: tracked
553
576
  tier: feature
554
577
  requires: ["<feature>-http.module.ts"]
555
- allows: ["<action>.controller.ts", "dto/<action>.{request,response}.ts", "<action>.mapper.ts", "*.spec.ts"]
556
- tests: unit-beside
578
+ allows: ["<action>.controller.ts", "dto/<action>.{request,response}.ts", "<action>.mapper.ts"]
579
+ tests: none
557
580
  composedBy: [api]
558
581
  rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED, BE_TRANSPORT_SHAPE]
559
582
  - id: be.transport.graphql
@@ -563,8 +586,8 @@ slots:
563
586
  tracked: tracked
564
587
  tier: feature
565
588
  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
589
+ allows: ["<action>.resolver.ts", "dto/<action>.{input,type,args}.ts", "<action>.mapper.ts"]
590
+ tests: none
568
591
  composedBy: [api]
569
592
  rules: [BE_DEFAULT_DENY, BE_INPUT_BOUNDED, BE_TRANSPORT_SHAPE]
570
593
  - id: be.transport.message
@@ -574,8 +597,8 @@ slots:
574
597
  tracked: tracked
575
598
  tier: feature
576
599
  requires: ["<feature>-message.module.ts"]
577
- allows: ["<event>.consumer.ts", "<event>.message.ts", "*.spec.ts"]
578
- tests: unit-beside
600
+ allows: ["<event>.consumer.ts"] # BE-CONVENTION 1.6: transport/message holds only consumers; a .message.ts is outside the closed suffix list
601
+ tests: none
579
602
  composedBy: [worker]
580
603
  rules: [BE_BACKGROUND_UNOWNED]
581
604
  - id: be.transport.schedule
@@ -585,8 +608,8 @@ slots:
585
608
  tracked: tracked
586
609
  tier: feature
587
610
  requires: ["<feature>-schedule.module.ts"]
588
- allows: ["<job>.job.ts", "*.spec.ts"]
589
- tests: unit-beside
611
+ allows: ["<job>.job.ts"]
612
+ tests: none
590
613
  composedBy: [worker]
591
614
  rules: [BE_BACKGROUND_UNOWNED]
592
615
  - id: be.transport.websocket
@@ -596,8 +619,8 @@ slots:
596
619
  tracked: tracked
597
620
  tier: feature
598
621
  requires: ["<feature>-websocket.module.ts"]
599
- allows: ["<channel>.gateway.ts", "dto/*.ts", "*.spec.ts"]
600
- tests: unit-beside
622
+ allows: ["<channel>.gateway.ts", "dto/*.ts"]
623
+ tests: none
601
624
  composedBy: [api]
602
625
  since: 1.0.0
603
626
  - id: be.feature.transport.cli
@@ -607,8 +630,8 @@ slots:
607
630
  tracked: tracked
608
631
  tier: feature
609
632
  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
633
+ allows: ["<name>.cli.ts", "dto/*.ts"] # .cli.ts: .command.ts is a CQRS message (HFS delta 3)
634
+ tests: none
612
635
  composedBy: [cli]
613
636
  rules: [BE_APPLICATION_IMPORTS_TRANSPORT, BE_FEATURE_NOT_COMPOSED, BE_ENTRYPOINT_ONLY_IN_APPS]
614
637
  since: 1.0.0
@@ -622,9 +645,9 @@ slots:
622
645
  tier: domain
623
646
  owner: true
624
647
  requires: [index.ts]
625
- allows: ["<capability>.module.ts", "<capability>.config.ts", "<capability>.options.ts", errors/, messages/, persistence/, policies/, "*.service.ts", "*.contracts.ts", "*.spec.ts"]
648
+ allows: ["<capability>.module.ts", "<capability>.config.ts", "<capability>.options.ts", errors/, messages/, persistence/, policies/, "*.service.ts", "*.service.spec.ts", "*.contracts.ts"]
626
649
  forbids: [testing/, exceptions/, utils/, helpers/, shared/]
627
- tests: unit-beside
650
+ tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts, nothing else
628
651
  budget: {indexExports: 60}
629
652
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_PUBLIC_SURFACE, BE_ERROR_HOME, BE_CONFIG_OWNER]
630
653
  - id: be.errors
@@ -633,17 +656,17 @@ slots:
633
656
  presence: optional
634
657
  tracked: tracked
635
658
  tier: inherit
636
- allows: ["<capability>.error.ts", "<name>.error.ts", "*.spec.ts"] # one family per capability, extends platform/errors DomainError
637
- tests: unit-beside
659
+ allows: ["<capability>.error.ts", "<name>.error.ts"] # one family per capability, extends platform/errors DomainError
660
+ tests: none
638
661
  rules: [BE_ERROR_HOME]
639
662
  - id: be.domain.messages
640
663
  profiles: [be]
641
- path: "src/modules/{domain,integrations}/<capability>/messages/"
642
- presence: optional # a capability that raises no user-facing text carries none
664
+ path: "src/modules/{domain,platform,integrations}/<capability>/messages/"
665
+ presence: optional # a capability (any tier) that raises no user-facing text carries none
643
666
  tracked: tracked
644
667
  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
668
+ allows: ["<capability>.messages.ts"] # one keyed catalog per capability, vi and en, read through the platform/i18n MessageCatalog port
669
+ tests: none
647
670
  rules: [BE_USER_COPY_LITERAL]
648
671
  since: 1.0.0
649
672
  - id: be.feature.messages
@@ -652,8 +675,8 @@ slots:
652
675
  presence: optional
653
676
  tracked: tracked
654
677
  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
678
+ allows: ["<feature>.messages.ts"] # one keyed catalog per feature, vi and en, read through the platform/i18n MessageCatalog port
679
+ tests: none
657
680
  rules: [BE_USER_COPY_LITERAL]
658
681
  since: 1.0.0
659
682
  - id: be.persistence
@@ -662,10 +685,10 @@ slots:
662
685
  presence: optional
663
686
  tracked: tracked
664
687
  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"]
688
+ requires: [connection.ts] # the capability's <c>Entities / <c>Migrations arrays (the owner index re-exports the two arrays by name); no CONNECTION alias: the connection is the one the apps register the arrays on
689
+ allows: ["entities/<table>.entity.ts", "migrations/<epochMs13>-<kebab-name>.ts", "<name>.sql.ts", "<name>.rows.ts", connection.ts]
667
690
  forbids: [index.ts, "<name>.repository.ts"]
668
- tests: unit-beside
691
+ tests: none
669
692
  rules: [BE_SCHEMA_OWNER, BE_SCHEMA_AUTHORITY, BE_SQL_OUTSIDE_PERSISTENCE]
670
693
  - id: be.platform
671
694
  profiles: [be]
@@ -684,7 +707,7 @@ slots:
684
707
  requiredInstances: {capability: [config, logging, errors, primitives, clock, i18n]}
685
708
  requires: [index.ts]
686
709
  forbids: [common/, utils/, shared/, testing/] # primitives live in platform/primitives
687
- tests: unit-beside
710
+ tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts, nothing else
688
711
  budget: {indexExports: 60}
689
712
  rules: [BE_TIER_DIRECTION, ARCH_OWNER_CYCLE, BE_MODULE_SHAPE]
690
713
  - id: be.integrations
@@ -695,7 +718,7 @@ slots:
695
718
  tier: integrations
696
719
  owner: true
697
720
  requires: [index.ts, "<provider>.config.ts"]
698
- tests: unit-beside
721
+ tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts, nothing else
699
722
  budget: {indexExports: 60}
700
723
  rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT]
701
724
  - id: be.integrations.model
@@ -704,10 +727,10 @@ slots:
704
727
  presence: opt-in
705
728
  tracked: tracked
706
729
  tier: integrations
707
- tests: unit-beside
730
+ tests: none
708
731
  why: an ML or LLM model client is an integration; weights and datasets are never in git
709
732
 
710
- # ----- BE: tests (owner test layout 2026-09-30). Unit specs sit beside their subject (<name>.spec.ts); everything else
733
+ # ----- BE: tests (owner test layout 2026-09-30). Unit specs are <name>.service.spec.ts beside their service and nothing else; everything else
711
734
  # lives under src/tests/, one folder per kind, and the folder and the file suffix always agree (a file whose suffix does
712
735
  # not match its folder matches no slot: HFS_SLOT_UNDECLARED).
713
736
  - id: be.tests.world
@@ -716,25 +739,57 @@ slots:
716
739
  presence: optional
717
740
  tracked: tracked
718
741
  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)
742
+ allows: [global-setup.ts, global-teardown.ts, use-test-world.ts, fakes/] # jest globalSetup/globalTeardown default-export (Jest API); plus root files with a role suffix (test-world-files, R47)
720
743
  tests: none
721
744
  why: >-
722
745
  the ONLY test infrastructure location, shared by integration and e2e: global-setup.ts starts the shared
723
746
  infrastructure (containers) and runs apps/migrate's exported bootstrap once; use-test-world.ts exports
724
747
  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
748
+ world.db.<connection> (the shared EntityManager), world.infra.<service> (latency/cut/restore on a REAL service of the repository's
749
+ own stack, `.starcistacks/<env>`, each behind toxiproxy), world.fake.<provider> (a network-edge fake of an external SaaS started by the world, with
726
750
  failNext/replayWebhook/delay), world.waitFor; fakes/<provider>/ holds the fake servers and payload fixtures, kit/ the inlined test helpers, and the world root may hold role-suffixed helpers (<name>.client.ts, <name>.contracts.ts, ...). Nothing is
727
751
  overridden in the DI container. It is the only test location that may import typeorm's DataSource or testcontainers,
728
752
  call migrate/runMigrations/synchronize, or write process.env.
753
+ - id: be.tests.world.kit
754
+ profiles: [be]
755
+ path: "src/tests/world/kit/"
756
+ presence: optional
757
+ tracked: tracked
758
+ tier: e2e
759
+ allows: ["<name>.ts"] # plain kebab names, like platform/primitives (BE-CONVENTION 1.11 names primitives/time.ts): the world's inlined test helpers (poll, free-ports)
760
+ tests: none
761
+ why: the helpers the test world needs (polling, free ports, transports), inlined so the world imports no other repository; a bare <name>.ts entry in allows is what admits a plain name in BE_SOURCE_FORM
729
762
  - id: be.tests.fixtures
730
763
  profiles: [be]
731
764
  path: "src/tests/fixtures/"
732
765
  presence: optional
733
766
  tracked: tracked
734
767
  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
768
+ allows: [i18n/] # i18n/ is the one folder that may carry localized text
769
+ tests: none
770
+ why: payload fixtures with a role suffix (the doubles come from @starci/jest-preset, not from here); the test data builders live in its builders/ slot; importable by unit specs, integration and e2e; never imports a feature
771
+ - id: be.tests.fixtures.builders
772
+ profiles: [be]
773
+ path: "src/tests/fixtures/builders/*.builder.ts"
774
+ presence: optional
775
+ tracked: tracked
776
+ tier: fixtures
777
+ tests: none
778
+ why: >-
779
+ the ONLY home of test data builders, one <area>.builder.ts per area, shared by every spec of that area: pure object builders
780
+ (commissionRow(overrides), accrueInput(overrides)) for unit specs and persisting builders (orderBuilder(world.db.primary).pending().build(overrides))
781
+ for integration and e2e; arrange only, constraints on, deterministic defaults. A .builder.ts anywhere else, and a *.repository.ts,
782
+ *.fixture.ts or *.factory.ts anywhere, is a BE_SOURCE_FORM finding
783
+ rules: [BE_SOURCE_FORM]
784
+ - id: be.tests.fixtures.i18n
785
+ profiles: [be]
786
+ path: "src/tests/fixtures/i18n/"
787
+ presence: optional
788
+ tracked: tracked
789
+ tier: fixtures
790
+ allows: ["<name>.<role>.ts"] # data files that deliberately carry localized text (a real Vietnamese string a parser or formatter must accept); no specs here
791
+ tests: none
792
+ why: the ONLY test-fixture location where Vietnamese or another language may appear in source (LANG_NOT_ENGLISH); placement is the marker, there is no comment pragma
738
793
  - id: be.tests.integration
739
794
  profiles: [be]
740
795
  path: "src/tests/integration/<capability>/*.integration-spec.ts"
@@ -794,7 +849,7 @@ slots:
794
849
  tier: none
795
850
  tests: none
796
851
  requires: [application-stacks.yaml]
797
- allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,k8s,terraform}/**",
852
+ allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,k8s,terraform}/**", "<env>/infra/metadata.json",
798
853
  "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/seeds/**"]
799
854
  forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
800
855
  rules: [HFS_STACKS_SHAPE, HFS_PLAINTEXT_SECRET, HFS_IDENTITY_CUSTODY]
@@ -821,11 +876,10 @@ slots:
821
876
  rules: [FE_APP_ISOLATION, FE_ERROR_BOUNDARY_MISSING, FE_NEXT_CONVENTIONS]
822
877
  - id: fe.app-optional
823
878
  profiles: [fe]
824
- path: "apps/<app>/{vitest.config.ts,playwright.config.ts,public/}"
879
+ path: "apps/<app>/public/"
825
880
  presence: optional
826
881
  tracked: tracked
827
882
  tier: none
828
- roles: {playwright: playwright.config.ts}
829
883
  tests: none
830
884
  - id: fe.route
831
885
  profiles: [fe]
@@ -837,8 +891,9 @@ slots:
837
891
  global-error.tsx, page.tsx, # the root page.tsx only redirects (FE_ROUTE_FILES_THIN)
838
892
  not-found.tsx, layout.tsx, # next-intl: a request outside every locale renders the root not-found inside a root layout
839
893
  "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}
894
+ "{icon,favicon,apple-icon,opengraph-image,twitter-image}.*", robots.ts, sitemap.ts, manifest.ts]
895
+ # providers.tsx (root or [locale]) is the client wrapper the server layout renders around its children to host context providers.
896
+ roles: {page: page.tsx, layout: layout.tsx, template: template.tsx, loading: loading.tsx, error: error.tsx, not-found: not-found.tsx, default: default.tsx, route: route.ts, global-error: global-error.tsx, providers: providers.tsx}
842
897
  tests: none
843
898
  rules: [FE_ROUTE_FILES_THIN, FE_CLIENT_BOUNDARY, FE_OWNER_REACHABLE]
844
899
  - id: fe.source-root-pinned
@@ -868,9 +923,9 @@ slots:
868
923
  owner: true
869
924
  kinds: [pages, layouts, overlays]
870
925
  requires: [index.tsx]
871
- allows: [index.tsx, component.tsx, classNames.ts, "*.spec.tsx", "*.spec.ts"]
926
+ allows: [index.tsx, component.tsx, classNames.ts]
872
927
  roles: {entry: index.tsx, drawing: component.tsx, styles: classNames.ts} # the connected entry, the pure drawing half and the styling declarations of a component owner
873
- tests: unit-beside
928
+ tests: none
874
929
  budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
875
930
  rules: [FE_SIZE_BUDGET, FE_OWNER_REACHABLE, FE_I18N_LITERAL]
876
931
  - id: fe.components
@@ -882,9 +937,9 @@ slots:
882
937
  owner: true
883
938
  layers: [blocks, composites, branches, leaves] # a layer imports only the layers after it
884
939
  requires: [index.tsx]
885
- allows: [index.tsx, component.tsx, classNames.ts, "*.spec.tsx", "*.spec.ts"]
940
+ allows: [index.tsx, component.tsx, classNames.ts]
886
941
  roles: {entry: index.tsx, drawing: component.tsx, styles: classNames.ts} # the connected entry, the pure drawing half and the styling declarations of a component owner
887
- tests: unit-beside
942
+ tests: none
888
943
  budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
889
944
  rules: [FE_CONNECTED_BLOCK_RENDER_PAIR, FE_I18N_LITERAL, FE_NATIVE_FORM_CONTROL, FE_CLIENT_BOUNDARY]
890
945
  - id: fe.hooks
@@ -895,9 +950,9 @@ slots:
895
950
  tier: hooks
896
951
  owner: true # a domain is an owner: other domains import it through index.ts only
897
952
  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
953
+ allows: [index.ts, "use<name>.ts", "<domain>.shared.ts"] # React hooks only; <domain>.shared.ts = sanctioned non-hook helpers
899
954
  roles: {entry: index.ts, shared: "<domain>.shared.ts"}
900
- tests: unit-beside
955
+ tests: none
901
956
  budget: {file: 200, dataHooks: 6, useState: 6}
902
957
  rules: [FE_HOOKS_ARE_HOOKS, FE_DATA_FRESHNESS]
903
958
  - id: fe.modules
@@ -909,7 +964,7 @@ slots:
909
964
  owner: true
910
965
  requiredInstances: {capability: [config, i18n, routes]}
911
966
  requires: [index.ts]
912
- tests: unit-beside
967
+ tests: none
913
968
  budget: {file: 400}
914
969
  rules: [FE_ENV_OWNER, FE_TRANSPORT_OWNER]
915
970
  - id: fe.modules.api
@@ -922,9 +977,9 @@ slots:
922
977
  # client.ts/outcome.ts (fe.transport.client/outcome) in a one-app repository, or the api package's
923
978
  # (fe.package.api.client/outcome) when the repository shares it (FE_TRANSPORT_OWNER, machine).
924
979
  requires: [index.ts]
925
- allows: [index.ts, client.ts, outcome.ts, "<domain>/read-*.ts", "<domain>/*.graphql", "<domain>/<domain>.mapper.ts", contract/, __generated__/, "*.spec.ts"]
980
+ allows: [index.ts, client.ts, outcome.ts, "<domain>/read-*.ts", "<domain>/*.graphql", "<domain>/<domain>.mapper.ts", contract/, __generated__/]
926
981
  roles: {entry: index.ts, reader: "read-*.ts"} # a reader is a server module (FE_CLIENT_REACHES_SERVER)
927
- tests: unit-beside
982
+ tests: none
928
983
  rules: [FE_TRANSPORT_OWNER, FE_HTTP_STATUS_COLLAPSE, FE_WIRE_GENERATED]
929
984
  - id: fe.transport.client
930
985
  profiles: [fe]
@@ -932,7 +987,7 @@ slots:
932
987
  presence: optional
933
988
  tracked: tracked
934
989
  tier: transport
935
- tests: unit-beside
990
+ tests: none
936
991
  why: the one fetch of a one-app repository (timeout, abort, 401/403 -> refused); a shared client is fe.package.api.client
937
992
  rules: [FE_TRANSPORT_OWNER]
938
993
  - id: fe.transport.outcome
@@ -941,7 +996,7 @@ slots:
941
996
  presence: optional
942
997
  tracked: tracked
943
998
  tier: transport
944
- tests: unit-beside
999
+ tests: none
945
1000
  why: the one Outcome union of a one-app repository; a shared union is fe.package.api.outcome
946
1001
  rules: [FE_HTTP_STATUS_COLLAPSE]
947
1002
  - id: fe.modules.config
@@ -950,7 +1005,7 @@ slots:
950
1005
  presence: required
951
1006
  tracked: tracked
952
1007
  tier: foundation
953
- tests: unit-beside
1008
+ tests: none
954
1009
  why: the only reader of process.env and NEXT_PUBLIC_*; fails fast, no localhost fallback
955
1010
  - id: fe.modules.i18n
956
1011
  profiles: [fe]
@@ -962,7 +1017,7 @@ slots:
962
1017
  # navigation, request config) is written once per repository: in the i18n package (fe.package.i18n, `createAppI18n`)
963
1018
  # when the repository has one, else in the only app's module.
964
1019
  requires: [index.ts]
965
- tests: unit-beside
1020
+ tests: none
966
1021
  rules: [FE_I18N_PLACEMENT, FE_I18N_CATALOG]
967
1022
  - id: fe.modules.brand
968
1023
  profiles: [fe]
@@ -979,7 +1034,7 @@ slots:
979
1034
  presence: required
980
1035
  tracked: tracked
981
1036
  tier: foundation
982
- tests: unit-beside
1037
+ tests: none
983
1038
  why: every href builder; FE_HREF_RESOLVES checks each against app/
984
1039
  - id: fe.modules.types
985
1040
  profiles: [fe]
@@ -987,7 +1042,7 @@ slots:
987
1042
  presence: optional
988
1043
  tracked: tracked
989
1044
  tier: foundation
990
- tests: unit-beside
1045
+ tests: none
991
1046
  why: shared types with no runtime behaviour; a component may import them
992
1047
  - id: fe.package.ui
993
1048
  profiles: [fe]
@@ -999,7 +1054,7 @@ slots:
999
1054
  kinds: [composites, branches, leaves] # the layer folders under src/ (a package owns no blocks)
1000
1055
  roles: {entry: index.tsx, drawing: component.tsx, styles: classNames.ts}
1001
1056
  requires: [package.json, src/index.ts, tsconfig.json]
1002
- tests: unit-beside
1057
+ tests: none
1003
1058
  budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
1004
1059
  why: built to dist with explicit named exports; only composites, branches and leaves; no unused export; no colour literal
1005
1060
  rules: [FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT, FE_STYLE_TOKEN_ONLY]
@@ -1011,7 +1066,7 @@ slots:
1011
1066
  tier: package
1012
1067
  owner: true
1013
1068
  requires: [package.json, src/index.ts, src/client.ts, src/outcome.ts, tsconfig.json]
1014
- tests: unit-beside
1069
+ tests: none
1015
1070
  why: the one transport client and the one Outcome union shared by every app of the repository; no app keeps its own
1016
1071
  rules: [FE_TRANSPORT_OWNER, FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT]
1017
1072
  - id: fe.package.api.client
@@ -1020,7 +1075,7 @@ slots:
1020
1075
  presence: optional # enabled with its package (fe.package.api is the opt-in)
1021
1076
  tracked: tracked
1022
1077
  tier: package
1023
- tests: unit-beside
1078
+ tests: none
1024
1079
  why: the repository's one fetch (timeout, abort, 401/403 -> refused)
1025
1080
  rules: [FE_TRANSPORT_OWNER]
1026
1081
  - id: fe.package.api.outcome
@@ -1029,7 +1084,7 @@ slots:
1029
1084
  presence: optional # enabled with its package (fe.package.api is the opt-in)
1030
1085
  tracked: tracked
1031
1086
  tier: package
1032
- tests: unit-beside
1087
+ tests: none
1033
1088
  why: the repository's one Outcome union
1034
1089
  rules: [FE_HTTP_STATUS_COLLAPSE]
1035
1090
  - id: fe.package.i18n
@@ -1040,25 +1095,9 @@ slots:
1040
1095
  tier: package
1041
1096
  owner: true
1042
1097
  requires: [package.json, src/index.ts, tsconfig.json]
1043
- tests: unit-beside
1098
+ tests: none
1044
1099
  why: the next-intl stack (routing, navigation, request config) written once, exported as the createAppI18n factory every app calls
1045
1100
  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
1101
 
1063
1102
  # Checks that read this manifest (all ship from .claude; none keeps its own path list)
1064
1103
  consumers: