@starci/hfs 4.7.34 → 4.7.36

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 (54) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +8 -11
  3. package/codemod/button-intent.mjs +356 -0
  4. package/lint/run.mjs +70 -29
  5. package/package.json +3 -2
  6. package/runtime/knowledge/grammars/index.yaml +111 -0
  7. package/runtime/knowledge/grammars/offset-pop/playbook.yaml +302 -0
  8. package/runtime/knowledge/grammars/starci/playbook.yaml +1190 -0
  9. package/runtime/knowledge/hfs/canon-pins.yaml +22 -8
  10. package/runtime/knowledge/hfs/fe-stack.yaml +12 -4
  11. package/runtime/knowledge/hfs/rules.yaml +163 -83
  12. package/runtime/knowledge/hfs/slots.yaml +3 -3
  13. package/runtime/knowledge/ui/presentation/font.yaml +101 -0
  14. package/runtime/knowledge/ui/presentation/padding.yaml +217 -0
  15. package/runtime/modules/kernel/failure-codes.yaml +102 -20
  16. package/runtime/modules/kernel/removed-vocabulary.yaml +29 -0
  17. package/runtime/modules/kernel/roles.yaml +1 -1
  18. package/runtime/packages/cli/src/catalog.generated.mjs +119 -9
  19. package/runtime/scripts/gates/canon-pins.mjs +103 -0
  20. package/runtime/scripts/gates/gate.mjs +3 -36
  21. package/runtime/scripts/gates/output.mjs +40 -0
  22. package/runtime/scripts/gates/test-world-run.mjs +2 -4
  23. package/runtime/scripts/guards/release-definition.mjs +22 -3
  24. package/runtime/scripts/hfs/architecture/config.mjs +8 -8
  25. package/runtime/scripts/hfs/check.mjs +1 -1
  26. package/runtime/scripts/hfs/db-restamp-verb.mjs +2 -2
  27. package/runtime/scripts/hfs/declaration-shape.mjs +8 -2
  28. package/runtime/scripts/hfs/grammar-presentation.mjs +152 -0
  29. package/runtime/scripts/hfs/rule-catalog.mjs +2 -2
  30. package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -0
  31. package/runtime/scripts/hfs/rules/fe-stack.mjs +44 -1
  32. package/runtime/scripts/hfs/slots.mjs +1 -0
  33. package/runtime/scripts/hfs/view.mjs +1 -0
  34. package/runtime/scripts/lib/canon-binding-text.mjs +129 -0
  35. package/runtime/scripts/lib/css-rules.mjs +18 -0
  36. package/runtime/scripts/lib/example-refs.mjs +1 -1
  37. package/runtime/scripts/lib/grammar-lint.mjs +48 -0
  38. package/runtime/scripts/lib/grammar-vendor-parity-jsx.mjs +360 -0
  39. package/runtime/scripts/lib/grammar-vendor-parity.mjs +110 -157
  40. package/runtime/scripts/lib/numeric-match.mjs +5 -0
  41. package/runtime/scripts/lib/release-version.mjs +119 -0
  42. package/runtime/scripts/lib/runtime-publication-tags.mjs +3 -46
  43. package/runtime/scripts/lib/source-package-imports.mjs +111 -0
  44. package/runtime/scripts/work/draw/draw-layer.mjs +5 -4
  45. package/scaffold/app.mjs +7 -5
  46. package/src/main.mjs +30 -15
  47. package/stack/config.mjs +2 -1
  48. package/stack/render.mjs +24 -3
  49. package/sync/index.mjs +8 -2
  50. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +0 -1
  51. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +0 -1
  52. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +0 -1
  53. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +0 -1
  54. package/templates/fe/tool-config/eslint.config.mjs +4 -1
@@ -21,30 +21,44 @@ pins:
21
21
  install: registry
22
22
  side: fe
23
23
  source: packages/heroicons/package.json
24
- why: 'StarCi custom Heroicons-compatible cuts; eslint-canon-fe (icon.mjs) admits @starci/heroicons/24/outline and /16/solid as icon sources, so an app that uses them gets this exact version.'
24
+ why: 'StarCi custom Heroicons-compatible cuts; eslint-grammar-starci (icon.mjs) admits @starci/heroicons/24/outline and /16/solid as icon sources, so an app that uses them gets this exact version.'
25
25
  '@starci/grammar':
26
- version: 0.12.1
26
+ version: 0.12.4
27
27
  group: starci
28
28
  install: registry
29
29
  side: fe
30
30
  source: packages/grammar/package.json
31
31
  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.'
32
32
  '@starci/eslint-canon-be':
33
- version: 3.4.43
33
+ version: 3.4.45
34
34
  group: starci
35
35
  install: registry
36
36
  side: be
37
37
  source: packages/eslint/be/package.json
38
38
  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.'
39
39
  '@starci/eslint-canon-fe':
40
- version: 8.4.42
40
+ version: 9.1.0
41
41
  group: starci
42
42
  install: registry
43
43
  side: fe
44
44
  source: packages/eslint/fe/package.json
45
45
  why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe 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; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
46
+ '@starci/eslint-grammar-starci':
47
+ version: 0.12.4
48
+ group: starci
49
+ install: registry
50
+ side: fe
51
+ source: packages/eslint/grammar-starci/package.json
52
+ why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
53
+ '@starci/eslint-grammar-offset-pop':
54
+ version: 0.12.4
55
+ group: starci
56
+ install: registry
57
+ side: fe
58
+ source: packages/eslint/grammar-offset-pop/package.json
59
+ why: 'the Offset Pop Grammar component API lint, versioned with @starci/grammar; its shared component contract comes from the StarCi family lint without copying rules.'
46
60
  '@starci/stylelint-canon':
47
- version: 2.0.11
61
+ version: 2.0.12
48
62
  group: starci
49
63
  install: registry
50
64
  side: fe
@@ -77,14 +91,14 @@ pins:
77
91
  source: packages/test-world/package.json
78
92
  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.'
79
93
  '@starci/cli':
80
- version: 1.7.34
94
+ version: 1.7.36
81
95
  group: starci
82
96
  install: registry
83
97
  side: both
84
98
  source: packages/cli/package.json
85
99
  why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
86
100
  '@starci/hfs':
87
- version: 4.7.34
101
+ version: 4.7.36
88
102
  group: starci
89
103
  install: registry
90
104
  side: both
@@ -380,7 +394,7 @@ pins:
380
394
  version: 2.2.0
381
395
  group: ui
382
396
  side: fe
383
- why: 'the base of the one application icon set (fe-stack icons, with @starci/heroicons for the glyphs it lacks); eslint-canon-fe (icon.mjs) admits @heroicons/react/24/outline and /16/solid.'
397
+ why: 'the base of the one application icon set (fe-stack icons, with @starci/heroicons for the glyphs it lacks); eslint-grammar-starci (icon.mjs) admits @heroicons/react/24/outline and /16/solid.'
384
398
  '@dicebear/core':
385
399
  version: 10.4.0
386
400
  group: ui
@@ -121,10 +121,11 @@ concerns:
121
121
  pattern: FE-UI
122
122
  forbids: ['@dicebear/*', boring-avatars, 'react-nice-avatar', multiavatar]
123
123
  why: >-
124
- Grammar owns circle avatars: pass the uploaded avatar as src, otherwise pass the user or identity id as seed on
125
- Avatar, AvatarButton or AvatarGroup members. Grammar renders deterministic DiceBear images with its fixed DNA style;
126
- @dicebear/core and @dicebear/styles are Grammar runtime dependencies pinned in canon-pins.yaml. Apps declare and
127
- import @starci/grammar, never @dicebear/*, and draw no avatar visuals. With neither src nor seed, initials remain.
124
+ Grammar owns avatars: pass the uploaded avatar as src and the user or identity id as seed on Avatar, AvatarButton
125
+ or AvatarGroup members. Without src, Grammar renders bundled DiceBear initials using the first and last words of
126
+ name, with colour seeded by seed when present and by name otherwise. kind="agent" adds the Grammar-owned bot badge
127
+ without changing the name rule. @dicebear/core and @dicebear/styles are Grammar runtime dependencies pinned in
128
+ canon-pins.yaml. Apps declare and import @starci/grammar, never @dicebear/*, and draw no avatar visuals.
128
129
  - id: theme
129
130
  tier: core
130
131
  packages: [next-themes]
@@ -237,6 +238,13 @@ concerns:
237
238
  pattern: FE-LINT
238
239
  forbids: []
239
240
  why: the canon lint and format configuration.
241
+ - id: grammar-lint
242
+ tier: opt
243
+ packages: ['@starci/eslint-grammar-starci', '@starci/eslint-grammar-offset-pop']
244
+ home: [fe.source-root-pinned]
245
+ pattern: FE-RULE-OWNERSHIP
246
+ forbids: []
247
+ why: The selected Grammar family's lint package runs beside the universal front-end canon; the family catalog declares the package and factory.
240
248
  - id: auth-supabase
241
249
  tier: opt
242
250
  packages: ['@supabase/supabase-js', '@supabase/ssr']
@@ -8,7 +8,7 @@
8
8
  # An enforcer is a check that exists or one still owed. `status: planned` marks one owed: the architecture machine, the
9
9
  # hfs CLI, stylelint and Sonar lanes fill them, and the entry's `status` is removed the day the check ships (the check
10
10
  # refuses a planned enforcer that already exists). An existing eslint enforcer is a rule id of the plugin
11
- # `@starci/eslint-canon-be` or `@starci/eslint-canon-fe`; an existing machine, hfs or work-validate enforcer names the file
11
+ # the backend, universal frontend or selected Grammar family lint package; an existing machine, hfs or work-validate enforcer names the file
12
12
  # (`at`) that emits one of the rule's codes. scripts/checks/check-hfs-rules.mjs holds all of this to the packages, the
13
13
  # scripts and modules/kernel/failure-codes.yaml.
14
14
  #
@@ -19,8 +19,8 @@
19
19
  schema: starci/hfs-rules@3
20
20
  version: 3.1.0
21
21
  gates:
22
- pre-commit: the managed .husky/pre-commit at the app root - starci app hygiene (staged .starciwork and .starcistacks paths, secrets guard, a new back-end logic file staged without its unit spec), typecheck, ESLint per side and stylelint on the staged files, prettier --check, the unit specs the staged files touch
23
- pre-push: typecheck, lint (starci app lint), format:check and the unit specs affected since origin/main; runs every pre-commit rule again
22
+ pre-commit: the managed L0 hook runs app hygiene, managed-file drift, ESLint per side and stylelint on staged source, and prettier on staged files; typecheck and targeted specs belong to the op gate under R225
23
+ pre-push: the managed app hook validates an annotated release tag and the recorded L4 evidence for HEAD, permitting the release refs and backup namespace under R225; it starts no source check, build or test
24
24
  settle: the op gate `starci gate run` over the op's changed files (merge guard, starci app lint --changed, codegen, tsc, the slice's specs; only findings new against the workflow's previous checkpoint block), re-read at starci kernel settle, which refuses a red done; a green settle commits the op's checkpoint on wf-<workflowId>
25
25
  land: the workflow's finish, the only time main is touched - gate.mjs over the whole workflow branch against its merge-base with main, the merge guard, review.verify of the exact head that lands, the branch rebased onto main, then main fast-forwarded and pushed
26
26
  ci: GitHub Actions runs the same pinned starci app lint, format:check, typecheck, unit with the per-file coverage threshold, build:be and build:fe, then the Sonar scan and gate
@@ -28,7 +28,8 @@ gates:
28
28
  runtime: "`starci runtime check` (npm run check) of the StarCi runtime repository itself: scripts/checks/check-runtime.mjs runs node --check, scripts/hfs/runtime-check.mjs with knowledge/hfs/runtime-slots.yaml, and the retained self-checks"
29
29
  enforcerKinds:
30
30
  eslint-be: a rule of the plugin @starci/eslint-canon-be (id without the plugin prefix)
31
- eslint-fe: a rule of the plugin @starci/eslint-canon-fe (id without the plugin prefix)
31
+ eslint-fe: a universal rule of @starci/eslint-canon-fe (id without the plugin prefix)
32
+ eslint-grammar-starci: a selected Grammar family rule sharing the common component API, resolved through the family canon binding (id without the plugin prefix; FE-RULE-OWNERSHIP)
32
33
  stylelint: a rule of the @starci/stylelint-canon package
33
34
  machine: a check of the architecture machine (scripts/hfs/architecture), which reads the import graph
34
35
  hfs: a check of the hfs CLI that reads the repository tree, configuration, contracts or catalogs
@@ -1141,7 +1142,7 @@ rules:
1141
1142
 
1142
1143
  - id: R56
1143
1144
  code: "FE_HOOKS_ARE_HOOKS"
1144
- law: "`hooks/` hold hooks only: `hooks/swr` (one SWR wrapper per modules/api operation), `hooks/rhf` (forms), `hooks/zustand/<store>`, `hooks/socketio`, `hooks/effects`, `hooks/reuseables` and `hooks/<domain>` with one shared file per domain; a product hook is defined only there and imported through its index. A function in `hooks/` is a hook when it calls a hook: a React, SWR or Redux hook, a `use*` of the app's own `hooks/` folders (a `hooks/swr` wrapper, a sibling domain hook; resolved by TypeScript symbol resolution), or a listed library hook (next-intl, next/navigation, react-hook-form, zustand); a file that exports a non-hook still fails (owner ruling 2026-10-16 \"Theo y tro\")."
1145
+ law: "`hooks/` hold hooks only: `hooks/swr` (one SWR wrapper per modules/api operation), `hooks/rhf` (forms), `hooks/zustand/<store>`, `hooks/socketio`, `hooks/effects`, `hooks/reuseables` and `hooks/<domain>` with one shared file per domain; a product hook is defined only there and imported through its index. A function in `hooks/` is a hook when it calls a hook: a React, SWR or Redux hook, a `use*` of the app's own `hooks/` folders (a `hooks/swr` wrapper, a sibling domain hook; resolved by TypeScript symbol resolution), or a listed library hook (next-intl, next/navigation, next-themes useTheme, react-hook-form, zustand); a file that exports a non-hook still fails (owner ruling 2026-10-16 \"Theo y tro\")."
1145
1146
  kinds: [lint, codemod]
1146
1147
  gates: [settle, land, ci]
1147
1148
  failureCodes: ["FE_HOOKS_ARE_HOOKS", "FE_CUSTOM_HOOK_LOCATION", "FE_BLOCK_PRODUCT_HOOK_DEFINITION", "FE_COMPONENT_DEEP_HOOK_IMPORT"]
@@ -1209,7 +1210,7 @@ rules:
1209
1210
  code: "FE_I18N_PLACEMENT"
1210
1211
  editions: [lite, full]
1211
1212
  docs: [docs/architecture.md#r59]
1212
- law: "`next-intl` with `[locale]`; the next-intl stack is written once per front end: `fe/packages/<family>-i18n` (`createAppI18n`) called by each app's `modules/i18n/index.ts`, or the only app's `modules/i18n/`."
1213
+ law: "`next-intl` with `[locale]`; multiple apps share one `fe/packages/<family>-i18n` stack through `createAppI18n`. A single app keeps routing and request configuration in `modules/i18n` and `createNavigation` in the `modules/routes` public entry. The proxy imports routing or a routing-only shared proxy export without evaluating navigation runtime."
1213
1214
  kinds: [check, codemod, lint]
1214
1215
  gates: [settle, land, ci]
1215
1216
  failureCodes: ["FE_I18N_PLACEMENT", "FE_I18N_NAVIGATION", "FE_I18N_FORMATTER"]
@@ -1252,9 +1253,10 @@ rules:
1252
1253
  - {path: packages/hfs/src/hfs-i18n-catalog-coverage.spec.mjs, editions: [lite, full]}
1253
1254
  - id: R61
1254
1255
  code: "FE_STYLE_TOKEN_ONLY"
1255
- law: "Token-only CSS; colour only in `brand.css`; every `@source` resolves (sub-check `FE_STYLE_SOURCE_UNRESOLVED`). Status colours follow HeroUI soft pairs: every status tone has `--<tone>-soft` and `--<tone>-soft-foreground` in light and dark, the soft foreground reaches 3:1 on its tint and on `--background`, body text 4.5:1, a solid tone is never text below 4.5:1, and text and icons take the soft foreground, never the solid tone."
1256
+ law: "Application CSS uses the selected Grammar tokens for layout and the declared brand owner for brand values; every source declaration resolves. Status foregrounds reach 3:1 against their tinted surface and page background in each theme, body text reaches 4.5:1, and an insufficiently contrasting solid tone is never text. The selected Grammar owns the concrete token pairs, typography, scale and visual values."
1257
+ docs: [knowledge/patterns/fe/index.yaml#FE-RULE-OWNERSHIP, packages/stylelint/README.md, packages/eslint/fe/docs/brand-values.md, docs/source-process.md#the-test-ladder, knowledge/hfs/rules.yaml#R225]
1256
1258
  kinds: [lint, design]
1257
- gates: [pre-commit, pre-push, settle, land, ci]
1259
+ gates: [pre-commit, settle, land, ci]
1258
1260
  failureCodes: ["FE_STYLE_TOKEN_ONLY", "FE_STYLE_SOURCE_UNRESOLVED"]
1259
1261
  enforcers:
1260
1262
  - {kind: stylelint, id: token-only}
@@ -1271,33 +1273,59 @@ rules:
1271
1273
  - {kind: stylelint, id: breakpoint-scale}
1272
1274
  - {kind: stylelint, id: source-resolves}
1273
1275
  - {kind: eslint-fe, id: no-raw-brand-value}
1274
- - {kind: eslint-fe, id: no-fractional-step}
1276
+ - {kind: eslint-grammar-starci, id: no-fractional-step}
1275
1277
  - {kind: eslint-fe, id: no-arbitrary-value}
1276
- - {kind: eslint-fe, id: no-hand-rolled-heading}
1277
- - {kind: eslint-fe, id: no-unresolved-token-class}
1278
- - {kind: eslint-fe, id: no-heading-tag-outside-heading-component}
1279
- - {kind: eslint-fe, id: status-text-uses-soft-foreground}
1278
+ - {kind: eslint-grammar-starci, id: no-hand-rolled-heading}
1279
+ - {kind: eslint-grammar-starci, id: no-unresolved-token-class}
1280
+ - {kind: eslint-grammar-starci, id: no-heading-tag-outside-heading-component}
1281
+ - {kind: eslint-grammar-starci, id: status-text-uses-soft-foreground}
1282
+ proves:
1283
+ - {path: packages/stylelint/source-resolves.spec.mjs, editions: [lite, full], codes: [FE_STYLE_SOURCE_UNRESOLVED]}
1284
+ - {path: packages/eslint/fe/brand-values.spec.mjs, editions: [lite, full], codes: [FE_STYLE_TOKEN_ONLY]}
1285
+ - {path: packages/eslint/fe/tokens.spec.mjs, editions: [lite, full], codes: [FE_STYLE_TOKEN_ONLY]}
1286
+ - {path: packages/eslint/grammar-starci/status-colors.spec.mjs, editions: [lite, full], codes: [FE_STYLE_TOKEN_ONLY]}
1287
+ - {path: packages/eslint/grammar-starci/typography.spec.mjs, editions: [lite, full], codes: [FE_STYLE_TOKEN_ONLY]}
1288
+ - {path: packages/eslint/grammar-starci/tokens.spec.mjs, editions: [lite, full], codes: [FE_STYLE_TOKEN_ONLY]}
1289
+ exemplifiedBy:
1290
+ - {path: packages/hfs/templates/fe/skeleton-lite/apps/web/src/app/globals.css, edition: lite}
1291
+ - {path: packages/hfs/templates/fe/skeleton/apps/app/src/app/globals.css, edition: full}
1292
+ - {path: packages/stylelint/source-resolves.spec.mjs, editions: [lite, full], codes: [FE_STYLE_SOURCE_UNRESOLVED]}
1293
+ - {path: packages/eslint/fe/brand-values.spec.mjs, editions: [lite, full], codes: [FE_STYLE_TOKEN_ONLY]}
1294
+ - {path: packages/eslint/fe/tokens.spec.mjs, editions: [lite, full], codes: [FE_STYLE_TOKEN_ONLY]}
1295
+ - {path: packages/eslint/grammar-starci/status-colors.spec.mjs, editions: [lite, full], codes: [FE_STYLE_TOKEN_ONLY]}
1296
+ - {path: packages/eslint/grammar-starci/typography.spec.mjs, editions: [lite, full], codes: [FE_STYLE_TOKEN_ONLY]}
1297
+ - {path: packages/eslint/grammar-starci/tokens.spec.mjs, editions: [lite, full], codes: [FE_STYLE_TOKEN_ONLY]}
1298
+ witness:
1299
+ - {path: packages/stylelint/source-resolves.spec.mjs, editions: [lite, full], codes: [FE_STYLE_SOURCE_UNRESOLVED]}
1300
+ - {path: packages/eslint/fe/brand-values.spec.mjs, editions: [lite, full], codes: [FE_STYLE_TOKEN_ONLY]}
1301
+ - {path: packages/eslint/fe/tokens.spec.mjs, editions: [lite, full], codes: [FE_STYLE_TOKEN_ONLY]}
1302
+ - {path: packages/eslint/grammar-starci/status-colors.spec.mjs, editions: [lite, full], codes: [FE_STYLE_TOKEN_ONLY]}
1303
+ - {path: packages/eslint/grammar-starci/typography.spec.mjs, editions: [lite, full], codes: [FE_STYLE_TOKEN_ONLY]}
1304
+ - {path: packages/eslint/grammar-starci/tokens.spec.mjs, editions: [lite, full], codes: [FE_STYLE_TOKEN_ONLY]}
1280
1305
  - id: R62
1281
1306
  code: "FE_NATIVE_FORM_CONTROL"
1282
1307
  editions: [lite, full]
1283
- docs: [docs/architecture.md#r62]
1308
+ docs: [docs/architecture.md#r62, knowledge/patterns/fe/index.yaml#FE-RULE-OWNERSHIP]
1284
1309
  proves:
1310
+ - {path: packages/eslint/grammar-starci/r62-structural-elements-coverage.spec.mjs, editions: [lite, full], codes: [FE_NATIVE_FORM_CONTROL]}
1285
1311
  - {path: packages/eslint/fe/r62-native-controls-coverage.spec.mjs, editions: [lite, full]}
1286
1312
  exemplifiedBy:
1313
+ - {path: packages/eslint/grammar-starci/r62-structural-elements-coverage.spec.mjs, editions: [lite, full], codes: [FE_NATIVE_FORM_CONTROL]}
1287
1314
  - {path: packages/hfs/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx, edition: lite}
1288
1315
  - {path: packages/hfs/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/component.tsx, edition: full}
1289
1316
  - {path: packages/eslint/fe/r62-native-controls-coverage.spec.mjs, editions: [lite, full]}
1290
1317
  witness:
1318
+ - {path: packages/eslint/grammar-starci/r62-structural-elements-coverage.spec.mjs, editions: [lite, full], codes: [FE_NATIVE_FORM_CONTROL]}
1291
1319
  - {path: packages/eslint/fe/r62-native-controls-coverage.spec.mjs, edition: lite, codes: [FE_NATIVE_FORM_CONTROL, FE_NATIVE_IMAGE]}
1292
1320
  - {path: packages/eslint/fe/r62-native-controls-coverage.spec.mjs, edition: full, codes: [FE_NATIVE_FORM_CONTROL, FE_NATIVE_IMAGE]}
1293
- law: "No raw form controls, native images or raw structural tags in product tiers; the grammar renders them."
1321
+ law: "Product tiers compose the selected Grammar renderers for form controls, images and structural elements that the Grammar owns; they never reproduce those renderers with raw HTML."
1294
1322
  kinds: [lint, design]
1295
1323
  gates: ["pre-commit", "settle", "land", "ci"]
1296
1324
  failureCodes: ["FE_NATIVE_FORM_CONTROL", "FE_NATIVE_IMAGE"]
1297
1325
  enforcers:
1298
1326
  - {kind: eslint-fe, id: no-native-form-control}
1299
1327
  - {kind: eslint-fe, id: no-native-img}
1300
- - {kind: eslint-fe, id: no-raw-structural-element}
1328
+ - {kind: eslint-grammar-starci, id: no-raw-structural-element}
1301
1329
  - {kind: eslint-fe, id: image-has-size}
1302
1330
 
1303
1331
  - id: R63
@@ -1836,7 +1864,7 @@ rules:
1836
1864
  gates: ["pre-commit","settle","land","ci"]
1837
1865
  - id: R92
1838
1866
  code: "FE_COMPONENT_API"
1839
- law: "A component exposes the canon surface: typed named props and slots, an exact sibling XBase presentational twin for every connected block, status through the slot view, no className or CSS doors, no resting twin or placeholder prop, class names in the colocated file; a pure component reaches no hook, router, locale or API, but may render a list of blocks supplied through render props without importing blocks or owning their data."
1867
+ law: "A component exposes the canon surface: typed named props and slots, an exact sibling XBase presentational twin for every connected block, status through the selected Grammar state renderer, no className or CSS doors, no resting twin or placeholder prop, class names in the colocated file; a pure component reaches no hook, router, locale or API, but may render a list of blocks supplied through render props without importing blocks or owning their data."
1840
1868
  kinds: [lint, design]
1841
1869
  gates: [pre-commit, settle, land, ci]
1842
1870
  failureCodes: ["FE_COMPONENT_API", "FE_COMPONENT_WORLD_OWNERSHIP", "FE_WORLD_OWNER_RENDER_BOUNDARY", "FE_CONNECTED_BLOCK_RENDER_PAIR", "FE_PURE_REACHES_DATA", "FE_PURE_WORLD_HOOK", "FE_PURE_WORLD_IMPORT"]
@@ -1850,7 +1878,7 @@ rules:
1850
1878
  - {kind: eslint-fe, id: base-props-atom}
1851
1879
  - {kind: eslint-fe, id: base-import-pair}
1852
1880
  - {kind: eslint-fe, id: no-data-status-shape}
1853
- - {kind: eslint-fe, id: slot-status-through-slotview}
1881
+ - {kind: eslint-grammar-starci, id: slot-status-through-slotview}
1854
1882
  - {kind: eslint-fe, id: presentational-purity}
1855
1883
  - {kind: eslint-fe, id: connected-block-has-presentational-twin}
1856
1884
  - {kind: eslint-fe, id: no-resting-twin-component}
@@ -1861,62 +1889,54 @@ rules:
1861
1889
  - {kind: eslint-fe, id: cn-arguments-are-single-tokens}
1862
1890
  - {kind: eslint-fe, id: component-purity}
1863
1891
  editions: [lite, full]
1864
- docs: [docs/architecture.md#r92]
1892
+ docs: [docs/architecture.md#r92, knowledge/patterns/fe/index.yaml#FE-RULE-OWNERSHIP]
1865
1893
  proves:
1894
+ - {path: packages/eslint/grammar-starci/r92-slot-status-coverage.spec.mjs, editions: [lite, full], codes: [FE_COMPONENT_API]}
1866
1895
  - {path: packages/eslint/fe/r92-fetch-barrel.spec.mjs, editions: [lite, full]}
1867
1896
  - {path: packages/eslint/fe/r92-component-surface-coverage.spec.mjs, editions: [lite, full]}
1868
1897
  - {path: packages/eslint/fe/r56-r92-graph-coverage.spec.mjs, editions: [lite, full]}
1869
1898
  exemplifiedBy:
1899
+ - {path: packages/eslint/grammar-starci/r92-slot-status-coverage.spec.mjs, editions: [lite, full], codes: [FE_COMPONENT_API]}
1870
1900
  - {path: packages/hfs/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/component.tsx, editions: [lite, full]}
1871
1901
  - {path: packages/eslint/fe/r92-fetch-barrel.spec.mjs, editions: [lite, full]}
1872
1902
  - {path: packages/eslint/fe/r92-component-surface-coverage.spec.mjs, editions: [lite, full]}
1873
1903
  - {path: packages/eslint/fe/r56-r92-graph-coverage.spec.mjs, editions: [lite, full]}
1874
1904
  witness:
1905
+ - {path: packages/eslint/grammar-starci/r92-slot-status-coverage.spec.mjs, editions: [lite, full], codes: [FE_COMPONENT_API]}
1875
1906
  - {path: packages/eslint/fe/r92-fetch-barrel.spec.mjs, editions: [lite, full]}
1876
1907
  - {path: packages/eslint/fe/r92-component-surface-coverage.spec.mjs, editions: [lite, full]}
1877
1908
  - {path: packages/eslint/fe/r56-r92-graph-coverage.spec.mjs, editions: [lite, full]}
1878
1909
  - id: R93
1879
1910
  code: "FE_VENDOR_BOUNDARY"
1880
- law: "Vendor primitives and icons reach components only through their named owner: the one icon set of knowledge/hfs/fe-stack.yaml (the heroicons family) through the icon leaf at the glyph scale, every vendor primitive behind a named owner; the Grammar package is imported only through its selected code and style entry and keeps its declared contract. Apps compose Grammar surfaces with layout-only CSS; surface context supplies nested field variants and panel list alignment, without raw surface paint or stacked horizontal list-wrapper padding."
1911
+ law: "Vendor primitives and icons reach components only through their named owner: one declared icon set through its icon owner, one icon size per region chosen from the selected Grammar scale, and every vendor primitive behind a named owner. The Grammar package is imported through its selected public code and style entries and keeps its declared contract. The Grammar owns the visual values and component API that realize these requirements."
1881
1912
  kinds: [lint, codemod]
1882
1913
  gates: [pre-commit, settle, land, ci]
1883
- failureCodes: ["FE_VENDOR_BOUNDARY", "ARCH_GRAMMAR_CONTRACT_INVALID", "ARCH_GRAMMAR_EXPORT_BYPASS", "FE_NESTED_VARIANT", "FE_LIST_PANEL_PADDING"]
1884
- enforcers:
1885
- - {kind: eslint-fe, id: no-vendor-icon-outside-icon-leaf}
1886
- - {kind: eslint-fe, id: heroicons-is-the-glyph-vendor}
1887
- - {kind: eslint-fe, id: no-off-scale-glyph-size}
1888
- - {kind: eslint-fe, id: no-icon-size-class}
1889
- - {kind: eslint-fe, id: destructive-menu-entry-needs-danger-tone}
1890
- - {kind: eslint-fe, id: no-decorative-icon-in-metric-cell}
1891
- - {kind: eslint-fe, id: vendor-primitive-has-named-owner}
1914
+ failureCodes: ["FE_VENDOR_BOUNDARY", "ARCH_GRAMMAR_CONTRACT_INVALID", "ARCH_GRAMMAR_EXPORT_BYPASS"]
1915
+ enforcers:
1916
+ - {kind: eslint-grammar-starci, id: no-vendor-icon-outside-icon-leaf}
1917
+ - {kind: eslint-grammar-starci, id: heroicons-is-the-glyph-vendor}
1918
+ - {kind: eslint-grammar-starci, id: no-off-scale-glyph-size}
1919
+ - {kind: eslint-grammar-starci, id: no-icon-size-class}
1920
+ - {kind: eslint-grammar-starci, id: destructive-menu-entry-needs-danger-tone}
1921
+ - {kind: eslint-grammar-starci, id: no-decorative-icon-in-metric-cell}
1922
+ - {kind: eslint-grammar-starci, id: vendor-primitive-has-named-owner}
1892
1923
  - {kind: eslint-fe, id: grammar-entry}
1893
- - {kind: eslint-fe, id: nested-control-variant}
1894
- - {kind: eslint-fe, id: no-list-panel-padding}
1895
1924
  - {kind: machine, id: grammar, at: scripts/hfs/architecture/frontend-grammar.mjs}
1896
1925
  editions: [lite, full]
1897
- docs: ["docs/code-pattern-enforcement.md#r93-vendor-and-grammar-ownership"]
1926
+ docs: ["docs/code-pattern-enforcement.md#r93-vendor-and-grammar-ownership", knowledge/patterns/fe/index.yaml#FE-RULE-OWNERSHIP]
1898
1927
  templateNa: "Vendor boundary violations and installed Grammar package contracts are checked against consumer ownership; no managed template scaffolds a vendor install or a forbidden import."
1899
1928
  proves:
1900
- - packages/eslint/fe/r93-vendor-coverage.spec.mjs
1929
+ - packages/eslint/grammar-starci/r93-vendor-coverage.spec.mjs
1901
1930
  - packages/eslint/fe/r93-grammar-ownership.spec.mjs
1902
- - packages/eslint/fe/menu-entries.spec.mjs
1903
- - packages/eslint/fe/surface-rules.spec.mjs
1931
+ - packages/eslint/grammar-starci/menu-entries.spec.mjs
1904
1932
  exemplifiedBy:
1905
- - {path: packages/eslint/fe/r93-vendor-coverage.spec.mjs, editions: [lite, full]}
1933
+ - {path: packages/eslint/grammar-starci/r93-vendor-coverage.spec.mjs, editions: [lite, full]}
1906
1934
  - {path: packages/eslint/fe/r93-grammar-ownership.spec.mjs, editions: [lite, full]}
1907
- - {path: packages/eslint/fe/menu-entries.spec.mjs, editions: [lite, full]}
1908
- - {path: packages/eslint/fe/surface-rules.spec.mjs, editions: [lite, full]}
1935
+ - {path: packages/eslint/grammar-starci/menu-entries.spec.mjs, editions: [lite, full]}
1909
1936
  witness:
1910
- - {path: packages/eslint/fe/menu-entries.spec.mjs, editions: [lite, full], codes: [FE_VENDOR_BOUNDARY]}
1911
- - {path: packages/eslint/fe/r93-vendor-coverage.spec.mjs, editions: [lite, full], codes: [FE_VENDOR_BOUNDARY]}
1937
+ - {path: packages/eslint/grammar-starci/menu-entries.spec.mjs, editions: [lite, full], codes: [FE_VENDOR_BOUNDARY]}
1938
+ - {path: packages/eslint/grammar-starci/r93-vendor-coverage.spec.mjs, editions: [lite, full], codes: [FE_VENDOR_BOUNDARY]}
1912
1939
  - {path: packages/eslint/fe/r93-grammar-ownership.spec.mjs, editions: [lite, full], codes: [ARCH_GRAMMAR_CONTRACT_INVALID, ARCH_GRAMMAR_EXPORT_BYPASS]}
1913
- - {path: packages/eslint/fe/surface-rules.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_LIST_PANEL_PADDING]}
1914
- cases:
1915
- - id: case-1
1916
- date: "2026-10-25"
1917
- source: owner
1918
- quote: "apps on Grammar write layout-only CSS; every visual element lives in @starci/grammar"
1919
- ruling: "ANATOMY-2 owns contextual field defaults, PADDING-0 owns panel list alignment, and COLLECTION-3 owns composite member avatars for conversation and group identity. Grammar supplies these capabilities; the FE canon rejects explicit primary fields inside surfaces, raw app surface paint and stacked horizontal list-wrapper padding."
1920
1940
  - id: R94
1921
1941
  code: "FE_SLOT_FILE_ROLE"
1922
1942
  law: "A front-end file is owned by a slot and named by it: a slot that owns a directory (route, feature, components, hooks, api module) holds only the files its `allows` list names, and holds them inside a folder of its own."
@@ -4978,27 +4998,27 @@ rules:
4978
4998
 
4979
4999
  - id: R286
4980
5000
  code: "FE_COPY_IN_CALL"
4981
- law: "Copy shown by validation and toast calls comes from the next-intl catalog, including Zod message arguments and toast titles or descriptions. Browser alert calls are forbidden. Inline SVG glyphs are forbidden outside a declared brand-mark leaf; ordinary icons use the one Heroicons family through the Icon leaf."
5001
+ law: "Validation and toast copy comes from the translation catalog, including validation messages, titles and descriptions. Browser alert calls are forbidden. Inline vector glyphs belong only to declared brand-mark owners; ordinary icons use the selected Grammar icon owner and its declared glyph family."
4982
5002
  kinds: [lint]
4983
5003
  gates: [pre-commit, settle, land, ci]
4984
5004
  failureCodes: ["FE_COPY_IN_CALL", "FE_NO_ALERT", "FE_ICON_INLINE_SVG"]
4985
5005
  enforcers:
4986
5006
  - {kind: eslint-fe, id: no-copy-in-call}
4987
5007
  - {kind: eslint-fe, id: no-alert}
4988
- - {kind: eslint-fe, id: no-inline-svg-outside-brand-mark-leaf}
5008
+ - {kind: eslint-grammar-starci, id: no-inline-svg-outside-brand-mark-leaf}
4989
5009
  editions: [lite, full]
4990
- docs: [docs/quality-bar.md#r286]
5010
+ docs: [docs/quality-bar.md#r286, knowledge/patterns/fe/index.yaml#FE-RULE-OWNERSHIP]
4991
5011
  exemplifiedBy:
4992
5012
  - {path: packages/eslint/fe/translation.spec.mjs, editions: [lite, full], codes: [FE_COPY_IN_CALL, FE_NO_ALERT]}
4993
- - {path: packages/eslint/fe/icon.spec.mjs, editions: [lite, full], codes: [FE_ICON_INLINE_SVG]}
5013
+ - {path: packages/eslint/grammar-starci/icon.spec.mjs, editions: [lite, full], codes: [FE_ICON_INLINE_SVG]}
4994
5014
  - {path: packages/hfs/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx, editions: [lite, full]}
4995
5015
  - {path: packages/hfs/templates/fe/skeleton/apps/__app__/src/modules/toast, editions: [lite, full]}
4996
5016
  proves:
4997
5017
  - {path: packages/eslint/fe/translation.spec.mjs, editions: [lite, full], codes: [FE_COPY_IN_CALL, FE_NO_ALERT]}
4998
- - {path: packages/eslint/fe/icon.spec.mjs, editions: [lite, full], codes: [FE_ICON_INLINE_SVG]}
5018
+ - {path: packages/eslint/grammar-starci/icon.spec.mjs, editions: [lite, full], codes: [FE_ICON_INLINE_SVG]}
4999
5019
  witness:
5000
5020
  - {path: packages/eslint/fe/translation.spec.mjs, editions: [lite, full], codes: [FE_COPY_IN_CALL, FE_NO_ALERT]}
5001
- - {path: packages/eslint/fe/icon.spec.mjs, editions: [lite, full], codes: [FE_ICON_INLINE_SVG]}
5021
+ - {path: packages/eslint/grammar-starci/icon.spec.mjs, editions: [lite, full], codes: [FE_ICON_INLINE_SVG]}
5002
5022
 
5003
5023
  - id: R287
5004
5024
  code: "FE_CALLBACK_SHAPE"
@@ -5021,21 +5041,21 @@ rules:
5021
5041
 
5022
5042
  - id: R288
5023
5043
  code: "FE_STACK_UNLISTED"
5024
- law: "Every direct FE workspace dependency belongs to knowledge/hfs/fe-stack.yaml or is declared in hfs.json fe.extraPackages with its name and a nonempty reason. Installed runtime dependencies carried by a listed Grammar package are not app declarations and raise no FE_STACK_UNLISTED; direct forbidden dependencies still raise FE_STACK_FORBIDDEN. Every product Next app declares the catalog's core runtime packages; codegen, lint, TypeScript and Tailwind tools may be installed once at the app root. The marketing landing app is exempt (lite and full) from core presence. GraphQL documents, including gql templates, match the back-end contracts, and SWR keys carry fetcher identities without an opt-in declaration."
5044
+ law: "Every direct FE workspace dependency belongs to knowledge/hfs/fe-stack.yaml or is declared in hfs.json fe.extraPackages with its name and a nonempty reason. Installed runtime dependencies carried by a listed Grammar package are not app declarations and raise no FE_STACK_UNLISTED; direct forbidden dependencies still raise FE_STACK_FORBIDDEN. Every product Next app declares the catalog's core runtime packages; codegen, lint, TypeScript and Tailwind tools may be installed once at the app root. The marketing landing app is exempt (lite and full) from core presence. A front end that selects sides.fe.grammar and declares @starci/eslint-canon-fe 9 or later also declares that family's lint package from knowledge/grammars/index.yaml, which carries its Grammar component rules (FE_STACK_FAMILY_LINT_MISSING). GraphQL documents, including gql templates, match the back-end contracts, and SWR keys carry fetcher identities without an opt-in declaration."
5025
5045
  kinds: [check]
5026
5046
  gates: [settle, land, ci]
5027
- failureCodes: ["FE_STACK_UNLISTED", "FE_STACK_CORE_MISSING"]
5047
+ failureCodes: ["FE_STACK_UNLISTED", "FE_STACK_CORE_MISSING", "FE_STACK_FAMILY_LINT_MISSING"]
5028
5048
  enforcers:
5029
5049
  - {kind: hfs, id: fe-stack-allowlist, at: scripts/hfs/rules/fe-stack.mjs}
5030
5050
  editions: [lite, full]
5031
5051
  docs: [docs/quality-bar.md#r288]
5032
5052
  proves:
5033
- - {path: packages/hfs/src/hfs-fe-stack.spec.mjs, editions: [lite, full], codes: [FE_STACK_UNLISTED, FE_STACK_CORE_MISSING]}
5053
+ - {path: packages/hfs/src/hfs-fe-stack.spec.mjs, editions: [lite, full], codes: [FE_STACK_UNLISTED, FE_STACK_CORE_MISSING, FE_STACK_FAMILY_LINT_MISSING]}
5034
5054
  exemplifiedBy:
5035
5055
  - {path: packages/hfs/templates/fe/app/package.json.tpl, editions: [lite, full]}
5036
- - {path: packages/hfs/src/hfs-fe-stack.spec.mjs, editions: [lite, full], codes: [FE_STACK_UNLISTED, FE_STACK_CORE_MISSING]}
5056
+ - {path: packages/hfs/src/hfs-fe-stack.spec.mjs, editions: [lite, full], codes: [FE_STACK_UNLISTED, FE_STACK_CORE_MISSING, FE_STACK_FAMILY_LINT_MISSING]}
5037
5057
  witness:
5038
- - {path: packages/hfs/src/hfs-fe-stack.spec.mjs, editions: [lite, full], codes: [FE_STACK_UNLISTED, FE_STACK_CORE_MISSING]}
5058
+ - {path: packages/hfs/src/hfs-fe-stack.spec.mjs, editions: [lite, full], codes: [FE_STACK_UNLISTED, FE_STACK_CORE_MISSING, FE_STACK_FAMILY_LINT_MISSING]}
5039
5059
  - id: R289
5040
5060
  code: "BE_APP_ENV_REQUIRED"
5041
5061
  law: "Every declared back-end app owns a tracked apps/<app>/src/<app>.env.ts exporting its typed environment interface and read<App>Env(env: EnvSource). main.ts constructs EnvSource once and delegates its reads to that reader, with no inline parsing. app.module.ts composes module lists only by spreading compose/<area>.ts groups; its own providers are limited to app-wide guards and filters."
@@ -5450,101 +5470,161 @@ rules:
5450
5470
  templateNa: "A storage grant is declared per integration; the canon scaffolds none, and the presigned-upload examples of both editions are the reference."
5451
5471
  - id: R313
5452
5472
  code: "FE_APP_SHELL_FRAME"
5453
- law: "FE-APP-SHELL-1 requires WorkspaceShell viewport=fixed to own the application frame, including persistent header and navigation; hand-built header/nav frames are refused."
5473
+ law: "FE-APP-SHELL-1 gives the selected Grammar shell ownership of the fixed application frame, navbar, subnav and sidebar, with independent navigation and content scroll; StarCi requires WorkspaceShell viewport=fixed with navigation resolving visibility to wide and navigationDrawer or a compactHeader containing a left Drawer with Sidebar presentation=drawer; application layout does not recreate this frame."
5454
5474
  kinds: [lint, design]
5455
5475
  gates: [pre-commit, settle, land, ci]
5456
5476
  failureCodes: ["FE_APP_SHELL_FRAME"]
5457
5477
  enforcers:
5458
- - {kind: eslint-fe, id: app-shell-frame}
5478
+ - {kind: eslint-grammar-starci, id: app-shell-frame}
5459
5479
  editions: [lite, full]
5460
5480
  docs:
5481
+ - knowledge/patterns/fe/index.yaml#FE-RULE-OWNERSHIP
5461
5482
  - {path: knowledge/ui/composition/app-shell.yaml, editions: [lite, full]}
5462
5483
  proves:
5463
- - {path: packages/eslint/fe/app-shell.spec.mjs, editions: [lite, full], codes: [FE_APP_SHELL_FRAME]}
5484
+ - {path: packages/eslint/grammar-starci/app-shell.spec.mjs, editions: [lite, full], codes: [FE_APP_SHELL_FRAME]}
5464
5485
  exemplifiedBy:
5465
- - {path: packages/eslint/fe/app-shell.spec.mjs, editions: [lite, full], codes: [FE_APP_SHELL_FRAME]}
5486
+ - {path: packages/eslint/grammar-starci/app-shell.spec.mjs, editions: [lite, full], codes: [FE_APP_SHELL_FRAME]}
5466
5487
  - {path: examples/full-app/fe/apps/app/src/features/layouts/ShopLayout/component.tsx, edition: full}
5467
5488
  - {path: examples/lite-app/fe/apps/web/src/features/layouts/WorkspaceLayout/component.tsx, edition: lite}
5468
5489
  witness:
5469
- - {path: packages/eslint/fe/app-shell.spec.mjs, editions: [lite, full], codes: [FE_APP_SHELL_FRAME]}
5490
+ - {path: packages/eslint/grammar-starci/app-shell.spec.mjs, editions: [lite, full], codes: [FE_APP_SHELL_FRAME]}
5470
5491
  templateNa: "The app composes its shell from product destinations and identity; full and lite example shells provide the reference."
5471
5492
  - id: R314
5472
5493
  code: "FE_NAVBAR_CONTROL_SIZE"
5473
- law: "FE-APP-SHELL-2 requires every navbar control to declare size=control so Grammar owns the common control height."
5494
+ law: "FE-APP-SHELL-2 keeps navbar controls at one common height and icons at one size per region from the selected Grammar scale; the Grammar owns their concrete values."
5474
5495
  kinds: [lint, design]
5475
5496
  gates: [pre-commit, settle, land, ci]
5476
5497
  failureCodes: ["FE_NAVBAR_CONTROL_SIZE"]
5477
5498
  enforcers:
5478
- - {kind: eslint-fe, id: navbar-control-size}
5499
+ - {kind: eslint-grammar-starci, id: navbar-control-size}
5479
5500
  editions: [lite, full]
5480
5501
  docs:
5502
+ - knowledge/patterns/fe/index.yaml#FE-RULE-OWNERSHIP
5481
5503
  - {path: knowledge/ui/composition/app-shell.yaml, editions: [lite, full]}
5482
5504
  proves:
5483
- - {path: packages/eslint/fe/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CONTROL_SIZE]}
5505
+ - {path: packages/eslint/grammar-starci/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CONTROL_SIZE]}
5484
5506
  exemplifiedBy:
5485
- - {path: packages/eslint/fe/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CONTROL_SIZE]}
5507
+ - {path: packages/eslint/grammar-starci/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CONTROL_SIZE]}
5486
5508
  - {path: examples/full-app/fe/packages/ecommerce-ui/src/branches/WorkspaceNavbar/component.tsx, edition: full}
5487
5509
  - {path: examples/lite-app/fe/apps/web/src/components/branches/Navbar/component.tsx, edition: lite}
5488
5510
  witness:
5489
- - {path: packages/eslint/fe/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CONTROL_SIZE]}
5511
+ - {path: packages/eslint/grammar-starci/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CONTROL_SIZE]}
5490
5512
  templateNa: "The app composes its shell from product destinations and identity; full and lite example shells provide the reference."
5491
5513
  - id: R315
5492
5514
  code: "FE_NAVBAR_CLUSTER"
5493
- law: "FE-APP-SHELL-3 requires the statically composed navbar right cluster to contain a tertiary language IconButton opening DropdownMenu, a theme Switch, a notification IconButton opening Popover and an account AvatarButton opening DropdownMenu, in that order."
5515
+ law: "FE-APP-SHELL-3 orders the navbar controls as language, theme, notifications and account, with each control and its overlay supplied by the selected Grammar."
5494
5516
  kinds: [lint, design]
5495
5517
  gates: [pre-commit, settle, land, ci]
5496
5518
  failureCodes: ["FE_NAVBAR_CLUSTER"]
5497
5519
  enforcers:
5498
- - {kind: eslint-fe, id: navbar-cluster}
5520
+ - {kind: eslint-grammar-starci, id: navbar-cluster}
5499
5521
  editions: [lite, full]
5500
5522
  docs:
5523
+ - knowledge/patterns/fe/index.yaml#FE-RULE-OWNERSHIP
5501
5524
  - {path: knowledge/ui/composition/app-shell.yaml, editions: [lite, full]}
5502
5525
  proves:
5503
- - {path: packages/eslint/fe/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CLUSTER]}
5526
+ - {path: packages/eslint/grammar-starci/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CLUSTER]}
5504
5527
  exemplifiedBy:
5505
- - {path: packages/eslint/fe/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CLUSTER]}
5528
+ - {path: packages/eslint/grammar-starci/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CLUSTER]}
5506
5529
  - {path: examples/full-app/fe/packages/ecommerce-ui/src/branches/WorkspaceNavbar/component.tsx, edition: full}
5507
5530
  - {path: examples/lite-app/fe/apps/web/src/components/branches/Navbar/component.tsx, edition: lite}
5508
5531
  witness:
5509
- - {path: packages/eslint/fe/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CLUSTER]}
5532
+ - {path: packages/eslint/grammar-starci/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CLUSTER]}
5510
5533
  templateNa: "The app composes its shell from product destinations and identity; full and lite example shells provide the reference."
5511
5534
  - id: R316
5512
5535
  code: "FE_APP_SHELL_BOTTOM_TABS"
5513
- law: "FE-APP-SHELL-4 keeps navigation in the Sidebar drawer and refuses a bottom tab bar in the application shell."
5536
+ law: "FE-APP-SHELL-4 preserves the selected Grammar shell navigation model across viewports; an application does not replace its sidebar-derived mobile drawer with an incompatible bottom tab bar."
5514
5537
  kinds: [lint, design]
5515
5538
  gates: [pre-commit, settle, land, ci]
5516
5539
  failureCodes: ["FE_APP_SHELL_BOTTOM_TABS"]
5517
5540
  enforcers:
5518
- - {kind: eslint-fe, id: no-shell-bottom-tabs}
5541
+ - {kind: eslint-grammar-starci, id: no-shell-bottom-tabs}
5519
5542
  editions: [lite, full]
5520
5543
  docs:
5544
+ - knowledge/patterns/fe/index.yaml#FE-RULE-OWNERSHIP
5521
5545
  - {path: knowledge/ui/composition/app-shell.yaml, editions: [lite, full]}
5522
5546
  proves:
5523
- - {path: packages/eslint/fe/app-shell.spec.mjs, editions: [lite, full], codes: [FE_APP_SHELL_BOTTOM_TABS]}
5547
+ - {path: packages/eslint/grammar-starci/app-shell.spec.mjs, editions: [lite, full], codes: [FE_APP_SHELL_BOTTOM_TABS]}
5524
5548
  exemplifiedBy:
5525
- - {path: packages/eslint/fe/app-shell.spec.mjs, editions: [lite, full], codes: [FE_APP_SHELL_BOTTOM_TABS]}
5549
+ - {path: packages/eslint/grammar-starci/app-shell.spec.mjs, editions: [lite, full], codes: [FE_APP_SHELL_BOTTOM_TABS]}
5526
5550
  - {path: examples/full-app/fe/apps/app/src/features/layouts/ShopLayout/component.tsx, edition: full}
5527
5551
  - {path: examples/lite-app/fe/apps/web/src/features/layouts/WorkspaceLayout/component.tsx, edition: lite}
5528
5552
  witness:
5529
- - {path: packages/eslint/fe/app-shell.spec.mjs, editions: [lite, full], codes: [FE_APP_SHELL_BOTTOM_TABS]}
5553
+ - {path: packages/eslint/grammar-starci/app-shell.spec.mjs, editions: [lite, full], codes: [FE_APP_SHELL_BOTTOM_TABS]}
5530
5554
  templateNa: "The app composes its shell from product destinations and identity; full and lite example shells provide the reference."
5531
5555
  - id: R317
5532
5556
  code: "FE_NAVBAR_CREATE"
5533
- law: "FE-APP-SHELL-5 places create actions on their pages and refuses them in the navbar."
5557
+ law: "FE-APP-SHELL-5 places create actions on the page that owns them and keeps them out of the global navbar."
5534
5558
  kinds: [lint, design]
5535
5559
  gates: [pre-commit, settle, land, ci]
5536
5560
  failureCodes: ["FE_NAVBAR_CREATE"]
5537
5561
  enforcers:
5538
- - {kind: eslint-fe, id: no-navbar-create}
5562
+ - {kind: eslint-grammar-starci, id: no-navbar-create}
5539
5563
  editions: [lite, full]
5540
5564
  docs:
5565
+ - knowledge/patterns/fe/index.yaml#FE-RULE-OWNERSHIP
5541
5566
  - {path: knowledge/ui/composition/app-shell.yaml, editions: [lite, full]}
5542
5567
  proves:
5543
- - {path: packages/eslint/fe/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CREATE]}
5568
+ - {path: packages/eslint/grammar-starci/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CREATE]}
5544
5569
  exemplifiedBy:
5545
- - {path: packages/eslint/fe/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CREATE]}
5570
+ - {path: packages/eslint/grammar-starci/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CREATE]}
5546
5571
  - {path: examples/full-app/fe/packages/ecommerce-ui/src/branches/WorkspaceNavbar/component.tsx, edition: full}
5547
5572
  - {path: examples/lite-app/fe/apps/web/src/components/branches/Navbar/component.tsx, edition: lite}
5548
5573
  witness:
5549
- - {path: packages/eslint/fe/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CREATE]}
5574
+ - {path: packages/eslint/grammar-starci/app-shell.spec.mjs, editions: [lite, full], codes: [FE_NAVBAR_CREATE]}
5550
5575
  templateNa: "The app composes its shell from product destinations and identity; full and lite example shells provide the reference."
5576
+ - id: R318
5577
+ code: "FE_NESTED_VARIANT"
5578
+ law: "Apps compose Grammar with layout-only CSS; paint utilities, arbitrary paint properties and inline style paint belong to Grammar. Grammar context owns all layer-aware field variants, nested Tabs defaults and the content edge of panel lists; apps omit static field variants, do not force primary Tabs inside a painted surface, do not paint raw surfaces, and add no horizontal list-wrapper padding within the nearest painted Grammar surface."
5579
+ kinds: [lint, design]
5580
+ gates: [pre-commit, settle, land, ci]
5581
+ failureCodes: ["FE_NESTED_VARIANT", "FE_LIST_PANEL_PADDING", "FE_GRAMMAR_LAYOUT_ONLY"]
5582
+ enforcers:
5583
+ - {kind: eslint-fe, id: nested-control-variant}
5584
+ - {kind: eslint-fe, id: no-list-panel-padding}
5585
+ - {kind: eslint-fe, id: no-app-surface-paint}
5586
+ editions: [lite, full]
5587
+ docs: ["docs/code-pattern-enforcement.md#r318-app-grammar-composition"]
5588
+ templateNa: "Product composition is proved by the FE canon's Grammar import and surface fixtures; a template cannot choose product slots or lists."
5589
+ proves:
5590
+ - {path: packages/eslint/fe/surface-rules.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_LIST_PANEL_PADDING, FE_GRAMMAR_LAYOUT_ONLY]}
5591
+ exemplifiedBy:
5592
+ - {path: packages/eslint/fe/surface-rules.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_LIST_PANEL_PADDING, FE_GRAMMAR_LAYOUT_ONLY]}
5593
+ witness:
5594
+ - {path: packages/eslint/fe/surface-rules.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_LIST_PANEL_PADDING, FE_GRAMMAR_LAYOUT_ONLY]}
5595
+ cases:
5596
+ - id: case-1
5597
+ date: "2026-10-25"
5598
+ source: owner
5599
+ quote: "apps on Grammar write layout-only CSS; every visual element lives in @starci/grammar"
5600
+ ruling: "ANATOMY-2 owns contextual field defaults, PADDING-0 owns panel list alignment, and COLLECTION-3 owns composite member avatars for conversation and group identity. Grammar supplies these capabilities; the FE canon rejects static field variants on either layer, primary Tabs inside painted surfaces, raw app surface paint and stacked horizontal list-wrapper padding inside painted surfaces."
5601
+
5602
+ - id: R319
5603
+ code: "FE_BUTTON_INTENT"
5604
+ law: "An app Button or IconButton declares an intent and never a variant; Grammar resolves the variant from intent and placement (CTA-6). A destructive confirm lives in a confirmation dialog, a render holds at most one commit, and a collection item holds none."
5605
+ kinds: [codemod, lint, design]
5606
+ gates: [pre-commit, settle, land, ci]
5607
+ failureCodes: ["FE_BUTTON_INTENT", "FE_BUTTON_INTENT_REQUIRED", "FE_BUTTON_NO_VARIANT", "FE_DESTRUCTIVE_CONFIRM_IN_DIALOG", "FE_SINGLE_COMMIT_PER_RENDER", "FE_NO_COMMIT_IN_COLLECTION_ITEM"]
5608
+ enforcers:
5609
+ - {kind: eslint-fe, id: button-intent-required}
5610
+ - {kind: eslint-fe, id: button-no-variant}
5611
+ - {kind: eslint-fe, id: destructive-confirm-in-dialog}
5612
+ - {kind: eslint-fe, id: single-commit-per-render}
5613
+ - {kind: eslint-fe, id: no-commit-in-collection-item}
5614
+ editions: [lite, full]
5615
+ docs:
5616
+ - knowledge/ui/composition/cta.yaml
5617
+ - docs/code-pattern-enforcement.md#r319-button-intent
5618
+ proves:
5619
+ - {path: packages/eslint/fe/button-intent.spec.mjs, editions: [lite, full], codes: [FE_BUTTON_INTENT, FE_BUTTON_INTENT_REQUIRED, FE_BUTTON_NO_VARIANT, FE_DESTRUCTIVE_CONFIRM_IN_DIALOG, FE_SINGLE_COMMIT_PER_RENDER, FE_NO_COMMIT_IN_COLLECTION_ITEM]}
5620
+ exemplifiedBy:
5621
+ - {path: packages/eslint/fe/button-intent.spec.mjs, editions: [lite, full], codes: [FE_BUTTON_INTENT, FE_BUTTON_INTENT_REQUIRED, FE_BUTTON_NO_VARIANT, FE_DESTRUCTIVE_CONFIRM_IN_DIALOG, FE_SINGLE_COMMIT_PER_RENDER, FE_NO_COMMIT_IN_COLLECTION_ITEM]}
5622
+ witness:
5623
+ - {path: packages/eslint/fe/button-intent.spec.mjs, editions: [lite, full], codes: [FE_BUTTON_INTENT, FE_BUTTON_INTENT_REQUIRED, FE_BUTTON_NO_VARIANT, FE_DESTRUCTIVE_CONFIRM_IN_DIALOG, FE_SINGLE_COMMIT_PER_RENDER, FE_NO_COMMIT_IN_COLLECTION_ITEM]}
5624
+ templateNa: "The intent of an action comes from the product decision in the .starciwork specification; a template cannot choose it."
5625
+ cases:
5626
+ - id: case-1
5627
+ date: "2026-10-25"
5628
+ source: owner
5629
+ quote: "Critique this carefully so the button choice is a system and not a feeling, understand? Update .claude."
5630
+ ruling: "The button variant follows the declared intent (owner ruling button-variant-follows-intent): the app passes intent and step, Grammar derives the variant from them and from the placement it reads from React context. The semantics are HeroUI's, so the lint is universal in the FE canon."