# SPDX-License-Identifier: MPL-2.0
# Gap Matrix — alerts × hypatia × bots × directives
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath)
#
# Scope: GitHub source-of-truth only (mirror-side sweep is PARKED — see
#        ~/Desktop/AI-WORK.md §0, requires Atlassian + GitLab tokens).
#        When the mirror sweep runs, a sibling gap-matrix-mirror-*.a2ml
#        will cover the delta-vs-GitHub column.
#
# Purpose: identify where a GitHub security signal does NOT currently
#          have end-to-end coverage through Hypatia rules + gitbot-fleet
#          + bot directives. Each gap row is an actionable wiring task.
#
# Method: structural matrix first (signal → rule → bot → directive),
#         then concrete-alert sample from 5 representative repos (2026-04-17).

@abstract:
Cross-reference of GitHub security signals against Hypatia rule coverage,
gitbot-fleet action capability, and bot directive presence. Output row =
one signal × one repo; gap = missing coverage at any stage of the pipeline.
@end

## Pipeline stages

[[stages]]
id = "signal"
description = "GitHub security signal (Dependabot, CodeQL, secret-scanning, workflow)"

[[stages]]
id = "rule"
description = "Hypatia rule module that classifies / routes the signal"

[[stages]]
id = "bot"
description = "gitbot-fleet bot that can take a physical action (PR, comment, patch)"

[[stages]]
id = "directive"
description = "Bot directive in .machine_readable/bot_directives/<bot>.a2ml governing per-repo behaviour"

## Structural matrix — signal → rule → bot → directive

[[coverage]]
signal_type = "dependabot.alert.open"
hypatia_rule = "Hypatia.Rules.DependabotAlerts (DA001-DA008, classify_alert, to_recipe, fixes_from_alerts)"
dispatch_clause = "FleetDispatcher.dispatch_routed_action({:dependabot_fix, recipe, pattern}) — wired commit dd64722"
bot = "rhodibot (substitute tier — PR open) | sustainabot (control tier — advisory) | rhodibot (eliminate tier — auto-dispatch via Fleet)"
directive_coverage = "present in all 44 migrated repos post-2026-04-17 A2ML sweep"
status = "wired"
notes = "Full pipeline: alert → classify → safety-triangle tier → dispatch → bot action → (Kin Gate + rate limiter) → PR/comment"

[[coverage]]
signal_type = "dependabot.alert.dismissed"
hypatia_rule = "classify_alert → :control tier (respects human dismissal)"
dispatch_clause = "control → sustainabot advisory only"
bot = "sustainabot"
directive_coverage = "yes"
status = "wired"
notes = "Explicit no-op on dismissed/fixed alerts — safety triangle respects human acceptance of risk."

[[coverage]]
signal_type = "codeql.finding"
hypatia_rule = "Hypatia.Rules.CodeSafety (partial — covers SHA-pinning, language policy, secret detection)"
dispatch_clause = "missing — no FleetDispatcher clause routes CodeQL alerts"
bot = "cipherbot (for crypto findings) | panicbot (for unsafe patterns) | rhodibot (for version pins)"
directive_coverage = "partial — cipherbot.a2ml and panicbot.a2ml present; no CodeQL-specific directive schema"
status = "GAP"
notes = "CodeQL alerts visible via `gh api repos/.../code-scanning/alerts` but no Hypatia rule currently consumes them. Symmetric work to DA001-DA008 pending."

[[coverage]]
signal_type = "secret_scanning.alert"
hypatia_rule = "Hypatia.Rules.SecurityErrors.detect_secrets/1 — detection only, no alert-listing"
dispatch_clause = "missing"
bot = "cipherbot would be the natural handler"
directive_coverage = "cipherbot.a2ml present but no secret-scanning-alert schema"
status = "GAP"
notes = "GitHub secret-scanning alerts not consumed by Hypatia. Cipherbot has the capability (rotate/revoke workflows exist) but no trigger path."

[[coverage]]
signal_type = "workflow.failure (CI red)"
hypatia_rule = "Hypatia.Rules.WorkflowAudit + Hypatia.Rules.CICDRules"
dispatch_clause = "FleetDispatcher dispatches to panicbot / rhodibot for SHA-pin drift + workflow repair"
bot = "rhodibot (SHA-pin updates) | panicbot (test-failure triage) | finishingbot (stale-PR pruning)"
directive_coverage = "rhodibot.a2ml, panicbot.a2ml, finishingbot.a2ml present"
status = "wired"
notes = "Note: GitHub Actions billing freeze (AI-WORK.md §0 blocker 2026-04-17) blinds this signal — cannot distinguish CI reds from billing stalls until resolved."

[[coverage]]
signal_type = "scorecard.score_drop"
hypatia_rule = "Hypatia.Rules.ScorecardCompliance"
dispatch_clause = "missing — rule reads scorecard but no dispatch to auto-remediate drops"
bot = "rhodibot (for branch-protection / SHA-pin fixes) | sustainabot (for maintenance-signal drops)"
directive_coverage = "yes for both bots"
status = "GAP"
notes = "Rule reports score but does not route to remediation. Wiring comparable to DA005-DA008 would be symmetric work."

[[coverage]]
signal_type = "dependabot.pr.open (pull_request)"
hypatia_rule = "none — Dependabot-generated PRs not inspected"
dispatch_clause = "missing"
bot = "rhodibot could merge safe ones; finishingbot could prune stale ones"
directive_coverage = "neither has a Dependabot-PR-specific directive schema"
status = "GAP"
notes = "Many Dependabot PRs sit open across the estate — a future DA009 rule + dispatch clause would close this loop."

## Sampled concrete data — 5 repos, 2026-04-17

[[sample]]
repo = "hyperpolymath/standards"
total_open_alerts = 5
distinct_advisories = 3
classification_summary = "5× :eliminate (rust patch bumps, confidence ≥0.95) | 0× :substitute | 0× :control"
notes = "All 5 are Rust low-severity patch-in-0.x-series. Post-wiring (commit dd64722), these should auto-route to rhodibot via dispatch_eliminate_via_fleet."

[[sample.advisories]]
ghsa = "GHSA-965h-392x-2mh5"
package = "rustls-webpki"
ecosystem = "rust"
vulnerable_range = ">= 0.101.0, < 0.103.12"
first_patched = "0.103.12"
bump_class = "patch (within 0.103 series)"
hypatia_classification = ":eliminate — strict-semver ecosystem + patch bump → confidence ≥ 0.95"
expected_route = "Fleet → rhodibot PR (auto-executable tier)"

[[sample.advisories]]
ghsa = "GHSA-xgp8-3hg3-c2mh"
package = "rustls-webpki"
ecosystem = "rust"
vulnerable_range = ">= 0.101.0, < 0.103.12"
first_patched = "0.103.12"
bump_class = "patch"
hypatia_classification = ":eliminate"
expected_route = "Fleet → rhodibot PR"

[[sample.advisories]]
ghsa = "GHSA-cq8v-f236-94qc"
package = "rand"
ecosystem = "rust"
vulnerable_range = ">= 0.7.0, < 0.9.3"
first_patched = "0.9.3"
bump_class = "major (pre-1.0 — 0.7 → 0.9 crosses minor under 0.x semantics)"
hypatia_classification = ":substitute — major bump flagged for review"
expected_route = "rhodibot PR with requires_human = true"

[[sample]]
repo = "hyperpolymath/gitbot-fleet"
total_open_alerts = 18
distinct_advisories = 3
classification_summary = "same 3 GHSAs as standards, multiplied across 6 workspace crates each"
notes = "18 = 3 advisories × 6 crates. Fleet dispatch should deduplicate at the advisory level (one PR per advisory, not per Cargo.lock occurrence). Worth verifying the dedup logic in fixes_from_alerts."

[[sample]]
repo = "hyperpolymath/boj-server"
total_open_alerts = 3
distinct_advisories = 3
classification_summary = "same 3 GHSAs, single occurrence each"
notes = "Clean 3-for-3 pattern. Disconnected status resolved 2026-04-20 (Deno permissions + Elixir backend). 365 tests passing."

[[sample]]
repo = "hyperpolymath/idaptik"
total_open_alerts = 1
distinct_advisories = 1
classification_summary = "1× rand GHSA-cq8v-f236-94qc :substitute"
notes = "Single major-bump item. Will require maintainer review post-rhodibot PR."

[[sample]]
repo = "hyperpolymath/hypatia"
total_open_alerts = 0
distinct_advisories = 0
classification_summary = "n/a"
notes = "Clean — Hypatia itself has no open Dependabot alerts. Expected for a pure-Elixir repo with few Rust dependencies."

## Identified wiring gaps (actionable follow-up)

[[gap]]
id = "gap-codeql-to-hypatia"
priority = "medium"
description = "CodeQL alerts not ingested by any Hypatia rule"
proposed_rule = "Hypatia.Rules.CodeqlAlerts — symmetric to DependabotAlerts; classify by severity + rule-id, dispatch via FleetDispatcher"
proposed_dispatch = "{:codeql_fix, recipe, pattern}"
blocked_by = "none — design work only"

[[gap]]
id = "gap-secret-scanning-to-cipherbot"
priority = "high"
description = "Secret-scanning alerts visible on GitHub but no Hypatia rule consumes them"
proposed_rule = "Hypatia.Rules.SecretScanning — list alerts, classify by secret-type, always :control tier (no auto-revoke)"
proposed_dispatch = "{:secret_rotation_advisory, alert, repo} → cipherbot advisory-only"
blocked_by = "none — safety-critical so must be control-tier only, no auto-execute"

[[gap]]
id = "gap-scorecard-auto-remediation"
priority = "low"
description = "ScorecardCompliance reports but does not route to rhodibot/sustainabot to remediate drops"
proposed_rule = "extend Hypatia.Rules.ScorecardCompliance with classify_drop/1 + to_recipe/1"
proposed_dispatch = "{:scorecard_fix, drop, remediation} → rhodibot PR"
blocked_by = "none"

[[gap]]
id = "gap-dependabot-pr-lifecycle"
priority = "medium"
description = "Dependabot-generated PRs accumulate unmerged; no auto-merge safe ones, no auto-prune stale ones"
proposed_rule = "Hypatia.Rules.DependabotAlerts extension DA009 — classify_dependabot_pr/1"
proposed_dispatch = "{:dependabot_pr_merge, pr} if safe → rhodibot | {:dependabot_pr_prune, pr} if stale → finishingbot"
blocked_by = "none"

[[gap]]
id = "gap-mirror-side-discrepancy"
priority = "deferred — PARKED"
description = "Unknown delta between GitHub-side findings and GitLab+Bitbucket mirrors"
proposed_rule = "Hypatia.Rules.MirrorDelta — compare source-of-truth GitHub findings against mirror-visible findings, flag discrepancies"
proposed_dispatch = "{:mirror_advisory_sync, delta} → sustainabot advisory"
blocked_by = "Atlassian API token refresh + GitLab PAT (see ~/Desktop/AI-WORK.md §0)"

## Coverage summary

[[summary]]
pipeline_stage = "signal types monitored"
count_wired = 3
count_gap = 4
gap_list = ["codeql", "secret_scanning", "scorecard_drop", "dependabot_pr_lifecycle"]

[[summary]]
pipeline_stage = "bots with directive coverage"
count_wired = 11
count_gap = 0
notes = "All 11 gitbot-fleet bots have .a2ml directives post-2026-04-17 migration."

[[summary]]
pipeline_stage = "repos sampled"
count = 5
total_open_alerts = 27
distinct_advisories = 3
auto_routable_eliminate = 23
requires_human_substitute = 4
control_only = 0

## Next-action recommendation

[[next_action]]
id = "verify-dedup"
owner = "CLAUDE"
description = "Verify fixes_from_alerts dedupes per-advisory so rhodibot opens one PR per GHSA, not one per Cargo.lock hit. Gitbot-fleet's 18 alerts / 3 advisories / 6 crates is the test case."
effort = "30 min"

[[next_action]]
id = "draft-codeql-rule"
owner = "CLAUDE"
description = "Draft Hypatia.Rules.CodeqlAlerts module mirroring DependabotAlerts structure. Non-invasive — compile + test in isolation before wiring."
effort = "2-3 hours"

[[next_action]]
id = "draft-secret-scanning-rule"
owner = "CLAUDE"
description = "Draft Hypatia.Rules.SecretScanning — strictly control-tier. Per user policy, no auto-revoke or auto-rotate; cipherbot advisory-only."
effort = "1-2 hours"

[[next_action]]
id = "resume-mirror-sweep"
owner = "JONATHAN"
description = "Refresh Atlassian API token + obtain GitLab PAT; drop both in ~/.netrc. Unblocks the MirrorDelta rule design + closes gap-mirror-side-discrepancy."
effort = "5 min after token in hand"
