Skip to content

Let the loader answer which rules run, and close the second reader - #5

Merged
HackingGate merged 10 commits into
mainfrom
fix/seam-attribution
Aug 12, 2026
Merged

Let the loader answer which rules run, and close the second reader#5
HackingGate merged 10 commits into
mainfrom
fix/seam-attribution

Conversation

@HackingGate

@HackingGate HackingGate commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Supersedes the earlier version of this branch, which patched the seam derivation
inside uphold_check.py. Those lines are gone instead.

The cause

uphold_check.py re-implemented config::load to reconcile a declaration:
bundled sets, inherit.paths, inherit.disabled_rules, and a repository's own
rule shadowing an inherited id. effective_rules_command has said since it was
written what that costs:

every second reader of them is a reader free to disagree with the engine about
which rules run. The reconciler in uphold_check.py is that second reader
today, and this is what ends it.

They disagreed about the seam a hookless rule runs at. files.* is the scan's,
command.before is a checker standing in front of a command, and both come back
with no git hooks. The reconciler read the second as the first, so a claim on
a shim-only rule reconciled green, exit 0, in a repository that pins
uphold-scan and nothing else
— over a rule the scan never touches. This
repository declares two such rules.

test_the_two_readers_of_the_policy_agree could not catch it: it compared
git_hooks, and both readers said []. Two implementations agreeing is not the
same as either being right.

What changed

Rule::seams returns scan / guard / shim from the same conditions the
three seams use to select rules, and it is on rules --effective --json for
anything else that has to ask.

The reconcile and the coverage report are uphold check and
uphold check --coverage. uphold-check becomes language: rust like every
other id in the manifest — it was the only language: script one, and
pre-commit and prek key an environment on (repo, language, version), so it now
shares the environment the other seven ids already build and costs no second
compile.

What stayed in Python is what never reads the policy: --explain, --list,
--review, --oscal, --init. A mode that cannot read the policy cannot
disagree with the loader. --oscal gates on the reconcile so it asks the binary
and treats an unreachable one as could-not-look; --review asks too but
survives a refusal, because it runs over a declaration somebody is still
writing.

uphold_check.py: 1,589 → 680 lines.

Three things the port fixes rather than carries

  • The runner configs are parsed. The script line-scanned them because it
    could take no dependency, which is how configs: — the key README.md tells
    every lefthook consumer to write under remotes: — was read as a command name.
  • package.repository is read through CARGO_PKG_REPOSITORY at compile
    time, so the slug cannot drift from the crate and works outside a checkout of
    this repository.
  • The coverage denominator counts what a seam supplies. records: N of M was
    computed from the claims, so a declaration whose only claim named a rule
    nothing runs reported one record as claimed two lines under the line saying
    that rule is supplied by nothing.

Verification

Both reconcilers were run against every repository in the fleet: 68 of 71
return the same exit code and the same claim count.

The three that differ have a policy config::load refuses outright —
command.before on a builtin — which the Python never validated. Worth
knowing separately: uphold scan already fails in those three today, and
they are unaffected only because the fleet pins v1.0.0, which predates that
refusal. Cutting the next tag exits 2 on every hook there.

  • cargo test — 294 tests green, including a new tests/check_cli.rs.
  • 35 behaviour tests moved from tests/test_uphold_check.py rather than
    going away, and two caught real regressions in the port: a lefthook remote
    given as a filesystem path, which is what scripts/consumer_check.sh writes,
    and the rule that a remote is only ours when ONE entry both names this
    repository and takes its config.
  • python3 -m pytest tests/ — 73 passed, 224 subtests.
  • cargo clippy --all-targets clean under the crate's own profile (which is what
    turned two panic!s into exit-2 paths); cargo fmt --check clean; the
    repository's own hooks pass on every commit.

Summary by CodeRabbit

  • New Features

    • Added the uphold check command to validate policy claims, detect unsupported or unfulfilled rules, and report execution coverage.
    • Added uphold check --coverage for coverage summaries and seam diagnostics.
    • Effective-rule output now identifies whether rules run through scanning, hooks, or command wrappers.
    • Added JSON seam details and the --upstream repository URL option.
  • Documentation

    • Updated README and reference documentation with the new commands and output formats.
  • Chores

    • Updated pre-commit and Lefthook integrations to use the Rust-based checker.

`rules --effective --json` emitted `git_hooks` and nothing else, so a rule that
fires at no git hook was indistinguishable from any other rule that fires at no
git hook -- and there are two unrelated kinds. `files.*` is the scan's;
`command.before` is a checker standing in front of a command, which runs when
the shim is on PATH ahead of the real one. A reader with only the hooks has to
guess between them.

`uphold_check.py` guessed the scan. Its `elif scan` branch credited every
hookless rule to `uphold scan`, so a claim on a shim-only rule reconciled green,
exit 0, in a repository that pins `uphold-scan` and nothing else -- over a rule
the scan never touches. This repository declares two such rules of its own,
`no-published-host-identity` and `no-published-markers`.

The drift test between the two readers did not catch it and could not have: it
compared `git_hooks`, and both readers agreed on the empty list. They were wrong
in the same direction, which is the failure mode a comparison of two
implementations has and a comparison against the answer does not.

So the loader says it. `Rule::seams` returns `scan` / `guard` / `shim` from the
same conditions the three seams use to select rules, the JSON carries it, and
the human form prints it where it used to print "no git hook" of both kinds.
`_rule_stages` reads `command.before` to match, and returns a `Where` carrying
both fields, so the drift test compares the seam as well.

A shim-only rule is reported as a seam this script cannot establish -- no runner
configuration here says whether the shim is on PATH -- rather than as one the
scan supplies. That is could-not-look, and `inventory_local` remains where a
repository asserts a seam this script cannot observe.
`--review` builds `claimed` from every entry in the declaration and `active`
from the entries whose rule a seam actually supplies. Two lines apart, one
filtered and one not, and the unfiltered one decided what a human is asked
about.

`review.route` drops an `automatable = "yes"` record when a rule claims it --
"a rule enforces it; a reviewer repeating it is noise" -- which is right when a
rule does enforce it. A claim naming a rule no seam here supplies enforces
nothing, so it is not that case: the record left the review document, and the
rule was absent from the "already active here" list the same document prints,
because that list IS filtered. Enforced by nothing, reviewed by nobody, and the
page showed no trace of either.

The reconcile refuses such a claim outright. This mode has to survive one,
because it runs over a declaration somebody is still writing -- so it filters
rather than refuses, and the record goes back to the reviewer it was taken from.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 49 minutes

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

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 532b0e5c-6212-4f5c-bd0d-6e48ca3c3c7d

📥 Commits

Reviewing files that changed from the base of the PR and between 7c2244b and 5745367.

📒 Files selected for processing (2)
  • src/check.rs
  • tests/check_cli.rs
📝 Walkthrough

Walkthrough

The Rust binary now performs policy reconciliation and coverage reporting. The Python script delegates engine-backed results for review and OSCAL generation. Hooks, workflows, documentation, and tests use the new uphold check entrypoint. Effective-rule output reports execution seams.

Changes

Binary-backed policy checking

Layer / File(s) Summary
Compiled principle catalog
src/catalog.rs
The binary embeds principle records, validates identifiers, caches metadata, and exposes lookup and claimable-ID helpers.
Seam-aware reconciliation and coverage
src/check.rs, src/config.rs
The checker discovers scan, guard, and shim seams, resolves rule suppliers, validates claims, and reports enforcement and coverage results.
CLI commands and seam output
src/main.rs, tests/scan_cli.rs
The CLI adds check, check --coverage, and --upstream. Effective-rule JSON and text output report configured seams.
Engine delegation and integration migration
uphold_check.py, .pre-commit-hooks.yaml, hooks/lefthook.yml, lefthook.yml, .github/workflows/test.yml, tests/check_cli.rs, tests/test_review.py, tests/test_uphold_check.py, README.md, docs/REFERENCE.md, .pre-commit-config.yaml
The Python script delegates reconciliation and supplier discovery to the binary. Hooks, workflows, documentation, and integration tests use the Rust checker.
Removed wrapper
.lefthook/pre-commit/uphold-check
The obsolete script-based Lefthook wrapper was removed.

Estimated code review effort: 5 (Critical) | ~100 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Checker
  participant Config
  participant Catalog
  participant Python
  CLI->>Checker: run check or check --coverage
  Checker->>Config: discover installed seams
  Checker->>Catalog: validate principle claims
  Checker-->>CLI: return reconciliation or coverage results
  Python->>Checker: request supplier evidence
  Checker-->>Python: return binary-backed suppliers
  Python-->>Python: generate review or OSCAL output
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes moving rule-seam determination to the loader and removing the duplicate policy reader, which matches the main changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 fix/seam-attribution

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.

@codecov-commenter

codecov-commenter commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.04082% with 39 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.63%. Comparing base (7549745) to head (5745367).

Files with missing lines Patch % Lines
src/check.rs 92.90% 29 Missing ⚠️
src/main.rs 75.00% 6 Missing ⚠️
src/catalog.rs 90.90% 4 Missing ⚠️

❌ Your patch status has failed because the patch coverage (92.04%) is below the target coverage (100.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main       #5      +/-   ##
==========================================
+ Coverage   87.19%   87.63%   +0.44%     
==========================================
  Files          22       24       +2     
  Lines        6605     7094     +489     
==========================================
+ Hits         5759     6217     +458     
- Misses        846      877      +31     

☔ 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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/REFERENCE.md`:
- Around line 146-151: Update the `seams` documentation to state that every
valid effective rule contains exactly one seam: `scan`, `guard`, or `shim`.
Clarify that multiple seams are not currently supported, `git.hooks` is valid
only for `builtin` rules, `command.before` only for `exec` rules, and
file-reading non-builtins support neither.

In `@uphold_check.py`:
- Around line 909-934: Update the early-return path before the declared-rule
loop so it parses an existing policy and runs coverage analysis even when
neither scan nor guard is configured. Ensure shim-only rules reach the “shim”
branch in the declared-rules loop and report the “stands in front of a command”
diagnostic instead of the generic runner note, while preserving behavior when no
policy exists.
- Around line 615-620: Update the hook validation in the rule-processing logic
around git.hooks to raise CouldNotLook as soon as any entry is not a string,
instead of filtering invalid values out. Preserve the existing set construction
for valid string hooks, and add a regression test covering an integer hook
entry.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ca45b7b5-fc95-4505-a7be-0b2505ff2608

📥 Commits

Reviewing files that changed from the base of the PR and between 7549745 and b93623e.

📒 Files selected for processing (7)
  • docs/REFERENCE.md
  • src/config.rs
  • src/main.rs
  • tests/scan_cli.rs
  • tests/test_review.py
  • tests/test_uphold_check.py
  • uphold_check.py

Comment thread docs/REFERENCE.md
Comment on lines +146 to +151
`seams` is `scan`, `guard`, `shim`, or more than one, and it is the half
`git_hooks` cannot express. An empty hook list is true of a content rule and of
a checker standing in front of a command alike, so a reader with only the hooks
has to guess between two unrelated places — and the reconciler guessed `scan`,
which credited a shim-only rule to a seam that never touches it. An empty
`seams` means nothing runs the rule at all, which the loader refuses.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Describe the valid seams cardinality correctly.

A loaded rule cannot currently have more than one seam. Validation permits git.hooks only on builtin rules and command.before only on exec rules. File-reading non-builtins cannot use either. State that each valid effective rule contains exactly one of scan, guard, or shim.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/REFERENCE.md` around lines 146 - 151, Update the `seams` documentation
to state that every valid effective rule contains exactly one seam: `scan`,
`guard`, or `shim`. Clarify that multiple seams are not currently supported,
`git.hooks` is valid only for `builtin` rules, `command.before` only for `exec`
rules, and file-reading non-builtins support neither.

Comment thread uphold_check.py Outdated
Comment thread uphold_check.py Outdated
@HackingGate
HackingGate marked this pull request as draft August 12, 2026 12:36
@HackingGate

Copy link
Copy Markdown
Owner Author

Holding this rather than merging. The seam derivation it adds to uphold_check.py sits in the ~500 lines that re-implement config::load, and those lines are being removed: the reconcile moves into the binary as uphold check, and uphold-check flips from language: script to language: rust — the only hook in the manifest that is not already Rust, sharing an environment the other seven ids already build.

The engine half here survives that port and is needed by it: Rule::seams and the seams field on rules --effective --json are how the reconcile asks the loader which seam runs a rule, instead of guessing from an empty hook list. Both defects and their tests will land in the port PR, which supersedes this one.

`uphold_check.py` re-implemented `config::load` to reconcile a declaration:
bundled sets, `inherit.paths`, `inherit.disabled_rules`, and a repository's own
rule shadowing an inherited id. Five interacting fields, read twice, by two
programs free to disagree -- and `effective_rules_command` has said since it was
written that this is what would end it: "every second reader of them is a reader
free to disagree with the engine about which rules run. The reconciler in
`uphold_check.py` is that second reader today."

They disagreed about the seam a hookless rule runs at. `files.*` is the scan's
and `command.before` is a checker standing in front of a command; both come back
with no git hooks, and the reconciler read the second as the first. A claim on a
shim-only rule reconciled green, exit 0, in a repository that pins `uphold-scan`
and nothing else -- over a rule the scan never touches. `Rule::seams` answers it
now, from the same conditions the three seams use to select rules, and it is on
`rules --effective --json` for anything else that has to ask.

The reconcile and the coverage report are `uphold check` and
`uphold check --coverage`. What stayed in Python is what never reads the policy
-- `--explain`, `--list`, `--review`, `--oscal`, `--init` -- because a mode that
cannot read the policy cannot disagree with the loader about which rules run.
`--oscal` gates on the reconcile, so it asks the binary and treats an
unreachable one as could-not-look; `--review` asks too but survives a refusal,
because it runs over a declaration somebody is still writing.

`uphold-check` becomes `language: rust` like every other id in the manifest. It
was the only `language: script` one, and pre-commit and prek key an environment
on (repo, language, version) -- so it now shares the environment the other seven
ids already build, and costs no second compile.

Three things the port fixes rather than carries:

The runner configs are PARSED. The script line-scanned them because it could
take no dependency, which is how `configs:` -- the key README.md tells every
lefthook consumer to write under `remotes:` -- was read as a command name.

`package.repository` is read at compile time through `CARGO_PKG_REPOSITORY`
rather than off disk, so the slug cannot drift from the crate and works outside
a checkout of this repository.

The coverage denominator counts what a seam SUPPLIES. `records: N of M` was
computed from the claims, so a declaration whose only claim named a rule nothing
runs reported one record as claimed two lines under the line saying that rule is
supplied by nothing.

Verified against the fleet: `uphold check` and the reader it replaces return the
same exit code and the same claim count in 68 of 71 repositories. The three that
differ have a policy `config::load` refuses outright -- `command.before` on a
`builtin` -- which the Python never validated and which `uphold scan` already
fails on today.

35 behaviour tests move from tests/test_uphold_check.py to tests/check_cli.rs
rather than going away, and two caught real regressions in the port: a lefthook
remote given as a filesystem path, which is what scripts/consumer_check.sh
writes, and the rule that a remote is only ours when ONE entry names this
repository and takes its config.
`uphold check` and `uphold check --coverage` where the reconcile used to be, and
a paragraph in the README on where the split falls: a mode that decides whether
a check passed reads the policy, and the loader that resolves the policy is the
binary. What is left in the script renders prose for a person and cannot
disagree with the engine about anything.
@HackingGate
HackingGate marked this pull request as ready for review August 12, 2026 12:57
@HackingGate HackingGate changed the title Ask the loader which seam runs a rule, instead of guessing from an empty hook list Let the loader answer which rules run, and close the second reader Aug 12, 2026
CI found the constraint `content_policy_rules` documented before this port
started: the modes that ask the engine run in a pre-commit environment, and the
binary is not on PATH there. `--review` is a `language: system` hook in this
repository's own config, so it ran on a machine holding cargo, a checkout, and
every ingredient except the one command nobody had run -- and reported
could-not-look on a tree that was fine.

Three attempts now, in order: a built binary under `target/`, then PATH, then
`cargo run --manifest-path`. The last is not a convenience. Neither caller
leaves this repository -- `--review` is in no consumer's manifest and `--oscal`
is run by hand -- so the fallback costs a consumer nothing and is the difference
between a hook that works in a fresh checkout and one that needs a build first.

A binary none of the three can produce is still could-not-look, and still exit
2. It is not a smaller answer or an older one.
Three call sites still invoked `uphold_check.py` for a mode it no longer has:
the `uphold-check-here` pre-commit hook, the `uphold-check` lefthook command,
and the trigger list on both, which still named `.cmd-shims/checks.enabled`.
They run `cargo run --quiet -- check` now, beside the `content-policy` and
`guards` commands that already did.

The Python tests that reach the engine skip where neither a built binary nor
cargo can answer, which is the precedent `test_the_two_readers_of_the_policy_agree`
already set: the catalog job runs on an image with no Rust toolchain by design,
and a test needing a `cargo build` to be meaningful must not report a red suite
to somebody who has not run one. What is skipped there is asserted in
tests/check_cli.rs, which runs where a toolchain exists -- so the behaviour is
covered, and it is covered in the language that can reach it.

Verified by running the suite with `target/` moved aside and a PATH holding
python3 and git and nothing else: 22 skip, the rest pass. With a toolchain, all
73 run.
The port scoped hook ids by the `repo:` url they were pinned under, on the
reasoning that another repository's `uphold-scan` establishes nothing here. The
reasoning is fine and the predicate is wrong, which the consumer harness caught:
`scripts/consumer_check.sh` clones this repository into a temporary directory
and pins it by PATH, so the last segment of its `repo:` is `hooks`. Every guard
the consumer pinned was read as belonging to somebody else, and a true claim was
refused in the one job that drives a real consumer end to end.

The id is specific enough on its own -- `uphold-guard-push` is this binary's
name for this binary's stage -- and the manifest is where the list comes from,
so an id cannot be published there and forgotten here. This is what the test
named `the_seam_is_found_by_a_published_id_not_by_one_repositorys_name` has been
saying since before the port; I ported the name and inverted the behaviour. It
now asserts what it says, over every published guard id and against a `repo:`
url naming no owner at all.

`names_this_repository` stays for the lefthook `remotes:` block, where the url
is the only thing there is to match on and both halves of one entry have to
agree.
The job proved the reconcile by running `uphold_check.py` from a directory that
is not this repository, which is the right shape and the wrong entry point: the
reconcile is `uphold check`. It runs on a toolchain-free image, so it builds
one, and builds rather than pinning a release because what is under test is this
commit's reconcile against this commit's manifest.

The consumer also gets a `policy/principles.toml`. It had none, and did not need
one while the reader tolerated a repository with no policy; the loader does not,
and a repository with nothing to resolve has nothing to reconcile against.

Both steps verified locally against the built binary: the starter declaration
reconciles at exit 0 in a tree with no runner configuration, and a directory
with no declaration at all is still exit 2.
`hooks/lefthook.yml` -- the config a lefthook consumer inherits -- still ran
`uphold_check.py` through `.lefthook/pre-commit/uphold-check`, so a clean commit
in the lefthook consumer was refused by a usage message.

That wrapper existed for one reason, written at the top of it: "uphold_check.py
is not in the binary, so it has to be reached IN THIS REPOSITORY -- and
lefthook's `scripts` is the one mechanism that resolves against the remote clone
rather than against the consumer's own tree." The reconcile is `uphold check`
now and PATH reaches it, so the job is a plain `run:` beside `uphold scan` and
`uphold guard`, and the wrapper is deleted.

It stays a `jobs:` entry rather than a command, for the reason already recorded:
`glob` is a job key, and a check with no firing condition would load the catalog
in front of every one-line fix while `.pre-commit-hooks.yaml` publishes the
opposite as the design.

The shell-lint globs in lefthook.yml go back to `scripts/*.sh`. The brace
pattern was there to catch an extensionless wrapper that no longer exists.

@coderabbitai coderabbitai 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.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

203-217: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Two lines still describe the script as the reconciler.

Line 206 tells the reader to run ./uphold_check.py for "this repo's own declaration". That invocation now prints usage and exits 2. Line 215 calls uphold_check.py the "reconciler; the hook other repos install"; .pre-commit-hooks.yaml now publishes uphold check under language: rust.

📝 Proposed fix
 python3 -m unittest discover -s tests
-./uphold_check.py                 # this repo's own declaration
+cargo run --quiet -- check        # this repo's own declaration
 cargo run --quiet -- guard --stage manual   # every pin still names a ref
principles/*.toml       canonical records
QUICK_REFERENCE.md      generated human index
REVIEW.md, AGENTS.md    generated review tier: the judgment no rule decides
name-index.json         generated lookup index: every name -> a record id
-uphold_check.py     reconciler; the hook other repos install
+uphold_check.py         the catalog: explain, list, init, review, OSCAL export
scripts/                analysis, catalog loading, validation, generation
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 203 - 217, Update the README command and
generated-file descriptions to match the current CLI: replace the obsolete
./uphold_check.py invocation with the supported catalog command, and revise the
uphold_check.py entry to describe it as the catalog supporting explain, list,
init, review, and OSCAL export rather than as a reconciler or installed hook.
🧹 Nitpick comments (6)
src/catalog.rs (1)

145-158: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider rejecting unrecognized status and automatable values.

deprecated() and refuses_automation() compare exact strings. A record that writes automatable = "No", "none", or status = "Deprecated" is then treated as claimable and not deprecated. The records are compiled in, so this is not exploitable today. A typed enum would turn a future typo into a parse failure instead of a silently wrong reconcile.

♻️ Proposed typed representation
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub(crate) enum Automatable {
+    Yes,
+    Partial,
+    No,
+}
+
 #[derive(Debug, Clone, Default, Deserialize)]
 pub(crate) struct Enforcement {
     #[serde(default)]
-    pub automatable: Option<String>,
+    pub automatable: Option<Automatable>,
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/catalog.rs` around lines 145 - 158, Replace the string-based status and
automatable handling used by Record::deprecated, Record::refuses_automation, and
Record::automatable with typed representations that reject unrecognized values
during record parsing, while preserving the existing semantics for valid
"deprecated" and "no" values.
.github/workflows/test.yml (1)

326-326: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop -r from the file copy.

policy/principles.toml is a single file. cp policy/principles.toml /tmp/consumer/policy/principles.toml is enough, and it fails loudly if the source ever becomes a directory.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml at line 326, Update the copy command in the
workflow to remove the recursive -r option when copying policy/principles.toml
to the consumer policy destination, preserving a direct single-file copy that
fails if the source is a directory.
tests/check_cli.rs (1)

91-116: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a case for the rejected argument form.

src/main.rs Lines 235-237 return exit 2 with usage: uphold check [--coverage] for any other argument list. No test here covers it, and Codecov reports 38 uncovered changed lines. One assertion on check(&root, &["--bogus"]) closes that gap cheaply.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/check_cli.rs` around lines 91 - 116, Add a test alongside the existing
exit-code contract tests that invokes check(&root, &["--bogus"]) and asserts
exit code 2, covering the rejected argument path in the check command while
preserving the existing setup and assertion style.
src/main.rs (1)

46-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document --upstream in USAGE.

--upstream is a supported option and uphold_check.py::upstream_url depends on it. USAGE lists --version and --help but not --upstream, so the printed help hides an interface another program calls.

📝 Proposed addition
   uphold shim <command> [args...]     check what a command would publish, then run it
+  uphold --upstream                  the repository url this binary was built from
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.rs` around lines 46 - 53, Update the USAGE help text to document the
supported --upstream option, including its expected value or argument, alongside
the existing global options such as --version and --help. Keep the wording
consistent with the other entries so uphold_check.py::upstream_url is
represented in the printed interface.
.pre-commit-hooks.yaml (1)

40-40: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

inherit.paths files do not fire this hook.

The trigger list names policy/principles.toml but not files reached through inherit.paths. tests/check_cli.rs::a_rule_inherited_through_inherit_paths_is_supplied shows an inherited file can define a claimed rule. An edit that removes that rule turns a true claim false, and this hook does not run. The same gap exists in .pre-commit-config.yaml and hooks/lefthook.yml. Consider widening the pattern to ^policy/.*\.toml$.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.pre-commit-hooks.yaml at line 40, Widen the file pattern for the hook in
`.pre-commit-hooks.yaml` from individual policy files to all TOML files under
`policy/`, so changes to files referenced through `inherit.paths` trigger it.
Preserve the existing configuration-file matches, including
`.pre-commit-config.yaml` and `lefthook.yml`.
tests/test_review.py (1)

30-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated needs_the_engine probe, and neither copy checks the exit code. Both test modules define byte-identical decorators. uphold_check.engine raises CouldNotLook only when no attempt starts; it returns the first CompletedProcess otherwise without reading returncode. An uphold on PATH that answers non-zero therefore leaves the decorated tests enabled, and they fail where the intent is to skip. Extract one helper and add the exit-code check in that single place.

  • tests/test_review.py#L30-L43: keep the implementation, add if answered.returncode != 0: return unittest.skip(...)(test), and move it into a shared test helper module.
  • tests/test_uphold_check.py#L26-L39: delete this copy and import the shared helper instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_review.py` around lines 30 - 43, Extract the duplicated
needs_the_engine decorator from tests/test_review.py:30-43 into a shared test
helper, retaining the CouldNotLook skip and also skipping when the returned
answered.returncode is non-zero. Update tests/test_uphold_check.py:26-39 to
delete its local copy and import the shared helper; both sites should use the
single corrected implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/test.yml:
- Around line 329-335: Update the workflow step named “A declaration that cannot
be read is not a pass” so it exercises an actual unreadable or malformed
declaration rather than an empty directory; create the required declaration
fixture in /tmp/unreadable and apply permissions or content that triggers the
intended read failure, while preserving the expected nonzero assertion for
uphold check. If the test is meant to cover missing policy instead, rename the
step accordingly.

In `@src/check.rs`:
- Around line 539-563: Update the validation branches in the declaration
enforcement loop to classify leftover tier, missing principle/rule, and
blank-field cases as refused claims rather than fatal inspection errors: record
each claim in failures and continue processing, unless the documented contract
explicitly defines malformed declarations as exit 2. If retaining Fatal::at,
revise the related doc comment to state that malformed declarations produce that
exit classification.
- Around line 289-312: Update the run-processing loop to collect and handle
every matching subcommand seam instead of stopping at the first match from
words.windows(2).find_map(...). Preserve the existing validation for the
preceding word and apply the scan and guard handling logic to each matching
pair, so commands such as “uphold scan && uphold guard” register both seams.
- Around line 610-625: Update the success path in run() to print every entry in
installed.unreadable alongside the existing installed.how notes, using the same
“could not look” reporting style as the coverage path while preserving the clean
exit behavior.
- Around line 274-277: Update lefthook_seams() to discover all supported
Lefthook configuration names, including lefthook.yml, lefthook.yaml, their
dot-prefixed forms, and -local variants, instead of returning early when
lefthook.yml is absent. Preserve seam detection and installed() behavior for
every valid configuration name.
- Around line 283-288: Update the stage-processing loop in the relevant check
flow to recognize Lefthook stage/group keys independently of the manifest-only
guards map, including uphold-manual. Parse each guard command’s --stage
argument, add the referenced manual guard to Installed.stages, and collect its
command names into Installed.local only when the stage/group key is valid; do
not use guards as the stage-key filter.

In `@tests/check_cli.rs`:
- Around line 672-691: Update
the_starter_declaration_is_valid_and_enforces_nothing so a missing python3
interpreter skips the test instead of panicking on Command::output().unwrap();
handle the Command construction/execution error using the test suite’s
established skip mechanism, while preserving the existing assertions when
python3 is available.

In `@tests/test_review.py`:
- Around line 30-43: Update needs_the_engine to inspect the CompletedProcess
returned by uphold_check.engine(ROOT, "--version") and skip the test when its
returncode is non-zero, while preserving the existing CouldNotLook skip behavior
and allowing execution only when the probe succeeds.

In `@tests/test_uphold_check.py`:
- Around line 137-145: Remove the class-level needs_the_engine decorator from
NoProseInRuntime and apply it only to the two test methods that invoke --review.
Leave test_the_catalog_modes_carry_no_record_prose_into_a_report undecorated so
the --list catalog assertion runs without a Rust toolchain.

In `@uphold_check.py`:
- Around line 240-278: Update engine_suppliers so strict=False preserves
evidence for claims that reconcile even when other claims fail; either make the
underlying engine output successful claim evidence on violation exits or have
engine_suppliers reconcile claims individually. Ensure run_review still receives
every valid rule in suppliers while strict=True continues refusing unreconciled
enforcement results.

---

Outside diff comments:
In `@README.md`:
- Around line 203-217: Update the README command and generated-file descriptions
to match the current CLI: replace the obsolete ./uphold_check.py invocation with
the supported catalog command, and revise the uphold_check.py entry to describe
it as the catalog supporting explain, list, init, review, and OSCAL export
rather than as a reconciler or installed hook.

---

Nitpick comments:
In @.github/workflows/test.yml:
- Line 326: Update the copy command in the workflow to remove the recursive -r
option when copying policy/principles.toml to the consumer policy destination,
preserving a direct single-file copy that fails if the source is a directory.

In @.pre-commit-hooks.yaml:
- Line 40: Widen the file pattern for the hook in `.pre-commit-hooks.yaml` from
individual policy files to all TOML files under `policy/`, so changes to files
referenced through `inherit.paths` trigger it. Preserve the existing
configuration-file matches, including `.pre-commit-config.yaml` and
`lefthook.yml`.

In `@src/catalog.rs`:
- Around line 145-158: Replace the string-based status and automatable handling
used by Record::deprecated, Record::refuses_automation, and Record::automatable
with typed representations that reject unrecognized values during record
parsing, while preserving the existing semantics for valid "deprecated" and "no"
values.

In `@src/main.rs`:
- Around line 46-53: Update the USAGE help text to document the supported
--upstream option, including its expected value or argument, alongside the
existing global options such as --version and --help. Keep the wording
consistent with the other entries so uphold_check.py::upstream_url is
represented in the printed interface.

In `@tests/check_cli.rs`:
- Around line 91-116: Add a test alongside the existing exit-code contract tests
that invokes check(&root, &["--bogus"]) and asserts exit code 2, covering the
rejected argument path in the check command while preserving the existing setup
and assertion style.

In `@tests/test_review.py`:
- Around line 30-43: Extract the duplicated needs_the_engine decorator from
tests/test_review.py:30-43 into a shared test helper, retaining the CouldNotLook
skip and also skipping when the returned answered.returncode is non-zero. Update
tests/test_uphold_check.py:26-39 to delete its local copy and import the shared
helper; both sites should use the single corrected implementation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c8c877df-84ee-48c0-8b38-0985dbfff164

📥 Commits

Reviewing files that changed from the base of the PR and between b93623e and 7c2244b.

📒 Files selected for processing (15)
  • .github/workflows/test.yml
  • .lefthook/pre-commit/uphold-check
  • .pre-commit-config.yaml
  • .pre-commit-hooks.yaml
  • README.md
  • docs/REFERENCE.md
  • hooks/lefthook.yml
  • lefthook.yml
  • src/catalog.rs
  • src/check.rs
  • src/main.rs
  • tests/check_cli.rs
  • tests/test_review.py
  • tests/test_uphold_check.py
  • uphold_check.py
💤 Files with no reviewable changes (1)
  • .lefthook/pre-commit/uphold-check
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/REFERENCE.md

Comment on lines 329 to +335
- name: A declaration that cannot be read is not a pass
run: |
set -euo pipefail
mkdir -p /tmp/unreadable
cd /tmp/unreadable
set +e
"$GITHUB_WORKSPACE/uphold_check.py"
"$GITHUB_WORKSPACE/target/release/uphold" check

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The step name does not match what the step exercises.

/tmp/unreadable is empty. uphold check therefore fails at policy discovery, not at reading a declaration. The exit code is 2 in both cases, so the assertion passes for the wrong reason, and a regression in the unreadable-declaration path would not be caught here. Write an actual malformed declaration into the directory, or rename the step to describe missing policy.

🧪 Proposed fix
-      - name: A declaration that cannot be read is not a pass
+      - name: A declaration that cannot be parsed is not a pass
         run: |
           set -euo pipefail
-          mkdir -p /tmp/unreadable
+          mkdir -p /tmp/unreadable/policy
+          cp policy/principles.toml /tmp/unreadable/policy/principles.toml
+          printf '[[enforce]] this is not toml\n' > /tmp/unreadable/policy/upheld.toml
           cd /tmp/unreadable
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: A declaration that cannot be read is not a pass
run: |
set -euo pipefail
mkdir -p /tmp/unreadable
cd /tmp/unreadable
set +e
"$GITHUB_WORKSPACE/uphold_check.py"
"$GITHUB_WORKSPACE/target/release/uphold" check
- name: A declaration that cannot be parsed is not a pass
run: |
set -euo pipefail
mkdir -p /tmp/unreadable/policy
cp policy/principles.toml /tmp/unreadable/policy/principles.toml
printf '[[enforce]] this is not toml\n' > /tmp/unreadable/policy/upheld.toml
cd /tmp/unreadable
set +e
"$GITHUB_WORKSPACE/target/release/uphold" check
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 329 - 335, Update the workflow step
named “A declaration that cannot be read is not a pass” so it exercises an
actual unreadable or malformed declaration rather than an empty directory;
create the required declaration fixture in /tmp/unreadable and apply permissions
or content that triggers the intended read failure, while preserving the
expected nonzero assertion for uphold check. If the test is meant to cover
missing policy instead, rename the step accordingly.

Comment thread src/check.rs
Comment on lines +274 to +277
let path = root.join("lefthook.yml");
if !path.is_file() {
return Ok(found);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

lefthook supported configuration file names lefthook.yaml lefthook-local

💡 Result:

Lefthook supports a variety of configuration file names and formats, allowing for both project-wide configurations and local developer-specific overrides [1][2]. Main Configuration Files Lefthook searches for a main configuration file using the base names lefthook or.lefthook, supporting YAML, TOML, JSON, and JSONC formats [1][2]. Supported filenames include [2]: - YAML: lefthook.yml, lefthook.yaml,.lefthook.yml,.lefthook.yaml,.config/lefthook.yml,.config/lefthook.yaml - TOML: lefthook.toml,.lefthook.toml,.config/lefthook.toml - JSON: lefthook.json,.lefthook.json,.config/lefthook.json - JSONC: lefthook.jsonc,.lefthook.jsonc,.config/lefthook.jsonc Only one main configuration file should be used per project [1][2]. Files without a leading dot are also looked up in the.config subdirectory [1][3]. Local Configuration Files Lefthook supports an additional local configuration file to merge with or override the main configuration [1][2]. This file uses the base name lefthook-local or.lefthook-local, followed by the appropriate extension (e.g.,.yml,.yaml,.json,.toml, or.jsonc) [1][2]. If you use a leading dot for your main configuration file (e.g.,.lefthook.yml), you must also use a leading dot for the local configuration file (e.g.,.lefthook-local.yml) [1][4]. The local configuration can be used independently without a main configuration file, which is useful for setting up developer-specific workflows that are not shared with the rest of the team [1][2]. It is common practice to add the local configuration file to your global.gitignore [1][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files and relevant symbols ---'
git ls-files | rg '(^|/)(check\.rs|Cargo\.toml|.*test.*|README.*)$' | head -80
rg -n -C 8 'lefthook\.yml|installed\(|let path = root\.join|found' src tests 2>/dev/null || true

printf '%s\n' '--- source outline ---'
ast-grep outline src/check.rs 2>/dev/null || true

Repository: HackingGate/uphold

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- src/check.rs structure and target ---'
ast-grep outline src/check.rs 2>/dev/null | head -120
sed -n '1,360p' src/check.rs

printf '%s\n' '--- check call sites and configuration loading ---'
rg -n -C 6 'check::|installed|lefthook|config::load|load\(' src tests -g '*.rs' -g '*.toml' -g '*.yml' -g '*.yaml' | head -300

Repository: HackingGate/uphold

Length of output: 34288


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- remaining check flow ---'
sed -n '350,700p' src/check.rs

printf '%s\n' '--- focused check tests and fixtures ---'
sed -n '1,240p' tests/check_cli.rs
sed -n '400,540p' tests/check_cli.rs
rg -n -C 4 'lefthook(-local)?\.(yml|yaml)|\.lefthook(-local)?\.(yml|yaml)|config file|configuration file' README.md docs src tests .github 2>/dev/null | head -240

Repository: HackingGate/uphold

Length of output: 38327


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

source = Path("src/check.rs").read_text()
pins = Path("src/pins.rs").read_text()

start = source.index("fn lefthook_seams")
end = source.index("\n/// The command names a lefthook config defines", start)
body = source[start:end]

joined_candidates = re.findall(r'root\.join\("([^"]+)"\)', body)
has_early_absent_return = bool(re.search(
    r'if !path\.is_file\(\) \{\s*return Ok\(found\);\s*\}',
    body,
    re.S,
))
supported_main_names = re.findall(
    r'^\s*"((?:\.?lefthook)\.(?:yml|yaml))",\s*$',
    pins,
    re.M,
)

print("lefthook_seams root.join candidates:", joined_candidates)
print("missing candidate returns empty Installed:", has_early_absent_return)
print("pins.rs main Lefthook names:", supported_main_names)

assert joined_candidates == ["lefthook.yml"]
assert has_early_absent_return
assert set(supported_main_names) >= {
    "lefthook.yml", "lefthook.yaml", ".lefthook.yml", ".lefthook.yaml"
}
PY

Repository: HackingGate/uphold

Length of output: 388


Read supported Lefthook configuration names. lefthook_seams() checks only lefthook.yml and returns no seams when it is absent. It must also handle lefthook.yaml, dot-prefixed names, and -local variants. Otherwise, valid configurations under those names make installed() reject their claims with exit 1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/check.rs` around lines 274 - 277, Update lefthook_seams() to discover all
supported Lefthook configuration names, including lefthook.yml, lefthook.yaml,
their dot-prefixed forms, and -local variants, instead of returning early when
lefthook.yml is absent. Preserve seam detection and installed() behavior for
every valid configuration name.

Comment thread src/check.rs
Comment on lines +283 to +288
for (stage, body) in &config.stages {
// Only a name git knows is a stage; `remotes`, `colors` and the rest of
// lefthook's top-level keys are not.
if !guards.contains_key(stage.as_str()) {
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Compare the stages published by the manifest against the stages used in runner configs.
set -uo pipefail

# The stages this manifest publishes for guard hooks.
rg -n -C6 'guard' .pre-commit-hooks.yaml || true

# Stage keys written in the repository's own lefthook configurations.
fd -i 'lefthook' -e yml -e yaml --exec rg -n '^[a-z][a-z-]*:' {} \;

Repository: HackingGate/uphold

Length of output: 5455


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- src/check.rs structure ---'
ast-grep outline src/check.rs

printf '%s\n' '--- relevant symbols and call sites ---'
rg -n -C8 'published|lefthook_commands|guards|config\.stages|uphold guard|local' src/check.rs src

printf '%s\n' '--- target implementation ---'
sed -n '230,390p' src/check.rs

printf '%s\n' '--- manifest and runner configuration files ---'
git ls-files | rg '(^|/)(lefthook|\.pre-commit-hooks)(\.ya?ml)?$|lefthook'

Repository: HackingGate/uphold

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- check.rs target implementation ---'
sed -n '245,385p' src/check.rs

printf '%s\n' '--- relevant configuration files ---'
git ls-files | rg '(^|/)(lefthook\.ya?ml|\.pre-commit-hooks\.yaml|README\.md|.*consumer.*)$'

printf '%s\n' '--- lefthook stage declarations ---'
while IFS= read -r file; do
  printf '%s\n' "--- $file"
  rg -n -C2 '^(pre-commit|commit-msg|pre-merge-commit|pre-push|manual|uphold-manual|[A-Za-z0-9_-]+):' "$file" || true
done < <(git ls-files | rg '(^|/)lefthook\.ya?ml$')

printf '%s\n' '--- stage terminology and hook-name references ---'
rg -n -C4 'git stage|git hook|hook name|pre-commit|commit-msg|pre-merge-commit|pre-push|manual|uphold-manual' README.md docs src .github 2>/dev/null | head -n 300

Repository: HackingGate/uphold

Length of output: 5912


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- repository lefthook configurations ---'
for file in lefthook.yml hooks/lefthook.yml; do
  if test -f "$file"; then
    printf '%s\n' "--- $file"
    cat -n "$file"
  fi
done

printf '%s\n' '--- manifest guard stages ---'
python3 - <<'PY'
import re
from pathlib import Path

text = Path(".pre-commit-hooks.yaml").read_text()
current_entry = None
current_stages = []
for line in text.splitlines():
    m = re.match(r"\s*entry:\s*(.*)$", line)
    if m:
        current_entry = m.group(1)
        current_stages = []
        continue
    m = re.match(r"\s*stages:\s*\[(.*)\]\s*$", line)
    if m and current_entry and re.search(r"\bguard\b", current_entry):
        stages = [x.strip().strip("'\"") for x in m.group(1).split(",")]
        print(f"{current_entry}: {stages}")
PY

printf '%s\n' '--- stage keys in both lefthook configurations ---'
python3 - <<'PY'
import re
from pathlib import Path

for name in ("lefthook.yml", "hooks/lefthook.yml"):
    path = Path(name)
    if not path.is_file():
        continue
    keys = []
    for line in path.read_text().splitlines():
        m = re.match(r"^([A-Za-z0-9_-]+):\s*$", line)
        if m and m.group(1) not in {"remotes", "colors", "skip_output", "extends", "assert_lefthook_version"}:
            keys.append(m.group(1))
    print(f"{name}: {keys}")
PY

Repository: HackingGate/uphold

Length of output: 10915


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- policy stages and guard consumers ---'
rg -n -C6 'guard::stages|hooks\s*=|pre-commit|commit-msg|pre-merge-commit|pre-push|manual' policy src tests scripts README.md 2>/dev/null | head -n 400

printf '%s\n' '--- tests for lefthook detection ---'
rg -n -C8 'lefthook|published|commands|manual|uphold-manual|guards' tests src/check.rs | head -n 500

printf '%s\n' '--- behavioral model of the two guarded loops ---'
python3 - <<'PY'
from pathlib import Path
import re

manifest = Path(".pre-commit-hooks.yaml").read_text()
guards = {}
entry = None
for line in manifest.splitlines():
    m = re.match(r"\s*entry:\s*(.*)$", line)
    if m:
        entry = m.group(1)
        continue
    m = re.match(r"\s*stages:\s*\[(.*)\]\s*$", line)
    if m and entry and re.search(r"\bguard\b", entry):
        for stage in (x.strip().strip("'\"") for x in m.group(1).split(",")):
            guards.setdefault(stage, entry)

for filename in ("lefthook.yml", "hooks/lefthook.yml"):
    text = Path(filename).read_text()
    stages = [
        m.group(1)
        for m in re.finditer(r"^([A-Za-z0-9_-]+):\s*$", text, re.MULTILINE)
        if m.group(1) not in {"remotes", "colors", "skip_output", "extends", "assert_lefthook_version"}
    ]
    skipped = [stage for stage in stages if stage not in guards]
    print(f"{filename}")
    print(f"  published guard stages: {sorted(guards)}")
    print(f"  stage keys:             {stages}")
    print(f"  skipped by both loops:  {skipped}")
PY

Repository: HackingGate/uphold

Length of output: 50376


Separate Lefthook stage keys from guard stages. guards contains only manifest stages, but lefthook.yml uses uphold-manual to run uphold guard --stage manual. Both loops skip that key. The manual guard is not added to Installed.stages, and its command names are omitted from Installed.local. Parse the guard's --stage argument and collect commands from valid Lefthook stage/group keys without using guards as the stage-key filter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/check.rs` around lines 283 - 288, Update the stage-processing loop in the
relevant check flow to recognize Lefthook stage/group keys independently of the
manifest-only guards map, including uphold-manual. Parse each guard command’s
--stage argument, add the referenced manual guard to Installed.stages, and
collect its command names into Installed.local only when the stage/group key is
valid; do not use guards as the stage-key filter.

Comment thread src/check.rs
Comment on lines +289 to +312
for run in runs_in(body) {
let words: Vec<&str> = run.split_whitespace().collect();
// The subcommand, and the word before it. Matched on the
// SUBCOMMAND and not the executable: this repository runs its own
// binary out of the tree with `cargo run -- scan`, a consumer runs
// `uphold scan` from PATH, and a third by absolute path. All three
// are the same seam, and a pattern anchored on the program name
// recognised only the middle one.
let Some((before, subcommand)) = words.windows(2).find_map(|pair| match pair {
[before, word @ ("scan" | "guard")] => Some((*before, *word)),
_ => None,
}) else {
continue;
};
if !(before.ends_with("uphold") || before == "--") {
continue;
}
direct = true;
if subcommand == "scan" && !words.contains(&"--text") {
found.scan = true;
} else if subcommand == "guard" {
found.stages.insert(stage.clone());
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

find_map reads only the first seam in a run: string.

words.windows(2).find_map(...) stops at the first scan or guard word. A single command that drives both seams registers only the first one. Example: run: uphold scan && uphold guard. The guard stage is then absent from found.stages, and a claim on a guard rule fails with exit 1 while the seam is installed.

Collect every match instead of the first.

🐛 Proposed fix
             let words: Vec<&str> = run.split_whitespace().collect();
-            let Some((before, subcommand)) = words.windows(2).find_map(|pair| match pair {
-                [before, word @ ("scan" | "guard")] => Some((*before, *word)),
-                _ => None,
-            }) else {
-                continue;
-            };
-            if !(before.ends_with("uphold") || before == "--") {
-                continue;
-            }
-            direct = true;
-            if subcommand == "scan" && !words.contains(&"--text") {
-                found.scan = true;
-            } else if subcommand == "guard" {
-                found.stages.insert(stage.clone());
-            }
+            for pair in words.windows(2) {
+                let [before, subcommand @ ("scan" | "guard")] = pair else {
+                    continue;
+                };
+                if !(before.ends_with("uphold") || *before == "--") {
+                    continue;
+                }
+                direct = true;
+                if *subcommand == "scan" && !words.contains(&"--text") {
+                    found.scan = true;
+                } else if *subcommand == "guard" {
+                    found.stages.insert(stage.clone());
+                }
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for run in runs_in(body) {
let words: Vec<&str> = run.split_whitespace().collect();
// The subcommand, and the word before it. Matched on the
// SUBCOMMAND and not the executable: this repository runs its own
// binary out of the tree with `cargo run -- scan`, a consumer runs
// `uphold scan` from PATH, and a third by absolute path. All three
// are the same seam, and a pattern anchored on the program name
// recognised only the middle one.
let Some((before, subcommand)) = words.windows(2).find_map(|pair| match pair {
[before, word @ ("scan" | "guard")] => Some((*before, *word)),
_ => None,
}) else {
continue;
};
if !(before.ends_with("uphold") || before == "--") {
continue;
}
direct = true;
if subcommand == "scan" && !words.contains(&"--text") {
found.scan = true;
} else if subcommand == "guard" {
found.stages.insert(stage.clone());
}
}
for run in runs_in(body) {
let words: Vec<&str> = run.split_whitespace().collect();
// The subcommand, and the word before it. Matched on the
// SUBCOMMAND and not the executable: this repository runs its own
// binary out of the tree with `cargo run -- scan`, a consumer runs
// `uphold scan` from PATH, and a third by absolute path. All three
// are the same seam, and a pattern anchored on the program name
// recognised only the middle one.
for pair in words.windows(2) {
let [before, subcommand @ ("scan" | "guard")] = pair else {
continue;
};
if !(before.ends_with("uphold") || *before == "--") {
continue;
}
direct = true;
if *subcommand == "scan" && !words.contains(&"--text") {
found.scan = true;
} else if *subcommand == "guard" {
found.stages.insert(stage.clone());
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/check.rs` around lines 289 - 312, Update the run-processing loop to
collect and handle every matching subcommand seam instead of stopping at the
first match from words.windows(2).find_map(...). Preserve the existing
validation for the preceding word and apply the scan and guard handling logic to
each matching pair, so commands such as “uphold scan && uphold guard” register
both seams.

Comment thread src/check.rs
Comment on lines +539 to +563
for (index, claim) in declaration.enforce.iter().enumerate() {
let at = format!("enforce[{index}]");
if claim.tier.is_some() {
return Err(Fatal::at(
&path,
format!(
"{at} carries a `tier`. The field is gone: a rule id resolves across \
every seam at once, so a claim naming one no longer has to say which. \
Drop the line."
),
));
}
let (Some(principle), Some(rule)) = (claim.principle.as_deref(), claim.rule.as_deref())
else {
return Err(Fatal::at(
&path,
format!("{at}: `principle` and `rule` are both required"),
));
};
if principle.trim().is_empty() || rule.trim().is_empty() {
return Err(Fatal::at(
&path,
format!("{at}: `principle` and `rule` must not be blank"),
));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

A leftover tier returns exit 2, which reads as could-not-look.

The doc comment at Lines 95-98 states that a leftover tier fails as a false claim. The code returns Fatal::at, which maps to exit 2. engine_suppliers in uphold_check.py (Lines 240-278) turns exit 2 into CouldNotLook. An author error in the declaration is then reported as an inability to inspect the repository, not as a refused claim.

The same applies to the missing-field and blank-field branches. If exit 2 is intended for a malformed declaration, state that in the doc comment at Lines 95-98. If a leftover tier is intended to be a refused claim, push it onto failures and continue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/check.rs` around lines 539 - 563, Update the validation branches in the
declaration enforcement loop to classify leftover tier, missing principle/rule,
and blank-field cases as refused claims rather than fatal inspection errors:
record each claim in failures and continue processing, unless the documented
contract explicitly defines malformed declarations as exit 2. If retaining
Fatal::at, revise the related doc comment to state that malformed declarations
produce that exit classification.

Comment thread src/check.rs
Comment on lines +610 to +625
if !failures.is_empty() {
eprintln!("enforcement claims refused ({DECLARATION}):");
for failure in &failures {
eprintln!("- {failure}");
}
return Ok(Exit::Violations);
}

println!("reconciled {} enforcement claims:", evidence.len());
for line in &evidence {
println!(" {line}");
}
for note in &installed.how {
println!(" note {note}");
}
Ok(Exit::Clean)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The success path hides installed.unreadable.

run() prints installed.how but never installed.unreadable. If every claim resolves and one runner configuration could not be read, the command exits 0 and reports nothing about the configuration it could not inspect. The coverage path reports the same holes as could not look at Lines 694-699.

Print the unreadable notes here as well.

🐛 Proposed fix
     for note in &installed.how {
         println!("  note  {note}");
     }
+    for note in &installed.unreadable {
+        println!("  could not look  {note}");
+    }
     Ok(Exit::Clean)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if !failures.is_empty() {
eprintln!("enforcement claims refused ({DECLARATION}):");
for failure in &failures {
eprintln!("- {failure}");
}
return Ok(Exit::Violations);
}
println!("reconciled {} enforcement claims:", evidence.len());
for line in &evidence {
println!(" {line}");
}
for note in &installed.how {
println!(" note {note}");
}
Ok(Exit::Clean)
if !failures.is_empty() {
eprintln!("enforcement claims refused ({DECLARATION}):");
for failure in &failures {
eprintln!("- {failure}");
}
return Ok(Exit::Violations);
}
println!("reconciled {} enforcement claims:", evidence.len());
for line in &evidence {
println!(" {line}");
}
for note in &installed.how {
println!(" note {note}");
}
for note in &installed.unreadable {
println!(" could not look {note}");
}
Ok(Exit::Clean)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/check.rs` around lines 610 - 625, Update the success path in run() to
print every entry in installed.unreadable alongside the existing installed.how
notes, using the same “could not look” reporting style as the coverage path
while preserving the clean exit behavior.

Comment thread tests/check_cli.rs
Comment on lines +672 to +691
#[test]
fn the_starter_declaration_is_valid_and_enforces_nothing() {
let root = workspace();
write(&root, "policy/principles.toml", GUARD_POLICY);
write(&root, ".pre-commit-config.yaml", PRE_COMMIT);
let starter = Command::new("python3")
.args([
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("uphold_check.py")
.to_str()
.unwrap(),
"--init",
])
.output()
.unwrap();
assert_eq!(starter.status.code().unwrap(), 0);
write(&root, "policy/upheld.toml", &stdout(&starter));
let output = check(&root, &[]);
assert_eq!(code(&output), 0, "{}{}", stdout(&output), stderr(&output));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

This test fails, rather than skips, where python3 is absent.

Command::new("python3") returns Err when no interpreter is on PATH, and .unwrap() turns that into a panic. The Python suite added needs_the_engine precisely so a missing toolchain reports a skip and not a red suite; this test does the reverse for the Python toolchain.

Either embed the starter declaration as a constant in this file, or skip when the interpreter is missing.

🧪 Proposed fix
-    let starter = Command::new("python3")
+    let Ok(starter) = Command::new("python3")
         .args([
             Path::new(env!("CARGO_MANIFEST_DIR"))
                 .join("uphold_check.py")
                 .to_str()
                 .unwrap(),
             "--init",
         ])
         .output()
-        .unwrap();
+    else {
+        // No interpreter here. tests/test_uphold_check.py asserts --init where one exists.
+        return;
+    };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[test]
fn the_starter_declaration_is_valid_and_enforces_nothing() {
let root = workspace();
write(&root, "policy/principles.toml", GUARD_POLICY);
write(&root, ".pre-commit-config.yaml", PRE_COMMIT);
let starter = Command::new("python3")
.args([
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("uphold_check.py")
.to_str()
.unwrap(),
"--init",
])
.output()
.unwrap();
assert_eq!(starter.status.code().unwrap(), 0);
write(&root, "policy/upheld.toml", &stdout(&starter));
let output = check(&root, &[]);
assert_eq!(code(&output), 0, "{}{}", stdout(&output), stderr(&output));
}
#[test]
fn the_starter_declaration_is_valid_and_enforces_nothing() {
let root = workspace();
write(&root, "policy/principles.toml", GUARD_POLICY);
write(&root, ".pre-commit-config.yaml", PRE_COMMIT);
let Ok(starter) = Command::new("python3")
.args([
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("uphold_check.py")
.to_str()
.unwrap(),
"--init",
])
.output()
else {
// No interpreter here. tests/test_uphold_check.py asserts --init where one exists.
return;
};
assert_eq!(starter.status.code().unwrap(), 0);
write(&root, "policy/upheld.toml", &stdout(&starter));
let output = check(&root, &[]);
assert_eq!(code(&output), 0, "{}{}", stdout(&output), stderr(&output));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/check_cli.rs` around lines 672 - 691, Update
the_starter_declaration_is_valid_and_enforces_nothing so a missing python3
interpreter skips the test instead of panicking on Command::output().unwrap();
handle the Command construction/execution error using the test suite’s
established skip mechanism, while preserving the existing assertions when
python3 is available.

Comment thread tests/test_review.py
Comment on lines +30 to +43
def needs_the_engine(test):
"""Skip where neither a built binary nor cargo can answer.

The same reason `test_the_two_readers_of_the_policy_agree` gives: a test
that needs a `cargo build` to be meaningful must not report a red suite to
somebody who has not run one, and the catalog job runs on an image with no
Rust toolchain by design. What is skipped here is asserted in
tests/check_cli.rs, which runs where a toolchain exists.
"""
try:
uphold_check.engine(ROOT, "--version")
except uphold_check.CouldNotLook as error:
return unittest.skip(str(error))(test)
return 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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The probe ignores the exit code, so it does not prove the engine can answer.

uphold_check.engine raises CouldNotLook only when every attempt fails to start. It returns the first CompletedProcess otherwise, without inspecting returncode. An uphold on PATH that is a different or older program returns non-zero and the decorator still marks the test as runnable. The test then fails where the intent is to skip.

Check the exit code as well.

🧪 Proposed fix
     try:
-        uphold_check.engine(ROOT, "--version")
+        answered = uphold_check.engine(ROOT, "--version")
     except uphold_check.CouldNotLook as error:
         return unittest.skip(str(error))(test)
+    if answered.returncode != 0:
+        return unittest.skip(f"the engine could not answer: {answered.stderr.strip()}")(
+            test
+        )
     return test
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def needs_the_engine(test):
"""Skip where neither a built binary nor cargo can answer.
The same reason `test_the_two_readers_of_the_policy_agree` gives: a test
that needs a `cargo build` to be meaningful must not report a red suite to
somebody who has not run one, and the catalog job runs on an image with no
Rust toolchain by design. What is skipped here is asserted in
tests/check_cli.rs, which runs where a toolchain exists.
"""
try:
uphold_check.engine(ROOT, "--version")
except uphold_check.CouldNotLook as error:
return unittest.skip(str(error))(test)
return test
def needs_the_engine(test):
"""Skip where neither a built binary nor cargo can answer.
The same reason `test_the_two_readers_of_the_policy_agree` gives: a test
that needs a `cargo build` to be meaningful must not report a red suite to
somebody who has not run one, and the catalog job runs on an image with no
Rust toolchain by design. What is skipped here is asserted in
tests/check_cli.rs, which runs where a toolchain exists.
"""
try:
answered = uphold_check.engine(ROOT, "--version")
except uphold_check.CouldNotLook as error:
return unittest.skip(str(error))(test)
if answered.returncode != 0:
return unittest.skip(f"the engine could not answer: {answered.stderr.strip()}")(
test
)
return test
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_review.py` around lines 30 - 43, Update needs_the_engine to
inspect the CompletedProcess returned by uphold_check.engine(ROOT, "--version")
and skip the test when its returncode is non-zero, while preserving the existing
CouldNotLook skip behavior and allowing execution only when the probe succeeds.

Comment on lines +137 to +145
@needs_the_engine
class NoProseInRuntime(unittest.TestCase):
"""`enforcement-needs-a-trigger`: the tool must not carry principle text."""

def test_output_contains_no_record_prose(self):
result = run(ROOT)
def test_the_catalog_modes_carry_no_record_prose_into_a_report(self):
# `--list` is the mode a runtime is most likely to pipe somewhere. The
# reconcile's half of this invariant moved with the reconcile and is
# asserted in tests/check_cli.rs.
result = run(ROOT, "--list")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The class-level skip removes a test that needs no engine.

test_the_catalog_modes_carry_no_record_prose_into_a_report runs --list. That mode reads the catalog only and never calls the binary. @needs_the_engine on the class skips it on an image with no Rust toolchain, which the docstring says is the image the catalog job uses. The one assertion that must hold there is then never made.

Move the decorator onto the two methods that run --review.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_uphold_check.py` around lines 137 - 145, Remove the class-level
needs_the_engine decorator from NoProseInRuntime and apply it only to the two
test methods that invoke --review. Leave
test_the_catalog_modes_carry_no_record_prose_into_a_report undecorated so the
--list catalog assertion runs without a Rust toolchain.

Comment thread uphold_check.py
Comment on lines +240 to +278
def engine_suppliers(root: Path, *, strict: bool = True) -> dict[str, list[str]]:
"""Which seams supply each rule, as the reconcile in the binary sees it.

claimable = {
record_id: record
for record_id, record in records.items()
if record.get("status") != "deprecated"
and record.get("enforcement", {}).get("automatable") != "no"
}
# Intersected with what is SUPPLIED, not merely with what was claimed. The
# orphans printed immediately above are claims naming a rule no seam here
# runs, and counting them here put them back into the numerator of the one
# number a reader takes away -- a record counted as claimed by a rule that
# this very report has just said does not exist.
enforced = {principle for principle, rule in claims if rule in supplied} & set(
claimable
)
unclaimable = len(records) - len(claimable)
lines.append("")
lines.append(
f"records: {len(enforced)} of {len(claimable)} claimable records are "
f"claimed by a rule here"
)
if unclaimable:
lines.append(
f" {unclaimable} record(s) are deprecated or declare "
f'enforcement.automatable = "no" and can never be claimed'
`strict` is the difference between the two callers. `--oscal` publishes an
assertion to an outside reader, so a declaration that does not reconcile has
nothing honest to export and the refusal travels. `--review` runs over a
declaration somebody is still writing: a claim naming a rule nothing
supplies is exactly the state it exists to help with, and refusing there
would take the review document away at the moment it is most wanted. The
evidence lines for the claims that DID hold are on stdout either way.
"""
answered = engine(root, "check")
if answered.returncode == 2:
raise CouldNotLook(answered.stderr.strip() or "uphold check could not look")
if answered.returncode != 0 and strict:
raise Refused(
"the enforcement claims do not reconcile, so there is nothing "
f"honest to export:\n{answered.stderr.strip()}"
)
lines.append(
" an unclaimed record is not a gap to close by writing a claim: a claim "
"without a rule behind it is the failure `enforcement-needs-a-trigger` names"
)
return lines, status
suppliers: dict[str, list[str]] = {}
for line in answered.stdout.splitlines():
if " <- " not in line or "enforced by" not in line:
continue
_, rest = line.split(" <- ", 1)
rule, by = rest.split(" enforced by ", 1)
# Folded back to the SEAM, because an OSCAL component is a thing that
# implements a control and the seam is that thing. `uphold check` names
# the evidence -- which stage, which scan -- and a component per stage
# would split one implementation across five.
seams = []
for part in by.split(","):
part = part.strip()
if not part:
continue
seams.append("local" if part.startswith("a hook") else "uphold")
for seam in seams:
if seam not in suppliers.setdefault(rule.strip(), []):
suppliers[rule.strip()].append(seam)
return suppliers

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

--review loses every supplied rule when one claim is false.

The docstring states that evidence lines reach stdout either way. src/check.rs::run does not do that. When failures is non-empty, it writes the refusals to stderr and returns Exit::Violations before the reconciled ... block that prints the {principle} <- {rule} enforced by ... lines. So on exit 1 stdout carries no evidence.

With strict=False, this function then returns an empty suppliers map. run_review computes claimed and active from that map, so a declaration with one false claim drops every valid claim from the review document as well. That is the exact failure the comment at Lines 590-601 says the filter exists to prevent.

Make the engine emit evidence for the claims that held even when others fail, or have --review reconcile per claim instead of relying on the exit-0 stdout block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@uphold_check.py` around lines 240 - 278, Update engine_suppliers so
strict=False preserves evidence for claims that reconcile even when other claims
fail; either make the underlying engine output successful claim evidence on
violation exits or have engine_suppliers reconcile claims individually. Ensure
run_review still receives every valid rule in suppliers while strict=True
continues refusing unreconciled enforcement results.

… path

The port asked "does this url name us", and most git urls cannot answer it.
lefthook takes any of them, and `scripts/consumer_check.sh` clones this
repository to a neutral `$WORK/hooks` on purpose -- so the url its consumer
writes carries neither the owner nor the repository name, every guard went
unrecognised, and a true claim was refused. Answering exit 1 there says the
claim is FALSE about a repository whose only fault is cloning from a path.

The question is the other way round, as it was before the port: a remote is
rejected only when it spells a forge `owner/name` and that pair is not ours.
Anything without a host is a path, and a path is unidentifiable rather than
foreign. Both spellings git accepts for a host are read.

The load-bearing half is untouched: the remote and `hooks/lefthook.yml` must
appear in the SAME entry, so a fork pinning its own config is still not credited
with running every guard here.
@HackingGate
HackingGate merged commit fc596df into main Aug 12, 2026
12 checks passed
@HackingGate
HackingGate deleted the fix/seam-attribution branch August 12, 2026 15:46
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