diff --git a/README.md b/README.md index f7da257..27bec18 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ Windows 上的 stdio MCP 客户端通过 `wsl.exe` 启动 Debian Bash;Node、 | `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 cleanup-history` | 预览 `.kt-workspace` 测试/验证历史产物清理;真实清理用 `pnpm run cleanup-history -- --execute`,只保留最近 3 轮,并保留模板、活动运行输入和 `.kt-workspace/toolchains`。 | | `pnpm run test:unit` | 只运行显式登记的 Node 22 单元测试,不触发完整 evidence、Obsidian 或多仓集成构造。 | | `pnpm run test:live2d` | 运行 Live2D analyzer、review schema、S192-S217 历史切片、完整调用目录、implementation-target、module migration、variable/identifier 与 runtime coverage 独立测试。 | | `pnpm run test:integration` | 只运行 workflow closeout、Obsidian、cleanup、doc-sync 和 synthetic evidence writer 集成测试。 | @@ -166,6 +166,8 @@ Windows 上的 stdio MCP 客户端通过 `wsl.exe` 启动 Debian Bash;Node、 | `root` | `/home/yemu2/KT` | | `mcp` | `mcp/ktWorkflow` | | `napcat` | `GitHub/NapCatQQ` | +| `networkAgent` | `Go/kt-network-agent` | +| `palworld` | `Go/palworld-direct-connect` | | `api` | `Node/kt-template-online-api` | | `admin` | `Vue/kt-template-admin` | | `blog` | `Vue/kt-blog-web` | @@ -210,7 +212,7 @@ Windows 上的 stdio MCP 客户端通过 `wsl.exe` 启动 Debian Bash;Node、 - Admin 页面测试前可先执行 `pnpm run admin-login -- --url ` 固化登录态,输出的 `storageState` 可作为后续 Playwright 用例前置状态。 - 接口改动后调用 `kt_api_test_plan`,并真实请求一次接口。 - 验证启动过本地服务后调用 `kt_cleanup_process_plan`,用只读计划核对路径、端口和 PID,再只结束本轮启动的进程。 -- 每轮测试/验证结束后先调用 `kt_cleanup_history`,或运行 `pnpm run cleanup-history -- --dry-run`;如果预览里 `deleted` 非空,确认范围后执行 `pnpm run cleanup-history -- --execute`,再复跑 dry-run 确认 `deleted=0`,让 `.kt-workspace` 历史产物只保留最近 3 轮并保留模板目录。Pio Live2D 的 `runtime-publish`、`runtime-export-moc-driven`、`probe-source-wordpress-parity`、`moc-runtime` 等活跃发布/基准输入会被保留,不按历史轮次删除。`kt_workstream_closeout` / `kt_workflow_loop_audit` 只认可明确的 `deleted=0` / `deleted=[]` 文本证据或结构化 `cleanupFinalDeleted=0`。 +- 每轮测试/验证结束后先调用 `kt_cleanup_history`,或运行 `pnpm run cleanup-history -- --dry-run`;如果预览里 `deleted` 非空,确认范围后执行 `pnpm run cleanup-history -- --execute`,再复跑 dry-run 确认 `deleted=0`,让 `.kt-workspace` 历史产物只保留最近 3 轮并保留模板目录。Pio Live2D 的 `runtime-publish`、`runtime-export-moc-driven`、`probe-source-wordpress-parity`、`moc-runtime` 等活跃发布/基准输入和 `.kt-workspace/toolchains` 会被保留,不按历史轮次删除。`kt_workstream_closeout` / `kt_workflow_loop_audit` 只认可明确的 `deleted=0` / `deleted=[]` 文本证据或结构化 `cleanupFinalDeleted=0`。 - 更新 Obsidian 图谱、模块页、文档矩阵或 `.obsidian` 配置后运行 `kt_obsidian_validate` / `pnpm run obsidian-validate`;整理工作流入口时再跑 `kt_obsidian_sync`。 - 改完文件后用 `kt_append_task_record` 先 `dryRun` 预览记录,再决定是否写入。 - 提交前调用 `kt_commit_checklist`,确认文件范围和提交信息。 diff --git a/src/__tests__/integration/obsidianAndCleanup.test.ts b/src/__tests__/integration/obsidianAndCleanup.test.ts index 74e1764..2974e54 100644 --- a/src/__tests__/integration/obsidianAndCleanup.test.ts +++ b/src/__tests__/integration/obsidianAndCleanup.test.ts @@ -89,6 +89,10 @@ test("preserves cleanup templates, workspace roots, and active Live2D runtime pa workspaceHistoryRoot?.preservedNames.includes("live2d-pio"), true, ); + assert.equal( + workspaceHistoryRoot?.preservedNames.includes("toolchains"), + true, + ); assert.equal( workspaceHistoryRoot?.preservedNames.includes("external"), false, diff --git a/src/__tests__/unit.index.test.ts b/src/__tests__/unit.index.test.ts index 55aa58d..7b119c8 100644 --- a/src/__tests__/unit.index.test.ts +++ b/src/__tests__/unit.index.test.ts @@ -6,5 +6,6 @@ import "./unit/deployObservation.test.js"; import "./unit/guardrailsAndVerification.test.js"; import "./unit/napcatAutomation.test.js"; import "./unit/reviewGovernance.test.js"; +import "./unit/vueTsxReview.test.js"; import "./unit/blogLive2DReviewRules.test.js"; import "./unit/qqbotReviewRules.test.js"; diff --git a/src/__tests__/unit/guardrailsAndVerification.test.ts b/src/__tests__/unit/guardrailsAndVerification.test.ts index 6cfc513..63d05bd 100644 --- a/src/__tests__/unit/guardrailsAndVerification.test.ts +++ b/src/__tests__/unit/guardrailsAndVerification.test.ts @@ -1,11 +1,11 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { registeredToolNames } from "../../core/constants.js"; +import { projectAliases, registeredToolNames } from "../../core/constants.js"; import { registerTools } from "../../registerTools.js"; import { hasEntityLoadTimeSerializationMutation } from "../../tools/envRisk.js"; import { buildGuardrails } from "../../tools/guardrails.js"; -import { buildGlobalCodeReview } from "../../tools/review.js"; +import { buildGlobalCodeReview, defaultReviewProjects } from "../../tools/review.js"; import { buildBusinessTestPlan, buildNapcatDeviceProfileCheck, @@ -13,6 +13,13 @@ import { import { buildVerificationPlan } from "../../tools/verification.js"; import { buildDbSyncPlan } from "../../tools/workflow.js"; +test("keeps both network runtimes inside project inspection and default global review", () => { + assert.equal(projectAliases.networkAgent, "Go/kt-network-agent"); + assert.equal(defaultReviewProjects.includes("networkAgent"), true); + assert.equal(projectAliases.palworld, "Go/palworld-direct-connect"); + assert.equal(defaultReviewProjects.includes("palworld"), true); +}); + test("adds refactor-specific before-edit and behavior-preservation guardrails", () => { const result = buildGuardrails({ project: "mcp", diff --git a/src/__tests__/unit/vueTsxReview.test.ts b/src/__tests__/unit/vueTsxReview.test.ts new file mode 100644 index 0000000..4dd0775 --- /dev/null +++ b/src/__tests__/unit/vueTsxReview.test.ts @@ -0,0 +1,40 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { findContextDependentVueTsxRenderHelpers } from "../../core/vueTsxReview.js"; + +/** Verifies safe setup-local render functions are not forced into arrow syntax. */ +test("allows function-declared Vue TSX slot render helpers without dynamic this", () => { + const source = ` + function renderAgentControls() { + return {status.value}; + } + function renderBodyCell({ record }) { + return {record.name}; + } + return () => ; + `; + + assert.deepEqual(findContextDependentVueTsxRenderHelpers(source), []); +}); + +/** Verifies only a render declaration that actually depends on dynamic this is flagged. */ +test("finds function-declared Vue TSX render helpers that read dynamic this", () => { + const source = ` + function renderSafe() { + function nestedOwner() { return this.label; } + return nestedOwner(); + } + function renderUnsafe() { + const read = () => this.label; + return {read()}; + } + `; + + assert.deepEqual(findContextDependentVueTsxRenderHelpers(source), [ + { line: 6, name: "renderUnsafe" }, + ]); +}); diff --git a/src/core/constants.ts b/src/core/constants.ts index 8f3e21e..39af8cc 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -7,6 +7,8 @@ export const projectAliases = { knife4j: 'Plugins/knife4j-swagger-vue3', mcp: 'mcp/ktWorkflow', napcat: 'GitHub/NapCatQQ', + networkAgent: 'Go/kt-network-agent', + palworld: 'Go/palworld-direct-connect', playground: 'Vue/kt-template-online-playground', root: '.', web: 'Vue/kt-template-online-web', @@ -20,6 +22,8 @@ export const projectLabels = { knife4j: 'Knife4j Swagger Vue3 插件', mcp: 'KT Workflow MCP', napcat: 'NapCatQQ Fork', + networkAgent: 'KT Network Agent', + palworld: 'Palworld Direct Connect PoC', playground: 'Playground 编辑器', root: 'KT 协作根目录', web: 'Web 前台展示', diff --git a/src/core/vueTsxReview.ts b/src/core/vueTsxReview.ts new file mode 100644 index 0000000..01513b8 --- /dev/null +++ b/src/core/vueTsxReview.ts @@ -0,0 +1,77 @@ +import ts from "typescript"; + +export interface ContextDependentRenderHelper { + line: number; + name: string; +} + +/** + * Finds function-declared render helpers whose own lexical scope reads dynamic `this`. + * Plain function declarations are valid slot callbacks; only an actual context dependency + * justifies recommending an arrow function. + * @param source - TSX, JSX, or Vue script source to inspect. + * @returns Render helper names and one-based declaration lines that depend on `this`. + */ +export function findContextDependentVueTsxRenderHelpers( + source: string, +): ContextDependentRenderHelper[] { + const sourceFile = ts.createSourceFile( + "review.tsx", + source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX, + ); + const findings: ContextDependentRenderHelper[] = []; + + /** Visits declarations while keeping each render helper's `this` owner isolated. */ + function visit(node: ts.Node): void { + if ( + ts.isFunctionDeclaration(node) && + node.name && + /^render[A-Z0-9_]/.test(node.name.text) && + functionReadsDynamicThis(node) + ) { + const position = sourceFile.getLineAndCharacterOfPosition( + node.getStart(sourceFile), + ); + findings.push({ line: position.line + 1, name: node.name.text }); + } + ts.forEachChild(node, visit); + } + + visit(sourceFile); + return findings; +} + +/** + * Checks `this` owned by one function declaration, including nested arrows but excluding + * nested functions or methods that establish their own dynamic `this`. + * @param declaration - Render helper declaration under review. + * @returns True when the helper's own context is read. + */ +function functionReadsDynamicThis( + declaration: ts.FunctionDeclaration, +): boolean { + let found = false; + + /** Walks the helper body until a different dynamic-`this` owner is reached. */ + function visit(node: ts.Node): void { + if (found) return; + if (node.kind === ts.SyntaxKind.ThisKeyword) { + found = true; + return; + } + if ( + node !== declaration && + ts.isFunctionLike(node) && + !ts.isArrowFunction(node) + ) { + return; + } + ts.forEachChild(node, visit); + } + + if (declaration.body) visit(declaration.body); + return found; +} diff --git a/src/tools/cleanup.ts b/src/tools/cleanup.ts index 273a2c7..8153462 100644 --- a/src/tools/cleanup.ts +++ b/src/tools/cleanup.ts @@ -61,6 +61,7 @@ const workspaceRootPreservedNames = new Set([ 'subagents', 'test-artifacts', 'test-logs', + 'toolchains', 'tmp', 'verify', ]); diff --git a/src/tools/envRisk.ts b/src/tools/envRisk.ts index 6183a8f..8d8df3b 100644 --- a/src/tools/envRisk.ts +++ b/src/tools/envRisk.ts @@ -5,6 +5,7 @@ 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 { findContextDependentVueTsxRenderHelpers } from "../core/vueTsxReview.js"; import { readTextIfExists, resolveProject, @@ -275,12 +276,12 @@ export async function scanTaskRisk( if (/\.(tsx|jsx|vue)$/.test(normalized)) { const source = readSmallFileIfExists(path.join(project.path, normalized)); - if (/\bfunction\s+render[A-Z0-9_]/.test(source)) { + if (findContextDependentVueTsxRenderHelpers(source).length > 0) { addRisk( "medium", file, - "Vue TSX render helper 使用 function 声明,容易在插槽渲染时踩 this/上下文问题。", - "改为箭头函数,并在嵌套组件插槽中显式传 default。", + "Vue TSX render helper 读取动态 this,插槽调用时可能丢失上下文。", + "改为箭头函数或移除动态 this 依赖;普通无 this 的函数声明无需改写。", ); } if ( diff --git a/src/tools/review.ts b/src/tools/review.ts index b2efebd..7bd5219 100644 --- a/src/tools/review.ts +++ b/src/tools/review.ts @@ -9,6 +9,7 @@ import type { } from "../types.js"; import { tryExecFile } from "../core/exec.js"; import { detectRepoType, parseGitStatusFiles } from "../core/project.js"; +import { findContextDependentVueTsxRenderHelpers } from "../core/vueTsxReview.js"; import { resolveProject, toPosix, workspaceRoot } from "../core/workspace.js"; import { scanTaskRisk } from "./envRisk.js"; export const defaultReviewProjects = [ @@ -19,6 +20,8 @@ export const defaultReviewProjects = [ "playground", "mcp", "napcat", + "networkAgent", + "palworld", "knife4j", "fnosK8s", ] as const; @@ -1981,21 +1984,6 @@ function collectProjectContentFindings( } if (includeContentScan) { - if ( - /\.(tsx|jsx|vue)$/.test(file) && - /\bfunction\s+render[A-Z0-9_]/.test(line) - ) { - findings.push({ - category: "vue-tsx-render-helper", - file, - line: index + 1, - level: "P2", - message: "Vue TSX render helper 使用 function 声明。", - project: project.relativePath, - suggestion: "改为箭头函数,并在嵌套组件插槽中显式传 default。", - }); - } - const credentialMatch = line.match(credentialAssignmentPattern); if ( credentialMatch && @@ -2018,6 +2006,21 @@ function collectProjectContentFindings( }); if (includeContentScan) { + if (/\.(tsx|jsx|vue)$/.test(file)) { + findings.push( + ...findContextDependentVueTsxRenderHelpers(lines.join("\n")).map( + (helper) => ({ + category: "vue-tsx-render-helper", + file, + line: helper.line, + level: "P2" as const, + message: `Vue TSX render helper ${helper.name} 依赖动态 this。`, + project: project.relativePath, + suggestion: "改为箭头函数或移除动态 this 依赖。", + }), + ), + ); + } findings.push( ...findNapcatImageGovernanceFindings(lines, { file, diff --git a/src/types.ts b/src/types.ts index 307ee83..62ec968 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,6 +6,8 @@ export type ProjectAlias = | 'knife4j' | 'mcp' | 'napcat' + | 'networkAgent' + | 'palworld' | 'playground' | 'root' | 'web';