Skip to content

Commit b4aef44

Browse files
Add release_decision.contribution_rules[] audit (v0.17) (#81)
* Add release_decision.contribution_rules[] audit (v0.17) Adds a deterministic per-finding audit of how each finding contributed to the release decision. Exactly one row per report.findings entry, including suppressed; the (rule, category) pair documents which branch of the gate fired. Bumps report_schema_version 0.16 -> 0.17. Documents the existing v0.8 classification as the new "Release decision truth table" in STABILITY.md (10 rows + prose explainers covering baseline asymmetry and exit-code-vs-decision split). No semantic change: decision, blockers[], review_items[], fail_policy.exit_code, and strict-mode exit are byte-identical to v0.16. The audit reflects existing behavior, it does not modify it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address PR review: id-less Finding crash, sample paths, contract surface P2 #1 — build_release_decision crashed on Finding(id=None) because ContributionRule.finding_id is required-as-string. Direct/internal callers (test fixtures, plugin checks emitting Findings before assign_finding_ids, explain-finding rebuilding from a stripped report) hit a Pydantic ValidationError. Fix the _rule helper to fall back through finding.fingerprint to finding.check_id, both of which are guaranteed-present strings on a valid Finding. Adds an explicit regression test exercising both the fingerprint and check_id fallbacks. P2 #2 — sample expected reports carried /private/tmp/shipgate_regen/ paths in generated_reports[] from the v0.17 regen. Restored to the original repo convention (relative paths like expected/report.md) by running scan from each sample dir with output_dir='expected'. P3 #3 — added release_decision.contribution_rules[] to the canonical agent surface: a one-line entry in docs/agent-contract-current.md "Read these first for release gating", a new entry in contract.MANUAL_REVIEW_SIGNALS so contract --json exposes it (with a comment explaining why reviewers triaging review_items want the audit row), and the regenerated llms-full.txt that aggregates the contract doc.
1 parent 1ca14d9 commit b4aef44

27 files changed

Lines changed: 5013 additions & 55 deletions

File tree

.well-known/agents-shipgate.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"trust_model": "static_by_default",
3232
"schemas": {
3333
"manifest": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/manifest-v0.1.json",
34-
"report": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.16.json",
34+
"report": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.17.json",
3535
"packet": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/packet-schema.v0.5.json",
3636
"checks_catalog": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/checks.json"
3737
},

AGENTS.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,9 @@ Other stable top-level fields:
249249
- `findings[].provenance_kind` (v0.15+, per-finding rule provenance — `static_declaration | ast_extraction | keyword_heuristic | regex_heuristic | policy_pack`; independent of `confidence`, useful for filtering heuristic-only findings)
250250
- `findings[].blocks_release` (v0.16+, explicit release-policy blockers from Action Surface Diff policies)
251251
- `action_surface_facts` / `action_surface_diff` (v0.16+, deterministic action snapshot and base/head action delta)
252+
- `release_decision.contribution_rules[]` (v0.17+, per-finding audit of how each finding contributed to the decision; one row per `report.findings` entry, with `category``{blocker, review_item, excluded}` and `rule``{policy_block_new, severity_block_new, policy_baseline_accepted, severity_baseline_accepted, review_required, sub_threshold, suppressed}`)
252253

253-
The full schema is at [`docs/report-schema.v0.16.json`](docs/report-schema.v0.16.json) (current; emitted reports carry `report_schema_version: "0.16"`). v0.16 adds first-class Action Surface Diff fields, on top of v0.15's per-finding `provenance_kind` enum, v0.14's `insufficient_evidence` value in the `release_decision.decision`/`agent_summary.verdict` enums, and v0.13's `codex_plugin_surface` block. Older reports validate against [`docs/report-schema.v0.15.json`](docs/report-schema.v0.15.json) (frozen reference). What's-stable is documented in [STABILITY.md](STABILITY.md).
254+
The full schema is at [`docs/report-schema.v0.17.json`](docs/report-schema.v0.17.json) (current; emitted reports carry `report_schema_version: "0.17"`). v0.17 adds the per-finding `release_decision.contribution_rules[]` audit, on top of v0.16's first-class Action Surface Diff fields, v0.15's per-finding `provenance_kind` enum, v0.14's `insufficient_evidence` value in the `release_decision.decision`/`agent_summary.verdict` enums, and v0.13's `codex_plugin_surface` block. Older reports validate against [`docs/report-schema.v0.16.json`](docs/report-schema.v0.16.json) (frozen reference). What's-stable is documented in [STABILITY.md](STABILITY.md).
254255

255256
**Release gating signal**: prefer `release_decision.decision` (`"blocked" | "review_required" | "insufficient_evidence" | "passed"`) over `summary.status`. The new field is **baseline-aware** — a baseline-matched critical surfaces in `release_decision.review_items` (accepted debt), not `release_decision.blockers`. `summary.status` stays baseline-blind for v0.7 compatibility, so a baseline-matched-only critical produces both `summary.status = "release_blockers_detected"` AND `release_decision.decision = "review_required"` (intentional divergence — see [STABILITY.md](STABILITY.md#release_decisiondecision-vs-summarystatus)). `insufficient_evidence` (added v0.14) signals that the scan saw too many low-confidence tools or source-loader warnings to be trustworthy; consumers that switch on the enum must fall back to `review_required` for unknown future values.
256257

@@ -316,7 +317,7 @@ validation and [`docs/manifest-v0.1.md`](docs/manifest-v0.1.md) for prose.
316317
### Where is the report schema?
317318

318319
Parse `agents-shipgate-reports/report.json` and validate against
319-
[`docs/report-schema.v0.16.json`](docs/report-schema.v0.16.json) (current).
320+
[`docs/report-schema.v0.17.json`](docs/report-schema.v0.17.json) (current).
320321
Older reports (`report_schema_version: "0.10"`) validate against the
321322
frozen [`docs/report-schema.v0.10.json`](docs/report-schema.v0.10.json).
322323
Do not scrape Markdown when JSON is available.
@@ -354,7 +355,8 @@ For the short, current statement of "which fields to read", see [`docs/agent-con
354355
| What | Path | Stable |
355356
|---|---|---|
356357
| Manifest schema | [`docs/manifest-v0.1.json`](docs/manifest-v0.1.json) | `0.1` |
357-
| Report schema (current) | [`docs/report-schema.v0.16.json`](docs/report-schema.v0.16.json) | `0.16` |
358+
| Report schema (current) | [`docs/report-schema.v0.17.json`](docs/report-schema.v0.17.json) | `0.17` |
359+
| Report schema (v0.16 frozen reference) | [`docs/report-schema.v0.16.json`](docs/report-schema.v0.16.json) | `0.16` |
358360
| Report schema (v0.15 frozen reference) | [`docs/report-schema.v0.15.json`](docs/report-schema.v0.15.json) | `0.15` |
359361
| Report schema (v0.14 frozen reference) | [`docs/report-schema.v0.14.json`](docs/report-schema.v0.14.json) | `0.14` |
360362
| Report schema (v0.13 frozen reference) | [`docs/report-schema.v0.13.json`](docs/report-schema.v0.13.json) | `0.13` |

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
## Unreleased
44

5+
- Added `release_decision.contribution_rules[]` — a deterministic
6+
per-finding audit of how each finding contributed to the release
7+
decision (M8 of the Trust Hardening Pass). Bumps
8+
`report_schema_version` to `0.17`. Exactly one row per
9+
`report.findings` entry (including suppressed) with `category`
10+
`{blocker, review_item, excluded}` and `rule` ∈ `{policy_block_new,
11+
severity_block_new, policy_baseline_accepted,
12+
severity_baseline_accepted, review_required, sub_threshold,
13+
suppressed}`. The new `STABILITY.md` "Release decision truth table"
14+
documents which `(rule, category)` pair fires for every
15+
`(blocks_release, severity, baseline_status, fail_on)` combination.
16+
Additive only: no semantic change to `decision`, `blockers[]`,
17+
`review_items[]`, `fail_policy.exit_code`, or strict-mode exit codes —
18+
the audit reflects existing behavior, it does not modify it. The
19+
field defaults to `[]` for legacy reports loaded via
20+
`explain-finding` so consumers never need an existence check.
521
- Replaced the hardcoded `if/elif` source-dispatch in `cli/scan.py` with a
622
real `ToolSourceAdapter` Protocol and `AdapterRegistry`. Every loader
723
(MCP, OpenAPI, OpenAI Agents SDK, Google ADK, LangChain, CrewAI, n8n,

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ Set `pr_comment: "true"` to post a compact PR summary:
190190

191191
## What it produces
192192

193-
- **Tool-Use Readiness Report** — `agents-shipgate-reports/report.{md,json,sarif}`. Markdown for human release review, JSON for tools and coding agents (current schema [v0.16](docs/report-schema.v0.16.json); gating signal is `release_decision.decision`; v0.16 adds first-class Action Surface Diff fields on top of v0.15's per-finding `provenance_kind`), SARIF for GitHub code-scanning workflows.
193+
- **Tool-Use Readiness Report** — `agents-shipgate-reports/report.{md,json,sarif}`. Markdown for human release review, JSON for tools and coding agents (current schema [v0.17](docs/report-schema.v0.17.json); gating signal is `release_decision.decision`; v0.17 adds the per-finding `release_decision.contribution_rules[]` audit on top of v0.16's first-class Action Surface Diff fields and v0.15's per-finding `provenance_kind`), SARIF for GitHub code-scanning workflows.
194194
- **Release Evidence Packet** — `agents-shipgate-reports/packet.{md,json,html}` (and `packet.pdf` with the `[pdf]` extras). Reviewer-shaped synthesis with fixed sections, including tool-surface and action-surface diffs when available. Governed by [packet schema v0.5](docs/packet-schema.v0.5.json) — see [STABILITY.md §Release Evidence Packet](STABILITY.md#release-evidence-packet-v05).
195195

196196
## Exit codes
@@ -226,7 +226,7 @@ Agents Shipgate is designed to be agent-friendly. If you're a coding agent (Clau
226226
- **[`prompts/`](prompts/)** — reusable prompts for common workflows
227227
- **[`skills/agents-shipgate/`](skills/agents-shipgate/)** + **[`.claude/commands/shipgate.md`](.claude/commands/shipgate.md)** — self-contained Claude Code skill (bundled prompts and CI recipe) and `/shipgate` slash command. See [`docs/agents/use-with-claude-code.md`](docs/agents/use-with-claude-code.md) to install in your own project.
228228
- **[`docs/ai-search-summary.md`](docs/ai-search-summary.md)** — human-readable summary for AI search, answer engines, and coding agents
229-
- **[`docs/manifest-v0.1.json`](docs/manifest-v0.1.json)** + **[`docs/report-schema.v0.16.json`](docs/report-schema.v0.16.json)** — JSON Schemas for live editor validation (current; emitted reports carry `report_schema_version: "0.16"`). v0.16 adds `action_surface_facts` and `action_surface_diff`; v0.15 added the per-finding `provenance_kind` enum. Read `release_decision.decision` for release gating in new consumers; read `agent_summary.first_recommended_action` for a deterministic next step.
229+
- **[`docs/manifest-v0.1.json`](docs/manifest-v0.1.json)** + **[`docs/report-schema.v0.17.json`](docs/report-schema.v0.17.json)** — JSON Schemas for live editor validation (current; emitted reports carry `report_schema_version: "0.17"`). v0.17 adds `release_decision.contribution_rules[]` (per-finding decision audit); v0.16 added `action_surface_facts` and `action_surface_diff`; v0.15 added the per-finding `provenance_kind` enum. Read `release_decision.decision` for release gating in new consumers; read `agent_summary.first_recommended_action` for a deterministic next step.
230230
- **[`docs/checks.json`](docs/checks.json)** — machine-readable check catalog
231231

232232
Every command has a `--json` form. Errors emit a structured `next_action` line on stderr when `AGENTS_SHIPGATE_AGENT_MODE=1`.
@@ -414,7 +414,7 @@ Agents Shipgate is a static, manifest-first scanner. It is intentionally narrow:
414414
- It does not verify runtime behavior, latency, prompt quality, or routing decisions.
415415
- It does not replace dynamic security testing or human security review of the underlying systems.
416416
- It only inspects what is declared in `shipgate.yaml`, local OpenAPI specs, MCP exports, simple OpenAI API artifacts, optional SDK AST metadata, static Google ADK/LangChain/CrewAI inputs, and static Codex plugin package metadata; tools that are not declared or statically discoverable are not scanned.
417-
- The manifest remains `version: "0.1"` so existing configs keep working. Current reports carry `report_schema_version: "0.16"` (additive over v0.15's provenance enum, adding `action_surface_facts` and `action_surface_diff`) while preserving the stable payload contract documented in the report schema.
417+
- The manifest remains `version: "0.1"` so existing configs keep working. Current reports carry `report_schema_version: "0.17"` (additive over v0.16, adding `release_decision.contribution_rules[]` — a deterministic per-finding audit of how each finding contributed to the release decision) while preserving the stable payload contract documented in the report schema.
418418

419419
See [ROADMAP.md](ROADMAP.md) for what is planned next.
420420

@@ -491,7 +491,7 @@ readers and AI search ingest.
491491
- [Check catalog](docs/checks.md)
492492
- [Policy packs](docs/policy-packs.md)
493493
- [Baseline workflow](docs/baseline.md)
494-
- [JSON report schema v0.16](docs/report-schema.v0.16.json)
494+
- [JSON report schema v0.17](docs/report-schema.v0.17.json)
495495
- [Trust model](docs/trust-model.md)
496496
- [AI search summary](docs/ai-search-summary.md)
497497
- [Design partners](docs/design-partners.md)

STABILITY.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ In `agents-shipgate-reports/report.json`, the following are guaranteed:
9696
- `findings[].blocks_release` (v0.16+) — explicit release-policy blocking bit. Built-in and user-defined Action Surface Diff policies, plus declarative policy-pack rules with `block: true`, set it for findings that must block release when active and unbaselined; ordinary severity-based gating still works for existing checks.
9797
- `action_surface_facts.actions[]` (v0.16+) — deterministic current action snapshot: action id, operation, effect, normalized risk tags, scopes, approval policy, safeguards, evidence, input fields, and stable hashes.
9898
- `action_surface_diff.{enabled, base, summary, added, removed, modified, notes}` (v0.16+) — reviewer-facing delta for what the agent can do vs. a prior report or v0.4 baseline. Policy findings derived from this diff can set `findings[].blocks_release=true` and affect `release_decision.decision` and strict-mode exit behavior.
99+
- `release_decision.contribution_rules[].{finding_id, fingerprint, check_id, category, rule, rationale}` (v0.17+) — deterministic per-finding audit of how each finding contributed to the release decision. Required + always present (defaults to `[]` for legacy reports loaded via `explain-finding`). Exactly one row per `report.findings` entry, including suppressed findings, so the audit set is exhaustive over the full findings list. `category` enum: `blocker | review_item | excluded`. `rule` enum: `policy_block_new | severity_block_new | policy_baseline_accepted | severity_baseline_accepted | review_required | sub_threshold | suppressed`. The (rule, category) pairs the gate can produce are exhaustively documented in [Release decision truth table](#release-decision-truth-table) below — reading the contribution rule is sufficient to predict the outcome for that finding without re-deriving the decision logic. The audit cannot disagree with `release_decision.{blockers,review_items}[]`: the same classification powers both. Adding `contribution_rules` does not change any existing behavior — `decision`, `blockers[]`, `review_items[]`, `fail_policy.exit_code`, and strict-mode exit codes are byte-identical to v0.16.
99100
- `baseline.{matched_count, new_count, resolved_count, path}` (when `--baseline` is used)
100101
- `tool_inventory[].{name, source_type, source_ref, risk_tags, auth_scopes, owner, confidence}`
101102
- `loaded_plugins[].{name, value, distribution, version, check_id}`
@@ -126,6 +127,31 @@ These are **intentionally different signals**, kept apart for backwards compatib
126127
| `release_decision.decision` | yes — baseline-matched criticals appear in `review_items`, not `blockers` | **yes (v0.8+)** |
127128
| `summary.status` | no — any unsuppressed critical flips status to `release_blockers_detected` | preserved for v0.7 callers |
128129

130+
#### Release decision truth table
131+
132+
The classification below is the contract for how every active finding lands in `release_decision.{blockers, review_items}[]` and which `contribution_rules[].rule` (v0.17+) fires for it. Same shape as the v0.8 implementation: this section documents existing behavior, it does not change it. Suppressed findings (`finding.suppressed=true`) are excluded entirely from the active set and audited as `category="excluded", rule="suppressed"`.
133+
134+
Notation: `fail_on` is `release_decision.fail_policy.fail_on` after `ci_mode` resolution (advisory → empty, strict → `["critical"]`, plus any explicit `--fail-on` override). `blocker_severities` = `{critical} ∪ fail_on`. `review_tier` = `{critical, high, medium}` (or any severity when `requires_human_review=true`).
135+
136+
| `blocks_release` | severity | baseline_status | severity in `blocker_severities`? | severity in `review_tier`? | category | `rule` | strict-mode exit |
137+
|---|---|---|---|---|---|---|---|
138+
| true | any | new / null | n/a | n/a | **blocker** | `policy_block_new` | 20 |
139+
| true | any | matched | n/a | yes | review_item | `policy_baseline_accepted` | 0 (with `--baseline-mode new-findings`) |
140+
| true | any | matched | n/a | no | excluded | `policy_baseline_accepted` | 0 (with `--baseline-mode new-findings`) |
141+
| true | any | resolved | n/a | n/a | excluded | (not produced; resolved findings are absent from the active set) | 0 |
142+
| false | any | new / null | yes | n/a | **blocker** | `severity_block_new` | 20 |
143+
| false | any | matched | yes | yes | review_item | `severity_baseline_accepted` | 0 (with `--baseline-mode new-findings`) |
144+
| false | any | matched | yes | no | excluded | `severity_baseline_accepted` | 0 (with `--baseline-mode new-findings`) |
145+
| false | any | new / null | no | yes | review_item | `review_required` | 0 |
146+
| false | any | matched | no | yes | review_item | `review_required` | 0 |
147+
| false | any | new / null / matched | no | no | excluded | `sub_threshold` | 0 |
148+
149+
**Why baseline-matched policy findings drop to `review_items`, not `blockers`.** `blocks_release=true` represents an explicit *policy* decision (Action Surface Diff rule, `action_surface:` manifest entry, or policy-pack rule with `block: true`) that the finding must block release **on first appearance**. A baseline accepts technical debt that already passed prior review — the project agreed to ship with that finding present. Treating baselined policy debt as a hard blocker would defeat the purpose of `baseline save`. The baseline-aware drop is symmetric for severity-driven blockers and policy blockers: both land in `review_items` once accepted into the baseline, both become hard blockers if newly introduced.
150+
151+
**Why `severity ∈ blocker_severities + matched + below review_tier` lands in `excluded`, not `review_items`.** A finding whose severity isn't in `{critical, high, medium}` (and which doesn't carry `requires_human_review=true`) has nothing for a human reviewer to act on per the v0.8 contract — it's been baselined and isn't severe enough to warrant attention. v0.17 records this in the audit so the (rare) edge case isn't silently invisible, but the `blockers[]`/`review_items[]` lists themselves are unchanged.
152+
153+
**Why exit code 20 depends on `--baseline-mode`.** `release_decision.{blockers, review_items}[]` always include the full set computed against `report.findings` (with suppressed excluded). The strict-mode exit code, however, is computed from `baseline_filtered_active(report, new_findings_only=...)` — when `--baseline-mode new-findings` is set (the default for the GitHub Action when `baseline:` is provided), baseline-matched policy and severity blockers are filtered out before the exit check, so exit is `0`. With `new_findings_only=False`, a matched policy blocker still triggers exit 20. The `release_decision` block remains baseline-aware in all cases; only the exit-code path changes mode.
154+
129155
Concretely: a scan with one baseline-matched critical and zero new findings produces `summary.status = "release_blockers_detected"` AND `release_decision.decision = "review_required"`. Both are correct under their respective contracts. New consumers should read `release_decision.decision`.
130156

131157
### Check IDs

docs/INDEX.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ A single entry point for human readers and AI agents walking the `docs/` tree.
2121
- [`checks.md`](checks.md) — full check catalog (human-readable)
2222
- [`checks.json`](checks.json) — machine-readable check catalog (regenerated each release)
2323
- [`manifest-v0.1.json`](manifest-v0.1.json) — JSON Schema for `shipgate.yaml`
24-
- [`report-schema.v0.16.json`](report-schema.v0.16.json) — JSON Schema for `report.json` (current; emitted reports carry `report_schema_version: "0.16"`, which adds first-class Action Surface Diff fields)
24+
- [`report-schema.v0.17.json`](report-schema.v0.17.json) — JSON Schema for `report.json` (current; emitted reports carry `report_schema_version: "0.17"`, which adds the per-finding `release_decision.contribution_rules[]` audit on top of v0.16's first-class Action Surface Diff fields)
2525
- [`agent-action-guide.md`](agent-action-guide.md) — per-category recipe for what to do with a finding (canonical fix per check category, last-resort suppression rules)
2626
- [`upstream-integrations.md`](upstream-integrations.md) — per-framework 60-second drop-in for adding Shipgate to an existing project (OpenAI Agents SDK, LangChain, CrewAI, ADK, MCP-only, OpenAPI-only, OpenAI Messages API, Anthropic Messages API)
27+
- [`report-schema.v0.16.json`](report-schema.v0.16.json) — frozen v0.16 reference schema; pre-v0.17 reports validate against this
2728
- [`report-schema.v0.15.json`](report-schema.v0.15.json) — frozen v0.15 reference schema; pre-v0.16 reports validate against this
2829
- [`report-schema.v0.14.json`](report-schema.v0.14.json) — frozen v0.14 reference schema; pre-v0.15 reports validate against this
2930
- [`report-schema.v0.13.json`](report-schema.v0.13.json) — frozen v0.13 reference schema; pre-v0.14 reports validate against this

0 commit comments

Comments
 (0)