{
  "openapi": "3.1.1",
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "info": {
    "title": "VerifyMCP public read API",
    "version": "1.0.0",
    "summary": "Read independent trust scores, tool surfaces and change history for MCP servers.",
    "description": "VerifyMCP scores every server in the official Model Context Protocol registry against a published rubric and re-scans daily. This API is the read side of that: the same data the site renders, in markdown, JSON and RSS.\n\nEvery operation is anonymous, read-only, safe to repeat and CORS-open. There is nothing to write and nothing to authenticate.\n\n**Finding a `{server}`/`{component}` pair.** These are the two path segments of any directory URL: `/servers/{server}/{component}`. Enumerate them from the sitemaps listed at `/sitemap.xml`, or look one up by name with `getSearchSuggestions`.\n\n**Stability.** The response bodies grow additively: new properties and new enum members may appear at any time, so validate permissively and do not treat any list here as closed. Existing properties are not removed or retyped without a version bump.\n\n**Prefer MCP for agents.** If you are an agent that speaks the Model Context Protocol, https://mcp.verifymcp.io exposes this same data as tools, with search and comparison built in. This spec is for everything else: CI checks, dashboards, scripts and generated clients.",
    "contact": {
      "name": "VerifyMCP",
      "email": "hello@verifymcp.io",
      "url": "https://verifymcp.io/about"
    },
    "license": {
      "name": "Terms of use",
      "url": "https://verifymcp.io/terms"
    }
  },
  "servers": [
    {
      "url": "https://verifymcp.io",
      "description": "Production"
    }
  ],
  "security": [],
  "externalDocs": {
    "description": "Scoring rubric, per-check guides and the MCP service",
    "url": "https://verifymcp.io/docs"
  },
  "tags": [
    {
      "name": "Trust reports",
      "description": "A component's full scored report, as an agent-readable document."
    },
    {
      "name": "Change feeds",
      "description": "What changed for one component and when: tools, parameters, capabilities, advisories and score movements."
    },
    {
      "name": "Search",
      "description": "Resolve a name to a server in the directory."
    }
  ],
  "paths": {
    "/servers/{server}/{component}.md": {
      "get": {
        "operationId": "getComponentTrustReport",
        "summary": "One component's trust report as markdown",
        "description": "The whole scored report for one component, serialised as markdown: the headline score, every check with its verdict and evidence, the tool surface, advisories and install details. This is the same view model the HTML page renders, so the two cannot disagree.\n\nThis is the *only* URL that serves markdown. The HTML page does not honour `Accept: text/markdown`, deliberately: the edge caches on URL alone, so a path with two representations serves whichever body it cached first to everyone. See `docs/edge-cache.md`.\n\nAnonymous: no API key, no session, no rate limit beyond the edge's. `Access-Control-Allow-Origin: *`, so a browser dashboard can call it directly. Cached for five minutes at the edge and in the client.",
        "tags": [
          "Trust reports"
        ],
        "parameters": [
          {
            "name": "server",
            "in": "path",
            "required": true,
            "description": "Slug of the server, as it appears in the directory URL. Derived from the registry name; find it via the sitemaps or the suggestion endpoint.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "examples": [
                "io-github-example-tools"
              ]
            }
          },
          {
            "name": "component",
            "in": "path",
            "required": true,
            "description": "Slug of one packaging channel or remote endpoint of that server. A server with an npm package and a hosted endpoint has two components, scored separately.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "examples": [
                "example-tools"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The trust report.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "A CommonMark document."
                }
              }
            }
          },
          "302": {
            "description": "The component slug is retired. `Location` points at the server's current headline channel, as the HTML page does. Temporary, because the target moves with the score.",
            "headers": {
              "Location": {
                "description": "The current `.md` URL for this server.",
                "schema": {
                  "type": "string",
                  "format": "uri-reference"
                }
              }
            }
          },
          "404": {
            "description": "No such server or component.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/servers/{server}/{component}.json": {
      "get": {
        "operationId": "getComponentChangeFeed",
        "summary": "One component's change feed as JSON",
        "description": "Every recorded change to this component, newest first, with a per-day roll-up alongside.\n\nRead `days[].tracked` before concluding anything from an empty day: a day with no events is not the same as a day we were not watching, and the feed states which it is rather than letting you infer it. `tracking_since` is the first day we recorded anything at all.\n\nAnonymous: no API key, no session, no rate limit beyond the edge's. `Access-Control-Allow-Origin: *`, so a browser dashboard can call it directly. Cached for five minutes at the edge and in the client.",
        "tags": [
          "Change feeds"
        ],
        "parameters": [
          {
            "name": "server",
            "in": "path",
            "required": true,
            "description": "Slug of the server, as it appears in the directory URL. Derived from the registry name; find it via the sitemaps or the suggestion endpoint.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "examples": [
                "io-github-example-tools"
              ]
            }
          },
          {
            "name": "component",
            "in": "path",
            "required": true,
            "description": "Slug of one packaging channel or remote endpoint of that server. A server with an npm package and a hosted endpoint has two components, scored separately.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "examples": [
                "example-tools"
              ]
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Lower bound on the event date, inclusive. Anything that is not exactly `YYYY-MM-DD` is ignored and no bound is applied.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "examples": [
                "2026-01-31"
              ]
            }
          },
          {
            "name": "materiality",
            "in": "query",
            "required": false,
            "description": "Comma-separated list of materiality levels to include, or `all`. Defaults to the security-relevant subset (`critical,security`), which is the useful default for monitoring. Unrecognised names are dropped; if that leaves nothing, the default applies.",
            "schema": {
              "type": "string",
              "examples": [
                "all",
                "critical,security",
                "functional"
              ]
            }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "description": "Restrict to events that moved the score one way. Omitted, or anything unrecognised, means no filter.",
            "schema": {
              "type": "string",
              "enum": [
                "regression",
                "improvement",
                "neutral"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of events to return, applied after the other filters. Clamped to 500; a non-positive or unparseable value falls back to the default.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The component's recorded changes, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChangeFeed"
                }
              }
            }
          },
          "404": {
            "description": "No such server or component, or change feeds are disabled on this deployment. Note this route does NOT redirect a retired component slug the way the HTML page does: a feed is a stream identity, and silently switching a subscriber to a different component's history would splice two records together undetectably.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/servers/{server}/{component}/changelog.json": {
      "get": {
        "operationId": "getComponentChangeFeedLegacyPath",
        "summary": "One component's change feed as JSON (older path)",
        "description": "Identical to `getComponentChangeFeed`, same handler, same body. This is the original address and it stays alive because subscribers are pointed at it. New integrations should use the suffix form, which is guessable from the page URL alongside `.md` and `.xml`.\n\nAnonymous: no API key, no session, no rate limit beyond the edge's. `Access-Control-Allow-Origin: *`, so a browser dashboard can call it directly. Cached for five minutes at the edge and in the client.",
        "tags": [
          "Change feeds"
        ],
        "parameters": [
          {
            "name": "server",
            "in": "path",
            "required": true,
            "description": "Slug of the server, as it appears in the directory URL. Derived from the registry name; find it via the sitemaps or the suggestion endpoint.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "examples": [
                "io-github-example-tools"
              ]
            }
          },
          {
            "name": "component",
            "in": "path",
            "required": true,
            "description": "Slug of one packaging channel or remote endpoint of that server. A server with an npm package and a hosted endpoint has two components, scored separately.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "examples": [
                "example-tools"
              ]
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Lower bound on the event date, inclusive. Anything that is not exactly `YYYY-MM-DD` is ignored and no bound is applied.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "examples": [
                "2026-01-31"
              ]
            }
          },
          {
            "name": "materiality",
            "in": "query",
            "required": false,
            "description": "Comma-separated list of materiality levels to include, or `all`. Defaults to the security-relevant subset (`critical,security`), which is the useful default for monitoring. Unrecognised names are dropped; if that leaves nothing, the default applies.",
            "schema": {
              "type": "string",
              "examples": [
                "all",
                "critical,security",
                "functional"
              ]
            }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "description": "Restrict to events that moved the score one way. Omitted, or anything unrecognised, means no filter.",
            "schema": {
              "type": "string",
              "enum": [
                "regression",
                "improvement",
                "neutral"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of events to return, applied after the other filters. Clamped to 500; a non-positive or unparseable value falls back to the default.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The component's recorded changes, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChangeFeed"
                }
              }
            }
          },
          "404": {
            "description": "No such server or component, or change feeds are disabled on this deployment. Note this route does NOT redirect a retired component slug the way the HTML page does: a feed is a stream identity, and silently switching a subscriber to a different component's history would splice two records together undetectably.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/servers/{server}/{component}.xml": {
      "get": {
        "operationId": "getComponentChangeFeedRss",
        "summary": "One component's change feed as RSS",
        "description": "The same events as `getComponentChangeFeed`, as an RSS 2.0 document, so a reader, a Slack webhook or a monitor can subscribe without writing any code.\n\nOne `<item>` per event rather than per day, so a critical change cannot be buried under three cosmetic ones in the same item. Anonymous: no API key, no session, no rate limit beyond the edge's. `Access-Control-Allow-Origin: *`, so a browser dashboard can call it directly. Cached for five minutes at the edge and in the client.",
        "tags": [
          "Change feeds"
        ],
        "parameters": [
          {
            "name": "server",
            "in": "path",
            "required": true,
            "description": "Slug of the server, as it appears in the directory URL. Derived from the registry name; find it via the sitemaps or the suggestion endpoint.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "examples": [
                "io-github-example-tools"
              ]
            }
          },
          {
            "name": "component",
            "in": "path",
            "required": true,
            "description": "Slug of one packaging channel or remote endpoint of that server. A server with an npm package and a hosted endpoint has two components, scored separately.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "examples": [
                "example-tools"
              ]
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Lower bound on the event date, inclusive. Anything that is not exactly `YYYY-MM-DD` is ignored and no bound is applied.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "examples": [
                "2026-01-31"
              ]
            }
          },
          {
            "name": "materiality",
            "in": "query",
            "required": false,
            "description": "Comma-separated list of materiality levels to include, or `all`. Defaults to the security-relevant subset (`critical,security`), which is the useful default for monitoring. Unrecognised names are dropped; if that leaves nothing, the default applies.",
            "schema": {
              "type": "string",
              "examples": [
                "all",
                "critical,security",
                "functional"
              ]
            }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "description": "Restrict to events that moved the score one way. Omitted, or anything unrecognised, means no filter.",
            "schema": {
              "type": "string",
              "enum": [
                "regression",
                "improvement",
                "neutral"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of events to return, applied after the other filters. Clamped to 200; a non-positive or unparseable value falls back to the default.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "RSS 2.0 rendering of the same events.",
            "content": {
              "application/rss+xml": {
                "schema": {
                  "type": "string",
                  "description": "An RSS 2.0 document."
                }
              }
            }
          },
          "404": {
            "description": "No such server or component, or change feeds are disabled. This route predates the JSON error body and answers with a plain-text `Not found`.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/servers/{server}/{component}/changelog.xml": {
      "get": {
        "operationId": "getComponentChangeFeedRssLegacyPath",
        "summary": "One component's change feed as RSS (older path)",
        "description": "Identical to `getComponentChangeFeedRss`, same handler, same body. Kept alive for existing subscribers; new ones should use the suffix form.\n\nOne `<item>` per event rather than per day, so a critical change cannot be buried under three cosmetic ones in the same item. Anonymous: no API key, no session, no rate limit beyond the edge's. `Access-Control-Allow-Origin: *`, so a browser dashboard can call it directly. Cached for five minutes at the edge and in the client.",
        "tags": [
          "Change feeds"
        ],
        "parameters": [
          {
            "name": "server",
            "in": "path",
            "required": true,
            "description": "Slug of the server, as it appears in the directory URL. Derived from the registry name; find it via the sitemaps or the suggestion endpoint.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "examples": [
                "io-github-example-tools"
              ]
            }
          },
          {
            "name": "component",
            "in": "path",
            "required": true,
            "description": "Slug of one packaging channel or remote endpoint of that server. A server with an npm package and a hosted endpoint has two components, scored separately.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "examples": [
                "example-tools"
              ]
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Lower bound on the event date, inclusive. Anything that is not exactly `YYYY-MM-DD` is ignored and no bound is applied.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "examples": [
                "2026-01-31"
              ]
            }
          },
          {
            "name": "materiality",
            "in": "query",
            "required": false,
            "description": "Comma-separated list of materiality levels to include, or `all`. Defaults to the security-relevant subset (`critical,security`), which is the useful default for monitoring. Unrecognised names are dropped; if that leaves nothing, the default applies.",
            "schema": {
              "type": "string",
              "examples": [
                "all",
                "critical,security",
                "functional"
              ]
            }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "description": "Restrict to events that moved the score one way. Omitted, or anything unrecognised, means no filter.",
            "schema": {
              "type": "string",
              "enum": [
                "regression",
                "improvement",
                "neutral"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of events to return, applied after the other filters. Clamped to 200; a non-positive or unparseable value falls back to the default.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "RSS 2.0 rendering of the same events.",
            "content": {
              "application/rss+xml": {
                "schema": {
                  "type": "string",
                  "description": "An RSS 2.0 document."
                }
              }
            }
          },
          "404": {
            "description": "No such server or component, or change feeds are disabled. This route predates the JSON error body and answers with a plain-text `Not found`.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/opensearch/suggest.json": {
      "get": {
        "operationId": "getSearchSuggestions",
        "summary": "Resolve a partial name to servers in the directory",
        "description": "Name completion over the directory, ranked exactly as the on-site search ranks: there is one matcher, not two. Use it to turn a name a user typed into the `{server}`/`{component}` pair the other operations need.\n\nOnly scored, listed servers are suggested, matching the directory's own default. Unscored and delisted entries are never returned.\n\nThe response is the OpenSearch suggestions format: a four-element array, not an object. Element 0 echoes the query exactly as sent (Firefox rejects a response whose echo differs), element 1 holds the display labels, element 2 is always empty, and element 3 holds the absolute URL for each label at the same index.\n\nDegrades to an empty suggestion set rather than erroring if the directory snapshot is cold, so it never 500s mid-keystroke.",
        "tags": [
          "Search"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "The partial name. Queries shorter than the searchable minimum, and a missing `q`, return an empty suggestion set rather than an error.",
            "schema": {
              "type": "string",
              "maxLength": 100,
              "examples": [
                "github"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "An OpenSearch suggestions tuple. Always four elements, even when there are no matches.",
            "content": {
              "application/x-suggestions+json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchSuggestions"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ChangeFeed": {
        "title": "VerifyMCP component change feed",
        "description": "The JSON feed of recorded changes to one MCP component, served at https://verifymcp.io/servers/{server}/{component}.json (and at the older .../{component}/changelog.json). The feed grows additively: new properties and new `kind` values may appear, so validate permissively and never assume a list here is closed.",
        "type": "object",
        "required": [
          "server",
          "component",
          "generated_at",
          "tracking_since",
          "counts",
          "events",
          "days"
        ],
        "additionalProperties": true,
        "properties": {
          "$schema": {
            "type": "string",
            "format": "uri",
            "description": "URL of this schema. Informational: VerifyMCP emits it, and reads nothing back."
          },
          "server": {
            "type": "string",
            "description": "Slug of the server the component belongs to."
          },
          "component": {
            "type": "string",
            "description": "Slug of the component (one packaging channel or remote endpoint of the server)."
          },
          "generated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this response was built. Not when the last change happened."
          },
          "tracking_since": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "The first day we recorded anything for this component. Null when we never have. A day before this date has no events because we were not watching, not because nothing happened.",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "counts": {
            "type": "object",
            "description": "How many events in `events[]` fall into each materiality. Counts the returned events, not the component's whole history.",
            "additionalProperties": true,
            "properties": {
              "critical": {
                "type": "integer",
                "minimum": 0
              },
              "security": {
                "type": "integer",
                "minimum": 0
              },
              "functional": {
                "type": "integer",
                "minimum": 0
              },
              "cosmetic": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "events": {
            "type": "array",
            "description": "Recorded changes, newest first, after the query filters and the limit.",
            "items": {
              "$ref": "#/components/schemas/ChangeFeed/$defs/event"
            }
          },
          "days": {
            "type": "array",
            "description": "One entry per day of score history, newest first. Carries whether we were recording that day, so an empty event list is never ambiguous.",
            "items": {
              "$ref": "#/components/schemas/ChangeFeed/$defs/day"
            }
          }
        },
        "$defs": {
          "materiality": {
            "type": "string",
            "description": "How much the change matters. `critical` and `security` are the default feed subscription.",
            "enum": [
              "critical",
              "security",
              "functional",
              "cosmetic"
            ]
          },
          "direction": {
            "type": "string",
            "description": "Whether the change made the component better, worse, or neither.",
            "enum": [
              "regression",
              "improvement",
              "neutral"
            ]
          },
          "event": {
            "type": "object",
            "required": [
              "id",
              "kind",
              "family",
              "subject_kind",
              "subject",
              "subject_child",
              "from_code",
              "to_code",
              "from_value",
              "to_value",
              "materiality",
              "direction",
              "score_delta",
              "detail",
              "occurred_on",
              "occurred_at",
              "observed_since",
              "source",
              "summary",
              "link"
            ],
            "additionalProperties": true,
            "properties": {
              "id": {
                "type": "string",
                "description": "Stable identifier for this event. Also the fragment on `link` and the guid in the RSS feed."
              },
              "kind": {
                "type": "string",
                "description": "What happened, as a dotted identifier such as `tool.added` or `advisory.published`. A PUBLIC CONTRACT: kinds are added over time but never renamed or repurposed, so match on the ones you know and ignore the rest. The families in use are check, tool, param, resource, prompt, server, protocol, transport, capability, capture, snapshot, package, advisory, malware, installscript, license, provenance, score and changelog."
              },
              "family": {
                "type": "string",
                "description": "The check family the event belongs to, when it came from a trust-score check. Empty string when it did not."
              },
              "subject_kind": {
                "type": "string",
                "description": "What sort of thing changed (for example `tool`, `param`, `resource`, `check`). Empty string when the event is about the component as a whole."
              },
              "subject": {
                "type": "string",
                "description": "The thing that changed: a tool name, a check code, a capability. Attacker-influenced text off the wire from the scanned server, so escape it before rendering."
              },
              "subject_child": {
                "type": "string",
                "description": "The nested thing that changed, such as a parameter of `subject`. Empty string when there is none. Attacker-influenced text, as `subject` is."
              },
              "from_code": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The previous check outcome code, when the event is a check result moving."
              },
              "to_code": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The new check outcome code, when the event is a check result moving."
              },
              "from_value": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The previous value, when the change was a value moving (a version, a transport, a licence). Null on `score.rubric_changed`, whose values are internal rubric identifiers we do not publish."
              },
              "to_value": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The new value. Null on `score.rubric_changed`, for the same reason as `from_value`."
              },
              "materiality": {
                "$ref": "#/components/schemas/ChangeFeed/$defs/materiality"
              },
              "direction": {
                "$ref": "#/components/schemas/ChangeFeed/$defs/direction"
              },
              "score_delta": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "How far this event moved the component's total score, when that is attributable to it alone."
              },
              "detail": {
                "type": "object",
                "description": "Extra context for the event, as flat string values. Keys vary by `kind` and may be added, so read the ones you need and tolerate the rest. Internal rubric identifiers are stripped here.",
                "additionalProperties": {
                  "type": "string"
                }
              },
              "occurred_on": {
                "type": "string",
                "format": "date",
                "description": "The day the change was recorded against, matching a `days[].date`.",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
              },
              "occurred_at": {
                "type": "string",
                "format": "date-time",
                "description": "When the scan that recorded the change ran. Use this to order events within a day; the RSS `pubDate` is the same instant."
              },
              "observed_since": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date",
                "description": "The earlier observation this change was compared against, when there was one.",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
              },
              "source": {
                "type": "string",
                "description": "`scan` means we watched it change. `backfill` means we reconstructed it from earlier scan data rather than observing it live.",
                "enum": [
                  "scan",
                  "backfill"
                ]
              },
              "summary": {
                "type": "string",
                "description": "A rendered one-line description, for convenience. English, and its wording may change; the machine fields above are the contract."
              },
              "link": {
                "type": "string",
                "format": "uri",
                "description": "Deep link to this row on the component page."
              }
            }
          },
          "day": {
            "type": "object",
            "required": [
              "date",
              "total",
              "delta",
              "tracked",
              "explained",
              "beyond_event_horizon",
              "attribution",
              "event_count"
            ],
            "additionalProperties": true,
            "properties": {
              "date": {
                "type": "string",
                "format": "date",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
              },
              "total": {
                "type": "number",
                "description": "The component's total trust score on this day, 0-100."
              },
              "delta": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "The move from the previous SCORED day, not the previous calendar day. Null when there is nothing to compare against."
              },
              "tracked": {
                "type": "boolean",
                "description": "Whether we were recording changes on this day. False means an empty event list says nothing: we were not watching yet."
              },
              "explained": {
                "type": "boolean",
                "description": "We were recording, and the recorded changes account for the day's move. False means we cannot claim they do."
              },
              "beyond_event_horizon": {
                "type": "boolean",
                "description": "This day is at or beyond the oldest event in `events[]`, so changes may have been recorded against it that this response did not return. Such a day is never `explained`. Narrow the query with `?since=` to see them."
              },
              "attribution": {
                "description": "What our own stored category scores say about a day that moved with no recorded change behind it, when they can say anything. Null otherwise. This is an observation about our numbers, not a recorded change.",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/ChangeFeed/$defs/attribution"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "event_count": {
                "type": "integer",
                "minimum": 0,
                "description": "How many recorded changes fall on this day. Counts the component's events for the day, which can exceed the number present in `events[]` once a filter or the limit has been applied."
              }
            }
          },
          "attribution": {
            "type": "object",
            "required": [
              "category",
              "from",
              "to",
              "delta",
              "others",
              "accrual",
              "partial",
              "compared_to",
              "summary"
            ],
            "additionalProperties": true,
            "properties": {
              "category": {
                "type": "string",
                "description": "The score category that moved furthest."
              },
              "from": {
                "type": "number",
                "description": "That category's score on the day compared against, on its own 0-100 scale."
              },
              "to": {
                "type": "number",
                "description": "That category's score on this day."
              },
              "delta": {
                "type": "number",
                "description": "`to` minus `from`, on the CATEGORY's own 0-100 scale. Do not reconcile it against the day's `delta`, which is a weighted total."
              },
              "others": {
                "type": "array",
                "description": "Other categories that also moved, with their own deltas.",
                "items": {
                  "type": "object",
                  "required": [
                    "category",
                    "delta"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "category": {
                      "type": "string"
                    },
                    "delta": {
                      "type": "number"
                    }
                  }
                }
              },
              "accrual": {
                "description": "Present when the stability observation window provably widened between the two days, which moves the score without anything about the component changing. Null otherwise.",
                "oneOf": [
                  {
                    "type": "object",
                    "required": [
                      "code",
                      "from_days",
                      "to_days"
                    ],
                    "additionalProperties": true,
                    "properties": {
                      "code": {
                        "type": "string"
                      },
                      "from_days": {
                        "type": "number"
                      },
                      "to_days": {
                        "type": "number"
                      }
                    }
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "partial": {
                "type": "boolean",
                "description": "A category could not be compared on both days, so a second cause is possible."
              },
              "compared_to": {
                "type": "string",
                "format": "date",
                "description": "The day compared against: the previous SCORED day, not the previous calendar day.",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
              },
              "summary": {
                "type": "string",
                "description": "A rendered summary, for convenience. English, and its wording may change; the machine fields above are the contract."
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "additionalProperties": true,
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable, machine-readable code. Branch on this, never on `message`.",
            "enum": [
              "not_found"
            ]
          },
          "message": {
            "type": "string",
            "description": "One-sentence explanation, in English, safe to show a human."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "Where to read about this surface and recover."
          }
        }
      },
      "SearchSuggestions": {
        "type": "array",
        "description": "The OpenSearch suggestions tuple: [query echo, labels, descriptions (always empty), URLs]. `labels` and `urls` are parallel arrays.",
        "minItems": 4,
        "maxItems": 4,
        "prefixItems": [
          {
            "type": "string",
            "description": "The query exactly as it was received."
          },
          {
            "type": "array",
            "description": "Server display names, best match first.",
            "items": {
              "type": "string"
            }
          },
          {
            "type": "array",
            "description": "Always empty. Present because the format requires it.",
            "items": {
              "type": "string"
            },
            "maxItems": 0
          },
          {
            "type": "array",
            "description": "Absolute URL of each suggestion's page, at the same index as its label.",
            "items": {
              "type": "string",
              "format": "uri"
            }
          }
        ]
      }
    }
  }
}