Architecture Overview
Understanding how Spry processes Markdown into executable workflows
High-Level Architecture
Spry Architecture
Hover over components for details
Spry transforms Markdown documents into executable workflows through a multi-stage pipeline. The architecture separates concerns into distinct layers: loading, parsing, graph construction, projection, and execution.
Processing Pipeline
Stage 1: Input/Loading
Location: lib/axiom/io/
The loading stage accepts Markdown files from multiple sources and normalizes them into a unified resource format.
// Load resources from files, URLs, or globs
const resources = await markdownASTs({
sources: ["runbook.md", "*.md", "https://example.com/doc.md"],
cwd: process.cwd()
});Key components:
resource.ts- Abstract resource loadingmod.ts- Pipeline orchestration- VFile integration for metadata tracking
Flexible Input
Spry can load Markdown from local files, glob patterns, or remote URLs, making it easy to compose documentation from multiple sources.
Stage 2: Parsing
Location: lib/axiom/remark/
Markdown is parsed into MDAST (Markdown Abstract Syntax Tree) using unified and remark plugins:
// Parser pipeline
unified()
.use(remarkParse)
.use(remarkFrontmatter)
.use(remarkGfm)
.use(remarkDirective)The result is a tree structure:
interface Root {
type: "root";
children: Node[];
data?: {
documentFrontmatter?: Record<string, unknown>;
};
}
interface Code {
type: "code";
lang: string;
meta: string;
value: string;
data?: {
codeFM?: ParsedCodeFrontmatter;
};
}This AST representation allows Spry to analyze and transform the document structure programmatically.
Stage 3: AST Enrichment
Location: lib/axiom/remark/
Remark plugins enhance the AST with semantic metadata and relationships:
| Plugin | Purpose |
|---|---|
doc-frontmatter.ts | Parse YAML frontmatter |
code-directive-candidates.ts | Identify PARTIAL directives |
actionable-code-candidates.ts | Mark executable cells |
code-contribute.ts | Handle file includes |
node-decorator.ts | Add semantic decorations |
These plugins transform a raw syntax tree into a semantically rich structure that understands the document's intent.
Stage 4: Graph Construction
Location: lib/axiom/edge/
Edge rules create semantic relationships between nodes, building a graph that represents the document's structure and dependencies:
// Build graph from AST
const graph = buildGraph(root, typicalRules());Edge Types:
| Edge Type | Description |
|---|---|
containedInSection | Hierarchical containment relationships |
codeDependsOn | Task dependency relationships |
frontmatterClassification | Semantic classification from frontmatter |
sectionSemanticId | Section identity and grouping |
nodesClassification | Node role classification |
The graph representation enables powerful queries and transformations that would be difficult with raw AST traversal.
Graph-Based Architecture
By representing documents as graphs, Spry can analyze dependencies, validate relationships, and optimize execution order automatically.
Stage 5: Projection
Location: lib/axiom/projection/
Projections create domain-specific views of the graph for different use cases:
// FlexibleProjection - UI-neutral view
const flex = buildFlexibleProjection(graph);
// PlaybookProjection - Execution view
const playbook = buildPlaybookProjection(graph);
// TreeProjection - Hierarchical view
const tree = buildTreeProjection(graph);FlexibleProjection:
interface FlexibleProjection {
documents: ProjectedDocument[];
nodes: ProjectedNode[];
edges: ProjectedEdge[];
hierarchies: ProjectedHierarchy[];
}PlaybookProjection:
interface PlaybookProjection {
executables: Executable[];
materializables: Materializable[];
directives: Directive[];
tasks: ExecutableTask[];
}Projections isolate domain concerns, making it easy to add new views without modifying core graph logic.
Stage 6: Execution
Location: lib/axiom/orchestrate/
Execute tasks from the playbook in the correct order, respecting dependencies:
// Build execution plan
const plan = executionPlan(tasks);
// Execute in topological order
await executeDAG(plan, {
executor: shellExecutor,
onTaskStart: (task) => console.log(`Starting: ${task.taskId()}`),
onTaskComplete: (task, result) => console.log(`Done: ${task.taskId()}`),
});The execution engine handles:
- Dependency resolution
- Parallel execution of independent tasks
- Error handling and rollback
- Progress reporting
Key Modules
lib/axiom/
Core semantic processing layer:
| Module | Description |
|---|---|
graph.ts | Graph construction and manipulation |
edge/ | Edge rules for relationship extraction |
projection/ | Domain-specific view builders |
orchestrate/ | Execution engine and DAG runner |
io/ | Resource loading and normalization |
remark/ | AST plugins and transformations |
mdast/ | AST utilities and hooks |
text-ui/ | CLI interface components |
lib/spawn/
Unified execution framework providing language-agnostic code execution:
| Module | Description |
|---|---|
shell.ts | Low-level process spawning (thin wrapper around Deno.Command) |
code-shell.ts | LanguageEngine interface and execution orchestration |
function-shell.ts | In-process function engines (env, envrc) |
os-shell.ts | OS shell engines (bash, sh, PowerShell, cmd, fish) |
sql-shell/ | SQL database engines (postgres, sqlite, duckdb) |
factory.ts | Catalog parsing and using() API for engine resolution |
Key concepts:
- Languages vs Runtimes: Languages are semantic (SQL, shell), runtimes are concrete (psql, bash)
- Execution Modes:
stdin,file,eval,auto- how code is passed to the runtime - Catalogs: YAML-based declarative runtime configuration
- LanguageEngine: Typed interface describing how a runtime should be invoked
Extensible Execution
The spawn layer abstracts execution details, making it easy to add support for new languages and runtimes without changing core logic.
lib/universal/
Shared utilities used across the system:
| Module | Description |
|---|---|
task.ts | DAG execution and planning |
code.ts | Language registry and specifications |
directive.ts | Directive parsing and handling |
posix-pi.ts | CLI flag parsing |
render.ts | Template interpolation |
resource.ts | Resource abstraction |
lib/playbook/
Domain-specific patterns and integrations:
| Module | Description |
|---|---|
sqlpage/ | SQLPage integration and configuration |
Data Flow Example
Here's how a document flows through the entire pipeline:
runbook.md
│
▼ [axiom/io/resource.ts]
VFile with content
│
▼ [axiom/remark/parser pipeline]
MDAST (raw)
│
▼ [axiom/remark/plugins]
MDAST (enriched with codeFM, decorations)
│
▼ [axiom/edge/orchestrate.ts]
Graph { root, edges: [...relationships] }
│
▼ [axiom/projection/playbook.ts]
PlaybookProjection { tasks: [...] }
│
▼ [axiom/orchestrate/task.ts]
ExecutionPlan { layers: [[task1], [task2, task3], [task4]] }
│
▼ [spawn/factory.ts → spawn/code-shell.ts → spawn/shell.ts]
LanguageEngine execution with catalog-based runtime resolution
│
▼
LanguageSpawnResult { stdout, stderr, exitCode, success }Each stage transforms the data into a form appropriate for the next stage, maintaining clear separation of concerns.
Extension Points
Spry's architecture provides multiple extension points for customization:
1. Custom Remark Plugins
Add new AST transformations:
const myPlugin: Plugin<[], Root> = () => {
return (tree) => {
visit(tree, "code", (node) => {
// Transform code nodes
});
};
};Use cases:
- Custom code block metadata
- Special directive handling
- Document validation rules
2. Custom Edge Rules
Add new relationship types:
const myRule: EdgeRule = {
rel: "myRelationship",
apply: function* (root) {
// Yield edges
yield { from: nodeA, to: nodeB, rel: "myRelationship" };
},
};Use cases:
- Custom dependency types
- Semantic relationships
- Cross-document references
3. Custom Projections
Create new domain views:
function myProjection(graph: Graph): MyProjection {
// Transform graph into domain-specific view
return { ... };
}Use cases:
- Custom report formats
- Specialized workflows
- Domain-specific validations
4. Custom Language Engines
Add new execution runtimes via lib/spawn:
import { LanguageEngine, ExecutionMode } from "./lib/spawn/code-shell.ts";
const myEngine: LanguageEngine<MyInit> = {
engineIdentity: "my-runtime",
supportedModes: ["stdin", "file"],
plan: (init, input, mode) => ({
argv: ["my-runtime", "--flag"],
stdin: mode === "stdin" ? input.code : undefined,
cleanup: async () => { /* cleanup temp files */ },
}),
};Engines can be registered in catalogs (YAML) for declarative configuration:
runtimes:
my-db:
engine: my-runtime
connection: ${env.MY_DB_URL}Use cases:
- Custom database engines
- Domain-specific languages
- Integration with proprietary tools
Designed for Extension
Every major component in Spry is designed to be extended without modifying core code. This makes Spry adaptable to diverse workflows and use cases.
Type System
Key types that define the system's structure:
// From lib/axiom/
type Graph<Rel, Edge> = {
root: Root;
edges: Edge[];
};
type GraphEdge<Rel> = {
rel: Rel;
from: Node;
to: Node;
};
// From lib/universal/
type Task<Baggage> = {
taskId: () => string;
taskDeps?: () => string[];
} & Baggage;
type ExecutionPlan<T> = {
tasks: T[];
layers: T[][];
order: T[];
};The type system ensures type safety throughout the pipeline while remaining flexible enough for diverse use cases.
Architecture Principles
Spry's architecture follows these core principles:
Separation of Concerns
Each layer has a single, well-defined responsibility. Parsing doesn't know about execution, and execution doesn't know about graph structure.
Progressive Enhancement
Documents start as simple Markdown and gain semantic meaning through each processing stage without losing information.
Graph-Based Reasoning
Representing documents as graphs enables powerful analysis and transformation that would be impossible with linear processing.
Extensibility
Every major component can be extended or replaced without modifying core code, enabling customization for diverse use cases.
Performance Considerations
Spry's architecture optimizes for:
- Lazy Evaluation: Projections are built on-demand, not eagerly
- Parallel Execution: Independent tasks execute concurrently
- Memory Efficiency: Streaming processing for large documents
- Caching: Parsed ASTs and graphs can be cached across invocations
For large-scale deployments, consider:
- Pre-parsing documents during CI/CD
- Caching projection results
- Parallelizing document processing across multiple cores
Summary
Spry's architecture transforms Markdown into executable workflows through a principled, multi-stage pipeline:
- Load documents from diverse sources
- Parse into abstract syntax trees
- Enrich with semantic metadata
- Build relationship graphs
- Project into domain-specific views
- Execute with dependency-aware orchestration
This architecture provides:
- Clarity: Each stage has a clear purpose
- Flexibility: Easy to extend at every level
- Reliability: Type-safe transformations throughout
- Performance: Optimized for real-world usage
How is this guide?
Last updated on