# LLM Knowledge Hub — Builder Playbook

> **For future Claude sessions.** This file is the canonical, self-contained restart guide for building or maintaining a Dnister AI Knowledge Hub.
> If you are a Claude instance picking up this work in a new session, read this end-to-end before touching code.
> Source-of-truth lives in the Wikibiz-labs/dnister monorepo. Memory files are local to one workstation; this playbook travels.

**Version**: 1.0 (2026-05-14, after Brovdi v3.18 / Valikhnovski v3.16)
**Maintainer**: Dnister AI (Mr. Strategy)
**Repo**: https://github.com/Wikibiz-labs/dnister (private)
**Public landing**: https://www.dnister.ai/llm/

---

## 1. What this product is

A **public-source knowledge hub** about a single person/organization, structured for simultaneous consumption by three audiences:

1. **Humans** — visitors who land via Google, social, or direct share. Get a clean, professional dossier with biography, achievements, operations/practice, photos, quotes.
2. **Journalists** — get a self-contained media kit: copyable bios (50/100/250 words), verified URLs, OPSEC/methodology disclosures, source-reliability tiers.
3. **LLM/RAG systems** (ChatGPT, Claude, Perplexity, Gemini) — get a machine-readable layer: JSON files, JSON-LD schema.org markup, `llms.txt` index, RAG chunk manifest, pre-built Q&A answer packs.

The hub is **not a brochure**. It is a reputation asset cited by sources, validated by an A-E reliability tier, and surfaceable as Rich Results in search engines.

**Pricing tiers** (as of 2026-05):
- **MVP** ($1.5K, ~12h) — single language, single hub page tree, basic JSON-LD, no graph
- **Standard** ($3K, ~18h) — UA + EN mirror, photo gallery, knowledge graph, media kit, compliance/methodology page
- **Premium** ($5K+, ~22h+) — everything above + custom content sections, deep-strike research, multimedia wall, recurring quarterly refresh

---

## 2. Live examples (as of 2026-05-14)

| Hub | UA | EN mirror | Source repo | Monorepo subdir |
|---|---|---|---|---|
| Rostyslav Valikhnovski (surgeon) | https://www.dnister.ai/llm/Valikhnovski/ | https://www.dnister.ai/llm/Valikhnovski/en/ | `D:\Claude Projects\valikhnovski_kb\` | `valikhnovski-hub/` |
| Robert Brovdi "Magyar" (military) | https://www.dnister.ai/llm/brovdi/ | https://www.dnister.ai/llm/brovdi/en/ | `D:\Claude Projects\brovdi_kb\` | `brovdi-hub/` |

Both hubs share infrastructure (sitemap.xml at `/llm/sitemap.xml`, IndexNow key at `/<key>.txt`) but have **independent code**. The two builders evolved separately and use different patterns — DO NOT try to merge them into one.

---

## 3. Architecture — canonical hub file structure

```
{client}_kb/                          # Source repo (NOT in monorepo, kept local-only for raw research)
├── kb.json                           # Source-of-truth: person + entities + edges + timeline + facts
├── kb_en.json                        # Translated mirror of kb.json (via Sonnet API)
├── portrait.jpg                      # Hero image (used for og:image too — REQUIRED for social preview)
├── build_pages.py    or build_site.py   # Inner pages builder
├── build_v2.py       (Brovdi only)      # Master builder for homepage + graph + style
├── build_en.py       or translate_en.py # EN mirror builder
├── patch_en_jsonld.py (Valikhnovski)    # One-shot Article JSON-LD patcher for EN files
├── validate_jsonld.py                   # Schema.org structural validator (production-wide)
└── site/                                # Build output → deployed to dnister.ai
    ├── index.html                       # Homepage with hero + dashboard + sections
    ├── biography.html, military-career.html, … (8-12 inner pages)
    ├── style.css, icon.svg, manifest.webmanifest
    ├── portrait.jpg
    ├── llms.txt                         # AI-crawler-readable hub index
    ├── data/
    │   ├── graph.json                   # Force-directed graph (entities + edges)
    │   ├── facts.json                   # Verifiable claims with source_ids + confidence
    │   ├── timeline.json                # Chronological events
    │   ├── sources.json                 # Source registry with tier metadata
    │   ├── quotes.json                  # Verbatim public statements
    │   ├── media.json                   # Verified media publications (tier A-E)
    │   ├── videos.json                  # Official video channels + topics
    │   ├── services.json                # Units / practice areas / treatment fields
    │   ├── international.json           # International interactions
    │   ├── answer_packs.json            # Pre-built Q&A for LLM/RAG
    │   ├── chunk_manifest.json          # RAG chunk index per page
    │   └── source_reliability.json      # Tier legend A-E
    ├── images/gallery/                  # Curated photos (CC-licensed)
    └── en/                              # EN mirror (~9-20 pages)
```

### JSON schemas (mandatory keys)

**facts.json** — list of objects:
```json
{
  "id": "f-kebab-case",
  "claim": "Ukrainian text",
  "claim_en": "English text",
  "category": "biography|military|operation|award|doctrine|external|communication",
  "date": "YYYY-MM-DD or YYYY-MM or YYYY",
  "entity_ids": ["..."],
  "source_ids": ["..."],
  "confidence": "high|medium|low",
  "page_url": "biography.html"
}
```

**sources.json** — list with `id`, `title`, `publisher`, `type`, `url`, `language`, `reliability_tier` (A-E), optional `notes`.

**media.json** — list with `id`, `title`, `publisher`, `type`, `year`, `topic`, `url`, `source_id`, `credibility_tier` (A-E), `summary`.

**services.json** — `{"groups": [...]}` where each group has `id`, `label_uk`, `label_en`, `type`, `summary_uk`, `summary_en`, `related_entity_ids`.

**international.json** — `{"items": [...]}` where each item has `id`, `label_uk`, `label_en`, `type`, `date`, `what_happened_uk/en`, `why_it_matters_uk/en`, `related_entities`, `source_ids`, optional `safety_note: "requires_human_review"`.

**answer_packs.json** — `{"packs": [...]}` with bilingual Q&A, `source_ids`, `confidence`.

**chunk_manifest.json** — `{"chunks": [...]}` mapping `id` → `page` → `section` → `keywords` → `related_fact_ids`.

### JSON-LD types embedded in HTML

| @type | Where | Purpose |
|---|---|---|
| `Person` / `MedicalOrganization` | Homepage `<head>` | Canonical identity for Google Knowledge Panel |
| `Dataset` | Homepage `<head>` | Lists all DataDownload distributions (one per JSON file) |
| `Article` / `ProfilePage` | Every inner page | Rich Results eligibility, ranks higher in news searches |
| `BreadcrumbList` | Inner pages (Valikhnovski only) | Breadcrumbs in SERP |
| `FAQPage` | Where Q&A exists | FAQ rich snippets |
| `VideoObject` (in ItemList) | Pages with video walls | Video carousel in Google |

Required Article fields: `@context`, `@type`, `headline`, `datePublished`, `author`, `publisher`, `url`. Author/publisher must be `{@type: Organization, name: "Dnister AI", url: "https://www.dnister.ai/"}`. Dates ISO-8601. URLs absolute https://. `validate_jsonld.py` enforces all of this — run it after every deploy.

---

## 4. Build pipeline

### Brovdi-style (two-builder pattern)

```bash
cd "D:\Claude Projects\brovdi_kb"
python build_pages.py     # Inner pages (biography, military-career, etc.) — run FIRST
python build_v2.py        # Homepage + graph + style.css + llms.txt + data/*.json — run LAST
python build_en.py        # EN mirror (9 pages from kb_en.json)
python validate_jsonld.py # Quality check
```

**Critical**: `build_v2.py` MUST run last. It owns `style.css`, `index.html`, `graph.html`, `data/*.json`, `llms.txt`. `build_pages.py` knows to skip those — but if you ever switch the order, the rich v2 homepage gets clobbered by the simple version.

### Valikhnovski-style (single builder)

```bash
cd "D:\Claude Projects\valikhnovski_kb"
python build_site.py             # All pages + JSON files in one pass
python translate_en.py           # EN mirror via Sonnet API (~$1.50, ~10 min)
python patch_en_jsonld.py        # Inject Article JSON-LD into EN files (idempotent)
python validate_jsonld.py        # Quality check (run from brovdi_kb — it covers both hubs)
```

### Deploy

```bash
# Static files via scp (SSH alias `dnister` configured locally)
cd site/
scp -p *.html dnister:dnister.ai/www/llm/{client}/
scp -p data/*.json dnister:dnister.ai/www/llm/{client}/data/
scp -p en/*.html dnister:dnister.ai/www/llm/{client}/en/
```

### IndexNow ping (after every deploy)

```python
import urllib.request, json
payload = {
    'host': 'www.dnister.ai',
    'key': '9d2edd92101b75386451ff89ad7030c3',
    'keyLocation': 'https://www.dnister.ai/9d2edd92101b75386451ff89ad7030c3.txt',
    'urlList': [...changed URLs...]
}
data = json.dumps(payload).encode('utf-8')
for ep in ['https://api.indexnow.org/indexnow', 'https://www.bing.com/indexnow']:
    req = urllib.request.Request(ep, data=data,
        headers={'Content-Type': 'application/json; charset=utf-8'}, method='POST')
    with urllib.request.urlopen(req, timeout=15) as r:
        print(f'{ep}: HTTP {r.status}')
```

A 200 OK means accepted+processed. A 202 means accepted, pending validation. Yandex direct often times out from local — IndexNow.org federates to it automatically. The IndexNow key (`9d2edd92101b75386451ff89ad7030c3`) is the shared `www.dnister.ai` key.

### Sitemap

One canonical sitemap at `/llm/sitemap.xml` listing both hubs (UA + EN). Update it on every deploy, bump `lastmod` to current ISO date. Backup the previous version: `sitemap.xml.bak-vN`.

---

## 5. Process — 5 phases for a new hub

| Phase | Output | Time | Notes |
|---|---|---|---|
| **1. Research** | `kb.json` skeleton + sources list + verified URLs | 4-8h | Manual. WebSearch + WebFetch the subject; cite each fact with source_id. Use reliability tiers A-E. |
| **2. Data layer** | All 16 JSON files populated | 2-4h | facts.json, timeline.json, sources.json first. media.json + services.json + international.json after. answer_packs.json last (synthesized from facts). |
| **3. HTML build** | All inner pages + homepage + graph | 3-5h | Adapt one of the two existing builders. Match design system: HUD aesthetic, dossier-card grid, hud-divider section headers. |
| **4. Polish** | Mobile @media, OG image, JSON-LD, llms.txt, PWA manifest | 2-3h | Run `validate_jsonld.py`. Test OG preview by hitting the URL with `curl -A "TelegramBot"`. |
| **5. Deploy** | Live + IndexNow + sitemap + Git tag | 1-2h | scp upload, sitemap refresh, IndexNow ping to Bing/IndexNow.org, monorepo commit + tag `vX.Y-{client}-{milestone}`. |

**Critical bottleneck: Phase 1 (research) is NOT automatable.** Future-Claude will be tempted to grep Wikipedia and call it done. Don't. The reliability-tier discipline (A — peer-reviewed/official, E — encyclopedia) is what makes the hub trustworthy. Every claim needs ≥1 source_id with tier ≤C.

---

## 6. Anti-patterns (learned the hard way)

These come from real regressions in Brovdi/Valikhnovski v1→v3:

- **DO NOT** use terminal/cyberpunk aesthetic. Hubs are for journalists. Olive/blue HUD on dark + serif body. No glitch effects, no animated terminals.
- **DO NOT** mock data ("87K+ surgeries", "534K subscribers") without `source_id`. If the number is from the client, mark with `safety_note: "requires_human_review"` and link to compliance.html.
- **DO NOT** hot-link external images (Wikipedia Commons is fine if you copy them locally to `site/images/gallery/`). Hot-links break or get hot-linked-to-death.
- **DO NOT** generate `index.html` from two builders (Brovdi's old bug). Pick one owner.
- **DO NOT** put emojis in topnav, hud-divider titles, or page headers. Editorial typography only. Emojis OK in `data-meta` chips if useful (e.g. ⚠ for OPSEC, ⭐ for featured) — never the cute kind (💪, 🎉, 🚀, 📊, etc.).
- **DO NOT** forget `og:image`. Telegram/Twitter/LinkedIn won't render a preview without it. Brovdi shipped v3.0 without it and looked broken in shares for ~5 versions.
- **DO NOT** use the wrong `og:type`. `profile` for hub root (index), `article` for inner pages. Article enables Article rich-result eligibility.
- **DO NOT** invent EN pages. If a UA page has no EN counterpart, the EN-button link should fall back to `/en/` (the EN homepage), not 404. See `EN_PAGES` set in [build_pages.py](../brovdi-hub/build_pages.py).
- **DO NOT** generate iframe-based embeds by default. Self-host or use Open Graph card patterns.
- **DO NOT** skip `validate_jsonld.py`. The Brovdi v3.3 regression broke graph for a week because we didn't check.

---

## 7. Versioning & rollback

Monorepo `main` is the live deployment record. Every meaningful change gets a tag in the form `vMAJOR.MINOR-{client}-{milestone}`. Example:

```
v3.13-sprint1-anchor              ← rollback before Sprint 2
v3.14-brovdi-content-depth        ← Sprint 2: 6 new JSON files + 30 facts
v3.15-brovdi-reach                ← Sprint 3: Article JSON-LD + IndexNow + sitemap
v3.16-valikhnovski-reach          ← Sprint 3 parity for Valikhnovski
v3.17-en-mirrors-reach            ← Sprint 3 for both /en/ mirrors
v3.18-sprint4-and-validation      ← Sprint 4 visible UI + JSON-LD validator pass
v3.19-og-images-and-playbook      ← og:image fix + this playbook
```

Each commit message includes a "Rollback anchor: vPREV" line. To roll back one feature: `git revert <SHA>`. To roll back to a known-good state: `git reset --hard vROLLBACK-ANCHOR` (destructive, ask user first).

---

## 8. Bootstrapping a new Claude session

When a fresh Claude session needs to pick up this work, paste this as the first message:

```
I'm continuing work on the Dnister AI LLM Knowledge Hub product.
Read these to bootstrap:

1. Playbook (canonical, public, travels with the product):
   https://www.dnister.ai/llm/HUB-BUILDER-PLAYBOOK.md

2. Memory files (local-only, may not exist if you're on a new machine):
   C:\Users\user\.claude\projects\D--Claude-Projects-Dnister-Strategy\memory\MEMORY.md

3. Live hubs (look at these to understand the product):
   - https://www.dnister.ai/llm/Valikhnovski/
   - https://www.dnister.ai/llm/brovdi/

4. Source code locations:
   - D:\Claude Projects\brovdi_kb\        (Brovdi hub)
   - D:\Claude Projects\valikhnovski_kb\  (Valikhnovski hub)
   - D:\Claude Projects\dnister-monorepo\ (monorepo with both + ROADMAP.md)

5. ROADMAP for next steps:
   D:\Claude Projects\dnister-monorepo\ROADMAP.md

Today's task is: [describe what you want done]
```

If the local files don't exist (new machine), the playbook above + the GitHub monorepo (https://github.com/Wikibiz-labs/dnister) are enough to reconstruct everything.

---

## 9. Required inputs from a new client

To start a new hub, the client must provide:

1. **Identity** — full name (UK/EN/native), callsign/title, birth date, birthplace, current role
2. **Portrait** — high-res JPG (≥1200×1200), public-source or owned, with usage rights confirmed
3. **Approved bio** — at minimum a 250-word summary they sign off on
4. **List of public sources** — Wikipedia entry, official websites, social profiles, key media interviews
5. **Awards/credentials** with dates and decree numbers (if applicable)
6. **OPSEC boundary** (military/medical clients) — what we can publish, what we can't. Document in compliance.html.
7. **Language scope** — UA-only, UA+EN, or UA+EN+other
8. **Recurring update commitment** — quarterly refresh? On-demand? This drives pricing tier.

If any of these is missing, the project is not ready to start. Don't begin coding until research is complete and the client has reviewed the proposed JSON-LD `Person` block.

---

## 10. Validation & quality gates

Before declaring a hub ready:

- [ ] `python validate_jsonld.py` returns `Total errors: 0`, `Total warnings: 0`
- [ ] **`python dnister-monorepo/tools/smoke_test_all_hubs.py` returns `0 errors`** — *(mandatory regression gate; catches the EN-mirror bug class — see §11 below)*
- [ ] `curl -A "TelegramBot" https://www.dnister.ai/llm/{client}/` returns OG meta with og:image, og:title, og:description, og:site_name
- [ ] Each inner page has Article (or ProfilePage for index) JSON-LD with all 5 required fields
- [ ] Sitemap lists every page; lastmod ≤ 7 days
- [ ] IndexNow returns HTTP 200/202 from both api.indexnow.org and bing.com
- [ ] Mobile Chrome DevTools renders correctly at 375×667 (iPhone SE)
- [ ] Reader/Research mode toggle works (R button in top-right)
- [ ] Photo gallery loads all images (no broken refs) **— scroll the page first; lazy-load images return `complete=false` until in viewport**
- [ ] EN mirror has all inner pages with Article JSON-LD + UA button in toolbar
- [ ] EN-button on EN pages flips to UA (`href="../<page>"`, label `UA`, title "Українська версія") — NEVER `href="en/<page>"` (would 404)
- [ ] Above-fold images: first 1-2 use `loading="eager"` + `fetchpriority="high"` on the LCP candidate; rest stay `loading="lazy"`
- [ ] llms.txt lists every data/*.json file in the machine-readable layer block
- [ ] Dataset JSON-LD distribution has one DataDownload per JSON file
- [ ] At least 30 facts in facts.json, each with ≥1 source_id

## 11. EN-mirror bug class — checklist for translate_en.py-style hubs

Hubs that derive EN from UA HTML (Valikhnovski-style `translate_en.py`) are exposed to 3 bug classes that hubs generating EN from scratch (Brovdi-style `build_en.py`) avoid:

| Bug | Symptom | Root cause | Fix |
|---|---|---|---|
| **EN→EN self-link** | EN page's "EN" button goes to `/en/en/page.html` → 404 → SPA fallback. User can't return to UA. | UA toolbar has `<a href="en/X" title="English version">EN</a>`. Translator copies verbatim. | Regex-rewrite in `add_language_switcher_link_back_to_uk()`: `<a href="en/X" title="English version">EN</a>` → `<a href="../X" title="Українська версія">UA</a>` |
| **EN graph link 404** | EN topnav has `<a href="graph.html">Knowledge graph</a>`. From `/en/page`, resolves to `/en/graph.html` (doesn't exist; graph is canvas-rendered, not translated). | Translator copies UA topnav verbatim. | Regex-rewrite: `<a href="graph.html">…</a>` → `<a href="../graph.html" title="Knowledge graph (UA — same data)">…</a>` |
| **EN images 404** | Relative `<img src="images/X">` from `/en/page` resolves to `/en/images/X` (doesn't exist; `en/images/` not created). Server returns HTTP 200 + text/html (SPA fallback) — looks fine to curl HEAD but browser shows broken image. | Translator copies UA HTML verbatim; UA paths assume images live as siblings. | Regex-rewrite: `<img...src="images/X">` → `<img...src="../images/X">` |

All three fixes belong in the same `add_language_switcher_link_back_to_uk()` function in `translate_en.py`. See `valikhnovski_kb/translate_en.py` for the canonical implementation. The smoke test `tools/smoke_test_all_hubs.py` catches all three.

**Tip for diagnosing**: `curl -sI ... -w "%{size_download}"` (HEAD) returns 0 for any URL — it doesn't download the body. Use `curl -s -o /dev/null -w "%{http_code}|%{content_type}|%{size_download}"` (GET) to detect SPA-fallback traps that masquerade as HTTP 200.

---

## Appendix A — How to grow the data layer

When the client wants more depth (Sprint 2 pattern), add JSON files in this order of leverage:

1. **answer_packs.json** — biggest LLM-discoverability win for ~30 min of work
2. **chunk_manifest.json** — required for proper RAG indexing
3. **media.json** — biggest journalist-utility win
4. **international.json** — only if subject has international footprint
5. **services.json** — only if subject's work organizes into discrete practice areas
6. **videos.json** — only if subject has official YouTube channel(s)

Then run validator, deploy, IndexNow ping, commit, tag, push.

## Appendix B — Translating to EN

Two approaches, depending on source:

- **From kb_en.json (Brovdi)** — translate `kb.json` → `kb_en.json` once (manually or via Sonnet API), then `build_en.py` rebuilds EN site from the translated JSON. Cleaner but requires maintaining two JSONs.
- **From UA HTML (Valikhnovski)** — `translate_en.py` takes existing UA HTML output and translates `<main>` chunks via Sonnet API while preserving structure. Then `patch_en_jsonld.py` injects Article JSON-LD post-translation. Cheaper to retrofit but EN content lags UA by one cycle.

Sonnet cost: ~$1.50-$3 per full hub translation. Always re-validate JSON-LD on EN side after translating.

---

*This playbook should evolve. When you ship a significant feature, add a section here. Tag the playbook update as `vN.M-playbook-update` in the monorepo. Push to /llm/HUB-BUILDER-PLAYBOOK.md so the public version stays the canonical reference.*
