pasika 0.3.2 → 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 +61 -5
- package/dist/cli/index.js +2 -2
- package/dist/enforcement/coverage.d.ts +5 -1
- package/dist/enforcement/coverage.js +43 -10
- package/dist/enforcement/parse-docs.js +5 -1
- package/package.json +4 -5
- package/enforcement/registry.json +0 -1145
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ 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
|
| --- | --- |
|
|
@@ -92,6 +92,8 @@ const { eslintConfig } = styleguide({
|
|
|
92
92
|
export default eslintConfig;
|
|
93
93
|
```
|
|
94
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
|
+
|
|
95
97
|
### Without Zirka
|
|
96
98
|
|
|
97
99
|
```ts
|
|
@@ -101,7 +103,9 @@ import { pasikaConfig } from "pasika/eslint";
|
|
|
101
103
|
export default [pasikaConfig];
|
|
102
104
|
```
|
|
103
105
|
|
|
104
|
-
|
|
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
|
|
105
109
|
|
|
106
110
|
| Rule | Enforces |
|
|
107
111
|
| --- | --- |
|
|
@@ -111,24 +115,76 @@ The ruleset applies to `src/**` only, so a repository without a `src/` tree pass
|
|
|
111
115
|
| `pasika/no-arbitrary-tailwind` | No arbitrary `-[value]` classes, including inside `cn()` conditionals |
|
|
112
116
|
| `pasika/enforce-cn-merge` | `cn()` instead of `+` or template literals; at most five classes per group |
|
|
113
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 |
|
|
114
120
|
| `pasika/enforce-barrel-exports` | A nested `index.ts` re-exports only its component |
|
|
115
121
|
| `pasika/component-placement` † | The folder a component's consumers imply |
|
|
116
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` |
|
|
117
173
|
|
|
118
174
|
### Documentation rules
|
|
119
175
|
|
|
120
|
-
The `pasika/*` markdown rules enforce the documentation guide over `docs/**/*.md
|
|
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`).
|
|
121
177
|
|
|
122
178
|
Run `pasika coverage --json` for the exact requirement each rule covers.
|
|
123
179
|
|
|
124
180
|
### † Cross-file rules
|
|
125
181
|
|
|
126
|
-
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:
|
|
127
183
|
|
|
128
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.
|
|
129
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.
|
|
130
186
|
|
|
131
|
-
|
|
187
|
+
All are inert in a repository with no `src/` tree.
|
|
132
188
|
|
|
133
189
|
## Development
|
|
134
190
|
|
package/dist/cli/index.js
CHANGED
|
@@ -64,7 +64,7 @@ program
|
|
|
64
64
|
registry: readRegistry(registryPath),
|
|
65
65
|
input: { hash: options.classify, kind: kind.data, ref: options.ref, note: options.note },
|
|
66
66
|
});
|
|
67
|
-
writeRegistry(registryPath, result.registry);
|
|
67
|
+
writeRegistry(registryPath, result.registry, docsRoot);
|
|
68
68
|
const change = result.previousKind === undefined
|
|
69
69
|
? `recorded as ${result.requirement.kind}`
|
|
70
70
|
: `reclassified from ${result.previousKind} to ${result.requirement.kind}`;
|
|
@@ -98,7 +98,7 @@ program
|
|
|
98
98
|
` unclassified ${String(report.issues.filter((issue) => issue.kind === "new").length)}`,
|
|
99
99
|
].join("\n"));
|
|
100
100
|
if (options.accept) {
|
|
101
|
-
writeRegistry(registryPath, report.nextRegistry);
|
|
101
|
+
writeRegistry(registryPath, report.nextRegistry, docsRoot);
|
|
102
102
|
const accepted = report.issues.filter((issue) => issue.kind === "changed" || issue.kind === "removed");
|
|
103
103
|
log(`\nAccepted ${String(accepted.length)} change(s) into ${REGISTRY_RELATIVE_PATH}.`);
|
|
104
104
|
log("Requirements reported as new still need a classification.");
|
|
@@ -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;
|
|
@@ -2,6 +2,7 @@ import { readdirSync, readFileSync, writeFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { allPasikaRuleIds } from "../eslint/pasika/index.js";
|
|
5
|
+
import { normalizeRequirement } from "./normalize.js";
|
|
5
6
|
import { parseDocs } from "./parse-docs.js";
|
|
6
7
|
import { MECHANICAL_KINDS, registrySchema } from "./types.js";
|
|
7
8
|
/** Ratio of shared words, used only to guess which stored requirement a reworded bullet came from. */
|
|
@@ -70,13 +71,13 @@ export function buildCoverageReport(options) {
|
|
|
70
71
|
if (recorded) {
|
|
71
72
|
matched.add(requirement.hash);
|
|
72
73
|
counts[recorded.kind] += 1;
|
|
73
|
-
nextRequirements.push({ ...recorded, doc });
|
|
74
|
+
nextRequirements.push({ ...recorded, doc, text: requirement.raw });
|
|
74
75
|
if (!isRefKnown(recorded)) {
|
|
75
76
|
issues.push({
|
|
76
77
|
kind: "unknown-ref",
|
|
77
78
|
doc,
|
|
78
79
|
line: requirement.line,
|
|
79
|
-
text: requirement.
|
|
80
|
+
text: requirement.raw,
|
|
80
81
|
detail: `${recorded.kind} ref "${recorded.ref ?? "(none)"}" does not exist`,
|
|
81
82
|
});
|
|
82
83
|
}
|
|
@@ -85,7 +86,7 @@ export function buildCoverageReport(options) {
|
|
|
85
86
|
kind: "missing-test",
|
|
86
87
|
doc,
|
|
87
88
|
line: requirement.line,
|
|
88
|
-
text: requirement.
|
|
89
|
+
text: requirement.raw,
|
|
89
90
|
detail: `no rule test is titled with this requirement`,
|
|
90
91
|
});
|
|
91
92
|
}
|
|
@@ -94,24 +95,24 @@ export function buildCoverageReport(options) {
|
|
|
94
95
|
// Not recorded under this hash: either a reworded requirement or a new one.
|
|
95
96
|
const candidate = registry.requirements
|
|
96
97
|
.filter((entry) => entry.doc === doc && !matched.has(entry.hash))
|
|
97
|
-
.map((entry) => ({ entry, score: similarity(entry.text, requirement.text) }))
|
|
98
|
+
.map((entry) => ({ entry, score: similarity(normalizeRequirement(entry.text), requirement.text) }))
|
|
98
99
|
.sort((left, right) => right.score - left.score)
|
|
99
100
|
.find(({ score }) => score >= 0.5);
|
|
100
101
|
if (candidate) {
|
|
101
102
|
matched.add(candidate.entry.hash);
|
|
102
103
|
counts[candidate.entry.kind] += 1;
|
|
103
|
-
nextRequirements.push({ ...candidate.entry, doc, text: requirement.
|
|
104
|
+
nextRequirements.push({ ...candidate.entry, doc, text: requirement.raw, hash: requirement.hash });
|
|
104
105
|
issues.push({
|
|
105
106
|
kind: "changed",
|
|
106
107
|
doc,
|
|
107
108
|
line: requirement.line,
|
|
108
|
-
text: requirement.
|
|
109
|
+
text: requirement.raw,
|
|
109
110
|
hash: requirement.hash,
|
|
110
111
|
detail: `was "${candidate.entry.text}" — re-verify ${candidate.entry.kind}${candidate.entry.ref ? ` ${candidate.entry.ref}` : ""}`,
|
|
111
112
|
});
|
|
112
113
|
}
|
|
113
114
|
else {
|
|
114
|
-
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 });
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
for (const entry of registry.requirements) {
|
|
@@ -169,7 +170,7 @@ export function classifyRequirement(options) {
|
|
|
169
170
|
}
|
|
170
171
|
const requirement = {
|
|
171
172
|
doc: match.doc,
|
|
172
|
-
text: match.requirement.
|
|
173
|
+
text: match.requirement.raw,
|
|
173
174
|
hash: match.requirement.hash,
|
|
174
175
|
kind: input.kind,
|
|
175
176
|
...(refs.length > 0 ? { ref: refs.join(", ") } : {}),
|
|
@@ -188,6 +189,22 @@ export function readRegistry(registryPath) {
|
|
|
188
189
|
}
|
|
189
190
|
return result.data;
|
|
190
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
|
+
}
|
|
191
208
|
/**
|
|
192
209
|
* Code-point order on doc then text. Deliberately not `localeCompare`, whose
|
|
193
210
|
* result depends on the host's locale data — a generated file that is committed
|
|
@@ -200,7 +217,23 @@ function compareRequirements(left, right) {
|
|
|
200
217
|
return left.text < right.text ? -1 : 1;
|
|
201
218
|
return 0;
|
|
202
219
|
}
|
|
203
|
-
|
|
204
|
-
|
|
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) };
|
|
205
238
|
writeFileSync(registryPath, `${JSON.stringify(sorted, null, 2)}\n`);
|
|
206
239
|
}
|
|
@@ -40,6 +40,7 @@ function findDocLinks(line) {
|
|
|
40
40
|
}
|
|
41
41
|
export function parseDoc(filePath, docsRoot) {
|
|
42
42
|
const body = readFileSync(filePath, "utf8");
|
|
43
|
+
const sourceLines = body.split("\n");
|
|
43
44
|
const prose = toProse(body, "$<content>");
|
|
44
45
|
const proseWithoutCode = toProse(body, "");
|
|
45
46
|
const fileName = path.basename(filePath);
|
|
@@ -56,7 +57,10 @@ export function parseDoc(filePath, docsRoot) {
|
|
|
56
57
|
}
|
|
57
58
|
if (/^\s*[-*]\s/.test(line) && RFC_2119.test(line)) {
|
|
58
59
|
const text = normalizeRequirement(line);
|
|
59
|
-
|
|
60
|
+
// The raw bullet as written in the source file (code spans and links intact),
|
|
61
|
+
// so a registry entry's text is greppable in the doc it came from.
|
|
62
|
+
const raw = (sourceLines[index] ?? "").replace(/^\s*[-*]\s+/, "");
|
|
63
|
+
requirements.push({ text, hash: hashRequirement(text), raw, line: lineNumber });
|
|
60
64
|
}
|
|
61
65
|
if (/^\s*\d+\.\s/.test(line)) {
|
|
62
66
|
steps.push({ line: lineNumber, text: line.replace(/^\s*\d+\.\s+/, ""), links: findDocLinks(line) });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pasika",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Reusable agent setup package",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,13 +19,12 @@
|
|
|
19
19
|
"pasika": "dist/cli/index.js"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
|
-
"dist"
|
|
23
|
-
"enforcement/registry.json"
|
|
22
|
+
"dist"
|
|
24
23
|
],
|
|
25
24
|
"scripts": {
|
|
26
25
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
27
26
|
"coverage": "tsx cli/index.ts coverage",
|
|
28
|
-
"docs": "eslint docs
|
|
27
|
+
"docs": "eslint docs",
|
|
29
28
|
"fix": "eslint . --fix",
|
|
30
29
|
"lint": "eslint .",
|
|
31
30
|
"prepack": "npm run build",
|
|
@@ -52,7 +51,7 @@
|
|
|
52
51
|
"prettier": "^3.8.1",
|
|
53
52
|
"tsx": "^4.23.12",
|
|
54
53
|
"typescript": "^5.9.2",
|
|
55
|
-
"zirka": "^0.0.
|
|
54
|
+
"zirka": "^0.0.42"
|
|
56
55
|
},
|
|
57
56
|
"peerDependencies": {
|
|
58
57
|
"typescript": ">=5.0.0"
|