Repository navigation
Emit guard verdicts to a caller-named file - #387
Conversation
With UPHOLD_VERDICTS set, `uphold guard --stage STAGE` appends one JSON line per rule it evaluated: schema_version, seam, stage, rule, set, outcome (pass, refuse, bypass, unknown) and, on a bypass, by. No line carries the message, diff or refs a rule read. Unset, nothing is written. The file is opened before any rule runs, so a path that cannot be written, or a variable set to nothing, is exit 2 while the run has decided nothing. A rule that could not look writes its unknown line and ends the run as before. uphold records nothing itself: signing, storing and publishing the lines belong to whatever runs the gate. Documented under the guards in docs/REFERENCE.md. Implements #386 for the guard seam; the shim and hook seams follow. Claude-Session: https://claude.ai/code/session_01B8LzC9YfC4RyVQGgFPH2VP
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing: 5
✨ Finishing Touches
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. Comment |
Codecov Report❌ Patch coverage is
❌ Your patch status has failed because the patch coverage (97.97%) 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 #387 +/- ##
==========================================
- Coverage 94.75% 94.74% -0.02%
==========================================
Files 51 52 +1
Lines 26214 26304 +90
==========================================
+ Hits 24839 24921 +82
- Misses 1375 1383 +8 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
* Write guard verdicts as JUnit evidence to a caller-named file With UPHOLD_JUNIT set to an absolute path, `uphold guard --stage STAGE` writes one JUnit XML document: one testsuite for the stage, package "uphold", and one testcase per rule the stage evaluated. A pass is a plain testcase, a refusal a failure, a rule that could not look an error, and a bypass a skipped testcase whose `allowed` property says whether the id or `all` was named. The classname is the bundled set or `local`; the properties carry builtin, set, stage and the binary's version. No testcase carries the message, diff, refs, hostname or user name a rule or a machine would give away. JUnit rather than a format of uphold's own, because the gate tier already writes, publishes and links JUnit under .test-evidence/, so a publisher reads a guard run with nothing new to parse. That replaces the JSON Lines record UPHOLD_VERDICTS wrote (#387), which never shipped in a release: src/verdicts.rs is gone with Sink::from_env, Sink::record, Outcome::name and the tests a_bypass_names_what_switched_the_rule_off_and_a_pass_does_not, a_sink_nobody_asked_for_writes_nothing and the helper written; from tests/guard_cli.rs go guard_with_verdicts, verdict_lines, outcome_of, verdicts_name_each_rules_answer_and_never_the_subject, a_verdicts_file_that_cannot_be_opened_stops_the_run_before_it_decides and an_empty_verdicts_variable_is_refused_and_not_read_as_unset, each replaced by a JUnit case. The file is created, never appended, after the policy loads and before the first rule runs: an unwritable path, an empty value or a relative one is exit 2 with nothing judged, and a stage with no rules writes a suite of zero tests. A rule that could not look writes its error and ends the run as before. `guard --text` with the variable set is exit 2, since that seam learns only what refused. The shim and `uphold hook` do not read the variable. Closes #386 for the guard seam; the shim and hook seams follow. Claude-Session: https://claude.ai/code/session_017kpZecUt1NSs3cjhU54ZtY * Add --junit FILE as the flag spelling of UPHOLD_JUNIT A flag is the conventional way a tool names its JUnit file, as pytest's --junitxml is, and it is what a gate task types. The variable stays for the run whose command line the caller does not own: a hook's argv is the hook definition's, pinned by the repository, and the environment is the one channel that reaches from `git push` through the runner to the guard. The flag wins when both are given. A relative flag value means the caller's directory, as it does for any other command; the variable's absolute-path rule is unchanged, since a hook runs from the top of the working tree. An empty value is exit 2 under either spelling. Claude-Session: https://claude.ai/code/session_017kpZecUt1NSs3cjhU54ZtY
What
UPHOLD_VERDICTS=FILE uphold guard --stage STAGEappends one JSON line per rule the stage evaluated. Unset, nothing is written and the run is unchanged.{"schema_version":1,"seam":"guard","stage":"commit-msg","rule":"prevent-ai-author","outcome":"refuse"} {"schema_version":1,"seam":"guard","stage":"commit-msg","rule":"prevent-ai-author","outcome":"bypass","by":"UPHOLD_ALLOW"}outcomeispass,refuse,bypassorunknown.unknownand ends the run with exit 2, as before.seamis in the line now so the shim and hook seams can add lines without a schema bump.Why
uphold decides; recording, signing and publishing belong to whatever runs the gate. A recorder that already folds task outcomes from a caller-named file into a signed statement can take these lines the same way. A
--no-verifypush runs no hook, so it produces no lines, and the verifier sees the missing record.Tests
tests/guard_cli.rs: pass, refuse and bypass lines over two runs appended to one file, no subject text,byonly on a bypass; an unopenable path and an empty variable are exit 2 before any guard reports.src/verdicts.rs: line shape.cargo test, clippy--all-targets,cargo fmt --checkanduphold scanpass locally.Implements #386 for the guard seam.
https://claude.ai/code/session_01B8LzC9YfC4RyVQGgFPH2VP