docs: record what guard.rule names and when it is absent - #83
Merged
Conversation
GuardInfo.rule was documented as "absent when the policy default decided". Policy::classify writes the literal string "default" in both default arms, so the parenthetical inverted the encoding: a consumer would have read "default" as a rule of that name matching, and absence as the default deciding. The README corrected in #66 was left as the only place stating the truth. Fix the documentation, not the behaviour. The literal "default" is strictly more informative than absence, which could not also express "no single rule decided the call"; re-encoding it would be a record schema change and belongs with the v2 work in #34. Record that decision in DESIGN.md's audit-stream decisions list, where a reader auditing the schema will find it, rather than only in a field comment. Document what the field really carries: the four synthetic names the guard emits beside the operator's own, that nothing reserves those spellings so a policy may define a rule called "default", and the six cases where the field genuinely is absent — the issue named three and missed the create gate, an unassessable id, and the attachment paths. A tool removed from the router (I13) is a separate case again: it records no guard object at all. Pin it with two audit-record tests: a default-decided call records "default" on the deny and allow sides alike, and a clean search records no rule even though a named rule granted every row it served. The clean path is load-bearing — with drops, note_scan's strict upgrade clears the rule regardless, so a dropping search cannot observe the property. Closes #67
Closed
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #67.
GuardInfo.rulewas documented as "absent when the policy default decided".Policy::classifywrites the literal"default"in both default arms (policy.rs:897,:901), so the parenthetical inverted the encoding — a consumer would read"default"as a rule of that name matching, and absence as the default deciding. The README corrected in #66 was left as the only place stating the truth.The decision
Doc fix, not behaviour fix. The literal
"default"stays. It is strictly more informative than absence, which cannot also express "no single rule decided the call". Re-encoding a default decision as absence would be a record-schema change and is deferred to the audit schema v2 work in #34; schema v1 records"default".Per AGENTS.md, that decision is recorded in
DESIGN.md's audit-stream decisions list — parallel to the #28 and #29 bullets — not only in a field comment, since #67 was filed precisely becauserule: "default"looked like an accident.What the documentation now says
"default", never an absent field."default","min_bug_age_days","<rule>:unreadable-metadata","unavailable". The:unreadable-metadatasuffix is scoped to a granting rule — an undecidable deny rule keeps its plain name, having denied for its own reason (policy.rs:886-891).Policy::validateperforms no rule-name check at all, so a policy may define a rule literally calleddefault; the field names what decided, it does not prove which kind of thing it was.Access, and the attachment withhold plus its bug-0 padding assessment.guardobject at all, already pinned byaudit_wiremock.rs:520.Tests
Two audit-record-level tests, plus a rename of
cell_upgrade_by_a_default_decision_clears_the_rule→cell_upgrade_by_a_rule_less_verdict_clears_the_rule(assertions byte-identical; the old name described a rule-less verdict, not a policy-default decision).The search test uses a clean scan deliberately. With drops,
note_scanmergesServedFiltered/rule: None— a strict rank upgrade that clears the rule unconditionally — so a dropping search cannot observe the property. A clean scan underdefault_action = "deny"with a named rule granting every row is where absence is genuinely attributable.Adversarial review before opening
Three independent reviewers attacked the uncommitted diff — source-truth, invariants, and convention lenses. Two returned NOT MERGE-SAFE. Findings and resolutions:
audit_wiremock.rs:520already pinsguard.is_none(). The accuracy PR introducing a false claim.server.rs:1996to note a"MUTANT-RULE"left the whole 418-test workspace green.note_scanwithdropped > 0was forcing theNone.DESIGN.mdbooked it as coverage that did not exist.left: Some("MUTANT-RULE"),right: None) in a scratch copy outside the repo.audit.rsshrank to a reason plus a citation.README.md:620contradicted the new rustdoc twice — an absence list reading as exhaustive while missing three verified categories, and:unreadable-metadataattributed to any undecidable rule rather than a granting one.Five non-blocking findings also applied: the
Access::Grantedrustdoc asymmetry, changelog voice in reference docs, thequicksearch_test-name prefix,.as_deref()in an assertion, and a doc-link fromnote_verdictto the full case list.Confirmed correct under refutation and left alone: the synthetic-name list is complete and exactly spelled; the "nothing reserves the name" warning was proven by a scratch binary built against the real
Policy::from_toml_str; the merge description is exact at every boundary; the default-decision test is non-vacuous (twopolicy.rsmutations each killed it).Verification
cargo fmt --check,cargo clippy --workspace --all-targets -- -D warnings,cargo clippy -p bugwarden --features gen --all-targets -- -D warnings,cargo test --workspace --all-targets --locked(12 suites, 418 tests, 0 failures), andcargo deny checkall pass — re-run independently, not taken on report.cargo doc --no-deps --workspaceemits the same 4 pre-existing warnings asmain, none new.Documentation-only outside the tests: no runtime behaviour, public signature, serde attribute, default, or
SCHEMA_VERSIONchanges.Follow-up
Nothing reserves the synthetic rule names, so an operator can make audit records ambiguous by naming a rule
default. Documented here, not fixed —validate()rejecting the reserved spellings would be a behaviour change. Filing separately.