{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://raw.githubusercontent.com/BourbonDog/amicus/main/schemas/council-tally.schema.json",
  "title": "amicus council-tally document",
  "description": "Deterministic council tally (`council tally --json`, amicus_council_tally). Council family v2.",
  "type": "object",
  "required": ["schemaVersion", "type", "meta", "judged", "streetCred", "findings", "runStats", "tierCounts"],
  "properties": {
    "schemaVersion": { "const": 2 },
    "type": { "const": "council-tally" },
    "meta": {
      "type": "object",
      "required": ["runId", "models"],
      "properties": {
        "runId": { "type": "string" },
        "runType": { "type": "string" },
        "date": { "type": "string" },
        "models": { "type": "array", "items": { "type": "string" } },
        "chair": { "type": ["string", "null"] },
        "claudeInCouncil": { "type": "boolean" },
        "seats": {
          "type": "array",
          "description": "v4.8, optional. The run's seat table, one {id, alias, role, lens, position} entry per BENCH seat in bench order, echoed verbatim from the tally input. The engine emits it only when the bench repeats an alias — the one case where the `alias#N` ids on findings[].raiserSeat, adjudications[].seat and runStats[].seat resolve to nothing else in the document, since meta.models is the ALIAS list. Bench-only: 'claude' is never a seat, so meta.models.length need not equal seats.length and the two must never be joined positionally. ABSENCE DOES NOT MEAN the bench had no repeated alias — hand-assembled and MCP-assembled input reaches tally() with no seat machinery behind it; it means only that no seat table is available. `items` is deliberately unconstrained: consumers must tolerate a table that is not an array of seat objects (the report and Workspace renderers fall back to alias space whole rather than failing), and the MCP input schema accepts the same latitude."
        }
      }
    },
    "judged": { "type": "boolean" },
    "streetCred": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["model"],
        "properties": {
          "model": { "type": "string" },
          "withSelf": { "type": ["number", "null"] },
          "peersOnly": { "type": ["number", "null"] },
          "perJudgeRank": {
            "type": "object",
            "description": "This row's rank position under each judge that ranked it. KEYED BY THE JUDGE'S SEAT ID where the judge has one that differs from its alias, and by the judge's alias otherwise (v4.8 T3.3) — the same emit-when-DIFFERENT rule as every other seat field in this document, so a bench with no repeated alias is keyed exactly as before. Before T3.3 the key was always the alias, which last-wins-collapsed two twin judges into one entry while withSelf/peersOnly averaged over both, so the map and the averages could disagree about the same row. ⚠️ They can still disagree on hand-assembled input that repeats a judge alias and carries no seat, because nothing in such a document can tell those judges apart."
          },
          "seat": {
            "type": "string",
            "description": "v4.8, optional. One row per SEAT rather than per alias, and this row's seat id, when it differs from `model` (its alias). SHIPPED at v4.8 T3.3 — street-cred.js :: computeStreetCred is the producer; this property was DECLARED one task earlier, at T3.2, and read 'documentation-only ahead of its producer' until T3.3 landed. The engine emits it only when the bench repeats an alias, so a unique-alias run carries no seat on any row and its document is byte-identical to a pre-v4.8 one. verdict.js :: buildVerdict carries it through its own closed streetCred projection, so the field is never silently stripped. No `additionalProperties: false` above means an additive field was always validated here even before this declaration; this documents the shape rather than changing what is accepted."
          }
        }
      }
    },
    "findings": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["id", "tier", "basis", "confidence"],
        "properties": {
          "id": { "type": "string" },
          "raiser": { "type": ["string", "null"] },
          "severity": { "type": ["string", "null"] },
          "tier": { "enum": ["Confirmed", "Contested", "Singleton", "Disputed"] },
          "basis": {
            "type": "object",
            "required": ["a", "d", "n"],
            "properties": { "a": { "type": "number" }, "d": { "type": "number" }, "n": { "type": "number" } }
          },
          "confidence": { "enum": ["thin", "solid"] },
          "tierOverride": { "type": ["object", "null"] },
          "adjudications": { "type": "array", "items": { "type": "object" } },
          "raiserSeat": {
            "type": ["string", "null"],
            "description": "v4.8, optional. The raising SEAT's id (e.g. 'deepseek#1'), echoed from the input finding. Emitted only when it differs from the raiser's alias, so a bench with no repeated alias never carries it; `raiser` stays the alias in every case. Resolve it against meta.seats."
          },
          "location": {
            "type": "string",
            "description": "v4.8, optional (SI-23). Echoed verbatim from the input finding when present; absent when the input finding carried none, so a pre-SI-23 document is byte-identical. tally() does not read it for tiering — it exists here only because the input schema (mcp-tools.js's amicus_council_tally) now preserves it instead of silently dropping it, matching what anonymize.js :: toGlobalFindings already carries on the engine's own path."
          },
          "claim": {
            "type": "string",
            "description": "v4.8, optional (SI-23 fix round 1, PR #183 council A1/B1). Echoed verbatim from the input finding when present, the same emit-when-present rule as `location` immediately above; absent when the input finding carried none. `claim` was already declared on the amicus_council_tally input schema before SI-23 R10 — R10 fixed `location`'s identical drop in this same outFindings map and left this sibling gap for a later round, closed here."
          },
          "sameModelCorroboration": {
            "const": true,
            "description": "v4.8, optional, TRUE ONLY (never emitted as false, so a document without it is byte-identical to a pre-v4.8 one). Set when, after the seat-aware raiser exclusion, at least one AGREEING peer of this finding shares the raiser's alias — corroboration that is real but came from another seat of the same model, so it is not independent. WRONG IN BOTH DIRECTIONS, by construction: the comparison is on the ALIAS only. It MISSES one model reached under two aliases (e.g. --models gpt-5,openai/gpt-5), because votes carry no resolved-executable id. And it FIRES FALSELY on a SPLIT alias — one alias whose two seats resolved to different executables — telling a reader to discount a genuinely independent cross-executable corroboration. The reliability ledger keys identity on (alias, resolvedModel) for the same run, so the two documents can disagree about what 'the same model' means. Treat this as 'worth a second look', never as proof."
          },
          "unattributedPeerDrops": {
            "type": "integer",
            "minimum": 1,
            "description": "v4.8, optional, EMITTED ONLY WHEN NON-ZERO (never emitted as 0, so a run that does not orphan exactly one side of a twin pair produces a document byte-identical to a pre-T-B2 one; the minimum states that emit rule rather than merely describing it). How many of this finding adjudications the peer split excluded WITHOUT being able to attribute them. TWO SHAPES produce it. (1) RAISER NAMED: excluded on the ALIAS branch while exactly ONE side of the pair carried a seat id - the finding has a raiserSeat and the vote has none, or the reverse; a seat-less vote bearing the raiser own alias cannot be told apart from the raiser own vote, so it is dropped. (2) RAISER NOT NAMED (v4.8 T-B4): the finding raiser is empty or missing and so is the vote judge, so the vote may be the unnamed raiser own and is not counted as peer signal. NEITHER SHAPE includes a vote the SEAT IDS settled: when the vote and the finding both carry a seat id the engine knows whose vote it is, so that exclusion is attributed rather than ambiguous and is never counted here. WARNING - on shape (1) this is an ANNOUNCEMENT, not a repair: that vote stays excluded, basis does not count it, and tier/confidence are computed without it, by owner ruling (mark explicitly, attribute nothing). On shape (2) the exclusion IS the change - those votes used to be counted, which let an unnamed raiser corroborate its own finding, so basis and tier can both move. Read it as: N votes on this finding were excluded without anyone being able to attribute them - each is EITHER a real peer signal being discarded OR the raiser own vote being correctly excluded, and the document does not say which, because nothing in it can. Treat it as a POSSIBLE undercount of up to N, never as an established one; not being able to tell is why the drop is announced at all. The defense brief computes the same number from the same function, so the two documents cannot disagree."
          },
          "debate": {
            "type": "object",
            "properties": {
              "action": { "enum": ["defended", "amended", "withdrawn", "no-response"] },
              "previousTier": { "type": ["string", "null"] }
            },
            "required": ["action"],
            "additionalProperties": false
          }
        }
      }
    },
    "runStats": { "type": "array", "items": { "type": "object" } },
    "tierCounts": {
      "type": "object",
      "required": ["Confirmed", "Contested", "Singleton", "Disputed"],
      "properties": {
        "Confirmed": { "type": "number" },
        "Contested": { "type": "number" },
        "Singleton": { "type": "number" },
        "Disputed": { "type": "number" }
      }
    }
  }
}
