Skip to main content

Wiki lint — the honesty loop

The lint pass (PLT-200) keeps a compounding wiki space truthful: contradictions, stale claims, orphan pages, hallucinated cross-references, and rotten provenance become queryable structured rows in wiki.lint_findings — never prose buried in a page body that a future ingest would overwrite. Retrieval can join against open findings and down-weight affected pages (ranking integration lands with PLT-201).

It is one of the two preconditions (with source retraction, PLT-204) for ever enabling autonomous ingest on a space.

Since PLT-255, wiki.lint_findings is the single finding store for the whole module: the lost_source findings written by source retraction live in the same table. The canonical needs-review contract is one query — a page needs review iff wiki.lint_findings has at least one row with page_id = <page> and status = 'open' — surfaced as openFindings on GET /api/pages/:id and as the ⚠ needs review (<check types>) annotation in the rebuilt space index.

Data model

One row per finding in wiki.lint_findings:

ColumnMeaning
space_idThe linted space.
page_idThe page the finding is about (nullable — space-level findings have no subject page).
target_page_idThe other page involved: the contradicting page, or the drifted provenance target.
check_typeOne of the check types below.
severityinfo / warning / error.
statusopenresolved / dismissed. A status flip with attribution — never deleted.
detailActionable description quoting/locating the offending claims.
detail_jsonMachine-readable payload (PLT-255). For lost_source: { retractedPageId, reason, depth, retypedFrom? }; for lost_space_reference: { spaceId, spaceSlug, reason, lostLinkCount }; {} otherwise.
detected_bywiki-lint for the mechanical pass; an agent principal (e.g. wiki-curator) otherwise.
page_revision_numRevision of page_id at judgement time (staleness anchor).

At most one open finding per (space, check type, page, target page) — enforced by a NULLS NOT DISTINCT partial unique index. Resolved/dismissed history accumulates freely. lost_source is the one exception: a page can derive from several retracted sources, so its open-dedup unit is (page, retracted source) instead — enforced by a dedicated partial unique index over detail_json->>'retractedPageId' (the retracted page cannot be the target_page_id anchor: it is soft-deleted, and the RLS visibility gate would hide the finding).

Findings are RLS-protected like page_links: a finding referencing a page above the caller's clearance (or soft-deleted) is invisible, so the findings table cannot serve as an oracle for classified pages.

Space-level findings (no page_id / target_page_id) have no page for RLS to clearance-gate and are visible to every tenant member — the same effective classification as the space's reserved log/index pages (spaces carry no classification). Writers must therefore keep space-level detail text at UNCLASSIFIED discipline; a finding about classified content must be anchored to the classified page via page_id so the policy hides it from under-cleared readers. This is enforced as a convention (wiki-curator hard rule 7), mirroring the existing convention for the space log.

Check types

Mechanical — computed by POST /api/spaces/:spaceId/lint; machine-owned (the recording endpoint rejects them):

Check typeDetects
orphan_pageA page with no parent, no children, and no typed links in either direction (reserved index/log pages excluded).
broken_crossrefAn [[…|id:<uuid>]] wikilink whose target page does not exist or is not visible — the hallucinated-link case.
provenance_driftA derived_from link whose target has newer revisions than the link's source_revision_num — the claim may be unsupported.

The mechanical pass is convergent: re-running it opens findings for new defects, refreshes the detail of persisting ones, and auto-resolves (resolved_by = 'wiki-lint') findings that no longer reproduce.

Curator-judged — recorded by the wiki-curator subagent via POST /api/spaces/:spaceId/lint/findings after reading the pages:

Check typeMeaning
contradictionTwo pages assert incompatible claims — including cross-source (different derived_from).
stale_claimA claim the rest of the space / world shows is no longer true. Recording this finding is how a page is marked stale — the page body is never edited to say so.
missing_conceptAn entity/concept referenced repeatedly but never given a page (space-level).
missing_crossrefTwo clearly related pages with no link between them.
data_gapA question the space should answer but cannot.

System — written only by database-side machinery; listable and resolvable through the lint surface, never recordable through it:

Check typeMeaning
lost_sourceThe page derives (transitively) from a retracted source (PLT-204/PLT-255). Inserted by the SECURITY DEFINER wiki.retract_source_execute; detail_json carries the retracted page id, reason, and depth.
lost_space_referenceThe page links into a space that was decommissioned (PLT-205) — its reference target has been torn down and the referencing claims need re-verifying. Inserted by the SECURITY DEFINER wiki.decommission_space on every external live page that linked into the dead space, anchored to the external page's own space, severity warning. detail_json carries { spaceId, spaceSlug, reason, lostLinkCount }. One open finding per external page (a second decommission touching an already-flagged page de-dupes).

REST surface

All routes are tenant-auth wrapped; write paths require the wiki:page:write permission.

RouteBehaviour
POST /api/spaces/:spaceId/lintRun the mechanical pass; returns the run report (counts).
GET /api/spaces/:spaceId/lint/findingsList findings — filter by status, checkType, pageId; paginated.
POST /api/spaces/:spaceId/lint/findingsRecord a curator-judged finding (409 on duplicate open shape).
PATCH /api/spaces/:spaceId/lint/findings/:findingIdResolve or dismiss an open finding (status flip, attributed).

MCP equivalents (constellation MCP server, REST-only clients): lint_space, list_lint_findings, record_lint_finding, resolve_lint_finding.

Non-destructive autonomy

The entire lint surface is non-destructive by construction (design review R6):

  • Findings are inserted or status-flipped — there is no delete path.
  • The lint pass never mutates linted pages. Marking stale = an open stale_claim finding.
  • Page deletes and synthesis-rewrites stay human-gated even in spaces whose ingest_policy.autonomy is autonomous. Because lint performs no gated write, it runs in both human_approved and autonomous spaces.

Visibility caveat

The mechanical scan runs under the caller's RLS context (tenant + clearance). A wikilink target that exists but is classified above the linting caller's clearance is reported as broken_crossref (a false positive); a reference to a soft-deleted page is a true positive. Run lint with a clearance that covers the space's content, or have the curator flag suspected classification false positives in its verdict instead of resolving them.

The wiki-curator subagent

.claude/agents/wiki-curator.md drives the loop on a documented cadence (weekly, per-release, and ad-hoc — mirroring memory-curator): run lint_space → judge drift candidates → sweep for semantic defects → record findings → re-check open findings → append_space_log (action: "lint") → emit the standard verdict bar. Its only page writes are the reserved maintenance surfaces (space log and index).