stemedb/docs/presentations/reveal/renderer.js
jordan 1ce4004807 feat: Complete Phase 2 (The Cortex) - query, lens, and API layers
This commit adds the read path (Cortex) to complement the write path (Spine):

## Crates
- stemedb-api: HTTP API with axum + utoipa OpenAPI
  - /v1/assert, /v1/query, /v1/epoch, /v1/skeptic, /v1/trace, /v1/audit
  - Metered endpoints with quota enforcement
  - Ed25519 signature verification
- stemedb-lens: Truth resolution lenses
  - RecencyLens, ConsensusLens, ConfidenceLens
  - VoteAwareConsensusLens (Ballot Box pattern)
  - TrustAwareAuthorityLens (The Hive pattern)
  - SkepticLens (conflict analysis)
  - EpochAwareLens (paradigm-safe queries)
- stemedb-query: Query engine with materialized views

## Storage Extensions
- VoteStore: Vote aggregation with cached counts
- TrustRankStore: Agent reputation with decay
- AuditStore: Query audit trail
- IndexStore: SP/P/S index structures
- SupersessionStore: Epoch supersession chains

## SDKs
- sdk/go/steme: Go HTTP client with Ed25519 signing
- sdk/go/adk: ADK-Go tools for AI agents

## Documentation
- Updated CLAUDE.md, architecture.md, roadmap.md
- New ai-lookup entries for all services
- Use case docs for consumer health intelligence
- Arena roadmap for simulation advancement

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 13:22:44 -07:00

319 lines
10 KiB
JavaScript

/**
* Episteme Presentation Renderer
*
* Data-driven slide rendering with multi-deck support.
*
* Modes:
* flat - single deck, slides left/right (default Reveal.js)
* combined - menu column (↓ browse, → enter), decks are vertical (↓↑ navigate, ← back)
*
* Slide types are registered in RENDERERS. To add a new type, add one function.
*/
(async function () {
const dataUrl = window.PRESENTATION_DATA_URL || '/generated/stemedb.json';
let data;
try {
const resp = await fetch(dataUrl);
data = await resp.json();
} catch (err) {
document.getElementById('slides').innerHTML = `
<section class="title-slide">
<h1>Error Loading Presentation</h1>
<p class="subtitle">Could not load ${dataUrl}</p>
<p>Run: <code>npm run generate:all</code></p>
</section>`;
Reveal.initialize();
return;
}
const root = document.getElementById('slides');
const isCombined = !!data.combined;
// ── Utilities ──────────────────────────────────────────────
function esc(text) {
const el = document.createElement('div');
el.textContent = text;
return el.innerHTML;
}
function actorInitials(actor) {
if (!actor?.label) return '??';
return actor.label.split(' ').map(w => w[0]).join('').toUpperCase().slice(0, 2);
}
function fragmentList(items, cls) {
return `<ul class="${cls}">${items.map(p => `<li class="fragment">${p}</li>`).join('')}</ul>`;
}
// ── Slide-Type Registry ────────────────────────────────────
//
// Each renderer receives (slide, ctx) and returns { css?, attrs?, html }.
// ctx = { actors, sectionNum }
const RENDERERS = {
title() {
return {
css: 'title-slide',
html: '<div class="slide-title"><h1 class="brand">stemedb</h1></div>',
};
},
hook(slide) {
return {
css: 'hook-slide',
html: `<div class="slide-hook">
<p class="hook-line">${slide.line}</p>
${slide.subline ? `<p class="hook-subline fragment">${slide.subline}</p>` : ''}
</div>`,
};
},
code(slide) {
return {
css: 'code-slide',
html: `<div class="slide-code">
<h2>${slide.title}</h2>
<div class="code-block"><pre><code>${esc(slide.code.trim())}</code></pre></div>
</div>`,
};
},
problem(slide) {
return {
css: 'problem-slide',
html: `<div class="slide-problem">
<h2>${slide.title}</h2>
${fragmentList(slide.points, 'problem-points')}
</div>`,
};
},
insight(slide) {
return {
css: 'insight-slide',
html: `<div class="slide-insight">
<h2>${slide.title}</h2>
${fragmentList(slide.points, 'insight-points')}
</div>`,
};
},
vision(slide) {
return {
css: 'vision-slide',
html: `<div class="slide-vision">
<h2>${slide.title}</h2>
${fragmentList(slide.points, 'vision-points')}
${slide.tagline ? `<p class="vision-tagline fragment">${slide.tagline}</p>` : ''}
</div>`,
};
},
'section-title'(slide, ctx) {
ctx.sectionNum++;
return {
html: `<div class="slide-section-title">
<span class="section-number">Section ${ctx.sectionNum}</span>
<h2>${slide.title}</h2>
<p class="subtitle">${slide.subtitle}</p>
<p class="description">${slide.description.trim()}</p>
</div>`,
};
},
contrast(slide) {
return {
css: 'contrast-slide',
html: `<div class="slide-contrast">
<h2>${slide.title}</h2>
<div class="contrast-container">
<div class="contrast-side before fragment">
<h3>${slide.before.title}</h3>
<ul>${slide.before.points.map(p => `<li>${p}</li>`).join('')}</ul>
</div>
<div class="contrast-side after fragment">
<h3>${slide.after.title}</h3>
<ul>${slide.after.points.map(p => `<li>${p}</li>`).join('')}</ul>
</div>
</div>
</div>`,
};
},
sequence(slide, ctx) {
return {
attrs: { 'data-auto-animate': '' },
html: renderSequence(slide, ctx.actors),
};
},
};
// ── Section Factory ────────────────────────────────────────
function createSection(slide, ctx) {
const render = RENDERERS[slide.type];
if (!render) return null;
const { css, attrs, html } = render(slide, ctx);
const el = document.createElement('section');
if (css) el.classList.add(css);
if (attrs) Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v));
el.innerHTML = html;
return el;
}
// ── Build: Flat (single deck) ──────────────────────────────
function buildFlat(slides, actors) {
const ctx = { actors: actors || {}, sectionNum: 0 };
for (const slide of slides) {
const el = createSection(slide, ctx);
if (el) root.appendChild(el);
}
}
// ── Build: Combined (multi-deck) ──────────────────────────
function buildCombined(decks) {
// Column 0: vertical menu stack
const menu = document.createElement('section');
// Title (reuses the registered title renderer — single source of truth)
menu.appendChild(createSection({ type: 'title' }, { actors: {}, sectionNum: 0 }));
// Deck entry cards
decks.forEach((deck, i) => {
const card = document.createElement('section');
card.classList.add('menu-slide');
card.innerHTML = `<div class="slide-menu-card">
<span class="menu-card-number">${String(i + 1).padStart(2, '0')}</span>
<h2>${deck.meta.title}</h2>
${deck.meta.subtitle ? `<p class="menu-card-subtitle">${deck.meta.subtitle}</p>` : ''}
<p class="menu-card-hint">press <span class="key-hint">&rarr;</span> to enter</p>
</div>`;
menu.appendChild(card);
});
root.appendChild(menu);
// Columns 1-N: each deck as a horizontal section wrapping vertical slides
for (const deck of decks) {
const col = document.createElement('section');
const ctx = { actors: deck.actors || {}, sectionNum: 0 };
for (const slide of deck.slides) {
if (slide.type === 'title') continue; // menu is the landing
const el = createSection(slide, ctx);
if (el) col.appendChild(el);
}
root.appendChild(col);
}
}
// ── Sequence Slide (complex layout, kept separate) ─────────
function renderSequence(slide, allActors) {
const actors = slide.actors.map(id =>
typeof id === 'string'
? allActors[id] || { id, label: id, color: '#666666' }
: id
);
const lookup = {};
actors.forEach(a => {
lookup[a.id] = a;
const key = Object.keys(allActors).find(k => allActors[k].id === a.id);
if (key) lookup[key] = a;
});
const actorsHtml = actors.map(a => `
<div class="actor">
<div class="actor-icon" style="background-color:${a.color}">${actorInitials(a)}</div>
<span class="actor-label">${a.label}</span>
</div>`).join('');
const stepsHtml = slide.steps.map((step, i) => {
const cls = step.danger ? 'danger' : step.success ? 'success' : step.warning ? 'warning' : '';
const from = lookup[step.from] || allActors[step.from] || { label: step.from };
const to = lookup[step.to] || allActors[step.to] || { label: step.to };
return `
<div class="step-card ${cls} fragment" data-fragment-index="${i}">
<div class="step-index">${i + 1}</div>
<div class="step-content">
<div class="step-header">
<div class="step-actors">
<span class="step-from">${from.label}</span>
<span class="step-arrow">&rarr;</span>
<span class="step-to">${to.label}</span>
</div>
<span class="step-action">${step.action}</span>
</div>
<div class="step-label">${step.label}</div>
${step.note ? `<div class="step-note">${step.note}</div>` : ''}
${step.callout ? `<div class="step-callout">${step.callout}</div>` : ''}
</div>
</div>`;
}).join('');
return `<div class="slide-sequence">
<div class="header"><h3>${slide.title}</h3></div>
<div class="actors-row">${actorsHtml}</div>
<div class="steps-container">${stepsHtml}</div>
</div>`;
}
// ── Keyboard: Combined Mode ────────────────────────────────
//
// Menu (h=0): ↓↑ browse cards, → enter deck
// Deck (h>0): ↓↑ navigate slides, ← back to menu
// → also advances (alias for ↓) so arrow-mashing works
function combinedKeys() {
return {
// → : menu → enter deck; inside deck → next slide
39: function () {
const { h, v } = Reveal.getIndices();
if (h === 0) {
Reveal.slide(v > 0 ? v : 1, 0);
} else {
Reveal.down();
}
},
// ← : first slide of deck → back to its menu card; deeper → prev slide
37: function () {
const { h, v } = Reveal.getIndices();
if (h > 0 && v === 0) {
Reveal.slide(0, h);
} else if (h > 0) {
Reveal.up();
}
},
};
}
// ── Render & Initialize ────────────────────────────────────
if (isCombined) {
buildCombined(data.decks);
} else {
buildFlat(data.slides, data.actors || {});
}
Reveal.initialize({
hash: true,
history: true,
controls: true,
progress: true,
center: true,
transition: 'fade',
transitionSpeed: 'fast',
keyboard: isCombined ? combinedKeys() : { 39: 'next', 37: 'prev' },
});
Reveal.on('fragmentshown', e => e.fragment.classList.add('visible'));
Reveal.on('fragmenthidden', e => e.fragment.classList.remove('visible'));
})();