Architecture Deep Dive
Spry is a sophisticated Markdown-based automation framework that transforms Markdown documents into executable workflows. It uses a layered architecture that parses, enriches, and executes code blocks embedded in Markdown files.
Architecture Layers
Section titled “Architecture Layers”1. Application Layer (Entry Points)
Section titled “1. Application Layer (Entry Points)”The top layer provides CLI interfaces for different use cases:
runbook/cli.ts- Shell script orchestration from Markdownsqlpage/cli.ts- SQLPage integration for web applicationstask/cli.ts- Generic task execution engine
Each CLI module provides commands that users interact with directly (e.g., spry runbook execute, spry task run).
2. Execution Engines
Section titled “2. Execution Engines”This layer handles the actual execution logic:
runbook/orchestrate- Executes shell commands from code blocks, manages dependencies, captures outputtask/execute- Generic task execution with DAG (Directed Acyclic Graph) planning and state managementsqlpage/playbook- Specialized execution for SQL queries and web content generation
Key Responsibilities:
- Parse task directives from code cells
- Build dependency graphs
- Execute tasks in correct order
- Capture and manage output
- Handle errors and retries
3. Markdown AST Pipeline
Section titled “3. Markdown AST Pipeline”The core transformation layer that enriches raw Markdown with metadata:
Plugin System (remark/plugin/)
Section titled “Plugin System (remark/plugin/)”Document-level plugins:
doc-frontmatter- Extracts YAML frontmatter from documentsdoc-schema- Validates document structure against schemas
Node-level plugins:
code-frontmatter- Parses processing instructions from code fence info stringscode-partial- Identifies reusable code blocksheading-frontmatter- Extracts metadata from heading commentsnode-classify- Categorizes nodes by type and purpose
AST Control (remark/mdastctl/)
Section titled “AST Control (remark/mdastctl/)”io.ts- File loading and AST reading/writingview.ts- AST visualization and debugging utilities
4. Markdown Document Model
Section titled “4. Markdown Document Model”High-level abstractions over the AST:
governedmd.ts- Core types:CodeCell,Issue,Sourcefluent-doc.ts- Builder API for constructing Markdown documents programmaticallynotebook/- Document structures:notebook.ts- Collection of cells with frontmatterplaybook.ts- Sections and execution contextpartial.ts- Reusable code block managementpseudo-cell.ts- Dynamically generated cells
5. Unified/Remark Parsing
Section titled “5. Unified/Remark Parsing”Foundation layer using the unified ecosystem:
- Parses raw Markdown into MDAST (Markdown Abstract Syntax Tree)
- Provides standardized tree transformation interface
- Enables plugin composition
6. Universal Utilities
Section titled “6. Universal Utilities”Cross-cutting utilities used throughout:
task.ts- DAG execution engine with dependency resolutionshell.ts- Shell command execution wrapperevent-bus.ts- Type-safe event systemdepends.ts- Dependency resolution algorithmscode.ts- Language registry and detectioninterpolate.ts- Template variable substitutionposix-pi.ts- Processing instruction parserresource.ts- Content fetching and caching
Core Concepts
Section titled “Core Concepts”Code Cells
Section titled “Code Cells”The fundamental unit of executable content:
interface CodeCell { kind: "code" language: string // e.g., "bash", "sql", "typescript" source: string // The actual code startLine: number // Source location endLine: number pi?: string // Processing instructions (flags) parsedPI?: PosixStylePI // Parsed flags and arguments attrs?: object // JSON5 attributes}Example Markdown:
```bash my-task --dep other-task --capture outputecho "Hello World"```Becomes a CodeCell with:
language: “bash”pi: “my-task —dep other-task —capture output”parsedPI.flags:{ dep: "other-task", capture: "output" }
Task Directives
Section titled “Task Directives”Instructions extracted from code cells that define executable tasks:
interface TaskDirective { nature: "TASK" | "CONTENT" // Executable vs display-only identity: string // Unique task name language: LanguageSpec // How to execute it deps?: string[] // Task dependencies}Tasks with dependencies form a DAG that determines execution order.
Processing Instructions (PI)
Section titled “Processing Instructions (PI)”POSIX-style flags in code fence info strings:
bash task-name --flag value --boolean-flag { "json": "attrs" }Parsed into:
- Positional tokens:
["bash", "task-name"] - Flags:
{ flag: "value", booleanFlag: true } - Attributes:
{ json: "attrs" }
Dependency Resolution
Section titled “Dependency Resolution”Tasks declare dependencies via --dep flags:
```bash task-aecho "First"```
```bash task-b --dep task-aecho "Depends on task-a"```The system:
- Builds dependency graph
- Detects cycles
- Computes topological order
- Executes in correct sequence
Data Flow
Section titled “Data Flow”From Markdown to Execution
Section titled “From Markdown to Execution”┌─────────────────┐│ Spryfile.md │ Raw Markdown source└────────┬────────┘ │ │ remark.parse() ▼┌─────────────────┐│ MDAST Tree │ Abstract Syntax Tree└────────┬────────┘ │ │ Plugins (doc-frontmatter, code-frontmatter, etc.) ▼┌─────────────────┐│ Enriched MDAST │ Tree with metadata in node.data└────────┬────────┘ │ │ Notebook constructor ▼┌─────────────────┐│ Notebook │ { frontmatter, cells[], issues[] }└────────┬────────┘ │ │ Playbook constructor ▼┌─────────────────┐│ Playbook │ Organized sections with context└────────┬────────┘ │ │ Task directive extraction ▼┌─────────────────┐│ TaskCell[] │ Cells with taskId() and taskDeps()└────────┬────────┘ │ │ DAG planning ▼┌─────────────────┐│ Execution Plan │ Topologically sorted task order└────────┬────────┘ │ │ Task execution ▼┌─────────────────┐│ Results │ Output, captured data, errors└─────────────────┘Key Design Patterns
Section titled “Key Design Patterns”1. Safe Node Data Pattern
Section titled “1. Safe Node Data Pattern”Type-safe metadata attachment to AST nodes:
// Define typed data keyconst codeFrontmatterNDF = nodeDataFactory<"codeFM", CodeFrontmatter>("codeFM")
// Use safely with type checkingif (codeFrontmatterNDF.is(node)) { const fm = node.data.codeFM // TypeScript knows the type}With validation:
const codeSpawnableSNDF = safeNodeDataFactory( "codeSpawnable", codeSpawnableSchema, // Zod schema { onAttachSafeParseError: ({ node, error }) => { // Handle validation errors } })2. Plugin Pipeline
Section titled “2. Plugin Pipeline”Plugins compose in order, each transforming the tree:
const processor = remark() .use(remarkGfm) // 1. GitHub Flavored Markdown .use(remarkFrontmatter) // 2. Enable YAML frontmatter .use(docFrontmatter) // 3. Parse frontmatter .use(codeFrontmatter) // 4. Parse code fence metadata .use(codePartials) // 5. Identify partials .use(nodeClassify) // 6. Classify nodes
const tree = processor.parse(markdown)processor.runSync(tree) // Run all pluginsEach plugin adds data to node.data[KEY] without modifying the original tree structure.
3. Event Bus Pattern
Section titled “3. Event Bus Pattern”Type-safe, decoupled communication:
interface TaskEvents { "task:start": { task: TaskCell; ctx: Context } "task:complete": { task: TaskCell; result: Result } "task:error": { task: TaskCell; error: Error }}
const bus = eventBus<TaskEvents>()
// Subscribebus.on("task:start", ({ task }) => { console.log(`Starting ${task.taskId()}`)})
// Emitbus.emit("task:start", { task, ctx })Used for:
- Task lifecycle events
- Shell command output
- Interpolation resolution
- Error reporting
4. DAG Execution Engine
Section titled “4. DAG Execution Engine”Generic task orchestration with dependency resolution:
interface Task { taskId(): string taskDeps(): string[]}
// Build execution planconst plan = planDAG(tasks, { getId: (t) => t.taskId(), getDeps: (t) => t.taskDeps()})
// Execute in topological orderawait executeDAG(plan, async (task, ctx) => { const result = await runTask(task) return ok({ ...ctx, results: [...ctx.results, result] })}, { eventBus })Features:
- Cycle detection
- Missing dependency errors
- Parallel execution (when possible)
- Error propagation
5. Schema-Driven Validation
Section titled “5. Schema-Driven Validation”Zod schemas for runtime validation and type inference:
const taskFlagsSchema = z.object({ descr: z.string().optional(), dep: flexibleTextSchema.optional(), capture: z.string().optional()}).transform((raw) => ({ description: raw.descr, dependencies: Array.isArray(raw.dep) ? raw.dep : [raw.dep], captureAs: raw.capture}))
type TaskFlags = z.infer<typeof taskFlagsSchema>Benefits:
- Runtime validation
- Automatic TypeScript types
- Transform/normalization
- Helpful error messages
Module Organization
Section titled “Module Organization”/lib/markdown/ - Document Model
Section titled “/lib/markdown/ - Document Model”Core document abstractions:
- governedmd.ts - Base types (
CodeCell,Issue,Source) - fluent-doc.ts - Builder API for creating Markdown
- notebook/ - Higher-level structures
- Notebook: cells + frontmatter
- Playbook: sections + execution context
- Partial: reusable blocks
- Pseudo-cell: generated cells
/lib/remark/ - AST Processing
Section titled “/lib/remark/ - AST Processing”Markdown tree manipulation:
-
mdast/ - MDAST utilities
safe-data.ts- Type-safe node dataissue.ts- Error/warning trackingnode-src-text.ts- Source extraction
-
mdastctl/ - High-level control
io.ts- File I/Oview.ts- Visualization
-
plugin/ - Transformations
- Document-level: frontmatter, schema
- Node-level: code metadata, partials
/lib/task/ - Task Execution
Section titled “/lib/task/ - Task Execution”Generic task engine:
- cell.ts - TaskCell and TaskDirective types
- execute.ts - Execution state management
- cli.ts - CLI commands
/lib/runbook/ - Shell Orchestration
Section titled “/lib/runbook/ - Shell Orchestration”Shell-specific execution:
- orchestrate.ts - CodeSpawnable type, execution logic
- cli.ts - Runbook commands
/lib/sqlpage/ - SQLPage Integration
Section titled “/lib/sqlpage/ - SQLPage Integration”Web application generation:
- cli.ts - Init and serve commands
- playbook.ts - SQL task definitions
- route.ts - URL routing
- content.ts - Page generation
/lib/universal/ - Shared Utilities
Section titled “/lib/universal/ - Shared Utilities”Cross-cutting concerns (50+ modules):
- Task execution (
task.ts) - Shell commands (
shell.ts) - Event system (
event-bus.ts) - Dependency resolution (
depends.ts) - Language detection (
code.ts) - Template interpolation (
interpolate.ts) - Flag parsing (
posix-pi.ts) - And many more…
Extension Points
Section titled “Extension Points”Custom Remark Plugins
Section titled “Custom Remark Plugins”Add new metadata to AST nodes:
import { Plugin } from "unified"import { visit } from "unist-util-visit"
export const myPlugin: Plugin = (options) => { return (tree) => { visit(tree, "code", (node) => { node.data ??= {} node.data.myCustomData = analyzeCode(node) }) }}Custom Task Inspectors
Section titled “Custom Task Inspectors”Handle new language types:
import { TaskDirectiveInspector } from "./cell.ts"
const myInspector: TaskDirectiveInspector = ({ cell, pb }) => { if (cell.language === "my-language") { return { nature: "TASK", identity: cell.parsedPI?.firstToken ?? "unnamed", language: myLanguageSpec, deps: cell.parsedPI?.flags.dep } } return false // Not handled by this inspector}Custom Event Handlers
Section titled “Custom Event Handlers”React to execution events:
const bus = eventBus<TaskExecEventMap>()
bus.on("task:start", ({ task }) => { console.log(`▶ ${task.taskId()}`)})
bus.on("task:complete", ({ task, result }) => { console.log(`✓ ${task.taskId()} - ${result.duration}ms`)})
bus.on("task:error", ({ task, error }) => { console.error(`✗ ${task.taskId()} - ${error.message}`)})Testing Strategy
Section titled “Testing Strategy”Tests follow the *_test.ts pattern adjacent to source files:
Deno.test("CodeFrontmatter plugin", async (t) => { await t.step("parses basic flags", () => { const md = "```bash task --flag\ncode\n```" const tree = pipeline().parse(md) pipeline().runSync(tree)
const node = findCode(tree) assertEquals(node.data.codeFM.pi.flags.flag, true) })
await t.step("handles dependencies", () => { const md = "```bash task --dep dep1 --dep dep2\ncode\n```" const tree = pipeline().parse(md) pipeline().runSync(tree)
const node = findCode(tree) assertEquals(node.data.codeFM.pi.flags.dep, ["dep1", "dep2"]) })})Run tests:
deno test --parallel --allow-allReal-World Example
Section titled “Real-World Example”Given this Markdown file:
---title: Deploy Applicationversion: 1.0---
# Deployment Tasks
```bash build --capture image-namedocker build -t myapp:latest .echo "myapp:latest"```
```bash test --dep builddocker run myapp:latest npm test```
```bash deploy --dep test --dep builddocker push ${image-name}kubectl apply -f deploy.yml```Spry will:
- Parse into MDAST with 3 code nodes
- Enrich with plugins:
- Extract frontmatter:
{ title: "Deploy Application", version: "1.0" } - Parse code metadata: flags, dependencies
- Extract frontmatter:
- Build Notebook with 3 CodeCells
- Create Playbook with execution context
- Extract TaskCells:
build(no deps, capturesimage-name)test(depends onbuild)deploy(depends ontestandbuild)
- Plan DAG:
build → test → deploy - Execute:
- Run
build, capture output →image-name = "myapp:latest" - Run
testwith built image - Run
deploy, interpolating${image-name}→docker push myapp:latest
- Run
Summary
Section titled “Summary”Spry’s architecture is built on these principles:
- Layered separation of concerns (parse → enrich → execute)
- Plugin composability for extensibility
- Type safety through Zod schemas and TypeScript
- Declarative dependencies via processing instructions
- Event-driven observability
- Universal utilities for code reuse
This design enables powerful automation workflows defined in readable Markdown, with strong guarantees about execution order and type safety throughout.