{
 "$schema": "https://json-schema.org/draft/2020-12/schema",
 "$id": "https://crawlcheck.io/schemas/resolve.json",
 "title": "CrawlCheck resolve answer (ResolveV1)",
 "description": "GET /api/v1/resolve?domain=: what an agent should know before acting on a domain. Signed with Ed25519 over sha256 of the canonical JSON of every field except sha256 and signature, message prefix crawlcheck-data-v1\\n; verify offline with crawlcheck-verify.mjs.",
 "type": "object",
 "required": [
  "kind",
  "v",
  "domain",
  "status",
  "answered_at",
  "sha256",
  "signature"
 ],
 "$defs": {
  "section": {
   "type": "object",
   "required": [
    "state",
    "observed_at"
   ],
   "properties": {
    "state": {
     "enum": [
      "declared",
      "observed",
      "not_measured"
     ]
    },
    "observed_at": {
     "type": [
      "string",
      "null"
     ]
    },
    "why": {
     "type": "string"
    }
   }
  }
 },
 "properties": {
  "kind": {
   "const": "crawlcheck-resolve"
  },
  "v": {
   "const": 1
  },
  "schema": {
   "type": "string"
  },
  "domain": {
   "type": "string"
  },
  "answered_at": {
   "type": "string"
  },
  "reading": {
   "type": "string"
  },
  "degraded": {
   "type": "object",
   "description": "spec 1.4.0 (dg1): sources that could not be read when the answer was made: sources [{source, why}], reading, policy (the failure policy URL), policy_v. Present only when something was missing.",
   "properties": {
    "sources": {
     "type": "array",
     "items": {
      "type": "object",
      "required": [
       "source",
       "why"
      ]
     }
    },
    "policy": {
     "type": "string"
    }
   }
  },
  "assurance": {
   "enum": [
    "partial"
   ],
   "description": "Present on just-in-time answers only. Full answers carry no assurance field."
  },
  "disputes": {
   "type": "object",
   "description": "spec 1.4.0: open (being re-measured), filed, ledger; open > 0 adds the doubt disputed to read and cite"
  },
  "status": {
   "enum": [
    "measured",
    "partial",
    "not_measured",
    "opted_out",
    "withheld"
   ],
   "description": "withheld (dg1): the opt-out list could not be read, so nothing about the domain is served. partial (jit1): a domain CrawlCheck had never measured, read live the moment it was asked about; assurance is partial and upgrade says when the full scan replaces it"
  },
  "why": {
   "type": "string"
  },
  "basis": {
   "type": "object",
   "description": "Present when the answer comes from the daily census or a just-in-time read rather than a full scan record: kind, run, rank, measured_at, scope; just_in_time adds budget_ms, took_ms and not_read_in_budget (paths not read in time, never reported absent)",
   "properties": {
    "kind": {
     "enum": [
      "census",
      "just_in_time"
     ]
    },
    "run": {
     "type": [
      "integer",
      "null"
     ]
    },
    "measured_at": {
     "type": [
      "string",
      "null"
     ]
    },
    "scope": {
     "type": "string"
    },
    "budget_ms": {
     "type": "integer"
    },
    "took_ms": {
     "type": "integer"
    },
    "not_read_in_budget": {
     "type": "array",
     "items": {
      "type": "string"
     }
    }
   }
  },
  "dns": {
   "type": "object",
   "description": "jit1, just-in-time answers: A/AAAA/CNAME from Cloudflare DNS over HTTPS, rcode, exists, dnssec_validated (the resolver's AD flag)"
  },
  "tls": {
   "type": "object",
   "description": "jit1, just-in-time answers: whether the homepage fetch completed over HTTPS with a trusted certificate, final host, HSTS header"
  },
  "upgrade": {
   "type": "object",
   "description": "jit1, just-in-time answers: state queued, to full; the change feed records assurance partial → full when the full scan lands"
  },
  "measurement": {
   "type": "object",
   "properties": {
    "requested": {
     "type": "boolean"
    },
    "first_request": {
     "type": "boolean"
    },
    "how": {
     "type": "string"
    }
   }
  },
  "subject": {
   "type": "object"
  },
  "freshness": {
   "type": [
    "object",
    "null"
   ],
   "description": "observed_at, age_hours, fresh_until, stale, last_verified_at, fresh_policy (rsv3, provisional), plus latest_report_id / is_latest / newer_scan_exists"
  },
  "observers": {
   "type": "object"
  },
  "rights": {
   "type": [
    "object",
    "null"
   ],
   "description": "rgt1: one value per use (train, ground, summarize, cite, transact): allowed | denied | limited | restricted_by_terms | not_declared | unknown, each with decided_by {signal, url, evidence}, the signals considered and the conflicts between them; merged from robots.txt per agent, Content-Signal, ai.txt, TDM-Rep, robots meta, X-Robots-Tag and the terms page"
  },
  "read_equivalence": {
   "type": [
    "object",
    "null"
   ],
   "description": "req1: whether the main text served to AI identities (GPTBot, ClaudeBot, PerplexityBot) equals what a browser receives on the homepage: verdict equivalent|minor_differences|different|not_served|unverifiable, per-identity text_ratio, instruction_like_text; api link holds the differing sentences. The cite action warns with served_text_differs"
  },
  "identity": {
   "type": [
    "object",
    "null"
   ],
   "description": "ident1: who this domain is and how many independent sources confirm it (state confirmed | unconfirmed | claimed | disputed | unknown, name, independent_confirmations, confirmed_by[], confirm_with); preflight names it in every receipt"
  },
  "facts": {
   "type": [
    "object",
    "null"
   ],
   "description": "rfact1: the site's claims an outside source was seen to agree with, as triples {subject, predicate, value, evidence[], observed_at}; signed with the answer. not_facts counts the claims that are page-only, unverified or contradicted."
  },
  "self_claims": {
   "type": [
    "object",
    "null"
   ],
   "description": "scl1: what the site says about itself (telephone, email, address, hours, prices, refund window, founded, years in business, API rate limits), claims[] each with type, value, status current|withdrawn, first_seen, last_seen; changes are in the change feed as section self_claims; api link for evidence and pages read"
  },
  "content_clock": {
   "type": [
    "object",
    "null"
   ],
   "description": "clk2 (provisional): when the main content of the homepage (and of ?page= on the same site) last changed, from CrawlCheck's own reads: content_sha256, first_observed, main_content_last_changed {between, and}, declared dates, api link"
  },
  "absences": {
   "type": "array",
   "description": "sa1: what CrawlCheck looked for and did not find, each with what was requested (method, URL, status), the vantage, the time and a scope saying what the absence does and does not mean. Written only for definite answers (404/410, a finished registry import, a completed field read)",
   "items": {
    "type": "object",
    "required": [
     "what",
     "result",
     "at",
     "from",
     "checked",
     "scope"
    ]
   }
  },
  "mcp_registry": {
   "type": "object",
   "description": "rsv4 (provisional): remote MCP servers the official MCP Registry lists on this domain and what each endpoint answered to the MCP handshake (listed, observed, initialized, auth_required, drifted, servers[])"
  },
  "impersonation": {
   "type": "object",
   "description": "spec 1.2.0: whether the domain looks built to be mistaken for a known brand (state, verdict, the brand frame it was compared against, and the technique when one is found)"
  },
  "control": {
   "type": "object",
   "description": "spec 1.3.0: whether control of the domain has been proven to CrawlCheck: state proven | unproven; proofs [{by: licence_holder | registry_claim, method: dns_txt | meta_tag | well_known_file | cloudflare_zone, how, since}]; organization {name, github, record} when its holder published one; claim (where to prove it) while unproven; why; note. Informational: it never changes a decision."
  },
  "transparency": {
   "type": "object",
   "description": "spec 1.2.0: this answer's place in the append-only answer log: state (included or pending; pending with confirmed false means the lookup did not answer within the time budget and the proof URL settles it), log_id, leaf_hash, batch, index, the inclusion path and the signed tree head it folds to"
  },
  "actions": {
   "type": "object",
   "description": "rsv3 (provisional): read, cite, connect, transact, administer - each { decision: allow|warn|require_confirmation|block|unsupported, minimum_assurance, assurance_found[], needs, why, max_age_hours }, decided from this answer at signing time"
  },
  "crawl_policy": {
   "allOf": [
    {
     "$ref": "#/$defs/section"
    }
   ],
   "properties": {
    "agents": {
     "type": "array",
     "items": {
      "type": "object",
      "required": [
       "agent",
       "allowed_homepage"
      ],
      "properties": {
       "agent": {
        "type": "string"
       },
       "allowed_homepage": {
        "type": "boolean"
       },
       "allowed_path": {
        "type": "boolean"
       },
       "has_own_group": {
        "type": "boolean"
       },
       "wildcard_rules_shadowed": {
        "type": "boolean"
       }
      }
     }
    }
   }
  },
  "delivery": {
   "allOf": [
    {
     "$ref": "#/$defs/section"
    }
   ],
   "properties": {
    "identities": {
     "type": "array",
     "items": {
      "type": "object",
      "required": [
       "identity",
       "status"
      ],
      "properties": {
       "identity": {
        "type": "string"
       },
       "status": {
        "type": [
         "integer",
         "null"
        ]
       },
       "words": {
        "type": [
         "integer",
         "null"
        ]
       },
       "blocker": {
        "type": [
         "string",
         "null"
        ]
       },
       "redirected_to": {
        "type": [
         "string",
         "null"
        ]
       }
      }
     }
    }
   }
  },
  "machine_files": {
   "allOf": [
    {
     "$ref": "#/$defs/section"
    }
   ],
   "properties": {
    "files": {
     "type": "array",
     "items": {
      "type": "object",
      "required": [
       "path",
       "status"
      ]
     }
    }
   }
  },
  "capabilities": {
   "allOf": [
    {
     "$ref": "#/$defs/section"
    }
   ],
   "properties": {
    "declared": {
     "type": "integer"
    },
    "verified": {
     "type": "integer"
    },
    "mismatches": {
     "type": "integer"
    },
    "certificate": {
     "type": [
      "object",
      "null"
     ]
    }
   }
  },
  "entity": {
   "allOf": [
    {
     "$ref": "#/$defs/section"
    }
   ],
   "properties": {
    "links": {
     "type": "array",
     "items": {
      "type": "object",
      "required": [
       "url",
       "state"
      ]
     }
    }
   }
  },
  "findings": {
   "type": "object",
   "required": [
    "access",
    "items"
   ],
   "properties": {
    "access": {
     "type": "string"
    },
    "items": {
     "type": "array"
    }
   }
  },
  "confidence": {
   "type": [
    "object",
    "null"
   ]
  },
  "contradictions": {
   "type": [
    "object",
    "null"
   ]
  },
  "evidence": {
   "type": "object"
  },
  "sha256": {
   "type": [
    "string",
    "null"
   ],
   "pattern": "^[0-9a-f]{64}$"
  },
  "signature": {
   "type": [
    "object",
    "null"
   ],
   "properties": {
    "alg": {
     "const": "Ed25519"
    },
    "kid": {
     "type": "string"
    },
    "message": {
     "type": "string"
    },
    "sig": {
     "type": "string"
    }
   }
  },
  "unsigned": {
   "type": "string"
  }
 }
}