Skip to content

feat(rhodibot): classify what a file check found - #547

Merged
hyperpolymath merged 1 commit into
mainfrom
feat/rhodibot-verdict-classification
Sep 19, 2026
Merged

hyperpolymath merged 1 commit into
mainfrom
feat/rhodibot-verdict-classification

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

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.

…lot's mistake

A criterion that names files has four possible answers -- present where the
canon records it, present under a location the canon retired, present
somewhere else, absent -- and they are not interchangeable. This adds the
classifier that decides which.

The rule it holds: the description decides what is required; `template_ref`
only says where the canon's own template keeps it. The pilot read
`template_ref` as the requirement and failed every repository that satisfied
the criterion elsewhere -- both of its false positives were criterion 1.2.2,
which wants "a .pre-commit-config.yaml with real hooks" and is satisfied by one
at the repository root even though the template keeps its copy under `ci/`.

A bare filename -- "STATE.a2ml", "Justfile" -- is therefore 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.

Deprecated locations are read from the canon rather than hardcoded. The canon
states that `.machine_readable/6a2/` is retired in prose, inside criterion
3.1.1's description -- "NOT 6a2/, which is deprecated 2026-06-30" -- and there
is no machine-readable 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 instead of quietly reclassifying every retired
path as ordinary.

A repository carrying 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: the move is under way, and saying "deprecated location" would overstate
it. An absent file also names what is sitting there in its place by extension
-- gitbot-fleet has CODE_OF_CONDUCT.adoc where 2.1.4 asks for .md.

Against two real repositories, from their trees on 2026-09-19:

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

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 the
1.2.2 false positive staying fixed.

Advisory only: rhodibot reports against canon criterion ids.

Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 53d8d2f9-c53c-46a8-89a7-dd627c0719d1

📥 Commits

Reviewing files that changed from the base of the PR and between e3d9e9c and f1091a8.

📒 Files selected for processing (3)
  • bots/rhodibot/src/canon.rs
  • bots/rhodibot/src/canon/verdict.rs
  • bots/rhodibot/tests/canon_lockstep.rs
 __________________________________________________________
< Not just a pretty face, but a pretty good code reviewer! >
 ----------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR

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.

@@ -0,0 +1,1044 @@
// SPDX-License-Identifier: MPL-2.0
@hyperpolymath
hyperpolymath merged commit cd0a100 into main Sep 19, 2026
37 of 43 checks passed
@hyperpolymath
hyperpolymath deleted the feat/rhodibot-verdict-classification branch September 19, 2026 14:09
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.

2 participants