Persisted Content Remediation
Safe workflow for fixing stored content bodies (not UI code) when format, encoding, or structure is wrong. Applies to any table/column (book_chapters.content, content_items.content, archive_items.content, etc.) and any source/target format: HTML, Markdown, MDX, plain text, hybrid, JSON.
When to use
- User asks to fix, normalize, convert, or clean up persisted content in a database
- Audit found format drift (e.g. Markdown stored where HTML is expected)
- Ingest pipeline skipped conversion or used a naive converter
- Hybrid bodies (HTML + leaked
## / ** / [^n])
- Wrong-field writes, truncation, empty placeholders, test/E2E pollution
- Not for: live editor UX, rendering component changes, or greenfield ingest (use the ingest skill instead)
Golden rules (non-negotiable)
- Audit before write. Classify every affected row; know the canonical target format from code (reader + service layer), not assumptions.
- MCP/database reads OK; MCP bulk writes NOT OK. Use Supabase MCP
execute_sql for classification and spot checks only. Remediation writes go through a local script + service role client.
- Deterministic transforms only. Libraries (
marked, remark, sanitize-html) — never paraphrase or "clean up" prose with the LLM.
- Scope to bad rows. UPDATE only IDs matching the audit query. Never "fix all rows in the table."
- Backup before apply. Export affected
(id, content, word_count, updated_at, …) to JSON or a backup table.
- Dry-run default. Script must support
--dry-run; write human-reviewable diffs before --apply.
- Re-audit after apply. Re-run classification SQL; counts of bad buckets should drop to zero (or documented exceptions).
- One tier at a time. Pure-format fixes before hybrid surgery before cosmetic cleanup.
Phase 0 — Discover canonical format
Before auditing data, read the code path that owns the field:
| Question |
Where to look |
| What format does the reader expect? |
Components using dangerouslySetInnerHTML, MDX renderer, ReactMarkdown, TipTap JSON |
| What format does write path produce? |
Service create* / update*, ingest mappers, cleanupContent() |
| What metadata must stay in sync? |
word_count, estimated_reading_time, updated_at |
| Is there a naive converter to avoid? |
e.g. repo markdownToHtml() — headings only, leaks inline MD |
Document finding in the audit report: target format, detection heuristics, forbidden double-conversion.
Phase 1 — Audit (read-only)
1a. Locate the authoritative table
-- Example: find populated content tables
SELECT table_name FROM information_schema.columns
WHERE table_schema = 'public' AND column_name = 'content'
ORDER BY table_name;
Confirm row counts; ignore empty duplicate/legacy tables.
1b. Classify formats in SQL
Use mutually exclusive buckets (top-down). Adapt patterns to the domain — see reference/audit-patterns.md.
Minimum buckets:
| Bucket |
Meaning |
target_format |
Matches canonical (e.g. HTML fragment) |
pure_source_format |
Entire body is wrong format (e.g. raw Markdown, no HTML) |
hybrid |
Target format + leaked source tokens |
empty_or_tiny |
Null, '', or placeholder (<p></p>) |
plain_text |
Neither target nor recognizable markup |
structured_other |
JSON, MDX/JSX, XML — rare but flag explicitly |
1c. Severity rank
Worst → mildest:
- Pure wrong format — visible breakage (literal
##, **)
- Hybrid structural leaks —
## headings inside HTML
- Inline artifacts — footnotes
[^1], stray bold, EPUB links
- Cosmetic noise — HTML comments (
<!-- page: N -->), entities
- Empty/test rows — delete or leave; not format conversion
1d. Write audit report
Path convention (ZenWrite): docs/build/<domain>/<topic>-audit.md
Include: TL;DR, canonical format evidence (code citations), distribution table, per-entity breakdown, severity, root-cause read, recommended tiers, SQL appendix.
Stop here unless user explicitly approves remediation.
Phase 2 — Plan remediation tiers
| Tier |
Typical action |
Risk |
| T1 Pure source → target |
Full doc conversion via library |
Low (with backup) |
| T2 Hybrid structural |
Re-ingest from source file OR HTML-aware partial convert |
Medium |
| T3 Inline artifacts |
Targeted rules (footnotes policy, bold, links) |
Medium–high |
| T4 Cosmetic strip |
Comment/whitespace normalization |
Low |
| T5 Row delete |
Test/E2E rows only |
Low (confirm with user) |
Do not combine tiers in one apply pass.
Phase 3 — Implement remediation script
Create under scripts/remediate-<domain>.ts (or repo convention). Required flags:
tsx scripts/remediate-<domain>.ts --dry-run [--bucket pure_markdown] [--book-id UUID] [--limit N]
tsx scripts/remediate-<domain>.ts --apply --backup ./backups/<timestamp>.json [--bucket ...]
Script requirements
// Pattern (adapt per repo)
import './load-local-env.js';
import { createClient } from '@supabase/supabase-js';
import { marked } from 'marked'; // or remark/unified — pin version, verify docs
import { createHash } from 'crypto';
import { writeFileSync, mkdirSync } from 'fs';
const dryRun = !process.argv.includes('--apply');
const backupPath = getArg('--backup');
// 1. SELECT only rows matching audit bucket (same SQL as Phase 1)
// 2. For each row: transform deterministically; NEVER call LLM
// 3. Validate: sha256 before/after, length ratio, format re-check
// 4. If validation fails → skip row, log to remediation-skipped.json
// 5. dry-run → write diff files; apply → UPDATE + sync word_count/reading_time
Transform selection
| Source → Target |
Tool |
Caution |
| MD → HTML |
marked / remark + rehype-sanitize |
Configure allowed tags to match reader |
| MDX → HTML |
MDX compile OR strip JSX if MDX not supported |
MDX ≠ MD; needs separate pipeline |
| HTML → HTML (hybrid) |
Parse DOM (node-html-parser, cheerio); convert text nodes only |
Never regex whole document |
| HTML → MD |
Only if target is MD (rare) |
Lossy; document why |
| Plain → HTML |
Wrap in <p>, escape entities |
Minimal |
| Strip comments |
DOM or targeted regex on known patterns |
Low risk |
Never run MD converter on already-HTML bodies unless you've extracted markdown islands first.
Validation gates (per row)
Skip and flag if any fail:
Diff output
Write to docs/build/<domain>/remediation-diffs/<id>.diff or a single JSONL with { id, beforeHead, afterHead, warnings }.
Phase 4 — Review gate
Before --apply:
- User reviews audit + sample diffs (minimum 3 rows: small, medium, large; include non-English if present)
- Confirm tier scope and row count
- Confirm backup path exists
Agent must not run --apply without explicit user approval.
Phase 5 — Apply and verify
# Apply
tsx scripts/remediate-<domain>.ts --apply --backup ./backups/2026-07-08-book-chapters.json --bucket pure_markdown
# Re-audit (same SQL as Phase 1)
# Expect: pure_source_format count → 0 for applied bucket
Print session report:
- Rows selected / transformed / skipped / failed
- Backup path
- Before/after bucket counts
- Skipped row IDs + reasons
- Suggested manual follow-ups
Tool choice matrix
| Task |
Tool |
Notes |
| Schema discovery |
Supabase MCP list_tables, execute_sql |
Read-only |
| Format classification |
Supabase MCP execute_sql |
Read-only |
| Spot-check bodies |
Supabase MCP or script SELECT |
Prefer script for large bodies |
| Backup export |
Local script → JSON |
Full fidelity |
| Transform |
Local script + library |
Deterministic |
| Write |
Local script UPDATE |
Transactional per batch optional |
| Bulk write via MCP |
Forbidden |
Truncation, no rollback, no diff review |
Anti-patterns
| Anti-pattern |
Why it fails |
| LLM rewrites chapter/article in chat |
Hallucination, truncation, tone drift |
UPDATE ... SET content = '...' via MCP with inline body |
Context limits; untested |
Run naive markdownToHtml() on hybrid HTML |
Double-wraps, breaks tags |
| Regex replace on 40k-char HTML |
Eats closing tags, nested structures |
| Fix all rows "while we're at it" |
Damages good content |
| Skip backup "it's just 56 rows" |
Irreversible without PITR |
| Assume MDX when codebase uses HTML |
Wrong target format |
Repo integration (ZenWrite)
| Resource |
Path |
| Example audit |
docs/build/books/chapter-content-format-audit.md |
| Naive converter (do not use for full remediation) |
server/services/admin/ingest/contentCleanup.ts |
| Chapter write path |
server/services/books/bookChapters.service.ts |
| Migration script pattern |
scripts/migrate-books-to-catalog.ts |
| Markdown lib |
marked in package.json |
| Supabase project |
Movemental vhaiiiykcukrlyvwlgip |
Invocation
/persisted-content-remediation audit book_chapters.content
/persisted-content-remediation plan --report docs/build/books/chapter-content-format-audit.md
/persisted-content-remediation script book-chapters --tier 1 --dry-run
Related skills
movement-leader-harvest-ingest — forward ingest (prevent recurrence)
type-safety-chain — if remediation touches schema/contracts
validate — post-change repo validation
Additional resources
- SQL classification templates: reference/audit-patterns.md
- Worked example walkthrough: reference/worked-example-book-chapters.md
1---2name: persisted-content-remediation3description: Safely audit and remediate persisted text/content fields in Supabase or Postgres when bodies are in the wrong format (HTML, Markdown, MDX, plain text, hybrid, JSON, empty). Use for content format drift, ingest bypass, wrong-field storage, leaked markup, or batch normalization — always audit-first, backup, dry-run, deterministic transforms, never AI-rewrite in place.4---56# Persisted Content Remediation78Safe workflow for fixing **stored content bodies** (not UI code) when format, encoding, or structure is wrong. Applies to any table/column (`book_chapters.content`, `content_items.content`, `archive_items.content`, etc.) and any source/target format: **HTML, Markdown, MDX, plain text, hybrid, JSON**.910## When to use1112- User asks to fix, normalize, convert, or clean up **persisted content** in a database13- Audit found format drift (e.g. Markdown stored where HTML is expected)14- Ingest pipeline skipped conversion or used a naive converter15- Hybrid bodies (HTML + leaked `##` / `**` / `[^n]`)16- Wrong-field writes, truncation, empty placeholders, test/E2E pollution17- **Not** for: live editor UX, rendering component changes, or greenfield ingest (use the ingest skill instead)1819## Golden rules (non-negotiable)20211. **Audit before write.** Classify every affected row; know the canonical target format from code (reader + service layer), not assumptions.222. **MCP/database reads OK; MCP bulk writes NOT OK.** Use Supabase MCP `execute_sql` for classification and spot checks only. Remediation writes go through a **local script** + service role client.233. **Deterministic transforms only.** Libraries (`marked`, `remark`, `sanitize-html`) — never paraphrase or "clean up" prose with the LLM.244. **Scope to bad rows.** UPDATE only IDs matching the audit query. Never "fix all rows in the table."255. **Backup before apply.** Export affected `(id, content, word_count, updated_at, …)` to JSON or a backup table.266. **Dry-run default.** Script must support `--dry-run`; write human-reviewable diffs before `--apply`.277. **Re-audit after apply.** Re-run classification SQL; counts of bad buckets should drop to zero (or documented exceptions).288. **One tier at a time.** Pure-format fixes before hybrid surgery before cosmetic cleanup.2930## Phase 0 — Discover canonical format3132Before auditing data, read the **code path** that owns the field:3334| Question | Where to look |35|----------|---------------|36| What format does the reader expect? | Components using `dangerouslySetInnerHTML`, MDX renderer, `ReactMarkdown`, TipTap JSON |37| What format does write path produce? | Service `create*` / `update*`, ingest mappers, `cleanupContent()` |38| What metadata must stay in sync? | `word_count`, `estimated_reading_time`, `updated_at` |39| Is there a naive converter to avoid? | e.g. repo `markdownToHtml()` — headings only, leaks inline MD |4041Document finding in the audit report: **target format**, **detection heuristics**, **forbidden double-conversion**.4243## Phase 1 — Audit (read-only)4445### 1a. Locate the authoritative table4647```sql48-- Example: find populated content tables49SELECT table_name FROM information_schema.columns50WHERE table_schema = 'public' AND column_name = 'content'51ORDER BY table_name;52```5354Confirm row counts; ignore empty duplicate/legacy tables.5556### 1b. Classify formats in SQL5758Use mutually exclusive buckets (top-down). Adapt patterns to the domain — see [reference/audit-patterns.md](reference/audit-patterns.md).5960Minimum buckets:6162| Bucket | Meaning |63|--------|---------|64| `target_format` | Matches canonical (e.g. HTML fragment) |65| `pure_source_format` | Entire body is wrong format (e.g. raw Markdown, no HTML) |66| `hybrid` | Target format + leaked source tokens |67| `empty_or_tiny` | Null, `''`, or placeholder (`<p></p>`) |68| `plain_text` | Neither target nor recognizable markup |69| `structured_other` | JSON, MDX/JSX, XML — rare but flag explicitly |7071### 1c. Severity rank7273Worst → mildest:74751. **Pure wrong format** — visible breakage (literal `##`, `**`)762. **Hybrid structural leaks** — `##` headings inside HTML773. **Inline artifacts** — footnotes `[^1]`, stray bold, EPUB links784. **Cosmetic noise** — HTML comments (`<!-- page: N -->`), entities795. **Empty/test rows** — delete or leave; not format conversion8081### 1d. Write audit report8283Path convention (ZenWrite): `docs/build/<domain>/<topic>-audit.md`8485Include: TL;DR, canonical format evidence (code citations), distribution table, per-entity breakdown, severity, root-cause read, recommended tiers, SQL appendix.8687**Stop here unless user explicitly approves remediation.**8889## Phase 2 — Plan remediation tiers9091| Tier | Typical action | Risk |92|------|----------------|------|93| T1 Pure source → target | Full doc conversion via library | Low (with backup) |94| T2 Hybrid structural | Re-ingest from source file OR HTML-aware partial convert | Medium |95| T3 Inline artifacts | Targeted rules (footnotes policy, bold, links) | Medium–high |96| T4 Cosmetic strip | Comment/whitespace normalization | Low |97| T5 Row delete | Test/E2E rows only | Low (confirm with user) |9899**Do not combine tiers in one apply pass.**100101## Phase 3 — Implement remediation script102103Create under `scripts/remediate-<domain>.ts` (or repo convention). Required flags:104105```106tsx scripts/remediate-<domain>.ts --dry-run [--bucket pure_markdown] [--book-id UUID] [--limit N]107tsx scripts/remediate-<domain>.ts --apply --backup ./backups/<timestamp>.json [--bucket ...]108```109110### Script requirements111112```typescript113// Pattern (adapt per repo)114import './load-local-env.js';115import { createClient } from '@supabase/supabase-js';116import { marked } from 'marked'; // or remark/unified — pin version, verify docs117import { createHash } from 'crypto';118import { writeFileSync, mkdirSync } from 'fs';119120const dryRun = !process.argv.includes('--apply');121const backupPath = getArg('--backup');122123// 1. SELECT only rows matching audit bucket (same SQL as Phase 1)124// 2. For each row: transform deterministically; NEVER call LLM125// 3. Validate: sha256 before/after, length ratio, format re-check126// 4. If validation fails → skip row, log to remediation-skipped.json127// 5. dry-run → write diff files; apply → UPDATE + sync word_count/reading_time128```129130### Transform selection131132| Source → Target | Tool | Caution |133|-----------------|------|---------|134| MD → HTML | `marked` / `remark` + `rehype-sanitize` | Configure allowed tags to match reader |135| MDX → HTML | MDX compile OR strip JSX if MDX not supported | MDX ≠ MD; needs separate pipeline |136| HTML → HTML (hybrid) | Parse DOM (`node-html-parser`, `cheerio`); convert text nodes only | Never regex whole document |137| HTML → MD | Only if target is MD (rare) | Lossy; document why |138| Plain → HTML | Wrap in `<p>`, escape entities | Minimal |139| Strip comments | DOM or targeted regex on known patterns | Low risk |140141**Never run MD converter on already-HTML bodies** unless you've extracted markdown islands first.142143### Validation gates (per row)144145Skip and flag if any fail:146147- [ ] `sha256(before)` recorded in backup148- [ ] Plain-text word count within ±15% (or policy threshold)149- [ ] `len(after) >= 0.85 * len(before)` for MD→HTML (HTML adds tags)150- [ ] Re-classification passes target bucket151- [ ] No `<script>`, event handlers, or disallowed tags (if sanitizing)152- [ ] Unicode/CJK/RTL preserved (spot-check non-Latin samples)153154### Diff output155156Write to `docs/build/<domain>/remediation-diffs/<id>.diff` or a single JSONL with `{ id, beforeHead, afterHead, warnings }`.157158## Phase 4 — Review gate159160Before `--apply`:1611621. User reviews audit + sample diffs (minimum 3 rows: small, medium, large; include non-English if present)1632. Confirm tier scope and row count1643. Confirm backup path exists165166**Agent must not run `--apply` without explicit user approval.**167168## Phase 5 — Apply and verify169170```bash171# Apply172tsx scripts/remediate-<domain>.ts --apply --backup ./backups/2026-07-08-book-chapters.json --bucket pure_markdown173174# Re-audit (same SQL as Phase 1)175# Expect: pure_source_format count → 0 for applied bucket176```177178Print session report:179180- Rows selected / transformed / skipped / failed181- Backup path182- Before/after bucket counts183- Skipped row IDs + reasons184- Suggested manual follow-ups185186## Tool choice matrix187188| Task | Tool | Notes |189|------|------|-------|190| Schema discovery | Supabase MCP `list_tables`, `execute_sql` | Read-only |191| Format classification | Supabase MCP `execute_sql` | Read-only |192| Spot-check bodies | Supabase MCP or script SELECT | Prefer script for large bodies |193| Backup export | Local script → JSON | Full fidelity |194| Transform | Local script + library | Deterministic |195| Write | Local script UPDATE | Transactional per batch optional |196| Bulk write via MCP | **Forbidden** | Truncation, no rollback, no diff review |197198## Anti-patterns199200| Anti-pattern | Why it fails |201|--------------|--------------|202| LLM rewrites chapter/article in chat | Hallucination, truncation, tone drift |203| `UPDATE ... SET content = '...'` via MCP with inline body | Context limits; untested |204| Run naive `markdownToHtml()` on hybrid HTML | Double-wraps, breaks tags |205| Regex replace on 40k-char HTML | Eats closing tags, nested structures |206| Fix all rows "while we're at it" | Damages good content |207| Skip backup "it's just 56 rows" | Irreversible without PITR |208| Assume MDX when codebase uses HTML | Wrong target format |209210## Repo integration (ZenWrite)211212| Resource | Path |213|----------|------|214| Example audit | `docs/build/books/chapter-content-format-audit.md` |215| Naive converter (do not use for full remediation) | `server/services/admin/ingest/contentCleanup.ts` |216| Chapter write path | `server/services/books/bookChapters.service.ts` |217| Migration script pattern | `scripts/migrate-books-to-catalog.ts` |218| Markdown lib | `marked` in `package.json` |219| Supabase project | Movemental `vhaiiiykcukrlyvwlgip` |220221## Invocation222223```224/persisted-content-remediation audit book_chapters.content225/persisted-content-remediation plan --report docs/build/books/chapter-content-format-audit.md226/persisted-content-remediation script book-chapters --tier 1 --dry-run227```228229## Related skills230231- **`movement-leader-harvest-ingest`** — forward ingest (prevent recurrence)232- **`type-safety-chain`** — if remediation touches schema/contracts233- **`validate`** — post-change repo validation234235## Additional resources236237- SQL classification templates: [reference/audit-patterns.md](reference/audit-patterns.md)238- Worked example walkthrough: [reference/worked-example-book-chapters.md](reference/worked-example-book-chapters.md)