{
  "openapi": "3.0.3",
  "info": {
    "title": "aeogeo.site Customer REST API",
    "version": "1.0.0",
    "description": "Deterministic AEO/GEO checks for being cited by AI — no inference on our bill. POST /api/scan returns an aggregate grade without a key and per-check detail with `Authorization: Bearer <key>`; keyed GET /api/checks and GET /api/rules feed CLI/GitHub-Action linting. POST /api/reports stores a shareable /r/{id} report. POST /api/checkout/keepz starts Pro checkout. MCP at https://mcp.aeogeo.site remains the primary agent surface. Errors use the FR-ERR envelope (ok:false with code, message, executable fix, and docs link)."
  },
  "servers": [
    {
      "url": "https://aeogeo.site",
      "description": "Production API and static apex."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/api/reports": {
      "post": {
        "operationId": "createReport",
        "summary": "Scan 1–2 URLs and store a shareable report",
        "description": "Keyless callers get an aggregate report. `Authorization: Bearer <key>` stores the keyed report and makes that key the owner who can DELETE it. Compare mode is two URLs and two quota charges. The record holds scan findings only, never page HTML, and expires in 90 days.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "urls"
                ],
                "properties": {
                  "urls": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 2,
                    "items": {
                      "type": "string",
                      "format": "uri"
                    }
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "single",
                      "compare"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Report created. `url` is the public /r/<id> link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "report"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "report": {
                      "type": "object",
                      "required": [
                        "id",
                        "url",
                        "mode",
                        "keyed",
                        "expires_at",
                        "ruleset_version"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "pattern": "^[0-9a-f]{32}$"
                        },
                        "url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "mode": {
                          "type": "string",
                          "enum": [
                            "single",
                            "compare"
                          ]
                        },
                        "keyed": {
                          "type": "boolean"
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "ruleset_version": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_url / blocked_url / invalid_json.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "subscription_past_due.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/r/{id}": {
      "get": {
        "operationId": "getReport",
        "summary": "Read a stored report",
        "description": "Self-contained HTML by default. `format=md` or `format=json` returns that body. `download=1` sets Content-Disposition. No API key. The id is the capability.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{32}$"
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "md",
                "json"
              ]
            }
          },
          {
            "name": "download",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "HTML, Markdown, or JSON for the stored report."
          },
          "404": {
            "description": "report_not_found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteReport",
        "summary": "Delete a report you own",
        "description": "Requires the Bearer key that created the report. Keyless reports cannot be deleted.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{32}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted."
          },
          "401": {
            "description": "invalid_api_key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "requires_pro — a keyless caller cannot delete a report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "report_not_found. A key that does not own the id gets the same 404.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/api/checkout/keepz": {
      "post": {
        "operationId": "keepzCheckout",
        "summary": "Start a Pro subscription checkout",
        "description": "Form posts redirect (303) to Keepz. JSON posts (`Content-Type: application/json` and `Accept` is not required) return `{ ok, url }` and do not include the integrator order id. The parent order amount is 0; the charge is the subscription plan (29 USD monthly or 290 USD annual).",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "plan"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "plan": {
                    "type": "string",
                    "enum": [
                      "pro_monthly",
                      "pro_annual"
                    ]
                  }
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "plan"
                ],
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "plan": {
                    "type": "string",
                    "enum": [
                      "pro_monthly",
                      "pro_annual"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON callers receive { ok: true, url }."
          },
          "303": {
            "description": "Form callers are redirected to the Keepz payment URL."
          },
          "400": {
            "description": "invalid_json / invalid email or plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "502": {
            "description": "fetch_failed — Keepz did not create the order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "internal_error — checkout secrets are missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/api/scan": {
      "post": {
        "operationId": "scanUrl",
        "summary": "Scan a public URL against the active ruleset",
        "description": "Call when an agent needs to check whether a live public URL is configured to be cited by AI engines. Deterministic: same URL + same ruleset -> same output. Keyless calls return an aggregate grade (FreeScanOutput) with no rule ids, per-check arrays, or fix hints. Pass `Authorization: Bearer <key>` (Pro/Team) to receive per-check detail (KeyedScanOutput). Respects per-domain/per-IP/plan daily quotas and serves cached results for up to 1h. Scanned content is never persisted.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": false,
            "description": "Optional Bearer API key. Omit for the free (aggregate) output; pass a Pro/Team key for detailed per-check results (FR-SCAN-4).",
            "schema": {
              "type": "string",
              "pattern": "^Bearer "
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The public URL to scan.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScanRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scan result. Keyless callers receive the aggregate FreeScanOutput; keyed callers receive KeyedScanOutput with a checks[] array.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/FreeScanOutput"
                    },
                    {
                      "$ref": "#/components/schemas/KeyedScanOutput"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid_json / invalid_url / blocked_url — body is not valid JSON, the url is not a public http(s) URL, or the target is on a blocked private range.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "invalid_api_key — the supplied Authorization header does not match an active key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "subscription_past_due — the key's subscription is past due (fix points at the payment link).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "413": {
            "description": "payload_too_large — the target page exceeds the 2MB scanner budget.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited — daily quota exhausted (keyless: 5/domain + 20/IP per day; keyed: plan quota). The fix tells the agent how long to wait or to pass a key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error — unexpected scanner failure; retry or contact support.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "502": {
            "description": "fetch_failed — the target page could not be fetched within the 10s / 2MB budget.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/api/checks": {
      "get": {
        "operationId": "listChecks",
        "summary": "Fetch applicable check descriptors",
        "description": "Call before linting content locally (CLI `aeogeo lint`, GitHub Action) to fetch the active check descriptors. The output is a ruleset version plus an array of checks; filter with `content_type` (CSV) to retrieve only the checks relevant to blog, landing, faq, or product pages. Requires a Pro or Team API key in `Authorization: Bearer <key>`.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "content_type",
            "in": "query",
            "required": false,
            "description": "Optional comma-separated content types (e.g. blog,landing). Checks whose applies_to intersects any listed type (or that apply universally) are returned; omit for all checks.",
            "schema": {
              "type": "string",
              "example": "blog,landing"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ruleset version plus the (optionally filtered) check descriptors for the CLI to run locally.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChecksResponse"
                }
              }
            }
          },
          "401": {
            "description": "invalid_api_key — missing/unknown Authorization header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "subscription_past_due — the key's subscription is past due.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "requires_pro — a free/keyless caller must pass a Pro or Team key to fetch descriptors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited — keyed daily quota exhausted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error — ruleset could not be loaded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/api/rules": {
      "get": {
        "operationId": "listRules",
        "summary": "Fetch full rules for a category (JIT, capped)",
        "description": "Call when an agent needs the current evidence-linked rule text for a category before writing or editing content. `category` is required; `limit` caps the per-call result to at most 5 rules (invariant #5 — bulk rule export is not offered, and per-day drip limits apply: Pro 150, Team 1500 rules/day). Requires a Pro or Team API key in `Authorization: Bearer <key>`.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": true,
            "description": "Rule category to fetch, e.g. content_template or schema. Missing category returns invalid_json with a fix telling the agent to pass ?category=.",
            "schema": {
              "type": "string",
              "example": "content_template"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum rules to return, capped at 5 (default 5). Values above 5 are clamped down. This is the per-call cap; combined with the per-day rules drip.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5,
              "default": 5
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ruleset version plus up to `limit` full rules for the requested category, sorted by id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RulesResponse"
                }
              }
            }
          },
          "400": {
            "description": "invalid_json — missing required `category` query parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "invalid_api_key — missing/unknown Authorization header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "402": {
            "description": "subscription_past_due — the key's subscription is past due.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "requires_pro — a free/keyless caller must pass a Pro or Team key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited — the per-day rules drip for this key is exhausted for today.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "internal_error — ruleset could not be loaded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key, e.g. `Authorization: Bearer ag_live_...`. Required for GET /api/checks and GET /api/rules; optional on POST /api/scan (omit for aggregate output, pass a Pro/Team key for per-check detail). Keys are shown once at https://aeogeo.site."
      }
    },
    "schemas": {
      "ScanRequest": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "pattern": "^https?://",
            "description": "Public http(s) URL to scan. Private/loopback ranges are refused (SSRF guard)."
          }
        }
      },
      "FreeScanOutput": {
        "type": "object",
        "required": [
          "ok",
          "url",
          "ruleset_version",
          "content_types",
          "groups",
          "overall",
          "findings",
          "upgrade",
          "cached"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Canonical scanned URL."
          },
          "ruleset_version": {
            "type": "integer",
            "description": "Ruleset version used for the scan."
          },
          "content_types": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "blog",
                "landing",
                "product",
                "faq"
              ]
            },
            "description": "What the page was inferred to be. Checks whose applies_to matches none of these are skipped, not failed. Empty means the type could not be determined, in which case every check runs."
          },
          "groups": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/FreeGroupGrade"
            },
            "description": "Per-check-group letter grade and counts. No per-check detail."
          },
          "overall": {
            "$ref": "#/components/schemas/FreeGroupGrade"
          },
          "findings": {
            "type": "array",
            "description": "Failed checks at readiness-ladder levels L0-L3 (the free fundamentals) and how to fix each. Never includes rule IDs, evidence, severity, or anything above L3.",
            "items": {
              "type": "object",
              "required": [
                "check_id",
                "group",
                "level",
                "message",
                "fix"
              ],
              "properties": {
                "check_id": {
                  "type": "string"
                },
                "group": {
                  "type": "string"
                },
                "level": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 3
                },
                "message": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "fix": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "upgrade": {
            "type": "string",
            "format": "uri",
            "description": "Upgrade pointer surfaced for keyless scans (https://aeogeo.site/pricing)."
          },
          "cached": {
            "type": "boolean",
            "description": "True when the result came from the 1h KV cache."
          }
        }
      },
      "FreeGroupGrade": {
        "type": "object",
        "required": [
          "grade",
          "failed",
          "total",
          "skipped"
        ],
        "properties": {
          "grade": {
            "type": "string",
            "enum": [
              "A",
              "B",
              "C",
              "D",
              "F",
              "not_evaluated"
            ],
            "description": "Letter grade, or not_evaluated when the ruleset ships no checks for this group."
          },
          "failed": {
            "type": "integer",
            "minimum": 0,
            "description": "Failing checks in this group."
          },
          "total": {
            "type": "integer",
            "minimum": 0,
            "description": "Non-skipped checks in this group."
          },
          "skipped": {
            "type": "integer",
            "minimum": 0,
            "description": "Checks skipped in this group."
          },
          "note": {
            "type": "string",
            "description": "Why this group graded as it did when that reason is not a failed check. Set when /robots.txt is missing (HTTP 4xx): crawlers may access the whole site."
          }
        }
      },
      "KeyedScanOutput": {
        "type": "object",
        "required": [
          "ok",
          "url",
          "ruleset_version",
          "content_types",
          "content_type_signals",
          "overall",
          "groups",
          "checks",
          "upgrade",
          "cached"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "ruleset_version": {
            "type": "integer"
          },
          "content_types": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "blog",
                "landing",
                "product",
                "faq"
              ]
            },
            "description": "What the page was inferred to be. Checks whose applies_to matches none of these are skipped, not failed. Empty means the type could not be determined, in which case every check runs."
          },
          "content_type_signals": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Which signals decided content_types, e.g. jsonld:Product, path:root."
          },
          "overall": {
            "$ref": "#/components/schemas/FreeGroupGrade"
          },
          "groups": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/FreeGroupGrade"
            }
          },
          "checks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/KeyedCheckOutput"
            },
            "description": "Per-check results with rule_id, severity, message, fix_hint, evidence."
          },
          "upgrade": {
            "type": "string",
            "format": "uri"
          },
          "cached": {
            "type": "boolean"
          }
        }
      },
      "KeyedCheckOutput": {
        "type": "object",
        "required": [
          "check_id",
          "rule_id",
          "group",
          "level",
          "status",
          "severity",
          "message",
          "fix_hint",
          "evidence"
        ],
        "properties": {
          "check_id": {
            "type": "string",
            "description": "Stable check descriptor id."
          },
          "rule_id": {
            "type": "string",
            "description": "Id of the rule backing this check (may be empty)."
          },
          "group": {
            "type": "string",
            "description": "Check group the rule belongs to (FR-SCAN-2)."
          },
          "level": {
            "type": "integer",
            "nullable": true,
            "description": "Readiness ladder level of the enforced rule (0-5), null when unlevelled."
          },
          "status": {
            "type": "string",
            "enum": [
              "pass",
              "fail",
              "skipped"
            ]
          },
          "severity": {
            "type": "string",
            "enum": [
              "error",
              "warn"
            ],
            "nullable": true
          },
          "message": {
            "type": "string",
            "nullable": true,
            "description": "Failure message when status is fail/skipped."
          },
          "fix_hint": {
            "type": "string",
            "nullable": true,
            "description": "Executable fix hint for a failing check."
          },
          "evidence": {
            "type": "object",
            "nullable": true,
            "additionalProperties": {
              "type": "string"
            },
            "description": "Selector/pattern/header location the check asserted against."
          }
        }
      },
      "CheckDescriptor": {
        "type": "object",
        "required": [
          "id",
          "rule_id",
          "engine",
          "target",
          "severity",
          "params",
          "message",
          "fix_hint"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable check descriptor id."
          },
          "rule_id": {
            "type": "string",
            "description": "Backing rule id."
          },
          "engine": {
            "type": "string",
            "enum": [
              "regex",
              "dom",
              "jsonld",
              "robots",
              "http",
              "text"
            ]
          },
          "target": {
            "type": "string",
            "enum": [
              "html",
              "text",
              "jsonld",
              "robots_txt",
              "headers"
            ]
          },
          "applies_to": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Content types this check applies to; absent/empty means universal."
          },
          "severity": {
            "type": "string",
            "enum": [
              "error",
              "warn"
            ]
          },
          "params": {
            "type": "object",
            "additionalProperties": true,
            "description": "Engine-specific assertion parameters."
          },
          "message": {
            "type": "string",
            "description": "Message surfaced when the check fails."
          },
          "fix_hint": {
            "type": "string",
            "description": "Executable remediation hint."
          }
        }
      },
      "ChecksResponse": {
        "type": "object",
        "required": [
          "version",
          "checks"
        ],
        "properties": {
          "version": {
            "type": "integer",
            "description": "Ruleset version of the returned descriptors."
          },
          "checks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CheckDescriptor"
            }
          }
        }
      },
      "FullRule": {
        "type": "object",
        "required": [
          "id",
          "slug",
          "status",
          "rule",
          "why",
          "category",
          "audience",
          "confidence",
          "applies_to",
          "implementation",
          "anti_pattern",
          "evidence",
          "checks",
          "updated"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "candidate",
              "retired"
            ]
          },
          "rule": {
            "type": "string",
            "description": "The rule statement an agent should follow."
          },
          "why": {
            "type": "string",
            "description": "Evidence-backed rationale."
          },
          "category": {
            "type": "string",
            "description": "Rule category (query param on GET /api/rules)."
          },
          "audience": {
            "type": "string"
          },
          "confidence": {
            "type": "string",
            "enum": [
              "strong",
              "moderate",
              "limited",
              "hypothesis",
              "high",
              "medium",
              "low"
            ]
          },
          "applies_to": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "implementation": {
            "type": "string",
            "description": "How to implement this rule."
          },
          "anti_pattern": {
            "type": "string",
            "description": "What to avoid."
          },
          "evidence": {
            "type": "array",
            "description": "FR-TRUST-27 records carry type, url, published, publisher and title. Rulesets published before that carry source and a free-form type.",
            "items": {
              "type": "object",
              "required": [
                "type"
              ],
              "properties": {
                "type": {
                  "type": "string",
                  "description": "One of first_party_docs, peer_reviewed, preprint, vendor_study, practitioner_article, news, transcript, transcript_autogen (older rulesets: free-form)."
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                },
                "published": {
                  "type": "string",
                  "description": "Publication date or year."
                },
                "publisher": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "source": {
                  "type": "string",
                  "description": "Older rulesets only."
                }
              }
            }
          },
          "checks": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Check descriptor ids that enforce this rule."
          },
          "remediation": {
            "type": "string",
            "description": "Executable fix instruction. Omitted when the rule has no real remediation (FR-TRUST-31)."
          },
          "updated": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RulesResponse": {
        "type": "object",
        "required": [
          "version",
          "rules"
        ],
        "properties": {
          "version": {
            "type": "integer",
            "description": "Ruleset version of the returned rules."
          },
          "rules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FullRule"
            },
            "description": "At most `limit` rules (max 5 per call, plus per-day drip)."
          }
        }
      },
      "ErrorBody": {
        "type": "object",
        "required": [
          "code",
          "message",
          "fix",
          "docs"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "method_not_allowed",
              "not_found",
              "invalid_json",
              "invalid_arguments",
              "invalid_email",
              "invalid_url",
              "blocked_url",
              "invalid_api_key",
              "subscription_past_due",
              "invalid_webhook_signature",
              "unauthorized",
              "requires_pro",
              "rate_limited",
              "fetch_failed",
              "page_unusable",
              "payload_too_large",
              "scan_budget_exceeded",
              "report_not_found",
              "internal_error"
            ],
            "description": "Stable machine-readable code. Documented at https://aeogeo.site/docs/errors#<code>."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation of the failure."
          },
          "fix": {
            "type": "string",
            "description": "Imperative instruction an agent can execute to correct the failure (FR-ERR-1)."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "Link to the human-readable page for this error code."
          }
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "required": [
          "ok",
          "error"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": false,
            "description": "Always false for error responses."
          },
          "error": {
            "$ref": "#/components/schemas/ErrorBody"
          }
        }
      }
    }
  }
}
