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>
319 lines
10 KiB
JavaScript
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">→</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">→</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'));
|
|
|
|
})();
|