From 41297c88c82ae08436ff42e9454f0df1f04c789e Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sat, 19 Sep 2026 10:14:10 +0200 Subject: [PATCH] The drift guard counted sites; now it names them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comparing the runner's Regexp.new count to six answers cardinality and nothing else. A runner edit that drops one pattern key and adds another in the same commit leaves the count at six, and the checker would go on compiling the key that no longer exists while never seeing the new one — a guard reporting success over input it stopped examining, which is the defect the check itself exists to remove. It now reads which case keys the runner takes and requires that set to equal the keys modelled here, with the keys that carry no pattern declared by name rather than inferred. Both directions fail: a pattern key the checker does not compile, and a modelled key the runner no longer reads. The count check stays, because it still catches a second pattern field added to a key already modelled, which leaves the key set unchanged. Proved by breaking the runner both ways: swapping reject_response for another key keeps the count at six and fails on identity, and adding a seventh key fails on both. Copilot raised this on #768; the pull request merged before the fix landed, so it follows here. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/check-eval-patterns.rb | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/scripts/check-eval-patterns.rb b/scripts/check-eval-patterns.rb index 13a3168b4..9229519a3 100755 --- a/scripts/check-eval-patterns.rb +++ b/scripts/check-eval-patterns.rb @@ -23,18 +23,41 @@ STRING_LISTS = %w[accept reject accept_response reject_response].freeze MATCH_LISTS = { "mocks" => "match", "expect_sequence" => "match" }.freeze MODELLED_SITES = STRING_LISTS.length + MATCH_LISTS.length +# Case keys the runner reads that carry no pattern. Declared rather than +# inferred: a key the runner starts reading has to be classified by someone, and +# the failure when it is not is this check going red. +NON_PATTERN_KEYS = %w[context file max_commands tags task].freeze errors = [] patterns = 0 # A checker that has fallen behind the runner reports success over the keys it -# still knows. The runner compiles one pattern kind per Regexp.new; if that -# count moves, a pattern kind was added or removed and this list has to move -# with it. -sites = File.read(runner).scan(/Regexp\.new\(/).length +# still knows. Two questions, because cardinality alone answers neither: a +# runner edit that drops one pattern key and adds another in the same commit +# leaves the count at six, and this checker would go on scanning the key that +# no longer exists while never seeing the new one (Copilot on #768). +runner_src = File.read(runner) +modelled_keys = (STRING_LISTS + MATCH_LISTS.keys).sort + +# Identity: every case key the runner reads is either one this checker +# compiles, or one declared here as carrying no pattern. +carriers = (runner_src.scan(/\bc\["([a-z_]+)"\]/).flatten.uniq - NON_PATTERN_KEYS).sort +unless carriers == modelled_keys + (carriers - modelled_keys).each do |key| + errors << "#{runner}: reads c[#{key.inspect}], which this checker does not compile — add it to " \ + "STRING_LISTS or MATCH_LISTS, or to NON_PATTERN_KEYS if it carries no pattern" + end + (modelled_keys - carriers).each do |key| + errors << "#{runner}: no longer reads c[#{key.inspect}], but this checker still compiles it" + end +end + +# Cardinality: catches a second pattern field added to a key already modelled, +# which leaves the key set unchanged. +sites = runner_src.scan(/Regexp\.new\(/).length if sites != MODELLED_SITES errors << "#{runner}: the runner compiles #{sites} pattern kinds, this checker models " \ - "#{MODELLED_SITES} (#{(STRING_LISTS + MATCH_LISTS.keys).join(", ")}) — update both together" + "#{MODELLED_SITES} (#{modelled_keys.join(", ")}) — update both together" end cases.each do |path|