{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://github.com/Imbad0202/academic-research-skills/shared/contracts/passport/literature_corpus_entry.schema.json",
  "title": "Material Passport Literature Corpus Entry",
  "description": "One entry in Material Passport Schema 9 literature_corpus[]. Represents a single piece of literature the user has included in their corpus. Produced by user-written adapters; see academic-pipeline/references/adapters/overview.md. ARS does NOT produce these entries itself.",

  "type": "object",
  "additionalProperties": false,

  "required": [
    "citation_key",
    "title",
    "authors",
    "year",
    "source_pointer"
  ],

  "properties": {
    "citation_key": {
      "type": "string",
      "minLength": 1,
      "pattern": "^[A-Za-z][A-Za-z0-9_:-]*$",
      "description": "Stable unique identifier for this entry within the passport. Typically BibTeX citekey (e.g., 'chen2024ai'). MUST be unique within literature_corpus[]. Uniqueness is enforced by the CI lint, not by JSON Schema."
    },
    "title": {
      "type": "string",
      "minLength": 1
    },
    "authors": {
      "type": "array",
      "minItems": 1,
      "items": { "$ref": "#/$defs/csl_name" },
      "description": "CSL-JSON name format. Each item is either a personal name ({family, given, ...}) or a corporate/institution name ({literal}). See CSL-JSON spec at https://docs.citationstyles.org/en/stable/specification.html#names."
    },
    "year": {
      "type": "integer",
      "minimum": 1000,
      "maximum": 2100,
      "description": "Publication year. If the adapter cannot determine a year, the entry MUST be rejected (go to rejection_log.yaml) rather than coerced to a placeholder."
    },
    "source_pointer": {
      "type": "string",
      "minLength": 1,
      "description": "Stable URI locating this work in the user's own KB. Examples: 'zotero://select/items/0_ABCD1234', 'obsidian://open?vault=kb&file=Chen2024', 'file:///path/to/refs/chen2024.pdf', 'https://doi.org/10.1234/xyz'. ARS does NOT dereference this; consumers that want the full text fetch it from the pointer themselves."
    },

    "venue":                  { "type": "string", "minLength": 1 },
    "doi": {
      "type": "string",
      "pattern": "^10\\.[0-9]{4,9}/[^\\s]+$",
      "description": "DOI without leading 'doi:' or URL prefix."
    },
    "tags": {
      "type": "array",
      "items": { "type": "string", "minLength": 1 },
      "description": "User-assigned tags from the source KB. Adapter-specific; treat as free-form."
    },

    "obtained_via": {
      "type": "string",
      "enum": ["zotero-api", "zotero-bbt-export", "obsidian-vault", "folder-scan", "manual", "other"],
      "description": "Strongly recommended. Which adapter produced this entry. 'other' is permitted for user-written custom adapters; those SHOULD also set adapter_name. Note: v3.6.4 reference implementations only use 'zotero-bbt-export', 'obsidian-vault', and 'folder-scan'. The remaining enum values ('zotero-api', 'manual') are reserved for user-written adapters and are permitted in the schema so such adapters remain valid without schema extension."
    },
    "obtained_at": {
      "type": "string",
      "format": "date-time",
      "description": "Strongly recommended. ISO 8601 timestamp when the adapter produced this entry."
    },
    "adapter_name": {
      "type": "string",
      "minLength": 1,
      "description": "Optional. Required when obtained_via='other'."
    },
    "adapter_version": {
      "type": "string",
      "minLength": 1
    },

    "abstract": {
      "type": "string",
      "description": "PRIVATE FIELD. May contain publisher-copyrighted material. Do NOT share passports containing this field publicly without confirming your right to do so. ARS consumers treat this as optional input; absence never causes failure."
    },
    "user_notes": {
      "type": "string",
      "description": "PRIVATE FIELD. User's own annotations from their KB. May contain copyrighted excerpts. Same sharing caveat as abstract."
    },

    "source_acquired": {
      "type": "boolean",
      "description": "v3.7.1 trust-chain field (spec § 3.1, D1). True when the original source artifact (PDF / HTML / dataset) is actually retrieved into the user's KB or workspace. Distinguishes 'we have the file' from 'we cite the bibliography record'. Adapter-set or AI-set; user-orthogonal (the user-set human-read signal is recorded in the §3.6 peer file, NOT here)."
    },
    "source_acquisition_date": {
      "type": "string",
      "format": "date-time",
      "description": "v3.7.1 trust-chain field. ISO 8601 timestamp when the original source was acquired. Only meaningful when source_acquired=true."
    },
    "source_acquisition_path": {
      "type": "string",
      "minLength": 1,
      "description": "v3.7.1 trust-chain field. Repo-relative or absolute path to the acquired source file. Only meaningful when source_acquired=true."
    },
    "source_verified_against_original": {
      "type": "boolean",
      "description": "v3.7.1 trust-chain field. True when an AI agent has cross-checked the bibliography entry's authors / year / title / venue against the actual content of the acquired source (NOT just a derivative bibliography). Per spec §3.1 firm rule #1, this MUST NOT be true unless source_acquired=true AND source_verification_method names a real method (not 'none')."
    },
    "source_verification_method": {
      "type": "string",
      "enum": ["codex_audit", "manual_grep", "vision_check", "none"],
      "description": "v3.7.1 trust-chain field. Method used to verify the entry against the original source. 'none' means no verification was performed; entries with 'none' MUST NOT set source_verified_against_original=true (spec §3.1 firm rule #1, round-2 R2-007 amend: 'none' is a valid enum but is forbidden in conjunction with verified=true)."
    },
    "description_source": {
      "type": "string",
      "pattern": "^(original_pdf|bibliography_v[0-9]+|secondary_summary)$",
      "description": "v3.7.1 trust-chain field. Origin of the descriptive metadata (title / authors / abstract). 'original_pdf' = read directly off the acquired source; 'bibliography_v<n>' (any non-negative integer n) = lifted from a derivative bibliography revision; 'secondary_summary' = paraphrased from another work. Surfaces description provenance independently of source acquisition. Spec § 3.1 yaml uses the literal `bibliography_v<n>` template, so the pattern accepts any integer n rather than hard-coding a finite revision count."
    },
    "description_last_audit": {
      "type": ["string", "null"],
      "description": "v3.7.1 trust-chain field. Round identifier of the most recent codex / cross-model audit that examined this entry's description, or null / the literal string 'none' when no such audit has run. The field-level type permits both null and 'none' broadly, but per spec §3.1 firm rule #2 the rule-#2 then-branch in `allOf` tightens this to the LITERAL STRING 'none' only when source_acquired=false (no original means audit cannot be substantive — round-6 codex P2 closure). null remains valid only when source_acquired=true and the entry is simply unaudited."
    },
    "contamination_signals_backfilled_at": {
      "type": "string",
      "format": "date-time",
      "description": "v3.7.3 backfill provenance (issue #105). ISO-8601 UTC timestamp set by scripts/migrate_literature_corpus_to_v3_7_3.py when contamination_signals was computed post-hoc on a pre-v3.7.3 entry rather than at ingest time. Absence means signals were either computed at ingest by bibliography_agent v3.7.3+ or have not been computed yet. Backward compat: pre-v3.7.3 entries lack both contamination_signals and this field; ingest-time entries (v3.7.3+) lack only this field."
    },
    "contamination_signals": {
      "type": "object",
      "additionalProperties": false,
      "description": "v3.7.3 + v3.9.0 contaminated-source advisory field (spec v3.7.3 §3.2 + v3.9.0 §3.4–§3.5). Up to four optional boolean signals computed at ingest time by bibliography_agent: v3.7.3 Vector 1 (preprint_post_llm_inflection — heuristic), v3.7.3 Vector 2 (semantic_scholar_unmatched — S2 lookup), v3.9.0 (openalex_unmatched + crossref_unmatched — triangulation extension). Surfaces at cite-time finalizer as CONTAMINATED-... marker annotation suffix; advisory only — does NOT block emission. Absence of the object means signals were not computed (legacy entries or check skipped). Presence with all populated *_unmatched fields false means lookup-based contamination evidence is absent for the queried indexes; `preprint_post_llm_inflection: true` is an independent contamination signal that remains active regardless of lookup field values. Manual entries are exempt from the three lookup fields per spec v3.9.0 §3.1 not-rule but still carry preprint_post_llm_inflection (heuristic). External motivation: Zhao et al. arXiv:2605.07723 (2026-05) documents post-2024-mid hallucination inflection in preprint corpora and §3 cross-index triangulation as viable false-positive reduction.",
      "properties": {
        "preprint_post_llm_inflection": {
          "type": "boolean",
          "description": "True when entry year >= 2024 AND venue is a preprint server. The v3.7.3 closed list of 10 servers (expanded from 6 to 10 per codex F6 / gemini review closure): arXiv, bioRxiv, medRxiv, SSRN, Research Square, Preprints.org, ChemRxiv, EarthArXiv, OSF Preprints (covers SocArXiv / PsyArXiv / other OSF-hosted services), TechRxiv. Per spec §3.2 Vector 1; threshold year derived from Zhao et al. 2026-05 mid-2024 inflection point analysis. Adapters that compute this signal MUST use the full 10-venue list; partial implementations from the schema description alone would suppress CONTAMINATED-PREPRINT advisories on the 4 newer venues (v3.7.3 codex round-4 F13 closure)."
        },
        "semantic_scholar_unmatched": {
          "type": "boolean",
          "description": "True when a Semantic Scholar API lookup (per references/semantic_scholar_api_protocol.md) returned no match by either DOI or title. The check fires only when obtained_via != 'manual' (user-curated entries are exempted; user has vouched). Per spec §3.2 Vector 2."
        },
        "openalex_unmatched": {
          "type": "boolean",
          "description": "True when an OpenAlex API lookup (per references/openalex_api_protocol.md) returned no match by DOI (with title cross-check) or title. Mirrors semantic_scholar_unmatched semantics. The check fires only when obtained_via != 'manual'. Per spec v3.9.0 §3.4."
        },
        "crossref_unmatched": {
          "type": "boolean",
          "description": "True when a Crossref API lookup (per references/crossref_api_protocol.md) returned no match by DOI (with title cross-check) or title. Mirrors semantic_scholar_unmatched semantics. The check fires only when obtained_via != 'manual'. Per spec v3.9.0 §3.5."
        }
      }
    }
  },

  "allOf": [
    {
      "description": "When obtained_via='other' (user-written custom adapter), adapter_name MUST be set so consumers can attribute the entry. Reference adapters (folder-scan, zotero-bbt-export, obsidian-vault) and other enum values do not trigger this constraint.",
      "if": {
        "properties": { "obtained_via": { "const": "other" } },
        "required": ["obtained_via"]
      },
      "then": {
        "required": ["adapter_name"]
      }
    },
    {
      "description": "v3.7.1 spec §3.1 firm rule #1 (round-2 R2-007 amend): source_verified_against_original=true REQUIRES source_acquired=true AND source_verification_method ∈ {codex_audit, manual_grep, vision_check}. The 'none' method is enumerated for shape uniformity but is FORBIDDEN in conjunction with verified=true.",
      "if": {
        "properties": { "source_verified_against_original": { "const": true } },
        "required": ["source_verified_against_original"]
      },
      "then": {
        "required": ["source_acquired", "source_verification_method"],
        "properties": {
          "source_acquired": { "const": true },
          "source_verification_method": {
            "enum": ["codex_audit", "manual_grep", "vision_check"]
          }
        }
      }
    },
    {
      "description": "v3.7.1 spec §3.1 firm rule #2 (round-6 codex P2 closure): source_acquired=false REQUIRES `description_last_audit: \"none\"` — the literal sentinel string, NOT null. Spec line 120 reads 'REQUIRES description_last_audit: none' (literal 'none'); spec line 111 yaml shows the value vocabulary as `<round_id> | none` with no null alternative. Round-1 closure made the field strictly required (presence enforced); round-6 closure removes the null alternative the field-level type permitted, since spec firm rule #2 mandates the literal sentinel. The top-level `type: [string, null]` on the field stays — null is still legal when source_acquired=true and the entry simply hasn't been audited yet — but the rule-#2 then-branch tightens to the literal string only.",
      "if": {
        "properties": { "source_acquired": { "const": false } },
        "required": ["source_acquired"]
      },
      "then": {
        "required": ["description_last_audit"],
        "properties": {
          "description_last_audit": {
            "type": "string",
            "const": "none"
          }
        }
      }
    },
    {
      "description": "v3.7.3 spec §3.2 cross-field rule (gemini review F5 closure): preprint_post_llm_inflection=true REQUIRES year>=2024. The flag's definition is `year >= 2024 AND venue is a preprint server`, so setting the flag true with a pre-2024 year is logically contradictory and the schema rejects it. The reverse (year>=2024 AND flag=false) is legal — flag may be false because the venue is not a preprint server.",
      "if": {
        "properties": {
          "contamination_signals": {
            "type": "object",
            "properties": {
              "preprint_post_llm_inflection": { "const": true }
            },
            "required": ["preprint_post_llm_inflection"]
          }
        },
        "required": ["contamination_signals"]
      },
      "then": {
        "properties": {
          "year": { "minimum": 2024 }
        }
      }
    },
    {
      "description": "v3.9.0 spec §3.1 extends v3.7.3 §3.2 manual-entry exemption (codex round-3 F11 closure) symmetrically: when obtained_via='manual' the bibliography_agent SKIPS all three lookups (Semantic Scholar / OpenAlex / Crossref) and OMITS the three *_unmatched fields. The schema enforces this contract: a manual entry MUST NOT carry any of semantic_scholar_unmatched / openalex_unmatched / crossref_unmatched. Setting any to true on a manual entry would surface CONTAMINATED-* on a user-vouched reference. The asymmetry: preprint_post_llm_inflection IS still computed for manual entries (pure heuristic, no lookup) and is not in the exemption scope.",
      "if": {
        "properties": { "obtained_via": { "const": "manual" } },
        "required": ["obtained_via"]
      },
      "then": {
        "not": {
          "properties": {
            "contamination_signals": {
              "type": "object",
              "anyOf": [
                { "required": ["semantic_scholar_unmatched"] },
                { "required": ["openalex_unmatched"] },
                { "required": ["crossref_unmatched"] }
              ]
            }
          },
          "required": ["contamination_signals"]
        }
      }
    }
  ],

  "$defs": {
    "csl_name": {
      "oneOf": [
        { "$ref": "#/$defs/csl_personal_name" },
        { "$ref": "#/$defs/csl_literal_name" }
      ]
    },
    "csl_personal_name": {
      "type": "object",
      "additionalProperties": false,
      "required": ["family"],
      "properties": {
        "family":                { "type": "string", "minLength": 1 },
        "given":                 { "type": "string" },
        "suffix":                { "type": "string" },
        "dropping-particle":     { "type": "string" },
        "non-dropping-particle": { "type": "string" },
        "comma-suffix":          { "type": ["string", "boolean"] },
        "static-ordering":       { "type": ["string", "boolean"] },
        "parse-names":           { "type": ["string", "boolean"] }
      }
    },
    "csl_literal_name": {
      "type": "object",
      "additionalProperties": false,
      "required": ["literal"],
      "properties": {
        "literal": { "type": "string", "minLength": 1 }
      }
    }
  }
}
