@starci/eslint-grammar-core 0.15.34 → 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.
Files changed (63) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/index.js +4 -0
  4. package/dist/lib/why.js +15 -0
  5. package/dist/mascot.d.ts +28 -0
  6. package/dist/mascot.js +166 -0
  7. package/dist/surface-depth.d.ts +7 -0
  8. package/dist/surface-depth.js +92 -0
  9. package/docs/mascot.md +60 -0
  10. package/docs/surface-depth.md +40 -0
  11. package/package.json +2 -2
  12. package/runtime/bundle.json +74 -0
  13. package/runtime/engine/by-code-unit.mjs +6 -6
  14. package/runtime/engine/model-config.mjs +98 -76
  15. package/runtime/engine/orca-config.mjs +22 -17
  16. package/runtime/engine/release-config.mjs +22 -18
  17. package/runtime/engine/removed-vocabulary.mjs +21 -20
  18. package/runtime/engine/resources-config.mjs +42 -35
  19. package/runtime/engine/runtime-root.mjs +11 -13
  20. package/runtime/engine/secrets.mjs +170 -102
  21. package/runtime/engine/sonar-config.mjs +16 -13
  22. package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
  23. package/runtime/knowledge/hfs/rules.yaml +95 -6
  24. package/runtime/knowledge/hfs/slots.yaml +2 -2
  25. package/runtime/knowledge/patterns/fe/ui.yaml +13 -1
  26. package/runtime/scripts/hfs/allows.mjs +37 -38
  27. package/runtime/scripts/hfs/architecture/config.mjs +302 -268
  28. package/runtime/scripts/hfs/architecture/side-boundary.mjs +2 -5
  29. package/runtime/scripts/hfs/architecture/type-context.mjs +255 -250
  30. package/runtime/scripts/hfs/architecture/typescript.mjs +358 -318
  31. package/runtime/scripts/hfs/declaration-shape.mjs +140 -103
  32. package/runtime/scripts/hfs/declaration-slots.mjs +64 -61
  33. package/runtime/scripts/hfs/edition-slots.mjs +43 -40
  34. package/runtime/scripts/hfs/param-names.mjs +6 -11
  35. package/runtime/scripts/hfs/rule-catalog.mjs +353 -257
  36. package/runtime/scripts/hfs/rule-params-shape.mjs +60 -47
  37. package/runtime/scripts/hfs/slot-app-view.mjs +69 -74
  38. package/runtime/scripts/hfs/slot-classify.mjs +42 -38
  39. package/runtime/scripts/hfs/slot-errors.mjs +8 -7
  40. package/runtime/scripts/hfs/slot-imports.mjs +70 -63
  41. package/runtime/scripts/hfs/slot-manifest-shape.mjs +74 -52
  42. package/runtime/scripts/hfs/slot-match.mjs +122 -111
  43. package/runtime/scripts/hfs/slot-path.mjs +0 -2
  44. package/runtime/scripts/hfs/slot-required.mjs +65 -59
  45. package/runtime/scripts/hfs/slot-semantic-problems.mjs +138 -112
  46. package/runtime/scripts/hfs/slot-side-problems.mjs +23 -19
  47. package/runtime/scripts/hfs/slots.mjs +175 -152
  48. package/runtime/scripts/hfs/typescript-programs.mjs +66 -69
  49. package/runtime/scripts/hfs/view.mjs +38 -40
  50. package/runtime/scripts/lib/glob.mjs +23 -14
  51. package/runtime/scripts/lib/grammar-lint.mjs +33 -37
  52. package/runtime/scripts/lib/i18n.mjs +32 -35
  53. package/runtime/scripts/lib/json-clone.mjs +45 -36
  54. package/runtime/scripts/lib/json.mjs +26 -14
  55. package/runtime/scripts/lib/list.mjs +9 -14
  56. package/runtime/scripts/lib/path-key.mjs +53 -38
  57. package/runtime/scripts/lib/read-text.mjs +30 -17
  58. package/runtime/scripts/lib/regex.mjs +1 -2
  59. package/runtime/scripts/lib/source-phrases.mjs +13 -13
  60. package/runtime/scripts/lib/ts-analysis.mjs +28 -26
  61. package/runtime/scripts/lib/ts-ast.mjs +36 -42
  62. package/runtime/scripts/lib/walk.mjs +33 -27
  63. package/runtime/scripts/lib/yaml-cached.mjs +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
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
+
12
+ ## 0.16.0 - 2026-10-30
13
+
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.
15
+ - Added: starci-grammar/generated-art-in-mascot-or-media-frame (R338, FE_GENERATED_ART_OUTSIDE_FRAME): a static /images/**.webp or .png src outside Mascot or MediaFrame is a finding; starci-grammar/mascot-asset-slot-required (R339, FE_MASCOT_ASSET_SLOT_MISSING): a Mascot names its assetSlot. Specs: src/mascot.spec.ts.
16
+
3
17
  ## 0.15.34 - 2026-10-30
4
18
 
5
19
  - Changed: the peer moves to eslint-canon-fe 9.5.23; the bundled canon pins name hfs 4.11.22 (app check and lint pass under 20 findings), eslint-canon-be 3.8.21, eslint-canon-fe 9.5.23 and the Grammar family 0.15.34. 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, 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
@@ -5,10 +5,12 @@ import { rules as chipLabelRules } from "./chip-label.js";
5
5
  import { rules as drawingRules } from "./drawing.js";
6
6
  import { rules as grammarBoundaryRules } from "./grammar-boundary.js";
7
7
  import { rules as iconRules } from "./icon.js";
8
+ import { rules as mascotRules } from "./mascot.js";
8
9
  import { rules as menuEntriesRules } from "./menu-entries.js";
9
10
  import { rules as pageRules } from "./page.js";
10
11
  import { rules as pageHeaderRules } from "./page-header.js";
11
12
  import { rules as statusColorsRules } from "./status-colors.js";
13
+ import { rules as surfaceDepthRules } from "./surface-depth.js";
12
14
  import { rules as tokensRules } from "./tokens.js";
13
15
  import { rules as typographyRules } from "./typography.js";
14
16
  import { rules as vendorRules } from "./vendor-boundary.js";
@@ -19,10 +21,12 @@ const CONTRIBUTIONS = [
19
21
  { law: "drawing", rules: drawingRules },
20
22
  { law: "grammar-boundary", rules: grammarBoundaryRules },
21
23
  { law: "icon", rules: iconRules },
24
+ { law: "mascot", rules: mascotRules },
22
25
  { law: "menu-entries", rules: menuEntriesRules },
23
26
  { law: "page", rules: pageRules },
24
27
  { law: "page-header", rules: pageHeaderRules },
25
28
  { law: "status-colors", rules: statusColorsRules },
29
+ { law: "surface-depth", rules: surfaceDepthRules },
26
30
  { law: "tokens", rules: tokensRules },
27
31
  { law: "typography", rules: typographyRules },
28
32
  { law: "vendor-boundary", rules: vendorRules },
package/dist/lib/why.js CHANGED
@@ -9,6 +9,16 @@ export const why = {
9
9
  en: "A StatusDot or Badge label in `<file>` starts with a lowercase letter.",
10
10
  fix: "Start the label with an uppercase letter (`Running`, not `running`); the rule fixes a literal label in place.",
11
11
  },
12
+ "generated-art-in-mascot-or-media-frame": {
13
+ code: "FE_GENERATED_ART_OUTSIDE_FRAME",
14
+ en: "Generated art (a `.webp` or `.png` under `/images/`) in `<file>` is drawn outside Grammar's `Mascot` and `MediaFrame`, so it takes whatever box the page gives it.",
15
+ fix: "Render a character with `<Mascot src size assetSlot />` (sm, md, lg or hero), or frame a scene or illustration in `<MediaFrame assetSlot>`.",
16
+ },
17
+ "mascot-asset-slot-required": {
18
+ code: "FE_MASCOT_ASSET_SLOT_MISSING",
19
+ en: "A `Mascot` in `<file>` names no `assetSlot`, so nothing ties the art it draws to the asset workflow's request.",
20
+ fix: "Pass `assetSlot` (the slot id of the request) and `assetSha256` once the file is delivered.",
21
+ },
12
22
  "navbar-cluster": {
13
23
  code: "FE_NAVBAR_CLUSTER",
14
24
  en: "Navbar action component kinds or order differ from the app-shell canon.",
@@ -44,6 +54,11 @@ export const why = {
44
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).",
45
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.",
46
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
+ },
47
62
  "no-shell-bottom-tabs": {
48
63
  code: "FE_APP_SHELL_BOTTOM_TABS",
49
64
  en: "Bottom tab navigation replaces the required mobile sidebar drawer.",
@@ -0,0 +1,28 @@
1
+ import type { Rule } from "eslint";
2
+ /**
3
+ * The mascot law (HFS R338 `FE_GENERATED_ART_OUTSIDE_FRAME`, R339 `FE_MASCOT_ASSET_SLOT_MISSING`): generated art renders
4
+ * at the Grammar's size scale and stays tied to the asset workflow's request.
5
+ *
6
+ * Generated art is a raster the asset workflow delivers under the public `/images/` tree, as `.webp` or `.png`. Drawn
7
+ * through a bare `Image` or `<img>` it takes whatever box its page gives it, so one mascot comes out at a different size on
8
+ * every screen and nothing ties the bytes to the slot that requested them. Grammar's `Mascot` owns the character scale
9
+ * (sm, md, lg, hero), and `MediaFrame` frames a scene or illustration; those two are its renderers.
10
+ *
11
+ * WHAT IT READS. A JSX `src` attribute whose value is statically a string (a literal, an expression container holding one,
12
+ * a template literal without expressions, or a `const` of the file initialised with one). A source computed at run time is
13
+ * not judged: the rule never guesses. `assetSlot` is read on every `Mascot` element (`Mascot` or `<ns>.Mascot`); a spread
14
+ * attribute may carry it, so an element with a spread is not judged.
15
+ *
16
+ * WHERE IT APPLIES. Product source (`isProductSource`: the route, feature, component, hook, foundation, module and transport
17
+ * tiers and the workspace packages). A script or a file no slot owns is not judged.
18
+ */
19
+ /** A generated raster under the public `/images/` tree: `.webp` or `.png`, any case, an optional query or fragment. */
20
+ export declare const GENERATED_ART: RegExp;
21
+ /** Generated art under `/images/` renders only through `Mascot` or inside a `MediaFrame`. */
22
+ export declare const generatedArtInMascotOrMediaFrame: Rule.RuleModule;
23
+ /** Every `Mascot` names the asset slot its art was requested under. */
24
+ export declare const mascotAssetSlotRequired: Rule.RuleModule;
25
+ /** The rules this law contributes to the plugin. */
26
+ export declare const rules: Record<string, Rule.RuleModule>;
27
+ /** Every rule of this law is an error. */
28
+ export declare const recommended: Record<string, "error">;
package/dist/mascot.js ADDED
@@ -0,0 +1,166 @@
1
+ import { isProductSource } from "@starci/eslint-canon-fe/lib/scope.mjs";
2
+ import { defineFamilyRules, defineRule } from "./lib/rule-contract.js";
3
+ /**
4
+ * The mascot law (HFS R338 `FE_GENERATED_ART_OUTSIDE_FRAME`, R339 `FE_MASCOT_ASSET_SLOT_MISSING`): generated art renders
5
+ * at the Grammar's size scale and stays tied to the asset workflow's request.
6
+ *
7
+ * Generated art is a raster the asset workflow delivers under the public `/images/` tree, as `.webp` or `.png`. Drawn
8
+ * through a bare `Image` or `<img>` it takes whatever box its page gives it, so one mascot comes out at a different size on
9
+ * every screen and nothing ties the bytes to the slot that requested them. Grammar's `Mascot` owns the character scale
10
+ * (sm, md, lg, hero), and `MediaFrame` frames a scene or illustration; those two are its renderers.
11
+ *
12
+ * WHAT IT READS. A JSX `src` attribute whose value is statically a string (a literal, an expression container holding one,
13
+ * a template literal without expressions, or a `const` of the file initialised with one). A source computed at run time is
14
+ * not judged: the rule never guesses. `assetSlot` is read on every `Mascot` element (`Mascot` or `<ns>.Mascot`); a spread
15
+ * attribute may carry it, so an element with a spread is not judged.
16
+ *
17
+ * WHERE IT APPLIES. Product source (`isProductSource`: the route, feature, component, hook, foundation, module and transport
18
+ * tiers and the workspace packages). A script or a file no slot owns is not judged.
19
+ */
20
+ /** A generated raster under the public `/images/` tree: `.webp` or `.png`, any case, an optional query or fragment. */
21
+ export const GENERATED_ART = /^\/images\/.+\.(?:webp|png)(?:[?#].*)?$/i;
22
+ /** The components that render generated art: the element itself, or an ancestor that frames it. */
23
+ const RENDERER = "Mascot";
24
+ const FRAME = "MediaFrame";
25
+ /** The element name of a JSX name: `Mascot`, or `G.Mascot` as `Mascot`. */
26
+ const nameOf = (name) => {
27
+ if (name.type === "JSXIdentifier")
28
+ return name.name;
29
+ if (name.type === "JSXMemberExpression")
30
+ return name.property.name;
31
+ return null;
32
+ };
33
+ /** The variable a name resolves to, walking the scope chain outwards. */
34
+ const variableOf = (scope, name) => {
35
+ for (let current = scope; current !== null; current = current.upper) {
36
+ const found = current.set.get(name);
37
+ if (found !== undefined)
38
+ return found;
39
+ }
40
+ return null;
41
+ };
42
+ /** The string an expression statically is, or null. */
43
+ const staticString = (expression, scopeOf) => {
44
+ if (expression.type === "Literal")
45
+ return typeof expression.value === "string" ? expression.value : null;
46
+ if (expression.type === "TemplateLiteral")
47
+ return expression.expressions.length === 0 ? expression.quasis.map((quasi) => quasi.value.cooked ?? "").join("") : null;
48
+ if (expression.type === "Identifier") {
49
+ const variable = variableOf(scopeOf(expression), expression.name);
50
+ const definition = variable?.defs.length === 1 ? variable.defs[0] : undefined;
51
+ if (definition?.type !== "Variable")
52
+ return null;
53
+ const declarator = definition.node;
54
+ const declaration = definition.parent;
55
+ if (declaration?.kind !== "const" || declarator.init === null || declarator.init.type === "Identifier")
56
+ return null;
57
+ return staticString(declarator.init, scopeOf);
58
+ }
59
+ return null;
60
+ };
61
+ /** The string a JSX attribute value statically is, or null. */
62
+ const attributeString = (value, scopeOf) => {
63
+ if (value === null)
64
+ return null;
65
+ if (value.type === "Literal")
66
+ return typeof value.value === "string" ? value.value : null;
67
+ if (value.type === "JSXExpressionContainer" && value.expression.type !== "JSXEmptyExpression")
68
+ return staticString(value.expression, scopeOf);
69
+ return null;
70
+ };
71
+ /** True when a `MediaFrame` element encloses the node. */
72
+ const insideFrame = (node) => {
73
+ // The Program's parent is null at run time though the tree's types say a node always has one.
74
+ for (let current = node.parent; current !== undefined && current !== null; current = current.parent) {
75
+ if (current.type === "JSXElement" && nameOf(current.openingElement.name) === FRAME)
76
+ return true;
77
+ }
78
+ return false;
79
+ };
80
+ /** Generated art under `/images/` renders only through `Mascot` or inside a `MediaFrame`. */
81
+ export const generatedArtInMascotOrMediaFrame = defineRule({
82
+ meta: {
83
+ type: "problem",
84
+ docs: { description: "R338: generated art (a .webp or .png under /images/) renders only through Grammar's Mascot or inside a MediaFrame." },
85
+ schema: [],
86
+ messages: {
87
+ outside: "`{{src}}` is generated art drawn by `<{{element}}>`, outside Grammar's `Mascot` and `MediaFrame`: it takes whatever box this page gives it, so the same art comes out at a different size on every screen. Render a character with `<Mascot src size assetSlot />` (sm, md, lg or hero) or frame a scene or illustration in `<MediaFrame assetSlot>`.",
88
+ },
89
+ },
90
+ create(context) {
91
+ if (!isProductSource(context))
92
+ return {};
93
+ const scopeOf = (node) => context.sourceCode.getScope(node);
94
+ return {
95
+ JSXAttribute(attribute) {
96
+ if (attribute.name.type !== "JSXIdentifier" || attribute.name.name !== "src")
97
+ return;
98
+ const src = attributeString(attribute.value, scopeOf);
99
+ if (src === null || !GENERATED_ART.test(src))
100
+ return;
101
+ const opening = attribute.parent;
102
+ if (opening.type !== "JSXOpeningElement")
103
+ return;
104
+ const element = nameOf(opening.name);
105
+ if (element === RENDERER || insideFrame(opening))
106
+ return;
107
+ context.report({ node: attribute, messageId: "outside", data: { src, element: element ?? "element" } });
108
+ },
109
+ };
110
+ },
111
+ });
112
+ /** True when an attribute names nothing: no value, an empty string, or `undefined`. */
113
+ const namesNothing = (attribute) => {
114
+ const value = attribute.value;
115
+ if (value === null)
116
+ return true;
117
+ if (value.type === "Literal")
118
+ return value.value === "";
119
+ if (value.type !== "JSXExpressionContainer")
120
+ return false;
121
+ const expression = value.expression;
122
+ if (expression.type === "JSXEmptyExpression")
123
+ return true;
124
+ if (expression.type === "Identifier")
125
+ return expression.name === "undefined";
126
+ if (expression.type === "Literal")
127
+ return expression.value === "" || expression.value === null;
128
+ if (expression.type === "TemplateLiteral")
129
+ return expression.expressions.length === 0 && expression.quasis.every((quasi) => quasi.value.cooked === "");
130
+ return false;
131
+ };
132
+ /** Every `Mascot` names the asset slot its art was requested under. */
133
+ export const mascotAssetSlotRequired = defineRule({
134
+ meta: {
135
+ type: "problem",
136
+ docs: { description: "R339: every Grammar Mascot names its asset slot (assetSlot), tying the rendered art to the asset workflow's request." },
137
+ schema: [],
138
+ messages: {
139
+ missing: "This `Mascot` names no `assetSlot`: nothing ties the art it draws to the asset workflow's request, so a drawing, a test or an audit cannot tell which slot it fills. Pass `assetSlot` (and `assetSha256` once the file is delivered).",
140
+ },
141
+ },
142
+ create(context) {
143
+ if (!isProductSource(context))
144
+ return {};
145
+ return {
146
+ JSXOpeningElement(opening) {
147
+ if (nameOf(opening.name) !== RENDERER)
148
+ return;
149
+ if (opening.attributes.some((attribute) => attribute.type === "JSXSpreadAttribute"))
150
+ return;
151
+ const slot = opening.attributes.find((attribute) => attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier" && attribute.name.name === "assetSlot");
152
+ if (slot === undefined || namesNothing(slot))
153
+ context.report({ node: opening, messageId: "missing" });
154
+ },
155
+ };
156
+ },
157
+ });
158
+ /** The rules this law contributes to the plugin. */
159
+ export const rules = {
160
+ "generated-art-in-mascot-or-media-frame": generatedArtInMascotOrMediaFrame,
161
+ "mascot-asset-slot-required": mascotAssetSlotRequired,
162
+ };
163
+ defineFamilyRules({ "generated-art-in-mascot-or-media-frame": generatedArtInMascotOrMediaFrame }, "knowledge/hfs/rules.yaml#R338");
164
+ defineFamilyRules({ "mascot-asset-slot-required": mascotAssetSlotRequired }, "knowledge/hfs/rules.yaml#R339");
165
+ /** Every rule of this law is an error. */
166
+ export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-grammar/${name}`, "error"]));
@@ -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"]));
package/docs/mascot.md ADDED
@@ -0,0 +1,60 @@
1
+ # Generated art renders through Mascot
2
+
3
+ Law module: `mascot.ts`. Catalogue: R338 FE_GENERATED_ART_OUTSIDE_FRAME, R339 FE_MASCOT_ASSET_SLOT_MISSING.
4
+
5
+ Generated art is a raster the asset workflow delivers under the public `/images/` tree, as `.webp` or `.png`. Grammar's
6
+ `Mascot` renders a character at one size scale (`sm` 96px, `md` 160px, `lg` 240px square, `hero` filling its host's
7
+ height up to its cap), always contained on a transparent ground; `MediaFrame` frames a scene or illustration. Both carry
8
+ the asset-slot props (`assetSlot`, `assetSha256`, `assetPrompt`) that tie the render to the request. Drawn through a bare
9
+ `Image` or `<img>`, the same art takes whatever box each page gives it.
10
+
11
+ Every rule below is an error in `coreGrammarConfig`; none can be switched off or suppressed inline. Both apply to
12
+ product source (the route, feature, component, hook, foundation, module and transport tiers and the workspace packages);
13
+ a script or a file no slot owns is not judged.
14
+
15
+ ## `starci-grammar/generated-art-in-mascot-or-media-frame`
16
+
17
+ A JSX `src` attribute whose value is statically a path under `/images/` ending in `.webp` or `.png` (any case, an
18
+ optional query or fragment) is a finding unless its element is a `Mascot` (`Mascot` or `<ns>.Mascot`) or a `MediaFrame`
19
+ element encloses it. The value is read from a string literal, an expression container holding one, a template literal
20
+ without expressions, or a `const` of the file initialised with one; a source computed at run time is not judged.
21
+ `.svg`, `.jpg` and paths outside `/images/` are not generated art here.
22
+
23
+ **Invalid** (`src/components/blocks/Onboarding/component.tsx`)
24
+
25
+ ```tsx
26
+ <Image src="/images/mascot/welcome.webp" alt="" />
27
+ ```
28
+
29
+ **Valid** (`src/components/blocks/Onboarding/component.tsx`)
30
+
31
+ ```tsx
32
+ <Mascot src="/images/mascot/welcome.webp" size="lg" assetSlot="onboarding.welcome" />
33
+
34
+ <MediaFrame assetSlot="landing.scene">
35
+ <Image src="/images/scene/landing.webp" alt="" />
36
+ </MediaFrame>
37
+ ```
38
+
39
+ **Finding code:** `FE_GENERATED_ART_OUTSIDE_FRAME`
40
+
41
+ ## `starci-grammar/mascot-asset-slot-required`
42
+
43
+ A `Mascot` element with no `assetSlot`, or with one that names nothing (`""`, `{undefined}`), is a finding, reported on
44
+ the element. An element with a spread attribute is not judged: the spread may carry the slot.
45
+
46
+ **Invalid**
47
+
48
+ ```tsx
49
+ <Mascot src="/images/mascot/welcome.webp" size="hero" />
50
+ ```
51
+
52
+ **Valid**
53
+
54
+ ```tsx
55
+ <Mascot src="/images/mascot/welcome.webp" size="hero" assetSlot="auth.sign-in" assetSha256={sha} />
56
+ ```
57
+
58
+ **Finding code:** `FE_MASCOT_ASSET_SLOT_MISSING`
59
+
60
+ Rule ownership: [FE-RULE-OWNERSHIP](../../../contexts/code/fe/README.md#the-placement-test-fe-rule-ownership).
@@ -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.15.34",
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.5.23",
44
+ "@starci/eslint-canon-fe": "9.6.2",
45
45
  "eslint": ">=9",
46
46
  "typescript": ">=5.9"
47
47
  },
@@ -0,0 +1,74 @@
1
+ {
2
+ "generated": [
3
+ "engine/by-code-unit.mjs",
4
+ "engine/model-config.mjs",
5
+ "engine/orca-config.mjs",
6
+ "engine/plain-object.mjs",
7
+ "engine/release-config.mjs",
8
+ "engine/removed-vocabulary.mjs",
9
+ "engine/resources-config.mjs",
10
+ "engine/runtime-root.mjs",
11
+ "engine/secrets.mjs",
12
+ "engine/sonar-config.mjs",
13
+ "engine/yaml.mjs",
14
+ "knowledge/grammars/core/playbook.yaml",
15
+ "knowledge/grammars/index.yaml",
16
+ "knowledge/hfs/canon-pins.yaml",
17
+ "knowledge/ui/presentation/font.yaml",
18
+ "knowledge/ui/presentation/padding.yaml",
19
+ "modules/kernel/removed-vocabulary.yaml",
20
+ "modules/models/registry.yaml",
21
+ "modules/models/runtimes.yaml",
22
+ "modules/models/tiers.yaml",
23
+ "scripts/hfs/allows.mjs",
24
+ "scripts/hfs/architecture/config.mjs",
25
+ "scripts/hfs/architecture/side-boundary.mjs",
26
+ "scripts/hfs/architecture/type-context.mjs",
27
+ "scripts/hfs/architecture/typescript.mjs",
28
+ "scripts/hfs/declaration-shape.mjs",
29
+ "scripts/hfs/declaration-slots.mjs",
30
+ "scripts/hfs/edition-slots.mjs",
31
+ "scripts/hfs/param-names.mjs",
32
+ "scripts/hfs/rule-catalog.mjs",
33
+ "scripts/hfs/rule-params-shape.mjs",
34
+ "scripts/hfs/slot-app-view.mjs",
35
+ "scripts/hfs/slot-classify.mjs",
36
+ "scripts/hfs/slot-errors.mjs",
37
+ "scripts/hfs/slot-imports.mjs",
38
+ "scripts/hfs/slot-manifest-shape.mjs",
39
+ "scripts/hfs/slot-match.mjs",
40
+ "scripts/hfs/slot-path.mjs",
41
+ "scripts/hfs/slot-required.mjs",
42
+ "scripts/hfs/slot-semantic-problems.mjs",
43
+ "scripts/hfs/slot-side-problems.mjs",
44
+ "scripts/hfs/slots.mjs",
45
+ "scripts/hfs/typescript-programs.mjs",
46
+ "scripts/hfs/view.mjs",
47
+ "scripts/lib/glob.mjs",
48
+ "scripts/lib/grammar-lint.mjs",
49
+ "scripts/lib/i18n.mjs",
50
+ "scripts/lib/json-clone.mjs",
51
+ "scripts/lib/json.mjs",
52
+ "scripts/lib/list.mjs",
53
+ "scripts/lib/path-key.mjs",
54
+ "scripts/lib/read-text.mjs",
55
+ "scripts/lib/regex.mjs",
56
+ "scripts/lib/source-phrases.mjs",
57
+ "scripts/lib/ts-analysis.mjs",
58
+ "scripts/lib/ts-ast.mjs",
59
+ "scripts/lib/walk.mjs",
60
+ "scripts/lib/yaml-cached.mjs"
61
+ ],
62
+ "own": [
63
+ "engine/config.mjs",
64
+ "engine/invalid-config.mjs",
65
+ "knowledge/hfs/rules.yaml",
66
+ "knowledge/hfs/slots.yaml",
67
+ "knowledge/patterns/fe/index.yaml",
68
+ "knowledge/patterns/fe/ui.yaml",
69
+ "knowledge/ui/composition/app-shell.yaml",
70
+ "knowledge/ui/proof/anatomy-source.yaml",
71
+ "modules/goal/source-phrases.yaml",
72
+ "scripts/hfs/manifest-shape.mjs"
73
+ ]
74
+ }
@@ -1,12 +1,12 @@
1
- // by-code-unit.mjs - the comparator of a bare `Array.prototype.sort()`, owned beside the engine files that digest and canonicalize (scripts/lib/list.mjs re-exports it).
2
-
1
+ // by-code-unit.mjs -the comparator of a bare `Array.prototype.sort()`, owned beside the engine files that digest and canonicalize (scripts/lib/list.mjs re-exports it).
3
2
  /**
4
3
  * The comparator of a bare `Array.prototype.sort()`: values compared as strings by UTF-16 code unit. Pass it to `sort` and
5
4
  * `toSorted` so the order is stated, and stays byte-identical to the default order that digests and canonical output depend on.
6
5
  */
7
6
  export const byCodeUnit = (a, b) => {
8
- const left = String(a);
9
- const right = String(b);
10
- if (left < right) return -1;
11
- return left > right ? 1 : 0;
7
+ const left = String(a);
8
+ const right = String(b);
9
+ if (left < right)
10
+ return -1;
11
+ return left > right ? 1 : 0;
12
12
  };