Files
NextEdu/docs/superpowers/plans/2026-07-07-documentation-system-redesign.md
SpecialX 5d9981fd7d
Some checks failed
CI / scheduled-backup (push) Has been skipped
CI / backup-verify (push) Has been skipped
CI / weekly-dr-drill (push) Failing after 0s
CI / build-deploy (push) Has been cancelled
CI / security-scan (push) Has been cancelled
docs(architecture): update impact map, data, audit reports, superpowers docs
- 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
2026-07-07 16:23:35 +08:00

2683 lines
82 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 文档体系重设计实施计划
> **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: PASS3 个测试全过)
- [ ] **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: PASS3 个测试全过)
- [ ] **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: PASS7 个测试全过)
- [ ] **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: PASS10 个测试全过)
- [ ] **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: PASS12 个测试全过)
- [ ] **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: PASS14 个测试全过)
- [ ] **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: PASS16 个测试全过)
- [ ] **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: PASS6 个测试全过)
- [ ] **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
**依赖:** 子计划 1arch.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?