From 61cc089730aafe0f827f620f750f378ab070ddba Mon Sep 17 00:00:00 2001 From: Ethan Date: Sun, 14 Jun 2026 15:20:51 -0700 Subject: [PATCH 1/6] feat(export): read x-okf snapshot from imported page frontmatter --- src/export/collect.ts | 15 ++++++++++++++- src/export/types.ts | 10 ++++++++++ test/okf-reexport-collect.test.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 test/okf-reexport-collect.test.ts diff --git a/src/export/collect.ts b/src/export/collect.ts index 75e1005..d88539e 100644 --- a/src/export/collect.ts +++ b/src/export/collect.ts @@ -25,7 +25,7 @@ import { sourceHashLookupFromSnapshot, type SourceHashLookup, } from "./provenance.js"; -import type { ExportPage, PageDirectory } from "./types.js"; +import type { ExportPage, PageDirectory, XOkfSnapshot } from "./types.js"; export { extractWikilinkSlugs }; @@ -81,6 +81,18 @@ function readContradictedBy(meta: Record): ContradictionRef[] | return refs.length > 0 ? refs : undefined; } +/** Read an imported page's `x-okf` snapshot (original OKF frontmatter + raw type), if present and well-formed. */ +function readXOkf(meta: Record): XOkfSnapshot | undefined { + const x = meta["x-okf"]; + if (!x || typeof x !== "object") return undefined; + const of = (x as Record).originalFrontmatter; + if (!of || typeof of !== "object") return undefined; + const snap: XOkfSnapshot = { originalFrontmatter: of as Record }; + const t = (x as Record).type; + if (typeof t === "string") snap.type = t; + return snap; +} + /** Validate and return PageKind from frontmatter, or undefined. */ function readPageKind(meta: Record): PageKind | undefined { const value = meta.kind; @@ -122,6 +134,7 @@ function toExportPage( links: extractWikilinkSlugs(raw.body), body: raw.body, kind: readPageKind(meta), + ...(readXOkf(meta) ? { xOkf: readXOkf(meta)! } : {}), advisoryConfidence: readAdvisoryConfidence(meta), provenanceState: readProvenanceState(meta), contradictedBy: readContradictedBy(meta), diff --git a/src/export/types.ts b/src/export/types.ts index 8788023..72cf253 100644 --- a/src/export/types.ts +++ b/src/export/types.ts @@ -26,6 +26,14 @@ import type { FreshnessStatus } from "../freshness/types.js"; */ export type ExportCitation = FlatCitation; +/** Snapshot of an imported doc's original OKF frontmatter, captured at import. Present ONLY on imported pages; drives verbatim re-export of foreign frontmatter. */ +export interface XOkfSnapshot { + /** Raw OKF `type` when it wasn't a known llmwiki kind (absent for known kinds). */ + type?: string; + /** Full original OKF frontmatter, verbatim. */ + originalFrontmatter: Record; +} + /** * Which wiki/ subdirectory a page lives in. @@ -71,6 +79,8 @@ export interface ExportPage { * `kind` was set on the wiki page rather than fabricating a default. */ kind?: PageKind; + /** Original OKF frontmatter snapshot when this page was imported from a foreign bundle; absent for native pages. */ + xOkf?: XOkfSnapshot; /** * Compiler's confidence estimate at export time. Advisory only — * once imported into any downstream store this field is mutable and diff --git a/test/okf-reexport-collect.test.ts b/test/okf-reexport-collect.test.ts new file mode 100644 index 0000000..944a0b6 --- /dev/null +++ b/test/okf-reexport-collect.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { mkdtemp, rm, mkdir, writeFile } from "fs/promises"; +import { tmpdir } from "os"; +import path from "path"; +import { collectExportPages } from "../src/export/collect.js"; + +let dir: string; +afterEach(async () => { if (dir) await rm(dir, { recursive: true, force: true }); }); + +const WITH_XOKF = + '---\ntitle: Cust\nx-okf:\n type: "BigQuery Table"\n originalFrontmatter:\n type: "BigQuery Table"\n vendorKey: 7\n---\n\nA table.\n'; +const WITHOUT_XOKF = "---\ntitle: Plain\n---\n\nPlain page.\n"; + +describe("collectExportPages reads x-okf snapshot", () => { + it("surfaces xOkf on imported pages and leaves native pages undefined", async () => { + dir = await mkdtemp(path.join(tmpdir(), "okf-rx-collect-")); + await mkdir(path.join(dir, "wiki/concepts"), { recursive: true }); + await writeFile(path.join(dir, "wiki/concepts/cust.md"), WITH_XOKF); + await writeFile(path.join(dir, "wiki/concepts/plain.md"), WITHOUT_XOKF); + const pages = await collectExportPages(dir); + const cust = pages.find((p) => p.slug === "cust")!; + const plain = pages.find((p) => p.slug === "plain")!; + expect(cust.xOkf?.type).toBe("BigQuery Table"); + expect(cust.xOkf?.originalFrontmatter.vendorKey).toBe(7); + expect(plain.xOkf).toBeUndefined(); + }); +}); From 2a76e930230b5ad16485d9d233038b52752b4210 Mon Sep 17 00:00:00 2001 From: Ethan Date: Sun, 14 Jun 2026 15:21:40 -0700 Subject: [PATCH 2/6] feat(export): reconstruct foreign frontmatter verbatim on re-export of imported pages --- src/export/okf/mapping.ts | 19 ++++++++++++++++--- test/okf-reexport-map.test.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 test/okf-reexport-map.test.ts diff --git a/src/export/okf/mapping.ts b/src/export/okf/mapping.ts index 006cfbb..1593071 100644 --- a/src/export/okf/mapping.ts +++ b/src/export/okf/mapping.ts @@ -4,7 +4,7 @@ */ import { createHash } from "node:crypto"; import path from "node:path"; -import type { ExportPage } from "../types.js"; +import type { ExportPage, XOkfSnapshot } from "../types.js"; import type { OkfFrontmatter, XLlmwiki, LinkResolver } from "./types.js"; import { slugify } from "../../utils/markdown.js"; @@ -37,8 +37,8 @@ export function safeRefName(file: string): string { return `${stem}-${hash}${ext}`; } -/** ExportPage -> OKF frontmatter. `type` is always non-empty (defaults to "concept"). */ -export function mapPageToOkfFrontmatter(page: ExportPage): OkfFrontmatter { +/** Build the refreshed x-llmwiki provenance block for a page (contentHash recomputed from the current body). */ +function buildXLlmwiki(page: ExportPage): XLlmwiki { const x: XLlmwiki = { schemaVersion: "0.1", contentHash: hashCanonicalBody(page.body), @@ -51,7 +51,20 @@ export function mapPageToOkfFrontmatter(page: ExportPage): OkfFrontmatter { if (page.freshnessStatus) x.freshnessStatus = page.freshnessStatus; if (page.aliases?.length) x.aliases = page.aliases; if (page.citations?.length) x.citations = page.citations; + return x; +} +/** Reproduce an imported doc's original OKF frontmatter verbatim; refresh ONLY x-llmwiki; force a non-empty type. */ +function reconstructForeignFrontmatter(xOkf: XOkfSnapshot, x: XLlmwiki): OkfFrontmatter { + const of = xOkf.originalFrontmatter; + const type = typeof of.type === "string" && of.type.trim() ? of.type : (xOkf.type ?? "concept"); + return { ...of, type, "x-llmwiki": x } as unknown as OkfFrontmatter; +} + +/** ExportPage -> OKF frontmatter. `type` is always non-empty (defaults to "concept"). */ +export function mapPageToOkfFrontmatter(page: ExportPage): OkfFrontmatter { + const x = buildXLlmwiki(page); + if (page.xOkf) return reconstructForeignFrontmatter(page.xOkf, x); const fm: OkfFrontmatter = { type: page.kind ?? "concept", "x-llmwiki": x }; if (page.title) fm.title = page.title; if (page.summary) fm.description = page.summary; diff --git a/test/okf-reexport-map.test.ts b/test/okf-reexport-map.test.ts new file mode 100644 index 0000000..d2508eb --- /dev/null +++ b/test/okf-reexport-map.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from "vitest"; +import { mapPageToOkfFrontmatter } from "../src/export/okf/mapping.js"; +import type { ExportPage } from "../src/export/types.js"; + +function page(overrides: Partial = {}): ExportPage { + return { + title: "T", slug: "t", pageDirectory: "concepts", path: "wiki/concepts/t.md", + summary: "", sources: [], tags: [], createdAt: "2026-01-01T00:00:00Z", + updatedAt: "2026-02-02T00:00:00Z", links: [], body: "A table.\n", kind: "concept", + citations: [], contentHash: "abc", sourceHashes: [], ...overrides, + } as ExportPage; +} + +describe("mapPageToOkfFrontmatter reconstructs foreign frontmatter", () => { + it("reproduces foreign type + keys and refreshes x-llmwiki for imported pages", () => { + const fm = mapPageToOkfFrontmatter(page({ + xOkf: { + type: "BigQuery Table", + originalFrontmatter: { + type: "BigQuery Table", title: "T", vendorKey: 7, + "x-llmwiki": { schemaVersion: "0.1", contentHash: "STALE", pageDirectory: "concepts" }, + }, + }, + })); + expect(fm.type).toBe("BigQuery Table"); + expect((fm as Record).vendorKey).toBe(7); + expect(fm["x-llmwiki"].contentHash).not.toBe("STALE"); + expect(fm["x-llmwiki"].pageDirectory).toBe("concepts"); + }); + + it("falls through to the native path when xOkf is absent", () => { + expect(mapPageToOkfFrontmatter(page({ kind: undefined })).type).toBe("concept"); + }); +}); From c1463fa58a38d6e0f8c6a577524db18dd216a401 Mon Sep 17 00:00:00 2001 From: Ethan Date: Sun, 14 Jun 2026 15:22:08 -0700 Subject: [PATCH 3/6] test(export): foreign-frontmatter + citation-idempotence round-trip --- test/okf-reexport-roundtrip.test.ts | 60 +++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 test/okf-reexport-roundtrip.test.ts diff --git a/test/okf-reexport-roundtrip.test.ts b/test/okf-reexport-roundtrip.test.ts new file mode 100644 index 0000000..cdabbdc --- /dev/null +++ b/test/okf-reexport-roundtrip.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { mkdtemp, rm, mkdir, writeFile, readFile } from "fs/promises"; +import { tmpdir } from "os"; +import path from "path"; +import { collectExportPages } from "../src/export/collect.js"; +import { buildOkfBundle } from "../src/export/okf/bundle.js"; +import { importOkfBundle } from "../src/import/okf-import.js"; +import { parseFrontmatter } from "../src/utils/markdown.js"; + +let dir: string; +afterEach(async () => { if (dir) await rm(dir, { recursive: true, force: true }); }); + +/** Stage a mapped import page into a fresh project's wiki/concepts/, then return its slug. */ +async function stageImported(root: string, slug: string, body: string): Promise { + await mkdir(path.join(root, "wiki/concepts"), { recursive: true }); + await writeFile(path.join(root, `wiki/concepts/${slug}.md`), body); +} + +const FOREIGN_DOC = "---\ntype: BigQuery Table\ntitle: Cust\nvendorKey: 7\n---\n\nA table.\n"; + +describe("OKF re-export honesty round-trips", () => { + it("reproduces an unknown foreign type + key verbatim through import -> export", async () => { + dir = await mkdtemp(path.join(tmpdir(), "okf-rx-foreign-")); + const bundleDir = path.join(dir, "foreign"); + await mkdir(path.join(bundleDir, "concepts"), { recursive: true }); + await writeFile(path.join(bundleDir, "concepts/t.md"), FOREIGN_DOC); + const proj = path.join(dir, "proj"); + const { pages } = await importOkfBundle(bundleDir, proj); + await stageImported(proj, pages[0].slug, pages[0].body); + const exp = await collectExportPages(proj); + const outDir = path.join(dir, "out"); + await buildOkfBundle(proj, exp, outDir); + const doc = await readFile(path.join(outDir, `concepts/${pages[0].slug}.md`), "utf-8"); + const { meta } = parseFrontmatter(doc); + expect(meta.type).toBe("BigQuery Table"); + expect(meta.vendorKey).toBe(7); + expect(meta["x-llmwiki"]).toBeDefined(); + }); + + it("regenerates exactly one # Citations section across export -> import -> export", async () => { + dir = await mkdtemp(path.join(tmpdir(), "okf-rx-cite-")); + const proj = path.join(dir, "proj"); + await mkdir(path.join(proj, "wiki/concepts"), { recursive: true }); + await writeFile(path.join(proj, "wiki/concepts/rag.md"), + "---\ntitle: RAG\nkind: concept\nsources: [rag.md]\n---\n\nText. ^[rag.md:1-2]\n"); + const expA = await collectExportPages(proj); + const outA = path.join(dir, "outA"); + await buildOkfBundle(proj, expA, outA); + const docA = await readFile(path.join(outA, "concepts/rag.md"), "utf-8"); + expect((docA.match(/^#\s+Citations\b/gm) ?? []).length).toBe(1); + const proj2 = path.join(dir, "proj2"); + const { pages } = await importOkfBundle(outA, proj2); + await stageImported(proj2, pages[0].slug, pages[0].body); + const expB = await collectExportPages(proj2); + const outB = path.join(dir, "outB"); + await buildOkfBundle(proj2, expB, outB); + const docB = await readFile(path.join(outB, `concepts/${pages[0].slug}.md`), "utf-8"); + expect((docB.match(/^#\s+Citations\b/gm) ?? []).length).toBe(1); + }); +}); From 302c9fd95220310291d97c1e3b06f03fb5f2d11e Mon Sep 17 00:00:00 2001 From: Ethan Date: Sun, 14 Jun 2026 15:26:36 -0700 Subject: [PATCH 4/6] refactor(export): table-drive x-llmwiki field copy to clear complexity gate --- src/export/okf/mapping.ts | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/export/okf/mapping.ts b/src/export/okf/mapping.ts index 1593071..96015bf 100644 --- a/src/export/okf/mapping.ts +++ b/src/export/okf/mapping.ts @@ -37,6 +37,27 @@ export function safeRefName(file: string): string { return `${stem}-${hash}${ext}`; } +/** + * Optional x-llmwiki fields, each paired with how to read its value off a page. + * Table-driven so {@link buildXLlmwiki} stays a single flat copy loop (no branch + * per field) — a non-empty value is copied, everything else is dropped. + */ +const OPTIONAL_XLLMWIKI_FIELDS: ReadonlyArray unknown]> = [ + ["sources", (p) => p.sources], + ["confidence", (p) => p.advisoryConfidence], + ["provenanceState", (p) => p.provenanceState], + ["contradictedBy", (p) => p.contradictedBy], + ["freshnessStatus", (p) => p.freshnessStatus], + ["aliases", (p) => p.aliases], + ["citations", (p) => p.citations], +]; + +/** True for values worth copying onto x-llmwiki: defined, and non-empty when array. */ +function isPresent(value: unknown): boolean { + if (value === undefined || value === null) return false; + return Array.isArray(value) ? value.length > 0 : true; +} + /** Build the refreshed x-llmwiki provenance block for a page (contentHash recomputed from the current body). */ function buildXLlmwiki(page: ExportPage): XLlmwiki { const x: XLlmwiki = { @@ -44,13 +65,10 @@ function buildXLlmwiki(page: ExportPage): XLlmwiki { contentHash: hashCanonicalBody(page.body), pageDirectory: page.pageDirectory, }; - if (page.sources?.length) x.sources = page.sources; - if (page.advisoryConfidence !== undefined) x.confidence = page.advisoryConfidence; - if (page.provenanceState) x.provenanceState = page.provenanceState; - if (page.contradictedBy?.length) x.contradictedBy = page.contradictedBy; - if (page.freshnessStatus) x.freshnessStatus = page.freshnessStatus; - if (page.aliases?.length) x.aliases = page.aliases; - if (page.citations?.length) x.citations = page.citations; + for (const [field, read] of OPTIONAL_XLLMWIKI_FIELDS) { + const value = read(page); + if (isPresent(value)) (x as unknown as Record)[field] = value; + } return x; } From fd4ef5944da5c75966a9cdf37c9f60e644ee41b1 Mon Sep 17 00:00:00 2001 From: Ethan Date: Sun, 14 Jun 2026 15:56:52 -0700 Subject: [PATCH 5/6] fix(export): re-export reflects current standard fields, preserves only foreign type+keys --- src/export/okf/mapping.ts | 50 +++++++++++++++++++++-------- test/okf-reexport-map.test.ts | 14 ++++++++ test/okf-reexport-roundtrip.test.ts | 12 ++++++- 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/export/okf/mapping.ts b/src/export/okf/mapping.ts index 96015bf..0552542 100644 --- a/src/export/okf/mapping.ts +++ b/src/export/okf/mapping.ts @@ -4,7 +4,7 @@ */ import { createHash } from "node:crypto"; import path from "node:path"; -import type { ExportPage, XOkfSnapshot } from "../types.js"; +import type { ExportPage } from "../types.js"; import type { OkfFrontmatter, XLlmwiki, LinkResolver } from "./types.js"; import { slugify } from "../../utils/markdown.js"; @@ -72,23 +72,47 @@ function buildXLlmwiki(page: ExportPage): XLlmwiki { return x; } -/** Reproduce an imported doc's original OKF frontmatter verbatim; refresh ONLY x-llmwiki; force a non-empty type. */ -function reconstructForeignFrontmatter(xOkf: XOkfSnapshot, x: XLlmwiki): OkfFrontmatter { - const of = xOkf.originalFrontmatter; - const type = typeof of.type === "string" && of.type.trim() ? of.type : (xOkf.type ?? "concept"); - return { ...of, type, "x-llmwiki": x } as unknown as OkfFrontmatter; -} +// Keys derived fresh from the CURRENT page (or llmwiki-owned), so they are stripped from +// the captured foreign snapshot before its unknown producer keys are carried through. Note +// `okfPath` lives on the x-okf block, not here; re-export deliberately omits x-okf entirely. +const RECONSTRUCT_STRIP = ["type", "title", "description", "tags", "timestamp", "x-llmwiki", "x-okf"]; -/** ExportPage -> OKF frontmatter. `type` is always non-empty (defaults to "concept"). */ -export function mapPageToOkfFrontmatter(page: ExportPage): OkfFrontmatter { - const x = buildXLlmwiki(page); - if (page.xOkf) return reconstructForeignFrontmatter(page.xOkf, x); - const fm: OkfFrontmatter = { type: page.kind ?? "concept", "x-llmwiki": x }; +/** Overlay the OKF standard fields from the CURRENT page so local edits are always reflected. */ +function applyStandardFields(fm: Record, page: ExportPage): void { if (page.title) fm.title = page.title; if (page.summary) fm.description = page.summary; if (page.tags?.length) fm.tags = page.tags; if (page.updatedAt) fm.timestamp = page.updatedAt; - return fm; +} + +/** + * Re-export an imported page: keep the raw foreign `type` and any unknown producer + * keys from the captured snapshot, but derive the OKF standard fields (title, + * description, tags, timestamp) from the CURRENT page so local edits are reflected, + * and refresh x-llmwiki. (Preserve foreign keys, not stale standard frontmatter.) + * + * Re-export places every doc at `/.md`: the llmwiki slug is the + * identity the OKF link rewriter + index TOC understand. The original bundle path is + * preserved durably under `x-okf.okfPath` for diagnosis; faithful reconstruction of + * nested original paths is intentionally deferred (it needs a non-slug link/index model). + */ +function reconstructForeignFrontmatter(page: ExportPage, x: XLlmwiki): OkfFrontmatter { + const of = page.xOkf!.originalFrontmatter; + const rawType = typeof of.type === "string" && of.type.trim() ? of.type : (page.xOkf!.type ?? "concept"); + const extras: Record = { ...of }; + for (const k of RECONSTRUCT_STRIP) delete extras[k]; + const fm: Record = { ...extras, type: rawType, "x-llmwiki": x }; + applyStandardFields(fm, page); + return fm as unknown as OkfFrontmatter; +} + +/** ExportPage -> OKF frontmatter. `type` is always non-empty (defaults to "concept"). */ +export function mapPageToOkfFrontmatter(page: ExportPage): OkfFrontmatter { + const x = buildXLlmwiki(page); + if (page.xOkf) return reconstructForeignFrontmatter(page, x); + const fm: Record = { type: page.kind ?? "concept", "x-llmwiki": x }; + applyStandardFields(fm, page); + return fm as unknown as OkfFrontmatter; } const WIKILINK = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g; diff --git a/test/okf-reexport-map.test.ts b/test/okf-reexport-map.test.ts index d2508eb..05e754e 100644 --- a/test/okf-reexport-map.test.ts +++ b/test/okf-reexport-map.test.ts @@ -28,6 +28,20 @@ describe("mapPageToOkfFrontmatter reconstructs foreign frontmatter", () => { expect(fm["x-llmwiki"].pageDirectory).toBe("concepts"); }); + it("derives standard fields from the CURRENT page, preserving only foreign type + keys", () => { + const fm = mapPageToOkfFrontmatter(page({ + title: "NewTitle", summary: "new", + xOkf: { + type: "BigQuery Table", + originalFrontmatter: { type: "BigQuery Table", title: "OldTitle", description: "old", vendorKey: 7 }, + }, + })); + expect(fm.type).toBe("BigQuery Table"); + expect((fm as Record).vendorKey).toBe(7); + expect(fm.title).toBe("NewTitle"); + expect(fm.description).toBe("new"); + }); + it("falls through to the native path when xOkf is absent", () => { expect(mapPageToOkfFrontmatter(page({ kind: undefined })).type).toBe("concept"); }); diff --git a/test/okf-reexport-roundtrip.test.ts b/test/okf-reexport-roundtrip.test.ts index cdabbdc..2b5674d 100644 --- a/test/okf-reexport-roundtrip.test.ts +++ b/test/okf-reexport-roundtrip.test.ts @@ -5,7 +5,7 @@ import path from "path"; import { collectExportPages } from "../src/export/collect.js"; import { buildOkfBundle } from "../src/export/okf/bundle.js"; import { importOkfBundle } from "../src/import/okf-import.js"; -import { parseFrontmatter } from "../src/utils/markdown.js"; +import { parseFrontmatter, buildFrontmatter } from "../src/utils/markdown.js"; let dir: string; afterEach(async () => { if (dir) await rm(dir, { recursive: true, force: true }); }); @@ -16,6 +16,13 @@ async function stageImported(root: string, slug: string, body: string): Promise< await writeFile(path.join(root, `wiki/concepts/${slug}.md`), body); } +/** Apply a local edit to a staged page's standard frontmatter fields, preserving x-okf. */ +async function editStaged(root: string, slug: string, edits: Record): Promise { + const file = path.join(root, `wiki/concepts/${slug}.md`); + const { meta, body } = parseFrontmatter(await readFile(file, "utf-8")); + await writeFile(file, `${buildFrontmatter({ ...meta, ...edits })}\n${body}`); +} + const FOREIGN_DOC = "---\ntype: BigQuery Table\ntitle: Cust\nvendorKey: 7\n---\n\nA table.\n"; describe("OKF re-export honesty round-trips", () => { @@ -27,6 +34,7 @@ describe("OKF re-export honesty round-trips", () => { const proj = path.join(dir, "proj"); const { pages } = await importOkfBundle(bundleDir, proj); await stageImported(proj, pages[0].slug, pages[0].body); + await editStaged(proj, pages[0].slug, { title: "Edited", summary: "edited summary" }); const exp = await collectExportPages(proj); const outDir = path.join(dir, "out"); await buildOkfBundle(proj, exp, outDir); @@ -34,6 +42,8 @@ describe("OKF re-export honesty round-trips", () => { const { meta } = parseFrontmatter(doc); expect(meta.type).toBe("BigQuery Table"); expect(meta.vendorKey).toBe(7); + expect(meta.title).toBe("Edited"); + expect(meta.description).toBe("edited summary"); expect(meta["x-llmwiki"]).toBeDefined(); }); From 2636b3a5b257e71b30743864830c8ad9dcb6c47c Mon Sep 17 00:00:00 2001 From: Ethan Date: Sun, 14 Jun 2026 15:58:54 -0700 Subject: [PATCH 6/6] feat(import): persist original okfPath durably on imported pages --- src/export/collect.ts | 2 ++ src/export/types.ts | 2 ++ src/import/okf-map.ts | 12 ++++++---- test/okf-map.test.ts | 5 +++++ test/okf-reexport-okfpath.test.ts | 34 +++++++++++++++++++++++++++++ test/okf-reexport-roundtrip.test.ts | 1 + 6 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 test/okf-reexport-okfpath.test.ts diff --git a/src/export/collect.ts b/src/export/collect.ts index d88539e..5eba8a2 100644 --- a/src/export/collect.ts +++ b/src/export/collect.ts @@ -90,6 +90,8 @@ function readXOkf(meta: Record): XOkfSnapshot | undefined { const snap: XOkfSnapshot = { originalFrontmatter: of as Record }; const t = (x as Record).type; if (typeof t === "string") snap.type = t; + const p = (x as Record).okfPath; + if (typeof p === "string") snap.okfPath = p; return snap; } diff --git a/src/export/types.ts b/src/export/types.ts index 72cf253..55edb70 100644 --- a/src/export/types.ts +++ b/src/export/types.ts @@ -30,6 +30,8 @@ export type ExportCitation = FlatCitation; export interface XOkfSnapshot { /** Raw OKF `type` when it wasn't a known llmwiki kind (absent for known kinds). */ type?: string; + /** Bundle-relative source path of the original OKF doc; durable across approval, for diagnosis. */ + okfPath?: string; /** Full original OKF frontmatter, verbatim. */ originalFrontmatter: Record; } diff --git a/src/import/okf-map.ts b/src/import/okf-map.ts index 34ed754..a7e051b 100644 --- a/src/import/okf-map.ts +++ b/src/import/okf-map.ts @@ -80,11 +80,15 @@ function baseFields(meta: Record, ctx: OkfMapContext, slug: str }; } -/** Verbatim snapshot of the source frontmatter; records the raw `type` only when foreign. */ -function buildXokf(meta: Record): Record { +/** + * Verbatim snapshot of the source frontmatter; records the raw `type` only when foreign. + * `okfPath` durably records the doc's bundle-relative source path so the original OKF + * identity survives review approval (the candidate-only `okfPath` is lost once live). + */ +function buildXokf(meta: Record, okfPath: string): Record { const rawType = typeof meta.type === "string" ? meta.type : "concept"; const known = KNOWN_KINDS.has(rawType); - return { ...(known ? {} : { type: rawType }), originalFrontmatter: meta }; + return { ...(known ? {} : { type: rawType }), okfPath, originalFrontmatter: meta }; } /** Assemble the llmwiki frontmatter fields from OKF standard + x-llmwiki blocks. */ @@ -93,7 +97,7 @@ function buildPageFields(doc: RawOkfDoc, ctx: OkfMapContext, slug: string): Reco const fields = baseFields(meta, ctx, slug); if (Array.isArray(meta.tags)) fields.tags = asStringArray(meta.tags); applyXLlmwiki(fields, (meta["x-llmwiki"] ?? {}) as Record); - fields["x-okf"] = buildXokf(meta); + fields["x-okf"] = buildXokf(meta, doc.relPath); return fields; } diff --git a/test/okf-map.test.ts b/test/okf-map.test.ts index f5cf7d4..0373649 100644 --- a/test/okf-map.test.ts +++ b/test/okf-map.test.ts @@ -63,4 +63,9 @@ describe("okfDocToPage", () => { expect((meta["x-okf"] as any).originalFrontmatter.vendorKey).toBe(7); expect(body).toContain("/concepts/missing.md"); }); + it("records the source relPath durably under x-okf.okfPath", () => { + const doc = { relPath: "concepts/t.md", meta: { type: "BigQuery Table" }, body: "Body.\n" }; + const { meta } = parseFrontmatter(okfDocToPage(doc, { bundleId: "b", titleOf: () => null }).body); + expect((meta["x-okf"] as any).okfPath).toBe("concepts/t.md"); + }); }); diff --git a/test/okf-reexport-okfpath.test.ts b/test/okf-reexport-okfpath.test.ts new file mode 100644 index 0000000..965ea23 --- /dev/null +++ b/test/okf-reexport-okfpath.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { mkdtemp, rm, readFile } from "fs/promises"; +import { tmpdir } from "os"; +import path from "path"; +import { writeCandidate, listCandidates } from "../src/compiler/candidates.js"; +import reviewApproveCommand from "../src/commands/review-approve.js"; +import { okfDocToPage } from "../src/import/okf-map.js"; +import { collectExportPages } from "../src/export/collect.js"; +import { parseFrontmatter } from "../src/utils/markdown.js"; + +let dir: string; const cwd = process.cwd(); +afterEach(async () => { process.chdir(cwd); if (dir) await rm(dir, { recursive: true, force: true }); }); + +const ctx = { bundleId: "b", titleOf: () => null }; +const FOREIGN = { relPath: "concepts/t.md", meta: { type: "BigQuery Table", title: "T" }, body: "Body.\n" }; + +describe("okfPath durability across approval + export", () => { + it("keeps x-okf.okfPath on the live page and surfaces it through collectExportPages", async () => { + dir = await mkdtemp(path.join(tmpdir(), "okf-okfpath-")); + const mapped = okfDocToPage(FOREIGN, ctx); + await writeCandidate(dir, { + title: mapped.title, slug: mapped.slug, summary: mapped.summary, sources: mapped.sources, + body: mapped.body, reviewMode: "imported", heldReasons: [{ code: "imported-okf" }], + targetDirectory: mapped.targetDirectory, okfPath: mapped.okfPath, + }); + process.chdir(dir); + const [c] = await listCandidates(dir); + await reviewApproveCommand(c.id); + const live = await readFile(path.join(dir, `wiki/concepts/${mapped.slug}.md`), "utf-8"); + expect((parseFrontmatter(live).meta["x-okf"] as any).okfPath).toBe("concepts/t.md"); + const exp = await collectExportPages(dir); + expect(exp.find((p) => p.slug === mapped.slug)!.xOkf?.okfPath).toBe("concepts/t.md"); + }); +}); diff --git a/test/okf-reexport-roundtrip.test.ts b/test/okf-reexport-roundtrip.test.ts index 2b5674d..ab87b00 100644 --- a/test/okf-reexport-roundtrip.test.ts +++ b/test/okf-reexport-roundtrip.test.ts @@ -45,6 +45,7 @@ describe("OKF re-export honesty round-trips", () => { expect(meta.title).toBe("Edited"); expect(meta.description).toBe("edited summary"); expect(meta["x-llmwiki"]).toBeDefined(); + expect(meta["x-okf"]).toBeUndefined(); // durable llmwiki-side record, never re-emitted to OKF }); it("regenerates exactly one # Citations section across export -> import -> export", async () => {