|
1 | 1 | import { mkdirSync, readdirSync, readFileSync, existsSync } from "node:fs" |
2 | 2 | import { createHash } from "node:crypto" |
3 | 3 | import path from "node:path" |
4 | | -import { writeFileAtomic, writeFileExclusive } from "./atomic-write" |
| 4 | +import { writeFileAtomic, writeFileExclusive, writeRecoverableFileExclusive } from "./atomic-write" |
5 | 5 |
|
6 | 6 | // V3 Document System (docs/28): the bedrock. All persistent state is a typed-document |
7 | 7 | // graph — small files, content-addressed, append-only with a supersede chain, bidirectional |
@@ -368,6 +368,26 @@ export class DocumentStore { |
368 | 368 | return next |
369 | 369 | } |
370 | 370 |
|
| 371 | + // Trusted built-in corpus files are recoverable from the packaged domain packs. New seeds can |
| 372 | + // therefore skip per-file fsync while retaining atomic visibility; updates still use the normal |
| 373 | + // append-only durable path so shipped corpus revisions preserve version history. |
| 374 | + seedActive(input: CreateDocInput): Doc { |
| 375 | + const cur = this.findLogical(input) |
| 376 | + if (cur) { |
| 377 | + const doc = this.upsert(input) |
| 378 | + if (doc.status !== "active") this.setStatus(doc.id, "active") |
| 379 | + return this.get(doc.id)! |
| 380 | + } |
| 381 | + |
| 382 | + const id = this.allocateId(input.type, input.domain ?? null, input.idSlug, input.description) |
| 383 | + let doc = { ...this.docFromInput(id, 1, input), status: "active" as const } |
| 384 | + this.assertKnowledgeConfidence(doc) |
| 385 | + this.assertLinkTargets(doc.links) |
| 386 | + doc = { ...doc, hash: computeHash(doc) } |
| 387 | + this.persistRecoverable(doc) |
| 388 | + return doc |
| 389 | + } |
| 390 | + |
371 | 391 | update(id: string, body: string, links?: readonly DocLink[]): Doc { |
372 | 392 | const cur = this.get(id) |
373 | 393 | if (!cur) throw new Error(`update: unknown doc ${id}`) |
@@ -607,6 +627,20 @@ export class DocumentStore { |
607 | 627 | } |
608 | 628 | this.indexDoc(doc) |
609 | 629 | } |
| 630 | + private persistRecoverable(doc: Doc): void { |
| 631 | + const dir = path.join(this.root, "docs", doc.type) |
| 632 | + mkdirSync(dir, { recursive: true }) |
| 633 | + const file = path.join(dir, `${idToFile(doc.id)}@v${doc.version}.json`) |
| 634 | + try { |
| 635 | + writeRecoverableFileExclusive(file, JSON.stringify(doc, null, 2)) |
| 636 | + } catch (error) { |
| 637 | + if ((error as NodeJS.ErrnoException)?.code !== "EEXIST") throw error |
| 638 | + const existing = this.readVersionFile(file) |
| 639 | + if (!existing || existing.hash !== doc.hash) |
| 640 | + throw new DocumentConflictError(doc.id, doc.version, existing?.hash ?? "<unreadable>", doc.hash) |
| 641 | + } |
| 642 | + this.indexDoc(doc) |
| 643 | + } |
610 | 644 | private replace(doc: Doc): void { |
611 | 645 | // rewrites the SAME version in place with new status/superseded_by; rehash so INV-2 holds. This |
612 | 646 | // is an intentional overwrite (not a new version), so it uses the crash-safe atomic OVERWRITE |
|
0 commit comments