Skip to content
Open
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
8 changes: 4 additions & 4 deletions src/lib/__tests__/lint-checks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,31 +84,31 @@ describe("getOnDiskSlugs", () => {
expect(slugs.sort()).toEqual(["alpha", "beta"]);
});

it("returns slugs from .md files excluding index.md and log.md (fallback)", async () => {
it("returns empty array when page-index is absent even if .md files exist", async () => {
const wikiDir = process.env.WIKI_DIR!;
await fs.writeFile(path.join(wikiDir, "alpha.md"), "# Alpha\n\nContent");
await fs.writeFile(path.join(wikiDir, "beta.md"), "# Beta\n\nContent");
await fs.writeFile(path.join(wikiDir, "index.md"), "# Index\n\n- alpha");
await fs.writeFile(path.join(wikiDir, "log.md"), "# Log\n\n- entry");

const slugs = await getOnDiskSlugs();
expect(slugs.sort()).toEqual(["alpha", "beta"]);
expect(slugs).toEqual([]);
});

it("returns empty array when directory does not exist", async () => {
const slugs = await getOnDiskSlugs();
expect(slugs).toEqual([]);
});

it("ignores non-.md files (fallback)", async () => {
it("returns empty array when page-index is absent regardless of file types on disk", async () => {
const wikiDir = process.env.WIKI_DIR!;
await fs.writeFile(path.join(wikiDir, "page.md"), "# Page\n\nContent");
await fs.writeFile(path.join(wikiDir, "readme.txt"), "Not a wiki page");
await fs.writeFile(path.join(wikiDir, "data.json"), "{}");
await fs.writeFile(path.join(wikiDir, ".hidden"), "secret");

const slugs = await getOnDiskSlugs();
expect(slugs).toEqual(["page"]);
expect(slugs).toEqual([]);
});

it("returns empty array for an empty directory", async () => {
Expand Down
29 changes: 28 additions & 1 deletion src/lib/__tests__/lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import os from "os";
import path from "path";
import { writeWikiPage, updateIndex, ensureDirectories, readLog } from "../wiki";
import type { IndexEntry } from "../types";
import { _resetStorage } from "../storage";
import { _resetStorage, getStorage } from "../storage";

// Mock the LLM module so lint never calls the real API
vi.mock("../llm", () => ({
Expand Down Expand Up @@ -90,6 +90,9 @@ describe("lint", () => {
{ slug: "hello", title: "Hello", summary: "A greeting page" },
];
await updateIndex(entries);
await getStorage().putIndex("pages", {
"hello": { slug: "hello", title: "Hello", summary: "A greeting page" },
});

const result = await lint();

Expand All @@ -109,6 +112,9 @@ describe("lint", () => {
);
// Create an empty index with no entries
await updateIndex([]);
await getStorage().putIndex("pages", {
"orphan": { slug: "orphan", title: "Orphan", summary: "s" },
});

const result = await lint();
const orphanIssues = result.issues.filter((i) => i.type === "orphan-page");
Expand Down Expand Up @@ -140,6 +146,9 @@ describe("lint", () => {
{ slug: "empty", title: "Empty Page", summary: "Barely anything here" },
];
await updateIndex(entries);
await getStorage().putIndex("pages", {
"empty": { slug: "empty", title: "Empty Page", summary: "Barely anything here" },
});

const result = await lint();
const emptyIssues = result.issues.filter((i) => i.type === "empty-page");
Expand All @@ -164,6 +173,10 @@ describe("lint", () => {
{ slug: "beta", title: "Beta Topic", summary: "Beta page" },
];
await updateIndex(entries);
await getStorage().putIndex("pages", {
"alpha": { slug: "alpha", title: "Alpha", summary: "Alpha page" },
"beta": { slug: "beta", title: "Beta Topic", summary: "Beta page" },
});

const result = await lint();
const crossRefIssues = result.issues.filter(
Expand Down Expand Up @@ -277,6 +290,10 @@ describe("lint", () => {
{ slug: "neural-network", title: "Neural Network", summary: "NN page" },
];
await updateIndex(entries);
await getStorage().putIndex("pages", {
"intro": { slug: "intro", title: "Intro", summary: "Intro page" },
"neural-network": { slug: "neural-network", title: "Neural Network", summary: "NN page" },
});

const result = await lint();
const crossRefIssues = result.issues.filter(
Expand Down Expand Up @@ -905,6 +922,10 @@ Every page must start with a level-1 heading.
{ slug: "transformer", title: "Transformer", summary: "Test" },
{ slug: "bert", title: "BERT", summary: "Test" },
]);
await getStorage().putIndex("pages", {
"transformer": { slug: "transformer", title: "Transformer", summary: "Test" },
"bert": { slug: "bert", title: "BERT", summary: "Test" },
});

const result = await lint();
const conceptIssues = result.issues.filter(
Expand Down Expand Up @@ -989,6 +1010,9 @@ Every page must start with a level-1 heading.
await updateIndex([
{ slug: "linker", title: "Linker", summary: "Test" },
]);
await getStorage().putIndex("pages", {
"linker": { slug: "linker", title: "Linker", summary: "Test" },
});

const result = await lint();
const brokenLinkIssues = result.issues.filter(
Expand Down Expand Up @@ -1069,6 +1093,9 @@ describe("lint with LintOptions", () => {
{ slug: "ghost", title: "Ghost", summary: "Does not exist on disk" },
];
await updateIndex(entries);
await getStorage().putIndex("pages", {
"orphan-only": { slug: "orphan-only", title: "Orphan Only", summary: "s" },
});

const result = await lint({ checks: ["orphan-page"] });

Expand Down
10 changes: 9 additions & 1 deletion src/lib/__tests__/maintenance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { createThread, addComment } from "../talk";
import { scanForMaintenance, rebuildDerivedIndexes } from "../maintenance";
import { listCommonsPages } from "../commons";
import { _resetStorage } from "../storage";
import { _resetStorage, getStorage } from "../storage";

let tmpDir: string;
const saved: Record<string, string | undefined> = {};
Expand Down Expand Up @@ -241,6 +241,10 @@ describe("scanForMaintenance", () => {
path.join(wikiDir, "orphan-page.md"),
"# Orphan\n\nThis page exists on disk but is not in the index.",
);
await getStorage().putIndex("pages", {
"indexed-page": { slug: "indexed-page", title: "indexed-page", summary: "s" },
"orphan-page": { slug: "orphan-page", title: "Orphan", summary: "s" },
});
const tasks = await scanForMaintenance();
expect(tasks).toContainEqual({
kind: "maintain",
Expand Down Expand Up @@ -334,6 +338,10 @@ describe("scanForMaintenance", () => {
logOp: "ingest",
crossRefSource: null,
});
await getStorage().putIndex("pages", {
"artificial-intelligence": { slug: "artificial-intelligence", title: "Artificial Intelligence", summary: "s" },
"machine-learning": { slug: "machine-learning", title: "Machine Learning", summary: "s" },
});
const tasks = await scanForMaintenance();
expect(tasks).toContainEqual({
kind: "maintain",
Expand Down
12 changes: 11 additions & 1 deletion src/lib/__tests__/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import {
} from "../../mcp";
import { vaultIdFor, listVaults, getVault, createVault } from "../vault";
import { readWikiPageWithFrontmatter } from "../wiki";
import { _resetStorage } from "../storage";
import { _resetStorage, getStorage } from "../storage";
import { _resetConfigCache } from "../config";
import { parseFrontmatter } from "../frontmatter";
import { registerAgent } from "../agents";
Expand Down Expand Up @@ -2496,6 +2496,11 @@ describe("lint_wiki", () => {
);
await writeIndex([]); // empty index

// Seed page-index so getOnDiskSlugs() sees the orphan page
await getStorage().putIndex("pages", {
"orphan-page": { slug: "orphan-page", title: "Orphan Page", summary: "s" },
});

const result = await handleLintWiki({ checks: ["orphan-page"] });
expect(result.issues.length).toBeGreaterThanOrEqual(1);
const orphanIssues = result.issues.filter((i) => i.type === "orphan-page");
Expand Down Expand Up @@ -4782,6 +4787,11 @@ describe("maintenance_scan", () => {
);
await writeIndex([]); // empty index → orphan-maint is an orphan

// Seed page-index so getOnDiskSlugs() sees the orphan page
await getStorage().putIndex("pages", {
"orphan-maint": { slug: "orphan-maint", title: "Orphan Maintenance", summary: "s" },
});

const result = await handleMaintenanceScan({});
expect(result).toHaveProperty("tasks");
expect(Array.isArray(result.tasks)).toBe(true);
Expand Down
30 changes: 6 additions & 24 deletions src/lib/lint-checks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { getStorage } from "./storage";
import type { FileEntry } from "./storage";
import { readWikiPage, readWikiPageWithFrontmatter, listWikiPages, wikiRelPath } from "./wiki";
import { readWikiPage, readWikiPageWithFrontmatter, listWikiPages } from "./wiki";
import { hasLLMKey, callLLM } from "./llm";
import { loadPageConventions } from "./schema";
import { extractWikiLinks } from "./links";
Expand Down Expand Up @@ -38,35 +36,19 @@ export const INFRASTRUCTURE_FILES = new Set(["index.md", "log.md"]);
/**
* Get all content page slugs known to the system.
*
* Primary path: read from the `_idx:pages` page-index (O(1) KV read), which
* aligns lint with the silo-primary read path used by `readWikiPage` /
* `listWikiPages`.
*
* Fallback (page-index not yet seeded): list `.md` files from the flat wiki
* directory, excluding infrastructure files. This preserves correct orphan /
* stale-index detection before the first `rebuildPageIndex()` run.
*
* Reads from the `_idx:pages` page-index (O(1) KV read), which aligns lint
* with the silo-primary read path used by `readWikiPage` / `listWikiPages`.
* Returns `[]` when the page-index is absent or unreadable.
*/
export async function getOnDiskSlugs(): Promise<string[]> {
try {
const idx = await getPageIndex();
if (idx) return Object.keys(idx);
} catch (err) {
logger.warn("lint", "page-index read failed; falling back to listFiles", err);
}

// Fallback: list flat wiki directory
let entries: FileEntry[];
try {
entries = await getStorage().listFiles(wikiRelPath(""));
} catch (err) {
logger.warn("lint", "listFiles wiki directory failed:", err);
return [];
logger.warn("lint", "page-index read failed", err);
}

return entries
.filter((e) => e.name.endsWith(".md") && !INFRASTRUCTURE_FILES.has(e.name))
.map((e) => e.name.replace(/\.md$/, ""));
return [];
}

/**
Expand Down
Loading