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: or has: term: is:orphan, is:untagged, is:stale, is:duplicate, has:dangling, has:open-questions and is: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 MCP search, 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]]), so has:dangling finds 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 in SCRAP_PUBLIC_URL (models and MCP clients link pages by full address). They are found through a SQL function wiki_links registered on each connection, so a check is one pass over the pages; backlinks and MCP read’s links use the same parser, markdown.wiki_targets.

  • Open questions are parsed from the body. has:open-questions finds a page with an Open questions section still open (rule in the search architecture page). A SQL function open_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, one count(*) 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’s DUE: 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’s TOPIC_DUE) and related pages (the nightly pass’s STALE).

  • is:outdated lists only outdated write-ups (#291): what the owner acts on, with Regenerate or scrap 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 outdated flag (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 --outdated writes 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’s checks.ts), and is documented with the other filters and in the MCP search description.

  • is:outdated is 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.