@starci/eslint-canon-fe 1.0.1 → 1.2.0

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/index.mjs CHANGED
@@ -90,6 +90,23 @@ export const ruleOwners = Object.fromEntries(
90
90
  CONTRIBUTIONS.flatMap((entry) => Object.keys(entry.rules).map((name) => [name, entry.law])),
91
91
  )
92
92
 
93
+ /**
94
+ * Every (law, rule name) pair as DECLARED, before any collapsing.
95
+ *
96
+ * `ruleOwners` above and `rules` below are both built with `Object.fromEntries`, which silently
97
+ * keeps the last writer when two laws declare one name. A guard that walks either of them cannot
98
+ * see a collision - the duplicate is gone before it looks, so every name appears exactly once BY
99
+ * CONSTRUCTION and the check passes forever while being blind.
100
+ *
101
+ * That is not hypothetical. The back-end twin had this exact shape, three rules were declared by two
102
+ * laws each, one copy was discarded on import, and its guard reported green throughout. This axis
103
+ * has no collision today; publishing the raw declarations is what keeps that a FACT rather than an
104
+ * assumption nobody can test.
105
+ */
106
+ export const ruleDeclarations = CONTRIBUTIONS.flatMap((entry) =>
107
+ Object.keys(entry.rules).map((name) => ({ law: entry.law, name })),
108
+ )
109
+
93
110
  /** Every rule this canon publishes, keyed by its published name. */
94
111
  export const rules = Object.fromEntries(
95
112
  CONTRIBUTIONS.flatMap((entry) => Object.entries(entry.rules)),
package/landmark.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Rules for the LANDMARK law - `fe/canon/patterns/landmark.md`.
2
+ * Rules for the LANDMARK law - `patterns/fe/landmark/INDEX.md`.
3
3
  *
4
4
  * WHY THESE EXIST. A registry makes the failure silent. A key named `dashboard-main` records the
5
5
  * intent exactly and renders a `div`, because the branch that draws registry nodes draws divs. An
@@ -51,13 +51,13 @@ export const isRouteFile = (filename) => /\/app\/(?:.*\/)?(?:layout|page)\.tsx$/
51
51
  * The landmark used to be a named branch, so "the outermost file that composes the screen" and "a
52
52
  * file under `app/`" were the same sentence — the branch was imported into the route file and that
53
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 LAYOUT-6
54
+ * came apart: the entry is rendered by whoever renders the screen's outermost frame, and FILE-6
55
55
  * says that is emphatically NOT the route file. A route names which page renders at which URL and
56
56
  * draws nothing.
57
57
  *
58
58
  * Held to route files only, the two laws then refused each other. Every page moved out of `app/` to
59
- * satisfy LAYOUT-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 LAYOUT-6 exists 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
61
  * prevent. A rule that can only be satisfied by breaking another rule is a finding about the rule.
62
62
  *
63
63
  * What the guarantee actually was, and still is: ONE main per document, owned by whoever owns the
@@ -213,10 +213,10 @@ export const rules = {
213
213
  *
214
214
  * A frame whose CONTRACT ENTRY declares `host: "main"` is not that. Nobody imported a landmark;
215
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 LAYOUT-6 says the route file is
216
+ * rendered by whoever renders the screen's outermost node - and FILE-6 says the route file is
217
217
  * emphatically not it, because a route mounts a page and draws nothing. Holding both shapes to
218
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 LAYOUT-6 exists to prevent.
219
+ * leave the page owner in the routing tree, which is the defect FILE-6 exists to prevent.
220
220
  */
221
221
  create(context) {
222
222
  const filename = context.filename || context.getFilename()
package/lint-adoption.mjs CHANGED
@@ -1,116 +1,116 @@
1
- /**
2
- * Repository-level audit for effective StarCi FE lint adoption.
3
- *
4
- * An ESLint config can import a plugin, copy a plugin, or call itself StarCi while still leaving
5
- * canonical rules switched off. The only useful evidence is the effective config ESLint applies
6
- * to a real production file. This audit compares that printed config with this canon.
7
- */
8
- /**
9
- * Where the component tree sits, per supported repository shape.
10
- *
11
- * ONE BRANCH, IN ONE PLACE. The difference between a monorepo and a single app is not a difference
12
- * of law - the rules, their severities and the inline-config refusal are identical. It is only
13
- * WHERE the law applies. Written out in each repository's config, that fact became two hand-kept
14
- * lists, and the lists did what hand-kept lists do: one enabled 43 rules, the other 53, and canon
15
- * had 49. Neither repository was wrong on purpose; nothing told either of them.
16
- *
17
- * The candidate path is included deliberately. A Preview candidate is the exact source Apply later
18
- * ports into production, and leaving it ungoverned means the one file that becomes production was
19
- * judged by nothing.
20
- */
21
- const LAYOUT_GLOBS = {
22
- monorepo: [
23
- "packages/ui/src/**/*.{ts,tsx}",
24
- "apps/*/src/**/*.{ts,tsx}",
25
- "**/candidate/src/**/*.{ts,tsx}",
26
- ],
27
- "single-app": [
28
- "src/**/*.{ts,tsx}",
29
- "**/candidate/src/**/*.{ts,tsx}",
30
- ],
31
- }
32
-
33
- /** The repository shapes this canon knows how to govern. */
34
- export const LAYOUTS = Object.keys(LAYOUT_GLOBS)
35
-
36
- /**
37
- * The one flat-config block that attaches this canon to a repository.
38
- *
39
- * A consuming `eslint.config.mjs` spreads the result and owns nothing else about the rules: not
40
- * which are on, not their severity, not whether an inline comment may switch one off. Its own
41
- * plugins, its own ignores and its own language options stay its own business - what it may not
42
- * keep is a second opinion about the law.
43
- *
44
- * @param {object} input - Attachment options.
45
- * @param {"monorepo" | "single-app"} input.layout - Which shape this repository has.
46
- * @param {object} input.plugin - The plugin object, imported from this canon.
47
- * @param {object} input.recommended - The recommendation, imported from this canon.
48
- * @param {object} input.linterOptions - The linter options, imported from this canon.
49
- * @returns {object} One flat-config block.
50
- */
51
- export const starciFeConfig = ({ layout, plugin, recommended, linterOptions }) => {
52
- const files = LAYOUT_GLOBS[layout]
53
- if (!files) {
54
- throw new Error(`unknown layout "${layout}" - expected one of: ${LAYOUTS.join(", ")}`)
55
- }
56
- /*
57
- * `recommended` is a FLAT rule map here, not a config object with a `rules` key - and reaching
58
- * for `.rules` on it is the mistake this line exists to stop repeating. Spread that way it
59
- * yields `undefined`, the block ends up with zero rules, and the repository lints clean while
60
- * being governed by nothing. That is the worst possible failure for this file: green, silent,
61
- * and indistinguishable from success. Both spellings are accepted so neither shape can produce
62
- * an empty block.
63
- */
64
- const rules = recommended?.rules ?? recommended
65
- if (!rules || Object.keys(rules).length === 0) {
66
- throw new Error("starciFeConfig received an empty recommendation - a config with no rules is not adoption")
67
- }
68
- return {
69
- files,
70
- linterOptions,
71
- plugins: { "starci-fe": plugin },
72
- rules: { ...rules },
73
- }
74
- }
75
-
76
- /** Convert every flat-config severity spelling to its numeric meaning. */
77
- const severityOf = (setting) => {
78
- const severity = Array.isArray(setting) ? setting[0] : setting
79
- if (severity === "error") return 2
80
- if (severity === "warn") return 1
81
- if (severity === "off") return 0
82
- return severity
83
- }
84
-
85
- /**
86
- * Compare an `eslint --print-config` result with the canonical recommendation.
87
- *
88
- * @param {object} printedConfig - Parsed effective ESLint config for one production probe file.
89
- * @param {Record<string, string | number | unknown[]>} expectedRules - Canonical rule levels.
90
- */
91
- export const auditLintAdoption = (printedConfig, expectedRules) => {
92
- const actualRules = printedConfig?.rules ?? {}
93
- const missing = []
94
- const nonError = []
95
-
96
- for (const name of Object.keys(expectedRules).sort()) {
97
- if (actualRules[name] === undefined) {
98
- missing.push(name)
99
- continue
100
- }
101
- if (severityOf(actualRules[name]) !== 2) nonError.push(name)
102
- }
103
-
104
- const refusesInlineConfig = printedConfig?.linterOptions?.noInlineConfig === true
105
- return {
106
- ok: missing.length === 0 && nonError.length === 0 && refusesInlineConfig,
107
- missing,
108
- nonError,
109
- refusesInlineConfig,
110
- }
111
- }
112
-
113
- /** Repository audits are gathered beside rules, but are not ESLint AST rules themselves. */
114
- export const audits = { "effective-config": auditLintAdoption }
115
- export const rules = {}
116
- export const recommended = {}
1
+ /**
2
+ * Repository-level audit for effective StarCi FE lint adoption.
3
+ *
4
+ * An ESLint config can import a plugin, copy a plugin, or call itself StarCi while still leaving
5
+ * canonical rules switched off. The only useful evidence is the effective config ESLint applies
6
+ * to a real production file. This audit compares that printed config with this canon.
7
+ */
8
+ /**
9
+ * Where the component tree sits, per supported repository shape.
10
+ *
11
+ * ONE BRANCH, IN ONE PLACE. The difference between a monorepo and a single app is not a difference
12
+ * of law - the rules, their severities and the inline-config refusal are identical. It is only
13
+ * WHERE the law applies. Written out in each repository's config, that fact became two hand-kept
14
+ * lists, and the lists did what hand-kept lists do: one enabled 43 rules, the other 53, and canon
15
+ * had 49. Neither repository was wrong on purpose; nothing told either of them.
16
+ *
17
+ * The candidate path is included deliberately. A Preview candidate is the exact source Apply later
18
+ * ports into production, and leaving it ungoverned means the one file that becomes production was
19
+ * judged by nothing.
20
+ */
21
+ const LAYOUT_GLOBS = {
22
+ monorepo: [
23
+ "packages/ui/src/**/*.{ts,tsx}",
24
+ "apps/*/src/**/*.{ts,tsx}",
25
+ "**/candidate/src/**/*.{ts,tsx}",
26
+ ],
27
+ "single-app": [
28
+ "src/**/*.{ts,tsx}",
29
+ "**/candidate/src/**/*.{ts,tsx}",
30
+ ],
31
+ }
32
+
33
+ /** The repository shapes this canon knows how to govern. */
34
+ export const LAYOUTS = Object.keys(LAYOUT_GLOBS)
35
+
36
+ /**
37
+ * The one flat-config block that attaches this canon to a repository.
38
+ *
39
+ * A consuming `eslint.config.mjs` spreads the result and owns nothing else about the rules: not
40
+ * which are on, not their severity, not whether an inline comment may switch one off. Its own
41
+ * plugins, its own ignores and its own language options stay its own business - what it may not
42
+ * keep is a second opinion about the law.
43
+ *
44
+ * @param {object} input - Attachment options.
45
+ * @param {"monorepo" | "single-app"} input.layout - Which shape this repository has.
46
+ * @param {object} input.plugin - The plugin object, imported from this canon.
47
+ * @param {object} input.recommended - The recommendation, imported from this canon.
48
+ * @param {object} input.linterOptions - The linter options, imported from this canon.
49
+ * @returns {object} One flat-config block.
50
+ */
51
+ export const starciFeConfig = ({ layout, plugin, recommended, linterOptions }) => {
52
+ const files = LAYOUT_GLOBS[layout]
53
+ if (!files) {
54
+ throw new Error(`unknown layout "${layout}" - expected one of: ${LAYOUTS.join(", ")}`)
55
+ }
56
+ /*
57
+ * `recommended` is a FLAT rule map here, not a config object with a `rules` key - and reaching
58
+ * for `.rules` on it is the mistake this line exists to stop repeating. Spread that way it
59
+ * yields `undefined`, the block ends up with zero rules, and the repository lints clean while
60
+ * being governed by nothing. That is the worst possible failure for this file: green, silent,
61
+ * and indistinguishable from success. Both spellings are accepted so neither shape can produce
62
+ * an empty block.
63
+ */
64
+ const rules = recommended?.rules ?? recommended
65
+ if (!rules || Object.keys(rules).length === 0) {
66
+ throw new Error("starciFeConfig received an empty recommendation - a config with no rules is not adoption")
67
+ }
68
+ return {
69
+ files,
70
+ linterOptions,
71
+ plugins: { "starci-fe": plugin },
72
+ rules: { ...rules },
73
+ }
74
+ }
75
+
76
+ /** Convert every flat-config severity spelling to its numeric meaning. */
77
+ const severityOf = (setting) => {
78
+ const severity = Array.isArray(setting) ? setting[0] : setting
79
+ if (severity === "error") return 2
80
+ if (severity === "warn") return 1
81
+ if (severity === "off") return 0
82
+ return severity
83
+ }
84
+
85
+ /**
86
+ * Compare an `eslint --print-config` result with the canonical recommendation.
87
+ *
88
+ * @param {object} printedConfig - Parsed effective ESLint config for one production probe file.
89
+ * @param {Record<string, string | number | unknown[]>} expectedRules - Canonical rule levels.
90
+ */
91
+ export const auditLintAdoption = (printedConfig, expectedRules) => {
92
+ const actualRules = printedConfig?.rules ?? {}
93
+ const missing = []
94
+ const nonError = []
95
+
96
+ for (const name of Object.keys(expectedRules).sort()) {
97
+ if (actualRules[name] === undefined) {
98
+ missing.push(name)
99
+ continue
100
+ }
101
+ if (severityOf(actualRules[name]) !== 2) nonError.push(name)
102
+ }
103
+
104
+ const refusesInlineConfig = printedConfig?.linterOptions?.noInlineConfig === true
105
+ return {
106
+ ok: missing.length === 0 && nonError.length === 0 && refusesInlineConfig,
107
+ missing,
108
+ nonError,
109
+ refusesInlineConfig,
110
+ }
111
+ }
112
+
113
+ /** Repository audits are gathered beside rules, but are not ESLint AST rules themselves. */
114
+ export const audits = { "effective-config": auditLintAdoption }
115
+ export const rules = {}
116
+ export const recommended = {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-fe",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",