pasika 0.1.0 → 0.1.4
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 +31 -25
- package/claude/hooks/.vulyk +3 -0
- package/claude/hooks/AGENTS.md +3 -0
- package/claude/hooks/claude-hooks.md +30 -0
- package/claude/{.claude/hooks → hooks}/notification.sh +0 -0
- package/claude/{.claude/hooks → hooks}/protect-files.sh +0 -0
- package/claude/scripts/render-settings.ts +25 -9
- package/dist/claude/scripts/render-settings.js +22 -9
- package/dist/eslint/pasika/index.d.ts +2 -0
- package/dist/eslint/pasika/index.js +14 -0
- package/dist/eslint/pasika/rules/organization-imports.d.ts +2 -0
- package/dist/eslint/pasika/rules/organization-imports.js +124 -0
- package/docs/agent-conventions.md +21 -0
- package/docs/code-organization-guide/code-organization-guide.md +65 -0
- package/docs/code-organization-guide/references/application-architecture-reference.md +107 -0
- package/docs/code-organization-guide/rules/component-placement-rule.md +135 -0
- package/docs/code-organization-guide/rules/configuration-rule.md +42 -0
- package/docs/code-organization-guide/rules/constants-rule.md +77 -0
- package/docs/code-organization-guide/rules/exports-and-imports-rule.md +81 -0
- package/docs/code-organization-guide/rules/folder-nesting-rule.md +83 -0
- package/docs/code-organization-guide/rules/hook-extraction-rule.md +142 -0
- package/docs/code-organization-guide/rules/interactive-component-rule.md +100 -0
- package/docs/code-organization-guide/rules/jsx-hygiene-rule.md +68 -0
- package/docs/code-organization-guide/rules/locales-rule.md +55 -0
- package/docs/code-organization-guide/rules/nameable-visual-concept-rule.md +66 -0
- package/docs/code-organization-guide/rules/native-prop-forwarding-rule.md +38 -0
- package/docs/code-organization-guide/rules/no-mixed-concerns-rule.md +64 -0
- package/docs/code-organization-guide/rules/repeated-structure-rule.md +93 -0
- package/docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md +116 -0
- package/docs/code-organization-guide/rules/sole-state-owner-rule.md +102 -0
- package/docs/code-organization-guide/rules/types-and-schemas-rule.md +138 -0
- package/docs/code-organization-guide/rules/utilities-rule.md +87 -0
- package/docs/documentation-guide/_templates/guide.md +19 -0
- package/docs/documentation-guide/_templates/reference.md +17 -0
- package/docs/documentation-guide/_templates/rule.md +21 -0
- package/docs/documentation-guide/documentation-guide.md +13 -0
- package/docs/documentation-guide/references/documentation-types-reference.md +9 -0
- package/docs/documentation-guide/rules/guide-creation-rule.md +112 -0
- package/docs/documentation-guide/rules/reference-creation-rule.md +132 -0
- package/docs/documentation-guide/rules/rule-creation-rule.md +81 -0
- package/docs/documentation-guide/rules/template-usage-rule.md +48 -0
- package/docs/shadcn-theme.md +121 -0
- package/docs/styling-guide/rules/class-composition-rule.md +32 -0
- package/docs/styling-guide/rules/color-role-naming-rule.md +115 -0
- package/docs/styling-guide/rules/component-state-rule.md +26 -0
- package/docs/styling-guide/rules/component-variant-rule.md +128 -0
- package/docs/styling-guide/rules/global-style-system-rule.md +63 -0
- package/docs/styling-guide/rules/style-placement-rule.md +49 -0
- package/docs/styling-guide/rules/tailwind-utility-rule.md +48 -0
- package/docs/styling-guide/rules/theme-and-utility-definition-rule.md +129 -0
- package/docs/styling-guide/rules/theme-token-rule.md +32 -0
- package/docs/styling-guide/styling-guide.md +17 -0
- package/package.json +24 -10
- package/AGENTS.md +0 -15
- package/docs/common/merge-behavior.md +0 -17
- package/docs/common/overview.md +0 -20
- /package/{CLAUDE.md → claude/hooks/CLAUDE.md} +0 -0
- /package/claude/{.claude/hooks → hooks}/status-line/index.js +0 -0
- /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
|
|
3
|
+
Reusable agent setup package, starting with Claude.
|
|
4
4
|
|
|
5
|
-
`pasika` stores portable
|
|
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
|
|
12
|
-
- reusable
|
|
11
|
+
- Claude first for v1
|
|
12
|
+
- reusable agent setup assets only
|
|
13
13
|
- shared docs at the repo root
|
|
14
|
-
- no project-specific
|
|
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
|
|
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
|
|
54
|
-
- project-specific
|
|
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
|
|
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
|
-
|
|
100
|
+
`npx pasika claude` also copies the packaged Claude hooks docs into `.claude/hooks/` in the target repo.
|
|
104
101
|
|
|
105
|
-
|
|
102
|
+
## ESLint Pasika Ruleset
|
|
106
103
|
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
106
|
+
```ts
|
|
107
|
+
// eslint.config.ts
|
|
108
|
+
import { pasikaConfig } from "pasika/eslint";
|
|
109
|
+
import { RuleSeverity, styleguide } from "zirka";
|
|
111
110
|
|
|
112
|
-
|
|
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,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}
|
|
111
|
-
const protectFilesCommand = `${packagePath}
|
|
112
|
-
const statusLineCommand = `node ${packagePath}
|
|
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
|
|
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}
|
|
145
|
+
`${packagePath}/hooks/notification.sh`,
|
|
146
146
|
);
|
|
147
147
|
const preToolUseEntries = upsertHookMatcherEntry(
|
|
148
148
|
baseSettings.hooks.PreToolUse,
|
|
149
149
|
"Edit|Write",
|
|
150
|
-
`${packagePath}
|
|
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}
|
|
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
|
-
|
|
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}
|
|
56
|
-
const protectFilesCommand = `${packagePath}
|
|
57
|
-
const statusLineCommand = `node ${packagePath}
|
|
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
|
|
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}
|
|
86
|
-
const preToolUseEntries = upsertHookMatcherEntry(baseSettings.hooks.PreToolUse, "Edit|Write", `${packagePath}
|
|
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}
|
|
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
|
-
|
|
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,14 @@
|
|
|
1
|
+
import { organizationImportsRule } from "./rules/organization-imports.js";
|
|
2
|
+
export const pasikaConfig = {
|
|
3
|
+
files: ["src/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}"],
|
|
4
|
+
plugins: {
|
|
5
|
+
pasika: {
|
|
6
|
+
rules: {
|
|
7
|
+
"organization-imports": organizationImportsRule,
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
rules: {
|
|
12
|
+
"pasika/organization-imports": "error",
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -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,21 @@
|
|
|
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
|
+
## Vulyk
|
|
17
|
+
|
|
18
|
+
- Make Vulyk registry changes with Vulyk commands, not by editing generated files.
|
|
19
|
+
- Treat `AGENTS.md`, `docs/external`, `.agents/skills`, and `.vulyk` as generated output.
|
|
20
|
+
- Use ephemeral commands such as `npx vulyk@latest ...`.
|
|
21
|
+
- Do not add Vulyk to `package.json` only to run the CLI.
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
For every JavaScript or TypeScript module covered by this guide, also follow the [Exports and Imports Rule](rules/exports-and-imports-rule.md).
|
|
6
|
+
|
|
7
|
+
## How To Organize a Component
|
|
8
|
+
|
|
9
|
+
Use this no matter whether you are adding a new component or extracting from existing code; the steps below apply in any case.
|
|
10
|
+
|
|
11
|
+
1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to choose the right layer and scope.
|
|
12
|
+
2. Pick the component's placement per the [Component Placement Rule](rules/component-placement-rule.md).
|
|
13
|
+
3. Classify the component as smart or dumb per the [Smart vs Dumb Component Rule](rules/smart-vs-dumb-component-rule.md).
|
|
14
|
+
4. Follow the [Native Prop Forwarding Rule](rules/native-prop-forwarding-rule.md) when a `src/shared/` component renders the same intrinsic root element in every rendered result, so its native prop contract remains available.
|
|
15
|
+
5. Keep each component file to exactly one React component per the [No Mixed Concerns Rule](rules/no-mixed-concerns-rule.md).
|
|
16
|
+
6. Extract interactive elements per the [Interactive Component Rule](rules/interactive-component-rule.md).
|
|
17
|
+
7. Extract sole-state-owner blocks per the [Sole State Owner Rule](rules/sole-state-owner-rule.md).
|
|
18
|
+
8. Extract repeated structures per the [Repeated Structure Rule](rules/repeated-structure-rule.md).
|
|
19
|
+
9. Extract a group of elements when one clear component name describes it per the [Nameable Visual Concept Rule](rules/nameable-visual-concept-rule.md).
|
|
20
|
+
10. For every component extracted in steps 6–9, repeat steps 2–5.
|
|
21
|
+
11. Repeat steps 6–9 for an extracted component only when its JSX has an applicable trigger; stop when no new component is required.
|
|
22
|
+
12. Nest a component when it gains exclusive children per the [Folder Nesting Rule](rules/folder-nesting-rule.md).
|
|
23
|
+
13. Keep the component's JSX clean per the [JSX Hygiene Rule](rules/jsx-hygiene-rule.md).
|
|
24
|
+
|
|
25
|
+
## How To Organize a Type or Schema
|
|
26
|
+
|
|
27
|
+
Use this when adding or moving a type or schema.
|
|
28
|
+
|
|
29
|
+
1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to choose the right layer and scope.
|
|
30
|
+
2. Follow the [Types and Schemas Rule](rules/types-and-schemas-rule.md).
|
|
31
|
+
|
|
32
|
+
## How To Organize a Constant
|
|
33
|
+
|
|
34
|
+
Use this when adding or moving a constant.
|
|
35
|
+
|
|
36
|
+
1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to choose the right layer and scope.
|
|
37
|
+
2. Follow the [Constants Rule](rules/constants-rule.md).
|
|
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 choose the right layer and scope.
|
|
44
|
+
2. Follow the [Utilities Rule](rules/utilities-rule.md).
|
|
45
|
+
|
|
46
|
+
## How To Organize Configuration
|
|
47
|
+
|
|
48
|
+
Use this when adding or moving an application configuration object.
|
|
49
|
+
|
|
50
|
+
1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to choose the right layer and scope.
|
|
51
|
+
2. Follow the [Configuration Rule](rules/configuration-rule.md).
|
|
52
|
+
|
|
53
|
+
## How To Organize a Custom Hook
|
|
54
|
+
|
|
55
|
+
Use this when extracting or moving a custom hook.
|
|
56
|
+
|
|
57
|
+
1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to choose the right layer and scope.
|
|
58
|
+
2. Follow the [Hook Extraction Rule](rules/hook-extraction-rule.md).
|
|
59
|
+
|
|
60
|
+
## How To Organize a Locale String
|
|
61
|
+
|
|
62
|
+
Use this when adding or moving user-facing text.
|
|
63
|
+
|
|
64
|
+
1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to choose the right layer and scope.
|
|
65
|
+
2. Follow the [Locales Rule](rules/locales-rule.md).
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Application Architecture Reference
|
|
2
|
+
|
|
3
|
+
Use this reference to look up the canonical shape of the repository's `src/` tree and its Layer Model.
|
|
4
|
+
|
|
5
|
+
## Layer Model
|
|
6
|
+
|
|
7
|
+
This section describes the repository's four layers, root support scope, and dependency direction. Use it to decide where a component or support file belongs and which layers it can import from.
|
|
8
|
+
|
|
9
|
+
| Layer or scope | Path | Permitted contents | Imports from |
|
|
10
|
+
| -------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
|
11
|
+
| `app` | `src/app/` | Next.js App Router framework-convention files and assets, plus styles required by routing files | compositions, features, shared, root |
|
|
12
|
+
| `compositions` | `src/compositions/` | Components and support files (`hooks/`, `types/`, `schemas/`, `constants/`, `utils/`) | compositions, features, shared, root |
|
|
13
|
+
| `features` | `src/features/<feature>/` | Components and support files (`hooks/`, `types/`, `schemas/`, `constants/`, `utils/`). `src/features/` itself holds feature folders only — never components or support folders | same feature, shared, root |
|
|
14
|
+
| `shared` | `src/shared/` | Components and support files (`hooks/`, `types/`, `schemas/`, `constants/`, `utils/`) | shared, root |
|
|
15
|
+
| `root` | `src/` | App-wide support files (`hooks/`, `types/`, `schemas/`, `constants/`, `utils/`), `locales/`, and `config/` | root support folders; a configuration module may also import files in its own `src/config/<config-name>/` directory |
|
|
16
|
+
|
|
17
|
+
Root support folders are `hooks/`, `types/`, `schemas/`, `constants/`, `utils/`, `config/`, and `locales/`.
|
|
18
|
+
|
|
19
|
+
## Closest Common Folder (CCF)
|
|
20
|
+
|
|
21
|
+
The CCF is the deepest eligible folder that contains every consumer of an item.
|
|
22
|
+
|
|
23
|
+
- Eligible folders are a nested component directory, a feature folder, a configuration-object directory, `src/features/`, `src/compositions/`, `src/shared/`, and `src/`.
|
|
24
|
+
- Support folders and `src/app/` do not determine the CCF. A consumer in a support folder counts at its enclosing eligible component, feature, composition, shared, configuration-object, or root scope. A Next.js route-file consumer counts at `src/` unless a rule explicitly excludes it. `src/features/` is a non-placeable CCF boundary; the relevant placement rule maps it to a destination.
|
|
25
|
+
- A consumer is a module under `src/` that uses the item. For initial placement only, include the origin module that contains the item before extraction; omit it after extraction. Apply a rule's import exclusions before calculating the CCF, then use it for placement unless that rule states otherwise.
|
|
26
|
+
|
|
27
|
+
## Application Structure
|
|
28
|
+
|
|
29
|
+
This tree shows the canonical directory layout and naming conventions for files and folders inside `src/`. It is included so readers can verify their structure matches the project standard at a glance.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
src/
|
|
33
|
+
├── app/ # Next.js framework-convention files, assets, and route styles
|
|
34
|
+
│ ├── globals.css # Global stylesheet imported by the root layout
|
|
35
|
+
│ ├── layout.tsx # Root layout
|
|
36
|
+
│ └── <route>/ # Next.js App Router routing files
|
|
37
|
+
├── compositions/ # Components stay flat siblings by default
|
|
38
|
+
│ ├── <ComponentA>.tsx # Smart component (PascalCase-named)
|
|
39
|
+
│ ├── <component-b>.tsx # Dumb component (kebab-case-named)
|
|
40
|
+
│ ├── <component-part>.tsx # Dumb component — shared by A and B, so it stays flat too
|
|
41
|
+
│ ├── <NestedComponent>/ # Smart (PascalCase-named) — or kebab-case-named for dumb; nested only once it gains exclusive children
|
|
42
|
+
│ │ ├── index.ts # Named-re-exports only the nested parent
|
|
43
|
+
│ │ ├── <NestedComponent>.tsx # Smart (PascalCase-named) — or kebab-case-named for dumb
|
|
44
|
+
│ │ ├── <nested-part>.tsx # Exclusive child — not in index.ts
|
|
45
|
+
│ │ ├── hooks/ # Nested-scoped support folders
|
|
46
|
+
│ │ ├── types/
|
|
47
|
+
│ │ ├── schemas/
|
|
48
|
+
│ │ ├── constants/
|
|
49
|
+
│ │ └── utils/
|
|
50
|
+
│ ├── hooks/ # Composition-scoped custom hooks
|
|
51
|
+
│ ├── types/ # Composition-scoped TypeScript types
|
|
52
|
+
│ ├── schemas/ # Composition-scoped validation schemas
|
|
53
|
+
│ ├── constants/ # Composition-scoped constants
|
|
54
|
+
│ └── utils/ # Composition-scoped pure functions
|
|
55
|
+
├── features/ # Feature folders only — no components, no support folders here
|
|
56
|
+
│ └── <feature>/ # One folder per feature — a feature can hold several components
|
|
57
|
+
│ ├── <ComponentA>.tsx # Smart component (PascalCase-named)
|
|
58
|
+
│ ├── <component-b>.tsx # Dumb component (kebab-case-named)
|
|
59
|
+
│ ├── <component-part>.tsx # Dumb component — shared by A and B, so it stays flat too
|
|
60
|
+
│ ├── <NestedComponent>/ # Smart (PascalCase-named) — or kebab-case-named for dumb; nested only once it gains exclusive children
|
|
61
|
+
│ │ ├── index.ts # Named-re-exports only the nested parent
|
|
62
|
+
│ │ ├── <NestedComponent>.tsx # Smart (PascalCase-named) — or kebab-case-named for dumb
|
|
63
|
+
│ │ ├── <nested-part>.tsx # Exclusive child — not in index.ts
|
|
64
|
+
│ │ ├── hooks/ # Nested-scoped support folders
|
|
65
|
+
│ │ ├── types/
|
|
66
|
+
│ │ ├── schemas/
|
|
67
|
+
│ │ ├── constants/
|
|
68
|
+
│ │ └── utils/
|
|
69
|
+
│ ├── hooks/ # Feature-scoped custom hooks
|
|
70
|
+
│ ├── types/ # Feature-scoped TypeScript types
|
|
71
|
+
│ ├── schemas/ # Feature-scoped validation schemas
|
|
72
|
+
│ ├── constants/ # Feature-scoped constants
|
|
73
|
+
│ └── utils/ # Feature-scoped pure functions
|
|
74
|
+
├── shared/ # Components stay flat siblings by default
|
|
75
|
+
│ ├── <ComponentA>.tsx # Smart component (PascalCase-named)
|
|
76
|
+
│ ├── <component-b>.tsx # Dumb component (kebab-case-named)
|
|
77
|
+
│ ├── <component-part>.tsx # Dumb component — shared by A and B, so it stays flat too
|
|
78
|
+
│ ├── <NestedComponent>/ # Smart (PascalCase-named) — or kebab-case-named for dumb; nested only once it gains exclusive children
|
|
79
|
+
│ │ ├── index.ts # Named-re-exports only the nested parent
|
|
80
|
+
│ │ ├── <NestedComponent>.tsx # Smart (PascalCase-named) — or kebab-case-named for dumb
|
|
81
|
+
│ │ ├── <nested-part>.tsx # Exclusive child — not in index.ts
|
|
82
|
+
│ │ ├── hooks/ # Nested-scoped support folders
|
|
83
|
+
│ │ ├── types/
|
|
84
|
+
│ │ ├── schemas/
|
|
85
|
+
│ │ ├── constants/
|
|
86
|
+
│ │ └── utils/
|
|
87
|
+
│ ├── hooks/ # Shared-scoped custom hooks
|
|
88
|
+
│ ├── types/ # Shared-scoped TypeScript types
|
|
89
|
+
│ ├── schemas/ # Shared-scoped validation schemas
|
|
90
|
+
│ ├── constants/ # Shared-scoped constants
|
|
91
|
+
│ └── utils/ # Shared-scoped pure functions
|
|
92
|
+
├── hooks/ # App-wide custom hooks
|
|
93
|
+
├── types/ # App-wide TypeScript types
|
|
94
|
+
├── schemas/ # App-wide validation schemas
|
|
95
|
+
├── constants/ # App-wide constants
|
|
96
|
+
├── utils/ # App-wide pure functions
|
|
97
|
+
├── config/ # Configuration objects that centralize application behavior
|
|
98
|
+
│ └── <config-name>/ # One folder per configuration object
|
|
99
|
+
│ ├── index.ts # Config object
|
|
100
|
+
│ ├── hooks/ # Config-only custom hooks
|
|
101
|
+
│ ├── types/ # Config-only TypeScript types
|
|
102
|
+
│ ├── schemas/ # Config-only validation schemas
|
|
103
|
+
│ ├── constants/ # Config-only constants
|
|
104
|
+
│ └── utils/ # Config-only pure functions
|
|
105
|
+
└── locales/ # App-wide locale strings
|
|
106
|
+
└── index.ts # Single locale-registration file
|
|
107
|
+
```
|