Lint Wiki
wiki.lintHealth-check your Obsidian wiki to detect orphans, broken links, stale sources, missing concept pages, singleton tags, and index.md parity issues. Get grouped findings with totals.
Instructions
Health-check the wiki: orphans, broken links, stale sources, missing concept pages, singleton tags, and index.md parity. Read-only; returns grouped findings with totals.
Operates on the session-active vault (see vault.current — selectable via vault.select) unless an explicit vaultPath argument is passed, which always wins.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| staleDays | No | ||
| wikiRoot | No | ||
| vaultPath | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/domain/wiki/lint.ts:105-251 (handler)Main handler function `lintWiki` that performs the wiki linting logic: checks for orphans, broken links, stale pages, missing concept/entity/source pages, singleton tags, and index.md parity. Returns structured findings with totals.
export async function lintWiki(context: DomainContext, args: WikiLintArgs) { const paths = resolveWikiPaths(context, args); const staleDays = args.staleDays ?? context.env.KOBSIDIAN_WIKI_STALE_DAYS; const noteIndex: NoteIndex = await collectNoteIndex(paths.vaultRoot); const wikiFiles = await loadWikiFiles(paths); const wikiPages = wikiFiles.filter( (file) => file.classification === "source" || file.classification === "concept" || file.classification === "entity", ); const pagePaths = new Set(wikiPages.map((file) => file.relative)); const inlinks = new Map<string, Set<string>>(); const outlinks = new Map<string, Set<string>>(); for (const page of wikiPages) { inlinks.set(page.relative, new Set()); outlinks.set(page.relative, new Set()); } const brokenLinks: BrokenLinkFinding[] = []; const missingPages: MissingPageFinding[] = []; for (const file of wikiFiles) { const sourceOutlinks = outlinks.get(file.relative); for (const link of extractLinksFromContent(file.content)) { if (link.type !== "wiki") continue; const normalized = normalizeLinkTarget(link.path); const resolved = resolveIndexedLink(normalized, noteIndex); if (!resolved) { brokenLinks.push({ sourcePath: file.relative, brokenLink: normalized, displayText: link.displayText, }); if (isInsideWiki(paths, normalized)) { missingPages.push({ sourcePath: file.relative, target: normalized, suggestedKind: classifyMissingTarget(paths, normalized), }); } continue; } if (pagePaths.has(resolved) && sourceOutlinks) { sourceOutlinks.add(resolved); inlinks.get(resolved)?.add(file.relative); } } } const orphans: OrphanFinding[] = []; for (const page of wikiPages) { const inCount = inlinks.get(page.relative)?.size ?? 0; const outCount = outlinks.get(page.relative)?.size ?? 0; if (inCount === 0 && outCount === 0) { orphans.push({ path: page.relative, reason: "No inbound or outbound wiki links" }); } } const stale: StaleFinding[] = []; for (const page of wikiPages) { const dateField = page.classification === "source" ? "ingested_at" : "updated"; const dateValue = coerceDateString(page.frontmatter[dateField]); if (!dateValue) continue; const age = ageInDays(dateValue); if (age > staleDays) { stale.push({ path: page.relative, kind: page.classification, ageDays: age, dateField }); } } const tagPages = new Map<string, string[]>(); for (const page of wikiPages) { for (const tag of getFrontmatterTags(page.frontmatter)) { const list = tagPages.get(tag) ?? []; list.push(page.relative); tagPages.set(tag, list); } } const tagSingletons: TagSingletonFinding[] = []; for (const [tag, pagesWithTag] of tagPages.entries()) { if (pagesWithTag.length === 1 && pagesWithTag[0]) { tagSingletons.push({ tag, page: pagesWithTag[0] }); } } tagSingletons.sort((left, right) => left.tag.localeCompare(right.tag)); const indexTargets = await readIndexLinkTargets(paths); const missingFromIndex: string[] = []; for (const page of wikiPages) { const normalized = normalizeLinkTarget(page.relative); const stem = path.basename(page.relative, ".md"); const inIndex = indexTargets.has(normalized) || indexTargets.has(stem) || indexTargets.has(page.relative) || indexTargets.has(`${page.relative.replace(/\.md$/, "")}`); if (!inIndex) missingFromIndex.push(page.relative); } const staleEntries: string[] = []; for (const target of indexTargets) { if (!resolveIndexedLink(target, noteIndex)) staleEntries.push(target); } const findings: WikiLintFindings = { orphans, brokenLinks, stale, missingPages, tagSingletons, indexMismatch: { missingFromIndex, staleEntries }, }; const totals = { orphans: orphans.length, brokenLinks: brokenLinks.length, stale: stale.length, missingPages: missingPages.length, tagSingletons: tagSingletons.length, indexMissingFromIndex: missingFromIndex.length, indexStaleEntries: staleEntries.length, all: orphans.length + brokenLinks.length + stale.length + missingPages.length + tagSingletons.length + missingFromIndex.length + staleEntries.length, }; const summary = totals.all === 0 ? `Wiki clean (${wikiPages.length} pages scanned)` : `Wiki lint: ${totals.all} findings across ${wikiPages.length} pages`; return { changed: false, target: paths.rootRelative, summary, pagesScanned: wikiPages.length, staleDays, totals, findings, }; } - src/schema/wiki.ts:115-120 (schema)Input schema `wikiLintArgsSchema` for the wiki.lint tool: accepts optional `staleDays` (int 1-3650), `wikiRoot` override, and `vaultPath`.
export const wikiLintArgsSchema = z.object({ staleDays: z.number().int().min(1).max(3650).optional(), wikiRoot: wikiRootOverrideSchema, vaultPath: z.string().optional(), }); export type WikiLintArgs = z.input<typeof wikiLintArgsSchema>; - src/server/tools/wiki.ts:78-87 (registration)Registration of the `wiki.lint` tool definition in the `wikiTools` array, mapping it to the `lintWiki` handler with READ_ONLY annotation.
{ name: "wiki.lint", title: "Lint Wiki", description: "Health-check the wiki: orphans, broken links, stale sources, missing concept pages, singleton tags, and index.md parity. Read-only; returns grouped findings with totals.", inputSchema: wikiLintArgsSchema, outputSchema: looseObjectSchema, annotations: READ_ONLY, handler: (context, args) => lintWiki(context, args as Parameters<typeof lintWiki>[1]), }, - src/domain/wiki/lint.ts:60-82 (helper)Helper functions `loadWikiFiles`, `readIndexLinkTargets`, `classifyMissingTarget`, `coerceDateString`, and `ageInDays` used by the lintWiki handler to load wiki files, check index links, classify missing targets, and calculate staleness.
async function loadWikiFiles(paths: WikiPaths): Promise<WikiFile[]> { if (!(await fileExists(paths.rootAbsolute))) return []; const absolutes = await walkMarkdownFiles(paths.rootAbsolute); const files = await Promise.all( absolutes.map(async (absolute): Promise<WikiFile | null> => { const relative = toVaultRelativePath(paths.vaultRoot, absolute); if (!isInsideWiki(paths, relative)) return null; const classification = classifyWikiPath(paths, relative); if (classification === "schema") return null; const content = await readUtf8(absolute); const parsed = parseFrontmatter(content); return { absolute, relative, classification, content, frontmatter: parsed.data, body: parsed.content, }; }), ); return files.filter((file): file is WikiFile => file !== null); } - src/domain/wiki/lint.ts:30-37 (helper)Type definitions for `WikiLintFindings` including orphans, brokenLinks, stale, missingPages, tagSingletons, and indexMismatch.
export type WikiLintFindings = { orphans: OrphanFinding[]; brokenLinks: BrokenLinkFinding[]; stale: StaleFinding[]; missingPages: MissingPageFinding[]; tagSingletons: TagSingletonFinding[]; indexMismatch: IndexMismatch; };