Developer Guide

Overview

This guide covers the internal architecture, APIs, and contribution workflow for the Antora Requirements Traceability Extension. It assumes familiarity with TypeScript, Antora’s extension system, and the traceability concepts described in the User Guide.

Architecture

Diagram

The extension sits inside Antora’s event pipeline. AntoraTraceabilityExtension registers for contentClassified (process items from AsciiDoc) and sitePublished (generate matrices and coverage). It delegates to RequirementsTraceabilityExtension which orchestrates the core components. Configuration flows in through ConfigLoader — either from a built-in preset or a user-provided YAML file.

Key architectural decisions:

  • Manual regex parsing over Asciidoctor.js block processors — simpler, version-independent, easier to test

  • Config-driven roles and relations — the graph doesn’t know about "requirements" or "tests"; it knows about items with roles and validated relations

  • Fire-and-forget async init — the Antora extension constructor starts initialization asynchronously; event handlers are registered after config loads

  • Unknown roles = warnings — graceful degradation for items with roles not yet in config

Project Structure

antora-asciidoc-tracing/
├── src/
│   ├── index.ts                     # RequirementsTraceabilityExtension (orchestrator)
│   ├── types.ts                     # Item, ItemRelationship interfaces
│   ├── TraceabilityGraph.ts         # In-memory graph with query + validation
│   ├── DocumentParser.ts            # Regex-based AsciiDoc parser
│   ├── MatrixGenerator.ts           # Config-driven matrix generation
│   ├── Neo4jExporter.ts             # Neo4j CSV + Cypher export
│   ├── TemplateRenderer.ts          # Mustache template loading + rendering
│   ├── antora-extension.ts          # Antora extension entry point
│   ├── cli.ts                       # Commander CLI entry point
│   ├── config/
│   │   └── TraceabilityConfig.ts    # ConfigLoader, types, presets
│   ├── presets/
│   │   ├── requirements-engineering.yml
│   │   ├── agile.yml
│   │   ├── medical-iec62304.yml
│   │   └── minimal.yml
│   └── templates/
│       ├── matrix.html.mustache
│       └── partials/
├── test/
│   ├── cli.test.ts
│   ├── antora-extension.test.ts
│   ├── graph-and-api.test.ts
│   └── ...
├── docs/
│   ├── user-guide.adoc
│   ├── developer-guide.adoc
│   └── adr/
├── scripts/
│   └── build.js                    # Post-compile copy script
├── package.json
├── tsconfig.json
├── tsconfig.test.json
└── README.adoc

Two TypeScript configurations:

  • tsconfig.json — production build: src/lib/

  • tsconfig.test.json — test build: src/ + test/lib/src/ + lib/test/

The build script (scripts/build.js) copies templates, presets, and compiled files to lib/ for npm distribution.

Core Components

RequirementsTraceabilityExtension

src/index.ts — the orchestrator. Owns the TraceabilityGraph, delegates processing to DocumentParser, generation to MatrixGenerator, and export to Neo4jExporter.

export class RequirementsTraceabilityExtension {
  readonly graph: TraceabilityGraph;
  configLoader?: ConfigLoader;
  currentFile: string | null;
}

// Factory methods
RequirementsTraceabilityExtension.createWithConfig(configPath?: string): Promise<RequirementsTraceabilityExtension>;
RequirementsTraceabilityExtension.createWithPreset(presetName: string): Promise<RequirementsTraceabilityExtension>;

Key methods:

Method Description

process(content, options)

Parse AsciiDoc content, register items and relationships in the graph, return ParserResult

processFiles(files)

Process multiple files, return per-file results and aggregate

getAllItems()

All items in the graph

getAllRelationships()

All relationships in the graph

getItemsByRole(role)

Filter items by role

getRelationships(id, type?)

Relationships from an item, optionally filtered by type

getRelatedItems(id, type?)

Items targeted by relationships from an item

getRoleStatistics()

Item counts per role

validate()

Run graph validation (orphaned references, missing targets)

getCoverageReport()

Per-role item counts and matrix-specific coverage

getConfigErrors()

Configuration validation errors

createNeo4jExporter()

Create a Neo4jExporter for the current graph

exportToNeo4jCSV(options)

Export directly to CSV

getMatrixDefinitions()

Matrix definitions from loaded config

isKnownRole(role)

Check if a role is in the loaded config

isRelationAllowed(src, tgt, type)

Check if a relation is allowed by config

getAllowedRelations(src, tgt)

Get allowed relation types between roles

listPresets()

List all built-in presets

getPreset(name)

Get a specific preset

clear()

Clear graph and reset state

resetWithConfig(configLoader)

Set a new ConfigLoader and update graph

Delegate methods on the extension (forward to graph):

  • findPath(from, to, maxDepth?) — BFS path between items

  • getImpactAnalysis(id) — all reachable items in both directions

  • getRelationshipsByRoles(src, tgt) — relationships between two roles

TraceabilityGraph

src/TraceabilityGraph.ts — in-memory directed graph. Items stored in a Map<string, Item> keyed by id. Relationships stored in a Map<string, ItemRelationship> keyed by composite key fromId-type-targetId.

export class TraceabilityGraph {
  // Node management
  addItem(item: Item): void;
  getItem(id: string): Item | undefined;
  getAllItems(): Item[];
  getItemsByRole(role: string): Item[];
  getAllRoles(): string[];

  // Relationship management
  addRelationship(relationship: ItemRelationship): void;
  getRelationship(id: string): ItemRelationship | undefined;
  getAllRelationships(): ItemRelationship[];
  getRelationships(fromId: string, type?: string): ItemRelationship[];
  getReverseRelationships(targetId: string, type?: string): ItemRelationship[];

  // Query
  getRelatedItems(itemId: string, relationType?: string): Item[];
  getItemsWithRelationTo(itemId: string, relationType?: string): Item[];
  getRelationshipsByRoles(sourceRole: string, targetRole: string): ItemRelationship[];
  getRoleStatistics(): Record<string, number>;

  // Graph algorithms
  findPath(fromId: string, toId: string, maxDepth?: number): string[] | null;
  getImpactAnalysis(itemId: string): string[];

  // Validation
  validate(): ValidationResult;  // { errors: string[], warnings: string[] }

  // Lifecycle
  clear(): void;
  merge(other: TraceabilityGraph): void;
  setConfigLoader(configLoader: ConfigLoader): void;
}

Internally uses private maps: _items, _relationships. Maintains indexes for fast role-based and type-based queries. The addRelationship method validates: . Source item exists . Target item exists . If ConfigLoader is set, relation is allowed between the two item roles

Invalid relations are stored anyway (graceful degradation) with a warning logged.

DocumentParser

src/DocumentParser.ts — regex-based AsciiDoc parser. Two-pass approach:

  1. First pass — find all [item, …​] block macros and extract id, role, title, status, attributes

  2. Second pass — scan item content for inline relationship macros: <type>:<TARGET-ID>[]

export interface ParserResult {
  items: Item[];
  relationships: ItemRelationship[];
  warnings: ParserWarning[];
  errors: ParserError[];
}

Parsing supports: - Block attributes on [item] macros - Multiple inline relationships per item - Old macro syntax detection ([req], [imp], etc.) with deprecation warnings - Malformed item handling with auto-generated ids - Source file and line tracking

Inline macro regex: /<relation-type>:<TARGET-ID>\[\]/ — relation types are user-defined, so the parser accepts any valid identifier before the colon. Inline macros are parsed during Pass 1 and stored in the graph. They are always stripped from rendered output in Pass 3 — traceability:outgoing[] and traceability:incoming[] are the sole rendering mechanisms.

MatrixGenerator

src/MatrixGenerator.ts — generates matrices from the graph using matrix definitions from configuration.

export class MatrixGenerator {
  constructor(graph: TraceabilityGraph, configLoader?: ConfigLoader);

  generateMatrix(name: string): GeneratedMatrix;
  generateDefaultMatrix(): GeneratedMatrix;
  generateCoverageReport(): { overall: number; complete: number; partial: number; missing: number; total: number };
}

export interface GeneratedMatrix {
  name: string;
  description?: string;
  rows: MatrixRow[];
  coverage: CoverageStats;
  generatedAt: string;
}

Matrix generation is config-driven: for each row role’s items, it queries the graph for relationships to column-role items, filtering by coverageRelations. The MatrixGenerator delegates HTML rendering to TemplateRenderer. CSV output is generated directly.

Neo4jExporter

src/Neo4jExporter.ts — exports the graph to Neo4j-compatible formats.

export class Neo4jExporter {
  constructor(graph: TraceabilityGraph);

  export(options: Neo4jExportOptions): Neo4jExportResult;
}

export interface Neo4jExportOptions {
  outputDir: string;
  format: 'csv' | 'cypher';
  includeContent?: boolean;       // default: true
  includeAllAttributes?: boolean;  // default: true
}

export interface Neo4jExportResult {
  format: 'csv' | 'cypher';
  nodeCount: number;
  relationshipCount: number;
  files: string[];
}

CSV export produces nodes.csv and relationships.csv. Cypher export produces a single import.cypher file. Special characters in content are properly escaped for both formats.

TemplateRenderer

src/TemplateRenderer.ts — Mustache template engine wrapper.

export class TemplateRenderer {
  constructor(templateDir?: string);

  render(templateName: string, data: any): string;
  renderMatrix(matrix: GeneratedMatrix, template?: string): string;
}

Loads templates from src/templates/ by default. Falls back to built-in templates when custom template directory is provided but specific files are missing. Uses mustache as the underlying renderer.

Configuration System

ConfigLoader

src/config/TraceabilityConfig.ts — loads, validates, and exposes configuration.

export class ConfigLoader {
  load(configPath?: string): CompleteConfig;
  loadPreset(presetName: string): Preset;
  getConfig(): CompleteConfig;
  reload(): CompleteConfig;
  listPresets(): { name: string; description: string; version: string }[];

  // Validation
  isKnownRole(role: string): boolean;
  isRelationAllowed(sourceRole, targetRole, relationType): boolean;
  getAllowedRelations(sourceRole, targetRole): string[];
  getMatrices(): MatrixDefinition[];
  getMatrix(name: string): MatrixDefinition | undefined;
}

Configuration loading flow:

  1. Load YAML file or use preset

  2. Normalize (roles to lowercase, initialize empty relations/matrices)

  3. If extends field present, merge with preset

  4. Validate final config (roles are non-empty strings, relations reference valid roles, matrices have valid row/column roles)

  5. Cache and expose

Presets are loaded from src/presets/*.yml (at build time, copied to lib/presets/). The loadPreset method searches built-in paths first, then common cwd locations for custom presets.

Preset System

Built-in presets are YAML files shipped with the package. Each preset defines:

name: requirements-engineering
description: "..."
version: 1.0.0
compatibility:
  minExtensionVersion: 0.7.0
traceability:
  roles: [...]
  relations: {...}
  matrices: [...]
neo4j:
  queries: [...]
documentation:
  description: "..."

The minExtensionVersion field in compatibility is reserved for future version checks. Currently informational.

Relation Validation

The TraceabilityGraph.addRelationship() method calls ConfigLoader.isRelationAllowed():

  1. Lowercase both roles and relation type

  2. Check both roles are in the config’s roles list

  3. Look up config.relations[sourceRole][targetRole]

  4. Check the relation type is in the allowed list

  5. Return true/false

If ConfigLoader is not set (no config loaded), all relations are allowed (unguarded mode). Unknown roles generate warnings; disallowed relations generate errors.

Antora Integration

AntoraTraceabilityExtension

src/antora-extension.ts — the Antora extension entry point. Implements the Antora extension interface:

export class AntoraTraceabilityExtension {
  static register(context: AntoraExtensionContext): void;

  getTraceabilityExtension(): RequirementsTraceabilityExtension;
}

Registration flow:

  1. register() creates an instance and stores it in a global map keyed by playbook

  2. Constructor calls initializeAsync() as fire-and-forget

  3. initializeAsync() loads config (from playbook’s extension config), creates RequirementsTraceabilityExtension, and registers event handlers

  4. Event handlers registered: contentClassified (process items), sitePublished (generate matrices)

Playbook config resolution:

The extension reads its config from the Antora playbook’s extensions array, matching by name antora-requirements-traceability. Config keys: enabled, preset, configPath, outputDir, generateMatrices, matrixFormats, includeInNavigation.

Lifecycle:

Diagram

Since initializeAsync is fire-and-forget, event handlers may not be registered for the first microtask. In tests and Antora, the build pipeline provides enough latency that handlers are always registered before events fire.

Access Pattern

The getTraceabilityExtension() method retrieves the underlying extension from the global map. Throws if called before initialization completes. This is used after events fire to verify data was populated.

Data Model

Item

src/types.ts:

export interface Item {
  id: string;                        // Unique identifier
  title: string;                     // Display title
  content?: string;                  // Raw AsciiDoc content
  role: string;                      // User-defined role
  status?: string;                   // Free-form status
  attributes: Record<string, string>; // Additional attributes
  sourceFile?: string;               // Source AsciiDoc file
  sourceLine?: number;               // Line number in source
}

ItemRelationship

export interface ItemRelationship {
  id: string;                        // Composite: "fromId-type-targetId"
  fromId: string;                    // Source item id
  targetId: string;                  // Target item id
  type: string;                      // User-defined relation type
  sourceFile?: string;
  line?: number;
  autoGenerated?: boolean;
  inverseOf?: string;
}

Configuration Types

src/config/TraceabilityConfig.ts:

export interface TraceabilityConfig {
  roles: string[];
  relations: Record<string, Record<string, string[]>>;
  matrices: MatrixDefinition[];
  extends?: string;                  // Preset name to extend
}

export interface MatrixDefinition {
  name: string;
  description?: string;
  rows: string;
  columns: string[];
  coverageRelations: Record<string, string[]>;
}

export interface Preset extends PresetMetadata {
  traceability: TraceabilityConfig;
  neo4j?: { queries: Neo4jQuery[] };
  documentation?: { description: string };
}

CLI Architecture

src/cli.ts — Commander-based CLI entry point.

npx antora-req-trace <command> [options]

Commands:

Command

Purpose

Key options

process

Process AsciiDoc files

-i <path>, -o <path>, -f <fmt>, --preset, --config

matrix

Generate matrices

-i <path>, -t <name>, -f <fmt>, -o <path>

validate

Validate traceability

-i <path>, --preset, --config

export neo4j

Neo4j CSV/Cypher export

-i <path>, --format, -o <path>

stats

Graph statistics

-i <path>, --preset, --config

preset <sub>

Preset management

list, show <name>, init -n <name> -o <path>

Global options (--config, --preset) are inherited by all commands. Each command creates an extension with the appropriate configuration, processes input if specified, then performs its action.

Contributing

Setup

git clone <repo-url>
cd antora-asciidoc-tracing
npm install
npm run build
npm test

Development Workflow

npm run build          # Compile TypeScript
npm test               # Run all tests (compiles test files first)
npm run test:coverage  # Run tests with c8 coverage
npm run lint           # Run biome checks
npm run format         # Auto-format with biome

The project uses pre-commit hooks (biome lint + format) via the pre-commit framework.

Testing

Tests use Mocha + Chai, written in TypeScript, compiled with tsconfig.test.json to lib/test/. The test suite covers:

Area

Test file

DocumentParser

Parsing [item] macros, inline relationships, edge cases

TraceabilityGraph

Item/relationship CRUD, queries, path finding, validation

MatrixGenerator

Matrix generation, coverage calculation, CSV/HTML export

Neo4jExporter

CSV export, Cypher export, escaping, options

ConfigLoader

YAML loading, preset loading, validation, merge

Antora Extension

Event handling, content processing, matrix generation, error paths

CLI

Process, matrix, validate, export, stats, preset commands

Graph Queries

merge, findPath, getImpactAnalysis, getRelatedItems, etc.

API Methods

All RequirementsTraceabilityExtension public methods

Writing tests:

import { expect } from 'chai';
import { RequirementsTraceabilityExtension } from '../src/index.js';

describe('MyFeature', () => {
  it('should do something', () => {
    const extension = new RequirementsTraceabilityExtension();
    extension.process(`...[#X, item, role=requirement]...`, { sourceFile: 'test.adoc' });
    expect(extension.getAllItems()).to.have.lengthOf(1);
  });
});

Run individual test files:

npx mocha 'lib/test/graph-and-api.test.js'

Code Style

  • TypeScript strict mode

  • ESM imports with .js extensions in import paths

  • JSDoc comments on public methods

  • No default exports (named exports only)

  • Biome for linting and formatting

Commit Conventions

feat: add new feature
fix: fix a bug
docs: update documentation
test: add or update tests
refactor: restructure code
chore: maintenance tasks

Build & Release

Build

npm run build

The build pipeline:

  1. tsc — compiles src/lib/ (tsconfig.json)

  2. scripts/build.js — copies compiled files to lib/src/, copies templates to lib/templates/ and lib/src/templates/, copies presets to lib/presets/ and lib/src/presets/

The dual lib/ and lib/src/ structure exists for compatibility: lib/ is the npm entry point, lib/src/ is referenced during development and testing.

Package Exports

{
  "exports": {
    ".": "./lib/index.js",
    "./antora-extension": "./lib/src/antora-extension.js",
    "./package.json": "./package.json"
  },
  "bin": {
    "antora-req-trace": "./lib/cli.js"
  }
}

Publishing

npm version 0.7.0    # Bump version
npm run build         # Fresh build
npm test              # Verify all 181 tests pass
npm publish           # Publish to npm

Version Compatibility

Node.js

14+ (recommended: 18+)

TypeScript

5.x (strict mode)

Antora

3.x

Module system

ESM only