pasika 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -9
- package/dist/cli/index.js +34 -40
- package/dist/cli/output.d.ts +3 -0
- package/dist/cli/output.js +11 -0
- package/dist/enforcement/coverage.d.ts +5 -1
- package/dist/enforcement/coverage.js +68 -39
- package/dist/enforcement/doctor.d.ts +16 -0
- package/dist/enforcement/doctor.js +249 -0
- package/dist/enforcement/parse-docs.js +5 -1
- package/dist/enforcement/types.d.ts +0 -3
- package/dist/enforcement/types.js +4 -13
- package/dist/eslint/pasika/ast-types.d.ts +122 -0
- package/dist/eslint/pasika/ast-types.js +1 -0
- package/dist/eslint/pasika/index.d.ts +58 -3
- package/dist/eslint/pasika/index.js +61 -1
- package/dist/eslint/pasika/rules/application-structure.d.ts +2 -0
- package/dist/eslint/pasika/rules/application-structure.js +152 -0
- package/dist/eslint/pasika/rules/component-conventions.d.ts +16 -0
- package/dist/eslint/pasika/rules/component-conventions.js +145 -0
- package/dist/eslint/pasika/rules/component-nesting.d.ts +12 -0
- package/dist/eslint/pasika/rules/component-nesting.js +69 -0
- package/dist/eslint/pasika/rules/config-extraction.d.ts +14 -0
- package/dist/eslint/pasika/rules/config-extraction.js +74 -0
- package/dist/eslint/pasika/rules/cross-feature-import.d.ts +2 -0
- package/dist/eslint/pasika/rules/cross-feature-import.js +75 -0
- package/dist/eslint/pasika/rules/css/apply-usage.d.ts +10 -0
- package/dist/eslint/pasika/rules/css/apply-usage.js +45 -0
- package/dist/eslint/pasika/rules/css/base-layer-pair.d.ts +10 -0
- package/dist/eslint/pasika/rules/css/base-layer-pair.js +49 -0
- package/dist/eslint/pasika/rules/css/css-variable-naming.d.ts +10 -0
- package/dist/eslint/pasika/rules/css/css-variable-naming.js +39 -0
- package/dist/eslint/pasika/rules/css/custom-utility-apply.d.ts +11 -0
- package/dist/eslint/pasika/rules/css/custom-utility-apply.js +52 -0
- package/dist/eslint/pasika/rules/css/global-css-location.d.ts +10 -0
- package/dist/eslint/pasika/rules/css/global-css-location.js +42 -0
- package/dist/eslint/pasika/rules/css/helpers.d.ts +29 -0
- package/dist/eslint/pasika/rules/css/helpers.js +93 -0
- package/dist/eslint/pasika/rules/css/index.d.ts +13 -0
- package/dist/eslint/pasika/rules/css/index.js +25 -0
- package/dist/eslint/pasika/rules/css/root-variables.d.ts +11 -0
- package/dist/eslint/pasika/rules/css/root-variables.js +53 -0
- package/dist/eslint/pasika/rules/css/rule-tester.d.ts +11 -0
- package/dist/eslint/pasika/rules/css/rule-tester.js +22 -0
- package/dist/eslint/pasika/rules/css/stylesheet-ordering.d.ts +11 -0
- package/dist/eslint/pasika/rules/css/stylesheet-ordering.js +76 -0
- package/dist/eslint/pasika/rules/css/surface-utility.d.ts +10 -0
- package/dist/eslint/pasika/rules/css/surface-utility.js +41 -0
- package/dist/eslint/pasika/rules/css/theme-reset.d.ts +9 -0
- package/dist/eslint/pasika/rules/css/theme-reset.js +36 -0
- package/dist/eslint/pasika/rules/css/theme-variable-namespace.d.ts +11 -0
- package/dist/eslint/pasika/rules/css/theme-variable-namespace.js +53 -0
- package/dist/eslint/pasika/rules/cva-appearance-props.d.ts +10 -0
- package/dist/eslint/pasika/rules/cva-appearance-props.js +77 -0
- package/dist/eslint/pasika/rules/cva-boolean-variants.d.ts +11 -0
- package/dist/eslint/pasika/rules/cva-boolean-variants.js +97 -0
- package/dist/eslint/pasika/rules/data-testid-case.d.ts +2 -0
- package/dist/eslint/pasika/rules/data-testid-case.js +76 -0
- package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +13 -1
- package/dist/eslint/pasika/rules/enforce-cn-merge.js +46 -44
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +53 -48
- package/dist/eslint/pasika/rules/filename-case.js +28 -5
- package/dist/eslint/pasika/rules/hook-complexity.d.ts +12 -0
- package/dist/eslint/pasika/rules/hook-complexity.js +114 -0
- package/dist/eslint/pasika/rules/hook-extraction.d.ts +11 -0
- package/dist/eslint/pasika/rules/hook-extraction.js +55 -0
- package/dist/eslint/pasika/rules/import-through-index.d.ts +2 -0
- package/dist/eslint/pasika/rules/import-through-index.js +52 -0
- package/dist/eslint/pasika/rules/interactive-component.d.ts +10 -0
- package/dist/eslint/pasika/rules/interactive-component.js +76 -0
- package/dist/eslint/pasika/rules/json/index.d.ts +5 -0
- package/dist/eslint/pasika/rules/json/index.js +9 -0
- package/dist/eslint/pasika/rules/json/no-cache-flag.d.ts +10 -0
- package/dist/eslint/pasika/rules/json/no-cache-flag.js +45 -0
- package/dist/eslint/pasika/rules/json/no-vulyk-dependency.d.ts +10 -0
- package/dist/eslint/pasika/rules/json/no-vulyk-dependency.js +41 -0
- package/dist/eslint/pasika/rules/json/rule-tester.d.ts +11 -0
- package/dist/eslint/pasika/rules/json/rule-tester.js +21 -0
- package/dist/eslint/pasika/rules/jsx-hygiene.d.ts +9 -0
- package/dist/eslint/pasika/rules/jsx-hygiene.js +108 -0
- package/dist/eslint/pasika/rules/locale-dotted-path.d.ts +10 -0
- package/dist/eslint/pasika/rules/locale-dotted-path.js +65 -0
- package/dist/eslint/pasika/rules/locale-placement.d.ts +13 -0
- package/dist/eslint/pasika/rules/locale-placement.js +163 -0
- package/dist/eslint/pasika/rules/locales-location.d.ts +11 -0
- package/dist/eslint/pasika/rules/locales-location.js +56 -0
- package/dist/eslint/pasika/rules/md/doc-kind-suffix.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/doc-kind-suffix.js +29 -0
- package/dist/eslint/pasika/rules/md/example-heading-description.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/example-heading-description.js +30 -0
- package/dist/eslint/pasika/rules/md/glossary-term-linking.d.ts +2 -0
- package/dist/eslint/pasika/rules/md/glossary-term-linking.js +99 -0
- package/dist/eslint/pasika/rules/md/guide-folder-entry-point.d.ts +2 -0
- package/dist/eslint/pasika/rules/md/guide-folder-entry-point.js +46 -0
- package/dist/eslint/pasika/rules/md/guide-link-anchors.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/guide-link-anchors.js +50 -0
- package/dist/eslint/pasika/rules/md/guide-overview-no-links.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/guide-overview-no-links.js +51 -0
- package/dist/eslint/pasika/rules/md/guide-states-no-requirement.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/guide-states-no-requirement.js +26 -0
- package/dist/eslint/pasika/rules/md/guide-step-single-link.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/guide-step-single-link.js +48 -0
- package/dist/eslint/pasika/rules/md/guide-step-single-sentence.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/guide-step-single-sentence.js +42 -0
- package/dist/eslint/pasika/rules/md/helpers.d.ts +25 -0
- package/dist/eslint/pasika/rules/md/helpers.js +37 -0
- package/dist/eslint/pasika/rules/md/index.d.ts +27 -0
- package/dist/eslint/pasika/rules/md/index.js +53 -0
- package/dist/eslint/pasika/rules/md/no-cross-document-link.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/no-cross-document-link.js +36 -0
- package/dist/eslint/pasika/rules/md/no-nested-how-to.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/no-nested-how-to.js +39 -0
- package/dist/eslint/pasika/rules/md/no-template-prompt.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/no-template-prompt.js +22 -0
- package/dist/eslint/pasika/rules/md/overview-length.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/overview-length.js +47 -0
- package/dist/eslint/pasika/rules/md/overview-present.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/overview-present.js +41 -0
- package/dist/eslint/pasika/rules/md/policy-no-examples.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/policy-no-examples.js +28 -0
- package/dist/eslint/pasika/rules/md/policy-single-document.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/policy-single-document.js +34 -0
- package/dist/eslint/pasika/rules/md/policy-subject-headings.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/policy-subject-headings.js +58 -0
- package/dist/eslint/pasika/rules/md/project-index.d.ts +17 -0
- package/dist/eslint/pasika/rules/md/project-index.js +67 -0
- package/dist/eslint/pasika/rules/md/reference-block-headings.d.ts +6 -0
- package/dist/eslint/pasika/rules/md/reference-block-headings.js +31 -0
- package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.d.ts +6 -0
- package/dist/eslint/pasika/rules/md/reference-no-rfc-vocabulary.js +26 -0
- package/dist/eslint/pasika/rules/md/requirement-present.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/requirement-present.js +41 -0
- package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/rfc-only-in-bullets.js +46 -0
- package/dist/eslint/pasika/rules/md/rule-paired-examples.d.ts +5 -0
- package/dist/eslint/pasika/rules/md/rule-paired-examples.js +36 -0
- package/dist/eslint/pasika/rules/md/rule-tester.d.ts +11 -0
- package/dist/eslint/pasika/rules/md/rule-tester.js +21 -0
- package/dist/eslint/pasika/rules/md/support-document-placement.d.ts +2 -0
- package/dist/eslint/pasika/rules/md/support-document-placement.js +36 -0
- package/dist/eslint/pasika/rules/md/title-matches-file-name.d.ts +2 -0
- package/dist/eslint/pasika/rules/md/title-matches-file-name.js +46 -0
- package/dist/eslint/pasika/rules/named-exports.d.ts +2 -0
- package/dist/eslint/pasika/rules/named-exports.js +49 -0
- package/dist/eslint/pasika/rules/no-eslint-disable.d.ts +10 -0
- package/dist/eslint/pasika/rules/no-eslint-disable.js +33 -0
- package/dist/eslint/pasika/rules/no-mixed-concerns.js +9 -22
- package/dist/eslint/pasika/rules/no-util-barrel.d.ts +2 -0
- package/dist/eslint/pasika/rules/no-util-barrel.js +48 -0
- package/dist/eslint/pasika/rules/pure-function-extract.d.ts +9 -0
- package/dist/eslint/pasika/rules/pure-function-extract.js +92 -0
- package/dist/eslint/pasika/rules/shared-style-dedup.d.ts +13 -0
- package/dist/eslint/pasika/rules/shared-style-dedup.js +91 -0
- package/dist/eslint/pasika/rules/stay-flat.d.ts +12 -0
- package/dist/eslint/pasika/rules/stay-flat.js +81 -0
- package/dist/eslint/pasika/rules/support-folder-shape.d.ts +2 -0
- package/dist/eslint/pasika/rules/support-folder-shape.js +59 -0
- package/dist/eslint/pasika/rules/type-extraction.d.ts +13 -0
- package/dist/eslint/pasika/rules/type-extraction.js +85 -0
- package/dist/eslint/pasika/rules/ui-state.d.ts +9 -0
- package/dist/eslint/pasika/rules/ui-state.js +99 -0
- package/dist/eslint/pasika/rules/util-file-name.d.ts +2 -0
- package/dist/eslint/pasika/rules/util-file-name.js +48 -0
- package/dist/eslint/pasika/rules/value-extraction.d.ts +13 -0
- package/dist/eslint/pasika/rules/value-extraction.js +48 -0
- package/package.json +14 -7
- package/dist/enforcement/docs-check.d.ts +0 -17
- package/dist/enforcement/docs-check.js +0 -162
- package/enforcement/registry.json +0 -1139
package/README.md
CHANGED
|
@@ -16,9 +16,9 @@ docs/
|
|
|
16
16
|
enforcement/
|
|
17
17
|
registry.json # requirement → enforcement, keyed by content hash
|
|
18
18
|
coverage.ts # reconciles the docs against the registry
|
|
19
|
-
docs-check.ts # the documentation guide, mechanically
|
|
20
19
|
eslint/
|
|
21
20
|
pasika/rules/ # the lint rules, with fixture tests beside them
|
|
21
|
+
pasika/rules/md/ # the documentation-guide rules, linting docs/ itself
|
|
22
22
|
cli/
|
|
23
23
|
index.ts # the `pasika` command
|
|
24
24
|
```
|
|
@@ -42,13 +42,12 @@ Requirements are identified by a hash of their canonical text, not by a hand-wri
|
|
|
42
42
|
}
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
The `note` field is where a check's known gap is recorded, so a partial check never reads as a complete one.
|
|
45
|
+
The `text` field is the bullet as written in the document (markdown links and code spans intact), so it is greppable in the doc it came from; the `hash` is computed from the same text with links collapsed to their text and code spans unwrapped, so editing a URL or adding backticks does not read as a change. The `note` field is where a check's known gap is recorded, so a partial check never reads as a complete one.
|
|
46
46
|
|
|
47
47
|
| Kind | Meaning |
|
|
48
48
|
| --- | --- |
|
|
49
49
|
| `eslint` | An ESLint rule reports it, and a fixture test titled with the requirement pins it |
|
|
50
50
|
| `doctor` | A `pasika doctor` check reports it |
|
|
51
|
-
| `docs-check` | A `pasika docs` check reports it |
|
|
52
51
|
| `planned` | Mechanically checkable, not written yet; `note` names the intended check |
|
|
53
52
|
| `judgment` | No mechanical check can decide it; `note` says why |
|
|
54
53
|
| `permission` | The requirement grants permission, so there is nothing to check |
|
|
@@ -57,9 +56,9 @@ The `note` field is where a check's known gap is recorded, so a partial check ne
|
|
|
57
56
|
|
|
58
57
|
## Commands
|
|
59
58
|
|
|
59
|
+
The documentation guide itself is linted: the `pasika/*` markdown rules run over `docs/**/*.md` and report title, overview, structure, example-pairing, and RFC 2119 violations at the exact node.
|
|
60
|
+
|
|
60
61
|
```bash
|
|
61
|
-
npx pasika docs # check a docs/ folder against the documentation guide
|
|
62
|
-
npx pasika docs --dir content # check another folder
|
|
63
62
|
npx pasika coverage # check that every requirement has recorded enforcement
|
|
64
63
|
npx pasika coverage --accept # record reworded and removed requirements
|
|
65
64
|
```
|
|
@@ -93,6 +92,8 @@ const { eslintConfig } = styleguide({
|
|
|
93
92
|
export default eslintConfig;
|
|
94
93
|
```
|
|
95
94
|
|
|
95
|
+
`zirka` composes the full pasika ruleset over four file scopes: TS/TSX under `src/**`, `globals.css` and other stylesheets, `package.json`, and markdown — each with its own ESLint language.
|
|
96
|
+
|
|
96
97
|
### Without Zirka
|
|
97
98
|
|
|
98
99
|
```ts
|
|
@@ -102,7 +103,9 @@ import { pasikaConfig } from "pasika/eslint";
|
|
|
102
103
|
export default [pasikaConfig];
|
|
103
104
|
```
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
`pasikaConfig` applies the TS/TSX rules to `src/**` only, so a repository without a `src/` tree passes it trivially. The CSS, JSON, and markdown language configs are composed by `zirka`'s `styleguide()`; the individual rule objects (`cssRules`, `jsonRules`, `mdRules`) are exported for manual wiring.
|
|
107
|
+
|
|
108
|
+
### TS/TSX rules
|
|
106
109
|
|
|
107
110
|
| Rule | Enforces |
|
|
108
111
|
| --- | --- |
|
|
@@ -112,20 +115,76 @@ The ruleset applies to `src/**` only, so a repository without a `src/` tree pass
|
|
|
112
115
|
| `pasika/no-arbitrary-tailwind` | No arbitrary `-[value]` classes, including inside `cn()` conditionals |
|
|
113
116
|
| `pasika/enforce-cn-merge` | `cn()` instead of `+` or template literals; at most five classes per group |
|
|
114
117
|
| `pasika/enforce-cva-variant-props` | `VariantProps<typeof …>` instead of hand-written unions |
|
|
118
|
+
| `pasika/cva-appearance-props` | Visual option props (`size`, `variant`, …) declared through CVA appearance props |
|
|
119
|
+
| `pasika/cva-boolean-variants` | Boolean appearance props placed on the CVA variant, not as standalone props |
|
|
115
120
|
| `pasika/enforce-barrel-exports` | A nested `index.ts` re-exports only its component |
|
|
116
121
|
| `pasika/component-placement` † | The folder a component's consumers imply |
|
|
117
122
|
| `pasika/support-file-placement` † | The folder a hook, type, schema, constant, or utility belongs in |
|
|
123
|
+
| `pasika/application-structure` | The path-based parts of the application structure and configuration rules |
|
|
124
|
+
| `pasika/named-exports` | Named exports for application files (framework routing files may default-export) |
|
|
125
|
+
| `pasika/data-testid-case` | `data-testid` casing where a rendered result carries one |
|
|
126
|
+
| `pasika/support-folder-shape` | A `constants/`, `types/`, or `schemas/` folder defines exports directly in `index.ts` or in named-re-exported files |
|
|
127
|
+
| `pasika/import-through-index` | An extracted constant, type, or schema is imported through its folder's `index.ts` |
|
|
128
|
+
| `pasika/util-file-name` | A single-function utility file is named in the function's kebab-case form |
|
|
129
|
+
| `pasika/no-util-barrel` | A utility is imported directly, without a barrel |
|
|
130
|
+
| `pasika/jsx-hygiene` | Calculations and complex conditions stay out of JSX children and attributes |
|
|
131
|
+
| `pasika/interactive-component` | Interactive elements are component boundaries when mixed with other content |
|
|
132
|
+
| `pasika/ui-state` | Native or ARIA state expression and Tailwind state variants |
|
|
133
|
+
| `pasika/cross-feature-import` | A component importing from two or more feature folders lives in a shared location |
|
|
134
|
+
| `pasika/pure-function-extract` | Pure functions extracted to `utils/`, even with one consumer |
|
|
135
|
+
| `pasika/hook-complexity` | Hook complexity limits (imperative categories per hook) |
|
|
136
|
+
| `pasika/locale-dotted-path` | A namespaced locale is read through its full dotted path |
|
|
137
|
+
| `pasika/locales-location` | Locales live in the named locales object |
|
|
138
|
+
| `pasika/hook-extraction` † | A hook with two or more consumers is extracted to its own file |
|
|
139
|
+
| `pasika/value-extraction` † | A value with cross-folder consumers is extracted |
|
|
140
|
+
| `pasika/config-extraction` † | A type, schema, or utility used outside its config module is moved |
|
|
141
|
+
| `pasika/component-nesting` † | A component is not nested solely because it has support files |
|
|
142
|
+
| `pasika/stay-flat` † | A component stays flat until it has exclusive children |
|
|
143
|
+
| `pasika/type-extraction` † | A type or schema with cross-folder consumers is extracted |
|
|
144
|
+
| `pasika/locale-placement` † | Shared locales at the top level, single-feature locales namespaced |
|
|
145
|
+
| `pasika/shared-style-dedup` † | A className combo used by two or more components becomes a named utility |
|
|
146
|
+
| `pasika/no-eslint-disable` | No `eslint-disable` directives |
|
|
147
|
+
|
|
148
|
+
### CSS rules
|
|
149
|
+
|
|
150
|
+
Applied to `src/**/globals.css` (and other stylesheets) through `@eslint/css` with tolerant Tailwind v4 parsing.
|
|
151
|
+
|
|
152
|
+
| Rule | Enforces |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| `pasika/theme-reset` | A `--*: initial` theme reset is present |
|
|
155
|
+
| `pasika/root-variables` | `:root` defines the CSS custom properties |
|
|
156
|
+
| `pasika/apply-usage` | `@layer base` uses `@apply` for declarations |
|
|
157
|
+
| `pasika/base-layer-pair` | The base layer applies `base-canvas` and `base-ink` |
|
|
158
|
+
| `pasika/stylesheet-ordering` | Imports → `@custom-variant` → `:root` → `@theme` → `@utility` → `@layer base` |
|
|
159
|
+
| `pasika/css-variable-naming` | Background vars named `--<role>-canvas`, text vars `--<role>-ink` |
|
|
160
|
+
| `pasika/custom-utility-apply` | `@utility` blocks use `@apply` |
|
|
161
|
+
| `pasika/surface-utility` | Repeated canvas+ink combos become a named surface utility |
|
|
162
|
+
| `pasika/theme-variable-namespace` | Utility class groups share a namespace prefix |
|
|
163
|
+
| `pasika/global-css-location` | Global CSS lives in the correct entry point |
|
|
164
|
+
|
|
165
|
+
### JSON rules
|
|
166
|
+
|
|
167
|
+
Applied to `package.json` through `@eslint/json`.
|
|
168
|
+
|
|
169
|
+
| Rule | Enforces |
|
|
170
|
+
| --- | --- |
|
|
171
|
+
| `pasika/no-cache-flag` | Lint scripts don't pass `--cache` |
|
|
172
|
+
| `pasika/no-vulyk-dependency` | `vulyk` is not in `dependencies` |
|
|
173
|
+
|
|
174
|
+
### Documentation rules
|
|
175
|
+
|
|
176
|
+
The `pasika/*` markdown rules enforce the documentation guide over `docs/**/*.md` (24 rules): file-name suffixes and titles, overview presence and length, guide step structure, Incorrect/Correct pairing, policy document shape, reference block headings, RFC 2119 placement, template hygiene, link anchoring, and glossary-term linking. They run through `@eslint/markdown`; `pasika coverage` verifies each has a test and a registry entry. Pasika's own `docs/` are linted by them in CI (`npm run docs`).
|
|
118
177
|
|
|
119
178
|
Run `pasika coverage --json` for the exact requirement each rule covers.
|
|
120
179
|
|
|
121
180
|
### † Cross-file rules
|
|
122
181
|
|
|
123
|
-
Where a component or
|
|
182
|
+
Where a component, hook, value, type, or style belongs depends on which files use it, so the rules marked † index the whole `src/` tree instead of looking at one file. Two consequences:
|
|
124
183
|
|
|
125
184
|
- **Do not pass `--cache`.** Move a file and the finding belongs to a *different* file, whose cache entry is unchanged — so ESLint would replay a stale verdict. `agent-policy.md` requires lint commands to run without it.
|
|
126
185
|
- The index is read from disk rather than from ESLint's file list, so a partial run such as `lint-staged` still judges against the true graph.
|
|
127
186
|
|
|
128
|
-
|
|
187
|
+
All are inert in a repository with no `src/` tree.
|
|
129
188
|
|
|
130
189
|
## Development
|
|
131
190
|
|
|
@@ -133,7 +192,6 @@ Both are inert in a repository with no `src/` tree.
|
|
|
133
192
|
npm run lint
|
|
134
193
|
npm run typecheck
|
|
135
194
|
npm run test
|
|
136
|
-
npm run docs
|
|
137
195
|
npm run coverage
|
|
138
196
|
npm run build
|
|
139
197
|
```
|
package/dist/cli/index.js
CHANGED
|
@@ -4,7 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { buildCoverageReport, classifyRequirement, readRegistry, writeRegistry, } from "../enforcement/coverage.js";
|
|
6
6
|
import { enforcementKindSchema } from "../enforcement/types.js";
|
|
7
|
-
import {
|
|
7
|
+
import { runDoctor } from "../enforcement/doctor.js";
|
|
8
|
+
import { log, error, json } from "./output.js";
|
|
8
9
|
const REGISTRY_RELATIVE_PATH = path.join("enforcement", "registry.json");
|
|
9
10
|
/** Walks up from `startDir` to the directory that holds the enforcement registry. */
|
|
10
11
|
function findRegistryRoot(startDir) {
|
|
@@ -30,30 +31,6 @@ function truncate(text, width) {
|
|
|
30
31
|
}
|
|
31
32
|
const program = new Command();
|
|
32
33
|
program.name("pasika").description("Applies and diagnoses the pasika framework.");
|
|
33
|
-
program
|
|
34
|
-
.command("docs")
|
|
35
|
-
.description("Check documentation against the documentation guide.")
|
|
36
|
-
.option("--dir <path>", "documentation folder to check", "docs")
|
|
37
|
-
.option("--json", "print findings as JSON")
|
|
38
|
-
.action((options) => {
|
|
39
|
-
const docsRoot = path.resolve(options.dir);
|
|
40
|
-
if (!existsSync(docsRoot)) {
|
|
41
|
-
console.error(`No documentation folder at ${docsRoot}`);
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
const { docs, findings } = checkDocs(docsRoot);
|
|
45
|
-
if (options.json) {
|
|
46
|
-
console.log(JSON.stringify({ documents: docs.length, findings }, undefined, 2));
|
|
47
|
-
process.exit(findings.length > 0 ? 1 : 0);
|
|
48
|
-
}
|
|
49
|
-
for (const finding of findings) {
|
|
50
|
-
console.log(` ✗ ${finding.doc}:${String(finding.line)} ${finding.check} ${finding.message}`);
|
|
51
|
-
}
|
|
52
|
-
console.log(findings.length === 0
|
|
53
|
-
? `\n✓ ${String(docs.length)} documents pass`
|
|
54
|
-
: `\n${String(docs.length)} documents checked · ${String(findings.length)} findings`);
|
|
55
|
-
process.exit(findings.length > 0 ? 1 : 0);
|
|
56
|
-
});
|
|
57
34
|
program
|
|
58
35
|
.command("coverage")
|
|
59
36
|
.description("Check that every documented requirement has recorded enforcement.")
|
|
@@ -66,19 +43,19 @@ program
|
|
|
66
43
|
.action((options) => {
|
|
67
44
|
const root = findRegistryRoot(process.cwd());
|
|
68
45
|
if (!root) {
|
|
69
|
-
|
|
46
|
+
error(`No ${REGISTRY_RELATIVE_PATH} found in this directory or any parent.`);
|
|
70
47
|
process.exit(1);
|
|
71
48
|
}
|
|
72
49
|
const docsRoot = path.join(root, "docs");
|
|
73
50
|
if (!existsSync(docsRoot)) {
|
|
74
|
-
|
|
51
|
+
error(`No documentation folder at ${docsRoot}. Run coverage inside the pasika repository.`);
|
|
75
52
|
process.exit(1);
|
|
76
53
|
}
|
|
77
54
|
const registryPath = path.join(root, REGISTRY_RELATIVE_PATH);
|
|
78
55
|
if (options.classify !== undefined) {
|
|
79
56
|
const kind = enforcementKindSchema.safeParse(options.kind);
|
|
80
57
|
if (!kind.success) {
|
|
81
|
-
|
|
58
|
+
error(`✗ --kind must be one of ${enforcementKindSchema.options.join(", ")}.`);
|
|
82
59
|
process.exit(1);
|
|
83
60
|
}
|
|
84
61
|
try {
|
|
@@ -87,14 +64,14 @@ program
|
|
|
87
64
|
registry: readRegistry(registryPath),
|
|
88
65
|
input: { hash: options.classify, kind: kind.data, ref: options.ref, note: options.note },
|
|
89
66
|
});
|
|
90
|
-
writeRegistry(registryPath, result.registry);
|
|
67
|
+
writeRegistry(registryPath, result.registry, docsRoot);
|
|
91
68
|
const change = result.previousKind === undefined
|
|
92
69
|
? `recorded as ${result.requirement.kind}`
|
|
93
70
|
: `reclassified from ${result.previousKind} to ${result.requirement.kind}`;
|
|
94
|
-
|
|
71
|
+
log(`✓ ${change}: ${result.requirement.text}`);
|
|
95
72
|
}
|
|
96
|
-
catch (
|
|
97
|
-
|
|
73
|
+
catch (err) {
|
|
74
|
+
error(`✗ ${err instanceof Error ? err.message : String(err)}`);
|
|
98
75
|
process.exit(1);
|
|
99
76
|
}
|
|
100
77
|
}
|
|
@@ -104,29 +81,46 @@ program
|
|
|
104
81
|
rulesDir: path.join(root, "eslint", "pasika", "rules"),
|
|
105
82
|
});
|
|
106
83
|
if (options.json) {
|
|
107
|
-
|
|
84
|
+
json(report);
|
|
108
85
|
process.exit(report.issues.length > 0 ? 1 : 0);
|
|
109
86
|
}
|
|
110
87
|
for (const issue of report.issues) {
|
|
111
88
|
const where = issue.line === undefined ? issue.doc : `${issue.doc}:${String(issue.line)}`;
|
|
112
|
-
|
|
113
|
-
|
|
89
|
+
log(` ✗ ${ISSUE_LABELS[issue.kind]} ${truncate(issue.text, 76)}`);
|
|
90
|
+
log(` ${where}${issue.detail ? `\n ${issue.detail}` : ""}`);
|
|
114
91
|
}
|
|
115
92
|
const { counts } = report;
|
|
116
|
-
|
|
93
|
+
log([
|
|
117
94
|
"",
|
|
118
95
|
`${String(report.total)} requirements · ${String(report.mechanical)} mechanically enforced`,
|
|
119
|
-
` eslint ${String(counts.eslint)} · doctor ${String(counts.doctor)}
|
|
96
|
+
` eslint ${String(counts.eslint)} · doctor ${String(counts.doctor)}`,
|
|
120
97
|
` planned ${String(counts.planned)} · judgment ${String(counts.judgment)} · permission ${String(counts.permission)}`,
|
|
121
98
|
` unclassified ${String(report.issues.filter((issue) => issue.kind === "new").length)}`,
|
|
122
99
|
].join("\n"));
|
|
123
100
|
if (options.accept) {
|
|
124
|
-
writeRegistry(registryPath, report.nextRegistry);
|
|
101
|
+
writeRegistry(registryPath, report.nextRegistry, docsRoot);
|
|
125
102
|
const accepted = report.issues.filter((issue) => issue.kind === "changed" || issue.kind === "removed");
|
|
126
|
-
|
|
127
|
-
|
|
103
|
+
log(`\nAccepted ${String(accepted.length)} change(s) into ${REGISTRY_RELATIVE_PATH}.`);
|
|
104
|
+
log("Requirements reported as new still need a classification.");
|
|
128
105
|
process.exit(report.issues.some((issue) => issue.kind === "new") ? 1 : 0);
|
|
129
106
|
}
|
|
130
107
|
process.exit(report.issues.length > 0 ? 1 : 0);
|
|
131
108
|
});
|
|
109
|
+
program
|
|
110
|
+
.command("doctor")
|
|
111
|
+
.description("Diagnose gaps between the repository and the pasika framework baseline.")
|
|
112
|
+
.option("--json", "print findings as JSON")
|
|
113
|
+
.action((options) => {
|
|
114
|
+
const findings = runDoctor(process.cwd());
|
|
115
|
+
if (options.json) {
|
|
116
|
+
json({ findings });
|
|
117
|
+
process.exit(findings.some((f) => f.severity === "error") ? 1 : 0);
|
|
118
|
+
}
|
|
119
|
+
for (const finding of findings) {
|
|
120
|
+
const icon = finding.severity === "error" ? "✗" : "⚠";
|
|
121
|
+
log(` ${icon} ${finding.check} ${finding.message}`);
|
|
122
|
+
}
|
|
123
|
+
log(findings.length === 0 ? "\n✓ No gaps found" : `\n${String(findings.length)} finding(s)`);
|
|
124
|
+
process.exit(findings.some((f) => f.severity === "error") ? 1 : 0);
|
|
125
|
+
});
|
|
132
126
|
program.parse();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* eslint-disable no-console -- cli-reports-to-terminal: stdout is the CLI's output channel */
|
|
2
|
+
export function log(message) {
|
|
3
|
+
console.log(message);
|
|
4
|
+
}
|
|
5
|
+
export function error(message) {
|
|
6
|
+
console.error(message);
|
|
7
|
+
}
|
|
8
|
+
export function json(data) {
|
|
9
|
+
console.log(JSON.stringify(data, undefined, 2));
|
|
10
|
+
}
|
|
11
|
+
/* eslint-enable no-console -- re-enable after CLI output block */
|
|
@@ -48,4 +48,8 @@ export declare function classifyRequirement(options: {
|
|
|
48
48
|
input: ClassifyInput;
|
|
49
49
|
}): ClassifyResult;
|
|
50
50
|
export declare function readRegistry(registryPath: string): Registry;
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Writes the registry sorted in the same order the requirements appear in the
|
|
53
|
+
* docs: document order, then line order within each document.
|
|
54
|
+
*/
|
|
55
|
+
export declare function writeRegistry(registryPath: string, registry: Registry, docsRoot: string): void;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { readdirSync, readFileSync, writeFileSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { allPasikaRuleIds } from "../eslint/pasika/index.js";
|
|
5
|
+
import { normalizeRequirement } from "./normalize.js";
|
|
6
6
|
import { parseDocs } from "./parse-docs.js";
|
|
7
7
|
import { MECHANICAL_KINDS, registrySchema } from "./types.js";
|
|
8
8
|
/** Ratio of shared words, used only to guess which stored requirement a reworded bullet came from. */
|
|
@@ -13,17 +13,27 @@ function similarity(left, right) {
|
|
|
13
13
|
const union = new Set([...leftWords, ...rightWords]).size;
|
|
14
14
|
return union === 0 ? 0 : shared / union;
|
|
15
15
|
}
|
|
16
|
-
/** Titles passed to `describe` or `test` in the rule test files. */
|
|
16
|
+
/** Titles passed to `describe` or `test` in the rule test files, recursing into subfolders. */
|
|
17
17
|
function collectTestTitles(rulesDir) {
|
|
18
18
|
const titles = new Set();
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
const visit = (dir) => {
|
|
20
|
+
for (const entry of readdirSync(dir)) {
|
|
21
|
+
const entryPath = path.join(dir, entry);
|
|
22
|
+
if (statSync(entryPath).isDirectory()) {
|
|
23
|
+
visit(entryPath);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!entry.endsWith(".test.ts"))
|
|
27
|
+
continue;
|
|
28
|
+
const body = readFileSync(entryPath, "utf8");
|
|
29
|
+
const titlePattern = /\b(?:describe|test|it)\(\s*(?:"(?<double>(?:[^"\\]|\\.)*)"|'(?<single>(?:[^'\\]|\\.)*)')/g;
|
|
30
|
+
for (const match of body.matchAll(titlePattern)) {
|
|
31
|
+
const title = match.groups?.double ?? match.groups?.single ?? "";
|
|
32
|
+
titles.add(title.replaceAll('\\\\"', '"').replaceAll("\\\\'", "'"));
|
|
33
|
+
}
|
|
25
34
|
}
|
|
26
|
-
}
|
|
35
|
+
};
|
|
36
|
+
visit(rulesDir);
|
|
27
37
|
return titles;
|
|
28
38
|
}
|
|
29
39
|
/** A requirement several checks cover lists them comma-separated. */
|
|
@@ -33,19 +43,16 @@ function refParts(ref) {
|
|
|
33
43
|
.map((part) => part.trim())
|
|
34
44
|
.filter(Boolean);
|
|
35
45
|
}
|
|
36
|
-
function isRefKnown(requirement
|
|
46
|
+
function isRefKnown(requirement) {
|
|
37
47
|
const parts = refParts(requirement.ref);
|
|
38
48
|
if (requirement.kind === "eslint")
|
|
39
|
-
return parts.length > 0 && parts.every((part) =>
|
|
40
|
-
if (requirement.kind === "docs-check")
|
|
41
|
-
return parts.length > 0 && parts.every((part) => docsChecks.has(part));
|
|
49
|
+
return parts.length > 0 && parts.every((part) => allPasikaRuleIds.includes(part));
|
|
42
50
|
// Doctor checks do not exist yet, so a `doctor` entry is a forward reference.
|
|
43
51
|
return true;
|
|
44
52
|
}
|
|
45
53
|
export function buildCoverageReport(options) {
|
|
46
54
|
const { docsRoot, registry, rulesDir } = options;
|
|
47
55
|
const docs = parseDocs(docsRoot);
|
|
48
|
-
const docsChecks = new Set(DOCS_CHECKS);
|
|
49
56
|
const testTitles = collectTestTitles(rulesDir);
|
|
50
57
|
const byHash = new Map(registry.requirements.map((requirement) => [requirement.hash, requirement]));
|
|
51
58
|
const matched = new Set();
|
|
@@ -53,10 +60,9 @@ export function buildCoverageReport(options) {
|
|
|
53
60
|
const counts = {
|
|
54
61
|
eslint: 0,
|
|
55
62
|
doctor: 0,
|
|
56
|
-
|
|
63
|
+
planned: 0,
|
|
57
64
|
judgment: 0,
|
|
58
65
|
permission: 0,
|
|
59
|
-
planned: 0,
|
|
60
66
|
};
|
|
61
67
|
const nextRequirements = [];
|
|
62
68
|
const parsed = docs.flatMap((doc) => doc.requirements.map((requirement) => ({ doc: doc.doc, requirement })));
|
|
@@ -65,13 +71,13 @@ export function buildCoverageReport(options) {
|
|
|
65
71
|
if (recorded) {
|
|
66
72
|
matched.add(requirement.hash);
|
|
67
73
|
counts[recorded.kind] += 1;
|
|
68
|
-
nextRequirements.push({ ...recorded, doc });
|
|
69
|
-
if (!isRefKnown(recorded
|
|
74
|
+
nextRequirements.push({ ...recorded, doc, text: requirement.raw });
|
|
75
|
+
if (!isRefKnown(recorded)) {
|
|
70
76
|
issues.push({
|
|
71
77
|
kind: "unknown-ref",
|
|
72
78
|
doc,
|
|
73
79
|
line: requirement.line,
|
|
74
|
-
text: requirement.
|
|
80
|
+
text: requirement.raw,
|
|
75
81
|
detail: `${recorded.kind} ref "${recorded.ref ?? "(none)"}" does not exist`,
|
|
76
82
|
});
|
|
77
83
|
}
|
|
@@ -80,7 +86,7 @@ export function buildCoverageReport(options) {
|
|
|
80
86
|
kind: "missing-test",
|
|
81
87
|
doc,
|
|
82
88
|
line: requirement.line,
|
|
83
|
-
text: requirement.
|
|
89
|
+
text: requirement.raw,
|
|
84
90
|
detail: `no rule test is titled with this requirement`,
|
|
85
91
|
});
|
|
86
92
|
}
|
|
@@ -89,24 +95,24 @@ export function buildCoverageReport(options) {
|
|
|
89
95
|
// Not recorded under this hash: either a reworded requirement or a new one.
|
|
90
96
|
const candidate = registry.requirements
|
|
91
97
|
.filter((entry) => entry.doc === doc && !matched.has(entry.hash))
|
|
92
|
-
.map((entry) => ({ entry, score: similarity(entry.text, requirement.text) }))
|
|
98
|
+
.map((entry) => ({ entry, score: similarity(normalizeRequirement(entry.text), requirement.text) }))
|
|
93
99
|
.sort((left, right) => right.score - left.score)
|
|
94
100
|
.find(({ score }) => score >= 0.5);
|
|
95
101
|
if (candidate) {
|
|
96
102
|
matched.add(candidate.entry.hash);
|
|
97
103
|
counts[candidate.entry.kind] += 1;
|
|
98
|
-
nextRequirements.push({ ...candidate.entry, doc, text: requirement.
|
|
104
|
+
nextRequirements.push({ ...candidate.entry, doc, text: requirement.raw, hash: requirement.hash });
|
|
99
105
|
issues.push({
|
|
100
106
|
kind: "changed",
|
|
101
107
|
doc,
|
|
102
108
|
line: requirement.line,
|
|
103
|
-
text: requirement.
|
|
109
|
+
text: requirement.raw,
|
|
104
110
|
hash: requirement.hash,
|
|
105
111
|
detail: `was "${candidate.entry.text}" — re-verify ${candidate.entry.kind}${candidate.entry.ref ? ` ${candidate.entry.ref}` : ""}`,
|
|
106
112
|
});
|
|
107
113
|
}
|
|
108
114
|
else {
|
|
109
|
-
issues.push({ kind: "new", doc, line: requirement.line, text: requirement.
|
|
115
|
+
issues.push({ kind: "new", doc, line: requirement.line, text: requirement.raw, hash: requirement.hash });
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
for (const entry of registry.requirements) {
|
|
@@ -150,20 +156,11 @@ export function classifyRequirement(options) {
|
|
|
150
156
|
if (input.kind === "eslint") {
|
|
151
157
|
if (refs.length === 0)
|
|
152
158
|
throw new Error('Kind "eslint" needs --ref naming the rule that reports it.');
|
|
153
|
-
const unknown = refs.filter((ref) => !
|
|
159
|
+
const unknown = refs.filter((ref) => !allPasikaRuleIds.includes(ref));
|
|
154
160
|
if (unknown.length > 0) {
|
|
155
161
|
throw new Error(`--ref ${unknown.map((ref) => `"${ref}"`).join(", ")} is not a rule in the plugin.`);
|
|
156
162
|
}
|
|
157
163
|
}
|
|
158
|
-
else if (input.kind === "docs-check") {
|
|
159
|
-
if (refs.length === 0)
|
|
160
|
-
throw new Error('Kind "docs-check" needs --ref naming the check that reports it.');
|
|
161
|
-
const known = new Set(DOCS_CHECKS);
|
|
162
|
-
const unknown = refs.filter((ref) => !known.has(ref));
|
|
163
|
-
if (unknown.length > 0) {
|
|
164
|
-
throw new Error(`--ref ${unknown.map((ref) => `"${ref}"`).join(", ")} is not a documentation check.`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
164
|
else if (input.kind !== "doctor" && refs.length > 0) {
|
|
168
165
|
throw new Error(`Kind "${input.kind}" takes no --ref, because nothing reports it.`);
|
|
169
166
|
}
|
|
@@ -173,7 +170,7 @@ export function classifyRequirement(options) {
|
|
|
173
170
|
}
|
|
174
171
|
const requirement = {
|
|
175
172
|
doc: match.doc,
|
|
176
|
-
text: match.requirement.
|
|
173
|
+
text: match.requirement.raw,
|
|
177
174
|
hash: match.requirement.hash,
|
|
178
175
|
kind: input.kind,
|
|
179
176
|
...(refs.length > 0 ? { ref: refs.join(", ") } : {}),
|
|
@@ -192,6 +189,22 @@ export function readRegistry(registryPath) {
|
|
|
192
189
|
}
|
|
193
190
|
return result.data;
|
|
194
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Sequence position of each requirement in the docs: doc order as `parseDocs`
|
|
194
|
+
* yields it, then line order within each doc. Entries whose hash no longer
|
|
195
|
+
* exists in the docs (removed, not yet accepted) sort after everything parsed.
|
|
196
|
+
*/
|
|
197
|
+
function buildDocOrder(docsRoot) {
|
|
198
|
+
const order = new Map();
|
|
199
|
+
let position = 0;
|
|
200
|
+
for (const doc of parseDocs(docsRoot)) {
|
|
201
|
+
for (const requirement of doc.requirements) {
|
|
202
|
+
order.set(requirement.hash, position);
|
|
203
|
+
position += 1;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return order;
|
|
207
|
+
}
|
|
195
208
|
/**
|
|
196
209
|
* Code-point order on doc then text. Deliberately not `localeCompare`, whose
|
|
197
210
|
* result depends on the host's locale data — a generated file that is committed
|
|
@@ -204,7 +217,23 @@ function compareRequirements(left, right) {
|
|
|
204
217
|
return left.text < right.text ? -1 : 1;
|
|
205
218
|
return 0;
|
|
206
219
|
}
|
|
207
|
-
|
|
208
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Writes the registry sorted in the same order the requirements appear in the
|
|
222
|
+
* docs: document order, then line order within each document.
|
|
223
|
+
*/
|
|
224
|
+
export function writeRegistry(registryPath, registry, docsRoot) {
|
|
225
|
+
const order = buildDocOrder(docsRoot);
|
|
226
|
+
const withPosition = registry.requirements.map((requirement) => ({
|
|
227
|
+
requirement,
|
|
228
|
+
position: order.get(requirement.hash) ?? Number.MAX_SAFE_INTEGER,
|
|
229
|
+
}));
|
|
230
|
+
withPosition.sort((left, right) => {
|
|
231
|
+
if (left.position !== right.position)
|
|
232
|
+
return left.position - right.position;
|
|
233
|
+
// Two entries with the same hash cannot coexist; this fallback orders
|
|
234
|
+
// entries whose hash the docs no longer contain, deterministically.
|
|
235
|
+
return compareRequirements(left.requirement, right.requirement);
|
|
236
|
+
});
|
|
237
|
+
const sorted = { requirements: withPosition.map((entry) => entry.requirement) };
|
|
209
238
|
writeFileSync(registryPath, `${JSON.stringify(sorted, null, 2)}\n`);
|
|
210
239
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pasika doctor` checks.
|
|
3
|
+
*
|
|
4
|
+
* Diagnoses gaps between a consumer repository and the pasika framework
|
|
5
|
+
* baseline. Scope: dependencies, configuration, managed files, and
|
|
6
|
+
* source structure. Code-level and path-level rules belong to ESLint.
|
|
7
|
+
*/
|
|
8
|
+
export interface DoctorFinding {
|
|
9
|
+
/** Which requirement this relates to. */
|
|
10
|
+
check: string;
|
|
11
|
+
/** Human-readable description. */
|
|
12
|
+
message: string;
|
|
13
|
+
/** Severity: error blocks adoption, warning is informational. */
|
|
14
|
+
severity: "error" | "warning";
|
|
15
|
+
}
|
|
16
|
+
export declare function runDoctor(cwd: string): DoctorFinding[];
|