From 3d62577db7608221591f50ff82ef1a42f26ab4c2 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Mon, 27 Jul 2026 08:59:53 -0700 Subject: [PATCH 1/2] feat(ledger): anchor persistence + public attempt log, success and failure alike (#9271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third sub-issue of #9267. migrations/0195 adds decision_ledger_anchors: every backend calls the same recordLedgerAnchorAttempt whether it succeeded or failed — there is no separate failure path a backend could simply not call. Per the mechanism research on #9267: anchoring is best-effort (it must never block a review), which means a failure could otherwise be silent. An operator whose anchoring quietly fails could regress the ledger back to tamper-evident-only with no visible signal. Recording every attempt on the same public listing, in the same shape regardless of outcome, is what makes anchoring's own health a fact anyone can observe rather than something only the operator's own logs show. GET /v1/public/decision-ledger/anchors serves the log, paginated newest-first, filterable by backend. backend_ref stores the full resolvable reference (for Rekor: shard URL + log index + key id + uuid, not a bare index that becomes unresolvable after the annual shard rotation). --- apps/loopover-ui/public/openapi.json | 4575 +++++++++-------- migrations/0195_decision_ledger_anchors.sql | 36 + src/api/routes.ts | 24 + src/openapi/spec.ts | 9 + src/review/ledger-anchor-persistence.ts | 147 + .../public-decision-ledger-routes.test.ts | 1 + .../public-ledger-anchors-route.test.ts | 93 + test/unit/ledger-anchor-persistence.test.ts | 131 + 8 files changed, 2753 insertions(+), 2263 deletions(-) create mode 100644 migrations/0195_decision_ledger_anchors.sql create mode 100644 src/review/ledger-anchor-persistence.ts create mode 100644 test/integration/public-ledger-anchors-route.test.ts create mode 100644 test/unit/ledger-anchor-persistence.test.ts diff --git a/apps/loopover-ui/public/openapi.json b/apps/loopover-ui/public/openapi.json index bad608d658..bdb2032d46 100644 --- a/apps/loopover-ui/public/openapi.json +++ b/apps/loopover-ui/public/openapi.json @@ -434,6 +434,82 @@ "merged" ] }, + "rulePrecision": { + "type": "object", + "properties": { + "windowDays": { + "type": "number" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ruleId": { + "type": "string" + }, + "decided": { + "type": "number" + }, + "confirmed": { + "type": "number" + }, + "precision": { + "type": "number", + "nullable": true + } + }, + "required": [ + "ruleId", + "decided", + "confirmed", + "precision" + ] + } + }, + "reversals": { + "type": "object", + "properties": { + "reopened": { + "type": "number" + }, + "reverted": { + "type": "number" + }, + "superseded": { + "type": "number" + } + }, + "required": [ + "reopened", + "reverted", + "superseded" + ] + }, + "latestBacktestRun": { + "type": "object", + "nullable": true, + "properties": { + "corpusChecksum": { + "type": "string" + }, + "at": { + "type": "string" + } + }, + "required": [ + "corpusChecksum", + "at" + ] + } + }, + "required": [ + "windowDays", + "rules", + "reversals", + "latestBacktestRun" + ] + }, "byProject": { "type": "array", "items": { @@ -465,94 +541,6 @@ ] } }, - "accuracyTrend": { - "type": "array", - "items": { - "type": "object", - "properties": { - "weekStart": { - "type": "string" - }, - "merged": { - "type": "number", - "nullable": true - }, - "closed": { - "type": "number", - "nullable": true - }, - "reversed": { - "type": "number", - "nullable": true - }, - "accuracyPct": { - "type": "number", - "nullable": true - } - }, - "required": [ - "weekStart", - "merged", - "closed", - "reversed", - "accuracyPct" - ] - } - }, - "reuseRateTrend": { - "type": "array", - "items": { - "type": "object", - "properties": { - "weekStart": { - "type": "string" - }, - "hits": { - "type": "number" - }, - "misses": { - "type": "number" - }, - "reuseRatePct": { - "type": "number", - "nullable": true - } - }, - "required": [ - "weekStart", - "hits", - "misses", - "reuseRatePct" - ] - } - }, - "reviewVolumeTrend": { - "type": "array", - "items": { - "type": "object", - "properties": { - "weekStart": { - "type": "string" - }, - "reviewed": { - "type": "number" - }, - "merged": { - "type": "number" - }, - "filteredPct": { - "type": "number", - "nullable": true - } - }, - "required": [ - "weekStart", - "reviewed", - "merged", - "filteredPct" - ] - } - }, "fleetAccuracy": { "type": "object", "properties": { @@ -560,16 +548,6 @@ "type": "number", "nullable": true }, - "instanceCount": { - "type": "number" - }, - "windowDays": { - "type": "number" - }, - "gamingFlagsCaught": { - "type": "number", - "nullable": true - }, "accuracyCiPct": { "type": "object", "nullable": true, @@ -646,10 +624,10 @@ "lambda": { "type": "number" }, - "n": { + "aiJudgedCoveragePct": { "type": "number" }, - "aiJudgedCoveragePct": { + "n": { "type": "number" }, "backfilledPct": { @@ -675,10 +653,10 @@ "lambda": { "type": "number" }, - "n": { + "aiJudgedCoveragePct": { "type": "number" }, - "aiJudgedCoveragePct": { + "n": { "type": "number" }, "backfilledPct": { @@ -699,6 +677,16 @@ "close", "merge" ] + }, + "instanceCount": { + "type": "number" + }, + "windowDays": { + "type": "number" + }, + "gamingFlagsCaught": { + "type": "number", + "nullable": true } }, "required": [ @@ -716,81 +704,93 @@ "gamingFlagsCaught" ] }, - "rulePrecision": { - "type": "object", - "properties": { - "windowDays": { - "type": "number" - }, - "rules": { - "type": "array", - "items": { - "type": "object", - "properties": { - "ruleId": { - "type": "string" - }, - "decided": { - "type": "number" - }, - "precision": { - "type": "number", - "nullable": true - }, - "confirmed": { - "type": "number" - } - }, - "required": [ - "ruleId", - "decided", - "confirmed", - "precision" - ] + "accuracyTrend": { + "type": "array", + "items": { + "type": "object", + "properties": { + "weekStart": { + "type": "string" + }, + "merged": { + "type": "number", + "nullable": true + }, + "closed": { + "type": "number", + "nullable": true + }, + "reversed": { + "type": "number", + "nullable": true + }, + "accuracyPct": { + "type": "number", + "nullable": true } }, - "reversals": { - "type": "object", - "properties": { - "reopened": { - "type": "number" - }, - "reverted": { - "type": "number" - }, - "superseded": { - "type": "number" - } + "required": [ + "weekStart", + "merged", + "closed", + "reversed", + "accuracyPct" + ] + } + }, + "reuseRateTrend": { + "type": "array", + "items": { + "type": "object", + "properties": { + "weekStart": { + "type": "string" }, - "required": [ - "reopened", - "reverted", - "superseded" - ] + "hits": { + "type": "number" + }, + "misses": { + "type": "number" + }, + "reuseRatePct": { + "type": "number", + "nullable": true + } }, - "latestBacktestRun": { - "type": "object", - "nullable": true, - "properties": { - "corpusChecksum": { - "type": "string" - }, - "at": { - "type": "string" - } + "required": [ + "weekStart", + "hits", + "misses", + "reuseRatePct" + ] + } + }, + "reviewVolumeTrend": { + "type": "array", + "items": { + "type": "object", + "properties": { + "weekStart": { + "type": "string" }, - "required": [ - "corpusChecksum", - "at" - ] - } - }, - "required": [ - "windowDays", - "rules", - "reversals", - "latestBacktestRun" - ] + "reviewed": { + "type": "number" + }, + "merged": { + "type": "number" + }, + "filteredPct": { + "type": "number", + "nullable": true + } + }, + "required": [ + "weekStart", + "reviewed", + "merged", + "filteredPct" + ] + } } }, "required": [ @@ -4776,6 +4776,43 @@ } ] }, + "ReviewRiskExplanation": { + "type": "object", + "properties": { + "preflight": { + "$ref": "#/components/schemas/PreflightResult" + }, + "roleContext": { + "allOf": [ + { + "$ref": "#/components/schemas/RoleContext" + }, + { + "nullable": true + } + ] + }, + "recommendation": { + "type": "string", + "enum": [ + "likely_duplicate", + "maintainer_lane", + "needs_author", + "review", + "watch" + ] + }, + "summary": { + "type": "string" + } + }, + "required": [ + "preflight", + "roleContext", + "recommendation", + "summary" + ] + }, "LocalBranchAnalysis": { "type": "object", "properties": { @@ -9037,6 +9074,10 @@ "type": "number", "nullable": true }, + "closeAuditHoldoutPct": { + "type": "number", + "nullable": true + }, "slopGateMode": { "type": "string", "enum": [ @@ -9137,6 +9178,12 @@ "minimum": 0, "exclusiveMinimum": true }, + "staleBaseAheadByThreshold": { + "type": "integer", + "nullable": true, + "minimum": 0, + "exclusiveMinimum": true + }, "mergeReadinessGateMode": { "type": "string", "enum": [ @@ -9169,6 +9216,22 @@ "block" ] }, + "contentLaneDeliverableGateMode": { + "type": "string", + "enum": [ + "off", + "advisory", + "block" + ] + }, + "backtestRegressionGateMode": { + "type": "string", + "enum": [ + "off", + "advisory", + "block" + ] + }, "slopGateMinScore": { "type": "number", "nullable": true @@ -9211,6 +9274,10 @@ "type": "number", "nullable": true }, + "aiReviewSalvageabilityMinScore": { + "type": "number", + "nullable": true + }, "aiReviewLowConfidenceDisposition": { "type": "string", "nullable": true, @@ -9274,6 +9341,18 @@ "type": "string" } }, + "issuePlanEnabled": { + "type": "boolean" + }, + "issuePlanExtraLabels": { + "type": "array", + "items": { + "type": "string" + } + }, + "issuePlanMilestoneReuse": { + "type": "boolean" + }, "linkedIssueLabelPropagation": { "type": "object", "properties": { @@ -9811,6 +9890,15 @@ "moderationBannedLabel": { "type": "string" }, + "fairnessAnalyticsMode": { + "type": "string", + "enum": [ + "inherit", + "off", + "enabled" + ], + "description": "Per-repo participation in cross-repo contributor fairness/accuracy analytics -- 'off' excludes this repo's gate decisions and moderation history from every aggregation, independent of whether the internal fairness-analytics routes are enabled fleet-wide." + }, "skipAutomationBotAuthors": { "type": "string", "enum": [ @@ -9827,6 +9915,22 @@ "enabled" ] }, + "openPrFileCollisionMode": { + "type": "string", + "enum": [ + "inherit", + "off", + "enabled" + ] + }, + "plannerMode": { + "type": "string", + "enum": [ + "inherit", + "off", + "enabled" + ] + }, "reviewEvasionProtection": { "type": "string", "enum": [ @@ -9842,6 +9946,22 @@ "reviewEvasionComment": { "type": "boolean" }, + "draftPrClosePolicy": { + "type": "string", + "enum": [ + "off", + "close" + ], + "description": "Off by default (opt-in, unlike reviewEvasionProtection's default-close). \"close\" enforces on ANY draft PR, including the very first one, before a review pass has had a chance to run -- distinct from reviewEvasionProtection's family, which only enforces after a review already ran or on the 2nd+ draft conversion." + }, + "synchronizeClosePolicy": { + "type": "string", + "enum": [ + "off", + "close" + ], + "description": "Off by default (opt-in, config-as-code only -- no dashboard/DB column). \"close\" closes a contributor's own PR immediately when they push an additional commit (synchronize) before it's been merged or closed -- this repo's review is one-shot, so the first push is the only push. Never fires for a push that isn't from the PR's own author (e.g. the engine's own rebase-if-behind), nor for the repo owner/admin, a protected automation author, or anyone with write+ collaborator access." + }, "mergeTrainMode": { "type": "string", "enum": [ @@ -9910,126 +10030,236 @@ "updatedAt": { "type": "string", "nullable": true + } + }, + "required": [ + "repoFullName", + "commentMode", + "publicAudienceMode", + "publicSignalLevel", + "checkRunMode", + "checkRunDetailLevel", + "regateSweepOrderMode", + "reviewCheckMode", + "gatePack", + "linkedIssueGateMode", + "duplicatePrGateMode", + "qualityGateMode", + "slopGateMode", + "mergeReadinessGateMode", + "manifestPolicyGateMode", + "selfAuthoredLinkedIssueGateMode", + "linkedIssueSatisfactionGateMode", + "contentLaneDeliverableGateMode", + "backtestRegressionGateMode", + "slopAiAdvisory", + "aiReviewMode", + "aiReviewByok", + "aiReviewAllAuthors", + "closeOwnerAuthors", + "autoLabelEnabled", + "typeLabelsEnabled", + "gittensorLabel", + "blacklistLabel", + "createMissingLabel", + "publicSurface", + "includeMaintainerAuthors", + "requireLinkedIssue", + "backfillEnabled", + "commandAuthorization" + ] + }, + "AutomationState": { + "type": "object", + "properties": { + "repoFullName": { + "type": "string" }, - "openPrFileCollisionMode": { - "type": "string", - "enum": [ - "inherit", - "off", - "enabled" - ] - }, - "plannerMode": { - "type": "string", - "enum": [ - "inherit", - "off", - "enabled" - ] - }, - "draftPrClosePolicy": { - "type": "string", - "enum": [ - "off", - "close" - ], - "description": "Off by default (opt-in, unlike reviewEvasionProtection's default-close). \"close\" enforces on ANY draft PR, including the very first one, before a review pass has had a chance to run -- distinct from reviewEvasionProtection's family, which only enforces after a review already ran or on the 2nd+ draft conversion." - }, - "fairnessAnalyticsMode": { - "type": "string", - "enum": [ - "inherit", - "off", - "enabled" - ], - "description": "Per-repo participation in cross-repo contributor fairness/accuracy analytics -- 'off' excludes this repo's gate decisions and moderation history from every aggregation, independent of whether the internal fairness-analytics routes are enabled fleet-wide." - }, - "issuePlanEnabled": { + "configured": { "type": "boolean" }, - "issuePlanExtraLabels": { - "type": "array", - "items": { - "type": "string" + "autonomy": { + "type": "object", + "properties": { + "review": { + "type": "string", + "enum": [ + "observe", + "auto_with_approval", + "auto" + ] + }, + "request_changes": { + "type": "string", + "enum": [ + "observe", + "auto_with_approval", + "auto" + ] + }, + "approve": { + "type": "string", + "enum": [ + "observe", + "auto_with_approval", + "auto" + ] + }, + "merge": { + "type": "string", + "enum": [ + "observe", + "auto_with_approval", + "auto" + ] + }, + "close": { + "type": "string", + "enum": [ + "observe", + "auto_with_approval", + "auto" + ] + }, + "label": { + "type": "string", + "enum": [ + "observe", + "auto_with_approval", + "auto" + ] + }, + "review_state_label": { + "type": "string", + "enum": [ + "observe", + "auto_with_approval", + "auto" + ] + }, + "update_branch": { + "type": "string", + "enum": [ + "observe", + "auto_with_approval", + "auto" + ] + }, + "assign": { + "type": "string", + "enum": [ + "observe", + "auto_with_approval", + "auto" + ] + } } }, - "issuePlanMilestoneReuse": { - "type": "boolean" + "autoMaintain": { + "type": "boolean", + "nullable": true }, - "staleBaseAheadByThreshold": { - "type": "integer", - "nullable": true, - "minimum": 0, - "exclusiveMinimum": true + "agentPaused": { + "type": "boolean" }, - "synchronizeClosePolicy": { - "type": "string", - "enum": [ - "off", - "close" - ], - "description": "Off by default (opt-in, config-as-code only -- no dashboard/DB column). \"close\" closes a contributor's own PR immediately when they push an additional commit (synchronize) before it's been merged or closed -- this repo's review is one-shot, so the first push is the only push. Never fires for a push that isn't from the PR's own author (e.g. the engine's own rebase-if-behind), nor for the repo owner/admin, a protected automation author, or anyone with write+ collaborator access." + "agentDryRun": { + "type": "boolean" }, - "contentLaneDeliverableGateMode": { + "mode": { "type": "string", "enum": [ - "off", - "advisory", - "block" + "paused", + "dry_run", + "live" ] }, - "backtestRegressionGateMode": { + "permissionReadiness": { "type": "string", "enum": [ - "off", - "advisory", - "block" + "not_required", + "ready", + "reconsent_required" ] }, - "closeAuditHoldoutPct": { - "type": "number", - "nullable": true + "actingActionClasses": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "review", + "request_changes", + "approve", + "merge", + "close", + "label", + "review_state_label", + "update_branch", + "assign" + ] + } }, - "aiReviewSalvageabilityMinScore": { - "type": "number", - "nullable": true + "pendingActionCount": { + "type": "number" } }, "required": [ "repoFullName", - "commentMode", - "publicAudienceMode", - "publicSignalLevel", - "checkRunMode", - "checkRunDetailLevel", - "regateSweepOrderMode", - "reviewCheckMode", - "gatePack", - "linkedIssueGateMode", - "duplicatePrGateMode", - "qualityGateMode", - "slopGateMode", - "mergeReadinessGateMode", - "manifestPolicyGateMode", - "selfAuthoredLinkedIssueGateMode", - "linkedIssueSatisfactionGateMode", - "contentLaneDeliverableGateMode", - "backtestRegressionGateMode", - "slopAiAdvisory", - "aiReviewMode", - "aiReviewByok", - "aiReviewAllAuthors", - "closeOwnerAuthors", - "autoLabelEnabled", - "typeLabelsEnabled", - "gittensorLabel", - "blacklistLabel", - "createMissingLabel", - "publicSurface", - "includeMaintainerAuthors", - "requireLinkedIssue", - "backfillEnabled", - "commandAuthorization" + "configured", + "autonomy", + "agentPaused", + "agentDryRun", + "mode", + "permissionReadiness", + "actingActionClasses", + "pendingActionCount" + ] + }, + "RepoDocRefreshResult": { + "oneOf": [ + { + "type": "object", + "properties": { + "opened": { + "type": "boolean", + "enum": [ + true + ] + }, + "reused": { + "type": "boolean" + }, + "pullNumber": { + "type": "integer" + }, + "url": { + "type": "string" + } + }, + "required": [ + "opened", + "reused", + "pullNumber", + "url" + ] + }, + { + "type": "object", + "properties": { + "opened": { + "type": "boolean", + "enum": [ + false + ] + }, + "reason": { + "type": "string" + } + }, + "required": [ + "opened", + "reason" + ] + } ] }, "InstallationRepair": { @@ -10573,6 +10803,10 @@ "type": "number", "nullable": true }, + "closeAuditHoldoutPct": { + "type": "number", + "nullable": true + }, "slopGateMode": { "type": "string", "enum": [ @@ -10613,6 +10847,22 @@ "block" ] }, + "contentLaneDeliverableGateMode": { + "type": "string", + "enum": [ + "off", + "advisory", + "block" + ] + }, + "backtestRegressionGateMode": { + "type": "string", + "enum": [ + "off", + "advisory", + "block" + ] + }, "slopGateMinScore": { "type": "number", "nullable": true @@ -10717,26 +10967,6 @@ "defaultAllowed", "commandOverrides" ] - }, - "contentLaneDeliverableGateMode": { - "type": "string", - "enum": [ - "off", - "advisory", - "block" - ] - }, - "backtestRegressionGateMode": { - "type": "string", - "enum": [ - "off", - "advisory", - "block" - ] - }, - "closeAuditHoldoutPct": { - "type": "number", - "nullable": true } }, "required": [ @@ -11319,6 +11549,10 @@ "minimum": 1, "maximum": 100 }, + "offset": { + "type": "integer", + "minimum": 0 + }, "hasMore": { "type": "boolean" }, @@ -11385,10 +11619,6 @@ "remediation" ] } - }, - "offset": { - "type": "integer", - "minimum": 0 } }, "required": [ @@ -13913,6 +14143,78 @@ "report" ] }, + "GateConfigEffectiveResponse": { + "type": "object", + "properties": { + "repoFullName": { + "type": "string" + }, + "effective": { + "type": "object", + "properties": { + "confidenceFloor": { + "type": "number", + "nullable": true + }, + "scopeCap": { + "type": "object", + "properties": { + "files": { + "type": "integer", + "nullable": true + }, + "lines": { + "type": "integer", + "nullable": true + } + }, + "required": [ + "files", + "lines" + ] + } + }, + "required": [ + "confidenceFloor", + "scopeCap" + ] + }, + "shadowPending": { + "type": "boolean" + } + }, + "required": [ + "repoFullName", + "effective", + "shadowPending" + ] + }, + "LiveGateThresholdsResponse": { + "type": "object", + "properties": { + "repoFullName": { + "type": "string" + }, + "confidence_floor": { + "type": "number", + "nullable": true + }, + "scope_cap_files": { + "type": "integer", + "nullable": true + }, + "scope_cap_lines": { + "type": "integer", + "nullable": true + } + }, + "required": [ + "repoFullName", + "confidence_floor", + "scope_cap_files", + "scope_cap_lines" + ] + }, "ContributorScoringProfile": { "type": "object", "properties": { @@ -14205,92 +14507,6 @@ "summary" ] }, - "PullRequestReviewability": { - "type": "object", - "properties": { - "repoFullName": { - "type": "string" - }, - "pullNumber": { - "type": "number" - }, - "generatedAt": { - "type": "string" - }, - "score": { - "type": "number" - }, - "action": { - "type": "string", - "enum": [ - "review_now", - "needs_author", - "likely_duplicate", - "close_or_redirect", - "watch", - "maintainer_lane" - ] - }, - "noiseSources": { - "type": "array", - "items": { - "type": "string" - } - }, - "whyThisHelps": { - "type": "array", - "items": { - "type": "string" - } - }, - "maintainerNextSteps": { - "type": "array", - "items": { - "type": "string" - } - }, - "privateSummary": { - "type": "string" - } - }, - "required": [ - "repoFullName", - "pullNumber", - "generatedAt", - "score", - "action", - "noiseSources", - "whyThisHelps", - "maintainerNextSteps", - "privateSummary" - ] - }, - "LiveGateThresholdsResponse": { - "type": "object", - "properties": { - "repoFullName": { - "type": "string" - }, - "confidence_floor": { - "type": "number", - "nullable": true - }, - "scope_cap_files": { - "type": "integer", - "nullable": true - }, - "scope_cap_lines": { - "type": "integer", - "nullable": true - } - }, - "required": [ - "repoFullName", - "confidence_floor", - "scope_cap_files", - "scope_cap_lines" - ] - }, "AmsMinerCohortComparison": { "type": "object", "properties": { @@ -14376,305 +14592,126 @@ "humanCohort" ] }, - "GateConfigEffectiveResponse": { - "type": "object", - "properties": { - "repoFullName": { - "type": "string" - }, - "effective": { - "type": "object", - "properties": { - "confidenceFloor": { - "type": "number", - "nullable": true - }, - "scopeCap": { - "type": "object", - "properties": { - "files": { - "type": "integer", - "nullable": true - }, - "lines": { - "type": "integer", - "nullable": true - } - }, - "required": [ - "files", - "lines" - ] - } - }, - "required": [ - "confidenceFloor", - "scopeCap" - ] - }, - "shadowPending": { - "type": "boolean" - } - }, - "required": [ - "repoFullName", - "effective", - "shadowPending" - ] - }, - "FindingTaxonomyDocument": { + "PullRequestReviewability": { "type": "object", "properties": { - "categories": { + "repoFullName": { + "type": "string" + }, + "pullNumber": { + "type": "number" + }, + "generatedAt": { + "type": "string" + }, + "score": { + "type": "number" + }, + "action": { + "type": "string", + "enum": [ + "review_now", + "needs_author", + "likely_duplicate", + "close_or_redirect", + "watch", + "maintainer_lane" + ] + }, + "noiseSources": { "type": "array", "items": { "type": "string" } }, - "severities": { + "whyThisHelps": { "type": "array", "items": { "type": "string" } - } - }, - "required": [ - "categories", - "severities" - ] - }, - "EnrichmentAnalyzersTaxonomyDocument": { - "type": "object", - "properties": { - "defaultProfile": { - "type": "string" }, - "analyzers": { + "maintainerNextSteps": { "type": "array", "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "category": { - "type": "string" - }, - "costClass": { - "type": "string" - }, - "profiles": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "name", - "category", - "costClass", - "profiles" - ] + "type": "string" } + }, + "privateSummary": { + "type": "string" } }, "required": [ - "defaultProfile", - "analyzers" + "repoFullName", + "pullNumber", + "generatedAt", + "score", + "action", + "noiseSources", + "whyThisHelps", + "maintainerNextSteps", + "privateSummary" ] }, - "AutomationState": { + "FindingTaxonomyDocument": { "type": "object", "properties": { - "repoFullName": { - "type": "string" - }, - "configured": { - "type": "boolean" - }, - "autonomy": { - "type": "object", - "properties": { - "review": { - "type": "string", - "enum": [ - "observe", - "auto_with_approval", - "auto" - ] - }, - "request_changes": { - "type": "string", - "enum": [ - "observe", - "auto_with_approval", - "auto" - ] - }, - "approve": { - "type": "string", - "enum": [ - "observe", - "auto_with_approval", - "auto" - ] - }, - "merge": { - "type": "string", - "enum": [ - "observe", - "auto_with_approval", - "auto" - ] - }, - "close": { - "type": "string", - "enum": [ - "observe", - "auto_with_approval", - "auto" - ] - }, - "label": { - "type": "string", - "enum": [ - "observe", - "auto_with_approval", - "auto" - ] - }, - "review_state_label": { - "type": "string", - "enum": [ - "observe", - "auto_with_approval", - "auto" - ] - }, - "update_branch": { - "type": "string", - "enum": [ - "observe", - "auto_with_approval", - "auto" - ] - }, - "assign": { - "type": "string", - "enum": [ - "observe", - "auto_with_approval", - "auto" - ] - } + "categories": { + "type": "array", + "items": { + "type": "string" } }, - "autoMaintain": { - "type": "boolean", - "nullable": true - }, - "agentPaused": { - "type": "boolean" - }, - "agentDryRun": { - "type": "boolean" - }, - "mode": { - "type": "string", - "enum": [ - "paused", - "dry_run", - "live" - ] - }, - "permissionReadiness": { - "type": "string", - "enum": [ - "not_required", - "ready", - "reconsent_required" - ] - }, - "actingActionClasses": { + "severities": { "type": "array", "items": { - "type": "string", - "enum": [ - "review", - "request_changes", - "approve", - "merge", - "close", - "label", - "review_state_label", - "update_branch", - "assign" - ] + "type": "string" } - }, - "pendingActionCount": { - "type": "number" } }, "required": [ - "repoFullName", - "configured", - "autonomy", - "agentPaused", - "agentDryRun", - "mode", - "permissionReadiness", - "actingActionClasses", - "pendingActionCount" + "categories", + "severities" ] }, - "RepoDocRefreshResult": { - "oneOf": [ - { - "type": "object", - "properties": { - "opened": { - "type": "boolean", - "enum": [ - true - ] - }, - "reused": { - "type": "boolean" - }, - "pullNumber": { - "type": "integer" - }, - "url": { - "type": "string" - } - }, - "required": [ - "opened", - "reused", - "pullNumber", - "url" - ] + "EnrichmentAnalyzersTaxonomyDocument": { + "type": "object", + "properties": { + "defaultProfile": { + "type": "string" }, - { - "type": "object", - "properties": { - "opened": { - "type": "boolean", - "enum": [ - false - ] + "analyzers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "category": { + "type": "string" + }, + "costClass": { + "type": "string" + }, + "profiles": { + "type": "array", + "items": { + "type": "string" + } + } }, - "reason": { - "type": "string" - } - }, - "required": [ - "opened", - "reason" - ] + "required": [ + "name", + "category", + "costClass", + "profiles" + ] + } } + }, + "required": [ + "defaultProfile", + "analyzers" ] }, "ContributorPrOutcomes": { @@ -14819,43 +14856,6 @@ "login", "marked" ] - }, - "ReviewRiskExplanation": { - "type": "object", - "properties": { - "preflight": { - "$ref": "#/components/schemas/PreflightResult" - }, - "roleContext": { - "allOf": [ - { - "$ref": "#/components/schemas/RoleContext" - }, - { - "nullable": true - } - ] - }, - "recommendation": { - "type": "string", - "enum": [ - "likely_duplicate", - "maintainer_lane", - "needs_author", - "review", - "watch" - ] - }, - "summary": { - "type": "string" - } - }, - "required": [ - "preflight", - "roleContext", - "recommendation", - "summary" - ] } }, "parameters": {}, @@ -14876,6 +14876,7 @@ "paths": { "/health": { "get": { + "summary": "Service liveness probe", "responses": { "200": { "description": "Service health", @@ -14887,12 +14888,12 @@ } } } - }, - "summary": "Service liveness probe" + } } }, "/v1/mcp/compatibility": { "get": { + "summary": "Public-safe API and MCP client compatibility metadata", "responses": { "200": { "description": "Public-safe API and MCP compatibility metadata", @@ -14904,12 +14905,12 @@ } } } - }, - "summary": "Public-safe API and MCP client compatibility metadata" + } } }, "/v1/public/stats": { "get": { + "summary": "Public homepage aggregate stats", "responses": { "200": { "description": "Public-safe homepage stats: lifetime PRs handled/merged/closed, gate + slop blocks, and reversal-grounded accuracy. Aggregate counts only.", @@ -14927,12 +14928,12 @@ "503": { "description": "Public stats are temporarily unavailable" } - }, - "summary": "Public homepage aggregate stats" + } } }, "/v1/public/github/repos/{owner}/{repo}/stats": { "get": { + "summary": "Public GitHub stars and forks for an allowlisted repository", "parameters": [ { "schema": { @@ -14968,12 +14969,12 @@ "503": { "description": "GitHub repository stats are unavailable" } - }, - "summary": "Public GitHub stars and forks for an allowlisted repository" + } } }, "/v1/public/repos/{owner}/{repo}/quality": { "get": { + "summary": "Public repository quality summary for an opted-in repository", "parameters": [ { "schema": { @@ -15009,12 +15010,12 @@ "503": { "description": "Public quality metrics are temporarily unavailable" } - }, - "summary": "Public repository quality summary for an opted-in repository" + } } }, "/v1/registry/snapshot": { "get": { + "summary": "Latest Gittensor registry snapshot", "responses": { "200": { "description": "Latest Gittensor registry snapshot", @@ -15034,12 +15035,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Latest Gittensor registry snapshot" + ] } }, "/v1/registry/changes": { "get": { + "summary": "Diff between the two latest registry snapshots", "responses": { "200": { "description": "Diff between latest registry snapshots", @@ -15059,12 +15060,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Diff between the two latest registry snapshots" + ] } }, "/v1/scoring/model": { "get": { + "summary": "Latest scoring model snapshot", "responses": { "200": { "description": "Latest private scoring model snapshot", @@ -15084,12 +15085,62 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Latest scoring model snapshot" + ] + } + }, + "/v1/finding-taxonomy": { + "get": { + "summary": "Canonical AI-review finding taxonomy", + "responses": { + "200": { + "description": "Finding categories and the severity ladder", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FindingTaxonomyDocument" + } + } + } + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + } + }, + "/v1/enrichment-analyzers": { + "get": { + "summary": "REES enrichment analyzer taxonomy", + "responses": { + "200": { + "description": "Default profile and the registered enrichment analyzers", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnrichmentAnalyzersTaxonomyDocument" + } + } + } + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] } }, "/v1/upstream/status": { "get": { + "summary": "Upstream Gittensor source and ruleset drift status", "responses": { "200": { "description": "Upstream Gittensor source/ruleset drift status", @@ -15109,12 +15160,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Upstream Gittensor source and ruleset drift status" + ] } }, "/v1/upstream/ruleset": { "get": { + "summary": "Latest normalized upstream Gittensor ruleset snapshot", "responses": { "200": { "description": "Latest normalized upstream Gittensor ruleset snapshot", @@ -15137,12 +15188,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Latest normalized upstream Gittensor ruleset snapshot" + ] } }, "/v1/upstream/drift": { "get": { + "summary": "Open and historical upstream drift reports", "responses": { "200": { "description": "Open and historical upstream drift reports", @@ -15181,12 +15232,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Open and historical upstream drift reports" + ] } }, "/v1/scoring/preview": { "post": { + "summary": "Generate a scoring preview artifact for a candidate contribution", "responses": { "200": { "description": "Private scoring preview artifact", @@ -15209,12 +15260,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Generate a scoring preview artifact for a candidate contribution" + ] } }, "/v1/sync/status": { "get": { + "summary": "Repository and installation sync status", "responses": { "200": { "description": "Repository and installation sync status", @@ -15234,12 +15285,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Repository and installation sync status" + ] } }, "/v1/readiness": { "get": { + "summary": "Operational readiness summary for the hosted API", "responses": { "200": { "description": "Operational readiness summary for hosted API, signal fidelity, and public-review preparation", @@ -15259,12 +15310,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Operational readiness summary for the hosted API" + ] } }, "/v1/installations": { "get": { + "summary": "List GitHub App installations and their health", "responses": { "200": { "description": "GitHub App installations and health", @@ -15305,12 +15356,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "List GitHub App installations and their health" + ] } }, "/v1/installations/{id}/health": { "get": { + "summary": "GitHub App installation health detail", "parameters": [ { "schema": { @@ -15343,12 +15394,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "GitHub App installation health detail" + ] } }, "/v1/installations/{id}/repair": { "get": { + "summary": "GitHub App installation repair diagnostics", "parameters": [ { "schema": { @@ -15381,12 +15432,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "GitHub App installation repair diagnostics" + ] } }, "/v1/installations/{id}/repair/refresh": { "post": { + "summary": "Recompute GitHub App installation repair diagnostics", "parameters": [ { "schema": { @@ -15419,12 +15470,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Recompute GitHub App installation repair diagnostics" + ] } }, "/v1/app/notification-model": { "get": { + "summary": "Opt-in notification model and PWA-readiness metadata", "responses": { "200": { "description": "Opt-in notification model and PWA-readiness metadata for control-panel routes", @@ -15559,12 +15610,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Opt-in notification model and PWA-readiness metadata" + ] } }, "/v1/repos": { "get": { + "summary": "List known repositories", "responses": { "200": { "description": "Known repositories", @@ -15587,12 +15638,101 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "List known repositories" + ] } }, "/v1/repos/{owner}/{repo}": { "get": { + "summary": "Repository detail", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "owner", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "repo", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Repository detail", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Repository" + } + } + } + }, + "404": { + "description": "Repository not found" + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + } + }, + "/v1/repos/{owner}/{repo}/intelligence": { + "get": { + "summary": "Canonical repository intelligence bundle", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "owner", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "repo", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Canonical repository intelligence bundle", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RepoIntelligence" + } + } + } + } + }, + "security": [ + { + "LoopOverBearer": [] + }, + { + "LoopOverSessionCookie": [] + } + ] + } + }, + "/v1/repos/{owner}/{repo}/issue-quality": { + "get": { + "summary": "Repository issue quality report", "parameters": [ { "schema": { @@ -15613,17 +15753,17 @@ ], "responses": { "200": { - "description": "Repository detail", + "description": "Cached or computed issue quality report for the repo", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Repository" + "$ref": "#/components/schemas/IssueQualityResponse" } } } }, "404": { - "description": "Repository not found" + "description": "Repo is unknown or has no issue-quality coverage yet" } }, "security": [ @@ -15633,12 +15773,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Repository detail" + ] } }, - "/v1/repos/{owner}/{repo}/intelligence": { + "/v1/repos/{owner}/{repo}/gate-config/effective": { "get": { + "summary": "Current effective self-tuned gate config for a repo (#6247)", "parameters": [ { "schema": { @@ -15659,14 +15799,20 @@ ], "responses": { "200": { - "description": "Canonical repository intelligence bundle", + "description": "Effective TunableOverride values (confidenceFloor / scopeCap.files / scopeCap.lines) with a shadowPending flag — never the raw override_audit history", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RepoIntelligence" + "$ref": "#/components/schemas/GateConfigEffectiveResponse" } } } + }, + "401": { + "description": "Missing or invalid static protected API token" + }, + "403": { + "description": "Static mcp credential is outside MCP_READ_REPO_ALLOWLIST for this repo" } }, "security": [ @@ -15676,12 +15822,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Canonical repository intelligence bundle" + ] } }, - "/v1/repos/{owner}/{repo}/issue-quality": { + "/v1/repos/{owner}/{repo}/live-gate-thresholds": { "get": { + "summary": "Live self-tuned gate thresholds for AMS probe (#6486)", "parameters": [ { "schema": { @@ -15702,17 +15848,20 @@ ], "responses": { "200": { - "description": "Cached or computed issue quality report for the repo", + "description": "Field-limited live (or soaking-shadow) TunableOverride values — confidence_floor / scope_cap_files / scope_cap_lines only", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IssueQualityResponse" + "$ref": "#/components/schemas/LiveGateThresholdsResponse" } } } }, + "403": { + "description": "Static mcp credential is outside MCP_READ_REPO_ALLOWLIST for this repo" + }, "404": { - "description": "Repo is unknown or has no issue-quality coverage yet" + "description": "No live or shadow gate override is active for this repo" } }, "security": [ @@ -15722,12 +15871,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Repository issue quality report" + ] } }, "/v1/repos/{owner}/{repo}/outcome-patterns": { "get": { + "summary": "Accepted and rejected pull request outcome patterns for a repository", "parameters": [ { "schema": { @@ -15768,12 +15917,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Accepted and rejected pull request outcome patterns for a repository" + ] } }, "/v1/repos/{owner}/{repo}/registration-readiness": { "get": { + "summary": "Gittensor registration readiness signal for repository owners", "parameters": [ { "schema": { @@ -15811,12 +15960,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Gittensor registration readiness signal for repository owners" + ] } }, "/v1/repos/{owner}/{repo}/gittensor-config-recommendation": { "get": { + "summary": "Recommended Gittensor configuration for a repository", "parameters": [ { "schema": { @@ -15854,12 +16003,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Recommended Gittensor configuration for a repository" + ] } }, "/v1/repos/{owner}/{repo}/focus-manifest": { "get": { + "summary": "Repository focus manifest and compiled policy", "parameters": [ { "schema": { @@ -15903,10 +16052,10 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Repository focus manifest and compiled policy" + ] }, "put": { + "summary": "Persist an API-backed focus manifest for a repository", "parameters": [ { "schema": { @@ -15953,12 +16102,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Persist an API-backed focus manifest for a repository" + ] } }, "/v1/repos/{owner}/{repo}/focus-manifest/refresh": { "post": { + "summary": "Refresh the persisted focus manifest from the repository file", "parameters": [ { "schema": { @@ -16002,12 +16151,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Refresh the persisted focus manifest from the repository file" + ] } }, "/v1/repos/{owner}/{repo}/agent/audit-feed": { "get": { + "summary": "Maintainer-scoped agent audit feed of executed actions and approval decisions", "parameters": [ { "schema": { @@ -16149,12 +16298,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Maintainer-scoped agent audit feed of executed actions and approval decisions" + ] } }, "/v1/repos/{owner}/{repo}/pulls/{number}/incident-reports": { "post": { + "summary": "Record a post-merge incident report for a pull request", "parameters": [ { "schema": { @@ -16274,12 +16423,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Record a post-merge incident report for a pull request" + ] } }, "/v1/app/incident-reports": { "post": { + "summary": "Record a post-merge incident report from the operator side", "requestBody": { "content": { "application/json": { @@ -16385,224 +16534,28 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Record a post-merge incident report from the operator side" - } - }, - "/v1/app/self-dogfood/registration-pack": { - "get": { - "responses": { - "200": { - "description": "Private self-dogfood registration pack for the LoopOver repo", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } - } - } - } - }, - "403": { - "description": "Insufficient role for maintainer-only self-dogfood report" - } - }, - "security": [ - { - "LoopOverBearer": [] - }, - { - "LoopOverSessionCookie": [] - } - ], - "summary": "Self-dogfood registration pack for the LoopOver repository" - } - }, - "/v1/repos/{owner}/{repo}/self-dogfood-registration-pack": { - "get": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "owner", - "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "repo", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Private self-dogfood registration pack when repo matches configured LoopOver target", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } - } - } - } - }, - "403": { - "description": "Insufficient role or repo is not the configured self-dogfood target" - } - }, - "security": [ - { - "LoopOverBearer": [] - }, - { - "LoopOverSessionCookie": [] - } - ], - "summary": "Self-dogfood registration pack when the repository matches the configured target" - } - }, - "/v1/repos/{owner}/{repo}/onboarding-pack/preview": { - "get": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "owner", - "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "repo", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Preview-only repo onboarding pack for accepted repositories", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } - } - } - } - }, - "403": { - "description": "Insufficient role" - }, - "404": { - "description": "Repository is not accepted or preview unavailable" - } - }, - "security": [ - { - "LoopOverBearer": [] - }, - { - "LoopOverSessionCookie": [] - } - ], - "summary": "Preview the onboarding pack for an accepted repository" - } - }, - "/v1/repos/{owner}/{repo}/contributor-issue-drafts/generate": { - "post": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "owner", - "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "repo", - "in": "path" - } - ], - "responses": { - "200": { - "description": "Generate maintainer-reviewed contributor issue drafts from repo policy (dry-run by default)", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } - } - } - } - }, - "400": { - "description": "Invalid request or explicit create without dryRun false" - }, - "403": { - "description": "Insufficient role" - } - }, - "security": [ - { - "LoopOverBearer": [] - }, - { - "LoopOverSessionCookie": [] - } - ], - "summary": "Generate maintainer-reviewed contributor issue drafts" + ] } - }, - "/v1/repos/{owner}/{repo}/settings": { - "get": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "owner", - "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "repo", - "in": "path" - } - ], + }, + "/v1/app/self-dogfood/registration-pack": { + "get": { + "summary": "Self-dogfood registration pack for the LoopOver repository", "responses": { "200": { - "description": "LoopOver repository automation settings", + "description": "Private self-dogfood registration pack for the LoopOver repo", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RepositorySettings" + "type": "object", + "additionalProperties": { + "nullable": true + } } } } + }, + "403": { + "description": "Insufficient role for maintainer-only self-dogfood report" } }, "security": [ @@ -16612,12 +16565,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Repository automation settings" + ] } }, - "/v1/repos/{owner}/{repo}/settings-preview": { - "post": { + "/v1/repos/{owner}/{repo}/self-dogfood-registration-pack": { + "get": { + "summary": "Self-dogfood registration pack when the repository matches the configured target", "parameters": [ { "schema": { @@ -16638,17 +16591,20 @@ ], "responses": { "200": { - "description": "Maintainer dry-run preview of the public surface decision for a sample PR (no GitHub mutation)", + "description": "Private self-dogfood registration pack when repo matches configured LoopOver target", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RepoSettingsPreview" + "type": "object", + "additionalProperties": { + "nullable": true + } } } } }, - "400": { - "description": "Invalid settings preview request" + "403": { + "description": "Insufficient role or repo is not the configured self-dogfood target" } }, "security": [ @@ -16658,12 +16614,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Dry-run the public surface decision for a sample pull request" + ] } }, - "/v1/repos/{owner}/{repo}/pulls/{number}/maintainer-packet": { + "/v1/repos/{owner}/{repo}/onboarding-pack/preview": { "get": { + "summary": "Preview the onboarding pack for an accepted repository", "parameters": [ { "schema": { @@ -16680,26 +16636,27 @@ "required": true, "name": "repo", "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "number", - "in": "path" } ], "responses": { "200": { - "description": "PR-specific maintainer review packet", + "description": "Preview-only repo onboarding pack for accepted repositories", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PullRequestMaintainerPacket" + "type": "object", + "additionalProperties": { + "nullable": true + } } } } + }, + "403": { + "description": "Insufficient role" + }, + "404": { + "description": "Repository is not accepted or preview unavailable" } }, "security": [ @@ -16709,12 +16666,12 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Maintainer review packet for a pull request" + ] } }, - "/v1/repos/{owner}/{repo}/pulls/{number}/reviewability": { - "get": { + "/v1/repos/{owner}/{repo}/contributor-issue-drafts/generate": { + "post": { + "summary": "Generate maintainer-reviewed contributor issue drafts", "parameters": [ { "schema": { @@ -16731,26 +16688,27 @@ "required": true, "name": "repo", "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "number", - "in": "path" } ], "responses": { "200": { - "description": "Private PR reviewability score and maintainer action", + "description": "Generate maintainer-reviewed contributor issue drafts from repo policy (dry-run by default)", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PullRequestReviewability" + "type": "object", + "additionalProperties": { + "nullable": true + } } } } + }, + "400": { + "description": "Invalid request or explicit create without dryRun false" + }, + "403": { + "description": "Insufficient role" } }, "security": [ @@ -16760,77 +16718,49 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Pull request reviewability score and maintainer action" + ] } }, - "/v1/contributors/{login}/profile": { - "get": { + "/v1/repos/{owner}/{repo}/issue-plan-drafts/generate": { + "post": { + "summary": "AI-plan repo issue drafts from a maintainer goal", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "login", + "name": "owner", "in": "path" - } - ], - "responses": { - "200": { - "description": "Contributor evidence profile", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ContributorProfile" - } - } - } - } - }, - "security": [ - { - "LoopOverBearer": [] }, - { - "LoopOverSessionCookie": [] - } - ], - "summary": "Contributor evidence profile" - } - }, - "/v1/contributors/{login}/decision-pack": { - "get": { - "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "login", + "name": "repo", "in": "path" } ], "responses": { "200": { - "description": "Canonical private contributor decision pack. May carry freshness 'stale' or 'rebuilding' when a background rebuild is in progress.", + "description": "AI-plan a small set of GitHub issue drafts from a maintainer-supplied planning goal (dry-run by default)", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ContributorDecisionPack" + "type": "object", + "additionalProperties": { + "nullable": true + } } } } }, - "202": { - "description": "Decision pack snapshot is missing; a background rebuild has been requested", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DecisionPackRefreshNeeded" - } - } - } + "400": { + "description": "Invalid request or explicit create without dryRun false" + }, + "403": { + "description": "Insufficient role" } }, "security": [ @@ -16840,29 +16770,37 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Canonical contributor decision pack" + ] } }, - "/v1/contributors/{login}/open-pr-monitor": { + "/v1/repos/{owner}/{repo}/settings": { "get": { + "summary": "Repository automation settings", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "login", + "name": "owner", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "repo", "in": "path" } ], "responses": { "200": { - "description": "Contributor open-PR monitor with classifications and public-safe next-step packets from cached metadata.", + "description": "LoopOver repository automation settings", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ContributorOpenPrMonitor" + "$ref": "#/components/schemas/RepositorySettings" } } } @@ -16875,21 +16813,13 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Contributor open-PR monitor with classifications and next-step packets" + ] } }, - "/v1/contributors/{login}/repos/{owner}/{repo}/decision": { + "/v1/repos/{owner}/{repo}/automation-state": { "get": { + "summary": "Derived agent automation state for a repository", "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "login", - "in": "path" - }, { "schema": { "type": "string" @@ -16909,21 +16839,11 @@ ], "responses": { "200": { - "description": "Repo-specific contributor decision from decision pack. May carry freshness 'stale' or 'rebuilding'.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RepoDecisionResponse" - } - } - } - }, - "202": { - "description": "Decision pack snapshot is missing; a background rebuild has been requested", + "description": "Maintainer-only derived automation view (mode, permission readiness, acting action classes, pending-approval count) that the raw /settings row does not include", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DecisionPackRefreshNeeded" + "$ref": "#/components/schemas/AutomationState" } } } @@ -16936,53 +16856,40 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Repository-specific contributor decision" + ] } }, - "/v1/preflight/pr": { + "/v1/repos/{owner}/{repo}/repo-docs/refresh": { "post": { - "responses": { - "200": { - "description": "Submission preflight result", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PreflightResult" - } - } - } - }, - "400": { - "description": "Invalid preflight input" - } - }, - "security": [ + "summary": "Open (or find the already-open) AGENTS.md/CLAUDE.md generation pull request", + "parameters": [ { - "LoopOverBearer": [] + "schema": { + "type": "string" + }, + "required": true, + "name": "owner", + "in": "path" }, { - "LoopOverSessionCookie": [] + "schema": { + "type": "string" + }, + "required": true, + "name": "repo", + "in": "path" } ], - "summary": "Run submission preflight for a pull request" - } - }, - "/v1/preflight/local-diff": { - "post": { "responses": { "200": { - "description": "Local diff preflight result", + "description": "The repo-doc pull request result -- opened (new or reused) or a reason it was not opened", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LocalDiffPreflightResult" + "$ref": "#/components/schemas/RepoDocRefreshResult" } } } - }, - "400": { - "description": "Invalid local diff preflight input" } }, "security": [ @@ -16992,59 +16899,43 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Run preflight against a local diff" + ] } }, - "/v1/local/branch-analysis": { - "post": { - "responses": { - "200": { - "description": "Private local branch analysis for MCP clients", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LocalBranchAnalysis" - } - } - } - }, - "400": { - "description": "Invalid local branch analysis input" - }, - "401": { - "description": "Unauthorized" - } - }, - "security": [ + "/v1/repos/{owner}/{repo}/settings-preview": { + "post": { + "summary": "Dry-run the public surface decision for a sample pull request", + "parameters": [ { - "LoopOverBearer": [] + "schema": { + "type": "string" + }, + "required": true, + "name": "owner", + "in": "path" }, { - "LoopOverSessionCookie": [] + "schema": { + "type": "string" + }, + "required": true, + "name": "repo", + "in": "path" } ], - "summary": "Analyze a local branch for MCP clients" - } - }, - "/v1/agent/runs": { - "post": { "responses": { - "202": { - "description": "Copilot-only agent run queued", + "200": { + "description": "Maintainer dry-run preview of the public surface decision for a sample PR (no GitHub mutation)", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AgentRunBundle" + "$ref": "#/components/schemas/RepoSettingsPreview" } } } }, "400": { - "description": "Invalid agent run request" - }, - "401": { - "description": "Unauthorized" + "description": "Invalid settings preview request" } }, "security": [ @@ -17054,60 +16945,48 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Queue an agent run" - }, + ] + } + }, + "/v1/repos/{owner}/{repo}/pulls/{number}/maintainer-packet": { "get": { + "summary": "Maintainer review packet for a pull request", "parameters": [ { "schema": { - "type": "string", - "minLength": 1, - "example": "jsonbored" + "type": "string" }, "required": true, - "description": "GitHub login that owns the agent runs.", - "name": "actorLogin", - "in": "query" + "name": "owner", + "in": "path" }, { "schema": { - "type": "string", - "example": "50" + "type": "string" }, - "required": false, - "description": "Maximum run bundles to return, clamped from 1 to 100.", - "name": "limit", - "in": "query" + "required": true, + "name": "repo", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "number", + "in": "path" } ], "responses": { "200": { - "description": "Recent agent run bundles for an authenticated actor", + "description": "PR-specific maintainer review packet", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "runs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AgentRunBundle" - } - } - }, - "required": [ - "runs" - ] + "$ref": "#/components/schemas/PullRequestMaintainerPacket" } } } - }, - "400": { - "description": "Missing actor login" - }, - "401": { - "description": "Unauthorized" } }, "security": [ @@ -17117,35 +16996,48 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "List persisted agent runs for an actor" + ] } }, - "/v1/agent/runs/{id}": { + "/v1/repos/{owner}/{repo}/pulls/{number}/reviewability": { "get": { + "summary": "Pull request reviewability score and maintainer action", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "id", + "name": "owner", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "repo", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "number", "in": "path" } ], "responses": { "200": { - "description": "Persisted agent run bundle", + "description": "Private PR reviewability score and maintainer action", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AgentRunBundle" + "$ref": "#/components/schemas/PullRequestReviewability" } } } - }, - "404": { - "description": "Agent run not found" } }, "security": [ @@ -17155,38 +17047,32 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Persisted agent run bundle" + ] } }, - "/v1/agent/plan-next-work": { - "post": { + "/v1/contributors/{login}/profile": { + "get": { + "summary": "Contributor evidence profile", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "login", + "in": "path" + } + ], "responses": { "200": { - "description": "Agent run completed with deterministic ranked actions", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AgentRunBundle" - } - } - } - }, - "202": { - "description": "Agent run needs snapshot refresh", + "description": "Contributor evidence profile", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AgentRunBundle" + "$ref": "#/components/schemas/ContributorProfile" } } } - }, - "400": { - "description": "Invalid agent request" - }, - "401": { - "description": "Unauthorized" } }, "security": [ @@ -17196,38 +17082,42 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Rank the next work items for an agent run" + ] } }, - "/v1/agent/preflight-branch": { - "post": { + "/v1/contributors/{login}/decision-pack": { + "get": { + "summary": "Canonical contributor decision pack", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "login", + "in": "path" + } + ], "responses": { "200": { - "description": "Agent run completed with deterministic ranked actions", + "description": "Canonical private contributor decision pack. May carry freshness 'stale' or 'rebuilding' when a background rebuild is in progress.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AgentRunBundle" + "$ref": "#/components/schemas/ContributorDecisionPack" } } } }, "202": { - "description": "Agent run needs snapshot refresh", + "description": "Decision pack snapshot is missing; a background rebuild has been requested", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AgentRunBundle" + "$ref": "#/components/schemas/DecisionPackRefreshNeeded" } } } - }, - "400": { - "description": "Invalid agent request" - }, - "401": { - "description": "Unauthorized" } }, "security": [ @@ -17237,38 +17127,32 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Preflight an agent branch before submission" + ] } }, - "/v1/agent/prepare-pr-packet": { - "post": { + "/v1/contributors/{login}/open-pr-monitor": { + "get": { + "summary": "Contributor open-PR monitor with classifications and next-step packets", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "login", + "in": "path" + } + ], "responses": { "200": { - "description": "Agent run completed with deterministic ranked actions", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AgentRunBundle" - } - } - } - }, - "202": { - "description": "Agent run needs snapshot refresh", + "description": "Contributor open-PR monitor with classifications and public-safe next-step packets from cached metadata.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AgentRunBundle" + "$ref": "#/components/schemas/ContributorOpenPrMonitor" } } } - }, - "400": { - "description": "Invalid agent request" - }, - "401": { - "description": "Unauthorized" } }, "security": [ @@ -17278,38 +17162,43 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Prepare a pull request packet for an agent run" + ] } }, - "/v1/agent/explain-blockers": { - "post": { - "responses": { - "200": { - "description": "Agent run completed with deterministic ranked actions", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AgentRunBundle" - } - } - } + "/v1/contributors/{login}/pr-outcomes": { + "get": { + "summary": "Contributor post-merge PR outcome history", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "login", + "in": "path" }, - "202": { - "description": "Agent run needs snapshot refresh", + { + "schema": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "maximum": 100 + }, + "required": false, + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Self-scoped post-merge outcome records with public-safe attribution (mirrors loopover_pr_outcome).", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AgentRunBundle" + "$ref": "#/components/schemas/ContributorPrOutcomes" } } } - }, - "400": { - "description": "Invalid agent request" - }, - "401": { - "description": "Unauthorized" } }, "security": [ @@ -17319,22 +17208,29 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Explain an agent run's current blockers" + ] } }, - "/v1/bounties": { + "/v1/contributors/{login}/notifications": { "get": { + "summary": "Contributor badge notification feed", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "login", + "in": "path" + } + ], "responses": { "200": { - "description": "Known bounty records", + "description": "The contributor's own badge notification feed (self-scoped), newest first, with an unread count.", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Bounty" - } + "$ref": "#/components/schemas/NotificationFeed" } } } @@ -17347,35 +17243,52 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "List known bounty records" + ] } }, - "/v1/bounties/{id}/advisory": { - "get": { + "/v1/contributors/{login}/notifications/read": { + "post": { + "summary": "Mark contributor notifications read", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "id", + "name": "login", "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, "responses": { "200": { - "description": "Bounty lifecycle advisory", + "description": "Marks the contributor's delivered badge notifications read; an absent/empty ids array marks all.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BountyAdvisory" + "$ref": "#/components/schemas/NotificationsMarked" } } } }, - "404": { - "description": "Bounty not found" + "400": { + "description": "Invalid mark-read body" } }, "security": [ @@ -17385,35 +17298,58 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Bounty lifecycle advisory" + ] } }, - "/v1/bounties/{id}/lifecycle": { + "/v1/contributors/{login}/repos/{owner}/{repo}/decision": { "get": { + "summary": "Repository-specific contributor decision", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "id", + "name": "login", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "owner", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "repo", "in": "path" } ], "responses": { "200": { - "description": "Bounty lifecycle transition history", + "description": "Repo-specific contributor decision from decision pack. May carry freshness 'stale' or 'rebuilding'.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BountyLifecycleEvents" + "$ref": "#/components/schemas/RepoDecisionResponse" } } } }, - "404": { - "description": "Bounty not found" + "202": { + "description": "Decision pack snapshot is missing; a background rebuild has been requested", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DecisionPackRefreshNeeded" + } + } + } } }, "security": [ @@ -17423,31 +17359,25 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Bounty lifecycle transition history" - } - }, - "/v1/github/webhook": { - "post": { - "responses": { - "202": { - "description": "Webhook queued" - }, - "401": { - "description": "Invalid webhook signature" - } - }, - "summary": "Receive a GitHub webhook delivery" + ] } }, - "/v1/orb/ingest": { + "/v1/preflight/pr": { "post": { + "summary": "Run submission preflight for a pull request", "responses": { "200": { - "description": "Batch accepted; returns { accepted: number }" + "description": "Submission preflight result", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PreflightResult" + } + } + } }, "400": { - "description": "Malformed JSON or invalid payload shape" + "description": "Invalid preflight input" } }, "security": [ @@ -17457,152 +17387,56 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Ingest a batch of Orb events" - } - }, - "/v1/auth/github/start": { - "get": { - "responses": { - "302": { - "description": "Redirects to GitHub web OAuth" - }, - "503": { - "description": "GitHub OAuth app secret is not configured" - } - }, - "summary": "Start GitHub web OAuth" - } - }, - "/v1/auth/github/callback": { - "get": { - "responses": { - "302": { - "description": "Completes GitHub web OAuth and redirects to the app" - } - }, - "summary": "Complete GitHub web OAuth and redirect to the app" - } - }, - "/v1/auth/github/device/start": { - "post": { - "responses": { - "200": { - "description": "Auth request completed" - }, - "201": { - "description": "Auth session created" - }, - "400": { - "description": "Invalid auth request" - }, - "401": { - "description": "Unauthorized" - }, - "429": { - "description": "Rate limited" - } - }, - "summary": "Start GitHub device-flow authentication" + ] } }, - "/v1/auth/github/device/poll": { + "/v1/preflight/review-risk": { "post": { + "summary": "Explain review risk for a planned pull request", "responses": { "200": { - "description": "Auth request completed" - }, - "201": { - "description": "Auth session created" + "description": "Review-risk explanation with preflight, role context, and recommendation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReviewRiskExplanation" + } + } + } }, "400": { - "description": "Invalid auth request" - }, - "401": { - "description": "Unauthorized" + "description": "Invalid preflight input" }, - "429": { - "description": "Rate limited" + "403": { + "description": "Forbidden when contributorLogin does not match the authenticated session" } }, - "summary": "Poll a pending GitHub device-flow authorization" - } - }, - "/v1/auth/github/session": { - "post": { - "responses": { - "200": { - "description": "Auth request completed" - }, - "201": { - "description": "Auth session created" - }, - "400": { - "description": "Invalid auth request" - }, - "401": { - "description": "Unauthorized" + "security": [ + { + "LoopOverBearer": [] }, - "429": { - "description": "Rate limited" + { + "LoopOverSessionCookie": [] } - }, - "summary": "Exchange a GitHub token for a LoopOver session" + ] } }, - "/v1/auth/logout": { + "/v1/preflight/local-diff": { "post": { + "summary": "Run preflight against a local diff", "responses": { "200": { - "description": "Auth request completed" - }, - "201": { - "description": "Auth session created" - }, - "400": { - "description": "Invalid auth request" - }, - "401": { - "description": "Unauthorized" - }, - "429": { - "description": "Rate limited" - } - }, - "summary": "End the current session" - } - }, - "/v1/auth/session": { - "get": { - "responses": { - "200": { - "description": "Current auth session, or signed_out when no app session is present" - } - }, - "summary": "Current authentication session" - } - }, - "/v1/app/overview": { - "get": { - "responses": { - "200": { - "description": "Live app overview assembled from backend data", + "description": "Local diff preflight result", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "$ref": "#/components/schemas/LocalDiffPreflightResult" } } } }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Insufficient role" + "400": { + "description": "Invalid local diff preflight input" } }, "security": [ @@ -17612,26 +17446,26 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Live app overview assembled from backend data" + ] } }, - "/v1/app/roles": { - "get": { + "/v1/local/branch-analysis": { + "post": { + "summary": "Analyze a local branch for MCP clients", "responses": { "200": { - "description": "Live app API response", + "description": "Private local branch analysis for MCP clients", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "$ref": "#/components/schemas/LocalBranchAnalysis" } } } }, + "400": { + "description": "Invalid local branch analysis input" + }, "401": { "description": "Unauthorized" } @@ -17643,26 +17477,26 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "App roles granted to the current session" + ] } }, - "/v1/app/miner-dashboard": { - "get": { + "/v1/agent/runs": { + "post": { + "summary": "Queue an agent run", "responses": { - "200": { - "description": "Live app API response", + "202": { + "description": "Copilot-only agent run queued", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "$ref": "#/components/schemas/AgentRunBundle" } } } }, + "400": { + "description": "Invalid agent run request" + }, "401": { "description": "Unauthorized" } @@ -17674,26 +17508,58 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Miner dashboard data" - } - }, - "/v1/app/maintainer-dashboard": { + ] + }, "get": { + "summary": "List persisted agent runs for an actor", + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "example": "jsonbored" + }, + "required": true, + "description": "GitHub login that owns the agent runs.", + "name": "actorLogin", + "in": "query" + }, + { + "schema": { + "type": "string", + "example": "50" + }, + "required": false, + "description": "Maximum run bundles to return, clamped from 1 to 100.", + "name": "limit", + "in": "query" + } + ], "responses": { "200": { - "description": "Live app API response", + "description": "Recent agent run bundles for an authenticated actor", "content": { "application/json": { "schema": { "type": "object", - "additionalProperties": { - "nullable": true - } + "properties": { + "runs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentRunBundle" + } + } + }, + "required": [ + "runs" + ] } } } }, + "400": { + "description": "Missing actor login" + }, "401": { "description": "Unauthorized" } @@ -17705,28 +17571,35 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Maintainer dashboard data" + ] } }, - "/v1/app/operator-dashboard": { + "/v1/agent/runs/{id}": { "get": { + "summary": "Persisted agent run bundle", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + } + ], "responses": { "200": { - "description": "Live app API response", + "description": "Persisted agent run bundle", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "$ref": "#/components/schemas/AgentRunBundle" } } } }, - "401": { - "description": "Unauthorized" + "404": { + "description": "Agent run not found" } }, "security": [ @@ -17736,26 +17609,36 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Operator dashboard data" + ] } }, - "/v1/app/commands": { - "get": { + "/v1/agent/plan-next-work": { + "post": { + "summary": "Rank the next work items for an agent run", "responses": { "200": { - "description": "Live app API response", + "description": "Agent run completed with deterministic ranked actions", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "$ref": "#/components/schemas/AgentRunBundle" + } + } + } + }, + "202": { + "description": "Agent run needs snapshot refresh", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentRunBundle" } } } }, + "400": { + "description": "Invalid agent request" + }, "401": { "description": "Unauthorized" } @@ -17767,26 +17650,36 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "@loopover command catalog" + ] } }, - "/v1/app/commands/usefulness": { - "get": { + "/v1/agent/preflight-branch": { + "post": { + "summary": "Preflight an agent branch before submission", "responses": { "200": { - "description": "Live app API response", + "description": "Agent run completed with deterministic ranked actions", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "$ref": "#/components/schemas/AgentRunBundle" + } + } + } + }, + "202": { + "description": "Agent run needs snapshot refresh", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentRunBundle" } } } }, + "400": { + "description": "Invalid agent request" + }, "401": { "description": "Unauthorized" } @@ -17798,26 +17691,36 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "@loopover command usefulness rollup" + ] } }, - "/v1/app/digest": { - "get": { + "/v1/agent/prepare-pr-packet": { + "post": { + "summary": "Prepare a pull request packet for an agent run", "responses": { "200": { - "description": "Live app API response", + "description": "Agent run completed with deterministic ranked actions", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "$ref": "#/components/schemas/AgentRunBundle" + } + } + } + }, + "202": { + "description": "Agent run needs snapshot refresh", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentRunBundle" } } } }, + "400": { + "description": "Invalid agent request" + }, "401": { "description": "Unauthorized" } @@ -17829,26 +17732,36 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Maintainer digest content" + ] } }, - "/v1/app/analytics/daily-rollups": { - "get": { + "/v1/agent/explain-blockers": { + "post": { + "summary": "Explain an agent run's current blockers", "responses": { "200": { - "description": "Live app API response", + "description": "Agent run completed with deterministic ranked actions", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "$ref": "#/components/schemas/AgentRunBundle" + } + } + } + }, + "202": { + "description": "Agent run needs snapshot refresh", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentRunBundle" } } } }, + "400": { + "description": "Invalid agent request" + }, "401": { "description": "Unauthorized" } @@ -17860,28 +17773,25 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Daily analytics rollups" + ] } }, - "/v1/app/analytics/mcp-compatibility": { + "/v1/bounties": { "get": { + "summary": "List known bounty records", "responses": { "200": { - "description": "Live app API response", + "description": "Known bounty records", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": { - "nullable": true + "type": "array", + "items": { + "$ref": "#/components/schemas/Bounty" } } } } - }, - "401": { - "description": "Unauthorized" } }, "security": [ @@ -17891,52 +17801,35 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "MCP client compatibility analytics" + ] } }, - "/v1/app/selfhost/queue/dead/{id}/replay": { - "post": { + "/v1/bounties/{id}/advisory": { + "get": { + "summary": "Bounty lifecycle advisory", "parameters": [ { "schema": { - "type": "string", - "example": "812" + "type": "string" }, "required": true, - "description": "Dead-letter job id.", "name": "id", "in": "path" } ], "responses": { "200": { - "description": "Job replayed", + "description": "Bounty lifecycle advisory", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "$ref": "#/components/schemas/BountyAdvisory" } } } }, - "400": { - "description": "Invalid job id" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Insufficient app role (operator only)" - }, "404": { - "description": "Dead-letter job not found" - }, - "501": { - "description": "This deployment's queue backend does not expose dead-letter admin" + "description": "Bounty not found" } }, "security": [ @@ -17946,52 +17839,35 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Replay a dead-letter queue job" + ] } }, - "/v1/app/selfhost/queue/dead/{id}": { - "delete": { + "/v1/bounties/{id}/lifecycle": { + "get": { + "summary": "Bounty lifecycle transition history", "parameters": [ { "schema": { - "type": "string", - "example": "812" + "type": "string" }, "required": true, - "description": "Dead-letter job id.", "name": "id", "in": "path" } ], "responses": { "200": { - "description": "Job deleted", + "description": "Bounty lifecycle transition history", "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "$ref": "#/components/schemas/BountyLifecycleEvents" } } } }, - "400": { - "description": "Invalid job id" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Insufficient app role (operator only)" - }, "404": { - "description": "Dead-letter job not found" - }, - "501": { - "description": "This deployment's queue backend does not expose dead-letter admin" + "description": "Bounty not found" } }, "security": [ @@ -18001,34 +17877,31 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Delete a dead-letter queue job" + ] } }, - "/v1/app/selfhost/queue/dead": { - "delete": { + "/v1/github/webhook": { + "post": { + "summary": "Receive a GitHub webhook delivery", "responses": { - "200": { - "description": "Dead-letter jobs purged", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } - } - } - } + "202": { + "description": "Webhook queued" }, "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Insufficient app role (operator only)" + "description": "Invalid webhook signature" + } + } + } + }, + "/v1/public/decision-ledger/verify": { + "get": { + "summary": "Verify a window of the hash-chained decision ledger (resumable via afterSeq)", + "responses": { + "200": { + "description": "Window verified clean; nextAfterSeq is the resume cursor (null at the tip). Every response also carries tipSeq/tipHash/totalCount for third-party checkpointing." }, - "501": { - "description": "This deployment's queue backend does not expose dead-letter admin" + "409": { + "description": "First break found: sequence_gap | predecessor_mismatch | row_hash_mismatch | short_tail (a record exists past the verified tip with no chain entry)" } }, "security": [ @@ -18038,57 +17911,49 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Purge all dead-letter queue jobs" - }, + ] + } + }, + "/v1/public/decision-ledger/row/{seq}": { "get": { + "summary": "Fetch one decision-ledger row by seq, so an external anchor can be bound back to the live chain", "parameters": [ { "schema": { - "type": "string", - "example": "25" - }, - "required": false, - "description": "Maximum rows to return, clamped from 1 to 100.", - "name": "limit", - "in": "query" - }, - { - "schema": { - "type": "string", - "example": "0" + "type": "string" }, - "required": false, - "description": "Pagination offset, floored to 0.", - "name": "offset", - "in": "query" + "required": true, + "name": "seq", + "in": "path" } ], "responses": { "200": { - "description": "Paginated dead-letter jobs for the self-host queue backend", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } - } - } - } + "description": "The chain row: seq, recordId, recordDigest, prevHash, rowHash, createdAt. Recompute sha256(prevHash || canonicalJson({seq, recordId, recordDigest, createdAt})) and compare against an anchored rowHash." }, "400": { - "description": "Invalid query" - }, - "401": { - "description": "Unauthorized" + "description": "seq is not a positive integer" }, - "403": { - "description": "Insufficient app role (operator only)" + "404": { + "description": "No ledger row at that seq (never appended -- distinct from a row with empty fields)" + } + }, + "security": [ + { + "LoopOverBearer": [] }, - "501": { - "description": "This deployment's queue backend does not expose dead-letter admin (e.g. Cloudflare)" + { + "LoopOverSessionCookie": [] + } + ] + } + }, + "/v1/public/decision-ledger/anchor-key": { + "get": { + "summary": "Published anchor-signing public keys with their full rotation history, for verifying an externally-published ledger anchor", + "responses": { + "200": { + "description": "{ keys: [{ keyId, publicKeySpki, notBefore, notAfter }], currentKeyId } — retired keys are retained so anchors signed under them stay verifiable; currentKeyId is null when unconfigured or the rotation state is ambiguous" } }, "security": [ @@ -18098,77 +17963,46 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "List dead-letter queue jobs" + ] } }, - "/v1/app/analytics/weekly-value-report": { + "/v1/public/decision-ledger/anchors": { "get": { + "summary": "Every external anchoring attempt, success and failure, paginated newest-first — anchoring's own health as a public fact", "parameters": [ { "schema": { "type": "string", "enum": [ - "public", - "operator" - ], - "example": "public" + "rekor", + "git", + "ots" + ] }, "required": false, - "description": "Report variant. Operator reports require the operator app role.", - "name": "variant", + "name": "backend", "in": "query" }, { "schema": { - "type": "string", - "example": "7" + "type": "string" }, "required": false, - "description": "Report window in days, clamped from 1 to 31.", - "name": "days", + "name": "before", "in": "query" }, { "schema": { - "type": "string", - "enum": [ - "json", - "markdown" - ], - "example": "markdown" + "type": "string" }, "required": false, - "description": "Response format. Omit or use json for the structured report; use markdown for copy-ready text.", - "name": "format", + "name": "limit", "in": "query" } ], "responses": { "200": { - "description": "Weekly value report as structured JSON or copy-ready Markdown", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } - } - }, - "text/markdown": { - "schema": { - "type": "string", - "example": "# Weekly LoopOver value report\n\n## Adoption metrics\n- Active users: 4\n" - } - } - } - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Insufficient app role for requested report variant" + "description": "{ anchors: [{ id, seq, rowHash, keyId, backend, backendRef, status, error, createdAt }], nextBefore } — a failed attempt is returned identically to a successful one, never filtered out or reshaped" } }, "security": [ @@ -18178,92 +18012,86 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Weekly value report" + ] } }, - "/v1/app/skipped-pr-audit": { + "/v1/public/decision-records/{owner}/{repo}/{pull}": { "get": { + "summary": "Fetch the latest published decision record for a PR, verbatim, plus its content digest", "parameters": [ { "schema": { - "type": "string", - "example": "50" + "type": "string" }, - "required": false, - "description": "Maximum rows to return, clamped from 1 to 100.", - "name": "limit", - "in": "query" + "required": true, + "name": "owner", + "in": "path" }, { "schema": { - "type": "string", - "example": "0" + "type": "string" }, - "required": false, - "description": "Number of parsed skip events to skip before returning rows (non-negative).", - "name": "offset", - "in": "query" + "required": true, + "name": "repo", + "in": "path" }, { "schema": { - "type": "string", - "example": "JSONbored/loopover" + "type": "string" }, - "required": false, - "description": "Optional repository filter. Browser sessions must have control-panel access to this repo.", - "name": "repoFullName", - "in": "query" + "required": true, + "name": "pull", + "in": "path" + } + ], + "responses": { + "200": { + "description": "The latest DecisionRecord for this PR + its recordDigest" + }, + "400": { + "description": "Invalid pull number" + }, + "404": { + "description": "No decision record persisted yet for this PR" + } + }, + "security": [ + { + "LoopOverBearer": [] }, + { + "LoopOverSessionCookie": [] + } + ] + } + }, + "/v1/public/eval-scores": { + "get": { + "summary": "Fetch EvalScoreRecords (#9215) -- the objective-eval-provider transport, digest-committed and independently re-derivable", + "parameters": [ { "schema": { - "type": "string", - "example": "not_official_gittensor_miner", - "enum": [ - "surface_off", - "missing_author", - "bot_author", - "ignored_author", - "maintainer_author", - "miner_detection_unavailable", - "not_official_gittensor_miner" - ] + "type": "string" }, "required": false, - "description": "Optional PR skip reason filter.", - "name": "reason", + "name": "subject", "in": "query" }, { "schema": { - "type": "string", - "example": "2026-05-30T00:00:00.000Z" + "type": "string" }, "required": false, - "description": "Optional lower timestamp bound.", "name": "since", "in": "query" } ], "responses": { "200": { - "description": "Private bounded audit export for skipped PR public-surface decisions", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SkippedPrAuditExport" - } - } - } - }, - "400": { - "description": "Invalid query" - }, - "401": { - "description": "Unauthorized" + "description": "{ records: EvalScoreRecord[] }, optionally filtered by subject id and/or minimum issuedAt -- degrades to an empty array on an internal read error rather than a non-200 status, matching loadPublicRulePrecision's own fail-safe contract" }, - "403": { - "description": "Insufficient app role or repository scope" + "404": { + "description": "Public stats disabled (same flag as /v1/public/stats)" } }, "security": [ @@ -18273,34 +18101,18 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Audit of pull requests the review agent skipped" + ] } }, - "/v1/app/commands/preview": { + "/v1/orb/ingest": { "post": { + "summary": "Ingest a batch of Orb events", "responses": { "200": { - "description": "Maintainer dry-run preview of a sanitized @loopover command response (no GitHub mutation)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CommandPreviewResponse" - } - } - } + "description": "Batch accepted; returns { accepted: number }" }, "400": { - "description": "Invalid request" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Insufficient app role" - }, - "404": { - "description": "Command not found" + "description": "Malformed JSON or invalid payload shape" } }, "security": [ @@ -18310,112 +18122,160 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Dry-run a sanitized @loopover command response" + ] } }, - "/v1/app/commands/feedback": { + "/v1/auth/github/start": { + "get": { + "summary": "Start GitHub web OAuth", + "responses": { + "302": { + "description": "Redirects to GitHub web OAuth" + }, + "503": { + "description": "GitHub OAuth app secret is not configured" + } + } + } + }, + "/v1/auth/github/callback": { + "get": { + "summary": "Complete GitHub web OAuth and redirect to the app", + "responses": { + "302": { + "description": "Completes GitHub web OAuth and redirects to the app" + } + } + } + }, + "/v1/auth/github/device/start": { "post": { + "summary": "Start GitHub device-flow authentication", "responses": { "200": { - "description": "Live app mutation or preview response", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } - } - } - } + "description": "Auth request completed" }, "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } - } - } - } + "description": "Auth session created" }, "400": { - "description": "Invalid request" + "description": "Invalid auth request" }, "401": { "description": "Unauthorized" - } - }, - "security": [ - { - "LoopOverBearer": [] }, - { - "LoopOverSessionCookie": [] + "429": { + "description": "Rate limited" } - ], - "summary": "Submit feedback on an @loopover command response" + } } }, - "/v1/app/digest/subscriptions": { + "/v1/auth/github/device/poll": { "post": { + "summary": "Poll a pending GitHub device-flow authorization", "responses": { "200": { - "description": "Live app mutation or preview response", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } - } - } - } + "description": "Auth request completed" }, "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } - } - } - } + "description": "Auth session created" }, "400": { - "description": "Invalid request" + "description": "Invalid auth request" }, "401": { "description": "Unauthorized" + }, + "429": { + "description": "Rate limited" } - }, - "security": [ - { - "LoopOverBearer": [] + } + } + }, + "/v1/auth/github/session": { + "post": { + "summary": "Exchange a GitHub token for a LoopOver session", + "responses": { + "200": { + "description": "Auth request completed" }, - { - "LoopOverSessionCookie": [] + "201": { + "description": "Auth session created" + }, + "400": { + "description": "Invalid auth request" + }, + "401": { + "description": "Unauthorized" + }, + "429": { + "description": "Rate limited" } - ], - "summary": "Manage maintainer digest subscriptions" + } } }, - "/v1/internal/jobs/refresh-registry": { + "/v1/auth/logout": { "post": { + "summary": "End the current session", "responses": { - "202": { - "description": "Registry refresh queued" + "200": { + "description": "Auth request completed" + }, + "201": { + "description": "Auth session created" + }, + "400": { + "description": "Invalid auth request" }, "401": { - "description": "Invalid internal token" + "description": "Unauthorized" + }, + "429": { + "description": "Rate limited" + } + } + } + }, + "/v1/auth/session": { + "get": { + "summary": "Current authentication session", + "responses": { + "200": { + "description": "Current auth session, or signed_out when no app session is present" + } + } + } + }, + "/v1/auth/github/token": { + "post": { + "summary": "Fetch the current session's live GitHub token (for AMS git operations)", + "responses": { + "200": { + "description": "The session's GitHub token", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + }, + "required": [ + "token" + ] + } + } + } + }, + "403": { + "description": "A browser session is required" + }, + "404": { + "description": "No GitHub token is available for this session" + }, + "429": { + "description": "Rate limited" } }, "security": [ @@ -18425,18 +18285,31 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Queue a registry refresh job" + ] } }, - "/v1/internal/jobs/backfill-registered-repos": { - "post": { + "/v1/app/overview": { + "get": { + "summary": "Live app overview assembled from backend data", "responses": { - "202": { - "description": "Registered repo backfill queued" + "200": { + "description": "Live app overview assembled from backend data", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } }, "401": { - "description": "Invalid internal token" + "description": "Unauthorized" + }, + "403": { + "description": "Insufficient role" } }, "security": [ @@ -18446,21 +18319,28 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Queue a registered-repository backfill job" + ] } }, - "/v1/internal/jobs/backfill-repo-segment": { - "post": { + "/v1/app/roles": { + "get": { + "summary": "App roles granted to the current session", "responses": { - "202": { - "description": "Repository segment backfill queued" - }, - "400": { - "description": "Invalid segment request" + "200": { + "description": "Live app API response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } }, "401": { - "description": "Invalid internal token" + "description": "Unauthorized" } }, "security": [ @@ -18470,21 +18350,28 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Queue a repository segment backfill job" + ] } }, - "/v1/internal/jobs/backfill-pr-details": { - "post": { + "/v1/app/miner-dashboard": { + "get": { + "summary": "Miner dashboard data", "responses": { - "202": { - "description": "Open PR detail backfill queued" - }, - "400": { - "description": "Invalid PR detail backfill request" + "200": { + "description": "Live app API response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } }, "401": { - "description": "Invalid internal token" + "description": "Unauthorized" } }, "security": [ @@ -18494,21 +18381,28 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Queue an open pull request detail backfill job" + ] } }, - "/v1/internal/jobs/generate-review-recap": { - "post": { + "/v1/app/maintainer-dashboard": { + "get": { + "summary": "Maintainer dashboard data", "responses": { - "202": { - "description": "Maintainer review recap digest queued (#1963)" - }, - "400": { - "description": "Missing repoFullName" + "200": { + "description": "Live app API response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } }, "401": { - "description": "Invalid internal token" + "description": "Unauthorized" } }, "security": [ @@ -18518,18 +18412,28 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Queue a maintainer review recap digest job" + ] } }, - "/v1/internal/jobs/refresh-scoring-model": { - "post": { + "/v1/app/operator-dashboard": { + "get": { + "summary": "Operator dashboard data", "responses": { - "202": { - "description": "Internal job queued" + "200": { + "description": "Live app API response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } }, "401": { - "description": "Invalid internal token" + "description": "Unauthorized" } }, "security": [ @@ -18539,18 +18443,28 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Queue a scoring model refresh job" + ] } }, - "/v1/internal/jobs/refresh-upstream-drift": { - "post": { + "/v1/app/commands": { + "get": { + "summary": "@loopover command catalog", "responses": { - "202": { - "description": "Internal job queued" + "200": { + "description": "Live app API response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } }, "401": { - "description": "Invalid internal token" + "description": "Unauthorized" } }, "security": [ @@ -18560,18 +18474,28 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Queue an upstream drift refresh job" + ] } }, - "/v1/internal/jobs/file-upstream-drift-issues": { - "post": { + "/v1/app/commands/usefulness": { + "get": { + "summary": "@loopover command usefulness rollup", "responses": { - "202": { - "description": "Internal job queued" + "200": { + "description": "Live app API response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } }, "401": { - "description": "Invalid internal token" + "description": "Unauthorized" } }, "security": [ @@ -18581,18 +18505,28 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Queue a job that files upstream drift issues" + ] } }, - "/v1/internal/jobs/build-contributor-evidence": { - "post": { + "/v1/app/digest": { + "get": { + "summary": "Maintainer digest content", "responses": { - "202": { - "description": "Internal job queued" + "200": { + "description": "Live app API response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } }, "401": { - "description": "Invalid internal token" + "description": "Unauthorized" } }, "security": [ @@ -18602,18 +18536,28 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Queue a contributor evidence build job" + ] } }, - "/v1/internal/jobs/build-contributor-decision-packs": { - "post": { + "/v1/app/analytics/daily-rollups": { + "get": { + "summary": "Daily analytics rollups", "responses": { - "202": { - "description": "Internal job queued" + "200": { + "description": "Live app API response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } }, "401": { - "description": "Invalid internal token" + "description": "Unauthorized" } }, "security": [ @@ -18623,18 +18567,28 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Queue a contributor decision pack build job" + ] } }, - "/v1/internal/jobs/build-burden-forecasts": { - "post": { + "/v1/app/analytics/mcp-compatibility": { + "get": { + "summary": "MCP client compatibility analytics", "responses": { - "202": { - "description": "Internal job queued" + "200": { + "description": "Live app API response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } }, "401": { - "description": "Invalid internal token" + "description": "Unauthorized" } }, "security": [ @@ -18644,18 +18598,52 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Queue a burden forecast build job" + ] } }, - "/v1/internal/jobs/generate-signal-snapshots": { + "/v1/app/selfhost/queue/dead/{id}/replay": { "post": { + "summary": "Replay a dead-letter queue job", + "parameters": [ + { + "schema": { + "type": "string", + "example": "812" + }, + "required": true, + "description": "Dead-letter job id.", + "name": "id", + "in": "path" + } + ], "responses": { - "202": { - "description": "Internal job queued" + "200": { + "description": "Job replayed", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } + }, + "400": { + "description": "Invalid job id" }, "401": { - "description": "Invalid internal token" + "description": "Unauthorized" + }, + "403": { + "description": "Insufficient app role (operator only)" + }, + "404": { + "description": "Dead-letter job not found" + }, + "501": { + "description": "This deployment's queue backend does not expose dead-letter admin" } }, "security": [ @@ -18665,39 +18653,52 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Queue a signal snapshot generation job" + ] } }, - "/v1/internal/jobs/generate-weekly-value-report": { - "post": { - "responses": { - "202": { - "description": "Internal job queued" - }, - "401": { - "description": "Invalid internal token" - } - }, - "security": [ - { - "LoopOverBearer": [] - }, + "/v1/app/selfhost/queue/dead/{id}": { + "delete": { + "summary": "Delete a dead-letter queue job", + "parameters": [ { - "LoopOverSessionCookie": [] + "schema": { + "type": "string", + "example": "812" + }, + "required": true, + "description": "Dead-letter job id.", + "name": "id", + "in": "path" } ], - "summary": "Queue a weekly value report job" - } - }, - "/v1/internal/jobs/repair-data-fidelity": { - "post": { "responses": { - "202": { - "description": "Internal job queued" + "200": { + "description": "Job deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } + }, + "400": { + "description": "Invalid job id" }, "401": { - "description": "Invalid internal token" + "description": "Unauthorized" + }, + "403": { + "description": "Insufficient app role (operator only)" + }, + "404": { + "description": "Dead-letter job not found" + }, + "501": { + "description": "This deployment's queue backend does not expose dead-letter admin" } }, "security": [ @@ -18707,18 +18708,34 @@ { "LoopOverSessionCookie": [] } - ], - "summary": "Queue a data fidelity repair job" + ] } }, - "/v1/internal/bounties/import": { - "post": { + "/v1/app/selfhost/queue/dead": { + "delete": { + "summary": "Purge all dead-letter queue jobs", "responses": { "200": { - "description": "Bounty snapshot imported" + "description": "Dead-letter jobs purged", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } }, "401": { - "description": "Invalid internal token" + "description": "Unauthorized" + }, + "403": { + "description": "Insufficient app role (operator only)" + }, + "501": { + "description": "This deployment's queue backend does not expose dead-letter admin" } }, "security": [ @@ -18728,40 +18745,57 @@ { "LoopOverSessionCookie": [] } + ] + }, + "get": { + "summary": "List dead-letter queue jobs", + "parameters": [ + { + "schema": { + "type": "string", + "example": "25" + }, + "required": false, + "description": "Maximum rows to return, clamped from 1 to 100.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "example": "0" + }, + "required": false, + "description": "Pagination offset, floored to 0.", + "name": "offset", + "in": "query" + } ], - "summary": "Import a bounty snapshot" - } - }, - "/v1/auth/github/token": { - "post": { - "summary": "Fetch the current session's live GitHub token (for AMS git operations)", "responses": { "200": { - "description": "The session's GitHub token", + "description": "Paginated dead-letter jobs for the self-host queue backend", "content": { "application/json": { "schema": { "type": "object", - "properties": { - "token": { - "type": "string" - } - }, - "required": [ - "token" - ] + "additionalProperties": { + "nullable": true + } } } } }, - "403": { - "description": "A browser session is required" + "400": { + "description": "Invalid query" }, - "404": { - "description": "No GitHub token is available for this session" + "401": { + "description": "Unauthorized" }, - "429": { - "description": "Rate limited" + "403": { + "description": "Insufficient app role (operator only)" + }, + "501": { + "description": "This deployment's queue backend does not expose dead-letter admin (e.g. Cloudflare)" } }, "security": [ @@ -18774,43 +18808,74 @@ ] } }, - "/v1/repos/{owner}/{repo}/live-gate-thresholds": { + "/v1/app/analytics/weekly-value-report": { "get": { - "summary": "Live self-tuned gate thresholds for AMS probe (#6486)", + "summary": "Weekly value report", "parameters": [ { "schema": { - "type": "string" + "type": "string", + "enum": [ + "public", + "operator" + ], + "example": "public" }, - "required": true, - "name": "owner", - "in": "path" + "required": false, + "description": "Report variant. Operator reports require the operator app role.", + "name": "variant", + "in": "query" }, { "schema": { - "type": "string" + "type": "string", + "example": "7" }, - "required": true, - "name": "repo", - "in": "path" + "required": false, + "description": "Report window in days, clamped from 1 to 31.", + "name": "days", + "in": "query" + }, + { + "schema": { + "type": "string", + "enum": [ + "json", + "markdown" + ], + "example": "markdown" + }, + "required": false, + "description": "Response format. Omit or use json for the structured report; use markdown for copy-ready text.", + "name": "format", + "in": "query" } ], "responses": { "200": { - "description": "Field-limited live (or soaking-shadow) TunableOverride values — confidence_floor / scope_cap_files / scope_cap_lines only", + "description": "Weekly value report as structured JSON or copy-ready Markdown", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LiveGateThresholdsResponse" + "type": "object", + "additionalProperties": { + "nullable": true + } + } + }, + "text/markdown": { + "schema": { + "type": "string", + "example": "# Weekly LoopOver value report\n\n## Adoption metrics\n- Active users: 4\n" } } } }, - "403": { - "description": "Static mcp credential is outside MCP_READ_REPO_ALLOWLIST for this repo" + "401": { + "description": "Unauthorized" }, - "404": { - "description": "No live or shadow gate override is active for this repo" + "403": { + "description": "Insufficient app role for requested report variant" } }, "security": [ @@ -18823,43 +18888,89 @@ ] } }, - "/v1/repos/{owner}/{repo}/gate-config/effective": { + "/v1/app/skipped-pr-audit": { "get": { - "summary": "Current effective self-tuned gate config for a repo (#6247)", + "summary": "Audit of pull requests the review agent skipped", "parameters": [ { "schema": { - "type": "string" + "type": "string", + "example": "50" }, - "required": true, - "name": "owner", - "in": "path" + "required": false, + "description": "Maximum rows to return, clamped from 1 to 100.", + "name": "limit", + "in": "query" + }, + { + "schema": { + "type": "string", + "example": "0" + }, + "required": false, + "description": "Number of parsed skip events to skip before returning rows (non-negative).", + "name": "offset", + "in": "query" + }, + { + "schema": { + "type": "string", + "example": "JSONbored/loopover" + }, + "required": false, + "description": "Optional repository filter. Browser sessions must have control-panel access to this repo.", + "name": "repoFullName", + "in": "query" + }, + { + "schema": { + "type": "string", + "enum": [ + "surface_off", + "missing_author", + "bot_author", + "ignored_author", + "maintainer_author", + "miner_detection_unavailable", + "not_official_gittensor_miner" + ], + "example": "not_official_gittensor_miner" + }, + "required": false, + "description": "Optional PR skip reason filter.", + "name": "reason", + "in": "query" }, { "schema": { - "type": "string" + "type": "string", + "example": "2026-05-30T00:00:00.000Z" }, - "required": true, - "name": "repo", - "in": "path" + "required": false, + "description": "Optional lower timestamp bound.", + "name": "since", + "in": "query" } ], "responses": { "200": { - "description": "Effective TunableOverride values (confidenceFloor / scopeCap.files / scopeCap.lines) with a shadowPending flag — never the raw override_audit history", + "description": "Private bounded audit export for skipped PR public-surface decisions", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GateConfigEffectiveResponse" + "$ref": "#/components/schemas/SkippedPrAuditExport" } } } }, + "400": { + "description": "Invalid query" + }, "401": { - "description": "Missing or invalid static protected API token" + "description": "Unauthorized" }, "403": { - "description": "Static mcp credential is outside MCP_READ_REPO_ALLOWLIST for this repo" + "description": "Insufficient app role or repository scope" } }, "security": [ @@ -18872,19 +18983,31 @@ ] } }, - "/v1/finding-taxonomy": { - "get": { - "summary": "Canonical AI-review finding taxonomy", + "/v1/app/commands/preview": { + "post": { + "summary": "Dry-run a sanitized @loopover command response", "responses": { "200": { - "description": "Finding categories and the severity ladder", + "description": "Maintainer dry-run preview of a sanitized @loopover command response (no GitHub mutation)", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FindingTaxonomyDocument" + "$ref": "#/components/schemas/CommandPreviewResponse" } } } + }, + "400": { + "description": "Invalid request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Insufficient app role" + }, + "404": { + "description": "Command not found" } }, "security": [ @@ -18897,19 +19020,41 @@ ] } }, - "/v1/enrichment-analyzers": { - "get": { - "summary": "REES enrichment analyzer taxonomy", + "/v1/app/commands/feedback": { + "post": { + "summary": "Submit feedback on an @loopover command response", "responses": { "200": { - "description": "Default profile and the registered enrichment analyzers", + "description": "Live app mutation or preview response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EnrichmentAnalyzersTaxonomyDocument" + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } } } } + }, + "400": { + "description": "Invalid request" + }, + "401": { + "description": "Unauthorized" } }, "security": [ @@ -18922,37 +19067,41 @@ ] } }, - "/v1/repos/{owner}/{repo}/automation-state": { - "get": { - "summary": "Derived agent automation state for a repository", - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "owner", - "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "repo", - "in": "path" - } - ], + "/v1/app/digest/subscriptions": { + "post": { + "summary": "Manage maintainer digest subscriptions", "responses": { "200": { - "description": "Maintainer-only derived automation view (mode, permission readiness, acting action classes, pending-approval count) that the raw /settings row does not include", + "description": "Live app mutation or preview response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AutomationState" + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } } } } + }, + "400": { + "description": "Invalid request" + }, + "401": { + "description": "Unauthorized" } }, "security": [ @@ -18965,37 +19114,36 @@ ] } }, - "/v1/repos/{owner}/{repo}/repo-docs/refresh": { + "/v1/internal/jobs/refresh-registry": { "post": { - "summary": "Open (or find the already-open) AGENTS.md/CLAUDE.md generation pull request", - "parameters": [ + "summary": "Queue a registry refresh job", + "responses": { + "202": { + "description": "Registry refresh queued" + }, + "401": { + "description": "Invalid internal token" + } + }, + "security": [ { - "schema": { - "type": "string" - }, - "required": true, - "name": "owner", - "in": "path" + "LoopOverBearer": [] }, { - "schema": { - "type": "string" - }, - "required": true, - "name": "repo", - "in": "path" + "LoopOverSessionCookie": [] } - ], + ] + } + }, + "/v1/internal/jobs/backfill-registered-repos": { + "post": { + "summary": "Queue a registered-repository backfill job", "responses": { - "200": { - "description": "The repo-doc pull request result -- opened (new or reused) or a reason it was not opened", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RepoDocRefreshResult" - } - } - } + "202": { + "description": "Registered repo backfill queued" + }, + "401": { + "description": "Invalid internal token" } }, "security": [ @@ -19008,40 +19156,42 @@ ] } }, - "/v1/contributors/{login}/pr-outcomes": { - "get": { - "summary": "Contributor post-merge PR outcome history", - "parameters": [ + "/v1/internal/jobs/backfill-repo-segment": { + "post": { + "summary": "Queue a repository segment backfill job", + "responses": { + "202": { + "description": "Repository segment backfill queued" + }, + "400": { + "description": "Invalid segment request" + }, + "401": { + "description": "Invalid internal token" + } + }, + "security": [ { - "schema": { - "type": "string" - }, - "required": true, - "name": "login", - "in": "path" + "LoopOverBearer": [] }, { - "schema": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true, - "maximum": 100 - }, - "required": false, - "name": "limit", - "in": "query" + "LoopOverSessionCookie": [] } - ], + ] + } + }, + "/v1/internal/jobs/backfill-pr-details": { + "post": { + "summary": "Queue an open pull request detail backfill job", "responses": { - "200": { - "description": "Self-scoped post-merge outcome records with public-safe attribution (mirrors loopover_pr_outcome).", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ContributorPrOutcomes" - } - } - } + "202": { + "description": "Open PR detail backfill queued" + }, + "400": { + "description": "Invalid PR detail backfill request" + }, + "401": { + "description": "Invalid internal token" } }, "security": [ @@ -19054,29 +19204,18 @@ ] } }, - "/v1/contributors/{login}/notifications": { - "get": { - "summary": "Contributor badge notification feed", - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "login", - "in": "path" - } - ], + "/v1/internal/jobs/generate-review-recap": { + "post": { + "summary": "Queue a maintainer review recap digest job", "responses": { - "200": { - "description": "The contributor's own badge notification feed (self-scoped), newest first, with an unread count.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotificationFeed" - } - } - } + "202": { + "description": "Maintainer review recap digest queued (#1963)" + }, + "400": { + "description": "Missing repoFullName" + }, + "401": { + "description": "Invalid internal token" } }, "security": [ @@ -19089,49 +19228,15 @@ ] } }, - "/v1/contributors/{login}/notifications/read": { + "/v1/internal/jobs/refresh-scoring-model": { "post": { - "summary": "Mark contributor notifications read", - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "login", - "in": "path" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "ids": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - }, + "summary": "Queue a scoring model refresh job", "responses": { - "200": { - "description": "Marks the contributor's delivered badge notifications read; an absent/empty ids array marks all.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotificationsMarked" - } - } - } + "202": { + "description": "Internal job queued" }, - "400": { - "description": "Invalid mark-read body" + "401": { + "description": "Invalid internal token" } }, "security": [ @@ -19144,25 +19249,15 @@ ] } }, - "/v1/preflight/review-risk": { + "/v1/internal/jobs/refresh-upstream-drift": { "post": { - "summary": "Explain review risk for a planned pull request", + "summary": "Queue an upstream drift refresh job", "responses": { - "200": { - "description": "Review-risk explanation with preflight, role context, and recommendation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReviewRiskExplanation" - } - } - } - }, - "400": { - "description": "Invalid preflight input" + "202": { + "description": "Internal job queued" }, - "403": { - "description": "Forbidden when contributorLogin does not match the authenticated session" + "401": { + "description": "Invalid internal token" } }, "security": [ @@ -19175,46 +19270,15 @@ ] } }, - "/v1/repos/{owner}/{repo}/issue-plan-drafts/generate": { + "/v1/internal/jobs/file-upstream-drift-issues": { "post": { - "summary": "AI-plan repo issue drafts from a maintainer goal", - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "owner", - "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "repo", - "in": "path" - } - ], + "summary": "Queue a job that files upstream drift issues", "responses": { - "200": { - "description": "AI-plan a small set of GitHub issue drafts from a maintainer-supplied planning goal (dry-run by default)", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "nullable": true - } - } - } - } - }, - "400": { - "description": "Invalid request or explicit create without dryRun false" + "202": { + "description": "Internal job queued" }, - "403": { - "description": "Insufficient role" + "401": { + "description": "Invalid internal token" } }, "security": [ @@ -19227,15 +19291,15 @@ ] } }, - "/v1/public/decision-ledger/verify": { - "get": { - "summary": "Verify a window of the hash-chained decision ledger (resumable via afterSeq)", + "/v1/internal/jobs/build-contributor-evidence": { + "post": { + "summary": "Queue a contributor evidence build job", "responses": { - "200": { - "description": "Window verified clean; nextAfterSeq is the resume cursor (null at the tip). Every response also carries tipSeq/tipHash/totalCount for third-party checkpointing." + "202": { + "description": "Internal job queued" }, - "409": { - "description": "First break found: sequence_gap | predecessor_mismatch | row_hash_mismatch | short_tail (a record exists past the verified tip with no chain entry)" + "401": { + "description": "Invalid internal token" } }, "security": [ @@ -19248,18 +19312,15 @@ ] } }, - "/v1/public/decision-records/{owner}/{repo}/{pull}": { - "get": { - "summary": "Fetch the latest published decision record for a PR, verbatim, plus its content digest", + "/v1/internal/jobs/build-contributor-decision-packs": { + "post": { + "summary": "Queue a contributor decision pack build job", "responses": { - "200": { - "description": "The latest DecisionRecord for this PR + its recordDigest" - }, - "400": { - "description": "Invalid pull number" + "202": { + "description": "Internal job queued" }, - "404": { - "description": "No decision record persisted yet for this PR" + "401": { + "description": "Invalid internal token" } }, "security": [ @@ -19269,62 +19330,60 @@ { "LoopOverSessionCookie": [] } - ], - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "owner", - "in": "path" + ] + } + }, + "/v1/internal/jobs/build-burden-forecasts": { + "post": { + "summary": "Queue a burden forecast build job", + "responses": { + "202": { + "description": "Internal job queued" }, + "401": { + "description": "Invalid internal token" + } + }, + "security": [ { - "schema": { - "type": "string" - }, - "required": true, - "name": "repo", - "in": "path" + "LoopOverBearer": [] }, { - "schema": { - "type": "string" - }, - "required": true, - "name": "pull", - "in": "path" + "LoopOverSessionCookie": [] } ] } }, - "/v1/public/eval-scores": { - "get": { - "summary": "Fetch EvalScoreRecords (#9215) -- the objective-eval-provider transport, digest-committed and independently re-derivable", - "parameters": [ + "/v1/internal/jobs/generate-signal-snapshots": { + "post": { + "summary": "Queue a signal snapshot generation job", + "responses": { + "202": { + "description": "Internal job queued" + }, + "401": { + "description": "Invalid internal token" + } + }, + "security": [ { - "schema": { - "type": "string" - }, - "required": false, - "name": "subject", - "in": "query" + "LoopOverBearer": [] }, { - "schema": { - "type": "string" - }, - "required": false, - "name": "since", - "in": "query" + "LoopOverSessionCookie": [] } - ], + ] + } + }, + "/v1/internal/jobs/generate-weekly-value-report": { + "post": { + "summary": "Queue a weekly value report job", "responses": { - "200": { - "description": "{ records: EvalScoreRecord[] }, optionally filtered by subject id and/or minimum issuedAt -- degrades to an empty array on an internal read error rather than a non-200 status, matching loadPublicRulePrecision's own fail-safe contract" + "202": { + "description": "Internal job queued" }, - "404": { - "description": "Public stats disabled (same flag as /v1/public/stats)" + "401": { + "description": "Invalid internal token" } }, "security": [ @@ -19337,28 +19396,15 @@ ] } }, - "/v1/public/decision-ledger/row/{seq}": { - "get": { - "summary": "Fetch one decision-ledger row by seq, so an external anchor can be bound back to the live chain", - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "seq", - "in": "path" - } - ], + "/v1/internal/jobs/repair-data-fidelity": { + "post": { + "summary": "Queue a data fidelity repair job", "responses": { - "200": { - "description": "The chain row: seq, recordId, recordDigest, prevHash, rowHash, createdAt. Recompute sha256(prevHash || canonicalJson({seq, recordId, recordDigest, createdAt})) and compare against an anchored rowHash." - }, - "400": { - "description": "seq is not a positive integer" + "202": { + "description": "Internal job queued" }, - "404": { - "description": "No ledger row at that seq (never appended -- distinct from a row with empty fields)" + "401": { + "description": "Invalid internal token" } }, "security": [ @@ -19371,12 +19417,15 @@ ] } }, - "/v1/public/decision-ledger/anchor-key": { - "get": { - "summary": "Published anchor-signing public keys with their full rotation history, for verifying an externally-published ledger anchor", + "/v1/internal/bounties/import": { + "post": { + "summary": "Import a bounty snapshot", "responses": { "200": { - "description": "{ keys: [{ keyId, publicKeySpki, notBefore, notAfter }], currentKeyId } — retired keys are retained so anchors signed under them stay verifiable; currentKeyId is null when unconfigured or the rotation state is ambiguous" + "description": "Bounty snapshot imported" + }, + "401": { + "description": "Invalid internal token" } }, "security": [ diff --git a/migrations/0195_decision_ledger_anchors.sql b/migrations/0195_decision_ledger_anchors.sql new file mode 100644 index 0000000000..f7d76702bc --- /dev/null +++ b/migrations/0195_decision_ledger_anchors.sql @@ -0,0 +1,36 @@ +-- #9271 (epic #9267): persistence for external decision-ledger anchoring attempts, success AND failure both. +-- +-- migrations/0180_decision_ledger.sql named its own honest limit up front: a self-operated chain is +-- tamper-EVIDENT, not tamper-PROOF. Anchoring (#9270 for the signed payload; #9272/#9273 for the Rekor and +-- git-commit backends) closes that by publishing a periodic checkpoint somewhere the operator does not +-- control. But per the mechanism research on #9267: if anchoring can fail SILENTLY (best-effort, so a failure +-- must never block a review), an operator could make every attempt "fail" and quietly regress the ledger back +-- to tamper-evident-only with no visible signal. Recording every attempt -- success or failure -- as a public +-- row is what closes that: "anchoring has been failing for a week" becomes a fact anyone can observe, not +-- something only the operator's own logs show. +CREATE TABLE IF NOT EXISTS decision_ledger_anchors ( + id TEXT PRIMARY KEY, + seq INTEGER NOT NULL, -- the ledger seq this anchor commits to (decision_ledger.seq) + row_hash TEXT NOT NULL, -- the anchored decision_ledger.row_hash at seq + payload_json TEXT NOT NULL, -- canonical-JSON'd LedgerAnchorPayload (#9270) -- the exact signed bytes + signature TEXT NOT NULL, -- base64 ECDSA signature over payload_json (#9270) + key_id TEXT NOT NULL, -- which published anchor-signing key signed this attempt + -- 'ots' (OpenTimestamps) is a named, tracked-but-not-yet-built backend per #9267's research -- included in + -- the CHECK now so a future implementation issue does not need its own migration just to widen this list. + backend TEXT NOT NULL CHECK (backend IN ('rekor', 'git', 'ots')), + -- Backend-specific resolvable reference, JSON. For Rekor: {shardBaseUrl, logIndex, logIdKeyId, uuid} -- + -- deliberately the FULL reference, not a bare logIndex, since Rekor's annual shard rotation makes a bare + -- index unresolvable later. For git: {repo, sha}. NULL on a failed attempt (there is no reference yet). + backend_ref TEXT, + -- R2 key for the full stored proof (Rekor's TransparencyLogEntry -- inclusion proof + signed checkpoint -- + -- needed for fully OFFLINE verification later, without trusting Rekor's continued availability). NULL on + -- a failed attempt, and NULL for backends with no separate proof blob to store. + proof_r2_key TEXT, + status TEXT NOT NULL CHECK (status IN ('ok', 'failed')), + error TEXT, -- populated on status='failed', NULL on 'ok' + created_at TEXT NOT NULL +); +-- The public listing (`GET /v1/public/decision-ledger/anchors`) paginates newest-first per backend and +-- overall; this index serves both without a table scan as the table grows. +CREATE INDEX IF NOT EXISTS decision_ledger_anchors_created_at ON decision_ledger_anchors (created_at DESC); +CREATE INDEX IF NOT EXISTS decision_ledger_anchors_backend_created_at ON decision_ledger_anchors (backend, created_at DESC); diff --git a/src/api/routes.ts b/src/api/routes.ts index f644e893a7..3dd40bbdec 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -313,6 +313,7 @@ import { isRagEnabled } from "../review/rag-wire"; import { loadPublicDecisionRecord, loadPublicLedgerRow, verifyDecisionLedger } from "../review/decision-record"; import { buildEvalScoreRecordsFromRulePrecision, filterEvalScoreRecords } from "../review/eval-score-records"; import { currentAnchorKey, parseAnchorPublicKeys } from "../review/ledger-anchor"; +import { loadPublicLedgerAnchors } from "../review/ledger-anchor-persistence"; import { getPublicStats, isPublicStatsEnabled, resolvePublicStatsManifestOverride } from "../review/public-stats"; import { loadPublicAccuracyTrend } from "../services/public-accuracy-trend"; import { loadPublicRulePrecision } from "../review/public-rule-precision"; @@ -1319,6 +1320,27 @@ export function createApp() { return c.json({ keys, currentKeyId: currentAnchorKey(keys)?.keyId ?? null }); }); + // #9271 (epic #9267): every anchoring attempt, success AND failure, paginated newest-first. This is what + // makes anchoring's own health a publicly checkable fact -- a failure is recorded and served exactly like a + // success (same shape, same listing), so "anchoring has been failing for a week" is something anyone can + // observe rather than only visible in the operator's own logs. Unauthenticated by design, same posture as + // every /v1/public/* sibling above. + app.get("/v1/public/decision-ledger/anchors", async (c) => { + // Built with spreads, not literal undefined-valued keys: exactOptionalPropertyTypes means an optional + // filter field must be OMITTED to mean "no filter", not present-with-value-undefined. + const backendParam = c.req.query("backend"); + const backend = backendParam === "rekor" || backendParam === "git" || backendParam === "ots" ? backendParam : undefined; + const before = c.req.query("before"); + const limit = Number(c.req.query("limit")) || undefined; + const result = await loadPublicLedgerAnchors(c.env, { + ...(backend !== undefined && { backend }), + ...(before !== undefined && { before }), + ...(limit !== undefined && { limit }), + }); + c.header("Cache-Control", "public, max-age=60, stale-while-revalidate=300"); + return c.json(result); + }); + // #9123: the decision record itself was persisted (decision_records) but never published anywhere a // contributor or a third party could fetch the full body — the only prior public surface was // renderDecisionRecordSection's bounded review-comment summary (12-char digest prefixes, and it omits @@ -6809,6 +6831,8 @@ function requiresApiToken(path: string): boolean { // #9270: the published anchor-signing public keys, added in the SAME PR as its route so the two cannot // drift the way #9120's sibling did. if (path === "/v1/public/decision-ledger/anchor-key") return false; + // #9271: the public anchor-attempt listing, added in the SAME PR as its route. + if (path === "/v1/public/decision-ledger/anchors") return false; // #9123: the new public decision-record read route — same unauthenticated posture as its ledger-verify // sibling immediately above, added in the SAME PR so the two can never drift apart the way #9120 did. The // pull segment matches any non-slash text (not just digits): an invalid pull number is the ROUTE's 400 to diff --git a/src/openapi/spec.ts b/src/openapi/spec.ts index 4fc08e3af0..67f085e8c2 100644 --- a/src/openapi/spec.ts +++ b/src/openapi/spec.ts @@ -1024,6 +1024,15 @@ export function buildOpenApiSpec() { 200: { description: "{ keys: [{ keyId, publicKeySpki, notBefore, notAfter }], currentKeyId } — retired keys are retained so anchors signed under them stay verifiable; currentKeyId is null when unconfigured or the rotation state is ambiguous" }, }, }); + registry.registerPath({ + method: "get", + path: "/v1/public/decision-ledger/anchors", + summary: "Every external anchoring attempt, success and failure, paginated newest-first — anchoring's own health as a public fact", + request: { query: z.object({ backend: z.enum(["rekor", "git", "ots"]).optional(), before: z.string().optional(), limit: z.string().optional() }) }, + responses: { + 200: { description: "{ anchors: [{ id, seq, rowHash, keyId, backend, backendRef, status, error, createdAt }], nextBefore } — a failed attempt is returned identically to a successful one, never filtered out or reshaped" }, + }, + }); registry.registerPath({ method: "get", path: "/v1/public/decision-records/{owner}/{repo}/{pull}", diff --git a/src/review/ledger-anchor-persistence.ts b/src/review/ledger-anchor-persistence.ts new file mode 100644 index 0000000000..a21820253c --- /dev/null +++ b/src/review/ledger-anchor-persistence.ts @@ -0,0 +1,147 @@ +// Persistence for external decision-ledger anchoring attempts (#9271, epic #9267; migrations/0195). Every +// backend (#9272 Rekor, #9273 git-commit, and any future one) calls ONE recording function whether it +// succeeded or failed -- there is no separate "failure log" a backend could simply not call. That is the +// whole point: per the mechanism research on #9267, an operator whose anchoring silently fails could quietly +// regress the ledger back to tamper-evident-only with no visible signal. A failure recorded exactly like a +// success, on the same public listing, is what makes anchoring's own health itself a publicly checkable fact. +import { nowIso, errorMessage } from "../utils/json"; +import type { LedgerAnchorPayload } from "./ledger-anchor"; + +export type LedgerAnchorBackend = "rekor" | "git" | "ots"; + +/** What a backend passes in to record ONE attempt -- success or failure, same shape either way. */ +export type LedgerAnchorAttemptInput = { + payload: LedgerAnchorPayload; + signature: string; + keyId: string; + backend: LedgerAnchorBackend; +} & ( + | { status: "ok"; backendRef: unknown; proofR2Key: string | null } + | { status: "failed"; error: unknown } +); + +/** One row exactly as it will be served publicly -- deliberately the SAME shape whether the attempt + * succeeded or failed, so a listing consumer cannot special-case failures into a different, hideable form. */ +export type PublicLedgerAnchor = { + id: string; + seq: number; + rowHash: string; + keyId: string; + backend: LedgerAnchorBackend; + backendRef: unknown; + status: "ok" | "failed"; + error: string | null; + createdAt: string; +}; + +/** + * Record one anchoring attempt. Never throws for the caller's OWN backend failure (that IS what `status: + * "failed"` records) -- only a genuine local persistence error propagates, matching `appendDecisionLedger`'s + * posture of letting a storage-layer failure be its own distinct problem rather than silently swallowing it + * into "the anchor attempt failed" too. + * + * `createdAt` is injectable (defaults to `nowIso()`), matching `loadPublicRulePrecision`'s own + * injectable-clock pattern -- this is NOT `payload.at` (the anchored tip's own captured timestamp, a + * property of the CHAIN) but "when this attempt was recorded" (a property of THIS row), and the two are + * naturally different values. Making it injectable is what lets a test control ordering deterministically + * instead of racing real wall-clock resolution across several inserts in a tight loop. + */ +export async function recordLedgerAnchorAttempt(env: Env, attempt: LedgerAnchorAttemptInput, createdAt: string = nowIso()): Promise { + const id = crypto.randomUUID(); + const payloadJson = JSON.stringify(attempt.payload); + const backendRef = attempt.status === "ok" ? JSON.stringify(attempt.backendRef) : null; + const proofR2Key = attempt.status === "ok" ? attempt.proofR2Key : null; + const error = attempt.status === "failed" ? errorMessage(attempt.error).slice(0, 500) : null; + + await env.DB.prepare( + `INSERT INTO decision_ledger_anchors + (id, seq, row_hash, payload_json, signature, key_id, backend, backend_ref, proof_r2_key, status, error, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind( + id, + attempt.payload.seq, + attempt.payload.rowHash, + payloadJson, + attempt.signature, + attempt.keyId, + attempt.backend, + backendRef, + proofR2Key, + attempt.status, + error, + createdAt, + ) + .run(); +} + +export type LedgerAnchorListFilter = { + backend?: LedgerAnchorBackend; + /** Cursor: return rows strictly older than this ISO timestamp. Omit for the first (newest) page. */ + before?: string; + limit?: number; +}; + +const DEFAULT_ANCHOR_LIST_LIMIT = 50; +const MAX_ANCHOR_LIST_LIMIT = 200; + +/** + * Public, paginated listing -- newest first, success and failure rows returned identically (no filtering out + * failures, no separate shape). `backendRef`/`error` come back parsed/typed exactly as `PublicLedgerAnchor` + * declares; a row with unparseable `backend_ref` JSON (never written by `recordLedgerAnchorAttempt` itself, + * but defensive against any future direct write) degrades to `null` rather than throwing a public endpoint. + */ +export async function loadPublicLedgerAnchors(env: Env, filter: LedgerAnchorListFilter = {}): Promise<{ anchors: PublicLedgerAnchor[]; nextBefore: string | null }> { + const limit = Math.max(1, Math.min(MAX_ANCHOR_LIST_LIMIT, filter.limit ?? DEFAULT_ANCHOR_LIST_LIMIT)); + const conditions: string[] = []; + const binds: unknown[] = []; + if (filter.backend) { + conditions.push("backend = ?"); + binds.push(filter.backend); + } + if (filter.before) { + conditions.push("created_at < ?"); + binds.push(filter.before); + } + const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + // Fetch one extra row to know whether a next page exists, without a separate COUNT query. + const rows = await env.DB.prepare( + `SELECT id, seq, row_hash AS rowHash, key_id AS keyId, backend, backend_ref AS backendRef, status, error, created_at AS createdAt + FROM decision_ledger_anchors ${where} + ORDER BY created_at DESC, id DESC + LIMIT ?`, + ) + .bind(...binds, limit + 1) + .all<{ id: string; seq: number; rowHash: string; keyId: string; backend: LedgerAnchorBackend; backendRef: string | null; status: "ok" | "failed"; error: string | null; createdAt: string }>(); + + /* v8 ignore next -- defensive: D1's .all() always returns a results array (even [] for zero rows); the ?? + * guards a driver-shape change, mirroring loadPublicRulePrecision's identical note on COUNT(*). */ + const results = rows.results ?? []; + const page = results.slice(0, limit); + // `> limit` guarantees `page` has exactly `limit` (>=1) elements, so `page[page.length - 1]` is always + /* v8 ignore next -- defined; the ?. only guards TypeScript's array-index type, not a reachable runtime case. */ + const nextBefore = results.length > limit ? (page[page.length - 1]?.createdAt ?? null) : null; + + const anchors: PublicLedgerAnchor[] = page.map((row) => ({ + id: row.id, + seq: row.seq, + rowHash: row.rowHash, + keyId: row.keyId, + backend: row.backend, + backendRef: parseBackendRef(row.backendRef), + status: row.status, + error: row.error, + createdAt: row.createdAt, + })); + + return { anchors, nextBefore }; +} + +function parseBackendRef(raw: string | null): unknown { + if (raw === null) return null; + try { + return JSON.parse(raw); + } catch { + return null; + } +} diff --git a/test/integration/public-decision-ledger-routes.test.ts b/test/integration/public-decision-ledger-routes.test.ts index 3e7b199dcc..82e110cb79 100644 --- a/test/integration/public-decision-ledger-routes.test.ts +++ b/test/integration/public-decision-ledger-routes.test.ts @@ -77,6 +77,7 @@ describe("public decision-ledger/decision-records routes answer WITHOUT credenti ["decision-ledger/verify", "/v1/public/decision-ledger/verify"], ["decision-ledger/row/:seq", "/v1/public/decision-ledger/row/1"], ["decision-ledger/anchor-key", "/v1/public/decision-ledger/anchor-key"], + ["decision-ledger/anchors", "/v1/public/decision-ledger/anchors"], ["decision-records/:owner/:repo/:pull", "/v1/public/decision-records/acme/widgets/1"], ["github/repos/:owner/:repo/stats", "/v1/public/github/repos/acme/widgets/stats"], ["repos/:owner/:repo/badge.svg", "/v1/public/repos/acme/widgets/badge.svg"], diff --git a/test/integration/public-ledger-anchors-route.test.ts b/test/integration/public-ledger-anchors-route.test.ts new file mode 100644 index 0000000000..8936e1db6e --- /dev/null +++ b/test/integration/public-ledger-anchors-route.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "vitest"; +import { createApp } from "../../src/api/routes"; +import { createTestEnv } from "../helpers/d1"; +import { recordLedgerAnchorAttempt } from "../../src/review/ledger-anchor-persistence"; +import { buildLedgerAnchorPayload } from "../../src/review/ledger-anchor"; + +// #9271 (epic #9267). The load-bearing behaviour: a failed attempt is served on this public listing exactly +// like a success, since that's the entire point of recording failures at all. + +describe("GET /v1/public/decision-ledger/anchors (#9271)", () => { + it("answers 200 with no Authorization header", async () => { + const env = createTestEnv(); + const response = await createApp().request("/v1/public/decision-ledger/anchors", {}, env); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ anchors: [], nextBefore: null }); + }); + + it("serves a FAILED anchor attempt on the public listing, identically shaped to a success", async () => { + const env = createTestEnv(); + await recordLedgerAnchorAttempt( + env, + { + payload: buildLedgerAnchorPayload({ seq: 7, rowHash: "e".repeat(64), totalCount: 7 }, "2026-07-27T12:00:00.000Z"), + signature: "c2ln", + keyId: "key1", + backend: "git", + status: "failed", + error: new Error("rate limited by the git remote"), + }, + "2026-07-27T12:00:00.000Z", + ); + + const response = await createApp().request("/v1/public/decision-ledger/anchors", {}, env); + const body = (await response.json()) as { anchors: Array>; nextBefore: string | null }; + expect(body.anchors).toHaveLength(1); + expect(body.anchors[0]).toMatchObject({ seq: 7, backend: "git", status: "failed", error: "rate limited by the git remote" }); + }); + + it("filters by ?backend=", async () => { + const env = createTestEnv(); + await recordLedgerAnchorAttempt( + env, + { payload: buildLedgerAnchorPayload({ seq: 1, rowHash: "a".repeat(64), totalCount: 1 }, "2026-07-27T12:00:00.000Z"), signature: "s", keyId: "k", backend: "rekor", status: "ok", backendRef: {}, proofR2Key: null }, + "2026-07-27T12:00:00.000Z", + ); + await recordLedgerAnchorAttempt( + env, + { payload: buildLedgerAnchorPayload({ seq: 2, rowHash: "b".repeat(64), totalCount: 2 }, "2026-07-27T12:01:00.000Z"), signature: "s", keyId: "k", backend: "git", status: "ok", backendRef: {}, proofR2Key: null }, + "2026-07-27T12:01:00.000Z", + ); + + const response = await createApp().request("/v1/public/decision-ledger/anchors?backend=rekor", {}, env); + const body = (await response.json()) as { anchors: Array<{ backend: string }> }; + expect(body.anchors.map((a) => a.backend)).toEqual(["rekor"]); + }); + + it("ignores an invalid ?backend= value rather than erroring (falls back to unfiltered)", async () => { + const env = createTestEnv(); + await recordLedgerAnchorAttempt( + env, + { payload: buildLedgerAnchorPayload({ seq: 1, rowHash: "a".repeat(64), totalCount: 1 }, "2026-07-27T12:00:00.000Z"), signature: "s", keyId: "k", backend: "rekor", status: "ok", backendRef: {}, proofR2Key: null }, + "2026-07-27T12:00:00.000Z", + ); + const response = await createApp().request("/v1/public/decision-ledger/anchors?backend=nonsense", {}, env); + expect(response.status).toBe(200); + const body = (await response.json()) as { anchors: unknown[] }; + expect(body.anchors).toHaveLength(1); + }); + + it("supports pagination via ?limit= and ?before=", async () => { + const env = createTestEnv(); + for (let i = 1; i <= 3; i += 1) { + await recordLedgerAnchorAttempt( + env, + { payload: buildLedgerAnchorPayload({ seq: i, rowHash: `${i}`.repeat(64).slice(0, 64), totalCount: i }, `2026-07-27T12:0${i}:00.000Z`), signature: "s", keyId: "k", backend: "rekor", status: "ok", backendRef: {}, proofR2Key: null }, + `2026-07-27T12:0${i}:00.000Z`, + ); + } + const first = await createApp().request("/v1/public/decision-ledger/anchors?limit=2", {}, env); + const firstBody = (await first.json()) as { anchors: Array<{ seq: number }>; nextBefore: string }; + expect(firstBody.anchors.map((a) => a.seq)).toEqual([3, 2]); + + const second = await createApp().request(`/v1/public/decision-ledger/anchors?limit=2&before=${encodeURIComponent(firstBody.nextBefore)}`, {}, env); + const secondBody = (await second.json()) as { anchors: Array<{ seq: number }>; nextBefore: string | null }; + expect(secondBody.anchors.map((a) => a.seq)).toEqual([1]); + expect(secondBody.nextBefore).toBeNull(); + }); + + it("sets the same Cache-Control posture as its public siblings", async () => { + const response = await createApp().request("/v1/public/decision-ledger/anchors", {}, createTestEnv()); + expect(response.headers.get("Cache-Control")).toBe("public, max-age=60, stale-while-revalidate=300"); + }); +}); diff --git a/test/unit/ledger-anchor-persistence.test.ts b/test/unit/ledger-anchor-persistence.test.ts new file mode 100644 index 0000000000..2981bac9e9 --- /dev/null +++ b/test/unit/ledger-anchor-persistence.test.ts @@ -0,0 +1,131 @@ +import { describe, expect, it } from "vitest"; +import { createTestEnv } from "../helpers/d1"; +import { loadPublicLedgerAnchors, recordLedgerAnchorAttempt, type LedgerAnchorAttemptInput } from "../../src/review/ledger-anchor-persistence"; +import { buildLedgerAnchorPayload } from "../../src/review/ledger-anchor"; + +// #9271 (epic #9267). The load-bearing property here is that a FAILURE is recorded and served exactly like a +// success -- no special-casing that could hide it, per the mechanism research on #9267. + +function okAttempt(overrides: Partial = {}): LedgerAnchorAttemptInput { + return { + payload: buildLedgerAnchorPayload({ seq: 1, rowHash: "a".repeat(64), totalCount: 1 }, "2026-07-27T12:00:00.000Z"), + signature: "c2ln", + keyId: "key1", + backend: "rekor", + status: "ok", + backendRef: { shardBaseUrl: "https://log2026-1.rekor.sigstore.dev", logIndex: 42, logIdKeyId: "kid", uuid: "uuid-1" }, + proofR2Key: "anchors/rekor/1.json", + ...overrides, + }; +} + +function failedAttempt(overrides: Partial = {}): LedgerAnchorAttemptInput { + return { + payload: buildLedgerAnchorPayload({ seq: 2, rowHash: "b".repeat(64), totalCount: 2 }, "2026-07-27T12:05:00.000Z"), + signature: "c2ln", + keyId: "key1", + backend: "git", + status: "failed", + error: new Error("rate limited"), + ...overrides, + }; +} + +describe("recordLedgerAnchorAttempt / loadPublicLedgerAnchors (#9271)", () => { + it("records a successful attempt with its backend reference and proof key", async () => { + const env = createTestEnv(); + await recordLedgerAnchorAttempt(env, okAttempt()); + + const { anchors } = await loadPublicLedgerAnchors(env); + expect(anchors).toHaveLength(1); + expect(anchors[0]).toMatchObject({ + seq: 1, + rowHash: "a".repeat(64), + keyId: "key1", + backend: "rekor", + status: "ok", + error: null, + backendRef: { shardBaseUrl: "https://log2026-1.rekor.sigstore.dev", logIndex: 42, logIdKeyId: "kid", uuid: "uuid-1" }, + }); + }); + + it("records a FAILED attempt with the SAME shape as success — not filtered out, not hidden", async () => { + const env = createTestEnv(); + await recordLedgerAnchorAttempt(env, failedAttempt()); + + const { anchors } = await loadPublicLedgerAnchors(env); + expect(anchors).toHaveLength(1); + expect(anchors[0]).toMatchObject({ seq: 2, backend: "git", status: "failed", error: "rate limited", backendRef: null }); + // Same field set on a failure as on a success — no special-cased shape that could hide it. + expect(Object.keys(anchors[0]!).sort()).toEqual(["backend", "backendRef", "createdAt", "error", "id", "keyId", "rowHash", "seq", "status"]); + }); + + it("a failed attempt is queryable identically to a success — filtering by backend returns both statuses", async () => { + const env = createTestEnv(); + await recordLedgerAnchorAttempt(env, okAttempt({ backend: "git", payload: buildLedgerAnchorPayload({ seq: 3, rowHash: "c".repeat(64), totalCount: 3 }, "2026-07-27T12:10:00.000Z") })); + await recordLedgerAnchorAttempt(env, failedAttempt({ backend: "git" })); + + const { anchors } = await loadPublicLedgerAnchors(env, { backend: "git" }); + expect(anchors.map((a) => a.status).sort()).toEqual(["failed", "ok"]); + }); + + it("filters by backend", async () => { + const env = createTestEnv(); + await recordLedgerAnchorAttempt(env, okAttempt({ backend: "rekor" })); + await recordLedgerAnchorAttempt(env, failedAttempt({ backend: "git" })); + + expect((await loadPublicLedgerAnchors(env, { backend: "rekor" })).anchors).toHaveLength(1); + expect((await loadPublicLedgerAnchors(env, { backend: "ots" })).anchors).toHaveLength(0); + }); + + it("paginates newest-first with a nextBefore cursor, and the cursor actually advances the page", async () => { + const env = createTestEnv(); + // createdAt (the row's OWN recorded time) is passed explicitly and distinctly per row -- several inserts + // in a tight loop would otherwise race real wall-clock resolution and could land on the same millisecond. + for (let i = 1; i <= 5; i += 1) { + await recordLedgerAnchorAttempt( + env, + okAttempt({ payload: buildLedgerAnchorPayload({ seq: i, rowHash: `${i}`.repeat(64).slice(0, 64), totalCount: i }, `2026-07-27T12:0${i}:00.000Z`) }), + `2026-07-27T12:0${i}:00.000Z`, + ); + } + + const first = await loadPublicLedgerAnchors(env, { limit: 2 }); + expect(first.anchors.map((a) => a.seq)).toEqual([5, 4]); // newest first + expect(first.nextBefore).not.toBeNull(); + + const second = await loadPublicLedgerAnchors(env, { limit: 2, before: first.nextBefore! }); + expect(second.anchors.map((a) => a.seq)).toEqual([3, 2]); + expect(second.nextBefore).not.toBeNull(); + + const third = await loadPublicLedgerAnchors(env, { limit: 2, before: second.nextBefore! }); + expect(third.anchors.map((a) => a.seq)).toEqual([1]); + expect(third.nextBefore).toBeNull(); // no more pages + }); + + it("clamps limit to [1, 200] rather than trusting caller input", async () => { + const env = createTestEnv(); + await recordLedgerAnchorAttempt(env, okAttempt()); + expect((await loadPublicLedgerAnchors(env, { limit: 0 })).anchors).toHaveLength(1); // clamped up to 1 + expect((await loadPublicLedgerAnchors(env, { limit: -5 })).anchors).toHaveLength(1); + }); + + it("degrades an unparseable backendRef to null rather than throwing a public endpoint", async () => { + const env = createTestEnv(); + await env.DB.prepare( + `INSERT INTO decision_ledger_anchors (id, seq, row_hash, payload_json, signature, key_id, backend, backend_ref, proof_r2_key, status, error, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .bind("manual-1", 9, "d".repeat(64), "{}", "sig", "key1", "rekor", "{not valid json", null, "ok", null, "2026-07-27T12:00:00.000Z") + .run(); + + const { anchors } = await loadPublicLedgerAnchors(env); + expect(anchors[0]?.backendRef).toBeNull(); + }); + + it("returns an empty list with no nextBefore on a fresh ledger", async () => { + const { anchors, nextBefore } = await loadPublicLedgerAnchors(createTestEnv()); + expect(anchors).toEqual([]); + expect(nextBefore).toBeNull(); + }); +}); From 67ea1fea93190d2db580c03c732c5ff8257f3a83 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:12:31 -0700 Subject: [PATCH 2/2] fix(db): add decision_ledger_anchors to RAW_SQL_ONLY_TABLES (#9271) check-schema-drift caught this on push: the migration 0195 table was never declared in src/db/schema.ts and wasn't in the raw-SQL-only allowlist either, matching decision_ledger's own existing entry (the table this one is the anchoring companion to). --- scripts/check-schema-drift.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/check-schema-drift.ts b/scripts/check-schema-drift.ts index d7d261d4a6..602de96dda 100644 --- a/scripts/check-schema-drift.ts +++ b/scripts/check-schema-drift.ts @@ -43,6 +43,7 @@ export const RAW_SQL_ONLY_TABLES: Set = new Set([ "contributor_gate_history", "decision_audit_labels", "decision_ledger", + "decision_ledger_anchors", "decision_records", "decision_replay_inputs", "ai_review_verdict_flips",