@starci/hfs 4.16.5 → 4.17.2

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 (76) hide show
  1. package/CHANGELOG.md +36 -3
  2. package/README.md +4 -4
  3. package/codemod/loading-states.d.mts +50 -0
  4. package/codemod/loading-states.mjs +384 -0
  5. package/codemod/main.d.mts +1 -1
  6. package/codemod/main.mjs +46 -5
  7. package/codemod/page-navigation.d.mts +50 -0
  8. package/codemod/page-navigation.mjs +278 -0
  9. package/codemod/section-frame.d.mts +50 -0
  10. package/codemod/section-frame.mjs +483 -0
  11. package/codemod/small-renames.d.mts +54 -0
  12. package/codemod/small-renames.mjs +614 -0
  13. package/codemod/surface-card-kinds.d.mts +49 -0
  14. package/codemod/surface-card-kinds.mjs +404 -0
  15. package/codemod/theme-tokens.d.mts +45 -0
  16. package/codemod/theme-tokens.mjs +114 -0
  17. package/codemod/tier-binding.d.mts +50 -0
  18. package/codemod/tier-binding.mjs +214 -0
  19. package/package.json +1 -1
  20. package/runtime/knowledge/grammars/core/playbook.yaml +41 -51
  21. package/runtime/knowledge/grammars/offset-pop/playbook.yaml +5 -5
  22. package/runtime/knowledge/hfs/canon-pins.yaml +7 -7
  23. package/runtime/knowledge/hfs/rules.yaml +255 -30
  24. package/runtime/knowledge/hfs/slots.yaml +12 -5
  25. package/runtime/knowledge/patterns/fe/folder.yaml +1 -1
  26. package/runtime/modules/kernel/failure-codes.yaml +23 -13
  27. package/runtime/scripts/hfs/architecture/clones.mjs +12 -2
  28. package/runtime/scripts/hfs/architecture/component-tiers.d.mts +46 -0
  29. package/runtime/scripts/hfs/architecture/component-tiers.mjs +161 -0
  30. package/runtime/scripts/hfs/architecture/config.mjs +1 -1
  31. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  32. package/runtime/scripts/hfs/architecture/frontend.mjs +2 -2
  33. package/runtime/scripts/hfs/architecture/index.mjs +6 -0
  34. package/runtime/scripts/hfs/architecture/render-graph.d.mts +125 -0
  35. package/runtime/scripts/hfs/architecture/render-graph.mjs +449 -0
  36. package/runtime/scripts/hfs/architecture/render-prop-across-server.d.mts +23 -0
  37. package/runtime/scripts/hfs/architecture/render-prop-across-server.mjs +58 -0
  38. package/runtime/scripts/hfs/architecture/surface-nesting.d.mts +44 -0
  39. package/runtime/scripts/hfs/architecture/surface-nesting.mjs +127 -0
  40. package/runtime/scripts/hfs/architecture/unit-spec-providers.d.mts +2 -1
  41. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +12 -5
  42. package/runtime/scripts/hfs/slot-manifest-shape.mjs +16 -1
  43. package/runtime/scripts/work/draw/draw-dna.mjs +1 -1
  44. package/scaffold/add-fe-layout.mjs +3 -3
  45. package/scaffold/add-fe-overlay.mjs +4 -4
  46. package/scaffold/add-fe-page.mjs +1 -1
  47. package/scaffold/add-fe-unit.d.mts +2 -1
  48. package/scaffold/add-fe-unit.mjs +14 -12
  49. package/scaffold/add.d.mts +1 -1
  50. package/scaffold/add.mjs +1 -1
  51. package/src/main.mjs +4 -4
  52. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/component.tsx +3 -4
  53. package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +8 -5
  54. package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +1 -3
  55. package/templates/fe/skeleton/packages/__project__-ui/src/branches/Providers/index.tsx +3 -3
  56. package/templates/fe/skeleton/packages/__project__-ui/src/composites/LocaleShell/index.tsx +3 -5
  57. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -1
  58. package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +1 -1
  59. package/templates/fe/skeleton-lite/apps/web/src/modules/brand/brand.css +1 -2
  60. package/templates/fe/units/block/component.tsx.tpl +2 -3
  61. package/templates/fe/units/branch/index.tsx.tpl +4 -3
  62. package/templates/fe/units/composite/index.tsx.tpl +3 -3
  63. package/templates/fe/units/frame/index.tsx.tpl +10 -0
  64. package/templates/fe/units/layout/index.tsx.tpl +2 -2
  65. package/templates/fe/units/leaf/index.tsx.tpl +3 -3
  66. package/templates/fe/units/page/component.tsx.tpl +2 -2
  67. package/templates/fe/units/page/index.tsx.tpl +2 -2
  68. package/templates/fe/units/page/stories.tsx.tpl +2 -2
  69. package/templates/fe/units/slot/SlotView.tsx.tpl +11 -10
  70. package/templates/fe/units/slot/data-slot.ts.tpl +20 -8
  71. package/templates/fe/units/slot/slot.shared.ts.tpl +52 -11
  72. package/templates/fe/units/slot/useSlotOfReads.ts.tpl +19 -0
  73. /package/templates/fe/skeleton/packages/__project__-ui/src/{composites → frames}/SiteShell/classNames.ts +0 -0
  74. /package/templates/fe/skeleton/packages/__project__-ui/src/{composites → frames}/SiteShell/index.tsx +0 -0
  75. /package/templates/fe/skeleton-lite/apps/web/src/components/{composites → frames}/SiteShell/classNames.ts +0 -0
  76. /package/templates/fe/skeleton-lite/apps/web/src/components/{composites → frames}/SiteShell/index.tsx +0 -0
@@ -75,6 +75,7 @@ rules:
75
75
  - {kind: machine, id: required-files, at: scripts/hfs/architecture/required-files.mjs}
76
76
  - {kind: machine, id: repository-tree-required, at: scripts/hfs/architecture/hfs.mjs}
77
77
  - {kind: runtime, id: runtime-required, at: scripts/hfs/runtime-check.mjs}
78
+ docs: [contexts/code/be/folder.md]
78
79
  - id: R03
79
80
  code: "HFS_UNTRACKED_ROOT_ENTRY"
80
81
  editions: [lite, full]
@@ -155,6 +156,7 @@ rules:
155
156
  failureCodes: ["HFS_AGENT_DATA_TRACKED"]
156
157
  enforcers:
157
158
  - {kind: work-validate, id: agent-data-tracked, at: scripts/work/validate/check-example-work.mjs}
159
+ docs: [contexts/hfs/README.md#rule-notes]
158
160
  - id: R09
159
161
  code: "HFS_IDENTITY_CUSTODY"
160
162
  law: "An identity points its secret at `secrets/identity-<slug>.enc`; UAT chooses by role."
@@ -164,6 +166,7 @@ rules:
164
166
  enforcers:
165
167
  - {kind: work-validate, id: sealed-custody-location, at: scripts/work/validate/check-work-artifacts.mjs}
166
168
  - {kind: work-validate, id: identity-custody-role, at: scripts/work/validate/check-example-work.mjs}
169
+ docs: [contexts/hfs/README.md#rule-notes]
167
170
  - id: R10
168
171
  code: "HFS_STACKS_SHAPE"
169
172
  editions: [lite, full]
@@ -336,6 +339,7 @@ rules:
336
339
  - {kind: eslint-be, id: no-inline-suppression}
337
340
  - {kind: eslint-fe, id: no-inline-lint-config}
338
341
  - {kind: stylelint, id: no-inline-lint-config}
342
+ docs: [contexts/code/shared/format.md]
339
343
  - id: R19
340
344
  code: "HFS_FORMAT"
341
345
  editions: ["lite", "full"]
@@ -365,6 +369,7 @@ rules:
365
369
  - {kind: eslint-fe, id: file-size-growth}
366
370
  - {kind: sonar, id: cognitive-complexity, at: knowledge/sonar-gate.yaml}
367
371
  - {kind: runtime, id: runtime-size, at: scripts/hfs/runtime-rules/size.mjs}
372
+ docs: [contexts/code/be/function.md]
368
373
  - id: R21
369
374
  code: "HFS_DUPLICATE_CODE"
370
375
  law: "No duplicated blocks or twice-defined helpers."
@@ -378,6 +383,7 @@ rules:
378
383
  - {kind: eslint-fe, id: duplicate-symbol}
379
384
  - {kind: eslint-fe, id: cross-app-duplicate}
380
385
  - {kind: sonar, id: duplicated-lines-density, at: knowledge/sonar-gate.yaml}
386
+ docs: [contexts/code/be/function.md]
381
387
  - id: R22
382
388
  code: "HFS_TS_STRICT"
383
389
  editions: ["lite", "full"]
@@ -474,10 +480,10 @@ rules:
474
480
  witness:
475
481
  - {path: packages/eslint/be/legacy-owner-graph.a16.spec.mjs, editions: [lite, full]}
476
482
  templateNa: "Tier and package direction are constraints on cross-owner import graphs; no dedicated cross-owner graph scaffold is generated."
477
- law: "The tier direction matrix; a shared package never imports an app. Front-end page and layout owners may import overlay entries; every other cross-owner feature-to-feature edge is forbidden, including layout-to-page and overlay-to-overlay. Components never import overlays. The one platform-to-domain edge is the lite entity registry (be.platform.database.entities) importing a capability's `<c>Entities` alone from its `index.ts`."
483
+ law: "The tier direction matrix; a shared package never imports an app. Front-end page and layout owners may import overlay entries; every other cross-owner feature-to-feature edge is forbidden, including layout-to-page and overlay-to-overlay. Components never import overlays. The one platform-to-domain edge is the lite entity registry (be.platform.database.entities) importing a capability's `<c>Entities` alone from its `index.ts`. A component library's tiers (`tiers.grammar`: leaf, branch, composite, frame) import nothing above themselves, and its `data-tier` stamp is its folder."
478
484
  kinds: [lint, design]
479
485
  gates: [settle, land, ci]
480
- failureCodes: ["BE_TIER_DIRECTION", "FE_TIER_DIRECTION", "ARCH_PACKAGE_IMPORTS_APP"]
486
+ failureCodes: ["BE_TIER_DIRECTION", "FE_TIER_DIRECTION", "FE_GRAMMAR_TIER_DIRECTION", "FE_GRAMMAR_TIER_STAMP", "ARCH_PACKAGE_IMPORTS_APP"]
481
487
  enforcers:
482
488
  - {kind: eslint-be, id: tier-direction}
483
489
  - {kind: eslint-fe, id: tier-direction}
@@ -1266,7 +1272,8 @@ rules:
1266
1272
  - {kind: stylelint, id: globals-shape}
1267
1273
  - {kind: stylelint, id: globals-import-order}
1268
1274
  - {kind: stylelint, id: no-token-redefinition}
1269
- - {kind: stylelint, id: brand-layer-shape}
1275
+ - {kind: stylelint, id: no-grammar-private-token}
1276
+ - {kind: stylelint, id: theme-layer-shape}
1270
1277
  - {kind: stylelint, id: status-contrast}
1271
1278
  - {kind: stylelint, id: no-css-module}
1272
1279
  - {kind: stylelint, id: no-class-selector}
@@ -1275,9 +1282,7 @@ rules:
1275
1282
  - {kind: eslint-fe, id: no-raw-brand-value}
1276
1283
  - {kind: eslint-grammar-core, id: no-fractional-step}
1277
1284
  - {kind: eslint-fe, id: no-arbitrary-value}
1278
- - {kind: eslint-grammar-core, id: no-hand-rolled-heading}
1279
1285
  - {kind: eslint-grammar-core, id: no-unresolved-token-class}
1280
- - {kind: eslint-grammar-core, id: no-heading-tag-outside-heading-component}
1281
1286
  - {kind: eslint-grammar-core, id: status-text-uses-soft-foreground}
1282
1287
  proves:
1283
1288
  - {path: packages/stylelint/source-resolves.spec.mjs, editions: [lite, full], codes: [FE_STYLE_SOURCE_UNRESOLVED]}
@@ -1859,7 +1864,7 @@ rules:
1859
1864
  editions: ["lite","full"]
1860
1865
  docs: ["docs/frontend-source-authoring.md#r91-source-form"]
1861
1866
  proves: [{"path":"packages/eslint/fe/r91-source-form.spec.mjs","editions":["lite","full"]}]
1862
- exemplifiedBy: [{"path":"packages/hfs/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/index.tsx","edition":"lite"},{"path":"packages/hfs/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx","edition":"full"},{"path":"packages/eslint/fe/r91-source-form.spec.mjs","editions":["lite","full"]}]
1867
+ exemplifiedBy: [{"path":"packages/hfs/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/index.tsx","edition":"lite"},{"path":"packages/hfs/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx","edition":"full"},{"path":"packages/eslint/fe/r91-source-form.spec.mjs","editions":["lite","full"]}]
1863
1868
  witness: [{"path":"packages/eslint/fe/r91-source-form.spec.mjs","edition":"lite","codes":["FE_SOURCE_FORM","FE_SOURCE_LAYOUT_INVALID"]},{"path":"packages/eslint/fe/r91-source-form.spec.mjs","edition":"full","codes":["FE_SOURCE_FORM","FE_SOURCE_LAYOUT_INVALID"]}]
1864
1869
  gates: ["pre-commit","settle","land","ci"]
1865
1870
  - id: R92
@@ -1875,6 +1880,7 @@ rules:
1875
1880
  - {kind: eslint-fe, id: no-public-classname-prop}
1876
1881
  - {kind: eslint-fe, id: no-public-frame-css-props}
1877
1882
  - {kind: eslint-fe, id: no-css-door-type-laundering}
1883
+ - {kind: eslint-fe, id: tier-children}
1878
1884
  - {kind: eslint-fe, id: base-props-atom}
1879
1885
  - {kind: eslint-fe, id: base-import-pair}
1880
1886
  - {kind: eslint-fe, id: no-data-status-shape}
@@ -1915,10 +1921,8 @@ rules:
1915
1921
  enforcers:
1916
1922
  - {kind: eslint-grammar-core, id: no-vendor-icon-outside-icon-leaf}
1917
1923
  - {kind: eslint-grammar-core, id: heroicons-is-the-glyph-vendor}
1918
- - {kind: eslint-grammar-core, id: no-off-scale-glyph-size}
1919
1924
  - {kind: eslint-grammar-core, id: no-icon-size-class}
1920
1925
  - {kind: eslint-grammar-core, id: destructive-menu-entry-needs-danger-tone}
1921
- - {kind: eslint-grammar-core, id: no-decorative-icon-in-metric-cell}
1922
1926
  - {kind: eslint-grammar-core, id: vendor-primitive-has-named-owner}
1923
1927
  - {kind: eslint-fe, id: grammar-entry}
1924
1928
  - {kind: machine, id: grammar, at: scripts/hfs/architecture/frontend-grammar.mjs}
@@ -3809,6 +3813,7 @@ rules:
3809
3813
  failureCodes: ["RIGHTS_ROLE_DENIED", "RIGHTS_GIT_PUSH", "RIGHTS_GIT_TAG", "RIGHTS_GIT_COMMIT", "RIGHTS_GIT_SYNC", "RIGHTS_NPM_PUBLISH", "RIGHTS_NPM_CI_UNLOCKED", "RIGHTS_SUITE_RUN", "RIGHTS_RELEASE_CUT", "RIGHTS_RAW_TOOL"]
3810
3814
  enforcers:
3811
3815
  - {kind: runtime, id: role-rights, at: scripts/hfs/runtime-rules/rights-policy.mjs}
3816
+ docs: [contexts/hfs/README.md#rule-notes]
3812
3817
  - id: R224
3813
3818
  code: "RIGHTS_PROTECTED_ZONE"
3814
3819
  law: "The protected zone declared once in modules/kernel/protected-zone.yaml is edited only by the owner path; a supervisor self-upgrade files an owner proposal, and an op never writes the .claude runtime checkout."
@@ -3818,6 +3823,7 @@ rules:
3818
3823
  failureCodes: ["RIGHTS_PROTECTED_ZONE", "RIGHTS_OP_RUNTIME_WRITE"]
3819
3824
  enforcers:
3820
3825
  - {kind: runtime, id: protected-zone, at: scripts/hfs/runtime-rules/rights-policy.mjs}
3826
+ docs: [contexts/hfs/README.md#rule-notes]
3821
3827
  - id: R225
3822
3828
  code: "RT_HOOK_SHAPE"
3823
3829
  law: "The app hook templates keep the gate model: pre-commit is L0 only (no typecheck or test run); pre-push invokes the bundled scripts/guards/release-push-gate.mjs policy for fast-forward main, a clean outgoing secret scan and annotated v* tags, then the harness's `starci deps check` (every committed @starci pin installable from npm, no local package copy ahead of npm). It runs no build, test, typecheck or lint."
@@ -3827,6 +3833,7 @@ rules:
3827
3833
  failureCodes: ["RT_HOOK_SHAPE", "RIGHTS_PUSH_NOT_RELEASE"]
3828
3834
  enforcers:
3829
3835
  - {kind: runtime, id: hook-shape, at: scripts/hfs/runtime-rules/hook-shape.mjs}
3836
+ docs: [contexts/hfs/README.md#rule-notes]
3830
3837
  - id: R227
3831
3838
  code: "RT_SYNTAX_INVALID"
3832
3839
  editions: [lite, full]
@@ -5468,7 +5475,6 @@ rules:
5468
5475
  gates: [pre-commit, settle, land, ci]
5469
5476
  failureCodes: ["FE_NESTED_VARIANT", "FE_LIST_PANEL_PADDING", "FE_GRAMMAR_LAYOUT_ONLY", "FE_DIVIDER_FULL_BLEED"]
5470
5477
  enforcers:
5471
- - {kind: eslint-fe, id: nested-control-variant}
5472
5478
  - {kind: eslint-fe, id: no-list-panel-padding}
5473
5479
  - {kind: eslint-fe, id: no-app-surface-paint}
5474
5480
  - {kind: eslint-fe, id: divider-full-bleed}
@@ -5479,15 +5485,15 @@ rules:
5479
5485
  templateNa: "Product composition is proved by the FE canon's Grammar import and surface fixtures; a template cannot choose product slots or lists."
5480
5486
  proves:
5481
5487
  - {path: packages/eslint/fe/divider-full-bleed.spec.mjs, editions: [lite, full], codes: [FE_DIVIDER_FULL_BLEED]}
5482
- - {path: packages/eslint/fe/surface-rules.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_LIST_PANEL_PADDING, FE_GRAMMAR_LAYOUT_ONLY]}
5488
+ - {path: packages/eslint/fe/surface-rules.spec.mjs, editions: [lite, full], codes: [FE_LIST_PANEL_PADDING, FE_GRAMMAR_LAYOUT_ONLY]}
5483
5489
  - {path: packages/eslint/fe/ui-rulings.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_GRAMMAR_LAYOUT_ONLY]}
5484
5490
  exemplifiedBy:
5485
5491
  - {path: packages/eslint/fe/divider-full-bleed.spec.mjs, editions: [lite, full], codes: [FE_DIVIDER_FULL_BLEED]}
5486
- - {path: packages/eslint/fe/surface-rules.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_LIST_PANEL_PADDING, FE_GRAMMAR_LAYOUT_ONLY]}
5492
+ - {path: packages/eslint/fe/surface-rules.spec.mjs, editions: [lite, full], codes: [FE_LIST_PANEL_PADDING, FE_GRAMMAR_LAYOUT_ONLY]}
5487
5493
  - {path: packages/eslint/fe/ui-rulings.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_GRAMMAR_LAYOUT_ONLY]}
5488
5494
  witness:
5489
5495
  - {path: packages/eslint/fe/divider-full-bleed.spec.mjs, editions: [lite, full], codes: [FE_DIVIDER_FULL_BLEED]}
5490
- - {path: packages/eslint/fe/surface-rules.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_LIST_PANEL_PADDING, FE_GRAMMAR_LAYOUT_ONLY]}
5496
+ - {path: packages/eslint/fe/surface-rules.spec.mjs, editions: [lite, full], codes: [FE_LIST_PANEL_PADDING, FE_GRAMMAR_LAYOUT_ONLY]}
5491
5497
  - {path: packages/eslint/fe/ui-rulings.spec.mjs, editions: [lite, full], codes: [FE_NESTED_VARIANT, FE_GRAMMAR_LAYOUT_ONLY]}
5492
5498
  cases:
5493
5499
  - id: case-1
@@ -5501,10 +5507,9 @@ rules:
5501
5507
  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."
5502
5508
  kinds: [codemod, lint, design]
5503
5509
  gates: [pre-commit, settle, land, ci]
5504
- 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"]
5510
+ failureCodes: ["FE_BUTTON_INTENT", "FE_BUTTON_INTENT_REQUIRED", "FE_DESTRUCTIVE_CONFIRM_IN_DIALOG", "FE_SINGLE_COMMIT_PER_RENDER", "FE_NO_COMMIT_IN_COLLECTION_ITEM"]
5505
5511
  enforcers:
5506
5512
  - {kind: eslint-fe, id: button-intent-required}
5507
- - {kind: eslint-fe, id: button-no-variant}
5508
5513
  - {kind: eslint-fe, id: destructive-confirm-in-dialog}
5509
5514
  - {kind: eslint-fe, id: single-commit-per-render}
5510
5515
  - {kind: eslint-fe, id: no-commit-in-collection-item}
@@ -5513,11 +5518,11 @@ rules:
5513
5518
  - knowledge/ui/composition/cta.yaml
5514
5519
  - docs/code-pattern-enforcement.md#r319-button-intent
5515
5520
  proves:
5516
- - {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]}
5521
+ - {path: packages/eslint/fe/button-intent.spec.mjs, editions: [lite, full], codes: [FE_BUTTON_INTENT, FE_BUTTON_INTENT_REQUIRED, FE_DESTRUCTIVE_CONFIRM_IN_DIALOG, FE_SINGLE_COMMIT_PER_RENDER, FE_NO_COMMIT_IN_COLLECTION_ITEM]}
5517
5522
  exemplifiedBy:
5518
- - {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]}
5523
+ - {path: packages/eslint/fe/button-intent.spec.mjs, editions: [lite, full], codes: [FE_BUTTON_INTENT, FE_BUTTON_INTENT_REQUIRED, FE_DESTRUCTIVE_CONFIRM_IN_DIALOG, FE_SINGLE_COMMIT_PER_RENDER, FE_NO_COMMIT_IN_COLLECTION_ITEM]}
5519
5524
  witness:
5520
- - {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]}
5525
+ - {path: packages/eslint/fe/button-intent.spec.mjs, editions: [lite, full], codes: [FE_BUTTON_INTENT, FE_BUTTON_INTENT_REQUIRED, FE_DESTRUCTIVE_CONFIRM_IN_DIALOG, FE_SINGLE_COMMIT_PER_RENDER, FE_NO_COMMIT_IN_COLLECTION_ITEM]}
5521
5526
  templateNa: "The intent of an action comes from the product decision in the .starciwork specification; a template cannot choose it."
5522
5527
  cases:
5523
5528
  - id: case-1
@@ -5576,7 +5581,7 @@ rules:
5576
5581
  ruling: "The label of a status chip starts with an uppercase letter (owner ruling chip-label-uppercase); StatusDot capitalises a text label it renders, and the lint refuses a lowercase literal label at its source."
5577
5582
  - id: R323
5578
5583
  code: "FE_PAGE_HEADER_HAND_ROLLED"
5579
- law: "A page is headed by Grammar's PageHeader; a Breadcrumbs trail followed by a SectionHeader in one parent is a page head composed by hand, with no band, no top inset and no separator."
5584
+ law: "A page is headed by the header of Grammar's Page; PageHeader and Breadcrumbs are Page's parts and are not exported, so importing either from the Grammar composes a page head by hand, with no band, no top inset and no separator."
5580
5585
  kinds: [lint]
5581
5586
  gates: [pre-commit, settle, land, ci]
5582
5587
  failureCodes: ["FE_PAGE_HEADER_HAND_ROLLED"]
@@ -5596,10 +5601,10 @@ rules:
5596
5601
  date: "2026-10-28"
5597
5602
  source: owner
5598
5603
  quote: "Publish Grammar 0.15.3 with PageHeader now, then move all 18 pages to it before deploying. (translated from Vietnamese)"
5599
- ruling: "A page is headed by Grammar's PageHeader (owner ruling page-header-band): a surface band whose content shares the page content's edges and which ends in its own full-width separator, page tabs standing on it; the lint refuses a page head composed by hand from Breadcrumbs and SectionHeader."
5604
+ ruling: "A page is headed by Grammar's Page header (owner ruling page-header-band): a surface band whose content shares the page content's edges and which ends in its own full-width separator, page tabs standing on it; the lint refuses a PageHeader or Breadcrumbs imported by hand, since both are Page's own parts."
5600
5605
  - id: R324
5601
5606
  code: "FE_PAGE_TABS_OUTSIDE_HEADER"
5602
- law: "Page tabs are the header's tabs; a Tabs that is the first element of a Page body or the next element after a PageHeader is a page tab bar drawn outside the band."
5607
+ law: "Page tabs are the header's tabs; a Tabs that is the first element of a Page body is a page tab bar drawn outside the band."
5603
5608
  kinds: [lint]
5604
5609
  gates: [pre-commit, settle, land, ci]
5605
5610
  failureCodes: ["FE_PAGE_TABS_OUTSIDE_HEADER"]
@@ -5622,7 +5627,7 @@ rules:
5622
5627
  ruling: "A route page is one Grammar Page (owner ruling page-frame); page tabs are its header's tabs, and the lint refuses a Tabs at the top of the Page body or right after a PageHeader."
5623
5628
  - id: R325
5624
5629
  code: "FE_ROUTE_PAGE_OUTSIDE_PAGE"
5625
- law: "A page owner draws one Grammar Page with nothing beside it; a sibling of the Page, or a bare PageHeader or PageContainer in the page owner, re-decides the inset and rhythm Page owns."
5630
+ law: "A page owner draws one Grammar Page with nothing beside it; a sibling of the Page re-decides the inset and rhythm Page owns, and PageHeader and PageContainer are Page's parts, not exports to import from the Grammar."
5626
5631
  kinds: [lint]
5627
5632
  gates: [pre-commit, settle, land, ci]
5628
5633
  failureCodes: ["FE_ROUTE_PAGE_OUTSIDE_PAGE"]
@@ -5958,7 +5963,7 @@ rules:
5958
5963
  ruling: "Mascot takes AssetSlotProps (assetSlot, assetSha256, assetPrompt) and emits them as data-asset-* on its root; the lint requires assetSlot on every Mascot."
5959
5964
  - id: R340
5960
5965
  code: "FE_RAISED_SURFACE_IN_SURFACE"
5961
- law: "A Grammar surface that accepts a depth prop (SurfaceListCard) does not pass depth=\"top\" when a SurfaceCard, SurfaceListCard or SurfaceAccordionCard of the same file encloses it: a nested surface is subordinate (outlined, BOUNDARY-5), never raised again (BOUNDARY-6, DNA-COMMON-NO-RAISED-SURFACE-IN-SURFACE); the Grammar resolves the depth from the surface context. A depth computed at run time is not judged."
5966
+ law: "A surface never stands inside another surface (owner decision, dialogs and drawers included): a SurfaceCard that a SurfaceCard, a Pane surface, or a Dialog, Drawer, AlertDialog or Popover of the same file encloses is refused. The Grammar has one depth, raised."
5962
5967
  kinds: [lint]
5963
5968
  gates: [pre-commit, settle, land, ci]
5964
5969
  failureCodes: ["FE_RAISED_SURFACE_IN_SURFACE"]
@@ -5977,8 +5982,8 @@ rules:
5977
5982
  - id: case-1
5978
5983
  date: "2026-10-31"
5979
5984
  source: owner
5980
- quote: "A list inside another surface is outlined (BOUNDARY-5, DNA-COMMON-NO-RAISED-SURFACE-IN-SURFACE); SurfaceListCard takes its depth from the surface context (grammar 0.16.2)."
5981
- ruling: "SurfaceListCard resolves its depth from the surface context like SurfaceCard and SurfaceAccordionCard; an app that forces depth=\"top\" on a surface nested in another surface raises a surface inside a surface, so the lint refuses it."
5985
+ quote: "A list inside another surface is outlined (BOUNDARY-5, DNA-COMMON-NO-RAISED-SURFACE-IN-SURFACE); the list SurfaceCard takes its depth from the surface context (grammar 0.16.2)."
5986
+ ruling: "A list SurfaceCard resolves its depth from the surface context like every SurfaceCard kind; an app that forces depth=\"top\" on a surface nested in another surface raises a surface inside a surface, so the lint refuses it."
5982
5987
  - id: R341
5983
5988
  code: "FE_FREESTYLE_REASON_MISSING"
5984
5989
  law: "A Grammar Freestyle region, the one sanctioned place where an app writes its own plain elements and CSS because no Grammar component fits, names its reason: a non-empty string literal saying why. The rules that judge hand-written elements, classes, styles and generated art leave the raw elements of a Freestyle subtree alone; a Grammar component inside it is judged as before. An element with a spread attribute is not judged."
@@ -6028,6 +6033,7 @@ rules:
6028
6033
  - id: R343
6029
6034
  code: "HFS_CHECK_CACHE_FORGED"
6030
6035
  editions: ["lite", "full"]
6036
+ docs: [contexts/hfs/README.md#rule-notes]
6031
6037
  law: "An app's committed check cache (.starcistacks/cache/) is trusted in CI only as it stands at the trusted base; a record whose key matches the tree and whose findings differ from the inherited or computed result is HFS_CHECK_CACHE_FORGED (a hard blocker at any count), a touched key without a current record is HFS_CHECK_CACHE_STALE and a missing cache is HFS_CHECK_CACHE_MISSING (both report-only warnings)."
6032
6038
  kinds: [codemod]
6033
6039
  gates: [ci]
@@ -6140,7 +6146,7 @@ rules:
6140
6146
  ruling: "One padding system for everything placed in a PaneGrid: the Pane publishes the inset and the PaneSection applies it, so no band adds its own."
6141
6147
  - id: R348
6142
6148
  code: "FE_PANE_GRID_FRAME"
6143
- law: "A Grammar PaneGrid is the one child of `Page chrome=\"panes\"` or stands inside a Pane; a PaneGrid drawn in a FillViewport, a SurfaceCard, SurfaceListCard or SurfaceAccordionCard, a Page of another chrome, or wrapped inside `Page chrome=\"panes\"` re-decides the frame and the bottom edge that Page owns. A grid whose frame lives in another file is judged where the frame is, and a Freestyle subtree is not judged."
6149
+ law: "A Grammar PaneGrid is the one child of `Page chrome=\"panes\"` or stands inside a Pane; a PaneGrid drawn in a SurfaceCard of any kind, a Page of another chrome, or wrapped inside `Page chrome=\"panes\"` re-decides the frame and the bottom edge that Page owns. A grid whose frame lives in another file is judged where the frame is, and a Freestyle subtree is not judged."
6144
6150
  kinds: [lint]
6145
6151
  gates: [pre-commit, settle, land, ci]
6146
6152
  failureCodes: ["FE_PANE_GRID_FRAME"]
@@ -6210,7 +6216,7 @@ rules:
6210
6216
  ruling: "One spacing scale with no 16; every container holds its content 12 in, and an app names relations through role props, never spacing utilities."
6211
6217
  - id: R351
6212
6218
  code: "FE_TABLE_IN_CARD"
6213
- law: "A DataTable is not placed inside a SurfaceCard, on the page or in a pane: plain rows with a few facts are a SurfaceListCard with the label outside and the note in its footer, and a table that belongs to a card is an owner decision named in this rule. The rule reads the JSX of the file: a DataTable enclosed by a SurfaceCard element, or rendered by a component of the same file that a SurfaceCard encloses."
6219
+ law: "A DataTable is not placed inside a SurfaceCard, on the page or in a pane: plain rows with a few facts are a SurfaceCard kind=list with the label outside and the note in its footer, and a table that belongs to a card is an owner decision named in this rule. The rule reads the JSX of the file: a DataTable enclosed by a SurfaceCard element, or rendered by a component of the same file that a SurfaceCard encloses."
6214
6220
  kinds: [lint]
6215
6221
  gates: [pre-commit, settle, land, ci]
6216
6222
  failureCodes: ["FE_TABLE_IN_CARD"]
@@ -6230,10 +6236,10 @@ rules:
6230
6236
  date: "2026-11-01"
6231
6237
  source: owner
6232
6238
  quote: "quá phức tạp mới nhét bảng vào card"
6233
- ruling: "Plain rows are a SurfaceListCard with a bottom note in its footer; a DataTable is not put in a SurfaceCard."
6239
+ ruling: "Plain rows are a SurfaceCard kind=\"list\" with a bottom note in its footer; a DataTable is not put in a SurfaceCard."
6234
6240
  - id: R352
6235
6241
  code: "FE_COMMAND_FILLS_HEADER"
6236
- law: "A page or pane command hugs its label and sits at the end of its header row: a Grammar Button with width=\"fill\" in the actions of a PageHeader or the labelEnd of a SurfaceCard or SurfaceListCard is refused; width=\"fill\" is for a form submit, an auth panel and a phone action bar, which a file cannot show and the rule does not judge."
6242
+ law: "A page or pane command hugs its label and sits at the end of its header row: a Grammar Button with width=\"fill\" in the actions of a Page header or the labelEnd of a SurfaceCard is refused; width=\"fill\" is for a form submit, an auth panel and a phone action bar, which a file cannot show and the rule does not judge."
6237
6243
  kinds: [lint]
6238
6244
  gates: [pre-commit, settle, land, ci]
6239
6245
  failureCodes: ["FE_COMMAND_FILLS_HEADER"]
@@ -6279,7 +6285,7 @@ rules:
6279
6285
  ruling: "A filter over a collection is a FilterButtonGroup of sm toggle buttons; the TagGroup has no selection."
6280
6286
  - id: R354
6281
6287
  code: "FE_HEADING_ABOVE_CARD"
6282
- law: "A card has a label by design, so a hand heading above it is wrong: a SectionHeader directly followed, in the same parent, by one SurfaceCard, SurfaceListCard or SurfaceAccordionCard, and a card whose ariaLabel is the header title, are refused; the title moves to the label, the count to fact, the action to labelEnd and the note to footer. Several cards under one header stay legal."
6288
+ law: "A card has a label by design, so a heading over it is wrong: a Section (not kind=\"list\") whose only child is one SurfaceCard (of any kind), and a card whose ariaLabel is the Section title, are refused; the title moves to the label, the count to fact, the action to labelEnd and the note to footer. Several cards under one Section stay legal."
6283
6289
  kinds: [lint]
6284
6290
  gates: [pre-commit, settle, land, ci]
6285
6291
  failureCodes: ["FE_HEADING_ABOVE_CARD"]
@@ -6299,4 +6305,223 @@ rules:
6299
6305
  date: "2026-11-01"
6300
6306
  source: owner
6301
6307
  quote: "bản thân card đã có label, vốn là mục đích thiết kế"
6302
- ruling: "A card carries its own title, count, action and note; a SectionHeader above a single card is refused."
6308
+ ruling: "A card carries its own title, count, action and note; a Section around a single card is refused."
6309
+ - id: R355
6310
+ code: "FE_SURFACE_CARD_KIND"
6311
+ law: "A SurfaceCard is one component whose kind the compiler checks, and what a kind draws is not drawn by hand around it: a card of the body kind holds no raw ul, ol, li or dl, no Divider, and no Heading standing as a second title, and a Section whose only child is one SurfaceCard frames one subject twice."
6312
+ kinds: [lint]
6313
+ gates: [pre-commit, settle, land, ci]
6314
+ failureCodes: ["FE_SURFACE_CARD_KIND"]
6315
+ enforcers:
6316
+ - {kind: eslint-grammar-core, id: surface-card-kind}
6317
+ editions: [lite, full]
6318
+ proves:
6319
+ - packages/eslint-grammar-core/src/surface-card.spec.ts
6320
+ exemplifiedBy:
6321
+ - {path: packages/eslint-grammar-core/src/surface-card.spec.ts, editions: [lite, full], codes: [FE_SURFACE_CARD_KIND]}
6322
+ witness:
6323
+ - {path: packages/eslint-grammar-core/src/surface-card.spec.ts, editions: [lite, full], codes: [FE_SURFACE_CARD_KIND]}
6324
+ - id: R356
6325
+ code: "FE_SURFACE_NESTING"
6326
+ law: "Across files, a card is not drawn inside a card, a Pane surface or an overlay, a DataTable is not drawn inside a card, a PaneGrid stands only in Page chrome=\"panes\" or a Pane, a Section or Heading holds more than one card, and a labelled card is not repeated in the peer-card layout Grid kind=\"cards\": the render graph replaces every component by what it draws and every children by what its caller handed it."
6327
+ kinds: [lint]
6328
+ gates: [pre-commit, settle, land, ci]
6329
+ failureCodes: ["FE_SURFACE_NESTING"]
6330
+ enforcers:
6331
+ - {kind: machine, id: surface-nesting, at: scripts/hfs/architecture/surface-nesting.mjs}
6332
+ editions: [lite, full]
6333
+ docs: [contexts/ui/components/card.md]
6334
+ templateNa: "A template draws no card, table or grid."
6335
+ proves:
6336
+ - {path: packages/hfs/src/surface-nesting.spec.mts, editions: [lite, full], codes: [FE_SURFACE_NESTING]}
6337
+ exemplifiedBy:
6338
+ - {path: packages/hfs/src/surface-nesting.spec.mts, editions: [lite, full], codes: [FE_SURFACE_NESTING]}
6339
+ witness:
6340
+ - {path: packages/hfs/src/surface-nesting.spec.mts, editions: [lite, full], codes: [FE_SURFACE_NESTING]}
6341
+ cases:
6342
+ - id: case-1
6343
+ date: "2026-11-01"
6344
+ source: owner
6345
+ quote: "Surface in surface is banned totally, dialogs and drawers included."
6346
+ ruling: "The one-file card laws (R340, R348, R351, R354, R366) are closed across files by the render graph; the machine reports the inner element and the chain that led to it."
6347
+ - id: R357
6348
+ code: "FE_TEXT_BLOCK"
6349
+ law: "Text holds a run of words: a block component or block element inside it is refused."
6350
+ kinds: [lint]
6351
+ gates: [pre-commit, settle, land, ci]
6352
+ failureCodes: ["FE_TEXT_BLOCK"]
6353
+ enforcers:
6354
+ - {kind: eslint-grammar-core, id: text-inline-only}
6355
+ editions: [lite, full]
6356
+ docs: [contexts/ui/components/text.md]
6357
+ templateNa: "A template writes no Text with a block inside."
6358
+ proves:
6359
+ - {path: packages/eslint-grammar-core/src/text-rules.spec.ts, editions: [lite, full], codes: [FE_TEXT_BLOCK]}
6360
+ exemplifiedBy:
6361
+ - {path: packages/eslint-grammar-core/src/text-rules.spec.ts, editions: [lite, full], codes: [FE_TEXT_BLOCK]}
6362
+ witness:
6363
+ - {path: packages/eslint-grammar-core/src/text-rules.spec.ts, editions: [lite, full], codes: [FE_TEXT_BLOCK]}
6364
+ cases:
6365
+ - id: case-1
6366
+ date: "2026-11-01"
6367
+ source: owner
6368
+ quote: "A card carries its own label; text is inline."
6369
+ ruling: "Text is inline only, so a layout cannot hide inside a paragraph."
6370
+ - id: R358
6371
+ code: "FE_TEXT_HEADING"
6372
+ law: "A strong Text that is the first child of a group and is followed only by blocks is a hand heading: the group takes a Section title, a card label or Heading."
6373
+ kinds: [lint]
6374
+ gates: [pre-commit, settle, land, ci]
6375
+ failureCodes: ["FE_TEXT_HEADING"]
6376
+ enforcers:
6377
+ - {kind: eslint-grammar-core, id: no-text-heading}
6378
+ editions: [lite, full]
6379
+ docs: [contexts/ui/components/text.md]
6380
+ templateNa: "A template writes no heading by hand."
6381
+ proves:
6382
+ - {path: packages/eslint-grammar-core/src/text-rules.spec.ts, editions: [lite, full], codes: [FE_TEXT_HEADING]}
6383
+ exemplifiedBy:
6384
+ - {path: packages/eslint-grammar-core/src/text-rules.spec.ts, editions: [lite, full], codes: [FE_TEXT_HEADING]}
6385
+ witness:
6386
+ - {path: packages/eslint-grammar-core/src/text-rules.spec.ts, editions: [lite, full], codes: [FE_TEXT_HEADING]}
6387
+ cases:
6388
+ - id: case-1
6389
+ date: "2026-11-01"
6390
+ source: owner
6391
+ quote: "No free heading in apps."
6392
+ ruling: "A heading is a Section title, a card label or Heading, never a strong Text."
6393
+ - id: R359
6394
+ code: "FE_ONE_HIGHLIGHT"
6395
+ law: "A render states one highlighted thing: a second isHighlight on a Grammar element in one component render is refused."
6396
+ kinds: [lint]
6397
+ gates: [pre-commit, settle, land, ci]
6398
+ failureCodes: ["FE_ONE_HIGHLIGHT"]
6399
+ enforcers:
6400
+ - {kind: eslint-grammar-core, id: one-highlight-per-render}
6401
+ editions: [lite, full]
6402
+ docs: [contexts/ui/components/card.md]
6403
+ templateNa: "A template highlights nothing."
6404
+ proves:
6405
+ - {path: packages/eslint-grammar-core/src/text-rules.spec.ts, editions: [lite, full], codes: [FE_ONE_HIGHLIGHT]}
6406
+ exemplifiedBy:
6407
+ - {path: packages/eslint-grammar-core/src/text-rules.spec.ts, editions: [lite, full], codes: [FE_ONE_HIGHLIGHT]}
6408
+ witness:
6409
+ - {path: packages/eslint-grammar-core/src/text-rules.spec.ts, editions: [lite, full], codes: [FE_ONE_HIGHLIGHT]}
6410
+ cases:
6411
+ - id: case-1
6412
+ date: "2026-11-01"
6413
+ source: owner
6414
+ quote: "One accent fill per render."
6415
+ ruling: "When two things are highlighted neither is."
6416
+ - id: R361
6417
+ code: "FE_RENDER_PROP_ACROSS_SERVER"
6418
+ law: "A function is not handed from a server file to a client component: React serialises props across that line and a function is not serialisable; pass data, or move the callback into the client branch that owns the interaction."
6419
+ kinds: [lint]
6420
+ gates: [pre-commit, settle, land, ci]
6421
+ failureCodes: ["FE_RENDER_PROP_ACROSS_SERVER"]
6422
+ enforcers:
6423
+ - {kind: machine, id: render-prop-across-server, at: scripts/hfs/architecture/render-prop-across-server.mjs}
6424
+ editions: [lite, full]
6425
+ docs: [contexts/code/fe/function.md]
6426
+ templateNa: "A template hands no function across the server line."
6427
+ proves:
6428
+ - {path: packages/hfs/src/surface-nesting.spec.mts, editions: [lite, full], codes: [FE_RENDER_PROP_ACROSS_SERVER]}
6429
+ exemplifiedBy:
6430
+ - {path: packages/hfs/src/surface-nesting.spec.mts, editions: [lite, full], codes: [FE_RENDER_PROP_ACROSS_SERVER]}
6431
+ witness:
6432
+ - {path: packages/hfs/src/surface-nesting.spec.mts, editions: [lite, full], codes: [FE_RENDER_PROP_ACROSS_SERVER]}
6433
+ cases:
6434
+ - id: case-1
6435
+ date: "2026-11-01"
6436
+ source: owner
6437
+ quote: "a block takes data"
6438
+ ruling: "A render callback is for a client branch; a block takes data and renders it itself."
6439
+ - id: R362
6440
+ code: "FE_GRAMMAR_TIER_STATE"
6441
+ law: "In the Grammar package a leaf and a composite hold no useState, useReducer or effect, and a component's own props and its ReactNode slots stay inside its tier's budget (leaf 8, branch 12, composite 12, frame 10 props; 3 slots). Both run as counting rules: their findings are the tier refactor list."
6442
+ kinds: [lint]
6443
+ gates: [pre-commit, settle, land, ci]
6444
+ failureCodes: ["FE_GRAMMAR_TIER_STATE", "FE_GRAMMAR_PROP_BUDGET"]
6445
+ enforcers:
6446
+ - {kind: runtime, id: tier-state, at: packages/grammar/scripts/lint/eslint-plugin.mts}
6447
+ - {kind: runtime, id: prop-budget, at: packages/grammar/scripts/lint/eslint-plugin.mts}
6448
+ editions: [lite, full]
6449
+ docs: [contexts/grammars/README.md#grammar-lint]
6450
+ templateNa: "A template ships no Grammar source."
6451
+ proves:
6452
+ - {path: packages/grammar/scripts/lint/eslint-plugin.mts, editions: [lite, full], codes: [FE_GRAMMAR_TIER_STATE, FE_GRAMMAR_PROP_BUDGET]}
6453
+ exemplifiedBy:
6454
+ - {path: packages/grammar/scripts/lint/eslint-plugin.mts, editions: [lite, full], codes: [FE_GRAMMAR_TIER_STATE, FE_GRAMMAR_PROP_BUDGET]}
6455
+ witness:
6456
+ - {path: packages/grammar/scripts/lint/eslint-plugin.mts, editions: [lite, full], codes: [FE_GRAMMAR_TIER_STATE, FE_GRAMMAR_PROP_BUDGET]}
6457
+ cases:
6458
+ - id: case-1
6459
+ date: "2026-11-02"
6460
+ source: owner
6461
+ quote: "The tier binding of the Grammar is a ladder; a leaf draws and a branch behaves."
6462
+ ruling: "State sits in branches; budgets keep a role from growing into two."
6463
+ - id: R363
6464
+ code: "FE_GRAMMAR_LITERAL"
6465
+ law: "A Grammar component holds no hardcoded colour, length, utility class or numeric style value (`no-literal-look`); its stylesheets read named roles and ramps."
6466
+ kinds: [lint]
6467
+ gates: [pre-commit, settle, land, ci]
6468
+ failureCodes: ["FE_GRAMMAR_LITERAL"]
6469
+ enforcers:
6470
+ - {kind: runtime, id: no-literal-look, at: packages/grammar/scripts/lint/eslint-plugin.mts}
6471
+ editions: [lite, full]
6472
+ docs: [contexts/grammars/README.md#grammar-lint]
6473
+ templateNa: "A template ships no Grammar source."
6474
+ proves:
6475
+ - {path: packages/grammar/scripts/lint/eslint-plugin.mts, editions: [lite, full], codes: [FE_GRAMMAR_LITERAL]}
6476
+ exemplifiedBy:
6477
+ - {path: packages/grammar/scripts/lint/eslint-plugin.mts, editions: [lite, full], codes: [FE_GRAMMAR_LITERAL]}
6478
+ witness:
6479
+ - {path: packages/grammar/scripts/lint/eslint-plugin.mts, editions: [lite, full], codes: [FE_GRAMMAR_LITERAL]}
6480
+ cases:
6481
+ - id: case-1
6482
+ date: "2026-11-02"
6483
+ source: owner
6484
+ quote: "A component names a role and the stylesheet gives it a value."
6485
+ ruling: "Literals live in the ramps."
6486
+ - id: R366
6487
+ code: "FE_LABEL_IN_GRID"
6488
+ law: "A SurfaceCard with a label repeated in a Grid kind=cards is a tile drawn with the wrong anatomy: a peer you recognise and open names itself inside (SurfaceCard kind=tile), and peers compared by a fact are one kind=list card."
6489
+ kinds: [lint]
6490
+ gates: [pre-commit, settle, land, ci]
6491
+ failureCodes: ["FE_LABEL_IN_GRID"]
6492
+ enforcers:
6493
+ - {kind: eslint-grammar-core, id: no-label-in-grid}
6494
+ editions: [lite, full]
6495
+ proves:
6496
+ - packages/eslint-grammar-core/src/surface-card.spec.ts
6497
+ exemplifiedBy:
6498
+ - {path: packages/eslint-grammar-core/src/surface-card.spec.ts, editions: [lite, full], codes: [FE_LABEL_IN_GRID]}
6499
+ witness:
6500
+ - {path: packages/eslint-grammar-core/src/surface-card.spec.ts, editions: [lite, full], codes: [FE_LABEL_IN_GRID]}
6501
+ - id: R367
6502
+ code: "FE_SWR_DEFAULT_ARRAY"
6503
+ law: "A loading state is drawn from the page's own reads: a hook folds a read to [] only while it also returns that read's isLoading, a slot takes its loading and error from the reads it draws and never from another slot, a background refresh (isValidating) never drives a loading flag or a status, a placeholder is not the ready data with its lists emptied, and an app draws no skeleton by hand (`no-hand-skeleton`): the Grammar components draw their loading structure from `isLoading`."
6504
+ kinds: [lint]
6505
+ gates: [pre-commit, settle, land, ci]
6506
+ failureCodes: ["FE_SWR_DEFAULT_ARRAY", "FE_FOREIGN_SLOT_STATUS", "FE_VALIDATING_AS_LOADING", "FE_PLACEHOLDER_OBJECT", "FE_HAND_SKELETON"]
6507
+ enforcers:
6508
+ - {kind: eslint-fe, id: no-swr-default-array}
6509
+ - {kind: eslint-fe, id: no-foreign-slot-status}
6510
+ - {kind: eslint-fe, id: no-validating-as-loading}
6511
+ - {kind: eslint-fe, id: no-placeholder-object}
6512
+ - {kind: eslint-fe, id: no-hand-skeleton}
6513
+ editions: [lite, full]
6514
+ docs: [contexts/ui/loading.md]
6515
+ templateNa: "A template draws no loading state of its own."
6516
+ proves:
6517
+ - {path: packages/eslint-canon-fe/src/skeleton-states.spec.ts, editions: [lite, full], codes: [FE_SWR_DEFAULT_ARRAY, FE_FOREIGN_SLOT_STATUS, FE_VALIDATING_AS_LOADING, FE_PLACEHOLDER_OBJECT, FE_HAND_SKELETON]}
6518
+ exemplifiedBy:
6519
+ - {path: packages/eslint-canon-fe/src/skeleton-states.spec.ts, editions: [lite, full], codes: [FE_SWR_DEFAULT_ARRAY, FE_FOREIGN_SLOT_STATUS, FE_VALIDATING_AS_LOADING, FE_PLACEHOLDER_OBJECT, FE_HAND_SKELETON]}
6520
+ witness:
6521
+ - {path: packages/eslint-canon-fe/src/skeleton-states.spec.ts, editions: [lite, full], codes: [FE_SWR_DEFAULT_ARRAY, FE_FOREIGN_SLOT_STATUS, FE_VALIDATING_AS_LOADING, FE_PLACEHOLDER_OBJECT, FE_HAND_SKELETON]}
6522
+ cases:
6523
+ - id: case-1
6524
+ date: "2026-11-01"
6525
+ source: owner
6526
+ quote: "The skeleton is the ready structure with its data missing; it is drawn from isLoading alone and from the reads the region shows."
6527
+ ruling: "Several reads feed one frame: loading is any read loading with no data; a key change reloads only the region keyed by it; a refresh keeps the content."
@@ -203,13 +203,20 @@ tiers:
203
203
  feature: {mayImport: [feature, components, hooks, foundation, modules, package], mayImportKinds: {feature: {from: [pages, layouts], to: [overlays]}}} # pages and layouts compose overlays and blocks; data goes through hooks/swr
204
204
  # A component never reaches data: config, routes, i18n and types are foundation; every other module and the api
205
205
  # transport are reached through a hook (owner decision 2026-09-29).
206
- components: {mayImport: [components, hooks, foundation, package], lowerLayerOnly: true} # blocks, composites, branches, leaves
206
+ components: {mayImport: [components, hooks, foundation, package], lowerLayerOnly: true} # blocks, frames, composites, branches, leaves
207
207
  # Across domains a hook is imported only through the other domain's public index (hooks/<domain>/index.ts), never deep.
208
208
  hooks: {mayImport: [hooks, foundation, modules, transport, package], mayImportSlots: {transport: {from: [fe.hooks.swr]}}} # only hooks/swr imports an operation
209
209
  foundation: {mayImport: [foundation, package], acyclic: true} # modules/{config,i18n,routes,types}
210
210
  modules: {mayImport: [foundation, modules, transport, package], acyclic: true}
211
211
  transport: {mayImport: [foundation, modules, package]} # modules/api: the one fetch
212
212
  package: {mayImport: [package]}
213
+ # A component library (packages/grammar): src/core/<tier>/<Name>/ and the data-tier stamp equal the tier folder. A tier imports nothing
214
+ # above itself (FE_GRAMMAR_TIER_DIRECTION); a shared untiered module under src/core is followed, so one hop of indirection hides nothing.
215
+ grammar:
216
+ leaf: {mayImport: [leaf]} # one element or one vendor part
217
+ branch: {mayImport: [leaf, branch]} # a leaf plus its own interaction state
218
+ composite: {mayImport: [leaf, branch, composite]} # a fixed arrangement of lower tiers
219
+ frame: {mayImport: [leaf, branch, composite, frame]} # owns a region; the one tier that takes children
213
220
  crossOwner: every import across owners targets the owner's public entry (index.ts or index.tsx); never export *.
214
221
  crossApp: apps never import each other; shared code is a packages/<pkg> slot.
215
222
 
@@ -2270,12 +2277,12 @@ slots:
2270
2277
  - id: fe.components
2271
2278
  since: 3.1.0
2272
2279
  profiles: [fe]
2273
- path: "apps/<app>/src/components/{blocks,composites,branches,leaves}/<name>/"
2280
+ path: "apps/<app>/src/components/{blocks,frames,composites,branches,leaves}/<name>/"
2274
2281
  presence: optional
2275
2282
  tracked: tracked
2276
2283
  tier: components
2277
2284
  owner: true
2278
- layers: [blocks, composites, branches, leaves] # a layer imports only the layers after it
2285
+ layers: [blocks, frames, composites, branches, leaves] # a layer imports only the layers after it
2279
2286
  requires: [index.tsx]
2280
2287
  allows: [index.tsx, component.tsx, classNames.ts]
2281
2288
  roles: {entry: index.tsx, drawing: component.tsx, styles: classNames.ts} # the connected entry, the pure drawing half and the styling declarations of a component owner
@@ -2618,12 +2625,12 @@ slots:
2618
2625
  tracked: tracked
2619
2626
  tier: package
2620
2627
  owner: true
2621
- kinds: [composites, branches, leaves] # the layer folders under src/ (a package owns no blocks)
2628
+ kinds: [frames, composites, branches, leaves] # the layer folders under src/ (a package owns no blocks)
2622
2629
  roles: {entry: index.tsx, drawing: component.tsx, styles: classNames.ts}
2623
2630
  requires: [package.json, src/index.ts, tsconfig.json]
2624
2631
  tests: none
2625
2632
  budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
2626
- why: built to dist with explicit named exports; only composites, branches and leaves; no unused export; no colour literal
2633
+ why: built to dist with explicit named exports; only frames, composites, branches and leaves; no unused export; no colour literal
2627
2634
  rules: [FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT, FE_STYLE_TOKEN_ONLY]
2628
2635
  - id: fe.package.api
2629
2636
  since: 3.1.0
@@ -364,7 +364,7 @@ rules:
364
364
  cases:
365
365
  - id: case-1
366
366
  when: "A Grammar component (runtime package)"
367
- write: "packages/grammar/src/core/<primitive|composite|branch|composition>/<Name>/index.tsx"
367
+ write: "packages/grammar/src/core/<leaf|branch|composite|frame>/<Name>/index.tsx"
368
368
  - id: case-2
369
369
  when: "A Grammar component's class strings and spec (runtime package)"
370
370
  write: "sibling classNames.ts and index.spec.tsx"