Spec Types
Specs are Markdown files with a YAML frontmatter block. The type field controls which
sections are required, how the parser validates the spec, and what code (if any) the
compiler generates.
Quick Reference
| Type | Use when | Generates |
|---|---|---|
bundle |
A module with several related functions/types | Implementation + tests |
function |
A single complex function needing full specification | Implementation + tests |
type |
A data structure or schema | Type definition + tests |
module |
Aggregating sub-specs into a single public API | Re-exports |
workflow |
A multi-step execution pipeline | Runner script |
reference |
Documenting a third-party library | Verification tests only |
When in doubt, start with bundle. It handles the vast majority of modules and is the
most flexible type.
bundle
The default type. Use it for any module that groups related functions and/or types. Each
item in a bundle has a one-line behavior field rather than a full spec section.
Key sections: # Overview, # Types (optional), # Functions, # Behavior,
# Examples
Bundle function fields:
| Field | Required | Description |
|---|---|---|
inputs |
Yes | Named parameters with types |
outputs |
Yes | Return type(s) |
behavior |
Yes | One-line description |
contract |
No | Pre/post conditions |
Example — score/manifest.spec.md, SpecSoloist's own build manifest:
---
name: manifest
type: bundle
tags:
- core
- builds
---
# Overview
Build manifest for tracking spec compilation state. Enables incremental builds by recording what was built, when, and from what inputs — so only changed specs (and their dependents) need recompilation.
# Types
## SpecBuildInfo
Build record for a single spec. Tracks the spec content hash at build time, the build timestamp, its dependencies, and the output files produced.
**Fields:** `spec_hash` (string), `built_at` (ISO timestamp string), `dependencies` (list of spec name strings), `output_files` (list of relative file path strings).
Must support round-tripping to/from dict for JSON serialization (`to_dict`, `from_dict`).
## BuildManifest
Collection of build records, persisted as `.specsoloist-manifest.json` in the build directory.
**Fields:** `version` (string, default `"1.0"`), `specs` (dict mapping spec name to `SpecBuildInfo`).
**Methods:**
- `get_spec_info(name)` -> `SpecBuildInfo` or `None`
- `update_spec(name, spec_hash, dependencies, output_files)` — record a successful build with current UTC timestamp
- `remove_spec(name)` — remove a spec's build record
- `save(build_dir)` — write manifest to `{build_dir}/.specsoloist-manifest.json` as JSON
- `load(build_dir)` (classmethod) — load manifest from file; return empty manifest if file doesn't exist or is corrupted/invalid JSON
## IncrementalBuilder
Determines which specs need rebuilding. Constructed with a `BuildManifest` and a `src_dir` string.
**Methods:**
- `needs_rebuild(spec_name, current_hash, current_deps, rebuilt_specs)` -> bool
- `get_rebuild_plan(build_order, spec_hashes, spec_deps)` -> list of spec names
# Functions
## compute_file_hash(path) -> string
Compute SHA-256 hash of a file's contents. Returns empty string if file doesn't exist.
## compute_content_hash(content) -> string
Compute SHA-256 hash of a string.
# Behavior
## Incremental rebuild logic
A spec needs rebuilding if ANY of:
1. It has never been built (not in manifest)
2. Its content hash has changed since last build
3. Its dependency list has changed since last build
4. Any of its current dependencies were rebuilt in this build cycle
`get_rebuild_plan` walks the build order (which is topological — dependencies first), checking each spec against the above rules. When a spec is marked for rebuild, it's added to the "rebuilt" set so downstream dependents will also trigger.
# Examples
| Scenario | needs_rebuild? | Why |
|----------|---------------|-----|
| Spec "foo" not in manifest | Yes | Never built |
| Spec "foo" hash changed from "abc" to "def" | Yes | Content changed |
| Spec "foo" deps were `[]`, now `["bar"]` | Yes | Dependencies changed |
| Spec "foo" unchanged, but dep "bar" was rebuilt this cycle | Yes | Dependency rebuilt |
| Spec "foo" unchanged, deps unchanged, no deps rebuilt | No | Nothing changed |
function
For a single function complex enough to warrant its own Behavior, Constraints, Contract,
and Test Scenarios sections. If you find yourself writing a bundle with one item, use
function instead.
Key sections: # Overview, # Interface (yaml:schema), # Behavior,
# Constraints, # Contract, # Test Scenarios
Example — examples/math/factorial.spec.md:
---
name: factorial
type: function
status: stable
tags:
- math
- pure
---
# 1. Overview
Computes the factorial of a non-negative integer. The factorial of n (written n!) is the product of all positive integers less than or equal to n.
# 2. Interface Specification
```yaml:schema
inputs:
n:
type: integer
minimum: 0
maximum: 20
description: "The non-negative integer to compute factorial of"
outputs:
result:
type: integer
minimum: 1
3. Functional Requirements (Behavior)
- FR-01: Return 1 when n is 0 (base case: 0! = 1).
- FR-02: Return the product of all positive integers from 1 up to n for n > 0.
- FR-03: Raise an error for negative inputs.
- FR-04: Raise an error for inputs exceeding 20 to prevent overflow in restricted environments.
4. Non-Functional Requirements (Constraints)
- NFR-Purity: Must be a pure function with no side effects.
- NFR-Performance: Implementation should favor iterative approach over deep recursion.
5. Design Contract
- Pre-condition: n is an integer where 0 <= n <= 20.
- Post-condition: result is equal to the mathematical factorial n!.
6. Test Scenarios
| Scenario | Input | Expected Output |
|---|---|---|
| Base case | n: 0 |
1 |
| Small integer | n: 1 |
1 |
| Typical case | n: 5 |
120 |
| Maximum supported | n: 20 |
2432902008176640000 |
| Negative input | n: -1 |
Error |
| Out of range | n: 21 |
Error |
---
## `type`
For a data structure or schema. No functions — just the shape of the data, its
constraints, and examples of valid/invalid values. The compiler generates a type
definition (dataclass, Pydantic model, TypeScript interface, etc.).
**Key sections:** `# Overview`, `# Schema` (yaml:schema), `# Constraints`, `# Examples`
**Example** — `examples/math/user.spec.md`:
```markdown
---
name: user
type: type
status: stable
tags:
- auth
- core
---
# 1. Overview
Represents a user account in the system with a unique identifier, email address, display name, and timestamps.
# 2. Interface Specification
```yaml:schema
properties:
id:
type: string
format: uuid
description: "Unique identifier, assigned at creation"
email:
type: string
format: email
description: "Primary email address, must be unique"
name:
type: string
minLength: 1
maxLength: 100
description: "Display name"
role:
type: string
enum: [user, admin, guest]
description: "Access level"
created_at:
type: string
format: date-time
description: "When the account was created (ISO 8601)"
updated_at:
type: string
format: date-time
description: "When the account was last modified (ISO 8601)"
required:
- id
- email
- role
- created_at
Constraints
- [NFR-01]: Email must be unique across all users
- [NFR-02]: ID must be immutable after creation
- [NFR-03]: updated_at must be >= created_at
Examples
| Valid | Notes |
|---|---|
{id: "550e8400-...", email: "jo@example.com", role: "user", created_at: "2024-01-01T00:00:00Z"} |
Minimal valid user |
{id: "...", email: "admin@example.com", name: "Admin", role: "admin", created_at: "...", updated_at: "..."} |
Full user |
| Invalid | Why |
|---|---|
{email: "a@b.com", role: "user"} |
Missing id, created_at |
{id: "...", email: "not-an-email", role: "user", created_at: "..."} |
Invalid email format |
{id: "...", email: "a@b.com", role: "superuser", created_at: "..."} |
Invalid role enum |
---
## `module`
Aggregates exports from sub-specs into a single public API surface. A module spec lists
what it re-exports and documents high-level behaviour — it doesn't re-specify what
sub-specs already cover. Use it when you want a single import point for consumers of a
package.
**Key sections:** `# Overview`, `# Exports` (list of re-exported names), method/class
docs that add module-level context.
**Example** — `score/resolver.spec.md`, SpecSoloist's own dependency resolver (excerpt):
```markdown
---
name: resolver
type: module
tags:
- core
- dependencies
---
# Overview
Dependency resolution for multi-spec builds. Given a set of specs that declare
dependencies on each other, this module can:
- Build a dependency graph
- Compute a valid build order (dependencies before dependents)
- Group specs into parallelizable levels
- Determine which specs are affected when one changes
- Detect and report circular dependencies and missing dependencies
# Exports
- `CircularDependencyError`: Exception raised when specs form a dependency cycle.
- `MissingDependencyError`: Exception raised when a spec depends on one that doesn't exist.
- `DependencyGraph`: A dependency graph with methods to query relationships.
- `DependencyResolver`: The main resolver that builds graphs and computes build orders.
# Error Types
## CircularDependencyError
An exception with a `cycle` attribute (list of spec names forming the cycle).
## MissingDependencyError
An exception with `spec` and `missing` attributes. `spec` is the name of the spec that
declared the dependency; `missing` is the name of the dependency that doesn't exist.
# DependencyResolver
## resolve_build_order(spec_names=None) -> list of strings
Compute a linear build order. Dependencies appear before dependents.
Alphabetical when no ordering constraint exists.
Raises `CircularDependencyError` if a cycle exists.
## get_parallel_build_order(spec_names=None) -> list of lists
Group specs into parallelizable levels. Each level can be compiled concurrently —
all dependencies of specs in level N are in levels 0..N-1.
# Examples
| Method | Input | Expected |
|--------|-------|----------|
| resolve_build_order | A depends on B | `["b", "a"]` |
| get_parallel_build_order | A,B independent; C depends on both | `[["a","b"], ["c"]]` |
workflow
For multi-step execution where data flows from one step to the next. Workflows reference other specs by name and wire outputs to inputs. The compiler generates an executable runner (Python function, JS module, etc.) that calls each step in order.
Workflows are for your target project — for example, an order processing pipeline
that validates, then charges, then confirms. SpecSoloist's own orchestration (the
conductor agent that runs sp conduct) is separate from this.
Key sections: # Overview, # Interface (yaml:schema), # Steps (yaml:steps),
# Error Handling
yaml:steps fields:
| Field | Description |
|---|---|
name |
Step identifier, used to reference its outputs downstream |
spec |
The spec to invoke |
inputs |
Map of input names to values: inputs.x or prev_step.outputs.y |
checkpoint |
If true, the workflow can resume from this step after a failure |
Example — examples/math/math_workflow.spec.md:
---
name: math_workflow
type: workflow
status: draft
dependencies:
- factorial
- is_prime
---
# 1. Overview
A demonstration workflow that computes the factorial of a number, then checks if the result is prime.
# 2. Interface Specification
```yaml:schema
inputs:
n:
type: integer
minimum: 0
maximum: 20
description: "Starting number for factorial"
outputs:
factorial_result:
type: integer
is_prime_result:
type: boolean
3. Steps
```yaml:steps - name: compute_factorial spec: factorial inputs: n: inputs.n
- name: check_prime
spec: is_prime
checkpoint: true
inputs:
n: compute_factorial.outputs.result
# 4. Functional Requirements (Behavior) - **FR-01**: Execute the `compute_factorial` step using the provided input `n`. - **FR-02**: Pass the output of the factorial calculation to the `check_prime` step. - **FR-03**: Return both the factorial result and the primality check result. # 5. Error Handling - If `compute_factorial` fails: Terminate the workflow and return the error. - If `check_prime` fails: Log the error and return the partial result containing the factorial.
reference
For documenting a third-party library. No implementation is generated. The spec body is injected into the prompts of dependent soloists as API documentation, grounding them in the real library API so they don't hallucinate method signatures or import paths.
When to use
Any time your project uses a library that is new enough (or niche enough) that LLMs may not have accurate knowledge of its API. FastHTML, Vercel AI SDK, and similar newer frameworks are good candidates.
Required sections
| Section | Required | Purpose |
|---|---|---|
# Overview |
Yes | Library name, package, version range, correct import path |
# API |
Yes | Functions, classes, and attributes your project actually uses |
# Verification |
Recommended | 3–10 lines compiled into tests/test_{name}.py |
Conventions
- Version range in
# Overview: Be explicit about which version the spec documents and note any breaking changes between versions. A missing version range triggers a quality warning fromsp validate. - Imports and gotchas first: Put the canonical import statement near the top. For libraries with common footguns (wrong module path, deprecated entry points), call them out explicitly.
- Verification makes it living docs: The
# Verificationsection compiles to a test file. CI fails if the library API drifts, turning the spec into verified documentation rather than a comment that goes stale. - Only document what you use: Reference specs aren't comprehensive library docs — they document the subset of the library that your project's soloists will call.
Dependency wiring
Specs that use the library declare the reference spec in their dependencies: frontmatter.
The conductor injects the reference spec body into those soloists' prompts automatically:
---
name: routes
type: bundle
dependencies:
- fasthtml_interface # reference spec — injected as context, not imported
- state
---
Example — examples/fasthtml_app/specs/fasthtml_interface.spec.md:
---
name: fasthtml_interface
type: reference
status: stable
---
# Overview
The subset of [FastHTML](https://fastht.ml) used in this project (`python-fasthtml >= 0.12`).
This spec exists so soloists have accurate API documentation — FastHTML is new enough that LLMs
may hallucinate its API.
Specs that depend on FastHTML components should list `fasthtml_interface` as a dependency.
No implementation is generated for this spec — it is API documentation only.
**Critical import:** Always import from `fasthtml.common`, never from `fasthtml` directly:
```python
from fasthtml.common import fast_app, serve, picolink, Main, Div, P, H1, Form, Input, Button, Title, Ul, Li
Testing: Use from starlette.testclient import TestClient. Never call serve() in test files —
guard with if __name__ == "__main__": serve().
API
fast_app()
Creates the FastHTML ASGI app and route decorator. Pass hdrs to inject <head> elements
such as CSS framework links.
Returns a tuple (app, rt) where app is the ASGI application and rt is a route decorator factory.
picolink
A pre-built <link> header element that loads Pico CSS from a CDN.
Pico CSS applies clean, semantic styles to plain HTML elements with no class names required.
Wrap page content in Main(..., cls="container") for centred, responsive layout.
from fasthtml.common import fast_app, picolink, Main
app, rt = fast_app(hdrs=(picolink,))
@rt("/")
def get():
return Title("My App"), Main(H1("Hello"), cls="container")
serve()
Starts the development server on localhost:5001. Never call in test files.
HTML components
All HTML components share this signature: Tag(*children, **attrs).
| Component | HTML element | Notes |
|---|---|---|
Div |
<div> |
General container |
P |
<p> |
Paragraph |
H1 |
<h1> |
Heading |
Form |
<form> |
Use hx_post, hx_swap, hx_target for HTMX |
Input |
<input> |
name= delivers form field to route handler |
Button |
<button> |
Submit button |
Title |
<title> |
Page title in head |
Main |
<main> |
Use cls="container" with Pico CSS for centred layout |
Ul |
<ul> |
Unordered list |
Li |
<li> |
List item |
Route registration
app, rt = fast_app()
@rt("/")
def get():
return Div(H1("Hello"), P("World"))
@rt("/todos")
def post(item: str): # form field 'item' injected as keyword arg
return Li(item)
Route handlers return HTML components directly. FastHTML serialises them to HTML strings.
To return multiple top-level elements, return a tuple: return H1("Title"), P("Body").
HTMX attributes on Form
Form(
Input(name="item", placeholder="Add todo"),
Button("Add"),
hx_post="/todos",
hx_swap="beforeend",
hx_target="#todo-list",
)
Starlette test client
from starlette.testclient import TestClient
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200
assert "<ul" in response.text
Verification
from fasthtml.common import fast_app, serve, picolink, Main, Div, P, H1, Form, Input, Button, Title, Ul, Li
app, rt = fast_app(hdrs=(picolink,))
assert callable(rt)
div = Div("hello", id="x")
assert "hello" in str(div)
form = Form(Input(name="item"), Button("Add"), hx_post="/add", hx_swap="beforeend")
assert "hx-post" in str(form)
main = Main(H1("Hello"), cls="container")
assert "container" in str(main)
```