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
17 changes: 16 additions & 1 deletion src/export/collect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -81,6 +81,20 @@ function readContradictedBy(meta: Record<string, unknown>): 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<string, unknown>): XOkfSnapshot | undefined {
const x = meta["x-okf"];
if (!x || typeof x !== "object") return undefined;
const of = (x as Record<string, unknown>).originalFrontmatter;
if (!of || typeof of !== "object") return undefined;
const snap: XOkfSnapshot = { originalFrontmatter: of as Record<string, unknown> };
const t = (x as Record<string, unknown>).type;
if (typeof t === "string") snap.type = t;
const p = (x as Record<string, unknown>).okfPath;
if (typeof p === "string") snap.okfPath = p;
return snap;
}

/** Validate and return PageKind from frontmatter, or undefined. */
function readPageKind(meta: Record<string, unknown>): PageKind | undefined {
const value = meta.kind;
Expand Down Expand Up @@ -122,6 +136,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),
Expand Down
77 changes: 66 additions & 11 deletions src/export/okf/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,82 @@ 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 {
/**
* 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<readonly [keyof XLlmwiki, (p: ExportPage) => 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 = {
schemaVersion: "0.1",
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<string, unknown>)[field] = value;
}
return x;
}

// 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"];

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<string, unknown>, 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 `<pageDirectory>/<slug>.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<string, unknown> = { ...of };
for (const k of RECONSTRUCT_STRIP) delete extras[k];
const fm: Record<string, unknown> = { ...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<string, unknown> = { type: page.kind ?? "concept", "x-llmwiki": x };
applyStandardFields(fm, page);
return fm as unknown as OkfFrontmatter;
}

const WIKILINK = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
Expand Down
12 changes: 12 additions & 0 deletions src/export/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ 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;
/** Bundle-relative source path of the original OKF doc; durable across approval, for diagnosis. */
okfPath?: string;
/** Full original OKF frontmatter, verbatim. */
originalFrontmatter: Record<string, unknown>;
}


/**
* Which wiki/ subdirectory a page lives in.
Expand Down Expand Up @@ -71,6 +81,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
Expand Down
12 changes: 8 additions & 4 deletions src/import/okf-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,15 @@ function baseFields(meta: Record<string, unknown>, ctx: OkfMapContext, slug: str
};
}

/** Verbatim snapshot of the source frontmatter; records the raw `type` only when foreign. */
function buildXokf(meta: Record<string, unknown>): Record<string, unknown> {
/**
* 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<string, unknown>, okfPath: string): Record<string, unknown> {
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. */
Expand All @@ -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<string, unknown>);
fields["x-okf"] = buildXokf(meta);
fields["x-okf"] = buildXokf(meta, doc.relPath);
return fields;
}

Expand Down
5 changes: 5 additions & 0 deletions test/okf-map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
27 changes: 27 additions & 0 deletions test/okf-reexport-collect.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
48 changes: 48 additions & 0 deletions test/okf-reexport-map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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> = {}): 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<string, unknown>).vendorKey).toBe(7);
expect(fm["x-llmwiki"].contentHash).not.toBe("STALE");
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<string, unknown>).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");
});
});
34 changes: 34 additions & 0 deletions test/okf-reexport-okfpath.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
71 changes: 71 additions & 0 deletions test/okf-reexport-roundtrip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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, buildFrontmatter } 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<void> {
await mkdir(path.join(root, "wiki/concepts"), { recursive: true });
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<string, unknown>): Promise<void> {
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", () => {
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);
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);
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.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 () => {
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);
});
});
Loading