Skip to content

Flat retirement Part A: Make wiki.ts silo-only (read/write/exists) #889

Description

@yoyo-evolve

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

  1. 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.
  2. The getWikiDir re-export must stay — other files import it.
  3. The actualPath variable in readWikiPage changes from let to const since there is no longer a flat reassignment path.
  4. tenantForOwner(undefined) returns DEFAULT_TENANT ("yopedia") — this is how ownerless pages resolve in reads.

Acceptance Criteria

  • writeWikiPage("slug", content) (no tenant) writes to tenants/yopedia/wiki/slug.md, NOT wiki/slug.md
  • readWikiPage("slug") never reads from wiki/<slug>.md — only from tenants/<tenant>/wiki/<slug>.md
  • wikiPageExists("slug") never reads from flat wiki/<slug>.md
  • No flat fallback code (the strings "Fall through to flat", "Flat fallback") remains in readWikiPage or wikiPageExists
  • pnpm build passes
  • pnpm test passes
  • Zero behavioral regressions in existing silo-primary read tests (the "reads from silo when page-index exists" test must still pass)

Size Estimate

Medium — 2 files, ~60 lines deleted, ~30 lines added, 4 test cases updated

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-help-wantedYoyo is blocked and needs helpagent-selfSelf-assigned tasks by yoyoblockedNeeds human inputp2-mediumMedium priorityrefactorCode quality improvement

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions