Skip to content

fix(trust): rank a declared arrival so a narrowing is not charged as a weakening - #869

Merged
wenzowski merged 1 commit into
mainfrom
claude/cloud-1394-rank-a-narrowing
Sep 5, 2026
Merged

fix(trust): rank a declared arrival so a narrowing is not charged as a weakening#869
wenzowski merged 1 commit into
mainfrom
claude/cloud-1394-rank-a-narrowing

Conversation

@wenzowski

@wenzowski wenzowski commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Closes CLOUD-1394

The defect

rule-predicate-changed fires on any difference in a rule's predicate columns (trust.rs serializes each Rule to JSON and compares per-column). It has no direction, so tightening a gate costs the same admission as loosening one: a groomed Weakens: pair plus a matching commit trailer, with the groom predating the work — which cannot be satisfied retroactively.

The repository has already paid for this twice by shaping the config around the gate. batten.toml:925-933 and :951-960 both record splitting a check into a new receipt row rather than widening an existing checks list, explicitly because a new row raises nothing. Where a new row is not available — a field on an existing row, which is PR #842's case — there is no such route.

The fix

RULE_NARROWING_ON_ARRIVAL, beside RULE_NON_PREDICATE and with the same census discipline. A key the table names, arriving where the base said nothing, is not charged. Everything else — a changed value, a departure, a resized list, an undeclared key arriving — stays unrankable and still fires.

The ranking is declared rather than inferred, and the counterexamples are why. The tempting structural rule — an optional key whose absence preserved the prior behaviour can only narrow — is false here three times over, each already written down in this repository:

key what it does on arrival
bypass_env makes the row suppressible (batten.toml:415-430 calls the smell correct "in its own terms")
key_shape resolves a non-matching subject to absent, which is could-not-look and therefore allow — its own doc says "it narrows what counts as a subject; it never denies"
when_present narrows which calls a mediated_call row gates, so the calls it stops gating are no longer refused

Each is an added, optional, absence-preserving key that widens. A partial ranking that read "unrankable" as "narrowing" would convert this gate's one false refusal into a false pass, which is the direction it has always been right about.

Seeded with two keys, each carrying its argument in the tuple: max_age (a receipt this row accepted can only stop qualifying by aging out; absent, existence is the verdict) and requires_field (three-valued and fails open on an absent field, so it can only add the Refuted deny it introduces).

Absent and null are one reading. A rule column carries skip_serializing_if, so a fresh optional field is missing from the base object; a nested row like CaptureQuery carries none and serializes the same field as null. A ranking that saw only one spelling would rank the rule and not the row inside it — which is exactly the shape CLOUD-1387 has.

What this does not do

Weaken the admission for changes that genuinely loosen a rule, or touch lint.rs's groom/trailer plumbing (CLOUD-1221's family, and correct).

key_at is not declared here, because it does not exist on main. PR #842 adds the field and its one-line row together, which is the unblocking.

Verification

  • Load-time tier, trust.rs: six new cases — the narrowing, the same key moving after it arrived, the key departing, the undeclared arrival (bypass_env, the measured counterexample), the nested-row reading, and the census.
  • Compiled-binary tier, crates/batten/tests/it/config_lint.rs: against a real base ref via pr_fixture. One case carries all three arms, because a case asserting only the narrowing passes trivially under a gate that stopped firing at all.
  • Shown able to fail: with RULE_NARROWING_ON_ARRIVAL emptied, the narrowing case fails and all three discriminating mirrors stay green.
  • mise run verify green.

@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 27 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 5ca6d556-8eab-49c8-b72a-0a37a73d7bb2

📥 Commits

Reviewing files that changed from the base of the PR and between dbdf6d2 and 48aab71.

📒 Files selected for processing (2)
  • crates/batten/src/trust.rs
  • crates/batten/tests/it/config_lint.rs

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.

Copy link
Copy Markdown
Contributor Author

SonarCloud Code Analysis reports a failed analysis on 85a28f5, and it is not this diff's.

Every other check on this head is skippedci, batten-check, bats, cross, commit-lint, semver, perf, windows, darwin-link, final — which is the design rather than a fault: CI does not run on drafts, so a draft iterates at zero CI cost. SonarCloud is not draft-aware, so it queued against a head whose jobs were all skipped and its run came back cancelled after 13s. There is no analysis to read and nothing in the diff it is pointing at.

Not re-running it: a re-run on a draft reproduces the same cancellation, and CI on this branch is meaningless until the PR is readied. That is blocked on something outside the diff, recorded in the PR body — no mise run task can start in this session, so verify never ran and land cannot drive this PR. Readying by hand is exactly what land exists to do instead, so this stays a draft until the toolchain provisions.


Generated by Claude Code

`rule-predicate-changed` fired on any difference in a rule's predicate
columns, so tightening a gate cost the same admission as loosening one: a
groomed `Weakens:` pair plus a matching trailer, with the groom predating
the work. This repository has twice paid that by shaping the config around
the gate rather than touching a row (batten.toml:925-933, :951-960), and
where a new row is not available — a field on an existing row — there is no
such route.

`RULE_NARROWING_ON_ARRIVAL` names the keys whose ARRIVAL can only add
refusals, beside `RULE_NON_PREDICATE` and with the same census. A key it
names, filled where the base said nothing, is not charged; a changed value,
a departure, a resized list and an undeclared arrival stay unrankable and
still fire.

Declared rather than inferred, because "an optional absence-preserving key
can only narrow" is false here three times over: `bypass_env` makes a row
suppressible (batten.toml:415-430), `key_shape` resolves a non-matching
subject to allow by its own doc, and `when_present` narrows which calls a
row gates at all. A ranking reading "unrankable" as "narrowing" would turn
this gate's one false refusal into a false pass.

Absent and `null` are one reading: a rule column carries
`skip_serializing_if` and a nested row like `CaptureQuery` does not, so a
ranking seeing one spelling would rank the rule and not the row inside it.

Closes CLOUD-1394
@wenzowski
wenzowski marked this pull request as ready for review September 5, 2026 17:10
@wenzowski
wenzowski force-pushed the claude/cloud-1394-rank-a-narrowing branch from 85a28f5 to 48aab71 Compare September 5, 2026 17:10
@sonarqubecloud

sonarqubecloud Bot commented Sep 5, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 48aab71 into main Sep 5, 2026
11 of 12 checks passed
@wenzowski
wenzowski deleted the claude/cloud-1394-rank-a-narrowing branch September 5, 2026 17:21
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