@shivaedev/quality 0.17.0 → 0.19.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.
Files changed (267) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +151 -508
  3. package/biome/declarations.json +0 -45
  4. package/biome/plugins/constant-names.grit +1 -1
  5. package/biome/plugins/effect-fn-functions.grit +1 -1
  6. package/biome/preset.json +16 -9
  7. package/dist/baseline/compare.d.ts.map +1 -1
  8. package/dist/baseline/format.d.ts.map +1 -1
  9. package/dist/baseline/legacy.d.ts.map +1 -1
  10. package/dist/baseline/rewrite.d.ts.map +1 -1
  11. package/dist/baseline/update.d.ts.map +1 -1
  12. package/dist/baseline/update.js.map +1 -1
  13. package/dist/biome/nativeEffectCall.d.ts +4 -0
  14. package/dist/biome/nativeEffectCall.d.ts.map +1 -0
  15. package/dist/biome/nativeEffectCall.js +34 -0
  16. package/dist/biome/nativeEffectCall.js.map +1 -0
  17. package/dist/biome/recognizeDiagnostics.d.ts +3 -0
  18. package/dist/biome/recognizeDiagnostics.d.ts.map +1 -0
  19. package/dist/biome/recognizeDiagnostics.js +58 -0
  20. package/dist/biome/recognizeDiagnostics.js.map +1 -0
  21. package/dist/biome/report.d.ts +10 -0
  22. package/dist/biome/report.d.ts.map +1 -1
  23. package/dist/biome/report.js +23 -5
  24. package/dist/biome/report.js.map +1 -1
  25. package/dist/biome/run.js.map +1 -1
  26. package/dist/biome/syntax.d.ts +12 -0
  27. package/dist/biome/syntax.d.ts.map +1 -0
  28. package/dist/biome/syntax.js +29 -0
  29. package/dist/biome/syntax.js.map +1 -0
  30. package/dist/cli/args.d.ts +10 -1
  31. package/dist/cli/args.d.ts.map +1 -1
  32. package/dist/cli/args.js +25 -3
  33. package/dist/cli/args.js.map +1 -1
  34. package/dist/cli/baseline.d.ts +1 -1
  35. package/dist/cli/baseline.d.ts.map +1 -1
  36. package/dist/cli/baseline.js.map +1 -1
  37. package/dist/cli/fix.d.ts +6 -6
  38. package/dist/cli/fix.d.ts.map +1 -1
  39. package/dist/cli/fix.js +1 -1
  40. package/dist/cli/fix.js.map +1 -1
  41. package/dist/cli/lint.d.ts.map +1 -1
  42. package/dist/cli/migrate.d.ts.map +1 -1
  43. package/dist/cli/run-main.d.ts +1 -1
  44. package/dist/cli/run-main.d.ts.map +1 -1
  45. package/dist/cli/run-main.js.map +1 -1
  46. package/dist/cli/shrink.d.ts.map +1 -1
  47. package/dist/cli.js +14 -0
  48. package/dist/cli.js.map +1 -1
  49. package/dist/config/decode.d.ts +3 -3
  50. package/dist/config/decode.d.ts.map +1 -1
  51. package/dist/config/decode.js +5 -2
  52. package/dist/config/decode.js.map +1 -1
  53. package/dist/config/load.d.ts.map +1 -1
  54. package/dist/config/load.js +11 -6
  55. package/dist/config/load.js.map +1 -1
  56. package/dist/config/resolve.d.ts.map +1 -1
  57. package/dist/config/resolve.js +8 -12
  58. package/dist/config/resolve.js.map +1 -1
  59. package/dist/config.d.ts +10 -11
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js.map +1 -1
  62. package/dist/decoded.d.ts +4 -4
  63. package/dist/decoded.d.ts.map +1 -1
  64. package/dist/decoded.js.map +1 -1
  65. package/dist/engine/baseline-file.d.ts.map +1 -1
  66. package/dist/engine/evaluate.d.ts.map +1 -1
  67. package/dist/engine/run-rules.d.ts.map +1 -1
  68. package/dist/engine/session.d.ts.map +1 -1
  69. package/dist/engine/session.js.map +1 -1
  70. package/dist/engine/violation.d.ts +1 -1
  71. package/dist/engine/violation.d.ts.map +1 -1
  72. package/dist/engine/violation.js.map +1 -1
  73. package/dist/exceptions/registry.d.ts.map +1 -1
  74. package/dist/git/base.d.ts.map +1 -1
  75. package/dist/git/command.d.ts.map +1 -1
  76. package/dist/git/history.d.ts.map +1 -1
  77. package/dist/hooks/command.d.ts +13 -0
  78. package/dist/hooks/command.d.ts.map +1 -0
  79. package/dist/hooks/command.js +20 -0
  80. package/dist/hooks/command.js.map +1 -0
  81. package/dist/hooks/install.d.ts +11 -0
  82. package/dist/hooks/install.d.ts.map +1 -0
  83. package/dist/hooks/install.js +52 -0
  84. package/dist/hooks/install.js.map +1 -0
  85. package/dist/hooks/location.d.ts +9 -0
  86. package/dist/hooks/location.d.ts.map +1 -0
  87. package/dist/hooks/location.js +22 -0
  88. package/dist/hooks/location.js.map +1 -0
  89. package/dist/hooks/preCommit.d.ts +5 -0
  90. package/dist/hooks/preCommit.d.ts.map +1 -0
  91. package/dist/hooks/preCommit.js +50 -0
  92. package/dist/hooks/preCommit.js.map +1 -0
  93. package/dist/hooks/shim.d.ts +4 -0
  94. package/dist/hooks/shim.d.ts.map +1 -0
  95. package/dist/hooks/shim.js +26 -0
  96. package/dist/hooks/shim.js.map +1 -0
  97. package/dist/hooks/uninstall.d.ts +4 -0
  98. package/dist/hooks/uninstall.d.ts.map +1 -0
  99. package/dist/hooks/uninstall.js +19 -0
  100. package/dist/hooks/uninstall.js.map +1 -0
  101. package/dist/imports/browserGlobals.d.ts +3 -0
  102. package/dist/imports/browserGlobals.d.ts.map +1 -0
  103. package/dist/imports/browserGlobals.js +125 -0
  104. package/dist/imports/browserGlobals.js.map +1 -0
  105. package/dist/imports/browserNames.d.ts +2 -0
  106. package/dist/imports/browserNames.d.ts.map +1 -0
  107. package/dist/imports/browserNames.js +78 -0
  108. package/dist/imports/browserNames.js.map +1 -0
  109. package/dist/imports/fences/check.d.ts +4 -0
  110. package/dist/imports/fences/check.d.ts.map +1 -0
  111. package/dist/imports/fences/check.js +10 -0
  112. package/dist/imports/fences/check.js.map +1 -0
  113. package/dist/imports/fences/declarations.d.ts +5 -0
  114. package/dist/imports/fences/declarations.d.ts.map +1 -0
  115. package/dist/imports/fences/declarations.js +29 -0
  116. package/dist/imports/fences/declarations.js.map +1 -0
  117. package/dist/imports/fences/discover.d.ts +5 -0
  118. package/dist/imports/fences/discover.d.ts.map +1 -0
  119. package/dist/imports/fences/discover.js +41 -0
  120. package/dist/imports/fences/discover.js.map +1 -0
  121. package/dist/imports/fences/match.js.map +1 -1
  122. package/dist/imports/graph.js +2 -2
  123. package/dist/imports/graph.js.map +1 -1
  124. package/dist/imports/projects.js.map +1 -1
  125. package/dist/imports/runtime.d.ts +5 -0
  126. package/dist/imports/runtime.d.ts.map +1 -0
  127. package/dist/imports/runtime.js +32 -0
  128. package/dist/imports/runtime.js.map +1 -0
  129. package/dist/imports/workspace.d.ts +1 -1
  130. package/dist/imports/workspace.d.ts.map +1 -1
  131. package/dist/imports/workspace.js +5 -1
  132. package/dist/imports/workspace.js.map +1 -1
  133. package/dist/inventory/collect.d.ts.map +1 -1
  134. package/dist/inventory/collect.js +8 -1
  135. package/dist/inventory/collect.js.map +1 -1
  136. package/dist/inventory/filesystem.d.ts.map +1 -1
  137. package/dist/inventory/ignore-scope.d.ts.map +1 -1
  138. package/dist/inventory/trackedPaths.d.ts +3 -0
  139. package/dist/inventory/trackedPaths.d.ts.map +1 -0
  140. package/dist/inventory/trackedPaths.js +26 -0
  141. package/dist/inventory/trackedPaths.js.map +1 -0
  142. package/dist/inventory/walk.d.ts +4 -4
  143. package/dist/inventory/walk.d.ts.map +1 -1
  144. package/dist/inventory/walk.js +32 -12
  145. package/dist/inventory/walk.js.map +1 -1
  146. package/dist/naming/testName.d.ts.map +1 -1
  147. package/dist/report/plural.d.ts.map +1 -1
  148. package/dist/report/render.d.ts.map +1 -1
  149. package/dist/report/stale.d.ts.map +1 -1
  150. package/dist/rules/built-in.d.ts +2 -3
  151. package/dist/rules/built-in.d.ts.map +1 -1
  152. package/dist/rules/built-in.js +6 -0
  153. package/dist/rules/built-in.js.map +1 -1
  154. package/dist/rules/comments/kinds.d.ts.map +1 -1
  155. package/dist/rules/comments/max-per-file.js +1 -1
  156. package/dist/rules/comments/max-per-file.js.map +1 -1
  157. package/dist/rules/comments/pattern-rule.d.ts.map +1 -1
  158. package/dist/rules/comments/scan.d.ts.map +1 -1
  159. package/dist/rules/files/folderNames.js +1 -1
  160. package/dist/rules/files/folderNames.js.map +1 -1
  161. package/dist/rules/files/otherNames.d.ts.map +1 -1
  162. package/dist/rules/files/otherNames.js +1 -10
  163. package/dist/rules/files/otherNames.js.map +1 -1
  164. package/dist/rules/imports/bundleInputs.d.ts +7 -0
  165. package/dist/rules/imports/bundleInputs.d.ts.map +1 -0
  166. package/dist/rules/imports/bundleInputs.js +77 -0
  167. package/dist/rules/imports/bundleInputs.js.map +1 -0
  168. package/dist/rules/imports/fences.d.ts +1 -2
  169. package/dist/rules/imports/fences.d.ts.map +1 -1
  170. package/dist/rules/imports/fences.js +4 -14
  171. package/dist/rules/imports/fences.js.map +1 -1
  172. package/dist/rules/imports/manifest.d.ts +2 -0
  173. package/dist/rules/imports/manifest.d.ts.map +1 -0
  174. package/dist/rules/imports/manifest.js +98 -0
  175. package/dist/rules/imports/manifest.js.map +1 -0
  176. package/dist/rules/imports/noTestCode.d.ts +2 -0
  177. package/dist/rules/imports/noTestCode.d.ts.map +1 -0
  178. package/dist/rules/imports/noTestCode.js +17 -0
  179. package/dist/rules/imports/noTestCode.js.map +1 -0
  180. package/dist/rules/imports/runtime.d.ts +2 -0
  181. package/dist/rules/imports/runtime.d.ts.map +1 -0
  182. package/dist/rules/imports/runtime.js +41 -0
  183. package/dist/rules/imports/runtime.js.map +1 -0
  184. package/dist/rules/max-lines.d.ts +0 -1
  185. package/dist/rules/max-lines.d.ts.map +1 -1
  186. package/dist/rules/max-lines.js +3 -10
  187. package/dist/rules/max-lines.js.map +1 -1
  188. package/dist/rules/suppressions/biome/configs.d.ts.map +1 -1
  189. package/dist/rules/suppressions/biome/declarations.d.ts.map +1 -1
  190. package/dist/rules/suppressions/biome/json.d.ts.map +1 -1
  191. package/dist/rules/suppressions/biome/resolve.d.ts.map +1 -1
  192. package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -1
  193. package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -1
  194. package/dist/rules/suppressions/biome-overrides.js +21 -4
  195. package/dist/rules/suppressions/biome-overrides.js.map +1 -1
  196. package/dist/rules/suppressions/biomeRecommended.js +2 -2
  197. package/dist/rules/suppressions/biomeRecommended.js.map +1 -1
  198. package/dist/rules/suppressions/directives.d.ts.map +1 -1
  199. package/dist/rules/suppressions/no-inline.js +1 -1
  200. package/dist/rules/suppressions/no-inline.js.map +1 -1
  201. package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -1
  202. package/dist/rules/syntax.d.ts.map +1 -1
  203. package/dist/standard-schema.d.ts +1 -1
  204. package/dist/standard-schema.d.ts.map +1 -1
  205. package/dist/standard-schema.js +10 -2
  206. package/dist/standard-schema.js.map +1 -1
  207. package/dist/vitest.d.ts +1 -3
  208. package/dist/vitest.d.ts.map +1 -1
  209. package/dist/vitest.js +11 -6
  210. package/dist/vitest.js.map +1 -1
  211. package/docs/debt-and-hooks.md +90 -0
  212. package/docs/modules.md +253 -0
  213. package/docs/naming.md +126 -0
  214. package/docs/north-star.md +61 -0
  215. package/docs/roadmap.md +37 -0
  216. package/docs/rules.md +308 -0
  217. package/docs/tooling.md +120 -0
  218. package/package.json +8 -6
  219. package/src/biome/nativeEffectCall.ts +45 -0
  220. package/src/biome/recognizeDiagnostics.ts +67 -0
  221. package/src/biome/report.ts +24 -6
  222. package/src/biome/syntax.ts +36 -0
  223. package/src/cli/args.ts +29 -3
  224. package/src/cli/baseline.ts +3 -3
  225. package/src/cli/fix.ts +13 -13
  226. package/src/cli/run-main.ts +1 -1
  227. package/src/cli.ts +14 -0
  228. package/src/config/decode.ts +7 -2
  229. package/src/config/load.ts +15 -6
  230. package/src/config/resolve.ts +10 -15
  231. package/src/config.ts +8 -9
  232. package/src/decoded.ts +3 -3
  233. package/src/engine/session.ts +3 -3
  234. package/src/engine/violation.ts +2 -2
  235. package/src/hooks/command.ts +34 -0
  236. package/src/hooks/install.ts +69 -0
  237. package/src/hooks/location.ts +28 -0
  238. package/src/hooks/preCommit.ts +68 -0
  239. package/src/hooks/shim.ts +28 -0
  240. package/src/hooks/uninstall.ts +19 -0
  241. package/src/imports/browserGlobals.ts +142 -0
  242. package/src/imports/browserNames.ts +80 -0
  243. package/src/imports/fences/check.ts +12 -0
  244. package/src/imports/fences/declarations.ts +38 -0
  245. package/src/imports/fences/discover.ts +37 -0
  246. package/src/imports/graph.ts +2 -2
  247. package/src/imports/projects.ts +2 -2
  248. package/src/imports/runtime.ts +37 -0
  249. package/src/imports/workspace.ts +7 -2
  250. package/src/inventory/collect.ts +8 -1
  251. package/src/inventory/trackedPaths.ts +28 -0
  252. package/src/inventory/walk.ts +65 -36
  253. package/src/rules/built-in.ts +6 -0
  254. package/src/rules/comments/max-per-file.ts +1 -1
  255. package/src/rules/files/folderNames.ts +1 -1
  256. package/src/rules/files/otherNames.ts +1 -10
  257. package/src/rules/imports/bundleInputs.ts +94 -0
  258. package/src/rules/imports/fences.ts +4 -16
  259. package/src/rules/imports/manifest.ts +116 -0
  260. package/src/rules/imports/noTestCode.ts +18 -0
  261. package/src/rules/imports/runtime.ts +43 -0
  262. package/src/rules/max-lines.ts +4 -10
  263. package/src/rules/suppressions/biome-overrides.ts +31 -4
  264. package/src/rules/suppressions/biomeRecommended.ts +2 -2
  265. package/src/rules/suppressions/no-inline.ts +1 -1
  266. package/src/standard-schema.ts +11 -3
  267. package/src/vitest.ts +11 -6
package/README.md CHANGED
@@ -1,576 +1,247 @@
1
1
  # @shivaedev/quality
2
2
 
3
- One quality gate for a repository: typed rules, one report, a baseline of existing debt and a registry of permanent exceptions, each with its reason.
3
+ Quality gives a repository an opinionated quality policy with sensible defaults, shared tool configuration and one gate. Adopt the policy instead of choosing rules and maintaining a complicated config; humans and agents can spend their time improving the code.
4
4
 
5
- Every rule is an error by default. A repository adopts the gate at once: it records its existing violations in the baseline, and from then on a finding the baseline does not cover fails, and fixed debt leaves the baseline as files change. A baseline that grows shows in the diff, where a reviewer judges it.
5
+ ## Why you want this
6
6
 
7
- ## Setup
7
+ A repository should not have to debate every lint rule, copy tool settings or decide which warnings an agent can ignore. Quality supplies the shared checks and repair guidance. The daily command is small:
8
8
 
9
9
  ```sh
10
- pnpm add --save-dev @shivaedev/quality
11
- ```
12
-
13
- On pnpm 11, installing into a project needs a decision on `msgpackr-extract`, which `effect` pulls in: pnpm refuses its build script by default, and `pnpm add` fails with `ERR_PNPM_IGNORED_BUILDS`. Record the decision under `allowBuilds` in `pnpm-workspace.yaml`, or run `pnpm approve-builds`; `false` skips the build. `pnpm dlx` and `pnpm add --global` need no setting.
14
-
15
- ```yaml
16
- allowBuilds:
17
- msgpackr-extract: false
18
- ```
19
-
20
- Node 24 or later loads `quality.config.ts` directly (type stripping), so no build step runs first. Use only erasable TypeScript in the config, and give relative imports their `.ts` extension.
21
-
22
- ```ts
23
- // quality.config.ts
24
- import { defineConfig } from "@shivaedev/quality/config.ts";
25
- import { noConsoleLog } from "./quality/no-console-log.ts";
26
-
27
- export default defineConfig({
28
- sources: ["src", "scripts"],
29
- exclude: ["src/components/ui/", "*.gen.ts"],
30
- local: [noConsoleLog],
31
- rules: {
32
- "structure/max-lines": { options: { source: 200, testFiles: ["e2e/"] } },
33
- "comments/max-per-file": { options: { max: 3 } },
34
- "local/no-console-log": "warn",
35
- },
36
- });
37
- ```
38
-
39
- | Key | Default | Meaning |
40
- | --- | --- | --- |
41
- | `sources` | `["."]` | Directories or files to check, relative to the config. A missing source is an error. |
42
- | `exclude` | `[]` | Paths never checked, in `.gitignore` syntax. Files ignored by git, `.git` and `node_modules` are always skipped. |
43
- | `extensions` | TypeScript and JavaScript modules | Which files rules read as sources. |
44
- | `registry` | `quality/registry.json` | Permanent exceptions. |
45
- | `baseline` | `quality/baseline.jsonl` | Existing violations that the gate lets through. |
46
- | `local` | `[]` | The repository's own rules, made with `defineRule`. |
47
- | `rules` | every rule at `error` | A level per rule id, or `{ level, options }`. |
48
-
49
- The config is typed: an unknown rule id, a misspelled option or an option of the wrong type fails to compile. It is also validated when loaded, so a JavaScript config gets the same checks.
50
-
51
- ## Levels
52
-
53
- - `error` is the default. An error-level violation that the baseline and the registry do not cover fails the gate.
54
- - `warn` reports without failing. It exists for the transition to a new rule and is meant to be temporary: move the rule to `error` and baseline what is left.
55
- - `off` disables the rule.
56
-
57
- ## Rules
58
-
59
- `structure/max-lines` keeps each module to one job: a source file may have 150 lines and [test code](#test-code) 300, counted the way an editor numbers them. Declaration files are exempt. Its options are `source` and `test` (the limits) and `testFiles`, `.gitignore` patterns of further folders whose files count as tests, such as a Playwright suite in `e2e/` (none by default). A file over its limit counts one violation for each line above it: 168 lines under a limit of 150 count 18.
60
-
61
- ### Comments
62
-
63
- A comment says why, never what the code already says or what it used to be. Seven rules hold every comment to that:
64
-
65
- | Rule | Reports | Options |
66
- | --- | --- | --- |
67
- | `comments/no-jsdoc` | Every `/** */` block, except a tool pragma the config allows | `allow` |
68
- | `comments/no-line-reference` | A line number: `file.ts:42`, `file.ts#L42`, `line 42` | none |
69
- | `comments/no-pr-reference` | A pull request or issue: `#123`, `PR 123`, `MR 123`, `pull request 123`, `merge request 123`, `issue 123`, `ticket 123`, `/pull/123`, `/pulls/123`, `/issues/123`, `/merge_requests/123`, `GH-123` | none |
70
- | `comments/no-banner` | A banner, divider or region: a line that starts or ends with three or more of `- = * # ~ _ + / \ ─ ━ ═ ┄ ┈`, `#region`, `#endregion` | none |
71
- | `comments/no-todo` | `TODO`, `FIXME`, `XXX` and `@todo` | none |
72
- | `comments/no-environment-pragma` | A test environment set by a `@vitest-environment` or `@jest-environment` pragma (and its `-options`); name the file `*.dom.test.ts` instead, see [Vitest projects](#vitest-projects) | none |
73
- | `comments/max-per-file` | A file with more than `max` comments, 2 by default | `max`, `allow` |
74
-
75
- The rules find comments with the TypeScript parser, so text inside strings, template literals, regular expressions and JSX never counts as a comment. They read the TypeScript and JavaScript modules among the sources and skip declaration files and the directives listed below. Each finding names the line its comment starts on.
76
-
77
- The default is no comments at all: a comment states only what the code cannot show, such as a constraint or a reason, and never narrates the code. The limit of `comments/max-per-file` is a crude tripwire against runaway comments, not a budget to fill, so a file that reaches it means something went wrong.
78
-
79
- `comments/max-per-file` counts comments this way:
80
-
81
- - A block comment counts once, however many lines it spans.
82
- - Line comments that each stand alone on adjacent lines form one run and count once. A blank line, code, a directive, or a comment after code on the same line starts a new one.
83
- - Allowed tool pragmas and directives are not counted: compiler and linter directives (`@ts-…`, triple-slash directives such as `/// <reference …>`, `biome-ignore…`, `eslint-disable…`, `eslint-enable…`, `oxlint-…`, `stylelint-…`, `deno-lint-ignore…`, `tslint:disable…`, `prettier-ignore`, Flow's `$FlowFixMe`, `$FlowIgnore`, `$FlowExpectedError`, `$FlowIssue` and `@noflow`), bundler annotations (`#__PURE__`, `@__PURE__`, `#__NO_SIDE_EFFECTS__`, `@__NO_SIDE_EFFECTS__`) and coverage hints (`c8 ignore`, `v8 ignore`, `istanbul ignore`). The suppression rules below report the directives that silence a check.
84
-
85
- A file over the limit counts one violation for each comment above it: 5 comments against a limit of 2 count 3. A finding names the first comment over the limit.
86
-
87
- A tool pragma is a comment whose every line starts with an allowed tag, such as `/** @jsxImportSource preact */`. `allow` lists the tags and is empty by default, so a pragma counts as a comment until the config allows its tag. Settings that a pragma would repeat in each file belong in the tool's config. `comments/no-jsdoc` and `comments/max-per-file` each take their own list, so give both the same one:
88
-
89
- ```ts
90
- const pragmas = ["@jsxImportSource", "@license"];
91
-
92
- export default defineConfig({
93
- rules: {
94
- "comments/no-jsdoc": { options: { allow: pragmas } },
95
- "comments/max-per-file": { options: { allow: pragmas } },
96
- },
97
- });
10
+ quality lint
98
11
  ```
99
12
 
100
- A repository with existing comments adopts the rules through the baseline, for example `quality baseline write --rule comments/no-jsdoc --rule comments/max-per-file`.
13
+ Built-in rules run at `error`, so an uncovered finding fails the gate and tells the author what to repair. An existing repository can record its debt in a baseline while keeping the checks strict: new findings above a file's recorded count fail, and fixing debt passes. You get one consistent standard without making every repository design it.
101
14
 
102
- ### Suppressions
15
+ ## Using it
103
16
 
104
- A check that is silenced at one site hides the problem instead of fixing it. Five rules close the escape hatches, and none of them takes registry exceptions: a registry entry that names one fails the gate as stale. A repository with existing suppressions adopts the rules through the baseline.
17
+ ### How to think about the gate
105
18
 
106
- | Rule | Reports | Options |
107
- | --- | --- | --- |
108
- | `suppressions/no-inline` | Every comment directive that silences a linter, the compiler or a formatter, except `@ts-expect-error` in a type test | none |
109
- | `suppressions/no-double-cast` | A cast through `unknown`, `any` or `never`: `x as unknown as T`, `x as any as T`, `x as never as T`, `<T><unknown>x` | none |
110
- | `suppressions/biome-overrides` | A Biome setting that turns a check off or down, or keeps files out of it, without a declaration, and a declaration that matches no setting | `declared` |
111
- | `suppressions/biome-recommended` | A rule the installed Biome recommends that the [Biome preset](#biome-preset) neither sets to `error` nor declares | none |
112
- | `suppressions/no-ignore-deprecations` | `compilerOptions.ignoreDeprecations` in a tsconfig, which silences TypeScript's errors for deprecated options | none |
113
-
114
- `suppressions/no-inline` reports these directives, wherever a line of a comment starts with one:
19
+ Adopt Quality's policy as a whole. Quality owns the shared rules, their defaults and the tool rules and fixes it deliberately disables internally. A repository supplies facts about its code and architecture; it does not need to choose a rule catalogue or warning levels.
115
20
 
116
- - Biome: `biome-ignore`, `biome-ignore-all` and `biome-ignore-start`, for lint, assist and format alike. The `biome-ignore-end` that closes a range is not reported again.
117
- - TypeScript: `@ts-ignore`, `@ts-expect-error` and `@ts-nocheck`. `@ts-check` turns checking on and is allowed.
118
- - Other linters: `eslint-disable`, `eslint-disable-line`, `eslint-disable-next-line` and inline rule settings such as `/* eslint no-console: "off" */`; the same `-disable` forms of `oxlint` and `stylelint`; `deno-lint-ignore` and `deno-lint-ignore-file`; `tslint:disable…`.
119
- - Formatters: `prettier-ignore`, since `prettier --check` fails on the code it skips.
120
- - Flow: `$FlowFixMe`, `$FlowIgnore`, `$FlowExpectedError`, `$FlowIssue` and `@noflow`.
21
+ A **rule** checks a repository and returns **findings**, each naming a file and what to change. Built-in and local rules always run at `error`. A finding normally counts once; a limit rule counts how far the file exceeds its limit. Use the defaults and keep checks strict. The config rejects warning levels, disabled rules, raised thresholds and inventory exclusions.
121
22
 
122
- Coverage hints (`c8 ignore`, `v8 ignore`, `istanbul ignore`) are allowed: they leave code out of a coverage measure and silence no linter or compiler. The rule reads the TypeScript and JavaScript modules among the sources, declaration files included, and every `.css`, `.scss` and `.less` file among the checked files, whatever `extensions` says. Each finding names the line its comment starts on and has the directive as its subject.
23
+ Two files explain findings the repository deliberately keeps:
123
24
 
124
- `suppressions/no-ignore-deprecations` reads every JSON file in the repository whose `compilerOptions` sets `ignoreDeprecations`, whatever the file is named, so a shared config such as `tsconfig/base.json` counts as much as a `tsconfig.json`. Like `manifests/sorted`, it walks the whole repository, not only the sources, and skips files ignored by git and `node_modules`. Each finding names the line of the setting. Replace the deprecated option it hides, then remove it.
25
+ - The **baseline** records existing debt by rule and file, as a maximum count. It makes strict checks usable before every file is cleaned up. `tighten` and `prune` lower the counts as debt disappears; explicitly writing a named rule again can raise them, and that diff needs review.
26
+ - The **registry** records permanent exceptions, each with a reason and optionally a finding's `subject`. A rule may refuse registry exceptions. An entry that no longer applies fails the gate instead of silently excusing future code.
125
27
 
126
- #### Type tests
28
+ The registry applies first, then the baseline. With the default rule settings, remaining findings fail in one report grouped by rule. A baseline entry with fewer findings left is a note, so removing debt passes immediately. An entry for an unknown or disabled rule fails; debt records must still name a check that runs.
127
29
 
128
- A type test proves that an API rejects what its types forbid, and TypeScript asserts a compile error only through `@ts-expect-error`, which fails as soon as the error it expects goes away. So `@ts-expect-error`, and no other directive, is allowed in a type test: a file named `*.typecheck.test.ts`, `*.typecheck.spec.ts` or `typecheck.test.ts` (or `.tsx`). The compiler checks these files; [Vitest projects](#vitest-projects) never run them. `@ts-ignore`, `@ts-nocheck` and every linter and formatter directive stay reported in them. A test that must pass a rejected value at run time, to prove the runtime refuses it too, calls the API through `Reflect.apply` instead of a directive.
30
+ The work has three parts: connect the repository to the shared tools once; describe an import boundary when its architecture needs one; run the gate and repair findings every day. Writing local rules is not an adoption requirement.
129
31
 
130
- `suppressions/no-double-cast` finds casts with the TypeScript parser, through parentheses and in either assertion syntax. A single cast is left to the linter, and `as const` is not a cast.
32
+ ### 1. Once per repository: connect the shared policy
131
33
 
132
- #### Declared Biome overrides
34
+ A new repository needs no `quality.config.ts`. Quality checks sources from the current directory with every built-in rule at its default error level. The default baseline and registry paths are `quality/baseline.jsonl` and `quality/registry.json`.
133
35
 
134
- A scope that truly cannot follow a lint rule keeps its exception in the Biome config, and the quality config declares it with a reason. `suppressions/biome-overrides` reads `biome.json` or `biome.jsonc` at the root, every nested `biome.json` and `biome.jsonc` among the checked files, and the configs each of them `extends`, resolved the way Biome resolves them (see [Shared presets](#shared-presets)). These settings count as overrides, at the top level and in every `overrides` entry:
36
+ Create a root `biome.json`:
135
37
 
136
- | Setting | Declared as |
137
- | --- | --- |
138
- | A lint rule at `off`, `warn` or `info`, as a string or a `level` | `lint/<group>/<rule>` |
139
- | A lint group at `off`, `warn` or `info`, or with `recommended: false` or `preset: "none"` | `lint/<group>` |
140
- | `linter.rules` with `recommended: false` or `preset: "none"` | `lint/recommended` |
141
- | A domain at `none` | `lint/domains/<domain>` |
142
- | `linter.enabled: false` | `lint` |
143
- | An assist action at `off`, such as `useSortedKeys` or `organizeImports` | `assist/<group>/<action>` |
144
- | `assist.actions` with `recommended: false` or `preset: "none"` | `assist/recommended` |
145
- | `assist.enabled: false` | `assist` |
146
- | `formatter.enabled: false` | `format` |
147
- | `enabled: false` in a language's `linter`, `assist` or `formatter`, such as `css.linter` | `<language>/lint`, `<language>/assist`, `<language>/format` |
148
- | An `includes` list in `files`, `linter`, `assist` or `formatter` that excludes a pattern with `!` or lacks `**` | `files/includes`, `lint/includes`, `assist/includes`, `format/includes` |
149
-
150
- Biome runs assist actions, key sorting among them, as part of `biome check` and reports what they would change, so turning one off is an override. Formatter options such as `indentStyle` or `lineWidth` choose a style, not an exception, and are not overrides. Settings that raise a rule or enable it are not overrides either. Rule options are not read, so an option that loosens a rule, such as a higher complexity limit, is left to review.
151
-
152
- ```ts
153
- export default defineConfig({
154
- rules: {
155
- "suppressions/biome-overrides": {
156
- options: {
157
- declared: [
158
- {
159
- rule: "lint/style/noDefaultExport",
160
- includes: ["*.config.ts"],
161
- reason: "Tools load their config files through the default export.",
162
- },
163
- {
164
- rule: "lint/suspicious/noConsole",
165
- includes: ["scripts/**"],
166
- reason: "Scripts report to the terminal they run in.",
167
- },
168
- ],
169
- },
170
- },
171
- },
172
- });
38
+ ```json
39
+ {
40
+ "extends": ["@shivaedev/quality/biome"]
41
+ }
173
42
  ```
174
43
 
175
- A declaration covers a setting when its `rule` and its `includes` match the setting's exactly, in any order. An `includes` list that keeps files out is declared as the whole list, so every pattern added to it needs the declaration to change too. A setting at the top level of the root config has the scope `["**"]`, and an `overrides` entry has its own `includes` (`["**"]` when it has none). Patterns in a nested config are relative to its folder, so they are declared with the folder in front: `"src/**"` in `packages/web/biome.json` is declared as `"packages/web/src/**"`, and its top level as `"packages/web/**"`. A setting without a declaration is reported at its line in the Biome config; a declaration that no setting matches is reported against the root config, so the list cannot outlive the overrides it explains.
44
+ Quality's `biome` rule asks for this shared configuration and reports Biome's error findings through the same baseline as the other rules. Quality ships the reasons for its own disabled Biome rules and selected disabled fixes. Extend it rather than copying the rule settings; record existing findings in the baseline instead of weakening the checks.
176
45
 
177
- #### Shared presets
178
-
179
- A config `extends` its entries the way Biome does:
180
-
181
- - An entry that starts with `./` or `../` is a file next to the config. Any other entry is first a file at the repository root, then a package in the root's `node_modules`, resolved through its `exports` (with the `biome` and `default` conditions, and `*` patterns), then its `main`, then a file inside it. A package may also name itself through the repository's own `package.json` `exports`. `"//"` in a nested config extends the root config, which is read on its own.
182
- - Biome reads only the entries of the config it loads: the `extends` of an extended config are not followed, so they are not read here either.
183
- - The extended configs apply from left to right and the config itself last. A later setting of a rule, a group, `recommended` or `enabled` replaces an earlier one, so a preset's weakening that the repository sets back to `error` is no override. A group set as a whole (`"suspicious": "error"`) replaces the settings of its rules; a rule set inside a group the preset turned off leaves the rest of the group off.
184
- - Lists are appended, not replaced: `overrides`, `plugins` and every `includes` list, `files.includes` among them. A repository's `files.includes` adds to the preset's, so each list is declared by whoever wrote it.
185
- - Patterns in an extended config resolve against the repository root, or against the folder of a nested config that extends it, as patterns in the config itself do.
186
-
187
- A package that ships a preset declares the weakenings the preset makes in a `declarations.json` beside the preset file, as an array of the same `{ rule, includes, reason }` entries the `declared` option takes. Its `includes` are written relative to the repository that extends the preset. Those weakenings count as declared, so the repository adds nothing for them:
46
+ Add the daily commands to `package.json`:
188
47
 
189
48
  ```json
190
- [
191
- {
192
- "rule": "lint/style/noDefaultExport",
193
- "includes": ["**/*.config.ts"],
194
- "reason": "Tools load their config files through the default export."
49
+ {
50
+ "scripts": {
51
+ "format": "quality fix",
52
+ "lint": "quality lint",
53
+ "typecheck": "tsc --noEmit"
195
54
  }
196
- ]
55
+ }
197
56
  ```
198
57
 
199
- A weakening the preset makes without such a declaration is reported at the line of the repository's `extends` entry, and the repository's own declaration covers it. A weakening the repository adds beyond the preset needs its own declaration, and a repository declaration that repeats one the preset ships is reported until it is removed. A preset entry that cannot be resolved or read, or whose `declarations.json` is invalid, is reported at the `extends` entry as well.
58
+ The optional pre-commit hook runs Quality's lint, then the `typecheck` script when it exists, then an optional `quality:commit` script for repository checks. It uses the manifest's `packageManager` (`npm`, `pnpm`, `yarn` or `bun`), defaulting to `npm` when omitted. Add `quality:commit` only when the repository has extra commit checks.
200
59
 
201
- #### Recommended Biome rules
60
+ Inspect the report and install the optional commit check explicitly:
202
61
 
203
- `suppressions/biome-recommended` holds the [Biome preset](#biome-preset) to every rule Biome recommends, so a Biome upgrade that adds a recommended rule, or recommends one the preset leaves below `error`, fails the gate until the preset takes a position on it. It asks the installed Biome, the one `quality` runs, which rules its `recommended` preset enables, through `biome rage --linter`, so the list is never written down. When the root Biome config extends `@shivaedev/quality/biome`, it reads that preset and its `declarations.json`, resolved from the repository root as under [Shared presets](#shared-presets); a repository that does not extend the preset is left to the `biome` rule, which asks for it. A recommended rule is in order when the preset's `linter.rules` sets it to `error`, directly or through its group's severity, or when a declaration names it, whatever the declaration's `includes`. Every other recommended rule is reported at the preset, one finding per rule: set it to `error`, or declare why not.
204
-
205
- ### Imports
206
-
207
- Four rules read the imports of the TypeScript and JavaScript modules among the sources. They build it once per run with the TypeScript compiler, without a bundler or another dependency.
208
-
209
- | Rule | Reports | Options |
210
- | --- | --- | --- |
211
- | `imports/cycles` | Modules that import each other at run time | none |
212
- | `imports/resolvable` | An import that resolves to nothing | `generated` |
213
- | `imports/fences` | An import that crosses a fence the config declares | `fences` |
214
- | `imports/aliased` | A relative import that leaves its own folder | none |
62
+ ```sh
63
+ quality lint
64
+ quality hooks install
65
+ ```
215
66
 
216
- Each import resolves the way the compiler resolves it, with the options of the nearest `tsconfig.json` and the projects it references, under bundler resolution: `paths`, `package.json` `imports` and `exports` and the `source` condition hold, so a workspace package resolves to its source. The graph reads static and dynamic imports, `export ... from`, `require()`, `require.resolve()`, `import.meta.resolve()` of a bare specifier (a relative one is URL arithmetic that never checks the path), `import()` types, `/// <reference types>`, `/// <reference path>` and JSDoc `@import` tags.
67
+ Installing the package does not install a hook. `hooks install` writes the repository's pre-commit hook. A hook Quality did not write is kept unless `--force` replaces it. For an existing repository, `quality baseline write` records error-level findings when there is no baseline; it refuses to regenerate an existing baseline without `--rule <id>`.
217
68
 
218
- - An import of a stylesheet, an image or JSON resolves to the file, and only when the file exists, whatever declaration describes it. A Node builtin resolves.
219
- - A runtime import must resolve to code: a declaration file (`.d.ts`, `.d.mts`, `.d.cts`, or an asset declaration such as `.d.css.ts`) satisfies only `import type` and `export type`, however the import reaches it, so a package that has only its `@types` package installed, or an `exports` entry that points at a declaration, is reported.
220
- - A bare import resolves when a declaration file of the importer's tsconfig project declares the module in a script (`declare module "virtual:*"`). A relative import always needs a real file, `declare module "*"` and patterns with more than one `*` never count, and a `declare module` inside a module is an augmentation, which declares nothing new.
221
- - A relative import or a `#` package import of a missing file resolves only inside a folder that `generated` names, such as a client a generator writes before the tests run. A `#` import lands where its `imports` entry in `package.json` points once the file exists. Each folder must be ignored by git, or at least every file an import names in it must be, hold no file git tracks but a `.gitkeep` or `.keep` placeholder, lie outside `node_modules` and hold a file that an import names. The import stays an edge to its path, so fences apply to it before the file exists.
222
- - A module that resolves into `node_modules` or outside the root is external, and its package is the one it resolves into, whatever alias the import uses. A `@types` package counts as the package it describes: `@types/hast` is `hast`, `@types/scope__name` is `@scope/name`.
69
+ Add a config only when the repository needs to supply facts such as source locations or import boundaries:
223
70
 
224
71
  ```ts
225
- "imports/resolvable": { options: { generated: ["packages/db/src/test-support/generated"] } },
226
- ```
227
-
228
- `imports/cycles` reports each group of modules that import each other at run time once, at the alphabetically first of them, with one loop through the group. Its count is the number of modules in the group. `import type`, `export type`, type references and imports in declaration files are left out; `import { type X }` stays a runtime import. Dynamic `import()` and `require()` count; `require.resolve()` does not. It takes no registry exceptions. Each finding of `imports/resolvable` has the import as its subject.
229
-
230
- The gate fails closed: when the sources hold no module at all, the imports rules stop the run instead of passing on an empty graph. Point `sources` at the code, or turn the rules off.
72
+ import { defineConfig } from "@shivaedev/quality/config.ts";
231
73
 
232
- #### Aliases
74
+ export default defineConfig({ sources: ["src"] });
75
+ ```
233
76
 
234
- A relative import names only a file in its own folder, such as `./format.ts`: the folder is one module. Every other import goes through an alias, so moving a file never rewrites a chain of `../`:
77
+ When a config exists, its folder is the root for paths and hook scripts. Without one, the current directory is the root. `sources` chooses the files the source rules read; a missing source stops the run. Directory discovery skips untracked files ignored by git and the `.git` and `node_modules` directories. Tracked files remain checked even when an ignore pattern matches them; a folder named `generated/` does not exempt authored code. Some repository rules, such as manifest sorting, inspect the whole repository even when `sources` names only `src`.
235
78
 
236
- - the `imports` of the importer's nearest `package.json`, such as `"#lib/*": "./src/lib/*"`, or `"#shared/*": "@demo/shared/*"`, which names another workspace package;
237
- - the name of another workspace package, as far as its `exports` reach.
79
+ `defineConfig` checks the config's types. The CLI also validates it: a broken config, unknown rule id or invalid options fails before checks run. An explicit `--config` path must exist. The [rule reference](https://github.com/ShivaeDev/platform/blob/main/packages/quality/docs/rules.md) and [naming reference](https://github.com/ShivaeDev/platform/blob/main/packages/quality/docs/naming.md) explain the shared checks. Their option inventory describes accepted inputs; you do not need to fill it out to adopt the policy.
238
80
 
239
- An import that already goes through a tsconfig `paths` alias is not relative, so the rule leaves it alone.
81
+ ### 2. When architecture needs it: describe an import boundary
240
82
 
241
- `imports/aliased` reports a relative import that goes up (`../format.ts`) or down into a subfolder (`./parts/deeper.ts`, or `./widgets` for `widgets/index.ts`). It reads static imports, `export ... from`, side-effect imports, `import()`, `require()`, `import()` types, JSDoc `@import` tags and the paths of `vi.mock`, `vi.doMock`, `vi.unmock`, `vi.doUnmock`, `vi.importActual` and `vi.importMock`, so a mock keeps pointing at the module it replaces. An escaped path, such as `"\x2e\x2e/format.ts"`, counts as the path it spells. Each finding has the import as its subject. Its message names the fix for where the import lands:
83
+ An app says where its code runs through folder names. A package adopts the runtime convention by having `src/client/` or `src/server/`; no config entry is needed. In that package, `src/client/` is browser code, `src/server/` is server code, and everything else is shared. Keep shared code wherever its topic belongs; there is no required `src/shared/` folder or runtime file suffix.
242
84
 
243
- - inside its own package: import through a `#` alias from the `imports` of the package's `package.json`, and declare one if none fits;
244
- - inside another workspace package: import it by that package's name, through a path its `exports` lists;
245
- - inside an installed package under `node_modules`: import that package by its name.
85
+ `imports/runtime` rejects client imports of server code or Node built-ins. Shared code may import neither client nor server code, use browser globals such as `window`, `document` or `localStorage`, or import Node built-ins. Both client and server code may import shared code. The check includes type imports and re-exports and resolves aliases through the shared import graph. Locally bound names and property names are not browser globals; portable web APIs such as `URL`, `fetch` and `AbortController` remain available to shared code. Test files and `test-support/` ship nowhere and are outside this runtime policy.
246
86
 
247
- The rule only reports; nothing rewrites the import. Whoever writes the import picks the alias, because only they know which conditions their runtimes, bundler and tests use.
87
+ Adoption is per package: packages without either runtime folder retain their existing policies. Existing explicit fences continue to run in both adopted and legacy packages, so hand-kept browser, server and Markdown lists can remain until their package moves. Runtime folders are detected independently of `sources`, including empty folders; `sources` still selects the code that is checked.
248
88
 
249
- #### Fences
89
+ The manifest owns ordinary workspace dependencies. When a package promises more, put `quality.fences.config.ts` beside its `package.json`, exporting an array of fences as the default. Quality discovers this file at the repository root and in workspace packages, even when `sources` includes only their source folders. The root config does not list boundary files.
250
90
 
251
- A fence is one prohibition, stated in the config without patterns:
91
+ A **fence** is a prohibition on imports, with a reason and one illegal and one legal example. Quality validates all discovered fences together, so duplicate names or contradictory examples stop the run. An unreadable file or invalid default export also stops the run. Fences remain an internal `imports/fences` module of Quality and use its shared import graph, resolver and report.
252
92
 
253
93
  ```ts
254
- import { defineConfig } from "@shivaedev/quality/config.ts";
255
- import { external, folders, packages } from "@shivaedev/quality/imports/fences/selectors.ts";
256
94
  import { fence } from "@shivaedev/quality/imports/fences/dsl.ts";
95
+ import { external, folders, packages } from "@shivaedev/quality/imports/fences/selectors.ts";
257
96
 
258
- const fences = [
259
- fence("ui-never-imports-server")
260
- .because("The UI ships to browsers.")
261
- .from(folders("packages/ui/src"))
262
- .mayNotImport(packages("server"))
263
- .demonstratedBy({
264
- illegal: ["packages/ui/src/app.ts", "packages/server/src/db.ts"],
265
- legal: ["packages/ui/src/app.ts", external("effect")],
266
- }),
267
- ];
268
-
269
- export default defineConfig({ rules: { "imports/fences": { options: { fences } } } });
270
- ```
271
-
272
- A fence has a name, a reason, the modules it holds (`from`) and one prohibition:
273
-
274
- - `mayNotImport(target)`: no module it holds imports the target.
275
- - `mayNotReach(target)`: nothing a module it holds imports, directly or through other modules of the repository, is the target. The finding names the path.
276
- - `mayImportOnly(...subjects).of(unit)`: the modules it holds import only the named modules or folders directly in a package's `src` folder (or the package folder) or in a folder.
277
-
278
- Targets are `packages(...)` (workspace packages by name, with or without their scope; a workspace package is a named `package.json` that `pnpm-workspace.yaml` or the root `package.json` `workspaces` includes, with `*`, `**`, `?`, `{a,b}` and `[...]` in its patterns, none of which match a folder whose name starts with `.` unless the pattern spells the dot, and a `packages` list or pattern the gate cannot read stops the run, and it holds every file below it that no deeper workspace package holds), `folders(...)`, `files(...)`, `modules(...)` (external packages by package name, and Node builtins), `scopes(...)` (every external package of a scope), `anyOf(...)`, `workspace` (every workspace package) and `anything`. Each takes `.except(...)`.
97
+ const uiBoundary = fence("ui-never-imports-server")
98
+ .because("The UI ships to browsers.")
99
+ .from(folders("packages/ui/src"))
100
+ .mayNotImport(packages("server"))
101
+ .demonstratedBy({
102
+ illegal: ["packages/ui/src/app.ts", "packages/server/src/db.ts"],
103
+ legal: ["packages/ui/src/app.ts", external("effect")],
104
+ });
279
105
 
280
- The config does not compile without `demonstratedBy`, and the rule checks the examples against the policy: each is a chain of imports from a file of the repository, which may end in `external(name)`. The illegal example must cross this fence and no other; the legal example must cross none. Every name a fence uses must exist: a package, a folder that holds checked files, a checked file, a subject of the unit. Two fences may not share a name, and each needs a reason. A policy that breaks any of this stops the run. Fences count type imports too.
106
+ export default [uiBoundary];
107
+ ```
281
108
 
282
- Fences guard shipped code, and [test code](#test-code) ships nowhere, so a fence never holds it: a test file or a file under `test-support/` may import across every fence. There is no option to check them.
109
+ This is `packages/ui/quality.fences.config.ts`; paths remain relative to the repository root. Existing fences supplied through `rules["imports/fences"].options.fences` join the same policy. This adds a fact about the repository's architecture to the shared import check. It does not select a different set of Quality rules. Use real checked files and workspace package names in these chains. The illegal chain must cross this fence alone; the legal chain must cross none. `mayNotImport` checks direct imports; `mayNotReach` also follows repository imports transitively; `mayImportOnly(...subjects).of(unit)` permits only named modules or folders of a unit. Fences count type imports but never hold test files or files under `test-support/`. The default `imports/no-test-code` rule rejects production imports of either, including type imports and re-exports. Move shared code into a production module that tests can import. The [import reference](https://github.com/ShivaeDev/platform/blob/main/packages/quality/docs/rules.md#imports) covers resolution, selectors and generated files.
283
110
 
284
- A finding has its fence's name as its subject, so a registry entry with that subject excuses one file from one fence.
111
+ ### Story tests
285
112
 
286
- ### Manifests
113
+ Write setup in the domain kit under `test-support/` and keep each test specific to its story. [`@shivaedev/test-story`](https://github.com/ShivaeDev/platform/tree/main/packages/test-story#readme) explains how to build the kit, traits and verbs over a real engine.
287
114
 
288
- `manifests/sorted` keeps every `package.json` in the repository in the key order of [sort-package-json](https://github.com/keithamus/sort-package-json), a dependency of this package. It walks the whole repository, not only the sources, and skips files ignored by git and `node_modules`. A manifest that is not valid JSON is reported too. `quality fix` sorts the manifests, and the rule takes no registry exceptions.
115
+ Quality's `tests/story-setup` rule reports top-level test functions named with the words `seed`, `make`, `build` or `setup`, and fixture-writing calls imported from Node's `fs` or `fs/promises`. It leaves source and `test-support/` files alone. This is a check for those syntax patterns; a passing test file still needs review of what it tests.
289
116
 
290
- ### Local rules
117
+ ### 3. Every day: repair findings and keep debt current
291
118
 
292
- ```ts
293
- import { defineRule } from "@shivaedev/quality/rule.ts";
294
-
295
- export const noConsoleLog = defineRule({
296
- id: "local/no-console-log",
297
- description: "Log through the application logger.",
298
- check: ({ sources }) =>
299
- sources.flatMap((file) =>
300
- file.lines.flatMap((text, index) =>
301
- text.includes("console.log(") ? [{ file: file.path, line: index + 1, message: "Logs to the console." }] : [],
302
- ),
303
- ),
304
- });
119
+ ```sh
120
+ quality lint
121
+ quality fix
122
+ quality lint
305
123
  ```
306
124
 
307
- A rule that must never be excused, like the suppression rules, sets `registrable: false`; the registry then refuses entries for it.
125
+ Lint reports the rule's guidance and each finding's file and line when available. Fix sorts manifests, applies Biome's enabled lint fixes and formatting, then repeats until a round changes nothing, for at most five rounds. It applies unsafe lint fixes too, so review the diff. It leaves object key order and the preset's disabled fixes to the author; passing `fix` does not mean lint passes.
308
126
 
309
- `check` receives the repository `root`, every checked path in `files`, the `sources` with their `text` and `lines`, `readText(path)` for any other file (undefined when absent) and the validated `options`. It returns findings, or a promise of them. A finding names its `file`, relative to the root (an absolute path under it is made relative), and its `message`, and optionally a `line`, a `subject` that tells apart exceptions of one rule in one file, a `count` of the violations it stands for (1 by default) and the `threshold` it applied. A rule with a limit reports one finding per file, with the amount over the limit as its `count` and the limit as its `threshold`.
127
+ When you fix debt, lower the entries of changed files:
310
128
 
311
- A rule with options declares them with any [Standard Schema](https://standardschema.dev), such as `Schema.toStandardSchemaV1(...)` from Effect. Options are an object: when the config gives none, the schema validates `{}`, so give each option a default or make it optional. A rule without an options schema refuses options.
312
-
313
- ## Baseline
314
-
315
- ```jsonl
316
- {"path":"src/legacy/sync.ts","rule":"comments/no-jsdoc","count":3}
317
- {"path":"src/server/db.ts","rule":"structure/max-lines","count":262}
129
+ ```sh
130
+ quality baseline tighten
318
131
  ```
319
132
 
320
- The baseline is a JSON Lines file with one entry per line: the violations of one rule in one file. `count` is how many violations the file may have. For a rule that counts occurrences, such as `comments/no-jsdoc`, that is the number of occurrences. For a rule with a limit, it is the amount over the limit: the entry above lets `src/server/db.ts` have 412 lines under a limit of 150. Entries are sorted by path and then rule, and each tool that changes the file rewrites only the lines it changes, so a diff names exactly the entries that moved and two branches conflict only when they touch the same or neighbouring entries.
321
-
322
- `quality lint` fails when a baselined file has more violations than its `count`; the report then lists all of that file's violations for the rule. A file at or below its entry passes. An entry that allows more than is left, including a file with no violations left, is listed as a note and does not fail, so fixing debt never breaks the build; `tighten` and `prune` lower it. An entry for a rule that is off or unknown fails until it is pruned. The baseline covers violations at any level.
323
-
324
- - `quality baseline write` records every error-level violation when there is no baseline yet. Once a baseline exists it refuses, unless `--rule <id>` names the rules to record: it then replaces the entries of those rules with what the files have now, and leaves every other entry as it is.
325
- - `quality baseline tighten` lowers and removes the entries of files changed since `HEAD`, and with `--staged`, of the files staged for the next commit. It carries the entry of a file that git sees as moved to the new path. Every other line stays byte for byte.
326
- - `quality baseline prune` does the same for every entry. It carries the entries of files moved since the merge base (see `--against` under [Command line](#command-line)), and prunes without following moves outside a git work tree.
327
- - `quality baseline migrate` moves a baseline from the earlier JSON format, `quality/baseline.json` or the file `--from` names, to the configured file, and removes the old one. An old entry that stored a size, such as a line count, becomes that size less the rule's current limit; an entry whose file is now within the limit is dropped.
133
+ `tighten` lowers entries for files changed since `HEAD`; `--staged` restricts those updates to staged files. `prune` considers every baseline entry. Both only lower or remove counts and can carry entries when git detects tracked moves. They count files on disk, so `tighten --staged` needs the unstaged changes stashed first.
328
134
 
329
- Neither `tighten` nor `prune` ever adds or raises an entry. They lower an entry to what its file has now, and remove it when that is 0. A moved file's entry keeps the lower of its old count and what the file has now. A move is seen when both of its sides are tracked, as after `git mv`; git's rename detection decides what counts as a move.
330
-
331
- ### Pre-commit
332
-
333
- Lowering entries in the commit that fixes them keeps the baseline current without a separate cleanup:
135
+ When a Quality upgrade adds a check, or a reviewed change needs to record one rule's findings again:
334
136
 
335
137
  ```sh
336
- quality baseline tighten --staged && git add quality/baseline.jsonl
337
- quality lint
138
+ quality baseline write --rule tests/story-setup
338
139
  ```
339
140
 
340
- `tighten` counts the files as they are on disk. When a commit stages only part of a file, stash the rest first (as lint-staged does), or the entry may be lowered below what the commit holds.
141
+ Keep the default error level. The command replaces that rule's entries and leaves other rules' entries alone. Any new or raised count belongs in the review and the pull request's Callouts. The [debt and hook guide](https://github.com/ShivaeDev/platform/blob/main/packages/quality/docs/debt-and-hooks.md) covers exception subjects, limit changes, renames and commit checks.
142
+
143
+ ### API
341
144
 
342
- ### Baseline growth
145
+ Import names from their defining module; there is no root entry. The package's `./*.ts` export pattern also exposes the gate's implementation modules. The [module reference](https://github.com/ShivaeDev/platform/blob/main/packages/quality/docs/modules.md) lists those exports; the following table lists the config, tool and extension entry points:
343
146
 
344
- A finding that the baseline does not cover always fails `quality lint`, locally and in CI; that is how new debt gets noticed. The baseline itself may grow: a reviewer sees each new or raised entry in the diff and judges it. When the failing findings are debt the baseline should keep, such as the debt of a file that moved or was renamed, record the rule again with `quality baseline write --rule <id>`. The report ends with that command for the rules that failed. Then call out the baseline growth in the pull request description.
147
+ | Module | Exports |
148
+ | --- | --- |
149
+ | `config.ts` | `defineConfig`, `QualityConfig`, `Level`, `RuleSetting`, `RuleSettings`, `BuiltInRules` |
150
+ | `rule.ts` | `defineRule`, `Rule`, `Configured`, `Finding`, `Findings`, `SourceFile`, `RuleInputs`, `RuleContext` |
151
+ | `standard-schema.ts` | `StandardSchemaV1`, `StandardResult`, `StandardIssue`, `describeIssue` |
152
+ | `imports/fences/dsl.ts` | `fence` |
153
+ | `imports/fences/selectors.ts` | `packages`, `folders`, `files`, `modules`, `scopes`, `anyOf`, `workspace`, `anything`, `external` |
154
+ | `imports/fences/model.ts` | `Fence`, `Target`, `Selector`, `Prohibition`, `Examples`, `Chain`, `ExampleStep`, `External` |
155
+ | `vitest.ts` | `testProjects`, `inheritTags` |
156
+ | `naming/testName.ts` | `testName`, `isTestCode`, `isTestSupport`, `ENVIRONMENTS`, `Environment`, `TestName` |
345
157
 
346
- A rule enters the baseline the same way: set it to `error`, run `quality baseline write --rule <id>` and commit both.
158
+ #### Config
347
159
 
348
- ### Changing a limit
160
+ | Key | Default | Meaning |
161
+ | --- | --- | --- |
162
+ | `sources` | `["."]` | Directories or files the source rules check, relative to the repository root. |
163
+ | `extensions` | `.ts`, `.tsx`, `.mts`, `.cts`, `.js`, `.jsx`, `.mjs`, `.cjs` | Source extensions. |
164
+ | `baseline` | `quality/baseline.jsonl` | Counts of existing findings. |
165
+ | `registry` | `quality/registry.json` | Permanent exceptions with reasons. |
166
+ | `local` | `[]` | Exposed extension API for rules made with `defineRule`; not required for adoption. |
167
+ | `rules` | Every rule at `error` | Options by rule id, such as import boundaries. An explicit `error` or `{ level: "error", options }` is also accepted. |
349
168
 
350
- A count depends on the configured limit, so changing a limit shifts every count of the rule. A looser limit leaves entries that allow more than is left: they pass, and `tighten` and `prune` lower them. A stricter limit makes the rule's files fail `quality lint`, since each is now further over the limit. Run `quality baseline write --rule <id>` to record the rule again under the new limit, and commit it with the config change. The raised entries show in the diff, and the pull request calls them out.
169
+ #### Local rules
351
170
 
352
- ## Registry
171
+ The exposed local-rule API supplements the gate; a repository does not need to use it to get the shared policy. The [rule reference](https://github.com/ShivaeDev/platform/blob/main/packages/quality/docs/rules.md#local-rule-api) gives complete extension examples. `defineRule` takes `id`, `description`, `check`, optional `options` and optional `registrable: false`. Its check receives `root`, checked paths in `files`, source files in `sources`, `readText(path)` and decoded `options`. A source file has `path`, `text` and editor-numbered `lines`; `readText` returns `undefined` for an absent file.
353
172
 
354
- ```json
355
- [
356
- {
357
- "rule": "structure/max-lines",
358
- "file": "src/generated/client.ts",
359
- "reason": "Generated from the OpenAPI document; splitting it would mean forking the generator."
360
- }
361
- ]
362
- ```
173
+ | Finding field | Meaning |
174
+ | --- | --- |
175
+ | `file` | Required. A path relative to the config root; an absolute path under it is made relative. |
176
+ | `message` | Required. What went wrong. |
177
+ | `line` | Optional. One-based source location. |
178
+ | `subject` | Optional. A stable name to distinguish exceptions within a rule and file. |
179
+ | `count` | Optional. Number of violations represented, one by default. |
180
+ | `threshold` | Optional. The limit the rule applied. |
363
181
 
364
- An entry covers a rule's violations in one file, for good, and must say why. With a `subject`, it covers only the violations with that subject. The registry applies before the baseline. An entry that covers nothing, or names a rule that is off, unknown or takes no exceptions, fails the gate until it is removed.
182
+ A limit rule reports its amount over the limit as `count` and the limit as `threshold`. The built-in file-size and comment-count rules use that shape; the baseline stores the count.
365
183
 
366
- ## Command line
184
+ #### Command line
367
185
 
368
186
  ```text
369
- quality lint [--config <file>] [--warnings summary|all]
187
+ quality lint [--config <file>]
370
188
  quality fix [--config <file>]
371
189
  quality baseline write [--config <file>] [--rule <id>]...
372
190
  quality baseline prune [--config <file>] [--against <ref>]
373
191
  quality baseline tighten [--config <file>] [--staged]
374
192
  quality baseline migrate [--config <file>] [--from <file>]
193
+ quality hooks install [--config <file>] [--force]
194
+ quality hooks uninstall
195
+ quality hooks pre-commit [--config <file>]
375
196
  ```
376
197
 
377
- `quality` alone runs `lint`. `--against <ref>` names the branch the work merges into; `prune` follows the moves since its merge base. Without it, `prune` takes `origin/HEAD`, `origin/main` or `origin/master`, whichever exists first, and prunes without following moves when it finds no merge base. With `--against`, a missing merge base exits 2; in a shallow clone, run `git fetch --unshallow` first. The report groups violations by rule and states each rule's description once; warnings are summarized per rule with the files that have the most, and `--warnings all` lists each one.
198
+ `quality` alone runs lint. The commands above use the default error-level policy. `prune --against` follows renames since the target's merge base; without a target it tries `origin/HEAD`, `origin/main`, then `origin/master`, and prunes without following moves when no merge base is available. An explicit target with unavailable history fails; fetch the needed history before rerunning. `migrate` converts a JSON baseline into the current JSON Lines format.
378
199
 
379
200
  | Exit code | Meaning |
380
201
  | --- | --- |
381
- | 0 | Passed. Warnings may remain. |
382
- | 1 | Failed: an uncovered error-level violation, a baselined file that got worse, a stale registry entry, or a baseline entry for a rule that is off or unknown. |
383
- | 2 | Could not run: no or invalid config, an invalid baseline or registry, a baseline left in the earlier format, a missing source, a rule that threw, git history `prune --against` cannot read, a `quality fix` that does not settle in 5 rounds, or a usage error. |
384
-
385
- ## Biome preset
202
+ | `0` | Passed. Retained debt or loose baseline entries may remain; see the severity limitation below. |
203
+ | `1` | Failed: uncovered errors, stale registry entries or baseline entries for unknown or disabled rules. |
204
+ | `2` | Could not run: invalid input or config, missing files or git history, a failed rule, fixes that do not settle, or invalid usage. |
386
205
 
387
- `@shivaedev/quality/biome` is one Biome setup for every repository. Biome is a dependency of this package, pinned to an exact version, and `quality` runs it, so a repository needs no Biome install of its own. The root `biome.json` extends the preset and adds only what is the repository's own:
206
+ ### Install, shared tooling and limits
388
207
 
389
- ```json
390
- {
391
- "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
392
- "extends": ["@shivaedev/quality/biome"]
393
- }
208
+ ```sh
209
+ pnpm add --save-dev @shivaedev/quality @effect/platform-node@4.0.0-rc.112 @effect/platform-node-shared@4.0.0-rc.112 effect@4.0.0-rc.112
394
210
  ```
395
211
 
396
- The preset sets:
397
-
398
- - **Formatting:** tabs, a line width of 150, double quotes, semicolons, trailing commas and operators at the start of a wrapped line. An object key keeps its quotes, because a quoted key marks a [foreign name](#foreign-names).
399
- - **Lint:** every rule Biome recommends, at `error`, and a list of stricter rules on top, among them `noUnsafeTypeAssertion`, `useBlockStatements`, `useNumericSeparators`, `useUnicodeRegex`, `useLiteralKeys`, `noFloatingPromises`, `useExhaustiveDependencies`, `it` for every test, no viewport that disables zoom (`noNonScalableViewport`), function declarations over function expressions, interfaces for object types, a cognitive complexity limit of 15, no nested ternaries, and the naming rules under [Naming](#naming).
400
- - **Imports:** organized in five groups: Node and Bun builtins, packages, `@shivaedev/*` packages, aliases and relative paths. Biome counts as an alias a specifier that starts with `#`, `@/`, `~`, `$` or `%`; a tsconfig path such as `@app/*` sorts with the packages, and Biome's `noUndeclaredDependencies` takes it for one, so name aliases in a form Biome recognizes.
401
- - **Assist:** organized imports and sorted keys, attributes, enum members, interface members and properties. Keys are sorted in JSON and in object literals alike, but only reported: `quality fix` leaves the order to the author. `package.json` is left out, because `manifests/sorted` gives it the order npm users expect.
402
- - **Plugins:** GritQL rules that ban ambient time, randomness, `console` and `process.env` for Effect's services, and the naming plugins under [Naming](#naming). They load from `./node_modules/@shivaedev/quality/biome/plugins`, so the package must be installed at the repository root.
403
- - Files ignored by git are skipped.
404
-
405
- The preset turns off `noUnusedVariables` and `noUnusedFunctionParameters`, because the tsconfig presets report them through TypeScript, allows default exports in `*.config.*` files, which tools load through the default export, and turns off `useComponentExportOnlyModules` in `*.test.*` and `*.spec.*` files, which define the helper and harness components they render. It also turns off `noProcessGlobal`, `useJsonImportAttributes`, `noMisusedPromises`, `useExhaustiveSwitchCases`, `useSortedClasses`, `noDelete`, `useConsistentArrayType`, `useConsistentCurlyBraces`, `noEqualsToNull` and `noSkippedTests`, because `quality fix` applies every lint fix and their fixes changed behavior or did not terminate on real code. It keeps `noUndeclaredClasses` off, because the rule cannot resolve a stylesheet imported through an alias and Tailwind utilities are not declared in CSS, and `noInlineStyles` off, because an inline style is the right tool for a value computed at run time. It declares these weakenings in its `declarations.json`, so `suppressions/biome-overrides` takes them as declared and `suppressions/biome-recommended` accepts the recommended rules among them. Every other weakening a repository adds is an override it declares with a reason.
212
+ Node 24 or later is required. The CLI loads the TypeScript config through Node's native import; use erasable syntax and `.ts` extensions for relative TypeScript imports. Installing the package does not install a hook; opt in with `quality hooks install`.
406
213
 
407
- An autofix applies only a change that removes no decision; a fix that can change behavior or delete something written on purpose reports only, and the author decides. The preset keeps `noAccessKey`, `noAriaHiddenOnFocusable`, `noAutofocus`, `noInteractiveElementToNoninteractiveRole`, `noNoninteractiveElementToInteractiveRole`, `noNoninteractiveTabindex`, `noRedundantRoles`, `useValidAriaProps`, `useValidAriaRole`, `noImportantStyles`, `noConstAssign`, `noUnusedPrivateClassMembers`, `useExhaustiveDependencies`, `noFloatingPromises`, `useConsistentTestIt`, `useRegexpTest`, `useUnicodeRegex`, `noNonNullAssertion`, `useAtIndex`, `noParametersOnlyUsedInRecursion` and `useNamingConvention` at `error` with their fixes off, and `quality fix` skips `noDuplicateObjectKeys` and `useSortedKeys`, whose fixes Biome's config cannot turn off.
214
+ The package includes `@shivaedev/quality/biome`, `@shivaedev/quality/tsconfig/base.json` for type-checking, and `@shivaedev/quality/tsconfig/package.json` for emitted JavaScript, declarations and maps. The [shared tooling guide](https://github.com/ShivaeDev/platform/blob/main/packages/quality/docs/tooling.md) explains their settings and complete configs.
408
215
 
409
- The `biome` rule runs `biome check` with the repository's config and reports each finding as `biome/<category>`, such as `biome/lint/style/useBlockStatements`, `biome/assist/source/useSortedKeys`, `biome/format` or `biome/plugin`, so Biome's findings go through the baseline like any other rule's. `quality baseline write --rule biome` takes in every Biome category at once. A finding below `error`, such as a rule a repository declared at `warn`, is not reported. The rule also asks for a root `biome.json` or `biome.jsonc` that extends the preset, and it takes no registry exceptions. A Biome config that Biome cannot load stops the run with Biome's message.
410
-
411
- `quality fix` sorts every `package.json`, then runs `biome check --write --unsafe`, which applies Biome's lint fixes, unsafe ones included, its assist actions, such as organized imports, and its formatting, except the fixes that report only. It then runs Biome's formatter once more, because a lint fix can leave code unformatted. One fix can make room for another, so it repeats both passes until a round rewrites nothing, at most 5 rounds; when files still change in the fifth round, it names them and exits 2. An unsafe fix can change behavior, such as `==` becoming `===`, so review what it changed. An editor that runs Biome on save uses the same version when it resolves Biome from the root `node_modules`, so a repository that wants that installs `@biomejs/biome` at the version this package pins.
412
-
413
- ## Naming
414
-
415
- A name says what a thing is, and the same kind of thing is named the same way everywhere. Every naming rule is an error without an autofix: nothing renames code or adds a `_` prefix. The message says what is wrong and which shapes are valid, and the author picks the name. Unused variables and parameters stay TypeScript errors; remove them instead of prefixing them.
416
-
417
- ### Identifiers
418
-
419
- | What | Valid shape | Checked by |
420
- | --- | --- | --- |
421
- | Variables, functions and parameters | camelCase: `itemCount`, `loadItem`. A component or a class is PascalCase. | `useNamingConvention` |
422
- | Acronyms | Spelled as a word: `HttpClient`, `userId`, `parseUrl`. Never `HTTPClient`, `userID` or `parseURL`. | `useNamingConvention` with `strictCase` |
423
- | Types, interfaces, classes and enums | PascalCase, without an `I` prefix: `Item`, not `IItem`. | `useNamingConvention` |
424
- | Type parameters | `T`, or `T` followed by a PascalCase name: `TItem`, `TResult`. Effect's positional `A`, `E` and `R` are valid as they are. | `useNamingConvention` |
425
- | Module-level constants | A string or number literal at module level is CONSTANT_CASE: `const MAX_ITEMS = 50`. A constant inside a function is camelCase. | `constant-names` plugin, `useNamingConvention` |
426
- | Object keys and type properties | camelCase or PascalCase, after any leading `_` or `$` (`_tag`, `$transaction`), also in a `Record`. A quoted key is a [foreign name](#foreign-names) and is not checked. | `key-names` plugin |
427
- | Schemas | PascalCase and named like their type: `const Item = Schema.Struct(...)` with `type Item = typeof Item.Type`. | `schema-names` plugin |
428
- | `Schema.Struct` fields | camelCase, also when quoted. Outside data keeps its keys only at the edge: `Schema.Struct({ createdAt: Schema.String }).pipe(Schema.encodeKeys({ createdAt: "created_at" }))`. | `schema-struct-keys` plugin, `key-names` plugin |
429
- | `Effect.fn` spans | `"Owner.operation"`, where the owner is the service or module and the operation is the name the function is bound to: `const loadItem = Effect.fn("ItemStore.loadItem")`, and `loadItem: Effect.fn("ItemStore.loadItem")` in an object. | `effect-fn-spans` plugin |
430
- | `Effect.gen` functions | A top-level function whose whole body is `Effect.gen` is an `Effect.fn`: `const loadItem = Effect.fn("ItemStore.loadItem")(function* (id: string) { … })`, or `Effect.fnUntraced` when it needs no span. | `effect-fn-functions` plugin |
431
- | Effect test bodies | In test code, a test takes the generator itself: `it.effect("loads the item", function* () { … })`, never `() => Effect.gen(function* () { … })`. The test helper runs it with `Effect.gen`. | `effect-test-bodies` plugin |
432
- | Service Layers | A static `layer` on the service, read as `ItemStore.layer`. No exported `ItemStoreLive` or `ItemStoreLayer` constant. A Layer that wires an application together stays unexported. | `service-layers` plugin |
433
- | Private members | A `#field`. No `private`, `protected` or `public` modifier. | `useConsistentMemberAccessibility` |
434
- | React | A function passed to an `onX` prop is `handleX`, an `onX` prop forwarded as it is, or a state setter passed as it is: `onOpenChange={setOpen}`. A context is `ThemeContext`, a ref `inputRef`, an id `fieldId`. A module that exports a component exports only components; a test may define the components it renders. | `handler-names` plugin, `useReactNamingConvention`, `useComponentExportOnlyModules` |
435
- | Booleans | A question: `isOpen`, `hasSave`, `canRetry`, `shouldFlush`. A name the DOM or React gives, such as `open` or `disabled`, stays. This is a convention only; no rule checks it. | Review |
436
-
437
- ### Foreign names
438
-
439
- Some names are not ours to choose: a query parameter an outside API reads, an environment variable, an operator a query builder takes. Write such a name as a quoted key, and the naming rules skip it:
216
+ For Vitest, install `vitest` and `happy-dom` when the repository has DOM tests, then use:
440
217
 
441
218
  ```ts
442
- const query = { "per_page": 50, "sort_by": "created" };
443
- const env = { "DATABASE_URL": url };
444
- const where = { "OR": [{ id }, { slug }] };
445
- ```
446
-
447
- An unquoted key is our own name and is camelCase or PascalCase. The formatter keeps the quotes as written, so a quoted key stays quoted. A `Schema.Struct` field is the exception: the struct is our model of the data, so the `schema-struct-keys` plugin reports a snake_case field even when it is quoted, and points to `Schema.encodeKeys`, which maps camelCase fields to the outside names at the edge.
448
-
449
- ### Re-exports
450
-
451
- No file re-exports, package entry files included: every module is imported from the file that defines it. `noBarrelFile` reports `export { … } from`, `noReExportAll` reports `export *`, the `type-re-exports` plugin reports `export type … from`, and `noExportedImports` reports an import that is exported again. Its message suggests `export … from`, which is a re-export as well: import the name where it is used instead.
452
-
453
- The plugins report under the one Biome category `plugin`, so their findings share the baseline rule `biome/plugin`.
454
-
455
- ### Files
456
-
457
- `files/named-after-export` names a code file after its main export, and the folders above it are the prefix. The export takes the words of the file name, in order, and may add words of its folders around them, in any order and in singular or plural. Folders count from the package root, without a leading `src`; the package name does not count. The file's first letter follows the export's case.
458
-
459
- | File | Exports | Valid |
460
- | --- | --- | --- |
461
- | `routers/items/list.ts` | `listItems` or `itemList` | Yes |
462
- | `routers/items/Create.ts` | `CreateItemRouter` | Yes |
463
- | `item/Panel.tsx` or `ItemPanel.tsx` | `ItemPanel`, with `ItemPanelProps` beside it | Yes |
464
- | `Panel/Panel.tsx` | `Panel`: the main file of a module folder repeats the folder | Yes |
465
- | `limits.ts` | `MAX_ITEMS` and `MAX_DEPTH`: a topic file of several exports is camelCase | Yes |
466
- | `ItemPanel.tsx` | `export default memo(ItemPanel)`: a default export counts under the name it resolves to | Yes |
467
- | `setupTests.ts` | Nothing, so it is camelCase | Yes |
468
- | `script/build-docs.ts` | Run by a `package.json` script or `bin`, or starts with `#!`, so it is kebab-case | Yes |
469
- | `order.ts` | `type Order` | No: the file's case follows the export, so it is `Order.ts` |
470
- | `maxItems.ts` | `MAX_ITEMS` alone | No: constants live in a topic file with related constants |
471
- | `items/listItems.ts` | `listItems` | No: the folder is the prefix, so it is `items/list.ts` |
472
-
473
- Test files are left to the test rules. The `toolOwned` option lists `.gitignore` patterns of files whose names a tool fixes, `*.config.*` and declaration files by default.
474
-
475
- ### Folders
476
-
477
- `files/folder-names` checks every folder above a checked file.
478
-
479
- - A module folder takes the name of its main file, inside it or beside it: `Panel/` with `Panel/Panel.tsx`, or `Dialog/` beside `Dialog.tsx`. A PascalCase folder without that file is a finding.
480
- - A package folder is its package name without the scope, so `@acme/ui-kit` lives in `ui-kit/`.
481
- - A folder that only groups files is kebab-case: `test-support/`, `.github/`.
482
- - A folder the `content` option lists is snake_case: `forest_path/`.
483
-
484
- The `toolOwned` option lists folders a tool names, `generated/` and `migrations/` by default.
485
-
486
- ### Other files
487
-
488
- `files/other-names` names the files that are not code.
489
-
490
- - Markdown, JSON, GritQL, images, fonts and SVG are kebab-case, with optional dotted parts: `release-notes.md`, `icons.sprite.svg`. Conventional upper-case names stay: `README.md`, `CHANGELOG.md`, `LICENSE`, `AGENTS.md`, `CLAUDE.md`, `CONTRIBUTING.md`, `SECURITY.md` and `SKILL.md`.
491
- - A stylesheet that one component imports is named after it: `ItemPanel.css` beside `ItemPanel.tsx`. A shared stylesheet is kebab-case: `form-controls.css`. An import is a static `import` in a code file, relative or through a `#` alias from the `imports` of its `package.json`, or an `@import` in a stylesheet; a stylesheet that another stylesheet imports is shared. A path in a string, such as a test that reads the file, is not an import.
492
- - A Prisma schema is camelCase: `schema.prisma`.
493
- - A file the `content` option lists is snake_case: `forest_path.json`. A double underscore separates the parts of an id: `forest_path__clearing.json`.
494
-
495
- The `toolOwned` option lists files a tool names: dotfiles, `package.json`, `tsconfig*.json`, `biome.json`, `*.config.*`, `migrations/` and `generated/` by default.
496
-
497
- ### Tests
498
-
499
- A test sits beside the code it covers, and its name says what it covers and where it runs.
500
-
501
- | File | Covers | Valid |
502
- | --- | --- | --- |
503
- | `cart/cart.test.ts` | `cart/cart.ts` | Yes |
504
- | `cart/cart.typecheck.test.ts` | The types of `cart/cart.ts`, checked by the compiler | Yes |
505
- | `cart/Basket.dom.test.tsx` | `cart/Basket.tsx`, in a DOM | Yes |
506
- | `cart/checkoutFlow.spec.ts` | A behaviour of the `cart/` folder as a whole, such as a flow across several files | Yes |
507
- | `cart/checkoutFlow.dom.spec.tsx` | The same, in a DOM | Yes |
508
- | `cart/checkoutFlow.typecheck.spec.ts` | The types of that behaviour, checked by the compiler | Yes |
509
- | `cart/totals.test.ts` | No `cart/totals.ts` beside it | No: a `.test` follows a file, and a test of the folder is a `.spec` |
510
- | `cart/cart.spec.ts` | | No: a `.spec` names a behaviour, so it may not share a stem with a file beside it |
511
- | `cart/cart.hydration.test.ts` | | No: an aspect gets its own file in the module's folder, or a `.spec` |
512
- | `cart/cart.postgres.test.ts` | | No: a database or a running app comes from the app's test fixture, not a file name |
513
- | `test/cart.test.ts` | | No: a test folder mirrors the source tree; the test sits beside `cart.ts` |
514
-
515
- `tests/follow` checks the name: `<file>[.<environment>].test.ts` beside its file, or `<behaviour>[.<environment>].spec.ts` in camelCase in the folder it covers. The environment is at most one of `dom`, `slow` and `typecheck`, the ones the [Vitest projects](#vitest-projects) run, and a test that imports `@testing-library/*` or reads `document` or `window` is a `.dom` test.
516
-
517
- `tests/colocated` checks the place: a test in a `test`, `tests`, `__tests__` or `spec` folder is a finding.
518
-
519
- Both rules take a `suites` option, `.gitignore` patterns of folders that hold tests with their own layout, such as tests across several packages or a Playwright suite, whose `.spec.ts` files mean something else. The rules skip those folders.
520
-
521
- ### Test code
522
-
523
- Test code is a test file, as `tests/follow` reads the name (`*.test.ts` or `*.spec.ts`, with any environment), or any file under a folder named `test-support/`. `test-support/` is the one folder name for the fixtures, harnesses and generated clients that tests share. Test code ships nowhere, so the rules that check shipped code skip it: `imports/fences` never holds it, and `structure/max-lines` gives it the test limit. The test rules still read only test files: a file under `test-support/` is not a test, so `tests/follow`, `tests/colocated` and `tests/story-setup` leave it alone.
524
-
525
- ## Story tests
526
-
527
- A test reads as a short story over the domain: what exists, what someone does, and what is true afterwards. The words come from a story kit that the repository keeps in `test-support/`, built with [`@shivaedev/test-story`](https://github.com/ShivaeDev/platform/tree/main/packages/test-story#readme), so a test says what it needs in domain words and never builds state by hand.
528
-
529
- ```ts
530
- shop.it("ships a paid order from stock", [hasInStock(3, "lamp"), hasInCart(1, "lamp")], function* ({ customer, warehouse }) {
531
- customer.checksOut();
532
- yield* warehouse.shipsEverything();
219
+ import { testProjects } from "@shivaedev/quality/vitest.ts";
220
+ import { defineConfig } from "vitest/config";
533
221
 
534
- expect(warehouse.shipped()).toEqual(["lamp"]);
535
- expect(warehouse.inStock("lamp")).toBe(2);
536
- });
222
+ export default defineConfig({ test: testProjects() });
537
223
  ```
538
224
 
539
- - **Setup is traits.** A trait is one sentence of setup in domain words and the change it makes, such as `hasInStock(3, "lamp")`. A test hands the traits it needs to the kit's `it`, which starts a fresh engine and seeds them, and a setup that recurs becomes one named trait made of others. A trait that asks for an impossible state refuses and fails the test, instead of seeding it quietly.
540
- - **Actions go through real entry points.** The test acts through the kit's verbs, which reach the command, service, route or UI path a user or caller reaches, never by writing internal state. A verb that lets the engine run, such as `warehouse.shipsEverything()`, steps it with `story.runUntil` until what it waits for holds.
541
- - **Failures print the story.** Every trait, verb and engine step tells a line of the story, and a failing test prints the story with where it stopped and the engine's state, so the failure says what happened, not only which values differ.
542
-
543
- `tests/story-setup` holds test files to this. In a test file, it reports:
225
+ `testProjects` runs unit tests and specs in Node and `.dom` tests and specs in happy-dom by default. Each test gets 30 seconds instead of Vitest's 5, because tests that start servers, browsers or CLIs take seconds on a busy CI runner. `.slow` tests run only when `--project slow` asks for them. Type tests are excluded from Vitest and belong in the compiler's input. The helper takes no options and rejects suite exclusions.
544
226
 
545
- - a top-level function whose name starts with the word `seed`, `make`, `build` or `setup`, declared as a function or as a variable that holds an arrow function or function expression: `function seedCart()`, `const makeRun = () => …`. The prefix is a whole word, so `builder` and `settings` pass;
546
- - a call that writes fixture files through Node's `fs` or `fs/promises`: `writeFile`, `appendFile`, `mkdir`, `mkdtemp`, `copyFile`, `cp` and `symlink`, each in its sync form too, whether imported by name, through a namespace or default import, or through `promises`.
227
+ #### PostgreSQL URL gate
547
228
 
548
- Each finding names its line and has the helper or the function it calls as its subject. Its message asks for the setup as traits of the story kit in `test-support/` and points here. A file under `test-support/` is not a test, so the kit itself builds state and writes files freely. The rule only reports; nothing rewrites a test. A repository adopts it through the baseline: `quality baseline write --rule tests/story-setup`.
229
+ A skipped test proves nothing. `noSkippedTests` reports unconditional skips as errors, and its automatic fix is disabled: repair the test or remove a claim it cannot prove. Slow tests belong in the slow project.
549
230
 
550
- ## Vitest projects
551
-
552
- `@shivaedev/quality/vitest.ts` sets up the tests of a package by file name, so no test file sets its environment with a pragma. It needs `vitest`, and `happy-dom` for DOM tests. A package's `vitest.config.ts` reads:
231
+ The **PostgreSQL URL gate** is the only permitted skip convention: a test that needs real PostgreSQL registers conditionally on its `PLATFORM_EFFECT_*_TEST_DATABASE_URL`. Skip only when that URL is absent; with a URL, execute the body against PostgreSQL and let connection or setup failures fail the test. Nothing else may skip. Keep the condition next to the database setup, either in the test file or its shared `test-support` module:
553
232
 
554
233
  ```ts
555
- import { testProjects } from "@shivaedev/quality/vitest.ts";
556
- import { defineConfig } from "vitest/config";
234
+ import { test } from "vitest";
235
+ import { databaseUrl } from "#test/postgres.ts";
557
236
 
558
- export default defineConfig({ test: testProjects() });
237
+ const integration = test.skipIf(databaseUrl === undefined);
559
238
  ```
560
239
 
561
- | Project | Files | Environment | Runs |
562
- | --- | --- | --- | --- |
563
- | `unit` | `*.test.ts`, `*.spec.ts` and the other test and spec files | Node | By default |
564
- | `dom` | `*.dom.test.tsx`, `*.dom.spec.tsx` and the other `.dom` files | happy-dom | By default |
565
- | `slow` | `*.slow.test.ts`, `*.slow.spec.ts` and the other `.slow` files | Node | Only with `vitest run --project slow` |
566
-
567
- Type tests (`*.typecheck.test.ts`, `*.typecheck.spec.ts` and `typecheck.test.ts`) are in no project: the compiler checks them. A test that is too slow for every run goes into the slow project instead of being skipped.
240
+ `databaseUrl` is the test database URL read by the repository's environment helper. Declare the real PostgreSQL tests with `integration(name, body)`. A conditionally registered suite or Effect tester follows the same URL gate. CI must supply the URL for every such test; a green run with missing URLs gives no PostgreSQL evidence.
568
241
 
569
- A folder that another runner owns, such as a Playwright suite of `.spec.ts` files, is left out with `testProjects({ exclude: ["e2e/**"] })`; the globs are added to every project's `exclude`.
242
+ #### Inherited tags
570
243
 
571
- ### Inherited tags
572
-
573
- Vitest gives an inline project only the [tags](https://vitest.dev/guide/test-tags) it declares itself, never those of a config file it `extends`. A test tagged in that file then fails with `cannot apply "<tag>" tag for this test` as soon as a root config runs it. A root config that gathers the projects of several packages, or whose projects extend a shared base config, passes them through `inheritTags`:
244
+ A root Vitest config that gathers inline projects from package configs uses `inheritTags(projects, root)` to copy the tags of each extended config into its project:
574
245
 
575
246
  ```ts
576
247
  import { inheritTags } from "@shivaedev/quality/vitest.ts";
@@ -578,52 +249,24 @@ import { defineConfig } from "vitest/config";
578
249
 
579
250
  const bakery = { extends: "./packages/bakery/vitest.config.ts", root: "./packages/bakery", test: { name: "bakery" } };
580
251
 
581
- export default defineConfig(async () => ({ test: { projects: await inheritTags([bakery], import.meta.dirname) } }));
252
+ export default defineConfig(async () => ({
253
+ test: { projects: await inheritTags([bakery], import.meta.dirname) },
254
+ }));
582
255
  ```
583
256
 
584
- `inheritTags(projects, root)` loads the config file of each inline project whose `extends` is a path, resolved against `root`, and adds the tags that file declares to the project's own, so `vitest run --tags-filter=<tag>` works across the workspace. A tag the project declares itself wins over an inherited one of the same name. The config may export an object, a promise or a function. A project that extends `true`, and a glob or file path, comes back unchanged, because Vitest already gives those the root's tags. The extended files load with Node's own `import`, as `vitest --configLoader native` loads configs, so each must be a module Node runs without a bundler.
585
-
586
- ## tsconfig presets
257
+ Vitest otherwise drops tags from an inline project's extended config. A project's own tag wins over an inherited tag with the same name; `extends: true`, globs and file-path projects stay unchanged. The [tooling guide](https://github.com/ShivaeDev/platform/blob/main/packages/quality/docs/tooling.md#inherited-tags) gives the config-loading limits.
587
258
 
588
- Two presets hold one strict, current TypeScript setup for every repository. They target TypeScript 7.
259
+ #### Configuration limits
589
260
 
590
- | Preset | Use |
591
- | --- | --- |
592
- | `@shivaedev/quality/tsconfig/base.json` | Type-checking with `noEmit`: applications, tests and scripts. |
593
- | `@shivaedev/quality/tsconfig/package.json` | Building a package: the base plus `declaration`, `declarationMap`, `sourceMap` and `rewriteRelativeImportExtensions`, so relative `.ts` imports are emitted as `.js`. |
261
+ All built-in and local rules run at `error`. Config settings cannot disable a rule or downgrade it to a warning. Record existing debt in the baseline instead. Source and test line limits can only be lowered from 150 and 300, and the comment limit from 2; custom test-file patterns and inventory exclusions are rejected.
594
262
 
595
- ```json
596
- {
597
- "extends": "@shivaedev/quality/tsconfig/base.json",
598
- "compilerOptions": {
599
- "lib": ["ESNext", "DOM"],
600
- "types": ["node"]
601
- },
602
- "include": ["src", "test"]
603
- }
604
- ```
605
-
606
- The base sets:
607
-
608
- - **Language:** `target` and `lib` ESNext, so the newest syntax is emitted as written and the newest built-ins are typed.
609
- - **Modules:** `module` ESNext, `moduleResolution` bundler, `moduleDetection` force, `verbatimModuleSyntax`, `isolatedModules`, `allowImportingTsExtensions`, `resolveJsonModule`, `noUncheckedSideEffectImports`, `forceConsistentCasingInFileNames` and `libReplacement: false`.
610
- - **JavaScript:** `allowJs` and `checkJs`, so JavaScript files are type-checked with the TypeScript ones.
611
- - **Erasable syntax only:** `erasableSyntaxOnly` rejects syntax that type stripping cannot erase, such as parameter properties.
612
- - **Checks:** `strict`, with `noImplicitAny`, `strictBuiltinIteratorReturn` and `useUnknownInCatchVariables` stated explicitly, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, `noImplicitOverride`, `noImplicitReturns`, `noFallthroughCasesInSwitch`, `noUnusedLocals`, `noUnusedParameters`, `allowUnreachableCode: false` and `allowUnusedLabels: false`.
613
- - **Compilers:** `stableTypeOrdering`, so a TypeScript 6 build orders types the way TypeScript 7 does, and `skipLibCheck`.
614
-
615
- `noPropertyAccessFromIndexSignature` stays off, so a map-like key is read with dot access, and `isolatedDeclarations` stays off, so exported values keep their inferred types. `types`, `jsx`, `paths`, `include` and the output directories belong to each project. A project that needs more built-ins, such as the DOM, sets `lib` itself and keeps `ESNext` in it.
616
-
617
- A package that type-checks its tests with one config and builds `src` with another extends both, the package preset last:
618
-
619
- ```json
620
- {
621
- "extends": ["./tsconfig.json", "@shivaedev/quality/tsconfig/package.json"],
622
- "compilerOptions": { "outDir": "dist", "rootDir": "src" },
623
- "include": ["src"]
624
- }
625
- ```
263
+ Consumer Biome declarations require a reason and matching scope. An individual lint rule or assist action can be excepted only for explicit files; whole checks, rule groups and blanket scopes are rejected. `files/includes` declarations keep `**` and list named directory exclusions, such as generated test clients. The [rule reference](https://github.com/ShivaeDev/platform/blob/main/packages/quality/docs/rules.md#declared-biome-overrides) describes the matching and stale-declaration checks. Quality's shipped exceptions remain part of its internal policy.
626
264
 
627
- ## Validation
265
+ Local-rule APIs supplement the shared policy. They are not setup requirements. Use the shared defaults; repository-specific inputs such as source locations and fences supply information the policy needs.
628
266
 
629
- `pnpm ready` checks formatting, TypeScript 7, the rules, the import graph against seeded repositories and the fence policy against its examples, config, discovery, registry, baseline and report behavior, the command line against seeded repositories and git histories, the Biome preset against every rule Biome recommends and against its declarations, its naming rules and plugins against seeded files that break and keep each one, the file, folder, other-file and test naming rules against seeded trees, `tests/story-setup` against seeded test, source and `test-support/` files, the `biome` and `manifests/sorted` rules and `quality fix` against seeded repositories, the Vitest projects against a seeded repository that Vitest runs, an installed tarball consumer that type-checks a config and runs the `quality` bin through a baseline that takes in the preset's lint and plugin findings, and installed consumers that extend each tsconfig preset, type-check a fixture with an expected error for each check the base turns on, and run the package preset's build output.
267
+ - The gate checks static source policy. A successful import graph, fence example or test naming check does not prove an application's HTTP, database or browser behavior.
268
+ - The source inventory and Biome have their own scopes. `quality fix` runs Biome from the config's folder; it is not restricted to the source-rule inventory.
269
+ - The installed hook checks files on disk and uses each committing worktree's config and dependencies. It runs later commands after a reported lint failure or a nonzero command exit; a setup or process-launch failure stops the run.
270
+ - A pre-existing `core.hooksPath` decides which hook git runs. Quality keeps it and reports when its installed hook needs to be called from that hook.
271
+ - Workspace imports require a declaration in the importing package’s `dependencies`, `peerDependencies` or `optionalDependencies`. Tests, `test-support`, package-root tooling folders (`script`, `scripts`, `tooling`) and package-root config files may also use `devDependencies`. Explicitly export-blocked bundle inputs may use them when no runtime module reaches those inputs in the scanned import graph; an executable or another public export keeps its imports in the runtime boundary. See the [manifest rule](https://github.com/ShivaeDev/platform/blob/main/packages/quality/docs/rules.md#manifest-dependencies).
272
+ - Import fences skip test code. Keep the tests honest through their real fixtures and review; use the registry only where the rule allows it.