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 fmt. - 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 fmt):
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 fmt. - The
Id:attribute is required for all entries (a missingId:is a warning; the entry receives no ULID untilmarkspec fmtis run). - Attribute order within the trailer is not significant for semantics, but
markspec fmtnormalizes it:Idfirst, thenType, then relations, thenLabels, then any remaining attributes. - The
Discipline:andDiscipline-frozen:attributes are author overrides of the discipline derivation pass (see Type taxonomy → Discipline classification).Discipline:is single-valued and matches/^[a-z][a-z0-9-]*$/;Discipline-frozen:carries a<kind>or<kind> @ <YYYY-MM-DD>snapshot that is excluded from re-derivation.
Computed fields. The parser emits Entry.bodyTokens — a flat
position-sorted token stream covering modal verbs, EARS triggers, Gherkin
keywords, $Identifier entity refs and inline code (ADR-016). These tokens are
not author-editable; they are recomputed on every parse and surface in the
compile-output entry record. After the discipline pass, an entry may also carry
Entry.derivedDiscipline (set of disciplines reached via Allocated-to).
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 fmt
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 check 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
│ │ ├── SoftwareInterface
│ │ └── HardwareInterface
│ ├── Record
│ └── Risk
├── Component (abstract)
│ ├── SoftwareComponent
│ └── HardwareComponent
├── 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 |
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).
SoftwareInterface
A software interface specification — an API surface (.proto, OpenAPI, WSDL)
defining the contract between two software components.
- Typical relations:
Provided-by/Required-by(generated inverses of a component’sProvides/Requires);Realized-by(generated inverse ofRealizes);Verified-by(generated inverse ofVerifies).
HardwareInterface
A hardware interface specification — a connector, bus, or pin-level boundary
spec. Carries Bus-protocol, Connector-type, Voltage-level,
Signal-direction.
- Typical relations:
Provided-by/Required-by;Realized-by.
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
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.
Discipline classification
Per ADR-017 and
ADR-018, the toolchain
derives a software-or-hardware discipline for each Requirement by walking the
Allocated-to graph upward from concrete Component and Unit types. The
result lands on Entry.derivedDiscipline? in compile output.
The set of components recognized as the discipline source-of-truth lives in core
(Item.discipline); profiles may extend the vocabulary with new components
but cannot reclassify existing core components. Three modes are available via
the profile’s discipline-mode: field — none, flat, tiered. See the
Profile guide for
selection criteria.
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 |
Discipline | No | Single | ADR-017 — author-asserted discipline kind (software, hardware, …). Overrides the classifier’s derivation channels. |
Discipline-frozen | No | Single | ADR-017 — cached snapshot of a past derivation: <kind> or <kind> @ <YYYY-MM-DD>. Excluded from re-derivation. |
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 fmt |
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 fmt. An entry without an Id: is “unstamped” —
the validator emits MSL-A010 in strict mode, and markspec fmt 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 fmt:
# 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 fmt 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 fmt)
Labels: ASIL-B, safety-critical
markspec fmt 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) |
SoftwareInterface | (none typical) | Provided-by, Required-by, Realized-by (generated) |
HardwareInterface | Bus-protocol, Voltage-level, Connector-type, Signal-direction | Provided-by, Required-by, 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 |
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 fmt 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 fmt):
- [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 | Depended-on-by | Component, Unit | Component |
Part-of | Has-part | Component, Unit | Component |
Provides | Provided-by | Component | Contract |
Requires | Required-by | Component | Contract |
Allocated-to | Allocates | Requirement | Component |
Realizes | Realized-by | Component, Unit | Contract, Requirement |
Generated-from | (none) | any | any |
Addresses | Addressed-by | Requirement | Change |
Provides / Requires are the symmetric provider/consumer relations between a
component and a Contract (including SoftwareInterface /
HardwareInterface): a provider Provides the contract, a consumer Requires
it. Allocated-to is not reused for provider-hosting — it keeps its single
requirement-allocation meaning.
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.
Allocated-to drives discipline classification. Per
ADR-017 the toolchain
walks Allocated-to edges from a requirement to the components it is allocated
to and tags the requirement with each reached discipline. The result lands on
Entry.derivedDiscipline? in compile output. Authors override the derived value
with the Discipline: trailer attribute and freeze it with
Discipline-frozen:.
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. The example
below uses a fictional @acme/compliance profile to illustrate the manifest
shape — MarkSpec does not bundle an ISO 26262 or ASPICE profile; see
Bundled profiles for what actually ships:
id: "@acme/compliance"
version: 1.0.0
description: "ISO 26262 compliance vocabulary"
extends: "@markspec/profile-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/profile-default
↓ extends
@acme/compliance
↓ 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/profile-default"
- "@acme/compliance"
- "./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: @acme/compliance@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.
Discipline kinds (profile.kinds)
A kind is a named engineering discipline — such as software, hardware,
or mechanical — used to classify entries for discipline-aware filtering,
reporting, and export. Kinds are declared in the profile.kinds map and
referenced from individual type declarations via a discipline: field.
See ADR-017 — Discipline Classification for the rationale and the full classification channel algorithm.
Kind declaration syntax
Each entry in profile.kinds maps a kind name to one of three YAML shapes:
profile:
kinds:
firmware:
description: Embedded firmware modules
mechanical: "Mechanical components and assemblies"
avionics:
types:
SoftwareRequirement:
extends: Requirement
discipline: software
FirmwareUnit:
extends: SoftwareUnit
# inherits 'software' from SoftwareUnit
| Shape | Meaning |
|---|---|
<name>: (null value) | Kind exists; no description |
<name>: "string" | Kind exists; bare string is the description shorthand |
<name>:\n description: "string" | Kind exists; description as a sub-field |
Kind-name lexical rule
Kind names must match ^[a-z][a-z0-9-]*$: lowercase letters, digits, and
hyphens, starting with a letter. The name mixed is reserved by the core engine
and may not be declared in a profile.
Per-type discipline: field
A type declaration may carry a discipline: field naming the kind that the type
belongs to:
profile:
types:
SoftwareRequirement:
extends: Requirement
discipline: software
The named kind must exist in the union of core-declared kinds and all
chain-declared kinds; referencing an unknown kind is PROFILE-DISCIPLINE-004.
Auto-inheritance. When a type does not carry discipline: explicitly, the
registry is built at compile time from the merged profile chain: the engine
walks the extends: chain upward until it finds a type with an explicit
discipline: assignment, or reaches a core type. Concretely, a type only needs
discipline: when it is either introducing a new kind or reassigning the kind
inherited from its parent.
Redeclaring core kinds
The core engine ships with three built-in kinds: system, software, and
hardware. Declaring one of these names in a profile does not change their
semantics but produces a PROFILE-DISCIPLINE-003 warning (the declaration is
idempotent and ignored). Declare a new kind name to introduce additional
disciplines such as firmware, mechanical, or avionics.
Discipline override and freeze
Profiles that declare additional kinds via profile.kinds: extend the
effective kind set consulted by the universal Discipline: and
Discipline-frozen: attributes (see ADR-017 Implementation backlog items 4 and
5). When an author writes Discipline: firmware on an entry, the classifier
accepts the value (channel 1, lenient) and the validator
(MSL-T025) succeeds when firmware is in
the effective kind set — i.e., declared by the active profile chain or one of
the core system / software / hardware kinds.
The freeze attribute follows the same rule for its kind component; see ADR-017 for the full lexical grammar and validator interaction matrix.
Discipline mode
The optional profile.discipline-mode: field declares whether the profile tiers
requirements by discipline (tiered), keeps them flat with discipline derived
via allocation (flat), or doesn’t use discipline at all (none). When
omitted, MarkSpec infers the mode from the profile’s type graph, in order:
tiered— any requirement-shaped type declaresdiscipline:.flat— the profile declares extended kinds (profile.kinds:), extends a core discipline-bearing type (SoftwareComponent,HardwareComponent,SoftwareInterface,HardwareInterface,SoftwareUnit,HardwareUnit), or declares any requirement-shaped type at all (even withoutdiscipline:).none— otherwise; the profile contributes nothing discipline-relevant.
The resolved mode shapes three behaviours today:
markspec doctorreports the mode and per-discipline entry counts.- The LSP entry-block scaffold completion lists mode-recommended types first.
markspec createprints a hint when the requested type isn’t recommended for the active mode.
ADR-017 Slice 4 will additionally gate the mixed-allocation validator rule and
the reporter --group-by discipline default on this flag.
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
MarkSpec ships one bundled profile:
| Profile | Purpose |
|---|---|
@markspec/profile-default | RFC 2119 modal keywords, Reference-url/Reference-document/License attributes, core relations (Satisfies, Verifies, …), DRAFT/RELEASED labels |
There is no bundled ISO 26262 or ASPICE profile. A compliance profile (ASIL
labels, functional-safety relations, ASPICE process attributes, …) is something
a project or organization authors itself, either as a local profile directory
(markspec profile new) or one it publishes and version-pins via
markspec.lock (see
ADR-022). The
@acme/compliance example used earlier on this page illustrates that shape; it
is not a real package.
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 check.
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.
All active rules
The Stage-2 prose-analysis build (ADR-021) ships the rules below. Severity and
score contribution drive the project-level roll-up surfaced by
markspec lint --format json (band counts + mean; no trend artifact in core, by
design — see
ADR-021).
| Code | Slug | Surface | Severity | Score |
|---|---|---|---|---|
| MSL-Q100 | ears-no-pattern | EARS | info | 1 |
| MSL-Q101 | ears-missing-actor | EARS | warning | 3 |
| MSL-Q102 | ears-negative-response | EARS | info | 1 |
| MSL-Q103 | ears-stacked-preconditions | EARS | warning | 3 |
| MSL-Q104 | ears-malformed-attempt | EARS | info | 1 |
| MSL-Q200 | modal-multiple | Modal sentence | warning | 3 |
| MSL-Q201 | modal-soft-in-normative | Modal sentence | info | 1 |
| MSL-Q300 | incose-r2-active-voice | Passive voice | warning | 3 |
| MSL-Q301 | incose-r3-subject-verb | Passive voice | info | 1 |
| MSL-Q302 | incose-r7-vague-term | INCOSE lexicon | warning | 3 |
| MSL-Q303 | incose-r8-escape-clause | INCOSE lexicon | warning | 3 |
| MSL-Q304 | incose-r9-open-ended | INCOSE lexicon | info | 1 |
| MSL-Q305 | incose-r10-superfluous-infinitive | INCOSE lexicon | info | 1 |
| MSL-Q310 | incose-r26-absolute | INCOSE lexicon | info | 1 |
| MSL-Q313 | incose-r16-not | INCOSE lexicon | info | 1 |
| MSL-Q400 | struct-title-length | Structural | info | 1 |
| MSL-Q401 | struct-body-length | Structural | info | 1 |
| MSL-Q500 | xref-glossary-undefined | Flagship cross-ref | warning | 3 |
| MSL-Q900 | disable-without-rationale | Suppression hygiene | warning | — |
| MSL-Q901 | disable-unknown-rule | Suppression hygiene | warning | — |
| MSL-Q902 | disable-unused | Suppression hygiene | info | — |
The flagship rule MSL-Q500 xref-glossary-undefined flags capitalized
proper-noun usage in normative prose that has no corresponding Definition
entry in the glossary. It ships with a glossary-only subset resolver
($Identifier / RIDL rules degrade-to-silent until ADR-016 marker pass lands).
The default severity is warning, and core ships an English-baseline allowlist
of universally-true capitalized words (calendar / geography / languages) at
packages/markspec/core/lexicons/capitalized-allow.txt — list-additive across
profile tiers, capped, and explicitly excludes domain vocabulary and
standards-body acronyms.
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 fmt) 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 fmt' 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). A disable that did not match any
diagnostic during a run fires MSL-Q902 (disable-unused) — one diagnostic per
unused code at the entry’s location — so stale escape hatches get pruned. These
three 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 check; the pre-commit hook (see the
git hooks recipe) 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": {
"release": "0.6.0",
"coreSchema": 1
},
"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.release | string | MarkSpec release version (informational only) |
generator.coreSchema | integer | Core schema version; currently 1 |
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 | string[] | Declared upstream reference URLs (see Federation) |
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.",
"bodyTokens": [
{ "kind": "modal", "value": "shall", "line": 42, "column": 22 }
],
"derivedDiscipline": ["software"],
"source": { "kind": "markdown" },
"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>",
"source.type": "markdown"
}
}
For an in-source entry (Rust ///, Kotlin /**, etc.), source is the
doc-comment variant:
{
"source": {
"kind": "doc-comment",
"language": "rust",
"function": "swt_brk_0001_debounce_filters_noise",
"rule": "outer-doc-comment"
}
}
| 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) |
bodyTokens | BodyToken[] | Flat token stream — modal verbs, EARS triggers, Gherkin keywords, $Identifier entity refs, inline code. ADR-016. |
derivedDiscipline | string[] | undefined | Disciplines reached by walking Allocated-to edges. ADR-017. |
source | EntrySource | Tagged union: {kind:"markdown"} or {kind:"doc-comment", language, function, rule}. Pre-0.6.0 was a plain string. |
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 the upstreams this project federates against — the
URLs of the references: upstreams declared in project.yaml. Downstream
projects can resolve trace targets that refer to entries in an upstream
project’s compile output.
{
"federation": ["https://ci.example.com/safety-project/api/"]
}
Upstream entries are never fetched at resolution time. Acquisition happens
once, during markspec lock:
markspec lockreadsproject.yaml’sdependencies:(federated git repositories) andreferences:(published upstream compile-output sites).- It resolves and pins each upstream in
markspec.lock:[[upstream.dependency]]rows for git repositories,[[upstream.registry]]rows for published sites. - It caches each upstream’s compiled snapshot (a
manifest.json+compiled.json/NDJSON pair) under.markspec/cache/upstreams/<id>/. check,compile, and the LSP hydrate those cached snapshots offline — no network access, no live HTTP fetch — so resolution is deterministic.
Federation is read-only and acyclic: a consumer may resolve a trace target into an upstream’s entries, but an upstream’s own re-exported targets never re-enter the consumer’s graph — each entry keeps a single authoritative source. A consumer never re-validates or re-classifies upstream entries, and an upstream cannot modify the consumer’s compile output. See ADR-031.
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, attributes, labels, and rules. Its normative specification lives in this annex; the Profiles and extensions chapter explains how to author, activate, and publish profiles.
B.1 Directory layout
<profile-id>/
├── markspec.yaml ← manifest + declarative content (required)
├── package.json ← required only when publishing to npm (see §B.14)
└── README.md ← recommended
markspec.yaml is authoritative — it is the only file the profile system reads.
When the profile is published to npm, markspec.yaml MUST sit at the package
root: the resolver runs npm pack and reads the manifest from the tarball root.
The markspec.yaml file has two regions: manifest fields (identity,
versioning, distribution) and the profile: content subtree (types,
attributes, labels, colors, conventions, prose, discipline declarations, and
document types).
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 (see §B.11) — local, git, or npm form |
markspec-schema | No | string | Core schema version pin (e.g. "1"); see §B.13 |
profile | No | map | The declarative content subtree (§B.3 onward) |
Any unrecognised top-level key is a PROFILE-LOAD-003 error.
Complete example:
id: "@myorg/safety"
version: 1.2.0
description: "ISO 26262 safety vocabulary"
license: MIT
extends: "npm:@markspec/profile-default@^1"
markspec-schema: "1"
profile:
attributes: []
labels: []
types: {}
documents:
types: []
frontMatter: []
extends:is a specifier, not a bare profile id. It must be one of the three specifier forms in §B.11 (./path,git+…#tag, ornpm:…@range). A bare@org/namewithout a scheme is rejected. Whenextends:is omitted the bundled default profile is spliced in as the implicit chain root (unless the consuming project setsdefault-profile: false).
The profile: block accepts these keys; any other is a PROFILE-LOAD-003
error:
| Block | Section | Purpose |
|---|---|---|
attributes | §B.4 | Universal attributes (apply to every type) |
types | §B.3 | Profile-declared entry types |
labels | §B.6 | Label concerns (flag / enum / set) |
colors | §B.7 | Semantic-name → palette-hue map |
conventions | §B.8 | Tunable engine conventions (e.g. modal-keywords) |
prose | §B.9 | Prose-analysis lexicons |
kinds | §B.10 | Discipline kinds |
discipline-mode | §B.10 | flat / tiered / none |
documents | §B.12 | Document types + front-matter attributes |
delivers | §B.15 | Delivered documents (corpus + docs-only files) |
B.3 Types (profile.types)
Each entry under profile.types declares one profile type. The key is the type
name (lowercase-with-hyphens by convention):
profile:
types:
software-requirement:
extends: Requirement
display-id-pattern: "SRS_{n:4d}"
description: "Software-level normative statement"
discipline: software
hazard:
extends: Risk
display-id-pattern: "HAZ_{n:3d}"
display-id-pattern-enforcement: error
required: [Mitigated-by]
color: hazard-red
traceability:
Mitigated-by:
target: [software-requirement]
cardinality: "1..N"
required: true
Type fields
| Field | Required | Notes |
|---|---|---|
extends | Yes | A core type name (PascalCase, §B.3.1) |
display-id-pattern | No | Pattern string; {n:Nd} is the numeric placeholder (§B.3.2) |
display-id-pattern-enforcement | No | off (default), warn, or error |
description | No | Human-readable purpose shown by markspec profile describe |
required | No | List of attribute/relation keys that MUST be present on this type |
attributes | No | Per-type attribute declarations (same shape as §B.4) |
traceability | No | Per-type relation rules, keyed by link name (§B.5) |
color | No | Semantic color name declared in profile.colors (§B.7) |
discipline | No | Non-empty string naming a kind in core ∪ chain kinds (§B.10) |
Any unrecognised type key is a PROFILE-TYPE-005 error.
Rules
extends:is required. Every profile type names a core-type parent. Omitting it isPROFILE-TYPE-001.- The parent must be a core type.
extends:must resolve to one of the core type names in §B.3.1; an unrecognised value isPROFILE-TYPE-002. - No shadowing. A profile type name must not duplicate a core type name
(
MSL-A040). - Convention. Profile type names use lowercase-with-hyphens; core names use PascalCase. This keeps a name’s origin unambiguous.
B.3.1 Core type names
There are 16 core type names — 4 abstract roots plus 12 concrete subtypes.
Fifteen are instantiable (every name except the purely abstract Item).
extends: on a profile type must name one of these:
| Group | Names |
|---|---|
| Abstract roots (4) | Item, Specification, Component, Unit |
Specification subtypes | Requirement, Test, Contract, Record, Risk |
Contract subtypes | SoftwareInterface, HardwareInterface |
Component subtypes | SoftwareComponent, HardwareComponent |
Unit subtypes | SoftwareUnit, HardwareUnit |
Item subtype | Definition |
Specification, Component, and Unit are abstract roots that are also
directly instantiable (usable as fallbacks when no concrete subtype fits);
Item is the only non-instantiable name.
B.3.2 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 |
{n:04d} | Leading-zero form, equivalent to {n:4d} | STK_AEB_{n:04d} | STK_AEB_0007 |
{name} | Named segment (no counter) — see below | SWC_{name} | SWC_LIGHT_CTRL |
The text before the placeholder is the literal prefix; the text after is the
literal suffix (e.g. REQ-{n:3d}-draft → REQ-012-draft). Width is a minimum,
not a maximum — numbers wider than the pad are left intact. markspec fmt
assigns the next available number; markspec next-id <type> prints it without
writing; markspec create / insert scaffold a full block.
Numbered vs named patterns (ADR-025). A pattern is numbered when it
carries exactly one {n} counter (the mintable, auto-incremented forms above)
or named when it carries no counter. A named pattern classifies types whose
IDs are named, not numbered — components such as SWC_LIGHT_CTRL or HWC_PIU.
It requires a non-empty literal prefix plus a trailing named placeholder (e.g.
SWC_{name}); the named placeholder captures the rest of the display ID,
underscores included. A bare {name} with no literal prefix is rejected — it
would match every ID. Named patterns are classification-only: there is no
counter to mint, so pair them with display-id-pattern-enforcement: off and
author the identifier by hand. markspec next-id / create / insert do not
auto-number a named type — they emit an upper-case placeholder template (e.g.
SWC_NAME, slug-valid so the scaffold still passes markspec check) to fill
in, and the LSP offers a matching ${1:NAME} scaffold completion.
A malformed pattern — more than one counter, an invalid or zero-width padding
specifier, a counter-less pattern with no literal prefix, or a duplicate named
placeholder — is a PROFILE-TYPE-008 error reported when the profile loads, not
an uncaught failure during validation.
display-id-pattern-enforcement controls whether an entry whose display ID does
not match the pattern is ignored (off), warned (warn), or rejected
(error).
B.4 Attributes (profile.attributes and per-type attributes)
profile.attributes declares universal attributes (valid on every type);
the identical shape under a type’s attributes: field declares type-scoped
attributes.
profile:
attributes:
- name: ASIL
type: enum
values: [QM, ASIL-A, ASIL-B, ASIL-C, ASIL-D]
required: false
description: "Automotive Safety Integrity Level"
- name: Mitigated-by
type: id-list
cardinality: "1..N"
inverse:
name: Mitigates
category: relation
Attribute fields
| Field | Required | Notes |
|---|---|---|
name | Yes | Trailer key name (e.g. ASIL, Mitigated-by) |
type | Yes | A value type from the table below |
required | No | Boolean; true makes the attribute mandatory (default false) |
cardinality | No | "lower..upper", e.g. "0..1", "1..1", "1..N". Defaults per value type |
values | enum | Required for type: enum; the closed value list (see grouping note) |
inverse | No | Only for id / id-list: {name, category} — the generated reverse edge |
description | No | Human-readable purpose |
Any unrecognised attribute key is a PROFILE-LOAD-003 error.
Value types (type:)
id, id-list, uri, url, path, path-or-id, enum, tag-list, text,
citation, external-id, integer, date, boolean.
Default cardinality is 0..N for the list types (id-list, tag-list) and
0..1 for every other type. A cardinality: string overrides the default; its
upper bound must be N (unbounded) or an integer ≥ the lower bound.
enum value grouping
An enum’s values: list may be bare strings, {name, description?} mappings,
or {group, description?, values: [...]} group objects (recursed). Group labels
and descriptions are documentation-only — validation matches against the leaf
value names.
B.5 Relations (per-type traceability + attribute inverse)
There is no top-level
profile.relationsblock. Relations are expressed two ways: per-type traceability rules, and per-attribute inverses.
Per-type traceability
A type’s traceability: field maps a trace-link key (e.g. Satisfies,
Mitigated-by) to a rule constraining its targets:
profile:
types:
software-requirement:
extends: Requirement
traceability:
Satisfies:
target: [system-requirement]
cardinality: "1..N"
required: true
description: "Each SRS satisfies at least one system requirement"
Trace-rule fields
| Field | Required | Notes |
|---|---|---|
target | Yes | Non-empty list of matchers: a type-name string, or {shape: identified|referenced} |
cardinality | No | "lower..upper"; defaults to 0..N |
required | No | Boolean (default false) |
description | No | Human-readable purpose |
Any unrecognised trace-rule key is a PROFILE-LOAD-003 error.
markspec profile show and markspec profile describe relation <key> surface
the resolved relations.
Attribute inverse
An id / id-list attribute may declare an inverse: ({name, category}).
MarkSpec materialises the reverse edge in compiled output, so a forward
Mitigated-by produces an inverse Mitigates on the target. inverse on a
non-id attribute is a PROFILE-LOAD-003 error.
B.6 Labels (profile.labels)
profile.labels may take two forms.
Form A — a list of names (each becomes a flag concern):
profile:
labels:
- DRAFT
- RELEASED
Form B — a mapping keyed by concern name, for enum / set concerns or to
attach descriptions:
profile:
labels:
DRAFT: "Work in progress; not reviewed" # string shorthand → flag
asil:
kind: enum
description: "Automotive Safety Integrity Level"
values:
QM: "Quality-managed"
ASIL-A: null
ASIL-B: { description: "Integrity level B" }
Label-concern fields (Form B)
| Field | Required | Notes |
|---|---|---|
kind | No | flag (default), enum, or set |
description | No | Human-readable meaning |
values | No | Mapping of value name → null | string | {description}. Not valid for flag |
List entries in Form A may also be grouped objects ({group, values: [...]});
groups are flattened to their leaf names.
B.7 Colors (profile.colors)
profile.colors maps a semantic color name to a palette hue. Type declarations
reference a semantic name via their color: field (§B.3).
profile:
colors:
requirement-blue: blue
hazard-red: red
- Each key must match
^[a-z][a-z0-9-]*$(MSL-PROFILE-COLOR-004otherwise). - Each value must be a palette hue name; an unknown hue is
MSL-PROFILE-COLOR-002.
B.8 Conventions (profile.conventions)
profile.conventions tunes engine conventions. Each key is a convention name
mapping to a settings object (plus an optional description).
profile:
conventions:
modal-keywords:
casing: rfc2119 # rfc2119 | iso | preserve
description: "Require lowercase shall/should/may"
The only recognised convention is modal-keywords; unknown convention names are
accepted with a PROFILE-LOAD-003 warning (forward compatibility). For
modal-keywords, the casing setting must be rfc2119, iso, or preserve.
B.9 Prose lexicons (profile.prose)
profile.prose.lexicons supplies project vocabulary to the prose-analysis
rules:
profile:
prose:
lexicons:
capitalized-allow: [API, ECU, LiDAR] # allowed mid-sentence capitals
sentence-abbrev: ["e.g.", "i.e.", "etc."] # non-terminal abbreviations
Both lists default to empty when absent.
B.10 Discipline kinds and mode
profile.kinds (map)
The optional profile.kinds map declares engineering disciplines that types may
be assigned to via their discipline: field. See
ADR-017 — Discipline Classification.
| Property | Required | Type | Notes |
|---|---|---|---|
| (key) | — | ^[a-z][a-z0-9-]*$ | Kind name; must not be mixed |
| (value) | — | null | string | KindDecl | null (declare only), string (description), or {description} |
When absent, only core-declared kinds are available.
Diagnostics:
| Code | Severity | Trigger |
|---|---|---|
PROFILE-DISCIPLINE-001 | error | Kind name does not match ^[a-z][a-z0-9-]*$ |
PROFILE-DISCIPLINE-002 | error | Kind name is the reserved word mixed |
PROFILE-DISCIPLINE-003 | warning | Kind name duplicates a core-declared kind (idempotent; declaration ignored) |
Per-type discipline:
A type may carry discipline: to assign it to a named kind (§B.3). The value
must be a non-empty string (PROFILE-DISCIPLINE-005) naming a kind in the union
of core kinds and chain-declared kinds (PROFILE-DISCIPLINE-004). When omitted,
the discipline is inherited by walking the extends: chain upward.
profile.discipline-mode
A profile may declare discipline-mode: to make its tiering intent explicit:
profile:
discipline-mode: tiered # flat | tiered | none
When omitted, the mode is inferred from the type graph: tiered when any
requirement-shaped type carries discipline:, flat when discipline-bearing
types exist without per-type assignment, otherwise none.
Diagnostics:
| Code | Severity | Trigger |
|---|---|---|
PROFILE-DISCIPLINE-006 | error | Value is not one of flat, tiered, none (case-sensitive) |
PROFILE-DISCIPLINE-007 | error | Value is not a scalar string |
B.11 Distribution and specifiers
A profile is referenced — in a project’s .markspec.yaml profiles: list and
in a manifest’s extends: field — using one of three specifier schemes:
| Specifier form | Resolves to |
|---|---|
./path/to/profile | Local directory relative to the declaring file |
git+<https|file>://host/repo.git[/subpath]#<tag> | Git source; shallow + sparse clone, cached globally |
npm:[@scope/]name@<version-range> | npm package; resolved via npm pack, cached globally |
Git auth is inherited from the user’s git configuration; npm resolution uses the
registry configured in .npmrc. The jsr: and raw https: schemes are
reserved for a future release.
markspec profile add <spec> validates the specifier and records it in
.markspec.yaml. It does not copy the profile into the repository — git and
npm sources are fetched and cached on demand when a profile-aware command runs.
B.12 Documents (profile.documents)
profile.documents declares document types and front-matter attributes (per
ADR-007):
profile:
documents:
types:
- id: srs-document
contains: [software-requirement]
description: "Software requirements specification"
frontMatter:
- name: classification
type: enum
values: [public, internal, confidential]
documents.types[].id is required; contains is a list of type names;
frontMatter uses the attribute shape from §B.4.
B.13 Versioning and compatibility
Core schema pin (markspec-schema)
markspec-schema: "1" pins the profile against version 1 of the core schema
contract. A profile whose pin exceeds the running binary’s CORE_SCHEMA_VERSION
is rejected (PROFILE-SCHEMA-001). When the pin is absent the profile loads
with a PROFILE-SCHEMA-002 warning recommending you add it.
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 | minor |
Add new relation with inverse | minor |
Make an attribute required: true | major |
| Remove a type, attribute, or relation | major |
| Rename a key | major |
Tighten cardinality | major |
Composition
A project’s .markspec.yaml accepts at most one content-bearing profile;
declaring more than one is a PROFILE-LOAD-006 error and no chain loads.
Compose standards by publishing a pre-merged profile, or by chaining via a
manifest’s single-parent extends: field. When profiles: is empty the bundled
default profile loads automatically unless the project sets
default-profile: false.
B.14 Validation and publishing
Validate a profile manifest before distributing it:
markspec profile publish --dir ./my-profile
profile publish parses the manifest and reports:
- YAML / schema errors (
PROFILE-LOAD-002,PROFILE-LOAD-003, thePROFILE-TYPE-*andPROFILE-DISCIPLINE-*families, …) markspec-schemamismatch (PROFILE-SCHEMA-001) or absence (PROFILE-SCHEMA-002)- Missing
description(PROFILE-PUB-001) andlicense(PROFILE-PUB-002) as warnings
It exits non-zero on any error. profile publish validates only — it does not
upload to a registry. Distribute the validated directory with git (commit +
tag) or npm (npm publish with markspec.yaml at the package root and a
package.json whose files includes it). See
Authoring and publishing a profile
in the guide for the end-to-end workflow.
B.15 Delivered documents (profile.delivers)
profile.delivers lists document files the profile ships to consuming projects
(per ADR-030). Each
file is flagged per file: a corpus file’s entries join the consumer’s
traceability graph (marked with Entry.origin provenance); a
documentation-only file is surfaced for reading, never parsed:
profile:
delivers:
- path: reference/platform-architecture.md
corpus: true
description: "Shared platform components and interfaces"
- path: reference/integration-guide.md
# corpus defaults to false → documentation-only
Delivers-item fields
| Field | Required | Type | Notes |
|---|---|---|---|
path | Yes | string | Relative to the profile directory; no .., no absolute paths |
corpus | No | boolean | Default false; true → entries join the graph (Markdown only) |
description | No | string | Shown by profile show and as the MCP resource description |
Any unrecognised item key, a non-list delivers:, a non-mapping item, or a
missing/empty path is a PROFILE-LOAD-003 error.
Rules
- The path must stay inside the profile directory. An absolute path (POSIX
or drive-letter) or any
..segment isPROFILE-DELIVERS-003. - Only Markdown is corpus-eligible.
corpus: trueon a non-.mdpath isPROFILE-DELIVERS-004. Docs-only files may be any readable file. - No duplicate paths. The same
pathtwice in one manifest isPROFILE-LOAD-003. - Merge across the chain is additive. The effective list is the union of
every tier’s declarations, keyed by
(profile-id, path), parent-tier first. A child cannot remove or override a parent’s delivered file; two tiers delivering the same relative path do not collide.
Load-time diagnostics
File existence is checked when the delivered corpus loads (every graph-consuming
command, the LSP, and the MCP server), raising PROFILE-DELIVERS-001/-002.
The same load also enforces a containment guard: a delivered file whose real,
symlink-resolved path escapes the profile package is refused and raises
PROFILE-DELIVERS-005 — the structural path check (-003) validates the
declared string, but a symlink can still point the resolved file outside the
package, so the real path is checked before the file is read. The table also
lists the two structural errors raised earlier, at manifest-parse time, when the
delivers: block is validated: PROFILE-DELIVERS-003 (a path escaping the
profile directory) and PROFILE-DELIVERS-004 (corpus: true on a non-Markdown
file).
| Code | Severity | Meaning |
|---|---|---|
PROFILE-DELIVERS-001 | error | Corpus file declared but missing from package |
PROFILE-DELIVERS-002 | warning | Docs-only file declared but missing |
PROFILE-DELIVERS-003 | error | path escapes the profile directory |
PROFILE-DELIVERS-004 | error | corpus: true on a non-Markdown file |
PROFILE-DELIVERS-005 | error | Delivered file’s real path escapes the package (symlink) |
A project entry re-declaring a display ID or Id: owned by a delivered corpus
entry fails validation with MSL-R014 (language spec §8.2) — the fix is to
rename the project entry; delivered corpus entries are read-only.