Skip to content

An adopt entry in [decide] may carry the repository's why, and uphold check prints it - #455

Merged
HackingGate merged 2 commits into
mainfrom
adopt-why
Oct 28, 2026
Merged

HackingGate merged 2 commits into
mainfrom
adopt-why

Conversation

@HackingGate

@HackingGate HackingGate commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Closes #454.

An entry in [decide] adopt may be a table { id, why } beside plain ids, and uphold check prints it as id: why.

adopted 2 record(s) for design rulings ([decide]):
  economy-of-mechanism
  unix-composability: every gate here is a separate command
  • Load, exit 2: an unknown key in the table (serde's own message names it), a blank why, and an entry that is neither an id nor a table.
  • uphold check, exit 1: a why equal to the record's summary or claim. The issue placed this at exit 2. It moved because it is a catalog question, and Decide's comment gives those to uphold check, as with an unknown id. A catalog rewording therefore cannot stop every command from loading the policy.
  • JSON: each entry keeps its written form, so a string entry is still a string under /decide/adopt.
  • Docs: the schema snapshot, REFERENCE.md's [decide] section, and the design-ruling skill's step 1, which reads the why as context and never as a citation.

The table form is deserialized by hand rather than with #[serde(untagged)], because untagged reports only "did not match any variant" and the issue asks that the bad entry be named.

Done-when checks: tests/check_cli.rs covers both forms printed and in JSON, one test covers the three load refusals, and one covers the summary and claim copies at exit 1. The schema snapshot test and the existing decide tests pass unchanged.

https://claude.ai/code/session_01BHpLckRt4BNGCdFG9vLyEP

… check prints it (#454)

An entry in `[decide] adopt` is a record id or a table `{ id, why }`, and
both forms mix in one list. `uphold check` prints the table entry as
`id: why`, as it already prints an order's reason. The why is the
consumer's fact: the catalog says what a record holds, the consumer says
why it rules by it. It stays optional, where an order's is required.

At load, exit 2 refuses a table with a key other than `id` and `why`, a
blank `why`, and a `why` equal to the record's `summary` or `claim`. The
JSON report keeps each entry in the form it was written, so a string
entry is still a string there. The schema, REFERENCE.md and the
design-ruling skill describe the form; the skill reads the why as
context, never as a citation.

Closes #454

Claude-Session: https://claude.ai/code/session_01BHpLckRt4BNGCdFG9vLyEP
@HackingGate HackingGate added the enhancement New feature or request label Oct 28, 2026
@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

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

Next included review available in 2 minutes.

Check out review usage here.

View limit details

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

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a54cf994-a141-41d5-bef9-23aa8a804fe5
Commits

Reviewing files that changed from the base of the PR and between 72b4d98 and 5958736.

Files selected for processing: 7
  • docs/REFERENCE.md
  • docs/schema/principles.schema.json
  • skills/design-ruling/SKILL.md
  • src/catalog.rs
  • src/check.rs
  • src/config.rs
  • tests/check_cli.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@uphold-review

uphold-review Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

uphold-review

Walkthrough

This push moves the check that refuses an adoption why copied from the catalog. It used to run in Decide::validate when the policy loads, which exited 2. It now runs in decided in uphold check, which exits 1. A new helper, copied_field in src/check.rs, compares the why against the record's summary and claim. The loader keeps only the shape checks: an unknown key and a blank why. REFERENCE.md, the Reasoned::why doc comment and the schema snapshot now say the copied-text refusal is reported by uphold check. In the tests, the summary and claim cases leave the exit-2 shape test for a new exit-1 test. The shape test gains a case for a non-string, non-table entry.

I re-judged the open separation-of-concerns thread on src/config.rs and it is resolved. All checks against the catalog in [decide] now live in decided, and Decide::validate no longer reads the catalog. That matches the Decide doc comment: the loader checks the table's shape, and uphold check answers questions about the catalog. The push adds no new violations.

Changes

File Change
docs/REFERENCE.md The exit-1 list in [decide] now includes a why that copies the record's summary or claim. The exit-2 list drops it and adds an adopt entry that is neither an id nor a table.
docs/schema/principles.schema.json The schema description of why now says a blank one is refused at load and a copied one is refused by uphold check.
src/check.rs Adds a copied_field helper. decided uses it to report an adoption why equal to the record's summary or claim as a refusal at exit 1.
src/config.rs Removes the catalog lookup and copied-text check from Decide::validate, and updates the Reasoned::why doc comment to match.
tests/check_cli.rs Moves the summary and claim cases into a new exit-1 test, an_adopt_why_that_copies_the_record_is_one, and adds a case for a non-string entry to the exit-2 shape test.

❌ uphold rules: uphold's rules refuse this · this repository's policy at 72b4d98

Rule output
$ uphold scan
policy check failed: no-running-os-identity-metadata (hostname)
Do not commit identity metadata from the running OS. The scan reads the current
username, home path, hostname and default route at runtime, then searches the
tracked files for what it read. Use neutral placeholders instead --
example-user, example-host, example.test, /srv/example.
.github/workflows/test.yml:127:          components: llvm-tools-preview
.github/workflows/test.yml:138:          tool: cargo-llvm-cov,nextest
docs/COVERAGE.md:73:| [Abstract syntax tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) | the program's structure without layout, so a rule matches a construct however it is spelled | [Clang AST](https://clang.llvm.org/docs/IntroductionToTheClangAST.html), [ast-grep](https://ast-grep.github.io/) |
docs/COVERAGE.md:79:| [High-level intermediate representation](https://en.wikipedia.org/wiki/Intermediate_representation) | names resolved and types assigned, so a check sees what an identifier refers to | [rustc HIR](https://rustc-dev-guide.rust-lang.org/hir.html), [MLIR](https://mlir.llvm.org/), [LLVM IR](https://llvm.org/docs/LangRef.html) |
docs/COVERAGE.md:80:| [Control-flow graph](https://en.wikipedia.org/wiki/Control-flow_graph) | every path a function can take, so a check can ask whether one statement is reached before another | [LLVM](https://llvm.org/), [Soot](https://soot-oss.github.io/soot/) |
docs/COVERAGE.md:81:| [Data-flow graph](https://en.wikipedia.org/wiki/Data_dependency) | which value flows into which operation | [Joern](https://joern.io/), [LLVM](https://llvm.org/) |
docs/COVERAGE.md:83:| [Static single-assignment form](https://en.wikipedia.org/wiki/Static_single-assignment_form) | each variable assigned once, so a value's definition is its name | [LLVM](https://llvm.org/), [GCC](https://gcc.gnu.org/) |
docs/COVERAGE.md:85:| [Def-use chains](https://en.wikipedia.org/wiki/Use-define_chain) | each definition linked to every use it reaches, and back | [LLVM](https://llvm.org/), [GCC](https://gcc.gnu.org/) |
docs/COVERAGE.md:86:| [Dominator tree](https://en.wikipedia.org/wiki/Dominator_(graph_theory)) | which statement runs before another on every path, so "checked before used" is decidable | [LLVM](https://llvm.org/), [GCC](https://gcc.gnu.org/) |
docs/COVERAGE.md:95:| [Dependency analysis](https://en.wikipedia.org/wiki/Dependence_analysis) | which statements must stay in order, the ground of loop transformation and slicing | [Polly](https://polly.llvm.org/), [isl](https://libisl.sourceforge.io/) |
docs/COVERAGE.md:96:| [Precision](https://en.wikipedia.org/wiki/Data-flow_analysis#Sensitivities): interprocedural, flow-, context- and path-sensitive | how much of the program an answer accounts for: across calls, in statement order, per call site, per branch; each costs time and removes false positives | [Infer](https://fbinfer.com/), [Clang Static Analyzer](https://clang-analyzer.llvm.org/), [SVF](https://github.com/SVF-tools/SVF) |
docs/COVERAGE.md:98:| [Interval](https://en.wikipedia.org/wiki/Interval_arithmetic), [octagon](https://doi.org/10.1007/s10990-006-8609-1) and [polyhedral](https://doi.org/10.1145/512760.512770) domains | bounds per variable; bounds on sums and differences of pairs; any linear relation: each more precise and more costly than the last | [APRON](https://github.com/antoinemine/apron), [Astree](https://www.absint.com/astree/), [Polly](https://polly.llvm.org/)/[isl](https://libisl.sourceforge.io/) |
docs/COVERAGE.md:149:| [Coverage-guided fuzzing](https://en.wikipedia.org/wiki/Fuzzing) | inputs mutated toward new code coverage until one fails | [AFL++](https://aflplus.plus/), [libFuzzer](https://llvm.org/docs/LibFuzzer.html), [honggfuzz](https://github.com/google/honggfuzz) |
docs/COVERAGE.md:158:| [Coverage analysis](https://en.wikipedia.org/wiki/Code_coverage) | which lines, branches or conditions a suite ran; a measure that stops measuring once it is a target, as [goodharts-law](../principles/goodharts-law.toml) says | [llvm-cov](https://llvm.org/docs/CommandGuide/llvm-cov.html), [gcov](https://gcc.gnu.org/onlinedocs/gcc/Gcov.html) |
docs/COVERAGE.md:166:| [Memory instrumentation](https://en.wikipedia.org/wiki/Instrumentation_(computer_programming)) | every access checked against allocation bounds and lifetimes | [AddressSanitizer](https://clang.llvm.org/docs/AddressSanitizer.html), [Valgrind](https://valgrind.org/) |
docs/COVERAGE.md:167:| [Race detection](https://en.wikipedia.org/wiki/Race_condition) | unsynchronized conflicting accesses found from the happens-before order of a run | [ThreadSanitizer](https://clang.llvm.org/docs/ThreadSanitizer.html), [Helgrind](https://valgrind.org/docs/manual/hg-manual.html) |
docs/COVERAGE.md:169:| [Deadlock analysis](https://en.wikipedia.org/wiki/Deadlock_(computer_science)) | lock-order cycles seen in one run, which can deadlock in another | [Helgrind](https://valgrind.org/docs/manual/hg-manual.html), [ThreadSanitizer](https://clang.llvm.org/docs/ThreadSanitizer.html) |
scripts/coverage.sh:49:for tool in llvm-cov nextest; do
scripts/coverage.sh:58:# this crate has no lib target -- and llvm-cov instruments that binary and
scripts/coverage.sh:73:echo "==> cargo llvm-cov ${args[*]}"
scripts/coverage.sh:74:cargo llvm-cov "${args[@]}"
toolchain.toml:87:# declared anyway because "cargo llvm-cov: no such subcommand" is not a message
toolchain.toml:90:name = "cargo-llvm-cov"
toolchain.toml:91:check = "cargo llvm-cov --version"
toolchain.toml:93:hint = "coverage-only; install: cargo install cargo-llvm-cov (or run: scripts/deps.sh install)"
toolchain.toml:186:check = "cargo llvm-cov --version >/dev/null 2>&1 && cargo nextest --version >/dev/null 2>&1"
toolchain.toml:188:cargo install --locked cargo-llvm-cov


$ uphold check
reconciled 19 enforcement claims:
  explicit-unknown <- catalog-tests  enforced by a hook installed here
  single-authoritative-source <- catalog-reference-current  enforced by a hook installed here
  make-illegal-states-unrepresentable <- catalog-validate  enforced by a hook installed here
  single-authoritative-source <- catalog-tests  enforced by a hook installed here
  explicit-unknown <- no-stale-hook-pins  enforced by uphold guard at manual
  single-authoritative-source <- no-stale-hook-pins  enforced by uphold guard at manual
  complete-mediation <- prevent-ai-author  enforced by uphold guard at commit-msg
  fail-safe-defaults <- prevent-public-push  enforced by uphold guard at pre-push
  observability <- removed-function-named  enforced by uphold guard at commit-msg
  least-privilege <- workflow-declares-permissions  enforced by uphold scan
  minimize-attack-surface <- workflow-declares-permissions  enforced by uphold scan
  compromise-recording <- no-local-merge  enforced by uphold guard at pre-merge-commit
  compromise-recording <- no-merge-commit  enforced by uphold guard at pre-commit
  data-minimization <- no-private-repo-names  enforced by uphold guard at commit-msg
  data-minimization <- no-running-os-identity-metadata  enforced by uphold scan
  chestertons-fence <- removed-function-named  enforced by uphold guard at commit-msg
  deprecation-discipline <- no-stale-hook-pins  enforced by uphold guard at manual
  reproducible-derived-state <- catalog-reference-current  enforced by a hook installed here
  reproducible-derived-state <- review-current  enforced by a hook installed here
  behavior  catalog-tests: unmeasured [not_requested] — no behavioral probe requested
  behavior  catalog-reference-current: unmeasured [not_requested] — no behavioral probe requested
  behavior  catalog-validate: unmeasured [not_requested] — no behavioral probe requested
  behavior  catalog-tests: unmeasured [not_requested] — no behavioral probe requested
  behavior  no-stale-hook-pins: unmeasured [not_requested] — no behavioral probe requested
  behavior  no-stale-hook-pins: unmeasured [not_requested] — no behavioral probe requested
  behavior  prevent-ai-author: unmeasured [not_requested] — no behavioral probe requested
  behavior  prevent-public-push: unmeasured [not_requested] — no behavioral probe requested
  behavior  removed-function-named: unmeasured [not_requested] — no behavioral probe requested
  behavior  workflow-declares-permissions: unmeasured [not_requested] — no behavioral probe requested
  behavior  workflow-declares-permissions: unmeasured [not_requested] — no behavioral probe requested
  behavior  no-local-merge: unmeasured [not_requested] — no behavioral probe requested
  behavior  no-merge-commit: unmeasured [not_requested] — no behavioral probe requested
  behavior  no-private-repo-names: unmeasured [not_requested] — no behavioral probe requested
  behavior  no-running-os-identity-metadata: unmeasured [not_requested] — no behavioral probe requested
  behavior  removed-function-named: unmeasured [not_requested] — no behavioral probe requested
  behavior  no-stale-hook-pins: unmeasured [not_requested] — no behavioral probe requested
  behavior  catalog-reference-current: unmeasured [not_requested] — no behavioral probe requested
  behavior  review-current: unmeasured [not_requested] — no behavioral probe requested
  note  lefthook.yml runs the binary
  note  lefthook.yml defines 20 command(s)
  note  .claude/settings.json runs `uphold hook`
adopted 10 record(s) for design rulings ([decide]):
  explicit-unknown
  parameterize-do-not-enumerate
  enforcement-needs-a-trigger
  psychological-acceptability
  single-authoritative-source
  mechanism-policy-separation
  least-power
  complete-mediation
  fail-safe-defaults
  economy-of-mechanism
  order  fail-safe-defaults over psychological-acceptability: A scope the shim cannot evaluate refuses by default, and `unresolved = "run"` is the named way out.
  order  explicit-unknown over psychological-acceptability: A seam that stands down says so on stderr; it never reports what it did not read as a pass.
  order  psychological-acceptability over complete-mediation: A hook that refuses routine work has its matcher deleted, and then it mediates nothing.
  order  least-power over parameterize-do-not-enumerate: `[stop] run` takes argv and no shell; a command that needs a pipeline names a script.

✅ uphold review

The review document asked about 33 of uphold's records; this push violates none of them.

This reviewed the push e4a2056..5958736: a new thread lies in a line the push added, and each open thread was judged again at this head. Use Full review on the uphold review check to review the whole change again.


Reviewed with uphold 1.38.0 by uphold-review 0.4.1 · 5958736 · claude-opus-5-5 · 33 s · $0.11 list

@uphold-review uphold-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New violations: 1 · each is a thread below.

Comment thread src/config.rs Outdated
Comment on lines +846 to +857
// An id the catalog lacks is `uphold check`'s to report, at exit 1.
if let Some(record) = crate::catalog::get(id).map_err(|error| error.to_string())? {
for (field, text) in [("summary", &record.summary), ("claim", &record.claim)] {
if why.trim() == text.trim() {
return Err(format!(
"`[decide] adopt` entry {id:?} has a `why` that is the record's \
`{field}`. The catalog already says what the record holds; `why` \
says why this repository adopts it"
));
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separation of Concerns · separation-of-concerns · level review

uphold's claim and questions

Organize a system so distinct concerns can be understood, changed, tested, and governed independently where their real dependencies permit it.

  • What distinct reasons would cause this component to change?
  • Which concern owns each invariant?
  • Does the proposed boundary reduce or merely relocate coupling?

Decide states how the work is divided (config.rs lines 728-730): the loader refuses the table's shape, and questions about the catalog this binary carries are answered by uphold check at exit 1 (decided in src/check.rs). The new check at lines 846-857 asks a catalog question in the loader: it fetches the record and compares the why against its summary and claim. The record's question is 'Which concern owns each invariant?' This change gives catalog-dependent [decide] checks two owners. Unknown and deprecated ids stay in decided; the copied-text check moves into Decide::validate. As a result, a change to a record's wording in the catalog now decides whether every command that reads the policy fails at load with exit 2, not just whether uphold check reports a refusal. The doc comment at 728-730 was not updated and now describes a split the code no longer follows.

🤖 Prompt for AI agents
In src/config.rs, `Decide::validate` (lines 846-857) looks each adopted record up with `crate::catalog::get` and refuses a `why` equal to the record's `summary` or `claim`. That contradicts the `Decide` doc comment at lines 728-730, which says catalog questions belong to `uphold check` at exit 1. Move the summary/claim comparison out of config.rs and into `decided` in src/check.rs, where the adopted record is already fetched. Push a failure such as `adopt: {id:?} has a `why` that is the record's `summary`...` onto `failures`, so it is reported with the other `[decide]` refusals at exit 1. Keep the blank-`why` and unknown-key refusals at load, because those are shape checks. Then update docs/REFERENCE.md (the exit-2 paragraph near the end of the `[decide]` section) and the `Reasoned::why` doc comment and schema description to match. Finally, change the summary and claim cases in `an_adopt_entry_of_the_wrong_shape_is_two_and_named` in tests/check_cli.rs to expect exit 1 and the refusal text from `uphold check`, or move them into a separate test.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer violated at 5958736. Resolve this thread when you agree.

@codecov-commenter

codecov-commenter commented Oct 28, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.98%. Comparing base (72b4d98) to head (5958736).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #455   +/-   ##
=======================================
  Coverage   94.97%   94.98%           
=======================================
  Files          56       56           
  Lines       28500    28550   +50     
=======================================
+ Hits        27068    27118   +50     
  Misses       1432     1432           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

… record

Whether a `why` repeats a record's `summary` or `claim` is a question about
the catalog this binary carries. `Decide`'s own comment gives those to
`uphold check` at exit 1, as it does an unknown or deprecated id, so a
catalog rewording no longer decides whether every command can load the
policy. The loader keeps the shape refusals: a blank `why`, an unknown
key, and an entry that is neither an id nor a table, now tested too.

Claude-Session: https://claude.ai/code/session_01BHpLckRt4BNGCdFG9vLyEP
@HackingGate
HackingGate merged commit bef7f83 into main Oct 28, 2026
12 of 14 checks passed
@HackingGate
HackingGate deleted the adopt-why branch October 28, 2026 13:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Design: an adopted record carries the consumer's reason, and uphold check prints it beside the id

2 participants