Phase 1 delivers the complete durability and storage layer:
- WAL with crash recovery: Append-only journal with BLAKE3 checksums,
fsync guarantees, and proper seek-to-EOF on reopen
- Storage engine: sled-backed KVStore with scan_prefix for range queries
- Content-addressed storage: H:{hash}, V:{hash}, E:{hash} key patterns
- Ingestor: Background worker tailing WAL, writing to KV with 8-byte
aligned record headers for rkyv zero-copy deserialization
- Comprehensive tests: 31 tests covering crash recovery, round-trips,
and multi-cycle durability
New crates: stemedb-wal, stemedb-storage, stemedb-ingest
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
227 lines
5.5 KiB
TypeScript
227 lines
5.5 KiB
TypeScript
#!/usr/bin/env npx ts-node
|
|
/**
|
|
* Presentation Generator
|
|
*
|
|
* Reads YAML sequence data and generates:
|
|
* 1. Mermaid sequence diagrams (.mmd)
|
|
* 2. JSON for Reveal.js slides
|
|
*
|
|
* Usage: npx ts-node generate.ts ../data/agile-agent-team.yaml
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as yaml from 'yaml';
|
|
|
|
interface Actor {
|
|
id: string;
|
|
label: string;
|
|
description: string;
|
|
color: string;
|
|
}
|
|
|
|
interface StepData {
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface Step {
|
|
id: string;
|
|
from: string;
|
|
to: string;
|
|
action: string;
|
|
label: string;
|
|
data?: StepData;
|
|
note?: string;
|
|
callout?: string;
|
|
danger?: boolean;
|
|
warning?: boolean;
|
|
success?: boolean;
|
|
}
|
|
|
|
interface Sequence {
|
|
id: string;
|
|
title: string;
|
|
subtitle: string;
|
|
description: string;
|
|
steps: Step[];
|
|
}
|
|
|
|
interface PresentationData {
|
|
meta: {
|
|
id: string;
|
|
title: string;
|
|
subtitle: string;
|
|
version: string;
|
|
};
|
|
actors: Record<string, Actor>;
|
|
sequences: Sequence[];
|
|
annotations: Record<string, {
|
|
color: string;
|
|
label: string;
|
|
icon: string;
|
|
}>;
|
|
}
|
|
|
|
// Generate Mermaid sequence diagram for a sequence
|
|
function generateMermaid(data: PresentationData, sequence: Sequence): string {
|
|
const lines: string[] = [];
|
|
|
|
lines.push('sequenceDiagram');
|
|
lines.push(` %% ${sequence.title}: ${sequence.subtitle}`);
|
|
lines.push('');
|
|
|
|
// Collect unique actors used in this sequence
|
|
const usedActors = new Set<string>();
|
|
for (const step of sequence.steps) {
|
|
usedActors.add(step.from);
|
|
usedActors.add(step.to);
|
|
}
|
|
|
|
// Add participant declarations
|
|
for (const actorKey of usedActors) {
|
|
const actor = data.actors[actorKey];
|
|
if (actor) {
|
|
lines.push(` participant ${actor.id} as ${actor.label}`);
|
|
}
|
|
}
|
|
lines.push('');
|
|
|
|
// Add steps
|
|
for (const step of sequence.steps) {
|
|
const fromActor = data.actors[step.from];
|
|
const toActor = data.actors[step.to];
|
|
|
|
if (!fromActor || !toActor) continue;
|
|
|
|
// Determine arrow style
|
|
let arrow = '->>';
|
|
if (step.action === 'response') {
|
|
arrow = '-->>';
|
|
}
|
|
|
|
// Add the message
|
|
const label = step.label.replace(/"/g, "'");
|
|
lines.push(` ${fromActor.id}${arrow}${toActor.id}: ${label}`);
|
|
|
|
// Add note if present
|
|
if (step.note) {
|
|
const noteText = step.note.replace(/"/g, "'");
|
|
const position = step.from === step.to ? 'over' : 'right of';
|
|
const noteActor = step.from === step.to ? fromActor.id : toActor.id;
|
|
|
|
if (step.danger) {
|
|
lines.push(` Note ${position} ${noteActor}: ⚠️ ${noteText}`);
|
|
} else if (step.success) {
|
|
lines.push(` Note ${position} ${noteActor}: ✓ ${noteText}`);
|
|
} else {
|
|
lines.push(` Note ${position} ${noteActor}: ${noteText}`);
|
|
}
|
|
}
|
|
|
|
lines.push('');
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
// Generate JSON for Reveal.js
|
|
function generateRevealJson(data: PresentationData): object {
|
|
const slides: object[] = [];
|
|
|
|
// Title slide
|
|
slides.push({
|
|
type: 'title',
|
|
title: data.meta.title,
|
|
subtitle: data.meta.subtitle,
|
|
});
|
|
|
|
// Generate slides for each sequence
|
|
for (const sequence of data.sequences) {
|
|
// Sequence title slide
|
|
slides.push({
|
|
type: 'section-title',
|
|
title: sequence.title,
|
|
subtitle: sequence.subtitle,
|
|
description: sequence.description,
|
|
});
|
|
|
|
// Create step slides - group steps for animation
|
|
const stepSlide = {
|
|
type: 'sequence',
|
|
sequenceId: sequence.id,
|
|
title: sequence.title,
|
|
actors: {} as Record<string, Actor>,
|
|
steps: sequence.steps.map((step, index) => ({
|
|
...step,
|
|
index,
|
|
fromActor: data.actors[step.from],
|
|
toActor: data.actors[step.to],
|
|
})),
|
|
};
|
|
|
|
// Collect actors for this sequence
|
|
for (const step of sequence.steps) {
|
|
if (data.actors[step.from]) {
|
|
stepSlide.actors[step.from] = data.actors[step.from];
|
|
}
|
|
if (data.actors[step.to]) {
|
|
stepSlide.actors[step.to] = data.actors[step.to];
|
|
}
|
|
}
|
|
|
|
slides.push(stepSlide);
|
|
}
|
|
|
|
return {
|
|
meta: data.meta,
|
|
actors: data.actors,
|
|
annotations: data.annotations,
|
|
slides,
|
|
};
|
|
}
|
|
|
|
// Main
|
|
function main() {
|
|
const args = process.argv.slice(2);
|
|
if (args.length < 1) {
|
|
console.error('Usage: npx ts-node generate.ts <input.yaml>');
|
|
process.exit(1);
|
|
}
|
|
|
|
const inputPath = path.resolve(args[0]);
|
|
const inputDir = path.dirname(inputPath);
|
|
const baseName = path.basename(inputPath, '.yaml');
|
|
|
|
// Read and parse YAML
|
|
const yamlContent = fs.readFileSync(inputPath, 'utf-8');
|
|
const data = yaml.parse(yamlContent) as PresentationData;
|
|
|
|
// Output directory
|
|
const outputDir = path.resolve(inputDir, '..', 'generated');
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
|
|
// Generate Mermaid for each sequence
|
|
for (const sequence of data.sequences) {
|
|
const mermaid = generateMermaid(data, sequence);
|
|
const mermaidPath = path.join(outputDir, `${baseName}-${sequence.id}.mmd`);
|
|
fs.writeFileSync(mermaidPath, mermaid);
|
|
console.log(`Generated: ${mermaidPath}`);
|
|
}
|
|
|
|
// Generate combined Mermaid
|
|
const allMermaid = data.sequences
|
|
.map(seq => generateMermaid(data, seq))
|
|
.join('\n\n---\n\n');
|
|
const combinedMermaidPath = path.join(outputDir, `${baseName}.mmd`);
|
|
fs.writeFileSync(combinedMermaidPath, allMermaid);
|
|
console.log(`Generated: ${combinedMermaidPath}`);
|
|
|
|
// Generate JSON for Reveal.js
|
|
const revealJson = generateRevealJson(data);
|
|
const jsonPath = path.join(outputDir, `${baseName}.json`);
|
|
fs.writeFileSync(jsonPath, JSON.stringify(revealJson, null, 2));
|
|
console.log(`Generated: ${jsonPath}`);
|
|
}
|
|
|
|
main();
|