29. Store health checks as search filters; outdated write-ups are the owner’s to regenerate#
Date: 2026-10-26 (#262)
Status#
Accepted. Stage 2 of the #104 roadmap (“a timer-driven lint pass that only reports”), part of scrap as an LLM wiki (#263); covers the “Vault lint” gap of #172. Builds on 27. Claude’s write-ups of boards, apart from the text and 28. Claude-maintained topic pages.
Context#
A store that grows by quick captures collects loose ends: pages nothing links to, pages with no
tags, todos nobody has touched for months, the same photo captured twice, [[links]] to pages
that are gone, and Claude’s derived data (board write-ups, topic syntheses, related pages) left
behind by edits or a prompt change. The owner needs to see them, and Claude’s derived data needs
to catch up without a prompt change making every page due at once.
Decision#
A finding is a search filter, not a stored report. Each check is an
is:orhas:term:is:orphan,is:untagged,is:stale,is:duplicate,has:dangling,has:open-questionsandis:outdated. They are SQL fragments in one module,store/health.py, used by search, so a finding is always current, works in the PWA’s search and MCPsearch, and needs no table, migration or new MCP tool.-before one excludes, as for any filter.Machine-made pages are left out where a finding would be noise. Topic pages (Claude’s, with no tags and an empty body) are never untagged, and a topic with a source in and its sources are never orphans. gc’s Orphaned assets pages (a capture of source
gc) are never untagged, stale, orphans or duplicates, and a file shared with one, or with a page in the trash, is no duplicate. An open todo due today or later is not stale: its due date brings it up. A page link is any[[...]]the server parses as one, outside code and not an embed; this includes forms the PWA draws as plain text ([[Some Title]]), sohas:danglingfinds links that don’t work. A Markdown link or autolink to a page’s address counts too (#290):/p/<slug>as a path, or on any origin inSCRAP_PUBLIC_URL(models and MCP clients link pages by full address). They are found through a SQL functionwiki_linksregistered on each connection, so a check is one pass over the pages; backlinks and MCPread’slinksuse the same parser,markdown.wiki_targets.Open questions are parsed from the body.
has:open-questionsfinds a page with anOpen questionssection still open (rule in the search architecture page). A SQL functionopen_questions(open_questions.py, on the Markdown parser) reads the section, and only a text that says “open questions” is parsed at all; nothing is stored.A Store health screen, counts only. Settings has Store health: each check with its count (
GET /api/health, onecount(*)a check), a tap running its search. Nothing is dismissed: a check that keeps finding a page the owner is happy with is the check to change.One definition of outdated per kind of derived data, in
store/health.py, shared with what refreshes it: a board write-up (writeups.OUTDATED, beside the worker’sDUE: a board image read, or the page’s text, title, kind or tags changed, after it, or another prompt version, or its text links a page in the trash or gone (#310)), a topic synthesis (the weekly pass’sTOPIC_DUE) and related pages (the nightly pass’sSTALE).is:outdatedlists only outdated write-ups (#291): what the owner acts on, with Regenerate orscrap enrich --writeups --outdated. The current write-up prompt version is given to the store by whoever makes it (Store.prompts,enrich.prompt_versions()), since the store sits below the prompts. Related pages and topic syntheses keep their definitions for the passes but are no finding: they clear on their own overnight or within the week, and as findings they buried the write-ups (right after a write-up is regenerated, its page’s related picks are older than it).An outdated write-up is the owner’s to regenerate. Only a newly read board photo writes one again on its own (as before). An edit to the page, or a new prompt, does not: an edit may be small, a write-up is an Opus call, and the owner may prefer the old one. The page’s write-up carries an
outdatedflag (the same SQL), and its card says quietly “The page, a page it links to, or Claude’s prompt has changed since this write-up.” beside Regenerate.scrap enrich --writeups --outdatedwrites again every outdated one (with--limit,--dry-run, the budget and the pause): how a new prompt is rolled out. The nightly related pass and the weekly topics pass are unchanged: related pages and topic syntheses stay automatic, as do summaries (enrich on save).No model-assisted lint call. Contradictions and open questions are in each topic’s synthesis, new topics come from weekly discovery, and suggested links from the related pass. A separate lint call would repeat them.
Consequences#
The checks cost no stored state and no Claude spend; the counts are cheap queries.
A new check is a fragment and a line in
CHECKS(and its name in the PWA’schecks.ts), and is documented with the other filters and in the MCPsearchdescription.is:outdatedis only work that waits for the owner. A related pass or topics pass that keeps failing does not show in Store health; the passes’ own logs and exit status say so. A write-up prompt change lists every board until it is rolled out with--writeups --outdated.No timer, CronJob or chart value changes: the scheduled jobs are as before (Scheduled jobs).
The checks are word- and link-based. If they prove too weak, semantic search (#53) is the next step, and a model-assisted check can be revisited.