id: lint kind: intent statement: “WHEN ah lint inspects specification files THE linter SHALL flag vague qualifiers, imperative UI steps, conjunctive step bloat, missing negative scenarios, missing non-goals, unresolved ambiguities, and entangled presentation concerns, emit findings in the shared check finding schema with warning severity by default, bridge specodelic findings for dual-format files without reimplementing specodelic rules, never skip a kind-intent file silently, relay spk graph typing violations and dangling references as spk.graph.* warnings, and report scenario-to-property trace gaps as advisory findings that never affect the exit code.”
lint Specification
Constraints
| id | kind | expr | traces_to |
|---|---|---|---|
| C-vague-qualifier | invariant | requirement bodies and scenario steps containing unbound qualitative terms without an adjacent numeric measurement emit a vague-qualifier finding suggesting a measurable condition; a qualifier with an adjacent numeric bound emits none | [[lint]] |
| C-imperative-step | invariant | WHEN/THEN steps describing UI mechanics (explicit click, URL navigation) emit an imperative-step finding suggesting business-action phrasing; declarative business steps emit none | [[lint]] |
| C-conjunctive-bloat | invariant | scenarios chaining more than the configured maximum of AND-linked steps emit a conjunctive-bloat finding suggesting a split; a scenario at exactly the limit is accepted; the maximum is configurable via [lint] max_and_steps in .espectacular/config.toml | [[lint]] |
| C-missing-negative | invariant | a requirement with no scenario exercising an error, rejection, or boundary condition emits a missing-negative-scenario finding suggesting a failure-mode scenario; one negative scenario suppresses it | [[lint]] |
| C-missing-non-goals | invariant | a spec capability file lacking a Non-Goals/Non Goals/non-goals/Out of Scope heading emits a missing-non-goals finding for that file | [[lint]] |
| C-unresolved-ambiguity | invariant | requirement or scenario text containing a bracketed NEEDS CLARIFICATION marker emits an unresolved-ambiguity finding including the marker as context; text without the marker emits none | [[lint]] |
| C-entangled-spec | invariant | a scenario with contract archetype PF or SA whose text references presentation-layer primitives (buttons, modals, colors, CSS selectors, component names, client routes) emits an entangled-spec finding suggesting the domain event or state; the same text in a BP scenario and domain-legitimate lookalike terms are not flagged | [[lint]] |
| C-finding-schema | invariant | lint findings use the same stable JSON envelope as ah check: each carries kind, severity, spec_path, message, suggested_action, playbook_command, plus scenario_id/scenario_title when scenario-specific; findings default to severity = "warning"; a malformed spec file emits a severity = "error" finding and exits non-zero | [[lint]] |
| C-dual-bridge | invariant | for dual-format files (id: spec frontmatter) ah lint invokes spk lint and relays each specodelic finding as kind = "spk.<rule_id>" with severity = "warning"; plain openspec files cause no specodelic invocation and identical pre-bridge output; a missing spk binary emits one advisory spk-unavailable finding with unchanged exit code; a non-envelope spk failure emits one advisory finding and linting continues | [[lint]] |
| C-no-silent-skip | invariant | a kind-intent file whose frontmatter id does not match its filename stem emits an spk-frontmatter-mismatch finding instead of being skipped silently | [[lint]] |
| C-graph-relay | invariant | spk graph typing violations and dangling references for dual-format files are relayed as spk.graph.* warning findings | [[lint]] |
| C-trace-advisory | advisory | verifies-dangling, scenario-unlinked, and property-untraced are reported at advisory severity and never affect the lint exit code | [[lint]] |
| C-plain-untouched | advisory | plain openspec files never trigger an spk invocation and produce no specodelic findings | [[lint]] |
Model
States
scanningbridgingreportedrelayingtracing
Transitions
| id | from | to | guard |
|---|---|---|---|
| t-textual-rules | scanning | scanning | [[lint.C-vague-qualifier]], [[lint.C-imperative-step]], [[lint.C-conjunctive-bloat]], [[lint.C-missing-negative]], [[lint.C-missing-non-goals]], [[lint.C-unresolved-ambiguity]], or [[lint.C-entangled-spec]] |
| t-bridge | scanning | bridging | [[lint.C-dual-bridge]] (dual-format file present) |
| t-inert | scanning | scanning | [[lint.C-dual-bridge]] (plain file — no invocation) |
| t-schema | bridging | reported | [[lint.C-finding-schema]] |
| t-degrade | bridging | reported | [[lint.C-dual-bridge]] (missing binary or failed invocation — advisory only) |
| t-relay | bridging | relaying | [[lint.C-no-silent-skip]] |
| t-graph | relaying | tracing | [[lint.C-graph-relay]] |
| t-trace | tracing | reported | [[lint.C-graph-relay]] |
| t-plain | relaying | reported | [[lint.C-no-silent-skip]] |
Properties
| id | kind | derives_from | generator | predicate |
|---|---|---|---|---|
| P-vague | unit | [[lint.C-vague-qualifier]] | bodies with unbound qualifiers vs numerically bounded qualifiers, in requirements and THEN steps | findings exactly in the unbound cases, with measurable-condition suggestions |
| P-imperative | unit | [[lint.C-imperative-step]] | WHEN steps with click/URL mechanics vs declarative business steps | findings exactly in the mechanical cases, with business-action suggestions |
| P-bloat | unit | [[lint.C-conjunctive-bloat]] | scenarios at limit-1, limit, and limit+1 AND-steps under default and custom maximums | finding only above the configured limit; exact limit accepted |
| P-negative | unit | [[lint.C-missing-negative]] | requirements with only happy-path scenarios vs one negative scenario | finding exactly in the happy-path-only case |
| P-non-goals | unit | [[lint.C-missing-non-goals]] | spec files with and without any non-goals heading variant | finding exactly when no variant is present |
| P-ambiguity | unit | [[lint.C-unresolved-ambiguity]] | bodies with and without bracketed NEEDS CLARIFICATION markers | finding with marker context exactly when present |
| P-entangled | unit | [[lint.C-entangled-spec]] | PF/SA scenarios with presentation primitives vs BP scenarios and domain lookalikes | findings exactly in the PF/SA primitive cases |
| P-schema | unit | [[lint.C-finding-schema]] | lint runs over well-formed and malformed specs | shared finding fields throughout, warning default, malformed → error + non-zero exit |
| P-bridge | unit | [[lint.C-dual-bridge]] | dual files with spk findings, plain files, missing binary, and failing invocation | relaying with spk. kind prefix; inert for plain files; advisory-only degradation in both failure modes |
| P-mismatch | unit | [[lint.C-no-silent-skip]] | a kind-intent file whose frontmatter id differs from its filename stem | lint emits spk-frontmatter-mismatch naming the file and the expected id |
| P-graph | unit | [[lint.C-graph-relay]] | a dual-format corpus containing a typing violation and a dangling reference | both are relayed as spk.graph.* warnings |
| P-trace | unit | [[lint.C-trace-advisory]] | a dual-format corpus with a dangling VERIFIES link, an unlinked scenario, and an untraced property | all three appear as advisory findings; the exit code is zero |
| P-plain | unit | [[lint.C-plain-untouched]] | a corpus of plain openspec spec files | lint output contains no specodelic findings and no spk invocation occurs |
Purpose
The spec linter: textual spec-shape findings (vague qualifiers, imperative steps, conjunctive bloat, missing negative scenarios and non-goals, unresolved ambiguities, entangled specs), the shared stable finding schema, and the dual-format specodelic bridge — complete since derive-contracts-from-specodelic: no silent skips, spk graph relay, and advisory scenario-to-property trace checks.
Requirements
Requirement: Vague Qualifier Detection
The system SHALL flag requirements and scenario steps that contain unbound qualitative terms without an adjacent numeric measurement.
Scenario: Flag unbound qualifier in requirement
- GIVEN a requirement body contains the unbound qualifier “fast” with no numeric bound (a line like “p95 < 200 ms” would pass)
- WHEN
ah lintruns - THEN the command emits a
vague-qualifierfinding for that requirement - AND the finding suggests adding a measurable response condition (e.g., “within 200 ms”)
- VERIFIES [[lint.P-vague]]
Scenario: Do not flag bounded qualifier
- GIVEN a requirement body contains “fast (p95 < 200 ms)”
- WHEN
ah lintruns - THEN no
vague-qualifierfinding is emitted for that requirement - AND no error-severity finding is emitted — a numeric bound satisfies the check
- VERIFIES [[lint.P-vague]]
Scenario: Flag qualifier in scenario step
- GIVEN a THEN step contains “the response is user-friendly” and the step line carries no numeric bound (a step like “served under 200 ms” would pass)
- WHEN
ah lintruns - THEN the command emits a
vague-qualifierfinding for that scenario step - VERIFIES [[lint.P-vague]]
Requirement: Imperative Step Detection
The system SHALL flag WHEN and THEN steps that describe UI mechanics rather than business intent, coupling the spec to implementation details.
Scenario: Flag explicit click step
- GIVEN a WHEN step contains “clicks the Submit button”
- WHEN
ah lintruns - THEN the command emits an
imperative-stepfinding - AND the finding suggests rephrasing to describe the business action (e.g., “submits the form”)
- VERIFIES [[lint.P-imperative]]
Scenario: Flag URL navigation step
- GIVEN a WHEN step contains “navigates to /dashboard/settings”
- WHEN
ah lintruns - THEN the command emits an
imperative-stepfinding - VERIFIES [[lint.P-imperative]]
Scenario: Do not flag declarative business steps
- GIVEN a WHEN step contains “the user submits a payment”
- WHEN
ah lintruns - THEN no
imperative-stepfinding is emitted - AND no error-severity finding is emitted — business phrasing passes cleanly
- VERIFIES [[lint.P-imperative]]
Requirement: Conjunctive Step Bloat Detection
The system SHALL flag scenarios that chain more than the configured maximum number of AND-linked steps, indicating a scenario that tests multiple behaviors at once.
Scenario: Flag overlong scenario
- GIVEN a scenario has eight AND-linked steps
- AND the configured maximum is five
- WHEN
ah lintruns - THEN the command emits a
conjunctive-bloatfinding - AND the finding suggests splitting the scenario into focused single-behavior scenarios
- VERIFIES [[lint.P-bloat]]
Scenario: Accept scenario within limit
- GIVEN a scenario has four AND-linked steps
- AND the configured maximum is five
- WHEN
ah lintruns - THEN no
conjunctive-bloatfinding is emitted for that scenario - VERIFIES [[lint.P-bloat]]
Scenario: Scenario exactly at limit is accepted
- GIVEN a scenario has five AND-linked steps
- AND the configured maximum is five
- WHEN
ah lintruns - THEN no
conjunctive-bloatfinding is emitted for that scenario — the limit boundary does not fail - VERIFIES [[lint.P-bloat]]
Scenario: Default maximum is configurable
- GIVEN
.espectacular/config.tomlsets[lint] max_and_steps = 3 - WHEN
ah lintruns - THEN the command uses 3 as the maximum AND-step count
- VERIFIES [[lint.P-bloat]]
Requirement: Missing Negative Scenario Detection
The system SHALL flag requirements that have no scenario exercising an error condition, rejection, or boundary violation, indicating incomplete behavioral specification.
Scenario: Flag requirement with only happy-path scenarios
- GIVEN a requirement has two scenarios, both describing successful outcomes
- AND no scenario contains rejection, error, or boundary language
- WHEN
ah lintruns - THEN the command emits a
missing-negative-scenariofinding - AND the finding suggests adding a scenario for the corresponding failure mode
- VERIFIES [[lint.P-negative]]
Scenario: Accept requirement with at least one negative scenario
- GIVEN a requirement has a scenario whose THEN step contains “the command exits non-zero”
- WHEN
ah lintruns - THEN no
missing-negative-scenariofinding is emitted for that requirement - VERIFIES [[lint.P-negative]]
Requirement: Missing Non-Goals Detection
The system SHALL flag spec capability files that lack an explicit Non-Goals or Out-of-Scope section, since their absence enables silent scope creep during AI-assisted implementation.
Scenario: Flag spec without non-goals section
- GIVEN a
spec.mdfile contains no heading matchingNon-Goals,Non Goals,non-goals, orOut of Scope - WHEN
ah lintruns - THEN the command emits a
missing-non-goalsfinding for that spec file - VERIFIES [[lint.P-non-goals]]
Scenario: Accept spec with non-goals section
- GIVEN a
spec.mdfile contains a## Non-Goalsheading - WHEN
ah lintruns - THEN no
missing-non-goalsfinding is emitted for that file — an explicit exclusion section is never rejected - VERIFIES [[lint.P-non-goals]]
Requirement: Unresolved Ambiguity Detection
The system SHALL flag requirements and scenarios that contain the bracketed NEEDS CLARIFICATION marker, indicating authoring-time decisions deferred but not yet resolved.
Scenario: Flag needs-clarification marker
- GIVEN a requirement body contains a bracketed NEEDS CLARIFICATION marker (e.g. an unresolved question about which auth provider applies, wrapped in square brackets)
- WHEN
ah lintruns - THEN the command emits an
unresolved-ambiguityfinding - AND the finding includes the marker text as context
- VERIFIES [[lint.P-ambiguity]]
Scenario: Accept requirement with no ambiguity markers
- GIVEN a requirement body contains no bracketed NEEDS CLARIFICATION marker, and no line pairs those two words inside square brackets
- WHEN
ah lintruns - THEN no
unresolved-ambiguityfinding is emitted for that requirement - AND resolved requirements emit no error — only deferred decisions are flagged
- VERIFIES [[lint.P-ambiguity]]
Requirement: Entangled Specification Detection
The system SHALL flag scenarios whose contract archetype is PF or SA but whose text references presentation-layer primitives (buttons, modals, colors, CSS selectors, component names, client routes), since the archetype declares the scenario belongs to a layer where UI mechanics must not appear. The same text in a BP scenario is not flagged, as transport mechanics are legitimate at a boundary seam.
Scenario: Flag UI primitive in PF scenario
- GIVEN a scenario with contract archetype
PFwhose THEN step contains “a green notification banner is displayed” - WHEN
ah lintruns - THEN the command emits an
entangled-specfinding - AND the finding suggests specifying the domain event or state instead of the visual primitive
- VERIFIES [[lint.P-entangled]]
Scenario: Do not flag transport mechanics in BP scenario
- GIVEN a scenario with contract archetype
BPwhose WHEN step contains “navigates to /dashboard/settings” - WHEN
ah lintruns - THEN no
entangled-specfinding is emitted for that scenario - VERIFIES [[lint.P-entangled]]
Scenario: Do not flag domain language resembling UI terms
- GIVEN a scenario with contract archetype
SAwhose text contains “the event routing layer delivers the message” - WHEN
ah lintruns - THEN no
entangled-specfinding is emitted, as the matcher list excludes domain-legitimate uses of superficially similar terms - AND no error-severity finding is emitted for the lookalike either
- VERIFIES [[lint.P-entangled]]
Requirement: Lint Finding Schema
The system SHALL emit lint findings using the same stable JSON envelope as ah check, enabling agent harnesses to consume both without separate parsing logic.
Scenario: Lint findings share check finding fields
- GIVEN
ah lint --jsonproduces findings - WHEN the JSON output is inspected
- THEN each finding contains
kind,severity,spec_path,message,suggested_action, andplaybook_command - AND findings that reference a specific scenario also contain
scenario_idandscenario_title - VERIFIES [[lint.P-schema]]
Scenario: Lint findings are warning severity by default
- GIVEN
ah lintproduces warning-severity findings for any lint check category - WHEN the JSON output is inspected
- THEN every finding has
severity = "warning" - VERIFIES [[lint.P-schema]]
Scenario: Malformed spec file is error severity
- GIVEN a spec file contains invalid Markdown that prevents scenario parsing
- WHEN
ah lintruns - THEN the command emits a finding with
severity = "error" - AND exits non-zero
- VERIFIES [[lint.P-schema]]
Requirement: Dual-Format Lint Bridge
The system SHALL relay specodelic lint findings for dual-format spec files into the shared lint finding schema by invoking spk lint, without reimplementing specodelic rules.
Scenario: Relay specodelic findings for dual-format files
- GIVEN a spec file carries
id: specfrontmatter (dual-format) - AND
spk lintreports findings for that file - WHEN
ah lintruns - THEN each specodelic finding appears in the output with
kind = "spk.<rule_id>" - AND every relayed finding has
severity = "warning" - VERIFIES [[lint.P-bridge]]
Scenario: Bridge is inert for plain openspec files
- GIVEN a spec file contains no dual-format frontmatter
- WHEN
ah lintruns - THEN no specodelic invocation occurs and the output is identical to pre-bridge behavior
- VERIFIES [[lint.P-bridge]]
Scenario: Missing spk binary is advisory only
- GIVEN a dual-format spec file
- AND the
spkbinary is not available on PATH - WHEN
ah lintruns - THEN the command emits a single advisory
spk-unavailablefinding - AND the exit code is unchanged relative to a successful warning-only lint run
- VERIFIES [[lint.P-bridge]]
Scenario: Specodelic invocation failure does not hard-fail
- GIVEN a dual-format spec file
- AND
spk lintexits with a non-envelope error or unparseable output - WHEN
ah lintruns - THEN the command emits a single advisory finding describing the failure
- AND continues linting the remaining spec files
- VERIFIES [[lint.P-bridge]]
Requirement: Specodelic Relay Completeness
The linter SHALL emit an spk-frontmatter-mismatch finding for every kind: intent file whose frontmatter id does not match its filename stem instead of skipping it silently, and SHALL relay spk graph typing violations and dangling references as spk.graph.* warning findings.
Scenario: Mismatched id is reported
- GIVEN a
kind: intentfile whose frontmatter id differs from its filename stem - WHEN
ah lintruns over it - THEN an
spk-frontmatter-mismatchfinding names the file and the expected id - AND no specodelic file is skipped without a finding
- VERIFIES [[lint.P-mismatch]]
Scenario: Graph findings relayed
- GIVEN a dual-format corpus containing a typing violation and a dangling reference
- WHEN
ah lintruns over the corpus - THEN both are relayed as
spk.graph.*warnings - VERIFIES [[lint.P-graph]]
Scenario: Clean dual-format file unchanged
- GIVEN a dual-format file that passes
spk lintandspk graph - WHEN
ah lintruns over it - THEN no specodelic findings are emitted for the file
Requirement: Scenario Property Trace
The linter SHALL report advisory trace findings — verifies-dangling, scenario-unlinked, and property-untraced — for dual-format files with Properties rows, without affecting the exit code.
Scenario: Dangling verifies link
- GIVEN a scenario carries
**VERIFIES** [[lint.P-missing]]and no such property exists - WHEN
ah lintruns over the file - THEN a
verifies-danglingadvisory finding is emitted
Scenario: Unlinked scenario and untraced property
- GIVEN a dual-format file with Properties rows where one scenario has no
VERIFIESlink and one property is cited by no scenario - WHEN
ah lintruns over the file - THEN
scenario-unlinkedandproperty-untracedadvisory findings are emitted - AND the exit code is zero
- VERIFIES [[lint.P-trace]]
Scenario: Plain files untouched
- GIVEN a corpus of plain openspec spec files
- WHEN
ah lintruns over the corpus - THEN no specodelic findings are emitted and no spk invocation occurs
- AND no error-severity trace finding is emitted — plain files never fail the trace gate
- VERIFIES [[lint.P-plain]]