check(sequence): add max/min count threshold, same as occurrence - #1162
Draft
theredspoon wants to merge 1 commit into
Draft
check(sequence): add max/min count threshold, same as occurrence#1162theredspoon wants to merge 1 commit into
theredspoon wants to merge 1 commit into
Conversation
Adds Max/Min int fields to Sequence, mirroring Occurrence's fields and semantics: unset (the zero value) keeps every existing sequence rule's behavior of one alert per match, unchanged. Setting either turns the rule into a density check, buffering matches instead of alerting per match and, after the scope's candidates are exhausted, firing once with the match count substituted into the message via core.CondSprintf, the same formatter occurrence already uses for this. Also fixes an early-exit bug the change surfaced: the existing literal-filter optimization skipped tagging entirely, and returned before any match was attempted, whenever the rule's required literal was absent from the block. That is the right call for a normal rule, but wrong for min, whose whole point is to detect too few matches, including zero. Skipped now specifically when Min is set. Real limitation, not fixed here: a sequence rule's declared scope is always narrowed to sentences (see sentenceScope), so this counts matches within one sentence, not across the several sentences of a paragraph. Verified this precisely: built the actual vale binary and ran a VerbTricolonDensity-shaped rule against real text, two tricolons packed into one long sentence fire correctly with the match count in the message, but two tricolons split across two separate sentences of the same paragraph produce no alert, since each sentence gets its own independent Run call with no state shared between them. Counting across sentence boundaries would need that scope-narrowing relaxed specifically for a rule that sets Max/Min, plus a guard against a match spanning the sentence break to avoid a new correctness problem. Neither is attempted here; documented on the new fields and in the PR description rather than left implicit. Refs vale-cli#1161
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #1161. Draft, not requesting review yet: the change works and is tested, but it does not fully solve the motivating use case in the issue, explained below, and I want that visible before anyone spends review time on it.
What this adds
Max/Min intonSequence, same names and semantics asOccurrence's. Unset (the zero value, every existingsequencerule today) keeps current behavior exactly, one alert per match. Setting either buffers matches instead of alerting per match, and after the scope's candidates are exhausted, fires once with the match count substituted into the message viacore.CondSprintf, the same formatteroccurrencealready uses for this.Also fixes a real bug the change surfaced: the existing literal-filter optimization returns before tagging or matching whenever the rule's required literal is absent from the block, correct for a normal rule, wrong for
min, whose whole point is to detect too few matches, including zero. Skipped now specifically whenMinis set.What I verified
Unit tests following the existing table-driven convention in
sequence_test.go:maxfiring once with the right count on repeated matches, staying silent under the threshold,minfiring the document-scoped fallback on zero matches (mirroringoccurrence's own zero-match branch), and confirming every existingsequencerule withMax/Minunset behaves identically to before.go teston every non-e2e package,golangci-lint run(v2.5, matching CI),gofmt -l, all clean. Did not run the e2e suite, it needs a toolchain (Ruby, Java, dita-ot, typst2vast, etc.) I don't have set up here; the change doesn't touch anything e2e would exercise, but flagging the gap rather than silently skipping it.Built the actual binary and ran a real
VerbTricolonDensity-shaped rule (max: 1, the tricolon pattern from #1161) against real text, not just unit tests:The real limitation
That second case is the actual motivating scenario in #1161, and it doesn't work. Root cause, confirmed by reading
internal/nlp/provider.go'sdoNLPandsequence.go'ssentenceScope: asequencerule's declared scope is unconditionally narrowed to sentences inNewSequence, regardless of what the YAML declares or whetherMax/Minis set. Each sentence gets its own independentRuncall with no state shared between them, confirmed empirically too: the plain unbounded version of the same rule, noMax/Minat all, already produces two separate alerts (one per sentence) for that same two-sentence input, which is what makes theMax/Mincounting invisible across sentences, there's nothing to count across, each call only ever sees its own sentence's matches.Closing that gap would need two more things I did not attempt here: relaxing
sentenceScope's narrowing specifically for a rule that setsMax/Min(tagging accuracy doesn't require it,f.TokensWithalready segments and tags per sentence internally regardless of the input block's size, so this looks safe on that front), and a guard against a match spanning a sentence boundary once multiple sentences are actually reachable in oneRuncall, to avoid a new correctness problem. Givensequence's scope handling has already had two real, subtle bugs found and fixed very recently (#1124, #1126), I'd rather flag this precisely than rush a second change into the same area under one PR.Happy to take this further if a
sentenceScoperelaxation along these lines looks like the right direction to you, or to narrow the issue to just the single-sentence case this PR actually delivers if that's more useful on its own.