docs(architecture): update impact map, data, audit reports, superpowers docs
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

- 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
This commit is contained in:
SpecialX
2026-07-07 16:23:35 +08:00
parent 7387d70289
commit 5d9981fd7d
15 changed files with 10598 additions and 78 deletions

View File

@@ -0,0 +1,134 @@
[
{
"id": "G2-001",
"file": "src/modules/exams/data-access.ts",
"lines": "L1",
"ruleId": "P-01",
"severity": "P0",
"dimension": "pattern",
"title": "文件首行缺少 import \"server-only\" 标记",
"description": "data-access.ts 首行为 `import { db } from \"@/shared/db\"`,未在文件头声明 `import \"server-only\"`。该文件包含直接 DB 访问exams/examQuestions 表的 CRUD若被客户端组件意外引入会将数据库连接与查询逻辑泄露到客户端 bundle造成安全漏洞。同模块的 data-access-error-collection.tsL1与 data-access-cross-module.tsL1均已正确声明唯独主文件遗漏。",
"recommendation": "在文件第一行(所有 import 之前)添加 `import \"server-only\"`。注意:必须位于首行,否则 next.js 的 server-only 边界检测可能不生效。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G2-002",
"file": "src/modules/grades/data-access-appeals.ts",
"lines": "L122-L151",
"ruleId": "F-01",
"severity": "P1",
"dimension": "performance",
"title": "getPendingAppealsForReviewRaw 在 JS 层过滤班级范围而非 SQL WHERE",
"description": "函数 WHERE 子句仅过滤 `gradeAppeals.status = 'pending'`L134未对 classIds 加任何过滤,导致 SQL 返回全库所有 pending 申诉(含 gradeRecord 全字段 innerJoin随后在 L141 用 `rows.filter((r) => classIds.includes(r.gradeRecord.classId))` 在 JS 层过滤。代码注释写明「在 JS 层过滤班级范围(避免复杂 SQL join但 innerJoin gradeRecords 已存在,加 `inArray(gradeRecords.classId, classIds)` 并不复杂。当 pending 申诉总量增长时(全校维度),单次查询会拉取大量无关行,造成内存与网络压力;同时若 JS filter 被误删将引发跨班级数据泄露。",
"recommendation": "在 L132-L137 的 `and()` 内追加 `inArray(gradeRecords.classId, classIds)` 条件classIds 为空时已在 L123 提前返回),删除 L140-L141 的 JS 层 filter直接返回 rows.map(...)。这样既收窄 SQL 结果集,又消除数据泄露风险。",
"effort": "S (≤30 分钟)"
},
{
"id": "G2-003",
"file": "src/modules/adaptive-practice/data-access-analytics.ts",
"lines": "L311-L384",
"ruleId": "F-01",
"severity": "P0",
"dimension": "performance",
"title": "getTeacherClassPracticeOverviewsRaw 在 Promise.all 内对每个班级循环发起 2 条 SQL2N+1 模式)",
"description": "函数对 classIds 数组执行两次 Promise.all 循环:(1) L320-L325 对每个 classId 调用 `getActiveStudentIdsByClassId(classId)`(每班 1 条 SQL共 N 条);(2) L328-L365 对每个班级再发起 1 条 `db.select().from(practiceSessions).where(inArray(studentId, ...))` 聚合查询(共 N 条)。加上 L317 的 getClassNamesByIds1 条),总计 2N+1 条 SQL。当教师所教班级数 N 较大(如年级主任辖 10+ 班级)时,单次请求产生 20+ 条 SQL且 Promise.all 仅并发 IO 不减少 DB 负载。",
"recommendation": "改为批量查询:(1) 一次性获取所有班级的学生 ID 映射(可用单条 SQL `SELECT classId, studentId FROM class_members WHERE classId IN (...) AND status='active'` 后在 JS 层 groupBy(2) 用单条聚合 SQL `SELECT classId, count(...), SUM(...), COUNT(DISTINCT studentId) FROM practiceSessions WHERE studentId IN (全部学生) GROUP BY studentId` 后在 JS 层按班级归并;或直接 JOIN class_members 按 classId 分组。目标:将 2N+1 降至 2-3 条 SQL。",
"effort": "M (≤2 小时)"
},
{
"id": "G2-004",
"file": "src/modules/adaptive-practice/data-access-analytics.ts",
"lines": "L320-L325",
"ruleId": "F-08",
"severity": "P1",
"dimension": "performance",
"title": "跨模块在循环内多次调用 getActiveStudentIdsByClassIdclasses 模块)",
"description": "在 Promise.all 内对每个 classId 单独调用 `@/modules/classes/data-access` 的 `getActiveStudentIdsByClassId`,属于 F-08 跨模块多次调用 getXxxByIds 模式。该函数内部本身可能已 cacheFn 包装,但首次填充缓存时仍会产生 N 条 SQL。应改用批量接口 `getActiveStudentIdsByClassIds(classIds)`(如不存在则需在 classes 模块新增)。",
"recommendation": "在 classes/data-access 新增 `getActiveStudentIdsByClassIds(classIds: string[]): Promise<Map<string, string[]>>` 批量接口(单条 SQL `WHERE classId IN (...)` 后 groupBy本函数改为一次调用获取全量映射。与 G2-003 的修复可合并执行。",
"effort": "M (≤2 小时)"
},
{
"id": "G2-005",
"file": "src/modules/grades/data-access-analytics.ts",
"lines": "L1-L831",
"ruleId": "S-01",
"severity": "P1",
"dimension": "structure",
"title": "文件 831 行超过 800 行警告阈值",
"description": "文件总计 831 行,超过 S-01 规则的 800 行警告线(虽未达 1000 行硬性上限。文件内含多个独立分析维度年级分布getGradeDistribution*、班级统计getClassGradeStats*、学生摘要getStudentGradeSummary*、排名getClassRanking*)等。职责虽同属 grades 分析,但可按分析维度进一步拆分以提升可维护性。",
"recommendation": "按分析维度拆分为 data-access-analytics-grade-distribution.ts / data-access-analytics-class-stats.ts / data-access-analytics-student-summary.ts 等,每个子文件 ≤ 300 行。或暂不拆分但监控增长,一旦逼近 1000 行必须拆分。",
"effort": "L (≤1 天)"
},
{
"id": "G2-006",
"file": "src/modules/homework/data-access.ts",
"lines": "L207-L212",
"ruleId": "A-02",
"severity": "P2",
"dimension": "architecture",
"title": "data-access 内联 computeOverdueCount 业务计算闭包",
"description": "在 getHomeworkAssignmentsRaw 的数据组装段内定义了 `computeOverdueCount` 闭包,包含条件分支 `if (!dueAt || dueAt > now) return 0` 及逾期人数推导逻辑 `Math.max(0, targetCount - submittedCount)`。虽为纯计算非状态机但「逾期」的业务定义dueAt 已过且未提交)属于业务规则,下沉到 data-access 后未来若规则变更(如加宽限期、按作业类型区分)需改 data-access 而非 actions/lib。属 A-02 边界情形。",
"recommendation": "将 computeOverdueCount 提取到 homework/lib/overdue.ts 作为纯函数 `computeOverdueCount(dueAt, targetCount, submittedCount, now)`data-access 仅负责数据获取与组装,业务规则集中到 lib。优先级较低可在重构窗口处理。",
"effort": "S (≤30 分钟)"
},
{
"id": "G2-007",
"file": "src/modules/grades/data-access-drafts.ts",
"lines": "L367-L382",
"ruleId": "P-04",
"severity": "P2",
"dimension": "pattern",
"title": "releaseDraftLock 返回值依赖隐式类型推断的元组解构",
"description": "L367-L378 执行 `db.update(gradeDrafts).set(...).where(...)` 后L381 用 `const [header] = result` 解构L382 返回 `(header?.affectedRows ?? 0) > 0`。drizzle MySQL 的 update 返回类型为 `MySqlRawQueryResult`(即 `[ResultSetHeader, FieldPacket[]]`header 类型由推断得到。代码逻辑正确,但依赖 drizzle 内部类型推断而非显式标注,未来 drizzle 版本变更返回类型时可能静默失效。函数签名已显式标注 `Promise<boolean>`L364属轻微模式偏差。",
"recommendation": "可在解构处补充类型注释 `const [header] = result as [ResultSetHeader, unknown]`(此处 as 属从 unknown/drizzle 内部类型收窄,符合豁免);或保持现状但增加单元测试覆盖锁释放场景。优先级低。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G2-008",
"file": "src/modules/diagnostic/data-access.ts",
"lines": "L1-L553",
"ruleId": "A-02",
"severity": "P2",
"dimension": "architecture",
"title": "诊断掌握度累积计算函数updateMasteryFrom*)含业务规则分支",
"description": "文件含 3 个掌握度累积函数updateMasteryFromSubmission / updateMasteryFromHomeworkSubmission / updateMasteryFromExamScore。这些函数内部包含掌握度合并算法加权平均/最大值取值等业务规则)与 DB 写入混合。掌握度计算属于诊断业务规则,理想分层应将算法提取到 diagnostic/lib/mastery-calculator.tsdata-access 仅负责读写 knowledgePointMastery 表。当前实现可行但职责混合,属 A-02 边界。",
"recommendation": "提取纯函数 `computeMasteryAfterSubmission(current: MasteryState, submission: SubmissionInput): MasteryState` 到 diagnostic/lib/data-access 函数改为:读取当前掌握度 → 调用纯函数计算新值 → 写回 DB。优先级中等可在掌握度算法需调整时一并重构。",
"effort": "M (≤2 小时)"
},
{
"id": "G2-009",
"file": "src/modules/adaptive-practice/data-access.ts",
"lines": "L307",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "sourceMeta as unknown 用于 JSON 序列化字段写入(属豁免范畴)",
"description": "L307 `sourceMeta: sourceMeta as unknown` 将类型化对象转为 unknown 以写入 JSON 列。此处的 as 属于「向 unknown 转换」的合规用法(框架 P-09 豁免:从 unknown 收窄或反向序列化)。仅作记录,非违规。",
"recommendation": "无需修改。若追求严谨,可改用 `JSON.parse(JSON.stringify(sourceMeta))` 显式序列化,但当前写法已合规。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G2-010",
"file": "src/modules/exams/data-access.ts",
"lines": "L316",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "new Map(generated.map((q) => [q.id, q] as const)) 使用 as const 构造 Map属豁免",
"description": "L316 `[q.id, q] as const` 用于向 Map 构造器提供 readonly tuple 类型。as const 属于 TypeScript 类型工具的合规用法P-09 豁免),非类型断言违规。仅作记录。",
"recommendation": "无需修改。",
"effort": "XS (≤15 分钟)"
},
{
"id": "G2-011",
"file": "src/modules/homework/data-access-write.ts",
"lines": "L16,L20",
"ruleId": "P-09",
"severity": "P3",
"dimension": "pattern",
"title": "import 语句中的 as 为模块别名(非类型断言)",
"description": "L16 `getClassTeacherById as getClassTeacherIdFromClass` 与 L20 `getExamWithQuestionsForHomework as getExamWithQuestionsFromExams` 为 ES module import 别名,用于避免跨模块同名函数冲突。非 P-09 规则所约束的类型断言。仅作记录,零违规。",
"recommendation": "无需修改。",
"effort": "XS (≤15 分钟)"
}
]