Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MarkSpec Model Reference

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

Two-layer architecture

The model is divided into two layers:

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

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

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

Key concepts

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

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

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

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

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

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

Reading guide

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

Entry format

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

Anatomy of an entry

- [SRS_BRK_0107] Sensor debouncing
         ↑              ↑
   display ID         title

  The sensor driver shall debounce raw inputs
  to eliminate noise.

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

Title line

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

- [DISPLAY_ID] Title text

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

Display ID

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

Valid examples:

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

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

Body text

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

- [SRS_BRK_0107] Sensor debouncing

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

  A secondary paragraph can elaborate further.

      Id: 01HGW2Q8MNP3RSTVWXYZABCDEF

Style rules for body text:

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

Trailer block

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

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

Rules:

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

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

In-source entries

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

Kotlin / Java / C example:

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

Rust example:

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

Supported languages and comment styles:

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

The markspec 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 typePurposeExtended by
ItemRoot; ultimate fallback for all type resolutionAll 15 concrete types; any profile subtype
SpecificationNormative statements; things that must or shallRequirement, Test, Contract, Record, Risk
ComponentSystem-level building blocks with identitySoftwareComponent, HardwareComponent
UnitFine-grained implementation-level elementsSoftwareUnit, HardwareUnit

Concrete types — Specification subtypes

Requirement

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

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

  The sensor driver shall debounce raw inputs to eliminate noise.

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

Test

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

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

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

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

Contract

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

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

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’s Provides / Requires); Realized-by (generated inverse of Realizes); Verified-by (generated inverse of Verifies).

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 of Mitigates from a Requirement).
  • Typical profile attributes: ASIL, Severity, Probability
- [HAZ_BRK_0003] Loss of braking under wheel-lock condition

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

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

Concrete types — Component subtypes

SoftwareComponent

A software module, service, or subsystem.

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

HardwareComponent

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

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

Concrete types — Unit subtypes

SoftwareUnit

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

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

HardwareUnit

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

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

Concrete types — Item subtypes

Definition

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

Objective

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

  • Typical profile attributes: Priority

Standard

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

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

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

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

Change

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

  • Typical profile attributes: Priority, Status

Type resolution chain

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

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

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

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

Two entry shapes

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

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

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

Profile subtypes

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

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

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

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

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).

AttributeRequiredCardinalityValue
IdYesSingleBare ULID (Authored shape) or URI with scheme (Reference shape)
TypeNoSingleCore or profile-declared type name
LabelsNoMultiFree-form tags; one Labels: line per tag
DisciplineNoSingleADR-017 — author-asserted discipline kind (software, hardware, …). Overrides the classifier’s derivation channels.
Discipline-frozenNoSingleADR-017 — cached snapshot of a past derivation: <kind> or <kind> @ <YYYY-MM-DD>. Excluded from re-derivation.
ReferencesNoMultiDisplay ID, optionally followed by a locator in […]
External-idNoSingleCross-system identifier (Jira, DOORS, …)
SupersedesNoSingleDisplay ID of the predecessor entry (Authored only)
Superseded-byGeneratedInverse of Supersedes; written by markspec fmt
DeprecatedNoSingleQuoted retirement reason string

Id

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

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

ULIDs are assigned by markspec 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:

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

Example:

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

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

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

Typical attributes by concrete type

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

Specification types

TypeTypical profile attributesTypical relation attributes
RequirementASIL, Priority, Safety-goalSatisfies, Derived-from
TestTest-level, Test-type, Test-resultVerifies, Tests
Contract(none typical)Satisfies, Realized-by (generated)
SoftwareInterface(none typical)Provided-by, Required-by, Realized-by (generated)
HardwareInterfaceBus-protocol, Voltage-level, Connector-type, Signal-directionProvided-by, Required-by, Realized-by (generated)
Record(none typical)(standalone)
RiskASIL, Severity, ProbabilityMitigated-by (generated)

Component types

TypeTypical profile attributesTypical relation attributes
SoftwareComponentVersion, License, SupplierPart-of, Depends-on
HardwareComponentVersion, SupplierPart-of

Unit types

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

Item types

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

Profile-declared attributes

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

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

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

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

Attribute ordering

markspec fmt normalizes the trailer attribute order:

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

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

Trace relations

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

Writing a relation

Relations look like any other trailer attribute — RelationName: TARGET_DISPLAY_ID. Multiple targets are written as one line per target (or CSV, normalized by markspec 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_0042
  • SRS_BRK_0107 --[Derived-from]--> STK_BRK_0003

Multiple targets for the same relation:

Satisfies: SYS_BRK_0042 Satisfies: SYS_BRK_0043

Generated inverses

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

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

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

Core relations

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

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

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:

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

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

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

Profiles and extensions

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

What a profile declares

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

Profile manifest structure

A profile manifest is a markspec.yaml file in a profile directory. 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:

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

The extends chain

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

@markspec/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 produce MSL-T020.
  • Unknown trailer keys produce MSL-A020.
  • No display-ID patterns, no domain relations, no domain attributes.
  • The type-resolution chain uses only steps 1, 5, 6, 7, and 8 (profile steps 2–4 do not apply).

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

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
ShapeMeaning
<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:

  1. tiered — any requirement-shaped type declares discipline:.
  2. 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 without discipline:).
  3. none — otherwise; the profile contributes nothing discipline-relevant.

The resolved mode shapes three behaviours today:

  • markspec doctor reports the mode and per-discipline entry counts.
  • The LSP entry-block scaffold completion lists mode-recommended types first.
  • markspec create prints 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, or Title — these are reserved core attribute keys.
  • Cannot shadow any of the 15 core concrete type names (MSL-A040 is raised for reserved-name conflicts). A profile type named Requirement is forbidden; use requirement (lowercase) as a distinct subtype.
  • Cannot remove core-defined attributes or demote core errors to warnings.
  • Cannot alter shape discrimination — shape is determined by the Id: format alone; no profile rule can change it.
  • Cannot add cardinality constraints to universal attributes (Id, Type, Labels, References, …).

Bundled profiles

MarkSpec ships one bundled profile:

ProfilePurpose
@markspec/profile-defaultRFC 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

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

Scaffolding a new profile

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

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

The scaffold creates:

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

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

Prose analysis

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

CodeSlugSurfaceSeverityScore
MSL-Q100ears-no-patternEARSinfo1
MSL-Q101ears-missing-actorEARSwarning3
MSL-Q102ears-negative-responseEARSinfo1
MSL-Q103ears-stacked-preconditionsEARSwarning3
MSL-Q104ears-malformed-attemptEARSinfo1
MSL-Q200modal-multipleModal sentencewarning3
MSL-Q201modal-soft-in-normativeModal sentenceinfo1
MSL-Q300incose-r2-active-voicePassive voicewarning3
MSL-Q301incose-r3-subject-verbPassive voiceinfo1
MSL-Q302incose-r7-vague-termINCOSE lexiconwarning3
MSL-Q303incose-r8-escape-clauseINCOSE lexiconwarning3
MSL-Q304incose-r9-open-endedINCOSE lexiconinfo1
MSL-Q305incose-r10-superfluous-infinitiveINCOSE lexiconinfo1
MSL-Q310incose-r26-absoluteINCOSE lexiconinfo1
MSL-Q313incose-r16-notINCOSE lexiconinfo1
MSL-Q400struct-title-lengthStructuralinfo1
MSL-Q401struct-body-lengthStructuralinfo1
MSL-Q500xref-glossary-undefinedFlagship cross-refwarning3
MSL-Q900disable-without-rationaleSuppression hygienewarning
MSL-Q901disable-unknown-ruleSuppression hygienewarning
MSL-Q902disable-unusedSuppression hygieneinfo

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 keywords signal the obligation level of a requirement. MarkSpec enforces the RFC 2119 / EARS convention that modals appear in lowercase:

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

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

Fires when any of the above keywords appears in uppercase (SHALL, MUST, …) inside body prose. The formatter (markspec 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.

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

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

GWT pattern

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

Recommended form:

- [SWT_BRK_0030] Debounce rejects short pulses

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

      Id: 01HGW3R9QNP4ABCDEFGHJKMNPQ
      Type: test
      Verifies: 01HGW2Q8MNP3RSTVWXYZABCDEF

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

INCOSE lexicon rules

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

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

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

Example

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

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

Structural quality rules

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

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

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

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

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

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

Suppression

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

- [SRS_BRK_0108] Legacy inherited requirement

  The system shall operate as appropriate for the ambient conditions.

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

Both Markspec-disable and Rationale must be present; a suppression without a rationale fires MSL-Q900 (disable-without-rationale). Citing an unknown rule code fires MSL-Q901 (disable-unknown-rule). 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".

FieldTypeNotes
markspecSchemaVersionintegerSchema version; currently 1
generator.releasestringMarkSpec release version (informational only)
generator.coreSchemaintegerCore schema version; currently 1
project.namestringFrom project.yaml
project.versionstringFrom project.yaml
counts.entriesintegerTotal number of entries
counts.edgesintegerTotal number of edges (including generated inverses)
entries.format"ndjson" | "inline"Which form is present
entries.filestringRelative path to the entry data
edges.format"ndjson" | "inline"Which form is present
edges.filestringRelative path to the edge data
sqliteMirrorstring | nullRelative path to mirror.db, or null
federationstring[]Declared upstream reference URLs (see Federation)
reservedobjectReserved for future use; consumers must ignore

Entry record

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

{
  "displayId": "SRS_BRK_0107",
  "id": "01HGW2Q8MNP3RSTVWXYZABCDEF",
  "shape": "Authored",
  "type": "requirement",
  "title": "Sensor debouncing",
  "body": "The sensor driver shall debounce raw inputs to eliminate noise.",
  "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"
  }
}
FieldTypeNotes
displayIdstringHuman-readable ID, e.g. SRS_BRK_0107
idstring | nullULID or URI; null if no Id: trailer
shape"Authored" | "Reference"Determined by Id: format
typestring | nullResolved type name; null if unresolved
titlestringEntry title text
bodystringEntry body text (trimmed)
bodyTokensBodyToken[]Flat token stream — modal verbs, EARS triggers, Gherkin keywords, $Identifier entity refs, inline code. ADR-016.
derivedDisciplinestring[] | undefinedDisciplines reached by walking Allocated-to edges. ADR-017.
sourceEntrySourceTagged 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
propertiesobjectObserved facts (see Properties namespaces)

Properties namespaces

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

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

Edge record

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

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

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

entries.idx

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

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

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

Small-project form (inline)

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

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

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

Privacy rules

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

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

Schema versioning

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

Compatibility rules:

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

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

Federation

manifest.federation lists 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:

  1. markspec lock reads project.yaml’s dependencies: (federated git repositories) and references: (published upstream compile-output sites).
  2. It resolves and pins each upstream in markspec.lock: [[upstream.dependency]] rows for git repositories, [[upstream.registry]] rows for published sites.
  3. It caches each upstream’s compiled snapshot (a manifest.json + compiled.json/NDJSON pair) under .markspec/cache/upstreams/<id>/.
  4. 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

FieldRequiredTypeNotes
idYesstringScoped identifier: @org/name or name
versionYesstringSemantic version (MAJOR.MINOR.PATCH)
descriptionNostringHuman-readable summary; recommended for publishing
licenseNostringSPDX identifier (e.g. MIT, Apache-2.0); recommended
extendsNostringParent-profile specifier (see §B.11) — local, git, or npm form
markspec-schemaNostringCore schema version pin (e.g. "1"); see §B.13
profileNomapThe 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, or npm:…@range). A bare @org/name without a scheme is rejected. When extends: is omitted the bundled default profile is spliced in as the implicit chain root (unless the consuming project sets default-profile: false).

The profile: block accepts these keys; any other is a PROFILE-LOAD-003 error:

BlockSectionPurpose
attributes§B.4Universal attributes (apply to every type)
types§B.3Profile-declared entry types
labels§B.6Label concerns (flag / enum / set)
colors§B.7Semantic-name → palette-hue map
conventions§B.8Tunable engine conventions (e.g. modal-keywords)
prose§B.9Prose-analysis lexicons
kinds§B.10Discipline kinds
discipline-mode§B.10flat / tiered / none
documents§B.12Document types + front-matter attributes
delivers§B.15Delivered 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

FieldRequiredNotes
extendsYesA core type name (PascalCase, §B.3.1)
display-id-patternNoPattern string; {n:Nd} is the numeric placeholder (§B.3.2)
display-id-pattern-enforcementNooff (default), warn, or error
descriptionNoHuman-readable purpose shown by markspec profile describe
requiredNoList of attribute/relation keys that MUST be present on this type
attributesNoPer-type attribute declarations (same shape as §B.4)
traceabilityNoPer-type relation rules, keyed by link name (§B.5)
colorNoSemantic color name declared in profile.colors (§B.7)
disciplineNoNon-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 is PROFILE-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 is PROFILE-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:

GroupNames
Abstract roots (4)Item, Specification, Component, Unit
Specification subtypesRequirement, Test, Contract, Record, Risk
Contract subtypesSoftwareInterface, HardwareInterface
Component subtypesSoftwareComponent, HardwareComponent
Unit subtypesSoftwareUnit, HardwareUnit
Item subtypeDefinition

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

PlaceholderMeaningExample patternExample output
{n:4d}Auto-increment, minimum 4 digits, paddedSRS_{n:4d}SRS_0042
{n:3d}Auto-increment, minimum 3 digits, paddedHAZ_{n:3d}HAZ_003
{n:04d}Leading-zero form, equivalent to {n:4d}STK_AEB_{n:04d}STK_AEB_0007
{name}Named segment (no counter) — see belowSWC_{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}-draftREQ-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

FieldRequiredNotes
nameYesTrailer key name (e.g. ASIL, Mitigated-by)
typeYesA value type from the table below
requiredNoBoolean; true makes the attribute mandatory (default false)
cardinalityNo"lower..upper", e.g. "0..1", "1..1", "1..N". Defaults per value type
valuesenumRequired for type: enum; the closed value list (see grouping note)
inverseNoOnly for id / id-list: {name, category} — the generated reverse edge
descriptionNoHuman-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.relations block. 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

FieldRequiredNotes
targetYesNon-empty list of matchers: a type-name string, or {shape: identified|referenced}
cardinalityNo"lower..upper"; defaults to 0..N
requiredNoBoolean (default false)
descriptionNoHuman-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)

FieldRequiredNotes
kindNoflag (default), enum, or set
descriptionNoHuman-readable meaning
valuesNoMapping 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-004 otherwise).
  • 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.

PropertyRequiredTypeNotes
(key)^[a-z][a-z0-9-]*$Kind name; must not be mixed
(value)null | string | KindDeclnull (declare only), string (description), or {description}

When absent, only core-declared kinds are available.

Diagnostics:

CodeSeverityTrigger
PROFILE-DISCIPLINE-001errorKind name does not match ^[a-z][a-z0-9-]*$
PROFILE-DISCIPLINE-002errorKind name is the reserved word mixed
PROFILE-DISCIPLINE-003warningKind 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:

CodeSeverityTrigger
PROFILE-DISCIPLINE-006errorValue is not one of flat, tiered, none (case-sensitive)
PROFILE-DISCIPLINE-007errorValue 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 formResolves to
./path/to/profileLocal 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 kindVersion bump
Add new optional attribute / labelminor
Add new typeminor
Add new relation with inverseminor
Make an attribute required: truemajor
Remove a type, attribute, or relationmajor
Rename a keymajor
Tighten cardinalitymajor

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, the PROFILE-TYPE-* and PROFILE-DISCIPLINE-* families, …)
  • markspec-schema mismatch (PROFILE-SCHEMA-001) or absence (PROFILE-SCHEMA-002)
  • Missing description (PROFILE-PUB-001) and license (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

FieldRequiredTypeNotes
pathYesstringRelative to the profile directory; no .., no absolute paths
corpusNobooleanDefault false; true → entries join the graph (Markdown only)
descriptionNostringShown 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 is PROFILE-DELIVERS-003.
  • Only Markdown is corpus-eligible. corpus: true on a non-.md path is PROFILE-DELIVERS-004. Docs-only files may be any readable file.
  • No duplicate paths. The same path twice in one manifest is PROFILE-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).

CodeSeverityMeaning
PROFILE-DELIVERS-001errorCorpus file declared but missing from package
PROFILE-DELIVERS-002warningDocs-only file declared but missing
PROFILE-DELIVERS-003errorpath escapes the profile directory
PROFILE-DELIVERS-004errorcorpus: true on a non-Markdown file
PROFILE-DELIVERS-005errorDelivered 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.