@starci/eslint-grammar-core 0.16.2 → 0.16.3
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 +6 -0
- package/README.md +1 -1
- package/dist/freestyle.d.ts +7 -0
- package/dist/freestyle.js +70 -0
- package/dist/grammar-boundary.js +3 -3
- package/dist/icon.js +3 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/lib/cross-file-inputs.d.ts +34 -0
- package/dist/lib/cross-file-inputs.js +31 -0
- package/dist/lib/rule-contract.d.ts +5 -0
- package/dist/lib/rule-contract.js +9 -0
- package/dist/lib/why.js +5 -0
- package/dist/mascot.js +3 -3
- package/dist/tokens.js +3 -3
- package/package.json +3 -3
- package/runtime/knowledge/grammars/core/playbook.yaml +12 -18
- package/runtime/knowledge/hfs/canon-pins.yaml +8 -8
- package/runtime/knowledge/hfs/rules.yaml +46 -0
- package/runtime/knowledge/patterns/fe/ui.yaml +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.3 - 2026-10-31
|
|
4
|
+
|
|
5
|
+
- Changed: the peer moves to eslint-canon-fe 9.6.3; the bundled canon pins name the 0.16.3 cohort (@starci/grammar, eslint-grammar-core and eslint-grammar-offset-pop 0.16.3, eslint-canon-fe 9.6.3, eslint-canon-be 3.9.3, stylelint-canon 2.3.0, jest-preset 2.6.0, hfs 4.13.0); ChatWorkspace and PaneLayout are removed with no alias; PaneGrid and Pane are the one generic pane grid: grammar names are generic. The core playbook names PaneGrid in its layout-frame and container lists.
|
|
6
|
+
- Added: starci-grammar/freestyle-has-reason (R341, FE_FREESTYLE_REASON_MISSING): a `<Freestyle>` region names a non-empty string-literal `reason`. no-raw-structural-element, no-fractional-step, generated-art-in-mascot-or-media-frame and no-inline-svg-outside-brand-mark-leaf do not report the raw elements inside a Freestyle subtree (owner ruling grammar-freestyle-region). Specs: src/freestyle.spec.ts.
|
|
7
|
+
- Added: `crossFileInputs` for the lint result cache of `starci app check` (ruling app-check-cache): the files each grammar rule reads beyond the linted file.
|
|
8
|
+
|
|
3
9
|
## 0.16.2 - 2026-10-31
|
|
4
10
|
|
|
5
11
|
- 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).
|
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, and a surface nested in another surface left subordinate (no forced `depth="top"`). 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"`), and a Freestyle region that names its reason (its raw elements are exempt from the paint and raw-element laws). 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
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Rule } from "eslint";
|
|
2
|
+
/** A Freestyle region always says why no Grammar component fits. */
|
|
3
|
+
export declare const freestyleHasReason: 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,70 @@
|
|
|
1
|
+
import { isProductSource } from "@starci/eslint-canon-fe/lib/scope.mjs";
|
|
2
|
+
import { defineFamilyRules, defineRule } from "./lib/rule-contract.js";
|
|
3
|
+
/**
|
|
4
|
+
* The Freestyle law (HFS R341 `FE_FREESTYLE_REASON_MISSING`; owner ruling `grammar-freestyle-region`): Freestyle is the one
|
|
5
|
+
* sanctioned escape hatch of the Grammar, a region where the app writes its own plain elements and app CSS because no
|
|
6
|
+
* Grammar component fits. It is a last resort, so it always says why: `reason` is a non-empty string literal such as
|
|
7
|
+
* `"sign-in brand art"`, and the Grammar stamps it on the DOM (`data-grammar-freestyle-reason`).
|
|
8
|
+
*
|
|
9
|
+
* WHAT IT READS. A `Freestyle` element (or `<ns>.Freestyle`) whose `reason` attribute is missing, is not a string literal
|
|
10
|
+
* (a literal, or an expression container holding a string literal or a template without substitutions), or is blank. An
|
|
11
|
+
* element with a spread attribute is not judged (the spread may carry the reason).
|
|
12
|
+
*
|
|
13
|
+
* THE OTHER HALF. The rules that judge hand-written elements, classes, styles and generated art leave the raw elements of a
|
|
14
|
+
* Freestyle subtree alone (`exemptFreestyle`); a Grammar component inside it is judged as before.
|
|
15
|
+
*
|
|
16
|
+
* WHERE IT APPLIES. Product source (`isProductSource`).
|
|
17
|
+
*/
|
|
18
|
+
/** The element name of a JSX name: `Freestyle`, or `G.Freestyle` as `Freestyle`. */
|
|
19
|
+
const nameOf = (name) => {
|
|
20
|
+
if (name.type === "JSXIdentifier")
|
|
21
|
+
return name.name;
|
|
22
|
+
if (name.type === "JSXMemberExpression")
|
|
23
|
+
return name.property.name;
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
26
|
+
/** The statically known text of an attribute value; null when it is computed. */
|
|
27
|
+
const literalText = (value) => {
|
|
28
|
+
if (value === null)
|
|
29
|
+
return null;
|
|
30
|
+
const node = value.type === "JSXExpressionContainer" ? value.expression : value;
|
|
31
|
+
if (node.type === "Literal")
|
|
32
|
+
return typeof node.value === "string" ? node.value : null;
|
|
33
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0)
|
|
34
|
+
return node.quasis[0]?.value.cooked ?? "";
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
/** A Freestyle region always says why no Grammar component fits. */
|
|
38
|
+
export const freestyleHasReason = defineRule({
|
|
39
|
+
meta: {
|
|
40
|
+
type: "problem",
|
|
41
|
+
docs: { description: "R341: a Freestyle region names its reason, a non-empty string literal saying why no Grammar component fits." },
|
|
42
|
+
schema: [],
|
|
43
|
+
messages: {
|
|
44
|
+
reason: "`<Freestyle>` needs `reason=\"...\"`, a non-empty string saying why no Grammar component fits (for example \"sign-in brand art\"). Freestyle is a last resort: name the reason, or compose a Grammar component instead.",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
create(context) {
|
|
48
|
+
if (!isProductSource(context))
|
|
49
|
+
return {};
|
|
50
|
+
return {
|
|
51
|
+
JSXOpeningElement(node) {
|
|
52
|
+
if (nameOf(node.name) !== "Freestyle")
|
|
53
|
+
return;
|
|
54
|
+
if (node.attributes.some((entry) => entry.type === "JSXSpreadAttribute"))
|
|
55
|
+
return;
|
|
56
|
+
const reason = node.attributes.find((entry) => entry.type === "JSXAttribute" && entry.name.type === "JSXIdentifier" && entry.name.name === "reason");
|
|
57
|
+
const text = reason === undefined ? null : literalText(reason.value);
|
|
58
|
+
if (text === null || text.trim() === "")
|
|
59
|
+
context.report({ node, messageId: "reason" });
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
/** The rules this law contributes to the plugin. */
|
|
65
|
+
export const rules = {
|
|
66
|
+
"freestyle-has-reason": freestyleHasReason,
|
|
67
|
+
};
|
|
68
|
+
defineFamilyRules(rules, "knowledge/hfs/rules.yaml#R341");
|
|
69
|
+
/** Every rule of this law is an error. */
|
|
70
|
+
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-grammar/${name}`, "error"]));
|
package/dist/grammar-boundary.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineFamilyRules, defineRule } from "./lib/rule-contract.js";
|
|
1
|
+
import { defineFamilyRules, defineRule, exemptFreestyle } from "./lib/rule-contract.js";
|
|
2
2
|
/**
|
|
3
3
|
* The boundary between product code and `@starci/grammar`.
|
|
4
4
|
*
|
|
@@ -55,7 +55,7 @@ const composesGrammar = (context, file) => {
|
|
|
55
55
|
return APP_SLOTS.has(slot) || hfs.tierOf(file) === "package";
|
|
56
56
|
};
|
|
57
57
|
/** No raw structural or text element in app source or in a package that composes the grammar. */
|
|
58
|
-
export const noRawStructuralElement = defineRule({
|
|
58
|
+
export const noRawStructuralElement = exemptFreestyle(defineRule({
|
|
59
59
|
meta: {
|
|
60
60
|
type: "problem",
|
|
61
61
|
docs: { description: "Page structure and text are grammar components, not raw `div`-family HTML; only `fe.package.ui` renderers draw raw tags." },
|
|
@@ -78,7 +78,7 @@ export const noRawStructuralElement = defineRule({
|
|
|
78
78
|
},
|
|
79
79
|
};
|
|
80
80
|
},
|
|
81
|
-
});
|
|
81
|
+
}));
|
|
82
82
|
/** The rules this law contributes to the plugin. */
|
|
83
83
|
export const rules = {
|
|
84
84
|
"no-raw-structural-element": noRawStructuralElement,
|
package/dist/icon.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineFamilyRules, defineRule } from "./lib/rule-contract.js";
|
|
1
|
+
import { defineFamilyRules, defineRule, exemptFreestyle } from "./lib/rule-contract.js";
|
|
2
2
|
/**
|
|
3
3
|
* The rules that hold `icon.md`.
|
|
4
4
|
*
|
|
@@ -240,7 +240,7 @@ const inDeclaredBrandMark = (sourceCode, node) => {
|
|
|
240
240
|
return false;
|
|
241
241
|
};
|
|
242
242
|
/** Inline SVG belongs only to an explicitly declared brand mark in a leaf entry. */
|
|
243
|
-
export const noInlineSvgOutsideBrandMarkLeaf = defineRule({
|
|
243
|
+
export const noInlineSvgOutsideBrandMarkLeaf = exemptFreestyle(defineRule({
|
|
244
244
|
meta: {
|
|
245
245
|
type: "problem",
|
|
246
246
|
docs: { description: "FE_ICON_INLINE_SVG: inline SVG is drawn only by an exported leaf entry component with a @brand-mark JSDoc declaration.", url: "knowledge/hfs/rules.yaml#R286" },
|
|
@@ -264,7 +264,7 @@ export const noInlineSvgOutsideBrandMarkLeaf = defineRule({
|
|
|
264
264
|
},
|
|
265
265
|
};
|
|
266
266
|
},
|
|
267
|
-
});
|
|
267
|
+
}));
|
|
268
268
|
/** The finding code for the copy-and-glyph law's inline SVG enforcer. */
|
|
269
269
|
Object.assign(why, {
|
|
270
270
|
"no-inline-svg-outside-brand-mark-leaf": {
|
package/dist/index.d.ts
CHANGED
|
@@ -43,3 +43,5 @@ export declare const coreGrammarConfig: (input: {
|
|
|
43
43
|
readonly hfs: GrammarHfsView;
|
|
44
44
|
readonly family?: GrammarFamilyId;
|
|
45
45
|
}) => Linter.Config[];
|
|
46
|
+
export { crossFileInputs } from "./lib/cross-file-inputs.js";
|
|
47
|
+
export type { CrossFileInput } from "./lib/cross-file-inputs.js";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { SOURCE_FILES, linterOptions as canonLinterOptions } from "@starci/eslin
|
|
|
3
3
|
import { rules as appShellRules } from "./app-shell.js";
|
|
4
4
|
import { rules as chipLabelRules } from "./chip-label.js";
|
|
5
5
|
import { rules as drawingRules } from "./drawing.js";
|
|
6
|
+
import { rules as freestyleRules } from "./freestyle.js";
|
|
6
7
|
import { rules as grammarBoundaryRules } from "./grammar-boundary.js";
|
|
7
8
|
import { rules as iconRules } from "./icon.js";
|
|
8
9
|
import { rules as mascotRules } from "./mascot.js";
|
|
@@ -19,6 +20,7 @@ const CONTRIBUTIONS = [
|
|
|
19
20
|
{ law: "app-shell", rules: appShellRules },
|
|
20
21
|
{ law: "chip-label", rules: chipLabelRules },
|
|
21
22
|
{ law: "drawing", rules: drawingRules },
|
|
23
|
+
{ law: "freestyle", rules: freestyleRules },
|
|
22
24
|
{ law: "grammar-boundary", rules: grammarBoundaryRules },
|
|
23
25
|
{ law: "icon", rules: iconRules },
|
|
24
26
|
{ law: "mascot", rules: mascotRules },
|
|
@@ -79,3 +81,5 @@ export const coreGrammarConfig = ({ hfs, family = "core" } = {}) => {
|
|
|
79
81
|
rules: { ...recommended },
|
|
80
82
|
}];
|
|
81
83
|
};
|
|
84
|
+
// The result cache of `starci app lint` (contexts/project/check-cache.md) reads the disk inputs the family rules declare.
|
|
85
|
+
export { crossFileInputs } from "./lib/cross-file-inputs.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files a rule of this canon reads that ESLint's own import graph does not carry.
|
|
3
|
+
*
|
|
4
|
+
* The result cache of `starci app lint` (contexts/project/check-cache.md, ESLint) re-lints a file when it or a file it reaches through imports
|
|
5
|
+
* changed. A rule that reads another file by path, with `fs` (a sibling, a directory listing, a manifest, a module file it follows by hand),
|
|
6
|
+
* has an input the import graph cannot see, so the rule declares it here and the cache dirties the files that depend on it:
|
|
7
|
+
*
|
|
8
|
+
* - `content`: the rule reads the bytes of the files the globs match; a changed, added or removed match dirties the dependants.
|
|
9
|
+
* - `exists`: the rule only asks whether a file is there or lists a folder; an added or removed match dirties the dependants.
|
|
10
|
+
* - `git`: the rule compares the file with git history (the merge-base with the base branch), which no file holds; a change of the merge-base
|
|
11
|
+
* dirties every dependant of more than `minLines` lines (the rule reads history only for a file over its size budget). `globs` is empty.
|
|
12
|
+
*
|
|
13
|
+
* `globs` are relative to the linted file's folder (`relativeTo: "file"`) or to the side root, the folder of the side's `eslint.config.mjs`
|
|
14
|
+
* (`relativeTo: "side"`); they use `*`, `**`, `?` and `{a,b}`, posix separators. `appliesTo` are side-root globs of the linted files that
|
|
15
|
+
* depend on the input. A declaration may be wider than the read, never narrower: a too wide one costs a lint, a too narrow one a wrong answer.
|
|
16
|
+
* `cross-file-inputs.spec.ts` proves the list complete: every source file that reads the disk is named here or accounted for, and a recorder of
|
|
17
|
+
* file reads checks each rule's real reads against these declarations and the import graph.
|
|
18
|
+
*/
|
|
19
|
+
/** One input of a rule that the import graph does not carry. */
|
|
20
|
+
export interface CrossFileInput {
|
|
21
|
+
/** The published rule name (without the plugin prefix). */
|
|
22
|
+
readonly rule: string;
|
|
23
|
+
readonly kind: "content" | "exists" | "git";
|
|
24
|
+
readonly relativeTo: "file" | "side";
|
|
25
|
+
readonly globs: readonly string[];
|
|
26
|
+
/** For `git`: the line count above which the rule reads history; a file of fewer lines does not depend on it. */
|
|
27
|
+
readonly minLines?: number;
|
|
28
|
+
/** The linted files (side-root globs) whose result depends on the input. */
|
|
29
|
+
readonly appliesTo: readonly string[];
|
|
30
|
+
/** The source file of this package (relative to `src/`) that performs the read; the audit checks it exists and that no reader is missing. */
|
|
31
|
+
readonly reader: string;
|
|
32
|
+
}
|
|
33
|
+
/** Every disk read of this canon's rules beyond the import graph. */
|
|
34
|
+
export declare const crossFileInputs: readonly CrossFileInput[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files a rule of this canon reads that ESLint's own import graph does not carry.
|
|
3
|
+
*
|
|
4
|
+
* The result cache of `starci app lint` (contexts/project/check-cache.md, ESLint) re-lints a file when it or a file it reaches through imports
|
|
5
|
+
* changed. A rule that reads another file by path, with `fs` (a sibling, a directory listing, a manifest, a module file it follows by hand),
|
|
6
|
+
* has an input the import graph cannot see, so the rule declares it here and the cache dirties the files that depend on it:
|
|
7
|
+
*
|
|
8
|
+
* - `content`: the rule reads the bytes of the files the globs match; a changed, added or removed match dirties the dependants.
|
|
9
|
+
* - `exists`: the rule only asks whether a file is there or lists a folder; an added or removed match dirties the dependants.
|
|
10
|
+
* - `git`: the rule compares the file with git history (the merge-base with the base branch), which no file holds; a change of the merge-base
|
|
11
|
+
* dirties every dependant of more than `minLines` lines (the rule reads history only for a file over its size budget). `globs` is empty.
|
|
12
|
+
*
|
|
13
|
+
* `globs` are relative to the linted file's folder (`relativeTo: "file"`) or to the side root, the folder of the side's `eslint.config.mjs`
|
|
14
|
+
* (`relativeTo: "side"`); they use `*`, `**`, `?` and `{a,b}`, posix separators. `appliesTo` are side-root globs of the linted files that
|
|
15
|
+
* depend on the input. A declaration may be wider than the read, never narrower: a too wide one costs a lint, a too narrow one a wrong answer.
|
|
16
|
+
* `cross-file-inputs.spec.ts` proves the list complete: every source file that reads the disk is named here or accounted for, and a recorder of
|
|
17
|
+
* file reads checks each rule's real reads against these declarations and the import graph.
|
|
18
|
+
*/
|
|
19
|
+
/** Every disk read of this canon's rules beyond the import graph. */
|
|
20
|
+
export const crossFileInputs = [
|
|
21
|
+
// the header an app shell imports is followed into the workspace packages by their manifests and entries
|
|
22
|
+
{ rule: "app-shell-frame", kind: "content", relativeTo: "side", globs: ["packages/*/package.json", "packages/*/src/**/*.{ts,tsx}"], appliesTo: ["**/*.{ts,tsx}"], reader: "lib/app-shell-owner-source.ts" },
|
|
23
|
+
{ rule: "navbar-cluster", kind: "content", relativeTo: "side", globs: ["packages/*/package.json", "packages/*/src/**/*.{ts,tsx}"], appliesTo: ["**/*.{ts,tsx}"], reader: "lib/app-shell-owner-source.ts" },
|
|
24
|
+
// a page owner is judged by the route files of its app that mount it and the layouts above them
|
|
25
|
+
{ rule: "page-tabs-in-header", kind: "content", relativeTo: "side", globs: ["**/src/app/**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}"], appliesTo: ["**/src/features/pages/**/*.{ts,tsx}"], reader: "lib/page-frame.ts" },
|
|
26
|
+
{ rule: "page-tabs-in-header", kind: "exists", relativeTo: "side", globs: ["**/src/app", "**/src/app/**"], appliesTo: ["**/src/features/pages/**/*.{ts,tsx}"], reader: "lib/page-frame.ts" },
|
|
27
|
+
{ rule: "route-page-is-one-page", kind: "exists", relativeTo: "side", globs: ["**/src/app", "**/src/app/**"], appliesTo: ["**/src/features/pages/**/*.{ts,tsx}"], reader: "lib/page-frame.ts" },
|
|
28
|
+
{ rule: "route-page-is-one-page", kind: "content", relativeTo: "side", globs: ["**/src/app/**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}"], appliesTo: ["**/src/features/pages/**/*.{ts,tsx}"], reader: "lib/page-frame.ts" },
|
|
29
|
+
// the theme an app defines its tokens in: the route slot's globals.css and the brand slot's brand.css
|
|
30
|
+
{ rule: "no-unresolved-token-class", kind: "content", relativeTo: "side", globs: ["**/globals.css", "**/brand.css"], appliesTo: ["**/*.{ts,tsx}"], reader: "tokens.ts" },
|
|
31
|
+
];
|
|
@@ -8,5 +8,10 @@ export declare const RULE_OWNERSHIP = "knowledge/patterns/fe/index.yaml#FE-RULE-
|
|
|
8
8
|
* visitor it returns carry that parser's node types; this is the one place that states so to ESLint's declarations.
|
|
9
9
|
*/
|
|
10
10
|
export declare const defineRule: (definition: RuleDefinition) => Rule.RuleModule;
|
|
11
|
+
/**
|
|
12
|
+
* A rule that leaves the raw elements of a Freestyle subtree alone (owner ruling grammar-freestyle-region): a finding about a
|
|
13
|
+
* plain element, or its class or style, inside `<Freestyle>` is dropped; a Grammar component inside it is judged as before.
|
|
14
|
+
*/
|
|
15
|
+
export declare const exemptFreestyle: (rule: Rule.RuleModule) => Rule.RuleModule;
|
|
11
16
|
/** Attach the governing ownership rule and the rule's behavioral source without changing enforcement. */
|
|
12
17
|
export declare const defineFamilyRules: (rules: Readonly<Record<string, Rule.RuleModule>>, knowledge: string) => Readonly<Record<string, Rule.RuleModule>>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { freestyleContext } from "@starci/eslint-canon-fe/lib/freestyle.mjs";
|
|
1
2
|
/** The ordered ownership test that governs every Grammar-specific lint rule. */
|
|
2
3
|
export const RULE_OWNERSHIP = "knowledge/patterns/fe/index.yaml#FE-RULE-OWNERSHIP";
|
|
3
4
|
/**
|
|
@@ -9,6 +10,14 @@ export const defineRule = (definition) => ({
|
|
|
9
10
|
meta: definition.meta,
|
|
10
11
|
create: (context) => definition.create(context),
|
|
11
12
|
});
|
|
13
|
+
/**
|
|
14
|
+
* A rule that leaves the raw elements of a Freestyle subtree alone (owner ruling grammar-freestyle-region): a finding about a
|
|
15
|
+
* plain element, or its class or style, inside `<Freestyle>` is dropped; a Grammar component inside it is judged as before.
|
|
16
|
+
*/
|
|
17
|
+
export const exemptFreestyle = (rule) => ({
|
|
18
|
+
...rule,
|
|
19
|
+
create: (context) => rule.create(freestyleContext(context)),
|
|
20
|
+
});
|
|
12
21
|
/** Attach the governing ownership rule and the rule's behavioral source without changing enforcement. */
|
|
13
22
|
export const defineFamilyRules = (rules, knowledge) => {
|
|
14
23
|
for (const rule of Object.values(rules)) {
|
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
|
+
"freestyle-has-reason": {
|
|
58
|
+
code: "FE_FREESTYLE_REASON_MISSING",
|
|
59
|
+
en: "A `<Freestyle>` region in `<file>` has no non-empty string `reason`.",
|
|
60
|
+
fix: "Say why no Grammar component fits: `<Freestyle reason=\"sign-in brand art\">`. If a Grammar component fits, compose it instead.",
|
|
61
|
+
},
|
|
57
62
|
"no-raised-surface-in-surface": {
|
|
58
63
|
code: "FE_RAISED_SURFACE_IN_SURFACE",
|
|
59
64
|
en: "A Grammar surface in `<file>` forces `depth=\"top\"` inside another surface, raising a surface inside a surface (BOUNDARY-6).",
|
package/dist/mascot.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isProductSource } from "@starci/eslint-canon-fe/lib/scope.mjs";
|
|
2
|
-
import { defineFamilyRules, defineRule } from "./lib/rule-contract.js";
|
|
2
|
+
import { defineFamilyRules, defineRule, exemptFreestyle } from "./lib/rule-contract.js";
|
|
3
3
|
/**
|
|
4
4
|
* The mascot law (HFS R338 `FE_GENERATED_ART_OUTSIDE_FRAME`, R339 `FE_MASCOT_ASSET_SLOT_MISSING`): generated art renders
|
|
5
5
|
* at the Grammar's size scale and stays tied to the asset workflow's request.
|
|
@@ -78,7 +78,7 @@ const insideFrame = (node) => {
|
|
|
78
78
|
return false;
|
|
79
79
|
};
|
|
80
80
|
/** Generated art under `/images/` renders only through `Mascot` or inside a `MediaFrame`. */
|
|
81
|
-
export const generatedArtInMascotOrMediaFrame = defineRule({
|
|
81
|
+
export const generatedArtInMascotOrMediaFrame = exemptFreestyle(defineRule({
|
|
82
82
|
meta: {
|
|
83
83
|
type: "problem",
|
|
84
84
|
docs: { description: "R338: generated art (a .webp or .png under /images/) renders only through Grammar's Mascot or inside a MediaFrame." },
|
|
@@ -108,7 +108,7 @@ export const generatedArtInMascotOrMediaFrame = defineRule({
|
|
|
108
108
|
},
|
|
109
109
|
};
|
|
110
110
|
},
|
|
111
|
-
});
|
|
111
|
+
}));
|
|
112
112
|
/** True when an attribute names nothing: no value, an empty string, or `undefined`. */
|
|
113
113
|
const namesNothing = (attribute) => {
|
|
114
114
|
const value = attribute.value;
|
package/dist/tokens.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { classTextVisitors } from "@starci/eslint-canon-fe/lib/class-text.mjs";
|
|
5
5
|
import { hfsOf } from "@starci/eslint-canon-fe/lib/hfs.mjs";
|
|
6
6
|
import { classOf, isProductSource } from "@starci/eslint-canon-fe/lib/scope.mjs";
|
|
7
|
-
import { defineFamilyRules, defineRule } from "./lib/rule-contract.js";
|
|
7
|
+
import { defineFamilyRules, defineRule, exemptFreestyle } from "./lib/rule-contract.js";
|
|
8
8
|
/**
|
|
9
9
|
* A fractional step, in any family that measures.
|
|
10
10
|
*
|
|
@@ -17,7 +17,7 @@ const LARGE_TEXT = /\btext-(?:xl|2xl|3xl|4xl|5xl)\b/;
|
|
|
17
17
|
/** A heavy weight. */
|
|
18
18
|
const HEAVY_WEIGHT = /\bfont-(?:bold|extrabold|black)\b/;
|
|
19
19
|
/** A fractional step is off the ladder, not between two rungs. */
|
|
20
|
-
export const noFractionalStep = defineRule({
|
|
20
|
+
export const noFractionalStep = exemptFreestyle(defineRule({
|
|
21
21
|
meta: {
|
|
22
22
|
type: "problem",
|
|
23
23
|
docs: { description: "A measurement takes a whole rung; a fractional step is off the scale." },
|
|
@@ -35,7 +35,7 @@ export const noFractionalStep = defineRule({
|
|
|
35
35
|
context.report({ node, messageId: "fractional", data: { cls: hit[0] } });
|
|
36
36
|
});
|
|
37
37
|
},
|
|
38
|
-
});
|
|
38
|
+
}));
|
|
39
39
|
/** Large text plus a heavy weight is a heading, and headings come from the component that owns both. */
|
|
40
40
|
export const noHandRolledHeading = defineRule({
|
|
41
41
|
meta: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/eslint-grammar-core",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
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.
|
|
44
|
+
"@starci/eslint-canon-fe": "9.6.3",
|
|
45
45
|
"eslint": ">=9",
|
|
46
46
|
"typescript": ">=5.9"
|
|
47
47
|
},
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"build": "node -e \"require('node:fs').rmSync('dist', {recursive: true, force: true})\" && tsc -p tsconfig.build.json && node -e \"const fs = require('node:fs'); for (const f of fs.readdirSync('dist', {recursive: true}).filter((n) => n.endsWith('.d.ts'))) fs.writeFileSync('dist/' + f, fs.readFileSync('dist/' + f, 'utf8').replace(/((?:from |import\\()['\\x22]\\.\\.?\\/[^'\\x22]+)\\.ts(['\\x22])/g, (all, head, quote) => head + '.js' + quote))\"",
|
|
56
56
|
"typecheck": "tsc -p tsconfig.json",
|
|
57
57
|
"prepack": "npm run build",
|
|
58
|
-
"test": "npm run typecheck && node --test \"src/**/*.spec.ts\""
|
|
58
|
+
"test": "npm run typecheck && node --import ./fixtures/fs-audit-preload.ts --test \"src/**/*.spec.ts\""
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@starci/eslint-canon-fe": "file:../eslint-canon-fe",
|
|
@@ -357,8 +357,8 @@ guidance:
|
|
|
357
357
|
NumberField, DateField, TimeField, Select, ComboBox, DatePicker, DateRangePicker, Checkbox,
|
|
358
358
|
CheckboxGroup, RadioGroup, OtpInput and PressableField resolve variant="secondary" inside it and
|
|
359
359
|
variant="primary" outside. Compose the surface and omit the field variant.
|
|
360
|
-
Switch and Slider have no layer variant; a Radio follows its RadioGroup.
|
|
361
|
-
|
|
360
|
+
Switch and Slider have no layer variant; a Radio follows its RadioGroup. PaneGrid's
|
|
361
|
+
collapsed-pane drawer also establishes the context. SurfaceCard and bounded SurfaceAccordionCard derive
|
|
362
362
|
data-grammar-surface-depth from the parent context. The top layer is raised and a nested layer is
|
|
363
363
|
subordinate; never raise both. The family DNA owns their surface paint.
|
|
364
364
|
FE_NESTED_VARIANT rejects a static field variant
|
|
@@ -597,23 +597,17 @@ presentation:
|
|
|
597
597
|
Retained ownership reference, not proof of the current package/API. Verify actual installed source/props/tokens before use; correct outdated rows within selected maintenance scope using [source and maintenance](index.yaml#source-and-maintenance).
|
|
598
598
|
A part that carries one rule per side or per property names that CSS property after the part (`item padding-inline`); one part and property has one rule.
|
|
599
599
|
rows:
|
|
600
|
-
- component:
|
|
601
|
-
element: drawer body,
|
|
600
|
+
- component: PaneGrid
|
|
601
|
+
element: drawer body, collapsing pane
|
|
602
602
|
rule: PADDING-0
|
|
603
|
-
- component:
|
|
604
|
-
element:
|
|
605
|
-
rule: PADDING-4
|
|
606
|
-
- component: ChatWorkspace
|
|
607
|
-
element: overlay rail, hasRail, not isCompactRail
|
|
608
|
-
rule: PADDING-4
|
|
609
|
-
- component: ChatWorkspace
|
|
610
|
-
element: rail trigger boundary padding-block, hasRail, isCompactRail
|
|
603
|
+
- component: PaneGrid
|
|
604
|
+
element: trigger bar padding-block, collapsing pane
|
|
611
605
|
rule: PADDING-2
|
|
612
|
-
- component:
|
|
613
|
-
element:
|
|
606
|
+
- component: PaneGrid
|
|
607
|
+
element: trigger bar padding-inline, collapsing pane
|
|
614
608
|
rule: PADDING-4
|
|
615
|
-
- component:
|
|
616
|
-
element:
|
|
609
|
+
- component: PaneGrid
|
|
610
|
+
element: drawer trigger, collapsing pane
|
|
617
611
|
rule: PADDING-3
|
|
618
612
|
- component: EmptyNotice
|
|
619
613
|
element: root
|
|
@@ -959,8 +953,8 @@ presentation:
|
|
|
959
953
|
source/props/tokens before use; correct outdated rows within selected maintenance scope using [source
|
|
960
954
|
and maintenance](index.yaml#source-and-maintenance).
|
|
961
955
|
rows:
|
|
962
|
-
- component:
|
|
963
|
-
element:
|
|
956
|
+
- component: PaneGrid
|
|
957
|
+
element: drawer trigger, collapsing pane
|
|
964
958
|
rule: FONT-2
|
|
965
959
|
- component: Heading
|
|
966
960
|
element: root, scale!="display", level=1
|
|
@@ -31,42 +31,42 @@ 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.
|
|
34
|
+
version: 0.16.3
|
|
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.
|
|
41
|
+
version: 3.9.3
|
|
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.
|
|
48
|
+
version: 9.6.3
|
|
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.
|
|
55
|
+
version: 0.16.3
|
|
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.
|
|
62
|
+
version: 0.16.3
|
|
63
63
|
group: starci
|
|
64
64
|
install: registry
|
|
65
65
|
side: fe
|
|
66
66
|
source: packages/eslint-grammar-offset-pop/package.json
|
|
67
67
|
why: 'the Offset Pop Grammar component API lint, versioned with @starci/grammar; its shared component contract comes from the StarCi family lint without copying rules.'
|
|
68
68
|
'@starci/stylelint-canon':
|
|
69
|
-
version: 2.
|
|
69
|
+
version: 2.3.0
|
|
70
70
|
group: starci
|
|
71
71
|
install: registry
|
|
72
72
|
side: fe
|
|
@@ -85,7 +85,7 @@ pins:
|
|
|
85
85
|
side: both
|
|
86
86
|
source: packages/prettier-config/package.json
|
|
87
87
|
'@starci/jest-preset':
|
|
88
|
-
version: 2.
|
|
88
|
+
version: 2.6.0
|
|
89
89
|
group: starci
|
|
90
90
|
install: registry
|
|
91
91
|
side: be
|
|
@@ -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.
|
|
109
|
+
version: 4.13.0
|
|
110
110
|
group: starci
|
|
111
111
|
install: registry
|
|
112
112
|
side: both
|
|
@@ -5979,3 +5979,49 @@ rules:
|
|
|
5979
5979
|
source: owner
|
|
5980
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
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."
|
|
5982
|
+
- id: R341
|
|
5983
|
+
code: "FE_FREESTYLE_REASON_MISSING"
|
|
5984
|
+
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."
|
|
5985
|
+
kinds: [lint]
|
|
5986
|
+
gates: [pre-commit, settle, land, ci]
|
|
5987
|
+
failureCodes: ["FE_FREESTYLE_REASON_MISSING"]
|
|
5988
|
+
enforcers:
|
|
5989
|
+
- {kind: eslint-grammar-core, id: freestyle-has-reason}
|
|
5990
|
+
editions: [lite, full]
|
|
5991
|
+
docs: [knowledge/patterns/fe/ui.yaml#FE-UI]
|
|
5992
|
+
templateNa: "A template writes no Freestyle region; the app adds one only when no Grammar component fits."
|
|
5993
|
+
proves:
|
|
5994
|
+
- packages/eslint-grammar-core/src/freestyle.spec.ts
|
|
5995
|
+
exemplifiedBy:
|
|
5996
|
+
- {path: packages/eslint-grammar-core/src/freestyle.spec.ts, editions: [lite, full], codes: [FE_FREESTYLE_REASON_MISSING]}
|
|
5997
|
+
witness:
|
|
5998
|
+
- {path: packages/eslint-grammar-core/src/freestyle.spec.ts, editions: [lite, full], codes: [FE_FREESTYLE_REASON_MISSING]}
|
|
5999
|
+
cases:
|
|
6000
|
+
- id: case-1
|
|
6001
|
+
date: "2026-10-31"
|
|
6002
|
+
source: owner
|
|
6003
|
+
quote: "cái trang này free style đi trò div và làm bth thôi, thêm 1 component Freestyle hoặc tên gì đó tương đương đi trò, ý là cho custom nhưng vẫm bám grammar trừ bất đắc dĩ ntn"
|
|
6004
|
+
ruling: "The Grammar has one sanctioned escape hatch, Freestyle: a region where the app writes its own plain divs and CSS because no Grammar component fits; it names its reason (the lint requires it), and the rest of the page still follows the Grammar."
|
|
6005
|
+
- id: R342
|
|
6006
|
+
code: "BE_ENTITY_EXTENDS_BASE"
|
|
6007
|
+
law: "An @Entity class does not repeat the columns every table shares: a database-set created_at or updated_at (CreateDateColumn, UpdateDateColumn, or a Column on those names, or on createdAt / updatedAt, with insert false, update false or a default) and, beside both, the generated uuid id are inherited from AbstractEntity or UuidAbstractEntity of @modules/platform/database (BE-PERSISTENCE-3, ruling entity-base). The finding names the base to extend: UuidAbstractEntity with the uuid key, AbstractEntity without it. A table whose timestamps the application sets (a plain Column), or that lacks one of them, extends nothing and maps its own; the base file platform/database/abstract.entity.ts is exempt."
|
|
6008
|
+
kinds: [lint]
|
|
6009
|
+
gates: [pre-commit, settle, land, ci]
|
|
6010
|
+
failureCodes: ["BE_ENTITY_EXTENDS_BASE"]
|
|
6011
|
+
enforcers:
|
|
6012
|
+
- {kind: eslint-be, id: entity-extends-base}
|
|
6013
|
+
editions: [lite, full]
|
|
6014
|
+
docs: [contexts/code/be/persistence.md]
|
|
6015
|
+
proves:
|
|
6016
|
+
- packages/eslint-canon-be/src/entity-base.spec.ts
|
|
6017
|
+
exemplifiedBy:
|
|
6018
|
+
- {path: packages/hfs/templates/be/table/entity.ts.tpl, editions: [lite, full]}
|
|
6019
|
+
- {path: packages/eslint-canon-be/src/entity-base.spec.ts, editions: [lite, full], codes: [BE_ENTITY_EXTENDS_BASE]}
|
|
6020
|
+
witness:
|
|
6021
|
+
- {path: packages/eslint-canon-be/src/entity-base.spec.ts, editions: [lite, full], codes: [BE_ENTITY_EXTENDS_BASE]}
|
|
6022
|
+
cases:
|
|
6023
|
+
- id: case-1
|
|
6024
|
+
date: "2026-10-31"
|
|
6025
|
+
source: owner
|
|
6026
|
+
quote: "Entities do not repeat the columns every table shares; the owner approved a lint so they never repeat them again (ruling entity-base, 2026-10-31)."
|
|
6027
|
+
ruling: "A table whose migration has created_at, updated_at (set by the database) and the generated uuid id maps them by extending AbstractEntity or UuidAbstractEntity from @modules/platform/database; an entity that declares them itself is a finding. A table without them extends nothing."
|
|
@@ -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, R340]
|
|
27
|
+
hfsRules: [R93, R244, R313, R314, R315, R316, R317, R318, R319, R320, R338, R339, R340, R341]
|
|
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.
|
|
@@ -93,5 +93,6 @@ rules:
|
|
|
93
93
|
- FE_GENERATED_ART_OUTSIDE_FRAME
|
|
94
94
|
- FE_MASCOT_ASSET_SLOT_MISSING
|
|
95
95
|
- FE_RAISED_SURFACE_IN_SURFACE
|
|
96
|
+
- FE_FREESTYLE_REASON_MISSING
|
|
96
97
|
manual: []
|
|
97
98
|
relatedExamples: [full-app-shell, lite-app-shell]
|