From 39303f951e2224e0274e93e12a434e477da8d910 Mon Sep 17 00:00:00 2001 From: xycld Date: Sat, 21 Mar 2026 16:06:07 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20remove=20L1=20signature=20extra?= =?UTF-8?q?ction=20=E2=80=94=20adopt=20Translation=20Validation=20philosop?= =?UTF-8?q?hy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVP is a language-agnostic protocol. The SignatureExtractor + CONTENT_DRIFT mechanism extracted TypeScript-specific signatures from L1 code to detect drift, violating SVP's top-down compilation philosophy. Verification belongs at the L3 contract layer, not by reverse-analyzing compiled artifacts. Pure subtraction: remove signatureHash, CONTENT_DRIFT, detectContentDrift, computeL1Signatures, createTypescriptExtractor, and all related types, tests, and i18n keys. Move typescript to devDependencies. 24 files changed, +50/-950 lines. --- package.json | 4 +- packages/cli/commands/prompt.ts | 8 +- packages/cli/load.test.ts | 17 -- packages/cli/load.ts | 58 +---- packages/core/check.test.ts | 81 ------ packages/core/check.ts | 18 -- packages/core/compile-plan.test.ts | 135 +--------- packages/core/compile-plan.ts | 41 +-- packages/core/extractors/typescript.ts | 85 ------- packages/core/fingerprint.test.ts | 238 ------------------ packages/core/fingerprint.ts | 58 ----- packages/core/hash.test.ts | 16 +- packages/core/hash.ts | 2 +- packages/core/i18n.ts | 6 - packages/core/index.ts | 8 - packages/core/l2.ts | 8 +- packages/core/scan.test.ts | 58 +---- packages/core/scan.ts | 36 +-- packages/core/store.test.ts | 17 -- packages/skills/__tests__/link.test.ts | 32 +-- .../skills/__tests__/scan-prompts.test.ts | 50 +--- packages/skills/adapters/shared.ts | 4 +- packages/skills/link.ts | 1 - packages/skills/prompts/scan.ts | 19 +- 24 files changed, 50 insertions(+), 950 deletions(-) delete mode 100644 packages/core/extractors/typescript.ts delete mode 100644 packages/core/fingerprint.test.ts delete mode 100644 packages/core/fingerprint.ts diff --git a/package.json b/package.json index c41d919..2f5bbb2 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "typescript": "^5.9.3", "@types/node": "^22.0.0", "@vitest/coverage-v8": "^4.1.0", "eslint": "^10.0.3", @@ -46,7 +47,6 @@ }, "dependencies": { "@inquirer/prompts": "^8.3.2", - "commander": "^14.0.3", - "typescript": "^5.9.3" + "commander": "^14.0.3" } } diff --git a/packages/cli/commands/prompt.ts b/packages/cli/commands/prompt.ts index 74f775a..9aa6c1a 100644 --- a/packages/cli/commands/prompt.ts +++ b/packages/cli/commands/prompt.ts @@ -94,7 +94,7 @@ function registerTaskPrompt(parent: Command, action: TaskAction, config: TaskPro let input; try { - input = await loadCheckInput(root, { computeSignatures: action === "review" }); + input = await loadCheckInput(root); } catch { console.error(`Error: cannot load .svp/ data from "${root}". Run \`forge init\` first.`); process.exitCode = 1; @@ -460,10 +460,8 @@ function registerScan(parent: Command): void { } } - // Collect scan context (with TS extractor for signature extraction) - const { createTypescriptExtractor } = await import("../../core/extractors/typescript.js"); - const extractor = createTypescriptExtractor(); - const scanContext = await collectScanContext({ root, dir: scanDir, maxFiles }, extractor); + // Collect scan context + const scanContext = await collectScanContext({ root, dir: scanDir, maxFiles }); if (scanContext.files.length === 0) { console.error( diff --git a/packages/cli/load.test.ts b/packages/cli/load.test.ts index 3f22ec4..098db59 100644 --- a/packages/cli/load.test.ts +++ b/packages/cli/load.test.ts @@ -85,7 +85,6 @@ describe("loadCheckInput", () => { expect(result.l4Flows).toEqual([]); expect(result.l2Blocks).toEqual([]); expect(result.l5).toBeUndefined(); - expect(result.l1SignatureHashes).toBeUndefined(); }); it("loads L3 blocks correctly", async () => { @@ -165,22 +164,6 @@ describe("loadCheckInput", () => { expect(result.l2Blocks[0]).toEqual(l2); }); - it("computeSignatures: false (default) does not populate l1SignatureHashes", async () => { - const cb = makeL2("sig-block", { signatureHash: "some-sig" }); - await writeL2(root, cb); - - const result = await loadCheckInput(root); - expect(result.l1SignatureHashes).toBeUndefined(); - }); - - it("computeSignatures: false explicitly does not populate l1SignatureHashes", async () => { - const cb = makeL2("sig-block-2", { signatureHash: "some-sig" }); - await writeL2(root, cb); - - const result = await loadCheckInput(root, { computeSignatures: false }); - expect(result.l1SignatureHashes).toBeUndefined(); - }); - it("returns empty arrays when .svp/ directories exist but are empty", async () => { // Write and then delete content so dirs exist but are empty; simplest is // just to test with a fresh empty root (no .svp at all) — directories that diff --git a/packages/cli/load.ts b/packages/cli/load.ts index b343bd3..181c646 100644 --- a/packages/cli/load.ts +++ b/packages/cli/load.ts @@ -12,22 +12,16 @@ import { readL4, readL5, checkCompatibility, - computeSignatureHash, - createTypescriptExtractor, } from "../core/index.js"; import type { CheckInput, L2CodeBlock, L3Block, L4Artifact, - FileFingerprint, } from "../core/index.js"; -/** 从 .svp/ 加载所有层数据,含可选的 L1 签名计算 */ -export async function loadCheckInput( - root: string, - options: { computeSignatures?: boolean } = {}, -): Promise { +/** 从 .svp/ 加载所有层数据 */ +export async function loadCheckInput(root: string): Promise { // Ensure .svp/ schema is compatible before reading await checkCompatibility(root); @@ -54,16 +48,10 @@ export async function loadCheckInput( if (cb !== null) l2Blocks.push(cb); } - // 计算 L1 签名哈希(可选,需要 L2 blocks 有 signatureHash 且文件存在) - let l1SignatureHashes: Map | undefined; - if (options.computeSignatures === true && l2Blocks.length > 0) { - l1SignatureHashes = await computeL1Signatures(root, l2Blocks); - } - // 扫描 nodes/ 目录收集已有文档列表 const existingNodeDocs = await scanExistingNodeDocs(root); - return { l5, l4Flows, l3Blocks, l2Blocks, l1SignatureHashes, existingNodeDocs }; + return { l5, l4Flows, l3Blocks, l2Blocks, existingNodeDocs }; } /** Scan nodes/{id}/docs.md, return nodeId set with existing docs */ @@ -86,44 +74,4 @@ async function scanExistingNodeDocs(root: string): Promise> { return result; } -/** 遍历 L2 blocks,提取 L1 文件的导出签名并计算聚合 hash */ -async function computeL1Signatures( - root: string, - l2Blocks: readonly L2CodeBlock[], -): Promise> { - const extractor = createTypescriptExtractor(); - const hashes = new Map(); - - for (const cb of l2Blocks) { - // 只处理有 signatureHash 的 L2(说明之前做过签名追踪) - if (cb.signatureHash === undefined) continue; - - // 只处理 TypeScript 文件 - if (cb.language !== "typescript") continue; - - const fingerprints: FileFingerprint[] = []; - let allFilesExist = true; - - for (const filePath of cb.files) { - const absPath = path.resolve(root, filePath); - try { - const s = await stat(absPath); - if (!s.isFile()) { - allFilesExist = false; - break; - } - const fp = await extractor.extract(absPath); - fingerprints.push(fp); - } catch { - allFilesExist = false; - break; - } - } - - if (allFilesExist && fingerprints.length > 0) { - hashes.set(cb.id, computeSignatureHash(fingerprints)); - } - } - return hashes; -} diff --git a/packages/core/check.test.ts b/packages/core/check.test.ts index f7da9e3..f6e73af 100644 --- a/packages/core/check.test.ts +++ b/packages/core/check.test.ts @@ -326,70 +326,6 @@ describe("check — graph structure", () => { }); }); -describe("check — content drift (L1 → L2)", () => { - it("detects CONTENT_DRIFT when L1 signature hash changed", () => { - const l3 = makeL3(); - const l2 = makeL2({ blockRef: l3.id, sourceHash: l3.contentHash, signatureHash: "old-sig" }); - - const report = check({ - l4Flows: [], - l3Blocks: [l3], - l2Blocks: [l2], - l1SignatureHashes: new Map([["validate-order-ts", "new-sig"]]), - }); - - const drift = report.issues.find((i) => i.code === "CONTENT_DRIFT"); - expect(drift).toBeDefined(); - expect(drift?.severity).toBe("warning"); - expect(drift?.layer).toBe("l2"); - }); - - it("no CONTENT_DRIFT when signature hash matches", () => { - const l3 = makeL3(); - const l2 = makeL2({ blockRef: l3.id, sourceHash: l3.contentHash, signatureHash: "same-sig" }); - - const report = check({ - l4Flows: [], - l3Blocks: [l3], - l2Blocks: [l2], - l1SignatureHashes: new Map([["validate-order-ts", "same-sig"]]), - }); - - const drift = report.issues.find((i) => i.code === "CONTENT_DRIFT"); - expect(drift).toBeUndefined(); - }); - - it("skips CONTENT_DRIFT when l1SignatureHashes not provided", () => { - const l3 = makeL3(); - const l2 = makeL2({ blockRef: l3.id, sourceHash: l3.contentHash, signatureHash: "old-sig" }); - - const report = check({ - l4Flows: [], - l3Blocks: [l3], - l2Blocks: [l2], - // no l1SignatureHashes - }); - - const drift = report.issues.find((i) => i.code === "CONTENT_DRIFT"); - expect(drift).toBeUndefined(); - }); - - it("skips CONTENT_DRIFT when L2 has no signatureHash", () => { - const l3 = makeL3(); - const l2 = makeL2({ blockRef: l3.id, sourceHash: l3.contentHash }); - // l2 has no signatureHash (legacy data) - - const report = check({ - l4Flows: [], - l3Blocks: [l3], - l2Blocks: [l2], - l1SignatureHashes: new Map([["validate-order-ts", "any-sig"]]), - }); - - const drift = report.issues.find((i) => i.code === "CONTENT_DRIFT"); - expect(drift).toBeUndefined(); - }); -}); // ── EventGraph 校验 ── @@ -855,23 +791,6 @@ describe("check — empty steps", () => { }); }); -describe("check — CONTENT_DRIFT edge cases", () => { - it("skips CONTENT_DRIFT when l1SignatureHashes map has no entry for L2 id", () => { - const l3 = makeL3(); - const l2 = makeL2({ blockRef: l3.id, sourceHash: l3.contentHash, signatureHash: "old-sig" }); - - const report = check({ - l4Flows: [], - l3Blocks: [l3], - l2Blocks: [l2], - // map exists but does not contain the L2's id "validate-order-ts" - l1SignatureHashes: new Map([["some-other-id", "any-sig"]]), - }); - - const drift = report.issues.find((i) => i.code === "CONTENT_DRIFT"); - expect(drift).toBeUndefined(); - }); -}); describe("check — SOURCE_DRIFT edge cases", () => { it("skips SOURCE_DRIFT when L3 not found for blockRef", () => { diff --git a/packages/core/check.ts b/packages/core/check.ts index c6952a7..b75afc8 100644 --- a/packages/core/check.ts +++ b/packages/core/check.ts @@ -38,11 +38,6 @@ export interface CheckInput { readonly l3Blocks: readonly L3Block[]; readonly l2Blocks: readonly L2CodeBlock[]; - // L1 语义指纹:L2 block id → 当前 L1 文件的 signatureHash - // 由调用方(CLI)提前计算,check 只做比对,不依赖提取器 - // 省略时跳过 CONTENT_DRIFT 检测 - readonly l1SignatureHashes?: ReadonlyMap; - // 已有文档的 L3 block id 集合(nodes//docs.md 存在的) // 省略时跳过 MISSING_NODE_DOCS 检测 readonly existingNodeDocs?: ReadonlySet; @@ -615,19 +610,6 @@ function checkDrift(input: CheckInput, lang: string): CheckIssue[] { }); } - // CONTENT_DRIFT: L1 导出签名变了,L2 记录的 signatureHash 过期 - if (input.l1SignatureHashes !== undefined && cb.signatureHash !== undefined) { - const currentHash = input.l1SignatureHashes.get(cb.id); - if (currentHash !== undefined && currentHash !== cb.signatureHash) { - issues.push({ - severity: "warning", - layer: "l2", - entityId: cb.id, - code: "CONTENT_DRIFT", - message: t(lang, "check.contentDrift", { id: cb.id }), - }); - } - } } return issues; diff --git a/packages/core/compile-plan.test.ts b/packages/core/compile-plan.test.ts index 7f41080..3d6e1bd 100644 --- a/packages/core/compile-plan.test.ts +++ b/packages/core/compile-plan.test.ts @@ -214,46 +214,6 @@ describe("compilePlan", () => { expect(compileTasks).toHaveLength(1); }); - it("detects content drift (L1 signatures changed)", () => { - const l3 = makeL3("validate", "Validate"); - const l2: L2CodeBlock = { - ...makeL2(l3), - signatureHash: "old-sig", - }; - - const plan = compilePlan({ - l4Flows: [], - l3Blocks: [l3], - l2Blocks: [l2], - l1SignatureHashes: new Map([["validate", "new-sig"]]), - }); - - expect(plan.summary.review).toBe(1); - const task = plan.tasks.find((t) => t.issueCode === "CONTENT_DRIFT"); - expect(task).toBeDefined(); - expect(task?.action).toBe("review"); - expect(task?.targetLayer).toBe("l3"); - expect(task?.reason).toContain("L1 exported signatures changed"); - }); - - it("no content drift task when signatures match", () => { - const l3 = makeL3("validate", "Validate"); - const l2: L2CodeBlock = { - ...makeL2(l3), - signatureHash: "same-sig", - }; - - const plan = compilePlan({ - l4Flows: [], - l3Blocks: [l3], - l2Blocks: [l2], - l1SignatureHashes: new Map([["validate", "same-sig"]]), - }); - - const task = plan.tasks.find((t) => t.issueCode === "CONTENT_DRIFT"); - expect(task).toBeUndefined(); - }); - it("returns empty plan for empty input", () => { const plan = compilePlan({ l4Flows: [], l3Blocks: [], l2Blocks: [] }); expect(plan.tasks).toEqual([]); @@ -386,68 +346,6 @@ describe("compilePlan — recompile context", () => { }); }); -describe("compilePlan — content drift context", () => { - it("includes L2 and L3 in review task for CONTENT_DRIFT", () => { - const l3 = makeL3("svc", "Service"); - const l2: L2CodeBlock = { ...makeL2(l3), signatureHash: "old-hash" }; - - const plan = compilePlan({ - l4Flows: [], - l3Blocks: [l3], - l2Blocks: [l2], - l1SignatureHashes: new Map([["svc", "new-hash"]]), - }); - - const task = plan.tasks.find((t) => t.issueCode === "CONTENT_DRIFT"); - expect(task).toBeDefined(); - const contextLayers = task!.context.map((c) => c.layer); - expect(contextLayers).toContain("l2"); - expect(contextLayers).toContain("l3"); - }); - - it("uses L3 id as targetId when L3 exists", () => { - const l3 = makeL3("handler", "Handler"); - const l2: L2CodeBlock = { ...makeL2(l3), signatureHash: "stale" }; - - const plan = compilePlan({ - l4Flows: [], - l3Blocks: [l3], - l2Blocks: [l2], - l1SignatureHashes: new Map([["handler", "fresh"]]), - }); - - const task = plan.tasks.find((t) => t.issueCode === "CONTENT_DRIFT"); - expect(task).toBeDefined(); - expect(task!.targetId).toBe("handler"); // L3 id - expect(task!.targetLayer).toBe("l3"); - }); - - it("falls back to entityId when L3 not found for CONTENT_DRIFT", () => { - // L2 references a missing L3 (blockRef points nowhere) AND has CONTENT_DRIFT - // We need to craft this carefully: the L2 must have a signatureHash mismatch, - // but its blockRef must not match any L3 in the input. - // The L2 id must be in l1SignatureHashes to trigger CONTENT_DRIFT. - const ghostL3 = makeL3("ghost-l3", "Ghost"); - const l2: L2CodeBlock = { - ...makeL2(ghostL3), - id: "orphan-block", - blockRef: "ghost-l3", // references L3 not in input - signatureHash: "outdated", - }; - - const plan = compilePlan({ - l4Flows: [], - l3Blocks: [], // ghost-l3 is absent - l2Blocks: [l2], - l1SignatureHashes: new Map([["orphan-block", "current"]]), - }); - - const task = plan.tasks.find((t) => t.issueCode === "CONTENT_DRIFT"); - expect(task).toBeDefined(); - // l3 not found → targetId falls back to issue.entityId which is the L2 block id - expect(task!.targetId).toBe("orphan-block"); - }); -}); describe("compilePlan — orphaned L2", () => { it("generates review task for L2 with MISSING_BLOCK_REF", () => { @@ -536,25 +434,19 @@ describe("compilePlan — summary", () => { const l2Stale = makeL2(l3StaleOrig); const l3StaleUpdated = makeL3("stale-block", "Stale Block", { description: "Changed" }); - // review (CONTENT_DRIFT): "drift-block" has signature mismatch - const l3Drift = makeL3("drift-block", "Drift Block"); - const l2Drift: L2CodeBlock = { ...makeL2(l3Drift), signatureHash: "old" }; - // update-ref: L4 flow references missing L3 block const l4Broken = makeL4("broken-flow", ["nonexistent"]); const plan = compilePlan({ l4Flows: [l4Broken], - l3Blocks: [l3New, l3StaleUpdated, l3Drift], - l2Blocks: [l2Stale, l2Drift], - l1SignatureHashes: new Map([["drift-block", "new"]]), + l3Blocks: [l3New, l3StaleUpdated], + l2Blocks: [l2Stale], }); expect(plan.summary.compile).toBe(1); expect(plan.summary.recompile).toBe(1); - expect(plan.summary.review).toBe(1); expect(plan.summary.updateRef).toBe(1); - expect(plan.summary.total).toBe(4); + expect(plan.summary.total).toBe(3); }); it("returns zero counts for empty input", () => { @@ -617,16 +509,12 @@ describe("compilePlan — complexity", () => { const l2Stale = makeL2(l3StaleOrig); const l3StaleUpdated = makeL3("stale-block", "Stale Block", { description: "Changed" }); - const l3Drift = makeL3("drift-block", "Drift Block"); - const l2Drift: L2CodeBlock = { ...makeL2(l3Drift), signatureHash: "old" }; - const l4Broken = makeL4("broken-flow", ["nonexistent"]); const plan = compilePlan({ l4Flows: [l4Broken], - l3Blocks: [l3New, l3StaleUpdated, l3Drift], - l2Blocks: [l2Stale, l2Drift], - l1SignatureHashes: new Map([["drift-block", "new"]]), + l3Blocks: [l3New, l3StaleUpdated], + l2Blocks: [l2Stale], }); const compileTask = plan.tasks.find((t) => t.action === "compile"); @@ -635,9 +523,6 @@ describe("compilePlan — complexity", () => { const recompileTask = plan.tasks.find((t) => t.action === "recompile"); expect(recompileTask?.complexity).toBe("standard"); - const reviewTask = plan.tasks.find((t) => t.action === "review"); - expect(reviewTask?.complexity).toBe("standard"); - const updateRefTask = plan.tasks.find((t) => t.action === "update-ref"); expect(updateRefTask?.complexity).toBe("light"); }); @@ -649,21 +534,17 @@ describe("compilePlan — complexity", () => { const l2Stale = makeL2(l3StaleOrig); const l3StaleUpdated = makeL3("stale-block", "Stale Block", { description: "Changed" }); - const l3Drift = makeL3("drift-block", "Drift Block"); - const l2Drift: L2CodeBlock = { ...makeL2(l3Drift), signatureHash: "old" }; - const l4Broken = makeL4("broken-flow", ["nonexistent"]); const plan = compilePlan({ l4Flows: [l4Broken], - l3Blocks: [l3New, l3StaleUpdated, l3Drift], - l2Blocks: [l2Stale, l2Drift], - l1SignatureHashes: new Map([["drift-block", "new"]]), + l3Blocks: [l3New, l3StaleUpdated], + l2Blocks: [l2Stale], }); expect(plan.summary.complexityCounts).toEqual({ heavy: 0, - standard: 3, // compile + recompile + review + standard: 2, // compile + recompile light: 1, // update-ref }); }); diff --git a/packages/core/compile-plan.ts b/packages/core/compile-plan.ts index a18f077..65adf9e 100644 --- a/packages/core/compile-plan.ts +++ b/packages/core/compile-plan.ts @@ -63,7 +63,6 @@ export function compilePlan(input: CheckInput, language = "en"): CompilePlan { const tasks: CompileTask[] = [ ...detectMissingCompilations(input, lang), ...detectRecompilations(input, lang), - ...detectContentDrift(input, lang), ...detectBrokenRefs(input, lang), ]; @@ -147,45 +146,7 @@ function detectRecompilations(input: CheckInput, lang: string): CompileTask[] { }); } -// ── 3. 内容漂移:L1 导出签名变了,需要向上对账 ── - -function detectContentDrift(input: CheckInput, lang: string): CompileTask[] { - const report = check(input, lang); - const driftIssues = report.issues.filter((i) => i.code === "CONTENT_DRIFT"); - - return driftIssues.map((issue): CompileTask => { - const cb = input.l2Blocks.find((b) => b.id === issue.entityId); - const l3 = cb === undefined ? undefined : input.l3Blocks.find((b) => b.id === cb.blockRef); - - const context: ContextRef[] = []; - if (cb !== undefined) { - context.push({ - layer: "l2", - id: cb.id, - label: t(lang, "compilePlan.label.l2CodeBlock", { files: cb.files.join(", ") }), - }); - } - if (l3 !== undefined) { - context.push({ - layer: "l3", - id: l3.id, - label: t(lang, "compilePlan.label.l3Verify", { name: l3.name }), - }); - } - - return { - action: "review", - targetLayer: "l3", - targetId: l3?.id ?? issue.entityId, - reason: t(lang, "compilePlan.reason.contentDrift"), - issueCode: "CONTENT_DRIFT", - context, - complexity: "standard", - }; - }); -} - -// ── 4. 断裂引用:L4 引用了不存在的 L3 ── +// ── 3. 断裂引用:L4 引用了不存在的 L3 ── function detectBrokenRefs(input: CheckInput, lang: string): CompileTask[] { const report = check(input, lang); diff --git a/packages/core/extractors/typescript.ts b/packages/core/extractors/typescript.ts deleted file mode 100644 index fe30368..0000000 --- a/packages/core/extractors/typescript.ts +++ /dev/null @@ -1,85 +0,0 @@ -// TypeScript 签名提取器 -// 使用 TypeScript Compiler API 从 .ts 文件提取导出符号的签名 -// 这是唯一依赖 typescript 包的模块 - -import { - createProgram, - isClassDeclaration, - isEnumDeclaration, - isFunctionDeclaration, - isInterfaceDeclaration, - isTypeAliasDeclaration, - isVariableDeclaration, - ModuleKind, - ModuleResolutionKind, - ScriptTarget, - TypeFormatFlags, -} from "typescript"; -import type { ExportedSymbol, FileFingerprint, SignatureExtractor } from "../fingerprint.js"; -import type { Declaration } from "typescript"; - -/** 从 TS 符号类型映射到我们的 kind */ -function mapSymbolKind(declarations: readonly Declaration[] | undefined): ExportedSymbol["kind"] { - if (declarations === undefined || declarations.length === 0) return "variable"; - const decl = declarations[0]; - - if (isFunctionDeclaration(decl)) return "function"; - if (isClassDeclaration(decl)) return "class"; - if (isInterfaceDeclaration(decl)) return "interface"; - if (isTypeAliasDeclaration(decl)) return "type"; - if (isEnumDeclaration(decl)) return "enum"; - if (isVariableDeclaration(decl)) return "variable"; - - return "variable"; -} - -/** 从 TS 文件提取导出符号 */ -function extractExports(filePath: string): ExportedSymbol[] { - const program = createProgram([filePath], { - target: ScriptTarget.ESNext, - module: ModuleKind.ESNext, - moduleResolution: ModuleResolutionKind.Bundler, - strict: true, - skipLibCheck: true, - noEmit: true, - }); - - const sourceFile = program.getSourceFile(filePath); - if (sourceFile === undefined) return []; - - const checker = program.getTypeChecker(); - const moduleSymbol = checker.getSymbolAtLocation(sourceFile); - if (moduleSymbol === undefined) return []; - - const exports = checker.getExportsOfModule(moduleSymbol); - const result: ExportedSymbol[] = []; - - for (const sym of exports) { - const kind = mapSymbolKind(sym.declarations); - const type = checker.getTypeOfSymbol(sym); - const signature = checker.typeToString( - type, - sourceFile, - TypeFormatFlags.NoTruncation | TypeFormatFlags.WriteArrowStyleSignature, - ); - - result.push({ - name: sym.name, - kind, - signature, - }); - } - - return result; -} - -/** 创建 TypeScript 签名提取器 */ -export function createTypescriptExtractor(): SignatureExtractor { - return { - extract: (filePath: string): Promise => - Promise.resolve({ - filePath, - exports: extractExports(filePath), - }), - }; -} diff --git a/packages/core/fingerprint.test.ts b/packages/core/fingerprint.test.ts deleted file mode 100644 index 996f795..0000000 --- a/packages/core/fingerprint.test.ts +++ /dev/null @@ -1,238 +0,0 @@ -// fingerprint.ts + extractors/typescript.ts 测试 - -import { writeFile, mkdir, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { createTypescriptExtractor } from "./extractors/typescript.js"; -import { buildFingerprint, computeSignatureHash } from "./fingerprint.js"; -import type { FileFingerprint } from "./fingerprint.js"; - -// ── computeSignatureHash 纯函数测试 ── - -describe("computeSignatureHash", () => { - it("produces stable hash for same input", () => { - const files: FileFingerprint[] = [ - { - filePath: "src/a.ts", - exports: [ - { name: "foo", kind: "function", signature: "(x: number) => string" }, - { name: "Bar", kind: "interface", signature: "Bar" }, - ], - }, - ]; - - const hash1 = computeSignatureHash(files); - const hash2 = computeSignatureHash(files); - expect(hash1).toBe(hash2); - }); - - it("is order-independent for files", () => { - const fileA: FileFingerprint = { - filePath: "src/a.ts", - exports: [{ name: "foo", kind: "function", signature: "(x: number) => void" }], - }; - const fileB: FileFingerprint = { - filePath: "src/b.ts", - exports: [{ name: "bar", kind: "function", signature: "() => string" }], - }; - - const hash1 = computeSignatureHash([fileA, fileB]); - const hash2 = computeSignatureHash([fileB, fileA]); - expect(hash1).toBe(hash2); - }); - - it("is order-independent for exports within a file", () => { - const v1: FileFingerprint = { - filePath: "src/a.ts", - exports: [ - { name: "foo", kind: "function", signature: "() => void" }, - { name: "bar", kind: "function", signature: "() => string" }, - ], - }; - const v2: FileFingerprint = { - filePath: "src/a.ts", - exports: [ - { name: "bar", kind: "function", signature: "() => string" }, - { name: "foo", kind: "function", signature: "() => void" }, - ], - }; - - expect(computeSignatureHash([v1])).toBe(computeSignatureHash([v2])); - }); - - it("changes when a signature changes", () => { - const v1: FileFingerprint = { - filePath: "src/a.ts", - exports: [{ name: "foo", kind: "function", signature: "(x: number) => void" }], - }; - const v2: FileFingerprint = { - filePath: "src/a.ts", - exports: [{ name: "foo", kind: "function", signature: "(x: string) => void" }], - }; - - expect(computeSignatureHash([v1])).not.toBe(computeSignatureHash([v2])); - }); -}); - -describe("buildFingerprint", () => { - it("builds fingerprint with hash", () => { - const files: FileFingerprint[] = [ - { - filePath: "src/a.ts", - exports: [{ name: "foo", kind: "function", signature: "() => void" }], - }, - ]; - const fp = buildFingerprint(files); - expect(fp.files).toEqual(files); - expect(fp.hash).toBeTruthy(); - expect(fp.hash).toBe(computeSignatureHash(files)); - }); -}); - -// ── TypeScript 提取器集成测试 ── - -describe("createTypescriptExtractor", () => { - let testDir: string; - - beforeEach(async () => { - testDir = path.join( - tmpdir(), - `svp-fp-${String(Date.now())}-${String(Math.random()).slice(2, 8)}`, - ); - await mkdir(testDir, { recursive: true }); - }); - - afterEach(async () => { - await rm(testDir, { recursive: true, force: true }); - }); - - it("extracts exported function signatures", async () => { - const filePath = path.join(testDir, "module.ts"); - await writeFile( - filePath, - ` -export function greet(name: string): string { - return "hello " + name; -} - -export function add(a: number, b: number): number { - return a + b; -} - -// 内部函数,不应被提取 -function internal(): void {} -`, - "utf8", - ); - - const extractor = createTypescriptExtractor(); - const fp = await extractor.extract(filePath); - - expect(fp.exports).toHaveLength(2); - - const names = fp.exports.map((e) => e.name); - expect(names).toContain("greet"); - expect(names).toContain("add"); - expect(names).not.toContain("internal"); - - const greet = fp.exports.find((e) => e.name === "greet"); - expect(greet?.kind).toBe("function"); - expect(greet?.signature).toContain("string"); - }); - - it("extracts exported interfaces and types", async () => { - const filePath = path.join(testDir, "types.ts"); - await writeFile( - filePath, - ` -export interface User { - readonly id: string; - readonly name: string; -} - -export type Status = "active" | "inactive"; - -// 非导出不应出现 -interface Internal { - x: number; -} -`, - "utf8", - ); - - const extractor = createTypescriptExtractor(); - const fp = await extractor.extract(filePath); - - const names = fp.exports.map((e) => e.name); - expect(names).toContain("User"); - expect(names).toContain("Status"); - expect(names).not.toContain("Internal"); - }); - - it("same hash when only implementation changes", async () => { - const filePath = path.join(testDir, "calc.ts"); - - // 版本1:原始实现 - await writeFile(filePath, `export function calc(x: number): number { return x * 2; }`, "utf8"); - const extractor = createTypescriptExtractor(); - const fp1 = await extractor.extract(filePath); - - // 版本2:改了内部实现,签名不变 - await writeFile( - filePath, - `export function calc(x: number): number { return x + x; /* optimized */ }`, - "utf8", - ); - const fp2 = await extractor.extract(filePath); - - expect(computeSignatureHash([fp1])).toBe(computeSignatureHash([fp2])); - }); - - it("different hash when signature changes", async () => { - const filePath = path.join(testDir, "calc.ts"); - - // 版本1 - await writeFile(filePath, `export function calc(x: number): number { return x * 2; }`, "utf8"); - const extractor = createTypescriptExtractor(); - const fp1 = await extractor.extract(filePath); - - // 版本2:参数类型变了 - await writeFile(filePath, `export function calc(x: string): string { return x + x; }`, "utf8"); - const fp2 = await extractor.extract(filePath); - - expect(computeSignatureHash([fp1])).not.toBe(computeSignatureHash([fp2])); - }); - - it("same hash when comments and formatting change", async () => { - const filePath = path.join(testDir, "util.ts"); - - await writeFile( - filePath, - `export function hello(name: string): string { return name; }`, - "utf8", - ); - const extractor = createTypescriptExtractor(); - const fp1 = await extractor.extract(filePath); - - // 加了注释和空行 - await writeFile( - filePath, - ` -// This is a greeting function -// Added lots of comments - -export function hello( - name: string, -): string { - // internal comment - return name; -} -`, - "utf8", - ); - const fp2 = await extractor.extract(filePath); - - expect(computeSignatureHash([fp1])).toBe(computeSignatureHash([fp2])); - }); -}); diff --git a/packages/core/fingerprint.ts b/packages/core/fingerprint.ts deleted file mode 100644 index bcf807b..0000000 --- a/packages/core/fingerprint.ts +++ /dev/null @@ -1,58 +0,0 @@ -// 语义指纹 — 从 L1 源文件提取导出签名,用于接口级漂移检测 -// core 只定义数据结构和 hash 计算,不依赖 TS 编译器 -// 实际提取逻辑由上层(CLI/runtime)注入 - -import { createHash } from "node:crypto"; - -// ── 数据结构 ── - -/** 单个导出符号的签名 */ -export interface ExportedSymbol { - readonly name: string; - readonly kind: "function" | "class" | "interface" | "type" | "variable" | "enum"; - readonly signature: string; // 签名文本,如 "(req: Request) => Response" -} - -/** 一个文件的语义指纹 */ -export interface FileFingerprint { - readonly filePath: string; - readonly exports: readonly ExportedSymbol[]; -} - -/** 一组文件的聚合指纹 */ -export interface SignatureFingerprint { - readonly files: readonly FileFingerprint[]; - readonly hash: string; // 所有导出签名的聚合 hash -} - -// ── 从已提取的符号计算 hash ── - -/** 从 FileFingerprint 列表计算聚合 hash */ -export function computeSignatureHash(files: readonly FileFingerprint[]): string { - // 排序确保稳定性(文件顺序 + 导出顺序) - const normalized = [...files] - .toSorted((a, b) => a.filePath.localeCompare(b.filePath)) - .map((f) => ({ - filePath: f.filePath, - exports: [...f.exports].toSorted((a, b) => a.name.localeCompare(b.name)), - })); - - // 直接 JSON.stringify(不用 computeHash,因为它的 replacer 对数组不友好) - const json = JSON.stringify(normalized); - return createHash("sha256").update(json).digest("hex").slice(0, 16); -} - -/** 构建完整指纹对象 */ -export function buildFingerprint(files: readonly FileFingerprint[]): SignatureFingerprint { - return { - files, - hash: computeSignatureHash(files), - }; -} - -// ── 提取器接口(由上层实现) ── - -/** 签名提取器 — 从源文件路径提取导出符号 */ -export interface SignatureExtractor { - readonly extract: (filePath: string) => Promise; -} diff --git a/packages/core/hash.test.ts b/packages/core/hash.test.ts index a07415e..8b2df46 100644 --- a/packages/core/hash.test.ts +++ b/packages/core/hash.test.ts @@ -117,7 +117,6 @@ function makeL2CodeBlock(overrides?: Partial): L2CodeBlock { files: ["src/orders/validate.ts", "src/orders/types.ts"], sourceHash: "l3hash001", contentHash: "l2hash001", - signatureHash: "sighash001", revision: makeRevision(), ...overrides, }; @@ -638,7 +637,7 @@ describe("hashL5", () => { // ── hashL2 ───────────────────────────────────────────────────────────────── // // L2CodeBlock top-level keys (after stripping contentHash/sourceHash/revision): -// blockRef, files, id, language, signatureHash. +// blockRef, files, id, language. describe("hashL2", () => { it("hashes L2CodeBlock fields and returns 16-char hex", () => { @@ -680,19 +679,6 @@ describe("hashL2", () => { expect(h1).toBe(h2); }); - it("different signatureHash values produce different hash", () => { - const h1 = hashL2(makeL2CodeBlock({ signatureHash: "sig001" })); - const h2 = hashL2(makeL2CodeBlock({ signatureHash: "sig999" })); - expect(h1).not.toBe(h2); - }); - - it("signatureHash present vs absent changes hash", () => { - const withSig = makeL2CodeBlock({ signatureHash: "sig001" }); - // Construct without signatureHash by omitting it (not just undefined) - const { signatureHash: _, ...rest } = withSig; - expect(hashL2(withSig)).not.toBe(hashL2(rest as L2CodeBlock)); - }); - it("empty files array produces a valid hash", () => { const block = makeL2CodeBlock({ files: [] }); expect(hashL2(block)).toMatch(/^[0-9a-f]{16}$/); diff --git a/packages/core/hash.ts b/packages/core/hash.ts index 7489a02..bf74be9 100644 --- a/packages/core/hash.ts +++ b/packages/core/hash.ts @@ -55,7 +55,7 @@ export function hashL5(blueprint: Omit) return computeHash(blueprint as Record); } -/** L2CodeBlock 的 contentHash:基于 id, blockRef, language, files, signatureHash */ +/** L2CodeBlock 的 contentHash:基于 id, blockRef, language, files */ export function hashL2( codeBlock: Omit, ): string { diff --git a/packages/core/i18n.ts b/packages/core/i18n.ts index 16d40d6..12f82d5 100644 --- a/packages/core/i18n.ts +++ b/packages/core/i18n.ts @@ -96,8 +96,6 @@ const messages: Record> = { 'L4 "{egName}" handler "{handlerId}" dataFlow {direction} references undeclared state key "{field}"', "check.sourceDrift": 'L2 "{id}" sourceHash ({sourceHash}) does not match L3 "{blockRef}" contentHash ({l3Hash}): L3 has changed since last compilation', - "check.contentDrift": - 'L2 "{id}" signatureHash mismatch: L1 exported signatures have changed since last sync', "check.selfReferencingFlow": 'L4 "{flowName}" step "{stepId}" calls itself (recursive flow reference)', "check.duplicateEvent": 'L4 "{egName}" has duplicate event handler for "{event}"', @@ -121,8 +119,6 @@ const messages: Record> = { 'L3 block "{name}" has no corresponding L2 code block — needs initial compilation', "compilePlan.reason.sourceDrift": "L3 contract changed since last compilation — L2 code is stale", - "compilePlan.reason.contentDrift": - "L1 exported signatures changed — review whether L3 contract still matches the code", "compilePlan.reason.missingBlockRef": "Flow references missing L3 block — step needs updating or L3 needs recreating", "compilePlan.reason.missingL2BlockRef": @@ -243,7 +239,6 @@ const messages: Record> = { 'L4 "{egName}" 处理器 "{handlerId}" dataFlow {direction} 引用了未声明的 state key "{field}"', "check.sourceDrift": 'L2 "{id}" 的 sourceHash ({sourceHash}) 与 L3 "{blockRef}" 的 contentHash ({l3Hash}) 不匹配:L3 自上次编译以来已变更', - "check.contentDrift": 'L2 "{id}" signatureHash 不匹配:L1 导出签名自上次同步以来已变更', "check.selfReferencingFlow": 'L4 "{flowName}" 步骤 "{stepId}" 调用了自身(递归 flow 引用)', "check.duplicateEvent": 'L4 "{egName}" 存在重复的事件处理器 "{event}"', "check.emptyState": 'L4 "{egName}" event-graph 没有 state 声明', @@ -260,7 +255,6 @@ const messages: Record> = { // ── compilePlan.* ── "compilePlan.reason.missingL2": 'L3 block "{name}" 没有对应的 L2 code block — 需要初始编译', "compilePlan.reason.sourceDrift": "L3 契约自上次编译以来已变更 — L2 代码已过时", - "compilePlan.reason.contentDrift": "L1 导出签名已变更 — 请审查 L3 契约是否仍与代码匹配", "compilePlan.reason.missingBlockRef": "Flow 引用了缺失的 L3 block — 需要更新步骤或重建 L3", "compilePlan.reason.missingL2BlockRef": "L2 code block 引用了缺失的 L3 block — 孤立代码需要审查", diff --git a/packages/core/index.ts b/packages/core/index.ts index a37e468..daa5403 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -60,14 +60,6 @@ export type { Complexity, } from "./compile-plan.js"; export { getDefaultComplexity } from "./compile-plan.js"; -export type { - ExportedSymbol, - FileFingerprint, - SignatureFingerprint, - SignatureExtractor, -} from "./fingerprint.js"; -export { computeSignatureHash, buildFingerprint } from "./fingerprint.js"; -export { createTypescriptExtractor } from "./extractors/typescript.js"; export { compilePlan } from "./compile-plan.js"; export type { InitOptions, InitResult } from "./init.js"; export { init } from "./init.js"; diff --git a/packages/core/l2.ts b/packages/core/l2.ts index a0361b6..b23b7df 100644 --- a/packages/core/l2.ts +++ b/packages/core/l2.ts @@ -14,15 +14,9 @@ export interface L2CodeBlock { readonly sourceHash: string; // 生成时 L3 的 contentHash(L3 改了就不匹配 → 需要重编译) readonly contentHash: string; // 本层内容哈希(L2 自身元数据 hash) - // 语义指纹:L1 导出符号签名的 hash(接口级漂移检测) - // 只有导出签名变化才触发 drift,内部实现/注释/格式化不会 - // 可选字段:旧数据或首次创建时可能没有 - readonly signatureHash?: string; - // 版本追踪 readonly revision: ArtifactVersion; } // L1 就是文件系统上的源代码文件,不需要额外的数据模型。 -// L2 通过 files 字段引用它们,通过 signatureHash 做接口级漂移检测。 -// 格式化、注释、内部实现变更不会触发 drift,只有导出签名变化才会。 +// L2 通过 files 字段引用它们。 diff --git a/packages/core/scan.test.ts b/packages/core/scan.test.ts index 173d271..428fc7c 100644 --- a/packages/core/scan.test.ts +++ b/packages/core/scan.test.ts @@ -3,7 +3,6 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { collectScanContext } from "./scan.js"; -import type { SignatureExtractor } from "./fingerprint.js"; // ── Helpers ── @@ -22,15 +21,6 @@ async function makeTempProject(files: Record): Promise { return tempDir; } -// Stub extractor that returns exports based on file content -const stubExtractor: SignatureExtractor = { - extract: (filePath: string) => - Promise.resolve({ - filePath, - exports: [{ name: "stubExport", kind: "function" as const, signature: "() => void" }], - }), -}; - afterEach(async () => { const { rm } = await import("node:fs/promises"); await rm(tempDir, { recursive: true, force: true }).catch((error: unknown) => error); @@ -39,44 +29,35 @@ afterEach(async () => { // ── Tests ── describe("collectScanContext", () => { - it("collects .ts files with exports via extractor", async () => { + it("collects .ts files", async () => { const root = await makeTempProject({ "src/index.ts": "export function hello() {}", "src/utils.ts": "export const FOO = 1;", }); - const result = await collectScanContext({ root, dir: "src", maxFiles: 50 }, stubExtractor); + const result = await collectScanContext({ root, dir: "src", maxFiles: 50 }); expect(result.files).toHaveLength(2); expect(result.summary.totalFiles).toBe(2); - expect(result.summary.totalExports).toBe(2); expect(result.summary.truncated).toBe(false); - - // Both files should have stub exports - for (const f of result.files) { - expect(f.exports).toHaveLength(1); - expect(f.exports[0].name).toBe("stubExport"); - } }); - it("includes non-.ts files with empty exports for directory awareness", async () => { + it("includes non-.ts files for directory awareness", async () => { const root = await makeTempProject({ "src/config.json": "{}", "src/readme.md": "# Hello", "src/index.ts": "export const x = 1;", }); - const result = await collectScanContext({ root, dir: "src", maxFiles: 50 }, stubExtractor); + const result = await collectScanContext({ root, dir: "src", maxFiles: 50 }); expect(result.files).toHaveLength(3); const jsonFile = result.files.find((f) => f.filePath.endsWith(".json")); expect(jsonFile).toBeDefined(); - expect(jsonFile!.exports).toHaveLength(0); const tsFile = result.files.find((f) => f.filePath.endsWith(".ts")); expect(tsFile).toBeDefined(); - expect(tsFile!.exports).toHaveLength(1); }); it("respects maxFiles cap and sets truncated flag", async () => { @@ -87,7 +68,7 @@ describe("collectScanContext", () => { } const root = await makeTempProject(files); - const result = await collectScanContext({ root, dir: "src", maxFiles: 3 }, stubExtractor); + const result = await collectScanContext({ root, dir: "src", maxFiles: 3 }); expect(result.files).toHaveLength(3); expect(result.summary.totalFiles).toBe(3); @@ -102,7 +83,7 @@ describe("collectScanContext", () => { "src/build/output.js": "var y = 2;", }); - const result = await collectScanContext({ root, dir: "src", maxFiles: 50 }, stubExtractor); + const result = await collectScanContext({ root, dir: "src", maxFiles: 50 }); expect(result.files).toHaveLength(1); expect(result.files[0].filePath).toContain("index.ts"); @@ -116,7 +97,7 @@ describe("collectScanContext", () => { "src/utils.test.tsx": "test('renders', () => {});", }); - const result = await collectScanContext({ root, dir: "src", maxFiles: 50 }, stubExtractor); + const result = await collectScanContext({ root, dir: "src", maxFiles: 50 }); expect(result.files).toHaveLength(1); expect(result.files[0].filePath).toContain("handler.ts"); @@ -129,7 +110,7 @@ describe("collectScanContext", () => { "src/types.d.ts": "declare module 'foo' {}", }); - const result = await collectScanContext({ root, dir: "src", maxFiles: 50 }, stubExtractor); + const result = await collectScanContext({ root, dir: "src", maxFiles: 50 }); expect(result.files).toHaveLength(1); expect(result.files[0].filePath).not.toContain(".d.ts"); @@ -138,29 +119,10 @@ describe("collectScanContext", () => { it("returns empty when dir does not exist", async () => { const root = await makeTempProject({}); - const result = await collectScanContext( - { root, dir: "nonexistent", maxFiles: 50 }, - stubExtractor, - ); + const result = await collectScanContext({ root, dir: "nonexistent", maxFiles: 50 }); expect(result.files).toHaveLength(0); expect(result.summary.totalFiles).toBe(0); - expect(result.summary.totalExports).toBe(0); - }); - - it("works without extractor — all files get empty exports", async () => { - const root = await makeTempProject({ - "src/index.ts": "export const a = 1;", - "src/utils.ts": "export const b = 2;", - }); - - const result = await collectScanContext({ root, dir: "src", maxFiles: 50 }); - - expect(result.files).toHaveLength(2); - expect(result.summary.totalExports).toBe(0); - for (const f of result.files) { - expect(f.exports).toHaveLength(0); - } }); it("sorts files by path", async () => { @@ -170,7 +132,7 @@ describe("collectScanContext", () => { "src/m.ts": "", }); - const result = await collectScanContext({ root, dir: "src", maxFiles: 50 }, stubExtractor); + const result = await collectScanContext({ root, dir: "src", maxFiles: 50 }); const paths = result.files.map((f) => f.filePath); expect(paths).toEqual([...paths].toSorted()); diff --git a/packages/core/scan.ts b/packages/core/scan.ts index 92bcf37..6d9b385 100644 --- a/packages/core/scan.ts +++ b/packages/core/scan.ts @@ -1,10 +1,9 @@ // scan — Brownfield reverse generation context collector -// Walks existing codebase, extracts TS signatures, builds structured context +// Walks existing codebase, builds structured context // for AI prompts that reverse-engineer SVP artifacts from existing code import { readdir, stat } from "node:fs/promises"; import path from "node:path"; -import type { ExportedSymbol, FileFingerprint, SignatureExtractor } from "./fingerprint.js"; // ── Types ── @@ -16,14 +15,12 @@ export interface ScanOptions { export interface ScannedFile { readonly filePath: string; // relative to root - readonly exports: readonly ExportedSymbol[]; } export interface ScanContext { readonly files: readonly ScannedFile[]; readonly summary: { readonly totalFiles: number; - readonly totalExports: number; readonly truncated: boolean; }; } @@ -51,10 +48,6 @@ function shouldExcludeFile(name: string): boolean { return EXCLUDE_FILE_PATTERNS.some((re) => re.test(name)); } -function isTypeScriptFile(name: string): boolean { - return /\.[jt]sx?$/.test(name) && !name.endsWith(".d.ts"); -} - // ── Recursive file walker ── async function walkDir(dir: string, root: string): Promise { @@ -92,10 +85,7 @@ async function walkDir(dir: string, root: string): Promise { // ── Main collector ── /** Collect scan context from an existing codebase for reverse generation prompts */ -export async function collectScanContext( - options: ScanOptions, - extractor?: SignatureExtractor, -): Promise { +export async function collectScanContext(options: ScanOptions): Promise { const { root, dir, maxFiles } = options; const scanDir = path.resolve(root, dir); @@ -106,32 +96,12 @@ export async function collectScanContext( const truncated = allFiles.length > maxFiles; const filesToProcess = allFiles.slice(0, maxFiles); - const scannedFiles: ScannedFile[] = []; - let totalExports = 0; - - for (const filePath of filesToProcess) { - if (isTypeScriptFile(filePath) && extractor !== undefined) { - // Extract TS signatures - const absPath = path.resolve(root, filePath); - try { - const fp: FileFingerprint = await extractor.extract(absPath); - scannedFiles.push({ filePath, exports: fp.exports }); - totalExports += fp.exports.length; - } catch { - // If extraction fails, include path only - scannedFiles.push({ filePath, exports: [] }); - } - } else { - // Non-TS files: include path only for directory structure awareness - scannedFiles.push({ filePath, exports: [] }); - } - } + const scannedFiles: ScannedFile[] = filesToProcess.map((filePath) => ({ filePath })); return { files: scannedFiles, summary: { totalFiles: scannedFiles.length, - totalExports, truncated, }, }; diff --git a/packages/core/store.test.ts b/packages/core/store.test.ts index ea3615c..4b26d1e 100644 --- a/packages/core/store.test.ts +++ b/packages/core/store.test.ts @@ -468,23 +468,6 @@ describe("store L2", () => { expect(loaded?.language).toBe("python"); }); - it("L2 with signatureHash undefined", async () => { - const block = makeL2("no-sig-hash"); - // makeL2 does not set signatureHash, so it should be absent - expect(block.signatureHash).toBeUndefined(); - await writeL2(root, block); - const loaded = await readL2(root, "no-sig-hash"); - expect(loaded?.signatureHash).toBeUndefined(); - }); - - it("L2 with signatureHash defined", async () => { - const block = makeL2("with-sig-hash", { signatureHash: "sig-fingerprint-abc" }); - await writeL2(root, block); - const loaded = await readL2(root, "with-sig-hash"); - expect(loaded).toEqual(block); - expect(loaded?.signatureHash).toBe("sig-fingerprint-abc"); - }); - it("multiple L2 blocks", async () => { const multiRoot = await mkdtemp(path.join(tmpdir(), "svp-l2-multi-")); try { diff --git a/packages/skills/__tests__/link.test.ts b/packages/skills/__tests__/link.test.ts index c66f138..74c5a1f 100644 --- a/packages/skills/__tests__/link.test.ts +++ b/packages/skills/__tests__/link.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from "vitest"; import { hashL2 } from "../../core/hash.js"; import { createL2Link, relinkL2 } from "../link.js"; -import type { L2CodeBlock } from "../../core/l2.js"; import type { L3Block } from "../../core/l3.js"; const baseRevision = { @@ -109,18 +108,9 @@ describe("relinkL2", () => { expect(relinked.revision.source).toEqual({ type: "ai", action: "recompile" }); }); - it("preserves signatureHash from existing L2", () => { - const l3 = makeL3(); - const existing: L2CodeBlock = { - ...createL2Link({ l3Block: l3, files: ["src/old.ts"] }), - signatureHash: "sig-hash-123", - }; - const relinked = relinkL2(existing, l3, ["src/new.ts"]); - - expect(relinked.signatureHash).toBe("sig-hash-123"); - }); }); + // ── Additional tests ── describe("createL2Link — additional", () => { @@ -151,26 +141,6 @@ describe("createL2Link — additional", () => { }); describe("relinkL2 — additional", () => { - it("preserves existing signatureHash when it is undefined", () => { - const l3 = makeL3(); - const existing = createL2Link({ l3Block: l3, files: ["src/old.ts"] }); - // createL2Link does not set signatureHash — should be undefined - expect(existing.signatureHash).toBeUndefined(); - - const relinked = relinkL2(existing, l3, ["src/new.ts"]); - expect(relinked.signatureHash).toBeUndefined(); - }); - - it("preserves existing signatureHash when it is defined", () => { - const l3 = makeL3(); - const existing: L2CodeBlock = { - ...createL2Link({ l3Block: l3, files: ["src/old.ts"] }), - signatureHash: "sig-abc-999", - }; - const relinked = relinkL2(existing, l3, ["src/new.ts"]); - expect(relinked.signatureHash).toBe("sig-abc-999"); - }); - it("revision source is ai/recompile", () => { const l3 = makeL3(); const existing = createL2Link({ l3Block: l3, files: ["src/old.ts"] }); diff --git a/packages/skills/__tests__/scan-prompts.test.ts b/packages/skills/__tests__/scan-prompts.test.ts index b618b97..e48b5b2 100644 --- a/packages/skills/__tests__/scan-prompts.test.ts +++ b/packages/skills/__tests__/scan-prompts.test.ts @@ -14,21 +14,14 @@ const baseRevision = { }; const makeScanContext = ( - files: Array<{ - filePath: string; - exports?: Array<{ name: string; kind: string; signature: string }>; - }>, + files: Array<{ filePath: string }>, truncated = false, ): ScanContext => { - const scannedFiles = files.map((f) => ({ - filePath: f.filePath, - exports: (f.exports ?? []) as ScanContext["files"][number]["exports"], - })); + const scannedFiles = files.map((f) => ({ filePath: f.filePath })); return { files: scannedFiles, summary: { totalFiles: scannedFiles.length, - totalExports: scannedFiles.reduce((sum, f) => sum + f.exports.length, 0), truncated, }, }; @@ -65,12 +58,7 @@ const makeFlow = (id: string, blockRefs: string[]): L4Flow => ({ describe("buildScanL3Prompt", () => { it("produces valid markdown with complexity header", () => { - const ctx = makeScanContext([ - { - filePath: "src/index.ts", - exports: [{ name: "hello", kind: "function", signature: "() => void" }], - }, - ]); + const ctx = makeScanContext([{ filePath: "src/index.ts" }]); const result = buildScanL3Prompt({ scanContext: ctx }); @@ -78,21 +66,12 @@ describe("buildScanL3Prompt", () => { expect(result).toContain("# Reverse-Engineer L3 Contracts"); }); - it("includes scanned file tree with exports", () => { - const ctx = makeScanContext([ - { - filePath: "src/handler.ts", - exports: [ - { name: "handleRequest", kind: "function", signature: "(req: Request) => Response" }, - ], - }, - ]); + it("includes scanned file list", () => { + const ctx = makeScanContext([{ filePath: "src/handler.ts" }]); const result = buildScanL3Prompt({ scanContext: ctx }); expect(result).toContain("src/handler.ts"); - expect(result).toContain("handleRequest"); - expect(result).toContain("(req: Request) => Response"); }); it("includes user intent when provided", () => { @@ -137,22 +116,12 @@ describe("buildScanL3Prompt", () => { expect(result).toContain("truncated"); }); - it("includes summary line with file and export counts", () => { - const ctx = makeScanContext([ - { - filePath: "src/a.ts", - exports: [ - { name: "foo", kind: "function", signature: "() => void" }, - { name: "bar", kind: "variable", signature: "string" }, - ], - }, - { filePath: "src/b.ts" }, - ]); + it("includes summary line with file count", () => { + const ctx = makeScanContext([{ filePath: "src/a.ts" }, { filePath: "src/b.ts" }]); const result = buildScanL3Prompt({ scanContext: ctx }); expect(result).toContain("2 files"); - expect(result).toContain("2 exported symbols"); }); it("includes L3 schema example", () => { @@ -201,10 +170,7 @@ describe("buildScanL4Prompt", () => { }); it("includes code structure for import pattern analysis", () => { - const ctx = makeScanContext([ - { filePath: "src/order.ts", exports: [{ name: "Order", kind: "class", signature: "Order" }] }, - { filePath: "src/payment.ts" }, - ]); + const ctx = makeScanContext([{ filePath: "src/order.ts" }, { filePath: "src/payment.ts" }]); const blocks = [makeL3("order")]; const result = buildScanL4Prompt({ scanContext: ctx, l3Blocks: blocks }); diff --git a/packages/skills/adapters/shared.ts b/packages/skills/adapters/shared.ts index ec4a126..96a3c62 100644 --- a/packages/skills/adapters/shared.ts +++ b/packages/skills/adapters/shared.ts @@ -901,7 +901,7 @@ description → 描述中间(转换逻辑) **L4EventGraph**: \`{ kind: "event-graph", id, name, state: {key: {type, description}}, handlers: [{id, event, steps[], dataFlows[]}], contentHash, revision }\` **L4StateMachine**: \`{ kind: "state-machine", id, name, entity, initialState, states: {name: {onEntry?, onExit?}}, transitions: [{from, to, event, guard?}], contentHash, revision }\` **L3Block**: \`{ id, name, input: Pin[], output: Pin[], validate: {}, constraints[], description, contentHash, revision }\` -**L2CodeBlock**: \`{ id, blockRef, language, files[], sourceHash, contentHash, signatureHash?, revision }\` +**L2CodeBlock**: \`{ id, blockRef, language, files[], sourceHash, contentHash, revision }\` ### L4 变体选择指南 @@ -1076,7 +1076,7 @@ description → describes the MIDDLE (transformation logic) **L4EventGraph**: \`{ kind: "event-graph", id, name, state: {key: {type, description}}, handlers: [{id, event, steps[], dataFlows[]}], contentHash, revision }\` **L4StateMachine**: \`{ kind: "state-machine", id, name, entity, initialState, states: {name: {onEntry?, onExit?}}, transitions: [{from, to, event, guard?}], contentHash, revision }\` **L3Block**: \`{ id, name, input: Pin[], output: Pin[], validate: {}, constraints[], description, contentHash, revision }\` -**L2CodeBlock**: \`{ id, blockRef, language, files[], sourceHash, contentHash, signatureHash?, revision }\` +**L2CodeBlock**: \`{ id, blockRef, language, files[], sourceHash, contentHash, revision }\` ### L4 Variant Selection Guide diff --git a/packages/skills/link.ts b/packages/skills/link.ts index a43826d..f7b8c0c 100644 --- a/packages/skills/link.ts +++ b/packages/skills/link.ts @@ -55,7 +55,6 @@ export function relinkL2( ...base, sourceHash: l3Block.contentHash, contentHash, - signatureHash: existing.signatureHash, revision: { rev: existing.revision.rev + 1, parentRev: existing.revision.rev, diff --git a/packages/skills/prompts/scan.ts b/packages/skills/prompts/scan.ts index 5d2ee43..74f9ba4 100644 --- a/packages/skills/prompts/scan.ts +++ b/packages/skills/prompts/scan.ts @@ -13,14 +13,7 @@ import type { ScanContext } from "../../core/scan.js"; function formatFileTree(ctx: ScanContext): string { const lines: string[] = []; for (const f of ctx.files) { - if (f.exports.length === 0) { - lines.push(`- ${f.filePath}`); - } else { - lines.push(`- ${f.filePath}`); - for (const exp of f.exports) { - lines.push(` ${exp.kind} ${exp.name}: ${exp.signature}`); - } - } + lines.push(`- ${f.filePath}`); } if (ctx.summary.truncated) { lines.push(` ... (truncated to ${String(ctx.summary.totalFiles)} files)`); @@ -29,7 +22,7 @@ function formatFileTree(ctx: ScanContext): string { } function summaryLine(ctx: ScanContext): string { - return `${String(ctx.summary.totalFiles)} files, ${String(ctx.summary.totalExports)} exported symbols${ctx.summary.truncated ? " (truncated)" : ""}`; + return `${String(ctx.summary.totalFiles)} files${ctx.summary.truncated ? " (truncated)" : ""}`; } // ── Phase 1: L1 → L3 ── @@ -70,7 +63,7 @@ export function buildScanL3Prompt(input: ScanL3Input): string { "# Reverse-Engineer L3 Contracts from Existing Code", "", "You are analyzing an existing codebase to extract L3 contract blocks for SVP.", - "L3 blocks are the interface specifications — each groups related exports into a logical unit.", + "L3 blocks are the interface specifications — each groups related files into a logical unit.", "", ...(input.userIntent === undefined ? [] : ["## System Intent", "", input.userIntent, ""]), "## Scanned Codebase", @@ -83,11 +76,11 @@ export function buildScanL3Prompt(input: ScanL3Input): string { "", "## Instructions", "", - "Analyze the scanned code and group related exports into logical L3 blocks.", + "Analyze the scanned code and group related files into logical L3 blocks.", "Each block represents one cohesive responsibility unit.", "", "For each block:", - "1. **Identify cohesion**: Group exports that work together (same domain, shared types)", + "1. **Identify cohesion**: Group files that work together (same domain, shared types)", "2. **Infer input pins**: From function parameters and imported types", "3. **Infer output pins**: From return types", "4. **Infer validate rules**: From parameter constraints visible in signatures", @@ -102,7 +95,7 @@ export function buildScanL3Prompt(input: ScanL3Input): string { "", "## Grouping Guidelines", "", - "- One file with multiple related exports → usually one L3 block", + "- One file with multiple related functions → usually one L3 block", "- Multiple files sharing a domain concept → consider one L3 block", "- A single large class → may split into multiple L3 blocks by responsibility", "- Utility/helper files → may group into a shared utility block or skip", From aca50153520eeb81c8e193c540e0da4ba3abd530 Mon Sep 17 00:00:00 2001 From: xycld Date: Sat, 21 Mar 2026 16:45:45 +0800 Subject: [PATCH 2/2] style: fix prettier formatting --- packages/cli/load.ts | 9 +-------- packages/core/check.test.ts | 2 -- packages/core/check.ts | 1 - packages/core/compile-plan.test.ts | 1 - packages/skills/__tests__/link.test.ts | 2 -- packages/skills/__tests__/scan-prompts.test.ts | 5 +---- 6 files changed, 2 insertions(+), 18 deletions(-) diff --git a/packages/cli/load.ts b/packages/cli/load.ts index 181c646..b6417fe 100644 --- a/packages/cli/load.ts +++ b/packages/cli/load.ts @@ -13,12 +13,7 @@ import { readL5, checkCompatibility, } from "../core/index.js"; -import type { - CheckInput, - L2CodeBlock, - L3Block, - L4Artifact, -} from "../core/index.js"; +import type { CheckInput, L2CodeBlock, L3Block, L4Artifact } from "../core/index.js"; /** 从 .svp/ 加载所有层数据 */ export async function loadCheckInput(root: string): Promise { @@ -73,5 +68,3 @@ async function scanExistingNodeDocs(root: string): Promise> { } return result; } - - diff --git a/packages/core/check.test.ts b/packages/core/check.test.ts index f6e73af..783e3af 100644 --- a/packages/core/check.test.ts +++ b/packages/core/check.test.ts @@ -326,7 +326,6 @@ describe("check — graph structure", () => { }); }); - // ── EventGraph 校验 ── function makeEventGraph(overrides: Partial = {}): L4EventGraph { @@ -791,7 +790,6 @@ describe("check — empty steps", () => { }); }); - describe("check — SOURCE_DRIFT edge cases", () => { it("skips SOURCE_DRIFT when L3 not found for blockRef", () => { // L2 references a blockRef that does not exist in l3Blocks diff --git a/packages/core/check.ts b/packages/core/check.ts index b75afc8..b35ada6 100644 --- a/packages/core/check.ts +++ b/packages/core/check.ts @@ -609,7 +609,6 @@ function checkDrift(input: CheckInput, lang: string): CheckIssue[] { }), }); } - } return issues; diff --git a/packages/core/compile-plan.test.ts b/packages/core/compile-plan.test.ts index 3d6e1bd..9f9d988 100644 --- a/packages/core/compile-plan.test.ts +++ b/packages/core/compile-plan.test.ts @@ -346,7 +346,6 @@ describe("compilePlan — recompile context", () => { }); }); - describe("compilePlan — orphaned L2", () => { it("generates review task for L2 with MISSING_BLOCK_REF", () => { const ghostL3 = makeL3("deleted-block", "Deleted Block"); diff --git a/packages/skills/__tests__/link.test.ts b/packages/skills/__tests__/link.test.ts index 74c5a1f..e8b1e24 100644 --- a/packages/skills/__tests__/link.test.ts +++ b/packages/skills/__tests__/link.test.ts @@ -107,10 +107,8 @@ describe("relinkL2", () => { expect(relinked.revision.parentRev).toBe(1); expect(relinked.revision.source).toEqual({ type: "ai", action: "recompile" }); }); - }); - // ── Additional tests ── describe("createL2Link — additional", () => { diff --git a/packages/skills/__tests__/scan-prompts.test.ts b/packages/skills/__tests__/scan-prompts.test.ts index e48b5b2..65f5682 100644 --- a/packages/skills/__tests__/scan-prompts.test.ts +++ b/packages/skills/__tests__/scan-prompts.test.ts @@ -13,10 +13,7 @@ const baseRevision = { timestamp: "2024-01-01T00:00:00Z", }; -const makeScanContext = ( - files: Array<{ filePath: string }>, - truncated = false, -): ScanContext => { +const makeScanContext = (files: Array<{ filePath: string }>, truncated = false): ScanContext => { const scannedFiles = files.map((f) => ({ filePath: f.filePath })); return { files: scannedFiles,