@starci/eslint-grammar-core 0.16.0 → 0.16.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.2 - 2026-10-31
4
+
5
+ - Changed: the peer moves to eslint-canon-fe 9.6.2; the bundled canon pins name @starci/grammar 0.16.2 (the horizontal Stepper connector line; SurfaceListCard takes its depth from the surface context).
6
+ - Added: starci-grammar/no-raised-surface-in-surface (R340, FE_RAISED_SURFACE_IN_SURFACE): a depth="top" forced on a SurfaceListCard that a SurfaceCard, SurfaceListCard or SurfaceAccordionCard of the same file encloses is a finding (BOUNDARY-6, DNA-COMMON-NO-RAISED-SURFACE-IN-SURFACE); the Grammar resolves a nested surface's depth from the surface context. Specs: src/surface-depth.spec.ts.
7
+
8
+ ## 0.16.1 - 2026-10-30
9
+
10
+ - Changed: the peer moves to eslint-canon-fe 9.6.1; the bundled canon pins name @starci/grammar 0.16.1 (accentCorner, Backdrop, the shell filling the viewport).
11
+
3
12
  ## 0.16.0 - 2026-10-30
4
13
 
5
14
  - Changed: the peer moves to eslint-canon-fe 9.6.0; the bundled runtime copies are now generated from the hfs build (TypeScript emitted as .mjs) per runtime/bundle.json, with a drift check in CI; the copies the package keeps as its own are listed there and stay hand-maintained; the bundled canon pins name hfs 4.12.0, eslint-canon-be 3.9.0, eslint-canon-fe 9.6.0, the Grammar family 0.16.0, jest-preset 2.5.3 and test-world 1.3.3. No rule of this package changed.
package/README.md CHANGED
@@ -19,7 +19,7 @@ Family helpers and the generated runtime closure belong to this package.
19
19
 
20
20
  The rule IDs remain stable when their namespace moves. Laws cover the fixed app shell and navbar,
21
21
  Grammar-owned structural elements, glyphs and icon sizes, destructive menu entries, status inks,
22
- chip labels that start uppercase, generated art rendered through `Mascot` or `MediaFrame` with every `Mascot` naming its asset slot, the page head composed by `PageHeader`, the route page as one `Page` with its tabs in the header, heading and token scales, vendor ownership and slot-status composition. Each rule's metadata cites
22
+ chip labels that start uppercase, generated art rendered through `Mascot` or `MediaFrame` with every `Mascot` naming its asset slot, the page head composed by `PageHeader`, the route page as one `Page` with its tabs in the header, heading and token scales, vendor ownership and slot-status composition, and a surface nested in another surface left subordinate (no forced `depth="top"`). Each rule's metadata cites
23
23
  its behavioral knowledge source and the governing ownership test. The R318 surface composition
24
24
  rules (nested control variants, panel-list alignment, layout-only CSS) stay in the universal canon.
25
25
 
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { rules as menuEntriesRules } from "./menu-entries.js";
10
10
  import { rules as pageRules } from "./page.js";
11
11
  import { rules as pageHeaderRules } from "./page-header.js";
12
12
  import { rules as statusColorsRules } from "./status-colors.js";
13
+ import { rules as surfaceDepthRules } from "./surface-depth.js";
13
14
  import { rules as tokensRules } from "./tokens.js";
14
15
  import { rules as typographyRules } from "./typography.js";
15
16
  import { rules as vendorRules } from "./vendor-boundary.js";
@@ -25,6 +26,7 @@ const CONTRIBUTIONS = [
25
26
  { law: "page", rules: pageRules },
26
27
  { law: "page-header", rules: pageHeaderRules },
27
28
  { law: "status-colors", rules: statusColorsRules },
29
+ { law: "surface-depth", rules: surfaceDepthRules },
28
30
  { law: "tokens", rules: tokensRules },
29
31
  { law: "typography", rules: typographyRules },
30
32
  { law: "vendor-boundary", rules: vendorRules },
package/dist/lib/why.js CHANGED
@@ -54,6 +54,11 @@ export const why = {
54
54
  en: "The page or layout owner `<file>` draws something beside its Page, a Page inside another (or inside the Page its tabbed segment's layout draws), a bare PageHeader or PageContainer, or no Page at all (itself, through its pure half, or through the one connected block it renders as its root).",
55
55
  fix: "Draw one Page: the band as its header, everything else in its body, the measure as its measure. When client data builds the header, render as the root the one connected block that draws the Page and pass the body as its children. Under a segment layout whose Page carries header.tabs, a page draws its body only; that layout's Page has nothing beside it, its head in its header. A page mounted only under the (auth) route group is framed by that group's layout.",
56
56
  },
57
+ "no-raised-surface-in-surface": {
58
+ code: "FE_RAISED_SURFACE_IN_SURFACE",
59
+ en: "A Grammar surface in `<file>` forces `depth=\"top\"` inside another surface, raising a surface inside a surface (BOUNDARY-6).",
60
+ fix: "Drop `depth`: a nested surface is subordinate (outlined, BOUNDARY-5) and the Grammar resolves it from the surface context.",
61
+ },
57
62
  "no-shell-bottom-tabs": {
58
63
  code: "FE_APP_SHELL_BOTTOM_TABS",
59
64
  en: "Bottom tab navigation replaces the required mobile sidebar drawer.",
@@ -0,0 +1,7 @@
1
+ import type { Rule } from "eslint";
2
+ /** A surface inside another surface never forces `depth="top"`: it is subordinate, and the Grammar resolves it. */
3
+ export declare const noRaisedSurfaceInSurface: Rule.RuleModule;
4
+ /** The rules this law contributes to the plugin. */
5
+ export declare const rules: Record<string, Rule.RuleModule>;
6
+ /** Every rule of this law is an error. */
7
+ export declare const recommended: Record<string, "error">;
@@ -0,0 +1,92 @@
1
+ import { isProductSource } from "@starci/eslint-canon-fe/lib/scope.mjs";
2
+ import { defineFamilyRules, defineRule } from "./lib/rule-contract.js";
3
+ /**
4
+ * The surface depth law (HFS R340 `FE_RAISED_SURFACE_IN_SURFACE`; Grammar BOUNDARY-5 and BOUNDARY-6, DNA
5
+ * `DNA-COMMON-NO-RAISED-SURFACE-IN-SURFACE`): a surface inside another surface is subordinate (outlined, BOUNDARY-5), never
6
+ * raised (BOUNDARY-6) again.
7
+ *
8
+ * Grammar's bounded surfaces read their depth from the surface context (`useSurfaceNesting`): `SurfaceCard`,
9
+ * `SurfaceAccordionCard` and `SurfaceListCard` draw outlined inside another surface and raised only at the top.
10
+ * `SurfaceListCard` alone also takes a `depth` prop that overrides the context, so `depth="top"` on a list that sits inside
11
+ * another surface raises a surface inside a surface.
12
+ *
13
+ * WHAT IT READS. A `depth` attribute whose value is the string `"top"` (a literal, or an expression container holding one)
14
+ * on a depth-accepting surface element (`SurfaceListCard` or `<ns>.SurfaceListCard`), when a `SurfaceCard`,
15
+ * `SurfaceListCard` or `SurfaceAccordionCard` element of the same file encloses it. A depth computed at run time is not
16
+ * judged, and neither is a surface nobody encloses in this file (its parent may nest it; Grammar resolves that).
17
+ *
18
+ * WHERE IT APPLIES. Product source (`isProductSource`).
19
+ */
20
+ /** The bounded surfaces that open a surface context for what they enclose. */
21
+ const SURFACES = new Set(["SurfaceCard", "SurfaceListCard", "SurfaceAccordionCard"]);
22
+ /** The surfaces that accept a `depth` prop overriding the one read from context. */
23
+ const DEPTH_PROP = new Set(["SurfaceListCard"]);
24
+ /** The element name of a JSX name: `SurfaceCard`, or `G.SurfaceCard` as `SurfaceCard`. */
25
+ const nameOf = (name) => {
26
+ if (name.type === "JSXIdentifier")
27
+ return name.name;
28
+ if (name.type === "JSXMemberExpression")
29
+ return name.property.name;
30
+ return null;
31
+ };
32
+ /** True when a bounded surface element encloses the element the opening tag belongs to. */
33
+ const insideSurface = (opening) => {
34
+ // The opening tag's parent is the element itself, so the walk starts above that; the Program's parent is null at run time
35
+ // though the tree's types say a node always has one.
36
+ for (let current = opening.parent?.parent; current !== undefined && current !== null; current = current.parent) {
37
+ if (current.type !== "JSXElement")
38
+ continue;
39
+ const name = nameOf(current.openingElement.name);
40
+ if (name !== null && SURFACES.has(name))
41
+ return true;
42
+ }
43
+ return false;
44
+ };
45
+ /** True when an attribute value is statically the string `"top"`. */
46
+ const isTop = (value) => {
47
+ if (value === null)
48
+ return false;
49
+ if (value.type === "Literal")
50
+ return value.value === "top";
51
+ if (value.type !== "JSXExpressionContainer")
52
+ return false;
53
+ const expression = value.expression;
54
+ if (expression.type === "Literal")
55
+ return expression.value === "top";
56
+ return expression.type === "TemplateLiteral" && expression.expressions.length === 0 && expression.quasis.length === 1 && expression.quasis[0]?.value.cooked === "top";
57
+ };
58
+ /** A surface inside another surface never forces `depth="top"`: it is subordinate, and the Grammar resolves it. */
59
+ export const noRaisedSurfaceInSurface = defineRule({
60
+ meta: {
61
+ type: "problem",
62
+ docs: { description: "R340: a Grammar surface nested in another surface does not force depth=\"top\" (BOUNDARY-6); a nested surface is subordinate (BOUNDARY-5)." },
63
+ schema: [],
64
+ messages: {
65
+ raised: "`<{{element}} depth=\"top\">` is nested inside another Grammar surface, so it raises a surface inside a surface (BOUNDARY-6, DNA-COMMON-NO-RAISED-SURFACE-IN-SURFACE): a nested surface is subordinate (outlined, BOUNDARY-5). Drop `depth` and let the Grammar resolve it from the surface context.",
66
+ },
67
+ },
68
+ create(context) {
69
+ if (!isProductSource(context))
70
+ return {};
71
+ return {
72
+ JSXAttribute(attribute) {
73
+ if (attribute.name.type !== "JSXIdentifier" || attribute.name.name !== "depth" || !isTop(attribute.value))
74
+ return;
75
+ const opening = attribute.parent;
76
+ if (opening.type !== "JSXOpeningElement")
77
+ return;
78
+ const element = nameOf(opening.name);
79
+ if (element === null || !DEPTH_PROP.has(element) || !insideSurface(opening))
80
+ return;
81
+ context.report({ node: attribute, messageId: "raised", data: { element } });
82
+ },
83
+ };
84
+ },
85
+ });
86
+ /** The rules this law contributes to the plugin. */
87
+ export const rules = {
88
+ "no-raised-surface-in-surface": noRaisedSurfaceInSurface,
89
+ };
90
+ defineFamilyRules(rules, "knowledge/hfs/rules.yaml#R340");
91
+ /** Every rule of this law is an error. */
92
+ export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-grammar/${name}`, "error"]));
@@ -0,0 +1,40 @@
1
+ # A nested surface stays subordinate
2
+
3
+ Law module: `surface-depth.ts`. Catalogue: R340 FE_RAISED_SURFACE_IN_SURFACE.
4
+
5
+ Grammar's bounded surfaces (`SurfaceCard`, `SurfaceAccordionCard`, `SurfaceListCard`) read their depth from the surface
6
+ context: raised at the top of a page (BOUNDARY-6), outlined inside another surface (BOUNDARY-5), so two raised surfaces never
7
+ stack (`DNA-COMMON-NO-RAISED-SURFACE-IN-SURFACE`). `SurfaceListCard` also takes a `depth` prop that overrides the context;
8
+ forcing `depth="top"` on a list that sits inside another surface raises a surface inside a surface.
9
+
10
+ Every rule below is an error in `coreGrammarConfig`; none can be switched off or suppressed inline. It applies to product
11
+ source (the route, feature, component, hook, foundation, module and transport tiers and the workspace packages); a script or
12
+ a file no slot owns is not judged.
13
+
14
+ ## `starci-grammar/no-raised-surface-in-surface`
15
+
16
+ A `depth` attribute whose value is statically the string `"top"` (a literal, an expression container holding one, or a
17
+ template literal without expressions) on a depth-accepting surface element (today `SurfaceListCard`, or `<ns>.SurfaceListCard`)
18
+ is a finding when a `SurfaceCard`, `SurfaceListCard` or `SurfaceAccordionCard` element of the same file encloses it. A depth
19
+ computed at run time, `depth="nested"`, and a surface no element of the file encloses are not judged. The fix is to drop
20
+ `depth` and let the Grammar resolve it from the surface context.
21
+
22
+ **Invalid** (`src/components/blocks/Runs/component.tsx`)
23
+
24
+ ```tsx
25
+ <SurfaceCard label="Run">
26
+ <SurfaceListCard label="Steps" depth="top" items={steps} />
27
+ </SurfaceCard>
28
+ ```
29
+
30
+ **Valid** (`src/components/blocks/Runs/component.tsx`)
31
+
32
+ ```tsx
33
+ <SurfaceCard label="Run">
34
+ <SurfaceListCard label="Steps" items={steps} />
35
+ </SurfaceCard>
36
+ ```
37
+
38
+ **Finding code:** `FE_RAISED_SURFACE_IN_SURFACE`
39
+
40
+ Rule ownership: [FE-RULE-OWNERSHIP](../../../contexts/code/fe/README.md#the-placement-test-fe-rule-ownership).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-grammar-core",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "description": "ESLint rules for the core Grammar's component, token and composition contracts.",
5
5
  "keywords": [
6
6
  "eslint",
@@ -41,7 +41,7 @@
41
41
  "node": ">=22"
42
42
  },
43
43
  "peerDependencies": {
44
- "@starci/eslint-canon-fe": "9.6.0",
44
+ "@starci/eslint-canon-fe": "9.6.2",
45
45
  "eslint": ">=9",
46
46
  "typescript": ">=5.9"
47
47
  },
@@ -31,35 +31,35 @@ pins:
31
31
  source: packages/event-stream/package.json
32
32
  why: 'the one server-sent-events client of every front end (fe-stack event-stream, owner:event-stream-package): the transport client layer (modules/api/sse/clients) creates one instance for the app with the bearer token source and the stream operations follow on it; it carries Last-Event-ID resume, the capped doubling backoff, the heartbeat timeout, the single 401 renewal and the useSWRSubscription adapter, so no app keeps a hand-written reader.'
33
33
  '@starci/grammar':
34
- version: 0.16.0
34
+ version: 0.16.2
35
35
  group: starci
36
36
  install: registry
37
37
  side: fe
38
38
  source: packages/grammar/package.json
39
39
  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.'
40
40
  '@starci/eslint-canon-be':
41
- version: 3.9.0
41
+ version: 3.9.2
42
42
  group: starci
43
43
  install: registry
44
44
  side: be
45
45
  source: packages/eslint-canon-be/package.json
46
46
  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.'
47
47
  '@starci/eslint-canon-fe':
48
- version: 9.6.0
48
+ version: 9.6.2
49
49
  group: starci
50
50
  install: registry
51
51
  side: fe
52
52
  source: packages/eslint-canon-fe/package.json
53
53
  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.'
54
54
  '@starci/eslint-grammar-core':
55
- version: 0.16.0
55
+ version: 0.16.2
56
56
  group: starci
57
57
  install: registry
58
58
  side: fe
59
59
  source: packages/eslint-grammar-core/package.json
60
60
  why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
61
61
  '@starci/eslint-grammar-offset-pop':
62
- version: 0.16.0
62
+ version: 0.16.2
63
63
  group: starci
64
64
  install: registry
65
65
  side: fe
@@ -106,7 +106,7 @@ pins:
106
106
  source: packages/ai-router/package.json
107
107
  why: 'the AI pool router of a back end: a NestJS module that routes OpenAI-compatible chat, stream and embeddings calls across keys, models and providers, with @starci/ai-router/testing for its specs; @starci/jest-preset compiles it as an ESM-only dependency.'
108
108
  '@starci/hfs':
109
- version: 4.12.0
109
+ version: 4.12.2
110
110
  group: starci
111
111
  install: registry
112
112
  side: both
@@ -5956,3 +5956,26 @@ rules:
5956
5956
  source: owner
5957
5957
  quote: "A Mascot reuses the provenance props of Image and MediaFrame; a Mascot without an assetSlot is a finding (relayed by the nivo-lite app)."
5958
5958
  ruling: "Mascot takes AssetSlotProps (assetSlot, assetSha256, assetPrompt) and emits them as data-asset-* on its root; the lint requires assetSlot on every Mascot."
5959
+ - id: R340
5960
+ 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."
5962
+ kinds: [lint]
5963
+ gates: [pre-commit, settle, land, ci]
5964
+ failureCodes: ["FE_RAISED_SURFACE_IN_SURFACE"]
5965
+ enforcers:
5966
+ - {kind: eslint-grammar-core, id: no-raised-surface-in-surface}
5967
+ editions: [lite, full]
5968
+ docs: [knowledge/patterns/fe/ui.yaml#FE-UI]
5969
+ templateNa: "A template forces no surface depth; the app composes its surfaces."
5970
+ proves:
5971
+ - packages/eslint-grammar-core/src/surface-depth.spec.ts
5972
+ exemplifiedBy:
5973
+ - {path: packages/eslint-grammar-core/src/surface-depth.spec.ts, editions: [lite, full], codes: [FE_RAISED_SURFACE_IN_SURFACE]}
5974
+ witness:
5975
+ - {path: packages/eslint-grammar-core/src/surface-depth.spec.ts, editions: [lite, full], codes: [FE_RAISED_SURFACE_IN_SURFACE]}
5976
+ cases:
5977
+ - id: case-1
5978
+ date: "2026-10-31"
5979
+ 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."
@@ -24,7 +24,7 @@ rules:
24
24
  - id: FE-UI
25
25
  title: Compose the selected Grammar
26
26
  kind: mandatory
27
- hfsRules: [R93, R244, R313, R314, R315, R316, R317, R318, R319, R320, R338, R339]
27
+ hfsRules: [R93, R244, R313, R314, R315, R316, R317, R318, R319, R320, R338, R339, R340]
28
28
  requirement: |-
29
29
  Components compose the selected Grammar through its published entry and props; application CSS carries
30
30
  layout only (css.yaml). A missing visual capability is added to that Grammar before the app consumes it.
@@ -92,5 +92,6 @@ rules:
92
92
  - FE_CONTROL_HEIGHT
93
93
  - FE_GENERATED_ART_OUTSIDE_FRAME
94
94
  - FE_MASCOT_ASSET_SLOT_MISSING
95
+ - FE_RAISED_SURFACE_IN_SURFACE
95
96
  manual: []
96
97
  relatedExamples: [full-app-shell, lite-app-shell]