diff --git a/.well-known/agents-shipgate.json b/.well-known/agents-shipgate.json index d4bbc66..cb9a951 100644 --- a/.well-known/agents-shipgate.json +++ b/.well-known/agents-shipgate.json @@ -31,7 +31,7 @@ "trust_model": "static_by_default", "schemas": { "manifest": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/manifest-v0.1.json", - "report": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.16.json", + "report": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.17.json", "packet": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/packet-schema.v0.5.json", "checks_catalog": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/checks.json" }, diff --git a/AGENTS.md b/AGENTS.md index 2c009de..fc5b906 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -249,8 +249,9 @@ Other stable top-level fields: - `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) - `findings[].blocks_release` (v0.16+, explicit release-policy blockers from Action Surface Diff policies) - `action_surface_facts` / `action_surface_diff` (v0.16+, deterministic action snapshot and base/head action delta) +- `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}`) -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). +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). **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. @@ -316,7 +317,7 @@ validation and [`docs/manifest-v0.1.md`](docs/manifest-v0.1.md) for prose. ### Where is the report schema? Parse `agents-shipgate-reports/report.json` and validate against -[`docs/report-schema.v0.16.json`](docs/report-schema.v0.16.json) (current). +[`docs/report-schema.v0.17.json`](docs/report-schema.v0.17.json) (current). Older reports (`report_schema_version: "0.10"`) validate against the frozen [`docs/report-schema.v0.10.json`](docs/report-schema.v0.10.json). 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 | What | Path | Stable | |---|---|---| | Manifest schema | [`docs/manifest-v0.1.json`](docs/manifest-v0.1.json) | `0.1` | -| Report schema (current) | [`docs/report-schema.v0.16.json`](docs/report-schema.v0.16.json) | `0.16` | +| Report schema (current) | [`docs/report-schema.v0.17.json`](docs/report-schema.v0.17.json) | `0.17` | +| Report schema (v0.16 frozen reference) | [`docs/report-schema.v0.16.json`](docs/report-schema.v0.16.json) | `0.16` | | Report schema (v0.15 frozen reference) | [`docs/report-schema.v0.15.json`](docs/report-schema.v0.15.json) | `0.15` | | Report schema (v0.14 frozen reference) | [`docs/report-schema.v0.14.json`](docs/report-schema.v0.14.json) | `0.14` | | Report schema (v0.13 frozen reference) | [`docs/report-schema.v0.13.json`](docs/report-schema.v0.13.json) | `0.13` | diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e4371..3124100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ ## Unreleased +- Added `release_decision.contribution_rules[]` — a deterministic + per-finding audit of how each finding contributed to the release + decision (M8 of the Trust Hardening Pass). Bumps + `report_schema_version` to `0.17`. Exactly one row per + `report.findings` entry (including suppressed) 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}`. The new `STABILITY.md` "Release decision truth table" + documents which `(rule, category)` pair fires for every + `(blocks_release, severity, baseline_status, fail_on)` combination. + Additive only: no semantic change to `decision`, `blockers[]`, + `review_items[]`, `fail_policy.exit_code`, or strict-mode exit codes — + the audit reflects existing behavior, it does not modify it. The + field defaults to `[]` for legacy reports loaded via + `explain-finding` so consumers never need an existence check. - Replaced the hardcoded `if/elif` source-dispatch in `cli/scan.py` with a real `ToolSourceAdapter` Protocol and `AdapterRegistry`. Every loader (MCP, OpenAPI, OpenAI Agents SDK, Google ADK, LangChain, CrewAI, n8n, diff --git a/README.md b/README.md index ebbd277..01e49b6 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ Set `pr_comment: "true"` to post a compact PR summary: ## What it produces -- **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. +- **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. - **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). ## Exit codes @@ -226,7 +226,7 @@ Agents Shipgate is designed to be agent-friendly. If you're a coding agent (Clau - **[`prompts/`](prompts/)** — reusable prompts for common workflows - **[`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. - **[`docs/ai-search-summary.md`](docs/ai-search-summary.md)** — human-readable summary for AI search, answer engines, and coding agents -- **[`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. +- **[`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. - **[`docs/checks.json`](docs/checks.json)** — machine-readable check catalog 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: - It does not verify runtime behavior, latency, prompt quality, or routing decisions. - It does not replace dynamic security testing or human security review of the underlying systems. - 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. -- 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. +- 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. See [ROADMAP.md](ROADMAP.md) for what is planned next. @@ -491,7 +491,7 @@ readers and AI search ingest. - [Check catalog](docs/checks.md) - [Policy packs](docs/policy-packs.md) - [Baseline workflow](docs/baseline.md) -- [JSON report schema v0.16](docs/report-schema.v0.16.json) +- [JSON report schema v0.17](docs/report-schema.v0.17.json) - [Trust model](docs/trust-model.md) - [AI search summary](docs/ai-search-summary.md) - [Design partners](docs/design-partners.md) diff --git a/STABILITY.md b/STABILITY.md index ec58276..6b84e86 100644 --- a/STABILITY.md +++ b/STABILITY.md @@ -96,6 +96,7 @@ In `agents-shipgate-reports/report.json`, the following are guaranteed: - `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. - `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. - `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. +- `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. - `baseline.{matched_count, new_count, resolved_count, path}` (when `--baseline` is used) - `tool_inventory[].{name, source_type, source_ref, risk_tags, auth_scopes, owner, confidence}` - `loaded_plugins[].{name, value, distribution, version, check_id}` @@ -126,6 +127,31 @@ These are **intentionally different signals**, kept apart for backwards compatib | `release_decision.decision` | yes — baseline-matched criticals appear in `review_items`, not `blockers` | **yes (v0.8+)** | | `summary.status` | no — any unsuppressed critical flips status to `release_blockers_detected` | preserved for v0.7 callers | +#### Release decision truth table + +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"`. + +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`). + +| `blocks_release` | severity | baseline_status | severity in `blocker_severities`? | severity in `review_tier`? | category | `rule` | strict-mode exit | +|---|---|---|---|---|---|---|---| +| true | any | new / null | n/a | n/a | **blocker** | `policy_block_new` | 20 | +| true | any | matched | n/a | yes | review_item | `policy_baseline_accepted` | 0 (with `--baseline-mode new-findings`) | +| true | any | matched | n/a | no | excluded | `policy_baseline_accepted` | 0 (with `--baseline-mode new-findings`) | +| true | any | resolved | n/a | n/a | excluded | (not produced; resolved findings are absent from the active set) | 0 | +| false | any | new / null | yes | n/a | **blocker** | `severity_block_new` | 20 | +| false | any | matched | yes | yes | review_item | `severity_baseline_accepted` | 0 (with `--baseline-mode new-findings`) | +| false | any | matched | yes | no | excluded | `severity_baseline_accepted` | 0 (with `--baseline-mode new-findings`) | +| false | any | new / null | no | yes | review_item | `review_required` | 0 | +| false | any | matched | no | yes | review_item | `review_required` | 0 | +| false | any | new / null / matched | no | no | excluded | `sub_threshold` | 0 | + +**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. + +**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. + +**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. + 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`. ### Check IDs diff --git a/docs/INDEX.md b/docs/INDEX.md index 3906eb3..87a6aef 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -21,9 +21,10 @@ A single entry point for human readers and AI agents walking the `docs/` tree. - [`checks.md`](checks.md) — full check catalog (human-readable) - [`checks.json`](checks.json) — machine-readable check catalog (regenerated each release) - [`manifest-v0.1.json`](manifest-v0.1.json) — JSON Schema for `shipgate.yaml` -- [`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) +- [`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) - [`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) - [`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) +- [`report-schema.v0.16.json`](report-schema.v0.16.json) — frozen v0.16 reference schema; pre-v0.17 reports validate against this - [`report-schema.v0.15.json`](report-schema.v0.15.json) — frozen v0.15 reference schema; pre-v0.16 reports validate against this - [`report-schema.v0.14.json`](report-schema.v0.14.json) — frozen v0.14 reference schema; pre-v0.15 reports validate against this - [`report-schema.v0.13.json`](report-schema.v0.13.json) — frozen v0.13 reference schema; pre-v0.14 reports validate against this diff --git a/docs/agent-contract-current.md b/docs/agent-contract-current.md index 14605ed..bf8023f 100644 --- a/docs/agent-contract-current.md +++ b/docs/agent-contract-current.md @@ -12,9 +12,9 @@ agents-shipgate contract --json - Latest release: `v0.10.0` (see [pyproject.toml](../pyproject.toml) for the in-tree version) - Runtime contract: `1` -- Current report schema: `0.16` — [`docs/report-schema.v0.16.json`](report-schema.v0.16.json) +- Current report schema: `0.17` — [`docs/report-schema.v0.17.json`](report-schema.v0.17.json) - Current packet schema: `0.5` — [`docs/packet-schema.v0.5.json`](packet-schema.v0.5.json) -- Frozen-reference report schemas: [`v0.15`](report-schema.v0.15.json), [`v0.14`](report-schema.v0.14.json), [`v0.13`](report-schema.v0.13.json), [`v0.12`](report-schema.v0.12.json), [`v0.11`](report-schema.v0.11.json), [`v0.10`](report-schema.v0.10.json), [`v0.9`](report-schema.v0.9.json), [`v0.8`](report-schema.v0.8.json), [`v0.7`](report-schema.v0.7.json), [`v0.6`](report-schema.v0.6.json), older +- Frozen-reference report schemas: [`v0.16`](report-schema.v0.16.json), [`v0.15`](report-schema.v0.15.json), [`v0.14`](report-schema.v0.14.json), [`v0.13`](report-schema.v0.13.json), [`v0.12`](report-schema.v0.12.json), [`v0.11`](report-schema.v0.11.json), [`v0.10`](report-schema.v0.10.json), [`v0.9`](report-schema.v0.9.json), [`v0.8`](report-schema.v0.8.json), [`v0.7`](report-schema.v0.7.json), [`v0.6`](report-schema.v0.6.json), older ## Read these first for release gating @@ -25,6 +25,7 @@ In `agents-shipgate-reports/report.json`: - `release_decision.review_items[]` — items the human reviewer should look at; includes baseline-matched accepted debt. - `release_decision.fail_policy.would_fail_ci` — `true`/`false`. Matches what the CI process will exit with. - `release_decision.reason` — one-sentence explanation suitable for a PR comment. +- `release_decision.contribution_rules[]` (v0.17+) — deterministic per-finding audit explaining how each `report.findings` entry was classified. Exactly one row per finding (including suppressed). Each row carries `{finding_id, fingerprint, check_id, category, rule, rationale}`. `category` ∈ `{blocker, review_item, excluded}`; `rule` ∈ `{policy_block_new, severity_block_new, policy_baseline_accepted, severity_baseline_accepted, review_required, sub_threshold, suppressed}`. Reading the contribution rule is sufficient to predict the gate outcome for that finding without re-deriving the decision logic — the closed grammar of `(rule, category)` pairs is documented in [STABILITY.md "Release decision truth table"](../STABILITY.md#release-decision-truth-table). The audit cannot disagree with `blockers[]` / `review_items[]` (the same classification powers both). The action exposes these as outputs `decision`, `blocker_count`, `review_item_count`, `ci_would_fail` (v0.8+). @@ -127,7 +128,7 @@ Companion prompt: [`prompts/explain-finding-to-user.md`](../prompts/explain-find - [STABILITY.md](../STABILITY.md) — full 0.x stability contract. Source of truth for everything above. - [AGENTS.md](../AGENTS.md) — agent-facing instructions: install, run, single-turn flow, error semantics. -- [`docs/report-schema.v0.16.json`](report-schema.v0.16.json) — machine-validatable JSON Schema for the current report. +- [`docs/report-schema.v0.17.json`](report-schema.v0.17.json) — machine-validatable JSON Schema for the current report. - [`docs/packet-schema.v0.5.json`](packet-schema.v0.5.json) — machine-validatable JSON Schema for the current packet. - [`docs/checks.json`](checks.json) — check catalog. diff --git a/docs/autofix-policy.md b/docs/autofix-policy.md index c519862..9b0e36a 100644 --- a/docs/autofix-policy.md +++ b/docs/autofix-policy.md @@ -204,7 +204,7 @@ from the hash so toggling `--suggest-patches` doesn't shift it. - [`checks.md`](checks.md) — full check catalog with rationale. - [`minimal-real-configs.md`](minimal-real-configs.md) — per-framework minimal manifests to build from. -- [`report-schema.v0.16.json`](report-schema.v0.16.json) — current JSON +- [`report-schema.v0.17.json`](report-schema.v0.17.json) — current JSON Schema for `report.json`. - [`AGENTS.md`](../AGENTS.md) — top-level agent instructions, install, trigger table. diff --git a/docs/baseline.md b/docs/baseline.md index 076b29b..f386f84 100644 --- a/docs/baseline.md +++ b/docs/baseline.md @@ -37,7 +37,7 @@ fail CI. Reports keep the v0.1 payload contract and add baseline fields: -- `report_schema_version: "0.16"` in current reports +- `report_schema_version: "0.17"` in current reports - `baseline.path` - `baseline.matched_count` - `baseline.new_count` diff --git a/docs/examples.md b/docs/examples.md index bbcff0d..5796128 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -47,7 +47,8 @@ The canonical fixture writes: - `agents-shipgate-reports/report.sarif` when requested or when using the GitHub Action The JSON output is the stable contract for tools and coding agents. See -[report-schema.v0.16.json](report-schema.v0.16.json) (current; emitted reports -carry `report_schema_version: "0.16"`, adding first-class -`action_surface_facts` and `action_surface_diff` on top of v0.15's -per-finding `provenance_kind` enum). +[report-schema.v0.17.json](report-schema.v0.17.json) (current; emitted reports +carry `report_schema_version: "0.17"`, adding the per-finding +`release_decision.contribution_rules[]` audit on top of v0.16's +first-class `action_surface_facts` and `action_surface_diff` and +v0.15's per-finding `provenance_kind` enum). diff --git a/docs/report-reading-for-agents.md b/docs/report-reading-for-agents.md index 0ebdcd6..21443bd 100644 --- a/docs/report-reading-for-agents.md +++ b/docs/report-reading-for-agents.md @@ -167,7 +167,7 @@ Surface the `next_action` to the user rather than scraping prose. The full diagn | Schema | Current | Frozen references | File | |---|---|---|---| -| Report | `0.16` | `0.15`, `0.14`, `0.13`, `0.12`, `0.11`, `0.10`, `0.9`, `0.8`, `0.7`, `0.6`, `0.5`, `0.4`, `0.3`, `0.2`, `0.1` | [`report-schema.v0.16.json`](report-schema.v0.16.json) | +| Report | `0.17` | `0.16`, `0.15`, `0.14`, `0.13`, `0.12`, `0.11`, `0.10`, `0.9`, `0.8`, `0.7`, `0.6`, `0.5`, `0.4`, `0.3`, `0.2`, `0.1` | [`report-schema.v0.17.json`](report-schema.v0.17.json) | | Packet | `0.5` | `0.4`, `0.3`, `0.2`, `0.1` | [`packet-schema.v0.5.json`](packet-schema.v0.5.json) | | Manifest | `0.1` | — | [`manifest-v0.1.json`](manifest-v0.1.json) | | CLI contract | `1` | — | `agents-shipgate contract --json` | diff --git a/docs/report-schema.v0.17.json b/docs/report-schema.v0.17.json new file mode 100644 index 0000000..91a4c12 --- /dev/null +++ b/docs/report-schema.v0.17.json @@ -0,0 +1,3929 @@ +{ + "$defs": { + "ActionApprovalFact": { + "additionalProperties": false, + "properties": { + "required": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Required" + }, + "threshold": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Threshold" + } + }, + "required": [ + "required", + "threshold" + ], + "title": "ActionApprovalFact", + "type": "object" + }, + "ActionEvidenceFact": { + "additionalProperties": false, + "properties": { + "approval_ticket": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Approval Ticket" + }, + "owner": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Owner" + }, + "runbook": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Runbook" + } + }, + "required": [ + "approval_ticket", + "owner", + "runbook" + ], + "title": "ActionEvidenceFact", + "type": "object" + }, + "ActionFact": { + "additionalProperties": false, + "properties": { + "action_id": { + "title": "Action Id", + "type": "string" + }, + "agent_id": { + "title": "Agent Id", + "type": "string" + }, + "approval_policy": { + "$ref": "#/$defs/ActionApprovalFact" + }, + "effect": { + "enum": [ + "read", + "write", + "destructive", + "external_communication", + "financial_write", + "production_operation", + "privileged_data_access", + "code_execution", + "identity_access" + ], + "title": "Effect", + "type": "string" + }, + "evidence": { + "$ref": "#/$defs/ActionEvidenceFact" + }, + "hashes": { + "$ref": "#/$defs/ActionSurfaceHashes" + }, + "input_fields": { + "items": { + "type": "string" + }, + "title": "Input Fields", + "type": "array" + }, + "input_schema_hash": { + "title": "Input Schema Hash", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "required_input_fields": { + "items": { + "type": "string" + }, + "title": "Required Input Fields", + "type": "array" + }, + "required_scopes": { + "items": { + "type": "string" + }, + "title": "Required Scopes", + "type": "array" + }, + "risk_tags": { + "items": { + "type": "string" + }, + "title": "Risk Tags", + "type": "array" + }, + "safeguards": { + "$ref": "#/$defs/ActionSafeguardsFact" + }, + "source_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Id" + }, + "source_type": { + "title": "Source Type", + "type": "string" + }, + "tool_id": { + "title": "Tool Id", + "type": "string" + }, + "tool_name": { + "title": "Tool Name", + "type": "string" + } + }, + "required": [ + "action_id", + "agent_id", + "approval_policy", + "effect", + "evidence", + "hashes", + "input_fields", + "input_schema_hash", + "operation", + "provider", + "required_input_fields", + "required_scopes", + "risk_tags", + "safeguards", + "source_id", + "source_type", + "tool_id", + "tool_name" + ], + "title": "ActionFact", + "type": "object" + }, + "ActionSafeguardsFact": { + "additionalProperties": false, + "properties": { + "audit_log": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Audit Log" + }, + "dry_run": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dry Run" + }, + "idempotency": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Idempotency" + }, + "rollback": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rollback" + } + }, + "required": [ + "audit_log", + "dry_run", + "idempotency", + "rollback" + ], + "title": "ActionSafeguardsFact", + "type": "object" + }, + "ActionSurfaceChange": { + "additionalProperties": false, + "properties": { + "action_id": { + "title": "Action Id", + "type": "string" + }, + "added": { + "items": { + "type": "string" + }, + "title": "Added", + "type": "array" + }, + "after": { + "default": null, + "title": "After" + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Agent Id" + }, + "before": { + "default": null, + "title": "Before" + }, + "operation": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Operation" + }, + "reason": { + "title": "Reason", + "type": "string" + }, + "removed": { + "items": { + "type": "string" + }, + "title": "Removed", + "type": "array" + }, + "severity": { + "default": "info", + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "title": "Severity", + "type": "string" + }, + "tool_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Name" + }, + "type": { + "enum": [ + "ACTION_ADDED", + "ACTION_REMOVED", + "ACTION_MODIFIED", + "SCOPE_EXPANDED", + "EFFECT_ESCALATED", + "RISK_TAG_ADDED", + "APPROVAL_REMOVED", + "SAFEGUARD_REMOVED", + "INPUT_SCHEMA_EXPANDED" + ], + "title": "Type", + "type": "string" + } + }, + "required": [ + "action_id", + "added", + "after", + "agent_id", + "before", + "operation", + "reason", + "removed", + "severity", + "tool_name", + "type" + ], + "title": "ActionSurfaceChange", + "type": "object" + }, + "ActionSurfaceDiff": { + "additionalProperties": false, + "properties": { + "added": { + "items": { + "$ref": "#/$defs/ActionSurfaceChange" + }, + "title": "Added", + "type": "array" + }, + "base": { + "$ref": "#/$defs/ToolSurfaceDiffBase" + }, + "enabled": { + "default": false, + "title": "Enabled", + "type": "boolean" + }, + "modified": { + "items": { + "$ref": "#/$defs/ActionSurfaceChange" + }, + "title": "Modified", + "type": "array" + }, + "notes": { + "items": { + "type": "string" + }, + "title": "Notes", + "type": "array" + }, + "removed": { + "items": { + "$ref": "#/$defs/ActionSurfaceChange" + }, + "title": "Removed", + "type": "array" + }, + "summary": { + "$ref": "#/$defs/ActionSurfaceDiffSummary" + } + }, + "required": [ + "added", + "base", + "enabled", + "modified", + "notes", + "removed", + "summary" + ], + "title": "ActionSurfaceDiff", + "type": "object" + }, + "ActionSurfaceDiffSummary": { + "additionalProperties": false, + "properties": { + "actions_added": { + "default": 0, + "title": "Actions Added", + "type": "integer" + }, + "actions_modified": { + "default": 0, + "title": "Actions Modified", + "type": "integer" + }, + "actions_removed": { + "default": 0, + "title": "Actions Removed", + "type": "integer" + }, + "approvals_removed": { + "default": 0, + "title": "Approvals Removed", + "type": "integer" + }, + "blocking_findings": { + "default": 0, + "title": "Blocking Findings", + "type": "integer" + }, + "effect_escalations": { + "default": 0, + "title": "Effect Escalations", + "type": "integer" + }, + "input_schema_expansions": { + "default": 0, + "title": "Input Schema Expansions", + "type": "integer" + }, + "risk_tags_added": { + "default": 0, + "title": "Risk Tags Added", + "type": "integer" + }, + "safeguards_removed": { + "default": 0, + "title": "Safeguards Removed", + "type": "integer" + }, + "scope_expansions": { + "default": 0, + "title": "Scope Expansions", + "type": "integer" + } + }, + "required": [ + "actions_added", + "actions_modified", + "actions_removed", + "approvals_removed", + "blocking_findings", + "effect_escalations", + "input_schema_expansions", + "risk_tags_added", + "safeguards_removed", + "scope_expansions" + ], + "title": "ActionSurfaceDiffSummary", + "type": "object" + }, + "ActionSurfaceFacts": { + "additionalProperties": false, + "properties": { + "actions": { + "items": { + "$ref": "#/$defs/ActionFact" + }, + "title": "Actions", + "type": "array" + }, + "snapshot_version": { + "default": "0.1", + "title": "Snapshot Version", + "type": "string" + } + }, + "required": [ + "actions", + "snapshot_version" + ], + "title": "ActionSurfaceFacts", + "type": "object" + }, + "ActionSurfaceHashes": { + "additionalProperties": false, + "properties": { + "identity_hash": { + "title": "Identity Hash", + "type": "string" + }, + "policy_hash": { + "title": "Policy Hash", + "type": "string" + }, + "risk_hash": { + "title": "Risk Hash", + "type": "string" + }, + "schema_hash": { + "title": "Schema Hash", + "type": "string" + } + }, + "required": [ + "identity_hash", + "policy_hash", + "risk_hash", + "schema_hash" + ], + "title": "ActionSurfaceHashes", + "type": "object" + }, + "AgentSummary": { + "additionalProperties": false, + "description": "Top-level summary block shaped for one-fetch agent consumption.\n\nDeterministic projection of (``release_decision``, ``findings[].agent_action``).\nA coding agent that wants the headline numbers can read this block\ninstead of traversing arrays. All fields are derived; this block\ncannot disagree with the underlying data.", + "properties": { + "auto_appliable_patches": { + "default": 0, + "title": "Auto Appliable Patches", + "type": "integer" + }, + "blocker_count": { + "default": 0, + "title": "Blocker Count", + "type": "integer" + }, + "first_recommended_action": { + "anyOf": [ + { + "$ref": "#/$defs/AgentSummaryAction" + }, + { + "type": "null" + } + ], + "default": null + }, + "headline": { + "title": "Headline", + "type": "string" + }, + "needs_human_review": { + "default": 0, + "title": "Needs Human Review", + "type": "integer" + }, + "review_item_count": { + "default": 0, + "title": "Review Item Count", + "type": "integer" + }, + "verdict": { + "enum": [ + "blocked", + "review_required", + "insufficient_evidence", + "passed" + ], + "title": "Verdict", + "type": "string" + } + }, + "required": [ + "auto_appliable_patches", + "blocker_count", + "first_recommended_action", + "headline", + "needs_human_review", + "review_item_count", + "verdict" + ], + "title": "AgentSummary", + "type": "object" + }, + "AgentSummaryAction": { + "additionalProperties": false, + "description": "A single recommended next step shaped for direct agent consumption.\n\nMirrors the ``next_actions[]`` shape used elsewhere in the contract\n(kind/command/why) so callers that already handle diagnostic\nnext_actions can reuse the same renderer here.", + "properties": { + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Command" + }, + "kind": { + "default": "command", + "enum": [ + "command", + "info" + ], + "title": "Kind", + "type": "string" + }, + "why": { + "title": "Why", + "type": "string" + } + }, + "required": [ + "command", + "kind", + "why" + ], + "title": "AgentSummaryAction", + "type": "object" + }, + "AppendPointerPatch": { + "additionalProperties": false, + "description": "Append a value to the list at a JSON pointer.", + "properties": { + "confidence": { + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "kind": { + "const": "append_pointer", + "default": "append_pointer", + "title": "Kind", + "type": "string" + }, + "pointer": { + "title": "Pointer", + "type": "string" + }, + "rationale": { + "title": "Rationale", + "type": "string" + }, + "target_file": { + "title": "Target File", + "type": "string" + }, + "target_format": { + "enum": [ + "yaml", + "json" + ], + "title": "Target Format", + "type": "string" + }, + "target_sha256": { + "title": "Target Sha256", + "type": "string" + }, + "value": { + "title": "Value" + } + }, + "required": [ + "target_file", + "pointer", + "value", + "target_format", + "confidence", + "rationale", + "target_sha256" + ], + "title": "AppendPointerPatch", + "type": "object" + }, + "BaselineDelta": { + "properties": { + "enabled": { + "title": "Enabled", + "type": "boolean" + }, + "matched_count": { + "default": 0, + "title": "Matched Count", + "type": "integer" + }, + "new_count": { + "default": 0, + "title": "New Count", + "type": "integer" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "resolved_count": { + "default": 0, + "title": "Resolved Count", + "type": "integer" + } + }, + "required": [ + "enabled", + "matched_count", + "new_count", + "resolved_count" + ], + "title": "BaselineDelta", + "type": "object" + }, + "BaselineSummary": { + "properties": { + "matched_count": { + "default": 0, + "title": "Matched Count", + "type": "integer" + }, + "new_count": { + "default": 0, + "title": "New Count", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "resolved_count": { + "default": 0, + "title": "Resolved Count", + "type": "integer" + } + }, + "required": [ + "path" + ], + "title": "BaselineSummary", + "type": "object" + }, + "CapabilityFact": { + "properties": { + "auth_scopes": { + "items": { + "type": "string" + }, + "title": "Auth Scopes", + "type": "array" + }, + "capability": { + "title": "Capability", + "type": "string" + }, + "control_status": { + "enum": [ + "missing", + "partial", + "present", + "unknown" + ], + "title": "Control Status", + "type": "string" + }, + "id": { + "title": "Id", + "type": "string" + }, + "included_reason": { + "enum": [ + "high_risk_tag", + "wildcard_exposure", + "referenced_by_critical_finding", + "referenced_by_high_finding", + "referenced_by_medium_finding" + ], + "title": "Included Reason", + "type": "string" + }, + "owner": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Owner" + }, + "related_findings": { + "items": { + "type": "string" + }, + "title": "Related Findings", + "type": "array" + }, + "risk_tags": { + "items": { + "type": "string" + }, + "title": "Risk Tags", + "type": "array" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + }, + "source_type": { + "title": "Source Type", + "type": "string" + }, + "tool_name": { + "title": "Tool Name", + "type": "string" + } + }, + "required": [ + "auth_scopes", + "capability", + "control_status", + "id", + "included_reason", + "owner", + "related_findings", + "risk_tags", + "source_ref", + "source_type", + "tool_name" + ], + "title": "CapabilityFact", + "type": "object" + }, + "CodexPluginAppSummary": { + "additionalProperties": true, + "properties": { + "connector_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Connector Id" + }, + "location": { + "$ref": "#/$defs/CodexPluginSourceLocation" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "plugin": { + "title": "Plugin", + "type": "string" + } + }, + "required": [ + "plugin", + "name", + "path" + ], + "title": "CodexPluginAppSummary", + "type": "object" + }, + "CodexPluginComponentPathIssue": { + "additionalProperties": true, + "properties": { + "component": { + "title": "Component", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "plugin": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Plugin" + }, + "reason": { + "title": "Reason", + "type": "string" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + } + }, + "required": [ + "component", + "path", + "reason" + ], + "title": "CodexPluginComponentPathIssue", + "type": "object" + }, + "CodexPluginHookStub": { + "additionalProperties": true, + "properties": { + "command": { + "title": "Command", + "type": "string" + }, + "confidence": { + "default": "medium", + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "location": { + "$ref": "#/$defs/CodexPluginSourceLocation" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "plugin": { + "title": "Plugin", + "type": "string" + }, + "risk_tags": { + "items": { + "type": "string" + }, + "title": "Risk Tags", + "type": "array" + } + }, + "required": [ + "plugin", + "name", + "command", + "path" + ], + "title": "CodexPluginHookStub", + "type": "object" + }, + "CodexPluginMarketplaceSummary": { + "additionalProperties": true, + "properties": { + "missing_policy_entries": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Missing Policy Entries", + "type": "array" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "path": { + "title": "Path", + "type": "string" + }, + "plugin_count": { + "default": 0, + "title": "Plugin Count", + "type": "integer" + }, + "skipped_entries": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Skipped Entries", + "type": "array" + }, + "source_id": { + "title": "Source Id", + "type": "string" + } + }, + "required": [ + "source_id", + "path" + ], + "title": "CodexPluginMarketplaceSummary", + "type": "object" + }, + "CodexPluginMcpServerStub": { + "additionalProperties": true, + "properties": { + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Command" + }, + "inventory_loaded": { + "default": false, + "title": "Inventory Loaded", + "type": "boolean" + }, + "inventory_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Inventory Path" + }, + "location": { + "$ref": "#/$defs/CodexPluginSourceLocation" + }, + "path": { + "title": "Path", + "type": "string" + }, + "plugin": { + "title": "Plugin", + "type": "string" + }, + "server": { + "title": "Server", + "type": "string" + } + }, + "required": [ + "plugin", + "server", + "path" + ], + "title": "CodexPluginMcpServerStub", + "type": "object" + }, + "CodexPluginSkillSummary": { + "additionalProperties": true, + "properties": { + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "duplicate": { + "default": false, + "title": "Duplicate", + "type": "boolean" + }, + "location": { + "$ref": "#/$defs/CodexPluginSourceLocation" + }, + "missing_fields": { + "items": { + "type": "string" + }, + "title": "Missing Fields", + "type": "array" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "path": { + "title": "Path", + "type": "string" + }, + "plugin": { + "title": "Plugin", + "type": "string" + } + }, + "required": [ + "plugin", + "path" + ], + "title": "CodexPluginSkillSummary", + "type": "object" + }, + "CodexPluginSourceLocation": { + "additionalProperties": false, + "properties": { + "source_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Path" + }, + "source_pointer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Pointer" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + }, + "source_start_column": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Start Column" + }, + "source_start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Start Line" + } + }, + "title": "CodexPluginSourceLocation", + "type": "object" + }, + "CodexPluginSummary": { + "additionalProperties": true, + "properties": { + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "duplicate_name": { + "default": false, + "title": "Duplicate Name", + "type": "boolean" + }, + "duplicate_root": { + "default": false, + "title": "Duplicate Root", + "type": "boolean" + }, + "location": { + "$ref": "#/$defs/CodexPluginSourceLocation" + }, + "manifest_path": { + "title": "Manifest Path", + "type": "string" + }, + "marketplace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Marketplace" + }, + "missing_fields": { + "items": { + "type": "string" + }, + "title": "Missing Fields", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "name_mismatch": { + "default": false, + "title": "Name Mismatch", + "type": "boolean" + }, + "root_path": { + "title": "Root Path", + "type": "string" + }, + "source_id": { + "title": "Source Id", + "type": "string" + }, + "version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Version" + } + }, + "required": [ + "source_id", + "name", + "root_path", + "manifest_path" + ], + "title": "CodexPluginSummary", + "type": "object" + }, + "CodexPluginSurface": { + "additionalProperties": true, + "properties": { + "app_count": { + "default": 0, + "title": "App Count", + "type": "integer" + }, + "apps": { + "items": { + "$ref": "#/$defs/CodexPluginAppSummary" + }, + "title": "Apps", + "type": "array" + }, + "component_path_issues": { + "items": { + "$ref": "#/$defs/CodexPluginComponentPathIssue" + }, + "title": "Component Path Issues", + "type": "array" + }, + "hook_stub_count": { + "default": 0, + "title": "Hook Stub Count", + "type": "integer" + }, + "hook_stubs": { + "items": { + "$ref": "#/$defs/CodexPluginHookStub" + }, + "title": "Hook Stubs", + "type": "array" + }, + "marketplace_count": { + "default": 0, + "title": "Marketplace Count", + "type": "integer" + }, + "marketplaces": { + "items": { + "$ref": "#/$defs/CodexPluginMarketplaceSummary" + }, + "title": "Marketplaces", + "type": "array" + }, + "mcp_inventory_file_count": { + "default": 0, + "title": "Mcp Inventory File Count", + "type": "integer" + }, + "mcp_inventory_files": { + "items": { + "type": "string" + }, + "title": "Mcp Inventory Files", + "type": "array" + }, + "mcp_server_stub_count": { + "default": 0, + "title": "Mcp Server Stub Count", + "type": "integer" + }, + "mcp_server_stubs": { + "items": { + "$ref": "#/$defs/CodexPluginMcpServerStub" + }, + "title": "Mcp Server Stubs", + "type": "array" + }, + "plugin_count": { + "default": 0, + "title": "Plugin Count", + "type": "integer" + }, + "plugins": { + "items": { + "$ref": "#/$defs/CodexPluginSummary" + }, + "title": "Plugins", + "type": "array" + }, + "skill_count": { + "default": 0, + "title": "Skill Count", + "type": "integer" + }, + "skills": { + "items": { + "$ref": "#/$defs/CodexPluginSkillSummary" + }, + "title": "Skills", + "type": "array" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "CodexPluginSurface", + "type": "object" + }, + "ContributionRule": { + "additionalProperties": false, + "description": "Per-finding audit row explaining how a finding contributed to the\nrelease decision.\n\nAdditive in v0.17. Every finding in `report.findings` produces\nexactly one ContributionRule. Reading the contribution rule is\nsufficient to predict the gate outcome for that finding without\nre-deriving the decision logic; the set of valid `(rule, category)`\npairs is the contract documented in STABILITY.md \"Release decision\ntruth table\".", + "properties": { + "category": { + "enum": [ + "blocker", + "review_item", + "excluded" + ], + "title": "Category", + "type": "string" + }, + "check_id": { + "title": "Check Id", + "type": "string" + }, + "finding_id": { + "title": "Finding Id", + "type": "string" + }, + "fingerprint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fingerprint" + }, + "rationale": { + "title": "Rationale", + "type": "string" + }, + "rule": { + "enum": [ + "policy_block_new", + "severity_block_new", + "policy_baseline_accepted", + "severity_baseline_accepted", + "review_required", + "sub_threshold", + "suppressed" + ], + "title": "Rule", + "type": "string" + } + }, + "required": [ + "category", + "check_id", + "finding_id", + "fingerprint", + "rationale", + "rule" + ], + "title": "ContributionRule", + "type": "object" + }, + "DeclaredIntention": { + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "intent_tags": { + "items": { + "type": "string" + }, + "title": "Intent Tags", + "type": "array" + }, + "kind": { + "enum": [ + "declared_purpose", + "prohibited_action", + "instruction_preview" + ], + "title": "Kind", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "text": { + "title": "Text", + "type": "string" + } + }, + "required": [ + "id", + "intent_tags", + "kind", + "source", + "text" + ], + "title": "DeclaredIntention", + "type": "object" + }, + "EvidenceCoverageDecision": { + "properties": { + "human_review_recommended": { + "title": "Human Review Recommended", + "type": "boolean" + }, + "level": { + "title": "Level", + "type": "string" + }, + "low_confidence_tool_count": { + "title": "Low Confidence Tool Count", + "type": "integer" + }, + "source_warning_count": { + "title": "Source Warning Count", + "type": "integer" + } + }, + "required": [ + "human_review_recommended", + "level", + "low_confidence_tool_count", + "source_warning_count" + ], + "title": "EvidenceCoverageDecision", + "type": "object" + }, + "FailPolicy": { + "properties": { + "ci_mode": { + "title": "Ci Mode", + "type": "string" + }, + "exit_code": { + "title": "Exit Code", + "type": "integer" + }, + "fail_on": { + "items": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "type": "string" + }, + "title": "Fail On", + "type": "array" + }, + "new_findings_only": { + "default": false, + "title": "New Findings Only", + "type": "boolean" + }, + "would_fail_ci": { + "title": "Would Fail Ci", + "type": "boolean" + } + }, + "required": [ + "ci_mode", + "exit_code", + "fail_on", + "new_findings_only", + "would_fail_ci" + ], + "title": "FailPolicy", + "type": "object" + }, + "Finding": { + "additionalProperties": true, + "properties": { + "agent_action": { + "enum": [ + "auto_apply", + "propose_patch_for_review", + "escalate_to_human", + "suppress_with_reason", + "informational" + ], + "type": "string" + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Agent Id" + }, + "autofix_safe": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Autofix Safe" + }, + "baseline_status": { + "anyOf": [ + { + "enum": [ + "new", + "matched", + "resolved" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline Status" + }, + "blocks_release": { + "default": false, + "title": "Blocks Release", + "type": "boolean" + }, + "category": { + "title": "Category", + "type": "string" + }, + "check_id": { + "title": "Check Id", + "type": "string" + }, + "confidence": { + "default": "medium", + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Docs Url" + }, + "evidence": { + "additionalProperties": true, + "title": "Evidence", + "type": "object" + }, + "fingerprint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fingerprint" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Id" + }, + "patches": { + "anyOf": [ + { + "items": { + "discriminator": { + "mapping": { + "append_pointer": "#/$defs/AppendPointerPatch", + "manual": "#/$defs/ManualPatch", + "remove_pointer": "#/$defs/RemovePointerPatch", + "set_pointer": "#/$defs/SetPointerPatch" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/SetPointerPatch" + }, + { + "$ref": "#/$defs/AppendPointerPatch" + }, + { + "$ref": "#/$defs/RemovePointerPatch" + }, + { + "$ref": "#/$defs/ManualPatch" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Patches" + }, + "provenance_kind": { + "enum": [ + "static_declaration", + "ast_extraction", + "keyword_heuristic", + "regex_heuristic", + "policy_pack" + ], + "type": "string" + }, + "recommendation": { + "title": "Recommendation", + "type": "string" + }, + "requires_human_review": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Requires Human Review" + }, + "severity": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "title": "Severity", + "type": "string" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/SourceReference" + }, + { + "type": "null" + } + ], + "default": null + }, + "suggested_patch_kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Patch Kind" + }, + "suppressed": { + "default": false, + "title": "Suppressed", + "type": "boolean" + }, + "suppression_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suppression Reason" + }, + "title": { + "title": "Title", + "type": "string" + }, + "tool_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Id" + }, + "tool_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Name" + } + }, + "required": [ + "agent_action", + "baseline_status", + "blocks_release", + "category", + "check_id", + "confidence", + "evidence", + "fingerprint", + "id", + "provenance_kind", + "recommendation", + "severity", + "suppressed", + "title" + ], + "title": "Finding", + "type": "object" + }, + "LoadedPolicyPack": { + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "rule_count": { + "title": "Rule Count", + "type": "integer" + }, + "version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Version" + } + }, + "required": [ + "id", + "name", + "path", + "rule_count" + ], + "title": "LoadedPolicyPack", + "type": "object" + }, + "ManualPatch": { + "additionalProperties": false, + "description": "No machine-applicable change. Carries human-readable instructions.\n\nUsed for every finding whose check ID has no v0.6 non-manual generator\nand for findings (like trace flips, per C6) that are intentionally\nnever auto-patched.", + "properties": { + "instructions": { + "title": "Instructions", + "type": "string" + }, + "kind": { + "const": "manual", + "default": "manual", + "title": "Kind", + "type": "string" + } + }, + "required": [ + "instructions" + ], + "title": "ManualPatch", + "type": "object" + }, + "Misalignment": { + "properties": { + "capability_refs": { + "items": { + "type": "string" + }, + "title": "Capability Refs", + "type": "array" + }, + "finding_refs": { + "items": { + "type": "string" + }, + "title": "Finding Refs", + "type": "array" + }, + "gap": { + "title": "Gap", + "type": "string" + }, + "id": { + "title": "Id", + "type": "string" + }, + "intention_refs": { + "items": { + "type": "string" + }, + "title": "Intention Refs", + "type": "array" + }, + "kind": { + "enum": [ + "policy_gap", + "scope_drift", + "prohibited_action_present", + "control_missing", + "intent_mismatch", + "undetected_gap" + ], + "title": "Kind", + "type": "string" + }, + "policy_requirement": { + "title": "Policy Requirement", + "type": "string" + }, + "release_implication": { + "title": "Release Implication", + "type": "string" + }, + "severity": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "title": "Severity", + "type": "string" + }, + "tool_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Name" + } + }, + "required": [ + "capability_refs", + "finding_refs", + "gap", + "id", + "intention_refs", + "kind", + "policy_requirement", + "release_implication", + "severity", + "tool_name" + ], + "title": "Misalignment", + "type": "object" + }, + "ReleaseConsequence": { + "properties": { + "blocker_misalignment_count": { + "default": 0, + "title": "Blocker Misalignment Count", + "type": "integer" + }, + "decision": { + "enum": [ + "blocked", + "review_required", + "insufficient_evidence", + "passed" + ], + "title": "Decision", + "type": "string" + }, + "fail_policy": { + "$ref": "#/$defs/FailPolicy" + }, + "review_misalignment_count": { + "default": 0, + "title": "Review Misalignment Count", + "type": "integer" + }, + "summary": { + "title": "Summary", + "type": "string" + } + }, + "required": [ + "blocker_misalignment_count", + "decision", + "fail_policy", + "review_misalignment_count", + "summary" + ], + "title": "ReleaseConsequence", + "type": "object" + }, + "ReleaseDecision": { + "properties": { + "baseline_delta": { + "$ref": "#/$defs/BaselineDelta" + }, + "blockers": { + "items": { + "$ref": "#/$defs/ReleaseDecisionItem" + }, + "title": "Blockers", + "type": "array" + }, + "contribution_rules": { + "items": { + "$ref": "#/$defs/ContributionRule" + }, + "title": "Contribution Rules", + "type": "array" + }, + "decision": { + "enum": [ + "blocked", + "review_required", + "insufficient_evidence", + "passed" + ], + "title": "Decision", + "type": "string" + }, + "evidence_coverage": { + "$ref": "#/$defs/EvidenceCoverageDecision" + }, + "fail_policy": { + "$ref": "#/$defs/FailPolicy" + }, + "reason": { + "title": "Reason", + "type": "string" + }, + "review_items": { + "items": { + "$ref": "#/$defs/ReleaseDecisionItem" + }, + "title": "Review Items", + "type": "array" + } + }, + "required": [ + "baseline_delta", + "blockers", + "contribution_rules", + "decision", + "evidence_coverage", + "fail_policy", + "reason", + "review_items" + ], + "title": "ReleaseDecision", + "type": "object" + }, + "ReleaseDecisionItem": { + "properties": { + "baseline_status": { + "anyOf": [ + { + "enum": [ + "new", + "matched", + "resolved" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline Status" + }, + "blocks_release": { + "default": false, + "title": "Blocks Release", + "type": "boolean" + }, + "check_id": { + "title": "Check Id", + "type": "string" + }, + "fingerprint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fingerprint" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Id" + }, + "severity": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + } + }, + "required": [ + "baseline_status", + "blocks_release", + "check_id", + "fingerprint", + "id", + "severity", + "title" + ], + "title": "ReleaseDecisionItem", + "type": "object" + }, + "RemovePointerPatch": { + "additionalProperties": false, + "description": "Remove the node at a JSON pointer.", + "properties": { + "confidence": { + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "kind": { + "const": "remove_pointer", + "default": "remove_pointer", + "title": "Kind", + "type": "string" + }, + "pointer": { + "title": "Pointer", + "type": "string" + }, + "rationale": { + "title": "Rationale", + "type": "string" + }, + "target_file": { + "title": "Target File", + "type": "string" + }, + "target_format": { + "enum": [ + "yaml", + "json" + ], + "title": "Target Format", + "type": "string" + }, + "target_sha256": { + "title": "Target Sha256", + "type": "string" + } + }, + "required": [ + "target_file", + "pointer", + "target_format", + "confidence", + "rationale", + "target_sha256" + ], + "title": "RemovePointerPatch", + "type": "object" + }, + "ReportSummary": { + "properties": { + "critical_count": { + "default": 0, + "title": "Critical Count", + "type": "integer" + }, + "evidence_coverage": { + "default": "static", + "title": "Evidence Coverage", + "type": "string" + }, + "high_count": { + "default": 0, + "title": "High Count", + "type": "integer" + }, + "human_review_recommended": { + "default": false, + "title": "Human Review Recommended", + "type": "boolean" + }, + "info_count": { + "default": 0, + "title": "Info Count", + "type": "integer" + }, + "low_count": { + "default": 0, + "title": "Low Count", + "type": "integer" + }, + "medium_count": { + "default": 0, + "title": "Medium Count", + "type": "integer" + }, + "status": { + "title": "Status", + "type": "string" + }, + "suppressed_count": { + "default": 0, + "title": "Suppressed Count", + "type": "integer" + } + }, + "required": [ + "status" + ], + "title": "ReportSummary", + "type": "object" + }, + "SetPointerPatch": { + "additionalProperties": false, + "description": "Set the value at a JSON pointer inside a YAML or JSON file.", + "properties": { + "confidence": { + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "kind": { + "const": "set_pointer", + "default": "set_pointer", + "title": "Kind", + "type": "string" + }, + "pointer": { + "title": "Pointer", + "type": "string" + }, + "rationale": { + "title": "Rationale", + "type": "string" + }, + "target_file": { + "title": "Target File", + "type": "string" + }, + "target_format": { + "enum": [ + "yaml", + "json" + ], + "title": "Target Format", + "type": "string" + }, + "target_sha256": { + "title": "Target Sha256", + "type": "string" + }, + "value": { + "title": "Value" + } + }, + "required": [ + "target_file", + "pointer", + "value", + "target_format", + "confidence", + "rationale", + "target_sha256" + ], + "title": "SetPointerPatch", + "type": "object" + }, + "SourceReference": { + "additionalProperties": true, + "properties": { + "end_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End Line" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Location" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "pointer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pointer" + }, + "ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref" + }, + "start_column": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start Column" + }, + "start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start Line" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "SourceReference", + "type": "object" + }, + "SuggestedScenario": { + "properties": { + "expected_control": { + "title": "Expected Control", + "type": "string" + }, + "given": { + "title": "Given", + "type": "string" + }, + "id": { + "title": "Id", + "type": "string" + }, + "scenario_type": { + "enum": [ + "approval", + "confirmation", + "idempotency_retry", + "least_privilege_scope", + "prohibited_action", + "wildcard_inventory", + "schema_boundary", + "prompt_scope_alignment", + "test_case_coverage" + ], + "title": "Scenario Type", + "type": "string" + }, + "source_findings": { + "items": { + "type": "string" + }, + "title": "Source Findings", + "type": "array" + }, + "source_misalignments": { + "items": { + "type": "string" + }, + "title": "Source Misalignments", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + } + }, + "required": [ + "expected_control", + "given", + "id", + "scenario_type", + "source_findings", + "source_misalignments", + "title" + ], + "title": "SuggestedScenario", + "type": "object" + }, + "ToolSurfaceControlChange": { + "additionalProperties": false, + "properties": { + "control": { + "enum": [ + "approval_policy", + "confirmation_policy", + "idempotency_evidence" + ], + "title": "Control", + "type": "string" + }, + "kind": { + "enum": [ + "added", + "removed", + "changed" + ], + "title": "Kind", + "type": "string" + }, + "reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "tool": { + "title": "Tool", + "type": "string" + } + }, + "required": [ + "control", + "kind", + "reason", + "source", + "tool" + ], + "title": "ToolSurfaceControlChange", + "type": "object" + }, + "ToolSurfaceControlFact": { + "additionalProperties": false, + "properties": { + "kind": { + "enum": [ + "approval_policy", + "confirmation_policy", + "idempotency_evidence" + ], + "title": "Kind", + "type": "string" + }, + "reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason" + }, + "source": { + "title": "Source", + "type": "string" + }, + "tool": { + "title": "Tool", + "type": "string" + } + }, + "required": [ + "kind", + "reason", + "source", + "tool" + ], + "title": "ToolSurfaceControlFact", + "type": "object" + }, + "ToolSurfaceDiff": { + "additionalProperties": false, + "properties": { + "base": { + "$ref": "#/$defs/ToolSurfaceDiffBase" + }, + "controls": { + "items": { + "$ref": "#/$defs/ToolSurfaceControlChange" + }, + "title": "Controls", + "type": "array" + }, + "enabled": { + "default": false, + "title": "Enabled", + "type": "boolean" + }, + "finding_deltas": { + "$ref": "#/$defs/ToolSurfaceFindingDeltas" + }, + "high_risk_effects": { + "items": { + "$ref": "#/$defs/ToolSurfaceHighRiskEffectChange" + }, + "title": "High Risk Effects", + "type": "array" + }, + "metadata_changes": { + "items": { + "$ref": "#/$defs/ToolSurfaceMetadataChange" + }, + "title": "Metadata Changes", + "type": "array" + }, + "notes": { + "items": { + "type": "string" + }, + "title": "Notes", + "type": "array" + }, + "policy_drift": { + "items": { + "$ref": "#/$defs/ToolSurfacePolicyDrift" + }, + "title": "Policy Drift", + "type": "array" + }, + "scopes": { + "items": { + "$ref": "#/$defs/ToolSurfaceScopeChange" + }, + "title": "Scopes", + "type": "array" + }, + "summary": { + "$ref": "#/$defs/ToolSurfaceDiffSummary" + }, + "tools": { + "items": { + "$ref": "#/$defs/ToolSurfaceToolChange" + }, + "title": "Tools", + "type": "array" + } + }, + "required": [ + "base", + "controls", + "enabled", + "finding_deltas", + "high_risk_effects", + "metadata_changes", + "notes", + "policy_drift", + "scopes", + "summary", + "tools" + ], + "title": "ToolSurfaceDiff", + "type": "object" + }, + "ToolSurfaceDiffBase": { + "additionalProperties": false, + "properties": { + "baseline_schema_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline Schema Version" + }, + "kind": { + "default": "none", + "enum": [ + "none", + "report", + "baseline" + ], + "title": "Kind", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "report_schema_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Report Schema Version" + } + }, + "required": [ + "baseline_schema_version", + "kind", + "path", + "report_schema_version" + ], + "title": "ToolSurfaceDiffBase", + "type": "object" + }, + "ToolSurfaceDiffSummary": { + "additionalProperties": false, + "properties": { + "accepted_debt": { + "default": 0, + "title": "Accepted Debt", + "type": "integer" + }, + "controls_added": { + "default": 0, + "title": "Controls Added", + "type": "integer" + }, + "controls_removed": { + "default": 0, + "title": "Controls Removed", + "type": "integer" + }, + "metadata_changes": { + "default": 0, + "title": "Metadata Changes", + "type": "integer" + }, + "new_findings": { + "default": 0, + "title": "New Findings", + "type": "integer" + }, + "new_high_risk_effects": { + "default": 0, + "title": "New High Risk Effects", + "type": "integer" + }, + "new_scopes": { + "default": 0, + "title": "New Scopes", + "type": "integer" + }, + "policy_drift_items": { + "default": 0, + "title": "Policy Drift Items", + "type": "integer" + }, + "removed_high_risk_effects": { + "default": 0, + "title": "Removed High Risk Effects", + "type": "integer" + }, + "removed_scopes": { + "default": 0, + "title": "Removed Scopes", + "type": "integer" + }, + "resolved_findings": { + "default": 0, + "title": "Resolved Findings", + "type": "integer" + }, + "tools_added": { + "default": 0, + "title": "Tools Added", + "type": "integer" + }, + "tools_changed": { + "default": 0, + "title": "Tools Changed", + "type": "integer" + }, + "tools_removed": { + "default": 0, + "title": "Tools Removed", + "type": "integer" + }, + "unchanged_findings": { + "default": 0, + "title": "Unchanged Findings", + "type": "integer" + } + }, + "required": [ + "accepted_debt", + "controls_added", + "controls_removed", + "metadata_changes", + "new_findings", + "new_high_risk_effects", + "new_scopes", + "policy_drift_items", + "removed_high_risk_effects", + "removed_scopes", + "resolved_findings", + "tools_added", + "tools_changed", + "tools_removed", + "unchanged_findings" + ], + "title": "ToolSurfaceDiffSummary", + "type": "object" + }, + "ToolSurfaceFacts": { + "additionalProperties": false, + "properties": { + "controls": { + "items": { + "$ref": "#/$defs/ToolSurfaceControlFact" + }, + "title": "Controls", + "type": "array" + }, + "policies": { + "items": { + "$ref": "#/$defs/ToolSurfacePolicyFact" + }, + "title": "Policies", + "type": "array" + }, + "scopes": { + "items": { + "$ref": "#/$defs/ToolSurfaceScopeFact" + }, + "title": "Scopes", + "type": "array" + }, + "tools": { + "items": { + "$ref": "#/$defs/ToolSurfaceToolFact" + }, + "title": "Tools", + "type": "array" + } + }, + "required": [ + "controls", + "policies", + "scopes", + "tools" + ], + "title": "ToolSurfaceFacts", + "type": "object" + }, + "ToolSurfaceFieldChange": { + "additionalProperties": false, + "properties": { + "after": { + "default": null, + "title": "After" + }, + "before": { + "default": null, + "title": "Before" + }, + "field": { + "title": "Field", + "type": "string" + } + }, + "required": [ + "after", + "before", + "field" + ], + "title": "ToolSurfaceFieldChange", + "type": "object" + }, + "ToolSurfaceFindingDeltaItem": { + "additionalProperties": false, + "properties": { + "baseline_status": { + "anyOf": [ + { + "enum": [ + "new", + "matched", + "resolved" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline Status" + }, + "check_id": { + "title": "Check Id", + "type": "string" + }, + "fingerprint": { + "title": "Fingerprint", + "type": "string" + }, + "severity": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "tool_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Name" + } + }, + "required": [ + "baseline_status", + "check_id", + "fingerprint", + "severity", + "title", + "tool_name" + ], + "title": "ToolSurfaceFindingDeltaItem", + "type": "object" + }, + "ToolSurfaceFindingDeltas": { + "additionalProperties": false, + "properties": { + "accepted_debt": { + "items": { + "$ref": "#/$defs/ToolSurfaceFindingDeltaItem" + }, + "title": "Accepted Debt", + "type": "array" + }, + "new_findings": { + "items": { + "$ref": "#/$defs/ToolSurfaceFindingDeltaItem" + }, + "title": "New Findings", + "type": "array" + }, + "resolved_findings": { + "items": { + "$ref": "#/$defs/ToolSurfaceFindingDeltaItem" + }, + "title": "Resolved Findings", + "type": "array" + }, + "unchanged_findings": { + "items": { + "$ref": "#/$defs/ToolSurfaceFindingDeltaItem" + }, + "title": "Unchanged Findings", + "type": "array" + } + }, + "required": [ + "accepted_debt", + "new_findings", + "resolved_findings", + "unchanged_findings" + ], + "title": "ToolSurfaceFindingDeltas", + "type": "object" + }, + "ToolSurfaceHashes": { + "additionalProperties": false, + "properties": { + "annotations": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Annotations" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "input_schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Input Schema" + }, + "output_schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Output Schema" + }, + "parameters": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Parameters" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + } + }, + "required": [ + "annotations", + "description", + "input_schema", + "output_schema", + "parameters", + "source_ref" + ], + "title": "ToolSurfaceHashes", + "type": "object" + }, + "ToolSurfaceHighRiskEffectChange": { + "additionalProperties": false, + "properties": { + "kind": { + "enum": [ + "added", + "removed", + "changed" + ], + "title": "Kind", + "type": "string" + }, + "tag": { + "title": "Tag", + "type": "string" + }, + "tool": { + "title": "Tool", + "type": "string" + } + }, + "required": [ + "kind", + "tag", + "tool" + ], + "title": "ToolSurfaceHighRiskEffectChange", + "type": "object" + }, + "ToolSurfaceMetadataChange": { + "additionalProperties": false, + "properties": { + "after": { + "default": null, + "title": "After" + }, + "before": { + "default": null, + "title": "Before" + }, + "kind": { + "enum": [ + "added", + "removed", + "changed" + ], + "title": "Kind", + "type": "string" + }, + "metadata": { + "title": "Metadata", + "type": "string" + }, + "tool": { + "title": "Tool", + "type": "string" + } + }, + "required": [ + "after", + "before", + "kind", + "metadata", + "tool" + ], + "title": "ToolSurfaceMetadataChange", + "type": "object" + }, + "ToolSurfacePolicyDrift": { + "additionalProperties": false, + "properties": { + "after_hash": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "After Hash" + }, + "after_summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "After Summary" + }, + "before_hash": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Before Hash" + }, + "before_summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Before Summary" + }, + "key": { + "title": "Key", + "type": "string" + }, + "kind": { + "enum": [ + "added", + "removed", + "changed" + ], + "title": "Kind", + "type": "string" + }, + "policy_kind": { + "title": "Policy Kind", + "type": "string" + } + }, + "required": [ + "after_hash", + "after_summary", + "before_hash", + "before_summary", + "key", + "kind", + "policy_kind" + ], + "title": "ToolSurfacePolicyDrift", + "type": "object" + }, + "ToolSurfacePolicyFact": { + "additionalProperties": false, + "properties": { + "key": { + "title": "Key", + "type": "string" + }, + "kind": { + "title": "Kind", + "type": "string" + }, + "summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Summary" + }, + "value_hash": { + "title": "Value Hash", + "type": "string" + } + }, + "required": [ + "key", + "kind", + "summary", + "value_hash" + ], + "title": "ToolSurfacePolicyFact", + "type": "object" + }, + "ToolSurfaceScopeChange": { + "additionalProperties": false, + "properties": { + "broad": { + "default": false, + "title": "Broad", + "type": "boolean" + }, + "kind": { + "enum": [ + "added", + "removed", + "changed" + ], + "title": "Kind", + "type": "string" + }, + "scope": { + "title": "Scope", + "type": "string" + }, + "scope_kind": { + "enum": [ + "tool_required", + "manifest_declared" + ], + "title": "Scope Kind", + "type": "string" + }, + "tool_names": { + "items": { + "type": "string" + }, + "title": "Tool Names", + "type": "array" + } + }, + "required": [ + "broad", + "kind", + "scope", + "scope_kind", + "tool_names" + ], + "title": "ToolSurfaceScopeChange", + "type": "object" + }, + "ToolSurfaceScopeFact": { + "additionalProperties": false, + "properties": { + "broad": { + "default": false, + "title": "Broad", + "type": "boolean" + }, + "kind": { + "enum": [ + "tool_required", + "manifest_declared" + ], + "title": "Kind", + "type": "string" + }, + "scope": { + "title": "Scope", + "type": "string" + }, + "tool_names": { + "items": { + "type": "string" + }, + "title": "Tool Names", + "type": "array" + } + }, + "required": [ + "broad", + "kind", + "scope", + "tool_names" + ], + "title": "ToolSurfaceScopeFact", + "type": "object" + }, + "ToolSurfaceSummary": { + "properties": { + "high_risk_tools": { + "title": "High Risk Tools", + "type": "integer" + }, + "missing_descriptions": { + "default": 0, + "title": "Missing Descriptions", + "type": "integer" + }, + "sources": { + "additionalProperties": { + "type": "integer" + }, + "title": "Sources", + "type": "object" + }, + "total_tools": { + "title": "Total Tools", + "type": "integer" + }, + "wildcard_tools": { + "default": 0, + "title": "Wildcard Tools", + "type": "integer" + } + }, + "required": [ + "total_tools", + "high_risk_tools" + ], + "title": "ToolSurfaceSummary", + "type": "object" + }, + "ToolSurfaceToolChange": { + "additionalProperties": false, + "properties": { + "changes": { + "items": { + "$ref": "#/$defs/ToolSurfaceFieldChange" + }, + "title": "Changes", + "type": "array" + }, + "kind": { + "enum": [ + "added", + "removed", + "changed" + ], + "title": "Kind", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "source_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Id" + }, + "source_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Type" + } + }, + "required": [ + "changes", + "kind", + "name", + "source_id", + "source_type" + ], + "title": "ToolSurfaceToolChange", + "type": "object" + }, + "ToolSurfaceToolFact": { + "additionalProperties": false, + "properties": { + "auth_scopes": { + "items": { + "type": "string" + }, + "title": "Auth Scopes", + "type": "array" + }, + "extraction_confidence": { + "default": "low", + "enum": [ + "low", + "medium", + "high" + ], + "title": "Extraction Confidence", + "type": "string" + }, + "has_description": { + "default": false, + "title": "Has Description", + "type": "boolean" + }, + "hashes": { + "$ref": "#/$defs/ToolSurfaceHashes" + }, + "name": { + "title": "Name", + "type": "string" + }, + "owner": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Owner" + }, + "risk_tags": { + "items": { + "type": "string" + }, + "title": "Risk Tags", + "type": "array" + }, + "source_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Id" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + }, + "source_type": { + "title": "Source Type", + "type": "string" + } + }, + "required": [ + "auth_scopes", + "extraction_confidence", + "has_description", + "hashes", + "name", + "owner", + "risk_tags", + "source_id", + "source_ref", + "source_type" + ], + "title": "ToolSurfaceToolFact", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.17.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": true, + "description": "JSON Schema for the Agents Shipgate Tool-Use Readiness Report. Generated from agents_shipgate.core.models.ReadinessReport with post-processing to preserve the v0.5 public contract. Do not edit by hand.", + "properties": { + "action_surface_diff": { + "$ref": "#/$defs/ActionSurfaceDiff" + }, + "action_surface_facts": { + "$ref": "#/$defs/ActionSurfaceFacts" + }, + "agent": { + "additionalProperties": true, + "title": "Agent", + "type": "object" + }, + "agent_summary": { + "$ref": "#/$defs/AgentSummary" + }, + "anthropic_surface": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Anthropic Surface" + }, + "api_surface": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Api Surface" + }, + "baseline": { + "anyOf": [ + { + "$ref": "#/$defs/BaselineSummary" + }, + { + "type": "null" + } + ], + "default": null + }, + "capability_facts": { + "items": { + "$ref": "#/$defs/CapabilityFact" + }, + "title": "Capability Facts", + "type": "array" + }, + "codex_plugin_surface": { + "anyOf": [ + { + "$ref": "#/$defs/CodexPluginSurface" + }, + { + "type": "null" + } + ], + "default": null + }, + "declared_intentions": { + "items": { + "$ref": "#/$defs/DeclaredIntention" + }, + "title": "Declared Intentions", + "type": "array" + }, + "environment": { + "additionalProperties": true, + "title": "Environment", + "type": "object" + }, + "findings": { + "items": { + "$ref": "#/$defs/Finding" + }, + "title": "Findings", + "type": "array" + }, + "frameworks": { + "additionalProperties": true, + "properties": { + "crewai": { + "additionalProperties": true, + "required": [ + "agent_count", + "class_tool_count", + "crew_count", + "dynamic_tool_surface_count", + "function_tool_count", + "prebuilt_tool_count", + "python_entrypoint_count", + "tool_inventory_file_count", + "warnings" + ], + "type": "object" + }, + "google_adk": { + "additionalProperties": true, + "required": [ + "agent_config_count", + "agent_count", + "callback_count", + "dynamic_toolset_count", + "eval_file_count", + "function_tool_count", + "long_running_tool_count", + "plugin_count", + "python_entrypoint_count", + "sub_agent_count", + "tool_inventory_file_count", + "toolset_count", + "trace_sample_count", + "warnings" + ], + "type": "object" + }, + "langchain": { + "additionalProperties": true, + "required": [ + "agent_tool_binding_count", + "dynamic_tool_surface_count", + "function_tool_count", + "python_entrypoint_count", + "structured_tool_count", + "tool_inventory_file_count", + "tool_node_count", + "warnings" + ], + "type": "object" + } + }, + "title": "Frameworks", + "type": "object" + }, + "generated_reports": { + "additionalProperties": { + "type": "string" + }, + "title": "Generated Reports", + "type": "object" + }, + "loaded_plugins": { + "items": { + "additionalProperties": true, + "required": [ + "check_id", + "distribution", + "name", + "value", + "version" + ], + "type": "object" + }, + "title": "Loaded Plugins", + "type": "array" + }, + "loaded_policy_packs": { + "items": { + "$ref": "#/$defs/LoadedPolicyPack" + }, + "title": "Loaded Policy Packs", + "type": "array" + }, + "manifest_dir": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Manifest Dir" + }, + "misalignments": { + "items": { + "$ref": "#/$defs/Misalignment" + }, + "title": "Misalignments", + "type": "array" + }, + "project": { + "additionalProperties": true, + "title": "Project", + "type": "object" + }, + "recommended_actions": { + "items": { + "type": "string" + }, + "title": "Recommended Actions", + "type": "array" + }, + "release_consequence": { + "$ref": "#/$defs/ReleaseConsequence" + }, + "release_decision": { + "$ref": "#/$defs/ReleaseDecision" + }, + "report_schema_version": { + "const": "0.17" + }, + "run_id": { + "title": "Run Id", + "type": "string" + }, + "schema_version": { + "const": "0.1" + }, + "source_warnings": { + "items": { + "type": "string" + }, + "title": "Source Warnings", + "type": "array" + }, + "suggested_scenarios": { + "items": { + "$ref": "#/$defs/SuggestedScenario" + }, + "title": "Suggested Scenarios", + "type": "array" + }, + "summary": { + "$ref": "#/$defs/ReportSummary" + }, + "tool_inventory": { + "items": { + "additionalProperties": true, + "required": [ + "auth_scopes", + "confidence", + "name", + "risk_tags", + "source_type" + ], + "type": "object" + }, + "title": "Tool Inventory", + "type": "array" + }, + "tool_surface": { + "$ref": "#/$defs/ToolSurfaceSummary" + }, + "tool_surface_diff": { + "$ref": "#/$defs/ToolSurfaceDiff" + }, + "tool_surface_facts": { + "$ref": "#/$defs/ToolSurfaceFacts" + } + }, + "required": [ + "action_surface_diff", + "action_surface_facts", + "agent", + "agent_summary", + "capability_facts", + "codex_plugin_surface", + "declared_intentions", + "environment", + "findings", + "frameworks", + "generated_reports", + "loaded_plugins", + "loaded_policy_packs", + "misalignments", + "project", + "recommended_actions", + "release_consequence", + "release_decision", + "report_schema_version", + "run_id", + "schema_version", + "source_warnings", + "suggested_scenarios", + "summary", + "tool_inventory", + "tool_surface", + "tool_surface_diff", + "tool_surface_facts" + ], + "title": "Agents Shipgate Readiness Report v0.17", + "type": "object" +} diff --git a/llms-full.txt b/llms-full.txt index 4180ec6..9264c47 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -274,8 +274,9 @@ Other stable top-level fields: - `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) - `findings[].blocks_release` (v0.16+, explicit release-policy blockers from Action Surface Diff policies) - `action_surface_facts` / `action_surface_diff` (v0.16+, deterministic action snapshot and base/head action delta) +- `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}`) -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). +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). **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. @@ -341,7 +342,7 @@ validation and [`docs/manifest-v0.1.md`](docs/manifest-v0.1.md) for prose. ### Where is the report schema? Parse `agents-shipgate-reports/report.json` and validate against -[`docs/report-schema.v0.16.json`](docs/report-schema.v0.16.json) (current). +[`docs/report-schema.v0.17.json`](docs/report-schema.v0.17.json) (current). Older reports (`report_schema_version: "0.10"`) validate against the frozen [`docs/report-schema.v0.10.json`](docs/report-schema.v0.10.json). Do not scrape Markdown when JSON is available. @@ -379,7 +380,8 @@ For the short, current statement of "which fields to read", see [`docs/agent-con | What | Path | Stable | |---|---|---| | Manifest schema | [`docs/manifest-v0.1.json`](docs/manifest-v0.1.json) | `0.1` | -| Report schema (current) | [`docs/report-schema.v0.16.json`](docs/report-schema.v0.16.json) | `0.16` | +| Report schema (current) | [`docs/report-schema.v0.17.json`](docs/report-schema.v0.17.json) | `0.17` | +| Report schema (v0.16 frozen reference) | [`docs/report-schema.v0.16.json`](docs/report-schema.v0.16.json) | `0.16` | | Report schema (v0.15 frozen reference) | [`docs/report-schema.v0.15.json`](docs/report-schema.v0.15.json) | `0.15` | | Report schema (v0.14 frozen reference) | [`docs/report-schema.v0.14.json`](docs/report-schema.v0.14.json) | `0.14` | | Report schema (v0.13 frozen reference) | [`docs/report-schema.v0.13.json`](docs/report-schema.v0.13.json) | `0.13` | @@ -815,9 +817,9 @@ agents-shipgate contract --json - Latest release: `v0.10.0` (see [pyproject.toml](../pyproject.toml) for the in-tree version) - Runtime contract: `1` -- Current report schema: `0.16` — [`docs/report-schema.v0.16.json`](report-schema.v0.16.json) +- Current report schema: `0.17` — [`docs/report-schema.v0.17.json`](report-schema.v0.17.json) - Current packet schema: `0.5` — [`docs/packet-schema.v0.5.json`](packet-schema.v0.5.json) -- Frozen-reference report schemas: [`v0.15`](report-schema.v0.15.json), [`v0.14`](report-schema.v0.14.json), [`v0.13`](report-schema.v0.13.json), [`v0.12`](report-schema.v0.12.json), [`v0.11`](report-schema.v0.11.json), [`v0.10`](report-schema.v0.10.json), [`v0.9`](report-schema.v0.9.json), [`v0.8`](report-schema.v0.8.json), [`v0.7`](report-schema.v0.7.json), [`v0.6`](report-schema.v0.6.json), older +- Frozen-reference report schemas: [`v0.16`](report-schema.v0.16.json), [`v0.15`](report-schema.v0.15.json), [`v0.14`](report-schema.v0.14.json), [`v0.13`](report-schema.v0.13.json), [`v0.12`](report-schema.v0.12.json), [`v0.11`](report-schema.v0.11.json), [`v0.10`](report-schema.v0.10.json), [`v0.9`](report-schema.v0.9.json), [`v0.8`](report-schema.v0.8.json), [`v0.7`](report-schema.v0.7.json), [`v0.6`](report-schema.v0.6.json), older ## Read these first for release gating @@ -828,6 +830,7 @@ In `agents-shipgate-reports/report.json`: - `release_decision.review_items[]` — items the human reviewer should look at; includes baseline-matched accepted debt. - `release_decision.fail_policy.would_fail_ci` — `true`/`false`. Matches what the CI process will exit with. - `release_decision.reason` — one-sentence explanation suitable for a PR comment. +- `release_decision.contribution_rules[]` (v0.17+) — deterministic per-finding audit explaining how each `report.findings` entry was classified. Exactly one row per finding (including suppressed). Each row carries `{finding_id, fingerprint, check_id, category, rule, rationale}`. `category` ∈ `{blocker, review_item, excluded}`; `rule` ∈ `{policy_block_new, severity_block_new, policy_baseline_accepted, severity_baseline_accepted, review_required, sub_threshold, suppressed}`. Reading the contribution rule is sufficient to predict the gate outcome for that finding without re-deriving the decision logic — the closed grammar of `(rule, category)` pairs is documented in [STABILITY.md "Release decision truth table"](../STABILITY.md#release-decision-truth-table). The audit cannot disagree with `blockers[]` / `review_items[]` (the same classification powers both). The action exposes these as outputs `decision`, `blocker_count`, `review_item_count`, `ci_would_fail` (v0.8+). @@ -930,7 +933,7 @@ Companion prompt: [`prompts/explain-finding-to-user.md`](../prompts/explain-find - [STABILITY.md](../STABILITY.md) — full 0.x stability contract. Source of truth for everything above. - [AGENTS.md](../AGENTS.md) — agent-facing instructions: install, run, single-turn flow, error semantics. -- [`docs/report-schema.v0.16.json`](report-schema.v0.16.json) — machine-validatable JSON Schema for the current report. +- [`docs/report-schema.v0.17.json`](report-schema.v0.17.json) — machine-validatable JSON Schema for the current report. - [`docs/packet-schema.v0.5.json`](packet-schema.v0.5.json) — machine-validatable JSON Schema for the current packet. - [`docs/checks.json`](checks.json) — check catalog. @@ -1889,7 +1892,7 @@ from the hash so toggling `--suggest-patches` doesn't shift it. - [`checks.md`](checks.md) — full check catalog with rationale. - [`minimal-real-configs.md`](minimal-real-configs.md) — per-framework minimal manifests to build from. -- [`report-schema.v0.16.json`](report-schema.v0.16.json) — current JSON +- [`report-schema.v0.17.json`](report-schema.v0.17.json) — current JSON Schema for `report.json`. - [`AGENTS.md`](../AGENTS.md) — top-level agent instructions, install, trigger table. diff --git a/llms.txt b/llms.txt index 8bfe79a..9954360 100644 --- a/llms.txt +++ b/llms.txt @@ -55,7 +55,7 @@ - Markdown report: `agents-shipgate-reports/report.md`. - JSON report: `agents-shipgate-reports/report.json`. -- JSON report schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.16.json +- JSON report schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.17.json - Release Evidence Packet (Markdown / JSON / HTML, optional PDF): `agents-shipgate-reports/packet.{md,json,html}`. - Packet schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/packet-schema.v0.5.json - SARIF report: `agents-shipgate-reports/report.sarif`. @@ -94,7 +94,7 @@ - Zero-install detector (stdlib-only Python; same structural verdict as `agents-shipgate detect --json` — emits the canonical `DetectResult` fields plus `script_version`, but NOT the CLI's `diagnostics` or `next_actions` arrays): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/tools/shipgate-detect.py - Zero-install paths overview (single-file detector, uvx, GitHub Action): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/zero-install.md - Manifest schema: https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/manifest-v0.1.json -- Report schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.16.json +- Report schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.17.json - Packet schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/packet-schema.v0.5.json - Current agent contract: https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/agent-contract-current.md diff --git a/samples/simple_crewai_agent/expected/report.json b/samples/simple_crewai_agent/expected/report.json index 92939f4..e3145c7 100644 --- a/samples/simple_crewai_agent/expected/report.json +++ b/samples/simple_crewai_agent/expected/report.json @@ -1,8 +1,8 @@ { "schema_version": "0.1", - "report_schema_version": "0.16", + "report_schema_version": "0.17", "run_id": "agents_shipgate_bdd9eed51efd7740", - "manifest_dir": "/Users/pengfeihu/.codex/worktrees/e9c4/shipgate/samples/simple_crewai_agent", + "manifest_dir": "/Users/threemoonslab/code/agents-shipgate/.claude/worktrees/heuristic-panini-4d4332/samples/simple_crewai_agent", "project": { "name": "simple-crewai-agent" }, @@ -80,7 +80,8 @@ "new_findings_only": false, "would_fail_ci": false, "exit_code": 0 - } + }, + "contribution_rules": [] }, "capability_facts": [], "declared_intentions": [ diff --git a/samples/simple_langchain_agent/expected/report.json b/samples/simple_langchain_agent/expected/report.json index 535692e..6c036ad 100644 --- a/samples/simple_langchain_agent/expected/report.json +++ b/samples/simple_langchain_agent/expected/report.json @@ -1,8 +1,8 @@ { "schema_version": "0.1", - "report_schema_version": "0.16", + "report_schema_version": "0.17", "run_id": "agents_shipgate_11eb2e94b84876b3", - "manifest_dir": "/Users/pengfeihu/.codex/worktrees/e9c4/shipgate/samples/simple_langchain_agent", + "manifest_dir": "/Users/threemoonslab/code/agents-shipgate/.claude/worktrees/heuristic-panini-4d4332/samples/simple_langchain_agent", "project": { "name": "simple-langchain-agent" }, @@ -79,7 +79,8 @@ "new_findings_only": false, "would_fail_ci": false, "exit_code": 0 - } + }, + "contribution_rules": [] }, "capability_facts": [], "declared_intentions": [ diff --git a/samples/simple_openai_api_agent/expected/report.json b/samples/simple_openai_api_agent/expected/report.json index 5e645b8..28a28a0 100644 --- a/samples/simple_openai_api_agent/expected/report.json +++ b/samples/simple_openai_api_agent/expected/report.json @@ -1,8 +1,8 @@ { "schema_version": "0.1", - "report_schema_version": "0.16", + "report_schema_version": "0.17", "run_id": "agents_shipgate_0adc60e9f77f2b2a", - "manifest_dir": "/Users/pengfeihu/.codex/worktrees/e9c4/shipgate/samples/simple_openai_api_agent", + "manifest_dir": "/Users/threemoonslab/code/agents-shipgate/.claude/worktrees/heuristic-panini-4d4332/samples/simple_openai_api_agent", "project": { "name": "simple-openai-api-agent", "owner": "support-platform" @@ -262,7 +262,169 @@ "new_findings_only": false, "would_fail_ci": false, "exit_code": 0 - } + }, + "contribution_rules": [ + { + "finding_id": "fp_c2d773062468ceac", + "fingerprint": "fp_c2d773062468ceac", + "check_id": "SHIP-SCHEMA-MISSING-BOUNDS", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_07538ba8f9532359", + "fingerprint": "fp_07538ba8f9532359", + "check_id": "SHIP-SCHEMA-BROAD-FREE-TEXT", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_58b3202c0a4d9793", + "fingerprint": "fp_58b3202c0a4d9793", + "check_id": "SHIP-AUTH-MISSING-SCOPE", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_45ef3ff3ce2cf187", + "fingerprint": "fp_45ef3ff3ce2cf187", + "check_id": "SHIP-AUTH-MISSING-SCOPE", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_a8a615b5a4f2597b", + "fingerprint": "fp_a8a615b5a4f2597b", + "check_id": "SHIP-SCOPE-PROHIBITED-TOOL-PRESENT", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_cf260ff7c72d64b7", + "fingerprint": "fp_cf260ff7c72d64b7", + "check_id": "SHIP-SCOPE-PROHIBITED-TOOL-PRESENT", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_e9d63903757dfe07", + "fingerprint": "fp_e9d63903757dfe07", + "check_id": "SHIP-SIDEFX-IDEMPOTENCY-MISSING", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_4466eb2871434dc5", + "fingerprint": "fp_4466eb2871434dc5", + "check_id": "SHIP-SIDEFX-IDEMPOTENCY-MISSING", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_9675d1799680d81d", + "fingerprint": "fp_9675d1799680d81d", + "check_id": "SHIP-API-FUNCTION-SCHEMA-STRICTNESS", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_29e718b3bbde0e7d", + "fingerprint": "fp_29e718b3bbde0e7d", + "check_id": "SHIP-API-FUNCTION-SCHEMA-STRICTNESS", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_64f825faa751b7f8", + "fingerprint": "fp_64f825faa751b7f8", + "check_id": "SHIP-API-STRUCTURED-OUTPUT-READINESS", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=medium); routed to review_items." + }, + { + "finding_id": "fp_1b64e136ace3472a_d6a46917", + "fingerprint": "fp_1b64e136ace3472a", + "check_id": "SHIP-API-PROMPT-TOOL-SCOPE-MISMATCH", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_1b64e136ace3472a_6f6fb033", + "fingerprint": "fp_1b64e136ace3472a", + "check_id": "SHIP-API-PROMPT-TOOL-SCOPE-MISMATCH", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=medium); routed to review_items." + }, + { + "finding_id": "fp_28483a22a9ed40cb", + "fingerprint": "fp_28483a22a9ed40cb", + "check_id": "SHIP-API-TIMEOUT-MISSING", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=medium); routed to review_items." + }, + { + "finding_id": "fp_b8df99c94ef3aa60", + "fingerprint": "fp_b8df99c94ef3aa60", + "check_id": "SHIP-API-TOOL-OUTPUT-SCHEMA-MISSING", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=medium); routed to review_items." + }, + { + "finding_id": "fp_2bf957380e89863f", + "fingerprint": "fp_2bf957380e89863f", + "check_id": "SHIP-API-RETRY-WITHOUT-IDEMPOTENCY", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_efb42c5b5aea7be6", + "fingerprint": "fp_efb42c5b5aea7be6", + "check_id": "SHIP-API-RETRY-WITHOUT-IDEMPOTENCY", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_245bcdb96d8220e2", + "fingerprint": "fp_245bcdb96d8220e2", + "check_id": "SHIP-API-TRACE-APPROVAL-MISSING", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=medium); routed to review_items." + }, + { + "finding_id": "fp_d9059d0c1f3540af", + "fingerprint": "fp_d9059d0c1f3540af", + "check_id": "SHIP-MANIFEST-HIGH-RISK-OWNER-MISSING", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_a95adeb6338f8b3e", + "fingerprint": "fp_a95adeb6338f8b3e", + "check_id": "SHIP-MANIFEST-HIGH-RISK-OWNER-MISSING", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + } + ] }, "capability_facts": [ { diff --git a/samples/support_refund_agent/expected/report.json b/samples/support_refund_agent/expected/report.json index 27330ef..7169369 100644 --- a/samples/support_refund_agent/expected/report.json +++ b/samples/support_refund_agent/expected/report.json @@ -1,8 +1,8 @@ { "schema_version": "0.1", - "report_schema_version": "0.16", + "report_schema_version": "0.17", "run_id": "agents_shipgate_6150f69e2312264e", - "manifest_dir": "/Users/pengfeihu/.codex/worktrees/e9c4/shipgate/samples/support_refund_agent", + "manifest_dir": "/Users/threemoonslab/code/agents-shipgate/.claude/worktrees/heuristic-panini-4d4332/samples/support_refund_agent", "project": { "name": "support-refund-agent", "owner": "support-platform", @@ -264,7 +264,153 @@ "new_findings_only": false, "would_fail_ci": false, "exit_code": 0 - } + }, + "contribution_rules": [ + { + "finding_id": "fp_fc02d8ecd30f2578", + "fingerprint": "fp_fc02d8ecd30f2578", + "check_id": "SHIP-INVENTORY-WILDCARD-TOOLS", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_ab60b01cb53cfcbe", + "fingerprint": "fp_ab60b01cb53cfcbe", + "check_id": "SHIP-SCHEMA-MISSING-BOUNDS", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_ff2f028953d1c220", + "fingerprint": "fp_ff2f028953d1c220", + "check_id": "SHIP-SCHEMA-BROAD-FREE-TEXT", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_acd63b899d49aa1c", + "fingerprint": "fp_acd63b899d49aa1c", + "check_id": "SHIP-SCHEMA-BROAD-FREE-TEXT", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_85f8513ad72cd9ea", + "fingerprint": "fp_85f8513ad72cd9ea", + "check_id": "SHIP-SCHEMA-FREEFORM-OUTPUT", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=medium); routed to review_items." + }, + { + "finding_id": "fp_d27325cbdbbf5483", + "fingerprint": "fp_d27325cbdbbf5483", + "check_id": "SHIP-AUTH-MANIFEST-BROAD-SCOPE", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_83852fbd6b440524", + "fingerprint": "fp_83852fbd6b440524", + "check_id": "SHIP-AUTH-SCOPE-COVERAGE-MISSING", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_d8e6d1865dae97cc", + "fingerprint": "fp_d8e6d1865dae97cc", + "check_id": "SHIP-AUTH-SCOPE-COVERAGE-MISSING", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_1f6cfd6b7daa9b7c", + "fingerprint": "fp_1f6cfd6b7daa9b7c", + "check_id": "SHIP-AUTH-SCOPE-COVERAGE-MISSING", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_12985c36a06026de", + "fingerprint": "fp_12985c36a06026de", + "check_id": "SHIP-SCOPE-PROHIBITED-TOOL-PRESENT", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_e090c62e390e70ab", + "fingerprint": "fp_e090c62e390e70ab", + "check_id": "SHIP-SCOPE-PROHIBITED-TOOL-PRESENT", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_f092940f62fbb012", + "fingerprint": "fp_f092940f62fbb012", + "check_id": "SHIP-POLICY-APPROVAL-MISSING", + "category": "blocker", + "rule": "severity_block_new", + "rationale": "severity=critical is in blocker tier (['critical']); baseline_status=null." + }, + { + "finding_id": "fp_a62ca2fd9a68a1d1", + "fingerprint": "fp_a62ca2fd9a68a1d1", + "check_id": "SHIP-POLICY-CONFIRMATION-MISSING", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_8e08a4fe6b0917f6", + "fingerprint": "fp_8e08a4fe6b0917f6", + "check_id": "SHIP-POLICY-CONFIRMATION-MISSING", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_dac8011e14c53777", + "fingerprint": "fp_dac8011e14c53777", + "check_id": "SHIP-SIDEFX-IDEMPOTENCY-MISSING", + "category": "blocker", + "rule": "severity_block_new", + "rationale": "severity=critical is in blocker tier (['critical']); baseline_status=null." + }, + { + "finding_id": "fp_0f8aaa912d589cf0", + "fingerprint": "fp_0f8aaa912d589cf0", + "check_id": "SHIP-SIDEFX-IDEMPOTENCY-MISSING", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_fd2577850cef1f87", + "fingerprint": "fp_fd2577850cef1f87", + "check_id": "SHIP-MANIFEST-HIGH-RISK-OWNER-MISSING", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=high); routed to review_items." + }, + { + "finding_id": "fp_39b9ae878f343d1b", + "fingerprint": "fp_39b9ae878f343d1b", + "check_id": "SHIP-MANIFEST-UNUSED-SCOPE", + "category": "review_item", + "rule": "review_required", + "rationale": "requires_human_review=true (severity=medium); routed to review_items." + } + ] }, "capability_facts": [ { @@ -2538,4 +2684,4 @@ "why": "Surface SHIP-POLICY-APPROVAL-MISSING on stripe.create_refund to the user; release is blocked and no auto-applicable patch is available." } } -} +} \ No newline at end of file diff --git a/scripts/generate_schemas.py b/scripts/generate_schemas.py index 79addea..2633a95 100644 --- a/scripts/generate_schemas.py +++ b/scripts/generate_schemas.py @@ -251,6 +251,9 @@ def write_report_schema() -> None: # v0.8 release_decision: pin required keys so consumers can rely on # the full block being present (Pydantic only marks fields without # defaults as required, but our consumers depend on the whole shape). + # v0.17 adds contribution_rules — a deterministic per-finding audit + # of how each finding contributed to the decision. Required + always + # present (defaults to []) so consumers never need an existence check. if "ReleaseDecision" in defs: defs["ReleaseDecision"]["required"] = sorted( [ @@ -261,6 +264,24 @@ def write_report_schema() -> None: "evidence_coverage", "baseline_delta", "fail_policy", + "contribution_rules", + ] + ) + if "ContributionRule" in defs: + # v0.17: pin the full audit-row contract. `fingerprint` is + # nullable but required-as-key (every emitted row carries the + # field; the value may be null for findings without a computed + # fingerprint). All other fields are required and non-nullable + # on the wire — build_release_decision emits one + # ContributionRule per report finding. + defs["ContributionRule"]["required"] = sorted( + [ + "finding_id", + "fingerprint", + "check_id", + "category", + "rule", + "rationale", ] ) if "ReleaseDecisionItem" in defs: diff --git a/skills/agents-shipgate/SKILL.md b/skills/agents-shipgate/SKILL.md index 8b5d750..7fadb1a 100644 --- a/skills/agents-shipgate/SKILL.md +++ b/skills/agents-shipgate/SKILL.md @@ -63,7 +63,7 @@ For non-GitHub CI (GitLab, CircleCI, Jenkins, Azure Pipelines, Buildkite, Bitbuc - **CLI surface** is frozen for `0.x` — see https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/STABILITY.md. - **Installed CLI contract**: when available, run `agents-shipgate contract --json` to verify local schema versions, `release_decision.decision`, and manual-review signal fields. Older installs should use [`docs/agent-contract-current.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md) or upgrade before automating against the local contract command. -- **Report JSON**: `report_schema_version: "0.16"`. Read `release_decision.decision` (`"blocked" | "review_required" | "insufficient_evidence" | "passed"`) **first** for release gating — it is baseline-aware. `insufficient_evidence` (added v0.14) fires when evidence coverage is degraded past threshold (at least half of scanned tools low-confidence — `ceil(N × 0.5)` with a minimum of 1 — or 4+ source warnings); switch on the enum with a `review_required` fallback for unknown values. For Action Surface Diff read `action_surface_facts`, `action_surface_diff`, and `findings[].blocks_release` (v0.16+) to understand added/removed/modified external actions and explicit release-policy blockers. For one-fetch summarization read the top-level `agent_summary` block (v0.12+) — `{verdict, headline, blocker_count, review_item_count, auto_appliable_patches, needs_human_review, first_recommended_action}`. For per-finding routing read `findings[].agent_action` (v0.12+; `auto_apply | propose_patch_for_review | escalate_to_human | suppress_with_reason | informational`) instead of synthesizing one from `autofix_safe`/`requires_human_review`/`suggested_patch_kind`. To filter findings by source reliability read `findings[].provenance_kind` (v0.15+; `static_declaration | ast_extraction | keyword_heuristic | regex_heuristic | policy_pack`) — independent of `confidence`. Codex plugin facts, when present, live under `codex_plugin_surface` (v0.13+). Do not gate on `summary.status` for new consumers; it is preserved for v0.7 callers and is baseline-blind. The full field list lives in [`docs/agent-contract-current.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md#read-these-first-for-release-gating); this skill links there instead of restating it. v0.11 adds optional `findings[].source.{path, start_line, end_line, start_column, pointer}` provenance keys (kept in v0.16). Reports validate against [`docs/report-schema.v0.16.json`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.16.json) (current). Frozen-reference older schemas (kept for legacy/pre-v0.16 reports): [`v0.15`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.15.json), [`v0.14`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.14.json), [`v0.13`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.13.json), [`v0.12`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.12.json), [`v0.11` (frozen)](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.11.json), [`v0.10` (frozen)](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.10.json), [`v0.9` (frozen)](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.9.json), [`v0.8` (frozen)](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.8.json), and [`v0.7` (frozen)](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.7.json). +- **Report JSON**: `report_schema_version: "0.17"`. Read `release_decision.decision` (`"blocked" | "review_required" | "insufficient_evidence" | "passed"`) **first** for release gating — it is baseline-aware. `insufficient_evidence` (added v0.14) fires when evidence coverage is degraded past threshold (at least half of scanned tools low-confidence — `ceil(N × 0.5)` with a minimum of 1 — or 4+ source warnings); switch on the enum with a `review_required` fallback for unknown values. For per-finding decision audit read `release_decision.contribution_rules[]` (v0.17+) — 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}`. For Action Surface Diff read `action_surface_facts`, `action_surface_diff`, and `findings[].blocks_release` (v0.16+) to understand added/removed/modified external actions and explicit release-policy blockers. For one-fetch summarization read the top-level `agent_summary` block (v0.12+) — `{verdict, headline, blocker_count, review_item_count, auto_appliable_patches, needs_human_review, first_recommended_action}`. For per-finding routing read `findings[].agent_action` (v0.12+; `auto_apply | propose_patch_for_review | escalate_to_human | suppress_with_reason | informational`) instead of synthesizing one from `autofix_safe`/`requires_human_review`/`suggested_patch_kind`. To filter findings by source reliability read `findings[].provenance_kind` (v0.15+; `static_declaration | ast_extraction | keyword_heuristic | regex_heuristic | policy_pack`) — independent of `confidence`. Codex plugin facts, when present, live under `codex_plugin_surface` (v0.13+). Do not gate on `summary.status` for new consumers; it is preserved for v0.7 callers and is baseline-blind. The full field list lives in [`docs/agent-contract-current.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md#read-these-first-for-release-gating); this skill links there instead of restating it. v0.11 adds optional `findings[].source.{path, start_line, end_line, start_column, pointer}` provenance keys (kept in v0.17). Reports validate against [`docs/report-schema.v0.17.json`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.17.json) (current). Frozen-reference older schemas (kept for legacy/pre-v0.17 reports): [`v0.16`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.16.json), [`v0.15`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.15.json), [`v0.14`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.14.json), [`v0.13`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.13.json), [`v0.12`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.12.json), [`v0.11` (frozen)](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.11.json), [`v0.10` (frozen)](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.10.json), [`v0.9` (frozen)](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.9.json), [`v0.8` (frozen)](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.8.json), and [`v0.7` (frozen)](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.7.json). - **Release Evidence Packet**: `agents-shipgate-reports/packet.{md,json,html}` (and `packet.pdf` with the `[pdf]` extras) is emitted alongside the report by default. The packet has fixed reviewer sections governed by [`docs/packet-schema.v0.5.json`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/packet-schema.v0.5.json) (current) — see [STABILITY.md §Release Evidence Packet](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/STABILITY.md#release-evidence-packet-v05). Use the packet for reviewer-shaped output; use the report for finding details. - **Single source of truth for the contract**: [`docs/agent-contract-current.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md). When the schema bumps, that file updates first. - **Exit codes**: `0` pass, `2` config error, `3` parse error, `4` other error, `20` strict-mode gate failure. diff --git a/src/agents_shipgate/ci/release_decision.py b/src/agents_shipgate/ci/release_decision.py index 68b2f5b..9b3adc7 100644 --- a/src/agents_shipgate/ci/release_decision.py +++ b/src/agents_shipgate/ci/release_decision.py @@ -3,12 +3,13 @@ import math from agents_shipgate.ci.exit_policy import ( - baseline_filtered_active, effective_fail_on, exit_code_for_report, ) from agents_shipgate.core.models import ( BaselineDelta, + ContributionRule, + ContributionRuleName, EvidenceCoverageDecision, FailPolicy, Finding, @@ -41,31 +42,103 @@ def build_release_decision( ) -> ReleaseDecision: fail_on_resolved = effective_fail_on(ci_mode, fail_on) - # blockers/review_items consider the full active set, NOT + # blockers/review_items consider the full findings set, NOT # new_findings_only: baseline-matched criticals must remain visible # as accepted debt in review_items. The new_findings_only filter # only affects fail_policy.exit_code (via exit_code_for_report). - active = baseline_filtered_active(report, new_findings_only=False) - + # v0.17: iterate report.findings directly so the contribution_rules + # audit row set is exhaustive (suppressed findings get an audit row + # too, classified as excluded/suppressed). blockers: list[ReleaseDecisionItem] = [] review_items: list[ReleaseDecisionItem] = [] + contribution_rules: list[ContributionRule] = [] blocker_severities: set[Severity] = {"critical", *fail_on_resolved} - for finding in active: + # v0.17: iterate the FULL findings list (not just `active`) so the + # audit row set is exhaustive over report.findings. The branching + # below mirrors the original active classification exactly — same + # `if/elif/elif` shape, same fall-through to silent-drop — so the + # blockers[]/review_items[] lists are byte-identical to v0.16. The + # only addition is one ContributionRule per finding documenting + # which branch fired (or, for the silent-drop tail, which baseline + # acceptance silently consumed it). + for finding in report.findings: + if finding.suppressed: + contribution_rules.append( + _rule( + finding, + category="excluded", + rule="suppressed", + rationale="Finding suppressed via checks.ignore in the manifest.", + ) + ) + continue + # Branch 1: explicit policy blocker, not baseline-matched. if finding.blocks_release and finding.baseline_status != "matched": blockers.append(_to_item(finding)) + contribution_rules.append( + _rule( + finding, + category="blocker", + rule="policy_block_new", + rationale=( + f"blocks_release=true and baseline_status=" + f"{finding.baseline_status or 'null'}; " + "explicit policy blocker." + ), + ) + ) continue + # Branch 2: severity in active blocker tier, not baseline-matched. if ( finding.baseline_status != "matched" and finding.severity in blocker_severities ): blockers.append(_to_item(finding)) + contribution_rules.append( + _rule( + finding, + category="blocker", + rule="severity_block_new", + rationale=( + f"severity={finding.severity} is in blocker tier " + f"({sorted(blocker_severities)}); " + f"baseline_status={finding.baseline_status or 'null'}." + ), + ) + ) continue + # Branch 3: review tier (severity C/H/M or requires_human_review). + # The rule name distinguishes WHY the finding landed here: + # - matched policy → policy_baseline_accepted + # - matched severity-tier → severity_baseline_accepted + # - otherwise → review_required (severity in C/H/M without + # matching blocker tier, or requires_human_review=True) if ( finding.severity in {"critical", "high", "medium"} or finding.requires_human_review is True ): review_items.append(_to_item(finding)) + contribution_rules.append( + _rule( + finding, + category="review_item", + rule=_review_rule_for(finding, blocker_severities), + rationale=_review_rationale_for(finding, blocker_severities), + ) + ) + continue + # Branch 4 (fall-through): sub-threshold or silently-accepted + # baseline debt below the review tier. Original code dropped + # these silently; v0.17 records why. + contribution_rules.append( + _rule( + finding, + category="excluded", + rule=_excluded_rule_for(finding, blocker_severities), + rationale=_excluded_rationale_for(finding, blocker_severities), + ) + ) low_confidence_tool_count = sum( 1 for tool in tools if tool.extraction_confidence != "high" @@ -137,6 +210,136 @@ def build_release_decision( evidence_coverage=evidence, baseline_delta=baseline_delta, fail_policy=fail_policy, + contribution_rules=contribution_rules, + ) + + +def _rule( + finding: Finding, + *, + category: str, + rule: ContributionRuleName, + rationale: str, +) -> ContributionRule: + # `Finding.id` and `Finding.fingerprint` are Python-Optional — + # `assign_finding_ids()` populates them on the normal scan path, + # but direct/internal callers (tests constructing minimal Findings, + # `explain-finding` rebuilding from a stripped report, plugin + # checks that emit Findings before id assignment) may pass + # findings with both unset. ContributionRule.finding_id is + # required-as-string on the wire, so fall back through fingerprint + # to check_id (which is always a non-empty string per the model + # contract). The audit row stays useful in every case: even + # without an id, a reviewer can match the row back to the finding + # via fingerprint or, last resort, the check_id. + return ContributionRule( + finding_id=finding.id or finding.fingerprint or finding.check_id, + fingerprint=finding.fingerprint, + check_id=finding.check_id, + category=category, # type: ignore[arg-type] + rule=rule, + rationale=rationale, + ) + + +def _review_rule_for( + finding: Finding, blocker_severities: set[Severity] +) -> ContributionRuleName: + """Disambiguate the rule name when a finding lands in review_items. + + Three cases reach the review-tier branch in build_release_decision: + - Policy finding (`blocks_release=True`) + baseline_status="matched": + would have been a `policy_block_new` blocker if not matched → + `policy_baseline_accepted`. + - Severity in active blocker tier + baseline_status="matched": + would have been `severity_block_new` if not matched → + `severity_baseline_accepted`. + - Otherwise (severity in {C,H,M} but not in blocker tier, or + requires_human_review=True): plain `review_required`. + """ + if finding.blocks_release and finding.baseline_status == "matched": + return "policy_baseline_accepted" + if ( + finding.baseline_status == "matched" + and finding.severity in blocker_severities + ): + return "severity_baseline_accepted" + return "review_required" + + +def _review_rationale_for( + finding: Finding, blocker_severities: set[Severity] +) -> str: + if finding.blocks_release and finding.baseline_status == "matched": + return ( + "blocks_release=true and baseline_status=matched; " + "accepted as policy debt and routed to review_items." + ) + if ( + finding.baseline_status == "matched" + and finding.severity in blocker_severities + ): + return ( + f"severity={finding.severity} is in blocker tier " + f"({sorted(blocker_severities)}) but baseline_status=matched; " + "accepted as debt." + ) + if finding.requires_human_review is True: + return ( + f"requires_human_review=true (severity={finding.severity}); " + "routed to review_items." + ) + return ( + f"severity={finding.severity}; below active blocker tier " + f"({sorted(blocker_severities)}) but in review tier " + "{critical, high, medium}." + ) + + +def _excluded_rule_for( + finding: Finding, blocker_severities: set[Severity] +) -> ContributionRuleName: + """Disambiguate the rule name when a finding falls through to excluded. + + Two reachable cases: + - blocks_release=True + matched + severity below review tier: + original code drops silently → `policy_baseline_accepted` (with + excluded category, since severity didn't reach the review fall- + through above). + - severity in blocker tier + matched + severity below review tier: + same shape → `severity_baseline_accepted`. + - Otherwise: plain `sub_threshold`. + """ + if finding.blocks_release and finding.baseline_status == "matched": + return "policy_baseline_accepted" + if ( + finding.baseline_status == "matched" + and finding.severity in blocker_severities + ): + return "severity_baseline_accepted" + return "sub_threshold" + + +def _excluded_rationale_for( + finding: Finding, blocker_severities: set[Severity] +) -> str: + if finding.blocks_release and finding.baseline_status == "matched": + return ( + "blocks_release=true and baseline_status=matched, but " + f"severity={finding.severity} is below review tier; " + "excluded from blockers and review_items." + ) + if ( + finding.baseline_status == "matched" + and finding.severity in blocker_severities + ): + return ( + f"severity={finding.severity} in blocker tier with " + "baseline_status=matched, but below review tier; excluded." + ) + return ( + f"severity={finding.severity}; below active blocker tier and " + "below review tier." ) diff --git a/src/agents_shipgate/contract.py b/src/agents_shipgate/contract.py index 7cb1a2b..d57e332 100644 --- a/src/agents_shipgate/contract.py +++ b/src/agents_shipgate/contract.py @@ -15,6 +15,12 @@ # Adding `gating_signal_values` would be a `contract_version: "2"` change. MANUAL_REVIEW_SIGNALS: tuple[str, ...] = ( "release_decision.review_items", + # v0.17: per-finding decision audit. Reviewers triaging + # `release_decision.review_items` use the corresponding + # `contribution_rules[]` row to see WHY each item was classified + # as a review item (`policy_baseline_accepted`, + # `severity_baseline_accepted`, or `review_required`). + "release_decision.contribution_rules", "findings[].requires_human_review", "findings[].blocks_release", "summary.human_review_recommended", diff --git a/src/agents_shipgate/core/models.py b/src/agents_shipgate/core/models.py index c1b63a8..c148f02 100644 --- a/src/agents_shipgate/core/models.py +++ b/src/agents_shipgate/core/models.py @@ -720,6 +720,56 @@ class FailPolicy(BaseModel): exit_code: int +# v0.17: explicit, deterministic per-finding audit of *why* each finding +# landed in `blockers[]`, `review_items[]`, or was excluded. The set of +# rule names below is the entire grammar of decisions the gate can make; +# the truth table in STABILITY.md "Release decision truth table" is the +# external contract for what each name means and when it fires. +ContributionRuleName = Literal[ + # Active blockers (drive `decision="blocked"` and, in strict mode, + # exit code 20 when the underlying finding is not baseline-matched + # via `--baseline-mode new-findings`). + "policy_block_new", + "severity_block_new", + # Accepted as baseline debt; visible in `review_items[]` instead of + # `blockers[]`. Never escalates the decision past `review_required`. + "policy_baseline_accepted", + "severity_baseline_accepted", + # Routed to `review_items[]` for human attention but does not block + # by itself. + "review_required", + # Below the active gate threshold AND below review tier; recorded + # for completeness so the audit table is exhaustive over + # report.findings. + "sub_threshold", + # Suppressed via `checks.ignore[]` in the manifest; excluded from + # the active set entirely. + "suppressed", +] + + +class ContributionRule(BaseModel): + """Per-finding audit row explaining how a finding contributed to the + release decision. + + Additive in v0.17. Every finding in `report.findings` produces + exactly one ContributionRule. Reading the contribution rule is + sufficient to predict the gate outcome for that finding without + re-deriving the decision logic; the set of valid `(rule, category)` + pairs is the contract documented in STABILITY.md "Release decision + truth table". + """ + + model_config = ConfigDict(extra="forbid") + + finding_id: str + fingerprint: str | None = None + check_id: str + category: Literal["blocker", "review_item", "excluded"] + rule: ContributionRuleName + rationale: str + + class ReleaseDecision(BaseModel): decision: ReleaseDecisionStatus reason: str @@ -728,6 +778,12 @@ class ReleaseDecision(BaseModel): evidence_coverage: EvidenceCoverageDecision baseline_delta: BaselineDelta fail_policy: FailPolicy + # v0.17: deterministic per-finding audit of how each finding + # contributed to the decision. Always present (defaults to []) so + # consumers that read `release_decision.contribution_rules` never + # need an existence check; older reports loaded via + # `explain-finding` or test helpers naturally get an empty list. + contribution_rules: list[ContributionRule] = Field(default_factory=list) DeclaredIntentionKind = Literal[ @@ -1306,7 +1362,7 @@ class ReadinessReport(BaseModel): model_config = ConfigDict(extra="allow") schema_version: str = "0.1" - report_schema_version: str = "0.16" + report_schema_version: str = "0.17" run_id: str # v0.6 (per C13): absolute path to the directory containing # shipgate.yaml. apply-patches uses this to enforce a containment diff --git a/tests/test_provenance_kind.py b/tests/test_provenance_kind.py index 01e4db1..298828b 100644 --- a/tests/test_provenance_kind.py +++ b/tests/test_provenance_kind.py @@ -25,7 +25,7 @@ Path("samples/simple_crewai_agent/shipgate.yaml"), ] -CURRENT_SCHEMA = Path("docs/report-schema.v0.16.json") +CURRENT_SCHEMA = Path("docs/report-schema.v0.17.json") def test_provenance_kind_enum_values(): diff --git a/tests/test_public_surface_contract.py b/tests/test_public_surface_contract.py index 555bb08..cabfbb9 100644 --- a/tests/test_public_surface_contract.py +++ b/tests/test_public_surface_contract.py @@ -50,7 +50,7 @@ # Frozen report schemas that still appear in public surfaces must be labeled as # frozen/legacy/older instead of being mistaken for the current schema. LEGACY_REPORT_SCHEMA_PATTERN = re.compile( - r"report-schema\.v0\.(?:7|8|9|10|11|12|13|14|15)\.json" + r"report-schema\.v0\.(?:7|8|9|10|11|12|13|14|15|16)\.json" ) ANY_REPORT_SCHEMA_PATTERN = re.compile(r"report-schema\.v0\.\d+\.json") ANY_PACKET_SCHEMA_PATTERN = re.compile(r"packet-schema\.v\d+\.\d+\.json") diff --git a/tests/test_release_decision.py b/tests/test_release_decision.py index 617af87..16ac6cc 100644 --- a/tests/test_release_decision.py +++ b/tests/test_release_decision.py @@ -396,3 +396,334 @@ def test_decision_reason_strings_are_deterministic( report = _report(findings=[_finding(**findings_kwargs)]) decision = _build(report, **build_kwargs) assert expected_keyword in decision.reason + + +# --------------------------------------------------------------------------- +# v0.17 contribution_rules truth table. +# +# The truth-table contract documented in STABILITY.md +# "Release decision truth table" describes which (rule, category) pair +# fires for every (blocks_release, severity, baseline_status, fail_on) +# combination. Each parametrized case below is one row of that table; +# the row is named so a failure points at the exact contract it +# violates. +# +# Inputs are kept minimal — one finding per case — so a regression in +# build_release_decision picks exactly one named test, and the +# contribution rule under test is the only audit row produced. +# --------------------------------------------------------------------------- + + +def _policy_finding( + *, + severity: str = "high", + baseline_status: str | None = None, + suppressed: bool = False, +) -> Finding: + f = _finding( + severity=severity, + baseline_status=baseline_status, + suppressed=suppressed, + check_id="check.policy", + ) + f.blocks_release = True + return f + + +@pytest.mark.parametrize( + "case_name,finding_factory,build_kwargs,expected_category,expected_rule,expected_in_blockers,expected_in_review", + [ + # ---- blocks_release=true paths ----------------------------------- + ( + "policy_block_new_unbaselined", + lambda: _policy_finding(severity="high", baseline_status="new"), + {"ci_mode": "advisory"}, + "blocker", + "policy_block_new", + True, + False, + ), + ( + "policy_block_new_no_baseline", + lambda: _policy_finding(severity="high", baseline_status=None), + {"ci_mode": "advisory"}, + "blocker", + "policy_block_new", + True, + False, + ), + ( + "policy_baseline_accepted_review_tier", + # severity in {C,H,M} → falls into review tier when matched + lambda: _policy_finding(severity="high", baseline_status="matched"), + {"ci_mode": "advisory"}, + "review_item", + "policy_baseline_accepted", + False, + True, + ), + ( + "policy_baseline_accepted_below_review_tier", + # severity below review tier → silently dropped in v0.16; v0.17 + # records the audit row but the finding stays excluded. + lambda: _policy_finding(severity="low", baseline_status="matched"), + {"ci_mode": "advisory"}, + "excluded", + "policy_baseline_accepted", + False, + False, + ), + # ---- severity-driven paths -------------------------------------- + ( + "severity_block_new_critical_default", + lambda: _finding(severity="critical", baseline_status="new"), + {"ci_mode": "advisory"}, # critical always in blocker_severities floor + "blocker", + "severity_block_new", + True, + False, + ), + ( + "severity_block_new_high_via_fail_on", + lambda: _finding(severity="high", baseline_status="new"), + {"ci_mode": "advisory", "fail_on": ["high"]}, + "blocker", + "severity_block_new", + True, + False, + ), + ( + "severity_baseline_accepted_critical", + lambda: _finding(severity="critical", baseline_status="matched"), + {"ci_mode": "advisory"}, + "review_item", + "severity_baseline_accepted", + False, + True, + ), + # ---- review-tier paths ------------------------------------------ + ( + "review_required_high_no_fail_on", + # severity=high but advisory-default fail_on=[] → not a blocker; + # severity in {C,H,M} → review_required. + lambda: _finding(severity="high", baseline_status="new"), + {"ci_mode": "advisory"}, + "review_item", + "review_required", + False, + True, + ), + ( + "review_required_medium_no_fail_on", + lambda: _finding(severity="medium", baseline_status="new"), + {"ci_mode": "advisory"}, + "review_item", + "review_required", + False, + True, + ), + ( + "review_required_low_with_human_review_flag", + # severity below review tier but requires_human_review=True + # explicitly routes to review_items. + lambda: _finding( + severity="low", baseline_status="new", requires_human_review=True + ), + {"ci_mode": "advisory"}, + "review_item", + "review_required", + False, + True, + ), + # ---- sub-threshold (excluded) ---------------------------------- + ( + "sub_threshold_low", + lambda: _finding(severity="low", baseline_status="new"), + {"ci_mode": "advisory"}, + "excluded", + "sub_threshold", + False, + False, + ), + ( + "sub_threshold_info", + lambda: _finding(severity="info", baseline_status="new"), + {"ci_mode": "advisory"}, + "excluded", + "sub_threshold", + False, + False, + ), + # ---- suppressed ------------------------------------------------ + ( + "suppressed_critical_excluded", + lambda: _finding( + severity="critical", baseline_status="new", suppressed=True + ), + {"ci_mode": "strict"}, + "excluded", + "suppressed", + False, + False, + ), + ], +) +def test_contribution_rules_truth_table( + case_name, + finding_factory, + build_kwargs, + expected_category, + expected_rule, + expected_in_blockers, + expected_in_review, +): + """Every row of STABILITY.md "Release decision truth table" is + exercised here. The audit's (category, rule) pair must match the + documented contract, and the underlying blockers[]/review_items[] + membership must agree with the audit (no contradictions allowed). + """ + finding = finding_factory() + report = _report(findings=[finding]) + decision = _build(report, **build_kwargs) + + # Audit row must exist for this finding. + rules_for = [r for r in decision.contribution_rules if r.finding_id == finding.id] + assert len(rules_for) == 1, ( + f"{case_name}: expected exactly one contribution rule for the " + f"finding, got {len(rules_for)}: {rules_for}" + ) + rule = rules_for[0] + assert rule.category == expected_category, ( + f"{case_name}: expected category={expected_category!r}, " + f"got {rule.category!r}; rationale: {rule.rationale}" + ) + assert rule.rule == expected_rule, ( + f"{case_name}: expected rule={expected_rule!r}, " + f"got {rule.rule!r}; rationale: {rule.rationale}" + ) + + # Audit must agree with the underlying lists. + in_blockers = any(b.id == finding.id for b in decision.blockers) + in_review = any(r.id == finding.id for r in decision.review_items) + assert in_blockers is expected_in_blockers, ( + f"{case_name}: in_blockers mismatch (rule said {expected_category!r})" + ) + assert in_review is expected_in_review, ( + f"{case_name}: in_review_items mismatch (rule said {expected_category!r})" + ) + + +def test_contribution_rules_audit_row_per_finding(): + """The audit must be exhaustive: one row per report.findings entry, + including suppressed findings. No finding can be silently absent. + """ + findings = [ + _finding(check_id="c1", severity="critical", baseline_status="new"), + _finding(check_id="c2", severity="high", baseline_status="matched"), + _finding(check_id="c3", severity="low", baseline_status="new"), + _finding( + check_id="c4", severity="critical", baseline_status="new", suppressed=True + ), + ] + report = _report(findings=findings) + decision = _build(report, ci_mode="strict") + + audit_finding_ids = {r.finding_id for r in decision.contribution_rules} + expected = {f.id for f in findings} + assert audit_finding_ids == expected, ( + "contribution_rules must contain exactly one row per finding " + f"(missing: {expected - audit_finding_ids}, " + f"extra: {audit_finding_ids - expected})" + ) + + # Each audit row's category is one of the documented values. + for rule in decision.contribution_rules: + assert rule.category in {"blocker", "review_item", "excluded"} + assert rule.rule in { + "policy_block_new", + "severity_block_new", + "policy_baseline_accepted", + "severity_baseline_accepted", + "review_required", + "sub_threshold", + "suppressed", + } + assert rule.rationale, "rationale must be non-empty" + + +def test_contribution_rules_audit_works_without_finding_id(): + """`Finding.id` is Python-Optional. Direct callers — internal tests, + plugin checks that emit Findings before `assign_finding_ids` runs, + `explain-finding` rebuilding from a stripped report — may invoke + `build_release_decision` with `finding.id is None`. The audit row's + `finding_id` is required-as-string on the wire, so the gate must + fall back through `fingerprint` to `check_id` rather than raising + a Pydantic ValidationError. Regression for P2 review feedback on + PR #81.""" + finding_with_fingerprint = Finding( + id=None, + fingerprint="fp_unit_test_fingerprint", + check_id="SHIP-UNIT-TEST-CHECK", + title="title", + severity="critical", + category="test", + recommendation="do the thing", + ) + finding_without_anything = Finding( + id=None, + fingerprint=None, + check_id="SHIP-UNIT-TEST-NOID", + title="title", + severity="high", + category="test", + recommendation="do the thing", + ) + report = _report(findings=[finding_with_fingerprint, finding_without_anything]) + # Should not raise. + decision = _build(report, ci_mode="strict", fail_on=["high"]) + + # Both audit rows present; fallback chain produced a usable id. + assert len(decision.contribution_rules) == 2 + by_check = {r.check_id: r for r in decision.contribution_rules} + # Fingerprint fallback when id is None but fingerprint is set. + assert ( + by_check["SHIP-UNIT-TEST-CHECK"].finding_id == "fp_unit_test_fingerprint" + ) + # check_id fallback when both id and fingerprint are None. + assert by_check["SHIP-UNIT-TEST-NOID"].finding_id == "SHIP-UNIT-TEST-NOID" + # finding_id is never the empty string. + for rule in decision.contribution_rules: + assert rule.finding_id, "finding_id must be non-empty" + + +def test_contribution_rules_default_to_empty_for_legacy_report(): + """A `ReleaseDecision` constructed without `contribution_rules` + (e.g., loaded from a v0.16 report via explain-finding, or built by + minimal test helpers) must accept the missing field and default to + an empty list. Forward-compat for old reports.""" + from agents_shipgate.core.models import ( + BaselineDelta, + EvidenceCoverageDecision, + FailPolicy, + ReleaseDecision, + ) + + decision = ReleaseDecision( + decision="passed", + reason="ok", + evidence_coverage=EvidenceCoverageDecision( + level="full", + human_review_recommended=False, + source_warning_count=0, + low_confidence_tool_count=0, + ), + baseline_delta=BaselineDelta(enabled=False), + fail_policy=FailPolicy( + ci_mode="advisory", + fail_on=[], + new_findings_only=False, + would_fail_ci=False, + exit_code=0, + ), + ) + assert decision.contribution_rules == [] diff --git a/tests/test_reports.py b/tests/test_reports.py index bef213b..422d319 100644 --- a/tests/test_reports.py +++ b/tests/test_reports.py @@ -30,6 +30,7 @@ REPORT_SCHEMA_V14 = Path("docs/report-schema.v0.14.json") REPORT_SCHEMA_V15 = Path("docs/report-schema.v0.15.json") REPORT_SCHEMA_V16 = Path("docs/report-schema.v0.16.json") +REPORT_SCHEMA_V17 = Path("docs/report-schema.v0.17.json") CURRENT_REPORT_SCHEMA_VERSION = str( ReadinessReport.model_fields["report_schema_version"].default ) @@ -510,8 +511,9 @@ def test_json_schema_is_published(): def test_json_report_validates_against_current_schema(tmp_path): - """Current schema includes action-surface diff fields on top of - v0.15 provenance_kind. Emitted reports must validate against it.""" + """Current schema (v0.17) adds release_decision.contribution_rules[] + on top of v0.16's action-surface diff fields and v0.15's + provenance_kind. Emitted reports must validate against it.""" from agents_shipgate.report.json_report import report_json_payload report, _ = run_scan( @@ -520,7 +522,7 @@ def test_json_report_validates_against_current_schema(tmp_path): formats=["json"], ci_mode="advisory", ) - schema = json.loads(REPORT_SCHEMA_V16.read_text(encoding="utf-8")) + schema = json.loads(REPORT_SCHEMA_V17.read_text(encoding="utf-8")) validate(instance=report_json_payload(report), schema=schema) @@ -861,6 +863,56 @@ def test_v10_schema_requires_release_decision_and_diffs(): } <= diff_required +def test_v17_schema_requires_contribution_rules(): + """v0.17 adds release_decision.contribution_rules[] — a deterministic + per-finding audit of how each finding contributed to the decision. + The field is required + always present (defaults to []) so consumers + never need an existence check. Locks the v0.17 contract; M8 of the + trust-hardening pass.""" + schema = json.loads(REPORT_SCHEMA_V17.read_text(encoding="utf-8")) + assert schema["properties"]["report_schema_version"] == {"const": "0.17"} + # ReleaseDecision still requires the v0.8 baseline of fields plus + # the v0.17 audit field. + release_decision_required = set(schema["$defs"]["ReleaseDecision"]["required"]) + assert "contribution_rules" in release_decision_required + # Every prior required key is preserved. + assert { + "decision", + "reason", + "blockers", + "review_items", + "evidence_coverage", + "baseline_delta", + "fail_policy", + } <= release_decision_required + # ContributionRule shape is pinned: every emitted row carries + # finding_id / fingerprint(key, may be null) / check_id / category / + # rule / rationale. + contrib_def = schema["$defs"]["ContributionRule"] + assert set(contrib_def["required"]) == { + "finding_id", + "fingerprint", + "check_id", + "category", + "rule", + "rationale", + } + # Both the rule enum and the category enum are inlined; agents that + # switch on the audit need to know the closed grammar. + rule_enum = set(contrib_def["properties"]["rule"]["enum"]) + assert rule_enum == { + "policy_block_new", + "severity_block_new", + "policy_baseline_accepted", + "severity_baseline_accepted", + "review_required", + "sub_threshold", + "suppressed", + } + category_enum = set(contrib_def["properties"]["category"]["enum"]) + assert category_enum == {"blocker", "review_item", "excluded"} + + def test_v16_schema_requires_action_surface_fields(): """v0.16 adds first-class Action Surface Diff facts and diff fields.""" schema = json.loads(REPORT_SCHEMA_V16.read_text(encoding="utf-8")) @@ -892,7 +944,7 @@ def test_current_schema_rejects_null_release_decision_and_consequence(tmp_path): """A current payload with null release blocks MUST fail validation. Regression for the original schema which emitted `anyOf: [ReleaseDecision, null]` and silently accepted null. Invariant - carries forward unchanged from v0.13/v0.14.""" + carries forward unchanged from v0.13/v0.14/v0.15/v0.16.""" import jsonschema from agents_shipgate.report.json_report import report_json_payload @@ -903,7 +955,7 @@ def test_current_schema_rejects_null_release_decision_and_consequence(tmp_path): formats=["json"], ci_mode="advisory", ) - schema = json.loads(REPORT_SCHEMA_V16.read_text(encoding="utf-8")) + schema = json.loads(REPORT_SCHEMA_V17.read_text(encoding="utf-8")) payload = report_json_payload(report) # Sanity: real payload validates.