@starci/eslint-canon-fe 8.2.17 → 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.
- package/CHANGELOG.md +13 -0
- package/README.md +19 -1
- package/docs/size-and-state-budget.md +6 -6
- package/drawing.mjs +65 -28
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
- package/runtime/knowledge/hfs/slots.yaml +99 -43
- package/runtime/knowledge/patterns/fe/folder.yaml +3 -0
- package/runtime/scripts/api/git/cat-file.mjs +5 -0
- package/runtime/scripts/api/git/diff.mjs +5 -0
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +110 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +20 -3
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
- package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
- package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
- package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
- package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +14 -9
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +21 -12
- package/runtime/scripts/hfs/architecture/register-once.mjs +13 -5
- package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +3 -6
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
- package/runtime/scripts/hfs/architecture/tiers.mjs +8 -1
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +1 -11
- package/runtime/scripts/hfs/manifest-shape.mjs +27 -2
- package/runtime/scripts/hfs/path-findings.mjs +32 -2
- package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
- package/runtime/scripts/hfs/slot-imports.mjs +3 -1
- package/runtime/scripts/hfs/slot-required.mjs +13 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
- package/runtime/scripts/lib/ts-analysis.mjs +20 -0
- package/size-and-state-budget.mjs +11 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
12
|
+
## 8.2.18 - 2026-10-14
|
|
13
|
+
|
|
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.
|
|
15
|
+
|
|
3
16
|
## 8.2.17 - 2026-10-14
|
|
4
17
|
|
|
5
18
|
- Fixed: `dead-exports` (`HFS_UNUSED_EXPORT`) counts an import by another public entry the slot declares in `entries` as a use: a `graphql/{queries,mutations}/types/<x>.ts` export its operation file `query-<x>.ts`/`mutation-<x>.ts` imports is part of that operation's signature and is no longer reported. A barrel or any other non-entry file of the owner still does not count, so an operation reached only through a re-export of `modules/api/index.ts` stays dead.
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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:**
|
|
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
|
|
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
|
|
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;
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
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
|
|
564
|
-
allows: [workspace.yaml,
|
|
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
|
|
591
|
-
#
|
|
592
|
-
#
|
|
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>/
|
|
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
|
|
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
|
-
|
|
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.
|
|
995
|
-
# among them (the test world migrates through the same list). It is required as soon as the
|
|
996
|
-
# or tracks a command, with its image apps/cli/Dockerfile (BE_CLI_REQUIRED): one image,
|
|
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
|
-
|
|
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:
|
|
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);
|