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:

  1. Decide the kind (object, float, view, relation, specification, or analyze).
  2. Define the type in schema. The schema.id field is the authoritative identifier.
  3. 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:

Listing : The descriptor a type module returns, annotated
-- 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:

Listing : Project layout with a local model
my-project/
  project.yaml            -- template: adr
  decisions.md
  models/
    adr/
      types/
        objects/
          decision.lua    -- the whole model
Listing : models/adr/types/objects/decision.lua — the complete 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:

Listing : decisions.md
# 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:

  1. models/default/types/ — loaded first.
  2. models/mymodel/types/ — loaded second. A descriptor with the same kind and schema.id replaces the default descriptor.

Descriptors with new identifiers supplement the default descriptors. The model inherits definitions that it does not replace.

5 Model directory layout

Listing : Model directory structure
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:

Listing : model.yaml — declaring a model dependency
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

Table : Type categories, their kind, and the hooks they typically declare
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.

Table : The hook catalogue: each hook's kind, tier, the one return type, and when it fires
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

Table : The frozen context fields, by tier
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:

Listing : types/objects/hlr.lua
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:

Listing : Authoring an HLR in Markdown
## 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:

Listing : types/relations/traces_to.lua
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):

Listing : Custom display text via a render_link hook
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.

Listing : types/views/symbol.lua
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.

Listing : A generated table view via build_block (types/views/traceability_matrix.lua)
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:

Listing : types/floats/sequence_diagram.lua
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:

Listing : Authoring a float
```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:

Listing : types/specifications/srs.lua
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

Table : Object schema fields
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

Table : Attribute definition fields
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

Table : Float schema fields
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

Table : Relation schema fields
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

Table : View schema fields
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

  1. Pick a model name (lowercase, matches the directory name).
  2. Create models/<name>/types/… with the categories you need.
  3. Return one descriptor with kind, schema, and optional hooks fields.
  4. Add only the hooks that implement required custom behavior.
  5. Set template: <name> in project.yaml.
  6. Run specc build and inspect the output.
  7. 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.

Listing : models/mymodel/analyze_queries/vc_missing_hlr.lua
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:

Listing : Suppressing an analyze query
validation:
  traceability_vc_to_hlr: ignore

12 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.

Listing : types/floats/plantuml.lua — an external-render float
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,
    },
}
Table : Task descriptor returned by prepare_task
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

Listing : Using a custom model in project.yaml
project:
  code: MYPROJ
  name: My Project

template: mymodel   # Loads models/default/ then models/mymodel/

doc_files:
  - srs.md

14 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

Guide: DOCX Customization

1 Introduction

SpecCompiler generates DOCX output through a multi-stage pipeline:

  1. SpecIR – Structured data in SQLite (objects, relations, floats, attributes).
  2. Pandoc AST – The emitter assembles a Pandoc document from the SpecIR.
  3. Pandoc DOCX Writer – Pandoc converts the AST to DOCX using a reference.docx for styles.
  4. Lua Filter – A format-specific filter converts SpecCompiler markers to OOXML (captions, bookmarks, math).
  5. 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:

  1. Auto-generated from presets (default): SpecCompiler builds a reference.docx from Lua style presets, storing it at {output_dir}/reference.docx.
  2. User-provided: Set docx.reference_doc in project.yaml to 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:

Listing : Preset table structure
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

Table : 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

Listing : Paragraph style definitions
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

Listing : Table style definition
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

Listing : Caption configuration per float type
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:

Listing : Preset inheritance
-- 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 overrides
  • models/{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 keepNext to 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:

Table : Postprocessor hook functions
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:

Listing : Custom DOCX postprocessor
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 M

4.4 The config Parameter

The config table passed to hooks contains:

  • template – The template name
  • docx – DOCX configuration from project.yaml
  • spec_metadata – Specification-level attributes (in create_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:

Table : Default DOCX filter conversions
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

Listing : DOCX configuration in project.yaml
# 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

  1. If docx.reference_doc is set, that file is used directly as the Pandoc reference document.
  2. If docx.preset is set (or defaults to the model’s styles), SpecCompiler generates {output_dir}/reference.docx from the preset.
  3. 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:

  1. Compute SHA-1 hash of the preset file content.
  2. Compare against the stored hash in the build_meta table (key-value store in specir.db).
  3. If the hashes match and reference.docx exists on disk, skip generation.
  4. If the preset changed or reference.docx is missing, regenerate and update the cache.

To force regeneration of the reference document, delete it:

Listing : Force reference document regeneration
rm -f build/reference.docx
./bin/speccompiler-core

SpecCompiler 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 #label references.
  • 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:

  1. 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.
  2. RESOLVE – Generate missing PIDs, resolve relation targets, and infer relation types from model constraints (see Equation 1).
  3. TRANSFORM – Resolve and number floats, render typed content, and rewrite resolved links.
  4. ANALYZE – Execute SQL policy views against SpecIR, apply configured severities, and report violations.
  5. EMIT – Assemble Pandoc documents from SpecIR, expand generated views, and generate configured outputs via parallel Pandoc subprocesses.
Figure Processing Pipeline

{
  "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:

Table : Runtime prerequisites
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):

Listing : Container install (Linux/macOS/WSL2)
curl -fsSL https://raw.githubusercontent.com/SpecIR/SpecCompiler/main/scripts/install.sh | bash

One command on Windows, in PowerShell (after Docker Desktop or Podman is installed):

Listing : Container install (Windows PowerShell)
irm https://raw.githubusercontent.com/SpecIR/SpecCompiler/main/scripts/install.ps1 | iex

install.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:

Listing : Build and install via Docker
docker build -t speccompiler-core:latest .
bash scripts/install.sh

2.4 Verifying Installation

After building, verify the image is available:

Listing : Verify Docker image availability
docker images speccompiler-core

To verify the tool runs correctly, navigate to a directory containing a project.yaml file and run:

Listing : Run wrapper command
specc build

2.5 The specc Wrapper

The specc command is a single wrapper shared by every install mode. Its mode is read from ~/.config/speccompiler/env:

Table : Wrapper modes
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-id instead).
  • -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

Listing : project.yaml 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.csl

3.2 Required Fields

Table : Required project 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

Table : Default configuration 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

Listing : Specification declaration
# srs: Software Requirements Specification @SRS-001

4.2 Spec Objects

Level 2-6 headers declare requirements, design elements, sections, or any typed element.

Pattern: ## type: Title @PID

Listing : Spec object declaration
## hlr: User Authentication @HLR-001
### llr: Password Validation @LLR-001
#### section: Implementation Notes

If @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:

Listing : Closing a section with ----
## 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:

Listing : Attribute declaration
## hlr: User Authentication @HLR-001

> priority: High

> status: Draft

> rationale: Required by security policy

Rules:

  • Each attribute blockquote must be separated by a blank line.
  • The first line must match key: value (where key is [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 to STRING datatype.
  • 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

Listing : PlantUML float syntax
```plantuml:diag-state{caption="State Machine"}
@startuml
[*] --> Active
Active --> Inactive
@enduml
```

4.4.2 Table

Listing : Table float syntax
```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:

Listing : CSV float syntax
```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 : Listing float syntax
```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.

Listing : Chart float syntax
```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

Listing : Math float syntax
```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

Table : Float syntax components
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:

Table : Default model 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.

Table : Relation syntax patterns
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:

S=i=14di,  di{0,1}S = \sum\limits_{i = 1}^{4}d_{i},\operatorname{~~}d_{i} \in \left\{ 0,1 \right\}

(1)

The four dimensions (d1d_{1} through d4d_{4}) correspond to selector, source attribute, source type, and target type as shown in Table 8:

Table : Type inference scoring dimensions
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 S=2S = 2) over the generic LABEL_REF base (selector # only = specificity S=1S = 1).

4.6 Views

Inline code with a specific prefix declares view placeholders:

Listing : Inline view placeholder
`toc:`

Default model view types:

Table : Default 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:

Listing : Include directive syntax
```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:

Listing : Include heading levels are relative to the include point
## 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)

Figure Layered Architecture

5.2.2 Component Table (list-table)

Table : System Components
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)

Table : Performance Metrics
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)

Listing : Service Initialization
def initialize(config):
    db = connect(config.db_url)
    auth = AuthService(db)
    return Application(auth, db)

5.2.5 Latency Model (Math)

L=Ttwork+Tprocessing+TdbL = T_{\neq tw\operatorname{or}k} + T_{proces\sin g} + T_{db}

(2)

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.

Table : Cross-reference selector comparison
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, ## Introduction produces the label section: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:

Cross-document references work identically. Because the companion guides are listed in the same project.yaml, these links resolve at build time:

5.5 Citations and Bibliography

SpecCompiler integrates with Pandoc’s citeproc processor for scholarly citations.

Step 1. Add bibliography configuration to project.yaml:

Listing : Bibliography configuration in project.yaml
bibliography: refs.bib
csl: ieee.csl

Step 2. Create a BibTeX file (refs.bib):

Listing : Example BibTeX file
@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:

Listing : Citation syntax examples
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 x=b±b24ac2ax = \frac{- b \pm \sqrt{b^{2} - 4ac}}{2a}, and Euler’s identity is eiπ+1=0e^{i\pi} + 1 = 0.

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:

Listing : Chart with gauss data injection
```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:

6 Invocation

6.1 Basic Usage

Listing : Basic invocation
specc build

Processes 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

Table : 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

Table : 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.preset or custom docx.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

Table : HTML5 output options
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:

Listing : Diagnostic NDJSON example
{"level":"error","message":"[missing_required] Object missing required attribute 'priority' on HLR-001","file":"srs.md","line":42}

8.2 Diagnostic Reference

Table : Validation diagnostics
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:

Listing : Suppressing a validation rule
validation:
  float_orphan: ignore              # suppress entirely
  unresolved_relation: warn         # downgrade to warning

Analyze-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

  1. File hashing – SHA-1 hash of each input file.
  2. Include dependency tracking – Tracked in build_graph table.
  3. Cache comparison – Current hashes vs build_cache table.
  4. Skip decision – Unchanged documents reuse cached SpecIR data.

9.2 Forcing a Full Rebuild

Listing : Force full rebuild
specc clean
specc build

10 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 status attribute 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

Listing : Clean stale build cache
specc clean
specc build

11.6 Debugging

Listing : Enable debug logging
SPECCOMPILER_LOG_LEVEL=DEBUG specc build

12 Known Limitations

  • No interactive validation – Batch mode only, no LSP or watch mode.
  • Native install is Ubuntu-firstscripts/install-native.sh automates 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

15 List of Listings

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