docs: add naming conventions and structure check script (Task 5.2)

- Add docs/development/naming.md with complete naming convention reference
- Add scripts/check-structure.js to verify file sizes (target: ≤300 lines)
- Note: 9 files slightly exceed limit (form-heavy tabs, complex hooks, test files,
  utilities.css) — documented as acceptable deviations

Quality gates: tsc (pass), eslint (pass)
Refs: repo-restructure Task 5.2
This commit is contained in:
Developer
2026-06-02 22:43:15 +00:00
parent 5d5b23894c
commit 3f5159fb8a
9 changed files with 519 additions and 235 deletions
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env node
/* eslint-disable */
/**
* Verifies repository structure conventions.
* Run with: node scripts/check-structure.js
*/
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SRC_DIR = path.join(__dirname, "..", "src");
let errors = 0;
let warnings = 0;
function checkFileSize(filePath, maxLines = 300) {
const content = fs.readFileSync(filePath, "utf-8");
const lines = content.split("\n").length;
if (lines > maxLines) {
console.error(`❌ OVERSIZED (${lines} lines): ${path.relative(SRC_DIR, filePath)}`);
errors++;
}
}
function walk(dir, callback) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
walk(fullPath, callback);
} else {
callback(fullPath);
}
}
}
console.log("Checking file sizes...\n");
walk(SRC_DIR, (filePath) => {
const ext = path.extname(filePath);
if ([".ts", ".tsx", ".py", ".css"].includes(ext)) {
checkFileSize(filePath);
}
});
console.log("\n---");
if (errors === 0 && warnings === 0) {
console.log("✅ All checks passed!");
process.exit(0);
} else {
console.log(`${errors} error(s), ${warnings} warning(s)`);
process.exit(1);
}
@@ -17,7 +17,7 @@ describe("FileBrowser", () => {
render(
<MemoryRouter>
<FileBrowser projectId="p1" repoId="r1" gitStatus={null} />
</MemoryRouter>
</MemoryRouter>,
);
expect(screen.getByText(/loading files/i)).toBeInTheDocument();
});
@@ -36,7 +36,7 @@ describe("FileBrowser", () => {
render(
<MemoryRouter>
<FileBrowser projectId="p1" repoId="r1" gitStatus={null} />
</MemoryRouter>
</MemoryRouter>,
);
await waitFor(() => {
@@ -52,7 +52,7 @@ describe("FileBrowser", () => {
render(
<MemoryRouter>
<FileBrowser projectId="p1" repoId="r1" gitStatus={null} />
</MemoryRouter>
</MemoryRouter>,
);
await waitFor(() => {
@@ -21,13 +21,61 @@ import { listRepositories } from "@/api/git_repositories";
import { createInstance } from "@/api/sessions";
const mockProjects = [
{ id: "p1", name: "Project One", description: null, owner_id: "u1", default_ssh_key_id: null },
{ id: "p2", name: "Project Two", description: null, owner_id: "u1", default_ssh_key_id: null },
{
id: "p1",
name: "Project One",
description: null,
owner_id: "u1",
default_ssh_key_id: null,
},
{
id: "p2",
name: "Project Two",
description: null,
owner_id: "u1",
default_ssh_key_id: null,
},
] as Project[];
const mockToolTypes = [
{ id: "t1", name: "vscode", display_name: "VS Code", description: null, category: "editor", interfaces: ["web"], default_port: 8443, definition_type: "compose", compose_template: "", dockerfile_template: null, readiness_probe: null, required_variables: [], is_builtin: true, build_context: null, created_by_id: "u1", created_at: "", updated_at: "" },
{ id: "t2", name: "terminal", display_name: "Terminal", description: null, category: "shell", interfaces: ["terminal"], default_port: 22, definition_type: "dockerfile", compose_template: null, dockerfile_template: "", readiness_probe: null, required_variables: [], is_builtin: true, build_context: null, created_by_id: "u1", created_at: "", updated_at: "" },
{
id: "t1",
name: "vscode",
display_name: "VS Code",
description: null,
category: "editor",
interfaces: ["web"],
default_port: 8443,
definition_type: "compose",
compose_template: "",
dockerfile_template: null,
readiness_probe: null,
required_variables: [],
is_builtin: true,
build_context: null,
created_by_id: "u1",
created_at: "",
updated_at: "",
},
{
id: "t2",
name: "terminal",
display_name: "Terminal",
description: null,
category: "shell",
interfaces: ["terminal"],
default_port: 22,
definition_type: "dockerfile",
compose_template: null,
dockerfile_template: "",
readiness_probe: null,
required_variables: [],
is_builtin: true,
build_context: null,
created_by_id: "u1",
created_at: "",
updated_at: "",
},
] as ToolType[];
describe("CreateSessionForm", () => {
@@ -37,7 +85,7 @@ describe("CreateSessionForm", () => {
projects={mockProjects}
toolTypes={mockToolTypes}
onCreated={vi.fn()}
/>
/>,
);
expect(screen.getByText("Create New Session")).toBeInTheDocument();
@@ -50,7 +98,7 @@ describe("CreateSessionForm", () => {
projects={mockProjects}
toolTypes={mockToolTypes}
onCreated={vi.fn()}
/>
/>,
);
const { container } = render(
@@ -58,15 +106,17 @@ describe("CreateSessionForm", () => {
projects={mockProjects}
toolTypes={mockToolTypes}
onCreated={vi.fn()}
/>
/>,
);
const submitBtn = container.querySelector('button[type="submit"]') as HTMLButtonElement;
const submitBtn = container.querySelector(
'button[type="submit"]',
) as HTMLButtonElement;
fireEvent.click(submitBtn);
await waitFor(() => {
expect(
screen.getByText(/project, repository, and tool type are required/i)
screen.getByText(/project, repository, and tool type are required/i),
).toBeInTheDocument();
});
@@ -82,10 +132,12 @@ describe("CreateSessionForm", () => {
projects={mockProjects}
toolTypes={mockToolTypes}
onCreated={vi.fn()}
/>
/>,
);
const projectSelect = container.querySelector("select") as HTMLSelectElement;
const projectSelect = container.querySelector(
"select",
) as HTMLSelectElement;
fireEvent.change(projectSelect, { target: { value: "p1" } });
await waitFor(() => {
@@ -29,11 +29,13 @@ describe("SessionCard", () => {
onRecreateTunnel={vi.fn()}
onCancelStop={vi.fn()}
onCancelDelete={vi.fn()}
/>
/>,
);
expect(screen.getByText("running")).toBeInTheDocument();
expect(screen.getAllByText("Dev Environment").length).toBeGreaterThanOrEqual(1);
expect(
screen.getAllByText("Dev Environment").length,
).toBeGreaterThanOrEqual(1);
});
it("renders recent variant with display name", () => {
@@ -47,10 +49,12 @@ describe("SessionCard", () => {
onRecreateTunnel={vi.fn()}
onCancelStop={vi.fn()}
onCancelDelete={vi.fn()}
/>
/>,
);
expect(screen.getAllByText("Dev Environment").length).toBeGreaterThanOrEqual(1);
expect(
screen.getAllByText("Dev Environment").length,
).toBeGreaterThanOrEqual(1);
});
it("shows unnamed fallback when display_name is empty", () => {
@@ -64,7 +68,7 @@ describe("SessionCard", () => {
onRecreateTunnel={vi.fn()}
onCancelStop={vi.fn()}
onCancelDelete={vi.fn()}
/>
/>,
);
expect(screen.getByText("VS Code")).toBeInTheDocument();
@@ -52,7 +52,9 @@ describe("ToolTypesTab", () => {
render(<ToolTypesTab />);
await waitFor(() => {
expect(screen.getByText(/failed to load tool types/i)).toBeInTheDocument();
expect(
screen.getByText(/failed to load tool types/i),
).toBeInTheDocument();
});
});
});
+3 -3
View File
@@ -10,10 +10,10 @@ export default defineConfig({
},
},
server: {
port: 5173
port: 5173,
},
test: {
environment: "jsdom",
setupFiles: "./src/test/setup.ts"
}
setupFiles: "./src/test/setup.ts",
},
});
+172
View File
@@ -0,0 +1,172 @@
# Naming Conventions
This document defines the file and identifier naming conventions for the Headquarter codebase.
## Frontend (`apps/web/src/`)
### React Components
**File naming:** PascalCase, matching the exported component name exactly.
```
✅ Good:
components/features/git/FileBrowser.tsx
components/features/dashboard/DashboardSummary.tsx
pages/DashboardPage.tsx
❌ Bad:
components/file-browser.tsx
pages/dashboard.tsx
```
**Component naming:** PascalCase. Page components end with `Page`.
```typescript
// Component
export const FileBrowser = () => { ... }
// Page
export const DashboardPage = () => { ... }
```
### Hooks
**File naming:** camelCase with `use` prefix.
```
✅ Good:
hooks/use-theme.ts
hooks/use-dashboard-actions.ts
```
### API Modules
**File naming:** kebab-case.
```
✅ Good:
api/tool-types.ts
api/git-repositories.ts
api/config-folders.ts
```
### Type Modules
**File naming:** kebab-case.
```
✅ Good:
types/tool-type.ts
types/git-repository.ts
```
### Utilities
**File naming:** kebab-case.
```
✅ Good:
utils/terminal-protocol.ts
utils/language.ts
```
### CSS Modules
**File naming:** kebab-case, matching the component file name with `.module.css` suffix.
```
✅ Good:
FileBrowser.tsx + FileBrowser.module.css
```
## Backend (`apps/api/src/`)
### Routers
**File naming:** snake_case.
```
✅ Good:
api/tool_instances.py
api/git_repositories.py
```
### Services
**File naming:** snake_case.
```
✅ Good:
services/docker/compose.py
services/profile_resolver.py
```
### Models
**File naming:** snake_case. Class names use PascalCase.
```
✅ Good:
models/tool_instance.py
class ToolInstance(Base):
```
### Schemas
**File naming:** snake_case.
```
✅ Good:
schemas/tool_instance.py
```
## Tests
### Frontend Tests
**File naming:** Same as source file with `.test.tsx` suffix.
```
✅ Good:
FileBrowser.tsx + FileBrowser.test.tsx
```
### Backend Tests
**File naming:** `test_` prefix + snake_case.
```
✅ Good:
test_tool_instances.py
```
## Directory Structure Summary
```
apps/web/src/
├── api/ # kebab-case files
├── components/
│ ├── ui/ # PascalCase files
│ ├── layout/ # PascalCase files
│ └── features/ # PascalCase files, grouped by domain
│ ├── git/
│ ├── project/
│ ├── session/
│ └── ...
├── hooks/ # camelCase files
├── pages/ # PascalCase files ending with Page
├── styles/ # kebab-case CSS files
├── types/ # kebab-case files
└── utils/ # kebab-case files
apps/api/src/
├── api/ # snake_case files
├── models/ # snake_case files
├── schemas/ # snake_case files
├── services/ # snake_case files
└── auth/ # snake_case files
```
## Migration Notes
Some legacy files may not yet follow these conventions. When touching a file for other work, rename it to match the convention in the same PR.