{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://raw.githubusercontent.com/yaniv-golan/cowork-harness/main/schema/verify-cassettes.json",
  "title": "VerifyCassettesEnvelope",
  "description": "Structured stdout of `cowork-harness verify-cassettes --output-format json` (SPEC \u00a711.1) \u2014 the token-free CI gate over committed cassettes (privacy scan + staleness). A covered surface of the 1.0 compatibility contract (SPEC \u00a712): parse this, not the human-readable text output.",
  "type": "object",
  "required": ["tool", "version", "command", "ok", "coverage", "results", "error"],
  "properties": {
    "tool": { "const": "cowork-harness", "description": "Shared machine-envelope discriminator (SPEC §11)." },
    "version": { "type": "string", "description": "cowork-harness package version that produced this envelope." },
    "error": {
      "type": ["object", "null"],
      "description": "null on success (verify-cassettes reports per-cassette failures inside results[], not here); the shared error-envelope object on a top-level usage/runtime failure."
    },
    "command": { "const": "verify-cassettes" },
    "ok": {
      "type": "boolean",
      "description": "false if any real privacy finding (cls != 'unscanned'), staleness message, unverifiable-class staleness, version mismatch, or unreadable cassette. Mirrors whether the exit code is non-zero, but NOT which one: exit 0 (ok:true, clean); exit 1 (ok:false, verification RAN and found a real problem — findings[], staleness[], or scenarioDrift[]); exit 3 (ok:false, verification could NOT complete — unverifiable[], version[], or error). A real finding always wins exit 1 over a co-occurring exit-3 signal in the same run."
    },
    "coverage": {
      "type": "object",
      "description": "Which scans actually ran \u2014 false under --skip-privacy / --skip-staleness / --skip-scenario-drift. A consumer gating on ok:true should also assert the coverage it expects (a skipped scan can't fail).",
      "required": ["privacy", "staleness", "scenarioDrift"],
      "properties": {
        "privacy": { "type": "boolean" },
        "staleness": { "type": "boolean" },
        "scenarioDrift": { "type": "boolean" }
      }
    },
    "results": {
      "type": "array",
      "description": "One entry per cassette file, including unreadable ones (tallied via `error`, never a crash that aborts the batch).",
      "items": {
        "type": "object",
        "required": ["file", "findings", "staleness", "unverifiable", "notes", "version", "scenarioDrift", "privacyScanned"],
        "properties": {
          "file": { "type": "string" },
          "findings": {
            "type": "array",
            "description": "Privacy-scan findings. cls 'unscanned' (a hash-only >cap artifact body \u2014 nothing committed to leak) is reported but does NOT fail the gate; every other class does.",
            "items": {
              "type": "object",
              "required": ["where", "cls", "sample"],
              "properties": {
                "where": { "type": "string" },
                "cls": {
                  "type": "string",
                  "enum": ["email", "currency", "domain", "path", "machine-inventory", "host-inventory", "unscanned", "binary"]
                },
                "sample": { "type": "string" }
              }
            }
          },
          "staleness": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Genuine drift messages \u2014 a StalenessFinding whose class is NOT 'unverifiable-*' (e.g. baseline/skill/shared-root/format/resolved-tier/prompt-assets), plus the session-shape fingerprint drift string. Verification RAN and found a real problem: exit 1. Empty under --skip-staleness. See `unverifiable` for the could-not-verify counterpart."
          },
          "unverifiable": {
            "type": "array",
            "items": { "type": "string" },
            "description": "StalenessFinding messages whose class IS 'unverifiable-*' (e.g. unverifiable-baseline/unverifiable-skill/unverifiable-tier/unverifiable-prompt-assets) \u2014 verification could NOT complete, distinct from a genuine `staleness` drift finding: exit 3 (unless the same run also carries a `staleness`/`findings`/`scenarioDrift` entry, in which case that real finding wins exit 1). Empty under --skip-staleness."
          },
          "notes": {
            "type": "array",
            "items": { "type": "string" },
            "description": "NON-failing informational channel (never affects ok / the exit code) \u2014 e.g. a pre-effectiveFidelity cassette with an explicit tier: statically knowable, nothing baseline-dependent to verify. Text output renders these as `\u00b7` rows."
          },
          "version": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Format/version failures: the cassette was written by a NEWER harness than this one understands. Always fails the gate (can't verify \u21d2 not green; exit 3, the could-not-verify bucket), independent of --skip-staleness."
          },
          "scenarioDrift": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Scenario-content (prompt) drift: the committed on-disk scenario's prompt diverged from the cassette's frozen prompt. A definite divergence \u2014 each message fails the gate (own bucket so --skip-staleness can't mask it; exit 1, a real finding). Empty under --skip-scenario-drift, or when the source isn't resolvable (that becomes a non-failing `notes` entry instead)."
          },
          "privacyScanned": {
          "type": "boolean",
          "description": "Whether the privacy scan actually RAN on this file. The scan needs a readable transcript (an `events` array of strings), NOT a valid cassette: a file that fails shape validation is still scanned, and reports both its findings and its `error`. So `error` does not answer 'was this checked' — this does. false means the scan could not run at all (unreadable JSON, no `events`, a crash mid-verify) or was disabled with --skip-privacy; in either case zero `findings` is an absence of evidence, not evidence of absence. A gate that treats 'could not verify' as 'verified clean' should key on this."
        },
        "error": {
            "type": "string",
            "description": "Present when the cassette file itself is malformed/unreadable (or its per-file check crashed); the file is tallied as a could-not-verify failure (exit 3) and the batch continues."
          }
        }
      }
    }
  }
}
