pasika 0.1.6 → 0.2.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.
Files changed (68) hide show
  1. package/README.md +40 -55
  2. package/dist/eslint/pasika/index.d.ts +18 -0
  3. package/dist/eslint/pasika/index.js +18 -3
  4. package/dist/eslint/pasika/rules/enforce-barrel-exports.d.ts +9 -0
  5. package/dist/eslint/pasika/rules/enforce-barrel-exports.js +78 -0
  6. package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +9 -0
  7. package/dist/eslint/pasika/rules/enforce-cn-merge.js +84 -0
  8. package/dist/eslint/pasika/rules/enforce-cva-variant-props.d.ts +9 -0
  9. package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +76 -0
  10. package/dist/eslint/pasika/rules/filename-case.js +10 -0
  11. package/dist/eslint/pasika/rules/import-boundaries.d.ts +2 -0
  12. package/dist/eslint/pasika/rules/{organization-imports.js → import-boundaries.js} +11 -1
  13. package/dist/eslint/pasika/rules/no-arbitrary-tailwind.d.ts +9 -0
  14. package/dist/eslint/pasika/rules/no-arbitrary-tailwind.js +107 -0
  15. package/dist/eslint/pasika/rules/no-mixed-concerns.d.ts +9 -0
  16. package/dist/eslint/pasika/rules/no-mixed-concerns.js +84 -0
  17. package/package.json +5 -16
  18. package/claude/hooks/.vulyk +0 -3
  19. package/claude/hooks/AGENTS.md +0 -3
  20. package/claude/hooks/CLAUDE.md +0 -1
  21. package/claude/hooks/claude-hooks.md +0 -30
  22. package/claude/hooks/notification.sh +0 -38
  23. package/claude/hooks/protect-files.sh +0 -21
  24. package/claude/hooks/status-line/index.js +0 -57
  25. package/claude/scripts/render-settings.ts +0 -223
  26. package/claude/settings.base.json +0 -38
  27. package/dist/claude/scripts/render-settings.js +0 -145
  28. package/dist/eslint/pasika/rules/organization-imports.d.ts +0 -2
  29. package/dist/eslint.config.js +0 -7
  30. package/dist/scripts/pasika.js +0 -59
  31. package/docs/agent-conventions.md +0 -27
  32. package/docs/claude/hooks.md +0 -30
  33. package/docs/code-organization-guide/code-organization-guide.md +0 -69
  34. package/docs/code-organization-guide/references/application-architecture-reference.md +0 -149
  35. package/docs/code-organization-guide/rules/component-placement-rule.md +0 -119
  36. package/docs/code-organization-guide/rules/configuration-rule.md +0 -42
  37. package/docs/code-organization-guide/rules/constants-rule.md +0 -74
  38. package/docs/code-organization-guide/rules/exports-and-imports-rule.md +0 -84
  39. package/docs/code-organization-guide/rules/folder-nesting-rule.md +0 -82
  40. package/docs/code-organization-guide/rules/hook-extraction-rule.md +0 -141
  41. package/docs/code-organization-guide/rules/interactive-component-rule.md +0 -97
  42. package/docs/code-organization-guide/rules/jsx-hygiene-rule.md +0 -67
  43. package/docs/code-organization-guide/rules/locales-rule.md +0 -53
  44. package/docs/code-organization-guide/rules/nameable-visual-concept-rule.md +0 -66
  45. package/docs/code-organization-guide/rules/no-mixed-concerns-rule.md +0 -63
  46. package/docs/code-organization-guide/rules/repeated-structure-rule.md +0 -93
  47. package/docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md +0 -112
  48. package/docs/code-organization-guide/rules/sole-state-owner-rule.md +0 -101
  49. package/docs/code-organization-guide/rules/types-and-schemas-rule.md +0 -139
  50. package/docs/code-organization-guide/rules/utilities-rule.md +0 -86
  51. package/docs/documentation-guide/_templates/grouped-reference.md +0 -11
  52. package/docs/documentation-guide/_templates/guide.md +0 -19
  53. package/docs/documentation-guide/_templates/rule.md +0 -21
  54. package/docs/documentation-guide/_templates/single-lookup-reference.md +0 -5
  55. package/docs/documentation-guide/documentation-guide.md +0 -13
  56. package/docs/documentation-guide/references/documentation-types-reference.md +0 -9
  57. package/docs/documentation-guide/rules/guide-creation-rule.md +0 -113
  58. package/docs/documentation-guide/rules/reference-creation-rule.md +0 -132
  59. package/docs/documentation-guide/rules/rule-creation-rule.md +0 -81
  60. package/docs/documentation-guide/rules/template-usage-rule.md +0 -49
  61. package/docs/shadcn-theme.md +0 -121
  62. package/docs/styling-guide/rules/arbitrary-value-rule.md +0 -31
  63. package/docs/styling-guide/rules/class-composition-rule.md +0 -52
  64. package/docs/styling-guide/rules/component-ui-state-rule.md +0 -53
  65. package/docs/styling-guide/rules/component-variant-rule.md +0 -125
  66. package/docs/styling-guide/rules/global-stylesheet-rule.md +0 -67
  67. package/docs/styling-guide/rules/theme-and-utility-definition-rule.md +0 -86
  68. package/docs/styling-guide/styling-guide.md +0 -14
package/README.md CHANGED
@@ -1,14 +1,13 @@
1
1
  # pasika
2
2
 
3
- Reusable agent setup package, starting with Claude.
3
+ Reusable agent setup package.
4
4
 
5
- `pasika` stores portable agent setup assets that individual repos can apply or adapt. Today it focuses on Claude, with room for Codex and other agent-specific setup flows later.
5
+ `pasika` stores portable agent setup assets that individual repos can apply or adapt.
6
6
 
7
7
  ## Scope
8
8
 
9
9
  This repo intentionally starts narrow:
10
10
 
11
- - Claude first for v1
12
11
  - reusable agent setup assets only
13
12
  - shared docs at the repo root
14
13
  - no project-specific workflows or business logic
@@ -16,32 +15,21 @@ This repo intentionally starts narrow:
16
15
  ## Layout
17
16
 
18
17
  ```text
19
- claude/
20
- settings.base.json
21
- hooks/
22
- status-line/
23
- index.js
24
- notification.sh
25
- protect-files.sh
26
- scripts/
27
- render-settings.ts
28
18
  docs/
29
- claude/
30
- hooks.md
31
- scripts/
32
- pasika.ts
19
+ code-organization-guide/
20
+ documentation-guide/
21
+ styling-guide/
22
+ agent-conventions.md
23
+ eslint/
24
+ pasika/
25
+ rules/
33
26
  dist/
34
27
  ...
35
- vulyk.json
36
- AGENTS.md
37
- CLAUDE.md
38
28
  ```
39
29
 
40
30
  ## What belongs here
41
31
 
42
32
  - portable setup scripts
43
- - base settings templates
44
- - shared docs and generated `CLAUDE.md`
45
33
  - shared naming and layout conventions
46
34
  - reusable source-organization lint rules
47
35
 
@@ -52,56 +40,39 @@ CLAUDE.md
52
40
  - repository-specific plugin choices
53
41
  - scripts that depend on a specific app, CI setup, or codebase
54
42
 
55
- ## CLI
56
-
57
- The current entry point is:
58
-
59
- ```bash
60
- npx pasika claude
61
- ```
62
-
63
- Optional flags:
64
-
65
- - `--target-dir <path>` writes into another repo
66
- - `--force` replaces an existing generated file instead of merging
67
-
68
43
  ## Development
69
44
 
70
- The CLI source lives in TypeScript.
71
-
72
45
  ```bash
73
46
  npm run lint
74
47
  npm run typecheck
75
48
  npm run build
76
49
  ```
77
50
 
78
- `npm run build` uses `vulyk docs` to emit root `AGENTS.md` and `CLAUDE.md`.
79
-
80
- ## Recommended Integration
81
-
82
- Hooks and helper executables should come from `node_modules`.
51
+ ## ESLint Pasika Ruleset
83
52
 
84
- That gives us:
53
+ Pasika ships enforceable lint rules derived from its documentation. Each rule carries a `@see` annotation linking to its source doc so future audits can verify rule/doc alignment.
85
54
 
86
- - versioned reusable scripts
87
- - easy upgrades through the package manager
88
- - no manual copying of hook files into every repo
55
+ ### Usage with Zirka (recommended)
89
56
 
90
- `settings.json` is different. It still needs to exist in each project repo, because Claude Code does not give us a clean inheritance model for it.
57
+ Enable pasika through Zirka's `styleguide`:
91
58
 
92
- So the recommended pattern is:
59
+ ```ts
60
+ // eslint.config.ts
61
+ import { RuleSeverity, styleguide } from "zirka";
93
62
 
94
- 1. install `pasika` as a dev dependency
95
- 2. run `npx pasika claude`
96
- 3. generate or merge into `.claude/settings.json` in the project
97
- 4. point hook commands at `./node_modules/pasika/claude/...`
98
- 5. keep project-specific plugin, skill, and rule decisions in the project repo
63
+ const { eslintConfig } = styleguide({
64
+ next: RuleSeverity.Error,
65
+ node: RuleSeverity.Error,
66
+ typescript: RuleSeverity.Error,
67
+ pasika: RuleSeverity.Error,
68
+ });
99
69
 
100
- `npx pasika claude` also copies the packaged Claude hooks docs into `.claude/hooks/` in the target repo.
70
+ export default eslintConfig;
71
+ ```
101
72
 
102
- ## ESLint Pasika Ruleset
73
+ ### Usage without Zirka
103
74
 
104
- Pasika packages its source-organization rules separately from its own Node and TypeScript lint configuration. A consumer composes the exported ruleset with Zirka and enables any framework-specific Zirka blocks it needs.
75
+ Import the config directly and compose with your own:
105
76
 
106
77
  ```ts
107
78
  // eslint.config.ts
@@ -116,3 +87,17 @@ const { eslintConfig } = styleguide({
116
87
 
117
88
  export default [...((await eslintConfig) ?? []), pasikaConfig];
118
89
  ```
90
+
91
+ ### Rule → Doc Mapping
92
+
93
+ Every rule's source file declares the documentation it enforces. Use this table to trace a lint hit back to its reasoning:
94
+
95
+ | Rule | Enforces | Source Doc |
96
+ |---|---|---|
97
+ | `pasika/filename-case` | Smart/PascalCase vs dumb/kebab-case file names | `docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md` |
98
+ | `pasika/import-boundaries` | Relative vs `@/*` imports, layer boundaries | `docs/code-organization-guide/rules/exports-and-imports-rule.md` |
99
+ | `pasika/no-mixed-concerns` | One React component per `.tsx` file | `docs/code-organization-guide/rules/no-mixed-concerns-rule.md` |
100
+ | `pasika/no-arbitrary-tailwind` | No arbitrary `-[value]` Tailwind classes | `docs/styling-guide/rules/arbitrary-value-rule.md` |
101
+ | `pasika/enforce-cn-merge` | Use `cn()` not `+`/template literals; ≤5 classes per group | `docs/styling-guide/rules/class-composition-rule.md` |
102
+ | `pasika/enforce-cva-variant-props` | Use `VariantProps<typeof>` not manual union types | `docs/styling-guide/rules/component-variant-rule.md` |
103
+ | `pasika/enforce-barrel-exports` | Nested `index.ts` only re-exports the parent component | `docs/code-organization-guide/rules/folder-nesting-rule.md` |
@@ -1,2 +1,20 @@
1
+ /**
2
+ * Pasika ESLint Ruleset
3
+ *
4
+ * Each rule enforces a specific documentation Rule from the `docs/` tree.
5
+ * Every rule file carries a `@see` annotation linking to the source doc.
6
+ * This mapping is maintained so future agents can audit rule/doc alignment.
7
+ *
8
+ * Rule → Doc mapping:
9
+ *
10
+ * filename-case → docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md
11
+ * import-boundaries → docs/code-organization-guide/rules/exports-and-imports-rule.md
12
+ * no-mixed-concerns → docs/code-organization-guide/rules/no-mixed-concerns-rule.md
13
+ * no-arbitrary-tailwind → docs/styling-guide/rules/arbitrary-value-rule.md
14
+ * enforce-cn-merge → docs/styling-guide/rules/class-composition-rule.md
15
+ * enforce-cva-variant-props → docs/styling-guide/rules/component-variant-rule.md
16
+ * enforce-barrel-exports → docs/code-organization-guide/rules/folder-nesting-rule.md
17
+ * + docs/code-organization-guide/rules/exports-and-imports-rule.md
18
+ */
1
19
  import type { Linter } from "eslint";
2
20
  export declare const pasikaConfig: Linter.Config;
@@ -1,17 +1,32 @@
1
1
  import { filenameCaseRule } from "./rules/filename-case.js";
2
- import { organizationImportsRule } from "./rules/organization-imports.js";
2
+ import { importBoundariesRule } from "./rules/import-boundaries.js";
3
+ import { noMixedConcernsRule } from "./rules/no-mixed-concerns.js";
4
+ import { noArbitraryTailwindRule } from "./rules/no-arbitrary-tailwind.js";
5
+ import { enforceCnMergeRule } from "./rules/enforce-cn-merge.js";
6
+ import { enforceCvaVariantPropsRule } from "./rules/enforce-cva-variant-props.js";
7
+ import { enforceBarrelExportsRule } from "./rules/enforce-barrel-exports.js";
3
8
  export const pasikaConfig = {
4
9
  files: ["src/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}"],
5
10
  plugins: {
6
11
  pasika: {
7
12
  rules: {
8
13
  "filename-case": filenameCaseRule,
9
- "organization-imports": organizationImportsRule,
14
+ "import-boundaries": importBoundariesRule,
15
+ "no-mixed-concerns": noMixedConcernsRule,
16
+ "no-arbitrary-tailwind": noArbitraryTailwindRule,
17
+ "enforce-cn-merge": enforceCnMergeRule,
18
+ "enforce-cva-variant-props": enforceCvaVariantPropsRule,
19
+ "enforce-barrel-exports": enforceBarrelExportsRule,
10
20
  },
11
21
  },
12
22
  },
13
23
  rules: {
14
24
  "pasika/filename-case": "error",
15
- "pasika/organization-imports": "error",
25
+ "pasika/import-boundaries": "error",
26
+ "pasika/no-mixed-concerns": "error",
27
+ "pasika/no-arbitrary-tailwind": "error",
28
+ "pasika/enforce-cn-merge": "error",
29
+ "pasika/enforce-cva-variant-props": "error",
30
+ "pasika/enforce-barrel-exports": "error",
16
31
  },
17
32
  };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * ESLint rule: pasika/enforce-barrel-exports
3
+ *
4
+ * Enforces the "Folder Nesting Rule" — index.ts only re-exports parent component.
5
+ *
6
+ * @see docs/code-organization-guide/rules/folder-nesting-rule.md
7
+ */
8
+ import type { Rule } from "eslint";
9
+ export declare const enforceBarrelExportsRule: Rule.RuleModule;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * ESLint rule: pasika/enforce-barrel-exports
3
+ *
4
+ * Enforces the "Folder Nesting Rule" — index.ts only re-exports parent component.
5
+ *
6
+ * @see docs/code-organization-guide/rules/folder-nesting-rule.md
7
+ */
8
+ import path from "node:path";
9
+ import fs from "node:fs";
10
+ function isPascalCase(str) {
11
+ return /^[A-Z][A-Za-z0-9]*$/.test(str);
12
+ }
13
+ function isKebabCase(str) {
14
+ return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(str);
15
+ }
16
+ const SUPPORT_FOLDERS = new Set(["types", "schemas", "hooks", "constants", "utils", "config", "locales"]);
17
+ export const enforceBarrelExportsRule = {
18
+ meta: {
19
+ schema: [],
20
+ type: "problem",
21
+ docs: {
22
+ description: "Enforce index.ts barrels only re-export the parent component.",
23
+ },
24
+ },
25
+ create(context) {
26
+ const filename = context.filename;
27
+ if (!filename)
28
+ return {};
29
+ const baseName = path.basename(filename);
30
+ if (baseName !== "index.ts" && baseName !== "index.cts" && baseName !== "index.mts")
31
+ return {};
32
+ const dirPath = path.dirname(filename);
33
+ const folderName = path.basename(dirPath);
34
+ const parentFolderName = path.basename(path.dirname(dirPath));
35
+ if (SUPPORT_FOLDERS.has(folderName))
36
+ return {};
37
+ if (!isPascalCase(folderName) && !isKebabCase(folderName))
38
+ return {};
39
+ const matchingTsx = fs.existsSync(path.join(dirPath, `${folderName}.tsx`)) ? folderName : null;
40
+ if (!matchingTsx)
41
+ return {};
42
+ if (!isPascalCase(parentFolderName) && !isKebabCase(parentFolderName))
43
+ return {};
44
+ const reExportedNames = new Set();
45
+ return {
46
+ ExportNamedDeclaration(node) {
47
+ if (!node.source)
48
+ return;
49
+ for (const spec of node.specifiers) {
50
+ if (spec.exported.type === "Identifier") {
51
+ reExportedNames.add(spec.exported.name);
52
+ }
53
+ }
54
+ },
55
+ "Program:exit"() {
56
+ if (reExportedNames.size === 0)
57
+ return;
58
+ if (!reExportedNames.has(matchingTsx)) {
59
+ context.report({
60
+ loc: { line: 1, column: 0 },
61
+ message: `index.ts in "${folderName}/" must re-export "${matchingTsx}". ` +
62
+ "See docs/code-organization-guide/rules/folder-nesting-rule.md",
63
+ });
64
+ return;
65
+ }
66
+ const nonParentExports = [...reExportedNames].filter((n) => n !== matchingTsx);
67
+ if (nonParentExports.length > 0) {
68
+ context.report({
69
+ loc: { line: 1, column: 0 },
70
+ message: `index.ts must not re-export exclusive children: ${nonParentExports.join(", ")}. ` +
71
+ `Only "${matchingTsx}" may be re-exported. ` +
72
+ "See docs/code-organization-guide/rules/folder-nesting-rule.md",
73
+ });
74
+ }
75
+ },
76
+ };
77
+ },
78
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * ESLint rule: pasika/enforce-cn-merge
3
+ *
4
+ * Enforces the "Class Composition Rule".
5
+ *
6
+ * @see docs/styling-guide/rules/class-composition-rule.md
7
+ */
8
+ import type { Rule } from "eslint";
9
+ export declare const enforceCnMergeRule: Rule.RuleModule;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * ESLint rule: pasika/enforce-cn-merge
3
+ *
4
+ * Enforces the "Class Composition Rule".
5
+ *
6
+ * @see docs/styling-guide/rules/class-composition-rule.md
7
+ */
8
+ function classCount(str) {
9
+ return str.split(/\s+/).filter(Boolean).length;
10
+ }
11
+ export const enforceCnMergeRule = {
12
+ meta: {
13
+ schema: [],
14
+ type: "problem",
15
+ docs: {
16
+ description: "Enforce cn() for conditional class merging.",
17
+ },
18
+ },
19
+ create(context) {
20
+ return {
21
+ JSXAttribute(node) {
22
+ if (node.name.name !== "className" && node.name.name !== "class")
23
+ return;
24
+ const valueNode = node.value;
25
+ if (!valueNode)
26
+ return;
27
+ if (valueNode.type === "Literal" && typeof valueNode.value === "string") {
28
+ if (classCount(valueNode.value) > 5) {
29
+ context.report({
30
+ node,
31
+ message: "Static className with more than 5 classes must use cn() with grouped string literals. " +
32
+ "See docs/styling-guide/rules/class-composition-rule.md",
33
+ });
34
+ }
35
+ return;
36
+ }
37
+ if (valueNode.type !== "JSXExpressionContainer" || !valueNode.expression)
38
+ return;
39
+ const expr = valueNode.expression;
40
+ if (expr.type === "BinaryExpression" && expr.operator === "+") {
41
+ context.report({
42
+ node,
43
+ message: "Use cn() instead of + operator for className. " +
44
+ "See docs/styling-guide/rules/class-composition-rule.md",
45
+ });
46
+ return;
47
+ }
48
+ if (expr.type === "TemplateLiteral" && expr.expressions.length > 0) {
49
+ context.report({
50
+ node,
51
+ message: "Use cn() instead of template literals with conditionals for className. " +
52
+ "See docs/styling-guide/rules/class-composition-rule.md",
53
+ });
54
+ return;
55
+ }
56
+ if (expr.type === "Literal" && typeof expr.value === "string") {
57
+ if (classCount(expr.value) > 5) {
58
+ context.report({
59
+ node,
60
+ message: "Static className with more than 5 classes must use cn() with grouped string literals. " +
61
+ "See docs/styling-guide/rules/class-composition-rule.md",
62
+ });
63
+ }
64
+ return;
65
+ }
66
+ if (expr.type === "CallExpression" && expr.callee.type === "Identifier" && expr.callee.name === "cn") {
67
+ for (const arg of expr.arguments) {
68
+ if (arg.type === "Literal" && typeof arg.value === "string") {
69
+ if (classCount(arg.value) > 5) {
70
+ context.report({
71
+ node: arg,
72
+ message: "Each cn() string argument must contain at most 5 class names. " +
73
+ "Group by styling concern. " +
74
+ "See docs/styling-guide/rules/class-composition-rule.md",
75
+ });
76
+ }
77
+ }
78
+ }
79
+ }
80
+ },
81
+ };
82
+ },
83
+ };
84
+ /* eslint-enable -- re-enable rules disabled for AST access @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument */
@@ -0,0 +1,9 @@
1
+ /**
2
+ * ESLint rule: pasika/enforce-cva-variant-props
3
+ *
4
+ * Enforces the "Component Variant Rule" — VariantProps from CVA.
5
+ *
6
+ * @see docs/styling-guide/rules/component-variant-rule.md
7
+ */
8
+ import type { Rule } from "eslint";
9
+ export declare const enforceCvaVariantPropsRule: Rule.RuleModule;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * ESLint rule: pasika/enforce-cva-variant-props
3
+ *
4
+ * Enforces the "Component Variant Rule" — VariantProps from CVA.
5
+ *
6
+ * @see docs/styling-guide/rules/component-variant-rule.md
7
+ */
8
+ export const enforceCvaVariantPropsRule = {
9
+ meta: {
10
+ schema: [],
11
+ type: "problem",
12
+ docs: {
13
+ description: "Enforce VariantProps from CVA instead of manual variant union types.",
14
+ },
15
+ },
16
+ create(context) {
17
+ const cvaDefinitions = new Map();
18
+ return {
19
+ VariableDeclarator(node) {
20
+ if (node.init?.type === "CallExpression" &&
21
+ node.init.callee.type === "Identifier" &&
22
+ node.init.callee.name === "cva" &&
23
+ node.id.type === "Identifier") {
24
+ const args = node.init.arguments;
25
+ if (args.length < 2)
26
+ return;
27
+ const options = args[1];
28
+ if (options?.type === "ObjectExpression") {
29
+ for (const prop of options.properties) {
30
+ if (prop.type === "Property" &&
31
+ prop.key.type === "Identifier" &&
32
+ prop.key.name === "variants" &&
33
+ prop.value.type === "ObjectExpression") {
34
+ const variantNames = prop.value.properties
35
+ .filter((p) => p.type === "Property" && p.key.type === "Identifier")
36
+ .map((p) => p.key.name);
37
+ if (variantNames.length > 0) {
38
+ cvaDefinitions.set(node.id.name, variantNames);
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ },
45
+ TSTypeAliasDeclaration(node) {
46
+ if (!node.id.name.endsWith("Props"))
47
+ return;
48
+ if (node.typeAnnotation.type !== "TSTypeLiteral")
49
+ return;
50
+ for (const member of node.typeAnnotation.members) {
51
+ if (member.type === "TSPropertySignature" &&
52
+ member.key.type === "Identifier" &&
53
+ member.typeAnnotation?.typeAnnotation?.type === "TSUnionType") {
54
+ const keyName = String(member.key.name);
55
+ const typeAnn = member.typeAnnotation.typeAnnotation;
56
+ const allStringLiterals = typeAnn.types.every((t) => t.type === "TSLiteralType" && t.literal.type === "StringLiteral");
57
+ if (allStringLiterals && typeAnn.types.length >= 2) {
58
+ for (const [, variantNames] of cvaDefinitions) {
59
+ if (variantNames.includes(keyName)) {
60
+ context.report({
61
+ node: member,
62
+ message: `Variant prop "${keyName}" duplicates CVA variant values. ` +
63
+ "Use VariantProps<typeof variants> instead of manually writing union types. " +
64
+ "See docs/styling-guide/rules/component-variant-rule.md",
65
+ });
66
+ return;
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+ },
73
+ };
74
+ },
75
+ };
76
+ /* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any -- re-enable after AST node access block */
@@ -1,3 +1,13 @@
1
+ /**
2
+ * ESLint rule: pasika/filename-case
3
+ *
4
+ * Enforces the naming conventions from the "Smart vs Dumb Component Rule":
5
+ * - Smart components: PascalCase.tsx
6
+ * - Dumb components: kebab-case.tsx
7
+ * - Non-component files: kebab-case
8
+ *
9
+ * @see docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md
10
+ */
1
11
  import path from "node:path";
2
12
  /**
3
13
  * Next.js App Router routing files that are exempt from filename-case checks.
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "eslint";
2
+ export declare const importBoundariesRule: Rule.RuleModule;
@@ -1,3 +1,13 @@
1
+ /**
2
+ * ESLint rule: pasika/import-boundaries
3
+ *
4
+ * Enforces the import conventions from the "Exports and Imports Rule":
5
+ * - Relative imports for nearby files (same folder, subfolder, one level up).
6
+ * - @/* alias for imports beyond one folder up.
7
+ * - Layer boundary enforcement (app → compositions → features → shared → root).
8
+ *
9
+ * @see docs/code-organization-guide/rules/exports-and-imports-rule.md
10
+ */
1
11
  import path from "node:path";
2
12
  const sourceRoot = path.resolve("src");
3
13
  const rootSupportFolders = new Set(["config", "constants", "hooks", "locales", "schemas", "types", "utils"]);
@@ -26,7 +36,7 @@ function isNearbyImport(filename, resolvedPath) {
26
36
  const nonParentSegments = segments.length - parentTraversals;
27
37
  return (parentTraversals === 0 && nonParentSegments <= 2) || (parentTraversals === 1 && nonParentSegments <= 1);
28
38
  }
29
- export const organizationImportsRule = {
39
+ export const importBoundariesRule = {
30
40
  meta: {
31
41
  schema: [],
32
42
  type: "problem",
@@ -0,0 +1,9 @@
1
+ /**
2
+ * ESLint rule: pasika/no-arbitrary-tailwind
3
+ *
4
+ * Enforces the "Arbitrary Value Rule" — no arbitrary-value Tailwind classes.
5
+ *
6
+ * @see docs/styling-guide/rules/arbitrary-value-rule.md
7
+ */
8
+ import type { Rule } from "eslint";
9
+ export declare const noArbitraryTailwindRule: Rule.RuleModule;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * ESLint rule: pasika/no-arbitrary-tailwind
3
+ *
4
+ * Enforces the "Arbitrary Value Rule" — no arbitrary-value Tailwind classes.
5
+ *
6
+ * @see docs/styling-guide/rules/arbitrary-value-rule.md
7
+ */
8
+ /**
9
+ * Matches Tailwind arbitrary-value utility classes like rounded-[13px], text-[#fff].
10
+ *
11
+ * Does NOT match variant prefixes like min-[400px]:flex-row or md:flex.
12
+ * The negative lookahead (?![-\w]*:) excludes anything followed by a colon + more.
13
+ *
14
+ * Note: Tailwind breakpoint variants (sm:, md:, lg:, etc.) don't use brackets,
15
+ * so they're never matched. Only bracket-based variants like min-[400px]: need
16
+ * the lookahead exclusion.
17
+ */
18
+ const ARBITRARY_VALUE_RE = /(?:^|(?<=\s))(?:[a-z]+(?:-[a-z]+)*)-\[[^\]]+\](?![-\w]*:)/g;
19
+ export const noArbitraryTailwindRule = {
20
+ meta: {
21
+ schema: [],
22
+ type: "problem",
23
+ docs: {
24
+ description: "Disallow Tailwind arbitrary-value classes like rounded-[13px].",
25
+ },
26
+ },
27
+ create(context) {
28
+ function checkQuasis(node, quasis) {
29
+ for (const quasi of quasis) {
30
+ ARBITRARY_VALUE_RE.lastIndex = 0;
31
+ const match = ARBITRARY_VALUE_RE.exec(quasi.value.raw);
32
+ if (match) {
33
+ context.report({
34
+ node,
35
+ message: `Tailwind arbitrary-value class "${match[0]}" is not allowed. ` +
36
+ "Use a named token or custom utility. " +
37
+ "See docs/styling-guide/rules/arbitrary-value-rule.md",
38
+ });
39
+ }
40
+ }
41
+ }
42
+ return {
43
+ JSXAttribute(node) {
44
+ if (node.name.name !== "className" && node.name.name !== "class")
45
+ return;
46
+ const valueNode = node.value;
47
+ if (!valueNode)
48
+ return;
49
+ if (valueNode.type === "Literal" && typeof valueNode.value === "string") {
50
+ ARBITRARY_VALUE_RE.lastIndex = 0;
51
+ const match = ARBITRARY_VALUE_RE.exec(valueNode.value);
52
+ if (match) {
53
+ context.report({
54
+ node,
55
+ message: `Tailwind arbitrary-value class "${match[0]}" is not allowed. ` +
56
+ "Use a named token or custom utility. " +
57
+ "See docs/styling-guide/rules/arbitrary-value-rule.md",
58
+ });
59
+ }
60
+ }
61
+ else if (valueNode.type === "JSXExpressionContainer" && valueNode.expression) {
62
+ const expr = valueNode.expression;
63
+ if (expr.type === "Literal" && typeof expr.value === "string") {
64
+ ARBITRARY_VALUE_RE.lastIndex = 0;
65
+ const match = ARBITRARY_VALUE_RE.exec(expr.value);
66
+ if (match) {
67
+ context.report({
68
+ node,
69
+ message: `Tailwind arbitrary-value class "${match[0]}" is not allowed. ` +
70
+ "Use a named token or custom utility. " +
71
+ "See docs/styling-guide/rules/arbitrary-value-rule.md",
72
+ });
73
+ }
74
+ }
75
+ else if (expr.type === "TemplateLiteral") {
76
+ checkQuasis(node, expr.quasis);
77
+ }
78
+ }
79
+ },
80
+ CallExpression(node) {
81
+ if (node.callee.type !== "Identifier")
82
+ return;
83
+ const calleeName = node.callee.name;
84
+ if (calleeName !== "cn" && calleeName !== "clsx" && calleeName !== "twMerge" && calleeName !== "twJoin")
85
+ return;
86
+ for (const arg of node.arguments) {
87
+ if (arg.type === "Literal" && typeof arg.value === "string") {
88
+ ARBITRARY_VALUE_RE.lastIndex = 0;
89
+ const match = ARBITRARY_VALUE_RE.exec(arg.value);
90
+ if (match) {
91
+ context.report({
92
+ node,
93
+ message: `Tailwind arbitrary-value class "${match[0]}" is not allowed. ` +
94
+ "Use a named token or custom utility. " +
95
+ "See docs/styling-guide/rules/arbitrary-value-rule.md",
96
+ });
97
+ }
98
+ }
99
+ else if (arg.type === "TemplateLiteral") {
100
+ checkQuasis(node, arg.quasis);
101
+ }
102
+ }
103
+ },
104
+ };
105
+ },
106
+ };
107
+ /* eslint-enable -- re-enable rules disabled for AST access @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument */
@@ -0,0 +1,9 @@
1
+ /**
2
+ * ESLint rule: pasika/no-mixed-concerns
3
+ *
4
+ * Enforces the "No Mixed Concerns Rule" — one React component per .tsx file.
5
+ *
6
+ * @see docs/code-organization-guide/rules/no-mixed-concerns-rule.md
7
+ */
8
+ import type { Rule } from "eslint";
9
+ export declare const noMixedConcernsRule: Rule.RuleModule;