pasika 0.5.17 → 0.6.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
@@ -88,8 +88,8 @@ The script refuses a hash no requirement has, a `ref` naming a rule that does no
88
88
 
89
89
  ```bash
90
90
  npm run build
91
- npm run dogfood -- ../some/repo # nextjsApp preset (default)
92
- npm run dogfood -- ../some/repo --preset=typescriptApp
91
+ npm run dogfood -- ../some/repo # pasikaNextjsApp preset (default)
92
+ npm run dogfood -- ../some/repo --preset=pasikaApp
93
93
  npm run dogfood -- ../some/repo --pasika-only # tally only pasika/* rules
94
94
  npm run dogfood -- ../some/repo --rule=css-entry-point --findings
95
95
  npm run dogfood -- ../some/repo --json # machine-readable report
@@ -115,18 +115,18 @@ const { eslintConfig } = styleguide({
115
115
  export default eslintConfig;
116
116
  ```
117
117
 
118
- For a plain TypeScript repository, enable `pasikaTypescriptApp` instead of `pasikaNextjsApp`; that preset covers the manifest, the zirka contract, and the docs only — the `src/**` source rules belong to the Next.js app preset. `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.
118
+ For a plain TypeScript repository, enable `pasikaApp` instead of `pasikaNextjsApp`; that preset covers the manifest, the zirka contract, and the docs only — the `src/**` source rules belong to the Next.js app preset. `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.
119
119
 
120
120
  ### Without Zirka
121
121
 
122
122
  ```ts
123
123
  // eslint.config.ts — plain TypeScript repository
124
- import { typescriptApp } from "pasika/eslint";
124
+ import { pasikaApp } from "pasika/eslint";
125
125
 
126
- export default typescriptApp;
126
+ export default pasikaApp;
127
127
  ```
128
128
 
129
- `typescriptApp` is the plain-TypeScript-repository preset — the package.json manifest, the zirka configuration contract, and the docs. It carries no `src/**` source block: source linting is the Next.js app's job. `nextjsApp` is the full framework preset: everything in `typescriptApp` plus the Next.js-stack manifest requirement, the `src/**` app source rules, and the Tailwind stylesheet blocks. The granular rule objects (`tailwindRules`, `repoPackageJsonRules`, `documentationRules`) stay exported for manual wiring.
129
+ `pasikaApp` is the plain-TypeScript-repository preset — the package.json manifest, the zirka configuration contract, and the docs. It carries no `src/**` source block: source linting is the Next.js app's job. `pasikaNextjsApp` is the full framework preset: everything in `pasikaApp` plus the Next.js-stack manifest requirement, the `src/**` app source rules, and the Tailwind stylesheet blocks. The granular rule objects (`tailwindRules`, `repoPackageJsonRules`, `documentationRules`) stay exported for manual wiring.
130
130
 
131
131
  The `src/**` blocks ship `@typescript-eslint/parser` themselves, so a standalone preset parses `.ts`/`.tsx` correctly on its own (`pasika` lists it as a runtime dependency).
132
132
 
@@ -231,7 +231,7 @@ Many of the TS/TSX rules call the TypeScript compiler API directly at lint time,
231
231
  ```bash
232
232
  npm run lint
233
233
  npm run typecheck
234
- npm run test
234
+ npm run test:unit
235
235
  npm run coverage
236
236
  npm run build
237
237
  npm run dogfood -- ../some/repo # requires a build; see Dogfooding above
@@ -119,6 +119,8 @@ declare const tailwindRules: {
119
119
  declare const repoPackageJsonRules: {
120
120
  "no-vulyk-dependency": _eslint_json.JSONRuleDefinition;
121
121
  "exact-version": _eslint_json.JSONRuleDefinition;
122
+ "lint-setup": _eslint_json.JSONRuleDefinition;
123
+ "vitest-coverage": _eslint_json.JSONRuleDefinition;
122
124
  };
123
125
  /** Package.json rules specific to a Next.js (or React) application. */
124
126
  declare const nextjsPackageJsonRules: {
@@ -147,6 +149,8 @@ declare const pasikaPlugin: {
147
149
  "nextjs-stack": _eslint_json.JSONRuleDefinition;
148
150
  "no-vulyk-dependency": _eslint_json.JSONRuleDefinition;
149
151
  "exact-version": _eslint_json.JSONRuleDefinition;
152
+ "lint-setup": _eslint_json.JSONRuleDefinition;
153
+ "vitest-coverage": _eslint_json.JSONRuleDefinition;
150
154
  "theme-reset": _eslint_css.CSSRuleDefinition;
151
155
  "root-variables": _eslint_css.CSSRuleDefinition;
152
156
  "apply-usage": _eslint_css.CSSRuleDefinition;
@@ -246,13 +250,16 @@ declare const allPasikaRuleIds: string[];
246
250
  * source block: source linting is the Next.js app's job.
247
251
  * Use this for a plain TypeScript repository that does not adopt the framework.
248
252
  */
249
- declare const typescriptApp: Linter.Config[];
253
+ declare const pasikaApp: Linter.Config[];
250
254
  /**
251
255
  * Next.js app preset: the full adopted-to-the-framework stack. Anything in
252
- * `typescriptApp` plus the framework-only blocks — the Next.js-stack manifest
256
+ * `pasikaApp` plus the framework-only blocks — the Next.js-stack manifest
253
257
  * requirement, the `src/**` app source rules, and the Tailwind stylesheet
254
- * rules. `typescriptApp` is a strict subset of `nextjsApp`.
258
+ * rules. `pasikaApp` is a strict subset of `pasikaNextjsApp`. Consuming this
259
+ * preset (spreading or iterating it) runs the TypeScript alignment diagnostic
260
+ * first, so a mismatched compiler major fails config load with an actionable
261
+ * error instead of crashing every type-aware rule later.
255
262
  */
256
- declare const nextjsApp: Linter.Config[];
263
+ declare const pasikaNextjsApp: Linter.Config[];
257
264
 
258
- export { allPasikaRuleIds, documentationRules, huskyRules, nextjsApp, nextjsPackageJsonRules, pasikaPlugin, repoPackageJsonRules, tailwindRules, typescriptApp, vulykRules };
265
+ export { allPasikaRuleIds, documentationRules, huskyRules, nextjsPackageJsonRules, pasikaApp, pasikaNextjsApp, pasikaPlugin, repoPackageJsonRules, tailwindRules, vulykRules };
@@ -4,6 +4,53 @@ import jsonPlugin from "@eslint/json";
4
4
  import markdown from "@eslint/markdown";
5
5
  import tsParser from "@typescript-eslint/parser";
6
6
 
7
+ // eslint/ts-alignment.ts
8
+ import { readFileSync } from "fs";
9
+ import { createRequire } from "module";
10
+ import { join } from "path";
11
+ var ownRequire = createRequire(import.meta.url);
12
+ var majorOf = (version) => Number(version.split(".")[0]);
13
+ function versionOf(packagePath) {
14
+ try {
15
+ const parsed = JSON.parse(readFileSync(packagePath, "utf8"));
16
+ if (typeof parsed !== "object" || parsed === null || !("version" in parsed)) return null;
17
+ return typeof parsed.version === "string" ? parsed.version : null;
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+ function resolveTypescriptAlignment() {
23
+ let bundled = null;
24
+ try {
25
+ bundled = versionOf(ownRequire.resolve("typescript/package.json"));
26
+ } catch {
27
+ bundled = null;
28
+ }
29
+ let resolved = null;
30
+ try {
31
+ const consumerRequire = createRequire(join(process.cwd(), "package.json"));
32
+ resolved = versionOf(consumerRequire.resolve("typescript/package.json"));
33
+ } catch {
34
+ resolved = null;
35
+ }
36
+ return { bundled, resolved };
37
+ }
38
+ function alignmentError(bundled, resolved) {
39
+ if (bundled === null || resolved === null) return null;
40
+ const bundledMajor = majorOf(bundled);
41
+ const resolvedMajor = majorOf(resolved);
42
+ if (bundledMajor === resolvedMajor) return null;
43
+ const direction = resolvedMajor < bundledMajor ? `Upgrade your typescript to ^${String(bundledMajor)} (pinned exact) so a single hoisted copy serves the whole pipeline.` : `Your repository is ahead of the compiler pasika ships. Pin typescript to ^${String(bundledMajor)} for now and upgrade pasika when it bundles the newer major.`;
44
+ return new Error(
45
+ `[pasika] TypeScript major mismatch: the pasikaNextjsApp preset parses src/** with its bundled @typescript-eslint/parser, which runs on typescript ${bundled}; your repository resolves typescript ${resolved} for the type-aware rules. Two TypeScript majors mean the types the parser builds use a different TypeFlags layout than the rules expect, so every type-aware rule crashes (e.g. "undefined is not iterable" in ts-api-utils). Align them on ${String(bundledMajor)}, then lint will run again. ${direction}`
46
+ );
47
+ }
48
+ function assertTypescriptAlignment() {
49
+ const { bundled, resolved } = resolveTypescriptAlignment();
50
+ const error = alignmentError(bundled, resolved);
51
+ if (error) throw error;
52
+ }
53
+
7
54
  // eslint/rules/filename-case.ts
8
55
  import path2 from "path";
9
56
 
@@ -473,7 +520,7 @@ var noArbitraryTailwindRule = {
473
520
  };
474
521
 
475
522
  // eslint/rules/unknown-utility.ts
476
- import { readdirSync, readFileSync, statSync } from "fs";
523
+ import { readdirSync, readFileSync as readFileSync2, statSync } from "fs";
477
524
  import path5 from "path";
478
525
  var PREFIX_NAMESPACES = {
479
526
  bg: ["color"],
@@ -752,7 +799,7 @@ function readInventory(files) {
752
799
  for (const file of files) {
753
800
  let css2;
754
801
  try {
755
- css2 = readFileSync(file, "utf8");
802
+ css2 = readFileSync2(file, "utf8");
756
803
  } catch {
757
804
  continue;
758
805
  }
@@ -922,7 +969,7 @@ import path7 from "path";
922
969
 
923
970
  // eslint/project/parse-module.ts
924
971
  import path6 from "path";
925
- import { readFileSync as readFileSync2 } from "fs";
972
+ import { readFileSync as readFileSync3 } from "fs";
926
973
  import ts2 from "typescript";
927
974
  var isPascalCase3 = (name) => /^[A-Z][A-Za-z0-9]*$/.test(name);
928
975
  var isHookName = (name) => /^use[A-Z]/.test(name);
@@ -959,7 +1006,7 @@ function classifyValue(name, initializer, isTsx) {
959
1006
  return "constant";
960
1007
  }
961
1008
  function parseModule(file) {
962
- const text = readFileSync2(file, "utf8");
1009
+ const text = readFileSync3(file, "utf8");
963
1010
  const isTsx = file.endsWith(".tsx") || file.endsWith(".jsx");
964
1011
  const sourceFile = ts2.createSourceFile(file, text, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TSX);
965
1012
  const imports = [];
@@ -3207,7 +3254,7 @@ var typeExtractionRule = {
3207
3254
 
3208
3255
  // eslint/rules/locale-placement.ts
3209
3256
  import path32 from "path";
3210
- import { readFileSync as readFileSync3 } from "fs";
3257
+ import { readFileSync as readFileSync4 } from "fs";
3211
3258
  import ts5 from "typescript";
3212
3259
  var LOCALE_ACCESS = /\blocales\.(?<key>[A-Za-z_$][\w$]*)/g;
3213
3260
  var camelCase = (name) => name.replace(/-[a-z]/g, (match) => match.slice(1).toUpperCase());
@@ -3264,7 +3311,7 @@ var localePlacementRule = {
3264
3311
  return candidateSegments.length === 2 && candidateSegments[0] === "locales" && candidateSegments[1]?.startsWith("index.");
3265
3312
  });
3266
3313
  if (!localesFile || file !== localesFile) return {};
3267
- const placement = localePlacement(readFileSync3(localesFile, "utf8"));
3314
+ const placement = localePlacement(readFileSync4(localesFile, "utf8"));
3268
3315
  if (!placement) return {};
3269
3316
  const keyReaders = /* @__PURE__ */ new Map();
3270
3317
  const keyFeatures = /* @__PURE__ */ new Map();
@@ -3276,7 +3323,7 @@ var localePlacementRule = {
3276
3323
  );
3277
3324
  if (!importsLocales) continue;
3278
3325
  const candidateSegments = segmentsOf(candidateFile, sourceRoot);
3279
- for (const match of readFileSync3(candidateFile, "utf8").matchAll(LOCALE_ACCESS)) {
3326
+ for (const match of readFileSync4(candidateFile, "utf8").matchAll(LOCALE_ACCESS)) {
3280
3327
  const key = match.groups?.key;
3281
3328
  if (key === void 0) continue;
3282
3329
  const readers = keyReaders.get(key) ?? /* @__PURE__ */ new Set();
@@ -3613,7 +3660,7 @@ var localeKeyShapeRule = {
3613
3660
 
3614
3661
  // eslint/rules/shared-style-dedup.ts
3615
3662
  import path35 from "path";
3616
- import { readFileSync as readFileSync4, statSync as statSync3 } from "fs";
3663
+ import { readFileSync as readFileSync5, statSync as statSync3 } from "fs";
3617
3664
  var CLASS_NAME = /className="(?<classes>[^"]+)"/g;
3618
3665
  var comboCache;
3619
3666
  function combosFor(index) {
@@ -3631,7 +3678,7 @@ function combosFor(index) {
3631
3678
  }
3632
3679
  const combos = /* @__PURE__ */ new Map();
3633
3680
  for (const file of files) {
3634
- const text = readFileSync4(file, "utf8");
3681
+ const text = readFileSync5(file, "utf8");
3635
3682
  for (const match of text.matchAll(CLASS_NAME)) {
3636
3683
  const classes = (match.groups?.classes ?? "").split(/\s+/).filter(Boolean);
3637
3684
  if (classes.length < 2) continue;
@@ -4541,7 +4588,7 @@ var noTemplatePromptRule = {
4541
4588
  import path41 from "path";
4542
4589
 
4543
4590
  // eslint/rules/documentation/project-index.ts
4544
- import { readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
4591
+ import { readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
4545
4592
  import path40 from "path";
4546
4593
  var KIND_BY_SUFFIX = [
4547
4594
  ["-rule.md", "rule"],
@@ -4559,7 +4606,7 @@ function listMarkdownFiles(dir) {
4559
4606
  });
4560
4607
  }
4561
4608
  function extractTitle(filePath) {
4562
- const content = readFileSync5(filePath, "utf8");
4609
+ const content = readFileSync6(filePath, "utf8");
4563
4610
  const match = /^# (?<title>.+)$/m.exec(content);
4564
4611
  return match?.groups?.title?.trim() ?? "";
4565
4612
  }
@@ -4842,10 +4889,10 @@ var noNestedHowToRule = {
4842
4889
  };
4843
4890
 
4844
4891
  // eslint/rules/documentation/glossary-term-linking.ts
4845
- import { readFileSync as readFileSync6 } from "fs";
4892
+ import { readFileSync as readFileSync7 } from "fs";
4846
4893
  import path42 from "path";
4847
4894
  function extractGlossaryTerms(filePath) {
4848
- const content = readFileSync6(filePath, "utf8");
4895
+ const content = readFileSync7(filePath, "utf8");
4849
4896
  const terms = [];
4850
4897
  const headingPattern = /^## (?<term>.+)$/gm;
4851
4898
  let match;
@@ -5444,7 +5491,7 @@ import { statSync as statSync6 } from "fs";
5444
5491
  import path46 from "path";
5445
5492
 
5446
5493
  // eslint/rules/tailwind/source-files.ts
5447
- import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
5494
+ import { readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
5448
5495
  import path44 from "path";
5449
5496
  var CSS_EXTENSIONS = [".css"];
5450
5497
  var MODULE_EXTENSIONS3 = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
@@ -5475,7 +5522,7 @@ function cachedTextReader() {
5475
5522
  let text = texts.get(file);
5476
5523
  if (text === void 0) {
5477
5524
  try {
5478
- text = readFileSync7(file, "utf8");
5525
+ text = readFileSync8(file, "utf8");
5479
5526
  } catch {
5480
5527
  text = "";
5481
5528
  }
@@ -5739,10 +5786,108 @@ var exactVersionRule = {
5739
5786
  }
5740
5787
  };
5741
5788
 
5742
- // eslint/rules/package-json/no-vulyk-dependency.ts
5789
+ // eslint/rules/package-json/lint-setup.ts
5743
5790
  function memberName2(member) {
5744
5791
  return member.name.type === "String" ? member.name.value : member.name.name;
5745
5792
  }
5793
+ function memberValue2(member) {
5794
+ return member?.value.type === "String" ? member.value.value : void 0;
5795
+ }
5796
+ function stringValues(value) {
5797
+ if (value.type === "String") return [value.value];
5798
+ if (value.type !== "Array") return [];
5799
+ return value.elements.flatMap((element) => element.value.type === "String" ? [element.value.value] : []);
5800
+ }
5801
+ var SOURCE_GLOB_PATTERN = /(?:^|[^a-z])(?:[cm]?[jt]sx?)(?:[^a-z]|$)/i;
5802
+ var DIRECT_ESLINT_PATTERN = /(?:^|&&|\|\||;)\s*(?:npx\s+)?eslint(?:\s|$)/;
5803
+ var REPOSITORY_ARGUMENT_PATTERN = /(?:^|\s)\.(?=\s|$)/;
5804
+ var PRETTIER_PATTERN = /\bprettier\b/;
5805
+ var CHECK_FLAG_PATTERN = /(?:^|\s)(?:--check|-c)(?:\s|$)/;
5806
+ function runsEslintDirectly(command) {
5807
+ return DIRECT_ESLINT_PATTERN.test(command);
5808
+ }
5809
+ function runsPrettier(command) {
5810
+ return PRETTIER_PATTERN.test(command);
5811
+ }
5812
+ function runsNamedScript(command, name) {
5813
+ return command.includes(`npm run ${name}`);
5814
+ }
5815
+ var lintSetupRule = {
5816
+ meta: {
5817
+ schema: [],
5818
+ type: "problem",
5819
+ docs: {
5820
+ description: "Require full-repository lint/format scripts and argument-free staged-file scripts that lint-staged runs by name."
5821
+ }
5822
+ },
5823
+ create(context) {
5824
+ return {
5825
+ Document(node) {
5826
+ const root = node.body;
5827
+ if (root.type !== "Object") return;
5828
+ const scripts = root.members.find((member) => memberName2(member) === "scripts");
5829
+ const scriptMembers = scripts?.value.type === "Object" ? scripts.value.members : [];
5830
+ const findScript = (name) => scriptMembers.find((member) => memberName2(member) === name);
5831
+ const lint = findScript("lint");
5832
+ const lintCommand = memberValue2(lint);
5833
+ if (lintCommand === void 0 || !runsEslintDirectly(lintCommand) || !REPOSITORY_ARGUMENT_PATTERN.test(lintCommand)) {
5834
+ context.report({
5835
+ node: lint ?? node,
5836
+ message: 'package.json must declare a "lint" script that runs ESLint across the repository (e.g. "eslint .").'
5837
+ });
5838
+ }
5839
+ const format = findScript("format");
5840
+ const formatCommand = memberValue2(format);
5841
+ if (formatCommand === void 0 || !runsPrettier(formatCommand) || !CHECK_FLAG_PATTERN.test(formatCommand) || !REPOSITORY_ARGUMENT_PATTERN.test(formatCommand)) {
5842
+ context.report({
5843
+ node: format ?? node,
5844
+ message: 'package.json must declare a "format" script that runs prettier --check across the repository.'
5845
+ });
5846
+ }
5847
+ const lintStaged = findScript("lint:staged");
5848
+ const lintStagedCommand = memberValue2(lintStaged);
5849
+ if (lintStagedCommand === void 0 || !runsEslintDirectly(lintStagedCommand) || REPOSITORY_ARGUMENT_PATTERN.test(lintStagedCommand)) {
5850
+ context.report({
5851
+ node: lintStaged ?? node,
5852
+ message: 'package.json must declare a "lint:staged" script that runs ESLint with no repository-wide argument (e.g. "eslint --fix").'
5853
+ });
5854
+ }
5855
+ const formatStaged = findScript("format:staged");
5856
+ const formatStagedCommand = memberValue2(formatStaged);
5857
+ if (formatStagedCommand === void 0 || !runsPrettier(formatStagedCommand) || REPOSITORY_ARGUMENT_PATTERN.test(formatStagedCommand)) {
5858
+ context.report({
5859
+ node: formatStaged ?? node,
5860
+ message: 'package.json must declare a "format:staged" script that runs prettier with no repository-wide argument (e.g. "prettier --write").'
5861
+ });
5862
+ }
5863
+ const lintStagedConfig = root.members.find((member) => memberName2(member) === "lint-staged");
5864
+ const runsStagedScript = (name) => lintStagedConfig?.value.type === "Object" && lintStagedConfig.value.members.some(
5865
+ (member) => stringValues(member.value).some((command) => runsNamedScript(command, name))
5866
+ );
5867
+ const hasStagedLintEntry = lintStagedConfig?.value.type === "Object" && lintStagedConfig.value.members.some(
5868
+ (member) => SOURCE_GLOB_PATTERN.test(memberName2(member)) && stringValues(member.value).some((command) => runsNamedScript(command, "lint:staged"))
5869
+ );
5870
+ if (!hasStagedLintEntry) {
5871
+ context.report({
5872
+ node: lintStagedConfig ?? node,
5873
+ message: 'package.json lint-staged must run "npm run lint:staged --" for staged JavaScript or TypeScript files.'
5874
+ });
5875
+ }
5876
+ if (!runsStagedScript("format:staged")) {
5877
+ context.report({
5878
+ node: lintStagedConfig ?? node,
5879
+ message: 'package.json lint-staged must run "npm run format:staged --" for staged files ESLint does not already format.'
5880
+ });
5881
+ }
5882
+ }
5883
+ };
5884
+ }
5885
+ };
5886
+
5887
+ // eslint/rules/package-json/no-vulyk-dependency.ts
5888
+ function memberName3(member) {
5889
+ return member.name.type === "String" ? member.name.value : member.name.name;
5890
+ }
5746
5891
  var noVulykDependencyRule = {
5747
5892
  meta: {
5748
5893
  schema: [],
@@ -5757,9 +5902,9 @@ var noVulykDependencyRule = {
5757
5902
  const root = node.body;
5758
5903
  if (root.type !== "Object") return;
5759
5904
  for (const key of ["dependencies", "devDependencies"]) {
5760
- const section = root.members.find((member) => memberName2(member) === key);
5905
+ const section = root.members.find((member) => memberName3(member) === key);
5761
5906
  if (section?.value.type !== "Object") continue;
5762
- const vulyk = section.value.members.find((member) => memberName2(member) === "vulyk");
5907
+ const vulyk = section.value.members.find((member) => memberName3(member) === "vulyk");
5763
5908
  if (vulyk) {
5764
5909
  context.report({
5765
5910
  node: vulyk,
@@ -5789,15 +5934,19 @@ var NEXTJS_STACK_DEV_DEPENDENCIES = [
5789
5934
  "prettier",
5790
5935
  "husky",
5791
5936
  "lint-staged",
5792
- "zirka"
5937
+ "zirka",
5938
+ "jsdom",
5939
+ "@vitejs/plugin-react",
5940
+ "@testing-library/react",
5941
+ "@testing-library/dom"
5793
5942
  ];
5794
- function memberName3(member) {
5943
+ function memberName4(member) {
5795
5944
  return member.name.type === "String" ? member.name.value : member.name.name;
5796
5945
  }
5797
5946
  function sectionPackages(root, section) {
5798
- const member = root.type === "Object" ? root.members.find((entry) => memberName3(entry) === section) : void 0;
5947
+ const member = root.type === "Object" ? root.members.find((entry) => memberName4(entry) === section) : void 0;
5799
5948
  if (member?.value.type !== "Object") return /* @__PURE__ */ new Set();
5800
- return new Set(member.value.members.map(memberName3));
5949
+ return new Set(member.value.members.map(memberName4));
5801
5950
  }
5802
5951
  var nextjsStackRule = {
5803
5952
  meta: {
@@ -5833,19 +5982,151 @@ var nextjsStackRule = {
5833
5982
  }
5834
5983
  };
5835
5984
 
5985
+ // eslint/rules/package-json/vitest-coverage.ts
5986
+ import { existsSync as existsSync2, readFileSync as readFileSync9 } from "fs";
5987
+ import path47 from "path";
5988
+ function memberName5(member) {
5989
+ return member.name.type === "String" ? member.name.value : member.name.name;
5990
+ }
5991
+ function memberValue3(member) {
5992
+ return member?.value.type === "String" ? member.value.value : void 0;
5993
+ }
5994
+ function stringValues2(value) {
5995
+ if (value.type === "String") return [value.value];
5996
+ if (value.type !== "Array") return [];
5997
+ return value.elements.flatMap((element) => element.value.type === "String" ? [element.value.value] : []);
5998
+ }
5999
+ var VITEST_CONFIG_NAMES = [
6000
+ "vitest.config.ts",
6001
+ "vitest.config.mts",
6002
+ "vitest.config.cts",
6003
+ "vitest.config.js",
6004
+ "vitest.config.mjs",
6005
+ "vitest.config.cjs"
6006
+ ];
6007
+ var THRESHOLD_METRICS = ["lines", "functions", "branches", "statements"];
6008
+ var SOURCE_GLOB_PATTERN2 = /(?:^|[^a-z])(?:[cm]?[jt]sx?)(?:[^a-z]|$)/i;
6009
+ var COVERAGE_FLAG_PATTERN = /(?:^|\s)--coverage(?:[=\s]|$)/;
6010
+ var RELATED_PATTERN = /\brelated\b/;
6011
+ var AUTO_UPDATE_PATTERN = /autoUpdate\s*:\s*true/;
6012
+ var CHANGED_FLAG_PATTERN = /--coverage\.changed\b/;
6013
+ var PER_FILE_FLAG_PATTERN = /--coverage\.thresholds\.perFile\b/;
6014
+ var AUTO_UPDATE_DISABLED_FLAG_PATTERN = /--coverage\.thresholds\.autoUpdate=false\b/;
6015
+ function hasEightyOrAboveFlag(command, metric) {
6016
+ return new RegExp(`--coverage\\.thresholds\\.${metric}=(?:8\\d|9\\d|100)\\b`).test(command);
6017
+ }
6018
+ var vitestCoverageRule = {
6019
+ meta: {
6020
+ schema: [],
6021
+ type: "problem",
6022
+ docs: {
6023
+ description: "Require Vitest unit-test scripts, the V8 provider, a rising coverage threshold, and an 80% floor on staged files via lint-staged."
6024
+ }
6025
+ },
6026
+ create(context) {
6027
+ return {
6028
+ Document(node) {
6029
+ const root = node.body;
6030
+ if (root.type !== "Object") return;
6031
+ const devDependencies = root.members.find((member) => memberName5(member) === "devDependencies");
6032
+ const devDependencyNames = new Set(
6033
+ devDependencies?.value.type === "Object" ? devDependencies.value.members.map(memberName5) : []
6034
+ );
6035
+ if (!devDependencyNames.has("vitest")) {
6036
+ context.report({ node, message: "vitest must be listed in package.json as a devDependency." });
6037
+ }
6038
+ if (!devDependencyNames.has("@vitest/coverage-v8")) {
6039
+ context.report({
6040
+ node,
6041
+ message: "@vitest/coverage-v8 must be listed in package.json as a devDependency."
6042
+ });
6043
+ }
6044
+ const scripts = root.members.find((member) => memberName5(member) === "scripts");
6045
+ const scriptMembers = scripts?.value.type === "Object" ? scripts.value.members : [];
6046
+ const unit = scriptMembers.find((member) => memberName5(member) === "test:unit");
6047
+ const unitCommand = memberValue3(unit);
6048
+ if (unitCommand === void 0 || !/\bvitest\b/.test(unitCommand) || COVERAGE_FLAG_PATTERN.test(unitCommand)) {
6049
+ context.report({
6050
+ node: unit ?? node,
6051
+ message: 'package.json must declare a "test:unit" script that runs Vitest without coverage.'
6052
+ });
6053
+ }
6054
+ const coverage = scriptMembers.find((member) => memberName5(member) === "test:unit:coverage");
6055
+ const coverageCommand = memberValue3(coverage);
6056
+ if (coverageCommand === void 0 || !/\bvitest\b/.test(coverageCommand) || !COVERAGE_FLAG_PATTERN.test(coverageCommand)) {
6057
+ context.report({
6058
+ node: coverage ?? node,
6059
+ message: 'package.json must declare a "test:unit:coverage" script that runs Vitest with coverage.'
6060
+ });
6061
+ }
6062
+ const configName = VITEST_CONFIG_NAMES.find((name) => existsSync2(path47.join(context.cwd, name)));
6063
+ if (!configName) {
6064
+ context.report({
6065
+ node,
6066
+ message: "No vitest config found. Create one with a coverage threshold above zero for lines, functions, branches, and statements."
6067
+ });
6068
+ return;
6069
+ }
6070
+ const content = readFileSync9(path47.join(context.cwd, configName), "utf8");
6071
+ for (const metric of THRESHOLD_METRICS) {
6072
+ if (!new RegExp(`\\b${metric}\\s*:\\s*[1-9]\\d*`).test(content)) {
6073
+ context.report({ node, message: `${configName} must set a coverage threshold above zero for ${metric}.` });
6074
+ }
6075
+ }
6076
+ if (!AUTO_UPDATE_PATTERN.test(content)) {
6077
+ context.report({
6078
+ node,
6079
+ message: `${configName} must set coverage.thresholds.autoUpdate to true.`
6080
+ });
6081
+ }
6082
+ const changedCoverage = scriptMembers.find((member) => memberName5(member) === "test:unit:coverage:staged");
6083
+ const changedCoverageCommand = memberValue3(changedCoverage);
6084
+ if (changedCoverageCommand === void 0 || !/\bvitest\b/.test(changedCoverageCommand) || !RELATED_PATTERN.test(changedCoverageCommand) || !COVERAGE_FLAG_PATTERN.test(changedCoverageCommand)) {
6085
+ context.report({
6086
+ node: changedCoverage ?? node,
6087
+ message: 'package.json must declare a "test:unit:coverage:staged" script that runs vitest related with coverage.'
6088
+ });
6089
+ }
6090
+ const lintStaged = root.members.find((member) => memberName5(member) === "lint-staged");
6091
+ const hasStagedChangedCoverage = lintStaged?.value.type === "Object" && lintStaged.value.members.some(
6092
+ (member) => SOURCE_GLOB_PATTERN2.test(memberName5(member)) && stringValues2(member.value).some((command) => command.includes("npm run test:unit:coverage:staged"))
6093
+ );
6094
+ if (!hasStagedChangedCoverage) {
6095
+ context.report({
6096
+ node: lintStaged ?? node,
6097
+ message: 'package.json lint-staged must run "npm run test:unit:coverage:staged" for staged JavaScript or TypeScript files.'
6098
+ });
6099
+ }
6100
+ const changedCoverageCommandText = changedCoverageCommand ?? "";
6101
+ const hasEightyFloor = THRESHOLD_METRICS.every(
6102
+ (metric) => hasEightyOrAboveFlag(changedCoverageCommandText, metric)
6103
+ );
6104
+ if (!CHANGED_FLAG_PATTERN.test(changedCoverageCommandText) || !PER_FILE_FLAG_PATTERN.test(changedCoverageCommandText) || !hasEightyFloor || !AUTO_UPDATE_DISABLED_FLAG_PATTERN.test(changedCoverageCommandText)) {
6105
+ context.report({
6106
+ node: changedCoverage ?? node,
6107
+ message: 'package.json "test:unit:coverage:staged" script must pass --coverage.changed, a --coverage.thresholds.perFile of at least 80 for lines, functions, branches, and statements, and --coverage.thresholds.autoUpdate=false.'
6108
+ });
6109
+ }
6110
+ }
6111
+ };
6112
+ }
6113
+ };
6114
+
5836
6115
  // eslint/rules/package-json/index.ts
5837
6116
  var repoPackageJsonRules = {
5838
6117
  "no-vulyk-dependency": noVulykDependencyRule,
5839
- "exact-version": exactVersionRule
6118
+ "exact-version": exactVersionRule,
6119
+ "lint-setup": lintSetupRule,
6120
+ "vitest-coverage": vitestCoverageRule
5840
6121
  };
5841
6122
  var nextjsPackageJsonRules = {
5842
6123
  "nextjs-stack": nextjsStackRule
5843
6124
  };
5844
6125
 
5845
6126
  // eslint/rules/husky/husky-hook.ts
5846
- import { existsSync as existsSync2, readFileSync as readFileSync8 } from "fs";
5847
- import path47 from "path";
5848
- function memberName4(member) {
6127
+ import { existsSync as existsSync3, readFileSync as readFileSync10 } from "fs";
6128
+ import path48 from "path";
6129
+ function memberName6(member) {
5849
6130
  return member.name.type === "String" ? member.name.value : member.name.name;
5850
6131
  }
5851
6132
  var huskyHookRule = {
@@ -5853,7 +6134,7 @@ var huskyHookRule = {
5853
6134
  schema: [],
5854
6135
  type: "problem",
5855
6136
  docs: {
5856
- description: "Require a pre-commit hook that runs lint-staged, typecheck, and the libyear drift check."
6137
+ description: "Require a pre-commit hook that runs lint-staged and the libyear drift check directly, and the typecheck and suppression-file ratchet through named package.json scripts."
5857
6138
  }
5858
6139
  },
5859
6140
  create(context) {
@@ -5861,29 +6142,39 @@ var huskyHookRule = {
5861
6142
  Document(node) {
5862
6143
  const root = node.body;
5863
6144
  if (root.type !== "Object") return;
5864
- const scriptName = context.filename;
5865
- if (!scriptName.endsWith("package.json")) return;
5866
- const hookPath = path47.join(context.cwd, ".husky", "pre-commit");
5867
- if (!existsSync2(hookPath)) {
6145
+ if (!context.filename.endsWith("package.json")) return;
6146
+ const hookPath = path48.join(context.cwd, ".husky", "pre-commit");
6147
+ if (!existsSync3(hookPath)) {
5868
6148
  context.report({
5869
6149
  node,
5870
6150
  message: "No .husky/pre-commit hook found. Configure husky to run checks before commits."
5871
6151
  });
5872
6152
  return;
5873
6153
  }
5874
- const content = readFileSync8(hookPath, "utf8");
6154
+ const content = readFileSync10(hookPath, "utf8");
6155
+ const scripts = root.members.find((member) => memberName6(member) === "scripts");
6156
+ const scriptNames = new Set(scripts?.value.type === "Object" ? scripts.value.members.map(memberName6) : []);
6157
+ const requireNamedScript = (name) => {
6158
+ if (!scriptNames.has(name)) {
6159
+ context.report({ node, message: `package.json must declare a "${name}" script.` });
6160
+ }
6161
+ if (!content.includes(`npm run ${name}`)) {
6162
+ context.report({ node, message: `.husky/pre-commit must run npm run ${name}.` });
6163
+ }
6164
+ };
5875
6165
  if (!content.includes("lint-staged")) {
5876
6166
  context.report({ node, message: ".husky/pre-commit must run lint-staged." });
5877
6167
  }
5878
- if (!content.includes("typecheck")) {
5879
- context.report({ node, message: ".husky/pre-commit must run npm run typecheck." });
5880
- }
6168
+ requireNamedScript("typecheck");
5881
6169
  if (!content.includes("libyear --limit-major-individual=1")) {
5882
6170
  context.report({ node, message: ".husky/pre-commit must run npx libyear --limit-major-individual=1." });
5883
6171
  }
5884
- const scripts = root.members.find((member) => memberName4(member) === "scripts");
6172
+ const suppressionsPath = path48.join(context.cwd, "eslint-suppressions.json");
6173
+ if (existsSync3(suppressionsPath)) {
6174
+ requireNamedScript("lint:prune");
6175
+ }
5885
6176
  if (scripts?.value.type === "Object") {
5886
- const prepare = scripts.value.members.find((member) => memberName4(member) === "prepare");
6177
+ const prepare = scripts.value.members.find((member) => memberName6(member) === "prepare");
5887
6178
  if (prepare?.value.type === "String" && !prepare.value.value.includes("husky")) {
5888
6179
  context.report({
5889
6180
  node: prepare,
@@ -5902,8 +6193,8 @@ var huskyRules = {
5902
6193
  };
5903
6194
 
5904
6195
  // eslint/rules/vulyk/vulyk-docs.ts
5905
- import { existsSync as existsSync3, readFileSync as readFileSync9 } from "fs";
5906
- import path48 from "path";
6196
+ import { existsSync as existsSync4, readFileSync as readFileSync11 } from "fs";
6197
+ import path49 from "path";
5907
6198
  var PASIKA_REPO = "Bredansky/pasika";
5908
6199
  var BASE_REQUIRED_DOCS = [
5909
6200
  { name: "documentation-guide", path: "docs/documentation-guide" },
@@ -5914,13 +6205,13 @@ var NEXTJS_REQUIRED_DOCS = [
5914
6205
  { name: "next-codebase-guide", path: "docs/next-codebase-guide" },
5915
6206
  { name: "next-tailwind-guide", path: "docs/next-tailwind-guide" }
5916
6207
  ];
5917
- function memberName5(member) {
6208
+ function memberName7(member) {
5918
6209
  return member.name.type === "String" ? member.name.value : member.name.name;
5919
6210
  }
5920
6211
  function hasDependency(root, name) {
5921
- const section = root.members.find((member) => memberName5(member) === "dependencies");
6212
+ const section = root.members.find((member) => memberName7(member) === "dependencies");
5922
6213
  if (section?.value.type !== "Object") return false;
5923
- return section.value.members.some((member) => memberName5(member) === name);
6214
+ return section.value.members.some((member) => memberName7(member) === name);
5924
6215
  }
5925
6216
  var vulykDocsRule = {
5926
6217
  meta: {
@@ -5936,16 +6227,16 @@ var vulykDocsRule = {
5936
6227
  if (!context.filename.endsWith("package.json")) return;
5937
6228
  const root = node.body;
5938
6229
  if (root.type !== "Object") return;
5939
- const projectRoot = path48.dirname(path48.resolve(context.filename));
5940
- const configPath = path48.join(projectRoot, "vulyk.config.ts");
5941
- if (!existsSync3(configPath)) {
6230
+ const projectRoot = path49.dirname(path49.resolve(context.filename));
6231
+ const configPath = path49.join(projectRoot, "vulyk.config.ts");
6232
+ if (!existsSync4(configPath)) {
5942
6233
  context.report({
5943
6234
  node,
5944
6235
  message: "No vulyk.config.ts found. Run npx vulyk@latest init to create one that tracks the framework's docs."
5945
6236
  });
5946
6237
  return;
5947
6238
  }
5948
- const config = readFileSync9(configPath, "utf8");
6239
+ const config = readFileSync11(configPath, "utf8");
5949
6240
  if (!config.includes(PASIKA_REPO)) {
5950
6241
  context.report({
5951
6242
  node,
@@ -5962,8 +6253,8 @@ var vulykDocsRule = {
5962
6253
  });
5963
6254
  }
5964
6255
  }
5965
- const agentsPath = path48.join(projectRoot, "AGENTS.md");
5966
- if (!existsSync3(agentsPath)) {
6256
+ const agentsPath = path49.join(projectRoot, "AGENTS.md");
6257
+ if (!existsSync4(agentsPath)) {
5967
6258
  context.report({
5968
6259
  node,
5969
6260
  message: "No AGENTS.md found. Run npx vulyk@latest agents to generate the agent file that routes to the tracked docs."
@@ -5980,7 +6271,7 @@ var vulykRules = {
5980
6271
  };
5981
6272
 
5982
6273
  // eslint/index.ts
5983
- var nextjsAppRules = {
6274
+ var pasikaNextjsAppRules = {
5984
6275
  // Framework-agnostic TypeScript rules.
5985
6276
  "filename-case": filenameCaseRule,
5986
6277
  "import-boundaries": importBoundariesRule,
@@ -6030,7 +6321,7 @@ var pasikaPlugin = {
6030
6321
  rules: {
6031
6322
  // The shared plugin must register every rule the preset blocks reference,
6032
6323
  // so all rule sets live here.
6033
- ...nextjsAppRules,
6324
+ ...pasikaNextjsAppRules,
6034
6325
  ...documentationRules,
6035
6326
  ...tailwindRules,
6036
6327
  ...repoPackageJsonRules,
@@ -6043,7 +6334,7 @@ var jsonLanguage = { languages: { json: jsonPlugin.languages.json } };
6043
6334
  function ruleIds(rules2) {
6044
6335
  return Object.keys(rules2).map((name) => `pasika/${name}`);
6045
6336
  }
6046
- var nextjsAppRuleIds = ruleIds(nextjsAppRules);
6337
+ var pasikaNextjsAppRuleIds = ruleIds(pasikaNextjsAppRules);
6047
6338
  var documentationRuleIds = ruleIds(documentationRules);
6048
6339
  var tailwindRuleIds = ruleIds(tailwindRules);
6049
6340
  var repoPackageJsonRuleIds = ruleIds(repoPackageJsonRules);
@@ -6051,7 +6342,7 @@ var nextjsPackageJsonRuleIds = ruleIds(nextjsPackageJsonRules);
6051
6342
  var huskyRuleIds = ruleIds(huskyRules);
6052
6343
  var vulykRuleIds = ruleIds(vulykRules);
6053
6344
  var allPasikaRuleIds = [
6054
- ...nextjsAppRuleIds,
6345
+ ...pasikaNextjsAppRuleIds,
6055
6346
  ...documentationRuleIds,
6056
6347
  ...tailwindRuleIds,
6057
6348
  ...repoPackageJsonRuleIds,
@@ -6059,7 +6350,7 @@ var allPasikaRuleIds = [
6059
6350
  ...huskyRuleIds,
6060
6351
  ...vulykRuleIds
6061
6352
  ];
6062
- var typescriptAppLanguageOptions = {
6353
+ var pasikaAppLanguageOptions = {
6063
6354
  parser: tsParser,
6064
6355
  parserOptions: {
6065
6356
  ecmaVersion: "latest",
@@ -6067,13 +6358,13 @@ var typescriptAppLanguageOptions = {
6067
6358
  ecmaFeatures: { jsx: true }
6068
6359
  }
6069
6360
  };
6070
- var nextjsAppConfig = {
6361
+ var pasikaNextjsAppConfig = {
6071
6362
  files: ["src/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}"],
6072
- languageOptions: typescriptAppLanguageOptions,
6363
+ languageOptions: pasikaAppLanguageOptions,
6073
6364
  plugins: {
6074
6365
  pasika: pasikaPlugin
6075
6366
  },
6076
- rules: Object.fromEntries(nextjsAppRuleIds.map((id) => [id, "error"]))
6367
+ rules: Object.fromEntries(pasikaNextjsAppRuleIds.map((id) => [id, "error"]))
6077
6368
  };
6078
6369
  var tailwindStructureRules = {
6079
6370
  files: ["src/**/globals.css"],
@@ -6097,7 +6388,7 @@ var tailwindImportGraph = {
6097
6388
  languageOptions: { tolerant: true },
6098
6389
  rules: { "pasika/css-entry-point": "error" }
6099
6390
  };
6100
- var typescriptAppPackageJsonConfig = {
6391
+ var pasikaAppPackageJsonConfig = {
6101
6392
  files: ["package.json"],
6102
6393
  plugins: {
6103
6394
  json: jsonLanguage,
@@ -6106,7 +6397,7 @@ var typescriptAppPackageJsonConfig = {
6106
6397
  language: "json/json",
6107
6398
  rules: Object.fromEntries([...repoPackageJsonRuleIds, ...huskyRuleIds, ...vulykRuleIds].map((id) => [id, "error"]))
6108
6399
  };
6109
- var nextjsAppPackageJsonConfig = {
6400
+ var pasikaNextjsAppPackageJsonConfig = {
6110
6401
  files: ["package.json"],
6111
6402
  plugins: {
6112
6403
  json: jsonLanguage,
@@ -6133,27 +6424,39 @@ var documentationConfig = {
6133
6424
  language: "markdown/gfm",
6134
6425
  rules: Object.fromEntries(documentationRuleIds.map((id) => [id, "error"]))
6135
6426
  };
6136
- var typescriptApp = [
6137
- typescriptAppPackageJsonConfig,
6427
+ var pasikaApp = [
6428
+ pasikaAppPackageJsonConfig,
6138
6429
  zirkaConfig,
6139
6430
  documentationConfig
6140
6431
  ];
6141
- var nextjsApp = [
6142
- ...typescriptApp,
6143
- nextjsAppPackageJsonConfig,
6144
- nextjsAppConfig,
6432
+ var pasikaNextjsAppWithDiagnostic = (preset) => {
6433
+ let checked = false;
6434
+ return new Proxy(preset, {
6435
+ get(target, property, receiver) {
6436
+ if (!checked && (property === Symbol.iterator || property === "length")) {
6437
+ checked = true;
6438
+ assertTypescriptAlignment();
6439
+ }
6440
+ return Reflect.get(target, property, receiver);
6441
+ }
6442
+ });
6443
+ };
6444
+ var pasikaNextjsApp = pasikaNextjsAppWithDiagnostic([
6445
+ ...pasikaApp,
6446
+ pasikaNextjsAppPackageJsonConfig,
6447
+ pasikaNextjsAppConfig,
6145
6448
  tailwindStructureRules,
6146
6449
  tailwindImportGraph
6147
- ];
6450
+ ]);
6148
6451
  export {
6149
6452
  allPasikaRuleIds,
6150
6453
  documentationRules,
6151
6454
  huskyRules,
6152
- nextjsApp,
6153
6455
  nextjsPackageJsonRules,
6456
+ pasikaApp,
6457
+ pasikaNextjsApp,
6154
6458
  pasikaPlugin,
6155
6459
  repoPackageJsonRules,
6156
6460
  tailwindRules,
6157
- typescriptApp,
6158
6461
  vulykRules
6159
6462
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pasika",
3
- "version": "0.5.17",
3
+ "version": "0.6.0",
4
4
  "description": "Reusable agent setup package",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,15 +22,23 @@
22
22
  "coverage": "tsx scripts/coverage.ts",
23
23
  "dogfood": "tsx scripts/dogfood.ts",
24
24
  "fix": "eslint . --fix",
25
+ "format": "prettier --check . --ignore-unknown",
26
+ "format:staged": "prettier --write --ignore-unknown",
25
27
  "lint": "eslint .",
28
+ "lint:staged": "eslint --fix",
26
29
  "prepack": "npm run build",
27
30
  "prepare": "husky",
28
- "test": "node --import tsx --test 'eslint/**/*.test.ts' 'scripts/**/*.test.ts' 'utils/**/*.test.ts'",
31
+ "test:unit": "vitest run",
32
+ "test:unit:coverage": "vitest run --coverage",
33
+ "test:unit:coverage:staged": "vitest related --run --coverage --coverage.changed --coverage.thresholds.perFile --coverage.thresholds.lines=80 --coverage.thresholds.functions=80 --coverage.thresholds.branches=80 --coverage.thresholds.statements=80 --coverage.thresholds.autoUpdate=false",
29
34
  "typecheck": "tsc --noEmit"
30
35
  },
31
36
  "lint-staged": {
32
- "!(*.ts|*.mts|*.cts|*.tsx|*.js|*.cjs|*.mjs|*.jsx)": "prettier --write --ignore-unknown",
33
- "*.ts|*.mts|*.cts|*.tsx|*.js|*.cjs|*.mjs|*.jsx": "npx eslint --fix"
37
+ "!(*.ts|*.mts|*.cts|*.tsx|*.js|*.cjs|*.mjs|*.jsx)": "npm run format:staged --",
38
+ "*.ts|*.mts|*.cts|*.tsx|*.js|*.cjs|*.mjs|*.jsx": [
39
+ "npm run lint:staged --",
40
+ "npm run test:unit:coverage:staged --"
41
+ ]
34
42
  },
35
43
  "dependencies": {
36
44
  "@typescript-eslint/parser": "8.68.0",
@@ -45,6 +53,7 @@
45
53
  "@types/estree": "1.0.9",
46
54
  "@types/mdast": "4.0.4",
47
55
  "@types/node": "26.4.0",
56
+ "@vitest/coverage-v8": "5.0.0",
48
57
  "eslint": "10.9.1",
49
58
  "husky": "9.1.7",
50
59
  "lint-staged": "17.4.1",
@@ -52,6 +61,7 @@
52
61
  "tsup": "8.5.1",
53
62
  "tsx": "4.23.12",
54
63
  "typescript": "6.0.3",
64
+ "vitest": "5.0.0",
55
65
  "zirka": "0.0.48",
56
66
  "zod": "4.4.3"
57
67
  },