@starci/eslint-canon-fe 1.3.2 → 1.4.1

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/icon.mjs CHANGED
@@ -27,7 +27,8 @@ const LABELLED_PROGRESS_ROW_RELATIVE = "composites/LabelledProgressRow/index.tsx
27
27
  * Matched as a PREFIX so a subpath cannot walk around it - the escape that started this was
28
28
  * `@phosphor-icons/react/dist/ssr`, which an equality check on the package name does not see.
29
29
  */
30
- const GLYPH_PACKAGES = [
30
+ const GLYPH_PACKAGES = [
31
+ "@starci/heroicons",
31
32
  "@phosphor-icons/",
32
33
  "lucide-react",
33
34
  "react-icons",
@@ -43,10 +44,10 @@ const GLYPH_PACKAGES = [
43
44
  /** Package-name signal for a glyph catalogue not yet listed explicitly. */
44
45
  const GLYPH_PACKAGE_NAME = /(?:icon|glyph|lucide|feather|tabler|fortawesome)/i
45
46
 
46
- /** The two Heroicon families selected by the product roles. */
47
- const HEROICON_PACKAGES = new Set([
48
- "@heroicons/react/24/outline",
49
- "@heroicons/react/16/solid",
47
+ /** The two StarCi entry points that re-export approved Heroicons and reviewed product cuts. */
48
+ const HEROICON_PACKAGES = new Set([
49
+ "@starci/heroicons/24/outline",
50
+ "@starci/heroicons/16/solid",
50
51
  ])
51
52
 
52
53
  /**
@@ -166,11 +167,11 @@ export const noVendorIconOutsideIconLeaf = {
166
167
  export const heroiconsIsTheGlyphVendor = {
167
168
  meta: {
168
169
  type: "problem",
169
- docs: { description: "Only the Heroicons heading and micro families may supply product glyphs." },
170
+ docs: { description: "Only the StarCi Heroicons extension heading and micro entry points may supply product glyphs." },
170
171
  schema: [],
171
172
  messages: {
172
173
  vendor:
173
- "`{{source}}` is not one of StarCi's two glyph families. Heading and leading roles use `@heroicons/react/24/outline`; chip roles use `@heroicons/react/16/solid`. Map the meaning there instead. Brand marks are exact house SVGs. Product reactions use the attributed Fluent Emoji Flat SVG subset checked into `public/reactions/`, never another glyph import.",
174
+ "`{{source}}` is not one of StarCi's two glyph entry points. Heading and leading roles use `@starci/heroicons/24/outline`; chip roles use `@starci/heroicons/16/solid`. The package re-exports approved upstream Heroicons and owns reviewed product cuts when upstream has no faithful meaning. Map the meaning there instead. Brand marks are exact house SVGs. Product reactions use the attributed Fluent Emoji Flat SVG subset checked into `public/reactions/`, never another glyph import.",
174
175
  },
175
176
  },
176
177
  create(context) {
package/landmark.mjs CHANGED
@@ -1,242 +1,242 @@
1
- /**
2
- * Rules for the LANDMARK law - `patterns/fe/landmark/INDEX.md`.
3
- *
4
- * WHY THESE EXIST. A registry makes the failure silent. A key named `dashboard-main` records the
5
- * intent exactly and renders a `div`, because the branch that draws registry nodes draws divs. An
6
- * application shipped with every region correctly named and not one landmark in the document, and
7
- * no gate had anything to say: a name in a key is not an element in a document.
8
- *
9
- * WHAT THEY HOLD, as two halves of one idea. A route layout that composes its own chrome must hand
10
- * the routed children to the landmark branch; and that branch may appear only in a ROUTE file,
11
- * because a second `main` is an ambiguous landmark rather than a stronger one.
12
- *
13
- * WHAT THEY CANNOT HOLD, stated rather than implied. A file-at-a-time rule cannot see that a layout
14
- * and a page under it BOTH opened a landmark. The rules narrow the place to route files and refuse
15
- * every tier below, which is where the mistake actually happens - a reading column named `*-main`
16
- * drawn with the landmark branch. The remaining cross-file case is a review question, and pretending
17
- * otherwise would be the rule claiming a guarantee it does not have.
18
- *
19
- * WHY THE `Tree` TEST IS PART OF THE FIRST RULE. Without it the rule fires on every layout that
20
- * touches `children`, including the root document shell and pass-through layouts that delegate
21
- * their chrome - and satisfying it there would put the second `main` in the document that the
22
- * companion rule refuses. The bluntness was real, was measured, and is the reason the condition is
23
- * narrower than "renders children".
24
- */
25
-
26
- import { contractHostOf } from "./contract.mjs"
27
-
28
- /** Forward-slash form of a filename, so Windows paths compare like every other path. */
29
- const normalize = (filename) => String(filename || "").replace(/\\/g, "/")
30
-
31
- /** A route layout: `layout.tsx` anywhere under an `app/` router directory. */
32
- export const isRouteLayoutFile = (filename) => /\/app\/(?:.*\/)?layout\.tsx$/.test(normalize(filename))
33
-
34
- /**
35
- * A route file: the layout OR the page of a route.
36
- *
37
- * WHY BOTH. The landmark belongs to the outermost file that composes the routed screen, and that is
38
- * not always a layout. A route with no chrome-composing layout - a scaffold, a standalone screen -
39
- * has its page as the outermost composer, and forcing the mark upward would mean inventing a layout
40
- * whose only job is to wrap. That was not a hypothetical: an early version of this rule allowed only
41
- * layouts, and it refused three route pages that were each the sole composer of their screen and
42
- * each held exactly one `main`.
43
- */
44
- export const isRouteFile = (filename) => /\/app\/(?:.*\/)?(?:layout|page)\.tsx$/.test(normalize(filename))
45
-
46
- /**
47
- * A page tier file: either half of a surface under `components/pages/<Name>/`.
48
- *
49
- * WHY THE PAGE TIER OWNS THE MARK TOO, AND WHY THIS WAS NOT ALWAYS TRUE.
50
- *
51
- * The landmark used to be a named branch, so "the outermost file that composes the screen" and "a
52
- * file under `app/`" were the same sentence — the branch was imported into the route file and that
53
- * was that. Then the mark moved into the contract entry as `host: "main"`, and the two sentences
54
- * came apart: the entry is rendered by whoever renders the screen's outermost frame, and FILE-6
55
- * says that is emphatically NOT the route file. A route names which page renders at which URL and
56
- * draws nothing.
57
- *
58
- * Held to route files only, the two laws then refused each other. Every page moved out of `app/` to
59
- * satisfy FILE-6 was reported for misplacing its landmark, and the only way to satisfy both at
60
- * once was to leave the page owner in the routing tree — which is the defect FILE-6 exists to
61
- * prevent. A rule that can only be satisfied by breaking another rule is a finding about the rule.
62
- *
63
- * What the guarantee actually was, and still is: ONE main per document, owned by whoever owns the
64
- * whole screen. A route file and a page component are both that. A block, composite, leaf, branch,
65
- * overlay or shared layout is not, and each of those still reports — which is the case this rule was
66
- * written for, because a landmark added there is a SECOND main rather than a stronger one.
67
- */
68
- export const isPageTierFile = (filename) =>
69
- /\/components\/pages\/[A-Z][A-Za-z0-9]*\/(?:index|component)\.tsx$/.test(normalize(filename))
70
-
71
- /** The branches that open a landmark element. One per element, by LANDMARK-1. */
72
- export const LANDMARK_BRANCHES = new Set(["Main"])
73
-
74
- /** The branch that composes an ordinary registry node, and therefore marks a layout as chrome-owning. */
75
- const TREE_BRANCH = "Tree"
76
-
77
- /**
78
- * The contract key a frame element is rendering, when it names one literally.
79
- *
80
- * @param node - A JSX opening element.
81
- * @returns The key, or null when the element takes no literal `contract`.
82
- */
83
- const contractKeyOf = (node) => {
84
- const attribute = (node.attributes || []).find(
85
- (candidate) => candidate.type === "JSXAttribute" && candidate.name && candidate.name.name === "contract",
86
- )
87
- const value = attribute && attribute.value
88
- return value && value.type === "Literal" && typeof value.value === "string" ? value.value : null
89
- }
90
-
91
- /**
92
- * True when this element draws a landmark.
93
- *
94
- * TWO SHAPES, AND THE SECOND IS THE ONE THAT LASTS. A landmark used to be a named branch, so this
95
- * asked for the name. The element now belongs to the contract entry - `host: "main"` - which is
96
- * why a repository can delete its `Main` branch entirely and still have exactly one main landmark:
97
- * the frame reads the host from the key. Asking only for the branch name reported those repositories
98
- * as having no landmark at all, which is the opposite of what they had done.
99
- *
100
- * @param node - A JSX opening element.
101
- * @param filename - The file being linted, used to locate the contract table.
102
- */
103
- const isLandmarkElement = (node, filename) =>
104
- isLandmarkBranchElement(node) || isHostMainFrame(node, filename)
105
-
106
- /**
107
- * True when this element is a landmark BRANCH, named directly.
108
- *
109
- * @param node - A JSX opening element.
110
- */
111
- const isLandmarkBranchElement = (node) =>
112
- node.type === "JSXOpeningElement"
113
- && node.name
114
- && node.name.type === "JSXIdentifier"
115
- && LANDMARK_BRANCHES.has(node.name.name)
116
-
117
- /**
118
- * True when this element is an ordinary frame whose CONTRACT ENTRY declares the landmark.
119
- *
120
- * @param node - A JSX opening element.
121
- * @param filename - The file being linted, used to locate the contract table.
122
- */
123
- const isHostMainFrame = (node, filename) => {
124
- if (node.type !== "JSXOpeningElement" || !node.name || node.name.type !== "JSXIdentifier") return false
125
- if (LANDMARK_BRANCHES.has(node.name.name)) return false
126
- const key = contractKeyOf(node)
127
- return key !== null && contractHostOf(filename, key) === "main"
128
- }
129
-
130
- /**
131
- * True when this file is a landmark branch's own implementation.
132
- *
133
- * That file draws the element by hand and must: it is the one place the element is allowed to
134
- * appear, exactly as `Tree` is the one place a `div` is.
135
- *
136
- * @param filename - The file being linted.
137
- */
138
- const isLandmarkBranchFile = (filename) =>
139
- [...LANDMARK_BRANCHES].some((name) => new RegExp(`/branches/${name}/`).test(normalize(filename)))
140
-
141
- export const rules = {
142
- "routed-page-is-a-main-landmark": {
143
- meta: {
144
- type: "problem",
145
- docs: {
146
- description:
147
- "A route layout that composes its own chrome must wrap the routed children in the Main branch, so the page is a landmark a reader can skip to.",
148
- },
149
- schema: [],
150
- messages: {
151
- missing:
152
- "This layout composes the page chrome but never marks the routed page. Wrap `children` in the `Main` branch so assistive technology can skip the navigation to reach the page - a registry key named `*-main` is a name, not a landmark.",
153
- },
154
- },
155
- create(context) {
156
- const filename = context.filename || context.getFilename()
157
- if (!isRouteLayoutFile(filename)) return {}
158
- let rendersChildren = false
159
- let composesChrome = false
160
- let usesLandmark = false
161
- let firstNode = null
162
- return {
163
- JSXExpressionContainer(node) {
164
- if (node.expression && node.expression.type === "Identifier" && node.expression.name === "children") {
165
- rendersChildren = true
166
- firstNode = firstNode || node
167
- }
168
- },
169
- Identifier(node) {
170
- // `children` reaches the tree through a slot callback as often as through JSX, so the
171
- // bare identifier counts too - otherwise a layout escapes by handing it to a builder.
172
- if (node.name === "children" && node.parent && node.parent.type !== "Property") {
173
- rendersChildren = true
174
- firstNode = firstNode || node
175
- }
176
- },
177
- JSXOpeningElement(node) {
178
- if (isLandmarkElement(node, filename)) usesLandmark = true
179
- if (node.name && node.name.type === "JSXIdentifier" && node.name.name === TREE_BRANCH) {
180
- composesChrome = true
181
- firstNode = firstNode || node
182
- }
183
- },
184
- "Program:exit"(node) {
185
- if (rendersChildren && composesChrome && !usesLandmark) {
186
- context.report({ node: firstNode || node, messageId: "missing" })
187
- }
188
- },
189
- }
190
- },
191
- },
192
-
193
- "main-landmark-belongs-to-a-route-file": {
194
- meta: {
195
- type: "problem",
196
- docs: {
197
- description:
198
- "The Main branch draws the document's one main landmark, so it belongs to the route file that composes the screen and nowhere below it.",
199
- },
200
- schema: [],
201
- messages: {
202
- misplaced:
203
- "This draws the document's ONE main landmark, so it belongs to whoever owns the whole screen - the route's `layout.tsx` or `page.tsx`, or the page surface under `components/pages/<Name>/`. Used here it puts a second `main` in the document, which is an ambiguous landmark rather than a stronger one - a registry key named `*-main` inside a block or composite is a reading column, and its entry must not declare `host: \"main\"`.",
204
- },
205
- },
206
- /*
207
- * TWO SHAPES, TWO DIFFERENT SETS OF FILES THAT MAY HOLD THEM, and collapsing them was the bug.
208
- *
209
- * The `Main` BRANCH is a component somebody imports in order to wrap a screen. It belongs to the
210
- * route file that composes that screen and nowhere below - a page reaching for it is the trap
211
- * this law was written for, because the thing it wraps is a reading COLUMN and the document then
212
- * has two mains.
213
- *
214
- * A frame whose CONTRACT ENTRY declares `host: "main"` is not that. Nobody imported a landmark;
215
- * the registry says which element this key's node opens, and the frame obeys. That entry is
216
- * rendered by whoever renders the screen's outermost node - and FILE-6 says the route file is
217
- * emphatically not it, because a route mounts a page and draws nothing. Holding both shapes to
218
- * route files only made the two laws refuse each other, and the only way to satisfy both was to
219
- * leave the page owner in the routing tree, which is the defect FILE-6 exists to prevent.
220
- */
221
- create(context) {
222
- const filename = context.filename || context.getFilename()
223
- if (isLandmarkBranchFile(filename)) return {}
224
- const isRoute = isRouteFile(filename)
225
- const isPageTier = isPageTierFile(filename)
226
- return {
227
- JSXOpeningElement(node) {
228
- if (isLandmarkBranchElement(node)) {
229
- if (!isRoute) context.report({ node, messageId: "misplaced" })
230
- return
231
- }
232
- if (isHostMainFrame(node, filename) && !isRoute && !isPageTier) {
233
- context.report({ node, messageId: "misplaced" })
234
- }
235
- },
236
- }
237
- },
238
- },
239
- }
240
-
241
- /** Every FE canon rule is an error; see `sources/fe/index.mjs` for why there is no warn tier. */
242
- export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
1
+ /**
2
+ * Rules for the LANDMARK law - `fe/canon/patterns/landmark.md`.
3
+ *
4
+ * WHY THESE EXIST. A registry makes the failure silent. A key named `dashboard-main` records the
5
+ * intent exactly and renders a `div`, because the branch that draws registry nodes draws divs. An
6
+ * application shipped with every region correctly named and not one landmark in the document, and
7
+ * no gate had anything to say: a name in a key is not an element in a document.
8
+ *
9
+ * WHAT THEY HOLD, as two halves of one idea. A route layout that composes its own chrome must hand
10
+ * the routed children to the landmark branch; and that branch may appear only in a ROUTE file,
11
+ * because a second `main` is an ambiguous landmark rather than a stronger one.
12
+ *
13
+ * WHAT THEY CANNOT HOLD, stated rather than implied. A file-at-a-time rule cannot see that a layout
14
+ * and a page under it BOTH opened a landmark. The rules narrow the place to route files and refuse
15
+ * every tier below, which is where the mistake actually happens - a reading column named `*-main`
16
+ * drawn with the landmark branch. The remaining cross-file case is a review question, and pretending
17
+ * otherwise would be the rule claiming a guarantee it does not have.
18
+ *
19
+ * WHY THE `Tree` TEST IS PART OF THE FIRST RULE. Without it the rule fires on every layout that
20
+ * touches `children`, including the root document shell and pass-through layouts that delegate
21
+ * their chrome - and satisfying it there would put the second `main` in the document that the
22
+ * companion rule refuses. The bluntness was real, was measured, and is the reason the condition is
23
+ * narrower than "renders children".
24
+ */
25
+
26
+ import { contractHostOf } from "./contract.mjs"
27
+
28
+ /** Forward-slash form of a filename, so Windows paths compare like every other path. */
29
+ const normalize = (filename) => String(filename || "").replace(/\\/g, "/")
30
+
31
+ /** A route layout: `layout.tsx` anywhere under an `app/` router directory. */
32
+ export const isRouteLayoutFile = (filename) => /\/app\/(?:.*\/)?layout\.tsx$/.test(normalize(filename))
33
+
34
+ /**
35
+ * A route file: the layout OR the page of a route.
36
+ *
37
+ * WHY BOTH. The landmark belongs to the outermost file that composes the routed screen, and that is
38
+ * not always a layout. A route with no chrome-composing layout - a scaffold, a standalone screen -
39
+ * has its page as the outermost composer, and forcing the mark upward would mean inventing a layout
40
+ * whose only job is to wrap. That was not a hypothetical: an early version of this rule allowed only
41
+ * layouts, and it refused three route pages that were each the sole composer of their screen and
42
+ * each held exactly one `main`.
43
+ */
44
+ export const isRouteFile = (filename) => /\/app\/(?:.*\/)?(?:layout|page)\.tsx$/.test(normalize(filename))
45
+
46
+ /**
47
+ * A page tier file: either half of a surface under `components/pages/<Name>/`.
48
+ *
49
+ * WHY THE PAGE TIER OWNS THE MARK TOO, AND WHY THIS WAS NOT ALWAYS TRUE.
50
+ *
51
+ * The landmark used to be a named branch, so "the outermost file that composes the screen" and "a
52
+ * file under `app/`" were the same sentence — the branch was imported into the route file and that
53
+ * was that. Then the mark moved into the contract entry as `host: "main"`, and the two sentences
54
+ * came apart: the entry is rendered by whoever renders the screen's outermost frame, and FILE-6
55
+ * says that is emphatically NOT the route file. A route names which page renders at which URL and
56
+ * draws nothing.
57
+ *
58
+ * Held to route files only, the two laws then refused each other. Every page moved out of `app/` to
59
+ * satisfy FILE-6 was reported for misplacing its landmark, and the only way to satisfy both at
60
+ * once was to leave the page owner in the routing tree — which is the defect FILE-6 exists to
61
+ * prevent. A rule that can only be satisfied by breaking another rule is a finding about the rule.
62
+ *
63
+ * What the guarantee actually was, and still is: ONE main per document, owned by whoever owns the
64
+ * whole screen. A route file and a page component are both that. A block, composite, leaf, branch,
65
+ * overlay or shared layout is not, and each of those still reports — which is the case this rule was
66
+ * written for, because a landmark added there is a SECOND main rather than a stronger one.
67
+ */
68
+ export const isPageTierFile = (filename) =>
69
+ /\/components\/pages\/[A-Z][A-Za-z0-9]*\/(?:index|component)\.tsx$/.test(normalize(filename))
70
+
71
+ /** The branches that open a landmark element. One per element, by LANDMARK-1. */
72
+ export const LANDMARK_BRANCHES = new Set(["Main"])
73
+
74
+ /** The branch that composes an ordinary registry node, and therefore marks a layout as chrome-owning. */
75
+ const TREE_BRANCH = "Tree"
76
+
77
+ /**
78
+ * The contract key a frame element is rendering, when it names one literally.
79
+ *
80
+ * @param node - A JSX opening element.
81
+ * @returns The key, or null when the element takes no literal `contract`.
82
+ */
83
+ const contractKeyOf = (node) => {
84
+ const attribute = (node.attributes || []).find(
85
+ (candidate) => candidate.type === "JSXAttribute" && candidate.name && candidate.name.name === "contract",
86
+ )
87
+ const value = attribute && attribute.value
88
+ return value && value.type === "Literal" && typeof value.value === "string" ? value.value : null
89
+ }
90
+
91
+ /**
92
+ * True when this element draws a landmark.
93
+ *
94
+ * TWO SHAPES, AND THE SECOND IS THE ONE THAT LASTS. A landmark used to be a named branch, so this
95
+ * asked for the name. The element now belongs to the contract entry - `host: "main"` - which is
96
+ * why a repository can delete its `Main` branch entirely and still have exactly one main landmark:
97
+ * the frame reads the host from the key. Asking only for the branch name reported those repositories
98
+ * as having no landmark at all, which is the opposite of what they had done.
99
+ *
100
+ * @param node - A JSX opening element.
101
+ * @param filename - The file being linted, used to locate the contract table.
102
+ */
103
+ const isLandmarkElement = (node, filename) =>
104
+ isLandmarkBranchElement(node) || isHostMainFrame(node, filename)
105
+
106
+ /**
107
+ * True when this element is a landmark BRANCH, named directly.
108
+ *
109
+ * @param node - A JSX opening element.
110
+ */
111
+ const isLandmarkBranchElement = (node) =>
112
+ node.type === "JSXOpeningElement"
113
+ && node.name
114
+ && node.name.type === "JSXIdentifier"
115
+ && LANDMARK_BRANCHES.has(node.name.name)
116
+
117
+ /**
118
+ * True when this element is an ordinary frame whose CONTRACT ENTRY declares the landmark.
119
+ *
120
+ * @param node - A JSX opening element.
121
+ * @param filename - The file being linted, used to locate the contract table.
122
+ */
123
+ const isHostMainFrame = (node, filename) => {
124
+ if (node.type !== "JSXOpeningElement" || !node.name || node.name.type !== "JSXIdentifier") return false
125
+ if (LANDMARK_BRANCHES.has(node.name.name)) return false
126
+ const key = contractKeyOf(node)
127
+ return key !== null && contractHostOf(filename, key) === "main"
128
+ }
129
+
130
+ /**
131
+ * True when this file is a landmark branch's own implementation.
132
+ *
133
+ * That file draws the element by hand and must: it is the one place the element is allowed to
134
+ * appear, exactly as `Tree` is the one place a `div` is.
135
+ *
136
+ * @param filename - The file being linted.
137
+ */
138
+ const isLandmarkBranchFile = (filename) =>
139
+ [...LANDMARK_BRANCHES].some((name) => new RegExp(`/branches/${name}/`).test(normalize(filename)))
140
+
141
+ export const rules = {
142
+ "routed-page-is-a-main-landmark": {
143
+ meta: {
144
+ type: "problem",
145
+ docs: {
146
+ description:
147
+ "A route layout that composes its own chrome must wrap the routed children in the Main branch, so the page is a landmark a reader can skip to.",
148
+ },
149
+ schema: [],
150
+ messages: {
151
+ missing:
152
+ "This layout composes the page chrome but never marks the routed page. Wrap `children` in the `Main` branch so assistive technology can skip the navigation to reach the page - a registry key named `*-main` is a name, not a landmark.",
153
+ },
154
+ },
155
+ create(context) {
156
+ const filename = context.filename || context.getFilename()
157
+ if (!isRouteLayoutFile(filename)) return {}
158
+ let rendersChildren = false
159
+ let composesChrome = false
160
+ let usesLandmark = false
161
+ let firstNode = null
162
+ return {
163
+ JSXExpressionContainer(node) {
164
+ if (node.expression && node.expression.type === "Identifier" && node.expression.name === "children") {
165
+ rendersChildren = true
166
+ firstNode = firstNode || node
167
+ }
168
+ },
169
+ Identifier(node) {
170
+ // `children` reaches the tree through a slot callback as often as through JSX, so the
171
+ // bare identifier counts too - otherwise a layout escapes by handing it to a builder.
172
+ if (node.name === "children" && node.parent && node.parent.type !== "Property") {
173
+ rendersChildren = true
174
+ firstNode = firstNode || node
175
+ }
176
+ },
177
+ JSXOpeningElement(node) {
178
+ if (isLandmarkElement(node, filename)) usesLandmark = true
179
+ if (node.name && node.name.type === "JSXIdentifier" && node.name.name === TREE_BRANCH) {
180
+ composesChrome = true
181
+ firstNode = firstNode || node
182
+ }
183
+ },
184
+ "Program:exit"(node) {
185
+ if (rendersChildren && composesChrome && !usesLandmark) {
186
+ context.report({ node: firstNode || node, messageId: "missing" })
187
+ }
188
+ },
189
+ }
190
+ },
191
+ },
192
+
193
+ "main-landmark-belongs-to-a-route-file": {
194
+ meta: {
195
+ type: "problem",
196
+ docs: {
197
+ description:
198
+ "The Main branch draws the document's one main landmark, so it belongs to the route file that composes the screen and nowhere below it.",
199
+ },
200
+ schema: [],
201
+ messages: {
202
+ misplaced:
203
+ "This draws the document's ONE main landmark, so it belongs to whoever owns the whole screen - the route's `layout.tsx` or `page.tsx`, or the page surface under `components/pages/<Name>/`. Used here it puts a second `main` in the document, which is an ambiguous landmark rather than a stronger one - a registry key named `*-main` inside a block or composite is a reading column, and its entry must not declare `host: \"main\"`.",
204
+ },
205
+ },
206
+ /*
207
+ * TWO SHAPES, TWO DIFFERENT SETS OF FILES THAT MAY HOLD THEM, and collapsing them was the bug.
208
+ *
209
+ * The `Main` BRANCH is a component somebody imports in order to wrap a screen. It belongs to the
210
+ * route file that composes that screen and nowhere below - a page reaching for it is the trap
211
+ * this law was written for, because the thing it wraps is a reading COLUMN and the document then
212
+ * has two mains.
213
+ *
214
+ * A frame whose CONTRACT ENTRY declares `host: "main"` is not that. Nobody imported a landmark;
215
+ * the registry says which element this key's node opens, and the frame obeys. That entry is
216
+ * rendered by whoever renders the screen's outermost node - and FILE-6 says the route file is
217
+ * emphatically not it, because a route mounts a page and draws nothing. Holding both shapes to
218
+ * route files only made the two laws refuse each other, and the only way to satisfy both was to
219
+ * leave the page owner in the routing tree, which is the defect FILE-6 exists to prevent.
220
+ */
221
+ create(context) {
222
+ const filename = context.filename || context.getFilename()
223
+ if (isLandmarkBranchFile(filename)) return {}
224
+ const isRoute = isRouteFile(filename)
225
+ const isPageTier = isPageTierFile(filename)
226
+ return {
227
+ JSXOpeningElement(node) {
228
+ if (isLandmarkBranchElement(node)) {
229
+ if (!isRoute) context.report({ node, messageId: "misplaced" })
230
+ return
231
+ }
232
+ if (isHostMainFrame(node, filename) && !isRoute && !isPageTier) {
233
+ context.report({ node, messageId: "misplaced" })
234
+ }
235
+ },
236
+ }
237
+ },
238
+ },
239
+ }
240
+
241
+ /** Every FE canon rule is an error; see `sources/fe/index.mjs` for why there is no warn tier. */
242
+ export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))