From 7e79b386006b3072a30474b4bed267c36534b7a6 Mon Sep 17 00:00:00 2001 From: sunlei Date: Fri, 12 Jun 2026 16:32:46 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=9B=BA=E5=8C=96=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96=E5=B7=A5=E4=BD=9C=E6=B5=81=E8=A7=84?= =?UTF-8?q?=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 112 +++++------ src/selfTest.ts | 439 +++++++++++++++++++++++++++---------------- src/tools/envRisk.ts | 279 ++++++++++++++++++--------- src/tools/review.ts | 384 +++++++++++++++++++++---------------- 4 files changed, 746 insertions(+), 468 deletions(-) diff --git a/README.md b/README.md index e7c1a7f..43e9fe5 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ - 生成专项组件工作流:KtTable、BlogArgon、AdminAuth、QQBot、FF14Plugin、NapCatLogin、SystemLog、Knife4jSwagger、FnosK8s 的防踩坑清单和验证点。 - 生成验证进程清理计划:按项目路径和端口给出 PowerShell 检查命令,不直接杀进程。 - 清理历史产物:统一治理 `.kt-workspace` 下的测试/验证产物,按目录最近修改时间只保留最近 3 轮,模板目录永久保留;CLI 默认 dry-run,真实清理必须显式传 `--execute`。 -- 检查 env 策略和变更风险:区分后端真实 env 与前端客户端 `.env*`,提醒锁文件、核心表格组件、部署链路和 Vue TSX 插槽写法等高风险改动。 -- 全局 CodeReview 只读扫描:汇总全部 KT 子仓库的 Git 状态、敏感文件跟踪、冲突标记、运行时调试输出、疑似凭据字面量、NapCat `latest` 镜像漂移风险、QQBot 重型服务单测导入风险、根目录生成产物、`TASKS.md` 最近记录字段结构和当前变更风险;默认只对变更文件做内容扫描,并放过短中文显示标签,避免历史误报污染上下文;任何文件改动后都要跑一遍。 +- 检查 env 策略和变更风险:区分后端真实 env 与前端客户端 `.env*`,提醒锁文件、核心表格组件、API 时间序列化 KtDateTime 列/DTO 装饰器入口、部署链路和 Vue TSX 插槽写法等高风险改动。 +- 全局 CodeReview 只读扫描:汇总全部 KT 子仓库的 Git 状态、敏感文件跟踪、冲突标记、运行时调试输出、疑似凭据字面量、NapCat `latest` 镜像漂移风险、QQBot 重型服务单测导入风险、根目录生成产物、`TASKS.md` 最近记录字段结构和当前变更风险;默认只对变更文件做内容扫描,并放过短中文显示标签和明显测试占位 token,避免历史误报污染上下文;任何文件改动后都要跑一遍。 - 生成或写入 `TASKS.md` 最近记录:默认 `dryRun=true`,确认后再落盘。 - 生成提交前检查清单:校验 KT commit message 约定。 @@ -69,62 +69,62 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login ## 工具列表 -| 工具 | 用途 | -| --- | --- | -| `kt_read_context` | 读取 KT 根目录紧凑上下文和最近任务记录,支持显式读取完整文档 | -| `kt_inspect_project` | 检查子项目仓库、包管理器、Node、env 和 Git 状态 | -| `kt_inspect_all_projects` | 一次性检查所有 KT 项目,适合多仓库联动任务 | -| `kt_guardrails` | 按任务类型生成开工、改动、禁止项和验证约束 | -| `kt_prepare_task` | 生成完整 work packet,降低开工偏差 | -| `kt_suggest_verification` | 生成轻量验证命令和注意事项 | -| `kt_create_page_test_case` | 生成页面级可视化测试用例 | -| `kt_api_test_plan` | 生成接口真实调用测试计划 | -| `kt_cleanup_history` | 清理 `.kt-workspace` 历史测试/验证产物,默认预览,执行时只保留最近 3 轮 | -| `kt_cleanup_process_plan` | 生成验证进程清理检查命令 | -| `kt_env_policy` | 检查 env 文件现状和提交策略 | -| `kt_risk_scan` | 扫描当前或传入变更文件的偏差风险 | -| `kt_global_code_review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅深扫变更文件 | -| `kt_finish_task` | 生成任务收尾包,可选执行验证和历史清理 | -| `kt_workstream_closeout` | 生成大方向完结闭环审计,检查测试证据、文档同步、历史清理、全局 review、问题记录、稳定解法和 ktWorkflow 升级 | -| `kt_workflow_loop_audit` | 审计上下文锚定、测试证据、文档同步、历史清理最终 `deleted=0`、问题固化、ktWorkflow 升级和全局 review | -| `kt_change_doc_sync` | 根据改动路径生成 README/API/AGENTS/docs/Obsidian/skill/ktWorkflow 文档同步清单 | -| `kt_commit_plan` | 生成多仓库提交计划、建议 commit message 和检查项 | -| `kt_push_plan` | 生成多仓库推送计划和远程异常提醒 | -| `kt_business_test_plan` | 生成固化业务链路测试计划,包含 QQBot SSE、FFLogs 和系统日志 | -| `kt_blocker_resolution` | 生成或写入卡点固化记录,提醒停止原样重试 | -| `kt_remote_health_check` | 生成或执行远程只读健康检查命令 | -| `kt_db_sync_plan` | 生成数据库同步安全向导 | -| `kt_component_workflow` | 输出专项组件/链路防踩坑工作流,包含 FnosK8s | -| `kt_obsidian_context` | 读取 Obsidian 索引上下文,支持按 `module` / `query` 返回模块页、文档矩阵、Canvas 摘要、代码锚点和相关规则文档 | -| `kt_obsidian_validate` | 只读校验 Obsidian JSON、Canvas、Base、Wiki 链接、Markdown 相对链接和旧引用 | -| `kt_obsidian_sync` | 审计 Obsidian 工作流入口、书签、核心插件、忽略目录和校验结果 | -| `kt_append_task_record` | 预览或写入 `TASKS.md` 最近记录 | -| `kt_commit_checklist` | 生成提交前检查清单并校验 commit message | +| 工具 | 用途 | +| -------------------------- | ------------------------------------------------------------------------------------------------------------- | +| `kt_read_context` | 读取 KT 根目录紧凑上下文和最近任务记录,支持显式读取完整文档 | +| `kt_inspect_project` | 检查子项目仓库、包管理器、Node、env 和 Git 状态 | +| `kt_inspect_all_projects` | 一次性检查所有 KT 项目,适合多仓库联动任务 | +| `kt_guardrails` | 按任务类型生成开工、改动、禁止项和验证约束 | +| `kt_prepare_task` | 生成完整 work packet,降低开工偏差 | +| `kt_suggest_verification` | 生成轻量验证命令和注意事项 | +| `kt_create_page_test_case` | 生成页面级可视化测试用例 | +| `kt_api_test_plan` | 生成接口真实调用测试计划 | +| `kt_cleanup_history` | 清理 `.kt-workspace` 历史测试/验证产物,默认预览,执行时只保留最近 3 轮 | +| `kt_cleanup_process_plan` | 生成验证进程清理检查命令 | +| `kt_env_policy` | 检查 env 文件现状和提交策略 | +| `kt_risk_scan` | 扫描当前或传入变更文件的偏差风险 | +| `kt_global_code_review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅深扫变更文件 | +| `kt_finish_task` | 生成任务收尾包,可选执行验证和历史清理 | +| `kt_workstream_closeout` | 生成大方向完结闭环审计,检查测试证据、文档同步、历史清理、全局 review、问题记录、稳定解法和 ktWorkflow 升级 | +| `kt_workflow_loop_audit` | 审计上下文锚定、测试证据、文档同步、历史清理最终 `deleted=0`、问题固化、ktWorkflow 升级和全局 review | +| `kt_change_doc_sync` | 根据改动路径生成 README/API/AGENTS/docs/Obsidian/skill/ktWorkflow 文档同步清单 | +| `kt_commit_plan` | 生成多仓库提交计划、建议 commit message 和检查项 | +| `kt_push_plan` | 生成多仓库推送计划和远程异常提醒 | +| `kt_business_test_plan` | 生成固化业务链路测试计划,包含 QQBot SSE、FFLogs 和系统日志 | +| `kt_blocker_resolution` | 生成或写入卡点固化记录,提醒停止原样重试 | +| `kt_remote_health_check` | 生成或执行远程只读健康检查命令 | +| `kt_db_sync_plan` | 生成数据库同步安全向导 | +| `kt_component_workflow` | 输出专项组件/链路防踩坑工作流,包含 FnosK8s | +| `kt_obsidian_context` | 读取 Obsidian 索引上下文,支持按 `module` / `query` 返回模块页、文档矩阵、Canvas 摘要、代码锚点和相关规则文档 | +| `kt_obsidian_validate` | 只读校验 Obsidian JSON、Canvas、Base、Wiki 链接、Markdown 相对链接和旧引用 | +| `kt_obsidian_sync` | 审计 Obsidian 工作流入口、书签、核心插件、忽略目录和校验结果 | +| `kt_append_task_record` | 预览或写入 `TASKS.md` 最近记录 | +| `kt_commit_checklist` | 生成提交前检查清单并校验 commit message | ## 可复用脚本 -| 脚本 | 用途 | -| --- | --- | -| `pnpm run admin-login` | 使用可见 Edge 打开 Admin 登录页,填写账号密码,拖动滑块,保存登录态和截图。默认账号来自初始化数据 `admin/123456`,生产或个人账号用 `KT_ADMIN_USERNAME` / `KT_ADMIN_PASSWORD` 或 CLI 参数覆盖。 | -| `pnpm run global-review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅对变更文件做内容深扫,输出 JSON 复审报告,并校验 `TASKS.md` 最近记录只保留范围、关键词、验证字段、NapCat `latest` 漂移和 QQBot 重型服务单测导入风险;确认误报时优先升级 `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。 | -| `pnpm run cleanup-history` | 预览 `.kt-workspace` 测试/验证历史产物清理;真实清理用 `pnpm run cleanup-history -- --execute`,只保留最近 3 轮并保留模板目录。 | -| `pnpm run workstream-closeout` | 生成大方向完结闭环审计;识别测试证据、文档同步、历史清理、全局 review、问题记录、稳定解法和 ktWorkflow 升级缺口。 | +| 脚本 | 用途 | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `pnpm run admin-login` | 使用可见 Edge 打开 Admin 登录页,填写账号密码,拖动滑块,保存登录态和截图。默认账号来自初始化数据 `admin/123456`,生产或个人账号用 `KT_ADMIN_USERNAME` / `KT_ADMIN_PASSWORD` 或 CLI 参数覆盖。 | +| `pnpm run global-review` | 对 KT 全部子仓库做只读全局 CodeReview 扫描,默认仅对变更文件做内容深扫,输出 JSON 复审报告,并校验 `TASKS.md` 最近记录只保留范围、关键词、验证字段、NapCat `latest` 漂移和 QQBot 重型服务单测导入风险;明显测试占位 token 不报凭据风险,确认误报时优先升级 `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。 | +| `pnpm run cleanup-history` | 预览 `.kt-workspace` 测试/验证历史产物清理;真实清理用 `pnpm run cleanup-history -- --execute`,只保留最近 3 轮并保留模板目录。 | +| `pnpm run workstream-closeout` | 生成大方向完结闭环审计;识别测试证据、文档同步、历史清理、全局 review、问题记录、稳定解法和 ktWorkflow 升级缺口。 | ## 项目别名 -| 别名 | 路径 | -| --- | --- | -| `root` | `D:/MyFiles/KT` | -| `mcp` | `mcp/ktWorkflow` | -| `api` | `Node/kt-template-online-api` | -| `admin` | `Vue/kt-template-admin` | -| `blog` | `Vue/kt-blog-web` | -| `knife4j` | `Plugins/knife4j-swagger-vue3` | -| `fnosK8s` | `Plugins/fnos-k8s-dashboard-fpk` | -| `web` | `Vue/kt-template-online-web` | +| 别名 | 路径 | +| ------------ | ----------------------------------- | +| `root` | `D:/MyFiles/KT` | +| `mcp` | `mcp/ktWorkflow` | +| `api` | `Node/kt-template-online-api` | +| `admin` | `Vue/kt-template-admin` | +| `blog` | `Vue/kt-blog-web` | +| `knife4j` | `Plugins/knife4j-swagger-vue3` | +| `fnosK8s` | `Plugins/fnos-k8s-dashboard-fpk` | +| `web` | `Vue/kt-template-online-web` | | `playground` | `Vue/kt-template-online-playground` | ## 开发说明 @@ -167,7 +167,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login ## 来源与许可证 -| 一级来源 | 使用方式 | License | -| --- | --- | --- | -| [multica-ai/andrej-karpathy-skills](https://github.com/multica-ai/andrej-karpathy-skills) | `skills/karpathy-guidelines` 和 ktWorkflow 防偏差闭环的行为准则来源 | MIT | -| [kepano/obsidian-skills](https://github.com/kepano/obsidian-skills) | Obsidian Markdown、Canvas、Bases 和 CLI 工作流的归纳整理能力来源 | MIT | +| 一级来源 | 使用方式 | License | +| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ------- | +| [multica-ai/andrej-karpathy-skills](https://github.com/multica-ai/andrej-karpathy-skills) | `skills/karpathy-guidelines` 和 ktWorkflow 防偏差闭环的行为准则来源 | MIT | +| [kepano/obsidian-skills](https://github.com/kepano/obsidian-skills) | Obsidian Markdown、Canvas、Bases 和 CLI 工作流的归纳整理能力来源 | MIT | diff --git a/src/selfTest.ts b/src/selfTest.ts index 1cd38b7..d04b31e 100644 --- a/src/selfTest.ts +++ b/src/selfTest.ts @@ -1,270 +1,368 @@ -import { mkdirSync, writeFileSync } from 'node:fs'; -import path from 'node:path'; +import { mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; -import { parseGlobalReviewCliArgs } from './core/cli.js'; -import { registeredToolNames } from './core/constants.js'; -import { buildBlockerResolution } from './tools/blocker.js'; -import { buildChangeDocSync } from './tools/docSync.js'; -import { resolveInsideRoot } from './core/workspace.js'; -import { buildGuardrails } from './tools/guardrails.js'; -import { readObsidianContext, syncObsidianWorkflow, validateObsidianVault } from './tools/obsidian.js'; -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, findNapcatImageGovernanceFindings, findQqbotCommandServiceTestImportFindings, findTaskRecordGovernanceFindings, isBenignCredentialReviewValue } from './tools/review.js'; -import { buildBusinessTestPlan } from './tools/testing.js'; -import { buildVerificationPlan } from './tools/verification.js'; -import { buildWorkstreamCloseout } from './tools/closeout.js'; -import { buildCommitPlan, buildComponentWorkflow, buildDbSyncPlan, buildFinishTask, buildPushPlan, buildRemoteHealthCheck } from './tools/workflow.js'; +import { parseGlobalReviewCliArgs } from "./core/cli.js"; +import { registeredToolNames } from "./core/constants.js"; +import { buildBlockerResolution } from "./tools/blocker.js"; +import { buildChangeDocSync } from "./tools/docSync.js"; +import { resolveInsideRoot } from "./core/workspace.js"; +import { hasEntityLoadTimeSerializationMutation } from "./tools/envRisk.js"; +import { buildGuardrails } from "./tools/guardrails.js"; +import { + readObsidianContext, + syncObsidianWorkflow, + validateObsidianVault, +} from "./tools/obsidian.js"; +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, + findNapcatImageGovernanceFindings, + findQqbotCommandServiceTestImportFindings, + findTaskRecordGovernanceFindings, + isBenignCredentialReviewValue, +} from "./tools/review.js"; +import { buildBusinessTestPlan } from "./tools/testing.js"; +import { buildVerificationPlan } from "./tools/verification.js"; +import { buildWorkstreamCloseout } from "./tools/closeout.js"; +import { + buildCommitPlan, + buildComponentWorkflow, + buildDbSyncPlan, + buildFinishTask, + buildPushPlan, + buildRemoteHealthCheck, +} from "./tools/workflow.js"; export async function runSelfTest(): Promise { const reviewClassifier = { - localizedSecretLabelAllowed: isBenignCredentialReviewValue('密钥'), - realTokenRejected: !isBenignCredentialReviewValue('eqc2Sbp8ufP2AcS6I2YReLqxnu3XJKC6D7C6PUcx'), + localizedSecretLabelAllowed: isBenignCredentialReviewValue("密钥"), + realTokenRejected: !isBenignCredentialReviewValue( + "eqc2Sbp8ufP2AcS6I2YReLqxnu3XJKC6D7C6PUcx", + ), + testTokenAllowed: isBenignCredentialReviewValue("token-test"), }; - if (!reviewClassifier.localizedSecretLabelAllowed || !reviewClassifier.realTokenRejected) { - throw new Error('review credential classifier self-check failed'); + if ( + !reviewClassifier.localizedSecretLabelAllowed || + !reviewClassifier.realTokenRejected || + !reviewClassifier.testTokenAllowed + ) { + throw new Error("review credential classifier self-check failed"); } const taskRecordFindings = findTaskRecordGovernanceFindings([ - '## 最近记录', - '### 2026-06-10:长记录污染', - '- 范围:root', - '- 关键词:TASKS 治理', - '- 页面测试用例:配置类任务,无页面测试。', - '- 验证:global-review', - '## 历史索引', + "## 最近记录", + "### 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'); + if ( + !taskRecordFindings.some( + (item) => item.category === "tasks-record-extra-field", + ) + ) { + throw new Error("TASKS record governance self-check failed"); } const napcatImageFindings = findNapcatImageGovernanceFindings( - ['QQBOT_NAPCAT_IMAGE=mlikiowa/napcat-docker:latest'], + ["QQBOT_NAPCAT_IMAGE=mlikiowa/napcat-docker:latest"], { - file: '.env.example', - project: 'Node/kt-template-online-api', - }, - ); - if (!napcatImageFindings.some((item) => item.category === 'napcat-latest-image')) { - throw new Error('NapCat image governance self-check failed'); - } - - const qqbotCommandServiceImportFindings = findQqbotCommandServiceTestImportFindings( - ["import { QqbotCommandService } from '@/qqbot/command/qqbot-command.service';"], - { - file: 'test/qqbot/command/qqbot-command-cooldown.service.spec.ts', - project: 'Node/kt-template-online-api', + file: ".env.example", + project: "Node/kt-template-online-api", }, ); + if ( + !napcatImageFindings.some((item) => item.category === "napcat-latest-image") + ) { + throw new Error("NapCat image governance self-check failed"); + } + + const qqbotCommandServiceImportFindings = + findQqbotCommandServiceTestImportFindings( + [ + "import { QqbotCommandService } from '@/qqbot/command/qqbot-command.service';", + ], + { + file: "test/qqbot/command/qqbot-command-cooldown.service.spec.ts", + project: "Node/kt-template-online-api", + }, + ); if ( !qqbotCommandServiceImportFindings.some( - (item) => item.category === 'qqbot-command-service-heavy-test-import', + (item) => item.category === "qqbot-command-service-heavy-test-import", ) ) { - throw new Error('QQBot command service test import governance self-check failed'); + throw new Error( + "QQBot command service test import 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, - splitAll: parseGlobalReviewCliArgs(['node', 'server', '--global-review', '--content-scan-mode', 'all']).contentScanMode, + dashedAll: parseGlobalReviewCliArgs([ + "node", + "server", + "--global-review", + "--content-scan-all", + ]).contentScanMode, + keyValueAll: parseGlobalReviewCliArgs([ + "node", + "server", + "--global-review", + "--contentScanMode=all", + ]).contentScanMode, + splitAll: parseGlobalReviewCliArgs([ + "node", + "server", + "--global-review", + "--content-scan-mode", + "all", + ]).contentScanMode, }; if ( - reviewCliParser.dashedAll !== 'all' || - reviewCliParser.keyValueAll !== 'all' || - reviewCliParser.splitAll !== 'all' + reviewCliParser.dashedAll !== "all" || + reviewCliParser.keyValueAll !== "all" || + reviewCliParser.splitAll !== "all" ) { - throw new Error('global-review CLI content scan mode parser self-check failed'); + throw new Error( + "global-review CLI content scan mode parser self-check failed", + ); } const refactorGuardrails = buildGuardrails({ - project: 'mcp', - taskType: 'refactor', - userRequest: '重构 KT 工作流护栏', + project: "mcp", + taskType: "refactor", + userRequest: "重构 KT 工作流护栏", }); const refactorGuardrailOk = - refactorGuardrails.guardrails.beforeEdit.some((item) => item.includes('重构前写清楚')) && - refactorGuardrails.guardrails.verification.notes.some((item) => item.includes('重构验证必须覆盖原行为')); + refactorGuardrails.guardrails.beforeEdit.some((item) => + item.includes("重构前写清楚"), + ) && + refactorGuardrails.guardrails.verification.notes.some((item) => + item.includes("重构验证必须覆盖原行为"), + ); if (!refactorGuardrailOk) { - throw new Error('refactor guardrail self-check failed'); + throw new Error("refactor guardrail self-check failed"); } const apiVerification = buildVerificationPlan({ - changeType: 'api', - project: 'api', + changeType: "api", + project: "api", }); - const apiVerificationCommands = apiVerification.commands.join('\n'); + const apiVerificationCommands = apiVerification.commands.join("\n"); if ( - !apiVerificationCommands.includes('pnpm exec jest --runInBand') || - apiVerificationCommands.includes('passWithNoTests') + !apiVerificationCommands.includes("pnpm exec jest --runInBand") || + apiVerificationCommands.includes("passWithNoTests") ) { - throw new Error('API Jest verification command self-check failed'); + throw new Error("API Jest verification command self-check failed"); + } + + const timeSerializationRisk = { + bad: hasEntityLoadTimeSerializationMutation( + "import { AfterLoad } from 'typeorm';\nexport const FormatDateTime = () => {}; formatDateTimeFields(this);", + ), + good: hasEntityLoadTimeSerializationMutation( + "import { Column } from 'typeorm';\nexport class KtDateTime extends Date {}\nexport const KtDateTimeColumn = () => Column({ transformer: { from: (value) => new KtDateTime(value), to: (value) => value } });", + ), + }; + if (!timeSerializationRisk.bad || timeSerializationRisk.good) { + throw new Error("time serialization mutation risk self-check failed"); } const databaseVerification = buildVerificationPlan({ - changeType: 'database', - project: 'api', + changeType: "database", + project: "api", }); - const databaseVerificationNotes = databaseVerification.notes.join('\n'); + const databaseVerificationNotes = databaseVerification.notes.join("\n"); if ( - !databaseVerificationNotes.includes('备份目标表') || - !databaseVerificationNotes.includes('SHOW COLUMNS') || - !databaseVerificationNotes.includes('真实接口 smoke') + !databaseVerificationNotes.includes("备份目标表") || + !databaseVerificationNotes.includes("SHOW COLUMNS") || + !databaseVerificationNotes.includes("真实接口 smoke") ) { - throw new Error('database migration verification self-check failed'); + throw new Error("database migration verification self-check failed"); } const closeoutNeedsUpgrade = buildWorkstreamCloseout({ - cleanupEvidence: ['cleanup-history dry-run deleted=0。'], - docSyncEvidence: ['kt_change_doc_sync requiredDocs 已处理。'], - reviewEvidence: ['global-review findings=0。'], - problemRecords: ['Jenkins 状态需要结合日志和 K8s 状态确认。'], - reusablePatterns: ['deploy-observation'], - stableSolutions: ['收尾时固定观测 build、commit、镜像、Deployment、Pod 和 smoke。'], - title: '发布闭环', - verificationEvidence: ['Jenkins #132 SUCCESS,K8s Pod Running。'], + cleanupEvidence: ["cleanup-history dry-run deleted=0。"], + docSyncEvidence: ["kt_change_doc_sync requiredDocs 已处理。"], + reviewEvidence: ["global-review findings=0。"], + problemRecords: ["Jenkins 状态需要结合日志和 K8s 状态确认。"], + reusablePatterns: ["deploy-observation"], + stableSolutions: [ + "收尾时固定观测 build、commit、镜像、Deployment、Pod 和 smoke。", + ], + title: "发布闭环", + verificationEvidence: ["Jenkins #132 SUCCESS,K8s Pod Running。"], }); if ((closeoutNeedsUpgrade.canReportComplete as boolean) !== false) { - throw new Error('workstream closeout upgrade gate self-check failed'); + throw new Error("workstream closeout upgrade gate self-check failed"); } const closeoutComplete = buildWorkstreamCloseout({ - cleanupEvidence: ['cleanup-history dry-run deleted=0。'], - docSyncEvidence: ['无需文档更新,已记录原因。'], + cleanupEvidence: ["cleanup-history dry-run deleted=0。"], + docSyncEvidence: ["无需文档更新,已记录原因。"], ktWorkflowUpdated: true, - problemRecords: ['无新卡点。'], - reviewEvidence: ['global-review findings=0。'], - reusablePatterns: ['none'], - stableSolutions: ['无新增稳定解法。'], - title: '文档收尾', - verificationEvidence: ['typecheck/self-test/global-review 通过。'], + problemRecords: ["无新卡点。"], + reviewEvidence: ["global-review findings=0。"], + reusablePatterns: ["none"], + stableSolutions: ["无新增稳定解法。"], + title: "文档收尾", + verificationEvidence: ["typecheck/self-test/global-review 通过。"], }); if ((closeoutComplete.canReportComplete as boolean) !== true) { - throw new Error('workstream closeout complete self-check failed'); + throw new Error("workstream closeout complete self-check failed"); } const closeoutInvalidCleanup = buildWorkstreamCloseout({ - cleanupEvidence: ['cleanup-history dry-run deleted=3。'], - docSyncEvidence: ['kt_change_doc_sync requiredDocs 已处理。'], - problemRecords: ['无新卡点。'], - reviewEvidence: ['global-review findings=0。'], - reusablePatterns: ['none'], - stableSolutions: ['无新增稳定解法。'], - title: '无效清理证据', - verificationEvidence: ['typecheck 通过。'], + cleanupEvidence: ["cleanup-history dry-run deleted=3。"], + docSyncEvidence: ["kt_change_doc_sync requiredDocs 已处理。"], + problemRecords: ["无新卡点。"], + reviewEvidence: ["global-review findings=0。"], + reusablePatterns: ["none"], + stableSolutions: ["无新增稳定解法。"], + title: "无效清理证据", + verificationEvidence: ["typecheck 通过。"], }); if ((closeoutInvalidCleanup.canReportComplete as boolean) !== false) { - throw new Error('workstream closeout cleanup evidence self-check failed'); + throw new Error("workstream closeout cleanup evidence self-check failed"); } const obsidianContext = readObsidianContext({ maxDocuments: 6, - module: 'ktWorkflow', + module: "ktWorkflow", }); - if ((obsidianContext.summary as { returnedDocuments?: number }).returnedDocuments === 0) { - throw new Error('obsidian context self-check failed'); + if ( + (obsidianContext.summary as { returnedDocuments?: number }) + .returnedDocuments === 0 + ) { + throw new Error("obsidian context self-check failed"); } if ((obsidianContext.summary as { codeAnchors?: number }).codeAnchors === 0) { - throw new Error('obsidian code anchor self-check failed'); + throw new Error("obsidian code anchor self-check failed"); } const obsidianValidation = validateObsidianVault({ includeStaleReferences: false, }); if ((obsidianValidation.summary as { errors?: number }).errors !== 0) { - throw new Error('obsidian validation self-check failed'); + throw new Error("obsidian validation self-check failed"); } const cleanupCliParser = { - defaultDryRun: parseCliCleanupArgs(['node', 'server', '--cleanup-history']).dryRun, - executeDryRun: parseCliCleanupArgs(['node', 'server', '--cleanup-history', '--execute']).dryRun, + defaultDryRun: parseCliCleanupArgs(["node", "server", "--cleanup-history"]) + .dryRun, + executeDryRun: parseCliCleanupArgs([ + "node", + "server", + "--cleanup-history", + "--execute", + ]).dryRun, }; if (!cleanupCliParser.defaultDryRun || cleanupCliParser.executeDryRun) { - throw new Error('cleanup CLI dry-run parser self-check failed'); + throw new Error("cleanup CLI dry-run parser self-check failed"); } const cleanupPreview = cleanupHistoryArtifacts({ dryRun: true, keep: 3, - roots: ['.kt-workspace/test-artifacts', '.kt-workspace'], + roots: [".kt-workspace/test-artifacts", ".kt-workspace"], }); const cleanupRoots = cleanupPreview.roots as Array<{ preservedNames: string[]; root: string; }>; - const testArtifactsRoot = cleanupRoots.find((item) => item.root === '.kt-workspace/test-artifacts'); - const workspaceHistoryRoot = cleanupRoots.find((item) => item.root === '.kt-workspace'); - if (!testArtifactsRoot?.preservedNames.includes('_templates')) { - throw new Error('cleanup template preservation self-check failed'); + const testArtifactsRoot = cleanupRoots.find( + (item) => item.root === ".kt-workspace/test-artifacts", + ); + const workspaceHistoryRoot = cleanupRoots.find( + (item) => item.root === ".kt-workspace", + ); + if (!testArtifactsRoot?.preservedNames.includes("_templates")) { + throw new Error("cleanup template preservation self-check failed"); } - if (!workspaceHistoryRoot?.preservedNames.includes('test-artifacts')) { - throw new Error('cleanup workspace root preservation self-check failed'); + if (!workspaceHistoryRoot?.preservedNames.includes("test-artifacts")) { + throw new Error("cleanup workspace root preservation self-check failed"); } const docSyncBangDream = await buildChangeDocSync({ - changedFiles: ['src/qqbot/plugins/bangDream/application/bangdream-application.service.ts'], - project: 'api', + changedFiles: [ + "src/qqbot/plugins/bangDream/application/bangdream-application.service.ts", + ], + project: "api", }); - const docSyncRequiredDocs = docSyncBangDream.requiredDocs as Array<{ path: string }>; - if (!docSyncRequiredDocs.some((item) => item.path === 'Node/kt-template-online-api/API.md')) { - throw new Error('doc sync API self-check failed'); + const docSyncRequiredDocs = docSyncBangDream.requiredDocs as Array<{ + path: string; + }>; + if ( + !docSyncRequiredDocs.some( + (item) => item.path === "Node/kt-template-online-api/API.md", + ) + ) { + throw new Error("doc sync API self-check failed"); } const loopAuditComplete = buildWorkflowLoopAudit({ cleanupExecuted: true, cleanupFinalDeleted: 0, cleanupPreviewDeleted: 3, - docSyncEvidence: ['kt_change_doc_sync requiredDocs 已处理。'], + docSyncEvidence: ["kt_change_doc_sync requiredDocs 已处理。"], ktWorkflowUpdated: true, - problemRecords: ['测试历史清理流程未自动执行。'], + problemRecords: ["测试历史清理流程未自动执行。"], reusablePattern: true, - reviewEvidence: ['global-review findings=0'], - stableSolutions: ['cleanup-history 默认 dry-run,显式 --execute 后复验 deleted=0。'], - taskTitle: '自动化闭环审计', - testCases: ['cleanup-history dry-run/execute 回归测试'], - verificationEvidence: ['typecheck/self-test/cleanup dry-run 通过'], + reviewEvidence: ["global-review findings=0"], + stableSolutions: [ + "cleanup-history 默认 dry-run,显式 --execute 后复验 deleted=0。", + ], + taskTitle: "自动化闭环审计", + testCases: ["cleanup-history dry-run/execute 回归测试"], + verificationEvidence: ["typecheck/self-test/cleanup dry-run 通过"], }); if ((loopAuditComplete.canReportComplete as boolean) !== true) { - throw new Error('workflow loop complete self-check failed'); + throw new Error("workflow loop complete self-check failed"); } const loopAuditBlocked = buildWorkflowLoopAudit({ cleanupPreviewDeleted: 1, - taskTitle: '缺证据闭环', + taskTitle: "缺证据闭环", }); if ((loopAuditBlocked.canReportComplete as boolean) !== false) { - throw new Error('workflow loop blocked self-check failed'); + throw new Error("workflow loop blocked self-check failed"); } const loopAuditMissingCleanup = buildWorkflowLoopAudit({ - docSyncEvidence: ['kt_change_doc_sync requiredDocs 已处理。'], - reviewEvidence: ['global-review findings=0。'], - taskTitle: '缺清理证据闭环', - testCases: ['self-test'], - verificationEvidence: ['typecheck 通过。'], + docSyncEvidence: ["kt_change_doc_sync requiredDocs 已处理。"], + reviewEvidence: ["global-review findings=0。"], + taskTitle: "缺清理证据闭环", + testCases: ["self-test"], + verificationEvidence: ["typecheck 通过。"], }); if ((loopAuditMissingCleanup.canReportComplete as boolean) !== false) { - throw new Error('workflow loop missing cleanup evidence self-check failed'); + throw new Error("workflow loop missing cleanup evidence self-check failed"); } const dbSyncPlan = buildDbSyncPlan({}); - const dbSyncCommands = (dbSyncPlan.commands as string[]).join('\n'); + const dbSyncCommands = (dbSyncPlan.commands as string[]).join("\n"); const backupName = dbSyncPlan.backupName as string; if ( - !backupName.includes('_') || - !dbSyncCommands.includes('.kt-workspace/db-sync') || - dbSyncCommands.includes('CREATE DATABASE IF NOT EXISTS') + !backupName.includes("_") || + !dbSyncCommands.includes(".kt-workspace/db-sync") || + dbSyncCommands.includes("CREATE DATABASE IF NOT EXISTS") ) { - throw new Error('DB sync plan safety self-check failed'); + throw new Error("DB sync plan safety self-check failed"); } const data = { context: readWorkflowContext({ taskRecordCount: 2 }), guardrails: buildGuardrails({ - project: 'mcp', - taskType: 'mcp', - userRequest: '扩展 KT 工作区 MCP 可复用能力', + project: "mcp", + taskType: "mcp", + userRequest: "扩展 KT 工作区 MCP 可复用能力", }), cleanupCliParser, cleanupPreview, @@ -275,26 +373,29 @@ export async function runSelfTest(): Promise { dryRun: true, keep: 3, }), - inspectAdmin: await inspectProject({ includeGit: false, project: 'admin' }), - inspectKnife4j: await inspectProject({ includeGit: false, project: 'knife4j' }), + inspectAdmin: await inspectProject({ includeGit: false, project: "admin" }), + inspectKnife4j: await inspectProject({ + includeGit: false, + project: "knife4j", + }), prepareTask: await prepareTask({ includeGit: false, - project: 'mcp', - taskType: 'mcp', - userRequest: '扩展 KT 工作区 MCP 可复用能力', + project: "mcp", + taskType: "mcp", + userRequest: "扩展 KT 工作区 MCP 可复用能力", }), review: await buildGlobalCodeReview({ includeContentScan: true, includeRootScan: true, maxFindingsPerProject: 10, - projects: ['mcp'], + projects: ["mcp"], }), reviewCliParser, reviewClassifier, refactorGuardrails, apiVerification, businessTestPlan: buildBusinessTestPlan({ - flow: 'system-log-visualization', + flow: "system-log-visualization", }), closeoutComplete, closeoutNeedsUpgrade, @@ -305,41 +406,47 @@ export async function runSelfTest(): Promise { obsidianValidation, blockerResolution: buildBlockerResolution({ attempts: 2, - evidence: 'self-test', - problem: '重复超时', - project: 'mcp', - solution: '改用固定脚本和 timeout 后再验证', + evidence: "self-test", + problem: "重复超时", + project: "mcp", + solution: "改用固定脚本和 timeout 后再验证", }), commitPlan: await buildCommitPlan({ - projects: ['mcp'], + projects: ["mcp"], }), componentWorkflow: buildComponentWorkflow({ - target: 'SystemLog', + target: "SystemLog", }), dbSyncPlan, finishTask: await buildFinishTask({ cleanupHistory: false, - projects: ['mcp'], + projects: ["mcp"], runValidation: false, }), pushPlan: await buildPushPlan({ - projects: ['mcp'], + projects: ["mcp"], }), remoteHealthCheck: await buildRemoteHealthCheck({ execute: false, - services: ['ssh', 'docker'], + services: ["ssh", "docker"], }), verification: buildVerificationPlan({ - changeType: 'mcp', - project: 'mcp', + changeType: "mcp", + project: "mcp", }), }; - const outputDir = resolveInsideRoot('.kt-workspace/verify/ktWorkflow'); + const outputDir = resolveInsideRoot(".kt-workspace/verify/ktWorkflow"); mkdirSync(outputDir, { recursive: true }); writeFileSync( - path.join(outputDir, 'self-test.json'), + path.join(outputDir, "self-test.json"), JSON.stringify(data, null, 2), - 'utf8', + "utf8", + ); + console.log( + JSON.stringify( + { ok: true, outputDir, tools: registeredToolNames.length }, + null, + 2, + ), ); - console.log(JSON.stringify({ ok: true, outputDir, tools: registeredToolNames.length }, null, 2)); } diff --git a/src/tools/envRisk.ts b/src/tools/envRisk.ts index 3725549..6183a8f 100644 --- a/src/tools/envRisk.ts +++ b/src/tools/envRisk.ts @@ -1,17 +1,22 @@ -import { existsSync, readFileSync, statSync } from 'node:fs'; -import path from 'node:path'; +import { existsSync, readFileSync, statSync } from "node:fs"; +import path from "node:path"; -import type { ProjectAlias } from '../types.js'; -import type { EnvPolicyInput, RiskScanInput } from '../types.js'; -import { tryExecFile } from '../core/exec.js'; -import { detectRepoType, parseGitStatusFiles } from '../core/project.js'; -import { readTextIfExists, resolveProject, toPosix, workspaceRoot } from '../core/workspace.js'; +import type { ProjectAlias } from "../types.js"; +import type { EnvPolicyInput, RiskScanInput } from "../types.js"; +import { tryExecFile } from "../core/exec.js"; +import { detectRepoType, parseGitStatusFiles } from "../core/project.js"; +import { + readTextIfExists, + resolveProject, + toPosix, + workspaceRoot, +} from "../core/workspace.js"; const frontendEnvProjectAliases = new Set([ - 'admin', - 'blog', - 'playground', - 'web', + "admin", + "blog", + "playground", + "web", ]); function allowsTrackedFrontendEnv(projectAlias: ProjectAlias | null): boolean { @@ -19,29 +24,60 @@ function allowsTrackedFrontendEnv(projectAlias: ProjectAlias | null): boolean { } function readSmallFileIfExists(filePath: string): string { - if (!existsSync(filePath)) return ''; + if (!existsSync(filePath)) return ""; try { const stats = statSync(filePath); - if (!stats.isFile() || stats.size > 512 * 1024) return ''; - return readFileSync(filePath, 'utf8'); + if (!stats.isFile() || stats.size > 512 * 1024) return ""; + return readFileSync(filePath, "utf8"); } catch { - return ''; + return ""; } } +export function hasEntityLoadTimeSerializationMutation( + source: string, +): boolean { + if (!/\bAfterLoad\b/.test(source)) return false; + if ( + !/\bKtDateTimeField\b|\btransformKtDateTimeFields\b|\bFormatDateTime\b|\bformatDateTimeFields\b/.test( + source, + ) + ) { + return false; + } + + const usesDateSubclass = /\bclass\s+KtDateTime\s+extends\s+Date\b/.test( + source, + ); + const returnsPlainFormattedValue = /return\s+formatKtDateTime\(\s*value/.test( + source, + ); + const assignsPlainFormattedValue = /\[[^\]]+\]\s*=\s*formatKtDateTime\(/.test( + source, + ); + + return ( + !usesDateSubclass || + returnsPlainFormattedValue || + assignsPlainFormattedValue + ); +} + export function buildEnvPolicy(input: EnvPolicyInput): Record { const project = resolveProject(input.project); const frontendEnvAllowed = allowsTrackedFrontendEnv(project.alias); - const envFiles = ['.env.development', '.env.production', '.env.example'].map((file) => { - const absolutePath = path.join(project.path, file); - return { - exists: existsSync(absolutePath), - file, - relativePath: toPosix(path.relative(workspaceRoot, absolutePath)), - }; - }); - const gitignore = readTextIfExists(path.join(project.path, '.gitignore')); + const envFiles = [".env.development", ".env.production", ".env.example"].map( + (file) => { + const absolutePath = path.join(project.path, file); + return { + exists: existsSync(absolutePath), + file, + relativePath: toPosix(path.relative(workspaceRoot, absolutePath)), + }; + }, + ); + const gitignore = readTextIfExists(path.join(project.path, ".gitignore")); const ignoredRules = gitignore .split(/\r?\n/) .map((line) => line.trim()) @@ -49,31 +85,40 @@ export function buildEnvPolicy(input: EnvPolicyInput): Record { return { envFiles, - ignoredRules: ignoredRules.filter((line) => line.includes('.env')), + ignoredRules: ignoredRules.filter((line) => line.includes(".env")), policy: frontendEnvAllowed ? [ - '前端项目的 .env.development/.env.production 可以跟随仓库,用于客户端构建变量。', - '前端 env 仍不得写入服务端密钥、数据库密码、WordPress 管理员密码、MinIO Secret 或 token。', - '.env.example 继续维护变量名和示例值。', + "前端项目的 .env.development/.env.production 可以跟随仓库,用于客户端构建变量。", + "前端 env 仍不得写入服务端密钥、数据库密码、WordPress 管理员密码、MinIO Secret 或 token。", + ".env.example 继续维护变量名和示例值。", ] : [ - '真实开发和生产配置只保留在本地,不提交。', - '.env.example 用于维护变量名和示例值。', - '接口、数据库、WordPress、MinIO 等真实账号密码不得写入文档或提交记录。', + "真实开发和生产配置只保留在本地,不提交。", + ".env.example 用于维护变量名和示例值。", + "接口、数据库、WordPress、MinIO 等真实账号密码不得写入文档或提交记录。", ], project, warnings: envFiles - .filter((file) => file.file !== '.env.example' && file.exists && !frontendEnvAllowed) + .filter( + (file) => + file.file !== ".env.example" && file.exists && !frontendEnvAllowed, + ) .map((file) => `${file.file} 存在,提交前确认已被 .gitignore 忽略。`), }; } -export async function scanTaskRisk(input: RiskScanInput): Promise> { +export async function scanTaskRisk( + input: RiskScanInput, +): Promise> { const project = resolveProject(input.project); let changedFiles = input.changedFiles || []; - if (changedFiles.length === 0 && detectRepoType(project.path) === 'git') { - const status = await tryExecFile('git', ['status', '--short'], project.path); + if (changedFiles.length === 0 && detectRepoType(project.path) === "git") { + const status = await tryExecFile( + "git", + ["status", "--short"], + project.path, + ); if (status.ok) { changedFiles = parseGitStatusFiles(status.stdout); } @@ -81,12 +126,12 @@ export async function scanTaskRisk(input: RiskScanInput): Promise = []; const addRisk = ( - level: 'high' | 'medium', + level: "high" | "medium", file: string, reason: string, suggestion: string, @@ -95,119 +140,175 @@ export async function scanTaskRisk(input: RiskScanInput): Promise\s*\{\s*(?!\{[\s\S]*?\bdefault\s*:)/.test(source)) { + if ( + /<(?:A?TabPane|Tabs\.TabPane)\b[\s\S]*?>\s*\{\s*(?!\{[\s\S]*?\bdefault\s*:)/.test( + source, + ) + ) { addRisk( - 'medium', + "medium", file, - 'TabPane 插槽疑似未显式声明 default。', - 'Vue TSX 嵌套组件插槽使用 {{ default: () => ... }} 或等价显式 default 写法。', + "TabPane 插槽疑似未显式声明 default。", + "Vue TSX 嵌套组件插槽使用 {{ default: () => ... }} 或等价显式 default 写法。", ); } if (/\bonUpdate:[A-Za-z_$][\w$]*/.test(source)) { addRisk( - 'medium', + "medium", file, - 'Vue TSX 中存在手写 onUpdate:* 绑定。', - '优先使用原生 v-model:value / v-model:activeKey 等写法,减少事件名偏差。', + "Vue TSX 中存在手写 onUpdate:* 绑定。", + "优先使用原生 v-model:value / v-model:activeKey 等写法,减少事件名偏差。", ); } if (/\bdestroyOnClose\b/.test(source)) { addRisk( - 'medium', + "medium", file, - 'Antdv Modal destroyOnClose 已废弃。', - '改用 destroyOnHidden,或优先复用 Vben Modal/Form 封装。', + "Antdv Modal destroyOnClose 已废弃。", + "改用 destroyOnHidden,或优先复用 Vben Modal/Form 封装。", ); } } @@ -218,9 +319,9 @@ export async function scanTaskRisk(input: RiskScanInput): Promise([ - 'admin', - 'blog', - 'playground', - 'web', + "admin", + "blog", + "playground", + "web", ]); const credentialAssignmentPattern = /\b(password|passwd|secret|token|webuiToken|accessToken|privateKey)\b\s*[:=]\s*(["'])([^"']+)\2/i; const napcatImageAssignmentPattern = /^\s*QQBOT_NAPCAT_IMAGE\s*=\s*([^#\s]+)\s*(?:#.*)?$/; -const taskRecordAllowedFields = new Set(['范围', '关键词', '验证']); -const taskRecordRequiredFields = ['范围', '关键词', '验证'] as const; +const taskRecordAllowedFields = new Set(["范围", "关键词", "验证"]); +const taskRecordRequiredFields = ["范围", "关键词", "验证"] as const; interface ReviewFinding { category: string; file?: string; line?: number; - level: 'P1' | 'P2' | 'P3'; + level: "P1" | "P2" | "P3"; message: string; project: string; suggestion: string; @@ -82,22 +87,23 @@ interface ReviewProjectResult { trackedSensitiveFiles: string[]; } -export function findTaskRecordGovernanceFindings(lines: string[]): ReviewFinding[] { +export function findTaskRecordGovernanceFindings( + lines: string[], +): ReviewFinding[] { const findings: ReviewFinding[] = []; - const recentStart = lines.findIndex((line) => line.trim() === '## 最近记录'); + 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 recentEnd = + nextSectionOffset < 0 ? lines.length : recentStart + 1 + nextSectionOffset; + let currentRecord: { + fields: Set; + heading: string; + line: number; + } | null = null; const pushMissingFieldFindings = () => { if (!currentRecord) return; @@ -106,24 +112,25 @@ export function findTaskRecordGovernanceFindings(lines: string[]): ReviewFinding if (currentRecord.fields.has(field)) continue; findings.push({ - category: 'tasks-record-missing-field', - file: 'TASKS.md', + category: "tasks-record-missing-field", + file: "TASKS.md", line: currentRecord.line, - level: 'P2', + level: "P2", message: `TASKS 最近记录缺少「${field}」字段。`, - project: 'root', - suggestion: '最近记录只保留范围、关键词、验证三项,详细过程迁入 docs 或历史索引。', + project: "root", + suggestion: + "最近记录只保留范围、关键词、验证三项,详细过程迁入 docs 或历史索引。", }); } }; for (let index = recentStart + 1; index < recentEnd; index += 1) { const line = lines[index].trim(); - if (line.startsWith('### ')) { + if (line.startsWith("### ")) { pushMissingFieldFindings(); currentRecord = { fields: new Set(), - heading: line.replace(/^###\s+/, ''), + heading: line.replace(/^###\s+/, ""), line: index + 1, }; continue; @@ -141,13 +148,14 @@ export function findTaskRecordGovernanceFindings(lines: string[]): ReviewFinding } findings.push({ - category: 'tasks-record-extra-field', - file: 'TASKS.md', + category: "tasks-record-extra-field", + file: "TASKS.md", line: index + 1, - level: 'P2', + level: "P2", message: `TASKS 最近记录「${currentRecord.heading}」包含额外字段「${field}」。`, - project: 'root', - suggestion: '把额外说明迁入 docs,TASKS 最近记录只保留范围、关键词、验证。', + project: "root", + suggestion: + "把额外说明迁入 docs,TASKS 最近记录只保留范围、关键词、验证。", }); } @@ -164,23 +172,24 @@ export function findNapcatImageGovernanceFindings( ): ReviewFinding[] { return lines.flatMap((line, index) => { const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) return []; + if (!trimmed || trimmed.startsWith("#")) return []; const matched = line.match(napcatImageAssignmentPattern); if (!matched) return []; const image = matched[1].trim(); - if (!image.endsWith(':latest')) return []; + if (!image.endsWith(":latest")) return []; return [ { - category: 'napcat-latest-image', + category: "napcat-latest-image", file: context.file, line: index + 1, - level: 'P3' as const, - message: 'NapCat 运行镜像使用 latest,生产排查会受到镜像漂移影响。', + level: "P3" as const, + message: "NapCat 运行镜像使用 latest,生产排查会受到镜像漂移影响。", project: context.project, - suggestion: '改为明确版本 tag 或 digest,并先在单账号容器观察稳定性后推广。', + suggestion: + "改为明确版本 tag 或 digest,并先在单账号容器观察稳定性后推广。", }, ]; }); @@ -193,11 +202,11 @@ export function findQqbotCommandServiceTestImportFindings( project: string; }, ): ReviewFinding[] { - const normalized = context.file.replaceAll('\\', '/'); + const normalized = context.file.replaceAll("\\", "/"); if ( - context.project !== 'Node/kt-template-online-api' || - !normalized.startsWith('test/qqbot/') || - !normalized.endsWith('.ts') + context.project !== "Node/kt-template-online-api" || + !normalized.startsWith("test/qqbot/") || + !normalized.endsWith(".ts") ) { return []; } @@ -207,30 +216,32 @@ export function findQqbotCommandServiceTestImportFindings( return [ { - category: 'qqbot-command-service-heavy-test-import', + category: "qqbot-command-service-heavy-test-import", file: context.file, line: index + 1, - level: 'P2' as const, - message: 'QQBot 测试直接导入 QqbotCommandService,容易拖入插件注册和 BangDream runtime 导致单测卡住。', + level: "P2" as const, + message: + "QQBot 测试直接导入 QqbotCommandService,容易拖入插件注册和 BangDream runtime 导致单测卡住。", project: context.project, - suggestion: '把纯策略抽到轻量函数测试,或通过 Nest TestingModule 明确 mock 掉插件 registry/runtime 依赖。', + suggestion: + "把纯策略抽到轻量函数测试,或通过 Nest TestingModule 明确 mock 掉插件 registry/runtime 依赖。", }, ]; }); } async function listGitTrackedFiles(projectPath: string): Promise { - const tracked = await tryExecFile('git', ['ls-files'], projectPath); + const tracked = await tryExecFile("git", ["ls-files"], projectPath); if (!tracked.ok) return []; return tracked.stdout .split(/\r?\n/) .map((file) => file.trim()) .filter(Boolean) - .map((file) => file.replaceAll('\\', '/')); + .map((file) => file.replaceAll("\\", "/")); } function shouldReadReviewFile(file: string): boolean { - const normalized = file.replaceAll('\\', '/'); + const normalized = file.replaceAll("\\", "/"); return !reviewSkipFilePattern.test(normalized); } @@ -241,33 +252,37 @@ function readProjectFileLines(projectPath: string, file: string): string[] { try { const stats = statSync(filePath); if (!stats.isFile() || stats.size > 1024 * 1024) return []; - return readFileSync(filePath, 'utf8').split(/\r?\n/); + return readFileSync(filePath, "utf8").split(/\r?\n/); } catch { return []; } } function isBenignCredentialValue(value: string): boolean { - const normalized = value.trim().replace(/^["']|["']$/g, '').toLowerCase(); + const normalized = value + .trim() + .replace(/^["']|["']$/g, "") + .toLowerCase(); if (!normalized) return true; - if (normalized.startsWith('$') || normalized.startsWith('${')) return true; - if (normalized.startsWith('<') || normalized.includes('example')) return true; + if (normalized.startsWith("$") || normalized.startsWith("${")) return true; + if (normalized.startsWith("<") || normalized.includes("example")) return true; + if (isObviousPlaceholderCredentialValue(normalized)) return true; if (/^\*+$/.test(normalized)) return true; if (isLocalizedDisplayLabelValue(normalized)) return true; return [ - 'false', - 'hidden', - 'masked', - 'null', - 'password', - 'please-replace-me-with-your-own-key', - 'redacted', - 'replace-me', - 'secret', - 'token', - 'true', - 'undefined', - 'your-token', + "false", + "hidden", + "masked", + "null", + "password", + "please-replace-me-with-your-own-key", + "redacted", + "replace-me", + "secret", + "token", + "true", + "undefined", + "your-token", ].includes(normalized); } @@ -275,6 +290,17 @@ export function isBenignCredentialReviewValue(value: string): boolean { return isBenignCredentialValue(value); } +function isObviousPlaceholderCredentialValue(value: string): boolean { + return ( + /^(test|mock|fake|dummy|demo)[-_]?(password|passwd|secret|token|key)?([_-]?\w*)?$/.test( + value, + ) || + /^(password|passwd|secret|token|key)[-_](test|mock|fake|dummy|demo|x)([_-]?\w*)?$/.test( + value, + ) + ); +} + function isLocalizedDisplayLabelValue(value: string): boolean { return ( value.length <= 24 && @@ -283,30 +309,45 @@ function isLocalizedDisplayLabelValue(value: string): boolean { ); } -function isRuntimeDebugAllowed(projectAlias: ProjectAlias | null, file: string): boolean { - const normalized = file.replaceAll('\\', '/'); +function isRuntimeDebugAllowed( + projectAlias: ProjectAlias | null, + file: string, +): boolean { + const normalized = file.replaceAll("\\", "/"); if (/^(README|skills\/|.*\.md$)/.test(normalized)) return true; - if (projectAlias === 'admin' && normalized.startsWith('internal/vite-config/')) { + if ( + projectAlias === "admin" && + normalized.startsWith("internal/vite-config/") + ) { return true; } - if (projectAlias === 'mcp' && /^(scripts\/|src\/server\.ts$|src\/selfTest\.ts$)/.test(normalized)) { + if ( + projectAlias === "mcp" && + /^(scripts\/|src\/server\.ts$|src\/selfTest\.ts$)/.test(normalized) + ) { return true; } - if (projectAlias === 'fnosK8s' && /^(scripts\/|src\/server\.ts$)/.test(normalized)) { + if ( + projectAlias === "fnosK8s" && + /^(scripts\/|src\/server\.ts$)/.test(normalized) + ) { return true; } return false; } function isCredentialHintAllowed(file: string, line: string): boolean { - const normalized = file.replaceAll('\\', '/'); + const normalized = file.replaceAll("\\", "/"); const trimmed = line.trimStart(); - if (trimmed.startsWith('<')) return true; + if (trimmed.startsWith("<")) return true; if (/\b(url|uri|dsn)\.password\s*=\s*['"]\*+['"]/.test(trimmed)) return true; return /(^|\/)views\/(demos|examples)\//.test(normalized); } -function isAllowedSensitiveTrackedFile(projectAlias: ProjectAlias | null, file: string): boolean { +function isAllowedSensitiveTrackedFile( + projectAlias: ProjectAlias | null, + file: string, +): boolean { if (allowedSensitiveTrackedFilePattern.test(file)) return true; if (projectAlias && frontendEnvProjectAliases.has(projectAlias)) { return /(^|\/)\.env($|\..+)/i.test(file); @@ -335,38 +376,44 @@ function collectProjectContentFindings( lines.forEach((line, index) => { if (/^(<<<<<<<|=======|>>>>>>>) /.test(line)) { findings.push({ - category: 'conflict-marker', + category: "conflict-marker", file, line: index + 1, - level: 'P1', - message: '发现 Git 冲突标记。', + level: "P1", + message: "发现 Git 冲突标记。", project: project.relativePath, - suggestion: '先解决冲突标记,再继续构建或提交。', + suggestion: "先解决冲突标记,再继续构建或提交。", }); } - if (!isRuntimeDebugAllowed(project.alias, file) && /\bdebugger;|console\.log\(/.test(line)) { + if ( + !isRuntimeDebugAllowed(project.alias, file) && + /\bdebugger;|console\.log\(/.test(line) + ) { debugFindings.push({ - category: 'runtime-debug', + category: "runtime-debug", file, line: index + 1, - level: /\bdebugger;/.test(line) ? 'P2' : 'P3', - message: '运行时代码中存在调试输出。', + level: /\bdebugger;/.test(line) ? "P2" : "P3", + message: "运行时代码中存在调试输出。", project: project.relativePath, - suggestion: '页面或服务运行时代码不要保留 console.log/debugger。', + suggestion: "页面或服务运行时代码不要保留 console.log/debugger。", }); } if (includeContentScan) { - if (/\.(tsx|jsx|vue)$/.test(file) && /\bfunction\s+render[A-Z0-9_]/.test(line)) { + if ( + /\.(tsx|jsx|vue)$/.test(file) && + /\bfunction\s+render[A-Z0-9_]/.test(line) + ) { findings.push({ - category: 'vue-tsx-render-helper', + category: "vue-tsx-render-helper", file, line: index + 1, - level: 'P2', - message: 'Vue TSX render helper 使用 function 声明。', + level: "P2", + message: "Vue TSX render helper 使用 function 声明。", project: project.relativePath, - suggestion: '改为箭头函数,并在嵌套组件插槽中显式传 default。', + suggestion: "改为箭头函数,并在嵌套组件插槽中显式传 default。", }); } @@ -378,13 +425,14 @@ function collectProjectContentFindings( !isBenignCredentialValue(credentialMatch[3]) ) { credentialHints.push({ - category: 'credential-literal', + category: "credential-literal", file, line: index + 1, - level: 'P2', + level: "P2", message: `疑似 ${credentialMatch[1]} 字面量赋值。`, project: project.relativePath, - suggestion: '确认是否为真实凭据;真实值应移入本地 env 或远程 Secret。', + suggestion: + "确认是否为真实凭据;真实值应移入本地 env 或远程 Secret。", }); } } @@ -418,7 +466,10 @@ function collectProjectContentFindings( }; } -function findWorkspaceFilesByName(names: Set, maxResults: number): string[] { +function findWorkspaceFilesByName( + names: Set, + maxResults: number, +): string[] { const results: string[] = []; const walk = (currentPath: string) => { @@ -433,7 +484,11 @@ function findWorkspaceFilesByName(names: Set, maxResults: number): strin } if (entry.isFile() && names.has(entry.name)) { - results.push(toPosix(path.relative(workspaceRoot, path.join(currentPath, entry.name)))); + results.push( + toPosix( + path.relative(workspaceRoot, path.join(currentPath, entry.name)), + ), + ); } } }; @@ -453,25 +508,33 @@ function findRootGeneratedArtifacts(): string[] { async function reviewProject( projectKey: string, input: Required< - Pick + Pick< + GlobalCodeReviewInput, + "contentScanMode" | "includeContentScan" | "maxFindingsPerProject" + > >, ): Promise { const project = resolveProject(projectKey); const repoType = detectRepoType(project.path); const status = - repoType === 'git' - ? await tryExecFile('git', ['status', '--short'], project.path) - : { ok: false, stderr: '', stdout: '' }; + repoType === "git" + ? await tryExecFile("git", ["status", "--short"], project.path) + : { ok: false, stderr: "", stdout: "" }; const changedFiles = status.ok ? parseGitStatusFiles(status.stdout) : []; - const trackedFiles = repoType === 'git' ? await listGitTrackedFiles(project.path) : []; + const trackedFiles = + repoType === "git" ? await listGitTrackedFiles(project.path) : []; const trackedFileSet = new Set(trackedFiles); const contentScannedFiles = - input.contentScanMode === 'all' + input.contentScanMode === "all" ? trackedFiles : changedFiles - .map((file) => file.replaceAll('\\', '/')) + .map((file) => file.replaceAll("\\", "/")) .filter((file) => shouldReadReviewFile(file)) - .filter((file) => trackedFileSet.has(file) || existsSync(path.join(project.path, file))); + .filter( + (file) => + trackedFileSet.has(file) || + existsSync(path.join(project.path, file)), + ); const trackedSensitiveFiles = trackedFiles.filter( (file) => sensitiveTrackedFilePattern.test(file) && @@ -486,12 +549,13 @@ async function reviewProject( const trackedSensitiveFindings = trackedSensitiveFiles .slice(0, input.maxFindingsPerProject) .map((file) => ({ - category: 'tracked-sensitive-file', + category: "tracked-sensitive-file", file, - level: 'P1', - message: '仓库仍跟踪真实环境或部署凭据类文件。', + level: "P1", + message: "仓库仍跟踪真实环境或部署凭据类文件。", project: project.relativePath, - suggestion: '使用 git rm --cached 移出索引,并提交 example 文件或文档说明。', + suggestion: + "使用 git rm --cached 移出索引,并提交 example 文件或文档说明。", })); return { @@ -519,7 +583,7 @@ export async function buildGlobalCodeReview( ? input.projects! : [...defaultReviewProjects]; const includeContentScan = input.includeContentScan ?? true; - const contentScanMode = input.contentScanMode ?? 'changed'; + const contentScanMode = input.contentScanMode ?? "changed"; const maxFindingsPerProject = Math.max( 1, Math.min(input.maxFindingsPerProject ?? 20, 200), @@ -536,34 +600,39 @@ export async function buildGlobalCodeReview( const rootLegacyFiles = input.includeRootScan === false ? [] - : findWorkspaceFilesByName(new Set(['deploy.js', 'deploy.json']), 50); + : findWorkspaceFilesByName(new Set(["deploy.js", "deploy.json"]), 50); const rootGeneratedArtifacts = input.includeRootScan === false ? [] : findRootGeneratedArtifacts(); const taskRecordFindings = input.includeRootScan === false ? [] - : findTaskRecordGovernanceFindings(readProjectFileLines(workspaceRoot, 'TASKS.md')); + : findTaskRecordGovernanceFindings( + readProjectFileLines(workspaceRoot, "TASKS.md"), + ); const findings = projectResults.flatMap((item) => [ ...item.findings, ...item.debugFindings, ...item.credentialHints, ]); const rootFindings = rootLegacyFiles.map((file) => ({ - category: 'legacy-deploy-file', + category: "legacy-deploy-file", file, - level: 'P2', - message: '发现旧部署遗留文件。', - project: 'root', - suggestion: '确认是否仍被脚本引用;无用则删除并从 Git 索引移除。', - })); - const rootGeneratedFindings = rootGeneratedArtifacts.map((file) => ({ - category: 'root-generated-artifact', - file, - level: 'P2', - message: '根目录存在生成态临时产物。', - project: 'root', - suggestion: '统一迁入 .kt-workspace 对应子目录,并更新生成脚本或测试流程。', + level: "P2", + message: "发现旧部署遗留文件。", + project: "root", + suggestion: "确认是否仍被脚本引用;无用则删除并从 Git 索引移除。", })); + const rootGeneratedFindings = rootGeneratedArtifacts.map( + (file) => ({ + category: "root-generated-artifact", + file, + level: "P2", + message: "根目录存在生成态临时产物。", + project: "root", + suggestion: + "统一迁入 .kt-workspace 对应子目录,并更新生成脚本或测试流程。", + }), + ); const allFindings = [ ...findings, ...rootFindings, @@ -575,11 +644,11 @@ export async function buildGlobalCodeReview( findings: allFindings, generatedAt: new Date().toISOString(), notes: [ - '本工具只读扫描,不会修改文件、删除文件或提交代码。', - 'P1 优先处理:冲突标记、被跟踪的真实 env/部署凭据文件。', - '默认 contentScanMode=changed,只对当前变更文件做 P2/P3 内容扫描,并放过短中文显示标签,避免历史误报污染上下文;需要全仓库深扫时传 all。', - 'P2 需要人工确认:疑似凭据字面量、旧部署文件、调试断点。', - 'P3 为清洁度问题:运行时 console.log 等。', + "本工具只读扫描,不会修改文件、删除文件或提交代码。", + "P1 优先处理:冲突标记、被跟踪的真实 env/部署凭据文件。", + "默认 contentScanMode=changed,只对当前变更文件做 P2/P3 内容扫描,并放过短中文显示标签,避免历史误报污染上下文;需要全仓库深扫时传 all。", + "P2 需要人工确认:疑似凭据字面量、旧部署文件、调试断点。", + "P3 为清洁度问题:运行时 console.log 等。", ], projects: projectResults.map((item) => ({ changedFiles: item.changedFiles, @@ -594,13 +663,14 @@ export async function buildGlobalCodeReview( trackedSensitiveFiles: item.trackedSensitiveFiles, })), summary: { - changedProjectCount: projectResults.filter((item) => item.changedFiles.length > 0) - .length, + changedProjectCount: projectResults.filter( + (item) => item.changedFiles.length > 0, + ).length, contentScanMode, findingCount: allFindings.length, - p1: allFindings.filter((item) => item.level === 'P1').length, - p2: allFindings.filter((item) => item.level === 'P2').length, - p3: allFindings.filter((item) => item.level === 'P3').length, + p1: allFindings.filter((item) => item.level === "P1").length, + p2: allFindings.filter((item) => item.level === "P2").length, + p3: allFindings.filter((item) => item.level === "P3").length, reviewedProjects: projectResults.length, rootGeneratedArtifacts, rootLegacyFiles,