chore: 固化时间序列化工作流规则
This commit is contained in:
parent
efaceb7a97
commit
7e79b38600
14
README.md
14
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 约定。
|
||||
|
||||
@ -70,7 +70,7 @@ 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 项目,适合多仓库联动任务 |
|
||||
@ -104,9 +104,9 @@ 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 复审报告,并校验 `TASKS.md` 最近记录只保留范围、关键词、验证字段、NapCat `latest` 漂移和 QQBot 重型服务单测导入风险;确认误报时优先升级 `src/tools/review.ts`,不要把误报沉积到上下文。 |
|
||||
| `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。 |
|
||||
@ -116,7 +116,7 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login
|
||||
## 项目别名
|
||||
|
||||
| 别名 | 路径 |
|
||||
| --- | --- |
|
||||
| ------------ | ----------------------------------- |
|
||||
| `root` | `D:/MyFiles/KT` |
|
||||
| `mcp` | `mcp/ktWorkflow` |
|
||||
| `api` | `Node/kt-template-online-api` |
|
||||
@ -168,6 +168,6 @@ 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 |
|
||||
|
||||
429
src/selfTest.ts
429
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<void> {
|
||||
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',
|
||||
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');
|
||||
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';"],
|
||||
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: "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<void> {
|
||||
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<void> {
|
||||
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));
|
||||
}
|
||||
|
||||
@ -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<ProjectAlias>([
|
||||
'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<string, unknown> {
|
||||
const project = resolveProject(input.project);
|
||||
const frontendEnvAllowed = allowsTrackedFrontendEnv(project.alias);
|
||||
const envFiles = ['.env.development', '.env.production', '.env.example'].map((file) => {
|
||||
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 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<string, unknown> {
|
||||
|
||||
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<Record<string, unknown>> {
|
||||
export async function scanTaskRisk(
|
||||
input: RiskScanInput,
|
||||
): Promise<Record<string, unknown>> {
|
||||
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<Record<string,
|
||||
|
||||
const risks: Array<{
|
||||
file: string;
|
||||
level: 'high' | 'medium';
|
||||
level: "high" | "medium";
|
||||
reason: string;
|
||||
suggestion: string;
|
||||
}> = [];
|
||||
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<Record<string,
|
||||
};
|
||||
|
||||
for (const file of changedFiles) {
|
||||
const normalized = file.replaceAll('\\', '/');
|
||||
const normalized = file.replaceAll("\\", "/");
|
||||
|
||||
if (/(^|\/)\.env($|\..+)/.test(normalized)) {
|
||||
if (allowsTrackedFrontendEnv(project.alias)) {
|
||||
addRisk(
|
||||
'medium',
|
||||
"medium",
|
||||
file,
|
||||
'前端构建环境文件有改动。',
|
||||
'确认只包含客户端公开配置,不包含服务端密钥、数据库密码或 token。',
|
||||
"前端构建环境文件有改动。",
|
||||
"确认只包含客户端公开配置,不包含服务端密钥、数据库密码或 token。",
|
||||
);
|
||||
} else {
|
||||
addRisk('high', file, '真实环境配置文件有改动。', '确认不会提交真实密钥和密码。');
|
||||
}
|
||||
}
|
||||
|
||||
if (/package\.json$|pnpm-lock\.yaml$|package-lock\.json$|yarn\.lock$/.test(normalized)) {
|
||||
addRisk('medium', file, '依赖或脚本发生变化。', '确认锁文件与 package.json 同步,并运行对应轻量验证。');
|
||||
}
|
||||
|
||||
if (normalized.includes('/components/ktTable/')) {
|
||||
addRisk(
|
||||
'medium',
|
||||
"high",
|
||||
file,
|
||||
'KtTable 是高频核心组件,布局和滚动性能容易回归。',
|
||||
'保持原生 antdv-next 表格能力,避免监听 scroll/resize 做高频状态更新。',
|
||||
"真实环境配置文件有改动。",
|
||||
"确认不会提交真实密钥和密码。",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
/package\.json$|pnpm-lock\.yaml$|package-lock\.json$|yarn\.lock$/.test(
|
||||
normalized,
|
||||
)
|
||||
) {
|
||||
addRisk(
|
||||
"medium",
|
||||
file,
|
||||
"依赖或脚本发生变化。",
|
||||
"确认锁文件与 package.json 同步,并运行对应轻量验证。",
|
||||
);
|
||||
}
|
||||
|
||||
if (/src\/admin\/|src\/.*controller|src\/.*service|src\/.*dto/.test(normalized)) {
|
||||
addRisk('medium', file, '后端接口或业务逻辑改动。', '本地启动或复用服务后真实调用对应接口。');
|
||||
}
|
||||
|
||||
if (/src\/common\/decorators\/format-date-time\.decorator\.ts|src\/.*\.(entity|dto)\.ts/.test(normalized)) {
|
||||
if (normalized.includes("/components/ktTable/")) {
|
||||
addRisk(
|
||||
'medium',
|
||||
"medium",
|
||||
file,
|
||||
'后端时间字段或 DTO/Entity 响应结构改动。',
|
||||
'确认数据源经 DTO/Entity 实例返回,FormatDateTime 不做递归响应遍历,时间统一 YYYY-MM-DD HH:mm:ss。',
|
||||
"KtTable 是高频核心组件,布局和滚动性能容易回归。",
|
||||
"保持原生 antdv-next 表格能力,避免监听 scroll/resize 做高频状态更新。",
|
||||
);
|
||||
}
|
||||
|
||||
if (/src\/common\/logger\/|src\/admin\/system-log\/|pino|loki|system-log/i.test(normalized)) {
|
||||
if (
|
||||
/src\/admin\/|src\/.*controller|src\/.*service|src\/.*dto/.test(
|
||||
normalized,
|
||||
)
|
||||
) {
|
||||
addRisk(
|
||||
'medium',
|
||||
"medium",
|
||||
file,
|
||||
'Pino/Loki 或系统日志链路改动。',
|
||||
'验证 API 日志写入、/system/logs 系列接口、Admin 系统日志页面和 Loki ready/query_range。',
|
||||
"后端接口或业务逻辑改动。",
|
||||
"本地启动或复用服务后真实调用对应接口。",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/src\/common\/decorators\/kt-date-time\.decorator\.ts|src\/.*\.(entity|dto)\.ts/.test(
|
||||
normalized,
|
||||
)
|
||||
) {
|
||||
addRisk(
|
||||
"medium",
|
||||
file,
|
||||
"后端时间字段或 DTO/Entity 响应结构改动。",
|
||||
"确认 Entity 使用 KtDateTimeColumn/KtCreateDateColumn/KtUpdateDateColumn,DTO 使用 KtDateTimeField + transformKtDateTimeFields;不要在响应包装里递归遍历,也不要把业务态 Date 改成普通字符串。",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
/src\/common\/decorators\/kt-date-time\.decorator\.ts$/.test(normalized)
|
||||
) {
|
||||
const source = readSmallFileIfExists(path.join(project.path, normalized));
|
||||
if (hasEntityLoadTimeSerializationMutation(source)) {
|
||||
addRisk(
|
||||
"high",
|
||||
file,
|
||||
"KtDateTime 装饰器疑似在 Entity 加载阶段把时间改成字符串。",
|
||||
"Entity hydrate 只能通过 TypeORM transformer 包装成 KtDateTime(Date 子类);DTO 只能在字段 transformer 边界转换;不要在 vbenSuccess/ToolsService.res 做全量递归序列化。",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
/src\/common\/logger\/|src\/admin\/system-log\/|pino|loki|system-log/i.test(
|
||||
normalized,
|
||||
)
|
||||
) {
|
||||
addRisk(
|
||||
"medium",
|
||||
file,
|
||||
"Pino/Loki 或系统日志链路改动。",
|
||||
"验证 API 日志写入、/system/logs 系列接口、Admin 系统日志页面和 Loki ready/query_range。",
|
||||
);
|
||||
}
|
||||
|
||||
if (/src\/qqbot\/plugins\/(ff14Market|fflogs)\//i.test(normalized)) {
|
||||
addRisk(
|
||||
'medium',
|
||||
"medium",
|
||||
file,
|
||||
'FF14/FFLogs 命令插件改动。',
|
||||
'确认中文查询和回显走字典映射,命令注册表同步,真实命令输出字段完整。',
|
||||
"FF14/FFLogs 命令插件改动。",
|
||||
"确认中文查询和回显走字典映射,命令注册表同步,真实命令输出字段完整。",
|
||||
);
|
||||
}
|
||||
|
||||
if (/src\/qqbot\/(account|napcat|connection)\//i.test(normalized)) {
|
||||
addRisk(
|
||||
'medium',
|
||||
"medium",
|
||||
file,
|
||||
'QQBot/NapCat 登录链路改动。',
|
||||
'验证新增账号和更新登录都能生成有效二维码,SSE 进度可见,旧 isLogin 状态不会短路。',
|
||||
"QQBot/NapCat 登录链路改动。",
|
||||
"验证新增账号和更新登录都能生成有效二维码,SSE 进度可见,旧 isLogin 状态不会短路。",
|
||||
);
|
||||
}
|
||||
|
||||
if (/apps\/web-antdv-next\/src\/views\/system\/(dict|log)\//.test(normalized)) {
|
||||
if (
|
||||
/apps\/web-antdv-next\/src\/views\/system\/(dict|log)\//.test(normalized)
|
||||
) {
|
||||
addRisk(
|
||||
'medium',
|
||||
"medium",
|
||||
file,
|
||||
'Admin 系统管理页改动。',
|
||||
'验证菜单权限、KtTable 展示、列表筛选和对应 API 响应结构。',
|
||||
"Admin 系统管理页改动。",
|
||||
"验证菜单权限、KtTable 展示、列表筛选和对应 API 响应结构。",
|
||||
);
|
||||
}
|
||||
|
||||
if (/Jenkinsfile|deploy\/|k8s\/|nginx.*\.conf/.test(normalized)) {
|
||||
addRisk('medium', file, '部署链路配置改动。', '只验证配置语法或生成产物,不主动触发远程部署。');
|
||||
addRisk(
|
||||
"medium",
|
||||
file,
|
||||
"部署链路配置改动。",
|
||||
"只验证配置语法或生成产物,不主动触发远程部署。",
|
||||
);
|
||||
}
|
||||
|
||||
if (/\.(tsx|jsx|vue)$/.test(normalized)) {
|
||||
const source = readSmallFileIfExists(path.join(project.path, normalized));
|
||||
if (/\bfunction\s+render[A-Z0-9_]/.test(source)) {
|
||||
addRisk(
|
||||
'medium',
|
||||
"medium",
|
||||
file,
|
||||
'Vue TSX render helper 使用 function 声明,容易在插槽渲染时踩 this/上下文问题。',
|
||||
'改为箭头函数,并在嵌套组件插槽中显式传 default。',
|
||||
"Vue TSX render helper 使用 function 声明,容易在插槽渲染时踩 this/上下文问题。",
|
||||
"改为箭头函数,并在嵌套组件插槽中显式传 default。",
|
||||
);
|
||||
}
|
||||
if (/<(?:A?TabPane|Tabs\.TabPane)\b[\s\S]*?>\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<Record<string,
|
||||
project,
|
||||
risks,
|
||||
safeDefaults: [
|
||||
'不覆盖用户已有改动。',
|
||||
'只运行必要验证。',
|
||||
'提交前重新检查 status 和 diff。',
|
||||
"不覆盖用户已有改动。",
|
||||
"只运行必要验证。",
|
||||
"提交前重新检查 status 和 diff。",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,43 +1,48 @@
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import type { GlobalCodeReviewInput, ProjectAlias, RepoType, ResolvedProject } from '../types.js';
|
||||
import { tryExecFile } from '../core/exec.js';
|
||||
import { detectRepoType, parseGitStatusFiles } from '../core/project.js';
|
||||
import { resolveProject, toPosix, workspaceRoot } from '../core/workspace.js';
|
||||
import { scanTaskRisk } from './envRisk.js';
|
||||
import type {
|
||||
GlobalCodeReviewInput,
|
||||
ProjectAlias,
|
||||
RepoType,
|
||||
ResolvedProject,
|
||||
} from "../types.js";
|
||||
import { tryExecFile } from "../core/exec.js";
|
||||
import { detectRepoType, parseGitStatusFiles } from "../core/project.js";
|
||||
import { resolveProject, toPosix, workspaceRoot } from "../core/workspace.js";
|
||||
import { scanTaskRisk } from "./envRisk.js";
|
||||
export const defaultReviewProjects = [
|
||||
'api',
|
||||
'admin',
|
||||
'blog',
|
||||
'web',
|
||||
'playground',
|
||||
'mcp',
|
||||
'knife4j',
|
||||
'fnosK8s',
|
||||
"api",
|
||||
"admin",
|
||||
"blog",
|
||||
"web",
|
||||
"playground",
|
||||
"mcp",
|
||||
"knife4j",
|
||||
"fnosK8s",
|
||||
] as const;
|
||||
|
||||
const reviewSkipDirectoryNames = new Set([
|
||||
'.git',
|
||||
'.turbo',
|
||||
'.vite',
|
||||
'coverage',
|
||||
'dist',
|
||||
'node_modules',
|
||||
'playwright-report',
|
||||
'test-results',
|
||||
".git",
|
||||
".turbo",
|
||||
".vite",
|
||||
"coverage",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"playwright-report",
|
||||
"test-results",
|
||||
]);
|
||||
|
||||
const reviewSkipFilePattern =
|
||||
/(^|\/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|.*\.map|.*\.png|.*\.jpg|.*\.jpeg|.*\.gif|.*\.webp|.*\.zip|.*\.tar|.*\.gz)$/i;
|
||||
|
||||
const rootGeneratedArtifactNames = new Set([
|
||||
'.codex-db-sync',
|
||||
'.codex-test-logs',
|
||||
'.codex-verify',
|
||||
'.playwright-mcp',
|
||||
'codex-test-logs',
|
||||
'test-artifacts',
|
||||
".codex-db-sync",
|
||||
".codex-test-logs",
|
||||
".codex-verify",
|
||||
".playwright-mcp",
|
||||
"codex-test-logs",
|
||||
"test-artifacts",
|
||||
]);
|
||||
|
||||
const sensitiveTrackedFilePattern =
|
||||
@ -46,24 +51,24 @@ const sensitiveTrackedFilePattern =
|
||||
const allowedSensitiveTrackedFilePattern = /(^|\/)\.env\.example$/i;
|
||||
|
||||
const frontendEnvProjectAliases = new Set<ProjectAlias>([
|
||||
'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:
|
||||
| {
|
||||
const recentEnd =
|
||||
nextSectionOffset < 0 ? lines.length : recentStart + 1 + nextSectionOffset;
|
||||
let currentRecord: {
|
||||
fields: Set<string>;
|
||||
heading: string;
|
||||
line: number;
|
||||
}
|
||||
| null = null;
|
||||
} | 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<string>(),
|
||||
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<string[]> {
|
||||
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<string>, maxResults: number): string[] {
|
||||
function findWorkspaceFilesByName(
|
||||
names: Set<string>,
|
||||
maxResults: number,
|
||||
): string[] {
|
||||
const results: string[] = [];
|
||||
|
||||
const walk = (currentPath: string) => {
|
||||
@ -433,7 +484,11 @@ function findWorkspaceFilesByName(names: Set<string>, 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<GlobalCodeReviewInput, 'contentScanMode' | 'includeContentScan' | 'maxFindingsPerProject'>
|
||||
Pick<
|
||||
GlobalCodeReviewInput,
|
||||
"contentScanMode" | "includeContentScan" | "maxFindingsPerProject"
|
||||
>
|
||||
>,
|
||||
): Promise<ReviewProjectResult> {
|
||||
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<ReviewFinding>((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<ReviewFinding>((file) => ({
|
||||
category: 'legacy-deploy-file',
|
||||
category: "legacy-deploy-file",
|
||||
file,
|
||||
level: 'P2',
|
||||
message: '发现旧部署遗留文件。',
|
||||
project: 'root',
|
||||
suggestion: '确认是否仍被脚本引用;无用则删除并从 Git 索引移除。',
|
||||
level: "P2",
|
||||
message: "发现旧部署遗留文件。",
|
||||
project: "root",
|
||||
suggestion: "确认是否仍被脚本引用;无用则删除并从 Git 索引移除。",
|
||||
}));
|
||||
const rootGeneratedFindings = rootGeneratedArtifacts.map<ReviewFinding>((file) => ({
|
||||
category: 'root-generated-artifact',
|
||||
const rootGeneratedFindings = rootGeneratedArtifacts.map<ReviewFinding>(
|
||||
(file) => ({
|
||||
category: "root-generated-artifact",
|
||||
file,
|
||||
level: 'P2',
|
||||
message: '根目录存在生成态临时产物。',
|
||||
project: 'root',
|
||||
suggestion: '统一迁入 .kt-workspace 对应子目录,并更新生成脚本或测试流程。',
|
||||
}));
|
||||
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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user