{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://netshow.ai/schema/ready-for-facelab-1.schema.json",
  "title": "NetShow Alive — FaceLab Handoff (netshow-alive/ready-for-facelab/1)",
  "description": "THE CONTRACT for the handoff artifact that carries filesystem facts from a lane that HAS a filesystem to a lane that does not.\n\nWHY IT EXISTS. The live-vrm seal door needs four things a browser cannot get by itself: the model's sha256, its bytes, its licence, and its expression inventory. A page has no filesystem. This document is how those facts travel from intake/racking/the facelab photo lane to the surface that seals.\n\nNOTE THE ID SHAPE. Unlike its two siblings, this format identifies itself with `schema` (a path-like string) rather than `fmt`. That is not a style choice available to change — the string 'netshow-alive/ready-for-facelab/1' is already hard-required by shipped code, and the discriminator key is already `schema` in the shipped fixture. Both are quoted below at their lines.\n\nDERIVED FROM TWO CONSUMERS, NOT FROM PROSE:\n  · tools/nsv-emit-live-vrm-row.mjs   — the older consumer, and the one that REFUSES. Builds a netshow-cast-live-1 row plus the §4.6 typed payload.\n  · staged-patches/opus5-livevrm-seal1-20260804/nsv-seal-livevrm.js nsvSealInputFromReady() — the newer consumer, staged. Shape-transforms this document into a seal input.\nThe written spec (READY-FOR-FACELAB-1.md, same lane) was read and is largely reproduced here, but WHERE ITS LINE CITATIONS DISAGREE WITH THE FILES, THE FILES WIN. The deltas found on 2026-08-05 are recorded in $defs.specCitationDeltas — the .md is substantively right and its line numbers have drifted.\n\nTHE CENTRAL DESIGN FACT. Both consumers hard-require EXACTLY ONE field: `schema`. Everything else degrades.\n  · nsv-emit-live-vrm-row.mjs:57  throws unless `ready` is an object\n  · nsv-emit-live-vrm-row.mjs:58-60  throws `unexpected ready schema \"…\" — want netshow-alive/ready-for-facelab/1` on any other value\n  · nsv-emit-live-vrm-row.mjs:61  `const agent = ready.agent || {}` — a MISSING agent block is tolerated, not refused\n  · nsv-seal-livevrm.js:372  'ready-not-object' · :373 'ready-schema-mismatch' · :376 `isObj(ready.agent) ? ready.agent : {}`\nSo `required: [\"schema\"]` is the whole of consumer truth. Requiring `agent` at the root would reject documents both shipped consumers accept. What a correct PRODUCER must emit is $defs.handoffComplete, and what is needed to reach a SEALABLE payload is $defs.sealReady.\n\nTHE ONE REFUSAL THAT IS NOT ABOUT SHAPE — and the reason this document is dangerous to publish carelessly. nsv-emit-live-vrm-row.mjs:73-74 cross-checks `agent.bytes` against the ACTUAL size of the .vrm on disk and throws `READY-FOR-FACELAB agent.bytes N != actual M — stale handoff, refuse`. That is a STALENESS GUARD over a file this schema cannot see. Keep bytes accurate; an out-of-date handoff is refused, not repaired. No JSON Schema can check it. nsv-validate.mjs reports the field as NOT-VERIFIABLE-HERE rather than implying it passed.\n\nCORPUS AS OF 2026-08-05T01:30Z — and this number was WRONG in the first draft of this file, which is worth saying plainly because the error is instructive. That draft claimed 'exactly TWO documents on the box declare this schema'. Two was the count under /home/netshow/voice-agent-demo, which is where this lane's whole census looked. THE BOX IS BIGGER THAN THE REPO. A wider sweep found:\n\n   5  under /home/netshow/voice-agent-demo\n       · s36-type6-wiring-20260802/fixtures/nsv-pos-plate-v1.ready.json — the ORIGINAL, minimal, predates the written spec\n       · its byte-identical twin under .worktrees/type6-wiring/\n       · opus5-livevrm-seal1-20260804/READY-FOR-FACELAB-1.example.json — the worked example\n       · s45-intake-q-20260804/own-ready.json — an intake lane's independently-shaped instance\n       · this lane's golden example\n  34  under /home/eving/alive-studio — REAL FaceLab job artifacts (round19 feedback-work and siblings),\n       the actual production population of this format, and the one no prior document counted\n  39  TOTAL\n\nAll 39 validate against this root. 13 of the 34 carry 71 expression groups and fail $defs.sealReady's downstream cap — correctly, and as a warning rather than a rejection. See the expression_inventory note for the false-reject this discovery corrected.\n\nThe first draft's 'N=2, small, stated plainly' was honest about its uncertainty and still wrong about its fact. The lesson is recorded rather than quietly patched: a census is only as wide as its search path.",
  "type": "object",
  "required": [
    "schema"
  ],
  "properties": {
    "schema": {
      "const": "netshow-alive/ready-for-facelab/1",
      "description": "REQUIRED, EXACT. The only hard requirement either consumer imposes.\n  · nsv-emit-live-vrm-row.mjs:58-60 throws on any other value.\n  · nsv-seal-livevrm.js:373 refuses 'ready-schema-mismatch — want netshow-alive/ready-for-facelab/1, got …'. The constant is declared at nsv-seal-livevrm.js:189.\n\nTHE ID WAS NOT INVENTED BY THE SPEC THAT DESCRIBES IT. It was already required by the row emitter and already carried by the s36 fixture before READY-FOR-FACELAB-1.md was written; the .md says so in its own §0. This matters for anyone tempted to renumber it: the string is load-bearing in shipped code."
    },
    "acceptedAt": {
      "type": "string",
      "description": "When the studio accepted this body. ISO-8601 UTC by convention. READ BY NOBODY — neither consumer references it. `format: date-time` is deliberately NOT asserted: the s36 fixture carries '2026-08-02T00:00:00Z' and the worked example '2026-08-04T21:45:13Z', both valid, but no code enforces it and this contract does not invent enforcement. $defs.handoffComplete asserts the format for producers."
    },
    "acceptedRun": {
      "type": "integer",
      "minimum": 1,
      "description": "Which acceptance run produced this. Integer >= 1 per READY-FOR-FACELAB-1.md §2. Unread by either consumer; the bound is producer discipline, and it is safe — both corpus documents carry 1."
    },
    "agent": {
      "type": "object",
      "description": "The body's filesystem facts — the payload of the whole handoff. Tolerated absent by both consumers (:61 and :376 both substitute {}), so it is optional HERE and required in $defs.handoffComplete. Every sub-field below is independently optional and independently degrades.",
      "properties": {
        "vrm": {
          "type": "string",
          "description": "A LOCAL ABSOLUTE PATH to the .vrm on the emitting host. NEVER a served URL — the served path differs per tree, so the URL is supplied separately at seal time (nsv-seal-livevrm.js:379 refuses 'model-url-or-id-required — the handoff carries a LOCAL path in agent.vrm, never a served url').\n\nHOST-PATH BY DESIGN, AND THEREFORE QUARANTINED. This is the one field in the estate that is SUPPOSED to hold an absolute filesystem path, which makes it the one field that must never be copied onward into a document a page renders. The sibling schema states the rule from the other side: nsv-agent-1.schema.json $defs.provenance.readyForFacelab copies sha256/bytes/title/license and pointedly NOT `vrm`. Read as a pair, the two files fence the leak from both ends."
        },
        "sha256": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$",
          "description": "sha-256 of the .vrm file. Reaches row.sha256 (nsv-emit-live-vrm-row.mjs:91, `typeof agent.sha256 === 'string' ? agent.sha256 : null` — a non-string degrades to null, it does not throw) and payload.model.sha256 via nsv-seal-livevrm.js:382.\n\nDOWNSTREAM IT IS RE-GATED AND LOWERCASED. nsv-liveface-bind.js:277-280 tests SHA256_HEX (:56, /^[0-9a-f]{64}$/) against `m.sha256.toLowerCase()` and stores the lowercased form, refusing 'model-sha256-invalid' otherwise. So an UPPERCASE digest survives the §4.6 gate (it is lowercased) but is written here in lowercase by every producer, and the pattern above is lowercase-only to match the fixtures and the sibling formats. A producer emitting uppercase is out of contract even though the downstream gate would forgive it."
        },
        "bytes": {
          "type": "integer",
          "exclusiveMinimum": 0,
          "description": "Size of the .vrm in bytes. THE STALENESS GUARD — nsv-emit-live-vrm-row.mjs:73-74 compares it to fs.statSync(vrmPath).size and THROWS on mismatch. This is the only field in the document whose wrongness is fatal rather than degrading, and its correctness is a fact about a file this schema cannot open. NOT VERIFIABLE BY ANY VALIDATOR that lacks the .vrm.\n\nBounds come from the downstream §4.6 gate, which is stricter than the handoff: nsv-liveface-bind.js:281-284 refuses 'model-bytes-invalid' unless bytes is a finite number, > 0, and an exact integer."
        },
        "title": {
          "type": "string",
          "maxLength": 120,
          "description": "Display name. Reaches row.name (nsv-emit-live-vrm-row.mjs:86) as `name || agent.title || path.basename(vrmPath, '.vrm')` — a three-step fallback, so absence is harmless. maxLength is READY-FOR-FACELAB-1.md's, not a code rule; no consumer caps it."
        },
        "license": {
          "type": "string",
          "maxLength": 60,
          "description": "THE BODY'S LICENCE — the .vrm file's, governing the model itself. Reaches row.license (nsv-emit-live-vrm-row.mjs:93) as `license || agent.license || 'NetShow'`; absence falls back to 'NetShow'.\n\nTWO LICENCES, NEVER CONFLATED (READY-FOR-FACELAB-1.md §5). This one governs the FILE. The sealed avatar PACKAGE has its own, at alive-package-1 `license {rights, owner}`. nsv-seal-livevrm.js keeps them in separate fields and never copies one into the other.\n\nmaxLength 60 is a REAL downstream rule, not decoration: nsv-liveface-bind.js:285-288 refuses 'model-license-invalid' over 60 chars, and strips <>&\"'` from what it keeps."
        }
      }
    },
    "face": {
      "type": "object",
      "description": "What the face can do, and (optionally) what was measured from the photo.",
      "properties": {
        "mode": {
          "type": "string",
          "description": "'liveface' marks a plate-rigged body. RECORDED, NOT ENFORCED — no consumer branches on it (READY-FOR-FACELAB-1.md §3 says so in terms, and a grep of both consumers confirms it). Left as a free string rather than an enum precisely because nothing validates it; inventing an enum here would be asserting a rule the estate does not have."
        },
        "expression_inventory": {
          "type": "array",
          "description": "MOVES-IN-PIXELS TRUTH — did this group actually move the face — NEVER a raw blendshape name list. That distinction is the whole point of the field.\n\nnsv-emit-live-vrm-row.mjs:77-82 filters to entries with a string `group` and maps `moves: e.moves === true`, so a malformed entry is DROPPED, not refused. Same treatment at nsv-seal-livevrm.js:387-390. A non-array degrades to []. NO CONSUMER OF THIS DOCUMENT CAPS THE ARRAY OR CONSTRAINS THE GROUP NAME — so neither does this root.\n\nTHE 64-ENTRY CAP AND THE GROUP PATTERN ARE DOWNSTREAM RULES AND HAVE BEEN MOVED TO $defs.sealReady. An earlier draft of this file asserted `maxItems: 64` and `pattern: ^[\\w-]{1,40}$` HERE, and it was wrong — provably. A census of the whole box (not just /home/netshow/voice-agent-demo) found 34 ready-for-facelab documents under /home/eving/alive-studio, and 13 of them are real, ACCEPTED FaceLab handoffs carrying 71 expression groups (round19 nekochan-job and siblings). The root cap rejected all 13. They are not malformed: every one of the 71 group names is downstream-legal, and the handoff's own two consumers accept them without complaint. The cap bites only later, when the filtered array becomes payload.expressions and meets nsv-liveface-bind.js:292-300 ('expressions-invalid' over 64, 'expression-entry-invalid' on a group failing the pattern).\n\nSo a 71-group handoff is a VALID handoff that is NOT YET SEALABLE — two different statements, and collapsing them into one made the schema reject documents the estate produced and accepted. Validate against $defs.sealReady (nsv-validate --profile) to learn the limit early; the root stays true to the format.",
          "items": {
            "type": "object",
            "properties": {
              "group": {
                "type": "string",
                "description": "The expression group name. The handoff's consumers require only that it be a STRING — nsv-emit-live-vrm-row.mjs:77-82 filters on `typeof e.group === 'string'` and drops anything else. The stricter /^[\\w-]{1,40}$/ is the downstream §4.6 rule (nsv-liveface-bind.js:296) and lives in $defs.sealReady."
              },
              "moves": {
                "type": "boolean",
                "description": "Strictly `=== true` downstream (:297). Any other value reads as false — so `moves: \"yes\"` is silently falsy, which is why this schema types it as a boolean."
              }
            }
          }
        },
        "tune_measured": {
          "type": "object",
          "description": "ADDITIVE · OPTIONAL · **NEVER THE PLATE TUNE**. The single most dangerous field in the document.\n\nIt is in FACE-BOX space — fractions of a detected face rect inside the 1250-square photo. The plate's real `tune` is in PLATE-CANVAS space — fractions of the full 1024-square canvas. THE SAME FIVE NAMES IN DIFFERENT COORDINATE SYSTEMS. Convert one into the other and the face lands in the wrong place with no error anywhere.\n\nTHE ESTATE GUARDS THIS IN THREE PLACES, and the guard is load-bearing:\n  · tools/nsv-facerig-gate.mjs:103-106 — 'the FILE is the authority for tune — READY-FOR-FACELAB's tune_measured is face-box fractions, NOT plate-canvas fractions, its own NOTE says so'\n  · tools/nsv-emit-live-vrm-row.mjs:23-25 — faceRig comes from the FILE's extras.nsvFaceRig, 'G24-verified; never tune_measured'\n  · nsv-seal-livevrm.js:374 — REQUIRES a faceRig argument and refuses 'faceRig-required' without one, surfacing this block only as `tuneMeasuredFaceBox` (:400) for an operator to look at. It will not synthesise a plate tune from it.\n\nCarry it for human reference and lineage. Do not convert it and seal the result. A conversion would need the face-box's placement ON the plate, which is a property of the 3D sculpt's UV layout and is recorded nowhere in this document — the .md §8 states the conversion is undefined here and estate-wide, and that remains true.\n\nThe `space` const below is the enforcement this schema CAN offer: it makes the coordinate system explicit in the bytes, so a consumer that reads this block has no excuse for assuming plate space.",
          "properties": {
            "space": {
              "const": "face-box",
              "description": "Names the coordinate system. Any other value means someone has repurposed the block and it must not be read as face-box."
            },
            "cx": {
              "type": "number"
            },
            "eyeY": {
              "type": "number"
            },
            "spread": {
              "type": "number"
            },
            "mouthY": {
              "type": "number"
            },
            "scale": {
              "type": "number"
            }
          }
        }
      }
    },
    "photo": {
      "type": "object",
      "description": "ADDITIVE · OPTIONAL · the 3D plate path. THE ONLY CORRECT HOME FOR THE PHOTO.\n\nWHY IT LIVES HERE AND NOWHERE ELSE. The type-6 payload has no image field: nsv-liveface-bind.js nsvValidateLiveVrmPayload (:265-302) builds its accepted value from a whitelist of exactly three keys — model, faceRig, expressions — and drops everything else. The plate paints on a TRANSPARENT canvas over the sculpt's own skin, so a sealed photo would travel as dead weight, be dropped at the wear gate, and make the package a lie about its contents. nsv-seal-livevrm.js:264-275 therefore refuses a photo BY NAME ('photo-not-carried-by-type-6') across a 10-key list rather than dropping it silently.\n\nSo the photo is UPSTREAM evidence: the thing a tune was measured FROM, never a thing that travels IN a package. This block is that upstream seat.\n\nSTATUS — NOT PRODUCED BY ANYTHING TODAY. READY-FOR-FACELAB-1.md §4 and §8 are explicit: no .ready.json emitter exists in the facelab, no facelab run has ever produced this block, and its fields were derived from the lab's source rather than captured from a session. The worked example carries `image: null` for exactly that reason. Every constraint below is therefore DERIVED, not OBSERVED, and is marked as such in the report's NOT-VERIFIED section.",
      "properties": {
        "image": {
          "type": [
            "string",
            "null"
          ],
          "description": "The facelab's JPEG data URL verbatim, or null.\n\nnull MEANS 'NOT CARRIED HERE', NEVER 'NO PHOTO EXISTED'. A 1250-square q0.85 JPEG is roughly 150-400 KB of base64, so a handoff travelling beside its photo legitimately sets null. If there was no photo at all, OMIT THE WHOLE `photo` BLOCK — that is the distinction the two states carry, and collapsing them loses real information."
        },
        "px": {
          "type": "integer",
          "description": "Square edge of the source canvas. 1250 in the lab (its fitCv canvas and its UB_PX constant). Not enforced anywhere; not pinned to a const here because nothing in code refuses another size."
        },
        "quality": {
          "type": "number",
          "minimum": 0,
          "maximum": 1,
          "description": "JPEG quality the lab used. 0.85 in the lab (UB_Q). The 0..1 bound is the format's own, not a NetShow rule."
        },
        "mime": {
          "enum": [
            "image/jpeg",
            "image/png",
            "image/webp"
          ],
          "description": "The three the lab's own data-URL regex admits. Enumerated because that regex is a real gate on the lab side, unlike px/quality which are merely its current settings."
        },
        "rect": {
          "type": "object",
          "description": "The measured face rect, as normalized fractions of the square. Ranges below are the lab's enforced sanitizeFaceRect bounds, carried from READY-FOR-FACELAB-1.md §3. Note x and y admit NEGATIVE values — a face may be framed partly outside the square — which is why the minimums are not 0.",
          "properties": {
            "x": {
              "type": "number",
              "minimum": -0.2,
              "maximum": 1
            },
            "y": {
              "type": "number",
              "minimum": -0.25,
              "maximum": 0.75
            },
            "w": {
              "type": "number",
              "minimum": 0.15,
              "maximum": 0.9
            },
            "h": {
              "type": "number",
              "minimum": 0.2,
              "maximum": 0.9
            }
          }
        },
        "rectSpace": {
          "const": "fraction-of-square",
          "description": "Names the coordinate system, exactly as `face.tune_measured.space` does. Same discipline, same reason."
        },
        "rectSource": {
          "enum": [
            "house-default",
            "measured",
            "user-framed"
          ],
          "description": "WHY THIS FIELD EXISTS, and it is not bookkeeping. The lab is byte-stable on purpose: it writes `rect` ONLY when the framing differs from the house default {x:0.2655, y:0.140, w:0.485, h:0.576}. So a house-framed upload writes NO rect at all, and a consumer seeing no rect must read the house default as the value rather than treating it as missing data. Emitting rectSource makes that inference explicit instead of folklore."
        },
        "skin": {
          "type": "string",
          "pattern": "^#[0-9a-fA-F]{6}$",
          "description": "Skin hex the lab captured. Distinct from the 3D rig's own `skin` in extras.nsvFaceRig — this one is measured from the photo, that one is the sculpt's."
        },
        "consent": {
          "type": "object",
          "description": "SPECIFIED BUT NOT COLLECTED. READY-FOR-FACELAB-1.md §4.4 is explicit that the lab does not capture consent or licence for uploaded photos today. It is specified here because a seal door should carry it, and naming the gap is better than leaving a silent hole where a face photo's rights should be.",
          "properties": {
            "owner": {
              "type": [
                "string",
                "null"
              ],
              "maxLength": 120
            },
            "rights": {
              "type": "string",
              "maxLength": 60
            }
          }
        }
      },
      "allOf": [
        {
          "$comment": "AGREEMENT 1 — `mime` must match the data-URI prefix actually inside `image`. The mime vocabulary is a CLOSED three-member set fixed by the lab's own alternation IMG_DATA_RE (/^data:image\\/(jpeg|png|webp);base64,…/ at public/alive-site/face-overlay-lab.html:2905), so the agreement is three if/then clauses keyed on the const values. A document declaring image/png beside a JPEG payload lies about itself, and nothing downstream would notice.\n\nEach clause fires only when `image` is a STRING, so `image: null` ('not carried here') stays legal under every mime — which is the real corpus case: one of the two photo-bearing documents on this box declares image/jpeg with image null.\n\nWHAT REMAINS INEXPRESSIBLE: that the base64 actually DECODES to an image of the declared type. A schema can check the label against the prefix; it cannot decode.",
          "allOf": [
            {
              "if": {
                "required": [
                  "mime",
                  "image"
                ],
                "properties": {
                  "mime": {
                    "const": "image/jpeg"
                  },
                  "image": {
                    "type": "string"
                  }
                }
              },
              "then": {
                "properties": {
                  "image": {
                    "pattern": "^data:image/jpeg;base64,"
                  }
                }
              }
            },
            {
              "if": {
                "required": [
                  "mime",
                  "image"
                ],
                "properties": {
                  "mime": {
                    "const": "image/png"
                  },
                  "image": {
                    "type": "string"
                  }
                }
              },
              "then": {
                "properties": {
                  "image": {
                    "pattern": "^data:image/png;base64,"
                  }
                }
              }
            },
            {
              "if": {
                "required": [
                  "mime",
                  "image"
                ],
                "properties": {
                  "mime": {
                    "const": "image/webp"
                  },
                  "image": {
                    "type": "string"
                  }
                }
              },
              "then": {
                "properties": {
                  "image": {
                    "pattern": "^data:image/webp;base64,"
                  }
                }
              }
            }
          ]
        },
        {
          "$comment": "AGREEMENT 2 — `rectSource: \"house-default\"` must mean the rect really IS the house default. `const` accepts OBJECT values in draft 2020-12, so this is directly expressible against USER_RECT {x:0.2655, y:0.140, w:0.485, h:0.576} (public/alive-site/face-overlay-lab.html:2845).\n\nCRUCIALLY IT PERMITS `rect` TO BE ABSENT, which is the lab's byte-stable behaviour: it writes `rect` ONLY when the framing differs from the house default (:4428-4430). So absent means house-default, and what is forbidden is declaring house-default beside a DIFFERENT rect. The corpus confirms both directions: one document says house-default and carries exactly USER_RECT; the other says user-framed and carries something else.\n\nWHAT REMAINS INEXPRESSIBLE: the INFERENCE. A validator cannot fill in USER_RECT when `rect` is absent — JSON Schema `default` is annotation only, never applied. A consumer must still make that substitution itself, which is exactly why emitting `rectSource` matters.",
          "if": {
            "required": [
              "rectSource"
            ],
            "properties": {
              "rectSource": {
                "const": "house-default"
              }
            }
          },
          "then": {
            "properties": {
              "rect": {
                "const": {
                  "x": 0.2655,
                  "y": 0.14,
                  "w": 0.485,
                  "h": 0.576
                }
              }
            }
          }
        }
      ],
      "$comment#agreements": "TWO CROSS-FIELD AGREEMENTS, EXPRESSED RATHER THAN DESCRIBED. Both were first written off as 'prose only' and both are in fact expressible in draft 2020-12; an adversarial review of this contract caught the error and they are now enforced. Neither is checked by any shipped code — they are contract-level guarantees this file adds, which is a legitimate thing for a published contract to do PROVIDED it is declared. It is declared here.\n\nCORROBORATION IS THIN AND SAID SO: only 2 of the 39 ready-for-facelab documents on this box carry a photo{} block at all (no facelab emitter exists — see §4 of the spec). Both satisfy both rules. Two instances is not a population, and if a third instance ever contradicts a rule below, the rule is what gives way, not the document."
    },
    "integration": {
      "type": "object",
      "description": "The integration triad. NULL BY DESIGN — the nulls are the FaceLab stage's to fill and are NOT missing data. nsv-emit-live-vrm-row.mjs:102-106 passes each through with `?? null`, and :83 substitutes {} for a non-object, so nothing here can refuse a document.\n\nSame triad as nsv-agent-1's `integration` block, and the same unresolved question rides with it: the estate has never stated whether `model` names the thinking model or the VRM body. Carried opaquely; a reader MUST NOT infer one.",
      "properties": {
        "voice": {
          "type": [
            "string",
            "null"
          ]
        },
        "model": {
          "type": [
            "string",
            "null"
          ]
        },
        "persona": {
          "type": [
            "string",
            "null"
          ]
        }
      }
    },
    "provenance": {
      "type": "object",
      "description": "Free-form lineage. Nothing branches on it today.\n\nDO NOT CONFUSE IT WITH ITS NAMESAKE. nsv-agent-1 has a `provenance.readyForFacelab` block that REFERENCES this document (schema/sha256/bytes/title/license, deliberately not `vrm`). That is a different document pointing at this one — not a field of this one, and not a place this block travels to.",
      "properties": {
        "runs": {
          "type": "integer",
          "minimum": 0
        },
        "feedback_history": {
          "type": "array"
        },
        "evidence": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    }
  },
  "$comment#top": "NO additionalProperties:false AT THE ROOT, and that is deliberate. Neither consumer walks the key set — both read named fields and ignore the rest — so closing the document would make this schema stricter than every implementation that reads it, in a format whose whole purpose is to carry additive blocks between lanes that ship at different times. The .md's own §2 marks `photo` and `face.tune_measured` as ADDITIVE and proves backward compatibility by running the older consumer over a document carrying them. A closed root would have made that impossible. Sub-objects are likewise left open for the same reason.",
  "$defs": {
    "handoffComplete": {
      "$comment": "OPT-IN PRODUCER PROFILE. Not referenced by the root. Validate against this to check YOUR EMITTER.",
      "description": "What READY-FOR-FACELAB-1.md §2 marks REQUIRED, which is more than the consumers enforce. A producer that emits less is shipping a handoff that degrades silently downstream — the title falls back to a basename, the licence falls back to 'NetShow', the sha becomes null — and every one of those degradations is a fact quietly invented on the reader's side.\n\nBoth corpus documents satisfy this profile.",
      "type": "object",
      "required": [
        "schema",
        "acceptedAt",
        "agent"
      ],
      "properties": {
        "acceptedAt": {
          "type": "string",
          "format": "date-time"
        },
        "agent": {
          "type": "object",
          "required": [
            "vrm",
            "sha256",
            "bytes",
            "title",
            "license"
          ]
        }
      }
    },
    "sealReady": {
      "$comment": "OPT-IN. The stricter question: is this handoff sufficient to reach a SEALABLE §4.6 payload?",
      "description": "A handoff can be complete and still not be sealable. nsv-seal-livevrm.js:370-402 needs, beyond a valid document: a `faceRig` SUPPLIED BY THE CALLER from the file's G24-verified extras (:374 — never from this document, see face.tune_measured), and a url or id from opts (:379 — never agent.vrm). Neither is a field of this format, which is why this profile checks what this document must contribute and states the rest as prose:\n  · agent.sha256 present and hex64 — or model.sha256 is simply omitted downstream (:382), losing the pin\n  · agent.bytes present, a positive integer — refused downstream at nsv-liveface-bind.js:281-284 if malformed\n  · agent.license within 60 chars — refused downstream at :285-288\n  · face.expression_inventory within 64 entries, each group matching /^[\\w-]{1,40}$/ — refused downstream at :292-300\nThe two things this document CANNOT supply are exactly the two the seal door demands from elsewhere. That asymmetry is the design, not a gap.",
      "type": "object",
      "required": [
        "schema",
        "agent"
      ],
      "properties": {
        "agent": {
          "type": "object",
          "required": [
            "sha256",
            "bytes"
          ],
          "properties": {
            "sha256": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            },
            "bytes": {
              "type": "integer",
              "exclusiveMinimum": 0
            },
            "license": {
              "type": "string",
              "maxLength": 60
            }
          }
        },
        "face": {
          "type": "object",
          "properties": {
            "expression_inventory": {
              "type": "array",
              "maxItems": 64,
              "items": {
                "type": "object",
                "properties": {
                  "group": {
                    "type": "string",
                    "pattern": "^[\\w-]{1,40}$"
                  }
                }
              },
              "description": "THE DOWNSTREAM §4.6 BOUNDS, which live here and NOT at the root — see the root field's own note for why. nsv-liveface-bind.js:292-300 refuses 'expressions-invalid' over 64 entries and 'expression-entry-invalid' on any group failing /^[\\w-]{1,40}$/.\n\nAs of 2026-08-05, 13 of the 39 ready-for-facelab documents on this box carry 71 groups and therefore fail THIS profile while being perfectly valid handoffs. That is the profile working: they cannot be sealed as-is, and a producer should learn it here rather than at the seal door. Whoever wires the seal path must decide whether to trim to the 64 that move or raise the gate's cap — that decision is not this contract's to make, and it is recorded in the report as an open item."
            }
          }
        }
      }
    },
    "specCitationDeltas": {
      "$comment": "DOCUMENTATION ONLY. Recorded because this contract claims to derive from implementations, so where a prose source disagrees with the file, the disagreement must be visible rather than quietly resolved.",
      "description": "READY-FOR-FACELAB-1.md is substantively accurate; several of its line citations have drifted from tools/nsv-emit-live-vrm-row.mjs as it was edited. Re-read 2026-08-05:\n  · schema throw — .md says L58-60 · FILE: 58-60. AGREES.\n  · bytes staleness refusal — .md says L84-87 · FILE: 73-74.\n  · row.name from agent.title — .md says L92 · FILE: 86.\n  · row.sha256 — .md says L96 · FILE: 91.\n  · row.license — .md says L98 · FILE: 93.\n  · expressions filter/map — .md says L89-93 · FILE: 77-82.\n  · integration passthrough — .md says L100-105 · FILE: 102-106.\n  · faceRig-never-tune_measured comment — .md says L18-21 · FILE: 23-25 (the faceRig/G24 lines of the header block). DRIFTED — and this schema inherited the .md's 18-21 into face.tune_measured, where it survived until intake s45-intake-ad-20260805 caught it (D4); corrected to :23-25 by fable5-jsoncontract-fix1-20260805.\n  · facerig-gate tune_measured note — .md says L106-108 · FILE: 103-106.\nEvery citation in THIS schema was re-derived and re-checked; one inheritance still slipped through (the L18-21 above). The machine audit (proof/check-citations.mjs) verifies its hand-built 155-row table, 155/155 — table-scoped coverage, not every published cite.",
      "type": "null"
    },
    "consumerRefusalIndex": {
      "$comment": "DOCUMENTATION ONLY. What each consumer can refuse, and which refusals a schema can pre-empt.",
      "description": "tools/nsv-emit-live-vrm-row.mjs — THROWS (exit 1 at the CLI):\n  :57  'READY-FOR-FACELAB content required'                       | schema: type:object\n  :58-60 'unexpected ready schema \"…\"'                            | schema: properties.schema const\n  :68  'G24.faceRig <verdict> on <file> …'                        | NOT SCHEMA-CHECKABLE — a property of the .vrm, gated before the handoff is trusted\n  :71  'gate passed but extras unreadable — investigate'          | NOT SCHEMA-CHECKABLE\n  :73-74 'agent.bytes N != actual M — stale handoff, refuse'      | NOT SCHEMA-CHECKABLE — needs the file on disk\n\nnsv-seal-livevrm.js nsvSealInputFromReady() — RETURNS {ok:false, reason}:\n  :372 'ready-not-object'                                          | schema: type:object\n  :373 'ready-schema-mismatch — want …, got …'                     | schema: properties.schema const\n  :374 'faceRig-required — supply the FILE's extras.nsvFaceRig …'  | NOT A FIELD OF THIS FORMAT (caller-supplied, by design)\n  :379 'model-url-or-id-required — the handoff carries a LOCAL path in agent.vrm, never a served url' | NOT A FIELD OF THIS FORMAT (caller-supplied)\n\nDOWNSTREAM, once this document has become a §4.6 payload — nsv-liveface-bind.js nsvValidateLiveVrmPayload:\n  :266 payload-not-object · :268 model-missing · :275 model-url-or-id-invalid\n  :278 model-sha256-invalid   <- agent.sha256\n  :282 model-bytes-invalid    <- agent.bytes\n  :286 model-license-invalid  <- agent.license (>60 chars)\n  :290 faceRig-invalid        <- the caller's faceRig, not this document's\n  :293 expressions-invalid    <- face.expression_inventory over 64\n  :296 expression-entry-invalid <- a group failing /^[\\w-]{1,40}$/\nThe four marked '<-' are the ones a producer can prevent by respecting this schema, which is why their bounds are asserted above rather than merely described.",
      "type": "null"
    }
  }
}
