Skip to content

EARS requirement-grammar check (FR-042) - #7

Merged
kreneskyp merged 12 commits into
mainfrom
ears-grammar-check
Jun 23, 2026
Merged

EARS requirement-grammar check (FR-042)#7
kreneskyp merged 12 commits into
mainfrom
ears-grammar-check

Conversation

@kreneskyp

Copy link
Copy Markdown
Contributor

What

Adds a deterministic requirement-grammar framework to quire-rs with EARS (Easy Approach to Requirements Syntax) as its first grammar, bound to archetypes via the existing (previously unconsumed) grammar_ref manifest hook.

Findings route into the validation result by severity — advisory warning by default (never blocks), promotable to error — so EARS ships advisory during corpus conversion and enforces afterward without moving subsystems.

Surface

  • src/grammar/ — framework (GrammarFinding/GrammarSeverity/dispatch) + EARS grammar (6-pattern classifier; non-singular / missing-subject / vague-response / non-canonical-trigger / unclassifiable checks; FR/NFR/StR dialects; fence/quote/table-cell-aware segmentation).
  • validate_documentValidationReason::Grammar + severity routing; runs on the resolved archetype (so --archetype on a typeless doc is still checked).
  • pythoncheck_grammar PyO3 binding surfaces findings directly.
  • spec — FR-042 + AC→TC (TC-657..666), matrix to 360/360, US-014 trace.

Bound only to FR Description/Behavior/Constraints, NFR Statement, StR Stakeholder Need. Passive voice is a deliberate non-goal. Also sweeps two stale artifact_type: fixtures to type:.

Verification

fmt-check, clippy -D warnings, 363 lib tests, audit-unsafe, and the Python binding test (fresh wheel) all green. Verified end-to-end on real corpus FRs.

Ordering

Foundation of the FR-042 series. Downstream PRs (quire-cli --summary, quoin lens/evals, spec-artifacts-process enum, spec-artifacts-iso skeletons) consume this; quire-cli is release-coupled to the tag bump of this engine.

🤖 Generated with Claude Code

Add a deterministic requirement-grammar framework with EARS as its first
grammar, bound to archetypes via the existing (previously unconsumed)
`grammar_ref` manifest hook. Findings route into the validation result by
severity — advisory `warning` by default (never blocks), promotable to
`error` — so EARS can ship advisory during corpus conversion and enforce
afterward without moving subsystems.

- src/grammar/: framework (GrammarFinding/GrammarSeverity/dispatch) + EARS
  grammar (6-pattern classifier; non-singular/missing-subject/vague-response/
  non-canonical-trigger/unclassifiable checks; FR/NFR/StR dialects;
  fence/quote/table-cell-aware statement segmentation).
- validate_document: ValidationReason::Grammar + severity routing; runs on
  the resolved archetype so a --archetype override on a typeless doc is
  still checked.
- python: check_grammar PyO3 binding surfaces findings directly.
- spec: FR-042 + AC→TC (TC-657..666), matrix to 360/360, US-014 trace.

Bound only to FR Description/Behavior/Constraints, NFR Statement, StR
Stakeholder Need. Passive voice is a deliberate non-goal. Also sweeps two
stale `artifact_type:` frontmatter fixtures to `type:` (type-rename cleanup).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kreneskyp
kreneskyp requested a review from a team as a code owner June 23, 2026 06:01
Agent IX and others added 11 commits June 23, 2026 07:15
Review/calibration follow-up. The flat verb lexicon over-flagged: vagueness
is a property of the OBJECT, not the verb — `provide an endpoint`, `process
push events`, `handle X by Y` are concrete despite a "vague" verb. Across the
corpus the old rule fired 791×, ~55% of them `provide` on concrete CRUD/API
objects.

vague-response is now object-aware: a verb is flagged only when its object is
abstract (`provide flexibility`, `handle errors gracefully`) or absent. A
concrete object surface or a mechanism/numeric qualifier suppresses it.
`be able to` (capability-not-behavior) is the one verb-intrinsic case and is
always flagged. Corpus: 791 → 216 vague-response findings (concrete CRUD/event
uses no longer flagged), calibrated against the full corpus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-042)

Object-aware vague-response follow-up. A backticked identifier in the response
object (`provide `CodeBlockEditor``, `support `ecvector` opclasses`) is the most
concrete object there is — but `normalize()` was stripping backticks before the
check, so named code objects fell back to the noun lexicon and leaked as
false positives. Keep backticks; treat a backticked object as a concrete signal,
so authors can reference things outside the fixed noun lexicon.

Corpus: vague-response 216 → 173 (43 named-identifier leaks suppressed). The
residual is be-able-to capability phrasing + genuinely-abstract objects.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (FR-043)

Concrete grammar vocabulary is now DATA, not engine (ADR 0009). The EARS
object-aware vague-response check (FR-042) no longer carries a hardcoded
~60-term software-noun list (`re_concrete_noun` removed); concrete terms
come from a mergeable manifest `lexicon:` registry, exactly like
edge_types/roles (FR-040).

- vocab/manifest/loader/registry: `lexicon` registry merged first-wins
  across modules (DuplicateLexiconTerm advisory; load_strict escalates);
  Registry::lexicon() + a precompiled lexicon_matcher() built once at load.
- grammar: vague_verb consumes the matcher; the bounded vague-quality
  lexicon, mechanism/bound qualifiers, and backtick hatch stay in-engine
  (generic, not domain vocabulary).
- run_grammar threads it: registry-backed validate_document_in_registry
  passes the merged lexicon; type-only validate_document passes empty
  (documented degradation). check_grammar PyO3 binding gains module_root.
- spec: FR-043 + ADR 0009 + matrix to 367/367 (TC-667..673).

High recall kept (flag-unless-concrete, NOT flipped). Baseline software
terms land in the spec-objects-* modules next (distributed, per ADR 0009).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (FR-043)

- GrammarLexicon matches the regular plural (`endpoint`→`endpoints`); corpus
  objects are frequently plural (mirrors the old concrete-noun `s?` rule).
- check_grammar's module_root accepts a search root (merging child modules)
  as well as a single module dir, like the CLI — so the merged project
  lexicon applies for the corpus report and review lens.

Corpus re-baseline with the distributed spec-objects-* lexicons: vague-response
149 (vs 173 under the old hardcoded engine list) — a strict superset, so no
regressions, and the soft-tail concretes (pagination/deployment/token) are now
suppressed too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GrammarLexicon now contributes both singular and plural forms per term,
including irregular plurals (`policy`→`policies`, `box`→`boxes`,
`endpoint`→`endpoints`) — corpus objects are frequently plural and the
prior `s?` missed `-ies`/`-es`. Corpus re-baseline (with the module top-up):
vague-response 149 → 122.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A repo's own authored glossary terms now suppress in its grammar check,
alongside the merged module lexicon (FR-043). The immutable Registry is
unchanged: project terms — which vary per repo — are composed into an
ad-hoc GrammarLexicon at validation time and injected.

- corpus::glossary_terms(&Spec): harvests the `Term` column of every
  `## Terms` table and the bold term of every `## Ubiquitous Language`
  bullet (reuses query::table_from_section + parse_bullet_list).
- Registry::lexicon_with(extra): composes module keys ∪ project terms.
- validate_document_in_registry_with_lexicon: injects an explicit lexicon
  (the existing entry point delegates to it with the module lexicon — one
  shared body).
- corpus::validate_bundle harvests the Spec once and validates each doc
  with the combined lexicon; advisory (never a bundle error).
- spec: FR-044 + integrity SpecReview + matrix to 374/374 (TC-674..680),
  authored via /specify → /spec-matrix → /spec-review. Also splits the
  FR-043 TC tests so each TC-id maps to its own test function.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y (FR-044)

Code-review follow-up: tc674_675 bundled two TCs (the one-test-per-TC
convention missed in the earlier split) — now tc674 (Terms table) + tc675
(Ubiquitous Language) via a shared fixture. Add malformed_glossary_harvests_
nothing for CON-2's malformed half (## Terms heading, no table → zero terms,
no panic), which TC-680 (absent glossary) did not cover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CLI per-validate path built a whole Spec (parse + graph-resolve every
file under --scope) just to read a few glossary tables. Add
glossary_terms_from_path: enumerate *.md, raw-read each, and parse ONLY the
docs whose text carries a `## Terms` / `## Ubiquitous Language` heading (cheap
has_glossary_heading line scan). The Spec-based glossary_terms stays for the
corpus path (validate_bundle), where the Spec is already loaded.

Tests: has_glossary_heading_gates_parsing (the parse-skip gate) +
glossary_terms_from_path_scopes_to_glossary_docs (one glossary among many
plain FRs → exactly the glossary terms, parity with the Spec-based harvester).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the AGPL relicense (merged from main): cargo-deny rejected the
crate's own AGPL-3.0-or-later license since the allow-list is permissive-only.
Add a per-crate exception for quire-rs so the License Check passes WITHOUT
permitting AGPL in third-party deps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kreneskyp
kreneskyp merged commit d4f0d6d into main Jun 23, 2026
6 checks passed
@kreneskyp
kreneskyp deleted the ears-grammar-check branch June 23, 2026 22:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant