Skip to content

/nr declares the audit lens's scope as an argument, not a paragraph the skill overrides - #156

Merged
thedavidmeister merged 5 commits into
mainfrom
2026-07-30-issue-154-nr-typed-scope
Jul 30, 2026
Merged

/nr declares the audit lens's scope as an argument, not a paragraph the skill overrides#156
thedavidmeister merged 5 commits into
mainfrom
2026-07-30-issue-154-nr-typed-scope

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Closes #154

nr.md step 5 described the audit lens's scope correctly and at length — the
changed lines plus the callees, callers, siblings sharing the changed invariant
and every current-behaviour claim, with "would understanding it change the ruling
on THIS diff?"
as the inclusion test, and explicit prohibitions on both the diff
alone and a whole-repo audit. None of it survived the invocation. Skill audit
loads a document whose own first rule is "Whole-repo snapshot, never a diff. …Do
not scope by recent changes / PR diff."
, and once that is loaded nr.md is not in
the room. Measured on rainlanguage/rain.deploy#21 the lens
ran whole-repo: twelve findings, five bearing on the PR and seven in code the diff
never touches, with the scope hand-typed as free text in an args string that
nothing could validate.

What changed

  • The scope is a declared argument. /nr invokes the skill with the literal
    pr:<number> as its declared scope, beside the dir the checkout returned and
    the changed-file list — a value, not a paragraph a reader is asked to remember.
  • The spelling is the skill's, not this repo's. The vocabulary is exactly three
    literals — whole-repo, pr:<number>, paths:<comma-separated globs> — the
    same strings its run stamp records verbatim, and they live in one bullet of their
    own so "declare a literal" has the closed list behind it. There is no scope=
    key
    : an earlier revision of this PR invented one from the run stamp's field
    name, which is a fourth spelling in the middle of a change whose whole point is
    that the vocabulary is closed. Corrected in f5b5964 once
    audit: scope is a declared input with three literals; the bendless rule is never diff-only claude-audit-skills#67 settled the
    caller-side form.
  • Built from typed results only. The <number> is the one inside the row's own
    pr field (owner/repo#n, the same string step 2 was addressed with), the
    changed-file list is pr_context.files whole with its filesTruncated caveat
    stated, and the tree is pr_checkout.dir. A scope the reader assembled is the
    same defect class #132 removed one level up: it looks identical to a derived
    one and nothing downstream can tell them apart.
  • Step 5's inclusion test is kept as the REASON, not the carrier. The argument
    says pr:<number>; the prose explains which surrounding files that admits and
    why — callees, callers, siblings sharing the invariant, and every
    current-behaviour claim the PR or issue makes.
  • The presented ruling names the scope it was formed at. One line, verbatim, so
    a reader can tell a PR-scoped review from a whole-repo sweep without inferring it
    from the proportion of findings that miss the diff. A failed checkout means no
    lens and no scope
    — never a scope that would have been declared.
  • One vocabulary, in constants. AUDIT_SCOPE_WHOLE_REPO / AUDIT_SCOPE_PR /
    AUDIT_SCOPE_PATHS and the AUDIT_SCOPES set they compose, beside
    MCP_COMMAND_NATIVE_TOOLS. The value /nr declares, the value a lens ledger
    records and the value a gate refuses on are definitionally the same three strings
    The lens ledger records that the skill ran but not at what scope, so a whole-repo or diff-only review passes NoLens #155 gates a verdict on
    them, and a second copy is a second spelling waiting to drift.
  • Plugin 0.5.0 → 0.6.0 in both the manifest and the marketplace listing (a
    content change is a release), and the README's /nr section records why the
    scope is an argument.

No tool grant changes, so command_contract / command_check are untouched and
the tests pinning that Bash is refused beside a typed read pass unchanged.

The open question the issue raises: is a whole-repo audit worth keeping?

Yes, and not in /nr. It stays available as a separate, explicit invocation of
the same skill with whole-repo declared, on a repo somebody named — the case the
issue describes, a repo you are about to take a dependency on. /nr gets no mode,
flag or argument that reaches it. Three reasons:

  1. /nr rules on one PR. Its lens exists to decide THAT merge. A whole-repo
    sweep answers a question nobody asked at this gate, and it answers it by burying
    the findings that bear on the diff under the ones that do not — five among
    twelve, measured. The human gate is exactly where that dilution is most
    expensive, because the next action is a merge.
  2. A whole-repo audit is a different job with different outputs. The skill files
    findings as audit-labelled issues and writes a whole-repo run stamp that
    rain-org-health reads as audit recency; /nr writes neither and rules on a PR
    instead. Folding the two into one command would mean one invocation whose
    obligations depend on an argument, which is the shape this issue is about.
  3. Whichever way a flag defaulted, the bug would still read as working. A scope
    that defaults to the widest reading is indistinguishable from one that was
    chosen. So there is no default to get wrong: the value is passed on every
    invocation, and the only scope /nr can pass is the PR one — pinned by a test,
    not by this paragraph.

whole-repo is therefore reachable, never reachable by omission. It is stated in
nr.md under its own heading so the next reader does not have to re-derive it.

claude-audit-skills#67 reaches the same answer from the other side, which is
worth recording: a request whose subject is a PR never resolves to whole-repo,
while a caller may still explicitly declare it — "a repo someone is about to
depend on". The two changes agree without having been written together.

Blocked by: the skill must accept the declaration

rainlanguage/claude-audit-skills#66 is open, and its
PR rainlanguage/claude-audit-skills#67 is open and
unmerged
. Until it lands, the shipped skill still carries the bendless
"whole-repo snapshot, never a diff" rule and reads a declared scope as ordinary
args text, so /nr declares a scope nothing yet parses.

This PR is built against that contract rather than around it — the three literals,
declared as a value. Restating step 5 more firmly is the thing that already failed,
and it is not what is here. The blocked half is one repo away and nothing in this
diff needs to change when it lands.

Not in this PR

review-prompt.txt's AUDIT LENS step carries the identical prose-only scope for
the vetter, and #155 — the ledger row that records the declared scope and the
record_gate that refuses a wrong one — is the change that gives that side
something to record. Kept out of this diff deliberately: #155 is in flight against
this same repo and the vetter's lens paragraph is one line both changes would have
to edit. If #155 lands without touching it, that is a follow-up issue, not a silent
gap.

QA

  • Discriminating tests:
    marketplace_tests::nr_declares_the_lens_scope_as_a_value_from_the_skills_own_vocabulary
    — fails on base (the pre-change nr.md declares no scope literal in its
    invocation at all; verified by reverting that bullet to prose-only, mutation M1
    below). marketplace_tests::nr_grants_the_two_reads_the_source_it_audits_and_the_lens
    still pins the grant set unchanged — Skill and Read beside four typed reads —
    and a_shell_grant_is_still_refused_beside_the_audit_lens still refuses Bash,
    both passing untouched, because this change adds no grant.
  • Mutations applied (all on the artefact under test, nr.md; each restored and the
    restore verified with git diff --quiet before the next, baseline re-run green):
    • M1 the declared literal removed from the invocation bullet → killed, "the
      bullet that invokes the skill must DECLARE pr:<number> as the scope"
    • M2 the declared literal changed to whole-repo → killed by the same assertion
    • M3 the declared literal changed to diff-only → killed by the same assertion
    • M4 a SECOND scope (whole-repo) added beside the right one in the invocation
      bullet → killed, "the invocation bullet also names whole-repo" — the assertion
      that exists because a quoted whole-repo RULE is prose while a backticked literal
      is a declaration
    • M5 the invocation bullet's anchor renamed → killed, "step 5 has no INVOKE bullet
      — the scope is declared in the invocation, so there is nowhere for it to be
      declared"
    • M6 paths: dropped from the vocabulary bullet → killed, "the vocabulary bullet
      omits "paths:""
    • M7 whole-repo dropped from the vocabulary bullet → killed. This mutant
      SURVIVED an earlier revision
      whose assertion was over the whole file: the file
      names whole-repo elsewhere, in the section about the scope this command
      deliberately does not pass, and a mention is not a statement of the closed set.
      Fixed in 888724c.
    • M8 the vocabulary bullet's anchor renamed → killed, "nr.md states no scope
      VOCABULARY"
    • Four distinct assertion sites fire across the eight mutants, so no assertion in
      the test is decoration.
  • Oracle: the audit skill itself — SKILL.md at rainlanguage/claude-audit-skills
    HEAD for the run stamp's three scope values, and
    audit: scope is a declared input with three literals; the bendless rule is never diff-only claude-audit-skills#67 for the caller-side
    form ("Scope is an input the caller declares … It is exactly one of three
    literals, and there are no others"). Read out of those files, not from what this
    repo would find convenient to pass — which is how the invented scope= key was
    caught rather than shipped.
  • Category check: the issue asks for (a) the scope as a typed argument rather than
    prose, (b) step 5's inclusion test kept as the reason for the scope, (c) the scope
    built from pr_context / pr_checkout results and never hand-assembled, (d) the
    presented ruling stating the scope the lens ran at, (e) a decision on whether a
    whole-repo audit is worth keeping and, if so, only as an explicit invocation. All
    five covered — (a) pr:<number> declared in the step-5 invocation bullet, (b) the
    "what pr:<number> admits, and why" bullet, (c) the "every part of that argument
    comes from a typed result" bullet, (d) the new paragraph in "Present the result"
    plus the carry-through bullet in step 5, (e) the whole-repo section in nr.md
    and the decision section of this body. The skill-side parse is human-queue: emit per-state owner (single source of truth) + split the two dual-owner states (human:reject, ai:blocked-on) #66/prompts: require a screenshot (or a stated why-not) for GUI claims #67 and is
    named as a blocked-by rather than worked around.

`nr.md` step 5 described the lens's scope correctly and at length — the changed
lines plus the callees, callers, siblings sharing the changed invariant and every
current-behaviour claim, with "would understanding it change the ruling on THIS
diff?" as the inclusion test, and explicit prohibitions on both the diff alone
and a whole-repo audit. None of it survived the invocation. `Skill audit` loads a
document whose own first rule is "whole-repo snapshot, never a diff", and once it
is loaded `nr.md` is not in the room: on rain.deploy#21 the lens ran whole-repo,
twelve findings, five bearing on the PR and seven in code the diff never touches,
with the scope hand-typed as free text nothing could validate.

- `/nr` now DECLARES the scope: `scope=pr:<number>` beside the `dir` and the
  changed-file list, from the three values the audit skill accepts (`whole-repo`,
  `pr:<number>`, `paths:<globs>` — the vocabulary its own run stamp records).
- Every part of that argument comes from a typed result: the number out of the
  row's own `pr` field, the file list out of `pr_context.files` with its
  `filesTruncated` caveat, the tree out of `pr_checkout.dir`. A hand-assembled
  scope is the defect #132 removed one level up.
- Step 5's inclusion test is kept as the REASON the PR scope admits the
  surrounding files it does, rather than as the only thing carrying the scope.
- The presented ruling names the scope it was formed at, so a PR-scoped review is
  distinguishable from a whole-repo sweep without counting how many findings miss
  the diff. No lens means no scope, never a scope that would have been declared.
- `whole-repo` stays reachable as a separate deliberate invocation on a repo
  somebody named, and `/nr` has no mode, flag or argument that declares it: a
  scope that defaults to the widest reading is indistinguishable from a chosen
  one, which is how this read as working.

The scope vocabulary is one set of constants rather than a literal per caller,
and a test pins that nr.md still declares a value from it and declares only the
PR one. Blocked on rainlanguage/claude-audit-skills#66, which is what makes the
skill parse the declaration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeister thedavidmeister self-assigned this Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@thedavidmeister, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0198c5a6-a8a7-4750-83c1-bcfcdf3b1710

📥 Commits

Reviewing files that changed from the base of the PR and between d06bc93 and 888724c.

📒 Files selected for processing (5)
  • .claude-plugin/marketplace.json
  • README.md
  • plugins/human-fsm/.claude-plugin/plugin.json
  • plugins/human-fsm/commands/nr.md
  • pr-review-report-rs/src/main.rs
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-07-30-issue-154-nr-typed-scope

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.

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

QA run — checks and output

All run from the pinned rainix shell CI itself uses
(nix develop github:rainlanguage/rainix/53e96a7d0a97d7c7c75c3b2412521324776fdac6#rust-shell),
with pr-review-report-rs/src/main.rs touched first so a warm target/ could not
mask a clippy lint.

1. rainix-rs-static (rustfmt + clippy, CI's own invocation)

Checking pr-review-report v0.1.0 (…/clone-154-nr-scope/pr-review-report-rs)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.52s
STATIC_RC=0

2. pre-commit run --all-files (the second step of rainix-rs-static.yaml)

denofmt reflowed the markdown on the first pass; the reflow is in the commit, and
the re-run is clean:

deadnix..................................................................Passed
denofmt..................................................................Passed
nil......................................................................Passed
nixfmt...................................................................Passed
no-consumer-prettier.....................................................Passed
prettier-rainix..........................................................Passed
rustfmt..................................................................Passed
shellcheck...............................................................Passed
statix...................................................................Passed
taplo....................................................................Passed
yamlfmt..................................................................Passed
PRECOMMIT_RC=0

3. cargo test --release -p pr-review-report (whole suite, not just the new test)

test result: ok. 616 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
test result: ok.   7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
test result: ok.  47 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
test result: ok.   7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
test result: ok.   7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

4. plugin-version-lockstep (the version-hygiene gate, locally)

ok   human-fsm 0.6.0
LOCKSTEP_RC=0

5. Mutation runs — the new test, each mutant restored afterwards

The artefact under test is nr.md, so the mutations are applied there. Baseline
before and after each: test result: ok. 1 passed.

M1 — the declared argument removed, step 5 reverted to prose-only (the exact
pre-change state of this file):

thread 'marketplace_tests::nr_declares_the_lens_scope_as_a_value_from_the_skills_own_vocabulary'
panicked at pr-review-report-rs/src/main.rs:29614:9:
/nr must DECLARE the PR scope as an argument; a scope only described in prose is the
#154 defect, and the skill's own whole-repo rule wins over a description

M2 — the declared value changed to scope=whole-repo (the never-the-default
invariant):

panicked at pr-review-report-rs/src/main.rs:29640:13:
/nr passes "whole-repo", but the human PR gate declares only pr:<number>;
whole-repo and paths: belong to an invocation somebody asked for

M3 — the declared value changed to scope=diff-only (a spelling outside the
skill's vocabulary):

panicked at pr-review-report-rs/src/main.rs:29632:13:
scope="diff-only" is not one of ["whole-repo", "pr:", "paths:"] — a spelling the
skill does not accept is free text again, which is what #154 removed

Three mutants, three distinct assertion sites (29614, 29640, 29632), so no
assertion in the test is decoration. git status clean after every restore.

6. The test is not passing by never running

repo_root_text returns None in the flake build sandbox, and a test that bails
there passes silently — the shape this repo has been bitten by. Verified it actually
executes:

cargo test -- --exact marketplace_tests::nr_declares_the_lens_scope_as_a_value_from_the_skills_own_vocabulary
running 1 test
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 615 filtered out

7. The grant set is unchanged, and Bash is still refused

This change adds no tool grant, so command_contract / command_check are
untouched. The tests that pin it all pass unchanged in the run above:
nr_grants_the_two_reads_the_source_it_audits_and_the_lens,
a_shell_grant_is_still_refused_beside_the_audit_lens (which asserts Bash last,
Bash first, bare Bash, Task, Write, Edit, Grep, Glob are each refused
beside a typed read), the_native_admission_is_by_exact_name_not_by_prefix, and
the_audit_lens_alone_is_refused_because_the_subject_arrives_typed.

Not verified here

The audit skill does not yet parse scope=
(rainlanguage/claude-audit-skills#66 is open with no PR),
so there is no end-to-end run showing the skill honouring the declaration. What is
verified is this side of the contract: the value is declared, it comes from typed
results, it is one of the three literals, and it is the PR one.

The first version of the test asserted `scope=pr:<number>` appeared somewhere in
`nr.md`. Moving the literal out of the invocation bullet into any other paragraph
survived it — and a document that mentions the scope while the CALL does not pass
it is precisely the #154 state. The assertion is now scoped to the bullet that
invokes the skill, and a missing bullet panics with what it was looking for.

Mutants killed, each at a distinct site: the declaration removed; the declaration
reworded out of the bullet but left elsewhere; the invoke bullet renamed; the
declared value changed to whole-repo or to a spelling outside the vocabulary; and
a SECOND spelling introduced elsewhere while the bullet stays correct (which is
what the per-value loop exists for).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:producer
QA-block repair: replaced QA-GUIDE section 8's evidence block in the PR body via pr-review-report repair-qa-block. Every byte outside the ## QA section is unchanged.

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

QA run — updated for 545648e

The earlier QA comment was against c6a6c5b, whose test asserted the
scope=pr:<number> literal appeared anywhere in nr.md. Adversarially
re-mutating it found a survivor, so both the test and the evidence below are the
current ones. The body's ## QA block has been re-transcribed with
repair-qa-block --replace to match.

The surviving mutant, and what it means

M4 — reword the literal out of the invocation bullet, leave it in the three other
paragraphs that discuss it.
The old assertion passed: the file still contained
scope=pr:<number>. But a document that mentions the scope while the CALL does not
pass it is exactly the #154 state one layer in, so the assertion is now taken over
the bullet that invokes the skill (**Invoke the skill … up to the next bullet),
and a bullet that has gone missing panics saying so rather than passing vacuously.

Mutation runs, nr.md (the artefact under test), each restored

mutant change killed at message
M1 scope=pr:<number> removed from the invocation bullet main.rs:29617 "the bullet that invokes the skill must DECLARE the PR scope as an argument…"
M2 declared value → scope=whole-repo main.rs:29617 same assertion — the bullet stops declaring the PR scope
M3 declared value → scope=diff-only main.rs:29617 same assertion
M4 literal reworded out of the bullet, left in 3 other paragraphs main.rs:29617 survived the previous test; killed now
M5 the invocation bullet's anchor renamed panic! "step 5 has no INVOKE bullet — the scope is declared in the invocation, so there is nowhere for it to be declared"
M6 last occurrence → scope=whole-repo, bullet left correct main.rs:29651 "/nr passes "whole-repo", but the human PR gate declares only pr:<number>…"
M7 last occurrence → scope=diff-only, bullet left correct main.rs:29643 "scope="diff-only" is not one of ["whole-repo", "pr:", "paths:"]…"

M6 and M7 are the ones that keep the invocation correct and introduce a second
spelling elsewhere — the case the per-value loop exists for, and the reason it is
not folded into the bullet assertion. Four distinct assertion sites fire across the
seven, so nothing in the test is decoration. Verbatim panic output:

--- M1 declaration removed from the invocation bullet (prose-only) ---
the bullet that invokes the skill must DECLARE the PR scope as an argument; a scope
only described in prose is the #154 defect, and the skill's own whole-repo rule wins
over a description. The bullet reads: " with the `Skill` tool** — `audit` — and
DECLARE THE SCOPE\n  AS AN ARGUMENT, not as something the reader is asked to
remember:\n  the `dir` the checkout returned, and the changed-file\n  list. …"
test result: FAILED. 0 passed; 1 failed

--- M5 the INVOKE bullet itself renamed ---
step 5 has no INVOKE bullet — the scope is declared in the invocation, so there is
nowhere for it to be declared
test result: FAILED. 0 passed; 1 failed

--- M6 second spelling elsewhere: last occurrence -> scope=whole-repo ---
panicked at pr-review-report-rs/src/main.rs:29651:13:
/nr passes "whole-repo", but the human PR gate declares only pr:<number>; whole-repo
and paths: belong to an invocation somebody asked for
test result: FAILED. 0 passed; 1 failed

--- M7 second spelling elsewhere: last occurrence -> scope=diff-only ---
panicked at pr-review-report-rs/src/main.rs:29643:13:
scope="diff-only" is not one of ["whole-repo", "pr:", "paths:"] — a spelling the
skill does not accept is free text again, which is what #154 removed
test result: FAILED. 0 passed; 1 failed

--- restored baseline ---
test result: ok. 1 passed; 0 failed

CI-equivalent checks, at 545648e

Everything from the pinned rainix shell CI uses
(nix develop github:rainlanguage/rainix/53e96a7d0a97d7c7c75c3b2412521324776fdac6#rust-shell),
with main.rs touched first so a warm target/ could not mask a clippy lint.

rainix-rs-static (rustfmt + clippy)        STATIC_RC=0

pre-commit run --all-files
  deadnix ....... Passed      no-consumer-prettier ... Passed    statix ... Passed
  denofmt ....... Passed      prettier-rainix ........ Passed    taplo .... Passed
  nil ........... Passed      rustfmt ................ Passed    yamlfmt .. Passed
  nixfmt ........ Passed      shellcheck ............. Passed

cargo test --release -p pr-review-report
  test result: ok. 616 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
  test result: ok.   7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
  test result: ok.  47 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
  test result: ok.   7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
  test result: ok.   7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

plugin-version-lockstep                    ok   human-fsm 0.6.0

The test is not passing by never running

repo_root_text returns None in the flake build sandbox, and a test that bails
there passes silently. Verified it executes:

cargo test -- --exact marketplace_tests::nr_declares_the_lens_scope_as_a_value_from_the_skills_own_vocabulary
running 1 test
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 615 filtered out

The grant set is unchanged, and Bash is still refused

No tool grant is added, so command_contract / command_check are untouched. The
tests pinning that all pass unchanged in the run above:
nr_grants_the_two_reads_the_source_it_audits_and_the_lens,
a_shell_grant_is_still_refused_beside_the_audit_lens (Bash last, Bash first,
bare Bash, Task, Write, Edit, Grep, Glob each refused beside a typed
read), the_native_admission_is_by_exact_name_not_by_prefix, and
the_audit_lens_alone_is_refused_because_the_subject_arrives_typed.

Not verified here

The audit skill does not yet parse scope=
(rainlanguage/claude-audit-skills#66 is open with no PR),
so there is no end-to-end run showing the skill honouring the declaration. What is
verified is this side of the contract: the value is declared in the invocation,
built from typed results, one of the three literals, and the PR one.

claude added 3 commits July 30, 2026 11:18
"The skill accepts exactly three scopes" is a claim about a parse that
claude-audit-skills#66 has not landed yet — a stated current behaviour the source
contradicts, which is the false-premise finding this very command teaches its
reader to look for. What IS true today is the vocabulary: the three values the
skill's run stamp records, under the field name `scope`. Both `nr.md` and the
README now say that, and both stay true after #66 lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-audit-skills#67 (the PR for #66, opened while this was in flight) settles
the caller-side spelling: "Scope is an input the caller declares … It is exactly
one of three literals, and there are no others" — `whole-repo`, `pr:<number>`,
`paths:<comma-separated globs>`. The string `scope=` appears nowhere in it. The
key was derived from the run stamp's FIELD name and passed off as the argument
form, which is inventing a fourth spelling in the middle of a change whose whole
point is that the vocabulary is closed — and #155's gate compares a recorded scope
against those literals, so a keyed value is one the gate refuses.

So `/nr` declares the literal `pr:<number>`. The closed vocabulary moves into its
own bullet, which lets the test assert the invocation bullet declares the PR scope
and NAMES NO OTHER — the quoted whole-repo RULE is prose, a backticked literal is
a declaration, and the two are now distinguishable. A third assertion requires the
file to state all three literals, so "declare a literal" has the list behind it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-mutating found a survivor: deleting `whole-repo` from the vocabulary bullet
still passed, because the file names `whole-repo` elsewhere — in the section about
the scope this command deliberately does NOT pass. A mention is not a statement of
the closed set, so the assertion moves onto the bullet that states it, and a file
with no vocabulary bullet at all panics saying what is missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

QA run — final, at 888724c

Supersedes the two earlier QA comments. Two corrections happened between them, both
found by re-mutating rather than by re-reading, and both are worth having on the
record because each was a surviving mutant:

  1. scope= was a key nobody defined. The first revision passed
    scope=pr:<number>, a key derived from the run stamp's field name.
    audit: scope is a declared input with three literals; the bendless rule is never diff-only claude-audit-skills#67 — opened while this
    was in flight — settles the caller-side form: "Scope is an input the caller
    declares … It is exactly one of three literals, and there are no others."
    The
    string scope= appears zero times in it. Fixed in f5b5964: the bare literal
    pr:<number> is declared. That also matters downstream —
    The lens ledger records that the skill ran but not at what scope, so a whole-repo or diff-only review passes NoLens #155 compares a recorded
    scope against those literals, so a keyed value is one its gate refuses.
  2. The vocabulary assertion was satisfied by the wrong occurrence. Deleting
    whole-repo from the vocabulary bullet still passed, because the file names
    whole-repo elsewhere — in the section about the scope this command deliberately
    does not pass. A mention is not a statement of the closed set. Fixed in 888724c.

Mutation runs — nr.md is the artefact, each mutant restored and the restore verified

Every restore is checked with git diff --quiet before the next mutant, because an
earlier round's git checkout silently reverted uncommitted edits and every mutant
after it was applied to the wrong file — the run looked like six kills and was one.
The baseline is committed before mutating now, and re-run green at the end.

mutant change killed by
M1 declared literal removed from the invocation bullet "the bullet that invokes the skill must DECLARE pr:<number> as the scope…"
M2 declared literal → whole-repo same assertion
M3 declared literal → diff-only same assertion
M4 a SECOND scope (whole-repo) added beside the right one "the invocation bullet also names whole-repo. /nr rules on ONE PR…"
M5 the invocation bullet's anchor renamed "step 5 has no INVOKE bullet — the scope is declared in the invocation, so there is nowhere for it to be declared"
M6 paths: dropped from the vocabulary bullet "the vocabulary bullet omits "paths:"…"
M7 whole-repo dropped from the vocabulary bullet "the vocabulary bullet omits "whole-repo"…" (survived the previous revision)
M8 the vocabulary bullet's anchor renamed "nr.md states no scope VOCABULARY — a command told to declare a literal, and not told which literals exist, is one invented spelling away from free text again"

Four distinct assertion sites fire across the eight, so no assertion is decoration.
M4 is the one that needs the prose/literal distinction to work at all: the
invocation bullet quotes the skill's whole-repo rule in italics, and the
assertion matches the backticked literal, so a quoted rule is not read as a declared
value.

Verbatim run:

--- M0 baseline ---            test result: ok. 1 passed; 0 failed
--- M1 ---                     test result: FAILED. 0 passed; 1 failed
--- M2 ---                     test result: FAILED. 0 passed; 1 failed
--- M3 ---                     test result: FAILED. 0 passed; 1 failed
--- M4 ---  the invocation bullet also names `whole-repo`. /nr rules on ONE PR, so it
            declares `pr:<number>` and nothing else — a whole-repo sweep is an
            invocation somebody asked for, never one this gate can reach
                               test result: FAILED. 0 passed; 1 failed
--- M5 ---  step 5 has no INVOKE bullet — the scope is declared in the invocation, so
            there is nowhere for it to be declared
                               test result: FAILED. 0 passed; 1 failed
--- M6 ---                     test result: FAILED. 0 passed; 1 failed
--- M7 ---                     test result: FAILED. 0 passed; 1 failed
--- M8 ---                     test result: FAILED. 0 passed; 1 failed
--- restored baseline ---      test result: ok. 1 passed; 0 failed
git status --short             (clean)

CI-equivalent checks

From the pinned rainix shell CI uses
(nix develop github:rainlanguage/rainix/53e96a7d0a97d7c7c75c3b2412521324776fdac6#rust-shell),
with main.rs touched first so a warm target/ could not mask a clippy lint.

rainix-rs-static (rustfmt + clippy)        STATIC_RC=0

pre-commit run --all-files
  deadnix ....... Passed      no-consumer-prettier ... Passed    statix ... Passed
  denofmt ....... Passed      prettier-rainix ........ Passed    taplo .... Passed
  nil ........... Passed      rustfmt ................ Passed    yamlfmt .. Passed
  nixfmt ........ Passed      shellcheck ............. Passed

cargo test --release -p pr-review-report
  test result: ok. 616 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
  test result: ok.   7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
  test result: ok.  47 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
  test result: ok.   7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
  test result: ok.   7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

plugin-version-lockstep                    ok   human-fsm 0.6.0

denofmt reflowed both markdown files on each pass and the reflow is in the
commits, so the hook is clean on a re-run rather than clean-by-not-looking.

The QA block was validated by the gate's own predicate

pr-review-report repair-qa-block rainlanguage/issue-pr-cron 156 \
  --block-file <the body's ## QA section> --replace --dry-run
rainlanguage/issue-pr-cron#156: body already carries this exact QA block — nothing to write

The test is not passing by never running

repo_root_text returns None in the flake build sandbox, and a test that bails
there passes silently. Verified it executes:

cargo test -- --exact marketplace_tests::nr_declares_the_lens_scope_as_a_value_from_the_skills_own_vocabulary
running 1 test
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 615 filtered out

The grant set is unchanged, and Bash is still refused

No tool grant is added, so command_contract / command_check are untouched. The
tests pinning that all pass unchanged in the run above:
nr_grants_the_two_reads_the_source_it_audits_and_the_lens,
a_shell_grant_is_still_refused_beside_the_audit_lens (Bash last, Bash first,
bare Bash, Task, Write, Edit, Grep, Glob each refused beside a typed
read), the_native_admission_is_by_exact_name_not_by_prefix, and
the_audit_lens_alone_is_refused_because_the_subject_arrives_typed.

Two things a reviewer should not read as verified

  • CodeRabbit's green is vacuous on this PR. Its check reports pass, and its
    comment says "Review limit reached … you've reached your PR review limit, so we
    couldn't start this review."
    That is the rate-limited coverage value, not a
    review, so "0 unresolved threads" here means nothing was looked at. Worth
    re-triggering before merge.
  • No end-to-end run shows the skill honouring the declaration, because
    audit: scope is a declared input with three literals; the bendless rule is never diff-only claude-audit-skills#67 is unmerged. What is
    verified is this side of the contract: the literal is declared in the invocation,
    built from typed results, one of the three, and the PR one.

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Reviewed 888724c: ready — /nr declares the bare pr: literal built from typed results, form verified compatible with #157's LensScope parser, whole-repo kept as a separate explicit invocation, mutation re-run from a committed baseline killed both real survivors. Blocked-by claude-audit-skills#67 is resolving on the human's own merge word for both.

@thedavidmeister
thedavidmeister merged commit 8953387 into main Jul 30, 2026
21 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.

/nr describes the lens scope in prose the skill's own rule overrides — pass pr:<n> as a typed argument

2 participants