Context
Part A of 3 in the flat retirement split (parent: #869, grandparent: #866).
No dependencies on other parts — can be built independently.
This issue makes writeWikiPage, readWikiPage, and wikiPageExists in src/lib/wiki.ts use the silo path exclusively. The flat fallback is removed from reads, and writes default to DEFAULT_TENANT when no tenant is specified.
Step-by-step Implementation Plan
Step 1: Default writeWikiPage to DEFAULT_TENANT (src/lib/wiki.ts)
Function: writeWikiPage (line 424)
Replace lines 432–434:
// BEFORE (lines 432-434):
const storagePath = tenant
? tenantWikiRelPath(tenant, `${slug}.md`)
: wikiRelPath(`${slug}.md`);
// AFTER:
const effectiveTenant = tenant ?? DEFAULT_TENANT;
const storagePath = tenantWikiRelPath(effectiveTenant, `${slug}.md`);
Also update the saveRevision call (line 443) to pass the effective tenant:
// BEFORE:
await saveRevision(slug, existing, author, reason, tenant);
// AFTER:
await saveRevision(slug, existing, author, reason, effectiveTenant);
Why: Every writeWikiPage call now writes to a silo path. Calls that omit tenant will write to tenants/yopedia/wiki/ instead of flat wiki/.
Step 2: Remove flat fallback from readWikiPage (src/lib/wiki.ts)
Function: readWikiPage (line 326)
Remove the flatPath variable (line 340), remove the if (pageIdx) gate (line 349), and remove the entire "Flat fallback" block (lines 365–374). The function should always resolve via the silo path.
Replace lines 339–378 with:
const storage = getStorage();
// Silo-only: resolve tenant path via O(1) page-index lookup.
// We must NOT call tenantForSlug() — its slow path triggers
// listWikiPages → scanWikiPagesUncached → readWikiPageWithFrontmatter →
// readWikiPage → infinite recursion.
let content: string | null = null;
const pageIdx = await getPageIndex();
const entry = pageIdx?.[slug];
const tenant = tenantForOwner(entry?.owner);
const siloPath = tenantWikiRelPath(tenant, `${slug}.md`);
const actualPath = path.join(getDataDir(), siloPath);
try {
content = await storage.readFile(siloPath);
} catch (e) {
if (!isEnoent(e)) {
logger.warn("wiki", `readWikiPage failed for "${slug}":`, e);
}
if (pageCache !== null) {
pageCache.set(slug, null);
}
return null;
}
Keep lines 380+ unchanged (title extraction, cache set, return). The actualPath is now a const assigned from the silo path — no flatPath variable needed.
Step 3: Remove flat fallback from wikiPageExists (src/lib/wiki.ts)
Function: wikiPageExists (line 288)
Remove the if (pageIdx) gate (line 300), the entire "Flat fallback" block (lines 315–322). Replace lines 296–322 with:
// Silo-only: resolve tenant path via O(1) page-index lookup.
const pageIdx = await getPageIndex();
const entry = pageIdx?.[slug];
const tenant = tenantForOwner(entry?.owner);
const siloPath = tenantWikiRelPath(tenant, `${slug}.md`);
try {
await storage.readFile(siloPath);
return true;
} catch (e) {
if (isEnoent(e)) return false;
throw e;
}
Step 4: Update tests in src/lib/__tests__/wiki.test.ts
4a — "readWikiPage falls back to flat when silo file is missing" (line 2110):
This test wrote to flat wiki/flat-only.md with a page-index entry pointing to a tenant, then expected the read to succeed via flat fallback. Now reads are silo-only, so this should return null.
Rename the test to "readWikiPage returns null when silo file is missing" and change assertion:
it("readWikiPage returns null when silo file is missing", async () => {
const storage = (await import("../storage")).getStorage();
await storage.writeFile(
`wiki/flat-only.md`,
"# Flat Only\n\nStill on flat.",
);
// Seed page index — silo path will 404
await storage.putIndex("pages", {
"flat-only": { slug: "flat-only", title: "Flat Only", summary: "s", owner: "bob" },
});
const page = await readWikiPage("flat-only");
expect(page).toBeNull();
});
4b — "readWikiPage reads from flat when page-index is absent" (line 2131):
Without a page-index, getPageIndex() returns null, so entry is undefined and tenantForOwner(undefined) returns DEFAULT_TENANT. The file must be at the silo path to be found.
Change the write path and assertions:
it("readWikiPage reads from silo default tenant when page-index is absent", async () => {
// No page index seeded — getPageIndex() returns null
// tenantForOwner(undefined) = DEFAULT_TENANT = "yopedia"
const storage = (await import("../storage")).getStorage();
await storage.writeFile(
`tenants/yopedia/wiki/no-index.md`,
"# No Index\n\nDirect silo read.",
);
const page = await readWikiPage("no-index");
expect(page).not.toBeNull();
expect(page!.title).toBe("No Index");
expect(page!.content).toContain("Direct silo read.");
expect(page!.path).toBe(
path.join(tmpDir, "tenants", "yopedia", "wiki", "no-index.md"),
);
});
4c — "wikiPageExists falls back to flat when silo missing" (line 2163):
Rename to "wikiPageExists returns false when silo file is missing" and change assertion:
it("wikiPageExists returns false when silo file is missing", async () => {
const storage = (await import("../storage")).getStorage();
await storage.putIndex("pages", {
"exists-flat": { slug: "exists-flat", title: "E", summary: "e", owner: "dave" },
});
await storage.writeFile(
`wiki/exists-flat.md`,
"# Exists\n\nFlat.",
);
const exists = await (await import("../wiki")).wikiPageExists("exists-flat");
expect(exists).toBe(false);
});
4d — "writes to flat path when tenant is omitted (backward compat)" (line 2200):
Now writing without a tenant defaults to DEFAULT_TENANT = silo. Change to verify the silo path:
it("writes to DEFAULT_TENANT silo when tenant is omitted", async () => {
await ensureDirectories();
await writeWikiPage("flat-write", "# Flat\n\nContent.");
const storage = (await import("../storage")).getStorage();
const content = await storage.readFile("tenants/yopedia/wiki/flat-write.md");
expect(content).toBe("# Flat\n\nContent.");
});
Gotchas
- Do NOT remove
wikiRelPath from wiki.ts — it is still used by ensureDirectories (line 214), updateIndexUnsafe (line ~663), and readIndexBaseEntries (line ~542) for infrastructure files.
- The
getWikiDir re-export must stay — other files import it.
- The
actualPath variable in readWikiPage changes from let to const since there is no longer a flat reassignment path.
tenantForOwner(undefined) returns DEFAULT_TENANT ("yopedia") — this is how ownerless pages resolve in reads.
Acceptance Criteria
Size Estimate
Medium — 2 files, ~60 lines deleted, ~30 lines added, 4 test cases updated
Context
Part A of 3 in the flat retirement split (parent: #869, grandparent: #866).
No dependencies on other parts — can be built independently.
This issue makes
writeWikiPage,readWikiPage, andwikiPageExistsinsrc/lib/wiki.tsuse the silo path exclusively. The flat fallback is removed from reads, and writes default toDEFAULT_TENANTwhen no tenant is specified.Step-by-step Implementation Plan
Step 1: Default
writeWikiPagetoDEFAULT_TENANT(src/lib/wiki.ts)Function:
writeWikiPage(line 424)Replace lines 432–434:
Also update the
saveRevisioncall (line 443) to pass the effective tenant:Why: Every
writeWikiPagecall now writes to a silo path. Calls that omittenantwill write totenants/yopedia/wiki/instead of flatwiki/.Step 2: Remove flat fallback from
readWikiPage(src/lib/wiki.ts)Function:
readWikiPage(line 326)Remove the
flatPathvariable (line 340), remove theif (pageIdx)gate (line 349), and remove the entire "Flat fallback" block (lines 365–374). The function should always resolve via the silo path.Replace lines 339–378 with:
Keep lines 380+ unchanged (title extraction, cache set, return). The
actualPathis now aconstassigned from the silo path — noflatPathvariable needed.Step 3: Remove flat fallback from
wikiPageExists(src/lib/wiki.ts)Function:
wikiPageExists(line 288)Remove the
if (pageIdx)gate (line 300), the entire "Flat fallback" block (lines 315–322). Replace lines 296–322 with:Step 4: Update tests in
src/lib/__tests__/wiki.test.ts4a — "readWikiPage falls back to flat when silo file is missing" (line 2110):
This test wrote to flat
wiki/flat-only.mdwith a page-index entry pointing to a tenant, then expected the read to succeed via flat fallback. Now reads are silo-only, so this should returnnull.Rename the test to
"readWikiPage returns null when silo file is missing"and change assertion:4b — "readWikiPage reads from flat when page-index is absent" (line 2131):
Without a page-index,
getPageIndex()returns null, soentryis undefined andtenantForOwner(undefined)returnsDEFAULT_TENANT. The file must be at the silo path to be found.Change the write path and assertions:
4c — "wikiPageExists falls back to flat when silo missing" (line 2163):
Rename to
"wikiPageExists returns false when silo file is missing"and change assertion:4d — "writes to flat path when tenant is omitted (backward compat)" (line 2200):
Now writing without a tenant defaults to
DEFAULT_TENANT= silo. Change to verify the silo path:Gotchas
wikiRelPathfrom wiki.ts — it is still used byensureDirectories(line 214),updateIndexUnsafe(line ~663), andreadIndexBaseEntries(line ~542) for infrastructure files.getWikiDirre-export must stay — other files import it.actualPathvariable inreadWikiPagechanges fromlettoconstsince there is no longer a flat reassignment path.tenantForOwner(undefined)returnsDEFAULT_TENANT("yopedia") — this is how ownerless pages resolve in reads.Acceptance Criteria
writeWikiPage("slug", content)(no tenant) writes totenants/yopedia/wiki/slug.md, NOTwiki/slug.mdreadWikiPage("slug")never reads fromwiki/<slug>.md— only fromtenants/<tenant>/wiki/<slug>.mdwikiPageExists("slug")never reads from flatwiki/<slug>.mdreadWikiPageorwikiPageExistspnpm buildpassespnpm testpassesSize Estimate
Medium — 2 files, ~60 lines deleted, ~30 lines added, 4 test cases updated