Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ allow = [
"Unicode-3.0",
"Zlib",
]
# Our own crate is AGPL-3.0-or-later (the project license); allow it here
# WITHOUT permitting AGPL in third-party dependencies — the allow-list above
# stays permissive-only.
exceptions = [
{ allow = ["AGPL-3.0-or-later"], crate = "quire-rs" },
]
confidence-threshold = 0.9

[bans]
Expand Down
73 changes: 73 additions & 0 deletions spec/assets/adr/0009-concrete-vocabulary-is-module-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
id: ADR-0009
title: "Concrete grammar vocabulary is module data, not engine"
type: ADR
---

# ADR 0009: Concrete grammar vocabulary is module data, not engine

**Status**: Accepted
**Date**: 2026-06-23
**Decision authority**: kreneskyp

## Context

The EARS `vague-response` check ([FR-042](../../functional/FR-042-requirement-grammar-check.md)) is
**object-aware**: a weak verb (`provide`/`support`/`process`/…) is flagged only when its object is
abstract or absent; a concrete object, a mechanism/numeric qualifier, or a backticked code
identifier suppresses it. This keeps high recall (flag-unless-concrete) while removing the worst
noise — a full-corpus pass went from 791 to 173 vague-response findings.

The "concrete object" test, however, leaned on a **hardcoded ~60-term software-noun list** baked
into the engine (`re_concrete_noun()` in `src/grammar/ears.rs`). A corpus study of the residual
soft tail showed this is a dead end: the leaks are ~40 distinct domain nouns (`pagination`,
`cursor`, `replica`, `backend`, …), almost all appearing exactly once. The concrete-noun space is
**unbounded and domain-specific** — every domain has its own — so no fixed engine list can close it,
and a software-noun list hardcoded into a domain-agnostic engine is a layering violation.

Two structural alternatives were weighed:

- **Flip the default** to *suppress-unless-vague* (flag only on an abstract-term match). This makes
the maintained lexicon small and bounded, but trades the failure mode from visible, self-
correcting false positives to **silent false negatives** — a tool that fails to catch vagueness
without telling anyone. Since the check will eventually enforce, a silent miss defeats the gate.
- **Grow the engine noun list.** Whack-a-mole against an unbounded, domain-specific set; bakes a
software assumption into a generic engine.

## Decision

**Concrete vocabulary is data supplied by modules, not code in the engine.**

- The engine keeps only the *bounded, generic* signals: the vague-quality lexicon, the
mechanism/numeric qualifiers, and the backtick hatch. `re_concrete_noun()` is removed.
- Modules declare a `lexicon` registry in `manifest.yaml`, merged first-wins across all loaded
modules exactly like `edge_types`/`roles` ([FR-040](../../functional/FR-040-object-edge-vocabulary.md)).
The grammar consumes the merged set ([FR-043](../../functional/FR-043-module-concrete-lexicon.md)).
- The baseline software vocabulary is **distributed across the domain `spec-objects-*` modules** —
each domain owns its terms, co-located with the objects that define them, and a software project
gets them by activating the modules it already uses.
- A project-scoped **Ubiquitous-Language** artifact layers on top later (possibly several, per
framework — DDD via `spec-objects-business`, EA via `spec-objects-enterprise`).

We deliberately **do not flip** the default: flag-unless-concrete is kept, so recall stays high and
the only failure mode is a visible, self-correcting false positive — which an author silences by
making the object concrete (backtick it, name it, or define it in the project glossary), each of
which improves the spec. The backtick hatch already lets authors reference code objects outside any
fixed lexicon.

## Consequences

- **Positive**: the noun list becomes each domain's bounded vocabulary, not an ever-growing engine
list; the engine stays domain-agnostic; defining a term to silence a finding is itself good
requirements hygiene (ISO/IEC/IEEE 29148 wants a project glossary for specially-used terms, and
does not require redefining common industry terms — so a shipped baseline is standards-aligned).
- **Negative / accepted**: the grammar's type-only path (`validate_document`, no registry) cannot
see the merged lexicon, so under it bare domain nouns are not suppressed (mechanism/bound/backtick
still are). The registry-backed path — the CLI and Python surfaces — is the standard one and
applies the full lexicon. Revisit only if the type-only path becomes a real consumer.

## Alternatives rejected

- **Flip to suppress-unless-vague** — rejected: silent false negatives defeat an enforcing gate.
- **Grow the hardcoded engine noun list** — rejected: unbounded, domain-specific, and a layering
violation in a generic engine.
1 change: 1 addition & 0 deletions spec/assets/adr/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ description: "Index of artifacts in this directory."
* [0006 Forbid Unsafe Retire Miri](./0006-forbid-unsafe-retire-miri.md)
* [0007 Internal Relative Path Links](./0007-internal-relative-path-links.md)
* [0008 Authorable Inverse Edges](./0008-authorable-inverse-edges.md)
* [0009 Concrete Vocabulary Is Module Data](./0009-concrete-vocabulary-is-module-data.md)
98 changes: 98 additions & 0 deletions spec/functional/FR-042-requirement-grammar-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
id: FR-042
title: "Requirement-Grammar Check (EARS)"
type: FR
relationships:
- target: "ix://agent-ix/quire-rs/spec/functional/FR-032"
type: "requires"
cardinality: "1:1"
- target: "ix://agent-ix/quire-rs/spec/functional/FR-010"
type: "requires"
cardinality: "1:1"
---

## Description

`quire-rs` SHALL provide a **grammar-check framework** that evaluates the
natural-language requirement statements inside requirement-bearing artifacts
against a registered **grammar**, and SHALL ship **EARS** (Easy Approach to
Requirements Syntax) as its first grammar. Grammar checking is a posture
distinct from declarative lint ([FR-036](./FR-036-declarative-lint-rules.md)) and
structural validation ([FR-032](./FR-032-validate-document.md)): a grammar
classifies and checks the *prose of individual normative statements*, whereas
lint checks declarative content patterns and validation checks document
structure.

The framework SHALL register each grammar against an `(archetype, section)`
binding, so a grammar runs only on the sections it governs. When a
requirement-bearing document is validated, the framework SHALL segment each
governed section into **normative statements** — a sentence, list item, or
table cell bearing a modal verb (`shall`/`shall not`) — and SHALL skip text
inside fenced code blocks, blockquotes, and reference lines.

The framework SHALL route every grammar finding into the validation result by
**severity**: when a finding's severity is `warning`, the framework SHALL record
it in `ValidationResult.warnings` without setting `is_valid` false; when the
severity is `error`, the framework SHALL record it in `ValidationResult.errors`
and SHALL fail validation. Severity SHALL be sourced from configuration so a
deployment can promote a grammar from advisory to enforcing without a code
change.

### EARS grammar

The EARS grammar SHALL classify each normative statement into exactly one
pattern — `ubiquitous`, `event` (`When …`), `state` (`While …`), `unwanted`
(`If … then …`), `optional` (`Where …`), or `complex` (a combination) — or SHALL
mark it `unclassifiable` when no pattern matches.

For each statement, the EARS grammar SHALL emit a finding when the statement
violates a clause rule:

1. **non-singular** — the statement contains more than one `shall`.
2. **missing-subject** — the statement names no system/actor subject.
3. **vague-response** — the response verb is vague (`support`, `handle`,
`manage`, `process`, `deal with`, `provide`, `enable`, `be able to`). The
check is **object-aware**: a verb is flagged only when its object is abstract
or absent — a concrete object surface (`provide an endpoint`, `process push
events`), a **backticked code identifier** (`provide \`CodeBlockEditor\``), or
a mechanism/quantitative qualifier (`handle X by Y`, `process within 16 ms`)
states a verifiable response and is not flagged. `be able to`
is the one verb-intrinsic case (capability-not-behavior) and is always flagged.
4. **non-canonical-trigger** — the statement leads with a non-EARS trigger
(`On …`, `Upon`, `After`, `Before`, `Once`, `During`) instead of `When`/
`While`.

The EARS grammar SHALL apply per-archetype dialects: an enumerated
`The <system> SHALL:` stem followed by a numbered response list SHALL count as a
single statement (not non-singular); a `StR` statement SHALL accept a stakeholder
or product subject (not only "the system"); and an `NFR` statement absent any
trigger SHALL NOT be reported as a defect. The EARS grammar SHALL NOT flag
passive voice.

### Non-goals (v1)

- The framework SHALL NOT judge whether the author chose the *semantically*
correct keyword (e.g. `When` used for a continuous `While` state); that
judgment is left to the agent review lens.
- No grammar other than EARS ships in v1; `GWT` (acceptance criteria) and the
`US` story grammar register onto the same framework later.

## Acceptance Criteria

| ID | Criteria | Verification |
|----|----------|--------------|
| FR-042-AC-1 | Each of the six EARS patterns (`ubiquitous`, `event`, `state`, `unwanted`, `optional`, `complex`) is classified from a representative statement, and a statement matching none is reported `unclassifiable`. | Test |
| FR-042-AC-2 | A statement with two `shall` clauses yields exactly one `non-singular` finding; an enumerated `The X SHALL:` stem followed by a numbered list yields none. | Test |
| FR-042-AC-3 | A statement with no system subject yields a `missing-subject` finding; a `StR` statement with a stakeholder subject (`The operator …`) does not. | Test |
| FR-042-AC-4 | A statement using a vague response verb (`shall support`) yields a `vague-response` finding; a passive-voice statement (`shall be included`) yields none. | Test |
| FR-042-AC-5 | A statement leading with `On startup, … shall …` yields a `non-canonical-trigger` finding; an `NFR` statement with no trigger yields none. | Test |
| FR-042-AC-6 | A grammar runs only against its bound `(archetype, section)` pairs: an EARS rule bound to FR `Description` produces no findings against an FR `Dependencies` section or an `IT` document. | Test |
| FR-042-AC-7 | A `warning`-severity finding is recorded in `ValidationResult.warnings` and leaves `is_valid` true; promoting the same finding to `error` records it in `errors` and sets `is_valid` false. | Test |
| FR-042-AC-8 | Each finding carries the offending statement excerpt, a 1-based line number, the matched pattern, and a severity. | Test |
| FR-042-AC-9 | Statements inside fenced code blocks, blockquotes, and reference lines are not segmented as normative statements and yield no findings. | Test |
| FR-042-AC-10 | The framework entry point is exposed through the Python (PyO3) binding and returns the same findings as the in-process Rust call for a fixture document. | Test |

## Dependencies

- **Upstream**: [FR-032](./FR-032-validate-document.md) (requires — findings merge into `ValidationResult`), [FR-010](./FR-010-query-api.md) (requires — section/table/list extraction)
- **Downstream**: the authoring (`/specify`), coverage (`/spec-matrix`), and review (`/spec-review`) workflows consume grammar findings; the `US-014` author-validates-markdown story is extended by this gate.
67 changes: 67 additions & 0 deletions spec/functional/FR-043-module-concrete-lexicon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
id: FR-043
title: "Module-Supplied Concrete Lexicon for the Grammar"
type: FR
relationships:
- target: "ix://agent-ix/quire-rs/spec/functional/FR-042"
type: "requires"
cardinality: "1:1"
- target: "ix://agent-ix/quire-rs/spec/functional/FR-040"
type: "requires"
cardinality: "1:1"
---

# [FR-043] Module-Supplied Concrete Lexicon for the Grammar

## Description

A Filament module MAY declare a `lexicon` registry in its `manifest.yaml` — a map of accepted
**concrete terms** to their definitions. The loader SHALL merge the per-module `lexicon` registries
across all loaded modules first-wins, mirroring the `edge_types`/`roles` registries ([FR-040](./FR-040-object-edge-vocabulary.md)).
A conflicting redeclaration of a term SHALL emit a non-fatal `DuplicateLexiconTerm` diagnostic.
The `Registry` SHALL expose the merged map through a `lexicon()` accessor. The `Registry` SHALL
also expose a precompiled matcher, built once at load, that recognises a lexicon term in a span of
text.

The EARS object-aware `vague-response` check ([FR-042](./FR-042-requirement-grammar-check.md)) SHALL
consume the merged lexicon as its set of concrete object nouns. The engine SHALL NOT carry a
hardcoded concrete-noun list — concrete vocabulary lives in the domain modules that define it, not
in the engine. A statement whose response object matches a lexicon term SHALL be treated as
concrete and SHALL NOT be flagged. The engine SHALL retain the bounded vague-quality lexicon, the
mechanism/numeric qualifiers, and the backticked-identifier suppression
([FR-042](./FR-042-requirement-grammar-check.md)) — these are generic, not domain vocabulary.

The merged lexicon reaches the grammar through the registry-backed validation path. When
`validate_document_in_registry` runs the grammar, the engine SHALL pass the `Registry`'s merged
lexicon to the check. When the type-only `validate_document` path runs the grammar without a
registry, the engine SHALL apply an empty lexicon: the mechanism, numeric-bound, and backticked
suppressions still apply, but bare domain-noun suppression does not. This degradation SHALL be
documented as the type-only path's known limitation.

The `check_grammar` Python binding SHALL accept an optional `module_root`. When `module_root` is
given, the binding SHALL load that module registry and apply its merged lexicon; when it is absent,
the binding SHALL apply an empty lexicon.

## Constraints

| ID | Constraint | Type | Validation |
|----|------------|------|------------|
| FR-043-CON-1 | A malformed `lexicon` entry SHALL fail module load like any other manifest shape error | Operational | Unit Test |
| FR-043-CON-2 | An empty or absent `lexicon` registry SHALL load without error | Operational | Unit Test |

## Acceptance Criteria

| ID | Criteria | Verification |
|----|----------|--------------|
| FR-043-AC-1 | A manifest declaring a `lexicon` entry loads, and `Registry::lexicon()` returns the merged map containing that term. | Test |
| FR-043-AC-2 | Two modules declaring the same term with different definitions are first-wins and emit one `DuplicateLexiconTerm` diagnostic; identical redeclaration emits none. | Test |
| FR-043-AC-3 | With a registry whose lexicon contains `pagination`, the statement `The system shall support pagination.` yields no `vague-response` finding; with the term removed, the same statement yields one. | Test |
| FR-043-AC-4 | The engine contains no hardcoded concrete-noun list: a bare domain noun absent from the lexicon (and not backticked, with no mechanism/bound) yields a `vague-response` finding under an empty lexicon. | Test |
| FR-043-AC-5 | Backticked-identifier, mechanism, and numeric-bound suppression (FR-042) still hold under an empty lexicon — they do not depend on the lexicon. | Test |
| FR-043-AC-6 | `validate_document_in_registry` applies the registry's merged lexicon; the type-only `validate_document` applies an empty lexicon (more findings) and never errors on the difference. | Test |
| FR-043-AC-7 | The `check_grammar` Python binding with a `module_root` applies that registry's lexicon; without one it applies an empty lexicon. | Test |

## Dependencies

- **Upstream**: [FR-042](./FR-042-requirement-grammar-check.md) (the grammar consumer), [FR-040](./FR-040-object-edge-vocabulary.md) (the merge-vocab registry pattern this mirrors)
- **Downstream**: the `spec-objects-*` modules ship the domain `lexicon` registries; a project Ubiquitous-Language artifact adds a project-scoped layer (future)
Loading
Loading