{
  "$schema": "https://scholar-sidekick.com/.well-known/sources.schema.json",
  "service": "scholar-sidekick",
  "service_url": "https://scholar-sidekick.com",
  "version": "2026-08-11",
  "transform_version": "2026-08-11",
  "openapi": "https://scholar-sidekick.com/openapi.json",
  "documentation": "https://scholar-sidekick.com/docs",
  "principles": "https://scholar-sidekick.com/engineering-principles",
  "verification_url": "https://scholar-sidekick.com/verification",
  "changelog_url": "https://scholar-sidekick.com/changelog",
  "status_url": "https://status.scholar-sidekick.com",
  "description": "Authoritative metadata sources used by Scholar Sidekick to resolve scholarly identifiers. Each identifier type lists its resolver chain in fallback order. All outbound fetches are HTTPS-only, host-allowlisted, time-bounded, and have bounded retries.",
  "resolvers": {
    "doi": {
      "chain": ["crossref", "doi.org", "datacite"],
      "primary_host": "api.crossref.org",
      "fallbacks": ["doi.org", "api.datacite.org"],
      "notes": "Crossref is consulted first; doi.org content negotiation is consulted on miss; DataCite is the final fallback for non-Crossref DOIs (datasets, theses, software). A best-effort Elsevier DOI healer runs last for mistyped Elsevier identifiers."
    },
    "pmid": {
      "chain": ["pubmed"],
      "primary_host": "eutils.ncbi.nlm.nih.gov",
      "fallbacks": [],
      "notes": "NCBI E-utilities (esummary/efetch) for PubMed identifiers."
    },
    "pmcid": {
      "chain": ["pmcid-converter", "pubmed"],
      "primary_host": "eutils.ncbi.nlm.nih.gov",
      "fallbacks": [],
      "notes": "PMCID is normalised to a PMID via NCBI's ID converter, then resolved through PubMed."
    },
    "isbn": {
      "chain": ["openlibrary", "googlebooks"],
      "primary_host": "openlibrary.org",
      "fallbacks": ["www.googleapis.com"],
      "notes": "Open Library is consulted first; Google Books is consulted on miss."
    },
    "issn": {
      "chain": ["issn"],
      "primary_host": "portal.issn.org",
      "fallbacks": [],
      "notes": "ISSN portal lookup for journal-level metadata."
    },
    "eissn": {
      "chain": ["issn"],
      "primary_host": "portal.issn.org",
      "fallbacks": [],
      "notes": "Same as ISSN; eISSN values are normalised to canonical form before lookup."
    },
    "arxiv": {
      "chain": ["arxiv-datacite", "arxiv"],
      "primary_host": "api.datacite.org",
      "fallbacks": ["export.arxiv.org"],
      "notes": "arXiv's DataCite DOI (10.48550/arXiv.{id}) is the robust primary — it is not rate-limited, unlike export.arxiv.org. The export.arxiv.org Atom API is the fallback, consulted when DataCite lacks the id or has a transport error."
    },
    "ads": {
      "chain": ["nasa-ads"],
      "primary_host": "api.adsabs.harvard.edu",
      "fallbacks": [],
      "notes": "NASA Astrophysics Data System API for bibcode resolution."
    },
    "iris": {
      "chain": ["who-iris"],
      "primary_host": "iris.who.int",
      "fallbacks": [],
      "notes": "WHO Institutional Repository for Information Sharing - resolves IRIS URLs to publication metadata."
    },
    "scholarly_url": {
      "chain": ["url-meta"],
      "primary_host": "varies (allowlisted publisher hosts only)",
      "fallbacks": [],
      "notes": "URL metadata extraction for scholarly publisher pages. Restricted to the allowlisted set in src/lib/http/allowlist.ts; arbitrary URLs are rejected."
    }
  },
  "guarantees": {
    "deterministic": "Identical inputs (identifier, style, format) at a given transform_version produce identical output bytes. transform_version labels the currently-deployed chain; it is not a selectable parameter and prior versions are not retained or served. The guarantee is consistency within the live version plus a drift signal (a changed value means re-baseline) — not a historical snapshot. It pins our processing, not the upstream metadata, which can change (surfaced via x-scholar-cache and upstream_fetched_at). Observational provenance fields that report live upstream state — x-scholar-cache, sources[].cache, upstream_fetched_at, and the opt-in _provenance.conflicts cross-check — are exempt from byte-determinism by design.",
    "provenance_headers": [
      "x-request-id",
      "x-scholar-cache",
      "x-scholar-formatter",
      "x-scholar-style-used",
      "x-scholar-transform-version",
      "x-csl-warning",
      "x-csl-alias",
      "x-csl-dependent",
      "x-csl-fetch-style-id"
    ],
    "network_safety": "All outbound fetches are HTTPS-only, host-allowlisted, AbortController-bounded with timeouts, and use bounded retries (no infinite retry loops). Response size is capped at 2 MB.",
    "ssrf_protection": "Arbitrary user-supplied URLs are not fetched; only identifier resolution against the allowlisted source hosts above is permitted."
  },
  "provenance_schema": {
    "description": "Shape of the opt-in per-item `_provenance` object. Enable with ?provenance=1 (standard), ?provenance=full (adds full_metadata), or ?provenance=conflicts (adds cross-source verification; also requires the ENABLE_PROVENANCE_CONFLICTS server flag). Modes combine, e.g. ?provenance=full,conflicts. Acknowledged via the x-scholar-provenance response header (1, full, conflicts, or a comma-joined combination). Additive and backward-compatible: all fields beyond transform_version/sources/resolved_at are optional and omitted when not applicable.",
    "activation": {
      "query_param": "provenance",
      "values": ["1", "full", "conflicts"],
      "header": "x-scholar-provenance"
    },
    "fields": {
      "transform_version": "string — pinned normalisation+formatter version (mirrors x-scholar-transform-version).",
      "resolved_at": "string (ISO 8601) — when this process resolved the item.",
      "request_id": "string — request correlation id (mirrors x-request-id); aligns with the verifier _provenance block.",
      "sources": "array of { name, url?, fetched_at?, upstream_fetched_at?, cache?(hit|miss|bypass), role?(primary|fallback), ttl_band?(recent|mid|frozen) } — sources that contributed the winning record. upstream_fetched_at is the upstream's own record timestamp (e.g. Crossref indexed/deposited) when available; ttl_band is the cache-TTL recency band applied (Phase 12g), present only when TTL calibration is enabled.",
      "fallbacks_tried": "array of { name, outcome(won|not_found|error|empty|skipped), reason? } — resolvers consulted before the winner; omitted when the primary won on the first try.",
      "fields_from_source": "array of string — CSL-relevant fields the upstream record populated.",
      "fields_absent": "array of string — CSL-relevant fields the schema permits but upstream did not supply.",
      "normalization": "array of { rule, from, to } — canonicalisation steps applied to the identifier(s) on this request's input.",
      "field_provenance": "object keyed by field → { source, upstream_fetched_at? } — reserved for per-field × per-source freshness; sparse/omitted today.",
      "full_metadata": "object { funders?, orcids?, ror_ids?, license?, clinical_trial_ids? } — rich linked-data graph; present only under ?provenance=full.",
      "trust_signals": "object { retraction?, open_access? } — per-signal summary; present only when ?checks= ran alongside ?provenance=. Each: { requested, evaluated, status?, skipped_reason?:'no_doi' }; open_access also carries oa_status? and pdf_url?.",
      "container_only": "boolean — true when the resolved record is a journal/container, not a work (e.g. the input was an ISSN/eISSN, which identifies a serial). Lets consumers avoid reading the journal name as a paper title. Omitted for ordinary work records (Phase 12j).",
      "conflicts": "object { checked_against?, status(agreed|conflict|unavailable), skipped_reason?(no_secondary_source|upstream_error|not_found), fields? } — cross-source verification (Phase 12a.V2b), present only under ?provenance=conflicts with the server flag on. The winning record is cross-checked against ONE independent source (e.g. PubMed for a Crossref DOI; Google Books vs Open Library). fields (only when status='conflict') is an array of { field(title|first_author|year|container), primary:{source,value}, secondary:{source,value}, similarity? }. Observational — exempt from byte-determinism.",
      "mode": "string — resolver mode that produced the item (Phase 12h): 'strict' (fixed-order chain, today's only mode; 'best-effort' reserved). Strict = deterministic ordering within the live transform_version, not a version pin or snapshot.",
      "selection_reason": "string — why the winning source was chosen; for strict, always 'static_chain_position'."
    }
  },
  "policy": {
    "cache_strategy": "two-tier (in-memory LRU + persistent KV); not-found entries cached to avoid repeated upstream lookups. Resolved-record TTL is calibrated per source and publication-recency band (recent/mid/frozen) — fresh data on recent works, longer caching on stable old ones (Phase 12g). Retraction (Crossref/Retraction Watch, optionally corroborated by Europe PMC) + open-access (Unpaywall) check results are cached separately with their own TTLs (retraction long, OA short), not keyed by transform_version.",
    "rate_limiting": "sliding-window per plan tier (anonymous/free/pro/ultra/mega); IETF + legacy X-RateLimit-* headers",
    "error_envelope": "{ \"ok\": false, \"code\": \"<ERROR_CODE>\", \"error\": \"<message>\" }"
  },
  "contact": {
    "email": "support@scholar-sidekick.com",
    "issues": "https://scholar-sidekick.com"
  }
}
