Skip to content

#73: the caller — a ratchet under UNNAMED refusals, because the missing leg was authority - #599

Merged
jobordu merged 1 commit into
mainfrom
teamlead/disposition-ratchet
Sep 6, 2026
Merged

#73: the caller — a ratchet under UNNAMED refusals, because the missing leg was authority#599
jobordu merged 1 commit into
mainfrom
teamlead/disposition-ratchet

Conversation

@jobordu

@jobordu jobordu commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Closes #73 by supplying the leg it has been missing for 16 days — which was authority, not code.

The state #73 was actually in

tools/disposition-scan.py already built the predicate, planted it four ways, and caught a
use-versus-mention bug in its own first version. Then it stopped, and said why:

"a RATCHET on this count is the shape that fits (#39), and committing other roles' files to a
floor is not this tool's call. It reports; someone else decides."

⇒ That is correct, and it is not a technical gap. Deciding that 45 files owned by other roles may
not get worse binds every one of those roles. An instrument cannot make that commitment on
their behalf. TEAMLEAD can. The issue sat with a working predicate and no caller because nobody
who could supply that had been asked for it.

Measured at 6ee6b07

POPULATION  61 non-test tools/*.py
  NAMED             3
  UNNAMED          45   ⇐ the floor recorded here
  NO-REFUSAL-PATH  13
  PARTITION        61

A ratchet, not a gate

A gating check reds 45 pre-existing files on its first run. A red naming 45 files nobody touched
gets reverted or ignored — worse than no check at all, because it teaches that the gate is noise.

This fails only if the count GROWS. History is never red; adoption lowers the floor; --record
refuses to raise it.

⛔ It does not write what it reads

This is the design decision, and it is taken from a defect measured today — #598:
index-watch.py records the sha it just reported on, so the run that finds drift is the run that
suppresses it. Re-running to confirm a finding destroys the finding, and its second exit 0 means
"did not check" while reading as "clean".

⇒ On a DROP this says the floor can be lowered and refuses to lower it. --record is a separate
act with its own diff and its own reviewer. The suite asserts byte-identical output and an
untouched baseline across two consecutive runs
, so this tool fails if it ever grows that shape.

⚠ Three things this file got wrong first — all caught by its own suite

  1. def report(root, out=sys.stdout) binds the default at definition time, so it captured
    the original stdout and redirect_stdout never reached it. Eight tests failed against empty
    output. Early binding, not a logic error.
  2. It failed its own rule. classify() credits a disposition only when it shares a physical
    source line
    with both the refusal text and the print(/stderr emit. Every refusal here named
    ADDABLE across implicitly concatenated lines, and scored UNNAMED. Conformed — and the
    limitation is now recorded in the README, because a line-oriented predicate has a formatting
    consequence that has nothing to do with whether a remedy is named clearly.
  3. The empty-population test passed for the wrong reason. It deleted every *.py — including
    disposition-scan.py — so it tripped the missing predicate guard and never reached the guard
    it is named for. It asserted VOID, got VOID, and went green. Two different VOIDs is exactly
    A correctly-reported absence is indistinguishable from an unfixable one — the VOID that cost four hours was true on every run #73's subject
    , so a test that cannot tell them apart is the wrong test. Now drives census()
    directly.

What it does not establish

⛔ Inherited from #73 against itself, and not quietly dropped: a check could pass while every
refusal names a remedy nobody can act on.
PRESENCE of a disposition is not USEFULNESS of one.
This counts the first. Nothing here measures the second, and a falling count is not evidence
that the dispositions named are any good.

index-watch now exits 1 — the indexed population moved, correctly, because this adds an
instrument. Its documented refresh is verdict-census.py --ledger, which runs every instrument's
main path; for a forge-touching instrument that performs the action. Not run here, and not
claimed as clean.

Verification

tools/test_disposition_ratchet.py 13/13
--self-test 7 legs, both directions named
idempotence output byte-identical, baseline sha unchanged across two runs
predicate delegated via importlib; an interface change VOIDs rather than guesses (#405)
this tool under its own predicate NAMED — a check that fails its own rule has no standing

🤖 Generated with Claude Code

https://claude.ai/code/session_01DTMf4EvaTEnDKXY47efRZZ

@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 13 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 4b91bcbf-7b32-4e2d-bc85-cc9da407a1f5

📥 Commits

Reviewing files that changed from the base of the PR and between 777b950 and d31c50d.

📒 Files selected for processing (4)
  • tools/README.md
  • tools/disposition-baseline.json
  • tools/disposition-ratchet.py
  • tools/test_disposition_ratchet.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

jobordu added a commit that referenced this pull request Sep 6, 2026
…time (#600)

Measured on PR #599's own CI run:

    ⛔ 5 age at merge   9s since creation   -> the advisory job went red

A workflow triggered BY a PR's creation observes an age of seconds BY
CONSTRUCTION, and it is not merging, so "was this old enough at merge?" is not
a question it can ask.

⛔ THIS IS THE THIRD TIME, AND THAT IS THE FINDING. merge-guard has three legs
that are facts about THE MERGE EVENT rather than about the PR:

    0 holder == session    a runner has no holder session      fixed 1st
    2 required gate        it is INSIDE the run it asks about  fixed 2nd
    5 age at merge         there is no merge yet               fixed HERE

⚠ I fixed 0, then 2, and each time treated it as a one-off. I never re-read the
remaining legs asking "which others have this property?" — so the same defect
shipped three times in one tool, and each red cost a CI round trip. One
instance is a bug; three is a class, and the class was visible after the first.

⇒ `--shape-only` now evaluates ONLY the shape legs — 1 base, 3 reviews, 4
three-dot diff — and NAMES each omission rather than passing it silently.

★ The control is a PAIR on identical input, because a skip with no
known-negative is a bypass:

    seconds-old PR + --shape-only   -> exit 0, "a merge that has not happened"
    seconds-old PR, no --shape-only -> exit 1, leg 5 BLOCKS

#224 measured 25 of 100 PRs merged inside 60s of creation. That guard is the
reason leg 5 exists, and the second test asserts the exemption did not weaken
it. Suite 26/26 (was 24).


Claude-Session: https://claude.ai/code/session_01DTMf4EvaTEnDKXY47efRZZ

Co-authored-by: Jonathan Borduas <jonathan.borduas@toolkit3d.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
disposition-scan.py built #73's predicate, planted it four ways, caught a
use-versus-mention bug in itself — then stopped, and said exactly why:

  "committing other roles' files to a floor is not this tool's call.
   It reports; someone else decides."

⇒ Right, and not a technical gap. Deciding that 45 files owned by other roles
may not get worse binds every one of those roles. It is a TEAMLEAD act. #73
sat with a working predicate and no caller for 16 days because the missing leg
was AUTHORITY, and nobody who could supply it had been asked.

★ A RATCHET, NOT A GATE. A gating check reds 45 pre-existing files on its first
run; a red naming 45 files nobody touched gets reverted or ignored, which is
worse than absent because it teaches the gate is noise. This fails ONLY IF THE
COUNT GROWS. Adoption lowers the floor; --record refuses to raise it.

⛔ IT DOES NOT WRITE WHAT IT READS — taken from #598, measured the same day.
index-watch.py records the sha it just reported on, so the run that finds drift
is the run that suppresses it. ⇒ On a DROP this says the floor CAN be lowered
and refuses to. The suite asserts byte-identical output AND an untouched
baseline across two consecutive runs.

⚠ THREE THINGS THIS FILE GOT WRONG FIRST, all caught by its own suite:
  1 `def report(root, out=sys.stdout)` binds the default AT DEFINITION TIME, so
    redirect_stdout never reached it. Eight tests failed on empty output.
  2 It FAILED ITS OWN RULE — classify() credits a disposition only on the same
    PHYSICAL SOURCE LINE as the refusal and the emit; every refusal here named
    ADDABLE across concatenated lines and scored UNNAMED.
  3 The empty-population test deleted disposition-scan.py too, so it tripped
    the missing-predicate guard and PASSED FOR THE WRONG REASON. Two different
    VOIDs is #73's own subject.

⛔ THE BOUND, from #73 against itself: a check could pass while every refusal
names a remedy nobody can act on. PRESENCE is not USEFULNESS. This counts the
first only.

⚠ REBUILT ON A CLEAN BASE. The previous branch was cut before #597 was SQUASH
merged, so a rebase replayed commits main holds only as one squashed commit and
conflicted; `git merge-tree` exit 1 and GitHub mergeable=false agreed. ⇒ Rather
than resolve a conflict against a base that no longer exists, this is cut from
current origin/main and the README edits re-applied to ITS copy — carrying the
old copy forward would have REVERTED #600's leg-5 fix silently.

check-tools-index 0 · test_index_watch 0 · test_disposition_ratchet 13/13 ·
test_merge_guard 26/26. All four READ from the runs, not assumed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTMf4EvaTEnDKXY47efRZZ
@jobordu
jobordu force-pushed the teamlead/disposition-ratchet branch from 5ad0af9 to d31c50d Compare September 6, 2026 00:08
@jobordu
jobordu merged commit 68343e0 into main Sep 6, 2026
6 checks passed
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.

A correctly-reported absence is indistinguishable from an unfixable one — the VOID that cost four hours was true on every run

1 participant