@brighterly/lib-fe-codestyle 0.2.0 → 0.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/README.md CHANGED
@@ -5,6 +5,10 @@ cs-app, app-tutor, app-admin.
5
5
 
6
6
  It holds the **mechanisms**. Domain rules stay in each app.
7
7
 
8
+ **New here?** [docs/how-it-works.md](docs/how-it-works.md) — how a check reads real
9
+ code, what the gate can and cannot cover, what is already enforced, how to extend it,
10
+ and how to run it.
11
+
8
12
  ## Subpaths
9
13
 
10
14
  | Import | What it is |
@@ -53,28 +57,30 @@ exported as data.
53
57
 
54
58
  | Rule | Level | Checks |
55
59
  |---|---|---|
56
- | `_R1` | error | bare `localStorage` / `sessionStorage` — the accessor itself throws on Safari ITP and in-app webviews |
57
- | `_R2` | error | SCSS rules inline in a Vue `<style>` block instead of a referenced `.scss` file |
58
- | `_R3` | error | `JSON.parse` with no enclosing `try`/`catch` — a corrupt entry breaks that path permanently |
59
- | `_R4` | error | `@font-face` declaring a font *stack* — an invalid descriptor, so the browser drops the face silently |
60
- | `_C1` | warn | hand-rolled browser API with a VueUse equivalent |
61
- | `_C2` | warn | a component duplicating an existing UI-kit component name |
62
- | `_C3` | warn | a literal colour in SCSS instead of a design token |
63
- | `_C4` | warn | a hand-rolled date format string, or `toISOString()` |
64
- | `_C5` | warn | a type file outside the `types/<entity>/{enums,interfaces,types}.ts` structure |
65
- | `_C6` | warn | a raw `font-family` / `font-size` / `font-weight` declaration instead of the app's typography source |
66
- | `_C7` | warn | `<script>` attribute order differing from the app's canonical (`setup lang` vs `lang setup`) |
67
-
68
- Level discipline: `error` is reserved for crash-class rules (`_R*` — a visitor-facing
69
- throw or silent production breakage); `warn` is for conventions and style choices
70
- (`_C*`). A consumer's `levels` ceiling on an `_R*` rule is NOT the tool for an existing
71
- backlog — fix the call sites or allowlist the reviewed exceptions with reasons.
60
+ | `core/R1` | warn* | bare `localStorage` / `sessionStorage` — the accessor itself throws on Safari ITP and in-app webviews |
61
+ | `core/R2` | error | SCSS rules inline in a Vue `<style>` block instead of a referenced `.scss` file |
62
+ | `core/R3` | warn* | `JSON.parse` with no enclosing `try`/`catch` — a corrupt entry breaks that path permanently |
63
+ | `core/R4` | error | `@font-face` declaring a font *stack* — an invalid descriptor, so the browser drops the face silently |
64
+ | `core/C1` | warn | hand-rolled browser API with a VueUse equivalent |
65
+ | `core/C2` | warn | a component duplicating an existing UI-kit component name |
66
+ | `core/C3` | warn | a literal colour in SCSS instead of a design token |
67
+ | `core/C4` | warn | a hand-rolled date format string, or `toISOString()` |
68
+ | `core/C5` | warn | a type file outside the `types/<entity>/{enums,interfaces,types}.ts` structure |
69
+ | `core/C6` | warn | a raw `font-family` / `font-size` / `font-weight` declaration instead of the app's typography source |
70
+ | `core/C7` | warn | `<script>` attribute order differing from the app's canonical (`setup lang` vs `lang setup`) |
71
+
72
+ Level discipline: `error` is for crash-class rules (`core/R*`); `warn` is for
73
+ conventions (`core/C*`). `core/R1`/`core/R3` (marked *) ship at `warn` **as an interim
74
+ state** (2026-09-25): their fleet backlog is real code nobody has bandwidth to migrate
75
+ and re-verify right now. Their target level is `error` — promote once the backlog
76
+ clears (a breaking-style bump). `core/R2`/`core/R4` stay `error`. A consumer's `levels`
77
+ ceiling is still never the tool for a backlog on an `error` rule — fix or allowlist.
72
78
  Underscore-prefixed SCSS partials (`_extends.scss`, `_colors-*.scss`, …) are declaration
73
- files by convention in every Brighterly app and are exempt from `_C3`/`_C6` wholesale.
79
+ files by convention in every Brighterly app and are exempt from `core/C3`/`core/C6` wholesale.
74
80
 
75
- `_R*` were ported from land-app, where each was a production scar (as `R1`, `R7`, `R18`,
81
+ `core/R*` were ported from land-app, where each was a production scar (as `R1`, `R7`, `R18`,
76
82
  `R28` — renumbered sequentially when the prefix landed, pre-publish, while no released
77
- version pinned the old tokens). `_C*` are conventions with
83
+ version pinned the old tokens). `core/C*` are conventions with
78
84
  no incident behind them *yet* — they ship at `warn` so a consuming app can measure what they
79
85
  find before anyone decides to block on them. Promoting one to `error` is a **major** bump.
80
86
 
@@ -100,21 +106,22 @@ A shared rule must not hardcode a consuming app's paths. Each app declares them
100
106
  // invariants/config.mts
101
107
  export const settings = {
102
108
  safeStorageModule: 'safeLocalStorage from @helpers/safe-local-storage',
103
- safeStorageFile: 'app/utils/helpers/safe-local-storage.ts', // _R1 auto-exempts the wrapper itself
109
+ safeStorageFile: 'app/utils/helpers/safe-local-storage.ts', // core/R1 auto-exempts the wrapper itself
104
110
  safeJsonParse: 'parseStoredJson() from @helpers/safe-local-storage',
105
111
  scssTokensFile: 'app/assets/scss/_colors.scss',
106
112
  uiKitRoot: 'app/components/UI',
107
113
  dateFormatModule: 'DateFormat from @ts-types/date/enums',
108
- typesRoot: 'app/types', // _C5 (default shown)
109
- typographySource: 'the font() mixin from @scss/_mixins.scss', // _C6 — cs-app points this at its AppText component instead
110
- scriptAttrOrder: 'setup-first', // _C7 — or 'lang-first' (default 'setup-first')
114
+ typesRoot: 'app/types', // core/C5 (default shown)
115
+ typographySource: 'the font() mixin from @scss/_mixins.scss', // core/C6 — cs-app points this at its AppText component instead
116
+ scriptAttrOrder: 'setup-first', // core/C7 — or 'lang-first' (default 'setup-first')
117
+ selfTestFile: 'invariants/self-test.mts', // its ignore-strings are fixture DATA — skipped by IG0/IG1
111
118
  }
112
119
  ```
113
120
 
114
121
  A missing key degrades the rule's *message*, never its firing. A rule that went quiet
115
122
  because a path was unset would be the exact silent failure this package exists to prevent.
116
- Two keys shape behaviour rather than the message — `typesRoot` (where `_C5` looks) and
117
- `scriptAttrOrder` (which direction `_C7` flags) — and both fall back to the documented
123
+ Two keys shape behaviour rather than the message — `typesRoot` (where `core/C5` looks) and
124
+ `scriptAttrOrder` (which direction `core/C7` flags) — and both fall back to the documented
118
125
  default above, so leaving them unset still never silences the rule.
119
126
 
120
127
  ## What consumers see in the IDE
@@ -124,7 +131,7 @@ requires opening this repo:
124
131
 
125
132
  - **`sharedRuleIds`** (from `…/invariants/rules`) — the catalog: one constant per rule,
126
133
  each JSDoc'd with level, what it checks, and the fix. Use them as `allowlists` /
127
- `levels` keys (`[sharedRuleIds._C6]: 'off'`) and hover for the contract. A test pins
134
+ `levels` keys (`[sharedRuleIds.core/C6]: 'off'`) and hover for the contract. A test pins
128
135
  the catalog to the shipped rules, so it cannot drift.
129
136
  - **`SharedRuleSettings`** (from `…/invariants`) — annotate the app's `settings` const
130
137
  with it to get completion plus the per-key contract, and a type error on an invalid
@@ -171,18 +178,18 @@ The ESLint config and its seven rules are already plain `.mjs` and ship as-is.
171
178
 
172
179
  ## Rule IDs
173
180
 
174
- Shared rules carry an **underscore prefix**: `_R1`, `_C3`. App-local rules stay bare:
175
- `R1`, `B2`. The prefix marks provenance — an `_`-id is shipped by this package, never
181
+ Shared rules carry the **`core/` prefix**: `core/R1`, `core/C3`. App-local rules stay bare:
182
+ `R1`, `B2`. The prefix marks provenance — a `core/`-id is shipped by this package, never
176
183
  defined, renumbered or recycled in a consumer — and it keeps the two number lines
177
184
  independent: every app allocates its own `R1, R2, …` naturally, with no risk of
178
185
  colliding with a rule the package ships later. `run()` still exits 2 on a duplicate,
179
186
  so a collision is loud, not silent.
180
187
 
181
- Within the package each family is numbered sequentially with no gaps (`_R1`–`_R4`,
182
- `_C1`–`_C7`) — enforced by `test/invariants/rule-ids.test.ts`. The family letter keeps
188
+ Within the package each family is numbered sequentially with no gaps (`core/R1`–`core/R4`,
189
+ `core/C1`–`core/C7`) — enforced by `test/invariants/rule-ids.test.ts`. The family letter keeps
183
190
  meaning provenance — `R` regression scar, `B` business, `C` convention, `L`
184
- linter-owned — not topic. The underscore does **not** mean private (everything here is
185
- the public surface); it means "owned by the library, hands off the numbering."
191
+ linter-owned — not topic. `core/` in a report means exactly one thing: this finding
192
+ comes from the library, and its numbering is the library's to own.
186
193
 
187
194
  ## Versioning is about blocking power
188
195
 
@@ -2,12 +2,12 @@
2
2
  * Convention rules — preferences we chose, with no production incident behind them.
3
3
  * Advisory by default (`warn`): nudged, not policed, and droppable by team consensus.
4
4
  *
5
- * `_C1` was land-app's `C1`, and reads the same everywhere. `_C2`-`_C4` are new, and came out of a
5
+ * `core/C1` was land-app's `C1`, and reads the same everywhere. `core/C2`-`core/C4` are new, and came out of a
6
6
  * real code review rather than an outage: a developer shipped a component that duplicated
7
7
  * an existing UI-kit one, with colours that were not design tokens. Review caught it;
8
8
  * nothing automated did, because the conventions existed only in reviewers\' heads.
9
9
  *
10
- * `_C5`-`_C7` (2026-09-23) come from the org's Notion frontend-conventions doc and from
10
+ * `core/C5`-`core/C7` (2026-09-23) come from the org's Notion frontend-conventions doc and from
11
11
  * mining 400 PR review comments across the four apps: entity-folder type organization,
12
12
  * typography through the app's declared source, and one canonical <script> attribute order.
13
13
  *
@@ -2,12 +2,12 @@
2
2
  * Convention rules — preferences we chose, with no production incident behind them.
3
3
  * Advisory by default (`warn`): nudged, not policed, and droppable by team consensus.
4
4
  *
5
- * `_C1` was land-app's `C1`, and reads the same everywhere. `_C2`-`_C4` are new, and came out of a
5
+ * `core/C1` was land-app's `C1`, and reads the same everywhere. `core/C2`-`core/C4` are new, and came out of a
6
6
  * real code review rather than an outage: a developer shipped a component that duplicated
7
7
  * an existing UI-kit one, with colours that were not design tokens. Review caught it;
8
8
  * nothing automated did, because the conventions existed only in reviewers\' heads.
9
9
  *
10
- * `_C5`-`_C7` (2026-09-23) come from the org's Notion frontend-conventions doc and from
10
+ * `core/C5`-`core/C7` (2026-09-23) come from the org's Notion frontend-conventions doc and from
11
11
  * mining 400 PR review comments across the four apps: entity-folder type organization,
12
12
  * typography through the app's declared source, and one canonical <script> attribute order.
13
13
  *
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { eachLine, setting } from './shared.js';
18
18
  const vueUseFirst = {
19
- id: '_C1',
19
+ id: 'core/C1',
20
20
  title: 'hand-rolled browser API with a VueUse equivalent',
21
21
  level: 'warn',
22
22
  why: 'VueUse handles the lifecycle, SSR and error paths we otherwise re-implement (and forget to tear down) per call site',
@@ -47,7 +47,7 @@ const vueUseFirst = {
47
47
  },
48
48
  };
49
49
  const uiKitReuseFirst = {
50
- id: '_C2',
50
+ id: 'core/C2',
51
51
  title: 'component duplicates an existing UI-kit component name',
52
52
  level: 'warn',
53
53
  once: true,
@@ -81,7 +81,7 @@ const uiKitReuseFirst = {
81
81
  },
82
82
  };
83
83
  const scssColourTokens = {
84
- id: '_C3',
84
+ id: 'core/C3',
85
85
  title: 'literal colour in SCSS instead of a design token',
86
86
  level: 'warn',
87
87
  why: 'a hardcoded colour is invisible to the token system — it survives theme changes and palette corrections silently, and only a side-by-side screenshot shows the drift',
@@ -108,7 +108,7 @@ const scssColourTokens = {
108
108
  },
109
109
  };
110
110
  const dateFormatEnum = {
111
- id: '_C4',
111
+ id: 'core/C4',
112
112
  title: 'hand-rolled date format string instead of the shared enum',
113
113
  level: 'warn',
114
114
  why: 'a format string written at the call site makes the date\'s shape an accident rather than a contract; `date`-validated endpoints accept several shapes, so a wrong one ships silently and surfaces later as bad data',
@@ -134,7 +134,7 @@ const dateFormatEnum = {
134
134
  },
135
135
  };
136
136
  const typesOrganization = {
137
- id: '_C5',
137
+ id: 'core/C5',
138
138
  title: 'type file outside the entity-folder structure',
139
139
  level: 'warn',
140
140
  once: true,
@@ -166,7 +166,7 @@ const typesOrganization = {
166
166
  },
167
167
  };
168
168
  const typographySource = {
169
- id: '_C6',
169
+ id: 'core/C6',
170
170
  title: 'raw font declaration instead of the app\'s typography source',
171
171
  level: 'warn',
172
172
  why: 'a hand-written font-family/size/weight is invisible to the UI-kit type scale — it survives font and scale changes silently, and the page drifts off-brand one declaration at a time (Notion: UI та стилі)',
@@ -179,7 +179,7 @@ const typographySource = {
179
179
  // Brighterly apps, decided 2026-09-24), plus non-underscore fonts/typography files.
180
180
  if (/\/(?:_[^/]*|fonts|typography)\.scss$/.test(file))
181
181
  return [];
182
- // @font-face declares a face, not a usage — _R4 polices those blocks.
182
+ // @font-face declares a face, not a usage — core/R4 polices those blocks.
183
183
  const stripped = code.replace(/@font-face\s*\{[^}]*\}/g, m => m.replace(/[^\n]/g, ' '));
184
184
  return eachLine(stripped, (line, n) => {
185
185
  const m = /\bfont(?:-family|-size|-weight)?\s*:/.exec(line);
@@ -190,7 +190,7 @@ const typographySource = {
190
190
  },
191
191
  };
192
192
  const scriptAttrOrder = {
193
- id: '_C7',
193
+ id: 'core/C7',
194
194
  title: 'script tag attribute order differs from the app canonical',
195
195
  level: 'warn',
196
196
  why: '<script setup lang="ts"> and <script lang="ts" setup> are compiler-equivalent, so nothing ever fails — the split just grows (723 vs 384 files in one app, Sep 2026) and every diff, codemod and grep has to handle both spellings',
@@ -10,7 +10,7 @@
10
10
  import type { Fixture } from '../self-test.js';
11
11
  export declare const commonFixtures: Record<string, Fixture>;
12
12
  /**
13
- * `_C2` and `_C5` are `once` rules over the whole file list — a single-file fixture
13
+ * `core/C2` and `core/C5` are `once` rules over the whole file list — a single-file fixture
14
14
  * cannot express them, so they are exempt from the fixture requirement and covered by
15
15
  * their own tests instead.
16
16
  */
@@ -8,47 +8,47 @@
8
8
  * `bad` must produce at least one finding, `good` none.
9
9
  */
10
10
  export const commonFixtures = {
11
- '_R1': {
11
+ 'core/R1': {
12
12
  file: 'app/a.ts',
13
13
  bad: 'const t = localStorage.getItem("k")',
14
14
  good: 'const t = safeLocalStorage.getItem("k")',
15
15
  },
16
- '_R2': {
16
+ 'core/R2': {
17
17
  file: 'app/components/A.vue',
18
18
  bad: '<template><div /></template>\n<style scoped lang="scss">.a { color: red; }</style>',
19
19
  good: '<template><div /></template>\n<style scoped lang="scss" src="@scss/components/A.scss" />',
20
20
  },
21
- '_R3': {
21
+ 'core/R3': {
22
22
  file: 'app/a.ts',
23
23
  bad: 'const v = JSON.parse(raw)',
24
24
  good: 'let v = null\ntry { v = JSON.parse(raw) } catch { v = null }',
25
25
  },
26
- '_R4': {
26
+ 'core/R4': {
27
27
  file: 'app/assets/scss/fonts.scss',
28
28
  bad: '@font-face { font-family: Rubik, sans-serif; src: url(a.woff2); }',
29
29
  good: '@font-face { font-family: Rubik; src: url(a.woff2); }',
30
30
  },
31
- '_C1': {
31
+ 'core/C1': {
32
32
  file: 'app/a.ts',
33
33
  bad: 'window.addEventListener("resize", onResize)',
34
34
  good: 'useEventListener(window, "resize", onResize)',
35
35
  },
36
- '_C3': {
36
+ 'core/C3': {
37
37
  file: 'app/assets/scss/components/Card.scss',
38
38
  bad: '.card { color: #1f2937; }',
39
39
  good: '.card { color: $slate-800; }',
40
40
  },
41
- '_C4': {
41
+ 'core/C4': {
42
42
  file: 'app/a.ts',
43
43
  bad: "const d = dayjs().format('YYYY-MM-DD')",
44
44
  good: 'const d = dayjs().format(DateFormat.DefaultDate)',
45
45
  },
46
- '_C6': {
46
+ 'core/C6': {
47
47
  file: 'app/assets/scss/components/Card.scss',
48
48
  bad: '.card { font-size: 14px; }',
49
49
  good: '.card { @include font(14px, 20px); color: $slate-800; }',
50
50
  },
51
- '_C7': {
51
+ 'core/C7': {
52
52
  // Two script tags in OPPOSITE orders, so the bad fixture violates whichever
53
53
  // direction the consuming app declares in settings.scriptAttrOrder. The good
54
54
  // fixture carries no lang attribute, so it is clean under either canonical.
@@ -58,8 +58,8 @@ export const commonFixtures = {
58
58
  },
59
59
  };
60
60
  /**
61
- * `_C2` and `_C5` are `once` rules over the whole file list — a single-file fixture
61
+ * `core/C2` and `core/C5` are `once` rules over the whole file list — a single-file fixture
62
62
  * cannot express them, so they are exempt from the fixture requirement and covered by
63
63
  * their own tests instead.
64
64
  */
65
- export const commonSelfTestSkip = ['_C2', '_C5'];
65
+ export const commonSelfTestSkip = ['core/C2', 'core/C5'];
@@ -9,7 +9,7 @@
9
9
  * C — convention: a preference we chose → team consensus
10
10
  * L — linter: a built-in ESLint rule → eslint.config.mjs
11
11
  *
12
- * Shared rule ids carry the underscore prefix (`_R1`); app-local ids stay bare (`R1`).
12
+ * Shared rule ids carry the `core/` prefix (`core/R1`); app-local ids stay bare (`R1`).
13
13
  * The prefix marks provenance and keeps the two number lines independent — and `run()`
14
14
  * still fails fast on a duplicate rather than letting an allowlist silently suppress
15
15
  * the wrong rule.
@@ -28,28 +28,28 @@ export declare const commonRules: Rule[];
28
28
  * instead. A test pins this map to `commonRules`, so it cannot drift.
29
29
  */
30
30
  export declare const sharedRuleIds: {
31
- /** `error` — bare `localStorage`/`sessionStorage` access. The accessor itself throws SecurityError on Safari ITP, Firefox strict mode and in-app webviews; `if (localStorage)` guards throw too. Fix: the app's safe storage wrapper (`settings.safeStorageModule`). */
32
- readonly _R1: "_R1";
31
+ /** `warn` (target `error`, interim 2026-09-25) — bare `localStorage`/`sessionStorage` access. The accessor itself throws SecurityError on Safari ITP, Firefox strict mode and in-app webviews; `if (localStorage)` guards throw too. Fix: the app's safe storage wrapper (`settings.safeStorageModule`). */
32
+ readonly 'core/R1': "core/R1";
33
33
  /** `error` — SCSS rules written inline in a Vue `<style>` block. Inline styles drift from the design tokens invisibly. Fix: a dedicated `.scss` file referenced via `<style scoped lang="scss" src="…" />`. */
34
- readonly _R2: "_R2";
35
- /** `error` — `JSON.parse` with no enclosing `try`/`catch`. A corrupt stored value crashes that code path permanently for the visitor. Fix: the app's guarded parse helper (`settings.safeJsonParse`) or try/catch. */
36
- readonly _R3: "_R3";
34
+ readonly 'core/R2': "core/R2";
35
+ /** `warn` (target `error`, interim 2026-09-25) — `JSON.parse` with no enclosing `try`/`catch`. A corrupt stored value crashes that code path permanently for the visitor. Fix: the app's guarded parse helper (`settings.safeJsonParse`) or try/catch. */
36
+ readonly 'core/R3': "core/R3";
37
37
  /** `error` — `@font-face` declaring a font *stack*. Inside `@font-face`, `font-family` names the face being defined; a comma-separated stack is invalid and the browser drops the face silently. Fix: single family name inside, the stack on the selectors that use it. */
38
- readonly _R4: "_R4";
38
+ readonly 'core/R4': "core/R4";
39
39
  /** `warn` — hand-rolled browser API with a VueUse equivalent (`new IntersectionObserver`, `addEventListener`, `setInterval`, …). VueUse owns the lifecycle/SSR/error paths. Fix: the composable named in the message, imported from `@vueuse/core`. */
40
- readonly _C1: "_C1";
40
+ readonly 'core/C1': "core/C1";
41
41
  /** `warn` — a component whose filename duplicates a UI-kit component (`settings.uiKitRoot`). A same-name second component splits the design system in two. Fix: import or extend the UI-kit one. */
42
- readonly _C2: "_C2";
42
+ readonly 'core/C2': "core/C2";
43
43
  /** `warn` — a literal colour (`#hex`, numeric `rgba()`) in SCSS/Vue instead of a palette token. Hardcoded colours survive theme and palette changes silently. Fix: a token from `settings.scssTokensFile`. */
44
- readonly _C3: "_C3";
44
+ readonly 'core/C3': "core/C3";
45
45
  /** `warn` — a hand-rolled date format string (`'YYYY-MM-DD'`) or `toISOString()`. The date's shape becomes an accident instead of a contract. Fix: the shared enum (`settings.dateFormatModule`). */
46
- readonly _C4: "_C4";
46
+ readonly 'core/C4': "core/C4";
47
47
  /** `warn` — a type file outside `types/<entity>/{enums,interfaces,types}.ts` (root under `settings.typesRoot`). Root-level or mixed-kind type files accrete unrelated entities. `.d.ts` ambient files are exempt. */
48
- readonly _C5: "_C5";
48
+ readonly 'core/C5': "core/C5";
49
49
  /** `warn` — a raw `font-family`/`font-size`/`font-weight` declaration. Hand-written fonts drift off the UI-kit type scale silently. Fix: the app's typography source (`settings.typographySource`). */
50
- readonly _C6: "_C6";
50
+ readonly 'core/C6': "core/C6";
51
51
  /** `warn` — `<script>` attribute order differing from the app canonical (`settings.scriptAttrOrder`, default `setup-first`). The two spellings are compiler-equivalent, so the split only ever grows. */
52
- readonly _C7: "_C7";
52
+ readonly 'core/C7': "core/C7";
53
53
  };
54
54
  /** Union of every id the shared pack ships — for typing app config keys. */
55
55
  export type SharedRuleId = keyof typeof sharedRuleIds;
@@ -9,7 +9,7 @@
9
9
  * C — convention: a preference we chose → team consensus
10
10
  * L — linter: a built-in ESLint rule → eslint.config.mjs
11
11
  *
12
- * Shared rule ids carry the underscore prefix (`_R1`); app-local ids stay bare (`R1`).
12
+ * Shared rule ids carry the `core/` prefix (`core/R1`); app-local ids stay bare (`R1`).
13
13
  * The prefix marks provenance and keeps the two number lines independent — and `run()`
14
14
  * still fails fast on a duplicate rather than letting an allowlist silently suppress
15
15
  * the wrong rule.
@@ -28,26 +28,26 @@ export const commonRules = [...rRules, ...cRules];
28
28
  * instead. A test pins this map to `commonRules`, so it cannot drift.
29
29
  */
30
30
  export const sharedRuleIds = {
31
- /** `error` — bare `localStorage`/`sessionStorage` access. The accessor itself throws SecurityError on Safari ITP, Firefox strict mode and in-app webviews; `if (localStorage)` guards throw too. Fix: the app's safe storage wrapper (`settings.safeStorageModule`). */
32
- _R1: '_R1',
31
+ /** `warn` (target `error`, interim 2026-09-25) — bare `localStorage`/`sessionStorage` access. The accessor itself throws SecurityError on Safari ITP, Firefox strict mode and in-app webviews; `if (localStorage)` guards throw too. Fix: the app's safe storage wrapper (`settings.safeStorageModule`). */
32
+ 'core/R1': 'core/R1',
33
33
  /** `error` — SCSS rules written inline in a Vue `<style>` block. Inline styles drift from the design tokens invisibly. Fix: a dedicated `.scss` file referenced via `<style scoped lang="scss" src="…" />`. */
34
- _R2: '_R2',
35
- /** `error` — `JSON.parse` with no enclosing `try`/`catch`. A corrupt stored value crashes that code path permanently for the visitor. Fix: the app's guarded parse helper (`settings.safeJsonParse`) or try/catch. */
36
- _R3: '_R3',
34
+ 'core/R2': 'core/R2',
35
+ /** `warn` (target `error`, interim 2026-09-25) — `JSON.parse` with no enclosing `try`/`catch`. A corrupt stored value crashes that code path permanently for the visitor. Fix: the app's guarded parse helper (`settings.safeJsonParse`) or try/catch. */
36
+ 'core/R3': 'core/R3',
37
37
  /** `error` — `@font-face` declaring a font *stack*. Inside `@font-face`, `font-family` names the face being defined; a comma-separated stack is invalid and the browser drops the face silently. Fix: single family name inside, the stack on the selectors that use it. */
38
- _R4: '_R4',
38
+ 'core/R4': 'core/R4',
39
39
  /** `warn` — hand-rolled browser API with a VueUse equivalent (`new IntersectionObserver`, `addEventListener`, `setInterval`, …). VueUse owns the lifecycle/SSR/error paths. Fix: the composable named in the message, imported from `@vueuse/core`. */
40
- _C1: '_C1',
40
+ 'core/C1': 'core/C1',
41
41
  /** `warn` — a component whose filename duplicates a UI-kit component (`settings.uiKitRoot`). A same-name second component splits the design system in two. Fix: import or extend the UI-kit one. */
42
- _C2: '_C2',
42
+ 'core/C2': 'core/C2',
43
43
  /** `warn` — a literal colour (`#hex`, numeric `rgba()`) in SCSS/Vue instead of a palette token. Hardcoded colours survive theme and palette changes silently. Fix: a token from `settings.scssTokensFile`. */
44
- _C3: '_C3',
44
+ 'core/C3': 'core/C3',
45
45
  /** `warn` — a hand-rolled date format string (`'YYYY-MM-DD'`) or `toISOString()`. The date's shape becomes an accident instead of a contract. Fix: the shared enum (`settings.dateFormatModule`). */
46
- _C4: '_C4',
46
+ 'core/C4': 'core/C4',
47
47
  /** `warn` — a type file outside `types/<entity>/{enums,interfaces,types}.ts` (root under `settings.typesRoot`). Root-level or mixed-kind type files accrete unrelated entities. `.d.ts` ambient files are exempt. */
48
- _C5: '_C5',
48
+ 'core/C5': 'core/C5',
49
49
  /** `warn` — a raw `font-family`/`font-size`/`font-weight` declaration. Hand-written fonts drift off the UI-kit type scale silently. Fix: the app's typography source (`settings.typographySource`). */
50
- _C6: '_C6',
50
+ 'core/C6': 'core/C6',
51
51
  /** `warn` — `<script>` attribute order differing from the app canonical (`settings.scriptAttrOrder`, default `setup-first`). The two spellings are compiler-equivalent, so the split only ever grows. */
52
- _C7: '_C7',
52
+ 'core/C7': 'core/C7',
53
53
  };
@@ -12,9 +12,13 @@
12
12
  import { findJsonParseCalls } from '../ast.js';
13
13
  import { eachLine, setting } from './shared.js';
14
14
  const bareStorage = {
15
- id: '_R1',
15
+ id: 'core/R1',
16
16
  title: 'bare localStorage / sessionStorage access',
17
- level: 'error',
17
+ // Target level is `error` (crash-class). Shipped at `warn` since 2026-09-25: the fleet
18
+ // backlog (cs-app ~42 sites, main-app ~28) is real code the team has no bandwidth to
19
+ // migrate and re-verify right now, and a rushed storage migration is its own risk.
20
+ // Promote back to `error` once the backlog is cleared — that is a major-style bump.
21
+ level: 'warn',
18
22
  why: 'the bare API throws SecurityError on Safari ITP, Firefox strict mode and in-app webviews (Instagram/Facebook/TikTok) — the `if (localStorage)` guard does not help, the property accessor itself throws',
19
23
  fix: 'use the app\'s safe storage wrapper instead of the bare API',
20
24
  scope: ['app/**/*.{ts,vue}'],
@@ -37,7 +41,7 @@ const bareStorage = {
37
41
  },
38
42
  };
39
43
  const scssInStyleBlock = {
40
- id: '_R2',
44
+ id: 'core/R2',
41
45
  title: 'SCSS rules inside a Vue <style> block',
42
46
  level: 'error',
43
47
  why: 'inline style blocks drift from the design tokens and are invisible to the SCSS build — the component stops sharing the system without anything failing',
@@ -66,9 +70,10 @@ const scssInStyleBlock = {
66
70
  },
67
71
  };
68
72
  const unguardedJsonParse = {
69
- id: '_R3',
73
+ id: 'core/R3',
70
74
  title: 'unguarded JSON.parse',
71
- level: 'error',
75
+ // Target level is `error` — see core/R1 above; same interim decision (2026-09-25).
76
+ level: 'warn',
72
77
  async: true,
73
78
  why: 'a truncated write or a hand-edited storage entry makes JSON.parse throw on every re-entry to that code path, until the visitor clears storage — the failure is permanent for them, not transient',
74
79
  fix: 'parse through the app\'s guarded helper, or wrap the call in try/catch',
@@ -83,7 +88,7 @@ const unguardedJsonParse = {
83
88
  },
84
89
  };
85
90
  const fontFaceStack = {
86
- id: '_R4',
91
+ id: 'core/R4',
87
92
  title: '@font-face with a font stack instead of one family name',
88
93
  level: 'error',
89
94
  why: 'inside @font-face, font-family names the face being defined; a comma-separated stack is an invalid descriptor and the browser drops the whole face silently. Every page then renders in the fallback font, nothing errors, and only a side-by-side screenshot shows it',
@@ -228,7 +228,9 @@ export const run = async (options) => {
228
228
  const ignoreCache = new Map();
229
229
  const ignoresFor = (file) => {
230
230
  if (!ignoreCache.has(file)) {
231
- const src = read(file);
231
+ // The consumer's self-test file holds literal `invariant-ignore:` strings as
232
+ // fixture DATA — parsing them would report the fixtures themselves (IG0/IG1).
233
+ const src = file === settings.selfTestFile ? null : read(file);
232
234
  ignoreCache.set(file, src === null ? { suppressions: new Map(), unreasoned: [], comments: [] } : parseIgnores(src));
233
235
  }
234
236
  return ignoreCache.get(file);
@@ -348,6 +350,7 @@ export const run = async (options) => {
348
350
  live, retired, allowlists, levels, ignores,
349
351
  configFile,
350
352
  configSource: read(configFile) ?? undefined,
353
+ pathExists: p => read(p) !== null,
351
354
  })) {
352
355
  findings.push({ ruleId: 'IG1', level: 'error', ...v });
353
356
  }
@@ -17,8 +17,8 @@ export type Level = 'error' | 'warn' | 'off';
17
17
  * IDs are load-bearing (`--rule=`, `invariant-ignore:`, allowlists, levels) —
18
18
  * retire one, never recycle it.
19
19
  */
20
- /** App-local ids are bare; shared-pack ids carry the underscore prefix (`_R1`). */
21
- export type RuleId = `${'' | '_'}${'R' | 'B' | 'C' | 'L'}${number}`;
20
+ /** App-local ids are bare; shared-pack ids carry the `core/` prefix (`core/R1`). */
21
+ export type RuleId = `${'' | 'core/'}${'R' | 'B' | 'C' | 'L'}${number}`;
22
22
  /**
23
23
  * Meta rules. They police the escape hatch rather than the app, so they have no
24
24
  * family letter — but they are addressable exactly like any other rule
@@ -56,24 +56,26 @@ export interface ReportedFinding extends Finding {
56
56
  * the documented default), never its firing.
57
57
  */
58
58
  export interface SharedRuleSettings {
59
- /** `_R1`/`_R3` fix messages: how this app names its safe storage wrapper, e.g. 'safeLocalStorage from @helpers/safe-local-storage'. */
59
+ /** `core/R1`/`core/R3` fix messages: how this app names its safe storage wrapper, e.g. 'safeLocalStorage from @helpers/safe-local-storage'. */
60
60
  safeStorageModule?: string;
61
- /** `_R1`: repo-relative path of the wrapper FILE itself — the one place bare storage access belongs, auto-exempted so no app has to allowlist its own wrapper. E.g. 'app/utils/helpers/safe-local-storage.ts'. */
61
+ /** `core/R1`: repo-relative path of the wrapper FILE itself — the one place bare storage access belongs, auto-exempted so no app has to allowlist its own wrapper. E.g. 'app/utils/helpers/safe-local-storage.ts'. */
62
62
  safeStorageFile?: string;
63
- /** `_R3` fix message: the guarded parse helper, e.g. 'parseStoredJson() from @helpers/safe-local-storage'. */
63
+ /** `core/R3` fix message: the guarded parse helper, e.g. 'parseStoredJson() from @helpers/safe-local-storage'. */
64
64
  safeJsonParse?: string;
65
- /** `_C3`: the palette file that legitimately DEFINES colour literals, e.g. 'app/assets/scss/_colors.scss'. */
65
+ /** `core/C3`: the palette file that legitimately DEFINES colour literals, e.g. 'app/assets/scss/_colors.scss'. */
66
66
  scssTokensFile?: string;
67
- /** `_C2`: root of the UI-kit tree whose component names must not be duplicated. Default 'app/components/UI'. */
67
+ /** `core/C2`: root of the UI-kit tree whose component names must not be duplicated. Default 'app/components/UI'. */
68
68
  uiKitRoot?: string;
69
- /** `_C4` fix message: the shared date-format enum, e.g. 'DateFormat from @ts-types/date/enums'. */
69
+ /** `core/C4` fix message: the shared date-format enum, e.g. 'DateFormat from @ts-types/date/enums'. */
70
70
  dateFormatModule?: string;
71
- /** `_C5`: where entity type folders live. Default 'app/types'. */
71
+ /** `core/C5`: where entity type folders live. Default 'app/types'. */
72
72
  typesRoot?: string;
73
- /** `_C6` fix message: this app's typography source — a mixin in some apps ('the font() mixin from @scss/_mixins.scss'), a component in others ('the AppText component'). */
73
+ /** `core/C6` fix message: this app's typography source — a mixin in some apps ('the font() mixin from @scss/_mixins.scss'), a component in others ('the AppText component'). */
74
74
  typographySource?: string;
75
- /** `_C7`: which `<script>` attribute order is canonical in this app. Default 'setup-first' (`<script setup lang="ts">`). */
75
+ /** `core/C7`: which `<script>` attribute order is canonical in this app. Default 'setup-first' (`<script setup lang="ts">`). */
76
76
  scriptAttrOrder?: 'setup-first' | 'lang-first';
77
+ /** The consumer file whose `invariant-ignore:` strings are self-test FIXTURE DATA, not real suppressions — the engine skips ignore-parsing it entirely, replacing per-file IG0/IG1 allowlist boilerplate. E.g. 'invariants/self-test.mts'. */
78
+ selfTestFile?: string;
77
79
  }
78
80
  /** What a rule's `check` receives. */
79
81
  export interface RuleContext {
@@ -164,6 +166,8 @@ export interface DanglingRefInput {
164
166
  configFile?: string;
165
167
  /** Contents of `configFile`, so a stale key can be reported on its own line. */
166
168
  configSource?: string;
169
+ /** Answers "does this repo-relative file exist?" — enables the allowlist-path staleness check. */
170
+ pathExists?: (path: string) => boolean;
167
171
  }
168
172
  /** An invariant a built-in ESLint rule already covers. */
169
173
  export interface LinterOwnedRule {
@@ -95,8 +95,8 @@ export const createReader = (root) => {
95
95
  };
96
96
  };
97
97
  export const toRelative = (root, p) => relative(root, resolve(root, p)).split(sep).join('/');
98
- // `_?` — shared-pack rules carry an underscore prefix (`_R1`); app-local rules are bare (`R1`).
99
- const IGNORE_RE = /invariant-ignore:\s*(_?[A-Z]{1,3}\d{1,3}(?:\s*,\s*_?[A-Z]{1,3}\d{1,3})*)\s*(?:—|–|--|-|:)?\s*(.*)$/;
98
+ // `(?:core\/)?` — shared-pack rules carry the core/ prefix (`core/R1`); app-local rules are bare (`R1`).
99
+ const IGNORE_RE = /invariant-ignore:\s*((?:core\/)?[A-Z]{1,3}\d{1,3}(?:\s*,\s*(?:core\/)?[A-Z]{1,3}\d{1,3})*)\s*(?:—|–|--|-|:)?\s*(.*)$/;
100
100
  /**
101
101
  * Parse every `invariant-ignore:` comment in a file.
102
102
  * Returns { suppressions: Map<lineNumber, Set<ruleId>>, unreasoned: [{ line, ids }] }
@@ -198,7 +198,9 @@ export const findDanglingRuleRefs = (input) => {
198
198
  const start = src.indexOf(block);
199
199
  if (start === -1)
200
200
  return 1;
201
- const rel = src.slice(start).search(new RegExp(`^\\s*${id}\\s*:`, 'm'));
201
+ // Ids may contain `/` (core/R1) and appear as quoted keys — escape and allow quotes.
202
+ const escaped = id.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
203
+ const rel = src.slice(start).search(new RegExp(`^\\s*(?:'|")?${escaped}(?:'|")?\\s*:`, 'm'));
202
204
  return rel === -1 ? 1 : lineOf(src, start + rel);
203
205
  };
204
206
  const explain = (id) => retired[id] ? `${id} was retired — ${retired[id]}` : `${id} does not name any rule`;
@@ -212,6 +214,21 @@ export const findDanglingRuleRefs = (input) => {
212
214
  continue;
213
215
  out.push({ file: configFile, line: lineOfKey('levels', id), message: `levels names "${id}", but ${explain(id)} — the severity override does nothing; drop it` });
214
216
  }
217
+ // Allowlist PATHS had no staleness guard: an entry whose file is gone (or renamed)
218
+ // is inert and silent. Found live in app-funnels — a dead BtnAuthGoogle.vue entry
219
+ // survived a fork divergence (2026-09-25). Checked only when the caller can answer
220
+ // existence, so the self-test can still exercise this function without a filesystem.
221
+ if (input.pathExists) {
222
+ for (const [id, entries] of Object.entries(allowlists)) {
223
+ if (!live.has(id))
224
+ continue; // the dead-id finding above already covers the whole entry
225
+ for (const path of Object.keys(entries ?? {})) {
226
+ if (input.pathExists(path))
227
+ continue;
228
+ out.push({ file: configFile, line: lineOfKey('allowlists', path), message: `allowlists["${id}"] names "${path}", but no such file exists — the exemption is inert; drop it or fix the path` });
229
+ }
230
+ }
231
+ }
215
232
  for (const { file, line, ids } of ignores) {
216
233
  for (const id of ids) {
217
234
  if (live.has(id))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brighterly/lib-fe-codestyle",
3
- "version": "0.2.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "description": "Shared invariant-validator engine, rule pack and ESLint config for Brighterly Nuxt frontend apps",