{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://github.com/cfi-co/articles/blob/main/schema.json",
  "title": "CFI.co Articles Transparency Archive - canonical record",
  "description": "Schema for the canonical machine record (articles/<year>/<id>-<slug>.json). The .md file is a verbatim, byte-faithful mirror of the body (raw HTML, kept unaltered for tamper-evidence, not cleaned for reading); the JSON is authoritative, and content_text is the clean plain-text surface for retrieval. Schema version 2.5 (2026-09-08: added classification.correction_history and the later_development value of classification.correction_class; both describe editorial annotation, both are emitted only where they apply, and neither moves any content_sha256). v2.4 (2026-07-22: excerpt relaxed from required to optional; it is empty across the corpus); v2.3 (2026-07-21: added content_text plus a root-level index.jsonl catalog); v2.2 (2026-07-08: added classification.license). Description corrected 2026-07-24: the .md twin is a verbatim mirror, not a cleaned view - metadata only, no record, hash or validation change; the pinned archive-2026-07-schema-2.4 release asset predates this correction. Producer: scripts/export.php (committed in this repository).",
  "x-schema-version": "2.5",
  "x-integrity": {
    "content_sha256": "SHA-256 (hex) of the verbatim content_html string. content_html is the canonical body; content_text does not affect this value.",
    "record_sha256": "SHA-256 (hex) of the record serialised WITHOUT the record_sha256 field, using PHP json_encode with JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE. Recompute with scripts/verify.sh. It covers content_text, so the derived plain text is as tamper-evident as the rest of the record. A PHP runtime is NOT required to check this: scripts/verify-record.py reproduces the same bytes using only the Python standard library (indent=4, separators=(',', ': '), ensure_ascii=False, original key order) and is tested against every record in this repository.",
    "manifest": "MANIFEST.sha256 at the repository root lists the SHA-256 of every tracked file.",
    "index": "index.jsonl at the repository root is a derived one-line-per-article catalog (id, record_id, url, classification, file paths, content_sha256, record_sha256) for enumerating the corpus in a single fetch. It is regenerated by scripts/export.php and covered by MANIFEST.sha256; the per-record JSON files remain canonical."
  },
  "type": "object",
  "required": [
    "id", "title", "slug", "url", "author", "published", "published_gmt",
    "modified_gmt", "categories", "classification", "content_html",
    "content_text", "content_sha256", "record_sha256"
  ],
  "properties": {
    "id": { "type": "integer", "description": "Stable numeric record ID (publishing-system post ID). Canonical record ID form: cfi-article-<id>." },
    "title": { "type": "string" },
    "slug": { "type": "string" },
    "url": { "type": "string", "format": "uri", "description": "Canonical CFI.co URL for this article." },
    "author": { "type": "string", "minLength": 1, "description": "WHO IS CREDITED for the piece - never who performed the posting. A personal name where a byline has been established (the alignment of 2026-08-15); the house label 'CFI.co Editorial' otherwise, which means no byline was established, NOT that CFI.co is asserted as the writer. Do not read it as evidence of authoring activity: contributors who hold an account here are posted on their behalf by staff, and staff post on the publisher's behalf. Internal staff usernames are never exposed. Was declared const 'CFI.co Editorial' until 2026-08-25, which the byline alignment of 2026-08-15 had already made untrue for 146 records." },
    "published": { "type": "string", "description": "Site-local publication timestamp, YYYY-MM-DD HH:MM:SS." },
    "published_gmt": { "type": "string", "description": "UTC publication timestamp, YYYY-MM-DD HH:MM:SS." },
    "modified_gmt": { "type": "string", "description": "UTC last-modified timestamp from the publishing system." },
    "categories": {
      "type": "array", "items": { "type": "string" },
      "description": "Substantive section/sector/region categories only; rotating homepage-curation categories are deliberately excluded for stability."
    },
    "classification": {
      "type": "object",
      "description": "Machine-readable labels. Lives INSIDE the hashed record, so labels are as tamper-evident as the content. Preserve intact when redistributing (licence s.4).",
      "required": [
        "content_class", "editorial_lens", "independence_status", "sponsor_disclosure",
        "sponsor_name", "article_status", "historical_status", "correction_status",
        "archive_policy", "provenance_layer", "wayback_status", "license"
      ],
      "properties": {
        "content_class": { "type": "string", "enum": ["editorial_analysis", "interview", "opinion_column", "review", "sponsored_article"] },
        "editorial_lens": { "type": "string", "const": "constructive_positive_lens", "description": "CFI.co's stated editorial stance (declared policy, not a per-article measurement)." },
        "independence_status": { "type": "string", "enum": ["independent_editorial", "contributed_editorial", "commercially_supported"] },
        "independence_basis": { "type": "string", "enum": ["assessed", "retrospective_review_2026-08", "default_pre_2025-11-09"], "description": "How much independence_status is worth on this record. assessed - an editor labelled the piece at publication with the practice in force (from 2025-11-09). retrospective_review_2026-08 - determined by the principals reviewing the back catalogue on 2026-08-25, after the fact. default_pre_2025-11-09 - no labelling practice existed and the field defaulted; nobody asked the question. Present on every record from 2026-08-25; absent from records written before that date." },
        "sponsor_disclosure": { "type": "string", "enum": ["none", "visible_and_machine_readable"] },
        "sponsor_name": { "type": "string", "description": "Disclosed sponsor; empty when not sponsored." },
        "article_status": { "type": "string", "const": "published" },
        "historical_status": { "type": "string", "const": "current_at_publication", "description": "Accurate to its time; judge recency against published." },
        "correction_status": { "type": "string", "enum": ["none", "revised"], "description": "Git history is the authoritative correction record; flips to revised when a later commit changes the article text OR a claim this record makes about it (content_class, independence_status, sponsor_disclosure, sponsor_name) OR the byline. One-way." },
        "correction_class": { "type": "string", "enum": ["factual_correction", "label_regime_change", "later_development", "unspecified"], "description": "Present only when correction_status is revised. Which kind of revision: factual_correction the article text changed and what it said was wrong; label_regime_change the text is byte-identical and a claim or the byline changed; later_development the text was accurate when published and carries a dated note because the position it states has since moved; unspecified revised before this field existed. later_development is DECLARED in scripts/correction-events.json and never inferred - a correction and a later-development note both rewrite the text, so the bytes cannot tell them apart. factual_correction is one-way and wins." },
        "correction_history": { "type": "array", "minItems": 2, "description": "Present only where a record has carried more than one correction event; a single event is fully described by correction_class. Ordered oldest first. Declared in scripts/correction-events.json, which is covered by MANIFEST.sha256 and its signature, so the event log is exactly as tamper-evident as the record it describes.", "items": { "type": "object", "required": ["date", "class"], "properties": { "date": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, "class": { "type": "string", "enum": ["factual_correction", "label_regime_change", "later_development", "unspecified"] } } } },
        "archive_policy": { "type": "string", "const": "no_delete" },
        "provenance_layer": { "type": "string", "const": "github_versioned" },
        "wayback_status": { "type": "string", "enum": ["archived", "submitted_pending", "not_found", "pending_check"], "description": "Independent Internet Archive corroboration; archived is claimed only when a real snapshot exists." },
        "wayback_first_snapshot": { "type": "string", "description": "Earliest Wayback capture, YYYYMMDDhhmmss. Present only when wayback_status = archived." },
        "wayback_snapshot_url": { "type": "string", "format": "uri", "description": "Present only when wayback_status = archived." },
        "license": { "type": "string", "const": "CFI-OAAL-1.0", "description": "CFI.co Open AI Access Licence v1.0. Canonical text: https://cfi.co/licence/oaal-1.0 and LICENCE.md in this repository." }
      }
    },
    "excerpt": { "type": "string", "description": "Optional editor-written excerpt; currently empty across the entire corpus, so it is not in the required array. A provenance-labelled summary (editor-written vs machine-generated) is a separate future track." },
    "content_html": { "type": "string", "description": "RAW, verbatim article body exactly as held in the publishing system - no reformatting, no filters, no HTML-to-Markdown conversion. This is the canonical body and the content_sha256 subject." },
    "content_text": { "type": "string", "description": "Derived plain-text rendering of content_html for retrieval/grounding consumers: HTML tags and comments removed, entities decoded, block elements turned into line breaks, whitespace tidied. Produced deterministically from content_html by scripts/export.php (pure string operations, no publishing-system filters). Convenience only - content_html remains canonical - but it is covered by record_sha256, so it is tamper-evident." },
    "content_sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
    "record_sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
  }
}
