diff --git a/README.md b/README.md index 84ad8ad..8514f63 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ - 生成验证进程清理计划:按项目路径和端口给出 PowerShell 检查命令,不直接杀进程。 - 清理历史产物:统一治理 `.kt-workspace` 下的测试/验证产物,按目录最近修改时间只保留最近 3 轮,模板目录永久保留;CLI 默认 dry-run,真实清理必须显式传 `--execute`。 - 检查 env 策略和变更风险:区分后端真实 env 与前端客户端 `.env*`,提醒锁文件、核心表格组件、部署链路和 Vue TSX 插槽写法等高风险改动。 -- 全局 CodeReview 只读扫描:汇总全部 KT 子仓库的 Git 状态、敏感文件跟踪、冲突标记、运行时调试输出、疑似凭据字面量、根目录生成产物和当前变更风险;默认只对变更文件做内容扫描,并放过短中文显示标签,避免历史误报污染上下文;任何文件改动后都要跑一遍。 +- 全局 CodeReview 只读扫描:汇总全部 KT 子仓库的 Git 状态、敏感文件跟踪、冲突标记、运行时调试输出、疑似凭据字面量、根目录生成产物、`TASKS.md` 最近记录字段结构和当前变更风险;默认只对变更文件做内容扫描,并放过短中文显示标签,避免历史误报污染上下文;任何文件改动后都要跑一遍。 - 生成或写入 `TASKS.md` 最近记录:默认 `dryRun=true`,确认后再落盘。 - 生成提交前检查清单:校验 KT commit message 约定。 @@ -106,7 +106,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login | 脚本 | 用途 | | --- | --- | | `pnpm run admin-login` | 使用可见 Edge 打开 Admin 登录页,填写账号密码,拖动滑块,保存登录态和截图。默认账号来自初始化数据 `admin/123456`,生产或个人账号用 `KT_ADMIN_USERNAME` / `KT_ADMIN_PASSWORD` 或 CLI 参数覆盖。 | -| `pnpm run global-review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅对变更文件做内容深扫,输出 JSON 复审报告;确认误报时优先升级 `src/tools/review.ts`,不要把误报沉积到上下文。 | +| `pnpm run global-review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅对变更文件做内容深扫,输出 JSON 复审报告,并校验 `TASKS.md` 最近记录只保留范围、关键词、验证字段;确认误报时优先升级 `src/tools/review.ts`,不要把误报沉积到上下文。 | | `pnpm run obsidian-context` | 输出 Obsidian 索引上下文;可传 `--module Admin`、`--query QQBot`、`--max-documents 10`。 | | `pnpm run obsidian-validate` | 校验 KT Obsidian vault 结构和链接;默认 warning 不让脚本失败,需要严格模式时传 `--fail-on-warnings`。 | | `pnpm run obsidian-sync` | 审计 Obsidian 工作流入口是否连通,并联动执行 validate。 | diff --git a/src/selfTest.ts b/src/selfTest.ts index a32e8c6..2f40819 100644 --- a/src/selfTest.ts +++ b/src/selfTest.ts @@ -12,7 +12,7 @@ import { cleanupHistoryArtifacts, parseCliCleanupArgs } from './tools/cleanup.js import { buildWorkflowLoopAudit } from './tools/loop.js'; import { inspectProject } from './tools/inspect.js'; import { prepareTask, readWorkflowContext } from './tools/task.js'; -import { buildGlobalCodeReview, isBenignCredentialReviewValue } from './tools/review.js'; +import { buildGlobalCodeReview, findTaskRecordGovernanceFindings, isBenignCredentialReviewValue } from './tools/review.js'; import { buildBusinessTestPlan } from './tools/testing.js'; import { buildVerificationPlan } from './tools/verification.js'; import { buildWorkstreamCloseout } from './tools/closeout.js'; @@ -26,6 +26,19 @@ export async function runSelfTest(): Promise { throw new Error('review credential classifier self-check failed'); } + const taskRecordFindings = findTaskRecordGovernanceFindings([ + '## 最近记录', + '### 2026-06-10:长记录污染', + '- 范围:root', + '- 关键词:TASKS 治理', + '- 页面测试用例:配置类任务,无页面测试。', + '- 验证:global-review', + '## 历史索引', + ]); + if (!taskRecordFindings.some((item) => item.category === 'tasks-record-extra-field')) { + throw new Error('TASKS record governance self-check failed'); + } + const reviewCliParser = { dashedAll: parseGlobalReviewCliArgs(['node', 'server', '--global-review', '--content-scan-all']).contentScanMode, keyValueAll: parseGlobalReviewCliArgs(['node', 'server', '--global-review', '--contentScanMode=all']).contentScanMode, diff --git a/src/tools/review.ts b/src/tools/review.ts index d1f0acc..f551601 100644 --- a/src/tools/review.ts +++ b/src/tools/review.ts @@ -54,6 +54,8 @@ const frontendEnvProjectAliases = new Set([ const credentialAssignmentPattern = /\b(password|passwd|secret|token|webuiToken|accessToken|privateKey)\b\s*[:=]\s*(["'])([^"']+)\2/i; +const taskRecordAllowedFields = new Set(['范围', '关键词', '验证']); +const taskRecordRequiredFields = ['范围', '关键词', '验证'] as const; interface ReviewFinding { category: string; @@ -78,6 +80,79 @@ interface ReviewProjectResult { trackedSensitiveFiles: string[]; } +export function findTaskRecordGovernanceFindings(lines: string[]): ReviewFinding[] { + const findings: ReviewFinding[] = []; + const recentStart = lines.findIndex((line) => line.trim() === '## 最近记录'); + if (recentStart < 0) return findings; + + const nextSectionOffset = lines + .slice(recentStart + 1) + .findIndex((line) => /^##\s+/.test(line.trim())); + const recentEnd = nextSectionOffset < 0 ? lines.length : recentStart + 1 + nextSectionOffset; + let currentRecord: + | { + fields: Set; + heading: string; + line: number; + } + | null = null; + + const pushMissingFieldFindings = () => { + if (!currentRecord) return; + + for (const field of taskRecordRequiredFields) { + if (currentRecord.fields.has(field)) continue; + + findings.push({ + category: 'tasks-record-missing-field', + file: 'TASKS.md', + line: currentRecord.line, + level: 'P2', + message: `TASKS 最近记录缺少「${field}」字段。`, + project: 'root', + suggestion: '最近记录只保留范围、关键词、验证三项,详细过程迁入 docs 或历史索引。', + }); + } + }; + + for (let index = recentStart + 1; index < recentEnd; index += 1) { + const line = lines[index].trim(); + if (line.startsWith('### ')) { + pushMissingFieldFindings(); + currentRecord = { + fields: new Set(), + heading: line.replace(/^###\s+/, ''), + line: index + 1, + }; + continue; + } + + if (!currentRecord) continue; + + const fieldMatch = line.match(/^-\s*([^::]+)[::]/); + if (!fieldMatch) continue; + + const field = fieldMatch[1].trim(); + if (taskRecordAllowedFields.has(field)) { + currentRecord.fields.add(field); + continue; + } + + findings.push({ + category: 'tasks-record-extra-field', + file: 'TASKS.md', + line: index + 1, + level: 'P2', + message: `TASKS 最近记录「${currentRecord.heading}」包含额外字段「${field}」。`, + project: 'root', + suggestion: '把额外说明迁入 docs,TASKS 最近记录只保留范围、关键词、验证。', + }); + } + + pushMissingFieldFindings(); + return findings; +} + async function listGitTrackedFiles(projectPath: string): Promise { const tracked = await tryExecFile('git', ['ls-files'], projectPath); if (!tracked.ok) return []; @@ -385,6 +460,10 @@ export async function buildGlobalCodeReview( : findWorkspaceFilesByName(new Set(['deploy.js', 'deploy.json']), 50); const rootGeneratedArtifacts = input.includeRootScan === false ? [] : findRootGeneratedArtifacts(); + const taskRecordFindings = + input.includeRootScan === false + ? [] + : findTaskRecordGovernanceFindings(readProjectFileLines(workspaceRoot, 'TASKS.md')); const findings = projectResults.flatMap((item) => [ ...item.findings, ...item.debugFindings, @@ -406,7 +485,12 @@ export async function buildGlobalCodeReview( project: 'root', suggestion: '统一迁入 .kt-workspace 对应子目录,并更新生成脚本或测试流程。', })); - const allFindings = [...findings, ...rootFindings, ...rootGeneratedFindings]; + const allFindings = [ + ...findings, + ...rootFindings, + ...rootGeneratedFindings, + ...taskRecordFindings, + ]; return { findings: allFindings,