Skip to content

feat(scorecard): 7-status taxonomy, per-row emission, antecedent propagation (schema 0.6) - #62

Merged
brettdavies merged 6 commits into
devfrom
feat/u2-7-status-emission
May 28, 2026
Merged

feat(scorecard): 7-status taxonomy, per-row emission, antecedent propagation (schema 0.6)#62
brettdavies merged 6 commits into
devfrom
feat/u2-7-status-emission

Conversation

@brettdavies

@brettdavies brettdavies commented May 27, 2026

Copy link
Copy Markdown
Owner

Summary

Ships U2 of the scorecard fairness taxonomy plan (docs/plans/2026-05-21-001-feat-scorecard-fairness-taxonomy-plan.md in the agentnative-site repo): the CLI now emits a 7-status taxonomy with per-row results, antecedent propagation for conditional requirements, and a bumped scorecard schema. The vendored spec snaps to agentnative-spec dev at commit b4f4d02 (PR brettdavies/agentnative#34, U1) so the new applicability.kind: conditional / antecedent.check_id shape parses against five conditional rows in p2 and p8.

The status enum gains opt_out (deliberate non-adoption) and n_a (conditional antecedent unmet); the existing five values are preserved. Summary gains matching counters. Every entry in results[] now represents one requirement row instead of one probe, carries the row's RFC 2119 tier (must/should/may), and names the originating probe in check_id. A probe whose Check::covers() lists multiple rows (e.g., p3-versionp3-must-version + p3-should-version-short) now produces multiple result entries. Two probes (p8-bundle-exists and p2-json-output) emit opt_out directly when their feature is absent; antecedent propagation rewrites every conditional row whose antecedent collapses to opt_out / n_a so downstream MUSTs and MAYs stop double-counting against tools that deliberately do not ship the prerequisite feature.

The badge score formula keeps its pass / (pass + warn + fail) shape but extends the exclusion set to cover the new statuses: opt_out is excluded from the denominator and n_a is excluded from both sides, per the plan's transitional posture (the final tier-weighted formula is the U3 spec issue, after the disambiguated input has been rescored). Text mode renders the new statuses as OPT and N/A badges; the summary line shows all seven counters.

Three parser strictness fixes the red team surfaced harden the build-time spec gate: whitespace-only check_id is rejected, mixed legacy if: plus new kind: in one applicability block now errors (instead of silently dropping the legacy prose), and any key inside antecedent beyond check_id errors by name so v2 compound-antecedent syntax (op: any_of) cannot ship under v1 semantics.

The Cargo.toml version and CHANGELOG.md are unchanged; both are release-PR artifacts per RELEASES.md.

Changelog

Added

  • New opt_out and n_a scorecard statuses surface in anc audit --output json (status field on each row and matching counters in summary). opt_out marks deliberate non-adoption (tool ships no --output flag, no AGENTS.md bundle); n_a marks a conditional requirement whose antecedent is unmet. Pre-0.6 consumers treat both as unknown and feature-detect.
  • Each entry in results[] now carries a tier field (must / should / may, or null for rows not in the registry) and a check_id field naming the probe that produced the row.
  • anc emit schema returns the schema 0.6 contract ($id: https://anc.dev/scorecard-v0.6.schema.json) with the new status enum values, summary counters, and per-row fields.

Changed

  • anc audit --output json emits one result entry per requirement-row instead of one per check_id. A probe like p3-version (covers p3-must-version and p3-should-version-short) now produces two distinct entries, each tier-stamped, so downstream scoring layers no longer need a coverage-matrix join to attribute a probe's outcome to a specific RFC 2119 level.
  • Conditional requirement rows whose antecedent collapses to opt_out or n_a are propagated to n_a in results[]; rows whose antecedent is skip or error inherit the indeterminacy. The propagated evidence string names the antecedent check id so the chain is legible from the JSON alone.
  • The badge denominator excludes opt_out (transitional) and excludes n_a from both sides, matching the plan's posture that no formula is provably fair until the input shape is disambiguated.
  • anc audit text mode renders OPT and N/A status badges alongside the existing five, and the summary line reports all seven counters.
  • p8-bundle-exists emits opt_out when no top-level AGENTS.md or SKILL.md is found (a malformed bundle still emits warn); p2-json-output emits opt_out when no --output or --format flag is detected at top level or in any subcommand.
  • The vendored agentnative-spec tree updates to dev commit b4f4d02 (PR feat(applicability): add machine-checkable conditional shape agentnative#34). Five rows in p2 and p8 migrate to the new applicability.kind: conditional / antecedent.check_id shape; the remaining 18 legacy applicability.if: <prose> rows stay as-is until each prerequisite grows a machine-readable check id.

Fixed

  • The spec parser now rejects three malformed inputs that previously fell through silently: antecedent.check_id containing only whitespace, an applicability block carrying both legacy if: and new kind: (the legacy branch only fired for single-key maps, so the prose was being dropped on the floor), and any key inside antecedent other than check_id (compound antecedents are deferred to v2 of the schema per the plan's Sub-decision 2b).

Documentation

  • schema/scorecard.schema.json regenerates against the 0.6 contract: new enum values, new required counters, tier and check_id on CheckResultView, and a refreshed examples[0] block.
  • coverage/matrix.json and docs/coverage-matrix.md regenerate against the new conditional applicability shape. Conditional rows surface with applicability.antecedent.check_id populated; legacy rows continue to emit applicability.condition: "<prose>".

Type of Change

  • feat: New feature (non-breaking change which adds functionality)

Related Issues/Stories

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing completed
  • All tests passing

Test Summary:

  • Unit and integration tests: 786 passing (29 net-new across parser, registry, scorecard, schema drift)
  • Pre-push gate: fmt, clippy -Dwarnings, full test suite, cargo-deny, shellcheck, Windows compat. All green.
  • Manual dogfood: anc audit . --output json produces a schema 0.6 scorecard with the self-target at the v0.4.0 baseline (no opt_out / n_a, since the repo ships both --output and AGENTS.md); anc audit --command echo --output json exercises the propagation chain end-to-end (summary.opt_out: 1 for p2-must-output-flag from direct emission, summary.n_a: 1 for p2-must-schema-print from antecedent propagation)
  • anc emit coverage-matrix --check clean (committed artifacts match the registry plus covers() declarations)

Files Modified

Modified:

  • build_support/parser.rs: New conditional applicability shape (kind: conditional + antecedent.check_id) parses alongside the legacy if: form; three strictness fixes for the malformed-input edge cases.
  • src/principles/registry.rs: Applicability::Conditional becomes a struct variant with condition and antecedent fields; new Antecedent type carries check_id. Two registry-consistency guards added.
  • src/principles/matrix.rs: Matrix renderer handles both the legacy condition and the new antecedent shape; matrix.json emits the new shape via additive serde fields.
  • src/types.rs: CheckStatus gains OptOut(String) and NotApplicable(String).
  • src/scorecard/mod.rs: SCHEMA_VERSION bumps to 0.6; new fan_out_per_row and propagate_antecedents helpers run inside build_scorecard; CheckResultView gains tier + check_id; Summary gains opt_out + n_a counters; score_pct, build_summary, format_text, format_text_raw, exit_code updated for the new variants. Twenty-seven new tests covering fan-out, propagation, score formula, summary invariants, serialization roundtrip.
  • src/checks/project/bundle_exists.rs: Emits OptOut when no bundle file is found; malformed bundle still emits Warn.
  • src/checks/behavioral/json_output.rs: Emits OptOut when no --output / --format flag is detected at top level or in any subcommand.
  • src/principles/spec/principles/p1-non-interactive-by-default.md through p8-discoverable-skill-bundle.md: Vendored at agentnative-spec dev SHA b4f4d02; five rows in p2 and p8 carry the new conditional shape.
  • schema/scorecard.schema.json: Schema 0.6 contract ($id, enum values, required counters, tier + check_id on results, refreshed example).
  • coverage/matrix.json and docs/coverage-matrix.md: Regenerated against the new applicability shape.
  • tests/integration.rs: Schema version assertion bumped to 0.6; the audit_profile diagnostic-only test looks up the per-row id p5-must-dry-run (with check_id: "p5-dry-run") under per-row emission.
  • tests/scorecard_metadata_security.rs: Schema version assertion bumped to 0.6.
  • tests/scorecard_schema_v05.rs: Schema version assertion bumped to 0.6; assert_v05_shape walks the new summary.opt_out / summary.n_a keys and every row's tier / check_id. Six schema-drift guards assert the committed schema agrees with the live scorecard.
  • tests/build_parser.rs: Eight parser red-team tests covering malformed antecedent shapes, whitespace-only check_id, mixed legacy plus new shape, smuggled v2 op: key, and a defense-in-depth emit_rust quote-escape test.

Created:

  • None.

Renamed:

  • None.

Deleted:

  • None.

Breaking Changes

  • No breaking changes

Schema 0.6 is an additive evolution of 0.5: every pre-0.6 key remains, the new keys are populated alongside the existing ones, and consumers that do not understand opt_out / n_a should treat them as skip (the conservative bucket, matching the contract in the schema's own description). The site renderer, leaderboard ingest, and any third-party agent consumer can ship their schema-0.6 support out-of-band without coordination.

The one shape change that warrants a heads-up: each entry in results[] is now keyed by requirement row id (e.g., p3-must-version) instead of probe id (e.g., p3-version). Consumers that pinned on the probe id should switch to the new check_id field on each row, which carries the probe provenance forward.

Deployment Notes

  • No special deployment steps required

The release PR will bump Cargo.toml and CHANGELOG.md. Downstream consumers (site renderer, Homebrew tap, registry rescore) consume the published artifact after the tag pushes; no out-of-band coordination needed.

Checklist

  • Code follows project conventions and style guidelines
  • Commit messages follow Conventional Commits
  • Self-review of code completed
  • Tests added/updated and passing
  • No new warnings or errors introduced
  • Changes are backward compatible (consumers feature-detect new keys)

Pin-vendors agentnative-spec at commit b4f4d02 (PR brettdavies/agentnative#34 squash-merge on dev) as the basis for U2 of the scorecard fairness taxonomy plan (docs/plans/2026-05-21-001-feat-scorecard-fairness-taxonomy-plan.md in the site repo).

The vendored tree carries U1: `applicability.kind: conditional` with `antecedent.check_id` shape on five requirement rows: `p2-must-schema-print` and `p2-should-schema-file` (antecedent `p2-json-output`), plus `p8-must-bundle-install`, `p8-may-install-all`, `p8-may-bundle-update` (antecedent `p8-bundle-exists`). Replaces the legacy `applicability.if: <prose>` shape on those rows. p8 prose re-expressed in if-X-then-Y construction.

VERSION stays at 0.4.0 by design because the spec-side VERSION bump is deferred to spec's release PR per U1's status notes. The build pipeline already reads the vendored tree at `src/principles/spec/` via `build.rs`; downstream U2 work consumes the new shape from there.

Resolved short SHA: b4f4d02 (printed by sync-spec.sh during vendoring; recorded here so the pin is traceable post-merge).
Extends the build-time spec parser, runtime registry, and matrix renderer to accept the new conditional applicability shape introduced by agentnative-spec PR #34 (vendored at b4f4d02 in the prior commit) while keeping the legacy `{ if: "<prose>" }` shape for the 18 spec rows that have not yet migrated.

Schema change (parsed YAML, runtime enum, matrix.json output):

- `Applicability::Conditional { condition: Option<String>, antecedent: Option<Antecedent> }` replaces the previous `Conditional(String)` tuple variant. At least one of `condition` or `antecedent` must be set; both is permitted (the new shape supplements the legacy prose with a machine-readable check id).
- `Antecedent { check_id }` is the v1 machine-readable form. Compound antecedents (`all_of` / `any_of`) are deferred to a future schema bump per plan Sub-decision 2b.
- The matrix.json row's `applicability` block uses `#[serde(skip_serializing_if = "Option::is_none")]` so legacy rows continue to emit `{ "kind": "conditional", "condition": "<prose>" }` and new rows emit `{ "kind": "conditional", "antecedent": { "check_id": "<id>" } }`. No site renderer changes required for the legacy path.

The parser rejects four error modes loudly: `kind:` values other than `conditional`; `antecedent:` mappings missing `check_id`; `kind: conditional` with neither `condition:` nor `antecedent:` set; `condition:` values that are not non-empty strings. Each error names the offending file, requirement id, and the malformed field so build failures are actionable.

This commit is the foundation for U2 of the scorecard fairness taxonomy plan (docs/plans/2026-05-21-001-feat-scorecard-fairness-taxonomy-plan.md in the site repo). Per-row emission, antecedent status propagation, and the schema 0.6 bump that consume this new shape land in the following commits on this branch.
Implements the bulk of U2 from the scorecard fairness taxonomy plan (docs/plans/2026-05-21-001-feat-scorecard-fairness-taxonomy-plan.md). Three changes ship together because they share the same pattern-match exhaustiveness contract on `CheckStatus`: the new variants, the per-row pipeline that emits them, and the test fixtures that pin the JSON shape.

**7-status taxonomy (Decision 1).** `CheckStatus` gains `OptOut(String)` for deliberate non-adoption and `NotApplicable(String)` for conditional rows whose antecedent is unmet. JSON serializes as `opt_out` and `n_a` respectively. Every match site (`build_summary`, `score_pct`, `exit_code`, `format_text`, `format_text_raw`, `CheckResultView::from_row`) now handles all seven variants exhaustively. The plan's transitional `score_pct` formula keeps the historic `pass / (pass + warn + fail)` shape and excludes opt_out from the denominator and n_a from both sides (per U2 work item: "minimal change that respects the new semantics without committing to a new formula"); the final formula is the U3 spec issue.

**Per-row emission (Decision 2c).** New `fan_out_per_row(raw, catalog)` walks each probe-level `CheckResult` and emits one row per requirement in `Check::covers()`, replacing the row `id` with the requirement id and carrying the probe's check id forward as provenance. A probe like `p3-version` (covers `p3-must-version` + `p3-should-version-short`) now produces two rows in `results[]`; a probe whose check is not yet wired into the registry passes through as a single row keyed by check id. `CheckResultView` gains `tier` (registry-looked-up `must` / `should` / `may`, or `null` for unknown ids) and `check_id` (probe provenance). The shape change drives the schema bump from 0.5 to 0.6; pre-0.6 consumers feature-detect.

**Antecedent propagation (Decision 2a).** New `propagate_antecedents(rows, raw)` reads each row's registry `Applicability` and applies the propagation table to conditional rows: `opt_out` / `n_a` antecedent collapses the consequent to `n_a`; `skip` and `error` inherit; `pass` / `warn` / `fail` leave the consequent untouched (the probe's own status stands). Evidence text on the rewritten row cites the antecedent check id and its status so the provenance is legible in the scorecard JSON.

`Summary` gains `opt_out` and `n_a` counters alongside the historic five so consumers can read the new buckets without scanning `results[]`. The badge derivation reads the per-row vector (post-propagation) so the embed URL the JSON emits and the post-summary text hint stay in lock-step. The audience classifier and coverage_summary continue to read raw probe results because signal classification keys on check ids and coverage counts requirements covered by the underlying probes.

Test updates pin the new shape: 14 new scorecard-module tests cover fan-out for single-row and multi-row probes, the full propagation table (pass/warn/fail/opt_out/n_a/skip/error antecedents), score_pct exclusions, summary counting, and the `tier` / `check_id` round-trip; integration tests update the suppression test to look up the row id under per-row emission; the schema_v05 drift guard adds 0.6 keys (summary counters, per-row `tier`/`check_id`) so consumers' shape contract is checked end-to-end.
…rtifacts

Wires the two probe-level cases where the 7-status taxonomy's `opt_out` carries clear semantics, then regenerates the committed schema artifacts so the JSON Schema, coverage matrix, and live scorecard agree on the shape introduced in the previous commits on this branch.

**Probe-level opt_out emissions.**

- `p8-bundle-exists` (project layer): the "no top-level `AGENTS.md` / `SKILL.md` found" branch now returns `OptOut` instead of `Warn`. A bundle that exists but is malformed (missing YAML frontmatter or `name:` field) is a real SHOULD violation and stays `Warn`. The opt_out signal feeds antecedent propagation: every conditional row whose antecedent is `p8-bundle-exists` (`p8-must-bundle-install`, `p8-may-install-all`, `p8-may-bundle-update`) collapses to `n_a` automatically when the project ships no bundle.
- `p2-json-output` (behavioral layer): the two "no `--output` / `--format` flag detected" branches (top-level help and per-subcommand probe) now return `OptOut`. The conditional rows `p2-must-schema-print` and `p2-should-schema-file` are antecedent-gated on `p2-json-output`, so a tool without structured output sees those rows propagate to `n_a` rather than counting twice against the score.

The probe-level changes are deliberately scoped to the two cases the plan calls out by name; other probes that currently emit `Skip` for "feature absent" are left as-is for follow-up work (the propagation table handles them correctly through whatever conditional antecedent they sit under, when the antecedent gets a machine-readable shape via `sync-spec`).

**Regenerated artifacts.**

- `schema/scorecard.schema.json` bumps `$id` to `https://anc.dev/scorecard-v0.6.schema.json`. The `CheckResultView` definition adds `tier` (enum of `must` / `should` / `may` / `null`) and `check_id`; the `Summary` definition adds `opt_out` and `n_a` counters; the `status` enum gains `opt_out` and `n_a` values; the example block updates to a per-row row with `tier` + `check_id` populated. The Summary's `required` list now names all seven counters.
- `coverage/matrix.json` and `docs/coverage-matrix.md` regenerate against the conditional-applicability schema landed in the spec-scaffolding commit. The five conditional rows in p2 and p8 surface with `applicability.kind: conditional` and `applicability.antecedent.check_id` populated; legacy rows continue to emit `applicability.condition: "<prose>"` until each prerequisite grows a check id in the verifier catalog.

Dogfood verification against `--command echo` (a CLI with no `--output` flag and no project context) produces a scorecard with `summary.opt_out: 1` for the `p2-must-output-flag` row (direct emission) and `summary.n_a: 1` for the `p2-must-schema-print` row (antecedent-propagated). Against the agentnative-cli repo itself (which has both `--output` and `AGENTS.md`), nothing routes through opt_out or n_a; the score stays at the v0.4.0 dogfood baseline.
Twenty-two adversarial tests across the four U2 surfaces, plus three parser strictness fixes the red team surfaced.

**Parser strictness fixes (`build_support/parser.rs`).** Three error modes that the original v1 parser accepted silently now hard-fail with a hint at the schema the author probably meant:

- `antecedent.check_id` is now trimmed before the non-empty check (whitespace-only strings were previously accepted as a valid id).
- Mixed legacy `if:` and new `kind:` in the same applicability block now errors. The legacy branch only fired for `map.len() == 1`, so the original parser silently dropped the `if:` prose when both keys were present, which would footgun any author mid-migration.
- Extra keys inside `antecedent` (beyond `check_id`) now error by name. Compound antecedents (`op: any_of | all_of`) are explicitly deferred to a future schema bump per plan Sub-decision 2b; silently ignoring v2 keys on a v1 row would let v2 syntax ship under v1 semantics.

**Parser red team (`tests/build_parser.rs`, +8 tests).** Whitespace-only `check_id`, `antecedent:` as a string, `antecedent:` as a list (the v2 compound shape), `kind: Conditional` with the wrong case, mixed legacy + new shape in one row, antecedent with `op:` smuggled alongside `check_id`, `antecedent: null`, empty antecedent mapping. Plus an `emit_rust` defense-in-depth test asserting that hostile characters in a `check_id` (quotes, backslashes) escape correctly in the generated Rust source.

**Registry consistency red team (`src/principles/registry.rs`, +2 tests).** Walks `REQUIREMENTS` and asserts every conditional row's `antecedent.check_id` resolves to a real check in `all_checks_catalog()`, because a typo or rename would silently mute propagation in production. A second test asserts no conditional row names its own covering check as its antecedent (the "self-gate" edge case where a row would gate its status against itself).

**Propagation red team (`src/scorecard/mod.rs`, +5 tests).** Idempotency (a second pass produces identical output), no-op when the antecedent didn't run (source-only or filtered run), `--audit-profile` suppression of the antecedent propagates as Skip with the suppression reason preserved, full pipeline end-to-end (fan-out → propagation → summary → score with opt_out + n_a counted separately).

**Score formula red team (`src/scorecard/mod.rs`, +4 tests).** Score is 0 with no panic when every row is opt_out or n_a (division-by-zero guard). One pass amid 999 n_a rows is 100% (n_a excluded from both numerator and denominator). Skip + Error continue to be excluded under the new taxonomy. `summary.total` equals the sum of all seven per-status counters (catches a future status variant added without updating `build_summary`).

**Serialization red team (`src/scorecard/mod.rs`, +2 tests).** Evidence strings containing quotes, backslashes, control chars (`\n`, `\t`, `\u{0007}`) and Unicode (zero-width joiner, RTL override) round-trip through serde → parse → assert without loss or escape corruption. Sanitization belongs at the render layer, not here; the scorecard's job is faithful pass-through.

**Schema drift red team (`tests/scorecard_schema_v05.rs`, +6 tests).** The committed `schema/scorecard.schema.json` is the consumer contract for the site renderer and third-party leaderboards. Tests assert: `$id` pins to the current `SCHEMA_VERSION` (0.6); the `status` enum lists all seven taxonomy values; `Summary.required` includes `opt_out` + `n_a`; `CheckResultView` includes `tier` (with the three RFC 2119 levels plus null) + `check_id`; the schema's own `examples[0]` block satisfies its own `required` lists (catches doc-vs-schema drift); a live scorecard from a real `anc audit` run carries every key in the top-level and per-row `required` lists.

The single supporting production change beyond the parser strictness is `#[derive(Debug)]` on `Summary` so red-team assertions can surface counter mismatches inline rather than panicking with a useless message.
…st-revised dates

Pulls the date-discipline fix from agentnative-spec PR #38 (`fix(principles): enforce last-revised discipline + backfill stale dates`). Seven principle files refresh their `last-revised` frontmatter to match the date their content actually changed; p3 was already correct and stays as-is.

The applicability semantics already vendored at `b4f4d02` (U1 of the scorecard fairness taxonomy plan) carry through unchanged; this is a frontmatter-only update. VERSION still reads `0.4.0` because the upstream spec defers the cut to a release PR; the resync is faithful.

No source changes here; the regenerated `REQUIREMENTS` slice via `build.rs` is byte-identical to the prior build because the parser does not consume `last-revised`.

Resolved short SHA: 1fb49fe (printed by sync-spec.sh during vendoring).
@brettdavies
brettdavies merged commit 3839696 into dev May 28, 2026
8 checks passed
@brettdavies
brettdavies deleted the feat/u2-7-status-emission branch May 28, 2026 01:59
brettdavies added a commit that referenced this pull request May 28, 2026
## Summary

The `--output text` view, its badge, and the process exit code were
computed from raw probe results, while `--output json` already ran the
schema-0.6 per-row + antecedent-propagation pipeline. So the terminal
printed probe ids (`p2-schema-print`) instead of requirement-row ids
(`p2-must-schema-print`), could never render `[N/A ]` (only propagation
produces `n_a`), and its row count, badge score, and exit code could
disagree with the JSON the site and scorer consume. A human auditing
`bat` saw `[FAIL] … (p2-schema-print)` for a MUST the tool is actually
exempt from.

This routes all three text-side surfaces through the same pipeline. The
fan-out + propagation pair that `build_scorecard` inlined is extracted
into `build_row_results`, and `main::run` derives one per-row set that
feeds `format_text`, `compute_badge`, and `exit_code`. Text and JSON now
agree on the row set, counts, badge score, and exit code. The JSON path
is byte-identical and the schema stays `0.6`.

## Changelog

### Changed

- `anc audit` (text mode) now reports the same requirement rows as
`--output json`: requirement-row ids (`p2-must-schema-print`) instead of
probe ids, with the requirement tier (`must`/`should`/`may`) shown on
each row.
- The process exit code now reflects the per-row result set in both
output modes. A requirement that collapses to `n_a` because its
conditional prerequisite is `opt_out` no longer forces a non-zero exit;
only a live `Fail`/`Error` exits `2` and a live `Warn` exits `1`.

### Fixed

- Text mode now renders `[N/A ]` (with antecedent evidence) for
conditional requirements whose prerequisite was opted out, instead of
showing a misleading `[FAIL]` on the probe id. Text row count and badge
score now match `--output json`.

## Type of Change

- [x] `fix`: Bug fix (non-breaking change which fixes an issue)

## Related Issues/Stories

- Story: n/a
- Issue: n/a
- Architecture:
`docs/plans/2026-05-27-002-fix-text-renderer-per-row-parity-plan.md`
- Related PRs: #62 (per-row pipeline + schema 0.6, JSON path only)

## Testing

- [x] Unit tests added/updated
- [x] Integration tests added/updated
- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

- New unit tests: `build_row_results_fans_out_then_propagates`,
`exit_code_drops_to_zero_when_consequent_propagates_to_n_a`,
`text_and_json_agree_on_a_propagated_conditional_row` (bat-shaped
fixture asserting consequent shows `n_a` not `fail`, row counts match,
badge scores match).
- New integration test:
`test_text_and_json_agree_on_row_count_and_exit_code` audits `ls` in
both modes and pins text row count == JSON `results.len()` and exit-code
parity, catching any revert that feeds the text renderer raw probe
results.
- Existing `format_text_*` / `format_text_raw` / R4 audit-profile
masking tests pass unchanged.

## Files Modified

**Modified:**

- `src/main.rs`: derive one per-row projection after the principle
filter; feed it to `format_text`, `compute_badge`, and `exit_code`.
- `src/scorecard/mod.rs`: add `build_row_results`; `build_scorecard`
calls it; `format_text` renders the tier suffix; consolidate the
`Level`→token mapping behind `Level::as_str`.
- `src/principles/registry.rs`: add `Level::as_str` as the single source
of truth for the `must`/`should`/`may` token.
- `tests/integration.rs`: text/JSON row-count + exit-code parity guard.
- `docs/plans/2026-05-27-002-fix-text-renderer-per-row-parity-plan.md`:
status `proposed` → `completed`.

**Created:**

- None.

**Renamed:**

- None.

**Deleted:**

- None.

## Breaking Changes

- [x] No breaking changes

The CLI surface and JSON schema are unchanged. The exit-code semantics
shift for one edge: a tool that raw-Fails a check whose requirement row
propagates to `n_a` now exits `0`/`1` instead of `2`. That is the
corrected behavior (the requirement does not apply), it matches what the
JSON `summary` already reported, and `anc` is `0.x` pre-launch. The
`--audit-profile` masking path (plan R4) is independent and unchanged.

## Deployment Notes

- [x] No special deployment steps required

## Post-Deploy Monitoring & Validation

No additional operational monitoring is required because `anc` is a
local CLI with no runtime service. The only downstream consumer is
`agentnative-site`, which reads the JSON scorecard; the JSON path is
byte-identical (schema stays `0.6`), so there is no consumer impact.
Spot-check after merge: `anc audit --command bat` (text) shows `[N/A ] …
(p2-must-schema-print) (must)` and no `[FAIL] … (p2-schema-print)`, and
`anc audit --command bat` / `--output json` agree on exit code.

## Checklist

- [x] Code follows project conventions and style guidelines
- [x] Commit messages follow [Conventional
Commits](https://www.conventionalcommits.org/)
- [x] Self-review of code completed
- [x] Tests added/updated and passing
- [x] No new warnings or errors introduced
- [x] Changes are backward compatible (or breaking changes documented)
brettdavies added a commit that referenced this pull request May 29, 2026
#64)

## Summary

Implements U3b of the scorecard fairness taxonomy plan: the CLI now
computes the final leaderboard score defined in `agentnative-spec`
`principles/scoring.md` (authority: `agentnative-spec@972c9d3`),
replacing the transitional formula shipped in U2.

The score now reflects shipped-binary behavior only. `score_pct` is
restricted to behavioral-layer rows; source- and project-layer checks
still emit in the scorecard but no longer move the score or badge
eligibility. Over the denominator set `D = {pass, warn, fail, opt_out}`,
the score is a credit-weighted ratio (`pass` 1.0, `warn` 0.5, `fail` and
`opt_out` 0.0) with `n_a` / `skip` / `error` excluded from `D`.
`opt_out` now counts in the denominator so deliberate non-adoption is
reflected. Tier weights are published flat (MUST = SHOULD = MAY = 1) but
encoded as named constants in the general `Σ w·credit / Σ w` form, so a
future non-flat re-tune is a one-line change. The badge eligibility
floor drops from 80% to 70%.

A consequence of the behavioral-only scope: `anc audit . --source` now
scores 0% and ineligible by design, because a source-only run has no
behavioral rows to score. anc dogfoods to 100% in `--binary` and full
mode (34 of 34 behavioral checks pass; 9 behavioral skips are excluded
from the denominator).

`Cargo.toml` version, `CHANGELOG.md`, and the tag publish are deferred
to the release PR that closes out this unit, per the plan.

## Changelog

### Changed

- The leaderboard score now reflects shipped-binary behavior only:
source- and project-layer checks no longer affect `score_pct` or badge
eligibility.
- `score_pct` is now credit-weighted: `warn` earns half credit and
`opt_out` counts against the score, replacing the prior pass-only ratio.
- Lower the agent-native badge eligibility floor from 80% to 70%.

## Type of Change

- [x] `feat`: New feature (non-breaking change which adds functionality)

## Related Issues/Stories

- Story: U3b in
`agentnative-site/docs/plans/2026-05-21-001-feat-scorecard-fairness-taxonomy-plan.md`
- Issue: None.
- Architecture: Formula authority is `agentnative-spec@972c9d3`
`principles/scoring.md`.
- Related PRs: brettdavies/agentnative#39 (spec formula, U3);
#62 (7-status taxonomy, U2).

## Testing

- [x] Unit tests added/updated
- [x] All tests passing

**Test Summary:**

- Unit + integration tests: 793 passing, 2 ignored, across 7 suites.
- Added: warn half-credit, behavioral-only exclusion, and the
`scoring.md` worked example (20 pass / 7 warn / 0 fail / 1 opt_out / 1
n_a / 14 skip → 84). Reworked the at-floor (now 70%),
opt_out-in-denominator, and full-pipeline propagation cases.
- `cargo fmt --check`, `cargo clippy --all-targets --all-features -D
warnings`, and `cargo test` all green.

## Files Modified

**Modified:**

- `src/scorecard/mod.rs`: rewrote `score_pct` (behavioral-only,
credit-weighted, tier-weighted); added `tier_weight` and `status_credit`
helpers; lowered `BADGE_ELIGIBILITY_FLOOR_PCT` to 70; updated badge
docstrings and tests.
- `README.md`: rewrote the Scoring section (7 statuses, credit formula,
behavioral-only scope, 70% floor, cohort bands), the dogfood table, and
the badge field reference.
- `CLAUDE.md`: updated the scorecard `badge` reference to the new
formula.

**Created:**

- None.

**Renamed:**

- None.

**Deleted:**

- None.

## Breaking Changes

- [x] No breaking changes

Scorecard schema stays at `0.6`; the change is in how `score_pct` is
computed, not in the JSON shape. Consumers reading `badge.score_pct` see
different values after rescore but no shape change.

## Deployment Notes

- [x] No special deployment steps required

The public registry rescore against this formula is U6 in the site repo,
gated on a published CLI release, not part of this PR.
brettdavies added a commit to brettdavies/agentnative-site that referenced this pull request May 29, 2026
)

## Summary

Site-side renderer support for the scorecard fairness taxonomy (plan
unit U4). The site can now render the schema 0.6 seven-status taxonomy
and load 0.6 scorecards alongside the existing 0.5 corpus during the
migration window. The live corpus stays 0.5 until the registry rescores
against the 0.6 CLI (plan unit U6), so this change is additive: 0.5
scorecards render exactly as before, and 0.6 scorecards gain the two new
statuses.

The two new statuses are `opt_out` (deliberate non-adoption, counts
against the score) and `n_a` (conditional antecedent unmet, excluded
from the score). Both now render distinct from `skip` on the per-tool
scorecard page and in the markdown twin.

## Changelog

### Added

- Render the schema 0.6 seven-status taxonomy on scorecard pages:
`opt_out` shows as `OPT-OUT` and `n_a` as `N/A`, each visually distinct
from `skip`. The site now loads schema 0.5 and 0.6 scorecards side by
side.

## Type of Change

- [x] `feat`: New feature (non-breaking change which adds functionality)

## Related Issues/Stories

- Story: n/a
- Issue: n/a
- Architecture:
docs/plans/2026-05-21-001-feat-scorecard-fairness-taxonomy-plan.md (unit
U4)
- Related PRs: brettdavies/agentnative-cli#62 (emits the schema 0.6
shape this consumes)

## Testing

- [x] Unit tests added/updated
- [x] Integration tests added/updated
- [x] Manual testing completed
- [x] All tests passing

**Test Summary:**

- Unit tests: 765 passing (full `bun test` suite)
- Integration tests: covered within the same suite (build + worker)
- Coverage: n/a (no coverage gate configured)

Notes: the 0.6 test fixture was validated against the published
scorecard 0.6 JSON Schema (ajv, draft 2020-12) before being committed.
`bun run build` is clean apart from three pre-existing registry-index
owner/repo warnings unrelated to this change.

## Files Modified

**Modified:**

- `src/shared/scorecard-format.mjs` (new `statusLabel()` source of
truth; markdown row formatter routes through it)
- `src/build/scorecards-render.mjs` (HTML check rows route through
`statusLabel()`)
- `src/build/scorecards.mjs` (load-time schema gate widened from `0.5`
to the set `{0.5, 0.6}`)
- `src/styles/site.css` (`check--opt_out` and `check--n_a` status
styles)
- `src/worker/score/summary-render.ts` (status union widened to the full
seven for type accuracy)
- `tests/build.test.ts` (0.6 fixture; schema acceptance and 7-status
render assertions)
- `tests/scorecard-format-shared.test.ts` (`statusLabel` and new-status
markdown rows)

**Created:**

- None.

**Renamed:**

- None.

**Deleted:**

- None.

## Key Features

- One status-to-label map shared by the HTML rows and the markdown twin,
so the two surfaces cannot drift.
- Schema gate is a set, not a single version, so the migration window
holds both 0.5 and 0.6 without a flag day.

## Benefits

- Unblocks the U6 registry rescore: the renderer is ready for 0.6 data
before that data is committed.
- Backward compatible: unknown statuses fall back to uppercase rather
than rendering blank.

## Breaking Changes

- [x] No breaking changes

## Deployment Notes

- [x] No special deployment steps required

## Checklist

- [x] Code follows project conventions and style guidelines
- [x] Commit messages follow [Conventional
Commits](https://www.conventionalcommits.org/)
- [x] Self-review of code completed
- [x] Tests added/updated and passing
- [x] No new warnings or errors introduced
- [x] Changes are backward compatible (or breaking changes documented)

## Additional Context

The plan's U4 line item about splitting a leaderboard "skip + error"
bucket describes code that does not exist: the leaderboard table renders
score and principles only, with no per-status bucket column. Per-status
rendering happens in the per-tool "All Checks" table, which this PR
updates. The badge floor-display constant and the leaderboard
methodology copy still describe the 0.5 formula and floor; rewriting
those is unit U5 (methodology), coordinated with the U6 rescore, so they
are intentionally untouched here.
brettdavies added a commit that referenced this pull request May 31, 2026
## Summary

Vendors agentnative-spec v0.5.0 (upstream commit `d5d4086`) into
`src/principles/spec/`. The diff is six files:
`VERSION` bump 0.4.0 → 0.5.0, the upstream `CHANGELOG.md`, and
prose-only edits on p3/p4/p5/p7 retargeting
deferred-work footnotes from "v0.4.0 PR" to "future PR" now that those
PRs have landed.

No new requirements arrive via this sync. Every substantive item the
upstream v0.5.0 release notes advertise already
shipped here ahead of the spec release (PRs #55, #62, #65, and the
scoring work):

- `p3-must-version` and `p3-should-version-short`
- The 7-status taxonomy with antecedent propagation (scorecard schema
0.6)
- The `check` → `audit` rename
- Badge floor lowered 80 → 70, with four cohort bands

This vendoring closes the loop so the in-tree registry and the upstream
tag point at the same content.

The only consumer-visible behavior change is the scorecard's
`spec_version` field, which starts emitting `"0.5.0"`
instead of `"0.4.0"`.

## Changelog

### Changed

- Scorecard `spec_version` now reports `"0.5.0"` (vendored
agentnative-spec bumped 0.4.0 → 0.5.0).

## Type of Change

- [x] `feat`: New feature (non-breaking change which adds functionality)

## Related Issues/Stories

- Upstream tag:
https://github.com/brettdavies/agentnative/releases/tag/v0.5.0
- Vendored commit: `d5d4086`
- Pre-shipped here: #55, #62, #65

## Testing

- [x] All tests passing

**Test Summary:**

- `cargo build`: clean
- `cargo test`: 793 passed, 2 ignored
- `anc emit coverage-matrix --check`: exit 0 (no registry drift)
- Pre-push hook (fmt, clippy `-Dwarnings`, test, cargo-deny, Windows
check): passed

## Files Modified

**Modified:**

- `src/principles/spec/VERSION`
- `src/principles/spec/CHANGELOG.md`
- `src/principles/spec/principles/p3-progressive-help-discovery.md`
- `src/principles/spec/principles/p4-fail-fast-actionable-errors.md`
-
`src/principles/spec/principles/p5-safe-retries-mutation-boundaries.md`
- `src/principles/spec/principles/p7-bounded-high-signal-responses.md`

## Breaking Changes

- [x] No breaking changes

## Deployment Notes

- [x] No special deployment steps required
This was referenced May 31, 2026
brettdavies added a commit that referenced this pull request Jun 1, 2026
## Summary

`v0.5.0` of `anc`. Vendored `agentnative-spec` advances `0.4.0` →
`0.5.0`. Scorecard JSON schema advances `0.5` → `0.7` in two steps
(7-status taxonomy at `0.6`; `check_id` → `audit_id` rename at `0.7`).
Coverage matrix JSON `schema_version` rebases from `"1.0"` to `"0.1"` so
it matches the project's pre-release `0.x` convention. Two new universal
P3 requirements (`p3-must-version`, `p3-should-version-short`) plus 22
additional behavioral checks across P1-P8 close the remaining behavioral
orphan coverage. CLI subcommand surface renames `check` → `audit` and
`generate` → `emit` (breaking). Badge eligibility floor lowers from 80%
to 70%, and the leaderboard score becomes credit-weighted on
shipped-binary behavior only. The Vale prose-check stack (`styles/`,
`.vale.ini`, `scripts/prose-check.sh`) moves to dev-only contributor
tooling; the workflow guard keeps it out of `main` going forward.

## Changelog

### Added

- 22 new behavioral checks across P1-P8 close the remaining behavioral
orphan coverage (verbose, color, raw, examples, defaults-in-help,
rich-tui, about/long-about, stdin-input, consistent-naming,
timeout-behavioral, structured exit codes, JSON error envelopes,
actionable errors, force/yes, read/write distinction, TTY-aware
verbosity, and more).
- Two universal P3 requirements (`p3-must-version`,
`p3-should-version-short`) verify that every CLI ships a `--version`
flag plus a short alias from the `-V` / `-v` / `-version` family.
- New `opt_out` and `n_a` scorecard statuses surface explicit
non-adoption and unmet-antecedent rows; each `results[]` entry now
carries `tier` plus `audit_id`. Pre-`0.6` consumers feature-detect.
- Top-level `--verbose` / `-v` (env `AGENTNATIVE_VERBOSE`),
`--examples`, `--color`, and `--raw` flags surface diagnostic detail,
curated invocation blocks, ANSI styling control, and pipe-friendly
`id<TAB>status` rows.
- `anc skill install --all` and `anc skill update [host|--all]` iterate
every known host in one invocation.

### Changed

- **BREAKING**: `anc check` renamed to `anc audit`; `anc generate`
renamed to `anc emit`; `anc schema` folded under `anc emit schema`. The
implicit-default-subcommand injection now writes `audit`. Update scripts
and CI invocations.
- **BREAKING**: scorecard `schema_version` advances `"0.5"` → `"0.7"`
(two-step), the `check_id` field renames to `audit_id`, and `results[]`
switches to one entry per requirement-row rather than per probe.
Consumers pinning the prior shape must feature-detect or update.
- Leaderboard `score_pct` is now credit-weighted on shipped-binary
behavior only: `warn` earns half credit, `opt_out` counts against,
source/project audits no longer affect the score. Badge eligibility
floor lowers from 80% to 70%.
- Vendored `agentnative-spec` advances `0.4.0` → `0.5.0`; scorecard
`spec_version` reports `"0.5.0"`.
- `coverage/matrix.json` `schema_version` bumps `"1.0"` → `"0.1"` so the
matrix follows the project's pre-release `0.x` convention used by the
CLI crate, scorecard schema, and vendored spec. The lone consumer
(`agentnative-site/src/build/coverage.mjs`) validates the field's
existence but does not pin a specific value, so this is safe.
- Vale prose-check stack (`styles/`, `.vale.ini`,
`scripts/prose-check.sh`) repositions as dev-only contributor tooling.
`guard-main-docs.yml` blocks those paths from `main` going forward.

### Fixed

- Error output under `--output json` / `--json` now emits a JSON
envelope with `error`, `kind`, `message` instead of clap's plain-text
rendering, so agents pinned to JSON can parse failures with one shape.
- Text mode now renders `[N/A]` (with antecedent evidence) for
conditional requirements whose prerequisite was opted out, instead of a
misleading `[FAIL]` on the probe id. Text row count and badge score
match `--output json`.
- The spec parser rejects three malformed conditional-applicability
inputs that previously fell through silently (whitespace-only
`antecedent.audit_id`, mixed legacy `if:` + new `kind:`, unknown keys
inside `antecedent`).

### Documentation

- `RELEASES-PREFLIGHT.md` lands as the operational pre-flight checklist
gating each release cut. `RELEASES.md` gains a `### Cherry-pick
conflicts on guarded paths` subsection and a `### Dev-direct exception`
subsection covering engineering-docs paths and the prose-check stack.
- Repo-wide prose pass: every tracked markdown doc plus the historical
sections of `CHANGELOG.md` carry the same per-occurrence punctuation
tightening applied to all 64 PR bodies in the v0.4.0 → v0.5.0 window.

## Type of Change

- [x] `BREAKING CHANGE`: Breaking API change (requires major version
bump)

## Related Issues/Stories

- Story: n/a
- Issue: n/a
- Architecture: `agentnative-spec` `v0.5.0` (vendored at upstream commit
`d5d4086`). Consumer-facing JSON shape moves to schema `0.7` with the
`audit_id` rename.
- Related PRs: cherry-picks of #55, #56, #57, #58, #59, #60, #61, #62,
#63, #64, #65, #66, #68, #69 from `dev`.

## Testing

- [x] All tests passing

**Test Summary:**

- `cargo test`: 793 passed, 2 ignored.
- `anc emit coverage-matrix --check`: exit 0.
- `cargo deny check advisories`: ok.
- Release-mechanics preflight per `RELEASES-PREFLIGHT.md`: green except
the cross-repo `agentnative-site` schema gap (tracked separately).

## Files Modified

**Modified:**

- Source: `src/audits/`, `src/scorecard/`, `src/principles/`,
`src/main.rs`, `src/cli.rs`, `src/audit.rs`, `src/types.rs`,
`src/runner/`, `src/source.rs`, `src/skill_install.rs`, and supporting
modules.
- Schema: `schema/scorecard.schema.json` regenerated to the `0.7`
contract.
- Vendored spec: `src/principles/spec/*` advances to `v0.5.0`.
- Coverage artifacts: `docs/coverage-matrix.md`, `coverage/matrix.json`
reflect 59 requirement rows.
- Workflows: `.github/workflows/guard-main-docs.yml` (extra_paths).
- Tracked docs: `README.md`, `AGENTS.md`, `CLAUDE.md`, `PRODUCT.md`,
`BRAND.md`, `RELEASES.md`, `RELEASES-RATIONALE.md`, `scripts/SYNCS.md`,
`.github/pull_request_template.md`, `CHANGELOG.md`.
- Scripts: `scripts/generate-changelog.sh` (duplicate-section guard),
`scripts/sync-spec.sh` (`--ref` flag and `gh api` transport),
`scripts/hooks/pre-push` (shellcheck step).
- Build identity: `Cargo.toml`, `Cargo.lock` (`0.5.0`).

**Created:**

- `RELEASES-PREFLIGHT.md` (operational pre-flight checklist).

**Renamed:**

- `anc check` → `anc audit` (subcommand surface).
- `anc generate` → `anc emit` (subcommand surface).
- Scorecard `check_id` → `audit_id` (JSON field).

**Deleted:**

- `.vale.ini`, `styles/`, `scripts/prose-check.sh`, plus the orphaned
helpers `scripts/sync-prose-tooling.sh`, `scripts/test-prose-check.mjs`,
`scripts/generate-pack-readme.mjs`. Replaced by the
`guard-main-docs.yml` `extra_paths` guard that keeps the stack off
`main` going forward.

## Breaking Changes

- [x] Breaking changes described below:

1. **Scorecard JSON shape**: `schema_version` `"0.5"` → `"0.7"`.
`check_id` renamed to `audit_id` (per-row and in
`coverage/matrix.json`). `results[]` switches to one entry per
requirement-row instead of per probe; each entry carries new `tier` and
`audit_id` fields. Status enum expands from 3 to 7 values (`pass`,
`warn`, `fail`, `opt_out`, `n_a`, `skip`, `error`). Consumers pinned to
`0.5` or the prior `check_id` field must update.
2. **Subcommand surface**: `anc check` renamed to `anc audit`; `anc
generate` renamed to `anc emit`; `anc schema` folded under `anc emit
schema`. Scripts and CI invocations pinned to the old names must update.

## Deployment Notes

- `agentnative-site` must add `'0.7'` to its `SUPPORTED_SCHEMA_VERSIONS`
allowlist before this release is tagged. Without that, the site's regen
pipeline rejects every `v0.5.0` scorecard upload.
- After tag publish: run `./scripts/sync-dev-after-release.sh v0.5.0 &&
git push origin dev` per `RELEASES.md` § After publish to backport
`Cargo.toml`, `Cargo.lock`, and `CHANGELOG.md` to `dev`.
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.

1 participant