Skip to content

feat(rhodibot): gate criteria on declared capabilities, and classify what a file check found - #545

Merged
hyperpolymath merged 1 commit into
mainfrom
feat/rhodibot-capability-gates
Sep 19, 2026
Merged

hyperpolymath merged 1 commit into
mainfrom
feat/rhodibot-capability-gates

Conversation

@hyperpolymath

@hyperpolymath hyperpolymath commented Sep 19, 2026 •

Copy link
Copy Markdown
Owner

Two steps of the four that turn the canon's criteria into checks. (The first, reading requirements from desc rather than template_ref, landed in #543.)


1. Capability gates -- 4d7eea4

26 of the canon's 74 criteria are gated on a capability. Asked as universal questions they demand files a repository has no reason to carry -- an FFI seam from a docs site, a container from a library.

src/canon/profile.rs reads .machine_readable/rsr-profile.a2ml:

  • effective set = (direct list | preset expansion) + add - remove, the gate table's own model;
  • Applicability is Universal, Declared(capability) or NotDeclared(capability); a gated criterion leaves the denominator as na rather than counting against the repository;
  • a misspelt capability, an unknown preset, a misspelt key, and removing something never declared are all refused. capabilities = ["russt"] quietly collapsing to an empty set would switch a whole gate of checks off and report the repository as clean.

The gate table is vendored and pinned like the criteria -- same release, same rule -- under [gates] in canon/pin.toml (b65ce754..., matching canon.lock's gates slot and the spine profile's gates_sha256). scripts/check-canon-drift.sh now checks both artefacts; both failure paths are exercised.

Against the five pilot repositories, which declare nothing: of the 31 criteria whose description names files, 22 are scored and 9 are na (6 governance-tier, 2 web-ui, 1 docs-site).

2. Verdict classification -- 96c70e3

The four answers a file check can give are not interchangeable: present where the canon records it, present under a location the canon retired, present elsewhere, absent.

src/canon/verdict.rs holds the rule the pilot established: the description decides what is required; template_ref only says where the canon's own template keeps it. Both of the pilot's false positives were criterion 1.2.2 -- satisfied by a root .pre-commit-config.yaml even though the template keeps its copy under ci/.

A bare filename is accepted in the two places that are unambiguously the repository's own: the canon's recorded location, or the repository root. Both halves of that rule were learned by running it against real trees:

  • "anywhere" passed 1.2.4 (.tool-versions) on a copy at robot-repo-automaton/templates/skeleton/.tool-versions -- another project's template skeleton;
  • "root only" would have failed gitbot-fleet's .machine_readable/descriptiles/*.a2ml, which satisfy 3.1.2-3.1.7 exactly where the canon keeps them.

Deprecated locations are read from the canon, not hardcoded: the canon states .machine_readable/6a2/ is retired in prose, inside criterion 3.1.1's description, and there is no machine-readable table. The sentence is parsed, resolved against the criterion's own template_ref, and asserted by a lockstep test -- a canon that stops saying it fails the test rather than quietly reclassifying every retired path as ordinary.

A repository holding the file in both a live place and a retired one is reported as relocated with the leftover named, not as living in the retired path. An absent file names what is sitting there instead (CODE_OF_CONDUCT.adoc where 2.1.4 asks for .md).

Against two real trees, read on 2026-09-19:

repository satisfied relocated deprecated missing
nesy-solver 12 4 3 3
gitbot-fleet 17 3 0 2

The pilot's own record for nesy-solver -- three findings, all descriptive files left under the retired path -- is asserted in the lockstep suite, as is 1.2.2 staying fixed.

Verification

  • cargo test --locked -- 117 lib + 10 lockstep + 50 integration, all pass
  • cargo clippy --locked --all-targets -- -D warnings -- clean
  • scripts/check-canon-drift.sh -- exit 0 against main; exit 1 when either pin is forged
  • rustfmt --edition 2024 --check clean on every file touched

Advisory only: rhodibot reports against canon criterion ids; hypatia remains the canon's single normative checker.

…capability

26 of the canon's 74 criteria are gated. Asked as universal questions they
demand files a repository has no reason to carry -- an FFI seam from a docs
site, a container from a library -- which is the same mistake as reading
`template_ref` as a requirement, one level up.

`src/canon/profile.rs` reads `.machine_readable/rsr-profile.a2ml` and answers
whether a criterion applies:

- effective set = (direct list | preset expansion) + add - remove, per the gate
  table's own model
- applicability is Universal, Declared(capability) or NotDeclared(capability);
  `na` leaves the denominator rather than counting against the repository
- a misspelt capability, an unknown preset, a misspelt key and removing
  something never declared are all refused: `capabilities = ["russt"]` quietly
  collapsing to an empty set would switch a whole gate of checks off and report
  the repository as clean

The gate table is vendored and pinned like the criteria -- same release, same
rule -- and `scripts/check-canon-drift.sh` now checks both artefacts against the
canon and against `canon.lock`. `GateTable::unknown_gates` fails if a criterion
gates on a capability the vocabulary does not define, because such a criterion
can never become applicable: that is how `deno` came to be listed.

Against the five pilot repositories, which declare nothing: of the 31 criteria
whose description names files, 22 are scored and 9 are `na` (6 governance-tier,
2 web-ui, 1 docs-site). The pilot's 11 universal criteria are all in the scored
set, so the earlier pilot numbers still hold.
@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 15 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: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: dd91ea8f-6159-4b54-9b3b-fa7552f8edbf

📥 Commits

Reviewing files that changed from the base of the PR and between 6c52b45 and 4d7eea4.

📒 Files selected for processing (6)
  • bots/rhodibot/canon/pin.toml
  • bots/rhodibot/canon/template-capability-gates.toml
  • bots/rhodibot/src/canon.rs
  • bots/rhodibot/src/canon/profile.rs
  • bots/rhodibot/tests/canon_lockstep.rs
  • scripts/check-canon-drift.sh

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.

@hyperpolymath
hyperpolymath merged commit e1393f1 into main Sep 19, 2026
31 of 35 checks passed
@hyperpolymath
hyperpolymath deleted the feat/rhodibot-capability-gates branch September 19, 2026 13:38
@hyperpolymath hyperpolymath changed the title feat(rhodibot): score gated criteria only when the repository declares the capability feat(rhodibot): gate criteria on declared capabilities, and classify what a file check found Sep 19, 2026
hyperpolymath added a commit that referenced this pull request Sep 19, 2026
Third of the four steps that turn the canon's criteria into checks
(steps one and two landed in #543 and #545).

A criterion that names files has four possible answers, and they are not
interchangeable:

| verdict | meaning | worth |
|---|---|---|
| present at the path | the file is where the canon records it | nothing
to do |
| present under a retired location | the file exists, in a place the
canon has retired | move it |
| present elsewhere | the file exists, in neither place | check it |
| absent | the file is not there | add it |

## The rule this holds

**The description decides what is required; `template_ref` only says
where the canon's own template keeps it.** The advisory pilot read
`template_ref` as the requirement and failed repositories that satisfied
the criterion elsewhere -- both of its false positives were criterion
1.2.2, satisfied by a root `.pre-commit-config.yaml` even though the
template keeps its copy under `ci/`.

A bare filename -- "STATE.a2ml", "Justfile" -- is accepted in the two
places that are unambiguously the repository's own: **the canon's
recorded location, or the repository root**. Everywhere else is
`Elsewhere`, a finding, with the canon's location named as a note. Both
halves of that rule were learned by running this against real trees:

- **"anywhere"** passed `1.2.4` (`.tool-versions`) on a copy at
`robot-repo-automaton/templates/skeleton/.tool-versions` -- another
project's template skeleton;
- **"root only"** would have failed gitbot-fleet's
`.machine_readable/descriptiles/*.a2ml`, which satisfy 3.1.2-3.1.7
exactly where the canon keeps them.

A description that gives a path (`.well-known/security.txt`) still means
that path: a copy elsewhere is `Elsewhere`.

## Retirements are read, not hardcoded

The canon states `.machine_readable/6a2/` is retired -- in prose, inside
criterion 3.1.1's description: "NOT 6a2/, which is deprecated
2026-06-30". There is no machine-readable deprecation table, so the
sentence is parsed, resolved against the criterion's own `template_ref`,
and asserted by a lockstep test. A canon that stops saying it fails the
test rather than quietly reclassifying every retired path as ordinary.

## Details the real trees forced

- a repository holding the file in **both** a live place and a retired
one is `relocated` with the leftover named, not `deprecated`: the move
is under way, and "deprecated location" would overstate it (nesy-solver,
3.1.2-3.1.4);
- an absent file names what is sitting there instead -- gitbot-fleet has
`CODE_OF_CONDUCT.adoc` where 2.1.4 asks for `.md`, and a bare "absent"
would send a reader looking for a file that is right there under another
name;
- severities are ordered `Missing > Deprecated > Relocated > Satisfied`.

## Against real repositories

Trees read on 2026-09-19:

| repository | satisfied | relocated | deprecated | missing |
|---|---|---|---|---|
| nesy-solver | 12 | 4 | 3 | 3 |
| gitbot-fleet | 17 | 3 | 0 | 2 |

The pilot's own record for nesy-solver -- three findings, all
descriptive files left under the retired path -- is asserted in the
lockstep suite, as is 1.2.2 staying fixed.

## Verification

- `cargo test --locked` -- 117 lib + 10 lockstep + 50 integration, all
pass
- `cargo clippy --locked --all-targets -- -D warnings` -- clean
- `scripts/check-canon-drift.sh` -- exit 0
- `rustfmt --edition 2024 --check` clean on every file touched

Advisory only: rhodibot reports against canon criterion ids; hypatia
remains the canon's single normative checker.

Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
hyperpolymath added a commit that referenced this pull request Sep 19, 2026
… line (#549)

Fourth and last step of the canon-sourced rules (#543, #545, #547). The
three
pieces before this one read the rules from the canon, decide which
criteria
apply, and classify what was found. Nothing ran them. This adds
`rhodibot canon`:

```sh
rhodibot canon --owner hyperpolymath --repo ubicity
rhodibot canon --path /path/to/checkout --format json
rhodibot canon --owner O --repo R --fail-on missing
```

**Advisory by default** — exits 0 whatever it finds. The canon
designates
hypatia's `rsr-conformance` as the single normative checker, so a gate
here would
be a second opinion claiming an authority it does not have. `--fail-on`
is opt-in.

## Two details that carry their weight

- `GitHubClient::tree_paths` asks once for the whole file list and
**refuses a
truncated response**. A partial tree is missing files, a missing file
reads as
an absent one, and the report would manufacture findings against a
repository
  that has the file.
- `get_file_content_if_present` distinguishes 404 from everything else:
an absent
`rsr-profile.a2ml` means "declares no capabilities", a 500 means "could
not read
the declaration". Treating the second as the first would silently shrink
the
check to the universal criteria and report a cleaner scorecard than the
truth.

`canon/report.rs` assembles the report (counts, findings worst-first,
retired
locations quoted) and serialises to JSON. `canon/local.rs` reads a
checkout,
preferring `git ls-files` over a walk — a walked list includes build
output, and a
`CODE_OF_CONDUCT.md` inside `target/` is not the repository's code of
conduct.

## The pilot re-run through the tool

Unauthenticated, against the five repositories of the advisory pilot:

| repository | at path | elsewhere | deprecated | absent |
|---|---|---|---|---|
| gitbot-fleet | 17 | 3 | 0 | 2 |
| ubicity | 7 | 2 | 6 | 7 |
| awesome-nickel | 8 | 1 | 7 | 6 |
| julia-professional-registry | 8 | 1 | 7 | 6 |
| nesy-solver | 12 | 4 | 3 | 3 |

**110 questions, 52 satisfied, 58 findings** (11 relocated, 23 at the
retired
`6a2/`, 24 absent). 22 of the canon's 74 criteria apply to every one of
them: 26
are gated on a capability none declares, 26 more ask content questions
rather
than naming files.

The 23 files under `.machine_readable/6a2/` are still 23, re-derived
from
criterion 3.1.1's own sentence about the retirement rather than from a
hardcoded
path, with the same per-repository breakdown as the hand run. Both of
the hand
run's false positives (criterion 1.2.2) now come back satisfied.

## Two findings the earlier scope could not see

- `2.1.3` asks for `SECURITY.md`; `ubicity` has `SECURITY.adoc`. The
verdict is
  `absent` and the report now says what is there instead. Same for
`CODE_OF_CONDUCT.md`/`CODE_OF_CONDUCT.adoc` at `2.1.4`. If `.adoc` is
the
  estate's convention, the canon is where that belongs.
- `2.2.1` is `relocated` for all five: the `.well-known/` files live
inside a
`www/` publication bundle, not at the repository root the criterion
names. The
  origin-versus-repository question is worth an answer.

## Verification

- `cargo test --locked` — 135 lib + 10 lockstep + 50 integration, all
pass
- `cargo clippy --locked --all-targets -- -D warnings` — clean
- new wiremock tests cover the tree listing (directories excluded), the
truncated
  tree refusal, and 404-versus-500 on the optional file read
- `scripts/check-canon-drift.sh` — exit 0 against upstream `main`

Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
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