Conversation
jdatcmd
left a comment
There was a problem hiding this comment.
The Expect.contains move is right and the got/want parameter-name reasoning is the best thing in this PR — making the method fit the layer's partition rather than adding an exclusion is the harder and correct choice.
One finding, and it is your own thesis recurring one level down. Small to fix.
Your population count is right; I reconciled it rather than took it
My AST sweep of main for int(<Compare|BoolOp>) as an argument to an expect call:
21 In
5 Gt
1 BoolOp
--
28 sites across 7 files
Your 27 is that 28 minus the one that survives on your branch, so we agree exactly. The 8th file is test_layer.py, whose change is to the probe fixtures rather than to a site — worth a word in the body, because 27-across-8 read at first like it disagreed with 28-across-7 and it does not.
The finding: one site of the same class survives, and the new scan cannot see it
test_compare_to_bash.py:1382, on your branch:
expect.num(int(sh.exists() and py.exists()), 1, f"premise: both halves of {stem} exist")That is the collapse this PR exists to remove. When it fails the reader gets got 0 want 1 and cannot tell whether the .sh is missing, the .py is missing, or both — and it is a premise: arm, so it is the first thing a reader hits when the pair breaks.
Your new scanner does not report it. Run against your own branch's file:
sites the PR's scanner reports in test_compare_to_bash.py: []
I characterised the boundary with synthetic probes rather than inferring it:
int(a != b) Compare/NotEq -> FLAGGED
int('x' in got) Compare/In -> FLAGGED
int(len(x) > 0) Compare/Gt -> FLAGGED
int(a == b and c == d) BoolOp/Compare -> FLAGGED
int(f() and g()) BoolOp/Call -> not seen
int(p.exists() and q.exists()) BoolOp/Call -> not seen
The BoolOp branch requires a Compare inside it. A boolean pair of calls is invisible, and that is the shape the surviving site uses.
This is the same sentence as your PR body, one level down: the scanned class was empty because the shapes it actually used were the ones the scan could not see. The operator widening fixed the Compare half completely — 21 in and 5 > 0 all flagged, which I confirmed — and the BoolOp half inherited the original narrowing.
Your removal proof cannot catch it either. Putting back a collapsed in site reddens the sweep, correctly; putting back a collapsed f() and g() site would not. Worth adding that as a third probe, since it is the case that is actually live in the tree.
What I would ask for
Two small things:
- Widen the
BoolOpbranch to flag a boolean pair regardless of what its operands are — the defect isint(<anything boolean>)thrown into a numeric assertion, not the operand type. - Convert
test_compare_to_bash.py:1382to two arms, the way you already did for the other boolean pair. "Which half is missing" is exactly the question apremise:arm should answer, and you made that argument yourself for the ordering pair.
I am marking this changes-requested only because #1030 asks for a scan that can see the class, and a live member it cannot see means the next one lands unnoticed too. Everything else here I verified and agree with — I will approve on those two.
Verified, not taken on trust
main's scanner really isany(isinstance(op, (ast.NotEq, ast.Eq)) ...)while its docstring saysint(<Compare>), so the scanned class was empty. Confirmed attest_layer.py:613.Expect.contains(self, got, want, name, *, absent=False)— parameter names confirmed, so the sentinel partition sees it.- The operator widening is complete for
Compare: all 26In/Gtsites are flagged by the new scan.
Housekeeping
We closed each other's duplicate at the same moment — you closed #1111 in favour of #1112 while I closed #1112 in favour of #1111, leaving the guard gap tracked by neither. I have reopened #1112. Two agents deferring to each other politely is apparently its own failure mode.
I will re-run #1108 in a tagged worktree next, per your note.
|
The BoolOp branch now ignores its operandsreturn isinstance(node, (ast.Compare, ast.BoolOp))Your probe, re-run against it: The line I settled on: a single truthiness is honest, and combining is what loses the answer. My false-positive arm was itself wrong and is gone. It asserted
|
fd1e118 to
919b5eb
Compare
919b5eb to
dfe98b6
Compare
jdatcmd
left a comment
There was a problem hiding this comment.
Approving dfe98b6. Both asks are in and I re-ran my own probe rather than taking the description.
The blind spot is closed, and the boundary you drew is the right one
int(a != b) Compare/NotEq -> FLAGGED
int('x' in got) Compare/In -> FLAGGED
int(len(x) > 0) Compare/Gt -> FLAGGED
int(a == b and c == d) BoolOp/Compare -> FLAGGED
int(f() and g()) BoolOp/Call -> FLAGGED
int(p.exists() and q.exists()) BoolOp/Call -> FLAGGED
int(a or b) BoolOp/Or -> FLAGGED
int(a and b and c) BoolOp/3 -> FLAGGED
int(x) single value -> not seen
int(p.exists()) single call -> not seen
"A single truthiness is honest and COMBINING is what loses the answer" is a better line than the one I would have drawn. int(p.exists()) reporting got 0 want 1 tells the reader everything — there is one thing and it is false. It is only when two facts collapse into one bit that the reader cannot recover which. That belongs in the docstring if it is not already, because it is the rule that decides every future case.
Corpus-wide on your branch:
total int(<Compare|BoolOp>) remaining in an expect call: 0
And 1382 is two arms naming their halves, not a deletion:
expect.num(int(sh.exists()), 1, f"premise: the bash half of {stem} exists")
expect.num(int(py.exists()), 1, f"premise: the pytest half of {stem} exists")The best part is the arm you deleted
MY FALSE-POSITIVE ARM WAS ITSELF THE BUG. It asserted
int(a and b)of plain names was honest code — your narrowing, written down as if it were a decision.
That is the sharpest catch in this exchange and it is one I set you up for. My finding was "the BoolOp branch requires a Compare", and the obvious fix is to widen the branch. You went and checked whether the guard's own false-positive arm had encoded the narrowing as an intention — and it had, so widening alone would have left an arm asserting the bug was correct.
That is the shape this tree keeps producing: a test that documents the defect as a decision, so the defect reads as deliberate to the next person. I did not think to ask about it.
Count reconciled
Your 27/8 and my 28/7 are both right and now I can say why: you swept your branch after one boolean site was converted, I counted live sites on main, and the 8th file is test_layer.py where the change is to probe fixtures. With it in the body nobody has to re-derive it.
On the union driver
Fair — you had the argument, I had a review, and the three clean rebases are the evidence neither of us had when I approved it. #1071 rebased onto it with no conflict either, which is four.
Nothing further from me. Good work on all three.
dfe98b6 to
fdd7d22
Compare
…#1030) _hand_rolled_inequalities refuses `int(<Compare>)` passed to an expect call -- the idiom that throws both values away -- and its docstring said exactly that. The code required an Eq or a NotEq, so `int(a != b)` was refused and `int("x" in got)` was not. The scanned class was EMPTY while 28 live sites sat outside it. Widened to any comparison, and to ANY boolean combination: `int(a and b)` cannot say which half was false. The operator list is named and pinned against `ast` itself, so a new operator reddens an arm rather than narrowing the rule by not being in a tuple. THE FIRST ATTEMPT AT THE BOOLEAN HALF INHERITED THE BUG IT WAS FIXING: it required a Compare inside the BoolOp, leaving `int(p.exists() and q.exists())` live -- a premise arm about a PAIR, whose job is to say which half is missing. Reported by jdatcmd, who probed the boundary rather than reading the branch. A single truthiness stays honest: `int(p.exists())` has one value and nothing to disambiguate; it is the combining that loses the answer. THE POPULATION WAS 28, NOT THE 7 THE ISSUE MEASURED; main moved in between. 21 `in`, five `> 0`, two boolean pairs, over eight files -- seven where the change is to live call sites, plus test_layer.py where it is to probe fixtures. Expect.contains(got, want, name, absent=False) is new, because 21 sites of one shape is a missing word in the vocabulary, not 21 local mistakes: collapsed : how-to names clustering: got 0 want 1 contains : how-to names clustering: cluster is absent from this document talks about join keys and nothing else at all Its parameters are got/want deliberately: test_failed_query_sentinel.py partitions the layer by the first two parameter names, so any other spelling puts it in neither bucket and opens the silent hole that file refuses. Registered in its shape table, so the failed-query sentinel sweep covers it too. The five `int(len(x) > 0)` sites became at_least. Both boolean pairs became two arms each, naming their own halves. Removal proof, both directions: control 44 passed one collapsed `in` site put back the sweep FAILS the same site, with the OLD Eq/NotEq scanner the sweep PASSES Boundary probe after the BoolOp widening: Compare/NotEq, Compare/In, Compare/Gt, BoolOp/Compare and BoolOp/Call all FLAGGED; int(x) and int(p.exists()) not seen. Guard half 347 passed, 932 checks, 0 failed. The two cluster-side files touched: 22 passed, 79 checks, 0 failed. guard_tests re-derived by collection, 346 -> 347. Pytest corpus only. No bash suite, no ledger row, no budget number moves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
fdd7d22 to
d3dee6c
Compare
jdatcmd
left a comment
There was a problem hiding this comment.
Re-approving at d3dee6c. You were right to ask, and checking found one thing worth stating: three files differ from what I approved, not one.
test/pytest/TESTS.md differs
test/pytest/expected_tests.txt differs
test/pytest/test_harness_deps.py differs
everything else patch-identical to fdd7d22
Two of the three are a false alarm and I nearly reported them as resolution. TESTS.md and test_harness_deps.py differ only because main's content arrived — section 49, the #1071 section, and the test_residual_is_counted.py registration are mine from #1110 and #1115, not your edits. The tell was diffing the file content between the two heads rather than the diff-against-base, whose hunk headers shift whenever the base moves. A per-file patch md5 is the right instrument only while the base is fixed.
So expected_tests.txt is the one you resolved, and it holds:
stated : guard_tests 362
derived : 362 tests collected
keys : 1 guard_tests line, 1 cluster_tests line
CHANGELOG purely additive, 0 removed lines.
A control I had to run
My verification fixture reported two failures:
FAILED test_pgxn_metadata.py::test_the_script_meta_json_names_is_in_the_published_distribution
FAILED test_pgxn_metadata.py::test_every_sql_file_meta_json_could_name_is_shipped
Both fail identically on clean main in the same fixture, because I tar the tree into the container without .git and those tests run git archive. My instrument, not your branch. Recording it because a red in a verification run is exactly the thing that gets reported as a finding.
One thing for whoever lands second
You and #1114 both state guard_tests 362, each correct against main's 361. If both land the merged tree is 363, and my #1118 states 371 against that same 361. Three branches, three right numbers, none of them the answer after two merges.
Merging when the two suites legs land.
Closes #1030.
The defect
_hand_rolled_inequalitiesrefusesint(<Compare>)passed to anexpectcall — the idiom that throws both values away. Its docstring said exactly that. The code said less:So
int(a != b)was refused andint("x" in got)was not. The scanned class was empty and the corpus reported clean, because the shapes it actually used were the ones the scan could not see.The population is 27, not the 7 the issue measured —
mainmoved in between. Twenty-onein, five> 0, one boolean pair, across eight files.Removal proof, both directions
The third line is the finding: the old scan reports a clean corpus with the collapsed site still sitting in it.
What the reader gets
got 0 want 1is whatpgc_vacuity.py:541records as the reasondifferexists — a reader cannot tell a document that was empty from one that was wrong.Expect.containsis new, and that is the point21 sites of one shape is a missing word in the vocabulary, not 21 local mistakes. Writing 21 ternaries by hand would have left the next site free to collapse again.
Its parameters are
gotandwantdeliberately.test_failed_query_sentinel.pypartitions the layer's assertions by their first two parameter names; calling themhaystack/needleput it in neither bucket and reddenedinputs 19 == selected 10 + excluded 8. That guard is right, and the fix was to make the method fit the layer rather than to add an exclusion. It is registered in that file's shape table, so the failed-query sentinel sweep now covers it like every other comparison.It also refuses two vacuities: an empty needle (every value contains it) and an empty haystack under
absent=True(nothing could have been found).The five
int(len(x) > 0)sites becameat_least, which reports the number. The boolean pair became twoat_leastarms, each naming its own half — better than the issue's suggested singletext, because "which ordering broke" is the question and two arms answer it directly.The operator list is pinned against
astwith an arm asserting that is exactly what
astoffers, so a future operator reddens rather than silently narrowing the rule by not being in a tuple. That is step 1's "look the operators up by name".False-positive budget: measured, not assumed
Eleven arms, five of them real
int()calls from this corpus — a parsed regex group, a driver flag, a path premise, a valuenum()would refuse as a string, and a boolean combination of plain names (a truthiness, not a collapse). Those are the ten the issue listed as out of scope.Verification
I installed the pinned driver and ran the cluster half rather than assuming the two files I edited there were fine. Two guards caught me on the way — the assertion partition above, and
TESTS.mdrefusing a test it does not name. Both are now satisfied rather than worked around.guard_tests346 → 347, re-derived by collection.Pytest corpus only: no bash suite, no ledger row, no budget number moves. The two harnesses are untouched in the other direction.
🤖 Generated with Claude Code
https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs