# Platform migration spec — Community Library (company 11699)

**Goal:** One server-side DB pass over every company-**11699** `community_templates` record to (A) set four visibility flags and (B) convert each `description` from Markdown to clean HTML (and strip an internal metadata block). Replaces a ~830-record-by-record UI grind. The conversion output has been reviewed and approved (Mad Max / Mike).

**Scope:** all `community_templates` rows where `companyID = 11699` — every type (Form Template, Form Theme, Document Generator, Landing Page Template, General Stack, Proposal, Profile). ~830 rows (53 admin-grid pages × 16). Do **not** touch other companies' rows.

---

## Part A — Visibility flags (set on every 11699 row)

| Field (form name) | Set to | Meaning |
|---|---|---|
| `CommunityTemplate[default]` | `1` (true) | System template ON |
| `CommunityTemplate[featured]` | `1` (true) | Featured ON |
| `CommunityTemplate[sort_weight]` | `8` | Sort weight |
| `CommunityTemplate[meta][show_creator_name]` | `0` (false) | Show creator name OFF |

All four are idempotent — safe to set even where already set (the ~18 embed-C form records already have them).

---

## Part B — Description: Markdown → HTML + strip AI INDEX

The `description` was authored in Markdown but the new Redactor WYSIWYG renders the stored text as HTML, so literal `**`, `-`, `---` and a trailing machine-metadata block show through. Convert to clean HTML and strip the metadata block. **Wording must not change — formatting + AI-INDEX removal only.**

### Idempotency guard (only convert rows that need it)
Convert a row's description **only if** it still looks like raw/literal Markdown or carries an AI-INDEX block. Skip already-clean HTML (e.g., record 8228 was already converted). A row needs conversion if ANY of:
- contains `--- AI INDEX ---` (case-insensitive, 2+ dashes either side), OR
- contains a literal `**bold**` run (`/\*\*[^*]+\*\*/`), OR
- has a line beginning with a Markdown bullet (`- `, `* `, `+ `) after stripping tags, OR
- has a standalone `---` rule line after stripping tags.

### Transform rules (must match the approved output)
1. **Strip the AI INDEX block first:** remove everything from the `--- AI INDEX ---` marker to end of string. Regex (tolerates leading tags/whitespace): `/(?:<[^>]*>|\s)*-{2,}\s*AI\s*INDEX\s*-{2,}[\s\S]*$/i`.
2. **Normalize any existing HTML to text + newlines:** `<br>`→`\n`; `<li ...>`→`\n- `; closing `</p|div|li|h1-6|ul|ol|blockquote>`→`\n`; strip all remaining tags.
3. **Decode entities** (`&amp; &lt; &gt; &quot; &#39; &nbsp;`).
4. **Block parse, line by line:**
   - blank line → block separator
   - a line of only `---` (3+) → drop (horizontal rule)
   - line matching `^[-*+]\s+(.*)` → list item (group consecutive items into one `<ul>`)
   - otherwise → `<p>…</p>`
5. **Inline formatting** (applied to each block's text, AFTER HTML-escaping `& < >`):
   - `**x**` → `<strong>x</strong>` (before single-`*`)
   - `*x*` → `<em>x</em>` (single asterisks, not adjacent to another `*`)
   - `` `x` `` → `x` (drop backticks; keep text)

### Reference implementation (the exact, approved transform — JavaScript)
```js
function decodeEntities(s){return s.replace(/&nbsp;/gi,' ').replace(/&amp;/gi,'&').replace(/&lt;/gi,'<').replace(/&gt;/gi,'>').replace(/&quot;/gi,'"').replace(/&#0?39;/gi,"'").replace(/&apos;/gi,"'");}
function inline(t){
  t=t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  t=t.replace(/\*\*([^*]+?)\*\*/g,'<strong>$1</strong>');
  t=t.replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g,'$1<em>$2</em>');
  t=t.replace(/`([^`]+)`/g,'$1');
  return t;
}
function stripAiIndex(s){return s.replace(/(?:<[^>]*>|\s)*-{2,}\s*AI\s*INDEX\s*-{2,}[\s\S]*$/i,'');}
function toPlainTextWithNewlines(s){
  return s.replace(/<\s*br\s*\/?\s*>/gi,'\n')
          .replace(/<\s*li[^>]*>/gi,'\n- ')
          .replace(/<\/(p|div|li|h[1-6]|ul|ol|blockquote)\s*>/gi,'\n')
          .replace(/<[^>]+>/g,'');
}
function descToHtml(input){
  if(input==null) return '';
  let s=String(input);
  s=stripAiIndex(s);
  s=toPlainTextWithNewlines(s);
  s=decodeEntities(s);
  s=s.replace(/\r\n?/g,'\n');
  const lines=s.split('\n'); const blocks=[]; let li=null;
  const flush=()=>{ if(li&&li.length) blocks.push('<ul>'+li.map(x=>'<li>'+inline(x)+'</li>').join('')+'</ul>'); li=null; };
  for(const raw of lines){
    const line=raw.trim();
    if(!line){ flush(); continue; }
    if(/^-{3,}$/.test(line)){ flush(); continue; }
    const m=line.match(/^[-*+]\s+(.*)$/);
    if(m){ (li||(li=[])).push(m[1].trim()); continue; }
    flush(); blocks.push('<p>'+inline(line)+'</p>');
  }
  flush();
  return blocks.join('');
}
function needsConversion(input){
  if(input==null) return false;
  const s=String(input);
  if(/-{2,}\s*AI\s*INDEX\s*-{2,}/i.test(s)) return true;
  if(/\*\*[^*]+\*\*/.test(s)) return true;
  const txt=toPlainTextWithNewlines(s);
  if(/(^|\n)\s*[-*+]\s+\S/.test(txt)) return true;
  if(/(^|\n)\s*-{3,}\s*(\n|$)/.test(txt)) return true;
  return false;
}
```
(Canonical source in the repo: `lib/desc-to-html.js`.)

### Example (before → after)
**Before (stored):**
```
**Travelers, Documents & Insurance** — Update / setup form (auth-only) for Travel...
- *Your travel party* — Traveler Roster (names/ages) [long text]; ...
--- AI INDEX ---
asset_type=form
niche=Travel
...
```
**After:**
```html
<p><strong>Travelers, Documents &amp; Insurance</strong> — Update / setup form (auth-only) for Travel...</p>
<ul><li><em>Your travel party</em> — Traveler Roster (names/ages) [long text]; ...</li>...</ul>
```
(AI INDEX block removed entirely.)

---

## Notes / safety
- **Wording unchanged** — only Markdown markers → tags, and the AI-INDEX block removed.
- **AI INDEX is internal metadata**, not customer copy — strip from the visible description. (It still lives in our repo manifests / form-index, so nothing is lost.)
- **Idempotent** — re-running is safe; `needsConversion()` skips already-clean rows.
- **Allowed output tags:** `<p> <strong> <em> <ul> <li>` — confirm these survive the Redactor sanitizer/allow-list so a later UI re-save doesn't strip them.
- **Round-trip check (recommended):** for a few rows, after migrating, open in Redactor → Save → confirm the description is stable (no re-mangling).

## Verification after the run
- Admin grid `/.../communityTemplate/index?CommunityTemplate[companyID]=11699` → Featured + Sort Weight columns populated on all rows; spot-check `default` (System) via edit form (`#default`) since the grid doesn't surface it.
- Spot-check several `/library/{id}` detail pages: description shows formatted (bold/bullets/paragraphs), no literal `**`/`-`, no AI INDEX block.
- I (Claude/Mad Max side) will verify a sample across types after the run.
