@brighterly/lib-fe-codestyle 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +234 -178
  2. package/dist/cli.d.ts +8 -0
  3. package/dist/cli.js +181 -0
  4. package/dist/config/define-config.d.ts +11 -0
  5. package/dist/config/define-config.js +33 -0
  6. package/dist/config/load-config.d.ts +15 -0
  7. package/dist/config/load-config.js +52 -0
  8. package/dist/config/schema.d.ts +58 -0
  9. package/dist/config/schema.js +139 -0
  10. package/dist/{invariants → engine}/ast.d.ts +8 -0
  11. package/dist/{invariants → engine}/ast.js +51 -0
  12. package/dist/engine/docs.d.ts +14 -0
  13. package/dist/engine/docs.js +72 -0
  14. package/dist/engine/meta.d.ts +32 -0
  15. package/dist/engine/meta.js +47 -0
  16. package/dist/engine/migrate.d.ts +22 -0
  17. package/dist/engine/migrate.js +175 -0
  18. package/dist/engine/overrides.d.ts +36 -0
  19. package/dist/engine/overrides.js +92 -0
  20. package/dist/engine/registry.d.ts +35 -0
  21. package/dist/engine/registry.js +100 -0
  22. package/dist/engine/report.d.ts +23 -0
  23. package/dist/engine/report.js +87 -0
  24. package/dist/engine/run.d.ts +57 -0
  25. package/dist/engine/run.js +353 -0
  26. package/dist/engine/self-test.d.ts +33 -0
  27. package/dist/engine/self-test.js +141 -0
  28. package/dist/engine/types.d.ts +232 -0
  29. package/dist/{invariants → engine}/types.js +2 -1
  30. package/dist/{invariants → engine}/util.d.ts +31 -39
  31. package/dist/{invariants → engine}/util.js +59 -86
  32. package/dist/index.d.ts +16 -0
  33. package/dist/index.js +14 -0
  34. package/dist/rules/convention.d.ts +20 -0
  35. package/dist/{invariants/rules → rules}/convention.js +103 -16
  36. package/dist/rules/index.d.ts +28 -0
  37. package/dist/{invariants/rules → rules}/index.js +40 -32
  38. package/dist/{invariants/rules → rules}/regression.d.ts +1 -1
  39. package/dist/{invariants/rules → rules}/regression.js +26 -6
  40. package/dist/{invariants/rules → rules}/shared.d.ts +1 -1
  41. package/dist/validate/index.d.ts +1 -1
  42. package/dist/validate/index.js +1 -1
  43. package/package.json +8 -13
  44. package/dist/invariants/index.d.ts +0 -7
  45. package/dist/invariants/index.js +0 -5
  46. package/dist/invariants/rules/convention.d.ts +0 -18
  47. package/dist/invariants/rules/fixtures.d.ts +0 -17
  48. package/dist/invariants/rules/fixtures.js +0 -89
  49. package/dist/invariants/rules/index.d.ts +0 -55
  50. package/dist/invariants/rules/linter.d.ts +0 -10
  51. package/dist/invariants/rules/linter.js +0 -9
  52. package/dist/invariants/run.d.ts +0 -53
  53. package/dist/invariants/run.js +0 -746
  54. package/dist/invariants/self-test.d.ts +0 -46
  55. package/dist/invariants/self-test.js +0 -84
  56. package/dist/invariants/types.d.ts +0 -222
  57. /package/dist/{invariants/rules → rules}/shared.js +0 -0
package/README.md CHANGED
@@ -1,31 +1,214 @@
1
1
  # `@brighterly/lib-fe-codestyle`
2
2
 
3
- Shared, deterministic code-review machinery for the Brighterly Nuxt apps — land-app,
4
- cs-app, app-tutor, app-admin.
3
+ Shared, deterministic code review for the Brighterly Nuxt apps: a rule engine with a
4
+ binary (`codestyle`), eleven package rules, and a shared ESLint flat config. A consumer
5
+ declares one file, `codestyle.config.ts`; its own domain rules live next to it.
5
6
 
6
- It holds the **mechanisms**. Domain rules stay in each app.
7
+ **New here?** [docs/how-it-works.md](docs/how-it-works.md) explains what one run does,
8
+ the proof forms, overrides and tags. Moving a repo from 0.5.x:
9
+ [docs/migration-1.0.md](docs/migration-1.0.md).
7
10
 
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
+ ## Quick start
11
12
 
12
- ## Subpaths
13
+ ```bash
14
+ pnpm add -D @brighterly/lib-fe-codestyle
15
+ ```
13
16
 
14
- | Import | What it is |
17
+ `typescript` and `@vue/compiler-sfc` are **peers**: the engine parses your files with the
18
+ same compiler instances your build uses. Every Brighterly app already has both.
19
+
20
+ ```ts
21
+ // codestyle.config.ts — the only required file
22
+ import { defineConfig } from '@brighterly/lib-fe-codestyle'
23
+ import repo from './codestyle/index.ts' // { custom: [...] } — this repo's own rules
24
+
25
+ export default defineConfig({
26
+ settings: { // app landmarks the package rules read
27
+ scssTokensFile: 'app/assets/scss/_colors.scss',
28
+ typographySource: 'the font() mixin',
29
+ scriptAttrOrder: 'setup-first',
30
+ },
31
+ rules: { // one level map for package AND custom rules
32
+ 'script-attr-order': 'error',
33
+ 'vueuse-first': 'off',
34
+ },
35
+ ...repo, // contributes `custom` only — levels stay in `rules`
36
+ overrides: [ // never "the rule is off" — always "this place, this reason"
37
+ { files: ['app/components/UI/Buttons/BtnAuth*.vue'], rules: { 'inline-scss': 'off' },
38
+ reason: 'UI-kit exception, decided 2026-08-18' },
39
+ { files: ['app/components/Views/ViewQuizExp132.vue'], rules: { 'journey-coverage': 'warn' },
40
+ reason: 'funnel still being built; until 2026-10-31' },
41
+ ],
42
+ })
43
+ ```
44
+
45
+ A rule not named in `rules` runs at its own `level`. Keep the spread object to `{ custom }`:
46
+ object spread replaces a same-named key, so a `rules` map inside it and the one above would
47
+ silently overwrite each other.
48
+
49
+ Two loading facts, because Node runs this file with its own type stripping:
50
+
51
+ - **Relative imports spell `.ts`** (`./codestyle/index.ts`, and inside `codestyle/` too) —
52
+ Node does not rewrite `.js` to `.ts`. The tsconfig that covers these files needs
53
+ `allowImportingTsExtensions`.
54
+ - **Erasable syntax only** — no `enum`, `namespace` or parameter properties. `codestyle`
55
+ names the file and the reason if one slips in. Node 22.18+ is required for a `.ts`
56
+ config; `codestyle.config.mjs` works on older Node — unless it imports `.ts` rule files,
57
+ which still need 22.18+.
58
+
59
+ ### Config keys
60
+
61
+ | Key | Default | What it is |
62
+ |---|---|---|
63
+ | `settings` | `{}` | app landmarks for the package rules (`SharedRuleSettings` — see below) |
64
+ | `rules` | `{}` | `name → 'off' \| 'warn' \| 'error'` for package and custom rules. An unknown name, an engine rule (`IG0`–`IG2`) or an ESLint/test-owned declaration here is a config error |
65
+ | `custom` | `[]` | this repo's rules (each carries a proof) and check-less declarations owned by ESLint or a test. Shape-checked: a rule has no `owner` and needs `level` (`off`/`warn`/`error`), a non-empty `scope`, a `check` function and `title`/`why`/`fix`; a declaration has `owner: 'eslint' \| 'test'` and `ref`/`title`/`why`. Errors name the field (`custom[2].level`) |
66
+ | `overrides` | `[]` | `{ files, rules, reason }` — a level for some rules on some files, reason mandatory |
67
+ | `files` | `['app', 'config', 'tests', 'codestyle']` | scan roots: directories, files or globs |
68
+ | `ignores` | `['node_modules', '.nuxt', '.output', 'dist', 'coverage']` | pruned from the walk: names, paths or globs |
69
+ | `ruleSources` | `['codestyle/']` | where this repo's rules live: a staged change there triggers the self-test; their files are exempt from ignore-comment parsing (fixture strings carry ignore comments on purpose) |
70
+ | `docs` | `[]` | `{ template, output, tags? }` rendered by `codestyle docs` |
71
+ | `validate` | `[]` | `{ name, run }` steps for `codestyle validate` |
72
+ | `precommit` | `[]` | `{ name, run }` steps `codestyle precommit` runs after the staged check |
73
+
74
+ An unknown top-level key is a config error, so a typo never silently does nothing.
75
+
76
+ ## Package rules
77
+
78
+ | Rule | Default | Catches |
79
+ |---|---|---|
80
+ | `no-bare-storage` | warn* | bare `localStorage` / `sessionStorage` — the accessor itself throws on Safari ITP and in in-app webviews |
81
+ | `inline-scss` | error | SCSS rules inside a Vue `<style>` block instead of a referenced `.scss` file |
82
+ | `guarded-json-parse` | warn* | `JSON.parse` with no enclosing `try`/`catch` — a corrupt stored value breaks that path permanently |
83
+ | `font-face-single-family` | error | `@font-face` declaring a font *stack* — an invalid descriptor, so the browser drops the face silently |
84
+ | `vueuse-first` | warn | a hand-rolled browser API with a VueUse equivalent |
85
+ | `ui-kit-unique-names` | warn | a component whose name duplicates a UI-kit component |
86
+ | `scss-tokens` | warn | a literal colour in SCSS instead of a design token |
87
+ | `date-format-enum` | warn | a hand-rolled date format string, or the date part cut out of `toISOString()` |
88
+ | `types-layout` | warn | a type file outside `types/<entity>/{enums,interfaces,types}.ts` |
89
+ | `typography-source` | warn | a raw `font-family` / `font-size` / `font-weight` instead of the app's typography source |
90
+ | `script-attr-order` | warn | `<script>` attribute order differing from the app's canonical |
91
+
92
+ `error` is reserved for crash-class rules; conventions ship at `warn` so each app measures
93
+ its backlog before anything blocks. `no-bare-storage` and `guarded-json-parse` (marked *)
94
+ ship at `warn` as an interim state while the fleet backlog exists; their target is
95
+ `error`. Underscore-prefixed SCSS partials are declaration files and exempt from
96
+ `scss-tokens` and `typography-source`. Hover a `packageRuleNames` member for the rule's
97
+ contract.
98
+
99
+ ### `settings`
100
+
101
+ ```ts
102
+ settings: {
103
+ safeStorageModule: 'safeLocalStorage from @helpers/safe-local-storage', // no-bare-storage / guarded-json-parse messages
104
+ safeStorageFile: 'app/utils/helpers/safe-local-storage.ts', // no-bare-storage exempts the wrapper itself
105
+ safeJsonParse: 'parseStoredJson() from @helpers/safe-local-storage',
106
+ scssTokensFile: 'app/assets/scss/_colors.scss',
107
+ uiKitRoot: 'app/components/UI', // ui-kit-unique-names (default shown)
108
+ dateFormatModule: 'DateFormat from @ts-types/date/enums',
109
+ typesRoot: 'app/types', // types-layout (default shown)
110
+ typographySource: 'the font() mixin from @scss/_mixins.scss',
111
+ scriptAttrOrder: 'setup-first', // or 'lang-first' (default 'setup-first')
112
+ }
113
+ ```
114
+
115
+ A missing key degrades the rule's *message* or falls back to the documented default —
116
+ never silences the rule.
117
+
118
+ ## Commands
119
+
120
+ | Command | What it does |
15
121
  |---|---|
16
- | `@brighterly/lib-fe-codestyle/invariants` | the invariants engine — `run(options)`, the rule contract, AST helpers, util |
17
- | `@brighterly/lib-fe-codestyle/invariants/rules` | the common rule pack, portable across all apps |
18
- | `@brighterly/lib-fe-codestyle/eslint` | the shared flat config, its seven local rules, and the maps behind them |
19
- | `@brighterly/lib-fe-codestyle/validate` | the aggregating runner behind `pnpm validate` |
122
+ | `codestyle check` | runs the rules. Flags: `--staged` (this commit), `--stdin --file=<path>` (check piped content as that file), `--json`, `--only=<a,b>` (by name; `--rule=<a,b>` is an alias, merged with `--only`), `--tags=<a,b>` (by tag — a tag no rule carries is exit 2), `--no-self-test`, `--no-color` |
123
+ | `codestyle list` | every rule: `name · level · owner · tags` |
124
+ | `codestyle self-test [--package]` | proves this repo's custom rules (`self-test: 0 rule(s) proven`, exit 0, when there are none); `--package` proves the package's own rules |
125
+ | `codestyle docs [--check]` | renders every `docs` entry; `--check` writes nothing and fails on a stale or missing output. No `docs` entries is exit 2 |
126
+ | `codestyle validate` | runs the `validate` steps — every step, one exit code. No steps is exit 2 |
127
+ | `codestyle precommit` | `check --staged` first; if it fails, the steps are skipped. Then the `precommit` steps run as one group — every step, one exit code. Any other argument is passed to `check` |
128
+ | `codestyle migrate [--write]` | moves a 0.5.x repo: suppression spelling, renamed ids, a config skeleton |
129
+
130
+ Every command takes `--root=<path>` (default: the working directory). Exit codes:
131
+ **0** clean or warnings only · **1** an error-level finding, a failed self-test, a stale doc
132
+ or a failed step · **2** the tool itself failed (bad flags, no or bad config — a malformed
133
+ `until` such as `until 2026-1-5` included — or a crash).
134
+
135
+ ## Suppressions and overrides
136
+
137
+ One line, reviewed: `// codestyle-ignore: <name> — <reason>` suppresses that line and the
138
+ next. The reason is mandatory. Three engine rules police the escape hatch:
139
+
140
+ - **IG0** — an ignore without a reason;
141
+ - **IG1** — a name that is not a live rule (a renamed package rule says what it became), or
142
+ an override that no longer applies (a glob that matches no file, an `until` that has passed);
143
+ - **IG2** — an ignore naming a rule declared `noIgnore`; the comment suppresses nothing.
144
+
145
+ `// invariant-ignore:` is still accepted through 1.x with a one-line deprecation warning per
146
+ file; `codestyle migrate --write` rewrites it.
147
+
148
+ A place-wide exemption is an **override**: `{ files, rules, reason }`. An override can only
149
+ lower a level, never raise it. A `rules` entry is the base level of its rule and a ceiling
150
+ over a finding's own level; without one, a finding's own level wins. A reason may carry `until YYYY-MM-DD`: from `until − 7` days
151
+ through `until` itself (inclusive) the exemption reports at `warn`; after the date it stops
152
+ applying and is reported. A malformed date is a config error. A rule
153
+ may require that date on every override of it (`exemptionsRequireUntil`).
20
154
 
21
- `exports` **is** the public surface. Anything not listed is private — a linked package
22
- resolves deep paths that break the moment it is published.
155
+ ## `--json`
23
156
 
24
- ## The one design rule: export data, never factories
157
+ ```ts
158
+ { schemaVersion: 1, ok, scanned, errors, warnings,
159
+ rules: [{ name, level, title, tags, owner: 'package' | 'custom' | 'eslint' | 'test' }],
160
+ findings: [{ rule, level, file, line, message }],
161
+ skipped?: { file, reason }, // --stdin: outside `files`, under `ignores`, or no rule scopes it
162
+ selfTest?: { ok, failures } } // present whenever the self-test ran
163
+
164
+ // a tool failure (exit 2) under --json:
165
+ { schemaVersion: 1, ok: false, error }
166
+ ```
167
+
168
+ `owner` is `package` (this package's rules), `custom` (the repo's own), `eslint` or `test`
169
+ (declarations: never run, here so their names stay live). `schemaVersion` changes only with
170
+ a breaking shape change. ESLint- and test-owned declarations appear in `rules` with no
171
+ findings. Output always drains before the process exits — a large report through a pipe
172
+ arrives whole.
173
+
174
+ ## What checks what
25
175
 
26
- Consumers compose. There is no `defineBrighterlyEslint({...})`, and there never will be —
27
- the four apps' configs are ~95% identical and the variance is mostly drift, so a factory
28
- would parameterise accidental difference.
176
+ | Tool | Answers | Runs |
177
+ |---|---|---|
178
+ | ESLint (+ this package's flat config and local rules) | "is this file well-formed by our style?" — one file's AST, IDE feedback, autofix | editor, pre-commit, CI |
179
+ | **This package** | "does the repo still satisfy its contracts?" — anything needing a second file, a registry, the test tree, SCSS/assets/config surfaces, project semantics, or an audited exemption | pre-commit (`--staged`), CI (whole repo), the AI write-time hook (`--stdin`) |
180
+ | Auditor | "what in the repo's state needs a human decision?" — dead code, dependency health, churn, LLM change review, and the trend of this package's warnings | explicit, human-triggered |
181
+
182
+ If one file's AST decides it and a plugin or a small shared rule can say it, it is ESLint —
183
+ declared in `custom` with `owner: 'eslint'` so there is one catalogue. If the author can fix
184
+ it before the commit lands, gate it here; if it needs history, external signals or judgment,
185
+ it belongs to the auditor, which reads `codestyle check --json` rather than grepping.
186
+
187
+ ## Wiring a consumer
188
+
189
+ ```jsonc
190
+ // package.json
191
+ "scripts": {
192
+ "codestyle": "codestyle check",
193
+ "codestyle:e2e": "codestyle check --tags=e2e",
194
+ "validate": "codestyle validate"
195
+ }
196
+ ```
197
+
198
+ - **Pre-commit**: the hook file is one line, `pnpm exec codestyle precommit`. Keep it in a
199
+ tracked directory and arm it from `postinstall` with `git config core.hooksPath <dir> || true`
200
+ (the `|| true` keeps installs without `.git`, such as Docker builds, green).
201
+ - **CI**: one step, `pnpm exec codestyle validate`, with the full chain declared in
202
+ `validate` (the codestyle check, lint, tests, typecheck).
203
+ - **AI write-time hook**: `codestyle check --stdin --file=<repo-relative path> [--tags=<x>]`
204
+ checks content before it reaches the disk; a path outside `files`, under `ignores`, or scoped
205
+ by no selected rule comes back as `skipped`.
206
+ - **Agent rules file**: declare `docs: [{ template, output, tags }]` and run `codestyle docs`;
207
+ the generated file says so in its first line and is never edited by hand. Put
208
+ `codestyle docs --check` in `validate` so drift fails the gate.
209
+ - **CLAUDE.md**: paste [docs/consumer-claude-md.md](docs/consumer-claude-md.md).
210
+
211
+ ## The ESLint config — export data, never factories
29
212
 
30
213
  ```js
31
214
  // an app that needs nothing extra
@@ -46,194 +229,67 @@ export default [
46
229
  ]
47
230
  ```
48
231
 
49
- ESLint **replaces** rule options rather than merging them, so appending an override without
50
- spreading the shared map silently discards every shared entry. That is why the maps are
51
- exported as data.
52
-
53
-
54
- ## What the rule pack ships
55
-
56
- ### Invariants — `@brighterly/lib-fe-codestyle/invariants/rules`
57
-
58
- | Rule | Level | Checks |
59
- |---|---|---|
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.
78
- Underscore-prefixed SCSS partials (`_extends.scss`, `_colors-*.scss`, …) are declaration
79
- files by convention in every Brighterly app and are exempt from `core/C3`/`core/C6` wholesale.
80
-
81
- `core/R*` were ported from land-app, where each was a production scar (as `R1`, `R7`, `R18`,
82
- `R28` — renumbered sequentially when the prefix landed, pre-publish, while no released
83
- version pinned the old tokens). `core/C*` are conventions with
84
- no incident behind them *yet* — they ship at `warn` so a consuming app can measure what they
85
- find before anyone decides to block on them. Promoting one to `error` is a **major** bump.
86
-
87
- Fixtures ship with the rules (`commonFixtures`), so `--self-test` works in any consumer
88
- without it having to write fixtures for rules it did not author.
89
-
90
- ### ESLint — `@brighterly/lib-fe-codestyle/eslint`
91
-
92
- Seven local rules: `padding-after-macros`, `async-components-after-macros`,
232
+ ESLint **replaces** rule options rather than merging them, so an override that does not
233
+ spread the shared map silently discards every shared entry — that is why the maps are
234
+ exported as data. Seven local rules: `padding-after-macros`, `async-components-after-macros`,
93
235
  `no-import-group-labels`, `ref-prefix-prop-mirrors`, `require-component-name`,
94
- `on-prefixed-callbacks`, and `optional-callback-chaining` (new — an optional callback
95
- invoked without `?.` throws for the first caller that omits it).
96
-
97
- Plus the shared flat config: restricted syntax, restricted imports, the types-organization
98
- blocks, and the `check-file` naming maps — all exported as data so a consumer can spread
99
- and extend them.
100
-
101
- ## App-specific paths: `settings`, not parameters
236
+ `on-prefixed-callbacks`, `optional-callback-chaining`.
102
237
 
103
- A shared rule must not hardcode a consuming app's paths. Each app declares them once:
104
-
105
- ```ts
106
- // invariants/config.mts
107
- export const settings = {
108
- safeStorageModule: 'safeLocalStorage from @helpers/safe-local-storage',
109
- safeStorageFile: 'app/utils/helpers/safe-local-storage.ts', // core/R1 auto-exempts the wrapper itself
110
- safeJsonParse: 'parseStoredJson() from @helpers/safe-local-storage',
111
- scssTokensFile: 'app/assets/scss/_colors.scss',
112
- uiKitRoot: 'app/components/UI',
113
- dateFormatModule: 'DateFormat from @ts-types/date/enums',
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
118
- }
119
- ```
238
+ ## Imports
120
239
 
121
- A missing key degrades the rule's *message*, never its firing. A rule that went quiet
122
- because a path was unset would be the exact silent failure this package exists to prevent.
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
125
- default above, so leaving them unset still never silences the rule.
126
-
127
- ## What consumers see in the IDE
128
-
129
- Three hover surfaces, so "what rules do we have and what do they look for" never
130
- requires opening this repo:
131
-
132
- - **`sharedRuleIds`** (from `…/invariants/rules`) — the catalog: one constant per rule,
133
- each JSDoc'd with level, what it checks, and the fix. Use them as `allowlists` /
134
- `levels` keys (`[sharedRuleIds.core/C6]: 'off'`) and hover for the contract. A test pins
135
- the catalog to the shipped rules, so it cannot drift.
136
- - **`SharedRuleSettings`** (from `…/invariants`) — annotate the app's `settings` const
137
- with it to get completion plus the per-key contract, and a type error on an invalid
138
- `scriptAttrOrder` value.
139
- - **ESLint squiggles** — every local rule ships `meta.docs.description`, which is what
140
- the editor shows on hover over a violation.
141
-
142
- PhpStorm / WebStorm notes: the IDE runs the **project's own ESLint binary** against the
143
- flat `eslint.config.mjs`, so rules defined inside this package just work — nothing to
144
- install or configure beyond the default "Automatic ESLint configuration". Two real
145
- behaviours to know: the ESLint daemon caches the resolved config, so after upgrading
146
- this package run *ESLint: Restart Service* (or reopen the project) to pick up new
147
- rules; and the invariants engine is **not** an IDE inspection — its findings appear in
148
- the terminal (`pnpm validate:invariants`) and the pre-commit hook only.
240
+ | Import | What it is |
241
+ |---|---|
242
+ | `@brighterly/lib-fe-codestyle` | `defineConfig`; the rule contract types (`Rule`, `RuleContext`, the proofs, …); the helpers custom rules share (`eachLine`, `setting`, `stripCodeComments`, `matchesAny`, the AST helpers, …); `packageRuleNames` |
243
+ | `@brighterly/lib-fe-codestyle/eslint` | the shared flat config, its local rules and the maps behind them |
149
244
 
150
- ## Wiring a consumer
245
+ Everything else is reached through the `codestyle` binary. `src/` ships compiled as `dist/`
246
+ (Node does not strip types inside `node_modules`); `eslint/` ships as plain `.mjs`.
151
247
 
152
- 1. `pnpm add -D @brighterly/lib-fe-codestyle` — plus, if not already direct devDeps,
153
- `@vue/compiler-sfc` and `typescript`. Both are **peers, not dependencies**, on
154
- purpose: the validator must parse your files with the *same* compiler instance your
155
- build uses, so the package dedupes onto the app's copy instead of carrying its own.
156
- Every Brighterly Nuxt app already has both in the store — the devDep line adds a
157
- symlink, not a download.
158
- 2. An `invariants/` directory with `cli.mts` (a ~20-line composition root — copy
159
- land-app's), `config.mts` (allowlists, levels, retired, unmaintained, `settings` —
160
- see above), your app-local rules, and `specs/` for checks needing real module
161
- resolution.
162
- 3. A missing peer is loud, never silent: pnpm warns at install (and auto-installs by
163
- default), and the engine's lazy `import()` throws on the first AST rule otherwise.
164
- 4. Arm the pre-commit hook from install, not by hand: keep the hook script in a tracked
165
- directory and add `git config core.hooksPath <that-dir> || true` to `postinstall`
166
- (the `|| true` keeps .git-less installs — Docker builds — green). Setup stays
167
- `pnpm i && pnpm dev`; a hand-made `.git/hooks` symlink dies with every fresh clone.
168
-
169
- An app with a pre-extraction fork of the engine (app-funnels) deletes its copied
170
- `ast/util/run/self-test` plumbing and keeps only domain rules + config; its existing
171
- compiler-sfc devDep already satisfies the peer.
172
-
173
- ## Why `src/` is compiled but `eslint/` is not
174
-
175
- Node refuses to strip types inside `node_modules`
176
- (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), so the TypeScript half ships as `dist/`.
177
- The ESLint config and its seven rules are already plain `.mjs` and ship as-is.
178
-
179
- ## Rule IDs
180
-
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
183
- defined, renumbered or recycled in a consumer — and it keeps the two number lines
184
- independent: every app allocates its own `R1, R2, …` naturally, with no risk of
185
- colliding with a rule the package ships later. `run()` still exits 2 on a duplicate,
186
- so a collision is loud, not silent.
187
-
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
190
- meaning provenance — `R` regression scar, `B` business, `C` convention, `L`
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.
248
+ In the IDE: `defineConfig` types the whole config, a `packageRuleNames` member's hover is
249
+ that rule's contract, and ESLint squiggles carry each local rule's description. The rule
250
+ engine is not an IDE inspection: its findings appear in the terminal and the pre-commit
251
+ hook. After upgrading this package, run *ESLint: Restart Service*.
193
252
 
194
253
  ## Versioning is about blocking power
195
254
 
196
255
  One question decides the bump: **can this change turn a green consumer red, or break
197
- something a consumer wrote against a rule id?** Yes → major. Adds signal without
256
+ something a consumer wrote against a rule name?** Yes → major. Adds signal without
198
257
  blocking → minor. Everything else → patch.
199
258
 
200
259
  | Change | Bump | Why |
201
260
  |---|---|---|
202
- | New invariant rule at `error` | **major** | can turn a green repo red on install |
261
+ | New package rule at `error` | **major** | can turn a green repo red on install |
203
262
  | Promoting a rule `warn` → `error` | **major** | same — new blocking power |
204
263
  | Widening a rule's `scope`, narrowing its `exclude`, or making its match stricter | **major** | existing code that passed now fails |
205
- | Renaming, renumbering or removing a rule id | **major** | breaks every `invariant-ignore:` comment, `allowlists`/`levels` key and doc that names the token — `IG1` starts firing in every consumer |
264
+ | Renaming or removing a rule name | **major** | breaks every `codestyle-ignore:` comment, `rules`/`overrides` key and doc that names it — `IG1` starts firing in every consumer |
206
265
  | New ESLint rule at `error`, or adding a selector to `restrictedSyntax` / `restrictedImports` / the `check-file` maps | **major** | the shared config is spread into every app's lint gate |
207
- | Engine behavior change (`run()` exit codes, finding shape, config contract) | **major** | consumers' composition roots are written against it |
208
- | New invariant rule at `warn` | minor | new signal, blocks nothing |
266
+ | Engine behaviour change (exit codes, the `--json` shape, the config or rule contract) | **major** | consumers' configs, hooks and CI are written against it |
267
+ | New package rule at `warn` | minor | new signal, blocks nothing |
209
268
  | New ESLint rule at `warn` | minor | same |
210
269
  | New export (a map, a helper, a settings key a rule *optionally* reads) | minor | additive surface |
211
270
  | Demoting a rule `error` → `warn`, or narrowing its scope | patch | strictly relaxing |
212
- | Bug fix that removes false positives, message/`why`/`fix` wording, fixture improvements | patch | nothing new fires |
271
+ | Bug fix that removes false positives, message/`why`/`fix` wording, proof improvements | patch | nothing new fires |
213
272
 
214
- Two edges worth naming: a bug fix that removes **false negatives** (the rule now
215
- catches what it always claimed to) is still **major** — correctness of intent does not
216
- change what happens in a consumer's CI. And a new `settings` key a rule *requires* is
217
- major; one it merely reads for a better message is minor (a missing key degrades the
218
- message, never the firing — see above).
273
+ Two edges worth naming: a bug fix that removes **false negatives** (the rule now catches what
274
+ it always claimed to) is still **major** — correctness of intent does not change what happens
275
+ in a consumer's CI. And a new `settings` key a rule *requires* is major; one it merely reads
276
+ for a better message is minor.
219
277
 
220
278
  ## Publishing
221
279
 
222
- The package is published **publicly** to npmjs.org under the `@brighterly` org — the
223
- same channel as `@brighterly/lib-lumee-ui`, so consumers need no registry config or
224
- token. `publishConfig.access: "public"` in `package.json` makes the scoped publish
225
- work without a paid org plan.
280
+ Published **publicly** to npmjs.org under the `@brighterly` org — the same channel as
281
+ `@brighterly/lib-lumee-ui`, so consumers need no registry config or token.
226
282
 
227
283
  ```bash
228
- npm login # must be a member of the @brighterly org
229
- npm version <patch|minor|major> # per the blocking-power table above
230
- npm publish # prepublishOnly rebuilds dist and runs the tests first
284
+ npm login # must be a member of the @brighterly org
285
+ npm version <patch|minor|major> # per the blocking-power table above
286
+ npm publish # prepublishOnly rebuilds dist and runs the tests first
231
287
  git push && git push --tags
232
288
  ```
233
289
 
234
- `dist/` is gitignored; `prepublishOnly` (`clean → build → test`) guarantees the
235
- tarball is built from the current sources, never a stale artifact.
290
+ `dist/` is gitignored; `prepublishOnly` (`clean → build → test`) guarantees the tarball is
291
+ built from the current sources.
236
292
 
237
293
  ## Design
238
294
 
239
- `land-app/docs/superpowers/specs/2026-09-22-fe-codestyle-design.md`
295
+ [docs/superpowers/specs/2026-10-16-consumer-architecture-v1-design.md](docs/superpowers/specs/2026-10-16-consumer-architecture-v1-design.md)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `codestyle` — a thin argv switch over the engine. No engine logic lives here.
4
+ *
5
+ * Exit codes: 0 clean · 1 a finding at error, a failed proof, a stale doc or a failed step ·
6
+ * 2 the tool itself failed (usage, config).
7
+ */
8
+ export {};