Skip to content
Architecture
AutoXXS (320px)XS (375px)SM (640px)MD (768px)LG (1024px)XL (1280px)XXL (1536px)
SketchMaterialiOSTamagui
DataInjectionKeyPatternsServiceTransactionProcessResearchProductQualityPerformanceSpecDomainFunctionTechnologyArchitectureConfigMiddlewareDataDatabaseDrizzleMigrationModelop-sqliteSchemaSQLState ManagementDraftKeystoneMergePatchPatchesPersistenceReactiveRedoStoreUndoTestingDeviceFactoryIsolationTypeScriptZodTopicsCommunicationBidsNVCDesignDesign ImplicationsEducationPedagogyFoundationsPsychologyAttachmentFloodingRelatingAuthentic RelatingUIEditorReact Native

Architecture

StarSpec is a compiler. The same .mdoc source files are processed by three independent passes, each targeting a different audience. No separate build step β€” pnpm compile runs all three.

Conceptual Model

graph Conceptual Model
Gallery

Compilation Pipeline

ASCII diagram of the three-pass pipeline
architecturepipelinediagram compilation-pipeline-ascii
content/**/*.mdoc
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ StarSpec Compiler β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ agent pass β”‚ β”‚ public pass β”‚ β”‚
β”‚ β”‚ (Data Ext.) β”‚ β”‚ (6-phase MDX) β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚instructions β”‚ β”‚ β”‚
β”‚ β”‚ pass β”‚ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β”‚
β–Ό β–Ό
dist/bundles/ src/content/docs/
{type}/{slug}.toon {type}/{slug}.mdx
starspec/**/*.toon sidebar.json
β”‚ summary.json
β”‚ β”‚
β–Ό β–Ό
MCP server Astro + Starlight
POST /mcp β†’ public docs site
POST /api/call
PassInputOutputPurpose
agentcontent/**/*.mdocdist/bundles/{type}/{slug}.toonExtract structured data for AI consumption
publiccontent/**/*.mdocsrc/content/docs/{type}/{slug}.mdTransform spec to human-readable MDX
instructionsinstructions/**/*.mdocdist/bundles/starspec/**/*.toonBundle engineering docs for agents

Repository Layout

starspec/
β”œβ”€β”€ content/ ← spec source files (app-specific)
β”‚ β”œβ”€β”€ roles/ *.role.mdoc
β”‚ β”œβ”€β”€ domains/ *.domain.mdoc
β”‚ β”œβ”€β”€ features/ *.feature.mdoc
β”‚ β”œβ”€β”€ flows/ *.flow.mdoc
β”‚ β”œβ”€β”€ milestones/ *.milestone.mdoc
β”‚ β”œβ”€β”€ stories/ *.story.mdoc
β”‚ β”œβ”€β”€ manifests/ *.manifest.mdoc
β”‚ β”œβ”€β”€ surfaces/ *.surface.mdoc
β”‚ β”œβ”€β”€ tours/ *.tour.mdoc
β”‚ └── articles/ *.article.mdoc
β”œβ”€β”€ instructions/ ← agent how-to guides (engine-owned)
β”‚ β”œβ”€β”€ tag/ *.tag.mdoc (tag reference docs)
β”‚ └── fragments/ reusable content fragments
β”œβ”€β”€ packages/
β”‚ └── compiler/
β”‚ └── src/
β”‚ β”œβ”€β”€ core/ ← SOLID Domain Model (19 modules + rendering/ + tags/)
β”‚ β”œβ”€β”€ processors/ ← Tag processors (31 files + aggregates/)
β”‚ β”œβ”€β”€ passes/ ← Compilation passes (agent/, public/, shared/, instructions.ts)
β”‚ β”œβ”€β”€ utils/ ← Shared utilities (api, bibtex, dbml, markdoc)
β”‚ └── cli.ts ← CLI entry point
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ components/ ← Unified Astro components
β”‚ β”œβ”€β”€ content/docs/ ← generated MDX (do not edit)
β”‚ └── styles/custom.css
β”œβ”€β”€ public/
β”‚ └── diagrams/ ← D2-rendered flow SVGs
β”œβ”€β”€ astro.config.ts
└── content/
└── starspec.config.json ← optional project config (mode, contentDir, outDir)

Domain Model (SOLID)

StarSpec uses a robust domain model in packages/compiler/src/core/ to encapsulate logic and ensure type safety across passes.

Core Classes

  • Entity: Centralized management of API prefixes (action:, event:, etc.), metadata (labels, colors), and normalization.
  • Tag: Abstract base class for all Markdoc tags. Constructor takes (node: Node). Provides type guards (Tag.is), scope detection (isVisible, isPublic, isAgent), and child traversal (findTypedChildren, findTypedChild). Not generic.
  • ContentBuilder: Fluent builder for assembling MDX page output. All document processors and aggregate processors use it instead of manual sections.push() + join(). Handles frontmatter, TLDR extraction, prose buffering with .ss-prose wrapping, column layout (ss-columns), headings, sections, and blocks. Constructor accepts (title, { columns? }).
  • SidebarBuilder: Fluent builder for assembling sidebar.json. Replaces the imperative section assembly in sidebarPhase(). Accepts a CompileMode ('full' | 'journal') and gates sections accordingly. In journal mode, only Journal-section items are included and promoted to top-level (no wrapping group). Methods: addProcessorEntries(), addProductAggregates(), addImplementationAggregates(), addReferenceItems(), addJournalAggregates(), addChangelog(), addInstructionsGroup(), build().
  • Document: Registry of document types (feature, domain, role, etc.) that handles directory and href mapping logic (toDir, toHref).
  • Markup: Centralized escaping for MDX (handling curly braces and self-closing tags) and TOON bundles.
  • ObjectPath: Safe recursive property manipulation β€” ObjectPath.push(bundle, dotPath, data) is how bundlePath inserts data into agent bundles.
  • AstWalker: Generic AST walker shared by agent and public passes. Takes an array of NodeVisitor instances. Multi-visitor descent: all matching visitors run before deciding whether to descend into children.
  • DiagnosticsCollector: Per-walk diagnostic accumulator used by AstWalker β€” collects issues during a single file’s traversal.
  • IndexRegistry: General-purpose index container for cross-document lookups.
  • TagIndex: Tag data index β€” maps bare tag id to parents, children, and properties. Built from *.tag.mdoc documents.
  • CompileContext: Context container for per-pass state.
  • FoldableTag: Base for collapsible tag UI rendering.
  • DocRef: Document cross-reference utilities.
  • scope (core/scope.ts): propagateScope(), isScoped(node, target), isPublicScoped(), isAgentScoped(). Re-exported via utils/markdoc.ts.

Rich Tag Models (core/tags/)

Complex tags are implemented as dedicated classes in core/tags/ (27 files). Each extends the base Tag class and encapsulates:

  • Internal Validation: Ensuring sub-tags are correctly placed and formatted.
  • Tree Traversal: Encapsulating complex lookahead logic (e.g., Step.getBranch() to identify sibling branches).
  • Extraction & Rendering Logic: Moving pass-specific logic out of the entry points and into the models.

Models include: Api, Blueprint, Carousel, Changeset, Citation, DataModel, Diagram, Domain, Example, Explanation, Feature, Flow, Glossary, Manifest, Milestone, Policy, Requirement, Role, Rule, Setting, Step, Story, Surface, TagDoc, Tldr, Tour. Tag name constants live in names.ts (TagNames enum).

Rendering Utilities (core/rendering/)

Rendering logic extracted from passes/public/ into reusable modules:

  • api.ts β€” renderApiSection(), renderApiEntry()
  • blocks.ts β€” Block-level rendering (requirements, criteria, etc.)
  • components.ts β€” Component and blueprint rendering
  • document.ts β€” frontmatter(), document-level MDX utilities
  • flow.ts β€” D2 flow rendering + renderD2ToFile()
  • interactions.ts β€” Clickable / interaction rendering
  • policies.ts β€” renderPoliciesSection()
  • surface.ts β€” Surface / wireframe rendering
  • utils.ts β€” TYPE_LABELS, typeToDir, idToHref, qnameToHref

Configuration

StarSpec reads an optional content/starspec.config.json from the project root. All fields are optional:

\{
"contentDir": "content",
"outDir": "public/bundles",
"mode": "full"
\}
FieldTypeDefaultDescription
contentDirstringcontentPath to .mdoc source files
outDirstringpublic/bundlesPath for TOON bundle output
mode"full" | "journal""full"Compilation mode (see below)

CLI flags override config file values: --content=, --out=, --mode=.

Compilation modes

ModeParsesAggregatesPassesSidebar
fullAll content/**/*.mdocAll 24 aggregate processorsagent β†’ instructions β†’ publicProduct, Journal, Reference, Specification, Implementation, Changelog, StarSpec
journalOnly articles/**/*.article.mdocJournal-relevant only (quotes, atoms, bibliography, article-tags, diagrams, examples, markers)public onlyJournal items promoted to top-level (no section wrapper)

In journal mode the agent pass, instructions pass, MCP bundle generation, and status page are all skipped. The Astro site hides spec-only UI chrome (MCP status indicator, viewport selector, wireframe theme selector) via the virtual:starspec/config Vite module.

Astro integration

astro.config.ts reads content/starspec.config.json and exposes it to components via a Vite virtual module:

import \{ isJournalMode \} from 'virtual:starspec/config';

Components use this to conditionally render spec-only UI elements. The sidebar in astro.config.ts also gates MCP-Server and System Status entries on !isJournalMode.

Compiler internals

AST walking β€” AstWalker + NodeVisitor

Both the agent pass and the public pass traverse the Markdoc AST using AstWalker. A visitor implements:

interface NodeVisitor {
matches(node: Markdoc.Node): boolean;
visit(node: Markdoc.Node, ctx: WalkContext): VisitResult; // 'continue' | 'skip-children'
}

The walker calls every matching visitor on a node before descending. If any visitor returns 'skip-children', the walker skips that node’s children for all visitors. This is the mechanism behind isContainer: a non-container processor returns 'skip-children' to prevent double-processing of child tags.

Scope propagation

Before each render pass, the compiler walks the full AST and propagates scope top-down via propagateScope() in core/scope.ts:

TypeScript propagateScope implementation
architecturescopetypescript scope-propagation-function
function propagateScope(node: Markdoc.Node, inherited: string | null): void {
const resolved = (node.attributes?.scope as string | undefined) ?? inherited;
if (node.attributes) node.attributes.scope = resolved;
for (const child of node.children ?? []) {
propagateScope(child, resolved);
}
}

Scope is checked with isScoped(node, 'agent') or isScoped(node, 'public') (shorthand: isAgentScoped() / isPublicScoped()). A node missing scope inherits from its parent. A child can narrow or widen scope by declaring its own scope attribute.

Validation

All extracted data is validated against extract schemas derived from Tag class schemas (see validation.ts). Tag classes in core/tags/ define the authoritative attribute schema; extract schemas extend these with computed fields (text, body, content). Validation errors are collected by a ValidationTracker and logged with file and path context. The pass continues even when validation fails, using the raw data, so a single bad tag doesn’t abort the whole compile. Per-walk diagnostics are also collected by DiagnosticsCollector instances passed through WalkContext.

Agent pass (passes/agent/)

Reads content/, parses the Markdoc AST, and extracts structured data into TOON bundles. Uses AstWalker with a NodeVisitor that iterates all registered processors. Bundle placement is determined by the processor’s declarative properties:

  1. merge() β€” for root processors that own the bundle shape
  2. bundlePath β€” for leaf processors that push items into a specific array path
  3. promotesIdentity β€” promotes tag id/title to the bundle root

After processing all files, the agent pass builds aggregate bundles (API, glossary, components, policies, etc.) using processor metadata (aggregatePath, aggregateIdKey).

Public pass (passes/public/) β€” 6-phase pipeline

The public pass is organized as a sequential pipeline of six phases:

PhaseModulePurpose
1. Parsephases/parse.tsParse all .mdoc files, resolve frontmatter
2. Extractphases/extract.tsRun processor extract() on each document
3. Build Indexphases/build-index.tsBuild cross-document indexes (feature↔domain, feature↔design, role↔feature, action↔surface, etc.)
4. Renderphases/render.tsRun processor render(), write MDX pages
5. Aggregatephases/aggregate.tsRun AggregateProcessor instances, write aggregate pages
6. Sidebarphases/sidebar.tsGenerate Starlight sidebar configuration from processor sidebar declarations

Each phase returns a typed result consumed by the next. Shared types live in phases/types.ts.

The sidebar is assembled by SidebarBuilder (core/SidebarBuilder.ts) in sidebarPhase(). Each processor declares a sidebar object that controls where and how its entries appear:

FieldPurpose
sectionTop-level group: Product, Specification, Implementation, Journal
groupSub-group label (e.g. Stories, Features, Flows)
weightSort order within the section (lower = higher)
collapsedWhether the group starts collapsed in the sidebar
groupByDomainWhen true, entries are further nested by domain into sub-groups

When groupByDomain is true, each entry’s domain is derived from (in order of precedence):

  1. The frontmatter domain field (used by articles)
  2. The first domain/ reference in the frontmatter context array (used by features, flows, surfaces)

Entries sharing a domain are grouped under the domain’s display title (from the corresponding {% domain %} document). Entries without a domain appear ungrouped at the end.

Example sidebar structure for Features with groupByDomain: true:

Specification
└─ Features (collapsed)
β”œβ”€ Conversation (collapsed)
β”‚ β”œβ”€ Structured Formats
β”‚ └─ ...
β”œβ”€ Memory & Reflection (collapsed)
β”‚ β”œβ”€ Export Data
β”‚ └─ ...
└─ Speech-To-Text (collapsed)
└─ Transcription

To enable domain grouping for a document type, add domain/ references to the frontmatter context array:

---
type: feature
id: export-data
title: Export Data
status: draft
context: [domain/memory-reflection, role/user]
---

Aggregate pages β€” AggregateProcessor

Cross-document aggregate pages (glossary, API reference, examples gallery, roadmap, etc.) are produced by dedicated AggregateProcessor classes in processors/aggregates/ (22 files). Each implements:

interface AggregateProcessor<TEntry> {
readonly id: string;
readonly outputPath?: string;
collect(acc, context): TEntry[];
prepare?(entries: TEntry[]): TEntry[];
render?(entries: TEntry[], context): string;
write?(entries: TEntry[], context, docsDir: string): void;
sidebarEntry?(entries: TEntry[]): AggregateSidebarEntry | null;
}

The aggregatePhase calls collect() β†’ prepare() β†’ render() (or write()) β†’ sidebarEntry() for each registered processor. Processors that need multiple output files (e.g. showroom) implement write() instead of render().

Shared indexes (passes/shared/)

passes/shared/indexes.ts builds indexes used by both the agent and public passes: bundleCache, tagDataIndex, policyIndex, apiNameIndex, eventNames, operationThrowsIndex. These are built once from all content files and passed into the CompilerContext.

Instructions pass (passes/instructions.ts)

Reads instructions/ and writes dist/bundles/starspec/**/*.toon. These are engine meta-documents: design loops, tag schemas, and examples that agents can query. Resolves {% include %} directives to inline fragment content.