pasika 0.3.9 → 0.4.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.
- package/README.md +19 -16
- package/dist/eslint/pasika/index.d.ts +19 -15
- package/dist/eslint/pasika/index.js +269 -132
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -48,9 +48,9 @@ Requirements are identified by a hash of their canonical text, not by a hand-wri
|
|
|
48
48
|
|
|
49
49
|
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.
|
|
50
50
|
|
|
51
|
-
Every requirement carries a `note` explaining how it is met: what the ref'd rule
|
|
51
|
+
Every requirement carries a `note` explaining how it is met: what the ref'd rule does and where it falls short, or — with no `ref` — how a reviewer or agent applies it by hand. When a rule governs the requirement's subject without fully deciding it (e.g. its placement), the `ref` still names that rule and the `note` says what stays judgment, so a partial check never reads as a complete one.
|
|
52
52
|
|
|
53
|
-
`npm run coverage` fails when a requirement has no recorded answer, when its text changed, when it disappeared, when its `ref` names a rule
|
|
53
|
+
`npm run coverage` fails when a requirement has no recorded answer, when its text changed, when it disappeared, when its `ref` names a rule that does not exist, or when a requirement a rule governs has no test titled with its text. Confirm a reworded requirement with `npx tsx scripts/coverage.ts --accept`.
|
|
54
54
|
|
|
55
55
|
## Commands
|
|
56
56
|
|
|
@@ -79,38 +79,40 @@ npx tsx scripts/coverage.ts --classify d311a1457a --ref pasika/import-boundaries
|
|
|
79
79
|
npx tsx scripts/coverage.ts --classify 041b665bd7 --note "no check can compare against the previous state"
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
The script refuses a hash no requirement has, a `ref` naming a rule
|
|
82
|
+
The script refuses a hash no requirement has, a `ref` naming a rule that does not exist, and a classification without a note — so a mismatch cannot reach the registry by hand. Re-running it on an already-recorded requirement replaces the earlier entry. All of it reads and writes `scripts/registry.json`.
|
|
83
83
|
|
|
84
84
|
## ESLint ruleset
|
|
85
85
|
|
|
86
86
|
### With Zirka (recommended)
|
|
87
87
|
|
|
88
88
|
```ts
|
|
89
|
-
// eslint.config.ts
|
|
89
|
+
// eslint.config.ts — Next.js application
|
|
90
90
|
import { RuleSeverity, styleguide } from "zirka";
|
|
91
91
|
|
|
92
92
|
const { eslintConfig } = styleguide({
|
|
93
93
|
next: RuleSeverity.Error,
|
|
94
94
|
node: RuleSeverity.Error,
|
|
95
95
|
typescript: RuleSeverity.Error,
|
|
96
|
-
|
|
96
|
+
pasikaNextjsApp: RuleSeverity.Error,
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
export default eslintConfig;
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
`zirka` composes the
|
|
102
|
+
For a plain TypeScript repository, enable `pasikaTypescriptApp` instead of `pasikaNextjsApp`. `zirka` composes the 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.
|
|
103
103
|
|
|
104
104
|
### Without Zirka
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
// eslint.config.ts
|
|
108
|
-
import {
|
|
106
|
+
```ts
|
|
107
|
+
// eslint.config.ts — plain TypeScript repository
|
|
108
|
+
import { typescriptApp } from "pasika/eslint";
|
|
109
|
+
|
|
110
|
+
export default typescriptApp;
|
|
111
|
+
```
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
``` `pasikaNext` is the full flat-config array: the `src/**` TS/TSX and Tailwind stylesheet blocks plus everything in `pasikaRepo`. `pasikaRepo` is the repository-level preset — every block that does not touch `src/**` (the package.json manifest, husky hooks, and docs) — and is a strict subset of `pasikaNext`. The granular rule objects (`tailwindRules`, `repoPackageJsonRules`, `documentationRules`) stay exported for manual wiring.
|
|
113
|
+
`typescriptApp` is the plain-TypeScript-repository preset — the package.json manifest, the zirka configuration contract, the `src/**` TypeScript app source, and the docs. `nextjsApp` is the full framework preset: everything in `typescriptApp` plus the Next.js-stack manifest requirement, the Next.js app source rules, and the Tailwind stylesheet blocks. The granular rule objects (`tailwindRules`, `repoPackageJsonRules`, `documentationRules`) stay exported for manual wiring.
|
|
112
114
|
|
|
113
|
-
Because the preset blocks wire ESLint's language plugins, using
|
|
115
|
+
Because the preset blocks wire ESLint's language plugins, using them directly (without `zirka`) requires `@eslint/css`, `@eslint/json`, and `@eslint/markdown` to be installed in the consuming project — they are `peerDependencies` of `pasika`. A `zirka`-based setup gets them automatically.
|
|
114
116
|
|
|
115
117
|
### TS/TSX rules
|
|
116
118
|
|
|
@@ -153,7 +155,7 @@ Because the preset blocks wire ESLint's language plugins, using `pasikaNext` dir
|
|
|
153
155
|
| `pasika/shared-style-dedup` † | A className combo used by two or more components becomes a named utility |
|
|
154
156
|
| `pasika/repeated-structure` | A block of elements repeated two or more times is extracted as a named component |
|
|
155
157
|
| `pasika/sole-state-owner` | A contiguous JSX part that is the sole consumer of a useState hook is extracted into a named component |
|
|
156
|
-
| `pasika/
|
|
158
|
+
| `pasika/zirka-baseline` | eslint, prettier, and TypeScript configuration come from zirka instead of being restated locally |
|
|
157
159
|
| `pasika/zod-schema-validation` | Runtime validation through Zod schemas, not hand-written type guards |
|
|
158
160
|
| `pasika/source-under-src` | Application source lives under `src/`, not in root-level folders |
|
|
159
161
|
|
|
@@ -176,17 +178,18 @@ Applied to `src/**/globals.css` (and other stylesheets) through `@eslint/css` wi
|
|
|
176
178
|
|
|
177
179
|
### Package.json rules
|
|
178
180
|
|
|
179
|
-
Applied to `package.json` through `@eslint/json`. The framework-agnostic subset (`no-vulyk-dependency`, `exact-version`) applies to any repository, including pasika itself; `nextjs-stack` applies to a Next.js/React application.
|
|
181
|
+
Applied to `package.json` through `@eslint/json`. The framework-agnostic subset (`no-vulyk-dependency`, `exact-version`) applies to any repository, including pasika itself; `nextjs-stack` applies to a Next.js/React application, and `vulyk-docs` to a repository adopting the framework.
|
|
180
182
|
|
|
181
183
|
| Rule | Enforces |
|
|
182
184
|
| ---------------------------- | ----------------------------------------------------------------------------------- |
|
|
183
185
|
| `pasika/no-vulyk-dependency` | `vulyk` is not in `dependencies` |
|
|
184
186
|
| `pasika/exact-version` | Dependency and devDependency versions are pinned exactly, never ranges |
|
|
185
187
|
| `pasika/nextjs-stack` | All Tech Stack Reference packages are listed in `package.json` (Next.js/React apps) |
|
|
188
|
+
| `pasika/vulyk-docs` | `vulyk.config.ts` tracks the framework's docs from pasika and `AGENTS.md` exists |
|
|
186
189
|
|
|
187
190
|
### Documentation rules
|
|
188
191
|
|
|
189
|
-
The `pasika/*` markdown rules enforce the documentation guide over `docs/**/*.md` (
|
|
192
|
+
The `pasika/*` markdown rules enforce the documentation guide over `docs/**/*.md` (22 rules): file-name suffixes and titles, overview shape (presence, sentence count, links), 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`; `npm run coverage` verifies each has a test and a registry entry. Pasika's own `docs/` are linted by them in CI (`npm run lint`).
|
|
190
193
|
|
|
191
194
|
Run `npx tsx scripts/coverage.ts --json` for the exact requirement each rule covers.
|
|
192
195
|
|
|
@@ -211,4 +214,4 @@ npm run typecheck
|
|
|
211
214
|
npm run test
|
|
212
215
|
npm run coverage
|
|
213
216
|
npm run build
|
|
214
|
-
|
|
217
|
+
```
|
|
@@ -77,9 +77,7 @@ type JsxOpeningElementNode = Rule.Node & {
|
|
|
77
77
|
declare const documentationRules: {
|
|
78
78
|
"doc-kind-suffix": _eslint_markdown.MarkdownRuleDefinition;
|
|
79
79
|
"title-matches-file-name": _eslint_markdown.MarkdownRuleDefinition;
|
|
80
|
-
|
|
81
|
-
"overview-length": _eslint_markdown.MarkdownRuleDefinition;
|
|
82
|
-
"guide-overview-no-links": _eslint_markdown.MarkdownRuleDefinition;
|
|
80
|
+
overview: _eslint_markdown.MarkdownRuleDefinition;
|
|
83
81
|
"guide-step-single-sentence": _eslint_markdown.MarkdownRuleDefinition;
|
|
84
82
|
"guide-step-single-link": _eslint_markdown.MarkdownRuleDefinition;
|
|
85
83
|
"guide-states-no-requirement": _eslint_markdown.MarkdownRuleDefinition;
|
|
@@ -99,6 +97,7 @@ declare const documentationRules: {
|
|
|
99
97
|
"guide-link-anchors": _eslint_markdown.MarkdownRuleDefinition;
|
|
100
98
|
"no-nested-how-to": _eslint_markdown.MarkdownRuleDefinition;
|
|
101
99
|
"glossary-term-linking": _eslint_markdown.MarkdownRuleDefinition;
|
|
100
|
+
"guide-mentions-documents": _eslint_markdown.MarkdownRuleDefinition;
|
|
102
101
|
};
|
|
103
102
|
|
|
104
103
|
declare const tailwindRules: {
|
|
@@ -129,7 +128,11 @@ declare const huskyRules: {
|
|
|
129
128
|
"husky-hook": _eslint_json.JSONRuleDefinition;
|
|
130
129
|
};
|
|
131
130
|
|
|
132
|
-
|
|
131
|
+
declare const vulykRules: {
|
|
132
|
+
"vulyk-docs": _eslint_json.JSONRuleDefinition;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** Every source rule, merged so the Next.js preset applies them all. */
|
|
133
136
|
declare const pasikaRules: {
|
|
134
137
|
"component-placement": eslint.Rule.RuleModule;
|
|
135
138
|
"application-structure": eslint.Rule.RuleModule;
|
|
@@ -182,23 +185,24 @@ declare const pasikaRules: {
|
|
|
182
185
|
"type-extraction": eslint.Rule.RuleModule;
|
|
183
186
|
"zod-schema-validation": eslint.Rule.RuleModule;
|
|
184
187
|
"source-under-src": eslint.Rule.RuleModule;
|
|
185
|
-
"
|
|
188
|
+
"zirka-baseline": eslint.Rule.RuleModule;
|
|
186
189
|
};
|
|
187
190
|
|
|
188
191
|
/** Every rule id, as they appear in configuration and in lint output. */
|
|
189
192
|
declare const allPasikaRuleIds: string[];
|
|
190
193
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
194
|
+
* TypeScript app preset: the framework-agnostic baseline — the package.json
|
|
195
|
+
* manifest (incl. husky hook and vulyk requirements), the zirka configuration
|
|
196
|
+
* contract, the `src/**` TS/TSX source, and the documentation markdown rules.
|
|
197
|
+
* Use this for a plain TypeScript repository.
|
|
195
198
|
*/
|
|
196
|
-
declare const
|
|
199
|
+
declare const typescriptApp: Linter.Config[];
|
|
197
200
|
/**
|
|
198
|
-
*
|
|
199
|
-
* `
|
|
200
|
-
*
|
|
201
|
+
* Next.js app preset: the full adopted-to-the-framework stack. Anything in
|
|
202
|
+
* `typescriptApp` plus the framework-only blocks — the Next.js-stack manifest
|
|
203
|
+
* requirement, the Next.js app source rules, and the Tailwind stylesheet
|
|
204
|
+
* rules. `typescriptApp` is a strict subset of `nextjsApp`.
|
|
201
205
|
*/
|
|
202
|
-
declare const
|
|
206
|
+
declare const nextjsApp: Linter.Config[];
|
|
203
207
|
|
|
204
|
-
export { allPasikaRuleIds, documentationRules, huskyRules, nextPackageJsonRules,
|
|
208
|
+
export { allPasikaRuleIds, documentationRules, huskyRules, nextPackageJsonRules, nextjsApp, pasikaRules, repoPackageJsonRules, tailwindRules, typescriptApp, vulykRules };
|
|
@@ -3266,43 +3266,88 @@ var sourceUnderSrcRule = {
|
|
|
3266
3266
|
}
|
|
3267
3267
|
};
|
|
3268
3268
|
|
|
3269
|
-
// eslint/rules/
|
|
3269
|
+
// eslint/rules/zirka-baseline.ts
|
|
3270
3270
|
import fs4 from "fs";
|
|
3271
3271
|
import path33 from "path";
|
|
3272
|
-
var
|
|
3272
|
+
var ESLINT_CONFIG = /^eslint\.config\.(?:ts|mts|cts|js|mjs|cjs)$/;
|
|
3273
|
+
var PRETTIER_CONFIGS = [
|
|
3274
|
+
"prettier.config.mjs",
|
|
3275
|
+
"prettier.config.cjs",
|
|
3276
|
+
"prettier.config.js",
|
|
3277
|
+
"prettier.config.ts",
|
|
3278
|
+
"prettier.config.mts",
|
|
3279
|
+
"prettier.config.cts"
|
|
3280
|
+
];
|
|
3281
|
+
var zirkaBaselineRule = {
|
|
3273
3282
|
meta: {
|
|
3274
3283
|
schema: [],
|
|
3275
3284
|
type: "problem",
|
|
3276
3285
|
docs: {
|
|
3277
|
-
description: "Require eslint
|
|
3286
|
+
description: "Require eslint, prettier, and TypeScript configuration to come from zirka."
|
|
3278
3287
|
}
|
|
3279
3288
|
},
|
|
3280
3289
|
create(context) {
|
|
3281
3290
|
const filename = path33.resolve(context.filename);
|
|
3282
3291
|
const basename = path33.basename(filename);
|
|
3283
|
-
if (
|
|
3292
|
+
if (!ESLINT_CONFIG.test(basename)) return {};
|
|
3293
|
+
const projectRoot = path33.dirname(filename);
|
|
3294
|
+
const report3 = (message) => {
|
|
3295
|
+
context.report({
|
|
3296
|
+
node: context.sourceCode.ast,
|
|
3297
|
+
loc: { line: 1, column: 0 },
|
|
3298
|
+
message
|
|
3299
|
+
});
|
|
3300
|
+
};
|
|
3284
3301
|
return {
|
|
3285
|
-
Program(
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
loc: { line: 1, column: 0 },
|
|
3291
|
-
message: "ESLint config must reference zirka. Use the pasika/zirka baseline configuration."
|
|
3292
|
-
});
|
|
3302
|
+
Program() {
|
|
3303
|
+
if (!referencesZirka(context.sourceCode)) {
|
|
3304
|
+
report3(
|
|
3305
|
+
'ESLint config must take its configuration from zirka (import { styleguide } from "zirka") instead of restating rules locally.'
|
|
3306
|
+
);
|
|
3293
3307
|
}
|
|
3294
|
-
const
|
|
3295
|
-
if (!fs4.existsSync(
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3308
|
+
const tsconfigPath = path33.join(projectRoot, "tsconfig.json");
|
|
3309
|
+
if (!fs4.existsSync(tsconfigPath)) {
|
|
3310
|
+
report3('No tsconfig.json found. Create one extending the zirka TypeScript base config ("zirka/typescript").');
|
|
3311
|
+
} else {
|
|
3312
|
+
let extendsValue;
|
|
3313
|
+
try {
|
|
3314
|
+
const parsed = JSON.parse(fs4.readFileSync(tsconfigPath, "utf8"));
|
|
3315
|
+
extendsValue = typeof parsed === "object" && parsed !== null && "extends" in parsed ? parsed.extends : void 0;
|
|
3316
|
+
} catch {
|
|
3317
|
+
report3(
|
|
3318
|
+
'tsconfig.json must be valid JSON and extend the zirka TypeScript base config ("zirka/typescript").'
|
|
3319
|
+
);
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
if (typeof extendsValue !== "string" || !extendsValue.startsWith("zirka")) {
|
|
3323
|
+
report3('tsconfig.json must extend the zirka TypeScript base config ("zirka/typescript").');
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
const prettierConfigFile = PRETTIER_CONFIGS.find((name) => fs4.existsSync(path33.join(projectRoot, name)));
|
|
3327
|
+
if (!prettierConfigFile) {
|
|
3328
|
+
report3(
|
|
3329
|
+
"No prettier config found. Create one that takes its configuration from zirka (styleguide({ prettier: true }).prettierConfig)."
|
|
3330
|
+
);
|
|
3331
|
+
} else {
|
|
3332
|
+
const content = fs4.readFileSync(path33.join(projectRoot, prettierConfigFile), "utf8");
|
|
3333
|
+
if (!content.includes("zirka")) {
|
|
3334
|
+
report3(
|
|
3335
|
+
"The prettier config must take its configuration from zirka (styleguide({ prettier: true }).prettierConfig) instead of restating it locally."
|
|
3336
|
+
);
|
|
3337
|
+
}
|
|
3301
3338
|
}
|
|
3302
3339
|
}
|
|
3303
3340
|
};
|
|
3304
3341
|
}
|
|
3305
3342
|
};
|
|
3343
|
+
function referencesZirka(sourceCode) {
|
|
3344
|
+
for (const statement of sourceCode.ast.body) {
|
|
3345
|
+
if (statement.type === "ImportDeclaration" && typeof statement.source.value === "string" && statement.source.value.startsWith("zirka")) {
|
|
3346
|
+
return true;
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
return /require\(\s*["']zirka["']\s*\)/.test(sourceCode.getText());
|
|
3350
|
+
}
|
|
3306
3351
|
|
|
3307
3352
|
// constants/rfc2119.ts
|
|
3308
3353
|
var RFC_2119_KEYWORDS = ["MUST NOT", "MUST", "SHOULD NOT", "SHOULD", "MAY"];
|
|
@@ -3391,90 +3436,10 @@ var titleMatchesFileNameRule = {
|
|
|
3391
3436
|
}
|
|
3392
3437
|
};
|
|
3393
3438
|
|
|
3394
|
-
// eslint/rules/documentation/overview
|
|
3395
|
-
var overviewPresentRule = {
|
|
3396
|
-
meta: {
|
|
3397
|
-
type: "problem",
|
|
3398
|
-
docs: {
|
|
3399
|
-
description: "Overview must follow the title in a documentation file.",
|
|
3400
|
-
recommended: true
|
|
3401
|
-
}
|
|
3402
|
-
},
|
|
3403
|
-
create(context) {
|
|
3404
|
-
return {
|
|
3405
|
-
root(node) {
|
|
3406
|
-
const filename = getFilename(context);
|
|
3407
|
-
if (!filename.endsWith(".md")) return;
|
|
3408
|
-
let titleLine = 0;
|
|
3409
|
-
for (const child of node.children) {
|
|
3410
|
-
if (child.type === "heading" && child.depth === 1) {
|
|
3411
|
-
titleLine = getLine(child);
|
|
3412
|
-
break;
|
|
3413
|
-
}
|
|
3414
|
-
}
|
|
3415
|
-
if (!titleLine) return;
|
|
3416
|
-
let hasOverview = false;
|
|
3417
|
-
for (const child of node.children) {
|
|
3418
|
-
if (child.type === "paragraph" && getLine(child) > titleLine && getTextContent(child).trim()) {
|
|
3419
|
-
hasOverview = true;
|
|
3420
|
-
break;
|
|
3421
|
-
}
|
|
3422
|
-
}
|
|
3423
|
-
if (!hasOverview) {
|
|
3424
|
-
context.report({
|
|
3425
|
-
node,
|
|
3426
|
-
message: "no overview follows the title"
|
|
3427
|
-
});
|
|
3428
|
-
}
|
|
3429
|
-
}
|
|
3430
|
-
};
|
|
3431
|
-
}
|
|
3432
|
-
};
|
|
3433
|
-
|
|
3434
|
-
// eslint/rules/documentation/overview-length.ts
|
|
3439
|
+
// eslint/rules/documentation/overview.ts
|
|
3435
3440
|
function countSentences(text) {
|
|
3436
3441
|
return text.split(/[.!?](?:\s+|$)/).filter((part) => part.trim() !== "").length;
|
|
3437
3442
|
}
|
|
3438
|
-
var overviewLengthRule = {
|
|
3439
|
-
meta: {
|
|
3440
|
-
type: "problem",
|
|
3441
|
-
docs: {
|
|
3442
|
-
description: "Overview must contain at most two sentences.",
|
|
3443
|
-
recommended: true
|
|
3444
|
-
}
|
|
3445
|
-
},
|
|
3446
|
-
create(context) {
|
|
3447
|
-
return {
|
|
3448
|
-
root(node) {
|
|
3449
|
-
const filename = getFilename(context);
|
|
3450
|
-
if (!filename.endsWith(".md")) return;
|
|
3451
|
-
let titleLine = 0;
|
|
3452
|
-
for (const child of node.children) {
|
|
3453
|
-
if (child.type === "heading" && child.depth === 1) {
|
|
3454
|
-
titleLine = getLine(child);
|
|
3455
|
-
break;
|
|
3456
|
-
}
|
|
3457
|
-
}
|
|
3458
|
-
if (!titleLine) return;
|
|
3459
|
-
for (const child of node.children) {
|
|
3460
|
-
if (child.type !== "paragraph" || getLine(child) <= titleLine) continue;
|
|
3461
|
-
const text = getTextContent(child).trim();
|
|
3462
|
-
if (!text || text.startsWith("#")) continue;
|
|
3463
|
-
const sentenceCount = countSentences(text);
|
|
3464
|
-
if (sentenceCount > 2) {
|
|
3465
|
-
context.report({
|
|
3466
|
-
node: child,
|
|
3467
|
-
message: `overview uses ${String(sentenceCount)} sentences, at most two are allowed`
|
|
3468
|
-
});
|
|
3469
|
-
}
|
|
3470
|
-
break;
|
|
3471
|
-
}
|
|
3472
|
-
}
|
|
3473
|
-
};
|
|
3474
|
-
}
|
|
3475
|
-
};
|
|
3476
|
-
|
|
3477
|
-
// eslint/rules/documentation/guide-overview-no-links.ts
|
|
3478
3443
|
function containsLink(node) {
|
|
3479
3444
|
if (node.type === "link") return true;
|
|
3480
3445
|
if ("children" in node) {
|
|
@@ -3482,11 +3447,11 @@ function containsLink(node) {
|
|
|
3482
3447
|
}
|
|
3483
3448
|
return false;
|
|
3484
3449
|
}
|
|
3485
|
-
var
|
|
3450
|
+
var overviewRule = {
|
|
3486
3451
|
meta: {
|
|
3487
3452
|
type: "problem",
|
|
3488
3453
|
docs: {
|
|
3489
|
-
description: "
|
|
3454
|
+
description: "Overview must exist, contain at most two sentences, and not link to other documents.",
|
|
3490
3455
|
recommended: true
|
|
3491
3456
|
}
|
|
3492
3457
|
},
|
|
@@ -3494,7 +3459,7 @@ var guideOverviewNoLinksRule = {
|
|
|
3494
3459
|
return {
|
|
3495
3460
|
root(node) {
|
|
3496
3461
|
const filename = getFilename(context);
|
|
3497
|
-
if (!filename.endsWith("
|
|
3462
|
+
if (!filename.endsWith(".md")) return;
|
|
3498
3463
|
let titleLine = 0;
|
|
3499
3464
|
for (const child of node.children) {
|
|
3500
3465
|
if (child.type === "heading" && child.depth === 1) {
|
|
@@ -3503,17 +3468,31 @@ var guideOverviewNoLinksRule = {
|
|
|
3503
3468
|
}
|
|
3504
3469
|
}
|
|
3505
3470
|
if (!titleLine) return;
|
|
3471
|
+
let sentenceCount = 0;
|
|
3472
|
+
let found = false;
|
|
3506
3473
|
for (const child of node.children) {
|
|
3474
|
+
if (child.type === "heading" && getLine(child) > titleLine) break;
|
|
3507
3475
|
if (child.type !== "paragraph" || getLine(child) <= titleLine) continue;
|
|
3508
3476
|
const text = getTextContent(child).trim();
|
|
3509
3477
|
if (!text || text.startsWith("#")) continue;
|
|
3478
|
+
found = true;
|
|
3510
3479
|
if (containsLink(child)) {
|
|
3511
3480
|
context.report({
|
|
3512
3481
|
node: child,
|
|
3513
|
-
message: "
|
|
3482
|
+
message: "overview links another document"
|
|
3514
3483
|
});
|
|
3515
3484
|
}
|
|
3516
|
-
|
|
3485
|
+
sentenceCount += countSentences(text);
|
|
3486
|
+
if (sentenceCount > 2) {
|
|
3487
|
+
context.report({
|
|
3488
|
+
node: child,
|
|
3489
|
+
message: `overview uses ${String(sentenceCount)} sentences, at most two are allowed`
|
|
3490
|
+
});
|
|
3491
|
+
break;
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
if (!found) {
|
|
3495
|
+
context.report({ node, message: "no overview follows the title" });
|
|
3517
3496
|
}
|
|
3518
3497
|
}
|
|
3519
3498
|
};
|
|
@@ -4288,13 +4267,91 @@ var glossaryTermLinkingRule = {
|
|
|
4288
4267
|
}
|
|
4289
4268
|
};
|
|
4290
4269
|
|
|
4270
|
+
// eslint/rules/documentation/guide-mentions-documents.ts
|
|
4271
|
+
import { existsSync } from "fs";
|
|
4272
|
+
import path39 from "path";
|
|
4273
|
+
function visitSteps3(node, check) {
|
|
4274
|
+
if (node.type === "list" && node.ordered) {
|
|
4275
|
+
for (const child of node.children) check(child);
|
|
4276
|
+
}
|
|
4277
|
+
if ("children" in node) {
|
|
4278
|
+
for (const child of node.children) visitSteps3(child, check);
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
function collectMarkdownLinks(node, out) {
|
|
4282
|
+
if (node.type === "link" && node.url.endsWith(".md")) out.push(node);
|
|
4283
|
+
if ("children" in node) {
|
|
4284
|
+
for (const child of node.children) collectMarkdownLinks(child, out);
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
function linkTarget(url) {
|
|
4288
|
+
return url.split("#")[0] ?? url;
|
|
4289
|
+
}
|
|
4290
|
+
var guideMentionsDocumentsRule = {
|
|
4291
|
+
meta: {
|
|
4292
|
+
type: "problem",
|
|
4293
|
+
docs: {
|
|
4294
|
+
description: "A Guide entry point must reference each owned Rule from a How To step, mention every Reference, and not link a missing document.",
|
|
4295
|
+
recommended: true
|
|
4296
|
+
}
|
|
4297
|
+
},
|
|
4298
|
+
create(context) {
|
|
4299
|
+
return {
|
|
4300
|
+
root(node) {
|
|
4301
|
+
const filename = getFilename(context);
|
|
4302
|
+
if (!filename.endsWith("-guide.md")) return;
|
|
4303
|
+
const docsRoot = findDocsRoot(filename);
|
|
4304
|
+
if (!docsRoot) return;
|
|
4305
|
+
const guideDir = path39.dirname(filename);
|
|
4306
|
+
if (path39.basename(filename, ".md") !== path39.basename(guideDir)) return;
|
|
4307
|
+
const docs = getProjectDocs(docsRoot);
|
|
4308
|
+
const owned = docs.filter((doc) => {
|
|
4309
|
+
const parent = path39.dirname(doc.filePath);
|
|
4310
|
+
return parent === path39.join(guideDir, "rules") || parent === path39.join(guideDir, "references");
|
|
4311
|
+
});
|
|
4312
|
+
const allLinks = [];
|
|
4313
|
+
collectMarkdownLinks(node, allLinks);
|
|
4314
|
+
const stepLinks = [];
|
|
4315
|
+
visitSteps3(node, (item) => {
|
|
4316
|
+
collectMarkdownLinks(item, stepLinks);
|
|
4317
|
+
});
|
|
4318
|
+
const mentionsOf = (links, fileName) => links.some((link) => link.url.split("/").pop() === fileName);
|
|
4319
|
+
for (const doc of owned) {
|
|
4320
|
+
if (doc.kind === "rule") {
|
|
4321
|
+
if (!mentionsOf(stepLinks, doc.fileName)) {
|
|
4322
|
+
context.report({
|
|
4323
|
+
node,
|
|
4324
|
+
message: `Guide entry point does not reference a Rule it owns from a How To step: ${doc.doc}`
|
|
4325
|
+
});
|
|
4326
|
+
}
|
|
4327
|
+
} else if (!mentionsOf(allLinks, doc.fileName)) {
|
|
4328
|
+
context.report({
|
|
4329
|
+
node,
|
|
4330
|
+
message: `Guide entry point does not mention a document it owns: ${doc.doc}`
|
|
4331
|
+
});
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
for (const link of allLinks) {
|
|
4335
|
+
const target = linkTarget(link.url);
|
|
4336
|
+
if (!target.endsWith(".md")) continue;
|
|
4337
|
+
const resolved = path39.normalize(path39.join(guideDir, target));
|
|
4338
|
+
if (!existsSync(resolved)) {
|
|
4339
|
+
context.report({
|
|
4340
|
+
node: link,
|
|
4341
|
+
message: `Guide links a document that does not exist: ${link.url}`
|
|
4342
|
+
});
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
};
|
|
4347
|
+
}
|
|
4348
|
+
};
|
|
4349
|
+
|
|
4291
4350
|
// eslint/rules/documentation/index.ts
|
|
4292
4351
|
var documentationRules = {
|
|
4293
4352
|
"doc-kind-suffix": docKindSuffixRule,
|
|
4294
4353
|
"title-matches-file-name": titleMatchesFileNameRule,
|
|
4295
|
-
|
|
4296
|
-
"overview-length": overviewLengthRule,
|
|
4297
|
-
"guide-overview-no-links": guideOverviewNoLinksRule,
|
|
4354
|
+
overview: overviewRule,
|
|
4298
4355
|
"guide-step-single-sentence": guideStepSingleSentenceRule,
|
|
4299
4356
|
"guide-step-single-link": guideStepSingleLinkRule,
|
|
4300
4357
|
"guide-states-no-requirement": guideStatesNoRequirementRule,
|
|
@@ -4313,7 +4370,8 @@ var documentationRules = {
|
|
|
4313
4370
|
"policy-subject-headings": policySubjectHeadingsRule,
|
|
4314
4371
|
"guide-link-anchors": guideLinkAnchorsRule,
|
|
4315
4372
|
"no-nested-how-to": noNestedHowToRule,
|
|
4316
|
-
"glossary-term-linking": glossaryTermLinkingRule
|
|
4373
|
+
"glossary-term-linking": glossaryTermLinkingRule,
|
|
4374
|
+
"guide-mentions-documents": guideMentionsDocumentsRule
|
|
4317
4375
|
};
|
|
4318
4376
|
|
|
4319
4377
|
// eslint/rules/tailwind/helpers.ts
|
|
@@ -4940,8 +4998,8 @@ var nextPackageJsonRules = {
|
|
|
4940
4998
|
};
|
|
4941
4999
|
|
|
4942
5000
|
// eslint/rules/husky/husky-hook.ts
|
|
4943
|
-
import { existsSync, readFileSync as readFileSync6 } from "fs";
|
|
4944
|
-
import
|
|
5001
|
+
import { existsSync as existsSync2, readFileSync as readFileSync6 } from "fs";
|
|
5002
|
+
import path40 from "path";
|
|
4945
5003
|
function memberName4(member) {
|
|
4946
5004
|
return member.name.type === "String" ? member.name.value : member.name.name;
|
|
4947
5005
|
}
|
|
@@ -4960,8 +5018,8 @@ var huskyHookRule = {
|
|
|
4960
5018
|
if (root.type !== "Object") return;
|
|
4961
5019
|
const scriptName = context.filename;
|
|
4962
5020
|
if (!scriptName.endsWith("package.json")) return;
|
|
4963
|
-
const hookPath =
|
|
4964
|
-
if (!
|
|
5021
|
+
const hookPath = path40.join(process.cwd(), ".husky", "pre-commit");
|
|
5022
|
+
if (!existsSync2(hookPath)) {
|
|
4965
5023
|
context.report({
|
|
4966
5024
|
node,
|
|
4967
5025
|
message: "No .husky/pre-commit hook found. Configure husky to run checks before commits."
|
|
@@ -4998,8 +5056,57 @@ var huskyRules = {
|
|
|
4998
5056
|
"husky-hook": huskyHookRule
|
|
4999
5057
|
};
|
|
5000
5058
|
|
|
5059
|
+
// eslint/rules/vulyk/vulyk-docs.ts
|
|
5060
|
+
import { existsSync as existsSync3, readFileSync as readFileSync7 } from "fs";
|
|
5061
|
+
import path41 from "path";
|
|
5062
|
+
var PASIKA_REPO = "Bredansky/pasika";
|
|
5063
|
+
var vulykDocsRule = {
|
|
5064
|
+
meta: {
|
|
5065
|
+
schema: [],
|
|
5066
|
+
type: "problem",
|
|
5067
|
+
docs: {
|
|
5068
|
+
description: "Require vulyk.config.ts to track the framework's docs from pasika and the generated AGENTS.md."
|
|
5069
|
+
}
|
|
5070
|
+
},
|
|
5071
|
+
create(context) {
|
|
5072
|
+
return {
|
|
5073
|
+
Document(node) {
|
|
5074
|
+
if (!context.filename.endsWith("package.json")) return;
|
|
5075
|
+
const projectRoot = path41.dirname(path41.resolve(context.filename));
|
|
5076
|
+
const configPath = path41.join(projectRoot, "vulyk.config.ts");
|
|
5077
|
+
if (!existsSync3(configPath)) {
|
|
5078
|
+
context.report({
|
|
5079
|
+
node,
|
|
5080
|
+
message: "No vulyk.config.ts found. Run npx vulyk@latest init to create one that tracks the framework's docs."
|
|
5081
|
+
});
|
|
5082
|
+
return;
|
|
5083
|
+
}
|
|
5084
|
+
const config = readFileSync7(configPath, "utf8");
|
|
5085
|
+
if (!config.includes(PASIKA_REPO)) {
|
|
5086
|
+
context.report({
|
|
5087
|
+
node,
|
|
5088
|
+
message: "vulyk.config.ts must track the framework's docs from the pasika repository."
|
|
5089
|
+
});
|
|
5090
|
+
}
|
|
5091
|
+
const agentsPath = path41.join(projectRoot, "AGENTS.md");
|
|
5092
|
+
if (!existsSync3(agentsPath)) {
|
|
5093
|
+
context.report({
|
|
5094
|
+
node,
|
|
5095
|
+
message: "No AGENTS.md found. Run npx vulyk@latest agents to generate the agent file that routes to the tracked docs."
|
|
5096
|
+
});
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
};
|
|
5100
|
+
}
|
|
5101
|
+
};
|
|
5102
|
+
|
|
5103
|
+
// eslint/rules/vulyk/index.ts
|
|
5104
|
+
var vulykRules = {
|
|
5105
|
+
"vulyk-docs": vulykDocsRule
|
|
5106
|
+
};
|
|
5107
|
+
|
|
5001
5108
|
// eslint/index.ts
|
|
5002
|
-
var
|
|
5109
|
+
var typescriptAppRules = {
|
|
5003
5110
|
"filename-case": filenameCaseRule,
|
|
5004
5111
|
"import-boundaries": importBoundariesRule,
|
|
5005
5112
|
"named-exports": namedExportsRule,
|
|
@@ -5014,9 +5121,9 @@ var repoSourceRules = {
|
|
|
5014
5121
|
"type-extraction": typeExtractionRule,
|
|
5015
5122
|
"zod-schema-validation": zodSchemaValidationRule,
|
|
5016
5123
|
"source-under-src": sourceUnderSrcRule,
|
|
5017
|
-
"
|
|
5124
|
+
"zirka-baseline": zirkaBaselineRule
|
|
5018
5125
|
};
|
|
5019
|
-
var
|
|
5126
|
+
var nextjsAppRules = {
|
|
5020
5127
|
"component-placement": componentPlacementRule,
|
|
5021
5128
|
"application-structure": applicationStructureRule,
|
|
5022
5129
|
"data-testid-case": dataTestIdCaseRule,
|
|
@@ -5045,32 +5152,43 @@ var nextSourceRules = {
|
|
|
5045
5152
|
"repeated-structure": repeatedStructureRule
|
|
5046
5153
|
};
|
|
5047
5154
|
var pasikaRules = {
|
|
5048
|
-
...
|
|
5049
|
-
...
|
|
5155
|
+
...typescriptAppRules,
|
|
5156
|
+
...nextjsAppRules
|
|
5050
5157
|
};
|
|
5051
5158
|
function ruleIds(rules2) {
|
|
5052
5159
|
return Object.keys(rules2).map((name) => `pasika/${name}`);
|
|
5053
5160
|
}
|
|
5161
|
+
var typescriptAppRuleIds = ruleIds(typescriptAppRules);
|
|
5162
|
+
var nextjsAppRuleIds = ruleIds(nextjsAppRules);
|
|
5054
5163
|
var pasikaRuleIds = ruleIds(pasikaRules);
|
|
5055
5164
|
var documentationRuleIds = ruleIds(documentationRules);
|
|
5056
5165
|
var tailwindRuleIds = ruleIds(tailwindRules);
|
|
5057
5166
|
var repoPackageJsonRuleIds = ruleIds(repoPackageJsonRules);
|
|
5058
5167
|
var nextPackageJsonRuleIds = ruleIds(nextPackageJsonRules);
|
|
5059
5168
|
var huskyRuleIds = ruleIds(huskyRules);
|
|
5169
|
+
var vulykRuleIds = ruleIds(vulykRules);
|
|
5060
5170
|
var allPasikaRuleIds = [
|
|
5061
5171
|
...pasikaRuleIds,
|
|
5062
5172
|
...documentationRuleIds,
|
|
5063
5173
|
...tailwindRuleIds,
|
|
5064
5174
|
...repoPackageJsonRuleIds,
|
|
5065
5175
|
...nextPackageJsonRuleIds,
|
|
5066
|
-
...huskyRuleIds
|
|
5176
|
+
...huskyRuleIds,
|
|
5177
|
+
...vulykRuleIds
|
|
5067
5178
|
];
|
|
5068
|
-
var
|
|
5179
|
+
var typescriptAppBlock = {
|
|
5180
|
+
files: ["src/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}"],
|
|
5181
|
+
plugins: {
|
|
5182
|
+
pasika: { rules: typescriptAppRules }
|
|
5183
|
+
},
|
|
5184
|
+
rules: Object.fromEntries(typescriptAppRuleIds.map((id) => [id, "error"]))
|
|
5185
|
+
};
|
|
5186
|
+
var nextjsAppBlock = {
|
|
5069
5187
|
files: ["src/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}"],
|
|
5070
5188
|
plugins: {
|
|
5071
|
-
pasika: { rules:
|
|
5189
|
+
pasika: { rules: nextjsAppRules }
|
|
5072
5190
|
},
|
|
5073
|
-
rules: Object.fromEntries(
|
|
5191
|
+
rules: Object.fromEntries(nextjsAppRuleIds.map((id) => [id, "error"]))
|
|
5074
5192
|
};
|
|
5075
5193
|
var tailwindGlobalsBlock = {
|
|
5076
5194
|
files: ["src/**/globals.css"],
|
|
@@ -5094,16 +5212,28 @@ var tailwindAnyCssBlock = {
|
|
|
5094
5212
|
languageOptions: { tolerant: true },
|
|
5095
5213
|
rules: { "pasika/global-css-location": "error" }
|
|
5096
5214
|
};
|
|
5097
|
-
var
|
|
5215
|
+
var repoManifestBlock = {
|
|
5098
5216
|
files: ["package.json"],
|
|
5099
5217
|
plugins: {
|
|
5100
5218
|
json: { languages: { json: jsonPlugin.languages.json } },
|
|
5101
|
-
pasika: { rules: { ...repoPackageJsonRules, ...
|
|
5219
|
+
pasika: { rules: { ...repoPackageJsonRules, ...huskyRules, ...vulykRules } }
|
|
5102
5220
|
},
|
|
5103
5221
|
language: "json/json",
|
|
5104
|
-
rules: Object.fromEntries(
|
|
5105
|
-
|
|
5106
|
-
|
|
5222
|
+
rules: Object.fromEntries([...repoPackageJsonRuleIds, ...huskyRuleIds, ...vulykRuleIds].map((id) => [id, "error"]))
|
|
5223
|
+
};
|
|
5224
|
+
var nextManifestBlock = {
|
|
5225
|
+
files: ["package.json"],
|
|
5226
|
+
plugins: {
|
|
5227
|
+
json: { languages: { json: jsonPlugin.languages.json } },
|
|
5228
|
+
pasika: { rules: nextPackageJsonRules }
|
|
5229
|
+
},
|
|
5230
|
+
language: "json/json",
|
|
5231
|
+
rules: Object.fromEntries(nextPackageJsonRuleIds.map((id) => [id, "error"]))
|
|
5232
|
+
};
|
|
5233
|
+
var zirkaBlock = {
|
|
5234
|
+
files: ["eslint.config.{ts,mts,cts,js,mjs,cjs}"],
|
|
5235
|
+
plugins: { pasika: { rules: { "zirka-baseline": zirkaBaselineRule } } },
|
|
5236
|
+
rules: { "pasika/zirka-baseline": "error" }
|
|
5107
5237
|
};
|
|
5108
5238
|
var docsBlock = {
|
|
5109
5239
|
files: ["docs/**/*.md"],
|
|
@@ -5115,16 +5245,23 @@ var docsBlock = {
|
|
|
5115
5245
|
language: "markdown/gfm",
|
|
5116
5246
|
rules: Object.fromEntries(documentationRuleIds.map((id) => [id, "error"]))
|
|
5117
5247
|
};
|
|
5118
|
-
var
|
|
5119
|
-
var
|
|
5248
|
+
var typescriptApp = [repoManifestBlock, zirkaBlock, typescriptAppBlock, docsBlock];
|
|
5249
|
+
var nextjsApp = [
|
|
5250
|
+
...typescriptApp,
|
|
5251
|
+
nextManifestBlock,
|
|
5252
|
+
nextjsAppBlock,
|
|
5253
|
+
tailwindGlobalsBlock,
|
|
5254
|
+
tailwindAnyCssBlock
|
|
5255
|
+
];
|
|
5120
5256
|
export {
|
|
5121
5257
|
allPasikaRuleIds,
|
|
5122
5258
|
documentationRules,
|
|
5123
5259
|
huskyRules,
|
|
5124
5260
|
nextPackageJsonRules,
|
|
5125
|
-
|
|
5126
|
-
pasikaRepo,
|
|
5261
|
+
nextjsApp,
|
|
5127
5262
|
pasikaRules,
|
|
5128
5263
|
repoPackageJsonRules,
|
|
5129
|
-
tailwindRules
|
|
5264
|
+
tailwindRules,
|
|
5265
|
+
typescriptApp,
|
|
5266
|
+
vulykRules
|
|
5130
5267
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pasika",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Reusable agent setup package",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"tsup": "8.5.1",
|
|
52
52
|
"tsx": "4.23.12",
|
|
53
53
|
"typescript": "6.0.3",
|
|
54
|
-
"zirka": "0.0.
|
|
54
|
+
"zirka": "0.0.46",
|
|
55
55
|
"zod": "4.4.3"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|