Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -46,7 +47,6 @@
},
"dependencies": {
"@inquirer/prompts": "^8.3.2",
"commander": "^14.0.3",
"typescript": "^5.9.3"
"commander": "^14.0.3"
}
}
8 changes: 3 additions & 5 deletions packages/cli/commands/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
17 changes: 0 additions & 17 deletions packages/cli/load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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
Expand Down
67 changes: 4 additions & 63 deletions packages/cli/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,11 @@ import {
readL4,
readL5,
checkCompatibility,
computeSignatureHash,
createTypescriptExtractor,
} from "../core/index.js";
import type {
CheckInput,
L2CodeBlock,
L3Block,
L4Artifact,
FileFingerprint,
} from "../core/index.js";
import type { CheckInput, L2CodeBlock, L3Block, L4Artifact } from "../core/index.js";

/** 从 .svp/ 加载所有层数据,含可选的 L1 签名计算 */
export async function loadCheckInput(
root: string,
options: { computeSignatures?: boolean } = {},
): Promise<CheckInput> {
/** 从 .svp/ 加载所有层数据 */
export async function loadCheckInput(root: string): Promise<CheckInput> {
// Ensure .svp/ schema is compatible before reading
await checkCompatibility(root);

Expand All @@ -54,16 +43,10 @@ export async function loadCheckInput(
if (cb !== null) l2Blocks.push(cb);
}

// 计算 L1 签名哈希(可选,需要 L2 blocks 有 signatureHash 且文件存在)
let l1SignatureHashes: Map<string, string> | 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 */
Expand All @@ -85,45 +68,3 @@ async function scanExistingNodeDocs(root: string): Promise<Set<string>> {
}
return result;
}

/** 遍历 L2 blocks,提取 L1 文件的导出签名并计算聚合 hash */
async function computeL1Signatures(
root: string,
l2Blocks: readonly L2CodeBlock[],
): Promise<Map<string, string>> {
const extractor = createTypescriptExtractor();
const hashes = new Map<string, string>();

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;
}
83 changes: 0 additions & 83 deletions packages/core/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,71 +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 校验 ──

function makeEventGraph(overrides: Partial<L4EventGraph> = {}): L4EventGraph {
Expand Down Expand Up @@ -855,24 +790,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", () => {
// L2 references a blockRef that does not exist in l3Blocks
Expand Down
19 changes: 0 additions & 19 deletions packages/core/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;

// 已有文档的 L3 block id 集合(nodes/<id>/docs.md 存在的)
// 省略时跳过 MISSING_NODE_DOCS 检测
readonly existingNodeDocs?: ReadonlySet<string>;
Expand Down Expand Up @@ -614,20 +609,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;
Expand Down
Loading
Loading