@guanlin4924/devflow-pi 0.1.69 → 0.1.71
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.
- package/package.json +1 -1
- package/plugins/devflow-core/skills/workflow-project-context/SKILL.md +2 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/python_ast.py +2 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +8 -3
- package/src/constants.ts +1 -1
- package/src/initialization-store.ts +93 -19
- package/src/project-context.ts +7 -3
- package/src/project-documents.ts +6 -1
- package/src/project-initializer.ts +68 -9
- package/src/project-migration.ts +3 -2
package/package.json
CHANGED
|
@@ -209,6 +209,8 @@ Run the database validator with identifiers returned by the scan:
|
|
|
209
209
|
devflow_skill_script(script="project-context-db-validate", args=["--database", "<control.db>", "--project-id", "<project-id>", "--workspace-id", "<workspace-id>", "--run-id", "<run-id>"])
|
|
210
210
|
```
|
|
211
211
|
|
|
212
|
+
The `--database` value must come from `devflow_project_context(action="scan").database` or the loaded project context `controlDatabasePath`. Never guess `./control.db`, `.devflow/control.db`, `.planning/code-index/index.sqlite`, an OMP agent database, or any SQLite found by searching user directories. Code-index SQLite is not the initialization control database and does not contain the initialization `schema_meta` table.
|
|
213
|
+
|
|
212
214
|
Completion requires a stable immutable input, matching context snapshot, accounted required categories, evidence for observed facts, a registered immutable index in full mode, revision-safe pointer publication, and separate document statuses. Workflow `STATE` and other non-initialization authorities must remain unchanged.
|
|
213
215
|
|
|
214
216
|
Report partial, unsupported, skipped, failed, and unknown coverage honestly. Initialization success does not mean tests passed, services ran, designs were approved, or requirements were delivered.
|
|
@@ -116,6 +116,8 @@ class _PythonVisitor(ast.NodeVisitor):
|
|
|
116
116
|
|
|
117
117
|
@staticmethod
|
|
118
118
|
def _signature(node):
|
|
119
|
+
if isinstance(node, ast.ClassDef):
|
|
120
|
+
return f"{node.name}()"
|
|
119
121
|
arguments = [argument.arg for argument in [*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs]]
|
|
120
122
|
if node.args.vararg:
|
|
121
123
|
arguments.append(f"*{node.args.vararg.arg}")
|
|
@@ -97,13 +97,15 @@ def _publish_snapshot(staging: Path, output: Path, counts: Dict[str, int], works
|
|
|
97
97
|
os.close(directory_fd)
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
def _workspace_state(root: Path, files: List[dict], changed: Set[str]) -> Dict[str, object]:
|
|
100
|
+
def _workspace_state(root: Path, files: List[dict], changed: Set[str], discovered_paths: Iterable[str] | None = None) -> Dict[str, object]:
|
|
101
101
|
indexed_paths = sorted(record["path"] for record in files)
|
|
102
|
+
discovered = sorted(set(discovered_paths or indexed_paths))
|
|
102
103
|
source_manifest_hash = _hash_lines(f"{record['path']}\0{record['content_hash']}" for record in files)
|
|
103
104
|
return {
|
|
104
105
|
"repositoryRevision": _git_output(root, ["rev-parse", "HEAD"]),
|
|
105
106
|
"contentManifestHash": source_manifest_hash,
|
|
106
107
|
"indexedPathsHash": _hash_lines(indexed_paths),
|
|
108
|
+
"discoveredPathsHash": _hash_lines(discovered),
|
|
107
109
|
"dirtyPathsAtBuild": sorted(changed),
|
|
108
110
|
"extractors": sorted(set(record.get("parser", "unknown") for record in files)),
|
|
109
111
|
"capabilities": {
|
|
@@ -210,12 +212,14 @@ def command_scan(args) -> int:
|
|
|
210
212
|
|
|
211
213
|
files, symbols, relations, diagnostics = _existing_after_removal(existing, changed)
|
|
212
214
|
indexed_paths = {record["path"] for record in files}
|
|
215
|
+
discovered_paths: Set[str] = set()
|
|
213
216
|
|
|
214
217
|
for codebase in codebases:
|
|
215
218
|
if not codebase.root.exists():
|
|
216
219
|
diagnostics.append(_diagnostic(codebase.name, "", 1, "warning", "skipped_file", f"codebase root does not exist: {codebase.root}", "discovery@1"))
|
|
217
220
|
continue
|
|
218
221
|
all_files = scan_files(codebase.root, excludes=args.exclude)
|
|
222
|
+
discovered_paths.update(_workspace_rel(root, path) for path in all_files)
|
|
219
223
|
if changed:
|
|
220
224
|
all_files = [path for path in all_files if _workspace_rel(root, path) in changed]
|
|
221
225
|
manifests = [path for path in all_files if path.name in {"package.json", "go.mod", "Cargo.toml", "pyproject.toml", "requirements.txt", "composer.json", "pubspec.yaml"}]
|
|
@@ -280,7 +284,7 @@ def command_scan(args) -> int:
|
|
|
280
284
|
"relations": len(relations),
|
|
281
285
|
"diagnostics": len(diagnostics),
|
|
282
286
|
},
|
|
283
|
-
_workspace_state(root, files, changed),
|
|
287
|
+
_workspace_state(root, files, changed, discovered_paths),
|
|
284
288
|
)
|
|
285
289
|
finally:
|
|
286
290
|
shutil.rmtree(staging, ignore_errors=True)
|
|
@@ -406,7 +410,8 @@ def _query_is_fresh(root: Path, snapshot: Dict[str, object], files: List[dict])
|
|
|
406
410
|
indexed_paths = sorted(record["path"] for record in files)
|
|
407
411
|
if not any(path.startswith("../") for path in indexed_paths):
|
|
408
412
|
current_paths = sorted(_workspace_rel(root, item) for item in scan_files(root))
|
|
409
|
-
|
|
413
|
+
expected_paths_hash = workspace.get("discoveredPathsHash") or workspace.get("indexedPathsHash")
|
|
414
|
+
if _hash_lines(current_paths) != expected_paths_hash:
|
|
410
415
|
return False
|
|
411
416
|
return True
|
|
412
417
|
|
package/src/constants.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { copyFile, cp, mkdir, readFile, realpath, stat, writeFile, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { Database } from "bun:sqlite";
|
|
5
6
|
|
|
@@ -263,12 +264,33 @@ export class InitializationStore {
|
|
|
263
264
|
await writeFile(migrationBackup, db.serialize(), { flag: "wx" });
|
|
264
265
|
}
|
|
265
266
|
}
|
|
266
|
-
db.transaction(() => {
|
|
267
|
+
const migrate = db.transaction(() => {
|
|
267
268
|
db.exec(SCHEMA);
|
|
269
|
+
const ensureColumn = (table: string, column: string, definition: string) => {
|
|
270
|
+
const columns = db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
271
|
+
if (!columns.some(item => item.name === column)) db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
272
|
+
};
|
|
273
|
+
ensureColumn("context_snapshots", "index_ref_json", "TEXT");
|
|
274
|
+
ensureColumn("document_jobs", "model_role", "TEXT");
|
|
275
|
+
ensureColumn("document_jobs", "output_hash", "TEXT");
|
|
276
|
+
ensureColumn("document_jobs", "conflict_path", "TEXT");
|
|
277
|
+
ensureColumn("document_jobs", "error", "TEXT");
|
|
268
278
|
const meta = db.query("SELECT version FROM schema_meta LIMIT 1").get() as { version: number } | null;
|
|
269
|
-
if (!meta)
|
|
270
|
-
|
|
271
|
-
|
|
279
|
+
if (!meta) {
|
|
280
|
+
db.query("INSERT INTO schema_meta(version) VALUES (?)").run(INITIALIZATION_SCHEMA_VERSION);
|
|
281
|
+
} else {
|
|
282
|
+
let version = meta.version;
|
|
283
|
+
if (version < 2) {
|
|
284
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_input_snapshots_project ON input_snapshots(project_id, workspace_id, config_id)");
|
|
285
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_document_jobs_status ON document_jobs(status, updated_at)");
|
|
286
|
+
version = 2;
|
|
287
|
+
}
|
|
288
|
+
db.query("UPDATE schema_meta SET version=?").run(version);
|
|
289
|
+
}
|
|
290
|
+
const verified = db.query("SELECT version FROM schema_meta LIMIT 1").get() as { version: number };
|
|
291
|
+
if (verified.version !== INITIALIZATION_SCHEMA_VERSION) throw new Error(`Initialization schema migration stopped at ${verified.version}`);
|
|
292
|
+
});
|
|
293
|
+
try { migrate(); } catch (error) { db.close(); throw new Error(`Initialization database migration failed: ${error instanceof Error ? error.message : String(error)}`); }
|
|
272
294
|
db.query("INSERT OR IGNORE INTO projects(id,name,created_at) VALUES (?,?,?)").run(context.manifest.projectId, context.manifest.name, now());
|
|
273
295
|
db.query("INSERT OR IGNORE INTO authority_state(project_id,authority,revision,switched_at) VALUES (?,?,?,?)").run(context.manifest.projectId, "legacy", 0, now());
|
|
274
296
|
return new InitializationStore(context, databasePath, db);
|
|
@@ -311,13 +333,23 @@ export class InitializationStore {
|
|
|
311
333
|
return { id, revision: row.revision, created: true };
|
|
312
334
|
}
|
|
313
335
|
|
|
314
|
-
createInputSnapshot(workspaceId: string, configId: string, files: Array<{ repository: string; path: string; contentHash: string; size?: number }>, stable =
|
|
336
|
+
createInputSnapshot(workspaceId: string, configId: string, files: Array<{ repository: string; path: string; contentHash: string; size?: number }>, stable = false): { id: string; fingerprint: string; created: boolean } {
|
|
337
|
+
const binding = this.db.query(`SELECT w.id AS workspaceId,c.id AS configId,w.project_id AS workspaceProject,c.project_id AS configProject,c.config_json AS config
|
|
338
|
+
FROM workspaces w JOIN scan_configs c ON c.project_id=w.project_id
|
|
339
|
+
WHERE w.id=? AND c.id=? AND w.project_id=?`).get(workspaceId, configId, this.context.manifest.projectId) as { workspaceId: string; configId: string; workspaceProject: string; configProject: string; config: string } | null;
|
|
340
|
+
if (!binding) throw new Error("Input snapshot workspace and scan config do not belong to the same project boundary");
|
|
341
|
+
const configured = new Map((parse<ScanConfigInput>(binding.config).repositories ?? []).map(repository => [repository.name, repository.relativePath]));
|
|
342
|
+
for (const file of files) {
|
|
343
|
+
if (!configured.has(file.repository)) throw new Error(`Input snapshot repository is not configured: ${file.repository}`);
|
|
344
|
+
const base = configured.get(file.repository)!;
|
|
345
|
+
if (base !== "." && !path.isAbsolute(base) && file.path !== base && !file.path.startsWith(`${base.replace(/\\/g, "/").replace(/\/$/, "")}/`)) throw new Error(`Input snapshot path is outside repository: ${file.repository}/${file.path}`);
|
|
346
|
+
}
|
|
315
347
|
const normalized = files.map(file => ({ ...file, path: validateRelative(file.path) })).sort((a, b) => `${a.repository}/${a.path}`.localeCompare(`${b.repository}/${b.path}`));
|
|
316
348
|
const manifestJson = canonicalJson(normalized);
|
|
317
349
|
const fingerprint = hash(manifestJson);
|
|
318
350
|
const existing = this.db.query("SELECT id,stable FROM input_snapshots WHERE workspace_id=? AND config_id=? AND fingerprint=?").get(workspaceId, configId, fingerprint) as { id: string; stable: number } | null;
|
|
319
351
|
if (existing) {
|
|
320
|
-
if (stable && !existing.stable)
|
|
352
|
+
if (stable && !existing.stable) throw new Error("Existing input snapshot is unstable; confirm it explicitly before publishing");
|
|
321
353
|
return { id: existing.id, fingerprint, created: false };
|
|
322
354
|
}
|
|
323
355
|
const id = randomUUID();
|
|
@@ -369,27 +401,39 @@ export class InitializationStore {
|
|
|
369
401
|
recordCollection(runId: string, leaseToken: string, input: { repository?: string; kind: FactKind; status: CoverageStatus; adapter: string; capability?: Record<string, unknown>; diagnostics?: unknown[]; facts?: FactInput[] }): void {
|
|
370
402
|
this.assertWritableRun(runId, leaseToken);
|
|
371
403
|
const transaction = this.db.transaction(() => {
|
|
372
|
-
|
|
404
|
+
const repository = input.repository ?? "";
|
|
405
|
+
this.db.query("DELETE FROM evidence WHERE fact_id IN (SELECT id FROM facts WHERE run_id=? AND repository=? AND kind=?)").run(runId, repository, input.kind);
|
|
406
|
+
this.db.query("DELETE FROM facts WHERE run_id=? AND repository=? AND kind=?").run(runId, repository, input.kind);
|
|
407
|
+
this.db.query("INSERT INTO collection_results(id,run_id,repository,kind,status,adapter,capability_json,diagnostics_json,created_at) VALUES (?,?,?,?,?,?,?,?,?) ON CONFLICT(run_id,repository,kind) DO UPDATE SET status=excluded.status,adapter=excluded.adapter,capability_json=excluded.capability_json,diagnostics_json=excluded.diagnostics_json,created_at=excluded.created_at").run(randomUUID(), runId, repository, input.kind, input.status, input.adapter, canonicalJson(input.capability ?? {}), canonicalJson(input.diagnostics ?? []), now());
|
|
373
408
|
for (const fact of input.facts ?? []) {
|
|
374
409
|
if (fact.kind !== input.kind) throw new Error(`Fact kind ${fact.kind} does not match collection ${input.kind}`);
|
|
375
410
|
if (!fact.objectKey || !fact.extractor) throw new Error("Facts require objectKey and extractor");
|
|
376
411
|
if (fact.evidenceLevel === "observed" && !fact.sourcePath) throw new Error("Observed facts require source evidence");
|
|
377
412
|
validateFactShape(fact);
|
|
378
413
|
const factId = randomUUID();
|
|
379
|
-
this.db.query("INSERT INTO facts(id,run_id,kind,object_key,repository,value_json,evidence_level,extractor,confidence,created_at) VALUES (?,?,?,?,?,?,?,?,?,?)
|
|
380
|
-
const stored =
|
|
414
|
+
this.db.query("INSERT INTO facts(id,run_id,kind,object_key,repository,value_json,evidence_level,extractor,confidence,created_at) VALUES (?,?,?,?,?,?,?,?,?,?)").run(factId, runId, fact.kind, fact.objectKey, fact.repository ?? repository, canonicalJson(fact.value), fact.evidenceLevel, fact.extractor, fact.confidence ?? "inferred", now());
|
|
415
|
+
const stored = { id: factId };
|
|
381
416
|
if (fact.sourcePath) this.db.query("INSERT OR IGNORE INTO evidence(id,fact_id,source_path,source_line,source_fingerprint,method,created_at) VALUES (?,?,?,?,?,?,?)").run(randomUUID(), stored.id, validateRelative(fact.sourcePath), fact.sourceLine ?? null, fact.sourceFingerprint ?? "unavailable", fact.extractor, now());
|
|
382
417
|
}
|
|
383
418
|
});
|
|
384
419
|
transaction();
|
|
385
420
|
}
|
|
386
421
|
|
|
387
|
-
cancelRun(runId: string): boolean {
|
|
388
|
-
|
|
422
|
+
cancelRun(runId: string, leaseToken: string, expectedRevision: number): boolean {
|
|
423
|
+
const row = this.db.query("SELECT status,lease_token AS leaseToken,lease_expires_at AS expires,workspace_id AS workspaceId,config_id AS configId FROM initialization_runs WHERE id=?").get(runId) as { status: RunStatus; leaseToken: string; expires: string; workspaceId: string; configId: string } | null;
|
|
424
|
+
if (!row) throw new Error(`Unknown initialization run: ${runId}`);
|
|
425
|
+
if (row.leaseToken !== leaseToken) throw new Error("Initialization lease token mismatch");
|
|
426
|
+
if (Date.parse(row.expires) < Date.now()) throw new Error("Initialization lease expired; recover the run before cancelling");
|
|
427
|
+
const current = this.db.query("SELECT revision FROM current_context WHERE workspace_id=? AND config_id=?").get(row.workspaceId, row.configId) as { revision: number } | null;
|
|
428
|
+
if (expectedRevision !== (current?.revision ?? 0)) throw new Error("Initialization run revision conflict");
|
|
429
|
+
const timestamp = now();
|
|
430
|
+
return this.db.query("UPDATE initialization_runs SET status='cancelled',updated_at=? WHERE id=? AND lease_token=? AND lease_expires_at>? AND status IN ('preparing','collecting','validating')").run(timestamp, runId, leaseToken, timestamp).changes === 1;
|
|
389
431
|
}
|
|
390
432
|
|
|
391
433
|
failRun(runId: string, leaseToken: string, error: string): boolean {
|
|
392
|
-
|
|
434
|
+
this.assertWritableRun(runId, leaseToken);
|
|
435
|
+
const timestamp = now();
|
|
436
|
+
return this.db.query("UPDATE initialization_runs SET status='failed',error=?,updated_at=? WHERE id=? AND lease_token=? AND lease_expires_at>? AND status IN ('preparing','collecting','validating')").run(error.slice(0, 4000), timestamp, runId, leaseToken, timestamp).changes === 1;
|
|
393
437
|
}
|
|
394
438
|
|
|
395
439
|
recoverRun(runId: string, leaseMs = 15 * 60_000): { leaseToken: string } {
|
|
@@ -399,11 +443,12 @@ export class InitializationStore {
|
|
|
399
443
|
if (["failed", "finished"].includes(row.status)) throw new Error(`Run cannot be recovered from ${row.status}`);
|
|
400
444
|
if (Date.parse(row.expires) >= Date.now()) throw new Error("Run lease is still active");
|
|
401
445
|
const leaseToken = randomUUID();
|
|
402
|
-
this.db.query("UPDATE initialization_runs SET lease_token=?,lease_expires_at=?,updated_at=? WHERE id=?").run(leaseToken, new Date(Date.now() + leaseMs).toISOString(), now(), runId);
|
|
446
|
+
const changed = this.db.query("UPDATE initialization_runs SET lease_token=?,lease_expires_at=?,updated_at=? WHERE id=? AND lease_expires_at<? AND status IN ('preparing','collecting','validating')").run(leaseToken, new Date(Date.now() + leaseMs).toISOString(), now(), runId, now());
|
|
447
|
+
if (changed.changes !== 1) throw new Error("Initialization run recovery conflict");
|
|
403
448
|
return { leaseToken };
|
|
404
449
|
}
|
|
405
450
|
|
|
406
|
-
publishRun(runId: string, leaseToken: string, options: { expectedRevision
|
|
451
|
+
publishRun(runId: string, leaseToken: string, options: { expectedRevision: number; completeness?: "complete" | "partial"; indexRef?: Record<string, unknown>; updateCurrent?: boolean }): { snapshotId: string; revision: number } {
|
|
407
452
|
const run = this.assertWritableRun(runId, leaseToken);
|
|
408
453
|
const input = this.db.query("SELECT stable,fingerprint FROM input_snapshots WHERE id=?").get(run.inputSnapshotId) as { stable: number; fingerprint: string };
|
|
409
454
|
if (!input.stable) throw new Error("Cannot publish an unstable input snapshot");
|
|
@@ -422,12 +467,31 @@ export class InitializationStore {
|
|
|
422
467
|
if (completeness === "complete" && blockers.length) throw new Error(`Required collection coverage is incomplete: ${blockers.join(", ")}`);
|
|
423
468
|
const current = this.db.query("SELECT revision FROM current_context WHERE workspace_id=? AND config_id=?").get(run.workspaceId, run.configId) as { revision: number } | null;
|
|
424
469
|
const revision = (current?.revision ?? 0) + 1;
|
|
425
|
-
if (options.expectedRevision !==
|
|
470
|
+
if (options.expectedRevision !== (current?.revision ?? 0)) throw new Error(`Current context revision conflict: expected ${options.expectedRevision}, actual ${current?.revision ?? 0}`);
|
|
471
|
+
if (options.indexRef) {
|
|
472
|
+
if (options.indexRef.inputFingerprint !== input.fingerprint || options.indexRef.workspaceId !== run.workspaceId || options.indexRef.configId !== run.configId) throw new Error("Index reference does not match initialization input snapshot");
|
|
473
|
+
if (options.indexRef.status === "complete" && (typeof options.indexRef.generation !== "string" || typeof options.indexRef.snapshotHash !== "string")) throw new Error("Complete index references require generation and snapshot hash");
|
|
474
|
+
if (options.indexRef.status === "complete") {
|
|
475
|
+
if (typeof options.indexRef.contentManifestHash !== "string" || typeof options.indexRef.repositoryRevision !== "string") throw new Error("Complete index references require content manifest and repository revision");
|
|
476
|
+
const indexRoot = path.resolve(this.context.root, String(options.indexRef.stagingPath ?? options.indexRef.path ?? ".planning/code-index"));
|
|
477
|
+
const immutable = path.resolve(indexRoot, String(options.indexRef.immutablePath ?? `snapshots/${options.indexRef.generation}`));
|
|
478
|
+
const snapshotFile = path.join(immutable, "SNAPSHOT.json");
|
|
479
|
+
if (!existsSync(snapshotFile)) throw new Error("Complete index reference does not point to an immutable generation");
|
|
480
|
+
const actual = hash(readFileSync(snapshotFile));
|
|
481
|
+
if (actual !== options.indexRef.snapshotHash) throw new Error("Code index snapshot hash does not match index reference");
|
|
482
|
+
const payload = JSON.parse(readFileSync(snapshotFile, "utf8")) as { generation?: string };
|
|
483
|
+
if (payload.generation !== options.indexRef.generation) throw new Error("Code index generation does not match SNAPSHOT.json");
|
|
484
|
+
const workspace = (payload as any).workspace ?? {};
|
|
485
|
+
if (workspace.contentManifestHash !== options.indexRef.contentManifestHash) throw new Error("Code index content manifest does not match index reference");
|
|
486
|
+
if (workspace.repositoryRevision !== options.indexRef.repositoryRevision) throw new Error("Code index repository revision does not match index reference");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
426
489
|
const facts = this.db.query("SELECT kind,repository,object_key,value_json,evidence_level,extractor,confidence FROM facts WHERE run_id=? ORDER BY kind,repository,object_key").all(runId);
|
|
427
490
|
const snapshotId = randomUUID();
|
|
428
491
|
const transaction = this.db.transaction(() => {
|
|
429
492
|
this.db.query("UPDATE initialization_runs SET status='validating',updated_at=? WHERE id=? AND lease_token=?").run(now(), runId, leaseToken);
|
|
430
|
-
|
|
493
|
+
const persistedIndexRef = options.indexRef ? Object.fromEntries(Object.entries(options.indexRef).filter(([key]) => key !== "stagingPath")) : null;
|
|
494
|
+
this.db.query("INSERT INTO context_snapshots(id,project_id,workspace_id,config_id,input_snapshot_id,run_id,completeness,content_hash,index_ref_json,created_at) VALUES (?,?,?,?,?,?,?,?,?,?)").run(snapshotId, this.context.manifest.projectId, run.workspaceId, run.configId, run.inputSnapshotId, runId, completeness, hash(canonicalJson(facts)), persistedIndexRef ? canonicalJson(persistedIndexRef) : null, now());
|
|
431
495
|
if (options.updateCurrent === false) {
|
|
432
496
|
// Shadow imports are immutable and queryable without becoming current.
|
|
433
497
|
} else if (current) {
|
|
@@ -484,6 +548,11 @@ export class InitializationStore {
|
|
|
484
548
|
return this.queryContext({ workspaceId, configId: current.configId, ...input });
|
|
485
549
|
}
|
|
486
550
|
|
|
551
|
+
currentRevision(workspaceId: string, configId: string): number {
|
|
552
|
+
const row = this.db.query("SELECT revision FROM current_context WHERE workspace_id=? AND config_id=?").get(workspaceId, configId) as { revision: number } | null;
|
|
553
|
+
return row?.revision ?? 0;
|
|
554
|
+
}
|
|
555
|
+
|
|
487
556
|
getInitializationConfiguration(workspaceId: string): Record<string, unknown> {
|
|
488
557
|
const row = this.db.query(`SELECT c.id,c.revision,c.content_hash AS contentHash,c.config_json AS config
|
|
489
558
|
FROM scan_configs c LEFT JOIN current_context p ON p.config_id=c.id AND p.workspace_id=?
|
|
@@ -498,11 +567,14 @@ export class InitializationStore {
|
|
|
498
567
|
|
|
499
568
|
compareSnapshots(fromSnapshotId: string, toSnapshotId: string): Record<string, unknown> {
|
|
500
569
|
if (fromSnapshotId === toSnapshotId) return { comparable: true, fromSnapshotId, toSnapshotId, added: [], changed: [], disappeared: [] };
|
|
501
|
-
const snapshots = this.db.query("SELECT id,config_id AS configId FROM context_snapshots WHERE id IN (?,?)").all(fromSnapshotId, toSnapshotId) as Array<{ id: string; configId: string }>;
|
|
570
|
+
const snapshots = this.db.query("SELECT id,project_id AS projectId,workspace_id AS workspaceId,config_id AS configId,input_snapshot_id AS inputSnapshotId FROM context_snapshots WHERE id IN (?,?)").all(fromSnapshotId, toSnapshotId) as Array<{ id: string; projectId: string; workspaceId: string; configId: string; inputSnapshotId: string }>;
|
|
502
571
|
if (snapshots.length !== 2) throw new Error("Both context snapshots must exist");
|
|
503
572
|
const fromMeta = snapshots.find(item => item.id === fromSnapshotId)!;
|
|
504
573
|
const toMeta = snapshots.find(item => item.id === toSnapshotId)!;
|
|
574
|
+
if (fromMeta.projectId !== toMeta.projectId || fromMeta.workspaceId !== toMeta.workspaceId) return { comparable: false, reason: "workspace_changed", added: [], changed: [], disappeared: [] };
|
|
505
575
|
if (fromMeta.configId !== toMeta.configId) return { comparable: false, reason: "scan_config_changed", added: [], changed: [], disappeared: [] };
|
|
576
|
+
const manifests = this.db.query("SELECT id,manifest_json AS manifest FROM input_snapshots WHERE id IN (?,?)").all(fromMeta.inputSnapshotId, toMeta.inputSnapshotId) as Array<{ id: string; manifest: string }>;
|
|
577
|
+
if (manifests.length !== 2) return { comparable: false, reason: "input_snapshot_missing", added: [], changed: [], disappeared: [] };
|
|
506
578
|
const rows = (snapshotId: string) => this.db.query(`SELECT f.kind,f.repository,f.object_key AS objectKey,f.value_json AS value
|
|
507
579
|
FROM facts f JOIN context_snapshots s ON s.run_id=f.run_id WHERE s.id=?`).all(snapshotId) as Array<{ kind: string; repository: string; objectKey: string; value: string }>;
|
|
508
580
|
const key = (row: { kind: string; repository: string; objectKey: string }) => `${row.kind}\0${row.repository}\0${row.objectKey}`;
|
|
@@ -539,7 +611,8 @@ export class InitializationStore {
|
|
|
539
611
|
const job = this.db.query("SELECT target_path AS targetPath,ownership,status FROM document_jobs WHERE id=?").get(jobId) as { targetPath: string; ownership: string; status: string } | null;
|
|
540
612
|
if (!job) throw new Error(`Unknown document job: ${jobId}`);
|
|
541
613
|
if (job.ownership === "human") throw new Error("Human-owned documents cannot be generated");
|
|
542
|
-
this.db.query("UPDATE document_jobs SET status='generating',error=NULL,updated_at=? WHERE id=? AND status IN ('queued','failed','pending_review')").run(now(), jobId);
|
|
614
|
+
const claimed = this.db.query("UPDATE document_jobs SET status='generating',error=NULL,updated_at=? WHERE id=? AND status IN ('queued','failed','pending_review')").run(now(), jobId);
|
|
615
|
+
if (claimed.changes !== 1) throw new Error(`Document job is no longer writable: ${jobId}`);
|
|
543
616
|
const target = path.resolve(this.context.root, job.targetPath);
|
|
544
617
|
if (target !== this.context.root && !target.startsWith(`${this.context.root}${path.sep}`)) throw new Error("Document path escapes project root");
|
|
545
618
|
let existing: string | null = null;
|
|
@@ -559,7 +632,8 @@ export class InitializationStore {
|
|
|
559
632
|
}
|
|
560
633
|
await mkdir(path.dirname(target), { recursive: true });
|
|
561
634
|
const temporary = `${target}.${randomUUID()}.tmp`; await writeFile(temporary, output, { encoding: "utf8", flag: "wx" }); await rename(temporary, target);
|
|
562
|
-
this.db.query("UPDATE document_jobs SET status='complete',output_hash=?,updated_at=? WHERE id=?").run(hash(output), now(), jobId);
|
|
635
|
+
const completed = this.db.query("UPDATE document_jobs SET status='complete',output_hash=?,updated_at=? WHERE id=? AND status='generating'").run(hash(output), now(), jobId);
|
|
636
|
+
if (completed.changes !== 1) throw new Error(`Document job completion conflict: ${jobId}`);
|
|
563
637
|
return { status: "complete", path: target };
|
|
564
638
|
}
|
|
565
639
|
|
package/src/project-context.ts
CHANGED
|
@@ -136,15 +136,17 @@ export async function loadProjectContext(root: string): Promise<ProjectContext>
|
|
|
136
136
|
const manifestPath = path.join(canonicalRoot, ".devflow", "project.json");
|
|
137
137
|
const manifest = validateManifest(JSON.parse(await readFile(manifestPath, "utf8")), manifestPath);
|
|
138
138
|
const planningRoot = await assertPlanningBoundary(canonicalRoot);
|
|
139
|
+
if (!(await exists(projectControlDatabasePath(manifest.projectId)))) throw new Error("Project initialization database is not registered; run explicit project initialization first");
|
|
139
140
|
return attachInitializationStore({ root: canonicalRoot, manifestPath, manifest, planningRoot });
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
async function attachInitializationStore(context: Omit<ProjectContext, "workspaceId" | "controlDatabasePath">): Promise<ProjectContext> {
|
|
143
144
|
const store = await InitializationStore.open(context);
|
|
144
145
|
try {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
const location = await realpath(context.root);
|
|
147
|
+
const workspace = store.listWorkspaces().find(item => item.location === location);
|
|
148
|
+
if (!workspace) throw new Error("Project workspace is not registered; run explicit project initialization first");
|
|
149
|
+
return { ...context, workspaceId: String(workspace.id), controlDatabasePath: projectControlDatabasePath(context.manifest.projectId) };
|
|
148
150
|
} finally {
|
|
149
151
|
store.close();
|
|
150
152
|
}
|
|
@@ -174,6 +176,8 @@ export async function initializeProject(root: string): Promise<{ context: Projec
|
|
|
174
176
|
createdAt: new Date().toISOString(),
|
|
175
177
|
};
|
|
176
178
|
await atomicWriteJson(manifestPath, manifest);
|
|
179
|
+
const bootstrapStore = await InitializationStore.open({ root: canonicalRoot, manifest });
|
|
180
|
+
try { await bootstrapStore.registerWorkspace(); } finally { bootstrapStore.close(); }
|
|
177
181
|
return {
|
|
178
182
|
context: await attachInitializationStore({ root: canonicalRoot, manifestPath, manifest, planningRoot }),
|
|
179
183
|
created: true,
|
package/src/project-documents.ts
CHANGED
|
@@ -77,6 +77,10 @@ function table(headers: string[], rows: unknown[][]): string {
|
|
|
77
77
|
}
|
|
78
78
|
function yamlString(value: string): string { return JSON.stringify(value); }
|
|
79
79
|
function frontmatter(fields: Record<string, string | boolean | string[] | null | undefined>): string {
|
|
80
|
+
if (fields.source_snapshot && !fields.snapshot_id) fields.snapshot_id = fields.source_snapshot;
|
|
81
|
+
if (fields.source_snapshot) fields.generated = true;
|
|
82
|
+
if (fields.source_snapshot && !fields.generated_at) fields.generated_at = new Date().toISOString();
|
|
83
|
+
if (fields.source_snapshot && !fields.source_revision) fields.source_revision = fields.code_index_generation ?? "unknown";
|
|
80
84
|
const lines = ["---"];
|
|
81
85
|
for (const [key, value] of Object.entries(fields)) {
|
|
82
86
|
if (value === undefined || value === null) continue;
|
|
@@ -629,7 +633,7 @@ function initializationInterfaceContracts(): InterfaceContract[] {
|
|
|
629
633
|
name: "scan",
|
|
630
634
|
purpose: "执行项目初始化扫描并发布上下文快照。",
|
|
631
635
|
request: { action: "scan", mode: "full|fast", repositories: [{ name: "app", path: ".", role: "frontend|backend|shared|other" }], requiredKinds: FACT_KINDS, documentMode: "template|none", requestId: "stable-id" },
|
|
632
|
-
response: { runId: "uuid", snapshotId: "uuid", completeness: "complete|partial", diagnostics: [], documents: [{ targetPath: ".context/project/api.md", status: "complete|conflict|failed" }] },
|
|
636
|
+
response: { runId: "uuid", snapshotId: "uuid", completeness: "complete|partial", database: "<control.db>", projectId: "uuid", workspaceId: "uuid", planningRoot: ".planning", diagnostics: [], documents: [{ targetPath: ".context/project/api.md", status: "complete|conflict|failed" }] },
|
|
633
637
|
reads: ["source tree", ".devflow/project.json"],
|
|
634
638
|
writes: ["control.db", ".planning/code-index/", ".planning/document-index.json", ".planning/knowledge-index.json", ".context/", ".planning/PROJECT.md"],
|
|
635
639
|
},
|
|
@@ -932,6 +936,7 @@ function renderKnowledgeDocuments(context: ProjectContext, facts: Fact[], intro:
|
|
|
932
936
|
const generation = codeIndexGeneration(snapshot);
|
|
933
937
|
const outputs: Array<{ targetPath: string; body: string }> = [];
|
|
934
938
|
outputs.push({ targetPath: ".context/index.md", body: `${frontmatter({ type: "knowledge-index", scope: "project", project: context.manifest.name, status: "draft", source_snapshot: snapshotId, code_index_generation: generation, managed: true })}# ${context.manifest.name} 项目知识库\n\n## 项目级文档\n\n${KNOWLEDGE_DOCUMENT_TYPES.map(type => `- [${documentTitle(type)}](/project/${type}.md)`).join("\n")}\n\n## 需求级文档\n\n- 新需求使用 [.context/requirements/_template/index.md](/requirements/_template/index.md) 作为文档包模板。\n\n## 机器索引\n\n- 文档索引:\`.planning/document-index.json\`\n- 代码索引:\`.planning/code-index/\`\n` });
|
|
939
|
+
outputs.push({ targetPath: ".context/spec/index.md", body: `${frontmatter({ type: "compat-spec-index", scope: "project", project: context.manifest.name, status: "compat", source_snapshot: snapshotId, code_index_generation: generation, managed: true })}# 兼容 Spec 入口\n\n本文件是项目初始化的兼容入口,用于旧流程查找 \`.context/spec/index.md\`。新的权威入口如下:\n\n- 项目知识库:[../index.md](../index.md)\n- 项目级文档:[../project/index.md](../project/index.md)\n- OpenWiki 风格知识层:[../knowledge/index.md](../knowledge/index.md)\n- 初始化接口目录:[../knowledge/interfaces/index.md](../knowledge/interfaces/index.md)\n- 文档索引:\`.planning/document-index.json\`\n- 知识索引:\`.planning/knowledge-index.json\`\n- 接口索引:\`.planning/interface-index.json\`\n- 代码索引:\`.planning/code-index/\`\n\n此兼容入口不表示需求、设计或 API 已批准;人工确认前所有生成内容保持 draft/compat 状态。\n` });
|
|
935
940
|
outputs.push({ targetPath: ".context/project/index.md", body: `${frontmatter({ type: "project-document-index", scope: "project", project: context.manifest.name, status: "draft", source_snapshot: snapshotId, code_index_generation: generation, managed: true })}# 项目级文档\n\n${KNOWLEDGE_DOCUMENT_TYPES.map(type => `- [${documentTitle(type)}](./${type}.md)`).join("\n")}\n` });
|
|
936
941
|
for (const type of KNOWLEDGE_DOCUMENT_TYPES) outputs.push({ targetPath: `.context/project/${type}.md`, body: projectKnowledgeBody(context, type, facts, intro, snapshotId, knowledge, generation) });
|
|
937
942
|
outputs.push({ targetPath: ".context/requirements/_template/index.md", body: `${frontmatter({ type: "requirement-document-index", scope: "requirement", project: context.manifest.name, requirement_id: "REQ-TEMPLATE", status: "template", source_snapshot: snapshotId, code_index_generation: generation, managed: true })}# 需求级文档模板\n\n${KNOWLEDGE_DOCUMENT_TYPES.map(type => `- [${documentTitle(type)}](./${type === "requirements" ? "requirement" : type}.md)`).join("\n")}\n` });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFile, readdir, stat } from "node:fs/promises";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, readdir, stat, rename, rm } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { FACT_KINDS, InitializationStore, type FactInput, type FactKind, type ScanConfigInput } from "./initialization-store.ts";
|
|
@@ -10,7 +10,17 @@ const TEXT_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".rb", ".g
|
|
|
10
10
|
|
|
11
11
|
type SourceFile = { repository: string; path: string; absolute: string; contentHash: string; size: number; text?: string };
|
|
12
12
|
type ScanDiagnostic = { repository: string; path: string; reason: "symlink" | "excluded" | "too_large" | "unreadable"; message?: string };
|
|
13
|
-
export type InitializationProgress = {
|
|
13
|
+
export type InitializationProgress = {
|
|
14
|
+
runId: string;
|
|
15
|
+
snapshotId?: string;
|
|
16
|
+
completeness?: "complete" | "partial";
|
|
17
|
+
diagnostics: string[];
|
|
18
|
+
documents?: Array<{ targetPath: string; status: string; outputPath?: string; error?: string }>;
|
|
19
|
+
database: string;
|
|
20
|
+
projectId: string;
|
|
21
|
+
workspaceId: string;
|
|
22
|
+
planningRoot: string;
|
|
23
|
+
};
|
|
14
24
|
|
|
15
25
|
function digest(value: string | Uint8Array): string { return `sha256:${createHash("sha256").update(value).digest("hex")}`; }
|
|
16
26
|
function extension(file: string): string { return path.extname(file).toLowerCase(); }
|
|
@@ -167,7 +177,7 @@ async function runProcess(argv: string[], cwd: string): Promise<{ stdout: string
|
|
|
167
177
|
|
|
168
178
|
async function buildCodeIndex(context: ProjectContext, config: ScanConfigInput): Promise<Record<string, unknown>> {
|
|
169
179
|
const script = path.resolve(import.meta.dir, "../plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py");
|
|
170
|
-
const output =
|
|
180
|
+
const output = `.planning/.code-index-pending-${randomUUID()}`;
|
|
171
181
|
const codebases = config.repositories.flatMap(repository => ["--codebase", `${repository.name}:${path.resolve(context.root, repository.relativePath)}:${repository.role}`]);
|
|
172
182
|
await runProcess(["python3", script, "scan", "--root", context.root, "--output", output, ...codebases], context.root);
|
|
173
183
|
await runProcess(["python3", script, "validate", "--index", output], context.root);
|
|
@@ -175,7 +185,29 @@ async function buildCodeIndex(context: ProjectContext, config: ScanConfigInput):
|
|
|
175
185
|
const snapshotPath = path.join(context.root, output, "SNAPSHOT.json");
|
|
176
186
|
const bytes = await readFile(snapshotPath); const snapshot = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
|
|
177
187
|
if (!snapshot.generation || !snapshot.immutablePath) throw new Error("Code index did not publish an immutable generation");
|
|
178
|
-
|
|
188
|
+
const workspace = (snapshot.workspace ?? {}) as Record<string, unknown>;
|
|
189
|
+
return { path: ".planning/code-index", stagingPath: output, generation: snapshot.generation, immutablePath: snapshot.immutablePath, snapshotHash: digest(bytes), contentManifestHash: workspace.contentManifestHash, repositoryRevision: workspace.repositoryRevision, status: "complete" };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function promoteCodeIndex(context: ProjectContext, indexRef: Record<string, unknown>): Promise<() => Promise<void>> {
|
|
193
|
+
const stagingPath = typeof indexRef.stagingPath === "string" ? path.resolve(context.root, indexRef.stagingPath) : null;
|
|
194
|
+
const target = path.resolve(context.root, ".planning/code-index");
|
|
195
|
+
if (!stagingPath) return async () => {};
|
|
196
|
+
const previous = `${target}.previous-${randomUUID()}`;
|
|
197
|
+
const existing = await stat(target).catch(() => null);
|
|
198
|
+
if (existing) await rename(target, previous);
|
|
199
|
+
try {
|
|
200
|
+
await rename(stagingPath, target);
|
|
201
|
+
delete indexRef.stagingPath;
|
|
202
|
+
return async () => {
|
|
203
|
+
await rm(target, { recursive: true, force: true });
|
|
204
|
+
if (existing) await rename(previous, target);
|
|
205
|
+
};
|
|
206
|
+
} catch (error) {
|
|
207
|
+
await rm(target, { recursive: true, force: true }).catch(() => {});
|
|
208
|
+
if (existing) await rename(previous, target).catch(() => {});
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
179
211
|
}
|
|
180
212
|
|
|
181
213
|
export async function runProjectInitialization(context: ProjectContext, input: { config?: ScanConfigInput; requestId?: string; documentMode?: "low-tier-model" | "template" | "none"; organizer?: import("./project-documents.ts").LowTierDocumentOrganizer; switchAuthority?: boolean } = {}): Promise<InitializationProgress> {
|
|
@@ -189,7 +221,15 @@ export async function runProjectInitialization(context: ProjectContext, input: {
|
|
|
189
221
|
const inputSnapshot = store.createInputSnapshot(workspace.id, storedConfig.id, manifest, false);
|
|
190
222
|
const run = store.startRun({ workspaceId: workspace.id, configId: storedConfig.id, inputSnapshotId: inputSnapshot.id, requestId: input.requestId });
|
|
191
223
|
activeRun = run;
|
|
192
|
-
if (run.reused) return {
|
|
224
|
+
if (run.reused) return {
|
|
225
|
+
runId: run.id,
|
|
226
|
+
snapshotId: store.getRun(run.id)?.publishedSnapshotId as string | undefined,
|
|
227
|
+
diagnostics,
|
|
228
|
+
database: context.controlDatabasePath,
|
|
229
|
+
projectId: context.manifest.projectId,
|
|
230
|
+
workspaceId: context.workspaceId,
|
|
231
|
+
planningRoot: context.planningRoot,
|
|
232
|
+
};
|
|
193
233
|
const grouped = collect(files, context, config); const required = new Set(config.requiredKinds ?? FACT_KINDS);
|
|
194
234
|
for (const repository of config.repositories) for (const kind of FACT_KINDS) {
|
|
195
235
|
const repositoryFacts = grouped.get(kind)!.filter(fact => fact.repository === repository.name);
|
|
@@ -218,13 +258,22 @@ export async function runProjectInitialization(context: ProjectContext, input: {
|
|
|
218
258
|
facts: status === "complete" ? repositoryFacts : [],
|
|
219
259
|
});
|
|
220
260
|
}
|
|
221
|
-
const indexRef = config.mode === "full"
|
|
261
|
+
const indexRef = config.mode === "full"
|
|
262
|
+
? { ...(await buildCodeIndex(context, config)), inputFingerprint: inputSnapshot.fingerprint, workspaceId: workspace.id, configId: storedConfig.id }
|
|
263
|
+
: { status: "skipped", reason: "explicit_fast_mode", inputFingerprint: inputSnapshot.fingerprint, workspaceId: workspace.id, configId: storedConfig.id };
|
|
222
264
|
const after = await scan(context, config);
|
|
223
265
|
const afterManifest = frozenManifest(after);
|
|
224
266
|
const verified = store.createInputSnapshot(workspace.id, storedConfig.id, afterManifest, false);
|
|
225
267
|
if (verified.fingerprint !== inputSnapshot.fingerprint) throw new Error("Source changed during initialization; refusing to publish mixed input");
|
|
226
268
|
store.confirmInputSnapshot(inputSnapshot.id, inputSnapshot.fingerprint);
|
|
227
|
-
const
|
|
269
|
+
const rollbackIndex = await promoteCodeIndex(context, indexRef);
|
|
270
|
+
let published: { snapshotId: string; revision: number };
|
|
271
|
+
try {
|
|
272
|
+
published = store.publishRun(run.id, run.leaseToken, { expectedRevision: store.currentRevision(workspace.id, storedConfig.id), indexRef });
|
|
273
|
+
} catch (error) {
|
|
274
|
+
await rollbackIndex();
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
228
277
|
if (input.switchAuthority !== false) {
|
|
229
278
|
const authority = store.getAuthority();
|
|
230
279
|
if (authority.authority === "legacy") store.switchAuthority("database", authority.revision);
|
|
@@ -241,7 +290,17 @@ export async function runProjectInitialization(context: ProjectContext, input: {
|
|
|
241
290
|
documents = [{ targetPath: ".planning/initialization/report.md", status: "failed", error: message }];
|
|
242
291
|
}
|
|
243
292
|
}
|
|
244
|
-
return {
|
|
293
|
+
return {
|
|
294
|
+
runId: run.id,
|
|
295
|
+
snapshotId: published.snapshotId,
|
|
296
|
+
completeness: result.snapshot.completeness,
|
|
297
|
+
diagnostics,
|
|
298
|
+
documents,
|
|
299
|
+
database: context.controlDatabasePath,
|
|
300
|
+
projectId: context.manifest.projectId,
|
|
301
|
+
workspaceId: context.workspaceId,
|
|
302
|
+
planningRoot: context.planningRoot,
|
|
303
|
+
};
|
|
245
304
|
} catch (error) {
|
|
246
305
|
const message = error instanceof Error ? error.message : String(error);
|
|
247
306
|
diagnostics.push(message);
|
package/src/project-migration.ts
CHANGED
|
@@ -22,7 +22,8 @@ export async function importLegacyInitialization(context: ProjectContext, source
|
|
|
22
22
|
try {
|
|
23
23
|
const workspace = await store.registerWorkspace();
|
|
24
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 })),
|
|
25
|
+
const input = store.createInputSnapshot(workspace.id, config.id, documents.map(item => ({ repository: "legacy-materials", path: item.relative, contentHash: item.contentHash, size: item.size })), false);
|
|
26
|
+
store.confirmInputSnapshot(input.id, input.fingerprint);
|
|
26
27
|
const run = store.startRun({ workspaceId: workspace.id, configId: config.id, inputSnapshotId: input.id, requestId });
|
|
27
28
|
if (!run.reused) store.recordCollection(run.id, run.leaseToken, {
|
|
28
29
|
kind: "evidence", status: "complete", adapter: "legacy-import@1",
|
|
@@ -30,7 +31,7 @@ export async function importLegacyInitialization(context: ProjectContext, source
|
|
|
30
31
|
});
|
|
31
32
|
const snapshot = run.reused && store.getRun(run.id)?.publishedSnapshotId
|
|
32
33
|
? { snapshotId: String(store.getRun(run.id)!.publishedSnapshotId) }
|
|
33
|
-
: store.publishRun(run.id, run.leaseToken, { completeness: "partial", updateCurrent: false });
|
|
34
|
+
: store.publishRun(run.id, run.leaseToken, { expectedRevision: 0, completeness: "partial", updateCurrent: false });
|
|
34
35
|
const imports = documents.map(item => store.registerHistoricalImport(workspace.id, item.relative, item.contentHash, snapshot.snapshotId));
|
|
35
36
|
const authority = store.getAuthority();
|
|
36
37
|
if (authority.authority === "legacy") store.switchAuthority("shadow", authority.revision);
|