pasika 0.5.18 → 0.6.1

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
@@ -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
@@ -98,6 +98,7 @@ declare const documentationRules: {
98
98
  "no-nested-how-to": _eslint_markdown.MarkdownRuleDefinition;
99
99
  "glossary-term-linking": _eslint_markdown.MarkdownRuleDefinition;
100
100
  "guide-mentions-documents": _eslint_markdown.MarkdownRuleDefinition;
101
+ "guide-section-shape": _eslint_markdown.MarkdownRuleDefinition;
101
102
  };
102
103
 
103
104
  declare const tailwindRules: {
@@ -119,6 +120,8 @@ declare const tailwindRules: {
119
120
  declare const repoPackageJsonRules: {
120
121
  "no-vulyk-dependency": _eslint_json.JSONRuleDefinition;
121
122
  "exact-version": _eslint_json.JSONRuleDefinition;
123
+ "lint-setup": _eslint_json.JSONRuleDefinition;
124
+ "vitest-coverage": _eslint_json.JSONRuleDefinition;
122
125
  };
123
126
  /** Package.json rules specific to a Next.js (or React) application. */
124
127
  declare const nextjsPackageJsonRules: {
@@ -147,6 +150,8 @@ declare const pasikaPlugin: {
147
150
  "nextjs-stack": _eslint_json.JSONRuleDefinition;
148
151
  "no-vulyk-dependency": _eslint_json.JSONRuleDefinition;
149
152
  "exact-version": _eslint_json.JSONRuleDefinition;
153
+ "lint-setup": _eslint_json.JSONRuleDefinition;
154
+ "vitest-coverage": _eslint_json.JSONRuleDefinition;
150
155
  "theme-reset": _eslint_css.CSSRuleDefinition;
151
156
  "root-variables": _eslint_css.CSSRuleDefinition;
152
157
  "apply-usage": _eslint_css.CSSRuleDefinition;
@@ -182,6 +187,7 @@ declare const pasikaPlugin: {
182
187
  "no-nested-how-to": _eslint_markdown.MarkdownRuleDefinition;
183
188
  "glossary-term-linking": _eslint_markdown.MarkdownRuleDefinition;
184
189
  "guide-mentions-documents": _eslint_markdown.MarkdownRuleDefinition;
190
+ "guide-section-shape": _eslint_markdown.MarkdownRuleDefinition;
185
191
  "filename-case": eslint.Rule.RuleModule;
186
192
  "import-boundaries": eslint.Rule.RuleModule;
187
193
  "named-exports": eslint.Rule.RuleModule;
@@ -174,7 +174,9 @@ function findSimpleRoot(component, _text, _filename) {
174
174
  };
175
175
  visit(body);
176
176
  const rendered = returns.filter((statement) => statement.expression?.kind !== ts.SyntaxKind.NullKeyword);
177
- const roots = rendered.map((statement) => statement.expression && ts.isExpression(statement.expression) ? rootFromExpression(statement.expression) : void 0).filter((root) => root !== void 0);
177
+ const roots = rendered.map(
178
+ (statement) => statement.expression && ts.isExpression(statement.expression) ? rootFromExpression(statement.expression) : void 0
179
+ ).filter((root) => root !== void 0);
178
180
  if (roots.length !== rendered.length || roots.length === 0) return void 0;
179
181
  const firstTag = roots[0]?.tagName;
180
182
  if (roots.some((root) => root.tagName !== firstTag)) return void 0;
@@ -2302,7 +2304,17 @@ var NEXT_ROUTING_FILES3 = /* @__PURE__ */ new Set([
2302
2304
  "sitemap",
2303
2305
  "twitter-image"
2304
2306
  ]);
2305
- var INTERACTIVE_ATTRIBUTES = /* @__PURE__ */ new Set(["onClick", "onChange", "onSubmit", "href", "onKeyDown", "onKeyUp", "onFocus", "onBlur", "htmlFor"]);
2307
+ var INTERACTIVE_ATTRIBUTES = /* @__PURE__ */ new Set([
2308
+ "onClick",
2309
+ "onChange",
2310
+ "onSubmit",
2311
+ "href",
2312
+ "onKeyDown",
2313
+ "onKeyUp",
2314
+ "onFocus",
2315
+ "onBlur",
2316
+ "htmlFor"
2317
+ ]);
2306
2318
  function tagName(node) {
2307
2319
  const name = node.openingElement?.name;
2308
2320
  if (name?.type !== "JSXIdentifier") return void 0;
@@ -2375,7 +2387,9 @@ function isDecorative(node) {
2375
2387
  return !meaningfulText && !isContentElement(name);
2376
2388
  }
2377
2389
  function isContentElement(name) {
2378
- return /^(?:h[1-6]|p|li|dt|dd|blockquote|pre|code|figcaption|caption|th|td|form|fieldset|select|textarea)$/.test(name);
2390
+ return /^(?:h[1-6]|p|li|dt|dd|blockquote|pre|code|figcaption|caption|th|td|form|fieldset|select|textarea)$/.test(
2391
+ name
2392
+ );
2379
2393
  }
2380
2394
  function isSoleContentOfWrapper(node) {
2381
2395
  const parent = node.parent;
@@ -4153,7 +4167,7 @@ var overviewRule = {
4153
4167
  meta: {
4154
4168
  type: "problem",
4155
4169
  docs: {
4156
- description: "Overview must exist, contain at most two sentences, and not link to other documents.",
4170
+ description: "Required documentation overviews must exist, contain at most two sentences, and not link to other documents.",
4157
4171
  recommended: true
4158
4172
  }
4159
4173
  },
@@ -4196,6 +4210,41 @@ var overviewRule = {
4196
4210
  if (!found) {
4197
4211
  context.report({ node, message: "no overview follows the title" });
4198
4212
  }
4213
+ const validateHeadedOverview = (heading, index, label) => {
4214
+ const overview = node.children[index + 1];
4215
+ if (overview?.type !== "paragraph") {
4216
+ context.report({
4217
+ node: heading,
4218
+ message: `no overview follows ${label}`
4219
+ });
4220
+ return;
4221
+ }
4222
+ if (containsLink(overview)) {
4223
+ context.report({
4224
+ node: overview,
4225
+ message: `overview of ${label} links another document`
4226
+ });
4227
+ }
4228
+ const sectionSentenceCount = countSentences(getTextContent(overview).trim());
4229
+ if (sectionSentenceCount > 2) {
4230
+ context.report({
4231
+ node: overview,
4232
+ message: `overview of ${label} uses ${String(sectionSentenceCount)} sentences, at most two are allowed`
4233
+ });
4234
+ }
4235
+ };
4236
+ const isGuide = filename.endsWith("-guide.md");
4237
+ const isReference = filename.endsWith("-reference.md");
4238
+ if (!isGuide && !isReference) return;
4239
+ for (const [index, child] of node.children.entries()) {
4240
+ if (child.type !== "heading" || child.depth === 1) continue;
4241
+ const title = getTextContent(child).trim();
4242
+ if (isGuide && child.depth === 2 && /^How To \S/.test(title)) {
4243
+ validateHeadedOverview(child, index, `guide section "${title}"`);
4244
+ } else if (isReference) {
4245
+ validateHeadedOverview(child, index, `reference block "${title}"`);
4246
+ }
4247
+ }
4199
4248
  }
4200
4249
  };
4201
4250
  }
@@ -5049,6 +5098,56 @@ var guideMentionsDocumentsRule = {
5049
5098
  }
5050
5099
  };
5051
5100
 
5101
+ // eslint/rules/documentation/guide-section-shape.ts
5102
+ var guideSectionShapeRule = {
5103
+ meta: {
5104
+ type: "problem",
5105
+ docs: {
5106
+ description: "Guide sections use How To headings, an overview, and numbered steps.",
5107
+ recommended: true
5108
+ }
5109
+ },
5110
+ create(context) {
5111
+ return {
5112
+ root(node) {
5113
+ if (!getFilename(context).endsWith("-guide.md")) return;
5114
+ const sections = [];
5115
+ for (const [index, child] of node.children.entries()) {
5116
+ if (child.type !== "heading" || child.depth === 1) continue;
5117
+ sections.push({ heading: child, index, title: getTextContent(child).trim() });
5118
+ }
5119
+ if (sections.length === 0) {
5120
+ context.report({ node, message: "guide must contain at least one How To section" });
5121
+ return;
5122
+ }
5123
+ for (const [sectionIndex, section] of sections.entries()) {
5124
+ if (section.heading.depth !== 2) {
5125
+ context.report({
5126
+ node: section.heading,
5127
+ message: `guide section heading "${section.title}" must be level two`
5128
+ });
5129
+ }
5130
+ if (!/^How To \S/.test(section.title)) {
5131
+ context.report({
5132
+ node: section.heading,
5133
+ message: `guide section heading "${section.title}" must start with "How To "`
5134
+ });
5135
+ }
5136
+ const nextSection = sections[sectionIndex + 1];
5137
+ const content = node.children.slice(section.index + 1, nextSection?.index ?? node.children.length);
5138
+ const hasOverviewThenNumberedList = content.length === 2 && content[0]?.type === "paragraph" && content[1]?.type === "list" && content[1].ordered === true;
5139
+ if (!hasOverviewThenNumberedList) {
5140
+ context.report({
5141
+ node: section.heading,
5142
+ message: `guide section "${section.title}" must consist of its overview followed by one numbered list`
5143
+ });
5144
+ }
5145
+ }
5146
+ }
5147
+ };
5148
+ }
5149
+ };
5150
+
5052
5151
  // eslint/rules/documentation/index.ts
5053
5152
  var documentationRules = {
5054
5153
  "doc-kind-suffix": docKindSuffixRule,
@@ -5073,7 +5172,8 @@ var documentationRules = {
5073
5172
  "guide-link-anchors": guideLinkAnchorsRule,
5074
5173
  "no-nested-how-to": noNestedHowToRule,
5075
5174
  "glossary-term-linking": glossaryTermLinkingRule,
5076
- "guide-mentions-documents": guideMentionsDocumentsRule
5175
+ "guide-mentions-documents": guideMentionsDocumentsRule,
5176
+ "guide-section-shape": guideSectionShapeRule
5077
5177
  };
5078
5178
 
5079
5179
  // eslint/rules/tailwind/helpers.ts
@@ -5786,10 +5886,108 @@ var exactVersionRule = {
5786
5886
  }
5787
5887
  };
5788
5888
 
5789
- // eslint/rules/package-json/no-vulyk-dependency.ts
5889
+ // eslint/rules/package-json/lint-setup.ts
5790
5890
  function memberName2(member) {
5791
5891
  return member.name.type === "String" ? member.name.value : member.name.name;
5792
5892
  }
5893
+ function memberValue2(member) {
5894
+ return member?.value.type === "String" ? member.value.value : void 0;
5895
+ }
5896
+ function stringValues(value) {
5897
+ if (value.type === "String") return [value.value];
5898
+ if (value.type !== "Array") return [];
5899
+ return value.elements.flatMap((element) => element.value.type === "String" ? [element.value.value] : []);
5900
+ }
5901
+ var SOURCE_GLOB_PATTERN = /(?:^|[^a-z])(?:[cm]?[jt]sx?)(?:[^a-z]|$)/i;
5902
+ var DIRECT_ESLINT_PATTERN = /(?:^|&&|\|\||;)\s*(?:npx\s+)?eslint(?:\s|$)/;
5903
+ var REPOSITORY_ARGUMENT_PATTERN = /(?:^|\s)\.(?=\s|$)/;
5904
+ var PRETTIER_PATTERN = /\bprettier\b/;
5905
+ var CHECK_FLAG_PATTERN = /(?:^|\s)(?:--check|-c)(?:\s|$)/;
5906
+ function runsEslintDirectly(command) {
5907
+ return DIRECT_ESLINT_PATTERN.test(command);
5908
+ }
5909
+ function runsPrettier(command) {
5910
+ return PRETTIER_PATTERN.test(command);
5911
+ }
5912
+ function runsNamedScript(command, name) {
5913
+ return command.includes(`npm run ${name}`);
5914
+ }
5915
+ var lintSetupRule = {
5916
+ meta: {
5917
+ schema: [],
5918
+ type: "problem",
5919
+ docs: {
5920
+ description: "Require full-repository lint/format scripts and argument-free staged-file scripts that lint-staged runs by name."
5921
+ }
5922
+ },
5923
+ create(context) {
5924
+ return {
5925
+ Document(node) {
5926
+ const root = node.body;
5927
+ if (root.type !== "Object") return;
5928
+ const scripts = root.members.find((member) => memberName2(member) === "scripts");
5929
+ const scriptMembers = scripts?.value.type === "Object" ? scripts.value.members : [];
5930
+ const findScript = (name) => scriptMembers.find((member) => memberName2(member) === name);
5931
+ const lint = findScript("lint");
5932
+ const lintCommand = memberValue2(lint);
5933
+ if (lintCommand === void 0 || !runsEslintDirectly(lintCommand) || !REPOSITORY_ARGUMENT_PATTERN.test(lintCommand)) {
5934
+ context.report({
5935
+ node: lint ?? node,
5936
+ message: 'package.json must declare a "lint" script that runs ESLint across the repository (e.g. "eslint .").'
5937
+ });
5938
+ }
5939
+ const format = findScript("format");
5940
+ const formatCommand = memberValue2(format);
5941
+ if (formatCommand === void 0 || !runsPrettier(formatCommand) || !CHECK_FLAG_PATTERN.test(formatCommand) || !REPOSITORY_ARGUMENT_PATTERN.test(formatCommand)) {
5942
+ context.report({
5943
+ node: format ?? node,
5944
+ message: 'package.json must declare a "format" script that runs prettier --check across the repository.'
5945
+ });
5946
+ }
5947
+ const lintStaged = findScript("lint:staged");
5948
+ const lintStagedCommand = memberValue2(lintStaged);
5949
+ if (lintStagedCommand === void 0 || !runsEslintDirectly(lintStagedCommand) || REPOSITORY_ARGUMENT_PATTERN.test(lintStagedCommand)) {
5950
+ context.report({
5951
+ node: lintStaged ?? node,
5952
+ message: 'package.json must declare a "lint:staged" script that runs ESLint with no repository-wide argument (e.g. "eslint --fix").'
5953
+ });
5954
+ }
5955
+ const formatStaged = findScript("format:staged");
5956
+ const formatStagedCommand = memberValue2(formatStaged);
5957
+ if (formatStagedCommand === void 0 || !runsPrettier(formatStagedCommand) || REPOSITORY_ARGUMENT_PATTERN.test(formatStagedCommand)) {
5958
+ context.report({
5959
+ node: formatStaged ?? node,
5960
+ message: 'package.json must declare a "format:staged" script that runs prettier with no repository-wide argument (e.g. "prettier --write").'
5961
+ });
5962
+ }
5963
+ const lintStagedConfig = root.members.find((member) => memberName2(member) === "lint-staged");
5964
+ const runsStagedScript = (name) => lintStagedConfig?.value.type === "Object" && lintStagedConfig.value.members.some(
5965
+ (member) => stringValues(member.value).some((command) => runsNamedScript(command, name))
5966
+ );
5967
+ const hasStagedLintEntry = lintStagedConfig?.value.type === "Object" && lintStagedConfig.value.members.some(
5968
+ (member) => SOURCE_GLOB_PATTERN.test(memberName2(member)) && stringValues(member.value).some((command) => runsNamedScript(command, "lint:staged"))
5969
+ );
5970
+ if (!hasStagedLintEntry) {
5971
+ context.report({
5972
+ node: lintStagedConfig ?? node,
5973
+ message: 'package.json lint-staged must run "npm run lint:staged --" for staged JavaScript or TypeScript files.'
5974
+ });
5975
+ }
5976
+ if (!runsStagedScript("format:staged")) {
5977
+ context.report({
5978
+ node: lintStagedConfig ?? node,
5979
+ message: 'package.json lint-staged must run "npm run format:staged --" for staged files ESLint does not already format.'
5980
+ });
5981
+ }
5982
+ }
5983
+ };
5984
+ }
5985
+ };
5986
+
5987
+ // eslint/rules/package-json/no-vulyk-dependency.ts
5988
+ function memberName3(member) {
5989
+ return member.name.type === "String" ? member.name.value : member.name.name;
5990
+ }
5793
5991
  var noVulykDependencyRule = {
5794
5992
  meta: {
5795
5993
  schema: [],
@@ -5804,9 +6002,9 @@ var noVulykDependencyRule = {
5804
6002
  const root = node.body;
5805
6003
  if (root.type !== "Object") return;
5806
6004
  for (const key of ["dependencies", "devDependencies"]) {
5807
- const section = root.members.find((member) => memberName2(member) === key);
6005
+ const section = root.members.find((member) => memberName3(member) === key);
5808
6006
  if (section?.value.type !== "Object") continue;
5809
- const vulyk = section.value.members.find((member) => memberName2(member) === "vulyk");
6007
+ const vulyk = section.value.members.find((member) => memberName3(member) === "vulyk");
5810
6008
  if (vulyk) {
5811
6009
  context.report({
5812
6010
  node: vulyk,
@@ -5836,15 +6034,19 @@ var NEXTJS_STACK_DEV_DEPENDENCIES = [
5836
6034
  "prettier",
5837
6035
  "husky",
5838
6036
  "lint-staged",
5839
- "zirka"
6037
+ "zirka",
6038
+ "jsdom",
6039
+ "@vitejs/plugin-react",
6040
+ "@testing-library/react",
6041
+ "@testing-library/dom"
5840
6042
  ];
5841
- function memberName3(member) {
6043
+ function memberName4(member) {
5842
6044
  return member.name.type === "String" ? member.name.value : member.name.name;
5843
6045
  }
5844
6046
  function sectionPackages(root, section) {
5845
- const member = root.type === "Object" ? root.members.find((entry) => memberName3(entry) === section) : void 0;
6047
+ const member = root.type === "Object" ? root.members.find((entry) => memberName4(entry) === section) : void 0;
5846
6048
  if (member?.value.type !== "Object") return /* @__PURE__ */ new Set();
5847
- return new Set(member.value.members.map(memberName3));
6049
+ return new Set(member.value.members.map(memberName4));
5848
6050
  }
5849
6051
  var nextjsStackRule = {
5850
6052
  meta: {
@@ -5880,19 +6082,151 @@ var nextjsStackRule = {
5880
6082
  }
5881
6083
  };
5882
6084
 
6085
+ // eslint/rules/package-json/vitest-coverage.ts
6086
+ import { existsSync as existsSync2, readFileSync as readFileSync9 } from "fs";
6087
+ import path47 from "path";
6088
+ function memberName5(member) {
6089
+ return member.name.type === "String" ? member.name.value : member.name.name;
6090
+ }
6091
+ function memberValue3(member) {
6092
+ return member?.value.type === "String" ? member.value.value : void 0;
6093
+ }
6094
+ function stringValues2(value) {
6095
+ if (value.type === "String") return [value.value];
6096
+ if (value.type !== "Array") return [];
6097
+ return value.elements.flatMap((element) => element.value.type === "String" ? [element.value.value] : []);
6098
+ }
6099
+ var VITEST_CONFIG_NAMES = [
6100
+ "vitest.config.ts",
6101
+ "vitest.config.mts",
6102
+ "vitest.config.cts",
6103
+ "vitest.config.js",
6104
+ "vitest.config.mjs",
6105
+ "vitest.config.cjs"
6106
+ ];
6107
+ var THRESHOLD_METRICS = ["lines", "functions", "branches", "statements"];
6108
+ var SOURCE_GLOB_PATTERN2 = /(?:^|[^a-z])(?:[cm]?[jt]sx?)(?:[^a-z]|$)/i;
6109
+ var COVERAGE_FLAG_PATTERN = /(?:^|\s)--coverage(?:[=\s]|$)/;
6110
+ var RELATED_PATTERN = /\brelated\b/;
6111
+ var AUTO_UPDATE_PATTERN = /autoUpdate\s*:\s*true/;
6112
+ var CHANGED_FLAG_PATTERN = /--coverage\.changed\b/;
6113
+ var PER_FILE_FLAG_PATTERN = /--coverage\.thresholds\.perFile\b/;
6114
+ var AUTO_UPDATE_DISABLED_FLAG_PATTERN = /--coverage\.thresholds\.autoUpdate=false\b/;
6115
+ function hasEightyOrAboveFlag(command, metric) {
6116
+ return new RegExp(`--coverage\\.thresholds\\.${metric}=(?:8\\d|9\\d|100)\\b`).test(command);
6117
+ }
6118
+ var vitestCoverageRule = {
6119
+ meta: {
6120
+ schema: [],
6121
+ type: "problem",
6122
+ docs: {
6123
+ description: "Require Vitest unit-test scripts, the V8 provider, a rising coverage threshold, and an 80% floor on staged files via lint-staged."
6124
+ }
6125
+ },
6126
+ create(context) {
6127
+ return {
6128
+ Document(node) {
6129
+ const root = node.body;
6130
+ if (root.type !== "Object") return;
6131
+ const devDependencies = root.members.find((member) => memberName5(member) === "devDependencies");
6132
+ const devDependencyNames = new Set(
6133
+ devDependencies?.value.type === "Object" ? devDependencies.value.members.map(memberName5) : []
6134
+ );
6135
+ if (!devDependencyNames.has("vitest")) {
6136
+ context.report({ node, message: "vitest must be listed in package.json as a devDependency." });
6137
+ }
6138
+ if (!devDependencyNames.has("@vitest/coverage-v8")) {
6139
+ context.report({
6140
+ node,
6141
+ message: "@vitest/coverage-v8 must be listed in package.json as a devDependency."
6142
+ });
6143
+ }
6144
+ const scripts = root.members.find((member) => memberName5(member) === "scripts");
6145
+ const scriptMembers = scripts?.value.type === "Object" ? scripts.value.members : [];
6146
+ const unit = scriptMembers.find((member) => memberName5(member) === "test:unit");
6147
+ const unitCommand = memberValue3(unit);
6148
+ if (unitCommand === void 0 || !/\bvitest\b/.test(unitCommand) || COVERAGE_FLAG_PATTERN.test(unitCommand)) {
6149
+ context.report({
6150
+ node: unit ?? node,
6151
+ message: 'package.json must declare a "test:unit" script that runs Vitest without coverage.'
6152
+ });
6153
+ }
6154
+ const coverage = scriptMembers.find((member) => memberName5(member) === "test:unit:coverage");
6155
+ const coverageCommand = memberValue3(coverage);
6156
+ if (coverageCommand === void 0 || !/\bvitest\b/.test(coverageCommand) || !COVERAGE_FLAG_PATTERN.test(coverageCommand)) {
6157
+ context.report({
6158
+ node: coverage ?? node,
6159
+ message: 'package.json must declare a "test:unit:coverage" script that runs Vitest with coverage.'
6160
+ });
6161
+ }
6162
+ const configName = VITEST_CONFIG_NAMES.find((name) => existsSync2(path47.join(context.cwd, name)));
6163
+ if (!configName) {
6164
+ context.report({
6165
+ node,
6166
+ message: "No vitest config found. Create one with a coverage threshold above zero for lines, functions, branches, and statements."
6167
+ });
6168
+ return;
6169
+ }
6170
+ const content = readFileSync9(path47.join(context.cwd, configName), "utf8");
6171
+ for (const metric of THRESHOLD_METRICS) {
6172
+ if (!new RegExp(`\\b${metric}\\s*:\\s*[1-9]\\d*`).test(content)) {
6173
+ context.report({ node, message: `${configName} must set a coverage threshold above zero for ${metric}.` });
6174
+ }
6175
+ }
6176
+ if (!AUTO_UPDATE_PATTERN.test(content)) {
6177
+ context.report({
6178
+ node,
6179
+ message: `${configName} must set coverage.thresholds.autoUpdate to true.`
6180
+ });
6181
+ }
6182
+ const changedCoverage = scriptMembers.find((member) => memberName5(member) === "test:unit:coverage:staged");
6183
+ const changedCoverageCommand = memberValue3(changedCoverage);
6184
+ if (changedCoverageCommand === void 0 || !/\bvitest\b/.test(changedCoverageCommand) || !RELATED_PATTERN.test(changedCoverageCommand) || !COVERAGE_FLAG_PATTERN.test(changedCoverageCommand)) {
6185
+ context.report({
6186
+ node: changedCoverage ?? node,
6187
+ message: 'package.json must declare a "test:unit:coverage:staged" script that runs vitest related with coverage.'
6188
+ });
6189
+ }
6190
+ const lintStaged = root.members.find((member) => memberName5(member) === "lint-staged");
6191
+ const hasStagedChangedCoverage = lintStaged?.value.type === "Object" && lintStaged.value.members.some(
6192
+ (member) => SOURCE_GLOB_PATTERN2.test(memberName5(member)) && stringValues2(member.value).some((command) => command.includes("npm run test:unit:coverage:staged"))
6193
+ );
6194
+ if (!hasStagedChangedCoverage) {
6195
+ context.report({
6196
+ node: lintStaged ?? node,
6197
+ message: 'package.json lint-staged must run "npm run test:unit:coverage:staged" for staged JavaScript or TypeScript files.'
6198
+ });
6199
+ }
6200
+ const changedCoverageCommandText = changedCoverageCommand ?? "";
6201
+ const hasEightyFloor = THRESHOLD_METRICS.every(
6202
+ (metric) => hasEightyOrAboveFlag(changedCoverageCommandText, metric)
6203
+ );
6204
+ if (!CHANGED_FLAG_PATTERN.test(changedCoverageCommandText) || !PER_FILE_FLAG_PATTERN.test(changedCoverageCommandText) || !hasEightyFloor || !AUTO_UPDATE_DISABLED_FLAG_PATTERN.test(changedCoverageCommandText)) {
6205
+ context.report({
6206
+ node: changedCoverage ?? node,
6207
+ 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.'
6208
+ });
6209
+ }
6210
+ }
6211
+ };
6212
+ }
6213
+ };
6214
+
5883
6215
  // eslint/rules/package-json/index.ts
5884
6216
  var repoPackageJsonRules = {
5885
6217
  "no-vulyk-dependency": noVulykDependencyRule,
5886
- "exact-version": exactVersionRule
6218
+ "exact-version": exactVersionRule,
6219
+ "lint-setup": lintSetupRule,
6220
+ "vitest-coverage": vitestCoverageRule
5887
6221
  };
5888
6222
  var nextjsPackageJsonRules = {
5889
6223
  "nextjs-stack": nextjsStackRule
5890
6224
  };
5891
6225
 
5892
6226
  // eslint/rules/husky/husky-hook.ts
5893
- import { existsSync as existsSync2, readFileSync as readFileSync9 } from "fs";
5894
- import path47 from "path";
5895
- function memberName4(member) {
6227
+ import { existsSync as existsSync3, readFileSync as readFileSync10 } from "fs";
6228
+ import path48 from "path";
6229
+ function memberName6(member) {
5896
6230
  return member.name.type === "String" ? member.name.value : member.name.name;
5897
6231
  }
5898
6232
  var huskyHookRule = {
@@ -5900,7 +6234,7 @@ var huskyHookRule = {
5900
6234
  schema: [],
5901
6235
  type: "problem",
5902
6236
  docs: {
5903
- description: "Require a pre-commit hook that runs lint-staged, typecheck, and the libyear drift check."
6237
+ 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."
5904
6238
  }
5905
6239
  },
5906
6240
  create(context) {
@@ -5908,29 +6242,39 @@ var huskyHookRule = {
5908
6242
  Document(node) {
5909
6243
  const root = node.body;
5910
6244
  if (root.type !== "Object") return;
5911
- const scriptName = context.filename;
5912
- if (!scriptName.endsWith("package.json")) return;
5913
- const hookPath = path47.join(context.cwd, ".husky", "pre-commit");
5914
- if (!existsSync2(hookPath)) {
6245
+ if (!context.filename.endsWith("package.json")) return;
6246
+ const hookPath = path48.join(context.cwd, ".husky", "pre-commit");
6247
+ if (!existsSync3(hookPath)) {
5915
6248
  context.report({
5916
6249
  node,
5917
6250
  message: "No .husky/pre-commit hook found. Configure husky to run checks before commits."
5918
6251
  });
5919
6252
  return;
5920
6253
  }
5921
- const content = readFileSync9(hookPath, "utf8");
6254
+ const content = readFileSync10(hookPath, "utf8");
6255
+ const scripts = root.members.find((member) => memberName6(member) === "scripts");
6256
+ const scriptNames = new Set(scripts?.value.type === "Object" ? scripts.value.members.map(memberName6) : []);
6257
+ const requireNamedScript = (name) => {
6258
+ if (!scriptNames.has(name)) {
6259
+ context.report({ node, message: `package.json must declare a "${name}" script.` });
6260
+ }
6261
+ if (!content.includes(`npm run ${name}`)) {
6262
+ context.report({ node, message: `.husky/pre-commit must run npm run ${name}.` });
6263
+ }
6264
+ };
5922
6265
  if (!content.includes("lint-staged")) {
5923
6266
  context.report({ node, message: ".husky/pre-commit must run lint-staged." });
5924
6267
  }
5925
- if (!content.includes("typecheck")) {
5926
- context.report({ node, message: ".husky/pre-commit must run npm run typecheck." });
5927
- }
6268
+ requireNamedScript("typecheck");
5928
6269
  if (!content.includes("libyear --limit-major-individual=1")) {
5929
6270
  context.report({ node, message: ".husky/pre-commit must run npx libyear --limit-major-individual=1." });
5930
6271
  }
5931
- const scripts = root.members.find((member) => memberName4(member) === "scripts");
6272
+ const suppressionsPath = path48.join(context.cwd, "eslint-suppressions.json");
6273
+ if (existsSync3(suppressionsPath)) {
6274
+ requireNamedScript("lint:prune");
6275
+ }
5932
6276
  if (scripts?.value.type === "Object") {
5933
- const prepare = scripts.value.members.find((member) => memberName4(member) === "prepare");
6277
+ const prepare = scripts.value.members.find((member) => memberName6(member) === "prepare");
5934
6278
  if (prepare?.value.type === "String" && !prepare.value.value.includes("husky")) {
5935
6279
  context.report({
5936
6280
  node: prepare,
@@ -5949,8 +6293,8 @@ var huskyRules = {
5949
6293
  };
5950
6294
 
5951
6295
  // eslint/rules/vulyk/vulyk-docs.ts
5952
- import { existsSync as existsSync3, readFileSync as readFileSync10 } from "fs";
5953
- import path48 from "path";
6296
+ import { existsSync as existsSync4, readFileSync as readFileSync11 } from "fs";
6297
+ import path49 from "path";
5954
6298
  var PASIKA_REPO = "Bredansky/pasika";
5955
6299
  var BASE_REQUIRED_DOCS = [
5956
6300
  { name: "documentation-guide", path: "docs/documentation-guide" },
@@ -5961,13 +6305,13 @@ var NEXTJS_REQUIRED_DOCS = [
5961
6305
  { name: "next-codebase-guide", path: "docs/next-codebase-guide" },
5962
6306
  { name: "next-tailwind-guide", path: "docs/next-tailwind-guide" }
5963
6307
  ];
5964
- function memberName5(member) {
6308
+ function memberName7(member) {
5965
6309
  return member.name.type === "String" ? member.name.value : member.name.name;
5966
6310
  }
5967
6311
  function hasDependency(root, name) {
5968
- const section = root.members.find((member) => memberName5(member) === "dependencies");
6312
+ const section = root.members.find((member) => memberName7(member) === "dependencies");
5969
6313
  if (section?.value.type !== "Object") return false;
5970
- return section.value.members.some((member) => memberName5(member) === name);
6314
+ return section.value.members.some((member) => memberName7(member) === name);
5971
6315
  }
5972
6316
  var vulykDocsRule = {
5973
6317
  meta: {
@@ -5983,16 +6327,16 @@ var vulykDocsRule = {
5983
6327
  if (!context.filename.endsWith("package.json")) return;
5984
6328
  const root = node.body;
5985
6329
  if (root.type !== "Object") return;
5986
- const projectRoot = path48.dirname(path48.resolve(context.filename));
5987
- const configPath = path48.join(projectRoot, "vulyk.config.ts");
5988
- if (!existsSync3(configPath)) {
6330
+ const projectRoot = path49.dirname(path49.resolve(context.filename));
6331
+ const configPath = path49.join(projectRoot, "vulyk.config.ts");
6332
+ if (!existsSync4(configPath)) {
5989
6333
  context.report({
5990
6334
  node,
5991
6335
  message: "No vulyk.config.ts found. Run npx vulyk@latest init to create one that tracks the framework's docs."
5992
6336
  });
5993
6337
  return;
5994
6338
  }
5995
- const config = readFileSync10(configPath, "utf8");
6339
+ const config = readFileSync11(configPath, "utf8");
5996
6340
  if (!config.includes(PASIKA_REPO)) {
5997
6341
  context.report({
5998
6342
  node,
@@ -6009,8 +6353,8 @@ var vulykDocsRule = {
6009
6353
  });
6010
6354
  }
6011
6355
  }
6012
- const agentsPath = path48.join(projectRoot, "AGENTS.md");
6013
- if (!existsSync3(agentsPath)) {
6356
+ const agentsPath = path49.join(projectRoot, "AGENTS.md");
6357
+ if (!existsSync4(agentsPath)) {
6014
6358
  context.report({
6015
6359
  node,
6016
6360
  message: "No AGENTS.md found. Run npx vulyk@latest agents to generate the agent file that routes to the tracked docs."
@@ -6130,9 +6474,7 @@ var tailwindStructureRules = {
6130
6474
  },
6131
6475
  language: "css/css",
6132
6476
  languageOptions: { tolerant: true },
6133
- rules: Object.fromEntries(
6134
- tailwindRuleIds.filter((id) => id !== "pasika/css-entry-point").map((id) => [id, "error"])
6135
- )
6477
+ rules: Object.fromEntries(tailwindRuleIds.filter((id) => id !== "pasika/css-entry-point").map((id) => [id, "error"]))
6136
6478
  };
6137
6479
  var tailwindImportGraph = {
6138
6480
  files: ["src/**/*.css"],
@@ -6180,11 +6522,7 @@ var documentationConfig = {
6180
6522
  language: "markdown/gfm",
6181
6523
  rules: Object.fromEntries(documentationRuleIds.map((id) => [id, "error"]))
6182
6524
  };
6183
- var pasikaApp = [
6184
- pasikaAppPackageJsonConfig,
6185
- zirkaConfig,
6186
- documentationConfig
6187
- ];
6525
+ var pasikaApp = [pasikaAppPackageJsonConfig, zirkaConfig, documentationConfig];
6188
6526
  var pasikaNextjsAppWithDiagnostic = (preset) => {
6189
6527
  let checked = false;
6190
6528
  return new Proxy(preset, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pasika",
3
- "version": "0.5.18",
3
+ "version": "0.6.1",
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,7 +61,8 @@
52
61
  "tsup": "8.5.1",
53
62
  "tsx": "4.23.12",
54
63
  "typescript": "6.0.3",
55
- "zirka": "0.0.48",
64
+ "vitest": "5.0.0",
65
+ "zirka": "0.0.65",
56
66
  "zod": "4.4.3"
57
67
  },
58
68
  "peerDependencies": {