pasika 0.1.1 → 0.1.6

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 (58) hide show
  1. package/README.md +31 -25
  2. package/claude/hooks/.vulyk +3 -0
  3. package/claude/hooks/AGENTS.md +3 -0
  4. package/claude/hooks/claude-hooks.md +30 -0
  5. package/claude/{.claude/hooks → hooks}/notification.sh +0 -0
  6. package/claude/{.claude/hooks → hooks}/protect-files.sh +0 -0
  7. package/claude/scripts/render-settings.ts +25 -9
  8. package/dist/claude/scripts/render-settings.js +22 -9
  9. package/dist/eslint/pasika/index.d.ts +2 -0
  10. package/dist/eslint/pasika/index.js +17 -0
  11. package/dist/eslint/pasika/rules/filename-case.d.ts +2 -0
  12. package/dist/eslint/pasika/rules/filename-case.js +90 -0
  13. package/dist/eslint/pasika/rules/organization-imports.d.ts +2 -0
  14. package/dist/eslint/pasika/rules/organization-imports.js +124 -0
  15. package/docs/agent-conventions.md +27 -0
  16. package/docs/code-organization-guide/code-organization-guide.md +69 -0
  17. package/docs/code-organization-guide/references/application-architecture-reference.md +149 -0
  18. package/docs/code-organization-guide/rules/component-placement-rule.md +119 -0
  19. package/docs/code-organization-guide/rules/configuration-rule.md +42 -0
  20. package/docs/code-organization-guide/rules/constants-rule.md +74 -0
  21. package/docs/code-organization-guide/rules/exports-and-imports-rule.md +84 -0
  22. package/docs/code-organization-guide/rules/folder-nesting-rule.md +82 -0
  23. package/docs/code-organization-guide/rules/hook-extraction-rule.md +141 -0
  24. package/docs/code-organization-guide/rules/interactive-component-rule.md +97 -0
  25. package/docs/code-organization-guide/rules/jsx-hygiene-rule.md +67 -0
  26. package/docs/code-organization-guide/rules/locales-rule.md +53 -0
  27. package/docs/code-organization-guide/rules/nameable-visual-concept-rule.md +66 -0
  28. package/docs/code-organization-guide/rules/no-mixed-concerns-rule.md +63 -0
  29. package/docs/code-organization-guide/rules/repeated-structure-rule.md +93 -0
  30. package/docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md +112 -0
  31. package/docs/code-organization-guide/rules/sole-state-owner-rule.md +101 -0
  32. package/docs/code-organization-guide/rules/types-and-schemas-rule.md +139 -0
  33. package/docs/code-organization-guide/rules/utilities-rule.md +86 -0
  34. package/docs/documentation-guide/_templates/grouped-reference.md +11 -0
  35. package/docs/documentation-guide/_templates/guide.md +19 -0
  36. package/docs/documentation-guide/_templates/rule.md +21 -0
  37. package/docs/documentation-guide/_templates/single-lookup-reference.md +5 -0
  38. package/docs/documentation-guide/documentation-guide.md +13 -0
  39. package/docs/documentation-guide/references/documentation-types-reference.md +9 -0
  40. package/docs/documentation-guide/rules/guide-creation-rule.md +113 -0
  41. package/docs/documentation-guide/rules/reference-creation-rule.md +132 -0
  42. package/docs/documentation-guide/rules/rule-creation-rule.md +81 -0
  43. package/docs/documentation-guide/rules/template-usage-rule.md +49 -0
  44. package/docs/shadcn-theme.md +121 -0
  45. package/docs/styling-guide/rules/arbitrary-value-rule.md +31 -0
  46. package/docs/styling-guide/rules/class-composition-rule.md +52 -0
  47. package/docs/styling-guide/rules/component-ui-state-rule.md +53 -0
  48. package/docs/styling-guide/rules/component-variant-rule.md +125 -0
  49. package/docs/styling-guide/rules/global-stylesheet-rule.md +67 -0
  50. package/docs/styling-guide/rules/theme-and-utility-definition-rule.md +86 -0
  51. package/docs/styling-guide/styling-guide.md +14 -0
  52. package/package.json +22 -12
  53. package/AGENTS.md +0 -15
  54. package/docs/common/merge-behavior.md +0 -17
  55. package/docs/common/overview.md +0 -20
  56. /package/{CLAUDE.md → claude/hooks/CLAUDE.md} +0 -0
  57. /package/claude/{.claude/hooks → hooks}/status-line/index.js +0 -0
  58. /package/claude/{.claude/settings.base.json → settings.base.json} +0 -0
package/README.md CHANGED
@@ -1,35 +1,31 @@
1
1
  # pasika
2
2
 
3
- Reusable base assets for Claude Code setups.
3
+ Reusable agent setup package, starting with Claude.
4
4
 
5
- `pasika` stores portable Claude base config assets that individual repos can copy from or adapt.
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.
6
6
 
7
7
  ## Scope
8
8
 
9
9
  This repo intentionally starts narrow:
10
10
 
11
- - Claude only for v1
12
- - reusable base config only
11
+ - Claude first for v1
12
+ - reusable agent setup assets only
13
13
  - shared docs at the repo root
14
- - no project-specific rules, workflows, or business logic
14
+ - no project-specific workflows or business logic
15
15
 
16
16
  ## Layout
17
17
 
18
18
  ```text
19
19
  claude/
20
+ settings.base.json
21
+ hooks/
22
+ status-line/
23
+ index.js
24
+ notification.sh
25
+ protect-files.sh
20
26
  scripts/
21
27
  render-settings.ts
22
- .claude/
23
- settings.base.json
24
- hooks/
25
- status-line/
26
- index.js
27
- notification.sh
28
- protect-files.sh
29
28
  docs/
30
- common/
31
- overview.md
32
- merge-behavior.md
33
29
  claude/
34
30
  hooks.md
35
31
  scripts/
@@ -43,21 +39,22 @@ CLAUDE.md
43
39
 
44
40
  ## What belongs here
45
41
 
46
- - portable hook scripts
42
+ - portable setup scripts
47
43
  - base settings templates
48
44
  - shared docs and generated `CLAUDE.md`
49
45
  - shared naming and layout conventions
46
+ - reusable source-organization lint rules
50
47
 
51
48
  ## What stays in project repos
52
49
 
53
- - final `.claude` folders
54
- - project-specific skills, rules, agents, and prompts
50
+ - final agent config folders in project repos
51
+ - project-specific rules, agents, and prompts
55
52
  - repository-specific plugin choices
56
53
  - scripts that depend on a specific app, CI setup, or codebase
57
54
 
58
55
  ## CLI
59
56
 
60
- The main entry point is:
57
+ The current entry point is:
61
58
 
62
59
  ```bash
63
60
  npx pasika claude
@@ -100,13 +97,22 @@ So the recommended pattern is:
100
97
  4. point hook commands at `./node_modules/pasika/claude/...`
101
98
  5. keep project-specific plugin, skill, and rule decisions in the project repo
102
99
 
103
- ## Merge Behavior
100
+ `npx pasika claude` also copies the packaged Claude hooks docs into `.claude/hooks/` in the target repo.
104
101
 
105
- By default, `pasika` merges its Claude base into an existing `.claude/settings.json`.
102
+ ## ESLint Pasika Ruleset
106
103
 
107
- That means:
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.
108
105
 
109
- - it updates the portable base pieces shipped by `pasika`
110
- - it preserves unrelated project-specific settings such as extra hooks, plugin config, and custom permissions
106
+ ```ts
107
+ // eslint.config.ts
108
+ import { pasikaConfig } from "pasika/eslint";
109
+ import { RuleSeverity, styleguide } from "zirka";
111
110
 
112
- Use `--force` only when you want to replace the existing file with the `pasika` base output.
111
+ const { eslintConfig } = styleguide({
112
+ next: RuleSeverity.Error,
113
+ node: RuleSeverity.Error,
114
+ typescript: RuleSeverity.Error,
115
+ });
116
+
117
+ export default [...((await eslintConfig) ?? []), pasikaConfig];
118
+ ```
@@ -0,0 +1,3 @@
1
+ 🍯 AGENTS.md
2
+ 🍯 CLAUDE.md
3
+ 🍯 claude-hooks.md
@@ -0,0 +1,3 @@
1
+ # Claude Hooks
2
+ Shared Claude hooks shipped by pasika.
3
+ Full documentation: docs/claude/hooks.md
@@ -0,0 +1,30 @@
1
+ # Claude Hooks
2
+
3
+ `pasika` ships the reusable shared Claude hooks.
4
+
5
+ ## `status-line/index.js`
6
+
7
+ Displays a two-line status view with:
8
+
9
+ - model name
10
+ - current folder
11
+ - git branch when available
12
+ - used context percentage as a 10-segment bar
13
+ - session cost
14
+ - session duration
15
+
16
+ ## `notification.sh`
17
+
18
+ Sends a desktop notification when Claude needs attention.
19
+
20
+ - macOS: `terminal-notifier` with click-to-focus VS Code behavior, falls back to `osascript`
21
+ - Linux: `notify-send`
22
+ - Windows: PowerShell message box
23
+
24
+ ## `protect-files.sh`
25
+
26
+ Blocks edits to:
27
+
28
+ - `.env` files
29
+ - `package-lock.json`
30
+ - files inside `.git/`
File without changes
File without changes
@@ -1,4 +1,4 @@
1
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { access, copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { constants } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
@@ -107,9 +107,9 @@ const mergeClaudeSettings = (
107
107
  baseSettings: ClaudeSettings,
108
108
  packagePath: string,
109
109
  ): ClaudeSettings => {
110
- const notificationCommand = `${packagePath}/.claude/hooks/notification.sh`;
111
- const protectFilesCommand = `${packagePath}/.claude/hooks/protect-files.sh`;
112
- const statusLineCommand = `node ${packagePath}/.claude/hooks/status-line/index.js`;
110
+ const notificationCommand = `${packagePath}/hooks/notification.sh`;
111
+ const protectFilesCommand = `${packagePath}/hooks/protect-files.sh`;
112
+ const statusLineCommand = `node ${packagePath}/hooks/status-line/index.js`;
113
113
 
114
114
  return {
115
115
  ...existingSettings,
@@ -136,18 +136,18 @@ const mergeClaudeSettings = (
136
136
  };
137
137
 
138
138
  export const renderClaude = async (targetDir: string, force: boolean, packageRoot: string): Promise<string[]> => {
139
- const templatePath = path.join(packageRoot, "claude/.claude/settings.base.json");
139
+ const templatePath = path.join(packageRoot, "claude/settings.base.json");
140
140
  const baseSettings = await readClaudeSettings(templatePath);
141
141
  const packagePath = "./node_modules/pasika/claude";
142
142
  const notificationEntries = upsertHookMatcherEntry(
143
143
  baseSettings.hooks.Notification,
144
144
  "",
145
- `${packagePath}/.claude/hooks/notification.sh`,
145
+ `${packagePath}/hooks/notification.sh`,
146
146
  );
147
147
  const preToolUseEntries = upsertHookMatcherEntry(
148
148
  baseSettings.hooks.PreToolUse,
149
149
  "Edit|Write",
150
- `${packagePath}/.claude/hooks/protect-files.sh`,
150
+ `${packagePath}/hooks/protect-files.sh`,
151
151
  );
152
152
  const settings: ClaudeSettings = {
153
153
  ...baseSettings,
@@ -159,14 +159,16 @@ export const renderClaude = async (targetDir: string, force: boolean, packageRoo
159
159
  statusLine: {
160
160
  ...baseSettings.statusLine,
161
161
  type: "command",
162
- command: `node ${packagePath}/.claude/hooks/status-line/index.js`,
162
+ command: `node ${packagePath}/hooks/status-line/index.js`,
163
163
  },
164
164
  };
165
165
 
166
166
  const outputDir = path.join(targetDir, ".claude");
167
167
  const outputPath = path.join(outputDir, "settings.json");
168
+ const hooksOutputDir = path.join(outputDir, "hooks");
168
169
 
169
170
  await mkdir(outputDir, { recursive: true });
171
+ await mkdir(hooksOutputDir, { recursive: true });
170
172
 
171
173
  const finalSettings =
172
174
  !force && (await fileExists(outputPath))
@@ -175,7 +177,21 @@ export const renderClaude = async (targetDir: string, force: boolean, packageRoo
175
177
 
176
178
  await writeFile(outputPath, `${JSON.stringify(finalSettings, null, 2)}\n`, "utf8");
177
179
 
178
- return [outputPath];
180
+ const outputs = [outputPath];
181
+ const hookDocFiles = ["AGENTS.md", "CLAUDE.md"] as const;
182
+
183
+ for (const fileName of hookDocFiles) {
184
+ const sourcePath = path.join(packageRoot, "claude/hooks", fileName);
185
+ if (!(await fileExists(sourcePath))) {
186
+ continue;
187
+ }
188
+
189
+ const destinationPath = path.join(hooksOutputDir, fileName);
190
+ await copyFile(sourcePath, destinationPath);
191
+ outputs.push(destinationPath);
192
+ }
193
+
194
+ return outputs;
179
195
  };
180
196
 
181
197
  const isDirectRun = process.argv[1] ? path.resolve(process.argv[1]) === __filename : false;
@@ -1,4 +1,4 @@
1
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { access, copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { constants } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
@@ -52,9 +52,9 @@ const upsertHookMatcherEntry = (entries, matcher, command) => {
52
52
  return nextEntries;
53
53
  };
54
54
  const mergeClaudeSettings = (existingSettings, baseSettings, packagePath) => {
55
- const notificationCommand = `${packagePath}/.claude/hooks/notification.sh`;
56
- const protectFilesCommand = `${packagePath}/.claude/hooks/protect-files.sh`;
57
- const statusLineCommand = `node ${packagePath}/.claude/hooks/status-line/index.js`;
55
+ const notificationCommand = `${packagePath}/hooks/notification.sh`;
56
+ const protectFilesCommand = `${packagePath}/hooks/protect-files.sh`;
57
+ const statusLineCommand = `node ${packagePath}/hooks/status-line/index.js`;
58
58
  return {
59
59
  ...existingSettings,
60
60
  $schema: baseSettings.$schema ?? existingSettings.$schema,
@@ -79,11 +79,11 @@ const mergeClaudeSettings = (existingSettings, baseSettings, packagePath) => {
79
79
  };
80
80
  };
81
81
  export const renderClaude = async (targetDir, force, packageRoot) => {
82
- const templatePath = path.join(packageRoot, "claude/.claude/settings.base.json");
82
+ const templatePath = path.join(packageRoot, "claude/settings.base.json");
83
83
  const baseSettings = await readClaudeSettings(templatePath);
84
84
  const packagePath = "./node_modules/pasika/claude";
85
- const notificationEntries = upsertHookMatcherEntry(baseSettings.hooks.Notification, "", `${packagePath}/.claude/hooks/notification.sh`);
86
- const preToolUseEntries = upsertHookMatcherEntry(baseSettings.hooks.PreToolUse, "Edit|Write", `${packagePath}/.claude/hooks/protect-files.sh`);
85
+ const notificationEntries = upsertHookMatcherEntry(baseSettings.hooks.Notification, "", `${packagePath}/hooks/notification.sh`);
86
+ const preToolUseEntries = upsertHookMatcherEntry(baseSettings.hooks.PreToolUse, "Edit|Write", `${packagePath}/hooks/protect-files.sh`);
87
87
  const settings = {
88
88
  ...baseSettings,
89
89
  hooks: {
@@ -94,17 +94,30 @@ export const renderClaude = async (targetDir, force, packageRoot) => {
94
94
  statusLine: {
95
95
  ...baseSettings.statusLine,
96
96
  type: "command",
97
- command: `node ${packagePath}/.claude/hooks/status-line/index.js`,
97
+ command: `node ${packagePath}/hooks/status-line/index.js`,
98
98
  },
99
99
  };
100
100
  const outputDir = path.join(targetDir, ".claude");
101
101
  const outputPath = path.join(outputDir, "settings.json");
102
+ const hooksOutputDir = path.join(outputDir, "hooks");
102
103
  await mkdir(outputDir, { recursive: true });
104
+ await mkdir(hooksOutputDir, { recursive: true });
103
105
  const finalSettings = !force && (await fileExists(outputPath))
104
106
  ? mergeClaudeSettings(await readClaudeSettings(outputPath), settings, packagePath)
105
107
  : settings;
106
108
  await writeFile(outputPath, `${JSON.stringify(finalSettings, null, 2)}\n`, "utf8");
107
- return [outputPath];
109
+ const outputs = [outputPath];
110
+ const hookDocFiles = ["AGENTS.md", "CLAUDE.md"];
111
+ for (const fileName of hookDocFiles) {
112
+ const sourcePath = path.join(packageRoot, "claude/hooks", fileName);
113
+ if (!(await fileExists(sourcePath))) {
114
+ continue;
115
+ }
116
+ const destinationPath = path.join(hooksOutputDir, fileName);
117
+ await copyFile(sourcePath, destinationPath);
118
+ outputs.push(destinationPath);
119
+ }
120
+ return outputs;
108
121
  };
109
122
  const isDirectRun = process.argv[1] ? path.resolve(process.argv[1]) === __filename : false;
110
123
  if (isDirectRun) {
@@ -0,0 +1,2 @@
1
+ import type { Linter } from "eslint";
2
+ export declare const pasikaConfig: Linter.Config;
@@ -0,0 +1,17 @@
1
+ import { filenameCaseRule } from "./rules/filename-case.js";
2
+ import { organizationImportsRule } from "./rules/organization-imports.js";
3
+ export const pasikaConfig = {
4
+ files: ["src/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}"],
5
+ plugins: {
6
+ pasika: {
7
+ rules: {
8
+ "filename-case": filenameCaseRule,
9
+ "organization-imports": organizationImportsRule,
10
+ },
11
+ },
12
+ },
13
+ rules: {
14
+ "pasika/filename-case": "error",
15
+ "pasika/organization-imports": "error",
16
+ },
17
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "eslint";
2
+ export declare const filenameCaseRule: Rule.RuleModule;
@@ -0,0 +1,90 @@
1
+ import path from "node:path";
2
+ /**
3
+ * Next.js App Router routing files that are exempt from filename-case checks.
4
+ * https://nextjs.org/docs/app/getting-started/project-structure#routing-files
5
+ */
6
+ const NEXT_ROUTING_FILES = new Set([
7
+ "page",
8
+ "layout",
9
+ "loading",
10
+ "error",
11
+ "not-found",
12
+ "route",
13
+ "template",
14
+ "default",
15
+ "middleware",
16
+ "instrumentation",
17
+ ]);
18
+ /** Suffixes that form a compound extension with the real extension (e.g. .example.tsx, .test.ts). */
19
+ const COMPOUND_SUFFIXES = new Set(["example", "test", "spec", "stories"]);
20
+ /**
21
+ * Checks whether a string is in kebab-case.
22
+ * Allows lowercase letters, digits, and hyphens (but not leading/trailing/double hyphens).
23
+ */
24
+ function isKebabCase(str) {
25
+ return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(str);
26
+ }
27
+ /**
28
+ * Checks whether a string is in PascalCase.
29
+ * Starts with an uppercase letter, followed by alphanumeric characters.
30
+ */
31
+ function isPascalCase(str) {
32
+ return /^[A-Z][A-Za-z0-9]*$/.test(str);
33
+ }
34
+ export const filenameCaseRule = {
35
+ meta: {
36
+ schema: [],
37
+ type: "problem",
38
+ docs: {
39
+ description: "Enforce pasika filename conventions: kebab-case by default, PascalCase for smart .tsx components.",
40
+ },
41
+ },
42
+ create(context) {
43
+ const filename = context.filename;
44
+ if (!filename) {
45
+ return {};
46
+ }
47
+ // Only check files under src/
48
+ const normalized = filename.replace(/\\/g, "/");
49
+ if (!normalized.includes("/src/")) {
50
+ return {};
51
+ }
52
+ const ext = path.extname(filename);
53
+ let base = path.basename(filename, ext);
54
+ // Strip compound suffix (e.g. ".example", ".test", ".stories", ".spec") to get the real base name
55
+ const baseExt = path.extname(base);
56
+ if (baseExt && COMPOUND_SUFFIXES.has(baseExt.slice(1))) {
57
+ base = path.basename(base, baseExt);
58
+ }
59
+ // Next.js routing files are exempt
60
+ if (NEXT_ROUTING_FILES.has(base)) {
61
+ return {};
62
+ }
63
+ // .tsx components may be PascalCase (smart) or kebab-case (dumb)
64
+ if (ext === ".tsx") {
65
+ if (!(isPascalCase(base) || isKebabCase(base))) {
66
+ return report(context, filename, ext);
67
+ }
68
+ return {};
69
+ }
70
+ // Everything else must be kebab-case
71
+ if (!isKebabCase(base)) {
72
+ return report(context, filename, ext);
73
+ }
74
+ return {};
75
+ },
76
+ };
77
+ function report(context, filename, ext) {
78
+ return {
79
+ Program(node) {
80
+ const convention = ext === ".tsx"
81
+ ? "Component files must be PascalCase.tsx (smart) or kebab-case.tsx (dumb)."
82
+ : "Non-component files must use kebab-case.";
83
+ context.report({
84
+ node,
85
+ loc: { line: 1, column: 0 },
86
+ message: `Filename "${path.basename(filename)}" does not match pasika conventions. ${convention}`,
87
+ });
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "eslint";
2
+ export declare const organizationImportsRule: Rule.RuleModule;
@@ -0,0 +1,124 @@
1
+ import path from "node:path";
2
+ const sourceRoot = path.resolve("src");
3
+ const rootSupportFolders = new Set(["config", "constants", "hooks", "locales", "schemas", "types", "utils"]);
4
+ const moduleExtensions = new Set([".cjs", ".cts", ".js", ".jsx", ".mjs", ".mts", ".ts", ".tsx"]);
5
+ const styleExtensions = new Set([".css", ".less", ".sass", ".scss"]);
6
+ function resolveSourceImport(filename, importPath) {
7
+ if (importPath.startsWith("@/")) {
8
+ return path.resolve(sourceRoot, importPath.slice(2));
9
+ }
10
+ if (importPath.startsWith(".")) {
11
+ return path.resolve(path.dirname(filename), importPath);
12
+ }
13
+ return undefined;
14
+ }
15
+ function sourceSegments(absolutePath) {
16
+ const relativePath = path.relative(sourceRoot, absolutePath);
17
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
18
+ return undefined;
19
+ }
20
+ return relativePath.split(path.sep);
21
+ }
22
+ function isNearbyImport(filename, resolvedPath) {
23
+ const relativePath = path.relative(path.dirname(filename), resolvedPath);
24
+ const segments = relativePath.split(path.sep).filter((segment) => segment !== ".");
25
+ const parentTraversals = segments.filter((segment) => segment === "..").length;
26
+ const nonParentSegments = segments.length - parentTraversals;
27
+ return (parentTraversals === 0 && nonParentSegments <= 2) || (parentTraversals === 1 && nonParentSegments <= 1);
28
+ }
29
+ export const organizationImportsRule = {
30
+ meta: {
31
+ schema: [],
32
+ type: "problem",
33
+ },
34
+ create(context) {
35
+ const filename = context.filename;
36
+ if (!filename) {
37
+ return {};
38
+ }
39
+ function reportImport(source) {
40
+ if (typeof source.value !== "string") {
41
+ return;
42
+ }
43
+ const importPath = source.value;
44
+ const resolvedPath = resolveSourceImport(filename, importPath);
45
+ if (!resolvedPath) {
46
+ return;
47
+ }
48
+ const importer = sourceSegments(filename);
49
+ const imported = sourceSegments(resolvedPath);
50
+ if (!importer || !imported || importer.length === 0 || imported.length === 0) {
51
+ return;
52
+ }
53
+ if (isNearbyImport(filename, resolvedPath) && importPath.startsWith("@/")) {
54
+ context.report({
55
+ node: source,
56
+ message: "Use a relative path for imports in the same folder, a descendant, or one folder up.",
57
+ });
58
+ }
59
+ if (!isNearbyImport(filename, resolvedPath) && importPath.startsWith(".")) {
60
+ context.report({
61
+ node: source,
62
+ message: "Use the @/* alias for imports beyond one folder up.",
63
+ });
64
+ }
65
+ const [importerLayer = "", importerFeature] = importer;
66
+ const [importedLayer = "", importedFeature] = imported;
67
+ const extension = path.extname(importPath);
68
+ const isCodeModule = !extension || moduleExtensions.has(extension);
69
+ const isAppLocalStyleImport = importerLayer === "app" && importedLayer === "app" && styleExtensions.has(extension);
70
+ const importedIsRootSupport = rootSupportFolders.has(importedLayer);
71
+ const importedIsSameConfig = importerLayer === "config" && importedLayer === "config" && importer[1] === imported[1];
72
+ const isAllowed = (!isCodeModule && importerLayer === "app" && isAppLocalStyleImport) ||
73
+ (isCodeModule &&
74
+ importerLayer === "app" &&
75
+ (importedLayer === "compositions" ||
76
+ importedLayer === "features" ||
77
+ importedLayer === "shared" ||
78
+ importedIsRootSupport)) ||
79
+ (isCodeModule &&
80
+ importerLayer === "compositions" &&
81
+ (importedLayer === "compositions" ||
82
+ importedLayer === "features" ||
83
+ importedLayer === "shared" ||
84
+ importedIsRootSupport)) ||
85
+ (isCodeModule &&
86
+ importerLayer === "features" &&
87
+ ((importedLayer === "features" && importerFeature === importedFeature) ||
88
+ importedLayer === "shared" ||
89
+ importedIsRootSupport)) ||
90
+ (isCodeModule && importerLayer === "shared" && (importedLayer === "shared" || importedIsRootSupport)) ||
91
+ (isCodeModule &&
92
+ rootSupportFolders.has(importerLayer) &&
93
+ importedIsRootSupport &&
94
+ (importerLayer !== "config" || importedLayer !== "config" || importedIsSameConfig));
95
+ if (!isAllowed) {
96
+ context.report({
97
+ node: source,
98
+ message: "This import violates the src layer boundary.",
99
+ });
100
+ }
101
+ }
102
+ return {
103
+ ImportDeclaration(node) {
104
+ reportImport(node.source);
105
+ },
106
+ ExportAllDeclaration(node) {
107
+ reportImport(node.source);
108
+ },
109
+ ExportNamedDeclaration(node) {
110
+ if (node.source) {
111
+ reportImport(node.source);
112
+ }
113
+ },
114
+ ImportExpression(node) {
115
+ reportImport(node.source);
116
+ },
117
+ CallExpression(node) {
118
+ if (node.callee.type === "Identifier" && node.callee.name === "require" && node.arguments[0]) {
119
+ reportImport(node.arguments[0]);
120
+ }
121
+ },
122
+ };
123
+ },
124
+ };
@@ -0,0 +1,27 @@
1
+ # Agent Conventions
2
+
3
+ Repo-wide engineering rules and commit hygiene for AI agents.
4
+
5
+ ## Data Contracts
6
+
7
+ - Use Zod schemas for runtime validation; prioritize them over ad hoc type guards.
8
+ - Prefer strict-by-default data contracts. Make fields required unless real runtime evidence shows they must be nullable or optional.
9
+ - Do not preserve backward compatibility by default. Prefer simplifying and updating all in-repo call sites unless the user explicitly asks for compatibility.
10
+
11
+ ## Code Quality
12
+
13
+ - Never use `eslint-disable` directives. Fix issues properly.
14
+ - Never commit with the `--no-verify` flag.
15
+
16
+ ## Browser Verification
17
+
18
+ - Use `agent-browser` to verify browser behavior when a task requires it.
19
+ - Before using `agent-browser`, run `agent-browser upgrade`.
20
+ - Run `agent-browser --help` for available commands and `agent-browser <command> --help` for command-specific usage.
21
+
22
+ ## Vulyk
23
+
24
+ - Make Vulyk registry changes with Vulyk commands, not by editing generated files.
25
+ - Treat `AGENTS.md`, `docs/external`, `.agents/skills`, and `.vulyk` as generated output.
26
+ - Use ephemeral commands such as `npx vulyk@latest ...`.
27
+ - Do not add Vulyk to `package.json` only to run the CLI.
@@ -0,0 +1,69 @@
1
+ # Code Organization Guide
2
+
3
+ This guide covers how to organize code in the repository's `src/` tree — placement, extraction, and module conventions — so structure stays consistent across contributors and reviewers.
4
+
5
+ ## How To Organize a Component
6
+
7
+ Use this no matter whether you are adding a new component or extracting from existing code; the steps below apply in any case.
8
+
9
+ 1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to identify where the item belongs in `src/`.
10
+ 2. Pick the component's placement per the [Component Placement Rule](rules/component-placement-rule.md).
11
+ 3. Classify the component as smart or dumb per the [Smart vs Dumb Component Rule](rules/smart-vs-dumb-component-rule.md).
12
+ 4. Keep each component file to exactly one React component per the [No Mixed Concerns Rule](rules/no-mixed-concerns-rule.md).
13
+ 5. Extract interactive elements per the [Interactive Component Rule](rules/interactive-component-rule.md).
14
+ 6. Extract sole-state-owner blocks per the [Sole State Owner Rule](rules/sole-state-owner-rule.md).
15
+ 7. Extract repeated structures per the [Repeated Structure Rule](rules/repeated-structure-rule.md).
16
+ 8. Extract a group of elements when one clear component name describes it per the [Nameable Visual Concept Rule](rules/nameable-visual-concept-rule.md).
17
+ 9. For every component extracted in steps 5–8, repeat steps 2–4.
18
+ 10. Repeat steps 5–8 for an extracted component only when one of those extraction conditions applies to its JSX; stop when no new component is required.
19
+ 11. Nest a component when it gains exclusive children per the [Folder Nesting Rule](rules/folder-nesting-rule.md).
20
+ 12. Keep the component's JSX clean per the [JSX Hygiene Rule](rules/jsx-hygiene-rule.md).
21
+ 13. Follow the [Exports and Imports Rule](rules/exports-and-imports-rule.md) so every component module has predictable exports and import paths.
22
+
23
+ ## How To Organize a Type or Schema
24
+
25
+ Use this when adding or moving a type or schema.
26
+
27
+ 1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to identify where the item belongs in `src/`.
28
+ 2. Follow the [Types and Schemas Rule](rules/types-and-schemas-rule.md).
29
+ 3. Follow the [Exports and Imports Rule](rules/exports-and-imports-rule.md) so the type or schema module has predictable exports and import paths.
30
+
31
+ ## How To Organize a Constant
32
+
33
+ Use this when adding or moving a constant.
34
+
35
+ 1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to identify where the item belongs in `src/`.
36
+ 2. Follow the [Constants Rule](rules/constants-rule.md).
37
+ 3. Follow the [Exports and Imports Rule](rules/exports-and-imports-rule.md) so the constant module has predictable exports and import paths.
38
+
39
+ ## How To Organize a Utility
40
+
41
+ Use this when adding, extracting, or moving a pure function.
42
+
43
+ 1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to identify where the item belongs in `src/`.
44
+ 2. Follow the [Utilities Rule](rules/utilities-rule.md).
45
+ 3. Follow the [Exports and Imports Rule](rules/exports-and-imports-rule.md) so the utility module has predictable exports and import paths.
46
+
47
+ ## How To Organize Configuration
48
+
49
+ Use this when adding or moving an application configuration module.
50
+
51
+ 1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to identify where the item belongs in `src/`.
52
+ 2. Follow the [Configuration Rule](rules/configuration-rule.md).
53
+ 3. Follow the [Exports and Imports Rule](rules/exports-and-imports-rule.md) so the configuration module has predictable exports and import paths.
54
+
55
+ ## How To Organize a Custom Hook
56
+
57
+ Use this when extracting or moving a custom hook.
58
+
59
+ 1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to identify where the item belongs in `src/`.
60
+ 2. Follow the [Hook Extraction Rule](rules/hook-extraction-rule.md).
61
+ 3. Follow the [Exports and Imports Rule](rules/exports-and-imports-rule.md) so the hook module has predictable exports and import paths.
62
+
63
+ ## How To Organize a Locale String
64
+
65
+ Use this when adding or moving user-facing text.
66
+
67
+ 1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to identify where the item belongs in `src/`.
68
+ 2. Follow the [Locales Rule](rules/locales-rule.md).
69
+ 3. Follow the [Exports and Imports Rule](rules/exports-and-imports-rule.md) so the locale module has predictable exports and import paths.