openspec package
Reviewed against commit 8a2cb42ad on 2026-10-23.
Reference: generated package page orchestrata_ultimate.openspec (7 modules, 723 lines of code); key modules repo, detect, parse, requirements_parser. For how specs, requirement ids and the RTM fit into orchestrata as a whole, see the overview.
The story
Orchestrata's own specification corpus lives in specs/NNN-*/spec.md, but the wider ecosystem the pipeline has to work inside increasingly uses the OpenSpec convention: an openspec/ directory holding per-capability specs written as ### Requirement: headings with #### Scenario: blocks, plus change proposals expressed as ADDED/MODIFIED/REMOVED/RENAMED requirement deltas. orchestrata_ultimate.openspec (SPEC-104) exists so the pipeline can meet such projects on their own terms — recognize the layout, read and validate the specs, and write new capabilities and change proposals back — without forcing a conversion into orchestrata's native format.
Everything starts with detection. detect_spec_folder(root) in detect.py inspects a project and returns a SpecFolder describing what it found: the spec root (or None), a format tag ("openspec", "orchestrata", or "unknown"), the capabilities present, and any change proposals. Detection prefers an openspec/ directory outright; failing that it inspects a specs/ folder and sniffs the first spec.md it finds — is_openspec(text) decides OpenSpec by its case-sensitive ### Requirement: + #### Scenario: markers (or a delta header), while a separate regex recognizes orchestrata's REQ-NNN- / ### FR-N / Spec ID markers. Detection is deliberately total — an absent or unrecognized folder yields "unknown" rather than an exception — so callers can probe any directory safely. When the format is OpenSpec, the caller opens an OpenSpecRepo(sf.root) and from that point on works exclusively through the repository surface.
Data flows through the package as plain dataclasses defined in models.py (Scenario, Requirement, OpenSpecDoc, Delta, SpecFolder). The parse.py and render.py modules are the markdown codec for those models: parse_spec turns spec text into an OpenSpecDoc (never raising on malformed input — the line-based parser returns whatever it could recover), and render_spec is its structural inverse, so a parse/render round-trip preserves the document's structure. parse_deltas reads change-delta sections into Delta blocks, and validate(doc) reports OpenSpec violations — a spec with no requirements, a requirement with no scenario, a requirement with no SHALL statement — as a plain list of strings rather than failing. repo.py composes the codec with file I/O: reading a capability parses the file on disk, writing one renders the model back, and create_change scaffolds a full change proposal (proposal.md via render_proposal, tasks.md via render_tasks, and an optional delta spec.md via render_delta).
The repository also carries the package's safety guarantees. A _safe(path) guard resolves every target and confines it to the spec root, raising OpenSpecError on any path that escapes (SEC-104-001), and create_change refuses to overwrite an existing change directory (SEC-104-002 / ERR-104-005), again raising OpenSpecError. The package's only outward collaboration is with core.traceability: detect, parse, render, and repo functions carry @traces decorators tying them to SPEC-104 requirement IDs, which is how the project's RTM tooling verifies coverage. Otherwise it is self-contained and dependency-light by design — pure dataclasses, a line-based parser, and filesystem operations under one root — which keeps it easy for volks and scripts elsewhere in the pipeline to call without infrastructure.
Key responsibilities
- Detect and classify a project's spec folder — OpenSpec vs orchestrata vs unknown — without ever raising (
detect.py). - Model OpenSpec structure as pure dataclasses: scenarios, requirements, capability docs, change deltas, and detection results (
models.py). - Parse OpenSpec markdown into those models, tolerating malformed input, and validate a parsed spec against OpenSpec rules (
parse.py). - Render the models back to OpenSpec markdown — capability specs, change deltas, proposals, and task checklists — as the structural inverse of parsing (
render.py). - Convert checklists to Prolog facts and mint the next requirement id for orchestrata's own spec format (
requirements_parser.py). - Read/create/edit capability specs and scaffold change proposals through a sandboxed repository, with every write confined to the spec root and change-id collisions refused (
repo.py).
Class diagram
The package is almost entirely functions and dataclasses. The data model is a small tree of composed dataclasses (generated from the real source, render-verified):
The only class with behaviour is the sandboxed repository, plus the package's one exception type:
Module dependency diagram
Internal module imports within orchestrata_ultimate.openspec (traceability is the external core.traceability collaborator; generated from the AST, render-verified):
Module inventory
| Module | Purpose |
|---|---|
openspec/__init__.py |
Package facade — re-exports detect_spec_folder, is_openspec, the data models, the parse/render functions, validate, OpenSpecRepo, and OpenSpecError. |
openspec/detect.py |
Locate a project's spec folder and recognize its format (SPEC-104 FR-001/002); never raises — unknown folders yield format "unknown". |
openspec/models.py |
OpenSpec data models — plain dataclasses (Scenario, Requirement, OpenSpecDoc, Delta, SpecFolder) plus the SpecFormat / DeltaOp literal aliases; pure data, no I/O. |
openspec/parse.py |
Parse OpenSpec markdown into models and validate a parsed doc (SPEC-104 FR-003/006/009); line-based, never raises on malformed input. |
openspec/render.py |
Render models back to OpenSpec markdown — specs, deltas, proposals, task checklists (SPEC-104 FR-004/007); structural inverse of parse. |
openspec/repo.py |
Repository operations (SPEC-104 FR-004/005/007/008) — read/create/edit specs and scaffold change proposals, writes confined to the spec root. |
openspec/requirements_parser.py |
Not part of the OpenSpec codec (added after the first version of this page): turns an orchestrata spec's requirements checklist into Prolog facts (parse_requirements(spec_id, checklist_path), run), finds story headings (find_story_headings) and generates the next requirement id (next_requirement_id). Used by the verification package and by scripts. |
requirements_parser.py
The one module in the package that speaks orchestrata's own spec format rather than OpenSpec. parse_requirements reads a spec's requirements checklist and returns Prolog fact strings for the governance engine, find_story_headings collects the story numbers present in spec.md, and next_requirement_id(spec_id, story, checklist_path, spec_md_path) returns the next free REQ-NNN-NNN id for a story. It shares nothing with the detect/parse/render/repo pipeline above; see verification for the consumer.
detect.py
Detection is the package's front door. is_openspec(text) checks for the distinctive, case-sensitive OpenSpec markers — a ### Requirement: plus a #### Scenario:, or a ## ADDED|MODIFIED|REMOVED|RENAMED Requirements delta header — and detect_spec_folder(root) walks a project to produce a SpecFolder describing the discovered layout. OpenSpec projects keep an openspec/ directory (whose specs/ and changes/ subfolders yield the capability and change lists, with changes/archive filtered out); orchestrata's own specs live under specs/NNN-*/spec.md and are recognized by sniffing the first spec.md for REQ-NNN- / FR-N markers. Because detection is total — an absent or unrecognized folder simply comes back as format "unknown" with root=None — callers can probe arbitrary directories without wrapping the call in error handling, and only proceed to the repository when the format is actually OpenSpec.
models.py
This module is the package's shared vocabulary: five dataclasses plus two type aliases, SpecFormat = Literal["openspec", "orchestrata", "unknown"] and DeltaOp = Literal["ADDED", "MODIFIED", "REMOVED", "RENAMED"]. A Scenario is one #### Scenario: block (its name plus raw WHEN/THEN lines), a Requirement is a ### Requirement: heading with its SHALL text and scenarios, an OpenSpecDoc is a full capability spec (capability name, purpose, requirements), a Delta is one ADDED/MODIFIED/REMOVED/RENAMED operation over a set of requirements, and a SpecFolder is the detection result. The models carry no I/O or behaviour of their own; every other module exchanges them, with parse and render acting as their markdown codec.
parse.py
The parser is line-based and forgiving. parse_spec(text, capability) recognizes ### Requirement: and #### Scenario: headings — a new ## heading ends the current requirement block — and assembles an OpenSpecDoc, also lifting the ## Purpose section into the doc's purpose. parse_deltas(text) recognizes ## ADDED|MODIFIED|REMOVED|RENAMED Requirements sections and yields a list of Deltas, reusing the same requirement parser per section. validate(doc) reports problems as a list of strings — no requirements, a requirement with no scenario, a requirement whose text has no SHALL — rather than failing. Nothing here raises on malformed input; the parser returns whatever structure it could recover, which keeps upstream tooling robust against hand-edited specs. Functions carry @traces decorators tying them to SPEC-104 requirements.
render.py
Rendering is the structural inverse of parsing: render_spec(doc) emits a capability spec.md (with a # … Specification, ## Purpose, and ## Requirements skeleton) such that parse_spec(render_spec(doc)) round-trips the document's structure. render_delta(deltas) emits the ## ADDED|MODIFIED|REMOVED|RENAMED Requirements blocks. The module also produces the change-proposal artifacts that repo.create_change writes to disk — render_proposal(why, what, impact="") for proposal.md (## Why / ## What Changes / optional ## Impact) and render_tasks(tasks, group="Implementation") for a numbered tasks.md checklist — so every byte the package writes flows through one rendering path.
repo.py
OpenSpecRepo is the read/write surface over a detected spec root, composing the parse/render codec with filesystem operations. On construction it resolves root and picks specs_dir (a specs/ subfolder when present, else the root itself) and changes_dir. capabilities() lists the capability directories that contain a spec.md, read_capability(name) parses one into an OpenSpecDoc (an empty doc if absent), write_capability(doc) renders one back (creating directories as needed), add_requirement(capability, requirement) reads-appends-rewrites in place, and create_change(...) scaffolds a complete change proposal — proposal.md, tasks.md, and an optional per-capability delta spec.md. The repository is also where the package's security requirements live: the _safe(path) guard confines every resolved path inside the spec root (SEC-104-001), and create_change refuses to overwrite an existing change directory (SEC-104-002 / ERR-104-005), raising OpenSpecError in both cases.
Class map (generated)
Generated from the live source AST by scripts/gen-module-class-diagrams.py — do not hand-edit. Abstract classes have a green border; members listed are those defined on the class itself (inherited members excluded). Solid empty-triangle arrows = in-package inheritance; dashed arrows into dashed boxes = external/stdlib/cross-package bases; filled diamond = composition (constructed in __init__); open diamond = aggregation (annotated/injected reference); diamond edge labels name the owning attribute.