Guide: Creating a Custom Model
1 Introduction
A model defines the vocabulary and behavior of specification documents. It declares object types, floats, relations, views, and verification rules.
The default model provides base
types such as SECTION, FIGURE, TABLE, and PLANTUML. A custom model can add or replace
types. SpecCompiler loads the custom model as an overlay on default.
Each extension module returns one Lua descriptor table. The
descriptor contains a kind, a schema, and an optional hooks table. The host registers the schema
and indexes the hooks. The value of schema.id identifies the type.
Create a type as follows:
- Decide the kind (
object,float,view,relation,specification, oranalyze). - Define the type in
schema. Theschema.idfield is the authoritative identifier. - Add a hook only when the type requires custom behavior. The hook name determines its context and return type.
Most object and float types contain only data. These types omit hooks and inherit the applicable
behavior.
2 Quick-start template
Each type module has the following structure:
-- File: models/<your-model>/types/<category>/<type-name>.lua
-- The module returns one descriptor table.
return {
-- kind: object | float | view | relation | specification | analyze.
-- It must match the category directory the file lives in.
kind = "object",
-- schema contains data. schema.id is authoritative.
-- These fields become columns in the SpecIR database.
schema = {
id = "...", -- required, uppercase convention, globally unique
-- extends = "...", -- inherit attributes + hooks from a base type
-- attributes = { ... },
},
-- hooks contains optional custom behavior.
-- The host classifies each hook by NAME, infers the role from the hooks
-- present, and rejects a function placed on any other top-level key. A
-- pure-data type (most objects/floats) omits this key entirely and
-- inherits the host default.
-- hooks = {
-- render = function(ctx) ... end, -- one hook whose name fits the intent
-- },
}Set template: <your-model>
in project.yaml. The build then
loads the model as an overlay on default.
3 Worked example: a one-file, project-local model
A project-local model can contain one descriptor. The loader first
searches SPECCOMPILER_HOME/models/<name>/.
It then searches the project working directory.
The following model defines architecture decisions as traceable objects:
my-project/
project.yaml -- template: adr
decisions.md
models/
adr/
types/
objects/
decision.lua -- the whole model
return {
kind = "object",
schema = {
id = "DECISION",
long_name = "Architecture Decision",
extends = "SECTION",
pid_prefix = "ADR",
pid_format = "%s-%03d",
attributes = {
{ name = "status", type = "ENUM",
values = { "Proposed", "Accepted", "Superseded" } },
{ name = "consequences", type = "XHTML" },
},
},
}No hooks key: rendering, PID
assignment, labels, and cross-reference resolution are all inherited.
Set template: adr in project.yaml and author decisions like
any other typed object:
# Architecture Decisions @SPEC-ADR-001
## DECISION: Use SQLite for the IR @ADR-001
> status: Accepted
> consequences: The build produces one file. SQL queries provide traceability data.
We store the intermediate representation in a SQLite database rather than
in-memory tables, so analyze queries can use SQL.
## DECISION: Run on stock pandoc
> status: Proposed
Use the system Pandoc with Lua and C extensions.The build registers DECISION
and assigns the second decision the PID ADR-002. It stores the attributes for
queries. It also resolves [ADR-001](@) references. The type
requires no custom behavior.
4 Overlay
Models layer as overlays on top of default. When project.yaml sets template: mymodel, the loader runs two
passes:
models/default/types/— loaded first.models/mymodel/types/— loaded second. A descriptor with the same kind andschema.idreplaces the default descriptor.
Descriptors with new identifiers supplement the default descriptors. The model inherits definitions that it does not replace.
5 Model directory layout
models/<your-model>/
types/
objects/ -- Object types (e.g., hlr.lua, vc.lua)
specifications/ -- Specification types (e.g., srs.lua)
floats/ -- Float types (e.g., sequence_diagram.lua)
views/ -- View types (e.g., symbol.lua)
relations/ -- Relation types (e.g., traces_to.lua)
analyze_queries/ -- Analyze descriptors (e.g., vc_missing_hlr.lua)
postprocessors/ -- Per-format post-processing (docx.lua, html5.lua)
filters/ -- Pandoc Lua filters per output format
styles/ -- Style presets (preset.lua, docx.lua, html.lua)
model.yaml -- Optional manifest: name, description, requires
Only types/ is required. All
other directories are optional.
A model that depends on another model declares it in model.yaml:
name: mymodel
description: My domain model
requires: [base_model] # Load this model first.The engine loads each model in requires before it loads the requesting
model. It loads each dependency once. A missing dependency stops the
build. A model can contain external tools in tools/. The five types/ directory names are fixed. Each
directory maps to one descriptor kind. A mismatch between the directory
and kind stops the build.
The loader supports single files such as types/floats/figure.lua. It also
supports directories with an init.lua file.
6 Type categories
| Category | kind |
Example file | Hooks typically declared |
|---|---|---|---|
| Object | "object" |
types/objects/hlr.lua |
none (omit hooks), or
render |
| Float | "float" |
types/floats/figure.lua |
transform, or
prepare_task + handle_result |
| Relation | "relation" |
types/relations/traces_to.lua |
none, render_link, or
resolve |
| View | "view" |
types/views/abbrev.lua |
render,
render_block, dataset,
build_block |
| Specification | "specification" |
types/specifications/srs.lua |
none (omit hooks) |
| Analyze query | "analyze" |
analyze_queries/vc_missing_hlr.lua |
message |
7 The descriptor and hook contract
The hooks table contains all
custom type behavior. Each hook accepts one frozen context table. The
polymorphic ctx.subject field
contains the hook input. The ctx.capability field identifies the
hook. Use ctx:require("field")
to require a non-nil field.
The hook name determines the context tier and return type. During model loading, the host validates each hook against the descriptor kind. An invalid hook stops the build.
Render-tier hooks run during EMIT. They receive the
render context and a resolved payload in ctx.subject. They return Pandoc AST.
Link hooks return display text.
Data-tier hooks run during a data or lifecycle phase. They receive a data context without a Pandoc element or output format. Each hook returns its documented value.
The host enforces return types. The host checks the
value when it dispatches a hook. An invalid value stops the build and
identifies the kind, type, and hook. A nil value selects inherited behavior.
The analyze-query message hook
must return a string.
Phase hooks participate in pipeline phases. Declare
them as on_<phase> functions
in hooks. Each phase hook runs
once per phase with (data, contexts, diagnostics). Use
schema.phase_prerequisites to
declare ordering constraints. The host names the handler <lowercase id>_handler.
| Hook | Kind(s) | Tier | Returns | When it fires |
|---|---|---|---|---|
render |
object, specification, float, view | render | Pandoc Block(s) / Inlines | EMIT, when the element of this type is being rendered. |
render_block |
view | render | Pandoc Block | EMIT, for a block view written as a fenced
```prefix code block. |
render_link |
relation | render | display string (or nil) |
EMIT, to produce the visible text of a link of this relation type. |
message |
analyze | render | diagnostic string | ANALYZE, once per row the SQL view returns. |
dataset |
view | data | { source | data | links } |
When a chart/data view needs its dataset
(ctx.subject.params). |
build_block |
view | data | Pandoc Block | When a TABLE_VIEW subtype
builds its generated table. |
transform |
float | data | resolved-AST string | TRANSFORM, to resolve a float internally
(ctx.subject.raw_content, .float). |
prepare_task |
float | data | task table (or nil to
skip) |
TRANSFORM, to spawn an external tool
(ctx.subject.float, .build_dir). |
handle_result |
float | data | (writes resolved AST) | After the external tool finishes
(ctx.subject.task, .success,
.stderr). |
resolve |
relation | data | { target, ambiguous } |
ANALYZE, to resolve a link of this
relation type (ctx.subject.target_text). |
A float is internal or external: declaring render together with prepare_task/handle_result is a contradiction and
the host rejects it.
Context fields
| Field | Meaning |
|---|---|
ctx.subject |
The hook-specific payload. Examples include an object, float, view parameters, relation target, or analyze-query row. |
ctx.data |
The DataManager (the SpecIR
database) for custom queries. Present on both tiers. |
ctx.spec_id |
Identifier of the specification being processed. Present on both tiers. |
ctx.log |
Logger (log.warn,
log.debug, …). Present on both tiers. |
ctx.pandoc |
The Pandoc module. Render tier only — a data hook has no Pandoc element yet. |
ctx.format |
Target output format (docx,
html5, gfm, json). Render
tier only. |
ctx.diagnostics,
ctx.model, ctx.config |
Diagnostics sink, model name, and project config. These fields are always in the render tier. A data context contains them when available. |
ctx.host |
The registry, for cross-hook dispatch
(e.g. ctx.host:get_hook("view", id, "build_block")). Render
tier. |
Read the resolved element and its attributes from
ctx.subject. Query the database only for data that the subject does not contain.
8 Five canonical templates
8.1 Object type
Create an object type with required attributes and a custom render.
The render hook reads its element and attributes from ctx.subject:
local render_utils = require("pipeline.shared.render_utils")
return {
kind = "object",
schema = {
id = "HLR",
long_name = "High-Level Requirement",
description = "A top-level system requirement",
extends = "TRACEABLE", -- inherit the standard traceable card
pid_prefix = "HLR",
pid_format = "%s-%03d",
attributes = {
{ name = "priority", type = "ENUM",
values = { "High", "Medium", "Low" },
min_occurs = 1, max_occurs = 1 },
{ name = "rationale", type = "XHTML" },
},
},
hooks = {
-- EMIT: ctx.subject carries { object, attributes, element, ... } already
-- resolved by the pipeline, so no DB query is needed for our own item.
render = function(ctx)
local obj = ctx.subject.object
local attrs = ctx.subject.attributes or {}
local priority = (attrs.priority or {}).value or "?"
local blocks = {}
local level = math.max((ctx.subject.header_level or 2) - 1, 1)
local hdr = ctx.pandoc.Header(level,
{ ctx.pandoc.Str(obj.pid .. ": " .. (obj.title_text or "")) })
render_utils.add_header_blocks(blocks, hdr)
table.insert(blocks, ctx.pandoc.Para({
ctx.pandoc.Strong({ ctx.pandoc.Str("Priority: ") }),
ctx.pandoc.Str(priority),
}))
return blocks
end,
},
}Usage:
## HLR: User Authentication @HLR-001
> priority: High
> rationale: Required by security policy section 4.2
The system shall authenticate users via username and password.Most object types do not require a render hook. Without this hook, the host
emits the standard PID, attributes, and body. Add render only for a different
layout.
8.2 Relation type
The common case is a simple subtype that narrows a base relation
(PID_REF for @ or LABEL_REF for #). No hooks needed — extends inherits the selector,
resolver, and default link display:
return {
kind = "relation",
schema = {
id = "TRACES_TO",
extends = "PID_REF",
long_name = "Traces To",
description = "Traceability link from one object to another",
source_type_ref = "LLR",
target_type_ref = "HLR",
},
}The link [HLR-001](@) inside an
LLR resolves as TRACES_TO.
When the default display text (PID for objects, "<caption> <number>" for
floats) isn’t right, add a render_link hook. It reads the
pre-resolved target from ctx.subject.target and returns the
display string (or nil to fall
back to the base type):
hooks = {
-- Render section refs as "<hierarchical-number> <title>", e.g. "3.4 Introduction".
render_link = function(ctx)
local target = ctx.subject.target
local title = target.title or ""
local number = target.pid and target.pid:match("sec([%d%.]+)$")
if number and title ~= "" then return number .. " " .. title end
if title ~= "" then return title end
return target.pid
end,
}ctx.subject.target contains the
resolved target. Object targets contain the kind, PID, title, and type.
Float targets contain the kind, caption, and number. Return a string to
replace the display text. Return nil to use inherited behavior. LABEL_REF and PID_REF provide default link
rendering.
For custom relation resolution, declare a resolve data hook. It reads the target
text and source identifier from ctx.subject. It returns { target, ambiguous }. Subtypes of
PID_REF and LABEL_REF inherit their resolvers.
8.3 Walkthrough custom view
Backtick syntax invokes views. An inline view uses
render and returns inlines. A
block view uses render_block and returns a block. The
inline_prefix and aliases fields select the view.
return {
kind = "view",
schema = {
id = "SYMBOL",
long_name = "Symbol",
description = "Engineering symbol with inline formatting",
aliases = { "sym" },
inline_prefix = "symbol",
},
hooks = {
-- EMIT: ctx.subject.element is the Pandoc Code element.
render = function(ctx)
local text = ctx.subject.element.text or ""
local content = text:match("^symbol:%s*(.+)$") or text:match("^sym:%s*(.+)$")
if not content then return nil end
return { ctx.pandoc.Emph({ ctx.pandoc.Str(content) }) }
end,
},
}Usage: The force is defined as
`symbol: F = ma`.
Database-backed views usually render a block. Extend
TABLE_VIEW and declare a build_block data hook. The inherited
render_block hook dispatches
build_block. A TABLE_VIEW subtype without build_block causes a load error.
return {
kind = "view",
schema = {
id = "TRACEABILITY_MATRIX",
extends = "TABLE_VIEW", -- inherits the shared render_block
long_name = "Traceability Matrix",
description = "HLR to VC traceability with test results",
inline_prefix = "traceability_matrix",
},
hooks = {
-- DATA: build the generated table. dctx.data is the SpecIR database.
-- dctx.subject.params holds any view parameters.
build_block = function(dctx)
local rows = dctx.data:query_all([[
SELECT hlr.pid AS hlr_pid, vc.pid AS vc_pid
FROM spec_relations r
JOIN spec_objects vc ON r.source_object_id = vc.id
JOIN spec_objects hlr ON r.target_object_id = hlr.id
WHERE vc.type_ref = 'VC' AND hlr.type_ref = 'HLR'
AND vc.specification_ref = :spec_id
ORDER BY hlr.pid, vc.pid
]], { spec_id = dctx.spec_id })
if not rows or #rows == 0 then
return pandoc.Para({ pandoc.Str("No HLR-VC traceability found.") })
end
local header = {
{ pandoc.Plain({ pandoc.Strong({ pandoc.Str("HLR") }) }) },
{ pandoc.Plain({ pandoc.Strong({ pandoc.Str("VC") }) }) },
}
local body = {}
for _, row in ipairs(rows) do
table.insert(body, {
{ pandoc.Plain({ pandoc.Str(row.hlr_pid or "") }) },
{ pandoc.Plain({ pandoc.Str(row.vc_pid or "") }) },
})
end
local tbl = pandoc.SimpleTable({}, { pandoc.AlignLeft, pandoc.AlignLeft },
{ 0, 0 }, header, body)
return pandoc.utils.from_simple_table(tbl)
end,
},
}A view that supplies chart data declares a dataset hook. The hook returns { source = ... } or the data shape
required by the chart type.
8.4 Walkthrough custom float type
Most floats require only a schema:
return {
kind = "float",
schema = {
id = "SEQUENCE",
long_name = "Sequence Diagram",
caption_format = "Figure",
counter_group = "FIGURE", -- shares numbering with FIGURE, PLANTUML, CHART
aliases = { "seq", "sequence" },
},
}Usage:
```seq:auth-flow{caption="User authentication flow"}
sequence diagram content here
```An internally rendered float can declare a transform data hook. The hook reads
ctx.subject.raw_content and ctx.subject.float. It returns a
resolved-AST string. See External renderers for process-based
rendering.
8.5 Specification type
Specification types define the top-level document kinds (SRS, SDD,
STD, …). They are pure-data — no hooks at all:
return {
kind = "specification",
schema = {
id = "SRS",
long_name = "Software Requirements Specification",
description = "MIL-STD-498 SRS document",
is_default = false,
implicit_aliases = { "Software Requirements Specification", "SRS" },
attributes = {
{ name = "version", type = "STRING" },
{ name = "date", type = "STRING" },
},
},
}Usage: # srs: My Project Requirements @SRS-MYPROJ-001
at the top of a .md file.
9 Schema field reference
The following tables define common schema fields. Some model hooks also read additional schema fields for rendering.
9.1 Object schema
| Field | Type | Default | Description |
|---|---|---|---|
id |
string | required | Unique identifier (uppercase convention). |
long_name |
string | same as id |
Human-readable name. |
description |
string | "" |
Description text. |
extends |
string | nil | Base type for attribute + hook inheritance
(e.g. TRACEABLE). |
is_default |
boolean | false | Headers without an explicit type match this type. |
is_composite |
boolean | false | Hierarchical object (contains children with their own PIDs). |
is_required |
boolean | false | The specification must contain at least one object of this type. |
pid_prefix |
string | nil | Prefix for auto-generated PIDs. |
pid_format |
string | nil | Printf format
(e.g. "%s-%03d"). |
aliases |
list | nil | Alternative identifiers for syntax matching. |
implicit_aliases |
list | nil | Titles that auto-resolve to this type
(e.g. "References" -> REFERENCES). |
attributes |
list | nil | Attribute definitions (see below). |
9.2 Attribute definitions
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | required | Attribute identifier. |
type |
string | "STRING" |
One of: STRING,
INTEGER (INT accepted), REAL,
BOOLEAN, DATE, ENUM,
XHTML. |
min_occurs |
integer | 0 | 0 = optional, 1 = required. |
max_occurs |
integer | 1 | Maximum number of values. |
min_value /
max_value |
number | nil | Bounds for numeric types. |
values |
list | nil | Required when
type = "ENUM". |
datatype_ref |
string | auto | Override the auto-generated datatype id.
For ENUM the default is
<TYPE_ID>_<attr_name>. |
9.3 Float schema
| Field | Type | Default | Description |
|---|---|---|---|
id |
string | required | Unique identifier. |
long_name |
string | same as id |
Human-readable name. |
description |
string | "" |
Description text. |
caption_format |
string | same as id |
Prefix used in output captions
(e.g. "Figure"). |
counter_group |
string | same as id |
Counter sharing group. Floats with the
same counter_group share one numbering sequence. |
aliases |
list | nil | Alternative fence prefixes. |
needs_external_render |
boolean | false | See External renderers. |
9.4 Relation schema
| Field | Type | Default | Description |
|---|---|---|---|
id |
string | required | Unique identifier. |
extends |
string | nil | Base relation type. Typically
PID_REF (@) or LABEL_REF
(#). Inherits selector, resolve, and default
render_link. |
link_selector |
string | inherited | Override the inherited selector
(@ or #). Rarely needed. |
source_type_ref |
string | nil | Constrain the source object type (nil = any). |
target_type_ref |
string | nil | Constrain the target type (nil = any). Comma-separated list accepted. |
source_attribute |
string | nil | Constrain to links inside this attribute. |
is_structural |
boolean | false | Derive from the containment hierarchy instead of an explicit link. |
9.5 Inference scoring
When multiple relation types match a link, the resolver scores each
candidate. A matching constraint adds one point. A mismatch removes the
candidate. A nil constraint adds
no points. The highest score wins. A tie produces an ambiguous_relation diagnostic.
9.6 View schema
| Field | Type | Default | Description |
|---|---|---|---|
id |
string | required | Unique identifier. |
long_name |
string | same as id |
Human-readable name. |
inline_prefix |
string | nil | Prefix for inline-code dispatch
("symbol" enables `symbol: ...`). |
aliases |
list | nil | Alternative prefixes for the same view type. |
materializer_type |
string | nil | Strategy ('toc',
'lof', …) for built-in materialised views. |
counter_group |
string | nil | Counter group for numbered views. |
view_subtype_ref |
string | nil | The view-element subtype this view
aggregates (e.g. ABBREV). |
needs_external_render |
boolean | false | See External renderers. |
10 Extension checklist
- Pick a model name (lowercase, matches the directory name).
- Create
models/<name>/types/…with the categories you need. - Return one descriptor with
kind,schema, and optionalhooksfields. - Add only the hooks that implement required custom behavior.
- Set
template: <name>inproject.yaml. - Run
specc buildand inspect the output. - Add an analyze descriptor if the domain has rules to enforce.
11 Analyze queries
Analyze queries are SQL-based validation rules that run during
ANALYZE. Each query uses a descriptor with kind = "analyze". Its schema defines a
SQL view. Its optional message
hook converts a result row into a diagnostic.
Place analyze descriptors under models/<name>/analyze_queries/.
The loader scans all Lua files in this directory. Use the policy key as
the file name when practical.
return {
kind = "analyze",
schema = {
id = "vc_missing_hlr",
view = "view_traceability_vc_missing_hlr",
policy_key = "traceability_vc_to_hlr",
sql = [[
CREATE VIEW IF NOT EXISTS view_traceability_vc_missing_hlr AS
SELECT vc.identifier AS object_id, vc.pid AS object_pid,
vc.title_text AS object_title, vc.from_file, vc.start_line
FROM spec_objects vc
WHERE vc.type_ref = 'VC'
AND NOT EXISTS (
SELECT 1 FROM spec_relations r
JOIN spec_objects target ON target.identifier = r.target_ref
WHERE r.source_ref = vc.identifier AND target.type_ref = 'HLR'
);
]],
},
hooks = {
-- ANALYZE: one call per row. ctx.subject.row contains the query result.
message = function(ctx)
local row = ctx.subject.row
local label = row.object_pid or row.object_title or row.object_id
return string.format(
"Verification case '%s' has no traceability link to an HLR", label)
end,
},
}The policy_key controls
diagnostic severity. An overlay descriptor with the same key replaces
the earlier descriptor. A schema with disabled = true removes the key. Set a
policy to ignore in project.yaml to suppress its
diagnostics:
validation:
traceability_vc_to_hlr: ignore12 External renderers
Floats that require an external tool set needs_external_render = true. They
declare prepare_task and handle_result data hooks. The pipeline
collects tasks, runs processes, and dispatches each result.
The prepare_task hook returns a
task descriptor. The handle_result
hook stores the resolved result. A float cannot declare render with either external-render
hook.
local float_base = require("pipeline.shared.float_base")
local task_runner = require("infra.process.task_runner")
return {
kind = "float",
schema = {
id = "PLANTUML",
long_name = "PlantUML Diagram",
caption_format = "Figure",
counter_group = "FIGURE",
aliases = { "puml", "plantuml" },
needs_external_render = true,
},
hooks = {
-- DATA: build the spawn task. dctx.subject contains { float, build_dir }.
-- dctx.log is the logger. Return the task descriptor, or nil to skip.
prepare_task = function(dctx)
local float = dctx.subject.float
local build_dir = dctx.subject.build_dir
local content = float.raw_content or ""
local hash = pandoc.sha1(content)
local diagrams = build_dir .. "/diagrams"
local puml_file = diagrams .. "/" .. hash .. ".puml"
local png_file = diagrams .. "/" .. hash .. ".png"
task_runner.ensure_dir(diagrams)
task_runner.write_file(puml_file, content)
return {
cmd = "plantuml",
args = { "-tpng", puml_file },
opts = { timeout = 30000 },
output_path = png_file, -- cache key: skipped when the file exists
context = { float = float, relative_path = "diagrams/" .. hash .. ".png" },
}
end,
-- DATA: write the resolved AST. dctx.subject carries
-- { task, success, stdout, stderr }. dctx.data is the SpecIR database.
handle_result = function(dctx)
local task = dctx.subject.task
local ctx = task.context
if not dctx.subject.success then
dctx.log.warn("PlantUML failed: %s", dctx.subject.stderr)
return
end
local json = string.format('{"png_paths":["%s"]}', ctx.relative_path)
float_base.update_resolved_ast(dctx.data, ctx.float.id, json)
end,
},
}| Field | Purpose |
|---|---|
cmd, args,
opts |
Process and options.
opts.timeout is in milliseconds. opts.cwd sets
the working directory. |
output_path |
File-based cache key. If the file already
exists, the process is not spawned — handle_result runs
immediately with the cached path. Put the content hash in the filename
so input changes trigger a fresh render. |
context |
Arbitrary table passed through to
handle_result (read as task.context). |
13 Project integration
project:
code: MYPROJ
name: My Project
template: mymodel # Loads models/default/ then models/mymodel/
doc_files:
- srs.md14 Troubleshooting
A type does not load. Check the file location and returned descriptor. The kind must match its category directory. Type-load errors stop the build. View module load errors produce warnings.
The host rejects a hook. Check that the hook is
valid for the descriptor kind. Put all custom behavior in hooks.
An overlay does not replace a type. Use the same
case-sensitive schema.id and kind
as the base descriptor.
A render hook produces no output. A TABLE_VIEW subtype must declare build_block. An object hook that returns
nil retains the original
element. Return the generated blocks to replace it.
An external renderer runs for unchanged input.
Include the content hash in output_path to use the file cache.
15 Pointers
- User manual — day-to-day authoring syntax.
- Engineering docs — the type system SDD defines the contract. Type discovery and model design define internal behavior.
- Concepts dictionary — vocabulary reference.
Guide: DOCX Customization
1 Introduction
SpecCompiler generates DOCX output through a multi-stage pipeline:
- SpecIR – Structured data in SQLite (objects, relations, floats, attributes).
- Pandoc AST – The emitter assembles a Pandoc document from the SpecIR.
- Pandoc DOCX Writer – Pandoc converts the AST to DOCX using a
reference.docxfor styles. - Lua Filter – A format-specific filter converts SpecCompiler markers to OOXML (captions, bookmarks, math).
- Postprocessor – Manipulates the generated DOCX ZIP archive (positioned floats, caption orphan prevention, template-specific OOXML).
Customization is available at three levels: style presets (fonts, spacing, page layout), filters (AST-to-OOXML conversion), and postprocessors (raw OOXML manipulation).
2 How Pandoc reference.docx Works
Pandoc uses a reference document as a style template for DOCX output. The reference document defines paragraph styles (Normal, Heading 1, Caption, etc.), page dimensions, margins, and default formatting. Pandoc does not copy content from the reference document – only styles and settings.
SpecCompiler manages the reference document in two ways:
- Auto-generated from presets (default): SpecCompiler builds a
reference.docxfrom Lua style presets, storing it at{output_dir}/reference.docx. - User-provided: Set
docx.reference_docinproject.yamlto use your own Word template.
3 Style Presets
Style presets are Lua files that declaratively define DOCX styles. They are located at:
models/{template}/styles/{preset}/preset.lua
3.1 Preset Table Structure
A preset file returns a Lua table with the following top-level keys:
return {
name = "My Preset",
description = "Custom document styles",
-- Page configuration
page = {
size = "A4", -- "Letter" or "A4"
orientation = "portrait", -- "portrait" or "landscape"
margins = {
top = "2.5cm",
bottom = "2.5cm",
left = "3cm",
right = "2cm",
},
},
-- Paragraph styles (array of style definitions)
paragraph_styles = { ... },
-- Table styles (array of table style definitions)
table_styles = { ... },
-- Caption formats per float type
captions = { ... },
-- Document settings
settings = {
default_tab_stop = 720, -- In twips (720 = 0.5 inch)
language = "en-US",
},
-- Optional: inherit from another preset
extends = {
template = "default", -- Base template
preset = "base", -- Base preset name
},
}3.2 Paragraph Style Fields
| Field | Type | Default | Description |
|---|---|---|---|
id |
string | required | Internal style ID (e.g., “Heading1”) |
name |
string | required | Display name in Word (e.g., “Heading 1”) |
based_on |
string | nil | Parent style ID for inheritance |
next |
string | nil | Style to apply to the next paragraph |
font.name |
string | nil | Font family name |
font.size |
number | nil | Font size in points |
font.color |
string | nil | Hex color without # (e.g.,
“2F5496”) |
font.bold |
boolean | nil | Bold text |
font.italic |
boolean | nil | Italic text |
spacing.line |
number | nil | Line spacing multiplier (1.0 = single, 1.15, 2.0, etc.) |
spacing.before |
number | nil | Space before paragraph in points |
spacing.after |
number | nil | Space after paragraph in points |
alignment |
string | nil | Text alignment: “left”, “center”, “right”, “both” (justified) |
indent.left |
string | nil | Left indent (e.g., “0.5in”, “1cm”) |
indent.right |
string | nil | Right indent |
keep_next |
boolean | nil | Keep with next paragraph (prevent orphaning) |
outline_level |
integer | nil | Outline level for TOC (0 = Heading 1, 1 = Heading 2, etc.) |
3.3 Paragraph Style Example
paragraph_styles = {
{
id = "Normal",
name = "Normal",
font = { name = "Calibri", size = 11 },
spacing = { line = 1.15, after = 8 },
alignment = "left",
},
{
id = "Heading1",
name = "Heading 1",
based_on = "Normal",
next = "Normal",
font = { name = "Calibri Light", size = 16, color = "2F5496" },
spacing = { before = 12, after = 0, line = 1.15 },
keep_next = true,
outline_level = 0,
},
{
id = "Caption",
name = "Caption",
based_on = "Normal",
font = { name = "Calibri", size = 9, italic = true },
spacing = { before = 0, after = 10, line = 1.15 },
},
}3.4 Table Styles
table_styles = {
{
id = "TableGrid",
name = "Table Grid",
borders = {
top = { style = "single", width = 0.5, color = "000000" },
bottom = { style = "single", width = 0.5, color = "000000" },
left = { style = "single", width = 0.5, color = "000000" },
right = { style = "single", width = 0.5, color = "000000" },
inside_h = { style = "single", width = 0.5, color = "000000" },
inside_v = { style = "single", width = 0.5, color = "000000" },
},
cell_margins = {
top = "0.05in",
bottom = "0.05in",
left = "0.08in",
right = "0.08in",
},
autofit = true,
},
}3.5 Caption Configuration
captions = {
figure = {
template = "{prefix} {number}: {title}",
prefix = "Figure",
separator = ": ",
style = "Caption",
},
table = {
template = "{prefix} {number}: {title}",
prefix = "Table",
separator = ": ",
style = "Caption",
},
listing = {
template = "{prefix} {number}: {title}",
prefix = "Listing",
separator = ": ",
style = "Caption",
},
}3.6 Preset Inheritance
Presets can extend other presets using the extends field. The child preset deeply
merges with the base, with child values taking precedence:
-- models/mymodel/styles/academic/preset.lua
return {
name = "Academic",
description = "Academic paper styles",
extends = {
template = "default", -- Base template
preset = "default", -- Base preset name
},
-- Override only what changes
page = {
size = "A4",
margins = { top = "2.5cm", bottom = "2.5cm", left = "3cm", right = "2cm" },
},
paragraph_styles = {
{
id = "Normal",
name = "Normal",
font = { name = "Times New Roman", size = 12 },
spacing = { line = 1.5, after = 0 },
alignment = "both", -- Justified
},
},
}The loader detects circular dependencies and reports them as errors.
3.7 Format-Specific Style Overrides
Beyond the main preset.lua, you
can provide format-specific style files:
models/{template}/styles/{preset}/docx.lua– DOCX-specific overridesmodels/{template}/styles/{preset}/html.lua– HTML-specific overrides
These files return tables with keys like float_styles and object_styles that are merged with the
base preset at emit time.
4 Postprocessors
Postprocessors manipulate the generated DOCX file after Pandoc produces it. They operate on raw OOXML inside the ZIP archive.
4.1 Loading
The base postprocessor (models/default/postprocessors/docx.lua)
is always loaded. It handles:
- Positioned floats – Converts inline images to anchored format with margin-relative positioning.
- Caption orphan prevention – Adds
keepNextto Caption-styled paragraphs.
Template-specific postprocessors are loaded from models/{template}/postprocessors/docx.lua.
4.2 Hook Interface
A template postprocessor exports functions that are called in sequence:
| Hook | Input | Purpose |
|---|---|---|
process_document(content, config, log) |
document.xml content |
Modify main document body |
process_styles(content, log, config) |
styles.xml content |
Modify or inject style definitions |
process_numbering(content, log) |
numbering.xml content |
Modify list numbering definitions |
process_content_types(content, log) |
[Content_Types].xml
content |
Add content type declarations |
process_settings(content, log) |
settings.xml content |
Modify document settings |
process_rels(content, log) |
document.xml.rels
content |
Add/modify relationship entries |
create_additional_parts(temp_dir, log, config) |
Temp directory path | Create new parts (headers, footers) |
All hooks are optional. Each receives the current XML content as a string and returns the modified content.
4.3 Writing a Custom Postprocessor
Create models/mymodel/postprocessors/docx.lua:
local M = {}
function M.process_document(content, config, log)
local modified = content
-- Example: Add custom watermark text to every paragraph
-- (Real implementations would use proper OOXML patterns)
log.debug("[MYMODEL-POST] Processing document.xml")
return modified
end
function M.process_styles(content, log, config)
local modified = content
-- Example: Inject a custom paragraph style
log.debug("[MYMODEL-POST] Processing styles.xml")
return modified
end
return M4.4
The config Parameter
The config table passed to hooks
contains:
template– The template namedocx– DOCX configuration fromproject.yamlspec_metadata– Specification-level attributes (increate_additional_parts)
5 Filters
Pandoc Lua filters run during the DOCX write phase and convert
SpecCompiler format markers to OOXML. The default filter (models/default/filters/docx.lua)
handles:
| Input Marker | Output |
|---|---|
RawBlock("speccompiler", "page-break") |
OOXML page break |
RawBlock("speccompiler", "vertical-space:NNNN") |
OOXML spacing (in twips) |
RawBlock("speccompiler", "bookmark-start:ID:NAME") |
OOXML bookmark start |
pandoc.Math (native inline or
display) |
OOXML math element (native
<m:oMath> via Pandoc’s texmath) |
Div.speccompiler-caption |
OOXML caption with SEQ field |
Div.speccompiler-numbered-equation |
OOXML numbered equation with tab layout |
Div.speccompiler-positioned-float |
Position markers for postprocessor |
Link with .ext
target |
Rewritten to .docx
target |
5.1 When to Use Filters vs Postprocessors
- Filters operate on the Pandoc AST before DOCX generation. Use them when you need to convert SpecCompiler markers to OOXML elements that Pandoc will then place in the document.
- Postprocessors operate on the raw OOXML after DOCX generation. Use them when you need to manipulate the final XML directly (style injection, image positioning, headers/footers).
6 project.yaml Configuration
6.1 DOCX-Specific Settings
# Output format entry
outputs:
- format: docx
path: build/docx/{spec_id}.docx
# DOCX-specific configuration
docx:
preset: default # Style preset name
# reference_doc: assets/reference.docx # Custom reference (overrides preset)6.2 Configuration Precedence
- If
docx.reference_docis set, that file is used directly as the Pandoc reference document. - If
docx.presetis set (or defaults to the model’s styles), SpecCompiler generates{output_dir}/reference.docxfrom the preset. - If neither is set, Pandoc uses its built-in default styles.
7 Reference Document Cache
When using presets, SpecCompiler caches the generated reference.docx to avoid regenerating it
on every build.
The cache works as follows:
- Compute SHA-1 hash of the preset file content.
- Compare against the stored hash in the
build_metatable (key-value store inspecir.db). - If the hashes match and
reference.docxexists on disk, skip generation. - If the preset changed or
reference.docxis missing, regenerate and update the cache.
To force regeneration of the reference document, delete it:
rm -f build/reference.docx
./bin/speccompiler-coreSpecCompiler Core User Manual
1 Introduction
1.1 What is SpecCompiler?
SpecCompiler is the reference compiler for CommonSpec, a structured Markdown language for typed, traceable specifications. It lowers CommonSpec into SpecIR (a SQLite-backed intermediate representation) and generates multiple output formats (DOCX, HTML5, GitHub-Flavored Markdown, and JSON Pandoc AST). It provides:
- Structured authoring: Define requirements, designs, and verification cases using a consistent syntax.
- Traceability: Link objects together with Project Identifier (PID) and
#labelreferences. - Validation: Check data integrity with Structured Query Language (SQL) analyze queries against the Specification Intermediate Representation (SpecIR).
- Multi-format output: Generate Word documents and web content from a single source.
SpecCompiler processes documents through a five-phase pipeline: INITIALIZE, RESOLVE, TRANSFORM, ANALYZE, and EMIT, as illustrated in Figure 1.
1.2 Scope
This manual covers:
- Installation and verification of the SpecCompiler-Core Docker image (see 2 Introduction).
- Configuration of project files (
project.yaml) as described in 4 Project Configuration. - Authoring specification documents using CommonSpec syntax (4 Project Configuration).
- Invocation of the tool and interpretation of its outputs (7 Invocation).
- Verification diagnostics and policy-key reference.
- Incremental build behavior and cache management.
- Type system configuration and custom model creation; for a detailed walkthrough, see creating-a-model: 5 Model directory layout in the companion model guide.
- Troubleshooting common problems.
1.3 Pipeline Summary
The processing pipeline consists of five phases:
- INITIALIZE – Extract specifications, spec objects, attributes, floats, relations, and view definitions from the parsed Pandoc Abstract Syntax Tree (AST) into the SpecIR stored in SQLite Database (SQLite), casting attribute values into typed columns.
- RESOLVE – Generate missing PIDs, resolve relation targets, and infer relation types from model constraints (see Equation 1).
- TRANSFORM – Resolve and number floats, render typed content, and rewrite resolved links.
- ANALYZE – Execute SQL policy views against SpecIR, apply configured severities, and report violations.
- EMIT – Assemble Pandoc documents from SpecIR, expand generated views, and generate configured outputs via parallel Pandoc subprocesses.
{
"xAxis": { "type": "category", "data": ["INITIALIZE", "RESOLVE", "TRANSFORM", "ANALYZE", "EMIT"] },
"yAxis": { "type": "value", "name": "Hook count" },
"series": [{ "type": "bar", "data": [6, 2, 5, 1, 3], "itemStyle": { "color": "#5470c6" } }]
}The ECharts config above (hook counts per phase) renders as a bar
chart under any model that provides the chart: float, such as abnt; the counts reflect the default
model. Custom models may add hooks in any phase.
2 Installation
2.1 Prerequisites
The following are required to run SpecCompiler-Core:
| Prerequisite | Minimum Version | Notes |
|---|---|---|
| Container engine | Docker 20.10+ or Podman 4+ | Docker Desktop, Docker Engine, or Podman (daemon/VM must be running) |
| Disk space | 2 GB | For the container image and build artifacts |
| Host OS | Linux, macOS, or Windows | The container runs Ubuntu 24.04 |
All dependency versions are pinned in scripts/versions.env.
2.2 Installing
One command on Linux, macOS, or WSL2 (requires docker or podman):
curl -fsSL https://raw.githubusercontent.com/SpecIR/SpecCompiler/main/scripts/install.sh | bashOne command on Windows, in PowerShell (after Docker Desktop or Podman is installed):
irm https://raw.githubusercontent.com/SpecIR/SpecCompiler/main/scripts/install.ps1 | iexinstall.sh installs the specc Command-Line Interface (CLI)
wrapper at ~/.local/bin/specc and
writes the engine and image reference to ~/.config/speccompiler/env; the
Windows installer places specc
under %LOCALAPPDATA%\SpecCompiler\bin and
adds it to the user PATH. If a local image exists it is used
automatically; otherwise, the GHCR image is pulled on first use.
2.3 Building the Image
There is a single image, built on Ubuntu 24.04 (Dockerfile): the stock apt pandoc, the
four compiled Lua C extensions, the SpecCompiler Lua source, and the
optional renderers (deno for model-owned chart: floats, PlantUML for puml: diagrams, python/reqif for ReqIF
interop). No compiler toolchain, no pandoc build. To build it locally
instead of pulling from GHCR, from the repository root:
docker build -t speccompiler-core:latest .
bash scripts/install.sh2.4 Verifying Installation
After building, verify the image is available:
docker images speccompiler-coreTo verify the tool runs correctly, navigate to a directory containing
a project.yaml file and run:
specc build2.5 The specc Wrapper
The specc command is a single
wrapper shared by every install mode. Its mode is read from ~/.config/speccompiler/env:
| Mode | Behavior |
|---|---|
SPECC_MODE=image |
Runs the container image via docker or
podman (written by install.sh) |
SPECC_MODE=native |
Invokes the host pandoc with the compiled
extensions (written by install-native.sh) |
The command surface is specc build [project.yaml] (default
file: project.yaml).
In image mode, the wrapper runs docker run --rm (or podman run --rm) with:
--user "$(id -u):$(id -g)"– Preserves host UID/GID (docker; rootless podman uses--userns=keep-idinstead).-v "$(pwd):/workspace"– Mounts current directory.-e "SPECCOMPILER_LOG_LEVEL=${SPECCOMPILER_LOG_LEVEL:-INFO}"– Passes log level.
Inside the container, the same wrapper runs in native mode and
invokes Pandoc with the SpecCompiler Lua filter. The -o /dev/null Pandoc flag is
intentional – actual output files are generated by the EMIT phase.
3 Project Configuration
All project configuration is specified in a project.yaml file located in the project
root directory.
3.1 Complete Configuration Reference
# ============================================================================
# Project Identification (REQUIRED)
# ============================================================================
project:
code: MYPROJ # Project code identifier (string, required)
name: My Project SRS # Human-readable project name (string, required)
# ============================================================================
# Type Model (REQUIRED)
# ============================================================================
template: default # Type model name (string, default: "default")
# Must match a directory under models/
# ============================================================================
# Logging Configuration (OPTIONAL)
# ============================================================================
logging:
level: info # DEBUG | INFO | WARN | ERROR (default: "INFO")
format: auto # auto | json | text (default: "auto")
color: true # ANSI color codes (default: true)
# ============================================================================
# Validation Policy (OPTIONAL)
# ============================================================================
validation:
missing_required: ignore
cardinality_over: ignore
invalid_cast: ignore
invalid_enum: ignore
invalid_date: ignore
bounds_violation: ignore
dangling_relation: ignore
unresolved_relation: ignore
# ============================================================================
# Input Files (REQUIRED)
# ============================================================================
output_dir: build/ # Base output directory (default: "build")
doc_files: # Markdown files to process, in order
- srs.md
- sdd.md
# ============================================================================
# Output Format Configurations (OPTIONAL)
# ============================================================================
outputs:
- format: docx
path: docx/{spec_id}.docx
- format: html5
path: www/{spec_id}.html
# ============================================================================
# DOCX Configuration (OPTIONAL)
# ============================================================================
docx:
preset: null # Style preset name (models/{template}/presets/)
# reference_doc: assets/reference.docx # Custom Word reference
# ============================================================================
# HTML5 Configuration (OPTIONAL)
# ============================================================================
html5:
number_sections: true
table_of_contents: true
toc_depth: 3
standalone: true
embed_resources: true
resource_path: build
# ============================================================================
# Bibliography and Citations (OPTIONAL)
# ============================================================================
bibliography: refs.bib
csl: ieee.csl3.2 Required Fields
| Field | Type | Description |
|---|---|---|
project.code |
string | Project code identifier |
project.name |
string | Human-readable project name |
doc_files |
list | One or more Markdown file paths to process |
3.3 Default Values
| Field | Default | Notes |
|---|---|---|
template |
default |
Built-in base model is always loaded |
output_dir |
build |
Also stores specir.db |
logging.level |
INFO |
Overridden by
SPECCOMPILER_LOG_LEVEL env var |
4 Document Authoring
CommonSpec extends standard Markdown with six constructs for specification documents. The syntax uses existing Markdown constructs (headers, blockquotes, code blocks, links) with specific patterns that the pipeline recognizes. See the CommonSpec Language Specification for the formal definition.
4.1 Specifications
Level 1 headers declare the top-level document container.
Pattern: # type: Title @PID
# srs: Software Requirements Specification @SRS-0014.2 Spec Objects
Level 2-6 headers declare requirements, design elements, sections, or any typed element.
Pattern: ## type: Title @PID
## hlr: User Authentication @HLR-001
### llr: Password Validation @LLR-001
#### section: Implementation NotesIf @PID is omitted, a PID is
auto-generated using the type’s pid_prefix and pid_format.
A spec object owns the content that follows it until the section is
closed. A section closes automatically at the next header of equal or
shallower level, and child headers must go exactly one level deeper
(jumping from ## to #### is reported as a broken
hierarchy). To close a section without opening a new
heading – so that the next paragraphs or included sections belong to the parent
– end it with a ---- thematic
break:
## Pesquisa-Ação @SEC-PA
### Considerações Iniciais
Intro that belongs to "Considerações Iniciais".
----
This paragraph belongs to the chapter, not the section above.The ---- is consumed (it never
renders as a horizontal rule). Do not use an empty ## as a “section reset”: an empty
heading is rejected because it would render as a blank numbered chapter
and shift all later numbering.
4.3 Attributes
Blockquotes declare attributes using the key: value pattern. They belong to the
most recently opened Specification or SpecObject header and do not need
to appear immediately after it:
## hlr: User Authentication @HLR-001
> priority: High
> status: Draft
> rationale: Required by security policyRules:
- Each attribute blockquote must be separated by a blank line.
- The first line must match
key: value(wherekeyis[A-Za-z0-9_]+). If not, the blockquote is treated as prose. The key does not need to be a registered attribute type; unregistered keys default toSTRINGdatatype. - Multi-line values are supported: continuation lines append to the preceding attribute.
- Supported datatypes:
STRING,INTEGER,REAL,BOOLEAN,DATE(YYYY-MM-DD),ENUM,XHTML.
4.4 Floats
Fenced code blocks with a typed first class declare numbered elements.
Pattern: ```type.lang:label{key="val"}
4.4.1 PlantUML Diagram
```plantuml:diag-state{caption="State Machine"}
@startuml
[*] --> Active
Active --> Inactive
@enduml
```4.4.2 Table
```list-table:tbl-interfaces{caption="External Interfaces"}
> header-rows: 1
> aligns: l,l,l
* - Interface
- Protocol
- Direction
* - GPS
- ARINC-429
- Input
```4.4.3 CSV Table
The Comma-Separated Values (CSV) float alias provides a compact syntax for tabular data:
```csv:tbl-data{caption="Sample Data"}
Name,Value,Unit
Temperature,72.5,F
Pressure,1013.25,hPa
```Both csv and list-table produce TABLE floats. Use
csv for simple tabular data and
list-table for tables with rich
Markdown content in cells. See 6.2 Floats in Practice for live examples
of each.
4.4.4 Listing (Code)
```listing.c:lst-init{caption="Initialization Routine"}
void init(void) {
setup_hardware();
}
```4.4.5 Chart (ECharts)
The default model does not
define the CHART float. Use an
overlay model that defines CHART
and its renderer. Install any external tool required by that model.
```chart:chart-coverage{caption="Test Coverage"}
{
"xAxis": { "data": ["Module A", "Module B"] },
"series": [{ "type": "bar", "data": [95, 87] }]
}
```Charts support data injection via view modules. Add view="gauss" and params="mean=0,sigma=1" to the code
fence attributes to inject generated data into the ECharts configuration
at render time. The Chart with Data View Injection example
below shows the syntax (the gauss view ships with the overlay
model that provides charts).
4.4.6 Math
```math:eq-force{caption="Newton's Second Law"}
F = ma
```Math floats use AsciiMath notation and are rendered to MathML for HTML5 output and OMML for DOCX. See Equation 1 and Equation 2 for live examples in this manual.
4.4.7 Float Syntax Summary
| Component | Description |
|---|---|
type |
Float type identifier (for example
figure, plantuml, csv,
list-table, listing, chart,
math) |
.lang |
Optional language hint for syntax highlighting |
:label |
Float label for cross-referencing; must be unique within the specification |
{key="val"} |
Key-value attributes; common attribute:
caption |
4.5 Relations (Links)
Links use the pattern [content](selector). Selectors are
not hardcoded – they are registered by relation types
in the model’s type system. Each relation type declares a link_selector field, and the pipeline
uses it for resolution and type inference. The default model registers the following
selectors:
| Selector | Registered by | Resolution |
|---|---|---|
@ |
PID_REF base (XREF_SEC and
model-specific types) |
PID lookup: same-spec first, then cross-document fallback |
# |
LABEL_REF base (XREF_FIGURE,
XREF_TABLE, XREF_LISTING, XREF_MATH, XREF_SECP) |
Scoped label resolution: local scope, then same-spec, then global |
@cite |
XREF_CITATION | Rewritten to pandoc Cite element (parenthetical) |
@citep |
XREF_CITATION | Rewritten to pandoc Cite element (in-text) |
Custom models can register additional selectors by defining relation
types with new link_selector
values.
| Syntax | Example | Description |
|---|---|---|
[PID](@) |
[HLR-001](@) |
Reference by PID |
[type:label](#) |
[fig:diagram](#) |
Typed float reference |
[scope:type:label](#) |
[REQ-001:fig:detail](#) |
Scoped float reference |
[key](@cite) |
[smith2024](@cite) |
Parenthetical citation |
[key](@citep) |
[smith2024](@citep) |
In-text citation |
4.5.1 Type Inference
After a link is resolved, the inference algorithm scores it against all registered relation types using 4 unweighted dimensions. Each matching dimension adds +1 to the specificity score. A constraint mismatch eliminates the candidate entirely. The total score for a candidate is computed as:
The four dimensions ( through ) correspond to selector, source attribute, source type, and target type as shown in Table 8:
| Dimension | Match | Constraint mismatch | No constraint (NULL) |
|---|---|---|---|
Selector (@,
#, @cite, etc.) |
+1 | Eliminated | +0 |
| Source attribute | +1 | Eliminated | +0 |
| Source type | +1 | Eliminated | +0 |
| Target type | +1 | Eliminated | +0 |
The highest-scoring candidate wins. If two candidates tie, the
relation is marked ambiguous. For example, [fig:diagram](#) resolving to a FIGURE
float will match XREF_FIGURE (selector # + target type FIGURE = specificity
)
over the generic LABEL_REF base
(selector # only = specificity
).
4.6 Views
Inline code with a specific prefix declares view placeholders:
`toc:`Default model view types:
| Type | Aliases | Description |
|---|---|---|
toc - — |
Table of Contents (TOC) from spec object headings | |
lof |
lot |
List of floats (figures, tables, etc.) |
abbrev |
sigla,
acronym |
Define an abbreviation inline: Full Meaning (ABBR) |
abbrev_list |
sigla_list,
acronym_list |
Render a sorted table of all abbreviations
defined via abbrev: |
math_inline |
eq, formula |
Inline math expression rendered to MathML/OMML |
gauss |
gaussian,
normal |
Generate Gaussian distribution data for chart floats |
4.7 Body Content
Prose paragraphs, lists, and tables between headers accumulate to the most recently opened Specification or Spec Object.
4.8 File Includes
Split large documents into multiple files using fenced code blocks
with the include class:
```include
path/to/chapter1.md
path/to/chapter2.md
```Each line is a file path relative to the including document’s
directory. Absolute paths are also supported. Lines starting with # are treated as comments and
ignored.
Include blocks are expanded recursively before the pipeline runs. Circular includes are detected and produce an error. The maximum nesting depth is 100 levels.
During expansion, SpecCompiler shifts all headings in an included
file by the same amount. The shallowest heading becomes a child of the
active heading. For example, an include under ## changes an included # to ###. It changes the included ## to ####. SpecCompiler preserves relative
depths and reports skipped levels. Nested includes combine these shifts.
A ---- marker before the
directive reduces the active level by one:
## Design
Content of Design. The include below nests under Design: the included
file's `#` becomes `###`.
```include
design_details.md
```
----
The `----` closed Design, so this include becomes a sibling `##` section:
```include
next_chapter.md
```SpecCompiler positions an included file relative to its shallowest heading. The first heading can have any level. Nested includes can produce Pandoc heading levels greater than 6. SpecCompiler preserves these levels. Each output format renders them according to its capabilities.
Included files are tracked in the build graph for incremental builds – a change to any included file triggers a rebuild.
5 Using the Default Model
5.1 Rationale
The default model ships a
complete document authoring toolkit so that authors can write structured
technical documents without defining custom types. It provides:
- Numbered floats – figures, tables, code listings, math equations, and PlantUML diagrams, each with automatic numbering and captions.
- Typed cross-references – relation types that resolve
@and#links to specific float and object categories, enabling the pipeline to render appropriate display text (for example, “Figure 3” or “Table 1”). - Bibliography citations – integration with Pandoc’s citeproc for parenthetical and in-text citation rendering from BibTeX files.
- Content views – generated content blocks such as TOC, list of figures, abbreviation tables, and inline math.
The following subsections demonstrate these features with live floats and cross-references. Every float, view, and link shown below is processed by SpecCompiler when this manual is built.
5.2 Floats in Practice
A SpecCompiler document can use all default float types. Each float has a type prefix, a label for cross-referencing, and a caption. The examples below are live – they are rendered when this manual is processed.
5.2.1 Architecture Diagram (PlantUML)
5.2.2 Component Table (list-table)
| Component | Layer | Technology |
|---|---|---|
| Web UI | Presentation | React |
| Auth Service | Business Logic | Node.js |
| Data Service | Business Logic | Python |
| Database | Persistence | PostgreSQL |
5.2.3 Performance Metrics (CSV)
| Metric | Target | Actual | Status |
|---|---|---|---|
| Response time (ms) | 200 | 185 | Pass |
| Throughput (req/s) | 1000 | 1120 | Pass |
| Error rate (%) | 1.0 | 0.3 | Pass |
| Memory usage (MB) | 512 | 487 | Pass |
5.2.4 Initialization Code (Listing)
def initialize(config):
db = connect(config.db_url)
auth = AuthService(db)
return Application(auth, db)5.2.5 Latency Model (Math)
5.2.6 Throughput Chart (ECharts)
{
"xAxis": { "type": "category", "data": ["Auth", "Data", "Search", "Notify"] },
"yAxis": { "type": "value", "name": "req/s" },
"series": [{ "type": "bar", "data": [1200, 3400, 890, 2100], "itemStyle": { "color": "#91cc75" } }]
}5.3 Cross-References
Every float and object defined above can be referenced from prose.
The following paragraph demonstrates cross-reference resolution using
the # selector.
The system architecture is depicted in Figure 2. Component details, including the
technology stack for each layer, are listed in Table 10.
Performance targets and actuals are compared in Table 11 – all
four metrics pass their thresholds. The initialization logic is shown in
Listing 20,
and the latency model driving performance requirements is defined by Equation 2.
Finally, throughput measurements by module are described by the chart
config shown above, which renders as a chart under overlay models that
provide the chart: float.
The @ selector resolves by PID
and works across documents. For example, this sentence references the
introduction of this manual: 1 INDEX. Cross-document references to the
companion guides also work; see creating-a-model: 5 Model directory
layout for the model directory layout and docx-customization: 3 Style Presets for
DOCX style presets.
| Selector | Syntax | Resolution |
|---|---|---|
@ (PID) |
[PID](@) |
Exact PID lookup. Same-spec first, then cross-document fallback. Never ambiguous. |
# (Label) |
[type:label](#) |
Scoped resolution: local scope, then same specification, then global. May be ambiguous if multiple matches at the same scope level. |
5.4 Section References
Headers without an explicit TYPE: prefix default to the SECTION
type. Sections receive auto-generated PIDs and labels that can be used
for cross-referencing:
- PID format:
{spec_pid}-sec{depth.numbers}– for example,SRS-sec1,SRS-sec1.2,SRS-sec2.3.1. Use the@selector:[SRS-sec1.2](@). - Label format:
section:{title-slug}– for example,## Introductionproduces the labelsection:introduction. Use the#selector:[section:introduction](#).
The @ selector performs an exact
PID lookup and is never ambiguous. The # selector uses scoped resolution
(closest scope wins), which is useful when multiple specifications have
sections with similar names.
For cross-document section references with the # selector, use the explicit scope
syntax: [SPEC-A:section:design](#) to target a
section labeled “design” within the specification whose PID is SPEC-A.
This manual references its own sections using both selectors. Here are examples that resolve within this document:
- By PID: 2 Introduction links to Installation, 4 Project Configuration links to Document Authoring.
- By label: 2.3 Pipeline Summary links to Pipeline Summary, 12 Troubleshooting links to Troubleshooting.
Cross-document references work identically. Because the companion
guides are listed in the same project.yaml, these links resolve at
build time:
- creating-a-model: 8.4 Walkthrough custom float type links to the float walkthrough in the model guide.
- docx-customization: 4 Postprocessors links to the DOCX customization guide.
- docx-customization: 3.6 Preset Inheritance links to preset inheritance in the DOCX guide.
5.5 Citations and Bibliography
SpecCompiler integrates with Pandoc’s citeproc processor for scholarly citations.
Step 1. Add bibliography configuration to project.yaml:
bibliography: refs.bib
csl: ieee.cslStep 2. Create a BibTeX file (refs.bib):
@article{smith2024,
author = {Smith, John},
title = {Advances in Systems Engineering},
journal = {IEEE Transactions},
year = {2024}
}
@book{jones2023,
author = {Jones, Alice},
title = {Software Architecture Patterns},
publisher = {O'Reilly},
year = {2023}
}Step 3. Use citation syntax in your document:
Recent work [smith2024](@cite) demonstrates the approach.
As Smith [smith2024](@citep) argues, the method is effective.
Multiple sources support this [smith2024;jones2023](@cite).[key](@cite)produces a parenthetical citation – for example, “(Smith, 2024)” in author-date styles or “[1]” in numeric styles.[key](@citep)produces an in-text citation – for example, “Smith (2024)” or “Smith [1]”.- Multiple keys separated by
;produce a grouped citation.
Processing pipeline: During the TRANSFORM phase,
citation links are rewritten to Pandoc Cite elements. During EMIT, Pandoc’s
citeproc processor formats citations and appends a bibliography list to
the document according to the configured CSL style.
5.6 Views in Practice
Views generate content blocks from the SpecIR.
5.6.1 Abbreviations
The abbrev: view defines
abbreviations inline. On first use, the full meaning is displayed
alongside the abbreviation. All definitions are collected for the abbrev_list view shown in the 17 List of
Abbreviations appendix.
This manual defines abbreviations on first use throughout the text. For example, Entity-Attribute-Value (EAV) is the database pattern used for flexible attributes, and Newline-Delimited JSON (NDJSON) is the format used for diagnostic output.
The syntax is: `abbrev: Full Meaning Text (ABBREVIATION)`.
The abbreviation goes in parentheses at the end.
5.6.2 Inline Math
The eq: prefix renders inline
math expressions using AsciiMath notation. For example, the quadratic
formula is
,
and Euler’s identity is
.
Inline math is useful for formulas within prose paragraphs, while
block math: floats (like Equation 1 and
Equation 2)
provide numbered equations with captions.
5.6.3 Chart with Data View Injection (Gauss)
Charts can load data dynamically from view modules using the view attribute. The gauss view generates a Gaussian
probability density function and injects it into the ECharts dataset.
The syntax below shows this – the view="gauss" attribute triggers the
data injection pipeline in models that provide the chart float:
```chart:chart-gauss{caption="Standard Normal Distribution" view="gauss" params="mean=0,sigma=1,xmin=-3,xmax=3,points=61"}
{
"xAxis": { "type": "value", "name": "x" },
"yAxis": { "type": "value", "name": "f(x)" },
"series": [{ "type": "line", "smooth": true }]
}
```The params attribute passes
mean, sigma, xmin, xmax, and points to the Gauss view’s dataset data hook. The hook returns an
ECharts dataset that replaces the chart’s placeholder data at render
time. This same mechanism supports custom data views that query the
SpecIR database; see creating-a-model: 8.3 Walkthrough custom
view in the model guide for details on creating view types.
5.6.4 Generated Lists
The [LOF] and [LOT] views produce navigable lists of figures and tables. These are rendered in the appendices of this manual:
- 14 List of Figures – generated by [LOF]
- 15 List of Tables – generated by [LOT]
- 17 List of Abbreviations – generated by
abbrev_list
6 Invocation
6.1 Basic Usage
specc buildProcesses all files from doc_files in the current directory’s
project.yaml. An alternative
project file can be specified: specc build my-project.yaml.
6.2 Environment Variables
| Variable | Default | Description |
|---|---|---|
SPECCOMPILER_LOG_LEVEL |
INFO |
Override log level: DEBUG,
INFO, WARN, ERROR |
SPECCOMPILER_HOME |
/opt/speccompiler |
SpecCompiler installation root (model and binary lookup) |
SPECCOMPILER_DIST |
/opt/speccompiler |
Distribution root (used internally for external renderers) |
SPECCOMPILER_IMAGE |
speccompiler-core:latest |
Docker image reference (overrides default in wrapper) |
NO_COLOR |
(unset) | Disable ANSI color codes in output |
6.3 Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success: all documents processed and outputs generated |
| 1 | Failure: Docker unavailable or configuration or pipeline error |
| 2 | Blocking diagnostics: analysis reported one or more errors |
7 Output Formats
Four output formats are supported. Multiple formats can be generated in a single run.
7.1 DOCX (Microsoft Word)
- Style presets via
docx.presetor customdocx.reference_doc. - Model-specific postprocessors for format transformations.
For a complete guide on customizing DOCX output – including paragraph styles, table styles, caption configuration, and postprocessors – see docx-customization: 3 Style Presets and docx-customization: 4 Postprocessors in the companion DOCX Customization guide.
7.2 HTML5
| Option | Type | Default | Description |
|---|---|---|---|
number_sections |
boolean | false | Add section numbering |
table_of_contents |
boolean | false | Generate table of contents |
toc_depth |
integer | 3 | Heading depth for TOC |
standalone |
boolean | false | Produce complete HTML document |
embed_resources |
boolean | false | Embed CSS and images inline |
7.3 Markdown (GitHub-Flavored Markdown (GFM))
GitHub-Flavored Markdown. Useful for review platforms and static site generators.
7.4 JSON (Pandoc AST)
Full Pandoc AST for programmatic integration with other tools.
8 Verification and Diagnostics
8.1 Diagnostic Output
Diagnostics are emitted in NDJSON format to stderr:
{"level":"error","message":"[missing_required] Object missing required attribute 'priority' on HLR-001","file":"srs.md","line":42}8.2 Diagnostic Reference
| Policy Key | Description |
|---|---|
spec_missing_required |
Specification missing required attribute |
spec_invalid_type |
Invalid specification type reference |
missing_required |
Spec object missing required attribute |
cardinality_over |
Attribute cardinality exceeded |
invalid_cast |
Attribute type cast failure |
invalid_enum |
Invalid enum value |
invalid_date |
Invalid date format (expected YYYY-MM-DD) |
bounds_violation |
Value outside declared bounds |
object_duplicate_pid |
Duplicate PID across spec objects |
float_orphan |
Float has no parent object (orphan) |
float_duplicate_label |
Duplicate float label in specification |
float_render_failure |
External render failure |
float_invalid_type |
Invalid float type reference |
unresolved_relation |
Unresolved link (PIDs are case-sensitive) |
dangling_relation |
Dangling relation (target not found) |
ambiguous_relation |
Ambiguous float reference |
view_materialization_failure |
View materialization failure |
8.3 Suppressing Validation Rules
Every diagnostic listed in Table 16 can be suppressed or downgraded
in project.yaml using its
policy key:
validation:
float_orphan: ignore # suppress entirely
unresolved_relation: warn # downgrade to warningAnalyze-query diagnostics default to error. Set a key to warn to continue the build with a
warning. Set it to ignore to
suppress the diagnostic. Custom analyze queries can define policy
keys.
9 Incremental Builds
9.1 Build Cache Mechanism
- File hashing – SHA-1 hash of each input file.
- Include dependency tracking – Tracked in
build_graphtable. - Cache comparison – Current hashes vs
build_cachetable. - Skip decision – Unchanged documents reuse cached SpecIR data.
9.2 Forcing a Full Rebuild
specc clean
specc build10 Type System and Models
10.1 Built-in Models
SpecCompiler includes the default and sw_docs models. The default model provides general-purpose
types. The sw_docs model adds
requirements-engineering and traceability types:
- Object types: HLR, LLR, NFR, VC, TR, FD, CSC, CSU, DIC, DD, SF (all extend a common TRACEABLE base with
statusattribute and PID auto-generation) - Specification types: SRS, SDD, SVC, SUM, TRR (document templates with version, status, date)
- Relation types: TRACES_TO, BELONGS, REALIZES, VERIFIES, XREF_DECOMPOSITION, XREF_DIC (traceability links with specificity-based inference)
- View types: TRACEABILITY_MATRIX, TEST_RESULTS_MATRIX, TEST_EXECUTION_MATRIX, COVERAGE_SUMMARY, REQUIREMENTS_SUMMARY (query-based tables materialized from the SpecIR)
- Analyze queries: Traceability-chain validation for VC-HLR, TR-VC, and FD-CSC/CSU coverage
- Postprocessor: Interactive single-file HTML5 web application
The docs/engineering_docs/
project uses sw_docs.
10.2 Custom Models
Set template: mymodel in project.yaml. Custom types layer as
overlays on top of default
(types with matching id
replace the default; new ids
add). The model directory is resolved under the SpecCompiler
installation (models/mymodel/)
first, then under the project directory itself — so a project can ship
its own model alongside its documents.
Each extension module returns one Lua descriptor table. The
descriptor contains a kind, a
schema, and optional hooks. Supported kinds are object, float, view, relation, specification, and analyze. The schema.id field is the authoritative
identifier. Each hook name determines its context and return type. A
descriptor can inherit shared behavior through schema.extends.
The Creating a Custom Model guide defines the directory layout, hook contract, schema fields, analyze queries, and external renderers.
11 Troubleshooting
11.1 Docker Not Running
Error: Docker is not running –
Start Docker daemon, verify with docker info.
11.2 No project.yaml Found
Run specc build from the
directory containing project.yaml.
11.3 PlantUML Render Failure
Verify PlantUML syntax, ensure Docker image has Java JRE, check @startuml/@enduml markers.
11.4 Unresolved Relations
PIDs are case-sensitive. Verify target PID exists in
doc_files. For cross-document
references, ensure both documents are listed in the same project.yaml.
11.5 Build Seems Stale
specc clean
specc build11.6 Debugging
SPECCOMPILER_LOG_LEVEL=DEBUG specc build12 Known Limitations
- No interactive validation – Batch mode only, no LSP or watch mode.
- Native install is Ubuntu-first –
scripts/install-native.shautomates Ubuntu 24.04 (apt); other distros must install the documented package equivalents manually before running it. - Single-writer SQLite – Concurrent builds cause locking errors; use separate output directories.
- Float labels per-specification – Same label can exist across specs; use scoped syntax for cross-spec references.
- PID case sensitivity –
[hlr-001](@)will not match@HLR-001.
13 List of Figures
14 List of Tables
- Table 1 - Runtime prerequisites
- Table 2 - Wrapper modes
- Table 3 - Required project fields
- Table 4 - Default configuration values
- Table 5 - Float syntax components
- Table 6 - Default model selectors
- Table 7 - Relation syntax patterns
- Table 8 - Type inference scoring dimensions
- Table 9 - Default view types
- Table 10 - System Components
- Table 11 - Performance Metrics
- Table 12 - Cross-reference selector comparison
- Table 13 - Environment variables
- Table 14 - Exit Codes
- Table 15 - HTML5 output options
- Table 16 - Validation diagnostics
15 List of Listings
- Listing 1 - Container install (Linux/macOS/WSL2)
- Listing 2 - Container install (Windows PowerShell)
- Listing 3 - Build and install via Docker
- Listing 4 - Verify Docker image availability
- Listing 5 - Run wrapper command
- Listing 6 - project.yaml reference
- Listing 7 - Specification declaration
- Listing 8 - Spec object declaration
- Listing 9 - Closing a section with ----
- Listing 10 - Attribute declaration
- Listing 11 - PlantUML float syntax
- Listing 12 - Table float syntax
- Listing 13 - CSV float syntax
- Listing 14 - Listing float syntax
- Listing 15 - Chart float syntax
- Listing 16 - Math float syntax
- Listing 17 - Inline view placeholder
- Listing 18 - Include directive syntax
- Listing 19 - Include heading levels are relative to the include point
- Listing 20 - Service Initialization
- Listing 21 - Bibliography configuration in project.yaml
- Listing 22 - Example BibTeX file
- Listing 23 - Citation syntax examples
- Listing 24 - Chart with gauss data injection
- Listing 25 - Basic invocation
- Listing 26 - Diagnostic NDJSON example
- Listing 27 - Suppressing a validation rule
- Listing 28 - Force full rebuild
- Listing 29 - Clean stale build cache
- Listing 30 - Enable debug logging
16 List of Abbreviations
| AST | Pandoc Abstract Syntax Tree |
| CLI | Command-Line Interface |
| CSV | Comma-Separated Values |
| EAV | Entity-Attribute-Value |
| GFM | GitHub-Flavored Markdown |
| NDJSON | Newline-Delimited JSON |
| PID | Project Identifier |
| SpecIR | Specification Intermediate Representation |
| SQL | Structured Query Language |
| SQLite | SQLite Database |