{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "exports.schema.json",
  "$comment": "CT-38, 13 October 2026: `$id` was \"https://provider-ledger.example/schema/exports.schema.json\" and is now relative. `provider-ledger.example` is reserved by RFC 2606 and resolves nowhere, and after db49b26 removed the site's four fallbacks this line was the last carrier of that host in the built tree -- in the one file a machine reads as the contract's identity. The site serves this file byte for byte at /schema/exports.schema.json (site/lib/served.mjs), so the host was a statement about where the contract lives that was false wherever it was served. WHY RELATIVE RATHER THAN AN ADDRESS. A relative `$id` resolves against the URI the schema was retrieved from (JSON Schema 2020-12 section 8.2.1), so a consumer that fetches this file from the deployment serving it takes that deployment's real address as the base URI without the address being written here. That is what an absolute `$id` was for, and it is obtained without putting a host into bytes that are published unchanged. It also ends an inconsistency this file has carried since birth: `change_event.schema.json` and `provider_record.schema.json` were written with relative `$id`s in d35c497 on 25 September 2026 and 885d0a1 gave this one an absolute reserved host the same day. WHAT BREAKS UNDER THE TWO ALTERNATIVES, measured rather than assumed. (a) The served address interpolated from LEDGER_SITE_URL at build time: site/scripts/check-served.mjs sha256-compares out/schema/exports.schema.json against this file and fails with \"differs from the contract in the repository\", and MANIFEST.json carries its sha256 and byte count, so a per-deployment `$id` either leaves the manifest stale after every deploy or makes the served copy differ from the contract it is a copy of. The identity would also move with the deployment, so a local build and production would publish one contract under two identities and a `$ref` resolved against either would miss the other. (b) A real host the project intends to own: no host is committed anywhere, because S-53 made LEDGER_SITE_URL compulsory with no default while the domain is unsettled, so the string would assert an ownership that may be false. Unlike a reserved name, which RFC 2606 guarantees will never resolve, a plausible real domain can resolve to a third party and hand a consumer somebody else's bytes as this contract -- it fails open where the reserved name failed closed, which is what makes it worse than the defect it would replace. (b) is the right answer on the day a domain is registered and committed, and nothing here stands in the way of it then. WHAT IS GIVEN UP, said rather than hidden: a relative `$id` is not globally unique, so a consumer keying a schema registry on `$id` alone could collide with another project's `exports.schema.json`. Accepted, because all 63 `$ref`s in this file are same-document fragments against 16 distinct `#/$defs/...` targets, measured 13 October 2026, so none resolves against the base URI, so nothing in the contract depends on the identity being either unique or dereferenceable. NO VERSION BUMP IS OWED: a bump is owed where something previously valid becomes invalid, and this narrows nothing. Measured 13 October 2026 against jsonschema 4.26.0: the schema is valid under the 2020-12 metaschema and the frozen set in tests/fixtures/exports validates with zero errors under either `$id`.",
  "title": "Provider Ledger export contract v10",
  "description": "The frozen shape of everything in data/exports. Its keys are the paths a run publishes under that directory: the seven files it always writes, named in `properties`, and the per-record and per-year subtrees, named by path in `patternProperties` because a record id and a year are not a fixed vocabulary (CT-63). The site, the API and the MCP server are built against this contract and against the golden fixtures in tests/fixtures/exports, so they never wait for real sources. The engine must produce exactly this. The version is in the title; what each version changed, what it cost and what it cannot do is in schema/README.md, beside this file. THE STANDING RULE, which decides whether a change here owes a bump: a bump is owed where something previously valid becomes invalid, and not where a reader's assumption breaks. GB-27 gave a record one published row per listing and took no bump, because every change it made was additive and every set published before it still validated (state/decisions.jsonl, 2026-10-04). What a particular clause costs is in the `description` or `$comment` beside that clause, where a reader meets it with the clause rather than ahead of it. WHY THE HISTORY IS NOT HERE. This description narrated five bumps in 1,950 words and 12,359 characters, 8 per cent of this file, which every consumer that reads the contract downloads with it, and it grew by one narration per bump -- the shape DOC-06 diagnosed in docs/GATE.md, in a file the 1,500-word documentation cap does not cover. The narration moved to schema/README.md, which already carried the two most recent versions' notes and is the file a reader of this folder meets first, rather than to a third place (CT-45, 18 October 2026). tests/contract/test_the_contract_states_its_position_and_not_its_history.py holds this string to the current position and holds schema/README.md to each version's reason, so neither the cap nor the history can be lost quietly.",
  "type": "object",
  "$defs": {
    "iso_date": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
    "iso_ts": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}" },
    "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
    "event_id": { "type": "string", "pattern": "^[0-9a-f]{16}$",
      "$comment": "CT-56, and the v8 bump. This file declares `event_id` at FOUR addresses -- on an event, on a withdrawal row, on an accreditation and on an entity's attestation -- and two of them said the same thing: this definition and $defs/event, which CT-55 pointed here at v7. The other three were a bare `type: string`, so one format stated two shapes for one field while the two digests that field travels with were patterned everywhere. That is the defect whatever it currently refuses, and it is CT-53's finding at a smaller scale. All four now reference this definition, so the pattern cannot diverge again -- a `$ref` and not three further copies of it, because repeating a definition is how 787bb6c's defect travelled, as the $comment on $defs/withdrawal's properties records. MEASURED 19 October 2026 across the three sets this repository ships: 839 published `event_id` values and 0 non-conforming, so the bump is owed because a shape previously valid becomes invalid and not because any published set does. 739 of the 839 were already governed through $defs/event; the 100 this closes are 46 withdrawals.json rows and 54 entities.json attestations, and no set here publishes an accreditation's at all." },
    "country": { "type": "string", "pattern": "^[A-Z]{2}$", "description": "ISO 3166-1 alpha-2, or XX for a multi-country source." },

    "basis": {
      "enum": ["accredited", "qualified", "recognised", "listed", "self_declared"],
      "description": "What the authority actually did, in its own words. accredited: it approves the provider and its approval is required -- this register's reading of the pages it has read, and nothing stronger than reading established it. qualified: the provider passed criteria the authority published, and in the pages this register has read it has not found both of accredited's limbs -- its approval of the provider, and a sentence making that approval a condition on the party the regime binds -- evidenced in the authority's own sentences. That is a statement about what was read; it is not a finding that the authority denied either limb. recognised: the provider passed a recognition process for a named regime. listed: published without the authority stating that it assesses the providers. self_declared: the provider asserted it and the authority relays it. Every one of these five is a finding about the pages this register has read and not about the authority's intent. Never record one as another, and never infer a missing basis: a consumer that meets a record without one must refuse it rather than assume the strongest value.",
      "$comment": "CT-32. Until 12 October 2026 qualified read \"the provider passed the authority's published criteria, but the authority declines to call it an approval and does not require its use\". Measured that day, after SR-19: eight sources carried the word and SEVEN contradicted that sentence in the authority's own published words. IMDA writes \"IMDA-accredited Access Point Provider\", MDEC \"MDEC-accredited PRSP\", Japan's Digital Agency heads its page \"International Accredited Peppol Certified Service Provider\", OpenPeppol \"Peppol Certified\", DIAN \"habilitándolo como proveedor tecnológico\", Serbia's Ministry gives consent under a decree, and Portugal's AT requires use outright. Only Saudi Arabia's ZATCA matched, and it matched exactly (\"not considered as an approval\"). So the definition published, about seven governments, a denial they had not made. Understating a basis is recoverable by a reader who has the authority's sentence beside it in basis_statement; attributing a denial is this register's own false statement, and no reading of the authority's page recovers it. A SIXTH VALUE BETWEEN accredited AND qualified WAS REFUSED and is not to be reopened (state/decisions.jsonl, 2026-10-12T21:30:00+00:00): five values are handled by api/basis.py, api/regimes.py, site/lib/basis.ts, the evidence rungs and the MCP tools, and accredited's two-limb test already draws the distinction a sixth would. WHY THE SECOND SENTENCE IS PART OF THE DEFINITION rather than a note: a step-down records what was read, and the UAE is the proof. Its list sentence alone would have stepped it down, and the requirement limb was found in Ministerial Decision No. 243 of 2025, Articles 5(1) and 6(7) (\"the Issuer and the Recipient ... shall appoint an Accredited Service Provider\"), reached from the Ministry's own eInvoicing page. Singapore, Malaysia, Japan and Colombia may each hold the same answer in a statute nobody has read: IRAS, LHDN, the National Tax Agency and DIAN's resolutions 0042 and 0165 were not reached. Without that sentence the definition reads as a finding about the authority rather than a statement about this register's reading. WHY THE OTHER COPIES SAY THIS IN OTHER WORDS: api/basis.py and evidence/wording.py write the same meaning out for readers, and three committed interface tests refuse the words \"accredited\", \"accreditation\", \"approved\" and \"approval of\" in any sentence about a qualified record, so that an assistant or an auditor skimming one never meets the stronger word there (tests/interfaces/test_basis.py, test_extracts.py, test_mcp_extract.py). This file names accredited because it is a contract read beside the value it names; those two say \"the two things a stronger reading would need\" and list them. Same claim, and a wording copied from here into either of them fails those tests. NO VERSION BUMP IS OWED: the enum is untouched and every set that validated before validates now -- the change is to what the word is published to mean, which is a correction of a false statement and not a narrowing of what conforms. CT-35, 12 October 2026: FIXING qualified MADE THIS LIST ASYMMETRIC, and the fix is in the same shape as the defect. qualified read \"in the pages this register has read it has not found\" while accredited read flatly \"it approves the provider and its approval is required\", so side by side the vocabulary implied that accredited was a finding about the world and qualified a confession of this register's ignorance. Both are findings about pages read. The UAE is accredited ONLY because somebody opened Ministerial Decision No. 243 of 2025 and read Articles 5(1) and 6(7); nothing stronger than reading established it, and a reader who infers that an accredited record rests on better evidence than a qualified one has been misled by this file. accredited therefore carries the frame in its own clause, because the asymmetry is what a reader meets reading the list side by side. WHY THE CLAUSE SAYS \"reading\" AND NOT \"found evidenced IN THE AUTHORITY'S OWN SENTENCES\", which is what it was first drafted to say and which would have been a THIRD false implication added while removing the second. Measured 12 October 2026: config/sources.yaml carries two accredited sources, and only one of them holds a sentence. ae_asp_list's requirement limb is Ministerial Decision No. 243 of 2025, Articles 5(1) and 6(7). pe_pse_list's own note records the opposite in terms -- scripts/local_fetch.py's REFUSED dict names the id, so no page of SUNAT's was requested by any route, \"no sentence of SUNAT's is held\", \"neither limb of accredited can be read\", and the word \"rests on the column headings ('N. DE RESOLUCION', 'AUTORIZADO DESDE') ... which show an authorisation per provider and nothing about whether use is required\". So a gloss promising both limbs in the authority's sentences would publish, about SUNAT, a reading nobody has made. The standing weaker-word rule (state/decisions.jsonl, 2026-10-12T20:10:00+00:00) is why that promise is nearly true and not true: it publishes the weaker word wherever a limb of the stronger one is not evidenced by the authority's own published sentence, so accredited normally DOES rest on sentences -- and pe_pse_list's note records the rule as UNDECIDABLE there rather than applied, because a source no page of which was requested gives no limb to call unevidenced. What IS true of both sources is that reading established the word and nothing stronger did, which is what the clause now says. THE HAZARD THAT NOTE NAMES IS NOT FIXED HERE AND IS NOT THIS FILE'S TO FIX: pe_pse_list carries no basis_ceiling, so engine.guards.check_basis_ceiling does not hold `accredited` back on it and only status: to_verify does. That is a source entry and a guard, not a contract clause; it is recorded here because the gloss was drafted against it and a later reader of this $comment should not have to rediscover it. WHAT THE OTHER THREE WORDS NEEDED, judged rather than assumed. `listed` read \"published with no assessment\" here and \"published without any assessment\" in $defs/accreditation, and both ASSERT A NEGATIVE ABOUT THE AUTHORITY -- that it did not assess -- which is CT-32's own defect class surviving in a word CT-32 and CT-34 both recorded as already safe. That premise was true of evidence/wording.py, which publishes \"without stating that it assesses\", and false of this contract; the contract now says what that copy says. `recognised` and `self_declared` report what a page shows about the PROVIDER -- that it passed a recognition process, that it declared something the authority relays -- rather than attributing a finding or a silence to the authority, so they take the governing sentence above and no clause of their own; repeating the clause five times would say nothing the one sentence does not. WHAT THE NEW CLAUSE PROMISES THAT exports/ CANNOT CASH, stated here rather than left to be found: \"the pages this register has read\" is not verifiable by a reader holding the published set alone. The pages are in config/sources.yaml notes and partly in authority_denials, and no published file lists the pages read per source. That is not a reason to drop the clause -- it is honest about its own limits, and a sentence that overclaims would be worse -- but it is why the source entry in countries.json owes a pages-read list, which is the stronger fix and would make every one of these five clauses checkable. NO VERSION BUMP AND NO READER AFFECTED: the enum is untouched, no property changed shape, and every set that validated before validates now."
    },

    "keys": {
      "description": "WHAT IS UNIQUE IN THESE FILES, in one place. `record_id` identifies a company on a source's list. It is stable across a removal and a relisting, which is what makes it the join to the chain's events and to history/<record_id>.json -- and it is exactly why it is NOT a key: since GB-27 a record that left a list and came back is published as one row per listing, so one `record_id` can carry two providers.json rows, two accreditations and two withdrawals.json rows. What is unique instead is a record and the day its listing opened: (record_id, first_seen) in providers.json, and (record_id, from) on the accreditations inside entities.json. The rule a reader can apply without any key at all is the complement of it, and it is the stronger statement: the listings of one record never claim a day in common. Periods meeting at a single day are a record removed and relisted on one day, not an overlap (GB-30's wording). So two listings of one record open on different days wherever either of them lasted more than a day, and a set publishing two that open on one day has stated the same key twice. The single shape the key does not separate on its own is a listing opened and closed inside the day a later listing opened -- the one case GB-30's rung reads as a removal and a relisting on one day rather than as an overlap -- and there `to` tells the two apart. A key is not a substitute for reading the period. withdrawals.json is keyed by the removal and not by the record -- (record_id, removed_at) -- because the row is a statement about a removal that really happened, it survives a relisting, and a record withdrawn twice holds two rows. `entity_id` IS unique in entities.json: one row per company. It is not unique in providers.json, where one company holds a row per listing per source, and it is the only identifier a withdrawals.json row is guaranteed to share with the company it is about, which is why the rung below matches a row to a company by it. The history subtree is one file per record whatever its listings -- history/<record_id>.json holds that record's whole history, both listings in one file, because the filename can hold a record and not a listing (engine/export_files.py write_history) -- and history/index.json has one row per record, not one per listing. NONE OF THIS IS A CONSTRAINT THIS FILE CAN EXPRESS. JSON Schema has no keyword for uniqueness over part of an item across the items of an array: `uniqueItems` would refuse only two wholly identical rows, which is the one duplicate shape that is a set saying the same thing twice rather than two things. So a set that breaks these keys validates here, and what refuses it is `record:the_rows_about_this_record_can_all_be_right` (evidence/record_rows.py), which asks whether every statement a set publishes about one record can be right together: it reads an overlap rather than a count precisely so that GB-27's two honest rows pass, and it reads one entity_id published under two names as well. The per-record history file is read by `record:history_file_agrees_with_the_chain` (evidence/history_file.py). A reader meets the reasoning here, where it meets the shape.",
      "$comment": "CT-15. The rule was already in $defs/provider, $defs/accreditation and $defs/entity, in three overlapping sentences, and was contradicted in a fourth file: schema/provider_record.schema.json's first_seen said (record_id, first_seen) was 'not a key' while $defs/provider said that pair is exactly what is unique. That contradiction is the defect CT-15 exists to end, so the whole rule lives here and each definition points at it. What those definitions keep is the sentence their own shape needs -- GB-27's one-row-per-listing wording and GB-30's about two periods meeting at a single day, both left as they stood -- so this adds the rule none of them stated rather than a fourth description of the part they did. Deliberately not referenced by any $ref: it constrains nothing and validates nothing, it is the contract's own account of its keys, and a $def is where a reader of this file looks for one. Do not delete it as dead -- $defs/provider, $defs/accreditation, $defs/entity, $defs/withdrawal and $defs/event all name it. Why it is stated and not required: requiring uniqueness on record_id would have outlawed GB-27's shape, which is the one thing CT-15 was told to check before writing anything (state/decisions.jsonl, 2026-10-04)."
    },

    "provider": {
      "type": "object",
      "description": "One LISTING of one record, not one record: `record_id` identifies a company on a source's list and a company can be listed, removed and listed again, so a record that left and came back is published as one row per listing (GB-27). `record_id` is therefore not unique in providers.json; what is unique is a record and the day its listing opened, and the listings of one record never claim a day in common. Each row's `first_seen` and `removed_at` are the two ends of that row's own listing, so a consumer reading one row has a period that is true of itself. Before GB-27 a relisting overwrote the earlier listing and the one row left published the whole stretch as listed, including the months the list did not have the company on it. $defs/keys states this file's key and every other one in the set, and names the rung that holds a set to them (CT-15).",
      "required": ["record_id", "entity_id", "source_id", "country", "regime", "provider_name", "status", "first_seen", "last_seen"],
      "properties": {
        "record_id": { "type": "string", "description": "One company on one source's list. Stable across a removal and a relisting, so it is the join to the chain's events and to history/<record_id>.json, and it is not a key: $defs/keys says what is, in this file and in every other one." },
        "entity_id": { "type": "string", "description": "The company this record belongs to. Set by the entity graph." },
        "source_id": { "type": "string" },
        "country": { "$ref": "#/$defs/country" },
        "regime": { "type": "string" },
        "provider_name": { "type": "string" },
        "provider_name_normalised": { "type": "string" },
        "attributes": { "type": "object" },
        "status": { "enum": ["listed", "withdrawn"] },
        "basis": { "$ref": "#/$defs/basis" },
        "method": { "enum": ["observed", "manual", "backfilled"], "default": "observed" },
        "first_seen": { "$ref": "#/$defs/iso_date",
          "description": "The day the run that first saw THIS listing read it. On a record with one listing that is also the day the company joined the list; on a record that left and came back, the earliest row carries the day it joined and each later row the day that listing began. The record's own history -- which does not restart on a return (EN-28) -- is the earliest of its rows, and the entity's `first_seen` in entities.json states it directly." },
        "last_seen": { "$ref": "#/$defs/iso_date" },
        "first_present": {
          "$ref": "#/$defs/iso_ts",
          "description": "On a confirmed `added` event: the run that first saw THIS addition, which is the end EN-14's detection_to_publication figure is measured from. Not `first_seen`, which is the record's own history and does not restart when a provider is added back after a withdrawal (engine/confirm.py) -- a confirmed addition carries both, and on a returning record they are different dates. Until EN-28 the figure was measured from `first_seen`, so an addition following a withdrawal the feed withholds (an owner hold, or a quarantine) published the record's whole life as the time that claim waited, and the exclusion built from the feed could not see the withdrawal to leave it out. Omitted where the run that confirms the claim cannot see which run first made it, so a reader finds a stated `undatable` rather than a figure measured from the wrong end."
        },
        "removed_at": { "oneOf": [{ "$ref": "#/$defs/iso_date" }, { "type": "null" }] },
        "confirmed_at": { "$ref": "#/$defs/iso_date",
          "description": "The day the run that confirmed this withdrawal read the list. Not `removed_at`, which is the earlier day the listing ended: under the two-run rule the run that first missed a record is never the run that published the removal (engine/confirm.py), so a reader holding one of the two days has to know which. A day and not a timestamp, like every other date on this row (engine/export_rows.py, PUBLISHED_AS_A_DAY), and the definition $defs/withdrawal already gives the same field. Published on 46 rows of the frozen set and named nowhere until CT-39, so any value at all validated under it." },
        "end_withheld_because": { "enum": ["owner_hold"], "description": "The policy under which this set holds back the removal that ended the period this row publishes, so the open end is not an observation. ABSENT on every period whose end this set states or has observed nothing about, which is every row of both sets this repository ships. Present, the row still says `status: listed` with no `removed_at` and no withdrawals.json row, because that is what the policy republishes (engine/owner_hold.py, docs/AUTONOMY.md section 3 item 5) -- and a reader must not read that shape as a statement that the record was on the list on every day from `first_seen` to today. One listing's end is withheld, so where the record has since been listed again the published period also covers days the list did not have it at all: both shapes are this one field, because a held removal and a held removal followed by a relisting are one withholding. SAME VOCABULARY as $defs/chain_index's `withheld_because`, which already publishes `owner_hold` over the very event whose body is held back, so this states nothing the index does not already admit and the two files cannot name one policy with two words. Only that one word is allowed, and not the other three the index may use: `provisional` is a reading no second run has confirmed, so the period honestly has no end yet; a source in `quarantine` publishes no row at all; and `private_source` withholds a snapshot file rather than an event. A word a reader has no sentence for is worse than an absence, so adding one is a change to this enum and not a publisher's choice. NEVER EVIDENCE ABOUT THE RECORD, in either direction. A forger can write this word as freely as they can flip an index entry to `owner_hold` and delete the body (GB-32, driven), so it may not make any check pass and may not excuse a set from one it would otherwise fail; all it can do is weaken the period it sits on, which is why a forger buys nothing with it. The reading that states the same limit from the index alone is `record:chain_agrees_with_period` (evidence/held_removals.py), and it consults this field nowhere." }
      }
    },

    "accreditation": {
      "type": "object",
      "description": "ONE LISTING of one record on one list, with the days it ran. A company removed from a list and listed again holds one accreditation per listing, each with its own `from` and `to`, and their periods never claim a day in common -- so the days between two of them are days no published period covers, which is what being off the list looks like here (GB-27). Two periods meeting at a single day are a record removed and relisted on one day and not an overlap. Before GB-27 a record had one accreditation however many times it had been listed, so a relisting published the whole stretch as one open period and asserted months of listing the authority's list did not have. The key this file is read by is (record_id, from), and $defs/keys states it with every other key in the set (CT-15).",
      "$comment": "CT-15, and a v2 bump: record_id joins this listing to the providers.json row, to the chain's events and to the record's history file, and it is half of the key $defs/keys states for this file -- so leaving it optional stated a key over a field a set need not publish. engine/export_entities.py has written it unconditionally since the file existed (_accreditation), and all 586 accreditations of the frozen contract set and all 10 of the demo's carry one, so no set this repository holds was invalidated. An accreditation that named no record could not be told from another listing of the same company on the same list, which is exactly the shape GB-27 made publishable.",
      "required": ["country", "regime", "source_id", "record_id", "status", "from"],
      "properties": {
        "country": { "$ref": "#/$defs/country" },
        "regime": { "type": "string" },
        "source_id": { "type": "string" },
        "record_id": { "type": "string", "description": "The record this listing is one of. Not unique here -- a record listed, removed and listed again holds one accreditation per listing under the same record_id -- which is what $defs/keys is about." },
        "event_id": { "$ref": "#/$defs/event_id",
          "description": "The event that opened this listing: the confirmed `added` whose record_id matches, or the `baseline` that first read the list where the company was already on it. Published so that a reader holding entities.json and the chain joins the two by an identifier rather than by matching source_id, record_id and a date and hoping no other event matches too -- `event_id` is on every event in changes.json, so a reference here can be looked up there and one naming no published event can be told from one that does. NOT IN chain_index.json, which CT-17 measured and which this sentence had wrong: the index publishes `n`, `event_hash`, `published` and `withheld_because` and no ids, in both sets this repository ships and in $defs/chain_index itself, so an id reaches only the bodies that travel -- a reference whose event is withheld cannot be found by it, and a line the chain never carried can be refused by `event_hash` alone. That is why `corroborated_by` below, whose whole question is the withheld case, owes a hash and not an id. OPTIONAL, and an absent one says this set does not say which event opened the listing: engine/export_entities.py publishes none today, and a set that states no reference is read as silent rather than as denying one. Never inferred by a reader from a matching date -- that is the join this field exists to replace. PATTERNED SINCE v8 (CT-56), as a `$ref` to $defs/event_id and not a fourth copy of the pattern: this was a bare `type: string` while the same field on an event was patterned, and no set this repository ships publishes an accreditation's `event_id` at all, so the clause refuses nothing today and exists for the first set that publishes one."
        },
        "status": { "enum": ["listed", "withdrawn"] },
        "basis": {
          "enum": ["accredited", "qualified", "recognised", "listed", "self_declared"],
          "$comment": "CT-33, 12 October 2026: this property carried \"default\": \"accredited\" and no longer does. Applying a default is ordinary documented JSON Schema behaviour, so a CONFORMING consumer reading a record with no basis read it as `accredited` -- publishing that a government approved a named company where the record says nothing about it. $defs/basis forty lines above states the rule the default contradicted (\"never infer a missing basis: a consumer that meets a record without one must refuse it rather than assume the strongest value\"), and this repository's own read path already refused one (api/basis.py, tests/interfaces/test_basis.py), so the default could only ever have harmed a reader holding the published contract rather than calling the API. THE DEFAULT RODE ON THE COPY AND NOT ON THE ORIGINAL: $defs/basis holds the vocabulary and never carried a default, and this property writes the five values out again. The guard is therefore a sweep over every subschema of every JSON file in schema/ rather than a check of this path -- tests/test_no_schema_gives_basis_a_default.py, which also refuses a default on the two further inline copies in change_event.schema.json and provider_record.schema.json. NO VERSION BUMP IS OWED: a bump is owed where something previously valid becomes invalid, and removing a default narrows nothing -- every set that validated before validates now, and neither set this repository ships has ever omitted a basis -- measured 12 October 2026, all 586 accreditations of the frozen contract set and all 10 of the demo's carry one, so no reader of either set has ever reached the default at all. What it does change is what an omission MEANS to a consumer applying defaults: an absent field, which is the truth, where it used to be a claim of approval. A consumer relying on the old default now reads a record as saying nothing, and that is the correction rather than the cost.",
          "description": "What the authority actually did, in its own words. accredited: it approves the provider and its approval is required -- this register's reading of the pages it has read, and nothing stronger than reading established it. qualified: the provider passed criteria the authority published, and in the pages this register has read it has not found both of accredited's limbs evidenced in the authority's own sentences. That is a statement about what was read, not a finding that the authority denied either limb. Measured on 12 October 2026, ZATCA was the one of the eight sources carrying this word whose own sentence also denies an approval, and that is its sentence, quoted in basis_statement, rather than this register's reading (CT-32; $defs/basis carries the same vocabulary, governs, and holds the whole reason). recognised: the provider passed a recognition process for a named regime, as HMRC says of its recognised software. listed: published without the authority stating that it assesses the providers. self_declared: the provider asserted it and the authority relays it. Every one of these five is a finding about the pages this register has read and not about the authority's intent. Never record one of these as another. api/basis.py, evidence/wording.py and site/lib/basis.ts write this vocabulary out again for their own readers and must be kept in step with it (CT-32)."
        },
        "from": { "$ref": "#/$defs/iso_date", "description": "The day this listing began: the run that first saw the record on the list on this listing. Not the day the company first joined the list, where it has been listed more than once -- that is the earliest of its accreditations, and the entity's own `first_seen`." },
        "to": { "oneOf": [{ "$ref": "#/$defs/iso_date" }, { "type": "null" }] },
        "basis_from": { "$ref": "#/$defs/iso_date",
          "description": "The day the declaration this accreditation publishes came into force, clamped into the period: `basis` and `basis_statement` speak for the days from here to `to`, and for no earlier day of this listing. An authority re-declares what inclusion on its list means while companies are listed, and `basis` is one word for the whole period -- the word in force when the record was last seen -- so without this day an honest set states the current word over days the authority had published another (GB-25, measured; the coordinator's C2 folded into GB-27). Equal to `from` where the declaration was already in force when the listing opened, which is the ordinary case. ABSENT, never guessed, where the chain records no declaration for the source, where a declaration it records cannot be dated, or where the published word was already replaced before this listing opened -- the last of those is a published set the engine refuses (engine/source_basis.unsupported_basis). A reader must not read an absent `basis_from` as the word covering the whole period; it means this set does not say which days it covers." },
        "basis_statement": { "type": "string",
          "description": "A copy of the authority's own sentence, so a consumer reading entities.json alone does not have to join another file. The source entry in countries.json governs if the two ever differ." },
        "corroborated_by": { "type": "array", "items": { "type": "string" }, "description": "Sources this set says agree with this listing: another register naming this same company. A CLAIM AND NOT A REFERENCE, which is the whole of what a reader may take from it. Each item is a bare `source_id` tying the claim to no event, so there is no `event_id` and no `event_hash` here to look up in `changes.json` or `chain_index.json`, and nothing in the published files joins the claim to a line of the record. So a reader MAY conclude that this set asserts the named source agrees about this company, and MAY NOT conclude that it does, that this set holds a record of it, or that the named source was ever read about this company at all. The id need not even be one this set describes: all 78 claims of the frozen contract set name `peppol_certified_providers`, which `countries.json` publishes no entry for, so they name nothing a reader can follow up (CT-17, measured). WHAT STANDS BEHIND A CLAIM TODAY IS THE WRITE PATH AND NOTHING THE FILES STATE: `engine.export_entities.unsupported_corroboration` (EN-26) refuses to export a claim whose chain holds no `corroborated` event naming this same `entity_id` and `source_id`, and `engine/export_checks.py` runs it on every run and every republication -- a guarantee about the writer, which a reader holding only these files has to trust, and for a register whose argument is that a reader need not trust it that is the wrong shape. `record:corroboration_claims_are_on_the_chain` (evidence/corroboration.py, EV-25) reads each claim against the bodies the reader holds and says so in those terms. THE SHAPE IS THE DEFECT AND THE CONTRACT OWES ITS REPAIR, named here rather than left to be found. Carrying no reference, a reader short of every body in the chain cannot tell a withheld supporting event from one that never existed, so the reading lands in the third state -- and a forger reaches that same third state by writing any id at all. A state the forger can choose is not a third state (state/decisions.jsonl), so this is a contract defect and not a limit of the reader. The repair is one list of `{source_id, event_hash}` with both required: the hash is what `chain_index.json` carries for every line of the chain whether its body travels or not, so a reference the index does not list is a line the chain never carried and can be refused, while one it lists with the body withheld is an absence a reader may legitimately hold. NEVER `event_id` ALONE here, which is where this field differs from `event_id` above: $defs/chain_index publishes `n`, `event_hash`, `published` and `withheld_because` and no ids at all, so an id can be looked up only among published bodies and the withheld case -- the only case this field's third state turns on -- stays unreachable. The reference arrives required and is never optional first, because an optional reference is the same state the forger chooses. It costs every claim a set cannot reference, which today is all of them: no engine writes a `corroborated` event yet (W7-02), so the set that first publishes this shape publishes no claim in it. A claim of agreement is never evidence of a conflict -- that is `conflicts_with` and the `conflict` event type, and the two are read apart on both paths." },
        "conflicts_with": { "type": "array", "items": { "type": "string" }, "description": "Sources this set says contradict this listing. The same shape as `corroborated_by` above, with the same silence, the same single guarantee on the write path and the same forger route into the third state, so it takes the same repair in the same change and not in a sibling item: two fields that mean mirror things may not be one checkable and one not, or which claim a reader can examine becomes whoever wrote the file's choice (GB-19's lesson). A reader MAY conclude that this set asserts the named source contradicts it, and MAY NOT conclude that it does. Neither set this repository ships populates this field -- 0 of the frozen contract set's 586 accreditations and 0 of the demo's -- so requiring the reference costs it nothing at all. Read only against `conflict` events: a `corroborated` event supports no claim here, which is the reading EN-26 made on the writer's side and EV-25 on the reader's." },
        "end_withheld_because": { "enum": ["owner_hold"], "description": "The policy under which this set holds back the removal that ended the period this row publishes, so the open end is not an observation. ABSENT on every period whose end this set states or has observed nothing about, which is every row of both sets this repository ships. Present, the row still says `status: listed` with no `removed_at` and no withdrawals.json row, because that is what the policy republishes (engine/owner_hold.py, docs/AUTONOMY.md section 3 item 5) -- and a reader must not read that shape as a statement that the record was on the list on every day from `first_seen` to today. One listing's end is withheld, so where the record has since been listed again the published period also covers days the list did not have it at all: both shapes are this one field, because a held removal and a held removal followed by a relisting are one withholding. SAME VOCABULARY as $defs/chain_index's `withheld_because`, which already publishes `owner_hold` over the very event whose body is held back, so this states nothing the index does not already admit and the two files cannot name one policy with two words. Only that one word is allowed, and not the other three the index may use: `provisional` is a reading no second run has confirmed, so the period honestly has no end yet; a source in `quarantine` publishes no row at all; and `private_source` withholds a snapshot file rather than an event. A word a reader has no sentence for is worse than an absence, so adding one is a change to this enum and not a publisher's choice. NEVER EVIDENCE ABOUT THE RECORD, in either direction. A forger can write this word as freely as they can flip an index entry to `owner_hold` and delete the body (GB-32, driven), so it may not make any check pass and may not excuse a set from one it would otherwise fail; all it can do is weaken the period it sits on, which is why a forger buys nothing with it. The reading that states the same limit from the index alone is `record:chain_agrees_with_period` (evidence/held_removals.py), and it consults this field nowhere." }
      }
    },

    "entity": {
      "type": "object",
      "required": ["entity_id", "canonical_name", "accreditations", "first_seen", "last_seen"],
      "description": "One company. The /as-at answer is computed from the from and to dates on its accreditations, so no separate point-in-time index is needed -- and since GB-27 an accreditation is one LISTING, so a company removed from a list and listed again holds two of them for one record_id and the days between them are covered by neither. `entity_id` is one company and is unique in entities.json; `record_id` on an accreditation is not, for the reason $defs/accreditation gives. $defs/keys states both, and the rest of the set's keys with them (CT-15).",
      "properties": {
        "entity_id": { "type": "string" },
        "canonical_name": { "type": "string" },
        "slug": { "type": "string" },
        "domains": { "type": "array", "items": { "type": "string" } },
        "accreditations": { "type": "array", "items": { "$ref": "#/$defs/accreditation" } },
        "attestations": {
          "type": "array",
          "description": "What the company itself says. Never overwrites an official field; always shown as provided by the company.",
          "items": {
            "type": "object",
            "required": ["ts", "field", "value", "event_id"],
            "additionalProperties": false,
            "$comment": "CLOSED BY CT-61, 21 October 2026, one of four shapes that item closed and the reason it closed any: this is the only shape in this contract whose content comes from the SUBJECT of a record rather than from an authority or from this register (AGENTS.md rule 7), so a key the contract does not declare here is a company's own text published under a name no clause bounds. The two producers were read rather than assumed: engine/export_entities.py writes `attestations: []` on every entity and no item at all, and scripts/golden/dataset.py writes exactly the five keys above on 54 of the frozen set's 288 entities. Measured under Draft202012Validator with this clause in place: all three shipped sets 0 errors, 54 items. WHAT IT COSTS, stated because the blast radius is the whole of why the other 31 open shapes stayed open: the layer-7 writer that is to publish these items cannot publish a field this list does not name until the contract names it, and for data this register did not observe that is the order the clause is for. The measurement behind all four closures, and behind the 31 refusals, is on $defs/withdrawal below.",
            "properties": {
              "ts": { "$ref": "#/$defs/iso_ts" },
              "field": { "type": "string" },
              "value": { "type": "string" },
              "disputes": { "type": ["string", "null"], "description": "record_id of the official record disputed, if any." },
              "event_id": { "$ref": "#/$defs/event_id" }
            }
          }
        },
        "first_seen": { "$ref": "#/$defs/iso_date", "description": "The earliest day any of this company's listings opened, so a company listed, removed and listed again still states the day it first joined (GB-27)." },
        "last_seen": { "$ref": "#/$defs/iso_date" }
      }
    },

    "event": {
      "type": "object",
      "required": ["event_id", "ts", "type", "source_id", "country", "event_hash", "prev_event_hash"],
      "$comment": "An event is published exactly as it was hashed, so a field may never be ADDED to one at publication time: it would no longer match its event_hash and would read as edited. EN-30 is the other side of that rule. Until EN-30 this comment said an event never carries basis at all, and drew the wrong conclusion from the right premise: an event hashed WITH the word matches its own hash, and an event hashed without it can never gain it. `basis` and `basis_statement` are therefore written into the body before it is hashed (engine/source_basis.py), on every event a provider_list or manual source produces, and published here exactly as hashed like every other field. That is what lets anything outside the four export files corroborate the word: a publisher who raises it in all four AND deletes the quoted sentence from both places that carry it is contradicted by the chain, because moving the word in an event moves that event's hash, every link after it, the run digest the timestamp proof covers, and chain_index.json's own hash inside the sealed file. A reader holding only the portable archive can establish it. The field stays optional, and an event that carries none is an absence to be read as silence: a page declares no basis, and the log is append-only (AGENTS.md rule 15), so events written before this field existed keep their shape for ever. A consumer may still read a record's basis from providers.json or entities.json by record_id; what it may not do is treat those files as the only statement of it.",
      "properties": {
        "event_id": { "$ref": "#/$defs/event_id",
          "description": "CT-55, and a v7 bump that refuses nothing. This was `type: string` while `event_hash` and `prev_event_hash` beside it were patterned, so the identifier a reader joins on -- withdrawals.json cites it, and since CT-18 an accreditation may -- admitted any string at all, including the empty one, while the two digests it travels with did not. Measured 19 October 2026: all 780 event documents in this tree carry a conforming id, so the clause catches no set this repository ships and the bump is owed by the rule rather than by a defect in any published file. Kept identical to event_id in change_event.schema.json, as a `$ref` and not a second definition of the same pattern (CT-39, CT-41)." },
        "ts": { "$ref": "#/$defs/iso_ts" },
        "type": {
          "enum": ["baseline", "page_baseline", "added", "removed", "modified", "page_changed",
                   "fetch_failed", "parse_failed", "source_anomaly", "provisional_cleared",
                   "corroborated", "conflict",
                   "alias_added", "entity_merged", "entity_split", "provider_claim",
                   "page_change_summary", "basis_changed", "source_bytes_changed"]
        },
        "confirmation": { "enum": ["provisional", "confirmed", "not_applicable"], "default": "not_applicable" },
        "removed_at": {
          "$ref": "#/$defs/iso_ts",
          "description": "The day the record left the official list: the run that first missed it, not the run that confirmed it. Every withdrawals.json row, every entities.json accreditation `to` and every extract period is dated from this field and from nothing else, so a published removal that omits it states its day only in files the chain does not constrain. Required by the allOf below on any removed event that is not provisional. Not the removed_at on $defs/provider or $defs/withdrawal, which are that record's date as those files publish it, in iso_date form; this is the field on the event as it was hashed."
        },
        "source_id": { "type": "string" },
        "source_url": { "type": "string",
          "description": "The address the reading was made from. OPTIONAL HERE AND REQUIRED IN change_event.schema.json, one of the two fields the two halves of this format state differently (CT-53, 19 October 2026). Every event this engine writes carries it -- all 763 event documents in this tree do, as an empty string on the entity decisions no fetch produced (engine/entity_events.py) -- so the difference costs a reader nothing today, and tightening this half would owe this contract a version bump for a refusal that catches no published set. site/lib/exports.ts already types it optional, which is the shape a consumer meets. The asymmetry runs this way on purpose: an event is published exactly as it was hashed, so the recorded half may require more than this one and never less." },
        "country": { "$ref": "#/$defs/country" },
        "regime": { "type": "string",
          "description": "Which regime this source's list belongs to -- e-invoicing, mtd-vat -- as the registry records it. OPTIONAL HERE AND REQUIRED IN change_event.schema.json: the disagreement CT-53 was opened to settle, and it stands, with the reason in both files. AN ABSENCE MEANS THE REGISTRY DECLARED NONE, never that this register did not look and never a regime a reader may supply: a regime is declared, never inferred from the country, the publisher or the name of the list (CT-28, G14-1). THE FORMAT HAS TWO ENCODINGS OF THAT SILENCE and a reader must treat them alike, reading neither as a regime: ABSENT, which scripts/golden/assembly.py writes for the frozen set's one regime-less source, citing this field's optionality in its own comment; and an EMPTY STRING, which engine/entity_events.py writes on the three entity-decision events and EN-91's pre-fetch refusal writes on its own event, for the reason entity_events.py's comment gives -- every other event in the log carries the field, so a reader diffing shapes should not have to ask whether a missing one means anything. A RUN WRITES NEITHER, settled by EN-91 on 19 October 2026 (commit 5552137). A registry entry that declares no regime -- an absent key, an empty value, whitespace, anything that is not a string -- is refused before the fetch as a `source_anomaly` with reason `regime_not_declared`, so that publisher's list is never read and no event about its contents is published at all; the refusal costs that one source and not the run, which is what it cost before. THE EMPTY STRING IS THIS FORMAT'S SINGLE ENCODING FOR AN EVENT CARRYING NO DECLARED REGIME, and exactly two kinds of event carry it: the three entity decisions no fetch produced (engine/entity_events.py) and that pre-fetch refusal, which carries the field because the recorded half requires it and reports the registry's raw value separately as `declared_regime` -- null for an absent key, \"\" for a blank one. Nothing else writes it. This sentence has been wrong twice and the corrections are the reason it is this long: it said a run would raise on such an entry, which was true of an absent key and not of a blank one -- `regime: \"\"` reached `result: ok` and was hashed onto every event of the source -- and it then said the choice of encoding was undecided. engine/run.py no longer indexes the key itself: engine/regime_declared.py is the one reader of the registry's `regime`, and tests/test_a_source_that_declares_no_regime_is_refused.py pins that from both directions over the AST of engine/, so a second reader in either of the old styles fails and so does the field quietly stopping being read. All 63 entries of config/sources.yaml and all 6 of config/sources.fixtures.yaml declare a non-empty regime, re-measured 19 October 2026, so no run has reached the refusal and the registry-wide assertion is itself a test. Four `page_changed` events of tests/fixtures/exports/changes.json carry no regime and are refused by the recorded half for it; they conform here, and this is the contract that governs a published event. This half is not tightened to match, because those four are the shape scripts/golden/dataset.py keeps on purpose -- the one country of that set whose sources declare nothing -- and giving them a regime moves 680 of this file's 731 event_hash values, every one of withdrawals.json's 45 rows and chain_index.json's last_event_hash, measured by building that set twice rather than estimated." },
        "record_id": { "type": "string",
          "description": "The (source_id, record_id) pair one company's standing on one source is kept under, read by engine/open_absences.py to fold the chain back into the absence map. Required below on provisional_cleared, added, modified and removed (CT-06, CT-07): missing it on any of those four would collapse distinct records onto a shared (source_id, None) claim. One record's events cover all of its listings -- the identifier does not restart on a relisting -- so this is the join $defs/keys describes and not a key here either." },
        "entity_id": { "type": "string",
          "description": "The company the event is about. Written by the entity graph's own decisions, which are about a company rather than a record: alias_added, entity_merged, entity_split (engine/entity_events.py). OPTIONAL, and a consumer must not depend on it, because the two sets this repository ships differ here and both are conforming: every added and removed event of the frozen contract set carries one (scripts/golden/assembly.py puts record_id, entity_id and provider_name on each), and no event this engine writes does. That is deliberate on the engine's side -- an event is published exactly as it was hashed, and which company a record belongs to is resolved after the fact from decisions that may be logged later, so freezing the answer into an append-only chain would let it go stale the day a decision moves the record (api/loader.py entity_by_event, I-04 and I-05, which joins through withdrawals.json's event_id instead). A consumer that needs the company of a per-record event joins through record_id and providers.json, or through that event_id, and does not read this field hoping it is there. Kept identical to entity_id in change_event.schema.json, which declared it for the first time in CT-18." },
        "provider_name": { "type": "string" },
        "before": {},
        "after": {},
        "detail": { "type": "string" },
        "cleared_type": {
          "enum": ["added", "modified", "removed"],
          "description": "Which kind of pending claim a provisional_cleared drops. Mirrors change_event.schema.json's cleared_type (CT-06): engine/open_absences.py, folding either this feed or the internal chain, closes an open absence on a provisional_cleared only when this is 'removed'."
        },
        "record_count": { "type": "integer", "minimum": 0,
          "description": "How many records the whole-list reading of a `baseline` counted in the file it read. Never the names in it: a baseline names no company, which is why a record already on a list when its source was first read has no `added` event of its own. Required below of every `baseline` (CT-16), because this is the only number a reader of the published files can hold that whole-list reading to -- evidence/baseline_cohort.py compares it with the records this set says joined that list on that day, and reads MORE claiming the day than the reading counted as a contradiction nothing a reader lacks can explain. An integer and never a boolean: `isinstance(True, int)` is true in Python, so a set publishing `true` here would be compared as 1 by a reader that did not check, and that reader does check. Not carried by a `page_baseline`, which reads a page and counts no records at all."
        },
        "snapshot_sha256": { "$ref": "#/$defs/sha256",
          "description": "The SHA-256 of the bytes the publisher served for this reading, as hashed into the event. WHAT A READER CAN DO WITH IT DEPENDS ON `snapshot_policy` ON THE SAME EVENT, and CT-52 is why that is written here rather than left to be worked out. Where that word is `public` this digest is a RECEIPT: the file is kept under `snapshots/` and travels in the portable archive as an `included` row, so a reader hashes the file and checks this value themselves. Where the word is `private` it is a COMMITMENT about bytes the register read and that nobody else can obtain: the file is written to `snapshots-private/`, which reaches no published set and no archive, and evidence/snapshots.py deliberately never looks there, so the archive's row for that digest is `absent` permanently rather than for one build (schema/snapshot_manifest.schema.md). AGENTS.md rule 16 keeps every snapshot BECAUSE it is the evidence behind every event; on that path nothing is deleted and nothing is retained for a reader, so the rule is true and empty there -- and until CT-52 nothing said so, which left a reader unable to tell a digest somebody holds from one whose bytes are nowhere. The derived check that survives the publisher replacing the file is `parsed_sha256`, reproducible from the records without the publisher's bytes; a reading that predates EV-55 carries none and stands on `record_count` alone, which any 149 names satisfy at 149 (evidence/baseline_cohort.py). Kept identical to snapshot_sha256 in change_event.schema.json." },
        "previous_snapshot_sha256": {
          "$ref": "#/$defs/sha256",
          "description": "On a `source_bytes_changed`: the digest of the bytes this source served when it was last read, which the bytes `snapshot_sha256` names replaced. The pair is the whole of the event: what it states is that the publisher's bytes at a declared address moved on a named run, and a reader holding either file can place it. Neither snapshot is ever deleted (AGENTS.md rule 16), and where the source's policy keeps the file itself out of the published set the two digests are still here, so the event stays checkable by anyone who holds the bytes (engine/snapshot_policy.py). Written on every event of this type and on no other: `engine/raw_bytes.replaced` returns a digest only where the chain already carries one for the last reading, so a state written before that digest was recorded says nothing rather than dating a replacement that may have happened on any day since -- a false date is the one contradiction a timestamp proof can never clear up. Kept identical to previous_snapshot_sha256 in change_event.schema.json. Same convention as the other digests here, a `$ref` to `$defs/sha256` rather than a second definition of one (CT-39, CT-41)."
        },
        "parsed_sha256": { "$ref": "#/$defs/sha256",
          "$comment": "CT-41 defines this property and the three below it, and each is defined against the writer that emits it rather than against a published instance -- which is the whole of why the item exists. Measured 13 October 2026: this file held zero occurrences of `parsed_sha256`, `redirects`, `final_url` and `row_filter`, every one of them is written into an event body by engine/run.py BEFORE the event is hashed, every one of them can reach changes.json, and no set this repository ships publishes any of the four. So the sweep in tests/test_the_contract_names_what_the_sets_publish.py cannot see them: it runs from the two shipped sets toward this contract, which measures what has been published and not what the writer can publish. Same convention as the other digests here, a $ref to $defs/sha256 and not a second one: CT-39 had to unpick `state` being reported unnamed because a walk stopped at a $ref, and a parallel definition would be the same defect written on purpose.",
          "description": "On a `baseline` or a `source_bytes_changed`: the digest of the records this run DERIVED from the file it read, as hashed into the event (EV-55, engine/run.py). Not `snapshot_sha256`, which is the digest of the publisher's bytes -- a file this register does not keep where it carries personal data, and which the publisher stops serving once it is replaced, after which nothing is left to check a baseline against. A party holding that file reproduces this value with engine.store.canonical over engine.normalise.build_records' output, so the records outlive their source file, which is the third-party corroboration a baseline otherwise cannot have: a baseline publishes `record_count` and names no company, so the chain cannot say whether one named record was among them, and a count of 149 is satisfied by any 149 names (evidence/baseline_cohort.py). ON THOSE TWO EVENTS AND NO OTHER, and engine/run.py scopes it there on purpose: an added, removed or modified event names its own record, so a whole-list digest on it is context rather than evidence, and putting it on the event base would spread it across all of them. CT-50 widened the scope and left that reason standing, because an event naming NO record is the other side of it rather than an exception to it: on a `source_bytes_changed` the publisher's bytes moved and the records did not, which is why the run says nothing about any company, and this digest is then not context beside a named provider but the whole of what the event claims about the parse (EN-80, engine/raw_bytes.py). A page source carries its extracted text's digest under this name in its run_summary.json row and on its `source_bytes_changed` too; `text_sha256` is the name on a `page_baseline` and a `page_changed` alone, because those two events are about the page's text having moved and this one is about the bytes it was read from." },
        "redirects": { "type": "array",
          "description": "Where the fetch was sent before the bytes arrived, one entry per hop in the order they were followed (engine/fetch.py). Written on EVERY event the source produces in that run and not only on a `fetch_failed`: engine/run.py puts the pair on the event base before the fetch result is judged, so a redirecting source carries both on its `added` and `removed` events too. `source_url` stays the address the registry declares, so these two fields are what let a reader see that the declared address and the one that served the file are not the same -- and the hop is recorded whether or not it was followed, because a refused redirect is part of the record.",
          "items": {
            "type": "object",
            "required": ["from", "to", "status_code"],
            "properties": {
              "from": { "type": "string", "minLength": 1,
                "description": "The address this hop was requested from." },
              "to": { "type": "string", "minLength": 1,
                "description": "Where its `Location` header pointed, resolved against `from`: engine/fetch.py urljoins a relative Location, which is the common case on a same-host move." },
              "status_code": { "enum": [301, 302, 303, 307, 308],
                "description": "The redirect status the publisher answered with. Only the five engine/fetch.REDIRECT_CODES treats as a redirect can appear: any other status ends the fetch instead of adding a hop." }
            }
          }
        },
        "final_url": { "type": "string", "minLength": 1,
          "description": "Where the bytes really came from, after the hops above. Written only beside `redirects` (engine/run.py), so its absence is the positive statement that the declared `source_url` is the address that served this file, rather than a set that did not record where it went." },
        "row_filter": { "type": "object",
          "required": ["column", "keep", "rows_read", "rows_dropped"],
          "additionalProperties": false,
          "$comment": "The fourth shape in this file to be closed, and the only one outside run_summary. (CT-61 closed four more on 21 October 2026 -- a withdrawal's evidence block, chain_index.json and its entries, and an entity's attestation -- so it is no longer the only one outside run_summary; the reason stated here is still its own and not theirs.) Closed for a reason this file's other open shapes do not have: `drop:` must not be publishable. engine/row_filter.py admits an allow-list and refuses a deny-list outright -- `set(spec) != {\"column\", \"keep\"}` raises row_filter_declaration_invalid -- because a filter that drops too little publishes a false claim about a named company and nothing downstream can catch it, while one that drops too much under-reports a list and the run says so. An open shape here would let a set publish `{\"column\": \"Estado\", \"drop\": [\"Revogado\"]}` with every required key present, which reads to a consumer as a filter this register applied and is a declaration the engine would have refused to run. engine/row_filter.counted returns exactly these four keys.",
          "description": "Which rows of the publisher's file this register read as part of the list, and how many it left out -- the registry's own declaration together with the two figures the run measured (SR-22, engine/row_filter.py, landed 13 October 2026). On every event a filtered source writes, including a `source_anomaly`, because engine/run.py folds it into the event base before the guards run. It is on the chain and not only in config/sources.yaml for the reason the snapshot policy is: the configuration is a value the operator rewrites with nothing published saying so, and a state the forger can choose is not a third state. A dropped row's own value is never published here: on another source that column could hold anything the publisher prints, including a person's name. What the reader checks it against is the snapshot the same event points at, which holds every row the filter dropped (AGENTS.md rule 16).",
          "properties": {
            "column": { "type": "string", "minLength": 1,
              "description": "The column of the publisher's file the filter read, as the registry names it. Never the name column and never a dropped one: engine/row_filter.validated raises a source_anomaly for either before any row is kept." },
            "keep": { "type": "array", "items": { "type": "string", "minLength": 1 },
              "minItems": 1,
              "description": "The values that stayed, as the registry declared them, in its own order. An allow-list, and there is no deny-list to publish: engine/row_filter.py refuses a `drop:` declaration outright, because a filter that drops too little publishes a false claim about a named company and nothing downstream can catch it, while one that drops too much under-reports a list and the run says so. Matching is exact and case-sensitive on the stripped cell text, so a differently cased value is one this register has not read." },
            "rows_read": { "type": "integer", "minimum": 0,
              "description": "Every row the parser handed over, before the filter: a figure about the publisher's file and not about this register's records. Subtracting `rows_dropped` does not give the number of records -- a row with no provider name is left out by the mapping whether a filter is declared or not, and many rows share one holder -- and the event beside this carries the record count. 0 is reachable and is not published as a reading: a file the filter kept no rows of fails engine/guards.check_row_count with `zero_rows`, so the only event that can carry a 0 here is a `source_anomaly`." },
            "rows_dropped": { "type": "integer", "minimum": 0,
              "description": "How many of those rows the filter left out. Smaller than `rows_read` on every event that is not a `source_anomaly`: engine/row_filter.counted raises `row_filter_kept_nothing` rather than publish a filter that kept none of a file's rows, because \"we dropped all of them\" is a filter that has stopped working and not a figure. Not expressed as a clause here, because JSON Schema cannot compare two properties of one object and a clause that compared each against a constant would refuse an honest list of some other size." }
          }
        },
        "basis": { "$ref": "#/$defs/basis",
          "description": "What the authority says inclusion on this list means, in its own word, as hashed into this event (EN-30). Mirrors basis on change_event.schema.json. Carried by every event a provider_list or manual source writes; absent on a page's events, and absent on any event written before this field existed, because the log is append-only. The source entry in countries.json governs what is published; this is where the word is dated." },
        "basis_statement": { "type": "string",
          "description": "The authority's own sentence, quoted, as hashed into this event. Present only where the source declares one. This is the copy a publisher cannot delete: the two published copies, on the accreditation in entities.json and on the source in countries.json, are both files a publisher writes." },
        "previous_basis": { "$ref": "#/$defs/basis",
          "description": "On a basis_changed event: the word the chain carried for this source until that run. Required by the allOf below, for the reason change_event.schema.json gives." },
        "previous_basis_statement": { "type": "string",
          "description": "On a basis_changed event: the sentence the chain carried until that run, quoted whole." },
        "first_present": { "$ref": "#/$defs/iso_ts",
          "$comment": "CT-39 defines this property and the ten below it. Each is written into an event body before it is hashed, each is published in changes.json by a set this repository ships, and this list named none of them -- so `lines_added: \"lots\"`, `quarantined: \"no\"` and a twelve-character `text_sha256` all validated. They were found by a sweep running from the published data toward the contract (tests/test_the_contract_names_what_the_sets_publish.py) rather than by reading the schema, because a check that walks the schema can only find shapes the schema already knows about. Each definition below was then read against the engine function that writes the field: a definition checked only against a published set is a claim with nothing holding it true, which is the route 787bb6c's `basis` default took in.",
          "description": "On a confirmed `added`: the run that first saw THIS addition, which is the end EN-14's detection_to_publication figure is measured from. Omitted where the run that confirms the claim cannot see which run first made it (engine/confirm.py), so the figure is stated `undatable` rather than measured from the wrong end. The same field and the same reading as `first_present` on $defs/provider and in change_event.schema.json, which has declared it since CT-18." },
        "changed_at": { "$ref": "#/$defs/iso_ts",
          "description": "On a confirmed `modified`: the run that first saw the records differ, which is the end EN-14's detection_to_publication figure is measured from for a modification. CT-54, and the reason this is a v7 bump. `engine/latency.py`'s DETECTION_FIELD is three entries -- `added` is dated from `first_present`, `removed` from `removed_at`, `modified` from this -- and the other two were named in both halves of the format while this one was named in NEITHER, so a set could publish any value at all, of any type, as the day this register saw a change. The gap survived CT-39's and CT-41's sweeps for the reason CT-41 wrote down: those run from the shipped sets toward this contract, and no set this repository ships has published a confirmed modification, so there was no instance to sweep from. Found instead by running the sweep from the writer, `engine/confirm.py`, which sets it on every confirmed `modified` it emits and on no other event -- the pending claim's own `since`, so the date is the run the difference was first seen and never the run that agreed with it. Measured 19 October 2026: 0 of the 780 event documents in this tree carry it, so this clause refuses nothing today and exists for the first run that confirms a modification. Kept identical to changed_at in change_event.schema.json, which states timestamps as `format: date-time` where this file states them as a pattern." },
        "first_seen": { "$ref": "#/$defs/iso_ts",
          "description": "The record's own first sighting, carried on a confirmed `added` and on a confirmed `removed` (engine/confirm.py). Not `first_present` above: this one does not restart when a provider is added back after a withdrawal, so on a returning record the two name different runs. A whole timestamp where providers.json publishes the day, because an event is published exactly as it was hashed." },
        "last_seen": { "$ref": "#/$defs/iso_ts",
          "description": "On a confirmed `removed`: the last run that read this provider on the list. A whole timestamp, as hashed." },
        "lines_added": { "type": "integer", "minimum": 0,
          "description": "On a `page_changed`: how many lines of the extracted text this run read that the run before it did not (engine/diff.py). Lines and not changes -- a reworded sentence is one line removed and one added." },
        "lines_removed": { "type": "integer", "minimum": 0,
          "description": "On a `page_changed`: how many lines the run before it read that this run did not (engine/diff.py)." },
        "sample_added": { "type": "array", "items": { "type": "string" },
          "description": "On a `page_changed`: the first few added lines, each truncated, so a reader sees what moved without the page (engine/diff.py). A sample and never the change itself: `text_sha256` is what the change is checked against." },
        "sample_removed": { "type": "array", "items": { "type": "string" },
          "description": "On a `page_changed`: the same, for the lines this run no longer reads." },
        "text_sha256": { "$ref": "#/$defs/sha256",
          "description": "On a `page_baseline` or a `page_changed`: the digest of the extracted text this run read, which the next run's text is compared against (engine/run.py). Not `snapshot_sha256`, which is the digest of the bytes the publisher served." },
        "quarantined": { "type": "boolean",
          "description": "Provenance: this event was written while its source was still inside its observation window, so nothing it says about a company reached a reader at the time (engine/run.py, engine/guards.in_quarantine). The engine writes it only where the window was open and only as true; `false` is allowed because a set stating a closed window states the same fact the other way. Never a string and never a number -- `\"no\"` and `0` are what this field admitted before CT-39, and a reader testing it for truth would read both as the opposite of what they say." },
        "snapshot_policy": { "type": "string", "minLength": 1,
          "description": "Where this source's evidence may be kept: the one word in force for this source at the moment the event was hashed, which `engine/run.py` reads from the registry through `snapshot_policy.word_in_force`.",
          "$comment": "DELIBERATELY NOT the two-word enum countries.json's source entry carries, and CT-44 (18 October 2026) corrected the two reasons this comment used to give, both of which had stopped being true. (1) `engine/run.py` no longer puts the registry's value raw on the event base: EN-69 made it fold `snapshot_policy.word_in_force`, so a spelling `policy_word` forgives (`Private`, `PRIVATE`, `private `) is hashed as the one word it means, and an absent key is hashed as the fail-closed `private`. (2) An enum here would refuse no event that exists: measured 18 October 2026, all 10 `snapshot_policy` values in data/changelog.jsonl and all 14 in demo/changelog.jsonl are already one of the two words, which tests/test_the_event_body_carries_the_policy_word_the_engine_read.py asserts of both chains so a reader can check it before tightening anything. THE REASON THE TYPE STAYS A STRING is the one that does hold: an event is published exactly as it was hashed and the log is append-only (AGENTS.md rule 15), so this property must keep admitting whatever is on a chain, including a chain this engine did not write. That cost is this sentence; an enum's cost is a contract that can refuse an honest event nobody is allowed to rewrite. The difference left between the two published shapes is deliberate: where the registry declares nothing an event still carries `private`, and `engine/export_countries.py` omits the key instead, so a set declaring no policy is never read as an accusation against it." },
        "owner_hold": { "enum": ["first_withdrawal"],
          "description": "Provenance on the one event an owner was shown before it published: a source's first confirmed withdrawal waits for an explicit yes (engine/owner_hold.py, docs/AUTONOMY.md section 3). One word because the writer has one and this is its only site, so there is no second copy to drift from. Publication is decided from the source's state and never from this marker, so the event publishes normally once the hold is released. Same policy $defs/provider's `end_withheld_because` and $defs/chain_index's `withheld_because` name; this names the event the owner was asked about." },
        "prev_event_hash": { "$ref": "#/$defs/sha256" },
        "event_hash": { "$ref": "#/$defs/sha256" }
      },
      "allOf": [
        {
          "$comment": "EV-13, mirroring GB-5/G5-2 in schema/change_event.schema.json, which already requires this of the event as written. The published feed's contract said nothing about the field that dates every withdrawal, so a second implementation reading only this file would have written the shape the chain refuses. Conditional rather than required: a provisional removal legitimately states no day, because it IS the day a withdrawal is dated from and it publishes nothing until a second run agrees, so an unconditional required would be wrong. A removal that does not claim to be provisional is judged as what it would publish, which is how engine/withdrawal_dates.py fails closed.",
          "if": {
            "required": ["type"],
            "properties": { "type": { "const": "removed" } },
            "not": {
              "required": ["confirmation"],
              "properties": { "confirmation": { "const": "provisional" } }
            }
          },
          "then": { "required": ["removed_at"] }
        },
        {
          "$comment": "CT-06, mirroring change_event.schema.json: engine/open_absences.py closes an open absence on a provisional_cleared only when cleared_type is 'removed'. Unconditional -- engine/confirm.py has no branch that clears a claim without saying which kind -- so requiring it outright is the permissive gap closed, not an honest shape refused.",
          "if": {
            "required": ["type"],
            "properties": { "type": { "const": "provisional_cleared" } }
          },
          "then": { "required": ["cleared_type"] }
        },
        {
          "$comment": "CT-06, mirroring change_event.schema.json: a provisional_cleared carries the (source_id, record_id) pair of whichever claim it drops, read by engine/open_absences.py the moment cleared_type is 'removed' to close that record's own absence. Missing record_id would collapse every such event onto a shared (source_id, None) claim. added, modified and removed key the same absence map and are just as load-bearing; CT-06 could not require it there yet because change_event.schema.json's tests/test_which_run_dates_a_removal.py (engine track) asserted a bare added and a bare provisional removed with no record_id validate cleanly. CT-07 requires it on those three below, once that test was updated in the same change.",
          "if": {
            "required": ["type"],
            "properties": { "type": { "const": "provisional_cleared" } }
          },
          "then": { "required": ["record_id"] }
        },
        {
          "$comment": "CT-07: added, modified and removed key the same (source_id, record_id) absence map as provisional_cleared, read the same way by engine/open_absences.py (see record_id's description above and the CT-06 report) -- but engine/diff.py is the only implementation that writes them today, and it always sets record_id, so the ninth gate judged this not blocking: the collapse this closes needs a second implementation to trigger it. It is required for that reader, not for the one implementation that already gets it right. `required: [\"type\"]` inside the if mirrors the guard above rather than relying only on `type` being in this $def's own top-level required, matching CT-09/CT-10's reasoning for keeping the guard even where it is not the only thing that would catch a missing type.",
          "if": {
            "required": ["type"],
            "properties": { "type": { "enum": ["added", "modified", "removed"] } }
          },
          "then": { "required": ["record_id"] }
        },
        {
          "$comment": "EN-30, mirroring change_event.schema.json, which already requires this of the event as written: a basis_changed event exists to date what the authority's word was before a run changed it, and one stating no previous word states nothing. engine/source_basis.moved writes it on every one it emits, so requiring it refuses no honest event -- and a second implementation reading only this file would otherwise be free to publish the shape that says nothing.",
          "if": {
            "required": ["type"],
            "properties": { "type": { "const": "basis_changed" } }
          },
          "then": { "required": ["previous_basis"] }
        },
        {
          "$comment": "CT-16, mirroring change_event.schema.json, and a v2 bump. engine/confirm.py writes record_count on every baseline it emits and GB-28's flat-file half (evidence/baseline_cohort.py) compares it with the records a set says joined at that baseline -- but the field was in neither schema, so a second implementation's baseline legitimately carried none and that half of the rung had nothing to compare. The rung states the absence rather than assuming the field, which is correct of a reader and is also the gap: a publisher the contract never asked for the number is not a publisher who withheld it. Conditional on `baseline` alone, not on both whole-list readings: a page_baseline reads a page, names no company and counts no records, so requiring a count of it would ask for a number that cannot exist -- excluded, not exempted, the same distinction GB-5 drew for a provisional removal and removed_at. Cost, measured rather than assumed: the demo's 3 baselines all carry one and its 1 page_baseline carries none; the frozen contract set has no baseline event at all; nothing else in the tree holds one. tests/test_which_run_dates_a_removal.py (engine track) asserted that a bare {\"type\": \"baseline\"} validates and is updated in the same change, as CT-07 did for record_id.",
          "if": {
            "required": ["type"],
            "properties": { "type": { "const": "baseline" } }
          },
          "then": { "required": ["record_count"] }
        },
        {
          "$comment": "CT-51, mirroring change_event.schema.json, which requires this of the event as written: a source_bytes_changed states that the bytes at a declared address moved on a named run, and one naming nothing they moved from states nothing a reader can check -- snapshot_sha256 alone names what is served now, which a reader can fetch, and the digest it replaced is the half that cannot be fetched back. engine/raw_bytes.event writes it on every event of this type and engine/run.py appends one only where a digest for the last reading exists, so requiring it refuses no honest event -- and a second implementation reading only this file would otherwise be free to publish the shape that says nothing.",
          "if": {
            "required": ["type"],
            "properties": { "type": { "const": "source_bytes_changed" } }
          },
          "then": { "required": ["previous_snapshot_sha256"] }
        }
      ]
    },

    "withdrawal": {
      "type": "object",
      "description": "The asset nobody else publishes. Every field here is needed by the evidence extract. One row per removal that happened, and it is never withdrawn by what happens next: a company listed again after a withdrawal keeps the row, because the row is a statement about a removal the authority's list really made and the relisting does not unmake it (GB-27, which fixed the engine deleting it). So a record can hold a withdrawals.json row and a `listed` accreditation at the same time -- the row's `removed_at` falls inside neither of its listings -- and a record withdrawn twice holds two rows, one per listing it closed, each citing its own event and its own pair of snapshots. So this file's key is (record_id, removed_at) and not record_id: $defs/keys states it with the others, and record_id is the one half of that key this list does not yet require (CT-15, and see that property's own description for why not).",
      "required": ["event_id", "entity_id", "provider_name", "country", "source_id", "removed_at", "evidence"],
      "$comment": "CT-11. G8-2 bought the third state by deleting a row's source_id, and this list has required it since the withdrawal shape was first written (885d0a1) -- so a validator actually run against this $def already refuses a row missing the key. What G8-2 exploited was that nothing in evidence.verify calls one: verify.py rebuilds and compares fields, and `_row_against_the_reason` read the row's source_id as a plain value rather than validating the row, so a deleted key silently read as 'no source named' instead of as damage -- a validation that was never run, not a clause that was never written. GB-12 closed that gap on the reader's side, in evidence/withdrawal_row.py, by treating an absent (or falsy) source_id as damage rather than a narrower reading. What `required` alone does not close, and this does: `required` only checks the key is present, not that it carries anything, so a row could satisfy it with `\"source_id\": \"\"` -- present on paper, naming nothing, the same vacuous shape CT-09's if/then guards against for a different keyword. `minLength` on the property closes that. CT-61, 21 OCTOBER 2026: THIS OBJECT STAYS OPEN, AND THE MEASUREMENT IS WHY. CT-60 struck `listed_from` from this shape and that made validation of the struck name strictly WEAKER: this object sets no `additionalProperties` and does not require `first_seen`, so the struck property's $ref to iso_date was the only bound on that name, and `listed_from: \"the fourteenth of never\"` and `listed_from: 7` were REFUSED BEFORE THE STRIKE AND VALIDATE AFTER IT (verified twice; state/decisions.jsonl, D-STRIKING-A-PROPERTY-FROM-AN-OPEN-OBJECT-REMOVES-A-BOUND-RATHER-THAN-ADDING-A-PROHIBITION). Striking a property from an object that is not closed removes a constraint; it does not add a prohibition. THE SWEEP, run rather than reasoned about: 62 object shapes in this file carry `properties` and 8 are now closed. 23 of the open ones are subschemas under if/then/not, which are conditions and not shapes -- driven over the three shipped sets, the 19 `if`s that carry properties match a published instance 1,429 times as committed and 0 times closed, so closing one silently disables the clause it guards: a baseline event with no `record_count` is refused today and VALIDATES with $defs/event allOf[5] `if` closed. That leaves 35 published shapes. WHY NOT THIS ONE. engine/export_rows.public_record is a dict comprehension over the whole stored record less four keys, so this row's vocabulary is stored state's vocabulary and not a list any writer spells out. Driven on the widest honest row this engine can write -- a manual source, and a provider whose only name is a person's own -- the row carries `method` and `name_withheld_because`, which this list declares nowhere, so a closed shape refuses a row the engine honestly produced; closing $defs/provider for the same field fails tests/test_a_withheld_name_is_published_as_a_withholding.py on providers.json, measured. Retrodicted over all 46 commits that have touched this file, a closed shape here would have refused a published withdrawals.json at 33 of them, on `attributes`, `basis`, `first_seen`, `last_seen`, `provider_name_normalised` and `status` -- each one a field the engine copied onto the row before this contract named it. WHAT CARRIES THE STRICTNESS INSTEAD: tests/contract/test_a_withdrawal_states_its_opening_day_under_one_name.py refuses a half-done rename by READING THE ROW, tests/test_the_contract_names_what_the_sets_publish.py refuses an unregistered published key, and tests/contract/test_every_object_in_this_contract_is_open_or_closed_on_purpose.py holds the open-or-closed decision per shape and re-measures each refusal, so neither half can rot. WHAT WOULD FLIP IT: declaring `method` and `name_withheld_because` here is additive and owes no bump (the GB-33 shape), and with them declared this row's only remaining cost is the pass-through writer -- the day public_record stops copying stored state through, this object can be closed and the reading above can lean on the validator.",
      "properties": {
        "event_id": { "$ref": "#/$defs/event_id" },
        "entity_id": { "type": "string" },
        "record_id": { "type": "string", "minLength": 1,
          "description": "The record whose listing this removal closed, and half of the key $defs/keys states for this file. NOT in the `required` list above, and that is a gap rather than a decision: every row of both sets this repository ships carries one, schema/provider_record.schema.json has required it of every record since it was written and this row is a copy of one (engine/export_rows.py closed_listing), and `record:the_rows_about_this_record_can_all_be_right` reading 3 -- a withdrawal row under a record_id no providers.json row and no accreditation carries -- can only be asked of a row that names a record. What held it back is not the cost to any set: tests/evidence/test_a_field_the_row_deletes.py pins this `required` list as an exact tuple (REQUIRED, line 45), so adding any name to it fails a test on a track CT-15 does not own and could not edit while another agent was in that directory. `minLength` is added here instead, for CT-11's reason and because it is the half of the requirement that needed no change to that list: a row that names no record is a gap this file still allows, but a row whose record_id is an empty string claims to name one and does not, and that is refused. Same shape as source_id below, where CT-11 found that `required` alone is satisfied by \"\"." },
        "provider_name": { "type": "string" },
        "country": { "$ref": "#/$defs/country" },
        "regime": { "type": "string" },
        "basis": { "$ref": "#/$defs/basis" },
        "source_id": { "type": "string", "minLength": 1 },
        "source_url": { "type": "string" },
        "removed_at": { "$ref": "#/$defs/iso_date" },
        "confirmed_at": { "$ref": "#/$defs/iso_date" },
        "provider_name_normalised": { "$ref": "#/$defs/provider/properties/provider_name_normalised",
          "$comment": "CT-39, and the four below it. A withdrawals.json row IS the published providers.json listing plus event_id and evidence (engine/export_rows.py closed_listing), so it carries the listing's own fields -- this one, attributes, status, first_seen and last_seen -- and this list named none of them: `status: \"maybe\"` and `attributes: 7` validated on a published withdrawal. Each points at the definition in $defs/provider rather than repeating it, because it is that field, written by that writer, copied key for key. Repeating a definition is how 787bb6c's defect travelled: the word was right in one place and stale in its copy." },
        "attributes": { "$ref": "#/$defs/provider/properties/attributes" },
        "status": { "$ref": "#/$defs/provider/properties/status" },
        "first_seen": { "$ref": "#/$defs/provider/properties/first_seen" },
        "last_seen": { "$ref": "#/$defs/provider/properties/last_seen" },
        "evidence": {
          "type": "object",
          "required": ["last_present_snapshot", "first_absent_snapshot", "event_hash", "run_digest"],
          "additionalProperties": false,
          "$comment": "CLOSED BY CT-61, 21 October 2026, and closed where the row around it is not. Both producers write a complete literal and neither copies a stored object through: engine/withdrawal_evidence.evidence_for returns exactly these six keys and raises WithdrawalEvidenceMissing rather than publishing a partial block, and scripts/golden/assembly.attach_withdrawal_evidence writes the same six. So a seventh key here is an edit somebody makes by hand in one of two functions, which is a contract change either way, where a seventh key on the ROW above arrives by itself out of a dict comprehension over stored state. WHY THIS BLOCK IS WORTH A CLAUSE: `timestamp_proof` is optional, so under an open shape a producer could publish `timestamp_proof_pending`, or misspell the name, and the block would state no proof at all with 0 errors -- and this block is the whole of what a reader checks a removal against (evidence/snapshot_pair.py, evidence/integrity.py). Measured under Draft202012Validator with this clause in place: 46 published evidence blocks across the frozen and demo sets, every one carrying exactly these six keys, 0 errors; data/exports publishes no withdrawal yet.",
          "properties": {
            "last_present_snapshot": { "$ref": "#/$defs/sha256" },
            "first_absent_snapshot": { "$ref": "#/$defs/sha256" },
            "event_hash": { "$ref": "#/$defs/sha256" },
            "prev_event_hash": { "$ref": "#/$defs/sha256",
              "description": "The hash of the event before the cited one, copied from the cited event's own body so a reader holding the feed can follow the citation backwards (engine/withdrawal_evidence.py); evidence/integrity.py checks the row echoes it. On every evidence block of both sets and named nowhere until CT-39, while $defs/sha256 sat in this file unused at this field and at `text_sha256` on an event." },
            "run_digest": { "$ref": "#/$defs/sha256" },
            "timestamp_proof": { "type": ["string", "null"] }
          }
        }
      }
    },

    "country_summary": {
      "type": "object",
      "required": ["country", "sources", "listed_providers"],
      "properties": {
        "country": { "$ref": "#/$defs/country" },
        "name": { "type": "string" },
        "regime": { "type": "string", "minLength": 1,
          "description": "The one regime every source under this country records. Present only where they all record the same one; where they record more than one it is absent and `regimes` carries them all, so this field is never a country's regimes narrowed to the first of them. Absent with `regimes` absent means no source here records a regime at all (CT-28)." },
        "regimes": { "type": "array", "items": { "type": "string", "minLength": 1 },
          "minItems": 2, "uniqueItems": true,
          "description": "Every regime this country's sources record, sorted, where they record more than one -- `[\"e-invoicing\", \"mtd-vat\"]` for a United Kingdom holding an e-invoicing list beside HMRC's Making Tax Digital for VAT software finder. Never one entry: one regime is `regime`. The two are mutually exclusive on purpose, so which key is present is itself a statement and neither can be read as the whole answer while the other holds part of it (CT-28)." },
        "listed_providers": { "type": "integer", "minimum": 0 },
        "withdrawn_providers": { "type": "integer", "minimum": 0 },
        "sources": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["id", "kind", "status"],
            "properties": {
              "id": { "type": "string" },
              "kind": { "enum": ["provider_list", "page"] },
              "method": { "const": "manual",
                "description": "How the reading was got, where that is not a fetch: a list a person read, with a snapshot and a written attestation (engine/manual.py). `kind` above says what the source IS -- a list a person read is still a provider_list, and engine/export_countries.source_entry publishes it as one -- so how it was got is this field, beside the attestation, where a reader can weigh it without opening the configuration. Only `manual` is admitted and the key is absent on every fetched source: the engine writes it on no other kind, so a `method: \"observed\"` here would be a value no run produces.",
                "$comment": "CT-41. The one key of the ten v6 names that this contract already held a definition of -- on $defs/provider, as the three-word enum a published row carries with `observed` as its default -- and that definition does not reach this shape, which is the CT-39 lesson in miniature: a key is named where it is published or it is not named at all. Not the same vocabulary either, and not made one: a row may be `backfilled`, a source entry may not, because engine/export_countries.py has exactly one branch that writes this field and it writes `manual`. NEITHER SHIPPED SET CAN EXERCISE IT, measured 13 October 2026: config/sources.fixtures.yaml declares no `kind: manual` source and neither does config/sources.yaml, so the branch is reached by no run of either, and a golden fixture that wrote it would be writing a field the engine reference set never produces -- which is the one thing tests/test_golden_fixtures_match_the_engine.py exists to refuse. It is defined here against engine/export_countries.py and waits for a manual source to be declared." },
              "attestation": { "type": "string",
                "description": "What the person who read this list says they did: a sentence naming what was read, where, and when, whitespace-collapsed and otherwise verbatim (engine/manual.attestation_of). Published only beside `method` above, because the engine writes the pair or neither -- an observation nobody vouched for looks exactly like a fetched fact and is not one, and engine/guards.check_attestation refuses the source before it is read rather than publishing an empty promise. Never a name, an email or a location, whoever wrote it (AGENTS.md rule 1)." },
              "status": { "enum": ["active", "to_verify", "quarantined", "retired"] },
              "regime": { "type": "string", "minLength": 1,
                "description": "Which regime this source's list belongs to -- `e-invoicing`, `mtd-vat` -- as the registry records it, and as every providers.json row and entities.json accreditation of this source carries it. Free text, not an enum: the set of regimes is open and a second implementation's word for one is not this one's to fix. Declared-only, like basis/publisher/reuse_terms/snapshot_policy: absent means the registry recorded no regime for this source, NOT that the source shares the country's or that one may be inferred from the country, the basis word or the URL (CT-28)." },
              "publisher": { "type": "string" },
              "publisher_basis": { "enum": ["stated", "read", "unread"],
                "description": "What `publisher` rests on (SR-13): stated -- the fetched page or file names itself; read -- the source was fetched but the notes record no self-naming text; unread -- the page has never been fetched and the name comes only from the URL's domain. Declared-only, like basis/reuse_terms/snapshot_policy: absent means the registry recorded nothing, not that the name is unread." },
              "source_url": { "type": "string" },
              "reuse_terms": { "type": "string" },
              "basis": { "$ref": "#/$defs/basis",
                "description": "What the authority says inclusion on this list means, in its own word, as the registry records it (engine/export_countries.py). Declared-only, like regime/publisher/reuse_terms/snapshot_policy: absent means the registry recorded no word for this source, and $defs/basis's rule holds here too -- a consumer meeting a record without one refuses it rather than assuming the strongest.",
                "$comment": "CT-39, and the one gap of the twenty-three it closed that let a false claim about a government validate. This entry declared twelve properties and sets no `additionalProperties`, so until 13 October 2026 `basis: \"GOVERNMENT_APPROVED_TOTALLY\"` on the shape a reader of /countries/ meets first validated with ZERO errors -- measured on tests/fixtures/exports/countries.json under Draft202012Validator -- while the five words governed $defs/provider, $defs/accreditation and $defs/event. CT-33's sweep could not see it: that one looks for a `default` wherever `basis` is DEFINED, and this shape did not define it at all. 787bb6c's defect rode on a copy; this one rode on the copy that was never made." },
              "basis_statement": { "type": "string",
                "description": "The authority's own sentence about what inclusion on its list does and does not mean. Quoted, not paraphrased.",
                "$comment": "CT-37, 13 October 2026, written here because this is the published field a ceiling would sit beside and the place the next person measuring \"basis_ceiling reaches no schema\" will look. THE DECISION: `basis_ceiling` is deliberately not a property of this entry or of any published shape. It stays in config/sources.yaml, where engine.guards.check_basis_ceiling reads it before a list is fetched. THE ARGUMENT FOR PUBLISHING IT, which is a real one: this register's whole claim is that it never asserts more than its source supports. `basis_statement` here is the authority's own sentence and is published; the ceiling is the dated human reading of the strongest word those sentences would bear; and a reader who can see the sentence but not the reading cannot check the reading. The house rule is that an absence a reader may legitimately hold is stated and silence is never acceptable (docs/WRITING.md, docs/RESUME.md). THE ARGUMENT AGAINST, which wins, and the measurements that decide it rather than the reasoning. FIRST, THE CEILING CANNOT DO THE JOB PUBLISHING IT WOULD BE FOR. A reader holding these files cannot use a published ceiling to check the published `basis`, because one writer writes both: a forger who raises `basis` raises the ceiling in the same pass. That is this project's second principle, that a state the forger can choose is not a third state, and it is why the ceiling was built on the write path (state/decisions.jsonl, 2026-10-05T12:51:33+00:00, \"The basis ceiling is a write-path guard the forger cannot write, not a field beside the word\"). SECOND, IT WOULD CARRY NO INFORMATION. Measured 13 October 2026: of the 63 sources in config/sources.yaml, 14 declare a ceiling, and in all 14 `basis` and `basis_ceiling` are the same word; both ceiling-bearing sources in config/sources.fixtures.yaml are the same again; no source anywhere declares a ceiling that differs from the word it publishes. So on every entry that could carry one, the field would reprint the word already printed beside it -- a bare `basis_ceiling: recognised` against `basis: recognised`, which invites reading the ceiling as the authority's own limit on itself. That is the misattribution rules 7 and 32 keep official material and this project's own material apart to prevent, and the one found and corrected on five surfaces in two days (I-22, I-23, CT-33, EV-61, EV-62). THIRD, ITS ABSENCE WOULD PUBLISH THE WRONG THING. 49 of the 63 sources declare no ceiling, and an absent ceiling does not mean an unbounded authority -- it means nobody has read that authority's pages yet. Publishing the field would turn this register's own backlog into an apparent statement about a government. WHAT WOULD REOPEN THIS, so the decision is not read as a ban on thinking: a source that declares a ceiling STRONGER than the word it publishes, which is an honest understatement the guard permits and which no source has yet made. On such a source the ceiling would say something `basis` does not, and the question is live again. WHAT A READER GETS INSTEAD: the authority's sentence here, `authority_denials` beside it, and the rung record:the_chain_records_this_basis, which settles the word for 99 of 99 upgradeable accreditations in any language without reading prose (evidence/basis_denials.py holds that argument and reaches the same conclusion about the ceiling)." },
              "authority_denials": {
                "type": "array", "minItems": 1,
                "description": "The authority's own sentences about what its list does NOT mean, and about what it chose to call it, each verbatim with the page it was read from and the day it was read (SR-14, EV-58). `basis_statement` above carries what the authority says it DID; this carries the sentence beside it. HMRC's page says both in consecutive sentences -- 'All software listed on this page has been through HMRC's recognition process' and 'HMRC cannot recommend one software product or service over another and is not responsible for any problems you have with software that you've bought' -- and until EV-58 a conformant set published the first and had nowhere to put the second. THE WHOLE ARRAY, WITH EVERY ROLE, NEVER PRE-FILTERED TO `denies`: `role` is what tells a denial from the authority choosing its own word for its own list, and a reader filters on it. Declared-only, like regime/basis/publisher/reuse_terms/snapshot_policy: absent means the registry records no sentence for this source, which is a stated absence and not a withheld one, and `minItems: 1` is here so a set states that absence by saying nothing rather than by publishing an empty array. Nothing aggregates above this entry: a regime is this register's classification and may be summarised per country, a sentence belongs to the authority that wrote it.",
                "items": {
                  "type": "object",
                  "required": ["role", "text", "url", "read_on"],
                  "properties": {
                    "role": { "enum": ["denies", "characterises"],
                      "description": "What the authority is doing in this sentence. denies -- it says its list does not mean something: HMRC 'cannot recommend', ZATCA's list 'is not considered as an approval'. characterises -- it names what its own word means, which is not a denial of anything: DGFiP's 'Une plateforme agréée est une plateforme immatriculée par l'administration fiscale'. A CLOSED VOCABULARY AND AN ENUM, where `regime` beside it is free text, because this word is not a label a second implementation may coin: a reader decides on it what to carry into a document, and a set whose word it cannot read must be refused rather than read as the safer of the two. Measured on this repository's registry when EV-58 landed: 24 of 32 recorded sentences are `characterises` and 8 are `denies`, and the two sources that are active record one denial and two characterisations between them -- HMRC's 'cannot recommend' and DGFiP's two namings -- so a consumer that read this array as denials would have had France's authority denying a certification nobody claimed." },
                    "text": { "type": "string", "minLength": 1,
                      "description": "The authority's sentence, verbatim: never paraphrased, never re-wrapped, never whitespace-collapsed, diacritics intact and NFC. One character changed is a different sentence and a reader comparing a document against this file is entitled to say so." },
                    "context": { "type": "string", "minLength": 1,
                      "description": "The authority's own neighbouring sentence, where the sentence above does not read without it -- Latvia's VID denial is a 'these' whose antecedent is the sentence before it. The authority's words, not the ledger's." },
                    "lang": { "type": "string", "minLength": 1,
                      "description": "The language the sentence is in, so a reader is not left to guess whether a Latvian sentence is a mistranscribed English one. The registry's own test is stricter than this (a two-letter code); the contract asks only that a set which states a language states something." },
                    "url": { "type": "string", "minLength": 1,
                      "description": "The authority's page this sentence was read from -- which may not be the source's own `source_url`: a list's denial often sits on the guidance page the list is linked from." },
                    "read_on": { "$ref": "#/$defs/iso_date",
                      "description": "The day a person read this sentence on that page. Required of every entry whatever its role, which is stricter than `evidence/authority_denials.py`'s own reader (that one requires the page and the day only of a `denies` entry, because only those reach a document). The contract is stricter on purpose: a sentence attributed to a government with no address and no date is not a quotation anyone can check, and publishing one would be this register making a claim about an authority that nobody can go and verify." },
                    "ledger_note": { "type": "string",
                      "description": "This ledger's own reading of the sentence, never the authority's, and labelled as such in the registry that records it. Published here because this file is the record of what the register holds; a DOCUMENT that set it beside the authority's sentence under one heading would be the fault this field exists to prevent, so `evidence/authority_denials.py` withholds it from an extract. The two decisions are separate and both are deliberate." },
                    "question": { "type": "string",
                      "description": "The heading the register filed this sentence under, which is the register's own and not the authority's. Published and withheld for the same reason as `ledger_note`." }
                  }
                }
              },
              "snapshot_policy": { "enum": ["public", "private"] },
              "quarantined": { "type": "boolean",
                "description": "Whether this source was still inside its observation window when the set was published, so no record of its own had reached a reader yet (engine/guards.in_quarantine). NOT declared-only, unlike basis and the four fields above it: the engine states it on every source entry, fetched or not, because it is this register's own fact about its own window rather than something the registry records. On all 49 frozen entries and all 5 demo entries and named nowhere until CT-39, so the field admitted `\"no\"`, `0`, a null or an object where a reader expects a boolean." },
              "last_updated": { "oneOf": [{ "$ref": "#/$defs/iso_ts" }, { "type": "null" }] }
            }
          }
        },
        "mandate": {
          "type": "object",
          "properties": {
            "next_deadline": { "oneOf": [{ "$ref": "#/$defs/iso_date" }, { "type": "null" }] },
            "description": { "type": "string" },
            "last_confirmed": { "$ref": "#/$defs/iso_date" },
            "page_source_id": { "type": "string" }
          }
        }
      },
      "$comment": "CT-28, G14-1, and the v3 bump this file's description declares. The two clauses below refuse two shapes v2 allowed. WHAT WENT WRONG WITHOUT THEM: `regime` was optional on a country and undefined on a source entry, so a published set in which no reader could tell HMRC's mtd-vat list from France's e-invoicing one was a VALID set -- measured by the fourteenth activation gate over a real build of 493 routes, with /countries/ printing 'not stated' for both countries and the GB pages titled 'e-invoicing providers on the official list' over 468 VAT-recognition entries. The rows held the distinction and the summary a reader meets first did not. WHAT THESE CLAUSES CANNOT DO, said here rather than left to be found: a set that records no regime anywhere still conforms, because a source whose registry entry states none must publish none (that is the declared-only rule the rest of this file follows) and a published file cannot tell that set from one that dropped the field. So these clauses close the half a contract can close -- the country level may not be silent about a regime its own source entries state, and the singular may not stand beside the plural -- and the engine is held to the other half by tests/test_the_regime_reaches_the_published_set.py, which drives the writer over a registry that records regimes and asserts every one of them arrives. EV-58 LATER ADDED `authority_denials` TO THE SOURCE ENTRY and that is the v4 bump, not this one: it defines a key that was previously undefined here, it adds no clause to this allOf, and it is the authority's own words where `regime` is this register's classification -- which is why one is summarised per country and the other is never aggregated above the source entry that records it. COST, measured rather than assumed: the frozen contract set, the demo export set and data/exports all validate unchanged, because each either carries no source-level regime at all (the clauses are then vacuous) or carries a country `regime` with no `regimes` beside it. THAT MEASUREMENT HELD BECAUSE NO SET EXERCISED THESE CLAUSES, AND CT-29 ENDED THAT: the frozen contract set in tests/fixtures/exports now records a regime on 47 of its 49 source entries, states `regimes` and no `regime` for one country whose mandate page and provider list sit in different regimes, and states neither key for one whose sources record none -- so the first clause is live on 24 countries and the second on all 25, where before CT-29 both were vacuous on every set this repository ships. All 25 validate. Why that is worth a sentence in a contract: the site's reader of this field (site/lib/regime.ts) and the end-to-end checks over it are built against that frozen set, so until CT-29 every guard on the two shapes a country can take besides the singular had never once met one.",
      "allOf": [
        { "if": { "required": ["sources"],
                  "properties": { "sources": { "contains": { "required": ["regime"] } } } },
          "then": { "anyOf": [{ "required": ["regime"] }, { "required": ["regimes"] }] } },
        { "not": { "required": ["regime", "regimes"] } }
      ]
    },

    "chain_index": {
      "type": "object",
      "description": "Every event in the chain, in order, by hash alone. The change feed may withhold an event's body — provisional claims about a named company are held until a second run agrees — but it may never withhold the fact that the line exists, because that is what makes a deletion undetectable. A verifier requires that every published event appears here in order, and that every gap is an entry marked withheld rather than an absence.",
      "required": ["count", "first_event_hash", "last_event_hash", "entries"],
      "additionalProperties": false,
      "$comment": "CLOSED BY CT-61, 21 October 2026, together with its `entries` items below. Writers read rather than assumed: engine/publication.py builds this object with exactly `count`, `first_event_hash`, `last_event_hash` and `entries`, and each entry with exactly `n`, `event_hash` and `published` plus `withheld_because` where a body is held; scripts/golden/assembly.build_chain_index writes the same keys. WHY THIS FILE AND NOT THE FEED BESIDE IT: this file's own hash goes into the run's sealed digest and into run_summary.stamp.index_sha256 (AUD-7), so a key the contract does not bound here is bytes inside a timestamped proof the contract says nothing about; and the file exists to keep a withheld body distinguishable from a deleted line, which the closed enum on `withheld_because` states and a free-text key beside it would undo -- that property's own description already says a forger would write one. Measured under Draft202012Validator with this clause in place: 1,393 index entries across the three shipped sets, 654 of them withheld, 0 errors.",
      "properties": {
        "count": { "type": "integer", "minimum": 0 },
        "first_event_hash": { "$ref": "#/$defs/sha256" },
        "last_event_hash": { "$ref": "#/$defs/sha256" },
        "entries": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["n", "event_hash", "published"],
            "additionalProperties": false,
            "$comment": "CLOSED BY CT-61 with the object above it, for that object's reason and one of its own: an entry is three facts about one event, and the `if/then` clause below requires a reason of an unpublished one. A key beside `withheld_because` that no clause bounds would be the free-text reason that property's description refuses, arriving under another name. Closing the ENTRY and not the `if` below it is the whole distinction CT-61 measured: a closed `if` matches nothing that carries another key, so the clause it guards would require nothing at all.",
            "properties": {
              "n": { "type": "integer", "minimum": 1, "description": "Position in the chain, from 1." },
              "event_hash": { "$ref": "#/$defs/sha256" },
              "published": { "type": "boolean", "description": "Whether this event's body appears in the change feed." },
              "withheld_because": {
                "enum": ["provisional", "quarantine", "owner_hold", "private_source"],
                "description": "Required when published is false. Never a free-text reason: a forger would write one."
              }
            },
            "allOf": [
              {
                "$comment": "CT-09, mirroring the if/then shape GB-5/EV-13 used for removed_at above: `required: [\"published\"]` inside the if, not just the properties check, so an entry that omits `published` altogether does not vacuously satisfy the if -- the same trap GB-9's route (a) exploited elsewhere, where a properties-only check let a deleted field buy a false ok=True. CT-10 (tenth gate): here that guard is redundant in practice, because entries.items already lists `published` under its own top-level `required` above, so an entry omitting it is rejected there before this if is ever reached at all -- it is not this clause that catches that shape. The guard is kept anyway, because it is the correct if/then shape regardless of what the enclosing required happens to list, and a guard that is redundant today costs nothing while a missing one would, if that outer required ever changed, reopen exactly the hole GB-9 found. Unconditional would be wrong: an entry whose body is published carries no reason and the contract does not ask it to.",
                "if": {
                  "required": ["published"],
                  "properties": { "published": { "const": false } }
                },
                "then": { "required": ["withheld_because"] }
              }
            ]
          }
        }
      }
    },

    "latency_state": {
      "enum": ["measured", "single_fetch", "never_fetched", "undatable", "nothing_measured"],
      "description": "Whether a latency figure could be measured, and where not, which honest absence it is. `single_fetch`: one run on record, so there is no gap between it and nothing. `never_fetched`: no run on record. `undatable`: a run timestamp that will not read, or one dated before the run before it -- engine/dating.py refuses such a line rather than skipping it. `nothing_measured`: no change carrying both ends of its own wait yet. A figure is omitted in all four, never published as 0 or null: both of those read as a measurement and neither is one."
    },
    "latency_why": {
      "type": "string",
      "description": "Why a figure is absent, in the row itself rather than in a note a consumer has to go and find."
    },
    "latency_wait": {
      "type": "object",
      "$comment": "NOT TO BE CLOSED, and CT-61 measured why rather than leaving it to be rediscovered: run_summary.latency.detection_to_publication.by_source[] names its fields by composing this definition with `allOf` and adds `source_id` beside it, and `additionalProperties` sees only the `properties` of the schema it sits in -- never a sibling's. So `additionalProperties: false` here refuses every by_source row the frozen and demo sets publish on `source_id`, and the same keyword on by_source[] itself refuses the five fields THIS definition names. Measured under Draft202012Validator on 21 October 2026: both closures refuse run_summary.json in both sets, with the contract otherwise unchanged. A shape composed by reference is closed, if at all, in the one place that sees every property -- which for these two is nowhere.",
      "description": "The spread of how long changes waited, in whole seconds, or the stated reason there is none. `median_seconds` is the ordinary median -- the middle value, or the mean of the two middle ones -- said here so a reader recomputing it gets this number and not a neighbouring definition's.",
      "required": ["state", "changes"],
      "properties": {
        "state": { "$ref": "#/$defs/latency_state" },
        "changes": { "type": "integer", "minimum": 0 },
        "median_seconds": { "type": "number", "minimum": 0 },
        "shortest_seconds": { "type": "integer", "minimum": 0 },
        "longest_seconds": { "type": "integer", "minimum": 0 },
        "why": { "$ref": "#/$defs/latency_why" }
      }
    },
    "run_summary": {
      "type": "object",
      "$comment": "CT-18, and the v2 bump this file's description declares: `feed` is required. GB-29 and EN-32 made a run publish the window it applied and EV-42 made a reader refuse a feed shorter than it -- measured by EV-42 and re-driven independently by the coordinator, the identity forgery went from 116 of 116 ok=True to 0 of 116 and the word forgery from 48 of 48 to 0 of 48 -- but the block was not required here, and the rung's own sentence says a set stating no window at all can be held to no length. So the forger deleted the block and the rung that closed the whole of gate blocker G12-5 went quiet. A STATE THE FORGER CAN CHOOSE IS NOT A THIRD STATE (docs/RESUME.md, six items have turned on it), so the only answer is to make the omission non-conforming and take the bump: a bump is exactly for a change that makes something previously valid invalid, and GB-27 took none precisely because its change was additive, so the two decisions agree rather than conflict. The alternative considered and rejected was requiring it only of sets published on or after a stated date, with a reader failing an omitting set by the set's own publication date. That date is `run_ts`, the first property below, in the same unsealed file as the block the forger is deleting -- run_summary.json carries no seal, because the stamp block lives inside it. Driven on the frozen contract set: `run_ts` moved back two years on an otherwise honest set reads ok=True, 60 checks, 0 failed, 3 unverified, exactly as the honest set does, and with the stamp block removed as well (pending_no_ots is a legitimate state, so a forger need not carry a proof) ok=True, 58 checks, 0 failed. And the two edits together, which is the shape the date gate would have had to catch: the word raised in all four files, the feed cut to 9, the block deleted AND `run_ts` moved back to before any plausible stated date reads ok=True, 51 checks, 0 failed, 6 unverified -- identical in every count to deleting the block alone, so the second edit costs the forger nothing. A timestamp proof could only show a set is at least as old as some instant, never that it is no older, so it cannot refute a date moved backwards. The date is the forger's to choose, so date-gating would have moved the hole from one field of this file to another. WHAT THIS DOES NOT DO, said here rather than left to be found: it does not make the extract reader refuse an omitting set. The reader may not read this file at all -- tests/evidence/test_offline.py records every path verification opens and fails if one lies outside the reader's own directory -- so a reader holding a third party's set still reports the absence in the third state. What changes is that the absence is no longer a shape the contract allows: the two sets this repository ships are held to it by tests/evidence/test_the_sets_we_ship_match_the_contract.py, and a second implementation reading this file must publish the block. The reader's own sentence for that absence still cites this file's `required` list as the reason a set without the block conforms (evidence/feed_length.py, `_WHERE[\"unstated\"]`) and is now wrong about it; correcting it, and deciding whether the reader should go further, is the evidence track's half and is not closed by this clause.",
      "required": ["run_ts", "sources", "feed"],
      "properties": {
        "run_ts": { "$ref": "#/$defs/iso_ts" },
        "sources": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["id", "result"],
            "properties": {
              "id": { "type": "string" },
              "result": { "enum": ["ok", "fetch_failed", "parse_failed", "source_anomaly", "quarantined", "skipped_to_verify"] },
              "events": { "type": "integer", "minimum": 0 },
              "quarantined": { "type": "boolean",
                "description": "Whether this source was inside its observation window during this run (engine/guards.in_quarantine). Published on a row whose `result` is ok; a run whose fetch failed writes no window and no row carries either field (engine/run.py)." },
              "quarantine_stored_until": { "$ref": "#/$defs/iso_ts",
                "description": "THE STORED INSTANT, AND NOT THE DAY THIS SOURCE FIRST PUBLISHES, which is `publishes_from` beside it. It is 14 days to the second after the source was first read, taken off the source's saved state so a restart cannot move it (engine/run.py, engine/quarantine.quarantine_until), and it is published because it is what `data/state` holds byte for byte: a reader has the engine's own record to join on, and the figure the window was applied to. A reader who takes it as the day this source's records appear is up to a day early, because since EN-94 the window closes at the END of the UTC day it falls on. Both live sources, measured 20 October 2026 and neither yet published: gb_recognised_software's stored 2026-10-26T13:15:01+00:00 publishes from 2026-10-27T00:00:00+00:00, and fr_pa_list's stored 2026-10-20T11:35:28+00:00 from the 21st. THE DERIVATION (CT-58, published by the engine since EN-95): midnight UTC beginning the day after the UTC day the stored instant falls on. The rounding goes up because the window is a floor rather than a target -- rounding it down would leave the clock time of the first fetch deciding how long a source was watched, which is the defect EN-94 closed rather than moved -- and the one exception is not a rounding at all: a window closing at or before the source's baseline is switched off, so quarantine_days 0 still publishes on the first run. engine/dating.within_to_the_day holds both halves of that and is what engine/quarantine.in_quarantine asks, so the derivation above is checkable against the rule the engine enforces rather than restated from it. This field was named `quarantine_until` until EN-95, which is the name it carried while it was the only instant published here; the old name is gone rather than kept beside the new pair, because two fields that both read as \"publication begins here\", with values up to 24 hours apart, are two numbers waiting to disagree. Both fields were published here and named nowhere until CT-39." },
              "publishes_from": { "$ref": "#/$defs/iso_ts",
                "description": "THE INSTANT THIS SOURCE'S RECORDS MAY START BEING PUBLISHED, which is what a reader computing a first-publication day takes -- never `quarantine_stored_until`, which is up to a day earlier. Midnight UTC beginning the day after the UTC day the stored instant falls on, with the one exception that is not a rounding: a window closing at or before the source's baseline is switched off, and the baseline itself is published here, so a source whose quarantine_days is 0 publishes from the moment it was first read. NOT A SECOND IMPLEMENTATION OF THAT RULE (EN-95): the engine derives this through engine/dating.closes_to_the_day, which is the single function engine/dating.within_to_the_day asks before it holds a source back, so the instant published here and the instant the guard enforces are one instant by construction and cannot drift apart. `engine/export.py` decides what reaches the site from the same state, so this is also the day the records behind this row appear. Published on a row whose `result` is ok, beside the stored instant it is derived from; a run whose fetch failed writes no window and no row carries either field (engine/run.py). On the day it names, this register publishes the source's records for the first time: France's fr_pa_list carries 2026-10-21T00:00:00+00:00, measured 20 October 2026 from its stored 2026-10-20T11:35:28+00:00 and a baseline of 2026-10-06T11:35:28+00:00." },
              "parsed_sha256": { "$ref": "#/$defs/sha256",
                "$comment": "CT-41. The last of EV-55's two publications of this digest to be named: `$defs/event`'s copy is the one on a baseline, and this is the per-source one every `ok` row has carried since EV-55 (engine/run.py reads it off the source's new state, one line covering both kinds). A reader is already waiting on it. site/lib/runKind.ts states, in the file rather than in a note, that a run which read a source and wrote no event is dated by an observation seal no published file carries, that this field would tell such a run from a pre-EN-48 one, and that the site will not read it because \"the engine publishes it undeclared and this site reads only what the contract declares\" (S-43). Neither shipped set publishes it -- measured 13 October 2026 on tests/fixtures/exports and demo/exports -- so the sweep that reads those two sets could not report it.",
                "description": "The digest of what this run DERIVED from the source it read: the records for a provider_list or manual source, the extracted text for a page (engine/run.py reads it off the source's saved state, which is where both kinds put it). Published on a row whose `result` is ok and on no other: a fetch that failed, a parse that failed and a source the run never requested derived nothing to hash. Two runs publishing the same value here read the same thing twice, which is what makes a quiet day a reading rather than a silence. It is NOT a digest of any published file in this set, and it is not the publisher's bytes -- that is `snapshot_sha256` on the events of the same run." },
              "rows_dropped": { "type": "integer", "minimum": 0,
                "description": "How many rows of the publisher's file a declared row filter left out of this reading (SR-22, engine/row_filter.counted_in_summary). Read off the event base rather than recomputed, so this file and the events of the same run cannot become two numbers. Published only for a source whose registry entry declares a filter; the declaration itself, the column and the values kept are on every event that source wrote, under `row_filter` in $defs/event." }
            }
          }
        },
        "guards": {
          "type": "object",
          "description": "The guard parameters the run that wrote these files enforced (EN-32), on EN-14's rule: each figure is passed into the export by the run that applied it rather than re-read from config/settings.yaml while the files are written, so a page showing one of these cannot show a number the run did not apply. /method described four guards and could state a figure for only one of them, and typed the other two from the configuration file as it read on the day the page was written. changelog_max_export is not here: it parameterises no guard, and since GB-29 it is published in the `feed` block below, where a set that applied no cap can say so in a word instead of stating a figure that would be false of it.",
          "required": ["enforced", "parameters"],
          "additionalProperties": false,
          "properties": {
            "enforced": { "type": "string",
              "description": "What the block is, in the engine's own words, for a page to print rather than paraphrase." },
            "parameters": {
              "type": "object",
              "description": "One entry per parameter, keyed by its name in config/settings.yaml so a reader can find the line it came from. Every entry a run writes is read through the same function in engine/guards.py that the guard itself reads it through.",
              "required": ["anomaly_max_vanished_fraction", "anomaly_min_vanished", "quarantine_days", "withdrawal_hold_hours"],
              "additionalProperties": {
                "type": "object",
                "required": ["value", "unit", "means"],
                "additionalProperties": false,
                "properties": {
                  "value": { "type": "number", "minimum": 0,
                    "description": "The figure the run applied. A fraction is a fraction of one, not a percentage: anomaly_max_vanished_fraction 0.2 is 20 per cent of the previous list." },
                  "unit": { "type": "string",
                    "description": "What the figure counts, so a page is not left to guess that 14 is days and 0.2 is a share." },
                  "means": { "type": "string",
                    "description": "What the parameter does, in the engine's own words. Published for the same reason latency's per-row `why` is: a page printing this cannot describe a rule differently from the code that enforces it." }
                }
              }
            }
          }
        },
        "feed": {
          "type": "object",
          "description": "The window changes.json was published under (GB-29, twelfth gate G12-5). The feed is capped from the old end, and while the cap was in no export a reader could not tell an honest window from one a forger chose: a feed cut to just short of a record's newest event reads exactly like a narrow cap, and every rung that reads events passes behind one -- 116 of 116 renamed records and 48 of 48 forged basis words did. One subtraction closes that, so one of its terms is published here and the other is the reader's own count out of the sealed index: an honest feed holds min(max_entries, the entries chain_index.json marks published), and a feed shorter than that is short of what this set says it published. No count of the published entries is published beside the figure, because a second copy of a number the reader already holds is two numbers waiting to disagree, and the copy would be the forger's to write. THREE CASES, and the third matters most. A set that capped: `window` is \"capped\" and `max_entries` is the cap the run applied -- the run's own figure, passed into the export by the run that applied it (EN-14's rule, EN-32), not the one config/settings.yaml now holds. A set that did not cap: `window` is \"uncapped\" and there is no figure at all, which is what the frozen contract set publishes, and the reason this overrides EN-32's decision rather than overruling its argument -- a figure there would have been false of that set. And a set with no `feed` block at all: that WAS the third case, and since CT-18 there is no such conforming set. The block is in run_summary.json's `required` list, for the reason that definition's own $comment gives -- it was the one of the three cases a forger could choose, and a state the forger chooses is not a third state. The distinction CT-18 turns on, kept here because this is where a reader meets the three cases: a publisher may no longer omit the block, and a READER holding a set that omits it is still not holding a damaged set. Such sets were published before this field existed, the reader cannot validate against this file at all (it opens no path outside its own directory), and `chain:feed_is_as_long_as_the_set_says` goes on stating that absence in the third state rather than failing it. What the requirement buys is that an omission is no longer something the contract permits: the sets this repository ships are held to it at build time, and a second implementation reading this file must publish it.",
          "required": ["file", "window", "enforced", "reader_rule"],
          "additionalProperties": false,
          "properties": {
            "file": { "const": "changes.json",
              "description": "The file this window is about, named rather than assumed: the complete feed is published as changes/<year>.json under no cap at all, and this block is a statement about the capped one." },
            "window": { "enum": ["capped", "uncapped"],
              "description": "Which of the two windows the run published under. A word and not a figure, because a set that capped nothing has no figure to state: engine/latency.py publishes a stated state for the same reason, where a 0 or a null would read to a consumer as a measurement." },
            "max_entries": { "type": "integer", "minimum": 0,
              "description": "How many of the newest published events the feed carries. Required where `window` is \"capped\" and forbidden where it is \"uncapped\", so a set cannot state a window and a figure that disagree, and a figure cannot be read as a window that was never applied." },
            "enforced": { "type": "string",
              "description": "What the block is, in the engine's own words, for a page to print rather than paraphrase." },
            "reader_rule": { "type": "string",
              "description": "The subtraction a reader does with this block and chain_index.json, in the engine's own words, so the rule travels with the files instead of living only in a verifier a reader has to have been given." }
          },
          "allOf": [
            {
              "$comment": "CT-09's if/then shape, for the reason given on chain_index.entries: `required` inside the if and not only a properties check, so a block that omits `window` altogether does not vacuously satisfy it -- the trap GB-9's route (a) exploited elsewhere, where a properties-only check let a deleted field buy a false pass. `window` is in the enclosing required too, which makes the guard redundant on this first clause and not on the second: the second is what stops an uncapped set carrying a figure, which is the shape EN-32 refused to publish.",
              "if": { "required": ["window"], "properties": { "window": { "const": "capped" } } },
              "then": { "required": ["max_entries"] }
            },
            {
              "if": { "required": ["window"], "properties": { "window": { "const": "uncapped" } } },
              "then": { "not": { "required": ["max_entries"] } }
            }
          ]
        },
        "api": {
          "type": "object",
          "description": "The free API tier's own published numbers (CT-03), read by the engine from api.allowance rather than kept as a second, hand-typed copy -- api/allowance.py's own docstring names the same reason: the number shown here and the number applied to a caller must not be able to become two different numbers.",
          "required": ["free_tier", "route_allowance", "daily_new_keys", "free_refusal"],
          "properties": {
            "free_tier": {
              "type": "object",
              "required": ["daily_requests", "requests_per_second", "daily_extracts"],
              "properties": {
                "daily_requests": { "type": "integer", "minimum": 0 },
                "requests_per_second": { "type": "integer", "minimum": 0 },
                "daily_extracts": { "type": "integer", "minimum": 0 }
              }
            },
            "route_allowance": {
              "type": "array",
              "description": "Routes carrying an allowance of their own on top of the daily request cap, e.g. /v1/extracts against daily_extracts.",
              "items": {
                "type": "object",
                "required": ["route", "tier_field", "header_label", "limit"],
                "properties": {
                  "route": { "type": "string" },
                  "tier_field": { "type": "string", "description": "The free_tier field this route's allowance is drawn from." },
                  "header_label": { "type": "string", "description": "The label its RateLimit-<label>-* response headers carry." },
                  "limit": { "type": "integer", "minimum": 0 }
                }
              }
            },
            "daily_new_keys": { "type": "integer", "minimum": 0,
              "description": "Daily cap on free-key issuance as a whole; see api/allowance.py DAILY_NEW_KEYS." },
            "free_refusal": { "type": "string",
              "description": "What a spent allowance costs a caller, in the API's own words." }
          }
        },
        "latency": {
          "type": "object",
          "description": "The latency this ledger can measure from its own run times, and nothing more (EN-14). S-07 killed latency measured against a date the source publishes: no parser reads one, so the only honest way to have one would be a human transcribing a publication date per record per run for ever, and applying one page timestamp to every change detected on that page manufactures a precision the record does not have. What is published here is what the ledger observes from its own clock: how often each source is read, and how long a change waited between the run that first saw it and the run that published it. Every figure is in whole seconds, because run timestamps are whole seconds, and every one is recomputable by a reader from the files named in `recomputable_from` -- a number nobody can check is worth less than no number. A figure that cannot be measured is never 0 and never null: it is a stated state, because both of those read to a consumer as a measurement.",
          "required": ["measures", "withdrawal_hold_hours", "recomputable_from", "fetch_interval", "detection_to_publication"],
          "properties": {
            "measures": { "type": "string",
              "description": "In plain words, what these figures are and are not: time from a change becoming visible to anyone polling the source to this ledger publishing it, NOT time from the authority's own decision. No source publishes when it decided, so the gap between the decision and the page is unmeasurable here and is in none of these figures. Published rather than left to the method page, because the figures travel without it." },
            "withdrawal_hold_hours": { "type": "integer", "minimum": 0,
              "description": "The hold a withdrawal serves between its first absence and publication (config/settings.yaml, docs/DEFAULTS.md section 3). Published beside the figure it dominates: detection_to_publication sits on top of this floor by design, so a reader can see the guard rather than mistake it for slowness. It is the floor the run that wrote these files enforced, not a constant." },
            "note": { "type": "string", "description": "Why the hold dominates the figure, in the ledger's own words." },
            "recomputable_from": { "type": "object",
              "description": "Which published file a reader recomputes each figure from. Not the same file in every set: a run writes the complete feed as changes/<year>.json and caps changes.json, and the golden fixture set publishes changes.json alone.",
              "required": ["fetch_interval", "detection_to_publication"],
              "properties": {
                "fetch_interval": { "type": "string" },
                "detection_to_publication": { "type": "string" }
              }
            },
            "fetch_interval": {
              "type": "object",
              "description": "How often each source is read. The run timestamps the figures are computed over are published beside them, so the figure is the reader's to recompute rather than the engine's to assert.",
              "required": ["fetches_kept_per_source", "sources", "aggregate"],
              "properties": {
                "fetches_kept_per_source": { "type": "integer", "minimum": 2,
                  "description": "How many run timestamps a source keeps. The list is published, so it cannot grow for ever; `fetches_on_record` says how many each figure is computed over." },
                "sources": {
                  "type": "array",
                  "description": "Every configured source, including ones never read: a source missing from this array cannot be told from one that was read and had no gap.",
                  "items": {
                    "type": "object",
                    "required": ["source_id", "state", "fetches_on_record", "fetched_at", "intervals"],
                    "properties": {
                      "source_id": { "type": "string" },
                      "state": { "$ref": "#/$defs/latency_state" },
                      "fetches_on_record": { "type": "integer", "minimum": 0 },
                      "fetched_at": { "type": "array", "items": { "$ref": "#/$defs/iso_ts" },
                        "description": "The runs that read this source, oldest first. A run whose fetch or parse failed writes no state, so it is not here; it is in the feed as fetch_failed, parse_failed or source_anomaly." },
                      "intervals": { "type": "integer", "minimum": 0 },
                      "interval_seconds": { "type": "array", "items": { "type": "integer", "minimum": 0 },
                        "description": "The gaps between consecutive runs in `fetched_at`, in whole seconds. Written only where state is `measured`." },
                      "median_seconds": { "type": "number", "minimum": 0 },
                      "shortest_seconds": { "type": "integer", "minimum": 0 },
                      "longest_seconds": { "type": "integer", "minimum": 0 },
                      "why": { "$ref": "#/$defs/latency_why" }
                    }
                  }
                },
                "aggregate": {
                  "type": "object",
                  "description": "Every interval on record pooled into one set -- the set published above, not an average of the per-source medians -- with a count of the sources in each state.",
                  "required": ["state", "intervals", "sources_measured", "sources_single_fetch", "sources_never_fetched", "sources_undatable"],
                  "properties": {
                    "state": { "$ref": "#/$defs/latency_state" },
                    "intervals": { "type": "integer", "minimum": 0 },
                    "sources_measured": { "type": "integer", "minimum": 0 },
                    "sources_single_fetch": { "type": "integer", "minimum": 0 },
                    "sources_never_fetched": { "type": "integer", "minimum": 0 },
                    "sources_undatable": { "type": "integer", "minimum": 0 },
                    "median_seconds": { "type": "number", "minimum": 0 },
                    "shortest_seconds": { "type": "integer", "minimum": 0 },
                    "longest_seconds": { "type": "integer", "minimum": 0 },
                    "why": { "$ref": "#/$defs/latency_why" }
                  }
                }
              }
            },
            "detection_to_publication": {
              "type": "object",
              "description": "How long each published change waited between the run that first saw it and the run that published it: two agreeing runs (engine/confirm.py) plus withdrawal_hold_hours for a withdrawal plus whatever an owner hold added. Measured over the published feed alone, so nothing here rests on a body a reader cannot read.",
              "required": ["aggregate", "by_type", "by_source", "left_out"],
              "properties": {
                "aggregate": { "$ref": "#/$defs/latency_wait" },
                "by_type": {
                  "type": "object",
                  "description": "One figure per change type. The detection run is read from the field that type carries it on: added -> first_present, modified -> changed_at, removed -> removed_at. An addition was read from first_seen until EN-28, which is the record's own history rather than this claim's.",
                  "properties": {
                    "added": { "$ref": "#/$defs/latency_wait" },
                    "modified": { "$ref": "#/$defs/latency_wait" },
                    "removed": { "$ref": "#/$defs/latency_wait" }
                  }
                },
                "by_source": {
                  "type": "array",
                  "items": { "allOf": [{ "$ref": "#/$defs/latency_wait" }],
                    "required": ["source_id"],
                    "properties": { "source_id": { "type": "string" } } }
                },
                "left_out": {
                  "type": "object",
                  "description": "Published changes deliberately not measured, counted rather than silently dropped.",
                  "required": ["returning_record", "published_on_one_observation", "undatable"],
                  "properties": {
                    "returning_record": { "type": "integer", "minimum": 0,
                      "description": "Additions of a provider that had a published withdrawal before them. Excluded since EN-14, when ts - first_seen on such an addition gave the record's whole life rather than this claim's wait. EN-28 removed that reason -- the event now carries first_present, this claim's own detection run -- and left the exclusion standing, because dropping it changes a published shape rather than a number and this count has a reader on the other side of it. A set may therefore count an addition here whose own wait it could state." },
                    "published_on_one_observation": { "type": "integer", "minimum": 0,
                      "description": "page_changed events. A mandate page is not a statement about a named company, so it is not held for a second run: its detection and its publication are the same run, and counting those zeros in would report a wait nobody served." },
                    "undatable": { "type": "integer", "minimum": 0,
                      "description": "Changes whose own detection run will not read as a timestamp, or is dated after the run that published them." }
                  }
                }
              }
            }
          }
        },
        "coverage": {
          "type": "object",
          "description": "Which UTC days this record can show the register looked on, and which it cannot (EN-46, engine/coverage.py). Published by every run and by every republication. Before it, a day nobody looked on and a day the register looked on and saw nothing published the same thing -- nothing -- which is the one state this project treats as unacceptable, so the third state is an absence STATED. A day is accounted for by an event dated on it or by an observation seal naming it (EN-48); a run seal dates nothing here, because a run that wrote no event seals the index again under the earlier run whose events it covers.",
          "$comment": "CT-41 defines this block and `schedule` below it. Both are written on every publication by engine/export.py and neither shipped set carries either -- measured 13 October 2026 on tests/fixtures/exports and demo/exports -- so this file named neither, and the sweep in tests/test_the_contract_names_what_the_sets_publish.py could not report them: it reads the two shipped sets, so a block the writer always writes and no set has published is invisible to it. The clauses below are read off engine/coverage.block rather than inferred from the published states, since there is no published instance to read. WHY THE STATE CLAUSES AND NOT ONLY THE ENUM: `state` is decided from the two lists in the same object (`UNOBSERVED_DAYS if unobserved else (NOT_SETTLED if unsettled else UNBROKEN)`), so without them a set could publish `unbroken` beside eighteen days it admits nothing observed -- a word that contradicts the figures beside it, which is the shape a forger would choose and the one GB-9's reading of snapshot_policy exists to refuse elsewhere in this file.",
          "required": ["unit", "means", "reader_rule", "state"],
          "properties": {
            "unit": { "const": "utc_day",
              "description": "What a day in these lists is, named rather than assumed: the UTC day of the instant, which is the only day this ledger has (GB-7, engine/dating.py)." },
            "means": { "type": "string",
              "description": "What the block states and what it does not, in the engine's own words, for a page to print rather than paraphrase -- including that an empty `days_unobserved` is the positive statement that there is no such day in the window." },
            "reader_rule": { "type": "string",
              "description": "The recomputation a reader does from the portable archive, in the engine's own words. It names the archive and not these exports on purpose: the published files alone can only ever show MORE such days than this block does, because changes.json is capped and a withheld body takes its `ts` out of the feed while chain_index.json lists it by hash alone." },
            "state": { "enum": ["no_record", "unbroken", "unobserved_days", "not_settled"],
              "description": "Which of the four this record is in. `no_record` -- no event and no observation seal at all, so there is no window to report and nothing has been observed to be unbroken. `unbroken` -- every day of the window is accounted for. `unobserved_days` -- at least one day in the window the register holds no reading of any source for. `not_settled` -- the only days unaccounted for are ones this reader cannot decide, which is a stated state and not a hedge." },
            "why": { "type": "string",
              "description": "Why there is no window, in the engine's own words. Written on `no_record` and on no other state (engine/coverage.block), where a figure of 0 days would read as a measurement." },
            "first_day": { "$ref": "#/$defs/iso_date",
              "description": "The earliest day this record accounts for." },
            "last_day": { "$ref": "#/$defs/iso_date",
              "description": "The end of the window, which is THIS RUN'S own day and not the last day on record: a run that read nothing writes neither an event nor a seal, so today is then exactly the day with no reading and a window stopping at the record would hide it. An event or seal dated later than the run reading it is a date nothing supports, so it does not extend the window either (engine/dating.out_of_order)." },
            "days_unobserved": { "type": "array", "items": { "$ref": "#/$defs/iso_date" },
              "description": "Every day between `first_day` and `last_day` that neither an event nor an observation seal accounts for, and that this reader can decide. NOT a claim that no run happened: a run whose sources were all skipped or inactive read nothing and declines to seal an observation on purpose. What is absent either way is an observation of that day, which is the thing that cannot be recovered afterwards, and that is what is named." },
            "days_not_settled": { "type": "array", "items": { "$ref": "#/$defs/iso_date" },
              "description": "The days with no reading that this reader cannot decide at all, kept apart from the list above rather than merged into it. A quiet day before the observation seal existed sealed nothing, so for those days a missed day and a quiet day are indistinguishable and no later reader can see behind it; publishing them as days with no observation would be a claim this record cannot support." },
            "settled_from": { "$ref": "#/$defs/iso_date",
              "description": "The first day an observation seal dates, which is the floor under what may be decided: a day earlier than this one goes in `days_not_settled` whatever else is true of it. Absent where this record carries no observation seal, in which case no day in it can be settled." },
            "why_not_settled": { "type": "string",
              "description": "Why those days cannot be decided, in the engine's own words, travelling with the figure rather than sitting in a note a consumer has to go and find. Every reason that applies is given, because a reader acting on one needs to know the others are in play." },
            "not_placed": {
              "type": "object",
              "description": "What this record holds that cannot be placed in time, which bounds what the lists above may claim: either could be the evidence for a day that would otherwise be reported as having none. Absent where everything placed.",
              "properties": {
                "digests": { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1,
                  "description": "The sealed files whose own `run_ts` will not read as an instant, by name. Named and not counted: a reader can go and look at the file." },
                "events": { "type": "integer", "minimum": 1,
                  "description": "How many events carry a `ts` that will not read as an instant. Counted and not named, because the chain is where they are identified." }
              }
            }
          },
          "allOf": [
            {
              "$comment": "The no_record branch of engine/coverage.block returns `why` and no window at all, so the sentence is required where the figures are absent -- the rule engine/latency_figures.py sets for every stated absence in this file: a state a reader has no sentence for is worse than an absence.",
              "if": { "required": ["state"], "properties": { "state": { "const": "no_record" } } },
              "then": { "required": ["why"] }
            },
            {
              "$comment": "And the other three states all carry the window and both lists, written together in one `out.update` call, so a set naming one of them and publishing no window would be stating a coverage it never measured.",
              "if": { "required": ["state"],
                      "properties": { "state": { "enum": ["unbroken", "unobserved_days", "not_settled"] } } },
              "then": { "required": ["first_day", "last_day", "days_unobserved", "days_not_settled"] }
            },
            {
              "$comment": "`unbroken` is reached only where both lists are empty. Without this clause the word and the figures beside it could contradict each other, which is the whole defect EN-46 is about told in one object.",
              "if": { "required": ["state"], "properties": { "state": { "const": "unbroken" } } },
              "then": { "properties": { "days_unobserved": { "maxItems": 0 },
                                        "days_not_settled": { "maxItems": 0 } } }
            },
            {
              "$comment": "`not_settled` is reached only where nothing is in the first list and something is in the second, and `unobserved_days` only where something is in the first.",
              "if": { "required": ["state"], "properties": { "state": { "const": "not_settled" } } },
              "then": { "properties": { "days_unobserved": { "maxItems": 0 },
                                        "days_not_settled": { "minItems": 1 } } }
            },
            {
              "if": { "required": ["state"], "properties": { "state": { "const": "unobserved_days" } } },
              "then": { "properties": { "days_unobserved": { "minItems": 1 } } }
            },
            {
              "$comment": "A day nothing settles is published with the reason it cannot be settled: engine/coverage.block writes `why_not_settled` exactly when that list is non-empty.",
              "if": { "required": ["days_not_settled"],
                      "properties": { "days_not_settled": { "minItems": 1 } } },
              "then": { "required": ["why_not_settled"] }
            }
          ]
        },
        "schedule": {
          "type": "object",
          "description": "How late this run started, against the schedule the platform reported for it (EN-46 part (a), engine/scheduled.py). Published by every run and by every republication, and published as a STATED state where there is no figure: a run nobody scheduled was never due at any moment, and 0 would read as a run that started on time. The figure matters because a run late enough crosses midnight UTC and the day it was due for then has no observation in the register at all -- all ten runs measured on 6 October 2026 fired between 4h35m and 6h59m late against an 18h43m margin.",
          "required": ["means", "state"],
          "properties": {
            "means": { "type": "string",
              "description": "What the figure is and is not, in the engine's own words: none of it is time this engine spent detecting a change, most of it is the platform's queue, and the rest is whatever the workflow did before the engine was reached. Written on every state, including the states with no figure." },
            "state": { "enum": ["measured", "not_scheduled", "no_event_payload", "no_cron_in_event",
                                "cron_not_placed", "run_not_dated", "not_stated"],
              "description": "Which of the seven this run is in. `measured` is the only one with a figure. `not_scheduled` -- a manual or local run, never due. `no_event_payload` -- a scheduled run whose event payload could not be read. `no_cron_in_event` -- the payload named no cron this reader recognises, and the cron in the workflow file is deliberately not fallen back on, because that file says what the repository asks for now and not what started this run. `cron_not_placed` -- the cron is published beside the state and is a shape this reader refuses to place rather than guess at. `run_not_dated` -- the run's own timestamp will not read. `not_stated` -- the caller handed this publication no run environment, which is what a republication of a set written before the block existed publishes." },
            "why": { "type": "string",
              "description": "Why there is no figure, in the engine's own words, one sentence per state (engine/scheduled.WHY). Written on every state but `measured`, where the figure is its own answer." },
            "cron": { "type": "string", "minLength": 1, "maxLength": 100,
              "pattern": "^\\s*[0-9*/,-]+(\\s+[0-9*/,-]+){4}\\s*$",
              "description": "The cron the run's own event named, verbatim, and never the one the workflow file holds now. Published on `measured` and on `cron_not_placed` -- where the state exists to say that this reader will not place THIS expression, so withholding it would withhold the subject of the sentence.",
              "$comment": "The value comes from a file outside this repository and is published verbatim, so the shape is held here as well as at the writer: five whitespace-separated fields of digits and the four operators cron gives them meaning with, and at most 100 characters. That is engine/scheduled._is_cron exactly. Anything longer is not an expression this reader failed to place, it is something else arriving in a published file. Placing it in time refuses more than this does -- day-of-month, month and day-of-week must all be `*`, since a cron using them fires on a rule two readers implement two ways -- and that refusal is published as `cron_not_placed` with the expression beside it." },
            "scheduled_ts": { "$ref": "#/$defs/iso_ts",
              "description": "The moment the run was due: the LATEST firing of that cron at or before this run, on the UTC clock. So where a run is later than one whole firing this is the firing it missed most recently and not the first one it missed -- a lower bound on the delay, never an overstatement of it. The earlier firings it passed over are the days `coverage` in this same file states." },
            "queue_seconds": { "type": "integer", "minimum": 0,
              "description": "Whole seconds from `scheduled_ts` to `run_ts`. Never negative: the due moment is the latest firing at or before the run, floored to the minute, so a run at 05:17:30 was due at 05:17:00 and the seconds between them are the figure." },
            "lower_bound": { "type": "string",
              "description": "The one thing the figure does not cover, in the engine's own words, published with the figure rather than left for a reader to work out. Travels with the figure and only with it: a caveat about a measurement belongs where the measurement is, and the six states above have none." }
          },
          "allOf": [
            {
              "$comment": "CT-41, read off engine/scheduled._body: the measured branch returns all four of these together, so a set claiming the state without them would be claiming a measurement it does not publish.",
              "if": { "required": ["state"], "properties": { "state": { "const": "measured" } } },
              "then": { "required": ["cron", "scheduled_ts", "queue_seconds", "lower_bound"] }
            },
            {
              "$comment": "And every other state is `_stated(...)`, which is the state and its sentence. An unexplained absence here is the silence this item is about, and it is also a shape a forger would choose, so it is not one this contract admits.",
              "if": { "required": ["state"],
                      "properties": { "state": { "not": { "const": "measured" } } } },
              "then": { "required": ["why"] }
            }
          ]
        },
        "stamp": {
          "type": "object",
          "properties": {
            "state": { "enum": ["stamped", "upgraded", "stamp_failed", "pending_no_ots"] },
            "digest_sha256": { "$ref": "#/$defs/sha256" },
            "first_event_hash": { "$ref": "#/$defs/sha256" },
            "last_event_hash": { "$ref": "#/$defs/sha256" },
            "index_sha256": { "$ref": "#/$defs/sha256",
              "description": "SHA-256 of chain_index.json as published for this run, covered by the same timestamp proof as digest_sha256. Without this the index is an unsigned file in the set a forger controls: deleting an event's body and flipping its entry to withheld would read as legitimate. Re-marking an entry moves this value, and a past timestamp cannot be forged." },
            "withheld_this_run": { "type": "integer", "minimum": 0,
              "description": "How many of this run's events had their bodies withheld. Lets a reader tell a run that honestly holds something back from one that merely claims to." }
          }
        },
        "counts": {
          "type": "object",
          "properties": {
            "entities": { "type": "integer" },
            "listed": { "type": "integer" },
            "withdrawn": { "type": "integer" },
            "events_total": { "type": "integer" },
            "sources_active": { "type": "integer" },
            "accreditations_without_basis": {
              "type": "integer",
              "minimum": 0,
              "description": "How many published accreditations carry no basis. `basis` is optional here and in $defs.accreditation because the engine never infers one a source did not declare, so this is what tells a consumer whether any record it must refuse got through. A run that publishes a non-zero count fails; the count is published either way, so zero is stated rather than inferred from silence."
            }
          }
        }
      }
    },

    "history_file": {
      "type": "object",
      "additionalProperties": false,
      "required": ["record_id", "record", "events"],
      "description": "One published file of `exports/history/`, the whole history of one record. $defs.keys and $defs.provider.record_id have named `history/<record_id>.json` as the join a reader follows since v2, and until v10 this contract declared no shape for it: the register published 155 of these on 21 October 2026 and the only thing holding their format was prose in two descriptions.",
      "$comment": "CT-63, 22 October 2026, and the v10 bump. MEASURED over the two sets that carry the subtree: 10 files in demo/exports and 155 in data/exports, union of top-level keys across all 165 exactly `events`, `record`, `record_id`, every file carrying all three. CLOSED, on the same ground as $defs/chain_index at v9 and not on a preference: engine/export_files.write_history writes this object as one literal of exactly three keys, so a fourth is a hand edit rather than state passing through, and the site's profile page is built from this file alone -- an undeclared key here is free text on the page a reader meets a named company through. The openness this file does need is INSIDE it: `record` is $defs/provider, which stays open because engine/export_rows.public_record copies stored state through (the $comment there measures it), so closing the wrapper binds the three keys the writer chose and binds nothing about the row it carries. It refuses 0 of the 165 published instances.",
      "properties": {
        "record_id": { "type": "string", "description": "The record whose history this is. engine/export_files.write_history writes the file at `history/<record_id>.json`, so this value and the filename are one fact published twice; `evidence/history_file._wrong` is what holds them to each other, because no JSON Schema clause can compare a document with its own name." },
        "record": { "$ref": "#/$defs/provider", "description": "The listing the record is on now, which is what a profile page shows at the top. A record that left a list and came back is published as one row per listing (GB-27) and this is the last of them by `first_seen`; every listing's events are in `events` either way, so the file is the record's whole history and not one period of it." },
        "events": { "type": "array", "items": { "$ref": "#/$defs/event" },
          "description": "The record's events, oldest first, opened by the baseline that began its FIRST listing where one exists. Not a slice of changes.json: that file is a capped feed, and these are why the subtree exists at all." }
      }
    },

    "history_index_entry": {
      "type": "object",
      "additionalProperties": false,
      "required": ["record_id", "provider_name", "country", "source_id", "status", "events"],
      "description": "One entry of `exports/history/index.json`, which enumerates the subtree. `evidence/published_set.SUBTREES` has read this file as the list of what `history/` should hold since EV-19, and `exports:changes_and_history_are_complete` is the check that reads it.",
      "$comment": "CT-63, v10. CLOSED: engine/export_files.write_history appends one literal of exactly these six keys per record. MEASURED 22 October 2026: 165 entries across the two sets that carry the file (10 demo, 155 live), every one carrying all six and no seventh. A CORRECTION A READER SHOULD NOT HAVE TO MAKE TWICE: the CT-62 agent reported the WRAPPER as carrying seven keys -- `country`, `events`, `provider_name`, `record`, `record_id`, `source_id`, `status` -- and that set is this entry's six plus `record`, so what it measured was an index entry and not a wrapper. The wrapper carries three.",
      "properties": {
        "record_id": { "type": "string", "description": "The record this entry points at, and the stem of the file that holds it." },
        "provider_name": { "type": "string" },
        "country": { "$ref": "#/$defs/country" },
        "source_id": { "type": "string" },
        "status": { "enum": ["listed", "withdrawn"], "description": "Copied from the listing the record is on now, so it is the same value as `record.status` in the file this entry points at." },
        "events": { "type": "integer", "minimum": 0,
          "description": "How many events the file holds. A reader comparing this with the file's own `events` array is checking the subtree is whole, which is what one listing per relisting broke before the writer keyed its index by record rather than by row." }
      }
    },

    "history_index": {
      "type": "array",
      "items": { "$ref": "#/$defs/history_index_entry" },
      "description": "`exports/history/index.json` whole: one entry per record, sorted by country then provider name."
    },

    "change_index_entry": {
      "type": "object",
      "additionalProperties": false,
      "required": ["year", "events"],
      "description": "One entry of `exports/changes/index.json`, which says which years the complete change log is split into and how many events each holds.",
      "$comment": "CT-63, v10. CLOSED: engine/export_files.write_year_files writes this object as a two-key literal per year. MEASURED 22 October 2026: 1 entry in each of the two sets that carry the file, `{events, year}` in both. A SECOND CORRECTION: CT-63's own acceptance describes `changes/index.json` as an object of {events, year}; the file is an ARRAY of them, which is why a subschema declaring the object alone would have refused it.",
      "properties": {
        "year": { "type": "integer", "description": "The year of the day the events are published under, not of their text: engine/export_files.write_year_files files an event by `dating.day(ts)`, so a run stamped 2027-01-01T05:00:00+14:00 is filed under 2026." },
        "events": { "type": "integer", "minimum": 0 }
      }
    },

    "change_index": {
      "type": "array",
      "items": { "$ref": "#/$defs/change_index_entry" },
      "description": "`exports/changes/index.json` whole: one entry per year, oldest first."
    },

    "change_year": {
      "type": "array",
      "items": { "$ref": "#/$defs/event" },
      "description": "One published file of `exports/changes/`, every event of one year. The same item shape as changes.json and not the same file: changes.json is a capped feed, and these are the complete log it is a window on.",
      "$comment": "CT-63, v10. Declared as its own definition rather than pointed at `#/properties/changes.json`, so a vocabulary gap found in a year file is reported at a pointer that names the file it was found in. MEASURED 22 October 2026: 8 events in demo/exports/changes/2026.json and 10 in data/exports/changes/2026.json, 0 undeclared keys against $defs/event in either."
    }
  },

  "required": ["providers.json", "entities.json", "withdrawals.json", "changes.json",
               "countries.json", "run_summary.json", "chain_index.json"],
  "properties": {
    "providers.json": { "type": "array", "items": { "$ref": "#/$defs/provider" } },
    "entities.json": { "type": "array", "items": { "$ref": "#/$defs/entity" } },
    "withdrawals.json": { "type": "array", "items": { "$ref": "#/$defs/withdrawal" } },
    "changes.json": { "type": "array", "items": { "$ref": "#/$defs/event" } },
    "countries.json": { "type": "array", "items": { "$ref": "#/$defs/country_summary" } },
    "run_summary.json": { "$ref": "#/$defs/run_summary" },
    "chain_index.json": { "$ref": "#/$defs/chain_index" }
  },
  "patternProperties": {
    "^history/[0-9a-f]{16}\\.json$": { "$ref": "#/$defs/history_file" },
    "^history/index\\.json$": { "$ref": "#/$defs/history_index" },
    "^changes/index\\.json$": { "$ref": "#/$defs/change_index" },
    "^changes/[0-9]{4}\\.json$": { "$ref": "#/$defs/change_year" }
  }
}
