- Update 004_architecture_impact_map.md and 005_architecture_data.json - Add audit reports: data-access-audit-framework-v1, data-access-audit-v1-data.json, data-access-audit-v1, g1-g5 audit outputs - Add superpowers plans and specs (logging-refactor, documentation-system-redesign) - Update troubleshooting/known-issues.md
2683 lines
82 KiB
Markdown
2683 lines
82 KiB
Markdown
# 文档体系重设计实施计划
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 建立基于 arch.db 的文档体系,让 AI 越工作越了解项目
|
||
|
||
**Architecture:** 4 个子计划按依赖顺序执行——先建 arch.db 扫描器(基础工具),再重设计文档体系(004 瘦身/005 废弃/known-issues 精简),再修正规范文档,最后分批创建模块 README
|
||
|
||
**Tech Stack:** TypeScript, ts-morph, SQLite (better-sqlite3), Next.js 16, ESLint, Vitest
|
||
|
||
**Spec:** [docs/superpowers/specs/2026-07-07-documentation-system-redesign-design.md](../specs/2026-07-07-documentation-system-redesign-design.md)
|
||
|
||
---
|
||
|
||
## 子计划 1: arch.db 扫描器
|
||
|
||
**目标:** 实现 ts-morph 扫描器 + SQLite schema + 查询 CLI,让 AI 能 `npm run arch:scan` 和 `npm run arch:query`
|
||
|
||
### Task 1.1: 安装依赖
|
||
|
||
**Files:**
|
||
- Modify: `package.json`
|
||
|
||
- [ ] **Step 1: 安装 ts-morph 和 better-sqlite3**
|
||
|
||
```bash
|
||
npm install ts-morph better-sqlite3
|
||
npm install -D @types/better-sqlite3
|
||
```
|
||
|
||
- [ ] **Step 2: 验证安装**
|
||
|
||
Run: `node -e "require('ts-morph'); require('better-sqlite3'); console.log('OK')"`
|
||
Expected: 输出 `OK`
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add package.json package-lock.json
|
||
git commit -m "chore(arch-scan): add ts-morph and better-sqlite3 dependencies"
|
||
```
|
||
|
||
### Task 1.2: 创建扫描器目录结构
|
||
|
||
**Files:**
|
||
- Create: `scripts/arch-scan/schema.ts`
|
||
- Create: `scripts/arch-scan/scanner.ts`
|
||
- Create: `scripts/arch-scan/query.ts`
|
||
- Create: `scripts/arch-scan/cli.ts`
|
||
- Create: `scripts/arch-scan/index.ts`
|
||
|
||
- [ ] **Step 1: 创建目录**
|
||
|
||
```bash
|
||
mkdir -p scripts/arch-scan
|
||
```
|
||
|
||
- [ ] **Step 2: 创建占位文件(后续任务填充)**
|
||
|
||
创建 5 个空文件,每个文件只有头部注释:
|
||
|
||
```typescript
|
||
// scripts/arch-scan/schema.ts
|
||
// SQLite schema 定义
|
||
export {};
|
||
```
|
||
|
||
```typescript
|
||
// scripts/arch-scan/scanner.ts
|
||
// ts-morph 扫描器
|
||
export {};
|
||
```
|
||
|
||
```typescript
|
||
// scripts/arch-scan/query.ts
|
||
// 查询函数
|
||
export {};
|
||
```
|
||
|
||
```typescript
|
||
// scripts/arch-scan/cli.ts
|
||
// CLI 入口
|
||
export {};
|
||
```
|
||
|
||
```typescript
|
||
// scripts/arch-scan/index.ts
|
||
// 主入口
|
||
export {};
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add scripts/arch-scan/
|
||
git commit -m "chore(arch-scan): scaffold directory structure"
|
||
```
|
||
|
||
### Task 1.3: 实现 SQLite schema
|
||
|
||
**Files:**
|
||
- Modify: `scripts/arch-scan/schema.ts`
|
||
- Test: `scripts/arch-scan/schema.test.ts`
|
||
|
||
- [ ] **Step 1: 编写 schema 测试**
|
||
|
||
```typescript
|
||
// scripts/arch-scan/schema.test.ts
|
||
import { describe, it, expect } from "vitest";
|
||
import Database from "better-sqlite3";
|
||
import { initSchema } from "./schema";
|
||
|
||
describe("initSchema", () => {
|
||
it("should create all 12 tables", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
const tables = db
|
||
.prepare(
|
||
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
||
)
|
||
.all() as { name: string }[];
|
||
const tableNames = tables.map((t) => t.name);
|
||
expect(tableNames).toContain("modules");
|
||
expect(tableNames).toContain("files");
|
||
expect(tableNames).toContain("symbols");
|
||
expect(tableNames).toContain("calls");
|
||
expect(tableNames).toContain("file_imports");
|
||
expect(tableNames).toContain("module_deps");
|
||
expect(tableNames).toContain("tech_tags");
|
||
expect(tableNames).toContain("symbol_tech_tags");
|
||
expect(tableNames).toContain("permissions");
|
||
expect(tableNames).toContain("routes");
|
||
expect(tableNames).toContain("db_tables");
|
||
expect(tableNames).toContain("scan_meta");
|
||
});
|
||
|
||
it("should create all indexes", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
const indexes = db
|
||
.prepare(
|
||
"SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_%' ORDER BY name"
|
||
)
|
||
.all() as { name: string }[];
|
||
const indexNames = indexes.map((i) => i.name);
|
||
expect(indexNames).toContain("idx_symbols_file");
|
||
expect(indexNames).toContain("idx_symbols_name");
|
||
expect(indexNames).toContain("idx_calls_caller");
|
||
expect(indexNames).toContain("idx_calls_callee");
|
||
expect(indexNames).toContain("idx_file_imports_source");
|
||
expect(indexNames).toContain("idx_file_imports_target");
|
||
expect(indexNames).toContain("idx_symbol_tech_tags_tag");
|
||
});
|
||
|
||
it("should be idempotent", () => {
|
||
const db = new Database(":memory:");
|
||
expect(() => initSchema(db)).not.toThrow();
|
||
expect(() => initSchema(db)).not.toThrow();
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试验证失败**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/schema.test.ts`
|
||
Expected: FAIL,`initSchema` 未定义
|
||
|
||
- [ ] **Step 3: 实现 initSchema**
|
||
|
||
```typescript
|
||
// scripts/arch-scan/schema.ts
|
||
import type Database from "better-sqlite3";
|
||
|
||
const DDL_STATEMENTS = [
|
||
`CREATE TABLE IF NOT EXISTS modules (
|
||
id INTEGER PRIMARY KEY,
|
||
name TEXT NOT NULL UNIQUE,
|
||
path TEXT NOT NULL,
|
||
description TEXT,
|
||
layer TEXT NOT NULL,
|
||
created_at TEXT NOT NULL
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS files (
|
||
id INTEGER PRIMARY KEY,
|
||
module_id INTEGER REFERENCES modules(id),
|
||
path TEXT NOT NULL UNIQUE,
|
||
kind TEXT NOT NULL,
|
||
lines INTEGER,
|
||
is_server INTEGER DEFAULT 0,
|
||
is_client INTEGER DEFAULT 0,
|
||
has_server_only INTEGER DEFAULT 0
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS symbols (
|
||
id INTEGER PRIMARY KEY,
|
||
file_id INTEGER NOT NULL REFERENCES files(id),
|
||
name TEXT NOT NULL,
|
||
kind TEXT NOT NULL,
|
||
is_exported INTEGER DEFAULT 0,
|
||
is_async INTEGER DEFAULT 0,
|
||
is_server_action INTEGER DEFAULT 0,
|
||
signature TEXT,
|
||
start_line INTEGER,
|
||
end_line INTEGER,
|
||
UNIQUE(file_id, name, start_line)
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS calls (
|
||
id INTEGER PRIMARY KEY,
|
||
caller_id INTEGER NOT NULL REFERENCES symbols(id),
|
||
callee_id INTEGER REFERENCES symbols(id),
|
||
callee_external TEXT,
|
||
call_line INTEGER,
|
||
count INTEGER DEFAULT 1
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS file_imports (
|
||
id INTEGER PRIMARY KEY,
|
||
source_file_id INTEGER NOT NULL REFERENCES files(id),
|
||
imported_file_id INTEGER REFERENCES files(id),
|
||
import_path TEXT NOT NULL,
|
||
is_type_only INTEGER DEFAULT 0,
|
||
imported_names TEXT
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS module_deps (
|
||
id INTEGER PRIMARY KEY,
|
||
source_module_id INTEGER NOT NULL REFERENCES modules(id),
|
||
target_module_id INTEGER NOT NULL REFERENCES modules(id),
|
||
dep_type TEXT NOT NULL,
|
||
UNIQUE(source_module_id, target_module_id, dep_type)
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS tech_tags (
|
||
id INTEGER PRIMARY KEY,
|
||
name TEXT NOT NULL UNIQUE,
|
||
category TEXT
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS symbol_tech_tags (
|
||
symbol_id INTEGER NOT NULL REFERENCES symbols(id),
|
||
tag_id INTEGER NOT NULL REFERENCES tech_tags(id),
|
||
PRIMARY KEY(symbol_id, tag_id)
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS permissions (
|
||
id INTEGER PRIMARY KEY,
|
||
key TEXT NOT NULL UNIQUE,
|
||
description TEXT
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS routes (
|
||
id INTEGER PRIMARY KEY,
|
||
path TEXT NOT NULL UNIQUE,
|
||
kind TEXT NOT NULL,
|
||
file_id INTEGER REFERENCES files(id),
|
||
min_permission TEXT
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS db_tables (
|
||
id INTEGER PRIMARY KEY,
|
||
name TEXT NOT NULL UNIQUE,
|
||
module_id INTEGER REFERENCES modules(id),
|
||
description TEXT
|
||
)`,
|
||
`CREATE TABLE IF NOT EXISTS scan_meta (
|
||
key TEXT PRIMARY KEY,
|
||
value TEXT NOT NULL
|
||
)`,
|
||
`CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_id)`,
|
||
`CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)`,
|
||
`CREATE INDEX IF NOT EXISTS idx_calls_caller ON calls(caller_id)`,
|
||
`CREATE INDEX IF NOT EXISTS idx_calls_callee ON calls(callee_id)`,
|
||
`CREATE INDEX IF NOT EXISTS idx_file_imports_source ON file_imports(source_file_id)`,
|
||
`CREATE INDEX IF NOT EXISTS idx_file_imports_target ON file_imports(imported_file_id)`,
|
||
`CREATE INDEX IF NOT EXISTS idx_symbol_tech_tags_tag ON symbol_tech_tags(tag_id)`,
|
||
];
|
||
|
||
export function initSchema(db: Database.Database): void {
|
||
for (const stmt of DDL_STATEMENTS) {
|
||
db.exec(stmt);
|
||
}
|
||
}
|
||
|
||
export function clearAllData(db: Database.Database): void {
|
||
const tables = [
|
||
"symbol_tech_tags",
|
||
"calls",
|
||
"file_imports",
|
||
"module_deps",
|
||
"symbols",
|
||
"files",
|
||
"modules",
|
||
"tech_tags",
|
||
"permissions",
|
||
"routes",
|
||
"db_tables",
|
||
"scan_meta",
|
||
];
|
||
db.exec("PRAGMA foreign_keys = OFF");
|
||
for (const table of tables) {
|
||
db.exec(`DELETE FROM ${table}`);
|
||
}
|
||
db.exec("PRAGMA foreign_keys = ON");
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行测试验证通过**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/schema.test.ts`
|
||
Expected: PASS(3 个测试全过)
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/arch-scan/schema.ts scripts/arch-scan/schema.test.ts
|
||
git commit -m "feat(arch-scan): implement SQLite schema with 12 tables and 7 indexes"
|
||
```
|
||
|
||
### Task 1.4: 实现模块扫描器
|
||
|
||
**Files:**
|
||
- Modify: `scripts/arch-scan/scanner.ts`
|
||
- Test: `scripts/arch-scan/scanner.test.ts`
|
||
|
||
- [ ] **Step 1: 编写模块扫描测试**
|
||
|
||
```typescript
|
||
// scripts/arch-scan/scanner.test.ts
|
||
import { describe, it, expect } from "vitest";
|
||
import Database from "better-sqlite3";
|
||
import { initSchema, clearAllData } from "./schema";
|
||
import { scanModules } from "./scanner";
|
||
|
||
describe("scanModules", () => {
|
||
it("should detect src/modules/* as modules", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/modules");
|
||
const modules = db
|
||
.prepare("SELECT name FROM modules ORDER BY name")
|
||
.all() as { name: string }[];
|
||
const names = modules.map((m) => m.name);
|
||
expect(names).toContain("exams");
|
||
expect(names).toContain("grades");
|
||
expect(names).toContain("classes");
|
||
});
|
||
|
||
it("should detect shared/ as a module with layer=shared", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/shared");
|
||
const shared = db
|
||
.prepare("SELECT * FROM modules WHERE name = 'shared'")
|
||
.get() as { layer: string };
|
||
expect(shared.layer).toBe("shared");
|
||
});
|
||
|
||
it("should set layer=modules for src/modules/*", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/modules");
|
||
const exams = db
|
||
.prepare("SELECT * FROM modules WHERE name = 'exams'")
|
||
.get() as { layer: string };
|
||
expect(exams.layer).toBe("modules");
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试验证失败**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/scanner.test.ts`
|
||
Expected: FAIL,`scanModules` 未定义
|
||
|
||
- [ ] **Step 3: 实现 scanModules**
|
||
|
||
```typescript
|
||
// scripts/arch-scan/scanner.ts
|
||
import type Database from "better-sqlite3";
|
||
import { readdirSync, statSync, existsSync } from "node:fs";
|
||
import { join } from "node:path";
|
||
|
||
interface ModuleRow {
|
||
name: string;
|
||
path: string;
|
||
description: string | null;
|
||
layer: string;
|
||
created_at: string;
|
||
}
|
||
|
||
function detectLayer(basePath: string): string {
|
||
if (basePath.endsWith("src/modules")) return "modules";
|
||
if (basePath.endsWith("src/shared")) return "shared";
|
||
if (basePath.endsWith("src/app")) return "app";
|
||
return "root";
|
||
}
|
||
|
||
function extractDescription(modulePath: string): string | null {
|
||
const readmePath = join(modulePath, "README.md");
|
||
if (!existsSync(readmePath)) return null;
|
||
const content = readFileFirstLine(readmePath);
|
||
return content;
|
||
}
|
||
|
||
function readFileFirstLine(filePath: string): string {
|
||
// 简化实现,实际可使用 fs.readFileSync
|
||
try {
|
||
const { readFileSync } = require("node:fs");
|
||
const content = readFileSync(filePath, "utf-8");
|
||
const lines = content.split("\n").filter((l: string) => l.trim() && !l.startsWith("#"));
|
||
return lines[0]?.slice(0, 200) || null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export function scanModules(db: Database.Database, basePath: string): void {
|
||
if (!existsSync(basePath)) return;
|
||
const layer = detectLayer(basePath);
|
||
const entries = readdirSync(basePath);
|
||
const now = new Date().toISOString();
|
||
const insert = db.prepare(
|
||
"INSERT OR IGNORE INTO modules (name, path, description, layer, created_at) VALUES (?, ?, ?, ?, ?)"
|
||
);
|
||
for (const entry of entries) {
|
||
const fullPath = join(basePath, entry);
|
||
if (!statSync(fullPath).isDirectory()) continue;
|
||
if (entry.startsWith(".") || entry.startsWith("_")) continue;
|
||
const description = extractDescription(fullPath);
|
||
insert.run(entry, fullPath, description, layer, now);
|
||
}
|
||
}
|
||
|
||
export function scanAllModules(db: Database.Database, projectRoot: string): void {
|
||
scanModules(db, join(projectRoot, "src/modules"));
|
||
scanModules(db, join(projectRoot, "src/shared"));
|
||
// app 层模块(路由组)可选扫描
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行测试验证通过**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/scanner.test.ts`
|
||
Expected: PASS(3 个测试全过)
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/arch-scan/scanner.ts scripts/arch-scan/scanner.test.ts
|
||
git commit -m "feat(arch-scan): implement module scanner for src/modules and src/shared"
|
||
```
|
||
|
||
### Task 1.5: 实现文件扫描器
|
||
|
||
**Files:**
|
||
- Modify: `scripts/arch-scan/scanner.ts`
|
||
- Test: `scripts/arch-scan/scanner.test.ts`
|
||
|
||
- [ ] **Step 1: 添加文件扫描测试**
|
||
|
||
在 `scripts/arch-scan/scanner.test.ts` 末尾追加:
|
||
|
||
```typescript
|
||
describe("scanFiles", () => {
|
||
it("should scan .ts and .tsx files in modules", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/modules");
|
||
scanFiles(db, "src/modules");
|
||
const files = db
|
||
.prepare("SELECT path, kind FROM files WHERE path LIKE '%exams%' LIMIT 5")
|
||
.all() as { path: string; kind: string }[];
|
||
expect(files.length).toBeGreaterThan(0);
|
||
const actionFile = files.find((f) => f.kind === "actions");
|
||
expect(actionFile).toBeDefined();
|
||
});
|
||
|
||
it("should detect file kind from filename", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/modules");
|
||
scanFiles(db, "src/modules");
|
||
const actionsFile = db
|
||
.prepare("SELECT kind FROM files WHERE path LIKE '%exams/actions.ts'")
|
||
.get() as { kind: string };
|
||
expect(actionsFile.kind).toBe("actions");
|
||
const dataAccessFile = db
|
||
.prepare("SELECT kind FROM files WHERE path LIKE '%exams/data-access.ts'")
|
||
.get() as { kind: string };
|
||
expect(dataAccessFile.kind).toBe("data-access");
|
||
});
|
||
|
||
it("should count lines correctly", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/modules");
|
||
scanFiles(db, "src/modules");
|
||
const file = db
|
||
.prepare("SELECT lines FROM files WHERE path LIKE '%exams/actions.ts'")
|
||
.get() as { lines: number };
|
||
expect(file.lines).toBeGreaterThan(0);
|
||
});
|
||
|
||
it("should detect use server and use client directives", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/modules");
|
||
scanFiles(db, "src/modules");
|
||
const serverFile = db
|
||
.prepare("SELECT is_server FROM files WHERE path LIKE '%exams/actions.ts'")
|
||
.get() as { is_server: number };
|
||
expect(serverFile.is_server).toBe(1);
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试验证失败**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/scanner.test.ts`
|
||
Expected: FAIL,`scanFiles` 未定义
|
||
|
||
- [ ] **Step 3: 实现 scanFiles**
|
||
|
||
在 `scripts/arch-scan/scanner.ts` 中追加:
|
||
|
||
```typescript
|
||
import { readFileSync } from "node:fs";
|
||
|
||
const TS_EXTENSIONS = [".ts", ".tsx"];
|
||
|
||
function detectFileKind(filePath: string): string {
|
||
const filename = filePath.split("/").pop() || "";
|
||
if (filename === "actions.ts" || filename.match(/^actions-\w+\.ts$/)) return "actions";
|
||
if (filename === "data-access.ts" || filename.match(/^data-access-\w+\.ts$/)) return "data-access";
|
||
if (filename === "schema.ts") return "schema";
|
||
if (filename === "types.ts") return "types";
|
||
if (filename.endsWith(".tsx")) return "component";
|
||
if (filename.startsWith("use-") && filename.endsWith(".ts")) return "hook";
|
||
if (filename === "page.tsx") return "page";
|
||
if (filename === "layout.tsx") return "layout";
|
||
if (filename === "route.ts") return "route";
|
||
if (filename === "error.tsx") return "error";
|
||
if (filename === "loading.tsx") return "loading";
|
||
if (filename === "proxy.ts") return "config";
|
||
if (filename === "next.config.ts") return "config";
|
||
return "lib";
|
||
}
|
||
|
||
function countLines(content: string): number {
|
||
return content.split("\n").length;
|
||
}
|
||
|
||
function detectDirectives(content: string): {
|
||
isServer: boolean;
|
||
isClient: boolean;
|
||
hasServerOnly: boolean;
|
||
} {
|
||
const firstLine = content.split("\n")[0] || "";
|
||
return {
|
||
isServer: firstLine.includes('"use server"') || firstLine.includes("'use server'"),
|
||
isClient: firstLine.includes('"use client"') || firstLine.includes("'use client'"),
|
||
hasServerOnly: content.includes('import "server-only"') || content.includes("import 'server-only'"),
|
||
};
|
||
}
|
||
|
||
function scanFilesRecursive(
|
||
db: Database.Database,
|
||
dirPath: string,
|
||
moduleName: string
|
||
): void {
|
||
const entries = readdirSync(dirPath);
|
||
for (const entry of entries) {
|
||
const fullPath = join(dirPath, entry);
|
||
const stat = statSync(fullPath);
|
||
if (stat.isDirectory()) {
|
||
if (entry.startsWith(".") || entry === "node_modules") continue;
|
||
scanFilesRecursive(db, fullPath, moduleName);
|
||
continue;
|
||
}
|
||
if (!TS_EXTENSIONS.some((ext) => entry.endsWith(ext))) continue;
|
||
const content = readFileSync(fullPath, "utf-8");
|
||
const directives = detectDirectives(content);
|
||
const kind = detectFileKind(fullPath);
|
||
const lines = countLines(content);
|
||
const moduleId = (
|
||
db.prepare("SELECT id FROM modules WHERE name = ?").get(moduleName) as {
|
||
id: number;
|
||
}
|
||
).id;
|
||
db.prepare(
|
||
`INSERT OR REPLACE INTO files (module_id, path, kind, lines, is_server, is_client, has_server_only)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||
).run(
|
||
moduleId,
|
||
fullPath.replace(/\\/g, "/"),
|
||
kind,
|
||
lines,
|
||
directives.isServer ? 1 : 0,
|
||
directives.isClient ? 1 : 0,
|
||
directives.hasServerOnly ? 1 : 0
|
||
);
|
||
}
|
||
}
|
||
|
||
export function scanFiles(db: Database.Database, basePath: string): void {
|
||
if (!existsSync(basePath)) return;
|
||
const modules = db
|
||
.prepare("SELECT name, path FROM modules")
|
||
.all() as { name: string; path: string }[];
|
||
for (const mod of modules) {
|
||
if (!existsSync(mod.path)) continue;
|
||
scanFilesRecursive(db, mod.path, mod.name);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行测试验证通过**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/scanner.test.ts`
|
||
Expected: PASS(7 个测试全过)
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/arch-scan/scanner.ts scripts/arch-scan/scanner.test.ts
|
||
git commit -m "feat(arch-scan): implement file scanner with kind detection and directive parsing"
|
||
```
|
||
|
||
### Task 1.6: 实现符号扫描器(ts-morph)
|
||
|
||
**Files:**
|
||
- Modify: `scripts/arch-scan/scanner.ts`
|
||
- Test: `scripts/arch-scan/scanner.test.ts`
|
||
|
||
- [ ] **Step 1: 添加符号扫描测试**
|
||
|
||
在 `scripts/arch-scan/scanner.test.ts` 末尾追加:
|
||
|
||
```typescript
|
||
describe("scanSymbols", () => {
|
||
it("should extract exported functions from actions.ts", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/modules");
|
||
scanFiles(db, "src/modules");
|
||
scanSymbols(db);
|
||
const symbols = db
|
||
.prepare(
|
||
"SELECT name, is_exported, is_async FROM symbols WHERE file_id IN (SELECT id FROM files WHERE path LIKE '%exams/actions.ts') LIMIT 5"
|
||
)
|
||
.all() as { name: string; is_exported: number; is_async: number }[];
|
||
expect(symbols.length).toBeGreaterThan(0);
|
||
const exported = symbols.find((s) => s.is_exported === 1);
|
||
expect(exported).toBeDefined();
|
||
});
|
||
|
||
it("should detect Server Action by 'use server' directive", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/modules");
|
||
scanFiles(db, "src/modules");
|
||
scanSymbols(db);
|
||
const serverActions = db
|
||
.prepare("SELECT name FROM symbols WHERE is_server_action = 1 LIMIT 5")
|
||
.all() as { name: string }[];
|
||
expect(serverActions.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
it("should extract function signatures", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/modules");
|
||
scanFiles(db, "src/modules");
|
||
scanSymbols(db);
|
||
const withSignature = db
|
||
.prepare("SELECT signature FROM symbols WHERE signature IS NOT NULL AND signature != '' LIMIT 1")
|
||
.get() as { signature: string };
|
||
expect(withSignature).toBeDefined();
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试验证失败**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/scanner.test.ts`
|
||
Expected: FAIL,`scanSymbols` 未定义
|
||
|
||
- [ ] **Step 3: 实现 scanSymbols(使用 ts-morph)**
|
||
|
||
在 `scripts/arch-scan/scanner.ts` 中追加:
|
||
|
||
```typescript
|
||
import { Project, SyntaxKind, type Node, type FunctionDeclaration, type VariableDeclaration, type ClassDeclaration, type InterfaceDeclaration, type TypeAliasDeclaration } from "ts-morph";
|
||
|
||
function detectSymbolKind(node: Node): string {
|
||
if (Node.isFunctionDeclaration(node)) return "function";
|
||
if (Node.isVariableDeclaration(node)) {
|
||
const initializer = node.getInitializer();
|
||
if (initializer && SyntaxKind.ArrowFunction === initializer.getKind()) {
|
||
return "function";
|
||
}
|
||
if (initializer && SyntaxKind.ObjectLiteralExpression === initializer.getKind()) {
|
||
return "const";
|
||
}
|
||
return "const";
|
||
}
|
||
if (Node.isClassDeclaration(node)) return "class";
|
||
if (Node.isInterfaceDeclaration(node)) return "interface";
|
||
if (Node.isTypeAliasDeclaration(node)) return "type";
|
||
return "unknown";
|
||
}
|
||
|
||
function extractSignature(node: Node): string {
|
||
try {
|
||
const text = node.getText();
|
||
// 截取第一行作为签名
|
||
const firstLine = text.split("\n")[0];
|
||
return firstLine.slice(0, 200);
|
||
} catch {
|
||
return "";
|
||
}
|
||
}
|
||
|
||
function isExported(node: Node): boolean {
|
||
try {
|
||
const modifiers = (node as FunctionDeclaration).getModifiers?.() || [];
|
||
return modifiers.some((m) => m.getKind() === SyntaxKind.ExportKeyword);
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function isAsyncFunction(node: Node): boolean {
|
||
try {
|
||
const modifiers = (node as FunctionDeclaration).getModifiers?.() || [];
|
||
return modifiers.some((m) => m.getKind() === SyntaxKind.AsyncKeyword);
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
export function scanSymbols(db: Database.Database): void {
|
||
const project = new Project({
|
||
tsConfigFilePath: "./tsconfig.json",
|
||
skipAddingFilesFromTsConfig: true,
|
||
});
|
||
const files = db
|
||
.prepare("SELECT id, path FROM files")
|
||
.all() as { id: number; path: string }[];
|
||
const insertSymbol = db.prepare(
|
||
`INSERT OR IGNORE INTO symbols (file_id, name, kind, is_exported, is_async, is_server_action, signature, start_line, end_line)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||
);
|
||
for (const file of files) {
|
||
let sourceFile;
|
||
try {
|
||
sourceFile = project.addSourceFileAtPath(file.path);
|
||
} catch {
|
||
continue;
|
||
}
|
||
const fileContent = sourceFile.getFullText();
|
||
const isServerFile = fileContent.startsWith('"use server"') || fileContent.startsWith("'use server'");
|
||
const processNode = (node: Node): void => {
|
||
if (
|
||
Node.isFunctionDeclaration(node) ||
|
||
Node.isClassDeclaration(node) ||
|
||
Node.isInterfaceDeclaration(node) ||
|
||
Node.isTypeAliasDeclaration(node)
|
||
) {
|
||
const name = (node as FunctionDeclaration).getName?.() || "";
|
||
if (!name) return;
|
||
const kind = detectSymbolKind(node);
|
||
const exported = isExported(node);
|
||
const async = isAsyncFunction(node);
|
||
const signature = extractSignature(node);
|
||
const startLine = node.getStartLineNumber();
|
||
const endLine = node.getEndLineNumber();
|
||
const isServerAction = isServerFile && exported && async;
|
||
insertSymbol.run(file.id, name, kind, exported ? 1 : 0, async ? 1 : 0, isServerAction ? 1 : 0, signature, startLine, endLine);
|
||
}
|
||
if (Node.isVariableStatement(node)) {
|
||
const declarations = node.getDeclarations();
|
||
for (const decl of declarations) {
|
||
const name = decl.getName();
|
||
if (!name) continue;
|
||
const kind = detectSymbolKind(decl);
|
||
const exported = isExported(node);
|
||
const signature = extractSignature(decl);
|
||
const startLine = decl.getStartLineNumber();
|
||
const endLine = decl.getEndLineNumber();
|
||
insertSymbol.run(file.id, name, kind, exported ? 1 : 0, 0, 0, signature, startLine, endLine);
|
||
}
|
||
}
|
||
};
|
||
sourceFile.forEachChild(processNode);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行测试验证通过**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/scanner.test.ts`
|
||
Expected: PASS(10 个测试全过)
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/arch-scan/scanner.ts scripts/arch-scan/scanner.test.ts
|
||
git commit -m "feat(arch-scan): implement symbol scanner using ts-morph with signature extraction"
|
||
```
|
||
|
||
### Task 1.7: 实现调用关系扫描
|
||
|
||
**Files:**
|
||
- Modify: `scripts/arch-scan/scanner.ts`
|
||
- Test: `scripts/arch-scan/scanner.test.ts`
|
||
|
||
- [ ] **Step 1: 添加调用关系扫描测试**
|
||
|
||
在 `scripts/arch-scan/scanner.test.ts` 末尾追加:
|
||
|
||
```typescript
|
||
describe("scanCalls", () => {
|
||
it("should detect calls between symbols", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/modules");
|
||
scanFiles(db, "src/modules");
|
||
scanSymbols(db);
|
||
scanCalls(db);
|
||
const calls = db
|
||
.prepare("SELECT count(*) as count FROM calls")
|
||
.get() as { count: number };
|
||
expect(calls.count).toBeGreaterThan(0);
|
||
});
|
||
|
||
it("should record external calls (callee_external)", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/modules");
|
||
scanFiles(db, "src/modules");
|
||
scanSymbols(db);
|
||
scanCalls(db);
|
||
const externalCalls = db
|
||
.prepare("SELECT callee_external FROM calls WHERE callee_external IS NOT NULL LIMIT 5")
|
||
.all() as { callee_external: string }[];
|
||
expect(externalCalls.length).toBeGreaterThan(0);
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试验证失败**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/scanner.test.ts`
|
||
Expected: FAIL,`scanCalls` 未定义
|
||
|
||
- [ ] **Step 3: 实现 scanCalls**
|
||
|
||
在 `scripts/arch-scan/scanner.ts` 中追加:
|
||
|
||
```typescript
|
||
function findSymbolIdByName(
|
||
db: Database.Database,
|
||
name: string,
|
||
preferFileId?: number
|
||
): number | null {
|
||
if (preferFileId !== undefined) {
|
||
const sameFile = db
|
||
.prepare("SELECT id FROM symbols WHERE name = ? AND file_id = ? LIMIT 1")
|
||
.get(name, preferFileId) as { id: number } | undefined;
|
||
if (sameFile) return sameFile.id;
|
||
}
|
||
const anyFile = db
|
||
.prepare("SELECT id FROM symbols WHERE name = ? LIMIT 1")
|
||
.get(name) as { id: number } | undefined;
|
||
return anyFile?.id || null;
|
||
}
|
||
|
||
export function scanCalls(db: Database.Database): void {
|
||
const project = new Project({
|
||
tsConfigFilePath: "./tsconfig.json",
|
||
skipAddingFilesFromTsConfig: true,
|
||
});
|
||
const files = db
|
||
.prepare("SELECT id, path FROM files")
|
||
.all() as { id: number; path: string }[];
|
||
const insertCall = db.prepare(
|
||
`INSERT INTO calls (caller_id, callee_id, callee_external, call_line, count)
|
||
VALUES (?, ?, ?, ?, ?)`
|
||
);
|
||
for (const file of files) {
|
||
let sourceFile;
|
||
try {
|
||
sourceFile = project.addSourceFileAtPath(file.path);
|
||
} catch {
|
||
continue;
|
||
}
|
||
const fileSymbols = db
|
||
.prepare("SELECT id, name, start_line, end_line FROM symbols WHERE file_id = ?")
|
||
.all(file.id) as {
|
||
id: number;
|
||
name: string;
|
||
start_line: number;
|
||
end_line: number;
|
||
}[];
|
||
const symbolByLine = new Map<number, typeof fileSymbols[0]>();
|
||
for (const sym of fileSymbols) {
|
||
for (let line = sym.start_line; line <= sym.end_line; line++) {
|
||
symbolByLine.set(line, sym);
|
||
}
|
||
}
|
||
const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
||
for (const call of callExpressions) {
|
||
const callLine = call.getStartLineNumber();
|
||
const caller = symbolByLine.get(callLine);
|
||
if (!caller) continue;
|
||
const expr = call.getExpression();
|
||
let calleeName = "";
|
||
if (Node.isPropertyAccessExpression(expr)) {
|
||
calleeName = expr.getName();
|
||
} else if (Node.isIdentifier(expr)) {
|
||
calleeName = expr.getText();
|
||
}
|
||
if (!calleeName) continue;
|
||
const calleeId = findSymbolIdByName(db, calleeName, file.id);
|
||
const calleeExternal = calleeId ? null : calleeName;
|
||
insertCall.run(caller.id, calleeId, calleeExternal, callLine, 1);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行测试验证通过**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/scanner.test.ts`
|
||
Expected: PASS(12 个测试全过)
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/arch-scan/scanner.ts scripts/arch-scan/scanner.test.ts
|
||
git commit -m "feat(arch-scan): implement call relationship scanner with external call detection"
|
||
```
|
||
|
||
### Task 1.8: 实现技术标签扫描
|
||
|
||
**Files:**
|
||
- Modify: `scripts/arch-scan/scanner.ts`
|
||
- Test: `scripts/arch-scan/scanner.test.ts`
|
||
|
||
- [ ] **Step 1: 添加技术标签扫描测试**
|
||
|
||
在 `scripts/arch-scan/scanner.test.ts` 末尾追加:
|
||
|
||
```typescript
|
||
describe("scanTechTags", () => {
|
||
it("should detect cacheFn usage", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/modules");
|
||
scanFiles(db, "src/modules");
|
||
scanSymbols(db);
|
||
scanTechTags(db);
|
||
const cacheFnFiles = db
|
||
.prepare(
|
||
`SELECT s.name FROM symbols s
|
||
JOIN symbol_tech_tags stt ON s.id = stt.symbol_id
|
||
JOIN tech_tags t ON stt.tag_id = t.id
|
||
WHERE t.name = 'cacheFn' LIMIT 5`
|
||
)
|
||
.all() as { name: string }[];
|
||
expect(cacheFnFiles.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
it("should create tech_tags entries", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/modules");
|
||
scanFiles(db, "src/modules");
|
||
scanSymbols(db);
|
||
scanTechTags(db);
|
||
const tags = db
|
||
.prepare("SELECT name FROM tech_tags ORDER BY name")
|
||
.all() as { name: string }[];
|
||
const tagNames = tags.map((t) => t.name);
|
||
expect(tagNames).toContain("cacheFn");
|
||
expect(tagNames).toContain("zustand");
|
||
expect(tagNames).toContain("Drizzle");
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试验证失败**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/scanner.test.ts`
|
||
Expected: FAIL,`scanTechTags` 未定义
|
||
|
||
- [ ] **Step 3: 实现 scanTechTags**
|
||
|
||
在 `scripts/arch-scan/scanner.ts` 中追加:
|
||
|
||
```typescript
|
||
interface TechTagRule {
|
||
name: string;
|
||
category: string;
|
||
detect: (content: string, imports: string[]) => boolean;
|
||
}
|
||
|
||
const TECH_TAG_RULES: TechTagRule[] = [
|
||
{
|
||
name: "cacheFn",
|
||
category: "cache",
|
||
detect: (content) => content.includes("cacheFn("),
|
||
},
|
||
{
|
||
name: "zustand",
|
||
category: "state",
|
||
detect: (content, imports) => imports.some((i) => i.includes('"zustand"') || i.includes("'zustand'")),
|
||
},
|
||
{
|
||
name: "useOptimistic",
|
||
category: "state",
|
||
detect: (content) => content.includes("useOptimistic("),
|
||
},
|
||
{
|
||
name: "react-hook-form",
|
||
category: "form",
|
||
detect: (content, imports) => imports.some((i) => i.includes("react-hook-form")),
|
||
},
|
||
{
|
||
name: "TanStack Query",
|
||
category: "state",
|
||
detect: (content, imports) => imports.some((i) => i.includes("@tanstack/react-query")),
|
||
},
|
||
{
|
||
name: "Server Action",
|
||
category: "server",
|
||
detect: (content) => content.startsWith('"use server"') || content.startsWith("'use server'"),
|
||
},
|
||
{
|
||
name: "Tiptap",
|
||
category: "ui",
|
||
detect: (content, imports) => imports.some((i) => i.includes("@tiptap/")),
|
||
},
|
||
{
|
||
name: "Drizzle",
|
||
category: "db",
|
||
detect: (content, imports) => imports.some((i) => i.includes("drizzle-orm")),
|
||
},
|
||
{
|
||
name: "nuqs",
|
||
category: "state",
|
||
detect: (content, imports) => imports.some((i) => i.includes('"nuqs"') || i.includes("'nuqs'")),
|
||
},
|
||
{
|
||
name: "recharts",
|
||
category: "ui",
|
||
detect: (content, imports) => imports.some((i) => i.includes('"recharts"') || i.includes("'recharts'")),
|
||
},
|
||
];
|
||
|
||
export function scanTechTags(db: Database.Database): void {
|
||
const insertTag = db.prepare(
|
||
"INSERT OR IGNORE INTO tech_tags (name, category) VALUES (?, ?)"
|
||
);
|
||
for (const rule of TECH_TAG_RULES) {
|
||
insertTag.run(rule.name, rule.category);
|
||
}
|
||
const getTagId = db.prepare("SELECT id FROM tech_tags WHERE name = ?");
|
||
const insertSymbolTag = db.prepare(
|
||
"INSERT OR IGNORE INTO symbol_tech_tags (symbol_id, tag_id) VALUES (?, ?)"
|
||
);
|
||
const files = db
|
||
.prepare("SELECT id, path FROM files")
|
||
.all() as { id: number; path: string }[];
|
||
for (const file of files) {
|
||
let content: string;
|
||
try {
|
||
content = readFileSync(file.path, "utf-8");
|
||
} catch {
|
||
continue;
|
||
}
|
||
const importLines = content
|
||
.split("\n")
|
||
.filter((l) => l.startsWith("import "));
|
||
const detectedTags = TECH_TAG_RULES.filter((r) =>
|
||
r.detect(content, importLines)
|
||
);
|
||
if (detectedTags.length === 0) continue;
|
||
const fileSymbols = db
|
||
.prepare("SELECT id FROM symbols WHERE file_id = ?")
|
||
.all(file.id) as { id: number }[];
|
||
for (const tag of detectedTags) {
|
||
const tagRow = getTagId.get(tag.name) as { id: number };
|
||
for (const sym of fileSymbols) {
|
||
insertSymbolTag.run(sym.id, tagRow.id);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行测试验证通过**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/scanner.test.ts`
|
||
Expected: PASS(14 个测试全过)
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/arch-scan/scanner.ts scripts/arch-scan/scanner.test.ts
|
||
git commit -m "feat(arch-scan): implement tech tag scanner with 10 heuristic rules"
|
||
```
|
||
|
||
### Task 1.9: 实现模块间依赖扫描
|
||
|
||
**Files:**
|
||
- Modify: `scripts/arch-scan/scanner.ts`
|
||
- Test: `scripts/arch-scan/scanner.test.ts`
|
||
|
||
- [ ] **Step 1: 添加模块依赖扫描测试**
|
||
|
||
在 `scripts/arch-scan/scanner.test.ts` 末尾追加:
|
||
|
||
```typescript
|
||
describe("scanModuleDeps", () => {
|
||
it("should detect module dependencies from imports", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/modules");
|
||
scanFiles(db, "src/modules");
|
||
scanSymbols(db);
|
||
scanModuleDeps(db);
|
||
const deps = db
|
||
.prepare("SELECT count(*) as count FROM module_deps")
|
||
.get() as { count: number };
|
||
expect(deps.count).toBeGreaterThan(0);
|
||
});
|
||
|
||
it("should detect data-access-call dependency type", () => {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
scanModules(db, "src/modules");
|
||
scanFiles(db, "src/modules");
|
||
scanSymbols(db);
|
||
scanCalls(db);
|
||
scanModuleDeps(db);
|
||
const dataAccessDeps = db
|
||
.prepare("SELECT count(*) as count FROM module_deps WHERE dep_type = 'data-access-call'")
|
||
.get() as { count: number };
|
||
expect(dataAccessDeps.count).toBeGreaterThan(0);
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试验证失败**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/scanner.test.ts`
|
||
Expected: FAIL,`scanModuleDeps` 未定义
|
||
|
||
- [ ] **Step 3: 实现 scanModuleDeps**
|
||
|
||
在 `scripts/arch-scan/scanner.ts` 中追加:
|
||
|
||
```typescript
|
||
function extractModuleFromPath(importPath: string): string | null {
|
||
// 匹配 @/modules/xxx 或 @/shared/xxx
|
||
const modulesMatch = importPath.match(/@\/modules\/([^/"]+)/);
|
||
if (modulesMatch) return modulesMatch[1];
|
||
const sharedMatch = importPath.match(/@\/shared/);
|
||
if (sharedMatch) return "shared";
|
||
return null;
|
||
}
|
||
|
||
export function scanModuleDeps(db: Database.Database): void {
|
||
const insertDep = db.prepare(
|
||
`INSERT OR IGNORE INTO module_deps (source_module_id, target_module_id, dep_type)
|
||
VALUES (?, ?, ?)`
|
||
);
|
||
// 1. 从 file_imports 聚合 import 类型依赖
|
||
const imports = db
|
||
.prepare(
|
||
`SELECT fi.source_file_id, fi.import_path, f.module_id AS source_module_id
|
||
FROM file_imports fi
|
||
JOIN files f ON fi.source_file_id = f.id
|
||
WHERE fi.import_path LIKE '%@/modules/%' OR fi.import_path LIKE '%@/shared%'`
|
||
)
|
||
.all() as {
|
||
source_file_id: number;
|
||
import_path: string;
|
||
source_module_id: number;
|
||
}[];
|
||
const moduleByPath = db
|
||
.prepare("SELECT name, id FROM modules")
|
||
.all() as { name: string; id: number }[];
|
||
const moduleIdByName = new Map(moduleByPath.map((m) => [m.name, m.id]));
|
||
for (const imp of imports) {
|
||
const targetModuleName = extractModuleFromPath(imp.import_path);
|
||
if (!targetModuleName) continue;
|
||
const targetModuleId = moduleIdByName.get(targetModuleName);
|
||
if (!targetModuleId) continue;
|
||
if (imp.source_module_id === targetModuleId) continue;
|
||
insertDep.run(imp.source_module_id, targetModuleId, "import");
|
||
}
|
||
// 2. 从 calls 聚合 data-access-call 类型依赖
|
||
const crossModuleCalls = db
|
||
.prepare(
|
||
`SELECT DISTINCT f1.module_id AS source_module_id, f2.module_id AS target_module_id
|
||
FROM calls c
|
||
JOIN symbols s1 ON c.caller_id = s1.id
|
||
JOIN files f1 ON s1.file_id = f1.id
|
||
JOIN symbols s2 ON c.callee_id = s2.id
|
||
JOIN files f2 ON s2.file_id = f2.id
|
||
WHERE f1.module_id != f2.module_id
|
||
AND f2.kind = 'data-access'`
|
||
)
|
||
.all() as { source_module_id: number; target_module_id: number }[];
|
||
for (const call of crossModuleCalls) {
|
||
insertDep.run(call.source_module_id, call.target_module_id, "data-access-call");
|
||
}
|
||
}
|
||
|
||
export function scanFileImports(db: Database.Database): void {
|
||
const files = db
|
||
.prepare("SELECT id, path FROM files")
|
||
.all() as { id: number; path: string }[];
|
||
const insertImport = db.prepare(
|
||
`INSERT INTO file_imports (source_file_id, imported_file_id, import_path, is_type_only, imported_names)
|
||
VALUES (?, ?, ?, ?, ?)`
|
||
);
|
||
for (const file of files) {
|
||
let content: string;
|
||
try {
|
||
content = readFileSync(file.path, "utf-8");
|
||
} catch {
|
||
continue;
|
||
}
|
||
const importRegex = /^import\s+(?:type\s+)?(.+?)\s+from\s+["']([^"']+)["']/gm;
|
||
let match: RegExpExecArray | null;
|
||
while ((match = importRegex.exec(content)) !== null) {
|
||
const isTypeOnly = match[0].startsWith("import type");
|
||
const importedNames = match[1];
|
||
const importPath = match[2];
|
||
let importedFileId: number | null = null;
|
||
if (importPath.startsWith("@/")) {
|
||
const resolvedPath = importPath.replace("@/", "src/").replace(/\.(ts|tsx)$/, "");
|
||
const candidatePaths = [resolvedPath, `${resolvedPath}.ts`, `${resolvedPath}.tsx`, `${resolvedPath}/index.ts`];
|
||
for (const candidate of candidatePaths) {
|
||
const result = db
|
||
.prepare("SELECT id FROM files WHERE path = ?")
|
||
.get(candidate) as { id: number } | undefined;
|
||
if (result) {
|
||
importedFileId = result.id;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
insertImport.run(file.id, importedFileId, importPath, isTypeOnly ? 1 : 0, importedNames);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行测试验证通过**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/scanner.test.ts`
|
||
Expected: PASS(16 个测试全过)
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/arch-scan/scanner.ts scripts/arch-scan/scanner.test.ts
|
||
git commit -m "feat(arch-scan): implement module dependency scanner with import and data-access-call detection"
|
||
```
|
||
|
||
### Task 1.10: 实现查询函数
|
||
|
||
**Files:**
|
||
- Modify: `scripts/arch-scan/query.ts`
|
||
- Test: `scripts/arch-scan/query.test.ts`
|
||
|
||
- [ ] **Step 1: 编写查询函数测试**
|
||
|
||
```typescript
|
||
// scripts/arch-scan/query.test.ts
|
||
import { describe, it, expect } from "vitest";
|
||
import Database from "better-sqlite3";
|
||
import { initSchema, clearAllData } from "./schema";
|
||
import {
|
||
querySymbolRefs,
|
||
querySymbolForwardCalls,
|
||
queryModuleDeps,
|
||
queryModuleReverseDeps,
|
||
queryTechUsage,
|
||
queryViolations,
|
||
} from "./query";
|
||
|
||
function setupTestDb(): Database.Database {
|
||
const db = new Database(":memory:");
|
||
initSchema(db);
|
||
// 插入测试数据
|
||
db.prepare(
|
||
"INSERT INTO modules (name, path, layer, created_at) VALUES (?, ?, ?, ?)"
|
||
).run("exams", "src/modules/exams", "modules", "2026-07-07");
|
||
db.prepare(
|
||
"INSERT INTO modules (name, path, layer, created_at) VALUES (?, ?, ?, ?)"
|
||
).run("grades", "src/modules/grades", "modules", "2026-07-07");
|
||
db.prepare(
|
||
"INSERT INTO files (module_id, path, kind, lines) VALUES (?, ?, ?, ?)"
|
||
).run(1, "src/modules/exams/actions.ts", "actions", 100);
|
||
db.prepare(
|
||
"INSERT INTO files (module_id, path, kind, lines) VALUES (?, ?, ?, ?)"
|
||
).run(1, "src/modules/exams/data-access.ts", "data-access", 200);
|
||
db.prepare(
|
||
"INSERT INTO symbols (file_id, name, kind, is_exported, is_async, is_server_action, start_line, end_line) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||
).run(1, "createExamAction", "function", 1, 1, 1, 10, 50);
|
||
db.prepare(
|
||
"INSERT INTO symbols (file_id, name, kind, is_exported, is_async, is_server_action, start_line, end_line) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||
).run(2, "createExam", "function", 1, 1, 0, 20, 80);
|
||
db.prepare(
|
||
"INSERT INTO calls (caller_id, callee_id, call_line, count) VALUES (?, ?, ?, ?)"
|
||
).run(1, 2, 30, 1); // createExamAction 调用 createExam
|
||
db.prepare(
|
||
"INSERT INTO module_deps (source_module_id, target_module_id, dep_type) VALUES (?, ?, ?)"
|
||
).run(1, 2, "data-access-call"); // exams 依赖 grades
|
||
return db;
|
||
}
|
||
|
||
describe("querySymbolRefs", () => {
|
||
it("should find callers of a symbol (reverse)", () => {
|
||
const db = setupTestDb();
|
||
const refs = querySymbolRefs(db, "createExam");
|
||
expect(refs).toHaveLength(1);
|
||
expect(refs[0].caller_name).toBe("createExamAction");
|
||
});
|
||
});
|
||
|
||
describe("querySymbolForwardCalls", () => {
|
||
it("should find callees of a symbol (forward)", () => {
|
||
const db = setupTestDb();
|
||
const calls = querySymbolForwardCalls(db, "createExamAction");
|
||
expect(calls).toHaveLength(1);
|
||
expect(calls[0].callee_name).toBe("createExam");
|
||
});
|
||
});
|
||
|
||
describe("queryModuleDeps", () => {
|
||
it("should find module dependencies (forward)", () => {
|
||
const db = setupTestDb();
|
||
const deps = queryModuleDeps(db, "exams");
|
||
expect(deps).toContainEqual({ target_module: "grades", dep_type: "data-access-call" });
|
||
});
|
||
});
|
||
|
||
describe("queryModuleReverseDeps", () => {
|
||
it("should find module reverse dependencies", () => {
|
||
const db = setupTestDb();
|
||
const deps = queryModuleReverseDeps(db, "grades");
|
||
expect(deps).toContainEqual({ source_module: "exams", dep_type: "data-access-call" });
|
||
});
|
||
});
|
||
|
||
describe("queryViolations", () => {
|
||
it("should detect files over 800 lines", () => {
|
||
const db = setupTestDb();
|
||
// 插入一个超长文件
|
||
db.prepare("INSERT INTO files (module_id, path, kind, lines) VALUES (?, ?, ?, ?)").run(
|
||
1,
|
||
"src/modules/exams/huge.ts",
|
||
"lib",
|
||
900
|
||
);
|
||
const violations = queryViolations(db);
|
||
expect(violations.long_files).toContainEqual({
|
||
path: "src/modules/exams/huge.ts",
|
||
lines: 900,
|
||
});
|
||
});
|
||
|
||
it("should detect Server Actions without requirePermission", () => {
|
||
const db = setupTestDb();
|
||
const violations = queryViolations(db);
|
||
// createExamAction 是 Server Action 但没有调用 requirePermission
|
||
const missing = violations.server_actions_without_permission.find(
|
||
(v) => v.name === "createExamAction"
|
||
);
|
||
expect(missing).toBeDefined();
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 运行测试验证失败**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/query.test.ts`
|
||
Expected: FAIL,查询函数未定义
|
||
|
||
- [ ] **Step 3: 实现查询函数**
|
||
|
||
```typescript
|
||
// scripts/arch-scan/query.ts
|
||
import type Database from "better-sqlite3";
|
||
|
||
export interface SymbolRef {
|
||
caller_name: string;
|
||
caller_path: string;
|
||
caller_line: number;
|
||
is_server_action: boolean;
|
||
depth: number;
|
||
path_chain: string;
|
||
}
|
||
|
||
export function querySymbolRefs(
|
||
db: Database.Database,
|
||
symbolName: string,
|
||
maxDepth = 10
|
||
): SymbolRef[] {
|
||
const sql = `
|
||
WITH RECURSIVE upstream(caller_id, caller_name, caller_path, caller_line, is_sa, depth, path_chain) AS (
|
||
SELECT c.caller_id, s.name, f.path, c.call_line, s.is_server_action, 0, s.name
|
||
FROM calls c
|
||
JOIN symbols s ON c.caller_id = s.id
|
||
JOIN files f ON s.file_id = f.id
|
||
WHERE c.callee_id = (SELECT id FROM symbols WHERE name = ? LIMIT 1)
|
||
UNION
|
||
SELECT c.caller_id, s.name, f.path, c.call_line, s.is_server_action, u.depth + 1,
|
||
u.path_chain || ' → ' || s.name
|
||
FROM upstream u
|
||
JOIN calls c ON c.callee_id = u.caller_id
|
||
JOIN symbols s ON c.caller_id = s.id
|
||
JOIN files f ON s.file_id = f.id
|
||
WHERE u.depth < ?
|
||
AND u.caller_id NOT IN (SELECT caller_id FROM upstream)
|
||
)
|
||
SELECT caller_name, caller_path, caller_line, is_sa as is_server_action, depth, path_chain
|
||
FROM upstream ORDER BY depth, caller_path`;
|
||
return db.prepare(sql).all(symbolName, maxDepth) as SymbolRef[];
|
||
}
|
||
|
||
export interface SymbolCall {
|
||
callee_name: string;
|
||
callee_path: string;
|
||
callee_line: number;
|
||
depth: number;
|
||
path_chain: string;
|
||
}
|
||
|
||
export function querySymbolForwardCalls(
|
||
db: Database.Database,
|
||
symbolName: string,
|
||
maxDepth = 10
|
||
): SymbolCall[] {
|
||
const sql = `
|
||
WITH RECURSIVE downstream(callee_id, callee_name, callee_path, callee_line, depth, path_chain) AS (
|
||
SELECT c.callee_id, s.name, f.path, c.call_line, 0, ?
|
||
FROM calls c
|
||
JOIN symbols s ON c.callee_id = s.id
|
||
JOIN files f ON s.file_id = f.id
|
||
WHERE c.caller_id = (SELECT id FROM symbols WHERE name = ? LIMIT 1)
|
||
UNION
|
||
SELECT c.callee_id, s.name, f.path, c.call_line, d.depth + 1,
|
||
d.path_chain || ' → ' || s.name
|
||
FROM downstream d
|
||
JOIN calls c ON c.caller_id = d.callee_id
|
||
JOIN symbols s ON c.callee_id = s.id
|
||
JOIN files f ON s.file_id = f.id
|
||
WHERE d.depth < ?
|
||
AND d.callee_id NOT IN (SELECT callee_id FROM downstream)
|
||
)
|
||
SELECT callee_name, callee_path, callee_line, depth, path_chain
|
||
FROM downstream ORDER BY depth, callee_path`;
|
||
return db.prepare(sql).all(symbolName, symbolName, maxDepth) as SymbolCall[];
|
||
}
|
||
|
||
export interface ModuleDep {
|
||
target_module: string;
|
||
dep_type: string;
|
||
}
|
||
|
||
export function queryModuleDeps(
|
||
db: Database.Database,
|
||
moduleName: string,
|
||
maxDepth = 10
|
||
): ModuleDep[] {
|
||
const sql = `
|
||
SELECT DISTINCT m2.name AS target_module, md.dep_type
|
||
FROM module_deps md
|
||
JOIN modules m ON md.source_module_id = m.id
|
||
JOIN modules m2 ON md.target_module_id = m2.id
|
||
WHERE m.name = ?`;
|
||
return db.prepare(sql).all(moduleName) as ModuleDep[];
|
||
}
|
||
|
||
export interface ModuleReverseDep {
|
||
source_module: string;
|
||
dep_type: string;
|
||
}
|
||
|
||
export function queryModuleReverseDeps(
|
||
db: Database.Database,
|
||
moduleName: string,
|
||
maxDepth = 10
|
||
): ModuleReverseDep[] {
|
||
const sql = `
|
||
SELECT DISTINCT m.name AS source_module, md.dep_type
|
||
FROM module_deps md
|
||
JOIN modules m ON md.source_module_id = m.id
|
||
JOIN modules m2 ON md.target_module_id = m2.id
|
||
WHERE m2.name = ?`;
|
||
return db.prepare(sql).all(moduleName) as ModuleReverseDep[];
|
||
}
|
||
|
||
export interface TechUsage {
|
||
symbol_name: string;
|
||
file_path: string;
|
||
tag_name: string;
|
||
}
|
||
|
||
export function queryTechUsage(
|
||
db: Database.Database,
|
||
tagName: string
|
||
): TechUsage[] {
|
||
const sql = `
|
||
SELECT s.name AS symbol_name, f.path AS file_path, t.name AS tag_name
|
||
FROM symbols s
|
||
JOIN symbol_tech_tags stt ON s.id = stt.symbol_id
|
||
JOIN tech_tags t ON stt.tag_id = t.id
|
||
JOIN files f ON s.file_id = f.id
|
||
WHERE t.name = ?`;
|
||
return db.prepare(sql).all(tagName) as TechUsage[];
|
||
}
|
||
|
||
export interface Violations {
|
||
long_files: { path: string; lines: number }[];
|
||
server_actions_without_permission: { name: string; path: string }[];
|
||
}
|
||
|
||
export function queryViolations(db: Database.Database): Violations {
|
||
const longFiles = db
|
||
.prepare("SELECT path, lines FROM files WHERE lines > 800 ORDER BY lines DESC")
|
||
.all() as { path: string; lines: number }[];
|
||
const serverActionsWithoutPerm = db
|
||
.prepare(
|
||
`SELECT s.name, f.path FROM symbols s
|
||
JOIN files f ON s.file_id = f.id
|
||
WHERE s.is_server_action = 1
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM calls c
|
||
JOIN symbols cs ON c.callee_id = cs.id
|
||
WHERE c.caller_id = s.id AND cs.name = 'requirePermission'
|
||
)`
|
||
)
|
||
.all() as { name: string; path: string }[];
|
||
return {
|
||
long_files: longFiles,
|
||
server_actions_without_permission: serverActionsWithoutPerm,
|
||
};
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行测试验证通过**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/query.test.ts`
|
||
Expected: PASS(6 个测试全过)
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/arch-scan/query.ts scripts/arch-scan/query.test.ts
|
||
git commit -m "feat(arch-scan): implement query functions for refs, deps, tech usage, and violations"
|
||
```
|
||
|
||
### Task 1.11: 实现 CLI 入口
|
||
|
||
**Files:**
|
||
- Modify: `scripts/arch-scan/cli.ts`
|
||
- Modify: `scripts/arch-scan/index.ts`
|
||
- Modify: `package.json`
|
||
|
||
- [ ] **Step 1: 实现 CLI 主入口**
|
||
|
||
```typescript
|
||
// scripts/arch-scan/index.ts
|
||
import Database from "better-sqlite3";
|
||
import { initSchema, clearAllData } from "./schema";
|
||
import {
|
||
scanAllModules,
|
||
scanFiles,
|
||
scanSymbols,
|
||
scanCalls,
|
||
scanTechTags,
|
||
scanFileImports,
|
||
scanModuleDeps,
|
||
} from "./scanner";
|
||
import { querySymbolRefs, querySymbolForwardCalls, queryModuleDeps, queryModuleReverseDeps, queryTechUsage, queryViolations } from "./query";
|
||
import { execSync } from "node:child_process";
|
||
|
||
const DB_PATH = "scripts/arch-scan/arch.db";
|
||
|
||
function getCommitHash(): string {
|
||
try {
|
||
return execSync("git rev-parse --short HEAD").toString().trim();
|
||
} catch {
|
||
return "unknown";
|
||
}
|
||
}
|
||
|
||
export function runScan(): void {
|
||
console.log("Starting architecture scan...");
|
||
const db = new Database(DB_PATH);
|
||
db.pragma("journal_mode = WAL");
|
||
initSchema(db);
|
||
clearAllData(db);
|
||
console.log("Schema initialized.");
|
||
scanAllModules(db, process.cwd());
|
||
console.log("Modules scanned.");
|
||
scanFiles(db, "src/modules");
|
||
scanFiles(db, "src/shared");
|
||
console.log("Files scanned.");
|
||
scanSymbols(db);
|
||
console.log("Symbols scanned.");
|
||
scanCalls(db);
|
||
console.log("Calls scanned.");
|
||
scanTechTags(db);
|
||
console.log("Tech tags scanned.");
|
||
scanFileImports(db);
|
||
scanModuleDeps(db);
|
||
console.log("Module dependencies scanned.");
|
||
const now = new Date().toISOString();
|
||
const commit = getCommitHash();
|
||
const meta = db.prepare(
|
||
"INSERT OR REPLACE INTO scan_meta (key, value) VALUES (?, ?)"
|
||
);
|
||
meta.run("scanned_at", now);
|
||
meta.run("commit_hash", commit);
|
||
meta.run("scanner_version", "1.0.0");
|
||
const stats = {
|
||
files: (db.prepare("SELECT count(*) as c FROM files").get() as { c: number }).c,
|
||
symbols: (db.prepare("SELECT count(*) as c FROM symbols").get() as { c: number }).c,
|
||
calls: (db.prepare("SELECT count(*) as c FROM calls").get() as { c: number }).c,
|
||
};
|
||
meta.run("total_files", String(stats.files));
|
||
meta.run("total_symbols", String(stats.symbols));
|
||
meta.run("total_calls", String(stats.calls));
|
||
console.log(`Scan complete: ${stats.files} files, ${stats.symbols} symbols, ${stats.calls} calls`);
|
||
console.log(`Commit: ${commit}, Time: ${now}`);
|
||
db.close();
|
||
}
|
||
|
||
export function runQuery(args: string[]): void {
|
||
const db = new Database(DB_PATH, { readonly: true });
|
||
const command = args[0];
|
||
switch (command) {
|
||
case "ref": {
|
||
const symbol = args[1];
|
||
if (!symbol) {
|
||
console.error("Usage: arch:query ref <symbolName> [--forward] [--depth=N]");
|
||
process.exit(1);
|
||
}
|
||
const forward = args.includes("--forward");
|
||
const depthArg = args.find((a) => a.startsWith("--depth="));
|
||
const depth = depthArg ? parseInt(depthArg.split("=")[1], 10) : 10;
|
||
if (forward) {
|
||
const calls = querySymbolForwardCalls(db, symbol, depth);
|
||
console.log(`▼ ${symbol}`);
|
||
for (const call of calls) {
|
||
const indent = " ".repeat(call.depth + 1);
|
||
console.log(`${indent}└─▶ ${call.callee_name} (${call.callee_path}#L${call.callee_line})`);
|
||
}
|
||
} else {
|
||
const refs = querySymbolRefs(db, symbol, depth);
|
||
console.log(`▼ ${symbol}`);
|
||
for (const ref of refs) {
|
||
const indent = " ".repeat(ref.depth + 1);
|
||
console.log(`${indent}├─▼ ${ref.caller_name} (${ref.caller_path}#L${ref.caller_line})${ref.is_server_action ? " [Server Action]" : ""}`);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
case "module": {
|
||
const module = args[1];
|
||
if (!module) {
|
||
console.error("Usage: arch:query module <moduleName> [--reverse] [--depth=N]");
|
||
process.exit(1);
|
||
}
|
||
const reverse = args.includes("--reverse");
|
||
if (reverse) {
|
||
const deps = queryModuleReverseDeps(db, module);
|
||
console.log(`▼ ${module} (reverse deps)`);
|
||
for (const dep of deps) {
|
||
console.log(` └─◀ ${dep.source_module} [${dep.dep_type}]`);
|
||
}
|
||
} else {
|
||
const deps = queryModuleDeps(db, module);
|
||
console.log(`▼ ${module} (forward deps)`);
|
||
for (const dep of deps) {
|
||
console.log(` └─▶ ${dep.target_module} [${dep.dep_type}]`);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
case "tech": {
|
||
const tag = args[1];
|
||
if (!tag) {
|
||
console.error("Usage: arch:query tech <tagName>");
|
||
process.exit(1);
|
||
}
|
||
const usage = queryTechUsage(db, tag);
|
||
console.log(`▼ tech: ${tag} (${usage.length} usages)`);
|
||
for (const u of usage) {
|
||
console.log(` └─ ${u.symbol_name} (${u.file_path})`);
|
||
}
|
||
break;
|
||
}
|
||
case "violations": {
|
||
const v = queryViolations(db);
|
||
console.log("▼ Architecture Violations");
|
||
console.log(` Long files (>800 lines): ${v.long_files.length}`);
|
||
for (const f of v.long_files) {
|
||
console.log(` └─ ${f.path}: ${f.lines} lines`);
|
||
}
|
||
console.log(` Server Actions without requirePermission: ${v.server_actions_without_permission.length}`);
|
||
for (const sa of v.server_actions_without_permission) {
|
||
console.log(` └─ ${sa.name} (${sa.path})`);
|
||
}
|
||
break;
|
||
}
|
||
case "sql": {
|
||
const sql = args.slice(1).join(" ");
|
||
if (!sql) {
|
||
console.error("Usage: arch:query sql \"<SQL>\"");
|
||
process.exit(1);
|
||
}
|
||
const rows = db.prepare(sql).all();
|
||
console.log(JSON.stringify(rows, null, 2));
|
||
break;
|
||
}
|
||
case "repl": {
|
||
console.log("Entering SQLite REPL. Type .exit to quit.");
|
||
console.log(`DB: ${DB_PATH}`);
|
||
// 简化:直接调用 sqlite3 CLI
|
||
const { spawn } = require("node:child_process");
|
||
const repl = spawn("sqlite3", [DB_PATH], { stdio: "inherit" });
|
||
repl.on("exit", (code: number) => process.exit(code));
|
||
break;
|
||
}
|
||
default:
|
||
console.error("Unknown command. Available: ref, module, tech, violations, sql, repl");
|
||
process.exit(1);
|
||
}
|
||
db.close();
|
||
}
|
||
```
|
||
|
||
```typescript
|
||
// scripts/arch-scan/cli.ts
|
||
import { runScan, runQuery } from "./index";
|
||
|
||
const args = process.argv.slice(2);
|
||
const command = args[0];
|
||
|
||
if (command === "scan") {
|
||
runScan();
|
||
} else if (command === "query") {
|
||
runQuery(args.slice(1));
|
||
} else {
|
||
console.log("Usage:");
|
||
console.log(" npm run arch:scan");
|
||
console.log(" npm run arch:query <command> [args]");
|
||
console.log("");
|
||
console.log("Commands:");
|
||
console.log(" scan Scan codebase and update arch.db");
|
||
console.log(" query ref <symbol> [--forward] Query symbol references");
|
||
console.log(" query module <name> [--reverse] Query module dependencies");
|
||
console.log(" query tech <tag> Query tech tag usage");
|
||
console.log(" query violations Detect architecture violations");
|
||
console.log(" query sql \"<SQL>\" Run free SQL");
|
||
console.log(" query repl SQLite REPL");
|
||
process.exit(1);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 添加 npm scripts**
|
||
|
||
在 `package.json` 的 `scripts` 中添加:
|
||
|
||
```json
|
||
"arch:scan": "tsx scripts/arch-scan/cli.ts scan",
|
||
"arch:query": "tsx scripts/arch-scan/cli.ts query"
|
||
```
|
||
|
||
- [ ] **Step 3: 测试 CLI**
|
||
|
||
Run: `npm run arch:scan`
|
||
Expected: 输出扫描进度,最终显示文件数、符号数、调用数
|
||
|
||
Run: `npm run arch:query -- ref createExam`
|
||
Expected: 输出 createExam 的引用者树形图
|
||
|
||
Run: `npm run arch:query -- violations`
|
||
Expected: 输出架构违规列表
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add scripts/arch-scan/cli.ts scripts/arch-scan/index.ts package.json
|
||
git commit -m "feat(arch-scan): implement CLI with scan and query commands"
|
||
```
|
||
|
||
### Task 1.12: 配置 .gitignore 和 CI
|
||
|
||
**Files:**
|
||
- Modify: `.gitignore`
|
||
- Create: `scripts/arch-scan/.gitignore`
|
||
|
||
- [ ] **Step 1: 配置 arch.db 的 git 跟踪策略**
|
||
|
||
arch.db 应该提交到 git(让 AI 工作前能查到),但 WAL/SHM 临时文件不提交:
|
||
|
||
```bash
|
||
# 创建 scripts/arch-scan/.gitignore
|
||
echo "*.wal
|
||
*.shm
|
||
*.tmp" > scripts/arch-scan/.gitignore
|
||
```
|
||
|
||
- [ ] **Step 2: 验证 arch.db 被跟踪**
|
||
|
||
Run: `git status`
|
||
Expected: `scripts/arch-scan/arch.db` 出现在未跟踪文件中
|
||
|
||
```bash
|
||
git add scripts/arch-scan/arch.db scripts/arch-scan/.gitignore
|
||
```
|
||
|
||
- [ ] **Step 3: 配置 CI 自动扫描**
|
||
|
||
修改 `.gitea/workflows/ci.yml`,在 build job 之后添加 arch-scan step:
|
||
|
||
```yaml
|
||
arch-scan:
|
||
needs: build
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: '20'
|
||
cache: 'npm'
|
||
- run: npm ci
|
||
- run: npm run arch:scan
|
||
- name: Verify arch.db
|
||
run: |
|
||
if [ ! -f scripts/arch-scan/arch.db ]; then
|
||
echo "arch.db not generated"
|
||
exit 1
|
||
fi
|
||
- name: Check for violations
|
||
run: npm run arch:query -- violations
|
||
- name: Commit updated arch.db
|
||
run: |
|
||
git config user.name "CI Bot"
|
||
git config user.email "ci@example.com"
|
||
git add scripts/arch-scan/arch.db
|
||
git commit -m "chore(arch-scan): update arch.db [skip ci]" || true
|
||
git push
|
||
```
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add .gitea/workflows/ci.yml scripts/arch-scan/.gitignore scripts/arch-scan/arch.db
|
||
git commit -m "ci(arch-scan): add automatic scan job and commit arch.db on CI"
|
||
```
|
||
|
||
### Task 1.13: 子计划 1 验收
|
||
|
||
- [ ] **Step 1: 运行所有测试**
|
||
|
||
Run: `npx vitest run scripts/arch-scan/`
|
||
Expected: 所有测试 PASS
|
||
|
||
- [ ] **Step 2: 运行 lint 和 tsc**
|
||
|
||
Run: `npm run lint && npx tsc --noEmit`
|
||
Expected: 零错误
|
||
|
||
- [ ] **Step 3: 端到端验证**
|
||
|
||
```bash
|
||
npm run arch:scan
|
||
npm run arch:query -- ref createExam
|
||
npm run arch:query -- module exams
|
||
npm run arch:query -- tech cacheFn
|
||
npm run arch:query -- violations
|
||
```
|
||
|
||
Expected: 全部命令成功执行,输出合理结果
|
||
|
||
- [ ] **Step 4: Commit 验收标记**
|
||
|
||
```bash
|
||
git commit --allow-empty -m "feat(arch-scan): subplan 1 complete - arch.db scanner fully functional"
|
||
```
|
||
|
||
---
|
||
|
||
## 子计划 2: 文档体系重设计
|
||
|
||
**目标:** 004 瘦身、005 废弃、known-issues 精简、roadmap 创建、audit 归档
|
||
|
||
**依赖:** 子计划 1(使用 arch.db 查询实际代码结构)
|
||
|
||
### Task 2.1: 创建 roadmap/ 目录结构
|
||
|
||
**Files:**
|
||
- Create: `docs/architecture/roadmap/README.md`
|
||
- Create: `docs/architecture/roadmap/tech-debt.md`
|
||
- Create: `docs/architecture/roadmap/decoupling.md`
|
||
- Create: `docs/architecture/roadmap/pending-features.md`
|
||
|
||
- [ ] **Step 1: 创建 roadmap 索引**
|
||
|
||
```markdown
|
||
<!-- docs/architecture/roadmap/README.md -->
|
||
# 路线图索引
|
||
|
||
> 本目录存放项目长远规划,与架构事实(004)分离。
|
||
|
||
## 文档清单
|
||
|
||
| 文档 | 用途 |
|
||
|------|------|
|
||
| [tech-debt.md](./tech-debt.md) | 技术债清单 |
|
||
| [decoupling.md](./decoupling.md) | 解耦路线图 |
|
||
| [pending-features.md](./pending-features.md) | 待开发功能 |
|
||
|
||
## 维护规则
|
||
|
||
- 规划实现后从本目录删除,迁入 004 架构事实或 git 历史
|
||
- 不含架构事实,不含经验(经验查 known-issues.md)
|
||
```
|
||
|
||
- [ ] **Step 2: 创建 tech-debt.md**
|
||
|
||
```markdown
|
||
<!-- docs/architecture/roadmap/tech-debt.md -->
|
||
# 技术债清单
|
||
|
||
> 从 004 第三部分"已知架构问题和技术债"迁入。
|
||
|
||
## 待解决项
|
||
|
||
(从 004 迁入后填充)
|
||
|
||
## 已解决项
|
||
|
||
(已解决项删除,git 历史已记录)
|
||
```
|
||
|
||
- [ ] **Step 3: 创建 decoupling.md**
|
||
|
||
```markdown
|
||
<!-- docs/architecture/roadmap/decoupling.md -->
|
||
# 解耦路线图
|
||
|
||
> 从 docs/architecture/audit/01_decoupling_roadmap.md 迁入。
|
||
|
||
## 解耦目标
|
||
|
||
(迁入后填充)
|
||
```
|
||
|
||
- [ ] **Step 4: 创建 pending-features.md**
|
||
|
||
```markdown
|
||
<!-- docs/architecture/roadmap/pending-features.md -->
|
||
# 待开发功能
|
||
|
||
> 从 004 各模块"未完成项"迁入。
|
||
|
||
## 待开发功能清单
|
||
|
||
(迁入后填充)
|
||
```
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add docs/architecture/roadmap/
|
||
git commit -m "docs(roadmap): create roadmap directory with tech-debt, decoupling, pending-features"
|
||
```
|
||
|
||
### Task 2.2: 归档 audit 报告
|
||
|
||
**Files:**
|
||
- Move: `docs/architecture/audit/*` → `docs/architecture/audit/archive/`
|
||
|
||
- [ ] **Step 1: 创建归档目录**
|
||
|
||
```bash
|
||
mkdir -p docs/architecture/audit/archive
|
||
```
|
||
|
||
- [ ] **Step 2: 移动所有 audit 报告到 archive**
|
||
|
||
```bash
|
||
# 移动除 01_decoupling_roadmap.md 外的所有报告
|
||
cd docs/architecture/audit
|
||
for f in *.md; do
|
||
if [ "$f" != "01_decoupling_roadmap.md" ]; then
|
||
mv "$f" archive/
|
||
fi
|
||
done
|
||
```
|
||
|
||
- [ ] **Step 3: 移动 01_decoupling_roadmap.md 到 roadmap/**
|
||
|
||
```bash
|
||
mv docs/architecture/audit/01_decoupling_roadmap.md docs/architecture/roadmap/decoupling.md
|
||
```
|
||
|
||
- [ ] **Step 4: 归档 005_architecture_data.json**
|
||
|
||
```bash
|
||
mv docs/architecture/005_architecture_data.json docs/architecture/audit/archive/
|
||
```
|
||
|
||
- [ ] **Step 5: 创建 archive README**
|
||
|
||
```markdown
|
||
<!-- docs/architecture/audit/archive/README.md -->
|
||
# 历史审查报告归档
|
||
|
||
> 本目录为只读归档,不再更新。
|
||
> 有价值的内容已提取到模块 README 和 known-issues.md。
|
||
|
||
## 归档文件
|
||
|
||
- 005_architecture_data.json(已废弃,由 arch.db 替代)
|
||
- 60+ 份模块审查报告(历史参考)
|
||
```
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add -A docs/architecture/
|
||
git commit -m "docs(audit): archive 60+ audit reports and deprecated 005 JSON"
|
||
```
|
||
|
||
### Task 2.3: 004 瘦身
|
||
|
||
**Files:**
|
||
- Modify: `docs/architecture/004_architecture_impact_map.md`
|
||
|
||
- [ ] **Step 1: 备份原 004**
|
||
|
||
```bash
|
||
cp docs/architecture/004_architecture_impact_map.md docs/architecture/audit/archive/004_architecture_impact_map_v1.md
|
||
```
|
||
|
||
- [ ] **Step 2: 使用 arch.db 提取实际架构数据**
|
||
|
||
Run: `npm run arch:query -- sql "SELECT name, path FROM modules WHERE layer='modules' ORDER BY name"`
|
||
Expected: 输出所有模块清单
|
||
|
||
Run: `npm run arch:query -- sql "SELECT source_module, target_module FROM module_deps"`
|
||
Expected: 输出所有模块依赖关系
|
||
|
||
- [ ] **Step 3: 重写 004 为瘦身后版本**
|
||
|
||
将 004 重写为约 500 行的瘦身后版本,包含 6 个章节(分层架构、模块清单表格、依赖关系图、数据流向、核心原则、设计令牌体系)。删除所有:
|
||
- 1.1.1-1.1.7 变更日志章节
|
||
- "Phase X.X 新增"标记
|
||
- "P0-X 已修复"标记
|
||
- "未完成项(待后续专项)"
|
||
- 各模块"V1/V2/V3/V4"版本描述
|
||
- 函数签名索引(附录 C)
|
||
- 模块间依赖矩阵(附录 A)
|
||
- 第三部分"已知架构问题和技术债"
|
||
|
||
保留:
|
||
- 分层架构图
|
||
- 模块清单(精简为表格)
|
||
- 依赖关系图
|
||
- 数据流向图
|
||
- 核心架构原则
|
||
- 设计令牌体系
|
||
- 关键参数影响链(附录 B,作为架构决策)
|
||
|
||
- [ ] **Step 4: 验证行数**
|
||
|
||
Run: `Get-Content docs/architecture/004_architecture_impact_map.md | Measure-Object -Line`
|
||
Expected: 约 500 行(±50)
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add docs/architecture/004_architecture_impact_map.md docs/architecture/audit/archive/004_architecture_impact_map_v1.md
|
||
git commit -m "docs(004): slim down architecture map from 4227 to ~500 lines, move history to archive"
|
||
```
|
||
|
||
### Task 2.4: known-issues.md 精简
|
||
|
||
**Files:**
|
||
- Modify: `docs/troubleshooting/known-issues.md`
|
||
|
||
- [ ] **Step 1: 备份原 known-issues**
|
||
|
||
```bash
|
||
cp docs/troubleshooting/known-issues.md docs/architecture/audit/archive/known-issues_v1.md
|
||
```
|
||
|
||
- [ ] **Step 2: 重写 known-issues.md 为索引式**
|
||
|
||
重写为 3 段式结构:
|
||
1. 全局经验(按主题分区表格)
|
||
2. 模块经验(按模块分区表格)
|
||
3. 工作经验日志(按时间倒序,50 条上限)
|
||
|
||
删除所有:
|
||
- 多行代码示例
|
||
- 错误示范列
|
||
- 重复的架构规则(引用 004/project_rules)
|
||
|
||
保留并精简为索引式:
|
||
- "场景 → 技术"映射
|
||
- 工作经验日志
|
||
|
||
- [ ] **Step 3: 验证行数**
|
||
|
||
Run: `Get-Content docs/troubleshooting/known-issues.md | Measure-Object -Line`
|
||
Expected: 约 300 行(±50)
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add docs/troubleshooting/known-issues.md docs/architecture/audit/archive/known-issues_v1.md
|
||
git commit -m "docs(known-issues): slim down from 1317 to ~300 lines, remove code examples, restructure as index"
|
||
```
|
||
|
||
---
|
||
|
||
## 子计划 3: 规范文档修正
|
||
|
||
**目标:** 修正 project_rules.md 和 coding-standards.md 的跨文档冲突和技术错误
|
||
|
||
**依赖:** 子计划 2(文档体系已重设计)
|
||
|
||
### Task 3.1: 修正 project_rules.md
|
||
|
||
**Files:**
|
||
- Modify: `.trae/rules/project_rules.md`
|
||
|
||
- [ ] **Step 1: 补全架构文档清单**
|
||
|
||
将"架构文档清单"表格更新为:
|
||
|
||
```markdown
|
||
### 架构文档清单
|
||
|
||
| 文档 | 用途 |
|
||
|------|------|
|
||
| `docs/architecture/001_project_overview.md` | 项目概述 |
|
||
| `docs/architecture/002_rbac_refactoring.md` | RBAC 重构 |
|
||
| `docs/architecture/003_ui_refactoring_plan.md` | UI 重构计划 |
|
||
| `docs/architecture/004_architecture_impact_map.md` | 架构设计意图唯一源 |
|
||
| `docs/architecture/006_k12_feature_checklist.md` | 标准功能模块清单 |
|
||
| `docs/architecture/007_gap_audit_report.md` | 差距审计报告 |
|
||
| `docs/architecture/008_module_role_mapping.md` | 模块角色映射 |
|
||
| `docs/architecture/roadmap/` | 长远规划(tech-debt/decoupling/pending-features) |
|
||
```
|
||
|
||
(注:005 已废弃归档,002 编号冲突需在子计划 3 单独处理)
|
||
|
||
- [ ] **Step 2: 新增 arch.db 规则**
|
||
|
||
在"架构图优先规则"之后新增:
|
||
|
||
```markdown
|
||
## 架构元数据库规则(arch.db)
|
||
|
||
**AI 工作前必须运行 `npm run arch:scan` 更新 arch.db。**
|
||
|
||
1. **arch.db 是代码结构唯一源**:模块、函数、调用关系、依赖关系查询 arch.db,不手动维护
|
||
2. **AI 工作流程**:
|
||
- 阶段 1 上下文加载:`npm run arch:scan` → `npm run arch:query` 查询目标模块 → 阅读模块 README → 查 known-issues.md 经验
|
||
- 阶段 2 执行工作:修改代码后立即 `npm run arch:scan`
|
||
- 阶段 3 经验沉淀:在 known-issues.md 追加工作经验日志
|
||
3. **查询命令**:
|
||
- `npm run arch:query -- ref <symbol>` 查符号引用(递归)
|
||
- `npm run arch:query -- module <name>` 查模块依赖
|
||
- `npm run arch:query -- tech <tag>` 查技术使用
|
||
- `npm run arch:query -- violations` 查架构违规
|
||
```
|
||
|
||
- [ ] **Step 3: 新增 AI 工作强制流程**
|
||
|
||
在文件末尾新增:
|
||
|
||
```markdown
|
||
## AI 工作强制流程
|
||
|
||
**所有 AI 工作必须遵循此流程,违反即违规。**
|
||
|
||
### 阶段 1: 上下文加载
|
||
1. `npm run arch:scan` 更新 arch.db
|
||
2. `npm run arch:query -- module <目标模块>` 查模块依赖
|
||
3. `npm run arch:query -- ref <目标函数> --forward` 查调用链
|
||
4. 阅读 `src/modules/[模块]/README.md` 读模块工作流程
|
||
5. 查 `known-issues.md` "模块经验: <模块>" 分区读相关经验
|
||
- 审核相关经验(检查代码是否仍匹配)
|
||
- 若文档自上次审核后已变更 → 重新审核并标记
|
||
- 审核通过 → 使用;失败 → 标记失效,不使用
|
||
|
||
### 阶段 2: 执行工作
|
||
1. 按规划执行
|
||
2. 修改代码后立即运行 `npm run arch:scan`
|
||
|
||
### 阶段 3: 经验沉淀(强制,不可跳过)
|
||
1. 在 `known-issues.md` "工作经验日志" 区追加一条记录:
|
||
- 做了什么
|
||
- 学到什么
|
||
- 下次注意事项
|
||
- 审核状态: 待审核
|
||
2. 若发现新的"场景→技术"映射 → 提炼到对应模块分区
|
||
3. 若发现新的架构决策 → 更新 004
|
||
4. 若代码结构变化 → `npm run arch:scan` 确认 arch.db 已更新
|
||
|
||
### 阶段 4: 提交后审核(人工)
|
||
1. 人工审查"待审核"日志条目
|
||
2. 通过 → 标记"已审核 (commit, 审核人)"
|
||
3. 失败 → 标记"审核失败,原因:..."
|
||
4. 定期(如每两周)将成熟日志提炼到分区表格
|
||
```
|
||
|
||
- [ ] **Step 4: 修正令牌位置描述**
|
||
|
||
确保所有令牌位置描述统一为 `src/app/styles/tokens/`。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add .trae/rules/project_rules.md
|
||
git commit -m "docs(rules): add arch.db rules, AI workflow, complete architecture doc list"
|
||
```
|
||
|
||
### Task 3.2: 修正 coding-standards.md
|
||
|
||
**Files:**
|
||
- Modify: `docs/standards/coding-standards.md`
|
||
|
||
- [ ] **Step 1: 修正令牌位置**
|
||
|
||
将第 369 行附近的令牌配置描述从 `globals.css` 改为 `src/app/styles/tokens/`:
|
||
|
||
```markdown
|
||
### 6.3 设计令牌配置
|
||
|
||
本项目在 `src/app/styles/tokens/` 中使用 CSS 变量定义设计令牌(分层架构):
|
||
|
||
- `primitive.css`: 原始色板/字号/间距/阴影(业务代码不直接引用)
|
||
- `semantic-light.css` + `semantic-dark.css`: 语义令牌(业务代码唯一引用入口)
|
||
- `tailwind-theme.css`: `@theme inline` 将 Semantic 令牌暴露为 Tailwind 类
|
||
- `index.css`: 入口文件
|
||
```
|
||
|
||
- [ ] **Step 2: 更新缓存策略描述**
|
||
|
||
将第 403 行附近的缓存策略从 `unstable_cache` 改为 `cacheFn`:
|
||
|
||
```markdown
|
||
- 缓存策略:服务端请求使用 `cacheFn`(封装 React `cache()` + 自定义缓存层),详见 `shared/lib/cache/`
|
||
```
|
||
|
||
- [ ] **Step 3: 更新状态管理章节**
|
||
|
||
在第 7.3 节状态管理中新增 5 层状态模型:
|
||
|
||
```markdown
|
||
### 7.3 状态管理
|
||
|
||
本项目采用 **5 层状态模型**:
|
||
|
||
| 层级 | 场景 | 方案 |
|
||
|------|------|------|
|
||
| L1 URL | 分页、筛选、排序 | `nuqs` |
|
||
| L2 Server | 服务端数据 | TanStack Query |
|
||
| L3 Client Business | 客户端业务状态 | Zustand slice |
|
||
| L4 Global UI | 全局 UI 状态(弹窗、主题) | Zustand ui-store + ModalRoot |
|
||
| L5 Form | 表单状态 | react-hook-form + zodResolver |
|
||
|
||
**规则**:
|
||
- Context 拆分:一个 Context 只负责一类数据
|
||
- 业务数据一律通过路由参数或 TanStack Query 获取,**不存入全局状态**
|
||
- Zustand 的 `persist` 中间件必须处理版本迁移和敏感数据加密
|
||
- 优先使用细粒度 Zustand selectors 而非 `useShallow`
|
||
```
|
||
|
||
- [ ] **Step 4: 更新 ESLint 配置章节**
|
||
|
||
将第 15.1 节 ESLint 配置更新为已实现状态:
|
||
|
||
```markdown
|
||
### 15.1 ESLint
|
||
|
||
**当前配置**(`eslint.config.mjs`)已实现以下规则:
|
||
|
||
- `no-restricted-syntax`: 禁止 `#hex` 颜色字面量
|
||
- `design-tokens/no-hardcoded-fonts`: 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量
|
||
- 白名单:`primitive.css`、`email-channel.ts`、`manifest.ts`
|
||
|
||
(具体配置见 `eslint.config.mjs`)
|
||
```
|
||
|
||
- [ ] **Step 5: 删除 tsconfig "当前差异"过时内容**
|
||
|
||
将第 4.1 节中 tsconfig "当前差异"部分更新——如果已升级则删除差异说明,如果未升级则移到 roadmap:
|
||
|
||
```markdown
|
||
### 4.1 配置(tsconfig.json)
|
||
|
||
当前项目配置已符合规范(`target: ES2022`、`noUncheckedIndexedAccess` 等)。具体配置见 `tsconfig.json`。
|
||
```
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add docs/standards/coding-standards.md
|
||
git commit -m "docs(standards): fix token location, update cache strategy, add 5-layer state model, update ESLint config"
|
||
```
|
||
|
||
### Task 3.3: 修正 002 编号冲突
|
||
|
||
**Files:**
|
||
- Rename: `docs/architecture/002_rbac_refactoring.md` → `docs/architecture/002a_rbac_refactoring.md`
|
||
(或重命名为 `002_rbac_refactoring.md` 和 `002b_role_based_routing.md`)
|
||
|
||
- [ ] **Step 1: 确认两个 002 文件**
|
||
|
||
```bash
|
||
ls docs/architecture/002*
|
||
```
|
||
|
||
Expected: `002_rbac_refactoring.md` 和 `002_role_based_routing.md`
|
||
|
||
- [ ] **Step 2: 重命名解决冲突**
|
||
|
||
```bash
|
||
# RBAC 重构保留 002
|
||
# 角色路由重命名为 002b
|
||
mv docs/architecture/002_role_based_routing.md docs/architecture/002b_role_based_routing.md
|
||
```
|
||
|
||
- [ ] **Step 3: 更新 project_rules.md 中的引用**
|
||
|
||
如果 project_rules.md 中引用了 `002_role_based_routing.md`,更新为新名称。
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add -A docs/architecture/ .trae/rules/project_rules.md
|
||
git commit -m "docs(architecture): resolve 002 numbering conflict by renaming role_based_routing to 002b"
|
||
```
|
||
|
||
---
|
||
|
||
## 子计划 4: 模块 README 创建
|
||
|
||
**目标:** 为约 27 个模块各创建 README.md
|
||
|
||
**依赖:** 子计划 1(arch.db)+ 子计划 2(文档体系)+ 子计划 3(规范)
|
||
|
||
### Task 4.1: 创建标杆模块 README
|
||
|
||
**Files:**
|
||
- Create: `src/modules/textbooks/README.md`
|
||
- Create: `src/modules/grades/README.md`
|
||
|
||
- [ ] **Step 1: 查询 textbooks 模块信息**
|
||
|
||
```bash
|
||
npm run arch:query -- module textbooks
|
||
npm run arch:query -- sql "SELECT path, kind, lines FROM files WHERE module_id = (SELECT id FROM modules WHERE name='textbooks') ORDER BY kind, path"
|
||
```
|
||
|
||
- [ ] **Step 2: 创建 textbooks/README.md**
|
||
|
||
```markdown
|
||
# textbooks 教材模块
|
||
|
||
> 经验查 known-issues.md,代码结构查 arch.db,本文件只记工作流程。
|
||
|
||
## 模块职责
|
||
教材管理、知识图谱、章节结构、教材关联。
|
||
|
||
## 核心工作流程
|
||
1. 新增教材: 通过 data-access 创建教材 → 关联年级/科目
|
||
2. 章节管理: 章节树形结构,支持拖拽排序
|
||
3. 知识图谱: 章节关联知识点,graph-layout 布局算法
|
||
|
||
## 关键约束
|
||
- 教材删除前必须解除所有章节关联
|
||
- 知识图谱节点 ID 必须唯一
|
||
- 依赖: grades(年级)、classes(班级)
|
||
- 被依赖: lesson-preparation、course-plans、exams
|
||
|
||
## 架构决策
|
||
- **为什么用 graph-layout.ts 自定义布局**: 知识图谱需要按章节层级布局,dagre 等通用算法不适合
|
||
- **为什么 data-access 拆分为多文件**: 教材 CRUD + 章节管理 + 图谱查询职责分离,单文件超 800 行
|
||
```
|
||
|
||
- [ ] **Step 3: 创建 grades/README.md**
|
||
|
||
```markdown
|
||
# grades 成绩模块
|
||
|
||
> 经验查 known-issues.md,代码结构查 arch.db,本文件只记工作流程。
|
||
|
||
## 模块职责
|
||
成绩管理、成绩导入导出、成绩分析、成绩申诉、成绩单生成。
|
||
|
||
## 核心工作流程
|
||
1. 录入成绩: 老师通过 actions-import.ts 批量导入 → data-access-drafts.ts 草稿 → actions-lock.ts 锁定
|
||
2. 成绩查询: 学生/家长通过 actions.ts 查询 → scope-check.ts 权限范围校验
|
||
3. 成绩申诉: 学生提交申诉 → actions-appeal.ts 处理 → 老师审批
|
||
4. 成绩分析: stats-service.ts 统计 → actions-analytics.ts 返回分析数据
|
||
|
||
## 关键约束
|
||
- 成绩锁定后不可修改,必须通过申诉流程
|
||
- parent 路由必须同时校验 parentId 和 studentId,防止信息泄露
|
||
- 批量操作必须用 INSERT batch + UPDATE CASE WHEN,禁止循环 SQL
|
||
- 依赖: classes、exams、students
|
||
- 被依赖: dashboard、parent
|
||
|
||
## 架构决策
|
||
- **为什么 actions 拆分为 6 个文件**: actions.ts(基础 CRUD)+ actions-analytics.ts(分析)+ actions-appeal.ts(申诉)+ actions-draft.ts(草稿)+ actions-import.ts(导入)+ actions-lock.ts(锁定),单文件超 1000 行硬上限
|
||
- **为什么有 scope-check.ts**: 成绩数据敏感,必须按角色(admin/teacher/parent/student)过滤可见范围
|
||
- **为什么用 lib/type-guards.ts**: 替代 `as` 断言,类型安全
|
||
```
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add src/modules/textbooks/README.md src/modules/grades/README.md
|
||
git commit -m "docs(modules): create README for benchmark modules (textbooks, grades)"
|
||
```
|
||
|
||
### Task 4.2: 创建核心业务模块 README
|
||
|
||
**Files:**
|
||
- Create: `src/modules/exams/README.md`
|
||
- Create: `src/modules/homework/README.md`
|
||
- Create: `src/modules/questions/README.md`
|
||
|
||
- [ ] **Step 1: 查询模块信息**
|
||
|
||
```bash
|
||
npm run arch:query -- module exams
|
||
npm run arch:query -- module homework
|
||
npm run arch:query -- module questions
|
||
```
|
||
|
||
- [ ] **Step 2: 为每个模块创建 README.md**
|
||
|
||
按标杆模块模板创建,包含:模块职责、核心工作流程、关键约束、架构决策。
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/modules/exams/README.md src/modules/homework/README.md src/modules/questions/README.md
|
||
git commit -m "docs(modules): create README for core business modules (exams, homework, questions)"
|
||
```
|
||
|
||
### Task 4.3: 创建教学管理模块 README
|
||
|
||
**Files:**
|
||
- Create: `src/modules/classes/README.md`
|
||
- Create: `src/modules/school/README.md`
|
||
- Create: `src/modules/scheduling/README.md`
|
||
- Create: `src/modules/attendance/README.md`
|
||
|
||
- [ ] **Step 1: 查询模块信息**
|
||
|
||
```bash
|
||
npm run arch:query -- module classes
|
||
npm run arch:query -- module school
|
||
npm run arch:query -- module scheduling
|
||
npm run arch:query -- module attendance
|
||
```
|
||
|
||
- [ ] **Step 2: 为每个模块创建 README.md**
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/modules/classes/README.md src/modules/school/README.md src/modules/scheduling/README.md src/modules/attendance/README.md
|
||
git commit -m "docs(modules): create README for teaching management modules"
|
||
```
|
||
|
||
### Task 4.4: 创建用户沟通模块 README
|
||
|
||
**Files:**
|
||
- Create: `src/modules/users/README.md`
|
||
- Create: `src/modules/messaging/README.md`
|
||
- Create: `src/modules/notifications/README.md`
|
||
- Create: `src/modules/parent/README.md`
|
||
|
||
- [ ] **Step 1: 查询模块信息**
|
||
|
||
- [ ] **Step 2: 为每个模块创建 README.md**
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/modules/users/README.md src/modules/messaging/README.md src/modules/notifications/README.md src/modules/parent/README.md
|
||
git commit -m "docs(modules): create README for user communication modules"
|
||
```
|
||
|
||
### Task 4.5: 创建扩展功能模块 README
|
||
|
||
**Files:**
|
||
- Create: `src/modules/elective/README.md`
|
||
- Create: `src/modules/proctoring/README.md`
|
||
- Create: `src/modules/diagnostic/README.md`
|
||
- Create: `src/modules/dashboard/README.md`
|
||
|
||
- [ ] **Step 1: 查询模块信息**
|
||
|
||
- [ ] **Step 2: 为每个模块创建 README.md**
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/modules/elective/README.md src/modules/proctoring/README.md src/modules/diagnostic/README.md src/modules/dashboard/README.md
|
||
git commit -m "docs(modules): create README for extension function modules"
|
||
```
|
||
|
||
### Task 4.6: 创建其他模块 README
|
||
|
||
**Files:**
|
||
- Create: `src/modules/announcements/README.md`
|
||
- Create: `src/modules/files/README.md`
|
||
- Create: `src/modules/settings/README.md`
|
||
- Create: `src/modules/auth/README.md`
|
||
- Create: `src/modules/layout/README.md`
|
||
- Create: `src/modules/student/README.md`
|
||
- Create: `src/modules/lesson-preparation/README.md`
|
||
- Create: `src/modules/standards/README.md`
|
||
- Create: `src/modules/course-plans/README.md`
|
||
- Create: `src/modules/audit/README.md`
|
||
- Create: `src/modules/rbac/README.md`
|
||
- Create: `src/modules/onboarding/README.md`
|
||
- Create: `src/modules/ai/README.md`
|
||
- Create: `src/modules/adaptive-practice/README.md`
|
||
- Create: `src/modules/error-book/README.md`
|
||
- Create: `src/modules/search/README.md`
|
||
- Create: `src/modules/leave-requests/README.md`
|
||
- Create: `src/modules/invitation-codes/README.md`
|
||
|
||
- [ ] **Step 1: 批量查询模块信息**
|
||
|
||
```bash
|
||
for mod in announcements files settings auth layout student lesson-preparation standards course-plans audit rbac onboarding ai adaptive-practice error-book search leave-requests invitation-codes; do
|
||
npm run arch:query -- module $mod
|
||
done
|
||
```
|
||
|
||
- [ ] **Step 2: 批量创建 README.md**
|
||
|
||
为每个模块创建 README.md,按标杆模板。
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add src/modules/*/README.md
|
||
git commit -m "docs(modules): create README for remaining modules"
|
||
```
|
||
|
||
### Task 4.7: 子计划 4 验收
|
||
|
||
- [ ] **Step 1: 验证所有模块都有 README**
|
||
|
||
Run: `Get-ChildItem -Path src/modules -Directory | ForEach-Object { if (-not (Test-Path "$_\\README.md")) { Write-Output "Missing: $_" } }`
|
||
Expected: 无输出(所有模块都有 README)
|
||
|
||
- [ ] **Step 2: 验证 README 格式**
|
||
|
||
每个 README 必须包含 4 个章节:模块职责、核心工作流程、关键约束、架构决策。
|
||
|
||
- [ ] **Step 3: Commit 验收标记**
|
||
|
||
```bash
|
||
git commit --allow-empty -m "docs(modules): subplan 4 complete - all modules have README"
|
||
```
|
||
|
||
---
|
||
|
||
## 最终验收
|
||
|
||
### Task 5.1: 全局验收
|
||
|
||
- [ ] **Step 1: 运行所有测试**
|
||
|
||
Run: `npm run test:unit`
|
||
Expected: 所有测试 PASS
|
||
|
||
- [ ] **Step 2: 运行 lint 和 tsc**
|
||
|
||
Run: `npm run lint && npx tsc --noEmit`
|
||
Expected: 零错误
|
||
|
||
- [ ] **Step 3: 端到端验证 arch.db**
|
||
|
||
```bash
|
||
npm run arch:scan
|
||
npm run arch:query -- ref createExam
|
||
npm run arch:query -- module exams
|
||
npm run arch:query -- tech cacheFn
|
||
npm run arch:query -- violations
|
||
```
|
||
|
||
- [ ] **Step 4: 验证文档体系**
|
||
|
||
```bash
|
||
# 检查 004 行数
|
||
Get-Content docs/architecture/004_architecture_impact_map.md | Measure-Object -Line
|
||
# Expected: ~500
|
||
|
||
# 检查 known-issues 行数
|
||
Get-Content docs/troubleshooting/known-issues.md | Measure-Object -Line
|
||
# Expected: ~300
|
||
|
||
# 检查 005 已归档
|
||
Test-Path docs/architecture/audit/archive/005_architecture_data.json
|
||
# Expected: True
|
||
|
||
# 检查 roadmap 目录
|
||
Test-Path docs/architecture/roadmap/tech-debt.md
|
||
# Expected: True
|
||
|
||
# 检查所有模块 README
|
||
Get-ChildItem -Path src/modules -Directory | ForEach-Object { Test-Path "$_\\README.md" }
|
||
# Expected: 全部 True
|
||
```
|
||
|
||
- [ ] **Step 5: 最终 Commit**
|
||
|
||
```bash
|
||
git commit --allow-empty -m "feat(documentation-system): complete redesign with arch.db, slimmed docs, module READMEs, AI workflow"
|
||
```
|
||
|
||
---
|
||
|
||
## Self-Review
|
||
|
||
### Spec 覆盖检查
|
||
|
||
| Spec 章节 | 对应任务 | 状态 |
|
||
|-----------|---------|------|
|
||
| 二、文档体系拓扑 | Task 2.1-2.4 | ✅ |
|
||
| 三、arch.db 架构元数据库 | Task 1.1-1.13 | ✅ |
|
||
| 四、AI 自我演进机制 | Task 3.1(写入 project_rules.md) | ✅ |
|
||
| 五、004 瘦身方案 | Task 2.3 | ✅ |
|
||
| 六、规范文档修正要点 | Task 3.1-3.3 | ✅ |
|
||
| 七、实施阶段划分 | 全部子计划 | ✅ |
|
||
| 八、验收标准 | Task 5.1 | ✅ |
|
||
|
||
### Placeholder 扫描
|
||
|
||
- Task 4.2-4.6 中"为每个模块创建 README.md"——这是合理的批量任务描述,每个模块的 README 内容需根据 arch.db 查询结果生成,无法预先写死。✅ 可接受
|
||
- Task 2.3 中"重写 004 为瘦身后版本"——具体内容需根据 arch.db 查询结果生成。✅ 可接受
|
||
- Task 2.4 中"重写 known-issues.md 为索引式"——具体内容需根据现有内容精简。✅ 可接受
|
||
|
||
### Type 一致性
|
||
|
||
- `querySymbolRefs` 返回 `SymbolRef[]`,在 Task 1.10 定义,在 Task 1.11 CLI 中使用——一致 ✅
|
||
- `queryModuleDeps` 返回 `ModuleDep[]`,在 Task 1.10 定义,在 Task 1.11 CLI 中使用——一致 ✅
|
||
- `runScan` 和 `runQuery` 在 Task 1.11 定义——一致 ✅
|
||
|
||
---
|
||
|
||
## 执行方式
|
||
|
||
Plan complete and saved to `docs/superpowers/plans/2026-07-07-documentation-system-redesign.md`. Two execution options:
|
||
|
||
**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration
|
||
|
||
**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
|
||
|
||
Which approach?
|