{
  "$defs": {
    "ActorView": {
      "additionalProperties": false,
      "description": "One Showrunner-controlled actor in view (breadsandcircuits-lhin).\n\nReplaces `WardenView`: the kernel's `wardens` slice became `actors` when\nthe Warden became a ROLE on one `Actor` kind marker (docs/specs/\nnpc-cast.md \u00a78), and `role` is the field that says which. Retired rather\nthan aliased, because a deprecated twin would put two names for one\nslice into every contestant prompt on every turn.",
      "properties": {
        "hp": {
          "title": "Hp",
          "type": "integer"
        },
        "id": {
          "title": "Id",
          "type": "integer"
        },
        "role": {
          "title": "Role",
          "type": "string"
        },
        "zone": {
          "title": "Zone",
          "type": "integer"
        }
      },
      "required": [
        "id",
        "role",
        "zone",
        "hp"
      ],
      "title": "ActorView",
      "type": "object"
    },
    "AllianceView": {
      "additionalProperties": false,
      "description": "One alliance the VIEWER is a party to, SCOPED (kernel.social.Contract).\n\n`OfferView`'s scoping note applies verbatim \u2014 nothing here re-filters by\nparty, because the kernel's `observe` already decided who may read what.\n\nThe parties are stored in the kernel's CANONICAL order (`party_a <\nparty_b`) and mirrored that way rather than re-labelled proposer/target: an\nalliance is symmetric, and one relationship with two byte forms is what\nthat ordering exists to refuse (breadsandcircuits-22o). `proposed_by` is\nthe separate fact of WHO ASKED (breadsandcircuits-cju), which the kernel\ndoes record, and which is what tells a viewer whether a proposal is theirs\nto accept or only to withdraw \u2014 it is mirrored rather than derived, because\nderiving it from the pair is exactly the invented asymmetry the canonical\norder forbids.\n\n`render_prompt` names both parties in this canonical order rather than\n\"who proposed\" (breadsandcircuits-93p); `proposed_by` is rendered as the\nseparate fact it is. What answering a pact looks like from the harness side\nstays out of scope here: the kernel's verbs are the only intake, and\n`AgentAction` passes them through opaquely.",
      "properties": {
        "formed_turn": {
          "title": "Formed Turn",
          "type": "integer"
        },
        "id": {
          "title": "Id",
          "type": "integer"
        },
        "party_a": {
          "title": "Party A",
          "type": "integer"
        },
        "party_b": {
          "title": "Party B",
          "type": "integer"
        },
        "proposed_by": {
          "title": "Proposed By",
          "type": "integer"
        },
        "status": {
          "title": "Status",
          "type": "string"
        },
        "until_turn": {
          "title": "Until Turn",
          "type": "integer"
        }
      },
      "required": [
        "id",
        "party_a",
        "party_b",
        "proposed_by",
        "formed_turn",
        "until_turn",
        "status"
      ],
      "title": "AllianceView",
      "type": "object"
    },
    "BetrayalView": {
      "additionalProperties": false,
      "description": "One recorded betrayal (kernel.social.Betrayal), as BOTH views publish it.\n\nThe kernel emits exactly one shape here, which makes this the\n`AllianceView`/`PublicAllianceView` pair's opposite and for the reason the\nkernel gives: a betrayal is PUBLIC the instant it lands, so there is nothing\nto scope and no second shape to discriminate (breadsandcircuits-6fg).\n\n`breaker` and `victim` are mirrored in the order the kernel stores them,\nNEVER canonicalised: unlike an alliance's parties these two play different\nroles, and re-sorting them would erase the one fact the record exists to\ncarry. The kernel's `contract_ref` is deliberately absent from the payload \u2014\nit is a store handle rather than a public name.\n\n`render_prompt` names both ends in that stored order (breadsandcircuits-93p)\n\u2014 \"13 betrayed 14\" and \"14 betrayed 13\" are opposite events. What a betrayal\nMEANS for reputation is not computed here: the kernel already charged the\nbreaker's score, and `ReputationView` carries the result, so a second\nderivation in the harness would be two answers to one question.",
      "properties": {
        "breaker": {
          "title": "Breaker",
          "type": "integer"
        },
        "turn": {
          "title": "Turn",
          "type": "integer"
        },
        "victim": {
          "title": "Victim",
          "type": "integer"
        }
      },
      "required": [
        "breaker",
        "victim",
        "turn"
      ],
      "title": "BetrayalView",
      "type": "object"
    },
    "ContestantView": {
      "additionalProperties": false,
      "properties": {
        "hp": {
          "title": "Hp",
          "type": "integer"
        },
        "id": {
          "title": "Id",
          "type": "integer"
        },
        "zone": {
          "title": "Zone",
          "type": "integer"
        }
      },
      "required": [
        "id",
        "zone",
        "hp"
      ],
      "title": "ContestantView",
      "type": "object"
    },
    "DeckView": {
      "additionalProperties": false,
      "description": "How much caprice the house has left (kernel.deck.Deck, narrowed).\n\n`plays_remaining` and NOTHING else, in both views: the deck's remaining\ncards are deliberately not published live (spec \u00a78's commit-and-reveal),\nso a mirror that grew a `cards` field would be mirroring a leak. The field\non `Observation` below is `None` in an un-decked match \u2014 a different fact\nfrom a deck with nothing left, on the `recommendation` key's rule.",
      "properties": {
        "plays_remaining": {
          "title": "Plays Remaining",
          "type": "integer"
        }
      },
      "required": [
        "plays_remaining"
      ],
      "title": "DeckView",
      "type": "object"
    },
    "DefectionView": {
      "additionalProperties": false,
      "description": "One recorded defection (kernel.social.Defection), as BOTH views publish it.\n\n`BetrayalView`'s situation and its reasoning \u2014 one kernel shape, nothing to\nscope, so no union \u2014 arriving at the guild (breadsandcircuits-zkgr,\ndocs/specs/guilds-shared-stash.md \u00a711). It is deliberately NOT given the\n`guilds` key's two-shape treatment one class up: what is IN a vault is\nmembers-only, but somebody walking out of it with more than they brought is\npublic the instant it lands, because the members left behind watch the vault\nempty in their own entry.\n\nFOUR FIELDS, and the two differences from `BetrayalView` are both the\nkernel's: there is no `victim`, because a guild departure leaves whoever is\nstill on the roster and that is derivable from `guild` rather than a fact\nabout the action; and there IS a `net_taken`, because it is what separates\nthe two records the show now has to publish side by side. A `Betrayal` is a\ndeclared walk-out from a pact whose only cost is reputation; a `Defection`\nis a walk-out with goods, and the count is the evidence.\n\n`net_taken` is mirrored as a plain `int` and is always positive upstream\n(`Defection.__post_init__` refuses anything else), which is NOT re-asserted\nhere on `ReputationView`'s reasoning: this file mirrors what the kernel\npublishes, and a second copy of a kernel invariant is a place for the two to\ndisagree.",
      "properties": {
        "breaker": {
          "title": "Breaker",
          "type": "integer"
        },
        "guild": {
          "title": "Guild",
          "type": "integer"
        },
        "net_taken": {
          "title": "Net Taken",
          "type": "integer"
        },
        "turn": {
          "title": "Turn",
          "type": "integer"
        }
      },
      "required": [
        "breaker",
        "guild",
        "turn",
        "net_taken"
      ],
      "title": "DefectionView",
      "type": "object"
    },
    "DungeonView": {
      "additionalProperties": false,
      "description": "Shared floor progress; no hidden item locations or private inventories.",
      "properties": {
        "descended": {
          "items": {
            "type": "integer"
          },
          "title": "Descended",
          "type": "array"
        },
        "exit_open": {
          "title": "Exit Open",
          "type": "boolean"
        },
        "exit_zone": {
          "title": "Exit Zone",
          "type": "integer"
        },
        "floor": {
          "title": "Floor",
          "type": "integer"
        },
        "floors": {
          "title": "Floors",
          "type": "integer"
        },
        "turns_remaining": {
          "title": "Turns Remaining",
          "type": "integer"
        }
      },
      "required": [
        "floor",
        "floors",
        "turns_remaining",
        "exit_open",
        "exit_zone",
        "descended"
      ],
      "title": "DungeonView",
      "type": "object"
    },
    "ExitClaimView": {
      "additionalProperties": false,
      "description": "A guild's spent claim on the Exit (kernel.social.ExitClaim).\n\nPresent only inside a member's own guild entry, and `None` there while the\nclaim is unspent \u2014 the kernel publishes the key either way\n(breadsandcircuits-qt94, docs/specs/exclusive-exit-claim.md \u00a73), and this\nfile's discipline is to mirror exactly what the kernel says.\n\nNo `guild` field, unlike the kernel component: the component carries one so\nit is self-describing in a component stream, but here it is nested inside\nthe very guild entry that names it, and a second copy of the id would be two\nbyte forms of one fact in a single payload \u2014 the rule the kernel's own\nguilds slice applies when it excludes the viewer's guild from the public\nhalf.",
      "properties": {
        "claimant": {
          "title": "Claimant",
          "type": "integer"
        },
        "turn": {
          "title": "Turn",
          "type": "integer"
        }
      },
      "required": [
        "claimant",
        "turn"
      ],
      "title": "ExitClaimView",
      "type": "object"
    },
    "GroundItemView": {
      "additionalProperties": false,
      "properties": {
        "id": {
          "title": "Id",
          "type": "integer"
        },
        "kind": {
          "title": "Kind",
          "type": "string"
        },
        "zone": {
          "title": "Zone",
          "type": "integer"
        }
      },
      "required": [
        "id",
        "kind",
        "zone"
      ],
      "title": "GroundItemView",
      "type": "object"
    },
    "GuildInviteView": {
      "additionalProperties": false,
      "description": "One invitation the viewer is an END of (kernel.social.GuildInvite).\n\n`OfferView`'s confidentiality situation exactly, which is what the kernel's\nspec \u00a711 says it is: an invitation is addressed, so it reaches its two ends\nand the global view carries none. ONE shape, therefore \u2014 there is no public\nhalf to discriminate against, unlike the guilds key above.\n\nAny status, mirrored as a plain `str` on `OfferView`'s reasoning: a contestant\nwhose invitation was rejected is entitled to know it was rejected, and a\nfifth status added upstream must parse rather than fail the whole\nobservation.",
      "properties": {
        "expires_turn": {
          "title": "Expires Turn",
          "type": "integer"
        },
        "guild": {
          "title": "Guild",
          "type": "integer"
        },
        "id": {
          "title": "Id",
          "type": "integer"
        },
        "invitee": {
          "title": "Invitee",
          "type": "integer"
        },
        "inviter": {
          "title": "Inviter",
          "type": "integer"
        },
        "status": {
          "title": "Status",
          "type": "string"
        },
        "turn": {
          "title": "Turn",
          "type": "integer"
        }
      },
      "required": [
        "id",
        "guild",
        "inviter",
        "invitee",
        "turn",
        "expires_turn",
        "status"
      ],
      "title": "GuildInviteView",
      "type": "object"
    },
    "GuildLedgerRow": {
      "additionalProperties": false,
      "description": "One member's vault ledger inside a member's own guild entry.\n\nTwo counts, not an item list (kernel.social.GuildMember): the question the\nguild mechanic asks is \"did you take more than you brought\", which is\narithmetic on two integers, moved one at a time by `deposit` and\n`withdraw` (breadsandcircuits-myue).",
      "properties": {
        "contestant": {
          "title": "Contestant",
          "type": "integer"
        },
        "deposited": {
          "title": "Deposited",
          "type": "integer"
        },
        "withdrawn": {
          "title": "Withdrawn",
          "type": "integer"
        }
      },
      "required": [
        "contestant",
        "deposited",
        "withdrawn"
      ],
      "title": "GuildLedgerRow",
      "type": "object"
    },
    "GuildView": {
      "additionalProperties": false,
      "description": "The viewer's OWN guild, in full (kernel.social.Guild).\n\n`AllianceView`'s scoping note applies verbatim \u2014 nothing here re-filters,\nbecause the kernel's `observe` already decided who may read what \u2014 with one\ndifference that matters for the union below: the kernel's guilds slice splits\nits two shapes on MEMBERSHIP rather than on status (breadsandcircuits-yehz,\ndocs/specs/guilds-shared-stash.md \u00a711), so a scoped observation carries this\nshape for at most one guild and `PublicGuildView` for all the others.\n\n`members` is the ROSTER as the kernel derived it, ascending, and it is\nmirrored as a plain list rather than reconstructed from `ledger`: the kernel\nstores membership in exactly one place (a `GuildMember` per contestant) and\npublishes it in exactly one order, so a harness that rebuilt the roster from\nanother key would be a second implementation of the one thing that design\ndeliberately has only one of.\n\n`vault` (breadsandcircuits-myue) is REQUIRED, not optional, now that the\nkernel emits it on every member entry: an optional field could not tell\n\"empty vault\" from \"the kernel stopped publishing one\", and this file's\ndiscipline is to mirror exactly what the kernel says. Ascending item id,\nas published.\n\n`claim` (breadsandcircuits-qt94) is REQUIRED AND NULLABLE, which is a\ndifferent thing from optional and is the distinction that matters here. The\nkernel emits the key on every member entry, carrying `None` while the\nguild's claim is unspent; a field defaulted to `None` would accept a payload\nthat omitted it, and \"this guild has not opened the Exit\" would become\nindistinguishable from \"this kernel does not publish claims\" \u2014 the exact\nambiguity `vault` was made required to avoid, on a key where the wrong\nreading sends a contestant to a Gate that will not open for it.",
      "properties": {
        "claim": {
          "anyOf": [
            {
              "$ref": "#/$defs/ExitClaimView"
            },
            {
              "type": "null"
            }
          ]
        },
        "formed_turn": {
          "title": "Formed Turn",
          "type": "integer"
        },
        "home_zone": {
          "title": "Home Zone",
          "type": "integer"
        },
        "id": {
          "title": "Id",
          "type": "integer"
        },
        "ledger": {
          "items": {
            "$ref": "#/$defs/GuildLedgerRow"
          },
          "title": "Ledger",
          "type": "array"
        },
        "members": {
          "items": {
            "type": "integer"
          },
          "title": "Members",
          "type": "array"
        },
        "status": {
          "title": "Status",
          "type": "string"
        },
        "until_turn": {
          "title": "Until Turn",
          "type": "integer"
        },
        "vault": {
          "items": {
            "$ref": "#/$defs/VaultItemView"
          },
          "title": "Vault",
          "type": "array"
        }
      },
      "required": [
        "id",
        "home_zone",
        "formed_turn",
        "until_turn",
        "status",
        "members",
        "vault",
        "claim",
        "ledger"
      ],
      "title": "GuildView",
      "type": "object"
    },
    "HeldItemView": {
      "additionalProperties": false,
      "properties": {
        "holder": {
          "title": "Holder",
          "type": "integer"
        },
        "id": {
          "title": "Id",
          "type": "integer"
        },
        "kind": {
          "title": "Kind",
          "type": "string"
        }
      },
      "required": [
        "id",
        "kind",
        "holder"
      ],
      "title": "HeldItemView",
      "type": "object"
    },
    "InterventionView": {
      "additionalProperties": false,
      "description": "One card the house played (kernel.deck.Intervention), as BOTH views\npublish it \u2014 the Editor's deck's honesty contract\n(docs/specs/editors-deck.md \u00a78, breadsandcircuits-8aj8).\n\n`BetrayalView`'s situation: one kernel shape, nothing to scope, so no\nunion. The kernel publishes the same rows to a spectator and to every\ncontestant, by design \u2014 a secret intervention is the rigging failure\nitself, and the \"will they take the bait\" beat needs a contestant to know\nthe house acted.\n\n`trigger` is mirrored AND rendered, unlike `RecommendationView.tag`, and\nthe asymmetry is the whole point of the field: the tag is the house's\nprivate label for bait, while the trigger is the house's PUBLIC reason\nfor acting \u2014 the thing the Showrunner's narration must never contradict\n(spec \u00a712) and the thing the audience checks a play against. `zone` and\n`target` are 0 where the card names no zone or no entity; what kind of\nentity `target` names is the card's business (`_INTERVENTION_TARGET_NOUNS`).\nD2's bounty, mercy window and surge name a zone; lockdown names an edge.",
      "properties": {
        "card": {
          "title": "Card",
          "type": "string"
        },
        "id": {
          "title": "Id",
          "type": "integer"
        },
        "target": {
          "title": "Target",
          "type": "integer"
        },
        "trigger": {
          "title": "Trigger",
          "type": "string"
        },
        "turn": {
          "title": "Turn",
          "type": "integer"
        },
        "zone": {
          "title": "Zone",
          "type": "integer"
        }
      },
      "required": [
        "id",
        "turn",
        "card",
        "trigger",
        "zone",
        "target"
      ],
      "title": "InterventionView",
      "type": "object"
    },
    "ItemsView": {
      "additionalProperties": false,
      "properties": {
        "ground": {
          "items": {
            "$ref": "#/$defs/GroundItemView"
          },
          "title": "Ground",
          "type": "array"
        },
        "held": {
          "items": {
            "$ref": "#/$defs/HeldItemView"
          },
          "title": "Held",
          "type": "array"
        }
      },
      "required": [
        "ground",
        "held"
      ],
      "title": "ItemsView",
      "type": "object"
    },
    "MapView": {
      "additionalProperties": false,
      "description": "Zone skeleton (always all six) plus the edges visible to the viewer.",
      "properties": {
        "edges": {
          "items": {
            "maxItems": 2,
            "minItems": 2,
            "prefixItems": [
              {
                "type": "integer"
              },
              {
                "type": "integer"
              }
            ],
            "type": "array"
          },
          "title": "Edges",
          "type": "array"
        },
        "zones": {
          "items": {
            "$ref": "#/$defs/ZoneView"
          },
          "title": "Zones",
          "type": "array"
        }
      },
      "required": [
        "zones",
        "edges"
      ],
      "title": "MapView",
      "type": "object"
    },
    "MessageView": {
      "additionalProperties": false,
      "description": "One public broadcast, without its private-traffic-sensitive store id.",
      "properties": {
        "speaker": {
          "title": "Speaker",
          "type": "integer"
        },
        "text": {
          "title": "Text",
          "type": "string"
        },
        "turn": {
          "title": "Turn",
          "type": "integer"
        },
        "zone": {
          "title": "Zone",
          "type": "integer"
        }
      },
      "required": [
        "speaker",
        "zone",
        "turn",
        "text"
      ],
      "title": "MessageView",
      "type": "object"
    },
    "OfferView": {
      "additionalProperties": false,
      "description": "One trade proposal the VIEWER is party to (kernel.social.Offer).\n\n`PrivateMessageView`'s scoping note applies verbatim \u2014 present only in a\nscoped observation, and nothing here re-filters by party, because the\nkernel's `observe` already decided who may read what.\n\nThe field asymmetry is the kernel's and is mirrored rather than smoothed\nover: `give` holds item entity IDS the proposer holds, `request` holds item\nKINDS, because no contestant can see another's inventory well enough to\nname its ids (breadsandcircuits-bpc). A mirror that \"helpfully\" made both\nsides ids would be inventing knowledge the agent does not have.\n\n`give_items` is that same rule read the other way (breadsandcircuits-x1iv,\ndocs/decisions.md 2026-08-15): the KERNEL now resolves each `give` id to a\nkind for the two parties of the offer, so the recipient can be told what it\nis being handed without this module inventing anything. Mirrored beside\n`give` rather than replacing it, exactly as the kernel publishes both \u2014\n`give` stays the offer's own tuple, `give_items` is its resolution.\n\nHOW A PROPOSAL READS is breadsandcircuits-3vw's call (docs/decisions.md,\n2026-08-11), and the ruling is: every offer the kernel hands this viewer\nrenders, whatever its status. Resolved ones are NOT filtered to the pending\nones, because an answer is the only feedback a proposer gets \u2014 an agent that\nproposed a swap and reads nothing back cannot tell an accepted offer from one\nstill on the table. The deadline is the one thing that does NOT print on a\nresolved entry;\n`harness.schemas._render_deadline` says why.\n\nAuthoring needs nothing here: an `offer` is submitted as an `AgentAction`\nlike any other verb, and `kernel.actions._check_offer` owns every rule about\nwhat may be in `give` and `request` \u2014 this module models no per-verb args.\n\nThe line no longer grows for the whole match, though the kernel still never\nprunes a resolved offer: `_log_line` windows it to the recent tail\n(breadsandcircuits-sxv0), under the one policy that governs every log-like\nline here rather than one picked for offers alone. What that policy owes\nTHIS slice specifically is `_offer_is_live` \u2014 a `pending` offer renders\nhowever old it is, because it is the one entry on these lines that is a\nquestion rather than a record.\n\nThe window does not reopen the 3vw ruling above, and the distinction is the\nwhole of how the two fit together: STATUS is still not a criterion \u2014 no\nresolved offer is dropped for being resolved, and a resolved entry inside\nthe window prints exactly as before \u2014 while RECENCY is one, and it applies\nto every status alike, `pending` excepted in the viewer's favour.\n\nOne case is where those two do rub, and it is recorded rather than papered\nover (docs/decisions.md, 2026-08-16). The window orders by an offer's OWN\nturn, because that is the only time the payload carries; an offer made\nearly and answered late therefore sits at the head, so under heavy traffic\na proposer can lose the answer 3vw wanted it to have while that answer is\nstill news. Fixing it properly needs a RESOLUTION turn the kernel does not\npublish (breadsandcircuits-yn66) \u2014 inventing one here from the status would\nbe the harness deriving a fact it cannot see.",
      "properties": {
        "expires_turn": {
          "title": "Expires Turn",
          "type": "integer"
        },
        "give": {
          "items": {
            "type": "integer"
          },
          "title": "Give",
          "type": "array"
        },
        "give_items": {
          "items": {
            "$ref": "#/$defs/OfferedItemView"
          },
          "title": "Give Items",
          "type": "array"
        },
        "id": {
          "title": "Id",
          "type": "integer"
        },
        "proposer": {
          "title": "Proposer",
          "type": "integer"
        },
        "recipient": {
          "title": "Recipient",
          "type": "integer"
        },
        "request": {
          "items": {
            "type": "string"
          },
          "title": "Request",
          "type": "array"
        },
        "status": {
          "title": "Status",
          "type": "string"
        },
        "turn": {
          "title": "Turn",
          "type": "integer"
        }
      },
      "required": [
        "id",
        "proposer",
        "recipient",
        "turn",
        "expires_turn",
        "give",
        "give_items",
        "request",
        "status"
      ],
      "title": "OfferView",
      "type": "object"
    },
    "OfferedItemView": {
      "additionalProperties": false,
      "description": "One entry of an offer's `give`, resolved (kernel.api.observe, x1iv).\n\n`kind` is None where the id names no live Item \u2014 the kernel says so rather\nthan dropping the entry, because a give item that has been consumed makes\nthe whole trade unfulfillable, and that is the fact the party answering\nmost needs. `harness.schemas._render_offered_item` is where it prints.",
      "properties": {
        "id": {
          "title": "Id",
          "type": "integer"
        },
        "kind": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "title": "Kind"
        }
      },
      "required": [
        "id",
        "kind"
      ],
      "title": "OfferedItemView",
      "type": "object"
    },
    "PrivateMessageView": {
      "additionalProperties": false,
      "description": "One whisper the VIEWER is party to (kernel.social.PrivateMessage).\n\nPresent only in a scoped observation: the kernel's global view carries no\nprivate message at all, so a payload parsed here either belongs to the\ncontestant this prompt is being built for or is empty. Nothing in this\nmodule re-filters by recipient \u2014 the kernel's `observe` already decided\nwho may read what, and a second, weaker copy of that rule here would be a\nplace for the two to disagree.",
      "properties": {
        "id": {
          "title": "Id",
          "type": "integer"
        },
        "recipient": {
          "title": "Recipient",
          "type": "integer"
        },
        "speaker": {
          "title": "Speaker",
          "type": "integer"
        },
        "text": {
          "title": "Text",
          "type": "string"
        },
        "turn": {
          "title": "Turn",
          "type": "integer"
        }
      },
      "required": [
        "id",
        "speaker",
        "recipient",
        "turn",
        "text"
      ],
      "title": "PrivateMessageView",
      "type": "object"
    },
    "PublicAllianceView": {
      "additionalProperties": false,
      "description": "One ACTIVE alliance as the GLOBAL view publishes it.\n\nThe kernel's alliances slice is the one key whose two views answer\ndifferent questions rather than the same one under a filter\n(breadsandcircuits-cju): a scoped view carries the viewer's own contracts at\nany status, in full; the global view carries every ACTIVE contract as the\npublic triple, because a FORMED alliance is spectator content (plan.md \u00a77)\nwhile an unanswered approach is not.\n\nSo this is a SECOND shape, not a subset the same model could parse\nleniently. Modelled as its own strict class rather than by making\n`AllianceView`'s extra fields optional, for `extra=\"forbid\"`'s reason: an\noptional field cannot tell \"the global view does not publish this\" from\n\"the kernel stopped emitting it\", and one of those is a bug the mirror\nexists to make loud. `status` is absent because it is `active` by\nconstruction, and the entity id because it is a store handle rather than a\npublic name.",
      "properties": {
        "party_a": {
          "title": "Party A",
          "type": "integer"
        },
        "party_b": {
          "title": "Party B",
          "type": "integer"
        },
        "until_turn": {
          "title": "Until Turn",
          "type": "integer"
        }
      },
      "required": [
        "party_a",
        "party_b",
        "until_turn"
      ],
      "title": "PublicAllianceView",
      "type": "object"
    },
    "PublicGuildView": {
      "additionalProperties": false,
      "description": "One ACTIVE guild as a non-member \u2014 or the GLOBAL view \u2014 receives it.\n\n`PublicAllianceView`'s situation and its reasoning for being a SECOND strict\nclass rather than a lenient reading of `GuildView`. What the kernel publishes\nhere is deliberately narrow: who has teamed up and where their vault stands\n(spec \u00a711). A contestant that could not see that a guild EXISTS could never\nask to join one, which is why this is published at all; what is IN the vault\nand who has been drawing on it stays with the membership.\n\n`status` is absent because it is `active` by construction, and `formed_turn`\nbecause it is bookkeeping \u2014 `PublicAllianceView`'s omissions exactly.",
      "properties": {
        "home_zone": {
          "title": "Home Zone",
          "type": "integer"
        },
        "id": {
          "title": "Id",
          "type": "integer"
        },
        "members": {
          "items": {
            "type": "integer"
          },
          "title": "Members",
          "type": "array"
        },
        "until_turn": {
          "title": "Until Turn",
          "type": "integer"
        }
      },
      "required": [
        "id",
        "home_zone",
        "members",
        "until_turn"
      ],
      "title": "PublicGuildView",
      "type": "object"
    },
    "RecommendationView": {
      "additionalProperties": false,
      "description": "The house's current advice to the VIEWER (kernel.social.Recommendation).\n\nScoped observations only \u2014 the kernel's global view carries no\n`recommendation` key at all (breadsandcircuits-83s), which is why the field\non `Observation` below is optional rather than nullable-and-required.\n\n`verb`/`arg` name an action that is a member of the viewer's own\n`legal_actions` for this turn; `tag` is the ENGINE's own label for it, one\nof optimal/neutral/sabotage. The tag is deliberately mirrored rather than\ndropped: it is the engine-side ground truth a later Showrunner narration\ncan be checked against, and dropping it here would leave nothing to check.\n\nThe tag is mirrored and NEVER rendered (breadsandcircuits-93p). plan.md:100\ncalls the advice \"engine-tagged internally\", and internally is the whole\ndevice: the tag is the house's honest label for bait it is handing out, so\na prompt reading \"the house suggests you attack the Warden (sabotage)\"\nwould end the \"will they take the bait\" beat plan.md:100 names. Rendering\nit would also run plan.md:128's dramatic irony backwards \u2014 the AUDIENCE is\nthe side that knows more, never the contestant. So it stays here, where the\nShowrunner surface and the replay can read it.",
      "properties": {
        "arg": {
          "title": "Arg",
          "type": "integer"
        },
        "tag": {
          "title": "Tag",
          "type": "string"
        },
        "verb": {
          "title": "Verb",
          "type": "string"
        }
      },
      "required": [
        "verb",
        "arg",
        "tag"
      ],
      "title": "RecommendationView",
      "type": "object"
    },
    "ReputationView": {
      "additionalProperties": false,
      "description": "One contestant's standing (kernel.social.Reputation), as both views emit it.\n\n`BetrayalView`'s situation exactly: the kernel publishes one shape, and it\npublishes it identically to a spectator and to every contestant\n(breadsandcircuits-b5w), so there is nothing to scope and no second shape to\ndiscriminate. The list carries the WHOLE field in a scoped observation too \u2014\na standings board is a census, not a function of where the viewer stands.\n\n`score` is a signed integer and is mirrored as one: it starts at 0, falls by\n1 per betrayal the contestant commits and rises by 1 per pact it holds to\nterm, so a negative value is ordinary rather than a parse error.\n\n`render_prompt` shows the board to contestants and spectators alike\n(breadsandcircuits-93p), which is the only rendering consistent with what\nthe kernel publishes: the scores are the same rows for everybody, so\nwithholding them from the players would be the harness inventing a\nconfidentiality the kernel does not have. It is also the slice with the\nmost obvious use \u2014 who has walked out on a pact before is precisely what a\ncontestant deciding whether to ally needs.",
      "properties": {
        "contestant": {
          "title": "Contestant",
          "type": "integer"
        },
        "score": {
          "title": "Score",
          "type": "integer"
        }
      },
      "required": [
        "contestant",
        "score"
      ],
      "title": "ReputationView",
      "type": "object"
    },
    "VaultItemView": {
      "additionalProperties": false,
      "description": "One item in a member's own guild's vault (kernel.items.StashedIn).\n\n`id` and `kind` only \u2014 no zone (the vault IS the location; the entry's\n`home_zone` says where it stands) and no holder (a stashed item is\nnobody's inventory, which is the design's whole point). A separate model\nfrom `GroundItemView`/`HeldItemView` for `GuildRefArgs`' reason one file\nover: the extra field each of those carries is a claim this item cannot\nmake.",
      "properties": {
        "id": {
          "title": "Id",
          "type": "integer"
        },
        "kind": {
          "title": "Kind",
          "type": "string"
        }
      },
      "required": [
        "id",
        "kind"
      ],
      "title": "VaultItemView",
      "type": "object"
    },
    "ZoneView": {
      "additionalProperties": false,
      "properties": {
        "id": {
          "title": "Id",
          "type": "integer"
        },
        "name": {
          "title": "Name",
          "type": "string"
        },
        "sanctuary": {
          "title": "Sanctuary",
          "type": "boolean"
        }
      },
      "required": [
        "id",
        "name",
        "sanctuary"
      ],
      "title": "ZoneView",
      "type": "object"
    }
  },
  "additionalProperties": false,
  "description": "A validated `kernel.api.observe()` payload \u2014 global or scoped view.\n\nextra=\"forbid\" throughout: when the kernel's observation grows a field\n(M2 memory, M3 communication), parsing here fails loudly instead of the\nprompt silently omitting information the agent was meant to see. That is\nexactly how `messages` arrived \u2014 the kernel's M3 `say` verb\n(breadsandcircuits-mkm) added the slice and every adapter test failed\nuntil it was mirrored here, which is the design working. `whisper`\n(breadsandcircuits-cq9) and `offer` (breadsandcircuits-bpc) arrived the\nsame way. The fuller M3 harness\nmessaging surface is breadsandcircuits-i5b's and the fuller offers surface\nis breadsandcircuits-3vw's, which closed it by contract-testing the slice\nagainst real `offer`/`accept_offer`/`reject_offer`/expiry payloads rather\nthan by growing the mirror; `propose_alliance`\n(breadsandcircuits-22o) arrived the same way and its fuller surface belongs\nwith alliance resolution (breadsandcircuits-cju). What is here is what the\nkernel now emits, mirrored so no observation field goes unread.\n\n`guilds` and `guild_invites` (breadsandcircuits-yehz) arrived the same way,\nand their mirror was deliberately PARSE-ONLY until G3: `render_prompt`\nprinted neither, because there was nothing an agent could usefully be told to\ndo about a guild whose vault opened no Gate. `defections`\n(breadsandcircuits-zkgr) arrives WITH the phase that changes that\n(docs/specs/guilds-shared-stash.md \u00a715), so all three render from here on \u2014\nan unrendered slice is a slice the agent cannot act on, which is the failure\n`extra=\"forbid\"` exists to make loud one layer earlier.",
  "properties": {
    "actors": {
      "items": {
        "$ref": "#/$defs/ActorView"
      },
      "title": "Actors",
      "type": "array"
    },
    "alliances": {
      "items": {
        "anyOf": [
          {
            "$ref": "#/$defs/AllianceView"
          },
          {
            "$ref": "#/$defs/PublicAllianceView"
          }
        ]
      },
      "title": "Alliances",
      "type": "array"
    },
    "betrayals": {
      "items": {
        "$ref": "#/$defs/BetrayalView"
      },
      "title": "Betrayals",
      "type": "array"
    },
    "contestants": {
      "items": {
        "$ref": "#/$defs/ContestantView"
      },
      "title": "Contestants",
      "type": "array"
    },
    "deck": {
      "anyOf": [
        {
          "$ref": "#/$defs/DeckView"
        },
        {
          "type": "null"
        }
      ]
    },
    "defections": {
      "items": {
        "$ref": "#/$defs/DefectionView"
      },
      "title": "Defections",
      "type": "array"
    },
    "dungeon": {
      "anyOf": [
        {
          "$ref": "#/$defs/DungeonView"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "guild_invites": {
      "items": {
        "$ref": "#/$defs/GuildInviteView"
      },
      "title": "Guild Invites",
      "type": "array"
    },
    "guilds": {
      "items": {
        "anyOf": [
          {
            "$ref": "#/$defs/GuildView"
          },
          {
            "$ref": "#/$defs/PublicGuildView"
          }
        ]
      },
      "title": "Guilds",
      "type": "array"
    },
    "interventions": {
      "items": {
        "$ref": "#/$defs/InterventionView"
      },
      "title": "Interventions",
      "type": "array"
    },
    "items": {
      "$ref": "#/$defs/ItemsView"
    },
    "map": {
      "$ref": "#/$defs/MapView"
    },
    "messages": {
      "items": {
        "$ref": "#/$defs/MessageView"
      },
      "title": "Messages",
      "type": "array"
    },
    "offers": {
      "items": {
        "$ref": "#/$defs/OfferView"
      },
      "title": "Offers",
      "type": "array"
    },
    "private_messages": {
      "items": {
        "$ref": "#/$defs/PrivateMessageView"
      },
      "title": "Private Messages",
      "type": "array"
    },
    "recommendation": {
      "anyOf": [
        {
          "$ref": "#/$defs/RecommendationView"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "reputation": {
      "items": {
        "$ref": "#/$defs/ReputationView"
      },
      "title": "Reputation",
      "type": "array"
    }
  },
  "required": [
    "map",
    "contestants",
    "items",
    "actors",
    "messages",
    "private_messages",
    "offers",
    "alliances",
    "betrayals",
    "reputation",
    "guilds",
    "guild_invites",
    "defections",
    "interventions",
    "deck"
  ],
  "title": "Observation",
  "type": "object"
}
