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:
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:
- Authored —
Id:is a bare ULID (01HGW2Q8MNP3RSTVWXYZABCDEF). The item originated in this project; ULIDs are assigned bymarkspec format. - Reference —
Id: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
| Chapter | What it covers |
|---|---|
| Entry format | Syntax and anatomy of entry blocks; the two shapes; in-source entries |
| Type taxonomy | The 4 abstract and 15 concrete types; the type-resolution chain; profile subtypes |
| Attributes | Universal attributes; per-type typical attributes; multi-value rules |
| Trace relations | Core relations; generated inverses; the trace graph |
| Profiles and extensions | Profile manifests; the extends chain; core-only mode; bundled profiles |
| Annex A — Compile output | The /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/defaultprofile 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
PascalCaseorTitle-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 bymarkspec format. - The
Id:attribute is required for all entries (a missingId:is a warning; the entry receives no ULID untilmarkspec formatis run). - Attribute order within the trailer is not significant for semantics, but
markspec formatnormalizes it:Idfirst, thenType, then relations, thenLabels, 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:
| Scheme | Example | Typical use |
|---|---|---|
urn: | urn:iso:std:iso:26262:-6:ed-2 | ISO standards, ISO URNs |
doi: | doi:10.1109/IEEESTD.2018.8299595 | Academic papers, IEEE standards |
pkg: | pkg:cargo/serde@1.0.197 | Open-source packages (purl format) |
https: | https://www.rfc-editor.org/rfc/rfc2119 | Web 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:
| Language | Comment 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 type | Purpose | Extended by |
|---|---|---|
Item | Root; ultimate fallback for all type resolution | All 15 concrete types; any profile subtype |
Specification | Normative statements; things that must or shall | Requirement, Test, Contract, Record, Risk |
Component | System-level building blocks with identity | SoftwareComponent, HardwareComponent, SoftwareInterface, HardwareInterface |
Unit | Fine-grained implementation-level elements | SoftwareUnit, 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(→ upstreamRequirementorObjective),Derived-from(→ otherRequirement). Verified byTestentries viaVerified-by(generated inverse ofVerifies). - 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(→SoftwareUnitorComponent). - 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 ofRealizesfrom aComponent).
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 ofMitigatesfrom aRequirement). - 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 aRequirementusesAllocated-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 ofRealizes).
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(→ContractorRequirement).
/**
* [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).
| Attribute | Required | Cardinality | Value |
|---|---|---|---|
Id | Yes | Single | Bare ULID (Authored shape) or URI with scheme (Reference shape) |
Type | No | Single | Core or profile-declared type name |
Labels | No | Multi | Free-form tags; one Labels: line per tag |
References | No | Multi | Display ID, optionally followed by a locator in […] |
External-id | No | Single | Cross-system identifier (Jira, DOORS, …) |
Supersedes | No | Single | Display ID of the predecessor entry (Authored only) |
Superseded-by | — | Generated | Inverse of Supersedes; written by markspec format |
Deprecated | No | Single | Quoted 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:
| Attribute | Value |
|---|---|
Reference-url | Canonical URL for the standard or package |
Reference-document | Human-readable citation string |
License | SPDX 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
| Type | Typical profile attributes | Typical relation attributes |
|---|---|---|
Requirement | ASIL, Priority, Safety-goal | Satisfies, Derived-from |
Test | Test-level, Test-type, Test-result | Verifies, Tests |
Contract | (none typical) | Satisfies, Realized-by (generated) |
Record | (none typical) | (standalone) |
Risk | ASIL, Severity, Probability | Mitigated-by (generated) |
Component types
| Type | Typical profile attributes | Typical relation attributes |
|---|---|---|
SoftwareComponent | Version, License, Supplier | Part-of, Depends-on |
HardwareComponent | Version, Supplier | Part-of |
SoftwareInterface | (none typical) | Part-of, Realized-by (generated) |
HardwareInterface | (none typical) | Part-of |
Unit types
| Type | Typical profile attributes | Typical relation attributes |
|---|---|---|
SoftwareUnit | (none typical) | Part-of, Realizes |
HardwareUnit | (none typical) | Part-of |
Item types
| Type | Typical profile attributes | Notes |
|---|---|---|
Definition | (none typical) | Used in glossary cross-check lint rule |
Objective | Priority | Upstream anchor for Requirement Satisfies chains |
Standard | Reference-url, Reference-document, License | Always Reference shape |
Change | Priority, Status | Links 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:
IdType- Relation attributes (
Satisfies,Derived-from,Verifies,Tests, …) LabelsReferencesExternal-idSupersedes/Superseded-byDeprecated- 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_0042SRS_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).
| Relation | Inverse | Typical source type | Typical target type |
|---|---|---|---|
Satisfies | Satisfied-by | Requirement | Requirement, Objective |
Derived-from | Derived-by | Requirement | Requirement |
Verifies | Verified-by | Test | Requirement, Contract |
Tests | Tested-by | Test | SoftwareUnit, Component |
Depends-on | Required-by | Component, Unit | Component |
Part-of | Has-part | Component, Unit | Component |
Allocated-to | Allocates | Requirement | Component |
Realizes | Realized-by | Component, Unit | Contract, Requirement |
Generated-from | (none) | any | any |
Addresses | Addressed-by | Requirement | Change |
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:
-
Parse pass — each
RelationName: VALUEline is parsed. Unknown relation keys raiseMSL-A020(unknown attribute) unless the key is declared by the active profile. -
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
| Element | Purpose |
|---|---|
| Subtypes | Domain-specific entry types with display-ID patterns (requirement extends Requirement) |
| Attributes | Extra trailer keys beyond the core universal set (ASIL, Priority, License) |
| Relations | Trace edges with cardinality and inverse rules (Mitigated-by, Addresses) |
| Label concerns | Structured label vocabulary (DRAFT, RELEASED, ASIL-B) |
| Conventions | Document-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:
| Field | Required | Notes |
|---|---|---|
id | Yes | Scoped identifier, e.g. @org/name or name |
version | Yes | Semantic version string |
description | No | Human-readable summary (recommended for publishing) |
extends | No | Parent profile specifier (local path or scoped ID) |
license | No | SPDX 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 produceMSL-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, orTitle— these are reserved core attribute keys. - Cannot shadow any of the 15 core concrete type names (
MSL-A040is raised for reserved-name conflicts). A profile type namedRequirementis forbidden; userequirement(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
| Profile | Purpose |
|---|---|
@markspec/default | RFC 2119 modal keywords, Reference-url/Reference-document/License attributes, core relations (Satisfies, Verifies, …), DRAFT/RELEASED labels |
@markspec/compliance-iso26262 | ASIL labels, functional-safety relations, Part 6 reference entries |
@markspec/compliance-aspice | ASPICE process attributes, traceability relations |
Profile commands
| Command | Purpose |
|---|---|
markspec profile show | Display 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 keyword analysis
Modal keywords signal the obligation level of a requirement. MarkSpec enforces the RFC 2119 / EARS convention that modals appear in lowercase:
| Keyword | Obligation |
|---|---|
shall | Mandatory |
shall not | Prohibited |
should | Recommended |
should not | Not recommended |
may | Optional |
must | External constraint (use shall for internal) |
must not | Prohibited (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.
| Form | Leading keyword | Template |
|---|---|---|
| Ubiquitous | (none) | The system shall … |
| State-driven | While | While state, system shall … |
| Event-driven | When | When event, system shall … |
| Unwanted | If | If condition, system shall … |
| Optional | Where | Where 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.
| Code | Name | Severity | Examples of flagged text |
|---|---|---|---|
| MSL-Q302 | incose-r7-vague-term | warning | some, several, many, adequate, sufficient, reasonable, as needed |
| MSL-Q303 | incose-r8-escape-clause | warning | as appropriate, where possible, if practicable, to the extent possible |
| MSL-Q304 | incose-r9-open-ended | info | including but not limited to, etc., and/or |
| MSL-Q305 | incose-r10-superfluous-infinitive | info | be able to, be designed to, in order to |
| MSL-Q310 | incose-r26-absolute | info | 100%, always, never, complete, entirely |
| MSL-Q313 | incose-r16-not | info | not (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".
| Field | Type | Notes |
|---|---|---|
markspecSchemaVersion | integer | Schema version; currently 1 |
generator.name | string | Always "markspec" |
generator.version | string | MarkSpec release version (informational only) |
project.name | string | From project.yaml |
project.version | string | From project.yaml |
counts.entries | integer | Total number of entries |
counts.edges | integer | Total number of edges (including generated inverses) |
entries.format | "ndjson" | "inline" | Which form is present |
entries.file | string | Relative path to the entry data |
edges.format | "ndjson" | "inline" | Which form is present |
edges.file | string | Relative path to the edge data |
sqliteMirror | string | null | Relative path to mirror.db, or null |
federation | array | Upstream registries (see Federation section) |
reserved | object | Reserved 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>"
}
}
| Field | Type | Notes |
|---|---|---|
displayId | string | Human-readable ID, e.g. SRS_BRK_0107 |
id | string | null | ULID or URI; null if no Id: trailer |
shape | "Authored" | "Reference" | Determined by Id: format |
type | string | null | Resolved type name; null if unresolved |
title | string | Entry title text |
body | string | Entry body text (trimmed) |
rawAttributes | {key, value}[] | All trailer attributes in source order |
location | {file, line, column} | Source file path, 1-based line and column |
properties | object | Observed 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.
| Namespace | Fields | Notes |
|---|---|---|
file.* | file.path, file.mtime, file.size | Always included |
git.* | git.sha, git.author, git.committer | Included when git history is available |
source.* | source.language, source.function | Included 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 }
| Field | Type | Notes |
|---|---|---|
from | string | Source display ID |
to | string | Target display ID |
kind | string | Relation name in lowercase-with-hyphens |
generated | boolean | true 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.contributorsis opt-in — it requires an explicit--with-contributorsflag. By default, onlygit.sha,git.author, andgit.committerare included, and only when git history is available.file.pathrecords 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
markspecSchemaVersionhigher 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, notgenerator.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:
- A display ID is not found in the local
entries.idx. - MarkSpec walks the federation list in order.
- For each federated entry, it fetches
<url>/manifest.jsonto confirm the schema version is compatible. - It then fetches
<url>/entries.idxand looks up the display ID. - If found, it fetches the specific byte range from
<url>/entries.ndjsonusing 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
| Field | Required | Type | Notes |
|---|---|---|---|
id | Yes | string | Scoped identifier: @org/name or name |
version | Yes | string | Semantic version (MAJOR.MINOR.PATCH) |
description | No | string | Human-readable summary; recommended for publishing |
license | No | string | SPDX identifier (e.g. MIT, Apache-2.0); recommended |
extends | No | string | Parent profile specifier — local path or scoped ID |
markspec-schema | No | string | Core 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
| Field | Required | Notes |
|---|---|---|
extends | Yes | Core type name (PascalCase) or another profile type in the same chain |
display-id-pattern | No | Pattern string; {n:Nd} is the numeric placeholder (N = minimum digits) |
description | No | Human-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
| Placeholder | Meaning | Example pattern | Example output |
|---|---|---|---|
{n:4d} | Auto-increment, minimum 4 digits, padded | SRS_{n:4d} | SRS_0042 |
{n:3d} | Auto-increment, minimum 3 digits, padded | HAZ_{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
| Field | Required | Notes |
|---|---|---|
key | Yes | Trailer key name; PascalCase or Hyphen-case; must not shadow core keys |
applies-to | Yes | List of profile type names; empty list means all profile types |
cardinality | No | single (default) or multi; multi allows repeated Key: lines |
values | No | Closed enumeration; open if omitted |
description | No | Human-readable purpose |
required | No | Boolean; 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 isMSL-A040. cardinality: multiallows repeating the attribute on separate trailer lines or as CSV on a single line.markspec formatnormalises 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
| Field | Required | Notes |
|---|---|---|
key | Yes | Trailer attribute name used by authors (Mitigated-by: HAZ_001) |
inverse | No | Name of the generated reverse edge; MarkSpec writes it in compiled output |
source-types | No | Profile types that may carry this relation; empty = any |
target-types | No | Profile types that may be targeted; empty = any |
cardinality | No | many-to-many (default), many-to-one, one-to-many, one-to-one |
description | No | Human-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]
| Field | Required | Notes |
|---|---|---|
name | Yes | Label value as it appears in Labels: trailer |
description | No | Human-readable meaning |
applies-to | No | Restrict 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 kind | Version bump |
|---|---|
| Add new optional attribute / label | minor |
Add new type (new extends: entry) | minor |
Add new relation with inverse | minor |
Make attribute required: true | major |
| Remove a type, attribute, or relation | major |
| Rename a key | major |
Change cardinality to more restrictive | major |
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 form | Resolves to |
|---|---|
@org/name | Latest vendored copy in profiles/@org/name/ |
@org/name@1.2.0 | Specific version; pinned in .markspec.yaml |
./path/to/profile | Local directory relative to project root |
npm:@org/name@^1.0 | npm 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:
| Element | Conflict resolution |
|---|---|
| Types | Child type overrides parent type of same name |
| Attributes | Child attribute overrides parent attribute of same key |
| Relations | Child relation overrides parent relation of same key |
| Labels | Union; 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-schemais present and ≤ currentCORE_SCHEMA_VERSIONdescriptionandlicensepresent (warnings if absent)
A zero-error exit indicates the profile is safe to distribute.