Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,39 @@ anc . --binary

# Source checks only (no binary execution)
anc . --source

# Suppress inapplicable MUSTs for a categorical exception
anc . --audit-profile human-tui
```

Bare `anc` (no arguments) prints help and exits 2. This is a non-negotiable fork-bomb guard: when agentnative dogfoods
itself, children spawned without arguments must not recurse into `check .`.

## Agent-facing JSON surface

`anc check <target> --output json` emits a `schema_version: "1.1"` scorecard. Four fields are additive to v1.1 and v1.1
consumers feature-detect them:

- `audience` — `"agent-optimized"` / `"mixed"` / `"human-primary"` / `null`. Derived from 4 signal behavioral checks
(`p1-non-interactive`, `p2-json-output`, `p7-quiet`, `p6-no-color-behavioral`). Informational only; never gates totals
or exit codes.
- `audience_reason` — present only when `audience` is `null`. Values: `"suppressed"` (signal check masked by
`--audit-profile`) or `"insufficient_signal"` (signal check never produced). Tells an agent *why* there's no label.
- `audit_profile` — echoes the applied `--audit-profile <category>` flag value. `null` when no profile is set.
- `coverage_summary.{must,should,may}.verified` — requirements verified by a check that actually ran. Checks suppressed
by `--audit-profile` do not count as verified; suppression means verification was intentionally skipped.

`--audit-profile` accepts exactly 4 values: `human-tui`, `file-traversal`, `posix-utility`, `diagnostic-only`.
Unknown values exit 2 with a structured error. The full per-category mapping of suppressed check IDs is committed to
`coverage/matrix.json` under the `audit_profiles` section — agents should read that file rather than scraping `--help`:

```bash
jaq '.audit_profiles' coverage/matrix.json
```

Suppressed checks appear in `results[]` as `status: "skip"` with evidence starting with `"suppressed by audit_profile:
"` (the shared prefix is pinned in `src/principles/registry.rs` as `SUPPRESSION_EVIDENCE_PREFIX`).

## Exit Codes

- `0` — all checks passed
Expand Down
15 changes: 11 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,18 @@ Key decisions already made:
Most source checks follow this structure (a few legacy helpers in `output_module.rs` and `error_types.rs` use
different helper shapes but still satisfy the core contract that `run()` is the sole `CheckResult` constructor):

- **Struct** implements `Check` trait with `id()`, `group()`, `layer()`, `applicable()`, `run()`
- **Struct** implements `Check` trait with `id()`, `label()`, `group()`, `layer()`, `applicable()`, `run()`
- **`check_x()` helper** takes `(source: &str)` (or `(source: &str, file: &str)` when evidence needs file location
context) and returns `CheckStatus` (not `CheckResult`) — this is the unit-testable core
- **`run()` is the sole `CheckResult` constructor** — uses `self.id()`, `self.group()`, `self.layer()` to build the
result. Never hardcode ID/group/layer string literals in `check_x()` or anywhere outside `run()`
- **No `Check` impl constructs `CheckResult` outside its own `run()`.** `run()` is the sole place each check assembles
its own result — never hardcode ID/group/layer/label string literals in `check_x()` or anywhere outside `run()`. The
runtime layer (`main::run`'s error and `--audit-profile` suppression branches) legitimately constructs `CheckResult`
as a *second* site — it's the runner, not a `Check` impl, and it uses `check.id()`, `check.label()`, `check.group()`,
`check.layer()` from the trait (never string literals). Test doubles (`FakeCheck` in `src/principles/matrix.rs` and
`src/scorecard/mod.rs`) similarly sidestep the rule by design.
- **`label()` returns `&'static str`** and feeds the `label` field in `run()`'s `CheckResult`. Having the label on the
trait also means the suppression and error branches can show the human label instead of falling back to the opaque
`id`. See `src/check.rs`.
- **Tests call `check_x()`** and match on `CheckStatus` directly, not `result.status`

This prevents ID triplication (the same string literal in `id()`, `run()`, and `check_x()`) and ensures the `Check`
Expand Down Expand Up @@ -139,7 +146,7 @@ deliberate commit, not a build-time artifact — the matrix is citable from outs
- `coverage_summary` — three-way `{must, should, may} × {total, verified}` counts, computed from the checks that
actually ran. Populated every run.
- `audience` — `Option<String>`, derived by `src/scorecard/audience.rs::classify()` from the 4 signal behavioral checks.
Emits `"agent_optimized"`, `"mixed"`, `"human_primary"`, or `null` when any signal check is missing from results
Emits `"agent-optimized"`, `"mixed"`, `"human-primary"`, or `null` when any signal check is missing from results
(including when suppressed by `--audit-profile`). The classifier is read-only over results and never gates totals or
exit codes — per CEO review Finding #3, label mismatches are fixed via registry, not classifier logic.
- `audit_profile` — `Option<String>`, echoes the applied `--audit-profile` flag value (`"human-tui"`,
Expand Down
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,15 +186,26 @@ Produces a scorecard (`schema_version: "1.1"`) with results, summary, and covera
"should": { "total": 16, "verified": 2 },
"may": { "total": 7, "verified": 0 }
},
"audience": null,
"audience": "agent-optimized",
"audit_profile": null
}
```

- `coverage_summary` — how many MUSTs/SHOULDs/MAYs the checks that ran actually verified, against the spec registry's
totals. See `docs/coverage-matrix.md` for the per-requirement breakdown.
- `audience` / `audit_profile` — reserved for v0.1.3 (audience classifier + `registry.yaml` suppression). Serialize as
`null` today; consumers should feature-detect.
totals. See `docs/coverage-matrix.md` for the per-requirement breakdown. Checks suppressed by `--audit-profile` do
**not** count toward `verified` — suppression means the requirement was not verified, even if the check is skipped
rather than run.
- `audience` — derived classification from 4 signal behavioral checks (`p1-non-interactive`, `p2-json-output`,
`p7-quiet`, `p6-no-color-behavioral`). Emits `agent-optimized` (0-1 Warns), `mixed` (2 Warns), or `human-primary` (3-4
Warns). Returns `null` when any signal check failed to run (source-only mode, missing runner, or `--audit-profile`
suppression). Informational only — never gates totals or exit codes. Values serialize as kebab-case to match
`audit_profile`'s format within the same JSON document.
- `audience_reason` — present only when `audience` is `null`. Values: `suppressed` (at least one signal check was masked
by `--audit-profile`) or `insufficient_signal` (signal check never produced, e.g. source-only run). Additive to schema
v1.1; v1.1 consumers feature-detect.
- `audit_profile` — echoes the applied `--audit-profile <category>` flag value (`human-tui`, `file-traversal`,
`posix-utility`, or `diagnostic-only`). `null` when no profile is set. See `coverage/matrix.json` under
`audit_profiles` for the committed per-category mapping of which check IDs each profile suppresses.

## Contributing

Expand Down
36 changes: 35 additions & 1 deletion coverage/matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -627,5 +627,39 @@
"total": 7,
"covered": 0
}
}
},
"audit_profiles": [
{
"name": "human-tui",
"description": "TUI-by-design tools (lazygit, k9s, btop). Interactive-prompt MUSTs suppressed; the TTY-driving contract is out of scope for verification.",
"suppresses": [
"p1-non-interactive",
"p1-flag-existence",
"p1-non-interactive-source",
"p1-tty-detection-source",
"p6-sigpipe"
]
},
{
"name": "file-traversal",
"description": "File-traversal utilities (fd, find). Subcommand-structure SHOULDs relaxed; these tools have no subcommands by design.",
"suppresses": []
},
{
"name": "posix-utility",
"description": "POSIX utilities (cat, sed, awk). Stdin-as-primary-input is their contract; P1 interactive-prompt MUSTs satisfied vacuously.",
"suppresses": [
"p1-non-interactive",
"p1-flag-existence",
"p1-non-interactive-source"
]
},
{
"name": "diagnostic-only",
"description": "Diagnostic tools (nvidia-smi, vmstat). No write operations, so the P5 mutation-boundary MUSTs do not apply.",
"suppresses": [
"p5-dry-run"
]
}
]
}
9 changes: 9 additions & 0 deletions src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ pub trait Check {
/// Unique identifier for this check (e.g., "code-unwrap", "p3-help").
fn id(&self) -> &str;

/// Human-readable one-line label. Surfaces in scorecard JSON
/// (`results[].label`) and text output. Every `run()` implementation
/// must hand this same string to `CheckResult`, and the suppression
/// and error branches in `main::run` use it so that a check
/// short-circuited by `--audit-profile` or an internal error
/// produces the same label the user would see on a successful run —
/// rather than falling back to the opaque `id`.
fn label(&self) -> &'static str;

/// Which principle or category this check belongs to.
fn group(&self) -> CheckGroup;

Expand Down
6 changes: 5 additions & 1 deletion src/checks/behavioral/bad_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ impl Check for BadArgsCheck {
"p4-bad-args"
}

fn label(&self) -> &'static str {
"Rejects invalid arguments"
}

fn group(&self) -> CheckGroup {
CheckGroup::P4
}
Expand Down Expand Up @@ -46,7 +50,7 @@ impl Check for BadArgsCheck {

Ok(CheckResult {
id: self.id().to_string(),
label: "Rejects invalid arguments".into(),
label: self.label().into(),
group: CheckGroup::P4,
layer: CheckLayer::Behavioral,
status,
Expand Down
6 changes: 5 additions & 1 deletion src/checks/behavioral/env_hints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ impl Check for EnvHintsCheck {
"p1-env-hints"
}

fn label(&self) -> &'static str {
"Flags advertise env-var bindings in --help"
}

fn group(&self) -> CheckGroup {
CheckGroup::P1
}
Expand All @@ -44,7 +48,7 @@ impl Check for EnvHintsCheck {

Ok(CheckResult {
id: self.id().to_string(),
label: "Flags advertise env-var bindings in --help".into(),
label: self.label().into(),
group: self.group(),
layer: self.layer(),
status,
Expand Down
8 changes: 6 additions & 2 deletions src/checks/behavioral/flag_existence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ impl Check for FlagExistenceCheck {
"p1-flag-existence"
}

fn label(&self) -> &'static str {
"Non-interactive gate flag advertised in --help"
}

fn group(&self) -> CheckGroup {
CheckGroup::P1
}
Expand Down Expand Up @@ -72,7 +76,7 @@ impl Check for FlagExistenceCheck {
if help_on_bare || stdin_clean_exit {
return Ok(CheckResult {
id: self.id().to_string(),
label: "Non-interactive gate flag advertised in --help".into(),
label: self.label().into(),
group: self.group(),
layer: self.layer(),
status: CheckStatus::Skip(
Expand Down Expand Up @@ -104,7 +108,7 @@ impl Check for FlagExistenceCheck {

Ok(CheckResult {
id: self.id().to_string(),
label: "Non-interactive gate flag advertised in --help".into(),
label: self.label().into(),
group: self.group(),
layer: self.layer(),
status,
Expand Down
6 changes: 5 additions & 1 deletion src/checks/behavioral/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ impl Check for HelpCheck {
"p3-help"
}

fn label(&self) -> &'static str {
"Help flag produces useful output"
}

fn group(&self) -> CheckGroup {
CheckGroup::P3
}
Expand Down Expand Up @@ -55,7 +59,7 @@ impl Check for HelpCheck {

Ok(CheckResult {
id: self.id().to_string(),
label: "Help flag produces useful output".into(),
label: self.label().into(),
group: CheckGroup::P3,
layer: CheckLayer::Behavioral,
status,
Expand Down
6 changes: 5 additions & 1 deletion src/checks/behavioral/json_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ impl Check for JsonOutputCheck {
"p2-json-output"
}

fn label(&self) -> &'static str {
"Structured output support"
}

fn group(&self) -> CheckGroup {
CheckGroup::P2
}
Expand Down Expand Up @@ -51,7 +55,7 @@ impl Check for JsonOutputCheck {

Ok(CheckResult {
id: self.id().to_string(),
label: "Structured output support".into(),
label: self.label().into(),
group: CheckGroup::P2,
layer: CheckLayer::Behavioral,
status,
Expand Down
10 changes: 7 additions & 3 deletions src/checks/behavioral/no_color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ impl Check for NoColorBehavioralCheck {
"p6-no-color-behavioral"
}

fn label(&self) -> &'static str {
"Respects NO_COLOR"
}

fn group(&self) -> CheckGroup {
CheckGroup::P6
}
Expand Down Expand Up @@ -47,9 +51,9 @@ impl Check for NoColorBehavioralCheck {

Ok(CheckResult {
id: self.id().to_string(),
label: "Respects NO_COLOR".into(),
group: CheckGroup::P6,
layer: CheckLayer::Behavioral,
label: self.label().into(),
group: self.group(),
layer: self.layer(),
status,
confidence: Confidence::High,
})
Expand Down
6 changes: 5 additions & 1 deletion src/checks/behavioral/no_pager_behavioral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ impl Check for NoPagerBehavioralCheck {
"p6-no-pager-behavioral"
}

fn label(&self) -> &'static str {
"Pager-using CLI ships --no-pager escape hatch"
}

fn group(&self) -> CheckGroup {
CheckGroup::P6
}
Expand All @@ -48,7 +52,7 @@ impl Check for NoPagerBehavioralCheck {

Ok(CheckResult {
id: self.id().to_string(),
label: "Pager-using CLI ships --no-pager escape hatch".into(),
label: self.label().into(),
group: self.group(),
layer: self.layer(),
status,
Expand Down
6 changes: 5 additions & 1 deletion src/checks/behavioral/non_interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ impl Check for NonInteractiveCheck {
"p1-non-interactive"
}

fn label(&self) -> &'static str {
"Non-interactive by default"
}

fn group(&self) -> CheckGroup {
CheckGroup::P1
}
Expand Down Expand Up @@ -98,7 +102,7 @@ impl Check for NonInteractiveCheck {

Ok(CheckResult {
id: self.id().to_string(),
label: "Non-interactive by default".into(),
label: self.label().into(),
group: CheckGroup::P1,
layer: CheckLayer::Behavioral,
status,
Expand Down
6 changes: 5 additions & 1 deletion src/checks/behavioral/quiet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ impl Check for QuietCheck {
"p7-quiet"
}

fn label(&self) -> &'static str {
"Quiet mode available"
}

fn group(&self) -> CheckGroup {
CheckGroup::P7
}
Expand Down Expand Up @@ -44,7 +48,7 @@ impl Check for QuietCheck {

Ok(CheckResult {
id: self.id().to_string(),
label: "Quiet mode available".into(),
label: self.label().into(),
group: CheckGroup::P7,
layer: CheckLayer::Behavioral,
status,
Expand Down
6 changes: 5 additions & 1 deletion src/checks/behavioral/sigpipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ impl Check for SigpipeCheck {
"p6-sigpipe"
}

fn label(&self) -> &'static str {
"Handles SIGPIPE gracefully"
}

fn group(&self) -> CheckGroup {
CheckGroup::P6
}
Expand Down Expand Up @@ -40,7 +44,7 @@ impl Check for SigpipeCheck {

Ok(CheckResult {
id: self.id().to_string(),
label: "Handles SIGPIPE gracefully".into(),
label: self.label().into(),
group: CheckGroup::P6,
layer: CheckLayer::Behavioral,
status,
Expand Down
6 changes: 5 additions & 1 deletion src/checks/behavioral/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ impl Check for VersionCheck {
"p3-version"
}

fn label(&self) -> &'static str {
"Version flag works"
}

fn group(&self) -> CheckGroup {
CheckGroup::P3
}
Expand Down Expand Up @@ -42,7 +46,7 @@ impl Check for VersionCheck {

Ok(CheckResult {
id: self.id().to_string(),
label: "Version flag works".into(),
label: self.label().into(),
group: CheckGroup::P3,
layer: CheckLayer::Behavioral,
status,
Expand Down
Loading