@starci/eslint-canon-fe 8.2.18 → 8.3.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 (41) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +19 -1
  3. package/docs/size-and-state-budget.md +6 -6
  4. package/drawing.mjs +65 -28
  5. package/package.json +1 -1
  6. package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
  7. package/runtime/knowledge/hfs/slots.yaml +99 -43
  8. package/runtime/knowledge/patterns/fe/folder.yaml +3 -0
  9. package/runtime/scripts/api/git/cat-file.mjs +5 -0
  10. package/runtime/scripts/api/git/diff.mjs +5 -0
  11. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +110 -0
  12. package/runtime/scripts/hfs/architecture/backend.mjs +20 -3
  13. package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
  14. package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
  15. package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
  16. package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
  17. package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
  18. package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
  19. package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
  20. package/runtime/scripts/hfs/architecture/dead-exports.mjs +14 -9
  21. package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
  22. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
  23. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
  24. package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
  25. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +21 -12
  26. package/runtime/scripts/hfs/architecture/register-once.mjs +13 -5
  27. package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
  28. package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
  29. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +3 -6
  30. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
  31. package/runtime/scripts/hfs/architecture/tiers.mjs +8 -1
  32. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
  33. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +1 -11
  34. package/runtime/scripts/hfs/manifest-shape.mjs +27 -2
  35. package/runtime/scripts/hfs/path-findings.mjs +32 -2
  36. package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
  37. package/runtime/scripts/hfs/slot-imports.mjs +3 -1
  38. package/runtime/scripts/hfs/slot-required.mjs +13 -0
  39. package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
  40. package/runtime/scripts/lib/ts-analysis.mjs +20 -0
  41. package/size-and-state-budget.mjs +11 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 8.3.0 - 2026-10-15
4
+ - Changed: layouts may import overlays beside pages (tier edge `feature -> feature` from pages and layouts to overlays); a layout hosts the route-selected overlay through `components/branches/OverlayHost` while pages stay server components (pattern FE-BOUNDARY-5). Carries the runtime mirror of the manifest and pins, rebased onto alpha.13 (eslint-canon-fe 8.2.18).
5
+
6
+ ## 8.2.17 - 2026-10-14
7
+ - Changed: `component-line-budget` gives a pure re-export `index.ts` the slot's `barrel` budget (400); `basePropsAtom` admits named `render*` callbacks in `props` of non-block pure halves for feature composition; the bundled machine carries the kind-scoped feature tier edge, canon-shape clone exclusions and in-segment i18n template reads.
8
+
9
+
10
+ - Changed: carry the runtime mirror of HFS manifest 3.1.0 (the back-end app composition standard, R245 and R246) and the canon pins of eslint-canon-be 3.3.0, HFS 4.3.0 and CLI 1.2.16. ESLint rule implementations are unchanged.
11
+
3
12
  ## 8.2.18 - 2026-10-14
4
13
 
5
14
  - Changed: carry the canon pins of eslint-canon-be 3.2.19, HFS 4.2.17 and CLI 1.2.17 in the runtime mirror. ESLint rule implementations are unchanged.
package/README.md CHANGED
@@ -45,13 +45,31 @@ class-name ownership. Reusable class names belong in a colocated `classNames.ts`
45
45
  be composed with HeroUI `cn` using one utility token per argument.
46
46
 
47
47
  The `drawing` law holds the split surfaces (blocks, pages, layouts, overlays under `components/`
48
- or `features/`): the pure `XBase` takes `{ state, props, on }` of atoms (`base-props-atom`), only its
48
+ or `features/`): the pure `XBase` takes `{ state, props, on }` of atoms, with named `render`/`renderX`
49
+ callbacks in feature `props` for block composition (`base-props-atom`), only its
49
50
  sibling `index.tsx` imports it and never re-exports it (`base-import-pair`), `XState` names drawn
50
51
  shapes and never a data status (`no-data-status-shape`), and a block renders each slot's
51
52
  loading/forbidden/error/empty through `SlotView` (`slot-status-through-slotview`). The law's name is
52
53
  the `drawing` slot role of `component.tsx`; a split-surface reference tree lives under
53
54
  `.claude/examples/`.
54
55
 
56
+ Page and layout owners in `features/{pages,layouts}/<Name>/index.tsx` may compose overlay and block public entries. A server
57
+ page may import and render a client overlay entry while remaining a server component: `FE_CLIENT_BOUNDARY` accepts the
58
+ overlay entry, and `FE_CLIENT_SERVER_IMPORT` / `FE_CLIENT_REACHES_SERVER` reject server-only imports from its client closure.
59
+ The only feature-to-feature edge is pages or layouts to overlays; layouts never import pages, overlays never import other
60
+ overlays, components never import overlays, and blocks never import another block. For a client list, a server page mounts an overlay whose connected entry reads list
61
+ identities and supplies a block renderer to its pure Base. A pure component or branch may render that keyed list by
62
+ calling the supplied render prop; it imports no block and fetches no data. The supplied renderer composes blocks only,
63
+ with no intrinsic markup or hook calls, and each connected item block still renders its exact sibling `XBase`.
64
+
65
+ A layout-level overlay host passes rendered overlay candidates into a client `features/overlays/OverlayRouteHost` entry.
66
+ Its hook under `hooks/overlays` reads `usePathname()` and `useSearchParams()` and supplies scalar selection to the pure
67
+ `components/branches/OverlayHost` branch. The layout imports the candidate overlay entries; the host and branch only
68
+ receive rendered nodes. No callback crosses the server-to-client boundary, and pages stay server components. This
69
+ composition-only host renders an existing public pure branch without a forwarding private Base; connected blocks still
70
+ require their exact sibling Base. The worked tree is in
71
+ [`knowledge/patterns/fe/boundaries.yaml`](../../../knowledge/patterns/fe/boundaries.yaml), `FE-BOUNDARY-5`.
72
+
55
73
  The one locale pattern everywhere is `next-intl`, a `[locale]` segment, default `vi`,
56
74
  `localePrefix: "as-needed"`, and `proxy.ts` instead of `middleware.ts`.
57
75
 
@@ -2,31 +2,31 @@
2
2
 
3
3
  Law module: `size-and-state-budget.mjs`. Catalogue: R65 FE_SIZE_AND_STATE_BUDGET.
4
4
 
5
- A component file is at most 300 lines; a connected unit holds at most 6 `useState` and reads at most 6 data hooks; there is no hand-written polling loop. `HFS_SIZE_GROWTH` (no growth against the parent commit) is the architecture machine's; these are the absolute file-local limits.
5
+ A drawing file is at most 300 lines, a connected `index.tsx` at most 200, and a hook implementation at most 200. A pure re-export `index.ts` has a separate 400-line budget. A connected unit holds at most 6 `useState` and reads at most 6 data hooks; there is no hand-written polling loop. `HFS_SIZE_GROWTH` (no growth against the parent commit) is the architecture machine's; these are the absolute file-local limits.
6
6
 
7
7
  Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
8
8
 
9
9
  ## `starci-fe/component-line-budget`
10
10
 
11
- A component file has at most 300 lines.
11
+ A file stays within the budget of its HFS slot: `component.tsx` 300 lines, connected `index.tsx` 200, hook implementation 200, module 400. Where the slot states `budget.barrel`, an `index.ts` containing only `export ... from` statements uses that separate 400-line limit. Named, type-only, star and namespace re-exports qualify, including multiline statements and comments. Imports, declarations, local exports, directives and empty statements keep the ordinary limit, as does an empty statement body. Another basename or `index.tsx` keeps its ordinary limit. The rule reads every limit from `knowledge/hfs/slots.yaml`; a trailing newline does not add a line.
12
12
 
13
13
  **Invalid** (`src/components/blocks/Feed/index.tsx`)
14
14
 
15
15
  ```ts
16
- // a 301-line component file
16
+ // a 201-line connected entry
17
17
  ```
18
18
 
19
19
  **Valid** (`src/components/blocks/Feed/index.tsx`)
20
20
 
21
21
  ```ts
22
- // 300 lines or fewer
22
+ // 200 lines or fewer
23
23
  ```
24
24
 
25
25
  **Finding code:** `FE_SIZE_AND_STATE_BUDGET`
26
26
 
27
- **Why:** `<file>` is `<n>` lines long, over the 300-line limit for a component.
27
+ **Why:** `<file>` is `<n>` lines long, over the limit its slot states for the file's role or its pure re-export barrel.
28
28
 
29
- **Fix:** Separate rendering from data and split each region into its own unit.
29
+ **Fix:** Split an oversized implementation by responsibility; narrow a barrel's public surface to the exports its owner needs.
30
30
 
31
31
  ## `starci-fe/unit-hook-budget`
32
32
 
package/drawing.mjs CHANGED
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * THE PURE HALF TAKES ATOMS. `component.tsx` receives `{ state, props, on }`: `props` is data
10
10
  * (strings, numbers, booleans, arrays, plain objects), `on` is actions whose arguments are data,
11
- * and the children it renders are written inside it, never handed in. A layout alone also takes
12
- * `children`, because the router puts the page there.
11
+ * and feature composition may also take named `render` callbacks inside `props` to render supplied
12
+ * blocks. A layout alone also takes `children`, because the router puts the page there.
13
13
  *
14
14
  * ONLY ITS OWN INDEX MAY IMPORT IT. Other tiers use the connected export, so the pure half stays a
15
15
  * private drawing surface that audit can render from a fixture.
@@ -38,6 +38,10 @@ const pureHalf = (context) =>
38
38
  /** Type names that carry a rendered tree rather than data. */
39
39
  const NON_ATOM_TYPE = /^(?:ReactNode|ReactElement|ReactPortal|JSX\.Element|Element|ComponentType|FC|FunctionComponent|ElementType|ComponentProps|RenderFunction)$/
40
40
 
41
+ /** Feature composition's render callbacks return a rendered value and take only data. */
42
+ const RENDER_RESULT_TYPE = /^(?:ReactNode|ReactElement|JSX\.Element)$/
43
+ const RENDER_PROP_NAME = /^render(?:[A-Z][A-Za-z0-9]*)?$/
44
+
41
45
  /** The printed name of a type reference, qualified names included. */
42
46
  const typeNameOf = (node) => {
43
47
  const name = node?.typeName
@@ -79,17 +83,17 @@ const keyOf = (member) => (member.key?.type === "Identifier" ? member.key.name :
79
83
 
80
84
  // -- DRAWING-1 ----------------------------------------------------------------------------------
81
85
 
82
- /** The pure half takes `{ state, props, on }` of atoms; only a layout also takes `children`. */
86
+ /** The pure half takes atom data and actions; feature composition may receive named render callbacks. */
83
87
  export const basePropsAtom = {
84
88
  meta: {
85
89
  type: "problem",
86
- docs: { description: "A pure `XBase` takes `state`, atom `props` and `on` actions; no component or tree crosses in." },
90
+ docs: { description: "A pure `XBase` takes `state`, atom `props` and `on` actions; feature composition may receive named render callbacks in `props`." },
87
91
  schema: [],
88
92
  messages: {
89
93
  tree:
90
94
  "`{{path}}` hands a rendered tree (`{{type}}`) into the pure half. Write the child inside `component.tsx` and pass it atoms (ids, strings, flags); a tree handed in cannot be drawn, audited or reused.",
91
95
  action:
92
- "`{{path}}` is a function inside `props`. Actions live in `on`, so `props` stays pure data that a fixture can hold.",
96
+ "`{{path}}` is a function inside `props`. Actions live in `on`; feature composition may receive named `render` callbacks that take atoms and return rendered blocks.",
93
97
  onData:
94
98
  "`on.{{key}}` is not an action. `on` holds functions only; data belongs in `props`.",
95
99
  member:
@@ -101,45 +105,78 @@ export const basePropsAtom = {
101
105
  if (!tier) return {}
102
106
  let types = new Map()
103
107
 
108
+ /** Resolve render results locally; an optional empty result does not make data a render prop. */
109
+ const isRenderResult = (node, seen = new Set()) => {
110
+ if (!node) return false
111
+ if (node.type === "TSUnionType") {
112
+ const empty = (part) => ["TSNullKeyword", "TSUndefinedKeyword"].includes(part.type)
113
+ || part.type === "TSLiteralType" && part.literal.value === null
114
+ return node.types.some((part) => isRenderResult(part, new Set(seen)))
115
+ && node.types.every((part) => empty(part) || isRenderResult(part, new Set(seen)))
116
+ }
117
+ if (node.type !== "TSTypeReference") return false
118
+ const name = typeNameOf(node)
119
+ if (RENDER_RESULT_TYPE.test(name.replace(/^React\./, ""))) return true
120
+ if (!types.has(name) || seen.has(name)) return false
121
+ return isRenderResult(types.get(name), new Set(seen).add(name))
122
+ }
123
+
124
+ const checkRenderCallback = (node, path, allowed) => {
125
+ if (!allowed || !isRenderResult(node.returnType?.typeAnnotation)) return false
126
+ for (const parameter of node.params ?? node.parameters ?? []) {
127
+ checkData(parameter.typeAnnotation?.typeAnnotation, `${path}(${parameter.name ?? "arg"})`, new Set(), false, true)
128
+ }
129
+ return true
130
+ }
131
+
132
+ const checkDataReference = (node, path, seen, renderProp, renderParameter) => {
133
+ const name = typeNameOf(node)
134
+ if (NON_ATOM_TYPE.test(renderParameter ? name.replace(/^React\./, "") : name)) {
135
+ context.report({ node, messageId: "tree", data: { path, type: name } })
136
+ return
137
+ }
138
+ for (const argument of node.typeArguments?.params ?? node.typeParameters?.params ?? []) checkData(argument, path, seen, false, renderParameter)
139
+ if (types.has(name) && !seen.has(name)) {
140
+ const nextSeen = renderProp || renderParameter ? new Set(seen) : seen
141
+ nextSeen.add(name)
142
+ checkData(types.get(name), path, nextSeen, renderProp, renderParameter)
143
+ }
144
+ }
145
+
146
+ const checkDataMember = (member, path, seen, renderParameter) => {
147
+ const memberPath = `${path}.${keyOf(member)}`
148
+ const allowsRender = tier !== "blocks" && path === "props" && RENDER_PROP_NAME.test(keyOf(member))
149
+ if (member.type === "TSMethodSignature") {
150
+ if (!checkRenderCallback(member, memberPath, allowsRender)) context.report({ node: member, messageId: "action", data: { path: memberPath } })
151
+ } else if (member.type === "TSPropertySignature") {
152
+ checkData(member.typeAnnotation?.typeAnnotation, memberPath, seen, allowsRender, renderParameter)
153
+ }
154
+ }
155
+
104
156
  /** Walk a data type: report trees and functions anywhere inside it. */
105
- const checkData = (node, path, seen) => {
157
+ const checkData = (node, path, seen, renderProp = false, renderParameter = false) => {
106
158
  if (!node) return
107
159
  switch (node.type) {
108
160
  case "TSFunctionType":
161
+ if (checkRenderCallback(node, path, renderProp)) return
109
162
  context.report({ node, messageId: "action", data: { path } })
110
163
  return
111
- case "TSTypeReference": {
112
- const name = typeNameOf(node)
113
- if (NON_ATOM_TYPE.test(name)) {
114
- context.report({ node, messageId: "tree", data: { path, type: name } })
115
- return
116
- }
117
- for (const argument of node.typeArguments?.params ?? node.typeParameters?.params ?? []) checkData(argument, path, seen)
118
- if (types.has(name) && !seen.has(name)) {
119
- seen.add(name)
120
- checkData(types.get(name), path, seen)
121
- }
164
+ case "TSTypeReference":
165
+ checkDataReference(node, path, seen, renderProp, renderParameter)
122
166
  return
123
- }
124
167
  case "TSUnionType":
125
168
  case "TSIntersectionType":
126
- for (const part of node.types) checkData(part, path, seen)
169
+ for (const part of node.types) checkData(part, path, seen, renderProp, renderParameter)
127
170
  return
128
171
  case "TSArrayType":
129
- checkData(node.elementType, path, seen)
172
+ checkData(node.elementType, path, seen, false, renderParameter)
130
173
  return
131
174
  case "TSTypeOperator":
132
- checkData(node.typeAnnotation, path, seen)
175
+ checkData(node.typeAnnotation, path, seen, false, renderParameter)
133
176
  return
134
177
  case "TSTypeLiteral":
135
178
  case "TSInterfaceBody":
136
- for (const member of node.members ?? node.body) {
137
- if (member.type === "TSMethodSignature") {
138
- context.report({ node: member, messageId: "action", data: { path: `${path}.${keyOf(member)}` } })
139
- } else if (member.type === "TSPropertySignature") {
140
- checkData(member.typeAnnotation?.typeAnnotation, `${path}.${keyOf(member)}`, seen)
141
- }
142
- }
179
+ for (const member of node.members ?? node.body) checkDataMember(member, path, seen, renderParameter)
143
180
  return
144
181
  default:
145
182
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-fe",
3
- "version": "8.2.18",
3
+ "version": "8.3.0",
4
4
  "description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin and attached by one factory.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,14 +29,14 @@ pins:
29
29
  source: packages/grammar/package.json
30
30
  why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
31
31
  '@starci/eslint-canon-be':
32
- version: 3.2.19
32
+ version: 3.3.1
33
33
  group: starci
34
34
  install: registry
35
35
  side: be
36
36
  source: packages/eslint/be/package.json
37
37
  why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
38
38
  '@starci/eslint-canon-fe':
39
- version: 8.2.18
39
+ version: 8.3.0
40
40
  group: starci
41
41
  install: registry
42
42
  side: fe
@@ -76,14 +76,14 @@ pins:
76
76
  source: packages/test-world/package.json
77
77
  why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
78
78
  '@starci/cli':
79
- version: 1.2.17
79
+ version: 1.3.0
80
80
  group: starci
81
81
  install: registry
82
82
  side: both
83
83
  source: packages/cli/package.json
84
84
  why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
85
85
  '@starci/hfs':
86
- version: 4.2.17
86
+ version: 4.3.0
87
87
  group: starci
88
88
  install: registry
89
89
  side: both
@@ -7,7 +7,7 @@
7
7
  # catalog READ this file through scripts/hfs/slots.mjs; none of them hardcodes a path.
8
8
  # Shape: modules/schemas/hfs-slots.schema.yaml. The repository side is hfs.json: modules/schemas/hfs-repo.schema.yaml.
9
9
  schema: starci/hfs-slots@3
10
- version: 3.0.0
10
+ version: 3.1.0
11
11
 
12
12
  versioning:
13
13
  # MAJOR.MINOR.PATCH of this manifest. A repository pins only the MAJOR in hfs.json ("hfs": 3).
@@ -31,6 +31,12 @@ versioning:
31
31
  # is the socket.io gateway in both editions), fe.route holds pages, layouts and the one health/live route handler, and the slots
32
32
  # marked `since: 3.0.0` are added: the Apollo, SWR, RHF, zustand and socket.io hooks, redux, modules/auth, modules/toast, the
33
33
  # providers, be.assets and the migration archive.
34
+ # 3.1.0 is a minor (owner ruling 2026-10-14, R245 and R246): every be.app.* slot allows `<app>.env.ts` and the composition groups
35
+ # `compose/<area>.ts` and budgets its app files (`app.module.ts` 250, `<app>.options.ts` 200, `<app>.env.ts` 150,
36
+ # `compose/<area>.ts` 120, `main.ts` 80); `<app>.env.ts` exports interface <App>Env and read<App>Env(env: EnvSource): <App>Env
37
+ # for fail-fast env parsing only, and `<app>.options.ts` holds env-derived options and port bindings only, with no parsing
38
+ # or environment reads (BE_APP_OPTIONS_COMPOSITION). Secrets use EnvSource.secret(): <NAME>_FILE first, plain <NAME> only
39
+ # when the file key is absent; .starcistacks owns custody. Capability <capability>.config.ts roles stay unchanged.
34
40
 
35
41
  # Field meaning, per slot.
36
42
  # id stable name, never changes meaning inside a major.
@@ -139,6 +145,7 @@ naming:
139
145
  # ---------------------------------------------------------------------------------------------------------
140
146
  # Tiers and the dependency direction matrix (owner granularity; type-only imports count; no cycles anywhere)
141
147
  # mayImport lists the tiers a file of that tier may import. Importing inside the same owner is always allowed.
148
+ # mayImportKinds narrows a permitted tier edge to its named from/to folder kinds; other kind pairs remain forbidden.
142
149
  # ---------------------------------------------------------------------------------------------------------
143
150
  tiers:
144
151
  be:
@@ -155,7 +162,7 @@ tiers:
155
162
  package: {mayImport: [package]}
156
163
  fe:
157
164
  route: {mayImport: [feature, foundation, modules, transport, package]} # app/** adapters
158
- feature: {mayImport: [components, hooks, foundation, modules, transport, package]}
165
+ feature: {mayImport: [feature, components, hooks, foundation, modules, transport, package], mayImportKinds: {feature: {from: [pages, layouts], to: [overlays]}}} # pages and layouts compose overlays and blocks; other feature-to-feature edges stay forbidden
159
166
  # A component never reaches data: config, routes, i18n and types are foundation; every other module and the api
160
167
  # transport are reached through a hook (owner decision 2026-09-29).
161
168
  components: {mayImport: [components, hooks, foundation, package], lowerLayerOnly: true} # blocks, composites, branches, leaves
@@ -501,6 +508,33 @@ slots:
501
508
  tests: none
502
509
  managedBy: docker-ignore
503
510
  rules: [HFS_MANAGED_FILE_DRIFT]
511
+ - id: app.deploy
512
+ profiles: [app]
513
+ # Production delivery tooling for the app's own processes: compose files, Caddyfile, *.env.example and ops shell scripts.
514
+ # Lite still keeps its backing services on Supabase and its encrypted custody in app.starcistacks; R06 scans every file here.
515
+ path: "deploy/"
516
+ presence: optional
517
+ tracked: tracked
518
+ tier: none
519
+ tests: none
520
+ rules: [HFS_PLAINTEXT_SECRET]
521
+ - id: app.ci-deploy
522
+ profiles: [app]
523
+ # Product-owned delivery workflows; the literal app.ci-db path remains the managed database workflow's more specific slot.
524
+ path: ".github/workflows/<app>-deploy.yml"
525
+ presence: optional
526
+ tracked: tracked
527
+ tier: none
528
+ tests: none
529
+ rules: [HFS_PLAINTEXT_SECRET]
530
+ - id: app.ci-authorize-deploy-key
531
+ profiles: [app]
532
+ path: ".github/workflows/authorize-deploy-key.yml"
533
+ presence: optional
534
+ tracked: tracked
535
+ tier: none
536
+ tests: none
537
+ rules: [HFS_PLAINTEXT_SECRET]
504
538
  - id: app.browser
505
539
  profiles: [app]
506
540
  # The browser journey of the product (Playwright, owner ruling: one minimal journey per example app, run manually): the config, its
@@ -536,6 +570,17 @@ slots:
536
570
  path: scripts/codegen.mjs
537
571
  liteManagedBy: codegen-lite
538
572
  rules: [HFS_SCRIPT_ONE_OFF, BE_SPEC_PLACEMENT, HFS_REPO_LOCAL_CHECK] # one-off codemods and fix-* scripts are agent output, not tooling
573
+ - id: app.scripts.delivery
574
+ profiles: [app]
575
+ editions: [lite]
576
+ # Product-owned environment, secret and build tooling, kept apart from lite's managed codegen script.
577
+ # Full already owns these files through app.scripts; its generic *.mjs pattern must not compete with build-*.mjs.
578
+ path: "scripts/{env-sync.mjs,with-secrets.mjs,set-secrets.ps1,build-*.mjs}"
579
+ presence: optional
580
+ tracked: tracked
581
+ tier: none
582
+ tests: none
583
+ rules: [HFS_PLAINTEXT_SECRET, HFS_SCRIPT_ONE_OFF, BE_SPEC_PLACEMENT, HFS_REPO_LOCAL_CHECK]
539
584
  - id: app.sides
540
585
  profiles: [app]
541
586
  # The two sides. Everything below be/ and fe/ is judged by the side's own slots (profiles be and fe) with the side folder as its root;
@@ -558,23 +603,13 @@ slots:
558
603
  "features/<feature>/ui/<name>/assets/<approved-file>", "features/<feature>/uat/<name>/{index,accounts,fixtures}.yaml",
559
604
  "features/<feature>/uat/<name>/{seed,cleanup}.sql", "_resources/{identities,environments,fixtures}/<slug>/resource.yaml"]
560
605
  forbids: [evidence/, logs/, reports/, captures/, runs/, "kernel-*/", "*.sqlite*", _derived/]
561
- # Lite keeps the product catalog, feature overviews, grammar and brand drawing records; UAT records belong to the forbidden slot below.
606
+ # Owner amendment 2026-10-14: lite keeps workspace.yaml for project identity and repo roles, plus brand drawing records,
607
+ # references and assets. No brand means no Work tree is needed; workspace.yaml is legal but optional.
608
+ litePresence: optional
562
609
  lite:
563
- requires: [.gitignore, workspace.yaml, index.yaml]
564
- allows: [workspace.yaml, index.yaml, "brand/**", shell/index.yaml, "features/<feature>/index.yaml", "features/<feature>/ui/<name>/index.yaml", "features/<feature>/ui/<name>/assets/<approved-file>"]
610
+ requires: [.gitignore]
611
+ allows: [workspace.yaml, "brand/**"]
565
612
  rules: [HFS_AGENT_DATA_TRACKED, HFS_IDENTITY_CUSTODY]
566
- - id: app.starciwork.uat
567
- profiles: [app]
568
- # The UAT records of the full edition: they are proven on the e2e world (identities, fixtures, br/ac evidence) that lite does
569
- # not have, so the trees are forbidden paths under lite only. In full this slot does not exist and .starciwork answers for them.
570
- editions: [lite]
571
- path: ".starciwork/{_resources/identities,_resources/fixtures,features/<feature>/uat,features/<feature>/br}/"
572
- presence: forbidden
573
- tracked: external
574
- tier: none
575
- tests: none
576
- goesTo: "the full edition: a lite app has no e2e world, so no UAT records (no identities or fixtures resources, no br/ac evidence, no uat directories)"
577
- since: 2.1.0
578
613
  - id: app.starcistacks
579
614
  profiles: [app]
580
615
  # The stack declarations and the sealed custody of the app, at the app root beside be/, fe/ and .starciwork (never under a side).
@@ -584,16 +619,16 @@ slots:
584
619
  tier: none
585
620
  tests: none
586
621
  requires: [application-stacks.yaml]
587
- allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,k8s,terraform}/**", "<env>/infra/metadata.json",
622
+ allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/{compose,k8s,terraform,swarm}/**", "<env>/infra/metadata.json",
588
623
  "<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
589
624
  forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
590
- # Lite has no self-hosted infrastructure (owner ruling 2026-10-12): .starcistacks holds env and secret custody only - the stack
591
- # declaration, the environment descriptor and the sealed secrets; the stack's one backing service is the Supabase project (the
592
- # local CLI stack or the cloud project). infra/ (compose, k8s, terraform), runtime/ and seeds/ are the full edition's.
625
+ # Lite keeps full's code structure with backing infrastructure replaced by Supabase (owner ruling 2026-10-13). The api and
626
+ # sidecars run on docker-swarm; its value-free stack belongs in <env>/infra/swarm/. The checker also refuses components whose
627
+ # images duplicate Supabase services: postgres, redis, kafka, keycloak, minio, rabbitmq, mysql and mongo.
593
628
  lite:
594
- requires: []
595
- allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep"]
596
- forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
629
+ requires: [application-stacks.yaml]
630
+ allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/swarm/**", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep"]
631
+ forbids: ["<env>/infra/{compose,k8s,terraform}/**", "<env>/runtime/**", "<env>/seeds/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
597
632
  rules: [HFS_STACKS_SHAPE, HFS_STACKS_IN_SIDE, HFS_PLAINTEXT_SECRET, HFS_IDENTITY_CUSTODY]
598
633
  - id: app.sops
599
634
  profiles: [app]
@@ -637,7 +672,7 @@ slots:
637
672
  goesTo: "<lanes root>/<project>/<lane>/ (outside every repository) for a lane; a Kernel workflow's worktree is created and owned by Orca outside the app checkout, never tracked, and removed by the runtime's host-side controller once the workflow's finish marked it release-pending"
638
673
  - id: app.plaintext-env
639
674
  profiles: [app]
640
- path: "{.env,.env.*,.secrets/,**/*.pem,**/*.key}"
675
+ path: "{.env,.env.*,secrets.env,.secrets/,**/*.pem,**/*.key}"
641
676
  presence: forbidden
642
677
  tracked: external
643
678
  tier: none
@@ -959,6 +994,23 @@ slots:
959
994
  rules: [BE_CONTRACT_BREAKING]
960
995
 
961
996
  # ----- BE: apps (one slot per app kind; a new kind = a new slot, minor bump) ---------------------------
997
+ # The files of every app (owner ruling 2026-10-14, R245 and R246, since 3.1.0), each with a line budget that
998
+ # BE_APP_COMPOSITION_BUDGET holds absolutely (not a ratchet):
999
+ # main.ts the entry: builds EnvSource once, imports and calls read<App>Env from <app>.env.ts, builds options and boots.
1000
+ # <app>.env.ts exports interface <App>Env and read<App>Env(env: EnvSource): <App>Env (for example ApiEnv/readApiEnv);
1001
+ # fail-fast environment parsing only, via EnvSource and unchanged capability parse helpers; no localhost
1002
+ # or default host fallback, logic, module composition, business code or feature imports. Only main.ts
1003
+ # and this app file may read process.env or EnvSource. The interface declares its own readonly parsed
1004
+ # members with parser-derived types, structurally compatible with the app's options;
1005
+ # the role name stays <app>.env.ts, never env.ts. Secrets use EnvSource.secret(): <NAME>_FILE first
1006
+ # for .starcistacks custody, plain <NAME> only when the file key is absent. A declared file supplies
1007
+ # exact contents; an empty or unreadable path stops boot without fallback.
1008
+ # <app>.options.ts the options: env-derived options plus port bindings (provider maps such as { approvalGate: X }) only;
1009
+ # no environment parsing or process.env/EnvSource reads; module, queue or scheduler composition here or in <app>.env.ts is
1010
+ # BE_APP_OPTIONS_COMPOSITION.
1011
+ # compose/<area>.ts a composition group: exports the module list of one area (an imports array, and the providers of the
1012
+ # area's global ports when it has any); composition only, no business logic or process.env/EnvSource read.
1013
+ # app.module.ts the composition root: imports the groups and spreads them, binds the app-wide guards and filter.
962
1014
  - id: be.app.api
963
1015
  profiles: [be]
964
1016
  path: "apps/<app>/src/"
@@ -969,11 +1021,11 @@ slots:
969
1021
  owner: true
970
1022
  minInstances: 1
971
1023
  requires: [main.ts, app.module.ts]
972
- allows: [main.ts, app.module.ts, "<app>.options.ts", operations.ts]
1024
+ allows: [main.ts, app.module.ts, "<app>.env.ts", "<app>.options.ts", "compose/<area>.ts", operations.ts]
973
1025
  tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
974
1026
  coverage: none
975
- budget: {app.module.ts: 250, main.ts: 80}
976
- rules: [BE_APP_COMPOSITION_ONLY, BE_APP_BUSINESS_ROLE, BE_DEFAULT_DENY, BE_ERROR_MASKED, BE_FEATURE_NOT_COMPOSED, BE_CONTEXT_OWNER]
1027
+ budget: {app.module.ts: 250, main.ts: 80, "<app>.env.ts": 150, "<app>.options.ts": 200, "compose/<area>.ts": 120}
1028
+ rules: [BE_APP_COMPOSITION_ONLY, BE_APP_BUSINESS_ROLE, BE_APP_OPTIONS_COMPOSITION, BE_APP_COMPOSITION_BUDGET, BE_DEFAULT_DENY, BE_ERROR_MASKED, BE_FEATURE_NOT_COMPOSED, BE_CONTEXT_OWNER]
977
1029
  - id: be.app.worker
978
1030
  profiles: [be]
979
1031
  path: "apps/<app>/src/"
@@ -983,17 +1035,19 @@ slots:
983
1035
  tier: app
984
1036
  owner: true
985
1037
  requires: [main.ts, app.module.ts]
986
- allows: [main.ts, app.module.ts, "<app>.options.ts"]
1038
+ allows: [main.ts, app.module.ts, "<app>.env.ts", "<app>.options.ts", "compose/<area>.ts"]
987
1039
  tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
988
1040
  coverage: none
989
- rules: [BE_APP_COMPOSITION_ONLY, BE_BACKGROUND_UNOWNED, BE_CONTEXT_OWNER]
1041
+ budget: {app.module.ts: 250, main.ts: 80, "<app>.env.ts": 150, "<app>.options.ts": 200, "compose/<area>.ts": 120}
1042
+ rules: [BE_APP_COMPOSITION_ONLY, BE_APP_OPTIONS_COMPOSITION, BE_APP_COMPOSITION_BUDGET, BE_BACKGROUND_UNOWNED, BE_CONTEXT_OWNER]
990
1043
  - id: be.app.cli
991
1044
  profiles: [be]
992
1045
  # The ONE app of every one-off action (migrate, seed, sync, backup, any operator command): be/apps/cli, on nest-commander.
993
1046
  # main.ts boots with CommandFactory.run (BE_CLI_BOOTSTRAP); app.module.ts composes the platform and the cli feature root
994
- # (src/features/cli/cli.module.ts); cli.options.ts carries what it needs from the environment, the connections it migrates
995
- # among them (the test world migrates through the same list). It is required as soon as the back end declares a connection
996
- # or tracks a command, with its image apps/cli/Dockerfile (BE_CLI_REQUIRED): one image, run as `cli <group> <command>`.
1047
+ # (src/features/cli/cli.module.ts); cli.env.ts exports CliEnv and readCliEnv(env: EnvSource): CliEnv, and cli.options.ts carries the env-derived values,
1048
+ # the connections it migrates among them (the test world migrates through the same list). It is required as soon as the
1049
+ # back end declares a connection or tracks a command, with its image apps/cli/Dockerfile (BE_CLI_REQUIRED): one image,
1050
+ # run as `cli <group> <command>`.
997
1051
  path: "apps/<app>/src/"
998
1052
  appKind: cli
999
1053
  presence: opt-in
@@ -1002,10 +1056,11 @@ slots:
1002
1056
  tier: app
1003
1057
  owner: true
1004
1058
  requires: [main.ts, app.module.ts]
1005
- allows: [main.ts, app.module.ts, "<app>.options.ts"]
1059
+ allows: [main.ts, app.module.ts, "<app>.env.ts", "<app>.options.ts", "compose/<area>.ts"]
1006
1060
  tests: none # an app is proven by the e2e world (useTestWorld({ apps })), never by a unit spec
1007
1061
  coverage: none
1008
- rules: [BE_CLI_REQUIRED, BE_CLI_BOOTSTRAP, BE_CLI_OWNER, BE_SCHEMA_AUTHORITY]
1062
+ budget: {app.module.ts: 250, main.ts: 80, "<app>.env.ts": 150, "<app>.options.ts": 200, "compose/<area>.ts": 120}
1063
+ rules: [BE_CLI_REQUIRED, BE_CLI_BOOTSTRAP, BE_CLI_OWNER, BE_SCHEMA_AUTHORITY, BE_APP_OPTIONS_COMPOSITION, BE_APP_COMPOSITION_BUDGET]
1009
1064
 
1010
1065
  # ----- BE: features ------------------------------------------------------------------------------------
1011
1066
  # src/features/ is grouped by TRIGGER KIND (owner decision 2026-10-02): features/api/<feature>/ (a request: application/ and
@@ -1515,7 +1570,7 @@ slots:
1515
1570
  requires: [index.ts, "<provider>.config.ts"]
1516
1571
  tests: unit-beside # <name>.service.spec.ts beside each <name>.service.ts; plus src/tests/integration/<provider>/ (R112)
1517
1572
  coverage: required
1518
- budget: {indexExports: 60}
1573
+ budget: {indexExports: 80} # a provider's port surface includes clients, config, options, tokens and errors
1519
1574
  rules: [BE_TIER_DIRECTION, BE_ERROR_HOME, BE_SECRET_DEFAULT, BE_INTEGRATION_SPEC_MISSING]
1520
1575
  - id: be.integrations.model
1521
1576
  profiles: [be]
@@ -1789,7 +1844,7 @@ slots:
1789
1844
  allows: [index.ts, "use<name>.ts", "<domain>.shared.ts"] # React hooks only; <domain>.shared.ts = sanctioned non-hook helpers
1790
1845
  roles: {entry: index.ts, shared: "<domain>.shared.ts"}
1791
1846
  tests: none
1792
- budget: {file: 200, dataHooks: 6, useState: 6}
1847
+ budget: {file: 200, barrel: 400, dataHooks: 6, useState: 6} # barrel: index.ts containing only export ... from statements
1793
1848
  rules: [FE_HOOKS_ARE_HOOKS, FE_DATA_FRESHNESS]
1794
1849
  - id: fe.hooks.swr
1795
1850
  profiles: [fe]
@@ -1806,7 +1861,7 @@ slots:
1806
1861
  "api/rest/queries/useGet*Swr.ts", "api/rest/mutations/use{Post,Put,Patch,Delete}*Swr.ts"]
1807
1862
  roles: {entry: index.ts, effects: SwrSideEffects.tsx}
1808
1863
  tests: none
1809
- budget: {file: 200}
1864
+ budget: {file: 200, barrel: 400}
1810
1865
  since: 3.0.0
1811
1866
  rules: [FE_HOOKS_ARE_HOOKS, FE_DATA_FRESHNESS]
1812
1867
  - id: fe.hooks.rhf
@@ -1822,7 +1877,7 @@ slots:
1822
1877
  allows: [index.ts, "use*Form.ts"]
1823
1878
  roles: {entry: index.ts}
1824
1879
  tests: none
1825
- budget: {file: 200}
1880
+ budget: {file: 200, barrel: 400}
1826
1881
  since: 3.0.0
1827
1882
  rules: [FE_HOOKS_ARE_HOOKS]
1828
1883
  - id: fe.hooks.zustand
@@ -1837,7 +1892,7 @@ slots:
1837
1892
  allows: [index.ts, store.ts, "use*.ts"]
1838
1893
  roles: {entry: index.ts, store: store.ts}
1839
1894
  tests: none
1840
- budget: {file: 200}
1895
+ budget: {file: 200, barrel: 400}
1841
1896
  since: 3.0.0
1842
1897
  rules: [FE_HOOKS_ARE_HOOKS]
1843
1898
  - id: fe.hooks.socketio
@@ -1853,7 +1908,7 @@ slots:
1853
1908
  allows: [index.ts, sockets.ts, SocketIoSideEffects.tsx, connectionStore.ts, "enums/*.ts", "types/*.ts", "use*.ts"]
1854
1909
  roles: {entry: index.ts, effects: SocketIoSideEffects.tsx}
1855
1910
  tests: none
1856
- budget: {file: 200}
1911
+ budget: {file: 200, barrel: 400}
1857
1912
  since: 3.0.0
1858
1913
  rules: [FE_HOOKS_ARE_HOOKS]
1859
1914
  - id: fe.hooks.effects
@@ -1869,7 +1924,7 @@ slots:
1869
1924
  allows: [index.ts, "use*.ts", "*SideEffect.tsx", "*SideEffects.tsx"]
1870
1925
  roles: {entry: index.ts, effects: "*SideEffect.tsx"}
1871
1926
  tests: none
1872
- budget: {file: 200}
1927
+ budget: {file: 200, barrel: 400}
1873
1928
  since: 3.0.0
1874
1929
  rules: [FE_HOOKS_ARE_HOOKS]
1875
1930
  - id: fe.hooks.reuseables
@@ -1884,7 +1939,7 @@ slots:
1884
1939
  allows: [index.ts, "use*.ts"]
1885
1940
  roles: {entry: index.ts}
1886
1941
  tests: none
1887
- budget: {file: 200}
1942
+ budget: {file: 200, barrel: 400}
1888
1943
  since: 3.0.0
1889
1944
  rules: [FE_HOOKS_ARE_HOOKS]
1890
1945
  - id: fe.redux
@@ -1909,6 +1964,7 @@ slots:
1909
1964
  tier: modules
1910
1965
  owner: true
1911
1966
  requiredInstances: {capability: [config, i18n, routes]}
1967
+ # A populated fe.modules instance with no .ts or .tsx files (CSS/assets only) needs no index.ts.
1912
1968
  requires: [index.ts]
1913
1969
  tests: none
1914
1970
  budget: {file: 400}
@@ -174,6 +174,9 @@ rules:
174
174
  component.tsx. Pure blocks, composites, branches and leaves use index.tsx only and never gain an
175
175
  empty twin. classNames.ts is present only when the unit owns class strings. Budgets: component.tsx 300 lines,
176
176
  connected index.tsx 200 lines, at most 6 data hooks and 6 useState per connected unit.
177
+ A composition-only feature may render an existing public pure component owner directly instead of creating a
178
+ forwarding component.tsx. The route-state overlay host of FE-BOUNDARY-5 renders components/branches/OverlayHost;
179
+ a connected block still requires its exact private Base twin.
177
180
  rationale: |
178
181
  Stable basenames make class names and the connected and presentational split discoverable without forwarding
179
182
  twins, and budgets keep a unit reviewable.
@@ -0,0 +1,5 @@
1
+ // cat-file.mjs — `git cat-file <args>`: the type, size or content of repository objects (-e, -p, --batch).
2
+ import { runGit } from './lib.mjs';
3
+
4
+ /** The spawn result {status, stdout, stderr, error} of `git cat-file ...args`; options are runGit's (cwd or dir, config, timeout, maxBuffer, input, encoding, env, git). */
5
+ export const catFile = (args = [], options = {}) => runGit(['cat-file', ...args], options);
@@ -0,0 +1,5 @@
1
+ // diff.mjs — `git diff <args>`: the changes between commits, the index and the working tree.
2
+ import { runGit } from './lib.mjs';
3
+
4
+ /** The spawn result {status, stdout, stderr, error} of `git diff ...args`; options are runGit's (cwd or dir, config, timeout, maxBuffer, input, encoding, env, git). */
5
+ export const diff = (args = [], options = {}) => runGit(['diff', ...args], options);