A conservative healer that catches and corrects factual drift in AI-generated content (e.g. a chapter's charter or identity number) at both the DB boundary and render.
Fact-Guard is a safety net for the one thing AI content must never get wrong: verifiable facts. A tasteful site that states the wrong charter number or a mangled name is worse than a plain one. It breaks trust, which is the top of the quality bar.
factGuardHeal($content, $biz, $opts) (includes/fact-guard.php) runs over generated content and conservatively corrects factual drift:
factGuardDeriveIdentity($name) parses a business name for a fraternal/civicidentity + number (Lodge / Post / Chapter / Council / Aerie / ... No. NNNN).
factGuardFixIdentityNumbers($data, $keyword, $number, $authoritative, $placeTokens)repairs a wrong number. The separator shape (No. / # / Number) is always eligible. The bare shape ("Chapter NNN") is corrected only when the number is authoritative (minted from the site's own address, never derived from a scraped name) and the mention is positively ours, so it never invents or over-corrects.
It is conservative by design — it fixes provable drift and leaves everything else alone. It runs at two boundaries: at generation (factGuardHeal(...) after decode in content generation) and at the DB boundary (hooked into saveGeneratedContent()), so both fresh builds and re-saves are guarded.
A fleet of fraternal-organization sites surfaced it: an AI given a chapter name would occasionally restate the charter number wrong. Fact-Guard is the general pattern: where a fact is derivable from hard input (the name, the Places data), verify the output against it. It complements the GIGO principle: GIGO says better input makes better voice; Fact-Guard says never let good input get corrupted on the way out. Extend it as new verifiable fact classes appear.
A conservative regex can no-op silently and still look right. The first corrector required a No. / Number / # separator between the keyword and the digits, so ordinary prose ("post 2020 renovation") could never be mangled. It was correct and it was tested. But the drift in the wild was uniformly the bare form ("Chapter NNN in <town>"). The heal matched nothing, changed nothing, threw nothing, and read as working for two months. Two things compounded it: the call sites passed no options, so the number was derived from the business name, which often carries none; and the generator was never handed the number (the one identity fact we mint rather than scrape) while its brief asked for a concrete founding fact. The prompt rewarded inventing precisely what nothing supplied.
A healer must report what it changed. A healer that cannot report what it changed is indistinguishable from a healer that does nothing. The stored-content repair now emits a real before/after per row.
Over-correcting is worse than the bug. The first cut of the bare-form rule rewrote every matching number to ours. On live pages that changed the number of a different organization sharing the building, of a visiting chapter, and of a sister chapter in another city. Each traded one wrong fact for a false claim about a different real organization. Enumerating "foreign" cues kept losing, so the rule was inverted: correct only a mention that is positively ours, and skip one preceded by another organization's or another chapter's cue, by "from", or by a place name that is not one of our own.
Leaving another organization's number intact is not a false claim about us. Changing
it is a false claim about them. The tie always breaks toward leaving it.
The cost is accepted: some pages never state their own number in prose. That is a gap, not a lie.
Traps worth remembering:
0042 is 42). A string compareflagged about 130 correct rows as contaminated and would have rewritten them all.
slash and unicode escaping even when nothing changed, which reported about 170 phantom rewrites.
\d{2,6} floor silently skipped them.
carries that chapter's website, email, phones, photos and social links too, and a text healer reaches none of them. A scrape of the wrong entity is untrustworthy field by field, so repair drops the derived scrape wholesale and never touches the geo-verified phone and address.
Fact-Guard is the net, not the first line. The better fix for most inaccuracies is to hand the generator the fact so it never invents a wrong one — no per-fact trap. Example (v2.2.330): an AI hero claimed a barbershop was open "Six Days" while its hours showed closed Sunday AND Monday (open 5). Root cause: the content-generation context (ai-content.php) fed name/address/rating/reviews but NOT the hours. Fix: inject the real weekday_text plus a computed "Open N days a week (closed …)" line. The model can't invent a schedule it's been handed. Operator rule: prefer accuracy by GIVING the model the fact over trapping for a specific mistake — same spirit as the muse custom_css prompt fix. Fact-Guard stays for the classes a prompt can't reliably prevent (e.g. an identity number the model mangles even when told).