Sign in

    ears

    Immutable EARS requirements and extraction from text and MDC documents

    ears
    requirements
    markdown
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    1 hour ago
    Downloads
    23

    #EARS

    moonrockz/ears parses and represents requirements written in EARS, the Easy Approach to Requirements Syntax. It supports ubiquitous, state driven, event driven, optional feature, unwanted behaviour and complex requirements.

    Import moonrockz/ears as @ears in moon.pkg. To work with an existing MDC document, also import moonrockz/mdc as @mdc.

    #One requirement

    let requirement = @ears.Requirement::Requirement(
    "While enabled, when polled, the sensor shall report temperature.",
    )
    // requirement.pattern() == @ears.Complex
    // requirement.preconditions() == ["enabled"]
    // requirement.trigger() is Some(Event("polled"))
    // requirement.system() == "sensor"
    // requirement.response() == "report temperature."

    parse(source) is an alias for the constructor. Both raise a typed ParseError on invalid syntax. Each case except EmptyRequirement has the SentencePosition in the normalized sentence where the parse failed (error.position()), and EmptyClause names its Keyword (Where, While, When or If). keywords lists the clause keywords. Requirement is opaque and immutable. Accessors expose an optional feature, a read-only sequence of preconditions, an optional event or unwanted trigger (a read-only Trigger), the system name and its response. pattern() derives the classification from those clauses. Multiple clauses produce Complex.

    The accepted grammar is:

    [Where <feature>,] [While <state>, ...] [When <event>, | If <unwanted condition>, then] the <system> shall <response>

    Where comes first, followed by zero or more While clauses, then at most one When or If clause. If requires then; the comma immediately before then is optional. Conditional clauses otherwise need comma separators. Keywords are case-insensitive. ASCII whitespace folds to a single space; the case and punctuation of free text remain intact. A final period is optional and remains part of response(). to_string() renders canonical keyword case and spacing and can be parsed back to an equal requirement.

    Clauses, the system name and the response must be nonempty. Clause contents are free text: the parser does not split English conjunctions into separate states or responses, infer actor identities, or judge whether a requirement is testable. A comma followed by an EARS keyword starts a new clause. In unwanted conditions, then is structural only when followed by the. Avoid other clause-opening keyword sequences inside a clause's free text. Requirement IDs, labels and sentence splitting are outside this grammar.

    #Plain-text specifications

    let spec = @ears.parse_text(
    "The sensor shall measure.\nWhen polled, the sensor shall reply.\n",
    )
    let requirement = spec.requirements()[0].requirement()

    Plain text contains one requirement per nonblank line. Every nonblank line is checked. Invalid lines become Diagnostic values and do not discard valid requirements on other lines. LF, CRLF, CR and a leading BOM are supported. Blank input yields an empty specification. Use the single-requirement constructor for a standalone sentence wrapped across lines.

    #Markdown and MDC

    let spec = @ears.parse_markdown(
    "---\ntitle: Sensor\n---\n- When **polled**, the sensor shall reply.\n",
    )
    // spec.document() retains the immutable MDC document and its frontmatter.

    let document = @mdc.Document::Document("The sensor shall measure.")
    let extracted = @ears.from_mdc(document)

    parse_markdown uses MDC's parser, including its optional YAML mapping header. strict and file pass through to MDC. Invalid frontmatter raises MDC's ParseError. EARS syntax failures appear in spec.diagnostics(). from_mdc uses an existing document without reparsing it.

    Write one requirement per paragraph or list-item paragraph. Paragraphs in blockquotes also count. Wrapped lines join with spaces. Emphasis and link text, inline code, autolinks and inline math supply sentence text. Headings, fenced and indented code blocks, HTML blocks, image alt text, tables, footnotes and frontmatter do not supply requirements.

    Extraction considers paragraphs starting with Where, While, When or If, or starting with The and containing the word shall, should or must. Those candidates either produce requirements or diagnostics. Other prose is ignored. This rule lets documents mix requirements with explanatory text; prose beginning with a conditional keyword can also produce a diagnostic.

    #Source context

    requirements() and diagnostics() return read-only arrays in source order. Each occurrence contains a validated requirement and its Origin. Each diagnostic contains its candidate text, typed error and origin.

    Origin::Text carries an @mdc.SourceRange that excludes indentation and newline characters. Origin::Markdown carries a path that resolves through spec.document().unwrap().root().at(path), and an optional @mdc.SourceRange covering the original paragraph, including inline markup and line wrapping.

    A range gives the positions of its first and last characters, as one-based lines and columns, and half-open UTF-16 offsets in the original input. Frontmatter and a leading BOM contribute to offsets. Columns count UTF-16 code units, including the BOM on the first line. For MDC documents parsed with locs=false, Markdown ranges are absent and paths remain available.

    #Dialects and lexicons

    Without a dialect, every function parses strict EARS: the <system> shall<response>. A Dialect adds vocabulary to the same grammar. Each method has a default, so a dialect gives only what it adds:

    MethodAddsExample
    modalsmodal verbs besides shall; with a dialect, not after one negates itmust
    determinerswords that start a subject besides theeach, an
    pronounswords that stand for a systemit
    system_nounsphrases that are a whole subjectMission Control
    lead_insphrases before the clausesotherwise, for each
    headingsMarkdown headings that start a requirement blocklevel 3, Requirement:

    parse(source, dialects=[...]), parse_text, parse_block, parse_markdown and from_mdc take a list of dialects and use the union of their vocabularies. Keywords match without letter case. A Requirement records its lead_in(), its subject() (Determined, Noun or Pronoun), its opener() and its modal() (verb and negation). system() gives the system, or for a pronoun the system that block parsing resolved it to. to_string() renders the requirement so that parsing it again with the same dialects gives an equal requirement.

    With a dialect, a comma inside a clause that a subject word follows (such as a or that) ends the clause only when it is the last such comma before the modal verb, and an If clause ends only at then. Without a dialect, both rules stay as in strict EARS, and not stays in the response.

    parse_block(text, dialects=[...]) splits a requirement text into sentences: a sentence ends at a blank line, or at ., ? or ! that white space or the end follows, outside backticks. Closing brackets and quote marks after the mark belong to the sentence. A pronoun subject refers to the system of the nearest earlier sentence; without one, the requirement stays and an UnresolvedPronoun diagnostic names it. With heading rules, parse_markdown gives one RequirementBlock for each matching heading (blocks()): its name, its heading range, and the requirements of the paragraphs up to the next heading.

    A Lexicon is a dialect as data. Build one in code with Lexicon(systems=[...]), or parse a lexicon file with Lexicon::from_yaml(text):

    name: mission-control modals: [must] determiners: [each] pronouns: [it] systems: [Mission Control] lead_ins: [otherwise, for each] headings: - level: 3 prefix: "Requirement:"

    Words (Word) have no white space, comma or full stop; phrases (Phrase) are words with single spaces. An unknown key, a value of the wrong kind and an invalid entry raise LexiconError with the line. openspec_dialect() gives the OpenSpec dialect: shall and must, the usual determiners, it and they, the lead-in otherwise, and the ### Requirement: heading.

    #Tests

    Focused regressions live beside the parser. Public behaviour has Gherkin scenarios in tests/ears-acceptance/ and reproducible generated laws in tests/ears-laws/.

    mise exec -- moon test pkgs/ears tests/ears-acceptance tests/ears-laws --target js

    Dialect

    pub(open) trait Dialect {
    fn name(Self) -> String
    fn modals(Self) -> Array[Word] = _
    fn determiners(Self) -> Array[Word] = _
    fn pronouns(Self) -> Array[Word] = _
    fn system_nouns(Self) -> Array[Phrase] = _
    fn lead_ins(Self) -> Array[Phrase] = _
    fn headings(Self) -> Array[HeadingRule] = _
    }

    A dialect adds vocabulary to the EARS grammar: modal verbs, subject openers (determiners and pronouns), system nouns, lead-in phrases and Markdown requirement headings. Every method except name has a default, so a dialect gives only what it adds. Several dialects combine: the parser uses the union of their vocabularies.

    LexiconError

    pub(all) suberror LexiconError {
    InvalidWord(String)
    InvalidPhrase(String)
    InvalidHeadingPrefix(String)
    InvalidHeadingLevel(Int)
    InvalidLexiconFile(line~ : Int, message~ : String)
    UnknownKey(key~ : String, line~ : Int)
    WrongType(key~ : String, line~ : Int, expected~ : ValueKind)
    InvalidEntry(key~ : String, line~ : Int, error~ : LexiconError)
    } derive(Eq,
    Debug
    )

    Raised when a lexicon value breaks a rule.

    LexiconError::equal

    LexiconError::not_equal

    fn LexiconError::not_equal(x : LexiconError, y : LexiconError) -> Bool

    ParseError

    pub(all) suberror ParseError {
    EmptyRequirement
    ExpectedSystem(at~ : SentencePosition)
    MissingShall(at~ : SentencePosition)
    MissingResponse(at~ : SentencePosition)
    EmptyClause(keyword~ : Keyword, at~ : SentencePosition)
    MissingSeparator(at~ : SentencePosition)
    MissingThen(at~ : SentencePosition)
    InvalidClauseOrder(at~ : SentencePosition)
    UnresolvedPronoun(pronoun~ : Word, at~ : SentencePosition)
    } derive(Eq,
    Debug
    )

    Why a sentence is not an EARS requirement. Each case except EmptyRequirement has the position in the normalized sentence where the parse failed.

    ParseError::equal

    fn ParseError::equal(ParseError, ParseError) -> Bool

    ParseError::not_equal

    fn ParseError::not_equal(x : ParseError, y : ParseError) -> Bool

    ParseError::position

    fn ParseError::position(self : ParseError) -> SentencePosition?

    The position where the parse failed, or None for an empty sentence.

    PositionError

    pub(all) suberror PositionError {
    NegativeOffset(Int)
    } derive(Eq,
    Debug
    )

    Raised when an offset is not a position in a sentence.

    PositionError::equal

    PositionError::not_equal

    fn PositionError::not_equal(x : PositionError, y : PositionError) -> Bool

    Diagnostic

    type Diagnostic derive(Eq,
    Debug
    )

    Diagnostic::equal

    fn Diagnostic::equal(Diagnostic, Diagnostic) -> Bool

    Diagnostic::error

    fn Diagnostic::error(self : Diagnostic) -> ParseError

    Diagnostic::not_equal

    fn Diagnostic::not_equal(x : Diagnostic, y : Diagnostic) -> Bool

    Diagnostic::origin

    fn Diagnostic::origin(self : Diagnostic) -> Origin

    Diagnostic::text

    fn Diagnostic::text(self : Diagnostic) -> String

    HeadingPrefix

    type HeadingPrefix derive(Eq, Hash,
    Debug
    )

    The start of a Markdown heading text that marks a requirement, such as Requirement:: not empty, with no line break.

    HeadingPrefix::HeadingPrefix

    fn HeadingPrefix::HeadingPrefix(text : String) -> HeadingPrefix raise LexiconError

    HeadingPrefix::equal

    HeadingPrefix::hash

    fn HeadingPrefix::hash(self : HeadingPrefix) -> Int

    HeadingPrefix::hash_combine

    fn HeadingPrefix::hash_combine(HeadingPrefix, Hasher) -> Unit

    HeadingPrefix::not_equal

    fn HeadingPrefix::not_equal(x : HeadingPrefix, y : HeadingPrefix) -> Bool

    HeadingPrefix::to_string

    fn HeadingPrefix::to_string(self : HeadingPrefix) -> String

    HeadingRule

    type HeadingRule derive(Eq, Hash,
    Debug
    )

    A Markdown heading that starts a requirement block: a heading of level whose text starts with prefix, with letter case. The rest of the text names the block.

    HeadingRule::HeadingRule

    HeadingRule::equal

    fn HeadingRule::equal(HeadingRule, HeadingRule) -> Bool

    HeadingRule::hash

    fn HeadingRule::hash(self : HeadingRule) -> Int

    HeadingRule::hash_combine

    fn HeadingRule::hash_combine(HeadingRule, Hasher) -> Unit

    HeadingRule::not_equal

    fn HeadingRule::not_equal(x : HeadingRule, y : HeadingRule) -> Bool

    HeadingRule::prefix

    fn HeadingRule::prefix(self : HeadingRule) -> HeadingPrefix

    Keyword

    pub(all) enum Keyword {
    Where
    While
    When
    If
    } derive(Eq, Hash,
    Debug
    )

    A clause keyword of EARS.
    impl Show for Keyword

    Keyword::equal

    fn Keyword::equal(Keyword, Keyword) -> Bool

    Keyword::hash

    fn Keyword::hash(self : Keyword) -> Int

    Keyword::hash_combine

    fn Keyword::hash_combine(Keyword, Hasher) -> Unit

    Keyword::not_equal

    fn Keyword::not_equal(x : Keyword, y : Keyword) -> Bool

    Keyword::output

    fn Keyword::output(self : Keyword, logger : &Logger) -> Unit

    Keyword::to_repr

    Keyword::to_string

    fn Keyword::to_string(self : Keyword) -> String

    The keyword in canonical case: Where, While, When or If.

    Keyword::word

    fn Keyword::word(self : Keyword) -> String

    The keyword in lower case, as the parser matches it without letter case.

    Lexicon

    A dialect defined as data: a name and lists of vocabulary. A caller builds one in code or reads one from a lexicon file with Lexicon::from_yaml.
    impl Dialect for Lexicon

    Lexicon::Lexicon

    fn Lexicon::Lexicon(name? : String, modals? : ArrayView[Word], determiners? : ArrayView[Word], pronouns? : ArrayView[Word], systems? : ArrayView[Phrase], lead_ins? : ArrayView[Phrase], headings? : ArrayView[HeadingRule]) -> Lexicon

    A lexicon. The name is free text for reports.

    Lexicon::determiners

    fn Lexicon::determiners(self : Lexicon) -> Array[Word]

    Lexicon::equal

    fn Lexicon::equal(Lexicon, Lexicon) -> Bool

    Lexicon::from_yaml

    fn Lexicon::from_yaml(text : String) -> Lexicon raise LexiconError

    Parses a lexicon file: a YAML mapping with the keys name, modals, determiners, pronouns, systems, lead_ins and headings. Each key is optional. A list key holds a list of texts; headings holds a list of mappings with level (1 to 6) and prefix. An unknown key, a value of the wrong kind and an invalid entry raise LexiconError with the line. It reads no file: the caller gives the text.

    Lexicon::headings

    fn Lexicon::headings(self : Lexicon) -> Array[HeadingRule]

    Lexicon::lead_ins

    fn Lexicon::lead_ins(self : Lexicon) -> Array[Phrase]

    Lexicon::modals

    fn Lexicon::modals(self : Lexicon) -> Array[Word]

    Lexicon::name

    fn Lexicon::name(self : Lexicon) -> String

    Lexicon::not_equal

    fn Lexicon::not_equal(x : Lexicon, y : Lexicon) -> Bool

    Lexicon::pronouns

    fn Lexicon::pronouns(self : Lexicon) -> Array[Word]

    Lexicon::system_nouns

    fn Lexicon::system_nouns(self : Lexicon) -> Array[Phrase]

    Lexicon::to_repr

    pub struct Modal {
    verb : Word
    negated : Bool
    } derive(Eq,
    Debug
    )

    The modal verb of a requirement, such as shall or must, and whether not follows it.

    Modal::equal

    fn Modal::equal(Modal, Modal) -> Bool

    Modal::not_equal

    fn Modal::not_equal(x : Modal, y : Modal) -> Bool

    Modal::to_repr

    Occurrence

    type Occurrence derive(Eq,
    Debug
    )

    Occurrence::equal

    fn Occurrence::equal(Occurrence, Occurrence) -> Bool

    Occurrence::not_equal

    fn Occurrence::not_equal(x : Occurrence, y : Occurrence) -> Bool

    Occurrence::origin

    fn Occurrence::origin(self : Occurrence) -> Origin

    Occurrence::requirement

    fn Occurrence::requirement(self : Occurrence) -> Requirement

    Origin

    Where a requirement or a diagnostic came from. A text range covers the sentence or the line without surrounding white space; a Markdown range covers the paragraph, including markup. Markdown paths resolve against the retained MDC document. Ranges are absent if that document was parsed with locs=false.

    Origin::equal

    fn Origin::equal(Origin, Origin) -> Bool

    Origin::not_equal

    fn Origin::not_equal(x : Origin, y : Origin) -> Bool

    Origin::to_repr

    Pattern

    pub(all) enum Pattern {
    Ubiquitous
    StateDriven
    EventDriven
    OptionalFeature
    UnwantedBehaviour
    Complex
    } derive(Eq,
    Debug
    )

    Pattern::equal

    fn Pattern::equal(Pattern, Pattern) -> Bool

    Pattern::not_equal

    fn Pattern::not_equal(x : Pattern, y : Pattern) -> Bool

    Pattern::to_repr

    Phrase

    type Phrase

    One or more words separated by single spaces, such as a system noun or a lead-in phrase, with no comma, full stop or line break. A phrase keeps its text as written and compares without letter case.
    impl Eq for Phrase
    impl Hash for Phrase
    impl Show for Phrase

    Phrase::Phrase

    fn Phrase::Phrase(text : String) -> Phrase raise LexiconError

    Phrase::equal

    fn Phrase::equal(self : Phrase, other : Phrase) -> Bool

    Phrase::hash

    fn Phrase::hash(self : Phrase) -> Int

    Phrase::hash_combine

    fn Phrase::hash_combine(self : Phrase, hasher : Hasher) -> Unit

    Phrase::lower

    fn Phrase::lower(self : Phrase) -> String

    The phrase in lower case, as the parser matches it.

    Phrase::not_equal

    fn Phrase::not_equal(x : Phrase, y : Phrase) -> Bool

    Phrase::output

    fn Phrase::output(self : Phrase, logger : &Logger) -> Unit

    Phrase::to_repr

    Phrase::to_string

    fn Phrase::to_string(self : Phrase) -> String

    The phrase as written.

    Requirement

    type Requirement derive(Eq,
    Debug
    )

    A validated EARS sentence. Only the parser can construct a requirement. Free-text clauses retain their case and punctuation, with whitespace folded.

    Requirement::Requirement

    fn Requirement::Requirement(source : String) -> Requirement raise ParseError

    Parse one sentence in strict EARS. Keywords are case-insensitive; clauses use comma separators. IF requires THEN, with an optional comma before THEN. Order: optional WHERE, zero or more WHILE, optional WHEN or IF, THE ... SHALL.

    Requirement::equal

    fn Requirement::equal(Requirement, Requirement) -> Bool

    Requirement::feature

    fn Requirement::feature(self : Requirement) -> String?

    Requirement::lead_in

    fn Requirement::lead_in(self : Requirement) -> String?

    The lead-in phrase before the clauses, such as Otherwise or For each document, as written.

    Requirement::modal

    fn Requirement::modal(self : Requirement) -> Modal

    Requirement::not_equal

    fn Requirement::not_equal(x : Requirement, y : Requirement) -> Bool

    Requirement::opener

    fn Requirement::opener(self : Requirement) -> Word?

    The subject opener as written: the determiner or the pronoun. A system noun has none.

    Requirement::pattern

    fn Requirement::pattern(self : Requirement) -> Pattern

    Requirement::preconditions

    fn Requirement::preconditions(self : Requirement) -> ReadOnlyArray[String]

    Requirement::resolved_system

    fn Requirement::resolved_system(self : Requirement) -> String?

    The system that block parsing resolved a pronoun subject to.

    Requirement::response

    fn Requirement::response(self : Requirement) -> String

    Requirement::subject

    fn Requirement::subject(self : Requirement) -> Subject

    Requirement::system

    fn Requirement::system(self : Requirement) -> String?

    The system: the words after the determiner, the system noun, or the system that a pronoun was resolved to. An unresolved pronoun has none.

    Requirement::to_string

    fn Requirement::to_string(self : Requirement) -> String

    Render canonical keyword case and spacing: the lead-in and the clauses as written, then the subject opener and the modal verb in lower case, with a capital first letter at the start. Response punctuation is retained. Parsing the text with the same dialects gives an equal requirement.

    Requirement::trigger

    fn Requirement::trigger(self : Requirement) -> Trigger?

    RequirementBlock

    type RequirementBlock derive(
    Debug
    )

    A requirement block: the requirements and diagnostics under one Markdown requirement heading. The name is the rest of the heading text, free text.

    RequirementBlock::diagnostics

    fn RequirementBlock::diagnostics(self : RequirementBlock) -> ReadOnlyArray[Diagnostic]

    RequirementBlock::heading

    The source range of the heading, when the document has locations.

    RequirementBlock::name

    fn RequirementBlock::name(self : RequirementBlock) -> String

    RequirementBlock::requirements

    fn RequirementBlock::requirements(self : RequirementBlock) -> ReadOnlyArray[Occurrence]

    SentencePosition

    type SentencePosition derive(Compare, Eq, Hash,
    Debug
    )

    A place in the normalized sentence: the UTF-16 offset of a character, counted from 0, or the length of the sentence for its end, such as the place after a final shall.

    SentencePosition::SentencePosition

    fn SentencePosition::SentencePosition(offset : Int) -> SentencePosition raise PositionError

    SentencePosition::compare

    SentencePosition::equal

    SentencePosition::hash

    fn SentencePosition::hash(self : SentencePosition) -> Int

    SentencePosition::hash_combine

    fn SentencePosition::hash_combine(SentencePosition, Hasher) -> Unit

    SentencePosition::not_equal

    fn SentencePosition::not_equal(x : SentencePosition, y : SentencePosition) -> Bool

    SentencePosition::offset

    fn SentencePosition::offset(self : SentencePosition) -> Int

    SentencePosition::op_ge

    SentencePosition::op_gt

    SentencePosition::op_le

    SentencePosition::op_lt

    SentencePosition::output

    fn SentencePosition::output(self : SentencePosition, logger : &Logger) -> Unit

    SentencePosition::to_string

    fn SentencePosition::to_string(self : SentencePosition) -> String

    Specification

    type Specification derive(
    Debug
    )

    An immutable extraction result. Syntax errors do not discard valid siblings. The Markdown constructor retains its MDC document and frontmatter. With Markdown requirement headings, blocks() gives the blocks, and requirements() and diagnostics() list the contents of every block.

    Specification::blocks

    fn Specification::blocks(self : Specification) -> ReadOnlyArray[RequirementBlock]

    The requirement blocks, in source order. Without heading rules, none.

    Specification::diagnostics

    fn Specification::diagnostics(self : Specification) -> ReadOnlyArray[Diagnostic]

    Specification::document

    Specification::requirements

    fn Specification::requirements(self : Specification) -> ReadOnlyArray[Occurrence]

    Subject

    pub enum Subject {
    Determined(opener~ : Word, system~ : String)
    Noun(String)
    Pronoun(opener~ : Word, resolved~ : String?)
    } derive(Eq,
    Debug
    )

    The subject of a requirement.

    Subject::equal

    fn Subject::equal(Subject, Subject) -> Bool

    Subject::not_equal

    fn Subject::not_equal(x : Subject, y : Subject) -> Bool

    Subject::to_repr

    Trigger

    pub enum Trigger {
    Event(String)
    Unwanted(String)
    } derive(Eq,
    Debug
    )

    The trigger of a requirement. Only the parser makes triggers, so the condition is never empty and its white space is folded.

    Trigger::equal

    fn Trigger::equal(Trigger, Trigger) -> Bool

    Trigger::not_equal

    fn Trigger::not_equal(x : Trigger, y : Trigger) -> Bool

    Trigger::to_repr

    ValueKind

    pub(all) enum ValueKind {
    Text
    List
    Mapping
    Number
    } derive(Eq,
    Debug
    )

    The kind of a YAML value that a lexicon key needs.

    ValueKind::equal

    fn ValueKind::equal(ValueKind, ValueKind) -> Bool

    ValueKind::not_equal

    fn ValueKind::not_equal(x : ValueKind, y : ValueKind) -> Bool

    Word

    type Word

    One vocabulary word, such as a modal verb, a determiner or a pronoun: not empty, with no white space, comma or full stop. A word keeps its text as written and compares without letter case.
    impl Eq for Word
    impl Hash for Word
    impl Show for Word

    Word::Word

    fn Word::Word(text : String) -> Word raise LexiconError

    Word::equal

    fn Word::equal(self : Word, other : Word) -> Bool

    Word::hash

    fn Word::hash(self : Word) -> Int

    Word::hash_combine

    fn Word::hash_combine(self : Word, hasher : Hasher) -> Unit

    Word::lower

    fn Word::lower(self : Word) -> String

    The word in lower case, as the parser matches it.

    Word::not_equal

    fn Word::not_equal(x : Word, y : Word) -> Bool

    Word::output

    fn Word::output(self : Word, logger : &Logger) -> Unit

    Word::to_repr

    Word::to_string

    fn Word::to_string(self : Word) -> String

    The word as written.

    from_mdc

    fn from_mdc(document :
    Document
    , dialects? : ArrayView[&Dialect]) -> Specification

    Extract requirements from an MDC document with the vocabulary of dialects.

    Without heading rules, extract one sentence per paragraph, including paragraphs in lists and blockquotes. Wrapped lines and inline formatting form a single sentence. Skip headings, code, HTML, tables, footnotes and frontmatter. Non-EARS prose is ignored; malformed candidates become diagnostics.

    With heading rules, each heading that matches a rule starts a requirement block, named by the rest of the heading text. The paragraphs after it, up to the next heading of any level, are split into sentences, and a pronoun subject refers to the system of the nearest earlier sentence of the block. Each sentence has the source span of its paragraph. No reparsing occurs.

    keywords

    let keywords : ReadOnlyArray[Keyword]

    Every clause keyword, in the order that a sentence uses them. The parser and the Markdown extraction use this one table.

    openspec_dialect

    fn openspec_dialect() -> Lexicon

    The OpenSpec dialect: the modal verbs shall and must, the usual determiners and pronouns, the lead-in otherwise, and the heading rule ### Requirement:. Project words belong in a project lexicon.

    parse

    fn parse(source : String, dialects? : ArrayView[&Dialect]) -> Requirement raise ParseError

    Parse one sentence with the vocabulary of dialects. Without a dialect, it is the strict grammar of Requirement::Requirement.

    parse_block

    fn parse_block(text : String, dialects? : ArrayView[&Dialect]) -> Specification

    Parse a requirement block: split it into sentences, parse each with the vocabulary of dialects, and resolve a pronoun subject to the system of the nearest earlier sentence. Spans are relative to text.

    parse_markdown

    fn parse_markdown(source : String, strict? : Bool, file? : String, dialects? : ArrayView[&Dialect]) -> Specification raise
    ParseError

    Parse Markdown with optional YAML frontmatter, then extract EARS sentences with the vocabulary of dialects. Invalid frontmatter raises MDC's ParseError. EARS errors are diagnostics.

    parse_text

    fn parse_text(source : String, dialects? : ArrayView[&Dialect]) -> Specification

    Parse one requirement per nonblank line, with the vocabulary of dialects. Every nonblank line is checked. Accept LF, CRLF, CR and a leading BOM. Spans exclude indentation/newlines.