Architecture

This document describes the architecture of the Antora Tracer extension using selected arc42 sections. Each section is a traceable [item] that references the requirements it addresses.

Introduction and Goals

ARC-001 — Project scope and architectural goals

Project scope and architectural goals

The Antora Tracer is an Antora extension that adds role-based requirements traceability to AsciiDoc documentation. It replaces fixed macro types with a single configurable [item] macro.

Architectural goals:

  • Enable users to define their own traceability domain model (roles, relations, matrices) through YAML configuration

  • Maintain a clean separation between parsing, graph storage, validation, matrix generation, and export

  • Ship with built-in presets for common domains without requiring users to write configuration

  • Integrate with Antora’s event pipeline without imposing runtime dependencies on users who don’t need specific features

  • Provide a standalone CLI for use outside Antora

Relationship graph for ARC-001

Solution Strategy

The following items describe how individual quality goals are addressed by architecture decisions documented in the ADRs and quality acceptance criteria in the Quality Attributes.

ARC-005 — Zero operational overhead

Zero operational overhead is achieved through an in-memory graph (ADR-003) no external services, and a minimal dependency tree (ADR-004). Optional features like Neo4j export degrade gracefully without warnings.

ARC-006 — Configurable without code changes

Configurable without code changes is achieved through YAML-based roles, relations, and matrices via ConfigLoader. Built-in presets can be extended or overridden. Items with unknown roles generate warnings, not errors — enabling incremental domain model adoption.

ARC-007 — Fail-fast with clear diagnostics

Fail-fast with clear diagnostics is achieved through role-based relation validation at processing time and post-hoc re-validation for cross-file references. Error messages include file, line, item IDs with roles, and allowed alternatives.

ARC-008 — No side effects on source files

No side effects on source files is achieved by performing all content transformations on in-memory buffers. Source .adoc files are never written to. Processing is idempotent.

ARC-009 — Testability by design

Testability by design is achieved through constructor-based dependency injection isolated test files per module, and graph state exposed for test access. The regex-based parser (ADR-002) is decoupled from Asciidoctor’s API.

ARC-010 — Future-proofness

Future-proofness is achieved through ESM with "type": "module" (ADR-001), TypeScript strict mode and a Node.js 20+ baseline (ADR-005). The zero-framework policy avoids dependency on libraries with short upgrade cycles.

ARC-011 — Performance through indexing

Performance is achieved through forward, reverse, and inverse relationship indexes (O(1) lookup), result caching with invalidation on mutation, and BFS path finding with configurable maxDepth.

Building Block View

ARC-002 — Component-level architecture

The extension is composed of eleven components organized in four layers. Arrows indicate dependencies, with constructor-injected dependencies labeled explicitly.

Diagram
Component Responsibility Key dependencies

AntoraTraceabilityExtension

Antora entry point. Registers contentClassified and sitePublished event handlers, reads playbook config, creates and orchestrates the core extension.

RequirementsTraceabilityExtension (created at init)

CLI (cli.ts)

Standalone CLI. Same core extension as Antora but without event handlers. Commander-based subcommands for processing, matrices, validation, export, next-id.

RequirementsTraceabilityExtension (created per command)

RequirementsTraceabilityExtension

Orchestrator. Owns the TraceabilityGraph, delegates to DocumentParser, MatrixGenerator, Neo4jExporter. Public API for programmatic use.

DocumentParser, TraceabilityGraph, MatrixGenerator, Neo4jExporter, ConfigLoader

DocumentParser

Regex-based AsciiDoc parser. Extracts [#ID, item, role=XXX] blocks and inline ` macros. Excludes `traceability: namespace. Verbatim blocks pre-scanned.

ConfigLoader (optional — for role validation)

TraceabilityGraph

In-memory directed graph. Forward and reverse relationship indexes for O(1) lookups. Provides query methods (findPath, getImpactAnalysis), validation (orphaned refs, circular refs), visualization (toDot, toVegaLite), and ID allocation (getNextId).

ConfigLoader (optional — for relation validation)

ConfigLoader

Loads built-in preset YAML, merges with user-provided config. Exposes isRelationAllowed(), getMatrices(), listPresets(), getAllowedRelations(). Validation queries used by graph and parser.

Built-in presets (src/presets/), user YAML config

MatrixGenerator

Config-driven matrix generation. Reads matrix definitions from config, queries graph for items by role, computes coverage with coverageRelations filter. Produces CSV output; delegates HTML to TemplateRenderer.

TraceabilityGraph, ConfigLoader, TemplateRenderer, optional LinkResolver

TemplateRenderer

Mustache wrapper. Loads templates from src/templates/. Supports custom template directories with fallback to built-ins.

Mustache templates

LinkResolver

Path resolution for matrix-to-item deep links. Normalizes sourceFile (strips pages/ prefix, .adoc extension). Detects URL sourceFiles (partial items) and returns them unchanged. Generates ../../page.html#ID links with configurable relativePathPrefix.

None (standalone utility)

Neo4jExporter

Graph-to-Neo4j CSV + Cypher export. Produces nodes.csv, relationships.csv, and import.cypher with proper escaping.

TraceabilityGraph, ConfigLoader

The following class diagram shows the public API of each component — the method groups and key signatures that form the contract boundaries between modules. See the API reference for full signatures.

Diagram

ConfigLoader

ARC-012 — Preset YAML schema

Presets are YAML files following a defined structure. Each preset is a self-contained versioned configuration that users can extend or override.

Key Type Description

name

string

Unique preset identifier (e.g., requirements-engineering). Used by --preset CLI flag and preset playbook option.

description

multiline string

Human-readable explanation of the preset’s domain, roles, and intended use.

version

semver string

Independent version number for the preset, separate from the extension version.

author

string

Preset author or organization.

tags

string[]

Keywords for categorisation (e.g., [software, ieee, medical]).

compatibility

object

minExtensionVersion: minimum extension version required. Ensures the preset doesn’t load on incompatible versions.

traceability.roles

string[]

Ordered list of valid role names. Items must use one of these roles.

traceability.relations

map

sourceRole → targetRole → [relationTypes]. Defines which relation types are valid between roles. Used for validation.

traceability.matrices

object[]

Array of matrix definitions. Each has name, rows (role), columns (role[]), and coverageRelations (map column role to coverage relation types).

neo4j.queries

object[]

Optional array of named Cypher queries (name, description, cypher). Shipped with the preset and usable post-export.

documentation

object

description (markdown string), examples (string[]). Inline documentation explaining the preset’s domain and usage.

Configuration resolution chain

Built-in presets and user config are merged by ConfigLoader. The resolved configuration is memoized and consumed independently by each component.

Diagram
Element Description

Built-in presets

YAML files in src/presets/ shipped with the extension. Define roles, relations, matrices, and optional Neo4j queries for common domains (requirements-engineering, agile, medical-iec62304, minimal). Selected via --preset flag or preset playbook config.

User config (traceability.yml)

Optional YAML file. Deep-merged on top of the preset. Overrides roles, adds custom matrices, extends relations. Path configured via --config flag or configPath playbook config.

ConfigLoader

Loads preset, deep-merges user config, memoizes the result. Exposes query methods: isKnownRole(), isRelationAllowed(), getAllowedRelations(), getMatrices(), getNeo4jQueries(). First consumer triggers resolution; subsequent calls hit the cache.

Resolved configuration

Memoized merge of preset + user overrides. Four sub-components: roles (validated by parser), relations (used for graph validation — both directions checked), matrices (consumed by MatrixGenerator), neo4j queries (shipped with export).

Consumers

DocumentParser uses roles only. TraceabilityGraph uses roles + relations for addRelationship() validation. MatrixGenerator reads matrix definitions. Neo4jExporter reads named Cypher queries. Each consumer queries independently through ConfigLoader — no shared mutable state.

ARC-018 — Config-driven validation

Relation validation is driven by the YAML configuration rather than hardcoded rules. ConfigLoader.isRelationAllowed() checks source role, target role and relation type against the configured relations map. Cross-file references are re-validated after all files are loaded to catch late-resolved targets. Error messages include file path, line number, and allowed alternatives (see ADR-004).

ARC-036 — Preset inheritance and config merge

A preset may declare a top-level extends field naming a parent preset. ConfigLoader.loadPreset() recursively loads the parent and deep-merges it under the child via mergeConfig(): roles are unioned, relations deep-merged, matrices overridden by name, and labels overridden key-by-key. Inheritance resolves transitively; a missing parent raises a "not found" error, and a circular chain raises a circular-inheritance error.

ARC-028 — Circular reference detection in graph validation

The validate() method calls findCircularReferences(), a DFS-based cycle detector that traverses all items and their outgoing relationships. A path stack and recursion set track visited nodes; when a node is encountered twice in the same path, a circular reference error is reported with the full cycle path (e.g., "REQ-001 → REQ-002 → REQ-001"). Auto-generated inverse relationships are skipped to avoid false positives.

ARC-029 — Partial file processing with source-repo links

The registerContentClassifier() handler processes both family: page and family: partial files. Partial items use Antora’s pre-computed fileUri as their sourceFile, making traceability matrix links point directly to the partial source in the content repository (e.g., GitHub blob URL). The LinkResolver detects URL sourceFile values and returns them unchanged, skipping the HTML path conversion. In buildXref(), URL-based items use AsciiDoc’s link: macro instead of xref: since Asciidoctor cannot resolve external URLs as cross-references. All three passes (graph population, macro expansion, and link substitution) apply to partials as well as pages, because partial content is inlined into pages and reaches the browser.

TraceabilityGraph

ARC-015 — In-memory processing with no side effects

All content transformations — parsing, macro expansion, link substitution title injection — operate on in-memory buffers. Source .adoc files are never written to. The Antora content catalog provides the text buffers; the extension modifies them in place and writes them back to the catalog without touching disk. This ensures idempotency and version-control safety (see ADR-003).

The graph has a well-defined lifecycle — understanding which operations are valid in each state explains why pass ordering matters:

Diagram
ARC-026 — GraphViz DOT and Vega-Lite serialization methods

toDot(fromId, depth?) generates GraphViz DOT source for a relationship graph using BFS traversal in both outgoing and incoming directions from the given item. Nodes are colored by role using a shared role-color palette; edges are labeled with the relationship type. Depth limits hopping. toVegaLite(itemId?) generates a Vega-Lite JSON spec for coverage charts — per-relationship-type for a specific item, or global role counts when called without an item ID. toConfigDot(config) renders the traceability configuration as DOT — roles as nodes and declared relations as labeled edges. Each method produces a source string that is encoded into Kroki URLs for rendering.

ARC-035 — Relation reverse declaration and canonical storage

The relations map is keyed as sourceRole → targetRole → type → { reverse }, each relation type declaring its authorable reverse name. TraceabilityGraph.addRelationship() canonicalizes a reverse-authored edge to the primary direction — authoring is_derived_from from a requirement stores UC → REQ : leads_to. Authoring the same logical edge from both sides deduplicates into one bidirectional edge driven by the reverse declaration. ConfigLoader.isRelationAllowed() derives the reverse direction, so the reverse type is allowed without a second relations entry.

AntoraTraceabilityExtension

ARC-016 — Event-driven Antora integration

The extension hooks into Antora’s event pipeline via register(): contentClassified triggers parsing, macro expansion, and matrix registration in the content catalog; sitePublished writes the standalone traceability output directory. Each event handler is self-contained, does not block the build pipeline, and receives the content catalog as a parameter. The same RequirementsTraceabilityExtension class is used by the CLI outside Antora, demonstrating the event handlers are optional.

ARC-024 — traceability:links[] and traceability:incoming[] link rendering

The expandRelationMacros() method in the Antora extension (not the DocumentParser) replaces traceability:links[] and traceability:incoming[] placeholders with formatted AsciiDoc content showing the item’s relationships. All three rendering macros (outgoing incoming, and links) share this single scan/render pipeline; only the relationship direction(s) they emit differ. The DocumentParser’s inline macro regex excludes these via a traceability: namespace guard; they are NOT parsed as relationships. Rendering is opt-in via the :traceability-links: document attribute. Display style (list, table inline), sort order (target ID, title, relation type), and collapsible sections are configurable via document attributes. Inline relationship macros (``) are always stripped from output — they are pure data markers; the link macros are the sole rendering mechanism.

ARC-037 — Display labels with humanize default

Relation types are displayed through a labels map in the config, mapping a type to a human-readable name. The map is display-only and never affects graph structure, merge, or validation. When a type has no labels entry, humanize(type) renders it: underscores become spaces, sentence-cased (is_derived_from → "Is derived from"). Incoming links use the reverse type’s label.

ARC-025 — traceability:graph[], traceability:graph-coverage[], and traceability:config-graph[] visualization macros

The expandGraphMacros(), expandCoverageMacros(), and expandConfigGraphMacros() methods replace traceability:graph[], traceability:graph-coverage[], and ` image::https://kroki.io/graphviz/svg/eJyFkUFLwzAYhu_7FaFeK0yY4pAKdTt48CTehpQsedt-mCU1X7Y5xf8uW0uHpe1yTPI8-fK-mgovq1K8eakg12QoHBbO5lSIn4kQXtoPTT55eX2YCGGdhlhxKSska_cVCw4HgyTybms1dJyTMdBRLHJng5UbJNEzzA6BlIzejwboAmLVd1xDTN9Ibqanu5HH55Y8NrAhEqujXDnjfBJdzdL5dDlvmHpzX1JALIxcwyT_0FqmwVTYjud2-nR_txjxNFStCODuIMv5LJ2lI4ITU-NbRqYko6NIT2tE0XK1pvJOgTkbTueisU_RE_n1Y7eDNt2cLDgWGhWs5szZWChnc0MqcLanUHZSH1RJrT2YwT1AW1lbxvm1ZoQeqmmpYXbSkJYBOlsf_tU4ONEOnnJq1efWBgkDqTkLbqyg4eeaAfkS3U1DOcuk4duv_f4BgYdG4Q[Traceability configuration graph] ` placeholders with Kroki-rendered images. traceability:graph[] generates a GraphViz DOT relationship graph for the enclosing item (or a specified item ID), colored by role with labeled edges. traceability:graph-coverage[] generates a Vega-Lite bar chart showing per-item or global coverage. ` image::https://kroki.io/graphviz/svg/eJyFkUFLwzAYhu_7FaFeK0yY4pAKdTt48CTehpQsedt-mCU1X7Y5xf8uW0uHpe1yTPI8-fK-mgovq1K8eakg12QoHBbO5lSIn4kQXtoPTT55eX2YCGGdhlhxKSska_cVCw4HgyTybms1dJyTMdBRLHJng5UbJNEzzA6BlIzejwboAmLVd1xDTN9Ibqanu5HH55Y8NrAhEqujXDnjfBJdzdL5dDlvmHpzX1JALIxcwyT_0FqmwVTYjud2-nR_txjxNFStCODuIMv5LJ2lI4ITU-NbRqYko6NIT2tE0XK1pvJOgTkbTueisU_RE_n1Y7eDNt2cLDgWGhWs5szZWChnc0MqcLanUHZSH1RJrT2YwT1AW1lbxvm1ZoQeqmmpYXbSkJYBOlsf_tU4ONEOnnJq1efWBgkDqTkLbqyg4eeaAfkS3U1DOcuk4duv_f4BgYdG4Q[Traceability configuration graph] ` renders the traceability configuration as a GraphViz DOT diagram of roles and declared relations. Rendering is opt-in via the :traceability-graph: document attribute.

ARC-027 — Matrix registration in the content catalog

The registerMatricesInCatalog() method generates matrix files (HTML, CSV, JSON) per component version during the contentClassified event and registers them in the Antora content catalog as attachment-family files via contentCatalog.addFile(). Registration happens before document conversion, so attachment$traceability/…​ xrefs in nav files and pages resolve without manual file copies. Matrices are registered under every module that has AsciiDoc content; a committed copy, if present, has its contents refreshed in place.

DocumentParser

ARC-017 — Regex-based parsing

AsciiDoc content is parsed using regular expressions rather than an Asciidoctor AST walker. This makes the parser independent of Asciidoctor API versions and testable with plain string inputs. Verbatim blocks (---- and …​. fences) are pre-scanned and excluded to avoid false positives from example code. Known limitations (] in quoted attribute values, indented macros) are handled with targeted fixes (see ADR-002).

The parser’s internal pipeline has four distinct steps — each builds on the previous:

Diagram

MatrixGenerator

ARC-019 — Config-driven matrix generation with coverage calculations

The MatrixGenerator reads matrix definitions from configuration, queries the graph for items by role, and computes per-cell coverage based on coverageRelations. Each cell shows related item IDs or for no relationships. CSV output is direct; HTML output delegates to TemplateRenderer. Coverage is calculated as overall, complete, partial, and missing counts. The requirements-engineering preset defines pairwise matrices.

ARC-038 — Matrix status column

The HTML matrix renders a single per-row status column with values done, partial, and missing, derived from the row’s column coverage. The per-row coverage percentage is no longer rendered as a separate column. The top coverage summary (done/partial/missing counts plus overall percentage) is preserved. CSV output is unchanged.

TemplateRenderer

ARC-020 — Mustache template rendering for HTML output

The TemplateRenderer wraps Mustache for HTML matrix output. Built-in templates ship with the extension in src/templates/. Users can specify a custom template directory — the renderer falls back to built-in templates for any files not found in the custom directory.

LinkResolver

ARC-021 — Path resolution for matrix-to-item links

The LinkResolver generates relative link paths from matrix output to component root. It normalizes sourceFile values by stripping the pages/ prefix and .adoc extension, producing ../../page.html#ID links. When an item carries a module attribute, the module name is prepended to the path (../../requirements/index.html#ID) so links resolve across modules in a multi-module Antora site; the ROOT module is skipped because Antora serves it directly under the component version directory. With Antora’s default indexify URL style, pages at the module root produce pagename/index.html instead of pagename.html. The relativePathPrefix option allows configurable base paths, and the indexify option (default true) controls whether root-level pages use index.html. When no LinkResolver is provided, items render as plain text without links.

Neo4jExporter

ARC-022 — Graph-to-Neo4j CSV and Cypher export

The Neo4jExporter serializes the traceability graph into nodes.csv and relationships.csv with proper headers, comma delimiters, and escaped special characters — importable via Neo4j’s LOAD CSV. The import.cypher format produces CREATE statements with :Item and role-specific labels. All item attributes are included as node properties. The export is optional and requires no Neo4j runtime dependency.

CLI Tool

ARC-023 — Commander-based CLI with subcommands

The CLI is built on Commander with subcommands for processing (process), matrix generation (matrix) validation (validate), Neo4j export (export neo4j), statistics (stats), preset management (preset), and ID allocation (next-id). Global --config and --preset flags apply to all commands. The CLI operates standalone — it does not require an Antora build pipeline.

ARC-030 — CI/CD PDF build and deployment pipeline

The GitHub Actions CI workflow (.github/workflows/ci.yml and pages.yml) builds and deploys the example site to GitHub Pages. The pages workflow builds the Antora HTML site, generates PDFs and DOCX via @antora/pdf-extension with Ruby/asciidoctor-pdf and pandoc (provisioned by devbox.json and Gemfile), and deploys a combined output with the landing page at root and docs at /docs/. PDF and DOCX builds use assembler profiles with custom nav files (nav-requirements.adoc, nav-architecture.adoc, nav-test-plan.adoc) to produce separate documents per profile. The existing single-document config (antora-assembler-pdf.yml) is preserved as a standalone option.

ARC-031 — Static landing page at GitHub Pages root

A static landing/index.html page with Tailwind CSS loaded from CDN serves as the project landing page at the GitHub Pages root (/). The page uses conemso branding (teal palette #108193, Roboto font) and targets two personas: developers/architects (docs-as-code) and requirements engineers/PMs (traceability/compliance). The Antora documentation site remains at /docs/. The CI workflow copies landing/ assets to public/ during deployment. No build step, npm dependencies, or static site generator required.

ARC-032 — Lunr search extension customization for item anchor indexing

The @antora/lunr-extension is configured with a custom indexing pass that traverses non-heading elements with id attributes inside <article class="doc">. Each such element becomes a separate search chunk with its id as the hash, enabling search results to link directly to item anchors (e.g., #REQ-002). Child elements with CSS class title provide the chunk title; elements without a .title child fall back to the id. Heading elements are excluded from this pass since they are already indexed by the existing heading-based section chunking.

ARC-033 — Example site: config extension and dashboard patterns

The example site demonstrates two architectural patterns. First, config extension: examples/traceability.yml extends the requirements-engineering preset with a custom use_case role and leads_to relation, showing how projects add domain-specific roles without modifying built-in presets. Second, the dashboard page (dashboard.adoc) embeds traceability:graph[] and traceability:graph-coverage[] macros to provide visual coverage at a glance, gated by the :traceability-graph: document attribute. Both patterns serve as reference implementations for users extending the extension to their own domains.

ARC-034 — Vale prose-lint extension

The bundled antora-vale-extension runs the Vale prose linter during contentClassified. It streams each page and partial buffer to vale --path=<source> via stdin and parses the JSON output, reporting findings with source file and line references. A configurable minLevel gates the build: findings at or above it fail the build, findings below are logged. The extension is opt-in and requires the vale and asciidoctor executables on PATH, failing with an actionable message when either is missing. A starter Vale style under src/vale/ encodes the machine-checkable parts of the documentation style guide.

Runtime View

ARC-003 — Processing flow during Antora build

The following sequence shows how an AsciiDoc file flows through the system during an Antora build:

Diagram
Participant Role in processing

Antora Build

Triggers contentClassified (document processing and matrix registration) and sitePublished (standalone output directory). Provides the content catalog.

AntoraTraceabilityExtension

Event handler registration. Orchestrates the processing passes and matrix registration in the content catalog. Reads playbook config to determine preset, config path, output directory.

RequirementsTraceabilityExtension

Core API. Owns the graph, delegates to parser and generators. Same class used by CLI without Antora events.

DocumentParser

Regex-based extraction of [item] blocks and inline relationship macros from raw AsciiDoc content. Returns ParserResult { items, relationships }.

TraceabilityGraph

In-memory graph. Validates relationships against config rules (isRelationAllowed). Provides query methods used by macro expansion and matrix generation.

MatrixGenerator

Queries graph for items by role, computes per-cell coverage using coverageRelations, delegates HTML rendering to TemplateRenderer.

TemplateRenderer

Mustache-based HTML rendering. Built-in templates with custom template directory support.

Neo4jExporter

Optional export. Serializes graph to CSV (nodes.csv, relationships.csv) or Cypher (import.cypher) via LOAD CSV or CREATE statements.

Pass ordering within contentClassified

The extension runs four passes inside contentClassified. The sequence diagram above shows the Antora-level flow; this diagram focuses on the internal pass structure.

Diagram
Pass What happens Scope Why ordering matters

Pass 1

processAsciiDocFile() — DocumentParser extracts items + relationships, populates TraceabilityGraph. Both pages and partials are processed.

Pages + partials

Graph must be complete before Pass 3 can resolve cross-file `` references. Both pages and partials are processed in all three passes — partial content is inlined into pages and reaches the browser.

Pass 2

expandRelationMacros(file, macroName) — replace traceability:outgoing[], traceability:incoming[], and traceability:links[] with formatted relationship lists (grouped by type, configurable style/order); macroName is outgoing, incoming, or links.

Pages + partials

Depends on complete graph from Pass 1 to enumerate relationships. Opt-in via :traceability-links: document attribute; forced on for partials.

Pass 2b

expandGraphMacros() + expandCoverageMacros() + expandConfigGraphMacros() — replace traceability:graph[], traceability:graph-coverage[], and ` image::https://kroki.io/graphviz/svg/eJyFkUFLwzAYhu_7FaFeK0yY4pAKdTt48CTehpQsedt-mCU1X7Y5xf8uW0uHpe1yTPI8-fK-mgovq1K8eakg12QoHBbO5lSIn4kQXtoPTT55eX2YCGGdhlhxKSska_cVCw4HgyTybms1dJyTMdBRLHJng5UbJNEzzA6BlIzejwboAmLVd1xDTN9Ibqanu5HH55Y8NrAhEqujXDnjfBJdzdL5dDlvmHpzX1JALIxcwyT_0FqmwVTYjud2-nR_txjxNFStCODuIMv5LJ2lI4ITU-NbRqYko6NIT2tE0XK1pvJOgTkbTueisU_RE_n1Y7eDNt2cLDgWGhWs5szZWChnc0MqcLanUHZSH1RJrT2YwT1AW1lbxvm1ZoQeqmmpYXbSkJYBOlsf_tU4ONEOnnJq1efWBgkDqTkLbqyg4eeaAfkS3U1DOcuk4duv_f4BgYdG4Q[Traceability configuration graph] ` with rendered diagram image references (DOT and Vega-Lite).

Pages + partials

Uses toDot() / toVegaLite() / toConfigDot(). Opt-in via :traceability-graph: document attribute. Independent of Pass 2.

Pass 3

substituteLinksInFile() — replace inline relationship macros with xref:page.html#ID (pages) or link:URL#ID (partials). Also runs injectTitleIds() and unindentItemMacros().

Pages + partials

Needs complete graph (all sourceFiles known) from Pass 1. This is why Pass 1 processes all files before any macro expansion.

Key timing characteristics:

  • Synchronous init: AntoraTraceabilityExtension constructor initializes the traceability graph synchronously (config/preset loading is a synchronous file read) before any event handler is registered. This eliminates the race where a contentClassified event could fire before the graph is ready.

  • Single-pass processing: process() call does both parsing and graph population in one call. No separate relationship processing pass needed.

  • Lazy rendering: Templates are loaded and compiled on first use, cached thereafter.

File state caching

The prepareFile() method computes file content, document attributes, and item block positions once per file, then feeds the same PreparedFile to all five macro-expansion methods. This eliminates redundant buffer conversions and regex scans that were previously repeated in each method.

Diagram

Concepts — Cross-cutting Concerns

ARC-013 — ESM module system

ESM ("type": "module" in package.json) is used throughout the project. This choice enables modern import/export syntax, the node: protocol for built-in modules, and alignment with browser JavaScript. The tradeoff is that Antora’s extension loader uses require(), requiring .cjs wrappers for integration points (see ADR-001).

ARC-014 — Dependency injection by constructor

All modules accept their dependencies via constructor parameters. There is no service locator, DI container, or global state. TraceabilityGraph receives ConfigLoader, MatrixGenerator receives TraceabilityGraph and ConfigLoader and DocumentParser receives ConfigLoader optionally. This makes every component independently testable without mocking frameworks (see ADR-004).

Architecture Decisions

ARC-004 — Key architectural decisions and their rationale

This section captures the design decisions from openspec/changes/unified-item-architecture/design.md.

Decision Rationale Alternatives Considered

Single [item] macro with role attribute

Simplifies mental model, enables user-defined roles without code changes, reduces parser duplication

Keep separate macros (two ways to do things), role as positional parameter (less readable)

Configuration-based roles and relations (YAML)

Separates concerns, enables validation, allows presets, version-controllable

Hardcoded roles (inflexible), JSON (less readable), JS config (more complex)

Role-based relation validation at processing time

Catches errors early, prevents invalid graphs, provides clear messages

No validation (allows invalid graphs), warning only (users miss issues), runtime validation (too late)

Mustache templates for HTML

Separates presentation from logic, enables user customization, already working from prior work

Embedded HTML generation (code duplication), other template engines (Mustache was already in place)

Neo4j export (not custom query engine)

Mature graph query capabilities, Cypher is industry standard, no need to build query language

Custom DSL (high effort), GraphQL (not graph-optimized), Gremlin (less popular)

Built-in presets for common domains

Gives starting points, reduces config burden, demonstrates best practices

No presets (must define from scratch), default roles (conflicts with flexibility goal)

No default roles

Forces explicit configuration, prevents domain model assumptions

Default roles (simpler but less flexible), fallback behavior (complex transition logic)

Unknown roles = warnings (not errors)

Graceful degradation, partial config allowed, incremental role adoption

Error on unknown (too strict), silent ignore (users miss config issues)

Manual regex parsing over Asciidoctor.js extensions

Simpler, version-independent, easier to test, works consistently across Asciidoctor.js versions

Asciidoctor.js block processors (complex, version-dependent API)

Config-driven matrix generation

Users define which roles go on rows and columns, which relations count as coverage

Hardcoded matrix types (req-impl, req-test, full) — inflexible for domain models with different roles

In-memory source substitution for clickable links

After processing, replace `` with Asciidoctor xrefs in the content catalog buffer. Two-pass approach ensures cross-file targets are resolved. No disk writes.

HTML post-processing (PDF-incompatible), Asciidoctor inline macro extension (API dependency)

Dedicated LinkResolver component

Separates path resolution from matrix generation, makes link generation configurable and testable in isolation. Robust itemToHtmlPath strips pages/ prefix and .adoc extension from any sourceFile format.

Link generation in MatrixGenerator (mixes concerns), template-level logic (Mustache cannot do complex path manipulation)