---
version: 0.32.2
feature_pitch:
  headline: "Facts join the system-of-record — your hot memory now lives in markdown"
  description: "v0.31 added hot-memory facts but they lived only in the database. v0.32.2 fences them in markdown so they survive `gbrain rebuild`. Migration v0_32_2 backfills existing facts to entity-page fences. Plus a 3-layer privacy strip and a CI gate that enforces the rule going forward."
  recipe: null
  tiers: null
---

# v0.32.2 Migration: facts join the system-of-record

## What this is

v0.32.2 closes a gap that's existed since v0.31 shipped hot-memory facts.
Facts were DB-only — drop the `facts` table and the data was gone. v0.32.2
makes them FS-canonical: every fact write lands in a `## Facts` fence on
the entity page first, then stamps the DB index. The DB is now a true
derived cache for facts.

## What the agent needs to do

**For existing v0.31 brains (most users):**

1. Run `gbrain apply-migrations --yes` to invoke the v0_32_2 orchestrator.
2. The orchestrator runs in two phases:
   - **phaseAFenceFacts** walks every legacy fact row (row_num IS NULL,
     entity_slug IS NOT NULL) and appends it to the entity's markdown
     fence atomically. Dry-run by default — runs in dry-run mode first,
     reports what WOULD happen, then runs again with `--write` after
     user confirms (or pass `--write` directly to skip the
     confirmation step).
   - **phaseBVerify** re-parses every touched fence + diffs against
     the DB. Mismatch → marks the migration partial; user runs
     `gbrain apply-migrations --force-retry 51`.
3. Refuses to run when any source's local_path has uncommitted git
   changes. Mirrors the v0.14.1 `dry-fix` safety posture so the user
   can review the diff before committing.
4. After completion: `git diff` shows the new fences; user can commit
   them and the facts now live in git alongside the rest of the brain.

**For brand-new v0.32.2 installs (rare):**

The schema migration v51 creates the columns from the start (fresh-install
parity via the v40 CREATE TABLE block). The v0_32_2 orchestrator has
nothing to backfill (no legacy rows). It runs phaseA + phaseB and
no-ops cleanly.

**For thin-client installs (no sources.local_path):**

Facts continue to write to the DB directly with a once-per-process stderr
warning naming the missing `sources.local_path`. The architecture doc
names this as the explicit DB-only exception. To enable fence writes,
configure `local_path` via the source setup.

## Things that changed for the agent

1. **`gbrain forget <id>` now rewrites markdown.** Previously `gbrain
   forget` called `engine.expireFact(id)` which UPDATEd the DB only.
   After `gbrain rebuild` (v0.32.3) the forget would evaporate. v0.32.2
   makes forget rewrite the fence: strikethrough the claim, set
   `valid_until=today`, append `forgotten: <reason>` to the context
   cell. New optional `--reason <text>` flag.

2. **MCP `get_page` strips private fact rows for remote callers.**
   Previously the strip fired only when the caller carried a takes-
   holders allow-list (a subagent privacy hole). v0.32.2 fires the
   strip whenever `ctx.remote === true`. Local CLI sees the full
   fence; subagent / remote MCP see world-only facts.

3. **The chunker strips private fact text.** `chunkText` calls
   `stripFactsFence({keepVisibility:['world']})` alongside the
   existing `stripTakesFence` before chunking. Private fact text
   never reaches `content_chunks.chunk_text`, never gets embedded,
   never returned by search.

4. **CI invariant gate.** `scripts/check-system-of-record.sh` is wired
   into `bun run verify`. It bans direct calls to `engine.insertFact`,
   `addLink`, `addLinksBatch`, `addTimelineEntry`, `upsertTake`,
   `insertFacts`, `expireFact` in `src/` + `scripts/`. Legitimate
   sites carry `// gbrain-allow-direct-insert: <reason>` comments on
   the same line. New code that tries to bypass the reconcile layer
   fails the build.

5. **New cycle phase `extract_facts`** runs between `extract` and
   `recompute_emotional_weight`. Reconciles the DB facts index from
   the fence on every affected entity page. Empty-fence guard refuses
   to run while v0.31 legacy rows are pending the backfill (a belt to
   the orchestrator's suspenders).

## Verification

After the agent runs the migration:

```bash
# Migration ran cleanly
gbrain apply-migrations --yes

# No fence write failures
test ! -f ~/.gbrain/facts.write_failures.jsonl || echo "WARN: some failures recorded"

# Each entity page with v0.31 facts now has a fence
ls people/ companies/ | head -5
grep -l 'gbrain:facts:begin' people/*.md | head -5

# Doctor passes
gbrain doctor
```

## Things to NOT do

- **Don't manually edit the v51 columns.** `row_num` and
  `source_markdown_slug` are managed by the fence-write path. Edit the
  markdown fence instead; the cycle phase reconciles.
- **Don't call `engine.insertFact` / `expireFact` from new code paths.**
  The CI gate blocks it. If you genuinely need a direct write (e.g.,
  for a new reconciler), add the `// gbrain-allow-direct-insert:
  <reason>` comment with a justification.
- **Don't delete the `facts.write_failures.jsonl` log without
  reading it.** It records pages where the fence parse-validate
  failed during a write; the .tmp file remains as quarantine
  evidence.

## See also

- `docs/architecture/system-of-record.md` — the canonical contract
  doc + the 3-layer privacy boundary
- `CHANGELOG.md` v0.32.2 entry — the release manifesto + numbers
- `scripts/check-system-of-record.sh` — the CI gate enforcing the
  rule going forward
