Compound Labs
Get the newsletter
THE STANDUPAnthropic fixes Claude 5.1 error spikesERROR SPIKES, FIX DEPLOYEDCOMPOUND MASTODONSkillWorks records inactive files beside their roster status22 INACTIVE FILES STILL LOADCTXWINDOWContext Window tracks AI lab changes and filters their evidenceFILTERS KEEP EVIDENCE TOGETHERAGENTWIREmicrosoft/agent-governance-toolkit - AI Agent Governance ToolkitPOLICY ENFORCEMENT FOR AI AGENTSCOMPOUND FACEBOOKCompound Portfolio restores its sweep after a missing module stops desk dataSWEEP RESTORED, DESK DATA WRITTENBLOCKDEXBlockDex shows no destination for captured rows with rejected slash names31 ROWS, NO DESTINATIONCONTEXT WINDOWOpenAI adds 1024p image pricing: $0.50 and $0.251024P IMAGE, $0.50 AND $0.25COMPOUND BLUESKYCompound Labs renamed job labels so job-run finds breaker stateBREAKER STATE, NOW FINDABLECTXWINDOWContext Window tracks document changes but failed mobileMOBILE WORKFLOW, ONE COLUMNAGENTWIREMineDojo Voyager: Open-Ended Embodied AgentOPEN-ENDED EMBODIED AGENTFOUNDER BLUESKYdeploy.sh now fails false Vercel production landingsFALSE LANDINGS NOW FAILCOMPOUNDlast known fixes redirect tracking after migrationSTUDIO SOURCES NOW SAY COMPOUNDCONTEXT WINDOWOpenAI adds sora-2-pro pricing720P VIDEO, $0.15, $0.30FOUNDER LINKEDINdeploy.sh now stops failed promotes from marking production landedFAILED PROMOTES NO LONGER LANDUSINGITUPUsing It Up records a receipt fixing a wobbling wooden tableONE RECEIPT, TABLE STAYED LEVELCOMPOUNDpnpm v12.4.2POSIX BIN SHIMS REPLACEDFOUNDER PEERLISTCompound Labs' deploy runner rejects failed promotesFAILED PROMOTES NO LONGER LANDTRUSTDESKTrustDesk fixes host migration redirects while keeping /api/ outOLD HOST, 308; API STAYS LIVE
Independent product R&D labFounded and run by Isaiah Kim, @kyisaiah47Newest commit Sep 16, 2026, AgentwireNewest writing Sep 16, 2026Site changelog Sep 16, 2026
Aug 15, 20264 min read

Fifteen optional keys broke my structured-output schema, and an array of pairs fixed more than the compile

Schema is too complex. That was the answer to the JSON schema I thought was the obvious one, and getting past it changed the data model in a way I would now choose on purpose.

The grid that would not compile

An ACORD 25 certificate of liability insurance is a grid of coverage rows, and each row carries up to fifteen named limit boxes: each_occurrence, general_aggregate, products_completed_ops_aggregate, el_disease_policy_limit, and eleven more. A real certificate fills in three or four of them. The rest are blank.

The compliance engine downstream reads limits as Partial<Record<LimitKey, number>>, so writing the extraction schema to match that map looked free. Two ways to express it, two failures, both recorded in the comment that now sits above the fragment: fifteen nullable values blew the 16-union parameter cap, and fifteen optional properties hit "Schema is too complex".

What shipped is one enum and one number:

// packages/coi-extract/src/schema.ts
const limitsItems = {
  type: "object",
  additionalProperties: false,
  required: ["key", "amount"],
  properties: {
    key: { type: "string", enum: LIMIT_KEYS },
    amount: { type: "number", description: "Whole US dollars." },
  },
};

// ...inside each policy row: limits: { type: "array", description: "One entry per limit box that has a figure. Omit blank boxes entirely.", items: limitsItems, }, ```

Insurance certificate verification interface with required liability coverages and amounts, Coverage types, dollar amounts, and certificate status at a glance.
Insurance certificate verification interface with required liability coverages and amounts, Coverage types, dollar amounts, and certificate status at a glance.

The pairs are the better model regardless

A map of fifteen nullable keys asks the reader to make a statement about every box on the form, including the empty ones. Pairs let absence be expressed by absence: a blank limit box is a pair the model never emits, and there is nothing to say about it.

That matters past the schema, because absent and zero are different findings. One of the normalization tests asserts "general_aggregate" in limits is false rather than checking for 0, since the engine reports a missing limit as "not stated" and a low one as short of the requirement. Those two produce different emails to the vendor's agent.

normalizeRecord folds the pairs back into the map the engine wants, and accepts the plain map too, so fixtures and hand written records exercise the same path without carrying the schema's compromise.

One smaller quirk in the same file: form is "occurrence" | "claims_made" | null, and an enum on a union type gets rejected with Enum value 'occurrence' does not match declared type. Two anyOf branches, one enum and one null, compile fine.

The schema constrains shape, and stops there

additionalProperties: false on every object plus required on every property gets you a parse you can type. It does not get you numeric bounds, string length, or date formats. Every value constraint that matters lives after the call returns.

RuleWhat COI_SCHEMA declaresWhat normalizeRecord checks
Object keysrequired on all properties, additionalProperties: falsenot re-checked
Date is a real calendar datetype: "string", the format lives in the descriptionregex plus a Date.parse round trip, so 2027-02-31 is rejected
Expiry falls after effective datenothingexpiresOn < effectiveOn drops the policy and flags the path
Limit is a plausible figuretype: "number", no min or max available<= 0 or > 1_000_000_000 becomes a review item
ADDL INSD tri-state["boolean", "null"]anything that is not a boolean is coerced to null, never false
form is one of two strings or nullanyOf branchesre-checked against the two literals

The billion dollar ceiling is not a guess about insurance. Its comment says these are almost always a policy number or a date read into a limit box, and the test that covers it feeds 8_892_310_000_000 against a fixture whose policy number is GL-889231.

Demote, never drop

The part I would port to any extraction pipeline: nothing that fails a value check is silently discarded. Each one appends a dotted path to uncertainFields, the compliance engine turns anything left there into an EXTRACTION_UNCERTAIN finding with severity review, and any review finding pushes the whole vendor to needs_review instead of compliant.

The same idea covers an unreadable issue date. Dropping it would fail the staleness check open, and passing through the garbage would fire a wrong warning, so issuedOn falls back to today's date, which makes that check a no op, and the review item carries the real problem to a person.

That's how we built CoverCheck.

---

One shipped product, taken apart, once a month. What it does, what it cost to build, what the pipeline behind it looks like, and what the numbers did, read off the repository and the live site, not written from memory. Join the list.

All writing