Skip to content

Collector: dispatch on parsed statement type, not first token#67

Merged
rmichaelthomas merged 1 commit into
mainfrom
fix/collector-statement-dispatch
Jul 21, 2026
Merged

Collector: dispatch on parsed statement type, not first token#67
rmichaelthomas merged 1 commit into
mainfrom
fix/collector-statement-dispatch

Conversation

@rmichaelthomas

Copy link
Copy Markdown
Owner

Summary

_collect_checkable_statements decided whether a line was checkable by testing whether its first token was a verb. starting "<date>" and until "<date>" are TokenType.CONNECTIVE, and inherited is TokenType.OPERATOR — both consumed statement-initial, before the verb, by _parse_one_operation (parser.py). A first-token-VERB guard therefore silently dropped every temporally-prefixed or inherited-prefixed statement: no error, no warning, just absent from the collected list — with check_source still reporting encodable=True, concealing the under-reporting.

Found by the Halverson re-run (liminate-dev PR #92): four statements across three documents excluded from every check, combined checked=17 where it should have been 21. inherited shares the identical defect and was never exercised by any corpus — fixed here too, not left for a second finding.

Changes

  • Inverts the collector: tokenize → reorder → parse every line (as before), then dispatch on the resulting AST node's type — the same pattern check_agreement already uses downstream via _VERB_NAMES and _iter_statements.
  • DefineNode registers into predicate_names before statements.append, preserving the forward-declaration order exactly.
  • run._collect_deontic_statements, check_agreement, the encoder, and the seven checks are untouched — this changes collection only.
  • Removed the now-unused _CHECK_DEONTIC_VERBS constant and TokenType import.

D3 — a guard against the class

Added a hand-written coverage table (test_collector_coverage_matches_parser_accepted_shapes, 42 parametrized cases) asserting the collector checks exactly one statement of the expected type for every deontic verb, define, and value-form remember, under every prefix combination the parser accepts (bare, starting, until, starting+until, inherited, starting+inherited, full stack). Deliberately not derived from parser.py internals — a specification of intended coverage, not a mirror of the implementation, so a future grammar addition without a matching row here is a gap the test can still expose once someone notices, not one it silently absorbs.

Test plan

  • Regression test: a starting-prefixed forbid returns checked=1, not 0
  • Same for until, starting+until, inherited, and the full canonical stack
  • A temporally-prefixed statement participates in findings (unless_swallows_rule fires correctly)
  • A define on a temporally-prefixed line still registers in predicate_names for later lines
  • checked count unchanged for a source with no prefixes — no double-counting, no newly-swept-in non-deontic lines
  • The C3 corpus re-verified: combined checked=21 (was 17) with the fix applied
  • Full suite: pytest tests/ -q → 1837 passed (1787 prior + 50 new)

🤖 Generated with Claude Code

_collect_checkable_statements decided whether a line was checkable by
testing whether its first token was a verb. starting "<date>" and
until "<date>" are TokenType.CONNECTIVE, and inherited is
TokenType.OPERATOR -- both consumed statement-initial, before the verb,
by _parse_one_operation (parser.py). A first-token-VERB guard therefore
silently dropped every temporally-prefixed or inherited-prefixed
statement: no error, no warning, just absent from the collected list,
with check_source still reporting encodable=True -- the headline result
concealed the under-reporting.

Found by the Halverson re-run (liminate-dev PR #92): four statements
across three documents excluded from every check, combined checked=17
where it should have been 21. inherited shares the identical defect and
was never exercised by any corpus -- fixed here too, not left for a
second finding.

Inverts the collector: tokenize -> reorder -> parse every line (as
before), then dispatch on the resulting AST node's type -- the same
pattern check_agreement already uses downstream via _VERB_NAMES and
_iter_statements. DefineNode registers into predicate_names before
statements.append, preserving the forward-declaration order exactly.
run._collect_deontic_statements, check_agreement, the encoder, and the
seven checks are untouched -- this changes collection only.

Adds a hand-written coverage table (test_collector_coverage_matches_
parser_accepted_shapes) asserting the collector checks exactly one
statement of the expected type for every deontic verb, define, and
value-form remember, under every prefix combination the parser accepts.
Deliberately not derived from parser.py internals -- a specification of
intended coverage, not a mirror of the implementation, so a future
grammar addition without a matching row here is a gap the test can still
expose once someone notices.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@rmichaelthomas
rmichaelthomas merged commit 37e6af9 into main Jul 21, 2026
2 checks passed
@rmichaelthomas
rmichaelthomas deleted the fix/collector-statement-dispatch branch July 21, 2026 21:41
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