# 文档体系重设计实施计划 > **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(); 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 [--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 [--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 "); 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 \"\""); 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 [args]"); console.log(""); console.log("Commands:"); console.log(" scan Scan codebase and update arch.db"); console.log(" query ref [--forward] Query symbol references"); console.log(" query module [--reverse] Query module dependencies"); console.log(" query tech Query tech tag usage"); console.log(" query violations Detect architecture violations"); console.log(" query 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 # 路线图索引 > 本目录存放项目长远规划,与架构事实(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 # 技术债清单 > 从 004 第三部分"已知架构问题和技术债"迁入。 ## 待解决项 (从 004 迁入后填充) ## 已解决项 (已解决项删除,git 历史已记录) ``` - [ ] **Step 3: 创建 decoupling.md** ```markdown # 解耦路线图 > 从 docs/architecture/audit/01_decoupling_roadmap.md 迁入。 ## 解耦目标 (迁入后填充) ``` - [ ] **Step 4: 创建 pending-features.md** ```markdown # 待开发功能 > 从 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 # 历史审查报告归档 > 本目录为只读归档,不再更新。 > 有价值的内容已提取到模块 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 ` 查符号引用(递归) - `npm run arch:query -- module ` 查模块依赖 - `npm run arch:query -- tech ` 查技术使用 - `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?