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
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 |
|---|---|
|
Parse AsciiDoc content, register items and relationships in the graph, return |
|
Process multiple files, return per-file results and aggregate |
|
All items in the graph |
|
All relationships in the graph |
|
Filter items by role |
|
Relationships from an item, optionally filtered by type |
|
Items targeted by relationships from an item |
|
Item counts per role |
|
Run graph validation (orphaned references, missing targets) |
|
Per-role item counts and matrix-specific coverage |
|
Configuration validation errors |
|
Create a Neo4jExporter for the current graph |
|
Export directly to CSV |
|
Matrix definitions from loaded config |
|
Check if a role is in the loaded config |
|
Check if a relation is allowed by config |
|
Get allowed relation types between roles |
|
List all built-in presets |
|
Get a specific preset |
|
Clear graph and reset state |
|
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:
-
First pass — find all
[item, …]block macros and extract id, role, title, status, attributes -
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:
-
Load YAML file or use preset
-
Normalize (roles to lowercase, initialize empty relations/matrices)
-
If
extendsfield present, merge with preset -
Validate final config (roles are non-empty strings, relations reference valid roles, matrices have valid row/column roles)
-
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():
-
Lowercase both roles and relation type
-
Check both roles are in the config’s
roleslist -
Look up
config.relations[sourceRole][targetRole] -
Check the relation type is in the allowed list
-
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:
-
register()creates an instance and stores it in a global map keyed by playbook -
Constructor calls
initializeAsync()as fire-and-forget -
initializeAsync()loads config (from playbook’s extension config), createsRequirementsTraceabilityExtension, and registers event handlers -
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:
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.
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 AsciiDoc files |
|
|
Generate matrices |
|
|
Validate traceability |
|
|
Neo4j CSV/Cypher export |
|
|
Graph statistics |
|
|
Preset management |
|
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
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 |
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'
Build & Release
Build
npm run build
The build pipeline:
-
tsc— compilessrc/→lib/(tsconfig.json) -
scripts/build.js— copies compiled files tolib/src/, copies templates tolib/templates/andlib/src/templates/, copies presets tolib/presets/andlib/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"
}
}