Badge Policy

August 3, 2026 · View on GitHub

ripr exposes two badges. Public README, crate, and extension-store badges are user-actionable repair counters: inbox-zero, not coverage. Diff-scoped artifacts preserve the legacy Finding exposure basis for PR-local summaries. Repo-scoped public badges use the canonical_actionable_gap basis: unresolved canonical repair items with a safe repair route, verification path, and receipt path. Seam-native classified repo seams remain an internal inventory basis, and explicit gap-decision-ledger targets remain the policy-backed projection bridge. This doc fixes the vocabulary, the counting rule, the JSON shape, and what the badge does and does not claim.

This is the contract that ripr check --format badge-json and --format badge-shields will render against. It pairs with RIPR-SPEC-0056, Static exposure model, Output schema, and Configuration.

Status

This is the policy document. The badge command, the test-intent and suppressions config files, the diff-scoped CI artifact pipeline, the repo-scoped artifact path, and the trunk-only public Shields endpoint have all landed under Campaign 4A. The current implementation status of each piece is tracked in the status table at the bottom of this doc and in .ripr/goals/active.toml (deleted in #1701 PR 3).

The public badge projection realignment has landed. This policy defines canonical_actionable_gap as the public basis, and generated endpoint snapshots now project unresolved actionable static repair gaps instead of seam-native inventory counts. Use cargo xtask badge-basis to audit the current endpoint basis before refreshing public JSON.

What each badge means

ripr 0

ripr found zero unsuppressed static exposure gaps under the configured policy.

Diff scope counts exposure-class findings from ripr check. Repo scope counts unresolved actionable canonical repair items eligible for public projection. The badge is a count-and-render policy on top of existing analyzer output.

ripr+ 0

ripr found zero unsuppressed static exposure gaps and zero unsuppressed
actionable test-efficiency findings.

ripr+ adds only actionable test-efficiency repair items that have been lifted into the same repair / verify / receipt model. A passing ripr+ is strictly stronger than a passing ripr.

Scope: diff vs repo

The same badge label renders at two different scopes depending on input. The two scopes have different audiences and different meanings, and conflating them produces misleading public signal.

Diff scope (scope: diff)

The badge counts findings within the diff under analysis — typically origin/main...HEAD for a PR. This is what ripr check produces by default and what cargo xtask badge-artifacts writes for PR step summaries.

  • Audience: PR reviewers, CI step summary, PR artifact uploads.
  • Meaning: "this PR's changed behavior has N unresolved findings under policy."
  • Not a meaningful README / marketplace / store badge. On main itself the diff vs origin/main is empty, so a diff-scoped pure ripr badge always reports 0. That is "nothing changed," not "the repo is clean."

Repo scope (scope: repo)

The badge counts unresolved actionable static repair gaps across the entire repo baseline using canonical_actionable_gap as its public basis. This is the only scope that should be published as a public README, crate page, or extension store badge.

  • Audience: anyone reading the repo cold from outside.
  • Meaning: "the current repo baseline has N unresolved actionable static repair gaps under policy."
  • Public repo scope uses canonical_actionable_gap as its basis. A counted item has a canonical gap identity, unresolved state, actionable repair route, safe verification command, receipt path, no suppression or intentional disposition, and eligibility for public projection.
  • Seam-native counts remain available as internal inventory in badge-basis, repo-exposure, seam-inventory, and evidence-quality reports. They should not be published as the README / crate / store headline unless the badge is explicitly labeled as seam inventory.
  • The CLI surface is --format repo-badge-json, --format repo-badge-shields, --format repo-badge-plus-json, and --format repo-badge-plus-shields; the xtask wrapper is cargo xtask repo-badge-artifacts.

Committed badges/*.json files are generated endpoint snapshots, not hand-authored copy. Ordinary PRs should not carry badge endpoint diffs; use cargo xtask badges or the Badge Endpoints workflow and let the generated badge: refresh public endpoints PR carry the endpoint update. cargo xtask check-badge-diff-policy enforces that ownership boundary while leaving README badge link/layout edits to normal docs review.

ripr+ fact source vs aggregation scope

cargo xtask test-efficiency-report is repo-wide as a fact source: it scans the entire test suite and records per-test evidence (reached_owners, oracle kind/strength, observed values, declared intent). Badge aggregation is scope-aware:

  • Diff-scoped ripr+ (--format badge-plus-json, --format badge-plus-shields) filters the test-efficiency ledger to entries related to the changed owners / findings in the analyzed diff. A test-efficiency entry is related when either:

    • the entry's bare or <path>::<name> form appears in any diff Finding.related_tests, or
    • the entry's reached_owners intersect the changed/probed owners drawn from Finding.probe.owner.

    Unrelated repo-wide debt (e.g. a likely_vacuous test in a module this PR doesn't touch) does not move the diff ripr+ headline. Suppressions and declared intent still apply to the filtered set: declared intent moves a related entry into intentional_test_efficiency_findings (out of the headline); matched suppressions move a related actionable entry from unsuppressed_test_efficiency_findings to suppressed_test_efficiency_findings.

  • Repo-scoped ripr+ (--format repo-badge-plus-json, --format repo-badge-plus-shields) counts test-efficiency items only when they are projected into the same actionable repair model as canonical gaps. Raw repo-wide test-efficiency inventory belongs in detailed reports.

The split keeps PR badges scoped to the tests that act on the changed code while README / store badges remain repo-baseline signals. Native badge JSON carries basis so consumers can distinguish finding_exposure diff artifacts, canonical_actionable_gap public repo artifacts, seam_native internal inventory artifacts, and gap_decision_ledger explicit projection artifacts.

Basis vocabulary

Badge-producing surfaces must name the basis they used. The basis tells readers whether a count is a repair queue, a PR-local exposure artifact, an internal inventory, or a policy-backed projection.

BasisPrimary scopePublic README / store headline?Meaning
canonical_actionable_gaprepoyesUnresolved canonical repair items with a repair route, verify command, receipt path, and public projection eligibility.
finding_exposurediffnoLegacy PR-local Finding / ExposureClass aggregation from ripr check.
seam_nativerepo inventorynoRepo seam inventory and static limitation pressure by SeamGripClass; useful internally, too broad for the public repair counter.
gap_decision_ledgerrepo projectionlegacy bridgeExplicit GapRecord projection targets supplied by policy or release tooling. Use when the ledger is the source of projection authority.

README and store badges must count user-actionable canonical repair items. If a badge intentionally reports seam-native inventory, its label and docs must say that plainly and it must not reuse the main ripr / ripr+ headline.

What repo seam-native inventory means — and does not mean

The repo seam-native inventory counts classified behavior seams from the seam inventory. At repo root, seam discovery excludes repository automation and fixture data (xtask/ and top-level fixtures/) so inventory reports represent the published ripr package surface instead of the repository harness. An individual fixture workspace is still analyzable when passed as --root.

Badge classification uses a compact, ranked related-test sample per seam. That keeps public endpoint refreshes operable while preserving the same SeamGripClass headline mapping as the detailed repo exposure report. It is not:

  • a complete inventory of every behavior seam in the repo
  • confirmation that every behavior is tested
  • confirmation of mutation adequacy
  • a coverage metric

Public README / store badges that derive from cargo xtask badge-artifacts are unsafe — that task generates diff-scoped artifacts only.

What neither badge proves

A green badge does not mean:

  • the code is fully tested
  • mutants would fail under the test suite
  • there are no behavioral bugs
  • coverage is high

A green badge means: under the static evidence ripr could gather, no unresolved gaps or actionable test-efficiency findings remain after applying the configured suppressions and test-intent declarations. Mutation testing remains the runtime confirmation step. See the closing wording in the product contract and STATIC_EXPOSURE_MODEL.md.

Why no denominator

The badge does not show 0/2300.

A denominator reads as a coverage fraction ("2300 things to cover, 0 covered"), which is exactly the wrong mental model. ripr is not measuring coverage; it is measuring whether changed behavior appears exposed to a meaningful oracle.

The badge is an inbox-zero counter: zero unresolved gaps is the target, like inbox zero. Scope, unknowns, suppressed findings, intentional findings, and total analyzed counts all live in the detailed JSON and markdown reports — not in the badge message.

Avoid:

ripr 0/2300        # reads as incomplete coverage
ripr coverage 0    # ripr is not a coverage tool
ripr uncovered 0   # same problem

Prefer:

ripr 0
ripr+ 0

Or, if disambiguation is needed in dense badge bars:

ripr gaps 0
ripr+ issues 0

Exposure-class counting

These come from ExposureClass in crates/ripr/src/domain/ and from the classification table in STATIC_EXPOSURE_MODEL.md.

Exposure-class counting is the finding_exposure basis used by diff-scoped badge artifacts. It remains documented and versioned for PR summaries and legacy consumers.

Exposure classCounts in riprCounts in ripr+Notes
weakly_exposedyesyesDefault exposure gap.
reachable_unrevealedyesyesDefault exposure gap.
no_static_pathyesyesDefault exposure gap.
exposednonoAlready exposed; not a gap.
infection_unknownnonoReported separately as unknowns.
propagation_unknownnonoReported separately as unknowns.
static_unknownnonoReported separately as unknowns.

Unknowns are first-class in ripr. They mean static analysis stopped, not that a gap exists. They are visible in the badge JSON's counts.unknowns, and visible in the human and JSON reports with their stop reasons. They do not move the badge number unless a future policy explicitly opts in via --include-unknowns.

Test-efficiency vocabulary (locked)

The badge counts use the exact strings emitted by cargo xtask test-efficiency-report. The source of truth is xtask/src/main.rs — function test_efficiency_class for the class string and test_efficiency_reasons for the reason strings. If you add a new class or reason there, update this table.

Per-test class field (exactly seven values)

class valueCounts in ripr+Triggered when
strong_discriminatornoStrong oracle and no other condition demoted the test.
useful_but_broadnoMedium- or weak-strength oracle that still asserts something. Visible in reports.
smoke_onlyyes (unless declared intent)Smoke-strength oracle (e.g. is_ok, is_err, unwrap).
likely_vacuousyesA reason includes no_assertion_detected.
possibly_circularyes (unless declared intent)A reason includes expected_value_computed_from_detected_owner_path.
duplicativeyes (unless declared intent)Test belongs to a duplicate-discriminator group: same owner set, role-aware activation signature, and oracle shape. Only strong_discriminator, useful_but_broad, and smoke_only entries are eligible to be promoted to duplicative; already-flagged classes are preserved.
opaquenoNo reached owners detected. Visible only; static analysis cannot judge.

Reason strings (exactly nine values)

These are not counted directly. They explain why a class fired and feed suggested next steps. The table below documents them so the badge JSON's reason_counts can be interpreted without reading source.

Reason stringWhat it indicates
no_assertion_detectedThe test body has no detected assertion. Demotes class to likely_vacuous.
smoke_oracle_onlyOracle class is Smoke (e.g. is_ok, unwrap, expect).
relational_oracleMedium-strength relational assertion (assert!(x > 0), is_empty, etc.).
broad_oracleWeak-strength oracle that asserts something but not exact behavior.
assertion_may_not_match_detected_ownerWeak-oracle test where the assertion target may not be the changed owner.
opaque_helper_or_fixture_boundaryNo owner call was statically resolved; demotes class to opaque.
no_activation_literal_detectedNo literal activation values found in the test body.
expected_value_computed_from_detected_owner_pathThe expected side of an assert_eq! calls back into the detected owner; demotes class to possibly_circular.
duplicate_activation_and_oracle_shapeThe test shares an owner set, role-aware activation signature, and oracle shape with at least one other test; appended to existing reasons (e.g. smoke_oracle_only) and promotes the class to duplicative.

Visible-but-not-counted by default

  • opaque — static analysis stopped. Counts in unknowns_test_efficiency, not in the ripr+ headline. Intentionally distinct from "vacuous."
  • useful_but_broad — broad oracle. Visible in reports as advisory. Becomes countable only when test-efficiency policy explicitly elevates it for the changed behavior, which is a future policy switch, not a v1 default.

Test intent is additive metadata, not a class

Declared test intent (e.g. intent = "smoke" in .ripr/test_intent.toml) is not rendered as a replacement class value. The original class (smoke_only, duplicative, useful_but_broad, etc.) is preserved so the report still tells reviewers what the static analyzer saw. Intent is a layered, owner-and-reason-stamped declaration on top of the signal:

{
  "name": "cli_prints_help",
  "class": "smoke_only",
  "declared_intent": {
    "intent": "smoke",
    "owner": "devtools",
    "reason": "CLI startup and help text smoke test.",
    "source": ".ripr/test_intent.toml"
  }
}

ripr+ consumes the declared_intent metadata to exclude declared intentional findings from its count. There is no intentional_smoke or intentional_duplicate class string — those would conflate the analyzer's signal with the user's declaration.

The metric label duplicate_discriminator_group_count (delivered in test-efficiency/report-and-metrics) is a count-of-groups label, not a class. Today the equivalent value is duplicate_groups.length in the test-efficiency JSON.

Seam-native repo inventory counting

Seam-native repo inventory uses the seam_native basis. These counts come from SeamGripClass in RIPR-SPEC-0005 and consume the configured seam severity from ripr.toml. They are useful for internal evidence-quality pressure, static limitation pressure, and analyzer health. They are not the public repair counter.

When ripr check --format repo-badge-json --gap-ledger <path> or another repo-badge format supplies a gap decision ledger, the native JSON uses basis = "gap_decision_ledger" and counts explicit projection_eligibility.ripr_zero_count or ripr_plus_count targets instead of recalculating from seam-native counts. That path is for policy-backed badge refreshes and remains a bridge until public endpoints are generated directly from canonical_actionable_gap.

Seam grip classCounts in repo riprNotes
weakly_grippedyes, unless configured offHeadline-eligible seam gap.
ungrippedyes, unless configured offHeadline-eligible seam gap.
reachable_unrevealedyes, unless configured offHeadline-eligible seam gap.
activation_unknownyes, unless configured offHeadline-eligible static limitation.
propagation_unknownyes, unless configured offHeadline-eligible static limitation.
observation_unknownyes, unless configured offHeadline-eligible static limitation.
discrimination_unknownyes, unless configured offHeadline-eligible static limitation.
opaquenoReported separately as unknowns when configured visible.
strongly_grippednoDefault severity is off.
intentionalnoDefault severity is off.
suppressednoDefault severity is off; if configured visible, counted in suppressed_exposure_gaps.

severity.seams.<class> = "off" omits that class from the badge headline and visible count buckets. Other severities keep the class visible to the badge mapping but do not change the badge color thresholds.

Counting rule

Diff-scoped ripr count:

ripr count =
    findings where exposure_class ∈ { weakly_exposed,
                                      reachable_unrevealed,
                                      no_static_path }
    minus suppressed exposure-gap findings

Repo-scoped public ripr count:

ripr count =
    canonical items where gap_state = unresolved
    and actionability = actionable
    and a repair route exists
    and a safe verify command exists
    and a receipt path exists
    and not suppressed
    and not intentional
    and eligible for public projection

ripr+ adds test-efficiency findings to whichever ripr repair basis was selected by scope:

ripr+ count =
    ripr count
  + tests where class ∈ { likely_vacuous,
                          possibly_circular,
                          smoke_only }
    and not declared intentional in .ripr/test_intent.toml
    and not suppressed in .ripr/suppressions.toml
  + tests in `duplicative` groups
    not declared intentional and not suppressed

Diff-scoped ripr+ uses the diff's related-test filter. Repo-scoped ripr+ adds only test-efficiency items lifted into the actionable repair model. The badge is a rendering policy over analyzer reports, not a separate analysis.

Internal seam-native inventory count:

seam inventory count =
    seams where seam_grip_class is headline eligible
    and configured seam severity != off

This count may appear in detailed reports, scorecards, and badge-basis audits. It must not drive the public ripr / ripr+ headline unless that endpoint is explicitly relabeled as seam inventory.

JSON wire shape

There is one native schema. The Shields response is a projection at the output boundary; it is never the source of truth.

Native (--format badge-json)

{
  "schema_version": "0.8",
  "kind": "ripr",
  "scope": "repo",
  "basis": "canonical_actionable_gap",
  "label": "ripr",
  "message": "0 actionable",
  "status": "pass",
  "color": "brightgreen",
  "analysis_complete": null,
  "analysis_outcome": null,
  "counts": {
    "unsuppressed_exposure_gaps": 0,
    "unsuppressed_test_efficiency_findings": 0,
    "intentional_test_efficiency_findings": 0,
    "suppressed_exposure_gaps": 0,
    "suppressed_test_efficiency_findings": 0,
    "unknowns": 0,
    "unknowns_test_efficiency": 0,
    "analyzed_findings": 0,
    "analyzed_seams": 120,
    "analyzed_gap_records": 0,
    "analyzed_tests": 0
  },
  "reason_counts": {
    "no_assertion_detected": 0,
    "smoke_oracle_only": 0,
    "relational_oracle": 0,
    "broad_oracle": 0,
    "assertion_may_not_match_detected_owner": 0,
    "opaque_helper_or_fixture_boundary": 0,
    "no_activation_literal_detected": 0,
    "expected_value_computed_from_detected_owner_path": 0,
    "duplicate_activation_and_oracle_shape": 0
  },
  "policy": {
    "include_unknowns": false,
    "fail_on_nonzero": false,
    "test_intent_path": ".ripr/test_intent.toml",
    "suppressions_path": ".ripr/suppressions.toml"
  },
  "warnings": [],
  "preview_skipped": [],
  "public_projection": {
    "state": "zero_actionable",
    "message": "0 actionable",
    "run_status": "full",
    "generated_at": "2026-06-20T00:00:00Z",
    "actionable_count": 0,
    "limited_reason": null,
    "stale_age_secs": 0,
    "source_report": "target/ripr/reports/repo-ripr-badge.json"
  }
}

kind is "ripr" or "ripr_plus". The _plus form adds unsuppressed_test_efficiency_findings to its message; the schema is otherwise identical so consumers can parse one shape.

schema_version is the badge-native schema. Bumping it is a public-contract change and must be called out in the PR. 0.3 adds basis and counts.analyzed_seams; 0.4 adds basis = "gap_decision_ledger" and counts.analyzed_gap_records; 0.5 adds basis = "canonical_actionable_gap" for public repair-item projection; 0.6 adds preview_skipped so consumers can detect when a preview-language diff was not analyzed and the badge is not a clean Rust-grade result (see Preview-language honesty below); 0.7 adds the public_projection object on repo-scoped public badges (see Public badge projection below); 0.8 adds nullable analysis_complete and typed analysis_outcome fields for diff-scoped badges, downgrading incomplete zero-finding input from pass/green to warn/yellow.

Public badge projection (RIPR-SPEC-0066)

Repo-scoped public badges (canonical_actionable_gap or gap_decision_ledger basis) carry a public_projection object that renders the badge into one closed user-facing state and supplies the machine-readable sidecar fields. Diff-scoped and internal badges omit it. The closed states are zero_actionable, actionable, limited, stale, and unknown, selected with fail-closed precedence (unknown > stale > limited > count): a degraded input — raw-finding basis, diff scope, missing or unreadable source report, missing generated_at, a limited_* run, or an over-age artifact — never resolves toward the cleaner-looking state, and a count is never carried alongside a degraded state. The native message / status / color are projected from this object, so the public Shields message reads as ripr: <n> actionable, ripr: limited, ripr: stale, or ripr: unknown. The sidecar fields (run_status, generated_at, actionable_count, limited_reason, stale_age_secs, source_report) are documented in docs/OUTPUT_SCHEMA.md.

Preview-language honesty

preview_skipped is an array of language names (e.g. ["typescript"]) that were detected in the analyzed diff but whose adapter was not enabled. Non-empty means the badge result is NOT a clean Rust-grade result — those files were silently skipped. Consumers MUST treat a non-empty list as a honesty signal even when status is already "warn" due to real findings.

When preview_skipped is non-empty and the headline count would otherwise be 0 (pass/green), the badge is downgraded to warn/yellow and message is set to "preview-skipped: <language>" instead of "0". This prevents a glanceable "passing" badge on un-analyzed behavior. The Shields projection carries the same message so img.shields.io badge links are also honest.

When preview_skipped is empty the badge behaves as before: clean Rust-only diffs with 0 findings render pass/brightgreen/0. The amber state does not fire on clean Rust — preview_skipped is populated only by the real producer in analysis/pipeline.rs (detect_preview_advisories) which sets enabled: enabled.contains(language). It is never populated for Rust-only diffs.

Scope and basis metadata (native only)

A scope field distinguishes PR artifacts from public repo badges. A basis field distinguishes legacy diff finding counts, public repair projections, internal seam-native inventory counts, and explicit ledger projections:

{
  "schema_version": "0.6",
  "kind": "ripr",
  "scope": "diff",
  "basis": "finding_exposure",
  "label": "ripr",
  "message": "3",
  "...": "..."
}
  • "scope": "diff" — diff-scoped (PR artifacts). Native JSON SHOULD also record base and head git refs so consumers can reproduce.
  • "scope": "repo" — repo-scoped (README / main endpoint).
  • "basis": "finding_exposure" — legacy Finding/ExposureClass aggregation, currently used by diff-scoped badge artifacts.
  • "basis": "canonical_actionable_gap" — public repo repair-item projection.
  • "basis": "seam_native"RepoSeam/SeamGripClass aggregation for internal inventory and transitional repo-scoped badge artifacts.
  • "basis": "gap_decision_ledger" — explicit GapRecord projection targets, used only when repo badge formats are invoked with --gap-ledger.

The Shields projection remains exactly four fields. Scope and basis metadata live only in native JSON, docs, and consumer tooling.

Shields projection (--format badge-shields)

{
  "schemaVersion": 1,
  "label": "ripr",
  "message": "0",
  "color": "brightgreen"
}

Shields requires schemaVersion (camelCase) and exactly four top-level fields. The projection is mechanical: drop everything except label, message, color; map schema_versionschemaVersion: 1.

Both formats are derived from the same internal BadgeSummary. That type is intentionally not public — it lives in a private rendering module (crates/ripr/src/output/badge/ when implemented) and the public API remains the JSON shape. This keeps cargo xtask check-public-api green and matches the existing pattern (output::json::render is private; the JSON contract is what's stable).

Colors and status thresholds

Conservative defaults. Tunable later.

countstatuscolor
0passbrightgreen
1–3warnyellow
4+warnorange
any, with --fail-on-nonzero and count > 0failred

status is independent of CI exit code. CI exit is governed by --fail-on-nonzero; the badge always renders. A warn status on main should never block a release on its own.

These thresholds will trip noisily on small diffs that legitimately have 4 weak findings. A diff-relative threshold (e.g. yellow at any nonzero, orange when ratio of unresolved-to-analyzed exceeds a bound) is on the table for v2 once we have real-world numbers from CI artifacts (PR ci/badge-artifacts). For v1, absolute is simpler to reason about.

CLI shape

The badge is a render-time policy over CheckOutput. Reuse ripr check rather than introducing a new top-level command:

ripr check --base origin/main --format badge-json
ripr check --base origin/main --format badge-shields

ripr check --base origin/main --format badge-plus-json
ripr check --base origin/main --format badge-plus-shields

The badge-plus-* formats read target/ripr/reports/test-efficiency.json (relative to --root). If the report is missing, the command renders a neutral ripr+ badge with message = "needs test-efficiency", warns on stderr, and exits successfully so badge generators do not publish a permanent unavailable state. CI artifact wiring (ci/badge-artifacts) should generate the report as part of the badge pipeline before publishing or enforcing a measured ripr+ value.

Reasoning. The current top-level commands are check, explain, context, doctor, lsp. Each is a distinct operation. A badge is the same operation as check rendered differently. Keeping it as a --format choice:

  • avoids growing the public CLI surface and the LSP/extension command tables
  • means --root, --base, --diff, --mode, --no-unchanged-tests already work without re-implementation
  • matches how --json and --format github already behave

If a dedicated ripr badge ergonomic alias is added later, this doc must be updated to call it out as a deliberate choice.

Useful flags (planned)

These belong on ripr check once the badge formats land. They are scoped to the badge formats — they do not affect human/json/github output.

FlagDefaultEffect
--include-unknownsoffAdd unknowns to the badge count.
--fail-on-nonzerooffExit nonzero when count > 0. CI-only knob.
--test-intent PATH.ripr/test_intent.tomlOverride the test-intent file.
--suppressions PATH.ripr/suppressions.tomlOverride the suppressions file.
--show-suppressedoffInclude suppressed findings in the human badge summary.

There are intentionally no inline allow/suppress CLI flags. Durable exceptions belong in files with reason and owner, not in shell history.

Test intent and suppressions

Two files, two purposes. Both are planned for Campaign 4A.

.ripr/test_intent.toml — positive declarations

Use when a test is intentionally smoke, intentionally duplicates a structurally similar test for a separate business case, or uses an opaque oracle by design. Declared tests stay visible in the report but do not move the ripr+ count.

[[test_intent]]
test = "cli_prints_help"
intent = "smoke"
reason = "CLI startup and help text smoke test."
owner = "devtools"

Supported intents (initial set): smoke, business_case_duplicate, opaque_external_oracle, integration_contract, performance_guard, documentation_example. Adding a new intent is a doc + schema PR, not an ad-hoc string.

.ripr/suppressions.toml — exceptions for non-intent cases

Use for known exposure gaps covered by oracles ripr cannot see today, or for accepted-risk cases pending later work.

[[suppressions]]
kind = "exposure_gap"
finding_id = "probe:src/pricing.rs:88:predicate"
reason = "Covered by integration test in tests/billing/integration.rs that ripr cannot statically inspect yet."
owner = "billing"
expires = "2026-09-01"

Rules (enforced when the loader lands):

  • reason required, free-form but durable
  • owner required
  • expires strongly encouraged; expired entries surface as a separate count
  • suppressed findings remain visible in the report
  • the badge counts.suppressed_* fields show the count

test_intent ships before suppressions so smoke and duplicate tests don't have to be "suppressed" merely for being intentional.

CI policy

Advisory by default. PR runs and main runs render different surfaces.

PR runs — diff-scoped

cargo xtask badge-artifacts invokes ripr check with the per-PR diff (git diff origin/main...HEAD) and writes diff-scoped artifacts:

cargo xtask badge-artifacts
# writes target/ripr/reports/ripr-badge.json (scope: diff, planned)
# writes target/ripr/reports/ripr-badge-shields.json
# writes target/ripr/reports/ripr-plus-badge.json
# writes target/ripr/reports/ripr-plus-badge-shields.json
# writes target/ripr/reports/ripr-badges.md

Used for the PR step summary and uploaded as the ripr-pr-reports artifact. CI does not fail on a nonzero badge count unless a workflow explicitly passes --fail-on-nonzero. These artifacts are not safe to publish as README badges — see "Scope: diff vs repo."

main runs — repo-scoped

cargo xtask repo-badge-artifacts (badge/repo-scope-artifacts) analyzes the full repo baseline rather than a diff and writes repo- scoped artifacts:

cargo xtask repo-badge-artifacts
# writes target/ripr/reports/repo-ripr-badge.json (scope: repo)
# writes target/ripr/reports/repo-ripr-badge-shields.json
# writes target/ripr/reports/repo-ripr-plus-badge.json
# writes target/ripr/reports/repo-ripr-plus-badge-shields.json
# writes target/ripr/reports/repo-ripr-badges.md

When a policy-backed gap decision ledger is the desired badge source, pass it explicitly:

cargo xtask repo-badge-artifacts --gap-ledger target/ripr/reports/gap-decision-ledger.json

That renders the same repo badge artifact filenames with basis = "gap_decision_ledger" and counts only the ledger's explicit projection_eligibility.ripr_zero_count and ripr_plus_count targets. The no-ledger public implementation renders basis = "canonical_actionable_gap". Seam-native counts remain available in internal inventory and audit reports.

cargo xtask badge-basis writes target/ripr/reports/badge-basis.{json,md} as an audit-only report. It decomposes the committed badges/*.json endpoint values, the current repo badge basis, compact seam-native inventory counts, test-efficiency counts, and whether an explicit gap-decision-ledger projection was available. It does not update badges/*.json; use it before a badge semantics PR or generated endpoint refresh to see whether the public badge names unresolved actionable static repair gaps with canonical_actionable_gap as the public basis. Seam-native inventory remains supporting/internal diagnostics, and ripr+ items must project into the same repair, verify, and receipt model before they affect the public headline.

ripr badge product contract

The ripr badge product contract is a single sentence:

ripr emits Shields-compatible JSON.

To put a ripr value into a README, that JSON has to be available at a stable public URL. Hosting the JSON is a separate, replaceable layerripr itself does not require any specific host.

QuestionAnswer
Who computes the value?A normal CI job that runs cargo xtask repo-badge-artifacts.
Who hosts the value?Any stable public surface that can serve the resulting Shields JSON.
Does a downstream user have to enable GitHub Pages?No. Pages is one host; it is not a requirement of ripr.

Compare to existing badges in this repo's README:

BadgeComputationHosting
CI statusGitHub ActionsGitHub
CodecovCI uploads coverageCodecov
crates.io versioncargo publishcrates.io / Shields
Open VSX downloadsregistryOpen VSX / Shields
ripr / ripr+ripr CI computes JSONself-hosted (see below)

The ripr/ripr+ row is the one without a third-party host. Long-term that gap is intended to close — see deferred/hosted-badge-service in docs/DEFERRED.md. In the meantime, a self-hosted host is required.

Self-hosted dogfood endpoint (this repo)

This repo's v1 dogfood endpoint is two Shields JSON files committed to main, served via raw.githubusercontent.com. It is not the standard downstream-user publishing model.

The committed files live at:

badges/ripr.json
badges/ripr-plus.json

Each is a minimal four-field Shields object, e.g.:

{
  "schemaVersion": 1,
  "label": "ripr",
  "message": "163",
  "color": "orange"
}

The README renders those endpoints via:

https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/EffortlessMetrics/ripr/main/badges/ripr.json
https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/EffortlessMetrics/ripr/main/badges/ripr-plus.json

Refreshing the committed files is one xtask command:

cargo xtask update-badge-endpoints

That regenerates target/ripr/reports/repo-ripr-{badge,plus-badge}-shields.json via repo_badge_artifacts() and copies the two Shields projections into badges/. Commit the resulting diff.

For a policy-backed endpoint refresh, provide the same explicit gap ledger:

cargo xtask badges --gap-ledger target/ripr/reports/gap-decision-ledger.json
cargo xtask badges --check --gap-ledger target/ripr/reports/gap-decision-ledger.json

This keeps public endpoint updates tied to the same GapRecord projection targets used by RIPR Zero and avoids treating raw report counts as badge authority.

Pinned contract for the endpoint

  • Only the two badges/*.json files are part of the public endpoint surface — no reports, no markdown, no diff-scoped artifacts, no target/ snapshots.
  • cargo xtask check-badge-diff-policy rejects badges/*.json diffs outside an explicit badge refresh branch, title, or work item. README badge link and layout edits remain source docs and can move through ordinary docs PRs.
  • The endpoint URL points at the main branch via raw.githubusercontent.com. Shields/CDN cache layers can take minutes to refresh after a main push.
  • Diff-scoped artifacts (ripr-badge-shields.json, ripr-plus-badge-shields.json) stay in per-PR step summaries and CI artifact uploads — never linked from public docs.
  • cargo xtask check-badge-endpoints verifies the committed files against a fresh repo-badge-artifacts run. It is not wired into the default CI gate set in v1: the headline drifts whenever production code or tests change, and requiring every PR to also refresh badges/ would be too much friction before the count stabilizes. Use it locally before campaign closeouts and after material analyzer changes.
  • The ripr 0 headline on main means: zero unresolved actionable canonical repair items under the current repo baseline and configured projection policy. It does not mean the repo is fully tested, that all behavior seams are gripped by oracles, or that runtime mutation confirmation would pass.

Why checked-in JSON, not GitHub Pages

An earlier shape of this work used a Pages deployment workflow with first-party actions/configure-pages / actions/upload-pages-artifact / actions/deploy-pages. That was over-engineered for v1 dogfood:

  • it required the repo owner to enable Pages
  • it added a workflow + policy/workflow_allowlist.txt entry + Pages permissions surface
  • it implied that downstream users should also enable Pages, which is not the long-term ripr user story

Checked-in JSON gives the same public-URL-on-main outcome with much less machinery, and badge changes show up in PR diffs — which is useful while the repo headline is still stabilizing.

What downstream users should do

If you want ripr and ripr+ badges in your own README today:

  1. Run ripr in your CI.
  2. Pick any stable public surface to serve the resulting Shields JSON: a committed badges/ directory in your repo (the pattern this repo uses), GitHub Pages, an organization-level badge-host repo, a static asset bucket, a Gist, or a hosted host. None is required.
  3. Point Shields at your URL via the https://img.shields.io/endpoint?url=... pattern.

A general-purpose hosted ripr badge service (so step 2 disappears) is tracked as deferred/hosted-badge-service. Until that lands, self-hosting is the v1 path.

Implementation status

Tracked alongside Campaign 4A and Campaign 5B in .ripr/goals/active.toml (deleted in #1701 PR 3) and docs/IMPLEMENTATION_CAMPAIGNS.md.

ComponentStatusSource
Test fact ledgerdonecargo xtask test-efficiency-report
Vacuity signals (the 6-class table above, minus duplicate)donesame
Duplicate-discriminator groupingdonetest-efficiency/duplicate-discriminator-v1
Test-efficiency report metricsdonetest-efficiency/report-and-metrics
Private BadgeSummary model and rendererdonebadge/summary-renderer-v1
ripr check --format badge-json / badge-shieldsdonebadge/ripr-count-v1
.ripr/test_intent.toml loaderdonetest-intent/v1
ripr check --format badge-plus-*donebadge/ripr-plus-count-v1
.ripr/suppressions.toml loaderdonesuppressions/v1
CI badge artifacts (diff-scoped, PR)doneci/badge-artifacts
Repo-scoped badge artifactsdonebadge/repo-scope-artifacts (cargo xtask repo-badge-artifacts)
Published Shields endpoint from maindonebadge/publish-main-endpoint (committed badges/*.json served via raw.githubusercontent.com; refresh with cargo xtask update-badge-endpoints)
Diff-scope ripr+ related-tests filterdonebadge/diff-ripr-plus-related-tests
Seam-native repo badge mappingdonebadge/seam-native-count-mapping
Badge-basis audit reportdonecargo xtask badge-basis
Actionable public badge basis policydonecanonical_actionable_gap definition in this doc
Canonical actionable endpoint generatordonepublic badge projection realignment
Internal seam-native inventory reportdonecargo xtask badge-basis internal inventory section and repo exposure reports

See also