@guanlin4924/devflow-pi 0.1.62 → 0.1.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { InitializationStore } from "./initialization-store.ts";
6
+ import type { ProjectContext } from "./project-context.ts";
7
+
8
+ function hash(content: Uint8Array | string): string { return `sha256:${createHash("sha256").update(content).digest("hex")}`; }
9
+ function relativeSource(context: ProjectContext, source: string): string {
10
+ const absolute = path.resolve(context.root, source); const relative = path.relative(context.root, absolute).replaceAll("\\", "/");
11
+ if (!relative || relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) throw new Error(`Legacy source escapes project root: ${source}`);
12
+ return relative;
13
+ }
14
+
15
+ export async function importLegacyInitialization(context: ProjectContext, sources: string[], requestId = `legacy-import-${randomUUID()}`): Promise<{ runId: string; snapshotId: string; imports: string[] }> {
16
+ if (!sources.length) throw new Error("At least one legacy source is required");
17
+ const documents = await Promise.all(sources.map(async source => {
18
+ const relative = relativeSource(context, source); const content = await readFile(path.join(context.root, relative));
19
+ return { relative, contentHash: hash(content), size: content.byteLength };
20
+ }));
21
+ const store = await InitializationStore.open(context);
22
+ try {
23
+ const workspace = await store.registerWorkspace();
24
+ const config = store.putScanConfig({ mode: "fast", repositories: [{ name: "legacy-materials", relativePath: ".", role: "other" }], requiredKinds: ["evidence"], adapterVersions: { "legacy-import": "1" } });
25
+ const input = store.createInputSnapshot(workspace.id, config.id, documents.map(item => ({ repository: "legacy-materials", path: item.relative, contentHash: item.contentHash, size: item.size })), true);
26
+ const run = store.startRun({ workspaceId: workspace.id, configId: config.id, inputSnapshotId: input.id, requestId });
27
+ if (!run.reused) store.recordCollection(run.id, run.leaseToken, {
28
+ kind: "evidence", status: "complete", adapter: "legacy-import@1",
29
+ facts: documents.map(item => ({ kind: "evidence" as const, objectKey: `historical:${item.relative}`, value: { sourcePath: item.relative, contentHash: item.contentHash, materialType: path.extname(item.relative).slice(1) || "text", importedAs: "historical", observed: false, approved: false }, evidenceLevel: "historical" as const, sourcePath: item.relative, sourceFingerprint: item.contentHash, extractor: "legacy-import@1", confidence: "exact" as const })),
30
+ });
31
+ const snapshot = run.reused && store.getRun(run.id)?.publishedSnapshotId
32
+ ? { snapshotId: String(store.getRun(run.id)!.publishedSnapshotId) }
33
+ : store.publishRun(run.id, run.leaseToken, { completeness: "partial", updateCurrent: false });
34
+ const imports = documents.map(item => store.registerHistoricalImport(workspace.id, item.relative, item.contentHash, snapshot.snapshotId));
35
+ const authority = store.getAuthority();
36
+ if (authority.authority === "legacy") store.switchAuthority("shadow", authority.revision);
37
+ return { runId: run.id, snapshotId: snapshot.snapshotId, imports };
38
+ } finally { store.close(); }
39
+ }
40
+
41
+ export async function exportInitializationSnapshot(context: ProjectContext, snapshotId: string, targetPath: string): Promise<{ path: string; contentHash: string }> {
42
+ const absolute = path.resolve(context.root, targetPath);
43
+ if (absolute !== context.root && !absolute.startsWith(`${context.root}${path.sep}`)) throw new Error("Export path escapes project root");
44
+ const store = await InitializationStore.open(context);
45
+ try {
46
+ const facts: unknown[] = []; let after: string | undefined; let snapshot: unknown;
47
+ do {
48
+ const page = store.queryContext({ snapshotId, after, limit: 500 }) as any; snapshot ??= page.snapshot; facts.push(...page.facts); after = page.nextCursor ?? undefined;
49
+ if (facts.length > 100_000) throw new Error("Snapshot export exceeds 100,000 facts");
50
+ } while (after);
51
+ const content = `${JSON.stringify({ version: 1, exportedAt: new Date().toISOString(), snapshot, facts }, null, 2)}\n`;
52
+ await mkdir(path.dirname(absolute), { recursive: true }); const temporary = `${absolute}.${randomUUID()}.tmp`;
53
+ await writeFile(temporary, content, { encoding: "utf8", flag: "wx" }); await rename(temporary, absolute);
54
+ return { path: absolute, contentHash: hash(content) };
55
+ } finally { store.close(); }
56
+ }
57
+
58
+ export async function switchInitializationAuthority(context: ProjectContext, authority: "legacy" | "shadow" | "database", expectedRevision: number, options: { compatibilityExport?: string } = {}): Promise<{ authority: string; revision: number }> {
59
+ const store = await InitializationStore.open(context);
60
+ try {
61
+ const current = store.getAuthority();
62
+ if (current.authority === "database" && authority === "legacy" && !options.compatibilityExport) throw new Error("Rollback to legacy authority requires a verified compatibility export");
63
+ if (options.compatibilityExport) await readFile(path.resolve(context.root, options.compatibilityExport));
64
+ return store.switchAuthority(authority, expectedRevision);
65
+ } finally { store.close(); }
66
+ }