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 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 or doctor check 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.
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 or doctor check 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`.
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 or doctor check 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`.
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
- pasika: RuleSeverity.Error,
96
+ pasikaNextjsApp: RuleSeverity.Error,
97
97
  });
98
98
 
99
99
  export default eslintConfig;
100
100
  ```
101
101
 
102
- `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.
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
- ````ts
107
- // eslint.config.ts
108
- import { pasikaNext } from "pasika/eslint";
106
+ ```ts
107
+ // eslint.config.ts — plain TypeScript repository
108
+ import { typescriptApp } from "pasika/eslint";
109
+
110
+ export default typescriptApp;
111
+ ```
109
112
 
110
- export default pasikaNext;
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 `pasikaNext` 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.
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/config-baseline` | `eslint.config.*` references zirka and `tsconfig.json` exists |
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` (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`; `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`).
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
- "overview-present": _eslint_markdown.MarkdownRuleDefinition;
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
- /** Every source rule, merged so the Next preset applies them all. */
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
- "config-baseline": eslint.Rule.RuleModule;
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
- * Repository-level preset: every block that does NOT touch `src/**` — the
192
- * package.json manifest (including husky hook requirements) and the
193
- * documentation-guide markdown rules. This governs the repository itself,
194
- * independent of any application source.
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 pasikaRepo: Linter.Config[];
199
+ declare const typescriptApp: Linter.Config[];
197
200
  /**
198
- * Framework-wide preset: the full adopted-to-the-framework stack. Anything in
199
- * `pasikaRepo` plus the `src/**` blocks — TS/TSX source rules and the Tailwind
200
- * stylesheet rules. `pasikaRepo` is a strict subset of `pasikaNext`.
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 pasikaNext: Linter.Config[];
206
+ declare const nextjsApp: Linter.Config[];
203
207
 
204
- export { allPasikaRuleIds, documentationRules, huskyRules, nextPackageJsonRules, pasikaNext, pasikaRepo, pasikaRules, repoPackageJsonRules, tailwindRules };
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/config-baseline.ts
3269
+ // eslint/rules/zirka-baseline.ts
3270
3270
  import fs4 from "fs";
3271
3271
  import path33 from "path";
3272
- var configBaselineRule = {
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 config to reference zirka and tsconfig.json to exist."
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 (!/^eslint\.config\.(?:ts|mts|cts|js|mjs|cjs)$/.test(basename)) return {};
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(node) {
3286
- const content = context.sourceCode.getText();
3287
- if (!content.includes("zirka")) {
3288
- context.report({
3289
- node,
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 projectRoot = path33.dirname(filename);
3295
- if (!fs4.existsSync(path33.join(projectRoot, "tsconfig.json"))) {
3296
- context.report({
3297
- node,
3298
- loc: { line: 1, column: 0 },
3299
- message: "No tsconfig.json found. Create one extending the pasika baseline."
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-present.ts
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 guideOverviewNoLinksRule = {
3450
+ var overviewRule = {
3486
3451
  meta: {
3487
3452
  type: "problem",
3488
3453
  docs: {
3489
- description: "Guide overview must not link to other documents.",
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("-guide.md")) return;
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: "guide overview links another document"
3482
+ message: "overview links another document"
3514
3483
  });
3515
3484
  }
3516
- break;
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
- "overview-present": overviewPresentRule,
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 path39 from "path";
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 = path39.join(process.cwd(), ".husky", "pre-commit");
4964
- if (!existsSync(hookPath)) {
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 repoSourceRules = {
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
- "config-baseline": configBaselineRule
5124
+ "zirka-baseline": zirkaBaselineRule
5018
5125
  };
5019
- var nextSourceRules = {
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
- ...repoSourceRules,
5049
- ...nextSourceRules
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 nextAppBlock = {
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: pasikaRules }
5189
+ pasika: { rules: nextjsAppRules }
5072
5190
  },
5073
- rules: Object.fromEntries(pasikaRuleIds.map((id) => [id, "error"]))
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 manifestBlock = {
5215
+ var repoManifestBlock = {
5098
5216
  files: ["package.json"],
5099
5217
  plugins: {
5100
5218
  json: { languages: { json: jsonPlugin.languages.json } },
5101
- pasika: { rules: { ...repoPackageJsonRules, ...nextPackageJsonRules, ...huskyRules } }
5219
+ pasika: { rules: { ...repoPackageJsonRules, ...huskyRules, ...vulykRules } }
5102
5220
  },
5103
5221
  language: "json/json",
5104
- rules: Object.fromEntries(
5105
- [...repoPackageJsonRuleIds, ...nextPackageJsonRuleIds, ...huskyRuleIds].map((id) => [id, "error"])
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 pasikaRepo = [manifestBlock, docsBlock];
5119
- var pasikaNext = [...pasikaRepo, nextAppBlock, tailwindGlobalsBlock, tailwindAnyCssBlock];
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
- pasikaNext,
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.9",
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.45",
54
+ "zirka": "0.0.46",
55
55
  "zod": "4.4.3"
56
56
  },
57
57
  "peerDependencies": {