Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MarkSpec Model Reference

MarkSpec parses Markdown and source-file doc comments into a structured item graph — a directed graph of typed, identified items connected by typed trace relations. This reference book is the complete, standalone description of that model.

Two-layer architecture

The model is divided into two layers:

Two-layer architecture: Profile layer on top of Core layer

Core layer — built into the toolchain. Four abstract types and fifteen concrete types cover the vocabulary needed for any traceable documentation project. Core also defines the universal attributes (Id, Type, Labels, …), the shape-discrimination rule (ULID vs. URI), and the 8-step type-resolution chain. Core behavior is version-locked and cannot be overridden by profiles.

Profile layer — domain-specific extensions declared in a markspec.yaml manifest. A profile can add subtypes (e.g., requirement extends Requirement with a display-ID pattern), domain attributes (e.g., ASIL, Priority), domain relations (e.g., Mitigated-by), label concerns, and document conventions. A project activates profiles via .markspec.yaml. When no profile is configured the toolchain runs in core-only mode.

Key concepts

Entry — the atomic unit of the model. Every entry has:

  • A display ID — human-readable, e.g., SRS_BRK_0107
  • A title — the first line of the Markdown list item
  • A body — prose paragraphs describing the item
  • A trailer block — indented key-value attributes (Id:, Type:, Labels:, …)

Shape — determined solely by the Id: value. Exactly two shapes exist:

  • AuthoredId: is a bare ULID (01HGW2Q8MNP3RSTVWXYZABCDEF). The item originated in this project; ULIDs are assigned by markspec format.
  • ReferenceId: is a URI with scheme (urn:, doi:, pkg:, https:). The item is an external standard, package, or open-source dependency.

Type — resolved from the Type: trailer attribute via an 8-step chain. Profiles declare concrete subtypes that extends: a core type. The final fallback is Item. Type and shape are orthogonal — any type can be either shape.

Relation — a typed, directed edge in the trace graph written as a trailer attribute (Satisfies: SYS_BRK_0042). MarkSpec generates inverse edges automatically in the compiled output (Satisfied-by). The author only writes the forward edge.

Reading guide

ChapterWhat it covers
Entry formatSyntax and anatomy of entry blocks; the two shapes; in-source entries
Type taxonomyThe 4 abstract and 15 concrete types; the type-resolution chain; profile subtypes
AttributesUniversal attributes; per-type typical attributes; multi-value rules
Trace relationsCore relations; generated inverses; the trace graph
Profiles and extensionsProfile manifests; the extends chain; core-only mode; bundled profiles
Annex A — Compile outputThe /api/ directory layout; manifest, entry, and edge schemas; federation

Entry format

An entry is the atomic unit of the MarkSpec model. It is a Markdown list item that follows a specific structure: a bracketed display ID, a title, an optional body, and an indented trailer block of key-value attributes.

Anatomy of an entry

- [SRS_BRK_0107] Sensor debouncing
         ↑              ↑
   display ID         title

  The sensor driver shall debounce raw inputs
  to eliminate noise.

      Id: 01HGW2Q8MNP3RSTVWXYZABCDEF
      Type: requirement
      Derived-from: SYS_BRK_0042
      Labels: ASIL-B
  ↑
  trailer block (4+ space indent)

Title line

The title line is a Markdown list item (-) containing the display ID in square brackets, followed by a space, followed by the title text:

- [DISPLAY_ID] Title text

The [ and ] are required. The display ID is case-sensitive. The title text extends to the end of the line; it cannot span multiple lines.

Display ID

A display ID is a non-empty string of letters, digits, underscores, hyphens, dots, and slashes. It must start with an alphanumeric character. Display IDs are case-sensitive: SRS_BRK_0107 and srs_brk_0107 are different IDs.

Valid examples:

SRS_BRK_0107
STK-001
pkg/my-lib@1.2.0
ISO.26262.6

By convention, profile-declared types use PREFIX_NNNN patterns (e.g., SRS_BRK_0107), where the prefix identifies the type and the number provides ordering. The markspec next-id command computes the next available number for a given prefix.

Body text

The body is one or more prose paragraphs, indented at least 2 spaces relative to the list item marker:

- [SRS_BRK_0107] Sensor debouncing

  The sensor driver shall debounce raw inputs to eliminate noise. This prevents
  spurious activations during normal driving.

  A secondary paragraph can elaborate further.

      Id: 01HGW2Q8MNP3RSTVWXYZABCDEF

Style rules for body text:

  • Use _emphasis_ sparingly — it is not recommended inside entries; prefer **strong** for key terms or `code` for identifiers.
  • Modal verbs (shall, should, may, must not) follow RFC 2119 semantics when the @markspec/default profile is active.
  • The body is optional for Reference-shape entries.
  • Blank lines between the title line, body paragraphs, and the trailer block are required.

Trailer block

The trailer block is a set of key-value attribute lines, each indented at least 4 spaces (6 spaces is the canonical indent produced by markspec format):

Id: 01HGW2Q8MNP3RSTVWXYZABCDEF Type: requirement Derived-from: SYS_BRK_0042
Labels: ASIL-B Labels: safety-critical

Rules:

  • Keys use PascalCase or Title-Case-With-Hyphens; they are case-sensitive.
  • Each line is Key: value; there is exactly one space after the colon.
  • Multi-value attributes (Labels, Satisfies, …) use one line per value. CSV form (Labels: ASIL-B, safety-critical) is accepted on input and normalized to one-per-line by markspec format.
  • The Id: attribute is required for all entries (a missing Id: is a warning; the entry receives no ULID until markspec format is run).
  • Attribute order within the trailer is not significant for semantics, but markspec format normalizes it: Id first, then Type, then relations, then Labels, then any remaining attributes.

The two shapes

The Id: value alone determines an entry’s shape. There is no other indicator — the shape is mechanically derived from whether the value is a bare ULID or a URI with a scheme prefix.

Authored shape

The Id: value is a 26-character uppercase base32 ULID:

- [SRS_BRK_0107] Sensor debouncing

  The sensor driver shall debounce raw inputs.

      Id: 01HGW2Q8MNP3RSTVWXYZABCDEF
      Type: requirement
      Labels: ASIL-B

ULIDs are time-ordered unique identifiers. They are assigned by markspec format on the first format run after an entry is created. Until then the entry is in an “unstamped” state; the validator emits MSL-A010 for unstamped entries in strict mode.

Authored entries represent items created in this project. They are expected to evolve over time (body edits, new attributes) while keeping the same ULID. The ULID is the stable identifier; the display ID is the human-readable alias.

Reference shape

The Id: value is a URI with a recognised scheme (urn:, doi:, pkg:, https:):

- [@ISO-26262-6] ISO 26262 Part 6

  Road vehicles — Functional safety — Part 6: Product development at the
  software level.

      Id: urn:iso:std:iso:26262:-6:ed-2
      Reference-document: ISO 26262-6:2018
      Reference-url: https://www.iso.org/standard/68388.html

The @ prefix convention: Reference entries often use [@slug] syntax where the @ signals to the author that this is a reference, not an authored item. The @ is stripped from the display ID in the model — the actual display ID stored is ISO-26262-6, not @ISO-26262-6. This is a syntactic convenience only.

Reference entries represent external standards, packages, or resources. Their Id: is a stable external identifier, not a MarkSpec-assigned ULID. The body is optional — it may contain a human-readable summary of the referenced document.

Accepted URI schemes for Reference shape:

SchemeExampleTypical use
urn:urn:iso:std:iso:26262:-6:ed-2ISO standards, ISO URNs
doi:doi:10.1109/IEEESTD.2018.8299595Academic papers, IEEE standards
pkg:pkg:cargo/serde@1.0.197Open-source packages (purl format)
https:https://www.rfc-editor.org/rfc/rfc2119Web documents, RFCs

In-source entries

MarkSpec can extract entries from source-file doc comments. The entry format inside a doc comment is identical to the Markdown format, embedded within the comment syntax of the host language.

Kotlin / Java / C example:

/**
 * [SWT_BRK_0030] Debounce test
 *
 * The debounce function shall reject inputs shorter than the
 * configured threshold.
 *
 *     Id: 01HGW3R9QNP4ABCDEFGHJKMNPQ
 *     Type: test
 *     Verifies: SRS_BRK_0107
 */
@Test
fun debounce() {
    // ...
}

Rust example:

#![allow(unused)]
fn main() {
/// [SWT_BRK_0030] Debounce test
///
/// The debounce function shall reject inputs shorter than the
/// configured threshold.
///
///     Id: 01HGW3R9QNP4ABCDEFGHJKMNPQ
///     Type: test
///     Verifies: SRS_BRK_0107
#[test]
fn debounce() {
    // ...
}
}

Supported languages and comment styles:

LanguageComment style
Rust/// line comments
Kotlin/** */ block comments
Java/** */ block comments
C / C++/** */ block comments

The markspec validate and markspec compile commands accept source files alongside Markdown files. The source.* properties namespace in the compile output records the source language and enclosing function name for in-source entries.

Blank line requirements

Blank lines are significant:

- [SRS_BRK_0107] Sensor debouncing ← blank line required Body paragraph one. ←
  blank line between paragraphs Body paragraph two (optional). ← blank line
  before trailer Id: 01HGW2Q8MNP3RSTVWXYZABCDEF Type: requirement

A title line immediately followed by a trailer (no body, no blank line) is accepted; the entry simply has an empty body:

- [SRS_BRK_0107] Sensor debouncing

      Id: 01HGW2Q8MNP3RSTVWXYZABCDEF
      Type: requirement

Note that even with no body, a blank line between the title and the trailer is required.

Type taxonomy

MarkSpec’s type system has two layers: a core layer built into the toolchain, and a profile layer that extends it with domain-specific subtypes. Core types are available in every project; profile subtypes require the declaring profile to be active.

Full type hierarchy

Item  (abstract — root)
├── Specification  (abstract)
│   ├── Requirement
│   ├── Test
│   ├── Contract
│   ├── Record
│   └── Risk
├── Component  (abstract)
│   ├── SoftwareComponent
│   ├── HardwareComponent
│   ├── SoftwareInterface
│   └── HardwareInterface
├── Unit  (abstract)
│   ├── SoftwareUnit
│   └── HardwareUnit
├── Definition
├── Objective
├── Standard
└── Change

Abstract types

Four abstract types structure the taxonomy. They cannot be used directly as Type: values — they exist as extension targets for the concrete types below them and for profile-declared subtypes.

Abstract typePurposeExtended by
ItemRoot; ultimate fallback for all type resolutionAll 15 concrete types; any profile subtype
SpecificationNormative statements; things that must or shallRequirement, Test, Contract, Record, Risk
ComponentSystem-level building blocks with identitySoftwareComponent, HardwareComponent, SoftwareInterface, HardwareInterface
UnitFine-grained implementation-level elementsSoftwareUnit, HardwareUnit

Concrete types — Specification subtypes

Requirement

Normative statement of what the system shall do. The primary entry type in most traceable documentation projects.

  • Typical relations: Satisfies (→ upstream Requirement or Objective), Derived-from (→ other Requirement). Verified by Test entries via Verified-by (generated inverse of Verifies).
  • Typical profile attributes: ASIL, Priority, Safety-goal
- [SRS_BRK_0107] Sensor debouncing

  The sensor driver shall debounce raw inputs to eliminate noise.

      Id: 01HGW2Q8MNP3RSTVWXYZABCDEF
      Type: requirement
      Satisfies: SYS_BRK_0042
      Labels: ASIL-B

Test

A test case, test procedure, or test result. Records what was checked and how.

  • Typical relations: Verifies (→ Requirement), Tests (→ SoftwareUnit or Component).
  • Typical profile attributes: Test-level, Test-type, Test-result
- [SWT_BRK_0030] Debounce unit test

  The debounce function shall reject inputs shorter than the configured
  threshold duration.

      Id: 01HGW3R9QNP4ABCDEFGHJKMNPQ
      Type: test
      Verifies: SRS_BRK_0107
      Labels: ASIL-B

Contract

An interface or API contract. Defines the boundary between two components or layers.

  • Typical relations: Satisfies (→ Requirement), Realized-by (generated inverse of Realizes from a Component).

Record

An immutable audit record, decision log, or meeting minute. Typically stands alone — not part of a Satisfies chain.

  • Records are append-only by convention. Do not edit the body of a published record; supersede it instead.

Risk

A hazard or FMEA entry. Profiles typically add safety-classification attributes.

  • Typical relations: Mitigated-by (generated inverse of Mitigates from a Requirement).
  • Typical profile attributes: ASIL, Severity, Probability
- [HAZ_BRK_0003] Loss of braking under wheel-lock condition

  If the braking actuator saturates, the wheel may lock, leading to loss of
  vehicle directional control.

      Id: 01HGW5F7GHJ8KLMNPQRSTVWXYZ
      Type: Risk
      Labels: ASIL-D

Concrete types — Component subtypes

SoftwareComponent

A software module, service, or subsystem.

  • Typical relations: Part-of (→ SoftwareComponent), Depends-on (→ SoftwareComponent), Allocates (generated inverse when a Requirement uses Allocated-to).

HardwareComponent

An ECU, sensor, actuator, or other physical hardware element.

  • Typical relations: Part-of (→ HardwareComponent), Realizes (→ Contract).
  • Typical profile attributes: Supplier, Part-number

SoftwareInterface

An API, IPC channel, or protocol definition — the contract between two software components.

  • Typical relations: Part-of (→ SoftwareComponent), Realized-by (generated inverse of Realizes).

HardwareInterface

A connector, bus, pin definition, or other physical interface.

  • Typical relations: Part-of (→ HardwareComponent).

Concrete types — Unit subtypes

SoftwareUnit

A function, class, or module — the implementation-level granularity unit. Often co-located with source code doc comments.

  • Typical relations: Part-of (→ SoftwareComponent), Realizes (→ Contract or Requirement).
/**
 * [BRK_DEB_001] Debounce unit
 *
 * The debounce unit filters out raw sensor pulses shorter than the
 * configured threshold.
 *
 *     Id: 01HGW6B3CDE7FGHJKMNPQRSTUV
 *     Type: SoftwareUnit
 *     Part-of: BRK_SW_001
 *     Realizes: SRS_BRK_0107
 */
class DebounceFilter(private val thresholdMs: Int) { ... }

HardwareUnit

A circuit, sub-assembly, or other fine-grained hardware element.

  • Typical relations: Part-of (→ HardwareComponent).

Concrete types — Item subtypes

Definition

A glossary term. Used in GLOSSARY.md or equivalent. A planned lint rule (MSL-Q020) will flag entries that use a term defined in the glossary without a References: link to the Definition entry.

Objective

A goal, OKR, or strategic objective. The upstream anchor for Requirement Satisfies chains. Objectives typically have no upstream targets — they are the roots of the traceability tree.

  • Typical profile attributes: Priority

Standard

A normative external document. Always Reference shape (Id: is a urn:, doi:, or https: URI). Profile adds Reference-url, Reference-document, and License attributes (via @markspec/default).

- [@ISO-26262-6] ISO 26262 Part 6

  Road vehicles — Functional safety — Part 6: Product development at the
  software level.

      Id: urn:iso:std:iso:26262:-6:ed-2
      Type: Standard
      Reference-document: ISO 26262-6:2018
      Reference-url: https://www.iso.org/standard/68388.html
      License: ISO-proprietary

Change

A change request, issue, or ticket. Linked to Requirement entries via Addresses / Addressed-by.

  • Typical profile attributes: Priority, Status

Type resolution chain

When MarkSpec resolves the type of an entry it walks 8 steps, first match wins:

1. Explicit Type: trailer value
   ↓ (absent or unrecognised)
2. Profile display-ID pattern match  (e.g. SRS_BRK_* → requirement)
   ↓ (no pattern matches)
3. Profile file-glob match  (e.g. tests/** → test)
   ↓ (no glob matches)
4. Profile document-level directive  (<!-- markspec:type requirement -->)
   ↓ (not set)
5. Core display-ID prefix heuristic
   ↓ (no match)
6. Core file-name heuristic  (GLOSSARY.md → Definition)
   ↓ (no match)
7. Inherited from parent entry  (nested list item)
   ↓ (no parent)
8. Fallback → Item

Steps 2–4 require a profile to be active. In core-only mode (no profile configured), only steps 1, 5, 6, 7, and 8 apply.

An explicit Type: value that names an unknown type raises MSL-T020. A profile can suppress this by declaring the type — the unknown-type check is relative to the active profile, not to the core type list.

Two entry shapes

The Id: trailer value determines an entry’s shape, independently of its type. Type and shape are fully orthogonal:

Id: 01HGW2Q8MNP3RSTVWXYZABCDEF     ← Authored (bare ULID)
Id: urn:iso:std:iso:26262:-6:ed-2  ← Reference (URI with scheme)

Shape affects serialization, which attributes are meaningful (e.g., Supersedes only makes sense for Authored entries), and how the entry is displayed in traceability reports.

Profile subtypes

Profiles declare additional concrete types via extends:. The new type inherits all rules and relations of its parent core type and can add domain-specific attributes and display-ID patterns:

profile:
  types:
    requirement:
      extends: Requirement
      display-id-pattern: "SRS_{n:4d}"
    hazard:
      extends: Risk
      display-id-pattern: "HAZ_{n:4d}"
    feature:
      extends: Requirement
      display-id-pattern: "FEAT_{n:4d}"

Naming convention: profile type names use lowercase-with-hyphens; core type names use PascalCase. This makes the origin unambiguous at a glance — requirement is a profile subtype of Requirement.

Profile subtypes appear in the type-resolution chain at step 1 (when used explicitly as the Type: value) and step 2 (when matched by their display-ID pattern). They cannot shadow core type names — attempting to declare a profile type named Requirement raises MSL-A040.

Attributes

Attributes are the key-value pairs that appear in an entry’s trailer block — the indented section below the body text. They carry the entry’s identity, type classification, trace links, and any domain-specific metadata declared by the active profile.

Universal attributes

These attributes apply to every entry regardless of type. They are defined by core and are always available, even in core-only mode (no profile configured).

AttributeRequiredCardinalityValue
IdYesSingleBare ULID (Authored shape) or URI with scheme (Reference shape)
TypeNoSingleCore or profile-declared type name
LabelsNoMultiFree-form tags; one Labels: line per tag
ReferencesNoMultiDisplay ID, optionally followed by a locator in […]
External-idNoSingleCross-system identifier (Jira, DOORS, …)
SupersedesNoSingleDisplay ID of the predecessor entry (Authored only)
Superseded-byGeneratedInverse of Supersedes; written by markspec format
DeprecatedNoSingleQuoted retirement reason string

Id

The Id: attribute is the primary stable identifier. Its format determines the entry’s shape:

Id: 01HGW2Q8MNP3RSTVWXYZABCDEF ← Authored (26-char ULID) Id:
urn:iso:std:iso:26262:-6:ed-2 ← Reference (URI with scheme)

ULIDs are assigned by markspec format. An entry without an Id: is “unstamped” — the validator emits MSL-A010 in strict mode, and markspec format will add one on the next run.

Type

The Type: attribute holds the entry’s type name. It may be a core concrete type (Requirement, Test, SoftwareComponent, …) or a profile-declared subtype name (requirement, hazard, feature, …).

Type: requirement

When absent, the toolchain resolves the type through the 8-step chain described in the Type taxonomy chapter. Unknown values raise MSL-T020.

Labels

Labels: is a multi-value attribute. Each value is a free-form tag. The preferred form is one line per tag; CSV is accepted and normalized by markspec format:

# One tag per line (preferred)

    Labels: ASIL-B
    Labels: safety-critical
    Labels: DRAFT

# CSV on a single line (accepted, normalized on format)

    Labels: ASIL-B, safety-critical, DRAFT

Labels are plain strings — they carry no semantics in core. Profiles can declare label concerns (a structured vocabulary of expected labels) and the validator then checks that label values are from the declared set.

References

References: links an entry to an external standard or resource by display ID, optionally followed by a locator in square brackets:

References: ISO-26262-6 [§4.3] References: RFC-2119 [§3] References: serde-1-0

The locator ([§4.3]) is free text — it is preserved verbatim in the compiled output but not parsed further.

External-id

External-id: records a cross-system identifier — the ID of a ticket in an issue tracker, a DOORS object ID, a SharePoint item ID, etc. This is a single-value attribute:

External-id: JIRA-4567 External-id: DOORS-MODULE-23/OBJ-0042

The value is opaque to MarkSpec. It is preserved in the compiled output and available for traceability matrix generation.

Supersedes and Superseded-by

Supersedes: records the display ID of a predecessor entry that this entry replaces. It is only meaningful for Authored entries (external URIs have their own versioning).

Supersedes: SRS_BRK_0042

When markspec format encounters a Supersedes: line, it writes the inverse Superseded-by: attribute on the target entry automatically. The author only writes Supersedes: — never Superseded-by:.

Deprecated

Deprecated: records a retirement reason. The value is a quoted string:

Deprecated: "Replaced by SRS_BRK_0107; no longer relevant after v2.0"

Deprecated entries are not removed — they remain in the compiled output, but the validator can be configured to warn or error on references to deprecated entries.

Multi-value attributes

Any attribute declared as cardinality: multi in the profile accepts one value per line. The two equivalent forms are:

# One value per line (preferred for readability)

    Labels: ASIL-B
    Labels: safety-critical

# CSV (accepted; normalized by markspec format)

    Labels: ASIL-B, safety-critical

markspec format always normalizes to the one-per-line form. If a CSV line contains a value that includes a comma (e.g., a citation locator like ISO-26262-6 [§4, §5]), the square-bracket content is treated as a single token and not split.

Authored-only attributes

Supersedes is only meaningful for Authored entries. Reference entries have stable external URIs as their Id: — versioning happens in the external system, not in MarkSpec.

Reference-only attributes

The following attributes are declared by the @markspec/default profile and apply specifically to Reference-shape entries:

AttributeValue
Reference-urlCanonical URL for the standard or package
Reference-documentHuman-readable citation string
LicenseSPDX identifier (e.g., Apache-2.0, ISO-proprietary)

Example:

- [@ISO-26262-6] ISO 26262 Part 6

      Id: urn:iso:std:iso:26262:-6:ed-2
      Reference-document: ISO 26262-6:2018
      Reference-url: https://www.iso.org/standard/68388.html
      License: ISO-proprietary

These attributes are not part of core — they require @markspec/default (or any profile that extends it) to be active. In core-only mode, using Reference-url raises MSL-A020 (unknown attribute key).

Typical attributes by concrete type

The tables below summarize typical attribute usage by type group. Attributes under “Typical profile attributes” are not defined by core — they require the relevant profile to be active. Only the universal attributes in the section above are available in core-only mode.

Specification types

TypeTypical profile attributesTypical relation attributes
RequirementASIL, Priority, Safety-goalSatisfies, Derived-from
TestTest-level, Test-type, Test-resultVerifies, Tests
Contract(none typical)Satisfies, Realized-by (generated)
Record(none typical)(standalone)
RiskASIL, Severity, ProbabilityMitigated-by (generated)

Component types

TypeTypical profile attributesTypical relation attributes
SoftwareComponentVersion, License, SupplierPart-of, Depends-on
HardwareComponentVersion, SupplierPart-of
SoftwareInterface(none typical)Part-of, Realized-by (generated)
HardwareInterface(none typical)Part-of

Unit types

TypeTypical profile attributesTypical relation attributes
SoftwareUnit(none typical)Part-of, Realizes
HardwareUnit(none typical)Part-of

Item types

TypeTypical profile attributesNotes
Definition(none typical)Used in glossary cross-check lint rule
ObjectivePriorityUpstream anchor for Requirement Satisfies chains
StandardReference-url, Reference-document, LicenseAlways Reference shape
ChangePriority, StatusLinks to Requirements via Addresses

Profile-declared attributes

A profile can declare additional attributes via its attributes: section:

profile:
  attributes:
    - key: ASIL
      applies-to: [requirement, hazard, test]
      cardinality: single
      values: [ASIL-A, ASIL-B, ASIL-C, ASIL-D, QM]

    - key: Priority
      applies-to: [requirement, change]
      cardinality: single
      values: [critical, high, medium, low]

Profile-declared attributes are validated just like core attributes: unknown values raise MSL-A022, wrong cardinality raises MSL-A013. The difference is that these rules only activate when the declaring profile is present.

Attribute ordering

markspec format normalizes the trailer attribute order:

  1. Id
  2. Type
  3. Relation attributes (Satisfies, Derived-from, Verifies, Tests, …)
  4. Labels
  5. References
  6. External-id
  7. Supersedes / Superseded-by
  8. Deprecated
  9. Any remaining profile-declared attributes (alphabetical)

Attribute order has no semantic significance — the normalized order is purely for readability and diff stability.

Trace relations

A relation is a typed, directed edge between two entries in the trace graph. Relations are written as trailer attributes in the source entry; the toolchain generates the inverse edge automatically in the compiled output.

Writing a relation

Relations look like any other trailer attribute — RelationName: TARGET_DISPLAY_ID. Multiple targets are written as one line per target (or CSV, normalized by markspec format):

- [SRS_BRK_0107] Sensor debouncing

  The sensor driver shall debounce raw inputs.

      Id: 01HGW2Q8MNP3RSTVWXYZABCDEF
      Type: requirement
      Satisfies: SYS_BRK_0042
      Derived-from: STK_BRK_0003
      Labels: ASIL-B

This declares two forward edges:

  • SRS_BRK_0107 --[Satisfies]--> SYS_BRK_0042
  • SRS_BRK_0107 --[Derived-from]--> STK_BRK_0003

Multiple targets for the same relation:

Satisfies: SYS_BRK_0042 Satisfies: SYS_BRK_0043

Generated inverses

When MarkSpec compiles the graph it writes the reverse edge for every forward relation. The author never writes the inverse — it appears only in the compiled output:

SRS_BRK_0107  --[Satisfies]-->    SYS_BRK_0042
SYS_BRK_0042  <--[Satisfied-by]-- SRS_BRK_0107   ← generated

In edges.ndjson (or in compiled.json), generated edges are marked with "generated": true. This flag allows consumers to distinguish authorial intent from toolchain bookkeeping.

Core relations

The following relations are declared by the @markspec/default profile and are available in any project that uses it. In core-only mode (no profile configured), these relation keys are treated as unknown attributes (MSL-A020).

RelationInverseTypical source typeTypical target type
SatisfiesSatisfied-byRequirementRequirement, Objective
Derived-fromDerived-byRequirementRequirement
VerifiesVerified-byTestRequirement, Contract
TestsTested-byTestSoftwareUnit, Component
Depends-onRequired-byComponent, UnitComponent
Part-ofHas-partComponent, UnitComponent
Allocated-toAllocatesRequirementComponent
RealizesRealized-byComponent, UnitContract, Requirement
Generated-from(none)anyany
AddressesAddressed-byRequirementChange

Note: Supersedes / Superseded-by are universal attributes (not relation attributes) — they are part of core and are available without any profile. See the Attributes chapter.

Cross-shape relations

Both Authored and Reference entries can appear as relation sources or targets. A requirement can cite a standard, and a component can depend on an open-source package:

# A requirement cites a normative section of an external standard

- [SRS_BRK_0107] Sensor debouncing

      Id: 01HGW2Q8MNP3RSTVWXYZABCDEF
      Type: requirement
      References: ISO-26262-6 [§4.3]

# A software component depends on an external package

- [BRK_SW_001] Braking software component

      Id: 01HGW4A5BCD6EFGHJKMNPQRSTUV
      Type: SoftwareComponent
      Depends-on: serde-1-0

Here ISO-26262-6 and serde-1-0 are the display IDs of Reference-shape entries elsewhere in the project (or in a federated upstream).

The trace graph

The following diagram illustrates a typical V-model traceability chain:

STK_BRK_0003          (Objective)
     |
     |  Satisfies ↑  Satisfied-by ↓ (generated)
     ▼
SYS_BRK_0042          (Requirement)
     |
     |  Satisfies ↑  Satisfied-by ↓ (generated)
     ▼
SRS_BRK_0107          (Requirement)  ──── Allocated-to ──► BRK_SW_001
     |                                                          (SoftwareComponent)
     |  Verified-by ↑  Verifies ↓                                   |
     ▼   (generated)                                                 |  Has-part ↓
SWT_BRK_0030          (Test)                               BRK_DEB_001
                                                                (SoftwareUnit)
                                                                     |
                                                                     └── Realizes ──► SRS_BRK_0107

Walking the graph upward (via Satisfied-by) traces requirements to their stakeholder objectives. Walking downward (via Verifies and Tests) traces requirements to their test coverage.

The markspec context command walks the Satisfies chain upward from any entry. The markspec dependents command lists all entries that reference a given entry.

Relation cardinality

Core relations are many-to-many by default. Profiles can restrict cardinality:

relations:
  - key: Superseded-by
    cardinality: single   # only one successor

If a single-cardinality relation appears more than once on the same entry, the validator raises MSL-A013.

Profile-declared relations

Profiles can add new relations beyond the core set:

profile:
  relations:
    - key: Mitigated-by
      inverse: Mitigates
      source-types: [hazard]
      target-types: [requirement]
      cardinality: many-to-many

    - key: Addresses
      inverse: Addressed-by
      source-types: [requirement]
      target-types: [change]

The source-types and target-types constraints are advisory — the validator warns (MSL-R010) when a relation is used between types that the profile did not intend. They are not hard errors because cross-type relations are sometimes legitimate in practice.

Reference-target locators

When a relation targets a Reference-shape entry, the target can include a locator in square brackets to point to a specific section or element:

References: ISO-26262-6 [§4.3] Satisfies: STK_SAFETY_0001
[acceptance-criterion-3]

Locators are free text — they are preserved verbatim in the compiled output but not parsed or validated further. Their meaning is determined by the consuming tool or reviewer.

Validation

The validator checks relations in two passes:

  1. Parse pass — each RelationName: VALUE line is parsed. Unknown relation keys raise MSL-A020 (unknown attribute) unless the key is declared by the active profile.

  2. Cross-file pass — after all files are parsed, unresolved references (display IDs that do not correspond to any entry in the project or federated upstreams) raise MSL-R001.

Circular relations (a Satisfies chain that returns to its source) are detected and reported as MSL-R020.

Profiles and extensions

A profile is a markspec.yaml manifest that extends the core type taxonomy with domain-specific vocabulary. Profiles are composable and chainable — a project selects a list of profiles that are merged into an effective profile that governs validation and tooling.

What a profile declares

ElementPurpose
SubtypesDomain-specific entry types with display-ID patterns (requirement extends Requirement)
AttributesExtra trailer keys beyond the core universal set (ASIL, Priority, License)
RelationsTrace edges with cardinality and inverse rules (Mitigated-by, Addresses)
Label concernsStructured label vocabulary (DRAFT, RELEASED, ASIL-B)
ConventionsDocument-level formatting rules (caption position, prose wrap)

Profile manifest structure

A profile manifest is a markspec.yaml file in a profile directory:

id: "@markspec/compliance-iso26262"
version: 1.0.0
description: "ISO 26262 compliance vocabulary"
extends: "@markspec/default"

profile:
  types:
    requirement:
      extends: Requirement
      display-id-pattern: "SRS_{n:4d}"
    hazard:
      extends: Risk
      display-id-pattern: "HAZ_{n:4d}"

  attributes:
    - key: ASIL
      applies-to: [requirement, hazard, test]
      cardinality: single
      values: [ASIL-A, ASIL-B, ASIL-C, ASIL-D, QM]

  relations:
    - key: Mitigated-by
      inverse: Mitigates
      source-types: [hazard]
      target-types: [requirement]

  labels:
    - name: functional-safety
    - name: DRAFT
    - name: RELEASED

Top-level manifest fields:

FieldRequiredNotes
idYesScoped identifier, e.g. @org/name or name
versionYesSemantic version string
descriptionNoHuman-readable summary (recommended for publishing)
extendsNoParent profile specifier (local path or scoped ID)
licenseNoSPDX identifier (recommended for publishing)

The extends chain

Profiles form an inheritance chain; each tier inherits all declarations from its parent and can add or override them (closest tier wins on conflicts):

@markspec/default
       ↓  extends
@markspec/compliance-iso26262
       ↓  extends
@myorg/safety-profile
       ↓  extends (project selects in .markspec.yaml)
project vocabulary

The effective profile is the merged result of all active tiers. The project’s .markspec.yaml activates profiles:

# .markspec.yaml  (at project root)
profiles:
  - "@markspec/default"
  - "@markspec/compliance-iso26262"
  - "./profiles/myorg"

Profiles are resolved in order; later entries in the list take precedence over earlier ones for conflicting declarations.

Effective profile

Run markspec profile show to inspect the active chain and its effective vocabulary:

Active profile: @markspec/compliance-iso26262@1.0.0

Entry types (2):
  - requirement: Stakeholder, system, or software requirement (SRS_{n:4d})
  - hazard: Hazard or FMEA entry (HAZ_{n:4d})

Attributes (1):
  - ASIL: ASIL classification (ASIL-A | ASIL-B | ASIL-C | ASIL-D | QM)

Relations (1):
  - Mitigated-by: inverse Mitigates; hazard → requirement

Use markspec profile describe type requirement for full detail on any profile element.

Core-only mode

A project with no .markspec.yaml (or an empty profiles list) runs in core-only mode:

  • Only the 4 abstract and 15 core concrete types are recognised.
  • Unknown Type: values produce MSL-T020.
  • Unknown trailer keys produce MSL-A020.
  • No display-ID patterns, no domain relations, no domain attributes.
  • The type-resolution chain uses only steps 1, 5, 6, 7, and 8 (profile steps 2–4 do not apply).

Core-only mode is useful for generic documentation where full traceability tooling is not needed.

What profiles cannot change

Profiles extend core — they cannot weaken or redefine it:

  • Cannot redefine Id, Type, or Title — these are reserved core attribute keys.
  • Cannot shadow any of the 15 core concrete type names (MSL-A040 is raised for reserved-name conflicts). A profile type named Requirement is forbidden; use requirement (lowercase) as a distinct subtype.
  • Cannot remove core-defined attributes or demote core errors to warnings.
  • Cannot alter shape discrimination — shape is determined by the Id: format alone; no profile rule can change it.
  • Cannot add cardinality constraints to universal attributes (Id, Type, Labels, References, …).

Bundled profiles

ProfilePurpose
@markspec/defaultRFC 2119 modal keywords, Reference-url/Reference-document/License attributes, core relations (Satisfies, Verifies, …), DRAFT/RELEASED labels
@markspec/compliance-iso26262ASIL labels, functional-safety relations, Part 6 reference entries
@markspec/compliance-aspiceASPICE process attributes, traceability relations

Profile commands

CommandPurpose
markspec profile showDisplay the active chain and effective vocabulary
markspec profile new <id>Scaffold a new profile directory with markspec.yaml
markspec profile add <spec>Add a profile specifier to .markspec.yaml
markspec profile describe <kind> <name>Show full detail for a profile element (type, attribute, relation, label, convention)
markspec profile publish [--dir <dir>]Validate a profile manifest for publishability

Scaffolding a new profile

# Create a new profile directory with a skeleton manifest
markspec profile new @myorg/my-profile

# Add it to the project
markspec profile add ./my-profile

The scaffold creates:

my-profile/
├── markspec.yaml   ← manifest (id, version, extends, profile: …)
└── README.md       ← human-readable description

Edit markspec.yaml to declare types, attributes, and relations. Run markspec profile publish to validate the manifest before distributing it.

Prose analysis

MarkSpec performs quality analysis on the body prose of authored entries — the paragraphs and list items that describe what a requirement means, not just its identity. Prose analysis runs as a separate markspec lint pass after parsing and validation; it does not block markspec validate.

Scope

Only authored entries of Specification-family types are analysed. The scope predicate is:

shape = Authored  AND  core type ∈ { Requirement, Test, Contract, Record, Risk }
                        or a profile subtype of any of the above

Reference-shape entries are excluded — they point to external documents whose prose MarkSpec does not own. Core types outside the Specification family (e.g. SoftwareComponent, Definition, Annotation) are also excluded; structural descriptions and glossary entries follow different writing conventions.

Suppression-hygiene rules (MSL-Q9xx) run on all authored entries regardless of type.

Modal keywords signal the obligation level of a requirement. MarkSpec enforces the RFC 2119 / EARS convention that modals appear in lowercase:

KeywordObligation
shallMandatory
shall notProhibited
shouldRecommended
should notNot recommended
mayOptional
mustExternal constraint (use shall for internal)
must notProhibited (external constraint)

MSL-M060 — modal-keyword-uppercase (warning)

Fires when any of the above keywords appears in uppercase (SHALL, MUST, …) inside body prose. The formatter (markspec format) rewrites uppercase modals to lowercase automatically, so this diagnostic appears only on files that have not been formatted.

warning[MSL-M060]: requirements.md:12 modal keyword 'SHALL' in body prose is
uppercase (spec §3.4.1 canonical form is lowercase; 'markspec format' will
rewrite it)

Verbatim blocks (fenced code, math, feature snippets) are excluded — modal keywords inside code examples are not checked.

MSL-M061 — missing-modal-keyword (info)

Fires on Requirement entries (and profile subtypes) whose body contains no modal keyword at all. This is a style hint: a requirement without an obligation word is often a description masquerading as a requirement.

info[MSL-M061]: requirements.md:7 Requirement entry contains no modal keyword
(shall / should / may / must) — consider declaring one to make the obligation
explicit

EARS pattern recognition

EARS (Easy Approach to Requirements Syntax) defines five body forms for requirement entries. MarkSpec recognises the leading keyword of each form and uses it internally for normalization — capitalisation is preserved at sentence start and lowercased mid-sentence.

FormLeading keywordTemplate
Ubiquitous(none)The system shall
State-drivenWhileWhile state, system shall
Event-drivenWhenWhen event, system shall
UnwantedIfIf condition, system shall
OptionalWhereWhere feature, system shall

The EARS form is currently used for normalization only; no lint rule fires on an EARS keyword choice. Future rule group ears is reserved for form-specific checks (e.g. ensuring state-driven requirements name a concrete state).

GWT pattern

GWT (Given / When / Then) is the standard body form for Test entries. MarkSpec accepts GWT prose without enforcing structural rules in the current phase; the three clauses are plain prose paragraphs.

Recommended form:

- [SWT_BRK_0030] Debounce rejects short pulses

  Given the debounce threshold is 10 ms, When a pulse of 5 ms arrives, Then the
  output shall remain unchanged.

      Id: 01HGW3R9QNP4ABCDEFGHJKMNPQ
      Type: test
      Verifies: 01HGW2Q8MNP3RSTVWXYZABCDEF

GWT-specific lint rules (detecting missing clauses, mixed-form bodies) are planned for a future phase.

INCOSE lexicon rules

These rules flag vocabulary patterns that the INCOSE Guide to Writing Requirements (GtWR) identifies as common sources of ambiguity. All rules apply to prose-bearing blocks (paragraphs, notes, list items); tables, code, and math blocks are excluded.

CodeNameSeverityExamples of flagged text
MSL-Q302incose-r7-vague-termwarningsome, several, many, adequate, sufficient, reasonable, as needed
MSL-Q303incose-r8-escape-clausewarningas appropriate, where possible, if practicable, to the extent possible
MSL-Q304incose-r9-open-endedinfoincluding but not limited to, etc., and/or
MSL-Q305incose-r10-superfluous-infinitiveinfobe able to, be designed to, in order to
MSL-Q310incose-r26-absoluteinfo100%, always, never, complete, entirely
MSL-Q313incose-r16-notinfonot (whole-word; excludes note, notation)

Rules MSL-Q302 and MSL-Q303 are warning severity — they flag text that routinely causes requirement verification ambiguity. The remaining rules are info — informational hints that the author should consider but that do not necessarily indicate a defect.

Example

warning[MSL-Q302]: src/braking/requirements.md:14 vague term 'sufficient' in
body prose — specify a measurable threshold instead (INCOSE GtWR R7)

warning[MSL-Q303]: src/braking/requirements.md:19 escape clause 'as appropriate'
weakens verifiability (INCOSE GtWR R8)

Structural quality rules

Two rules check the shape of entries rather than their vocabulary.

MSL-Q400 — struct-title-length (info)

The entry title should be between 3 and 120 characters. A 1–2 character title is almost certainly incomplete; a title longer than 120 characters is usually a sentence that belongs in the body.

MSL-Q401 — struct-body-length (info)

The entry body should contain between 5 and 500 words. An entry with fewer than 5 words has no meaningful description; one exceeding 500 words is likely describing multiple concerns that should be split.

Both thresholds are hard-coded defaults in the current phase. Profile-level configuration (e.g. prose.struct.title.maxLength) is planned but not yet implemented.

Suppression

A rule can be silenced for a specific entry by adding two trailer attributes:

- [SRS_BRK_0108] Legacy inherited requirement

  The system shall operate as appropriate for the ambient conditions.

      Id: 01HGW2Q8MNP3RSTVWXYZABCDEF
      Markspec-disable: MSL-Q303
      Rationale: Verbatim from customer SRS version 1.2; cannot be rephrased
                 without an approved change request.

Both Markspec-disable and Rationale must be present; a suppression without a rationale fires MSL-Q900 (disable-without-rationale). Citing an unknown rule code fires MSL-Q901 (disable-unknown-rule). These two hygiene rules run on all authored entries regardless of type and cannot themselves be suppressed.

Running prose analysis

markspec lint <paths...>           # info, warning, error output to stderr
markspec lint --format json <paths...>  # structured JSON to stdout
markspec lint --strict <paths...>  # promote warnings to errors (exit 1)

The lint subcommand is separate from validate; the pre-commit hook (markspec hook) does not run lint — lint is a review-time quality gate, not a commit blocker.

Annex A — Compile output

markspec compile --output <dir> <paths...> writes the compiled traceability graph to a directory of static files — the /api/ directory. This output is the archival, published artifact: what CI produces, what downstream projects federate against, and what auditors and rendering pipelines consume.

The compile output is designed to be served as static files (e.g., on GitHub Pages or GitLab Pages) and consumed by downstream tools without requiring a running MarkSpec process.

Generating the compile output

# Compile all Markdown and source files in docs/ and src/
markspec compile --output api/ docs/**/*.md src/**/*.rs

# Force streaming form (NDJSON) regardless of entry count
markspec compile --output api/ --split-threshold 0 docs/**/*.md

# Add a SQLite mirror for analytics consumers
markspec compile --output api/ --with-sqlite docs/**/*.md

Directory layout

<output-dir>/
├── manifest.json        always present
├── compiled.json        small projects (< 1 000 entries by default)
 or
├── entries.ndjson       large projects (≥ split-threshold)
├── entries.idx          index for O(1) entry lookup by display ID
└── edges.ndjson         trace edges (forward + generated inverses)

The threshold between the two forms is controlled by --split-threshold (default: 1 000 entries). Both forms contain the same data — consumers should check manifest.entries.format to determine which is present.

Manifest schema

manifest.json is always small enough to parse in full. It is the entry point for all consumers — read it first, then follow its pointers to the entry and edge data.

{
  "markspecSchemaVersion": 1,
  "generator": {
    "name": "markspec",
    "version": "0.5.0"
  },
  "project": {
    "name": "my-project",
    "version": "1.2.0"
  },
  "counts": {
    "entries": 1234,
    "edges": 456
  },
  "entries": {
    "format": "ndjson",
    "file": "entries.ndjson"
  },
  "edges": {
    "format": "ndjson",
    "file": "edges.ndjson"
  },
  "sqliteMirror": null,
  "federation": [],
  "reserved": {}
}

For small projects, entries.format is "inline" and entries.file is "compiled.json".

FieldTypeNotes
markspecSchemaVersionintegerSchema version; currently 1
generator.namestringAlways "markspec"
generator.versionstringMarkSpec release version (informational only)
project.namestringFrom project.yaml
project.versionstringFrom project.yaml
counts.entriesintegerTotal number of entries
counts.edgesintegerTotal number of edges (including generated inverses)
entries.format"ndjson" | "inline"Which form is present
entries.filestringRelative path to the entry data
edges.format"ndjson" | "inline"Which form is present
edges.filestringRelative path to the edge data
sqliteMirrorstring | nullRelative path to mirror.db, or null
federationarrayUpstream registries (see Federation section)
reservedobjectReserved for future use; consumers must ignore

Entry record

Each entry record appears as one JSON object — either as a line in entries.ndjson (streaming form) or as a value in the entries map in compiled.json (inline form).

{
  "displayId": "SRS_BRK_0107",
  "id": "01HGW2Q8MNP3RSTVWXYZABCDEF",
  "shape": "Authored",
  "type": "requirement",
  "title": "Sensor debouncing",
  "body": "The sensor driver shall debounce raw inputs to eliminate noise.",
  "rawAttributes": [
    { "key": "Id", "value": "01HGW2Q8MNP3RSTVWXYZABCDEF" },
    { "key": "Type", "value": "requirement" },
    { "key": "Derived-from", "value": "SYS_BRK_0042" },
    { "key": "Labels", "value": "ASIL-B" }
  ],
  "location": { "file": "docs/requirements.md", "line": 42, "column": 1 },
  "properties": {
    "file.path": "docs/requirements.md",
    "file.mtime": "2026-05-19T07:00:00Z",
    "git.sha": "a88ba34",
    "git.author": "Alice <alice@example.com>"
  }
}
FieldTypeNotes
displayIdstringHuman-readable ID, e.g. SRS_BRK_0107
idstring | nullULID or URI; null if no Id: trailer
shape"Authored" | "Reference"Determined by Id: format
typestring | nullResolved type name; null if unresolved
titlestringEntry title text
bodystringEntry body text (trimmed)
rawAttributes{key, value}[]All trailer attributes in source order
location{file, line, column}Source file path, 1-based line and column
propertiesobjectObserved facts (see Properties namespaces)

Properties namespaces

The properties object is partitioned by namespace prefix. Only namespaces that are available are populated — a property whose source data is absent is omitted entirely rather than set to null.

NamespaceFieldsNotes
file.*file.path, file.mtime, file.sizeAlways included
git.*git.sha, git.author, git.committerIncluded when git history is available
source.*source.language, source.functionIncluded for in-source entries (doc comments)
sync.*(various)Never included — privacy boundary (see Privacy rules)

Edge record

Each edge record appears as one JSON object in edges.ndjson (streaming form) or in the edges array in compiled.json (inline form).

{ "from": "SRS_BRK_0107", "to": "SYS_BRK_0042", "kind": "satisfies",    "generated": false }
{ "from": "SYS_BRK_0042", "to": "SRS_BRK_0107", "kind": "satisfied-by", "generated": true  }
FieldTypeNotes
fromstringSource display ID
tostringTarget display ID
kindstringRelation name in lowercase-with-hyphens
generatedbooleantrue for inverse edges written by MarkSpec

The kind field uses lowercase-with-hyphens form (satisfies, not Satisfies). This matches the display ID convention used in entries.idx.

entries.idx

entries.idx is a JSON object mapping display ID to byte offset in entries.ndjson. This allows O(1) random access to any entry without reading the full NDJSON file:

{
  "SRS_BRK_0107": 0,
  "SRS_BRK_0108": 1847,
  "SYS_BRK_0042": 3694
}

A consumer looking up SRS_BRK_0107 reads the offset (0), seeks to that position in entries.ndjson, reads one line, and parses the JSON object.

Small-project form (inline)

For projects below the split-threshold, all data is in compiled.json:

{
  "entries": {
    "SRS_BRK_0107": {
      "displayId": "SRS_BRK_0107",
      "id": "01HGW2Q8MNP3RSTVWXYZABCDEF",
      "shape": "Authored",
      "type": "requirement",
      "title": "Sensor debouncing",
      "body": "The sensor driver shall debounce raw inputs to eliminate noise.",
      "rawAttributes": [ ... ],
      "location": { "file": "docs/requirements.md", "line": 42, "column": 1 },
      "properties": { ... }
    }
  },
  "edges": [
    { "from": "SRS_BRK_0107", "to": "SYS_BRK_0042", "kind": "satisfies", "generated": false }
  ]
}

The entries field is a map keyed by display ID. The edges field is a flat array. Both forms carry identical data — the split is a performance optimization for large projects, not a semantic distinction.

Privacy rules

The following rules govern what is and is not serialized in the compile output. The output is designed to be published world-readable; these rules exist to prevent sensitive data from leaking into the artifact.

  • sync.* properties are never serialized. They may contain external-system tokens, user IDs, session timestamps, or workspace paths that should not appear in a published artifact.
  • git.contributors is opt-in — it requires an explicit --with-contributors flag. By default, only git.sha, git.author, and git.committer are included, and only when git history is available.
  • file.path records the path as written in the compile command, which may be relative or absolute depending on the invocation. CI pipelines should use project-relative paths for reproducible output.

Schema versioning

markspecSchemaVersion is a monotonically increasing integer. The current version is 1.

Compatibility rules:

  • Consumers must reject output with a markspecSchemaVersion higher than they support.
  • Consumers must ignore unknown keys within any object. Schema evolution is additive-only — new fields are added, existing fields are never removed or renamed within a major version.
  • Consumers must use markspecSchemaVersion, not generator.version, for compatibility checks. The generator version is informational.

When a breaking change is needed, markspecSchemaVersion is incremented and a migration guide is published.

Federation

manifest.federation lists upstream registries that this project federates against. Downstream projects can resolve display IDs that refer to entries in an upstream project’s compile output.

{
  "federation": [
    {
      "id": "upstream-safety",
      "url": "https://ci.example.com/safety-project/api/",
      "markspecSchemaVersion": 1
    }
  ]
}

Resolution works as follows:

  1. A display ID is not found in the local entries.idx.
  2. MarkSpec walks the federation list in order.
  3. For each federated entry, it fetches <url>/manifest.json to confirm the schema version is compatible.
  4. It then fetches <url>/entries.idx and looks up the display ID.
  5. If found, it fetches the specific byte range from <url>/entries.ndjson using an HTTP Range request.

Federation is read-only and acyclic — the protocol is just static file fetches. There is no federation server. A federated project cannot modify the local compile output.

SQLite mirror

markspec compile --output <dir> --with-sqlite produces an additional mirror.db file alongside the NDJSON files. This is the same data in SQLite form, for analytics consumers (coverage dashboards, traceability explorers, etc.) that prefer SQL queries over NDJSON.

manifest.sqliteMirror points to the mirror file when present:

{
  "sqliteMirror": "mirror.db"
}

The SQLite mirror is never used as the LSP’s working index — the LSP maintains its own in-memory index rebuilt from source files. The mirror is an output artifact only.

Table schema (abbreviated):

CREATE TABLE entries (
  display_id  TEXT PRIMARY KEY,
  id          TEXT,
  shape       TEXT NOT NULL,
  type        TEXT,
  title       TEXT NOT NULL,
  body        TEXT NOT NULL,
  file        TEXT NOT NULL,
  line        INTEGER NOT NULL
);

CREATE TABLE edges (
  from_id     TEXT NOT NULL,
  to_id       TEXT NOT NULL,
  kind        TEXT NOT NULL,
  generated   INTEGER NOT NULL  -- 0 or 1
);

CREATE TABLE properties (
  display_id  TEXT NOT NULL,
  key         TEXT NOT NULL,
  value       TEXT NOT NULL
);

Annex B — Profile manifest schema

A profile is a versioned, distributable directory that extends the core type taxonomy with domain-specific vocabulary. Its normative specification lives in this annex; the Profiles and extensions chapter explains how to author and activate profiles.

B.1 Directory layout

<profile-id>/
├── markspec.yaml        ← manifest + declarative content (required)
└── README.md            ← recommended

The markspec.yaml file has two regions: manifest fields (identity, versioning, distribution) and the profile: content subtree (types, attributes, relations, labels).

B.2 Top-level manifest fields

FieldRequiredTypeNotes
idYesstringScoped identifier: @org/name or name
versionYesstringSemantic version (MAJOR.MINOR.PATCH)
descriptionNostringHuman-readable summary; recommended for publishing
licenseNostringSPDX identifier (e.g. MIT, Apache-2.0); recommended
extendsNostringParent profile specifier — local path or scoped ID
markspec-schemaNostringCore schema version pin (e.g. "1"); see §B.7

Complete example:

id: "@myorg/safety"
version: 1.2.0
description: "ISO 26262 safety vocabulary"
license: MIT
extends: "@markspec/default"
markspec-schema: "1"

profile:
  types: { ... }
  attributes: []
  relations: []
  labels: []

B.3 Types (profile.types)

Each entry under profile.types declares one concrete profile type:

profile:
  types:
    software-requirement:
      extends: Requirement
      display-id-pattern: "SRS_{n:4d}"
      description: "Software-level normative statement"
    hazard:
      extends: Risk
      display-id-pattern: "HAZ_{n:4d}"
    safety-requirement:
      extends: software-requirement   # profile-declared parent
      display-id-pattern: "SAF_{n:4d}"

Type fields

FieldRequiredNotes
extendsYesCore type name (PascalCase) or another profile type in the same chain
display-id-patternNoPattern string; {n:Nd} is the numeric placeholder (N = minimum digits)
descriptionNoHuman-readable purpose shown by markspec profile describe

Rules

  • extends: is required. Every profile type must name a parent. Omitting it is a profile-load error (PROFILE-TYPE-001).
  • Target must resolve. The parent must be one of the 19 core type names (4 abstract + 15 concrete) or another profile type in the same effective profile. An unresolved target is PROFILE-TYPE-002.
  • No cycles. The inheritance graph must be acyclic and must root at a core type (PROFILE-TYPE-003).
  • No shadowing. Profile type names must not duplicate any of the 19 core type names (MSL-A040 / PROFILE-TYPE-004).
  • Convention. Profile type names use lowercase-with-hyphens; core names use PascalCase. This makes origin unambiguous.

display-id-pattern syntax

PlaceholderMeaningExample patternExample output
{n:4d}Auto-increment, minimum 4 digits, paddedSRS_{n:4d}SRS_0042
{n:3d}Auto-increment, minimum 3 digits, paddedHAZ_{n:3d}HAZ_003
{scope}Project-scope tag (reserved, future)

markspec format assigns the next available number. markspec next-id <type> prints it without writing.

B.4 Attributes (profile.attributes)

profile:
  attributes:
    - key: ASIL
      applies-to: [software-requirement, hazard, test]
      cardinality: single
      values: [QM, ASIL-A, ASIL-B, ASIL-C, ASIL-D]
      description: "Automotive Safety Integrity Level"
    - key: Test-level
      applies-to: [test]
      cardinality: single
      values: [unit, integration, system, acceptance]
    - key: Priority
      applies-to: []       # empty = applies to all profile types
      cardinality: single
      values: [low, medium, high, critical]

Attribute fields

FieldRequiredNotes
keyYesTrailer key name; PascalCase or Hyphen-case; must not shadow core keys
applies-toYesList of profile type names; empty list means all profile types
cardinalityNosingle (default) or multi; multi allows repeated Key: lines
valuesNoClosed enumeration; open if omitted
descriptionNoHuman-readable purpose
requiredNoBoolean; true makes the attribute mandatory for the listed types

Constraints

  • Attribute keys must not conflict with the core universal set (Id, Type, Labels, References, External-id, Supersedes, Superseded-by, Deprecated). Shadowing a core key is MSL-A040.
  • cardinality: multi allows repeating the attribute on separate trailer lines or as CSV on a single line. markspec format normalises CSV to one-per-line.

B.5 Relations (profile.relations)

profile:
  relations:
    - key: Mitigated-by
      inverse: Mitigates
      source-types: [hazard]
      target-types: [software-requirement]
      cardinality: many-to-many
      description: "Links a hazard to requirements that address it"
    - key: Addresses
      inverse: Addressed-by
      source-types: [software-requirement]
      target-types: [change]
      cardinality: many-to-one

Relation fields

FieldRequiredNotes
keyYesTrailer attribute name used by authors (Mitigated-by: HAZ_001)
inverseNoName of the generated reverse edge; MarkSpec writes it in compiled output
source-typesNoProfile types that may carry this relation; empty = any
target-typesNoProfile types that may be targeted; empty = any
cardinalityNomany-to-many (default), many-to-one, one-to-many, one-to-one
descriptionNoHuman-readable purpose

Relations declared in a profile are validated by markspec validate. A relation attribute on an entry not in source-types is MSL-R085. A target not in target-types is MSL-R086.

B.6 Labels (profile.labels)

profile:
  labels:
    - name: DRAFT
      description: "Work in progress; not reviewed"
    - name: RELEASED
      description: "Approved and baselined"
    - name: ASIL-B
      description: "Automotive Safety Integrity Level B"
      applies-to: [software-requirement, hazard]
FieldRequiredNotes
nameYesLabel value as it appears in Labels: trailer
descriptionNoHuman-readable meaning
applies-toNoRestrict to listed profile types; empty = any

Labels not declared in the active profile produce MSL-L010 (unknown label concern).

B.7 Versioning and compatibility

Core schema pin (markspec-schema)

markspec-schema: "1" pins the profile against version 1 of the core schema contract. The toolchain rejects a profile whose markspec-schema value is higher than the running binary’s CORE_SCHEMA_VERSION.

markspec-schema: "1"   # integer string; "1" is the current value

Profile version and semver rules

Change kindVersion bump
Add new optional attribute / labelminor
Add new type (new extends: entry)minor
Add new relation with inverseminor
Make attribute required: truemajor
Remove a type, attribute, or relationmajor
Rename a keymajor
Change cardinality to more restrictivemajor

Compatibility window

A consumer project pins profile versions in .markspec.yaml. The toolchain supports the declared version only — no implicit upgrade, no downgrade. Run markspec profile add <spec>@<version> to pin explicitly.

B.8 Distribution and specifiers

Profiles are referenced in .markspec.yaml using a specifier string:

Specifier formResolves to
@org/nameLatest vendored copy in profiles/@org/name/
@org/name@1.2.0Specific version; pinned in .markspec.yaml
./path/to/profileLocal directory relative to project root
npm:@org/name@^1.0npm registry (resolved and vendored on profile add)

Vendoring: markspec profile add <spec> downloads the profile and writes it into profiles/ under the project root. Vendored profiles are committed to version control — the project owns a reproducible copy.

B.9 Extends chain and conflict resolution

When multiple profiles are active (the .markspec.yaml profiles: list), they are merged into a single effective profile. Resolution order: later entries in the list take precedence over earlier ones.

# .markspec.yaml
profiles:
  - "@markspec/default"          # lowest precedence
  - "@markspec/compliance-iso26262"
  - "./profiles/myorg"           # highest precedence

Within a single profile’s extends: inheritance chain, child declarations override parent declarations for the same key.

Conflict rules:

ElementConflict resolution
TypesChild type overrides parent type of same name
AttributesChild attribute overrides parent attribute of same key
RelationsChild relation overrides parent relation of same key
LabelsUnion; duplicate name keeps child description

B.10 Validation and publishing

Validate a profile manifest before distributing it:

markspec profile publish --dir ./my-profile

This checks:

  • All required fields present (id, version)
  • All extends: targets resolve
  • No shadowing of core type names (MSL-A040)
  • No shadowing of core attribute keys
  • markspec-schema is present and ≤ current CORE_SCHEMA_VERSION
  • description and license present (warnings if absent)

A zero-error exit indicates the profile is safe to distribute.