# Calchis Determination Standard, version 1

`calchis-determination/1`

**Status: proposed.** One implementation exists (this repository). It has not
been reviewed by any standards body, adopted by any counterparty, or referenced
in any offering document. Published for review and criticism. Nothing below
should be read as a claim of adoption.

**Version 2** (`docs/determination-standard-v2.md`, 2026-10-17) adds
cat-in-a-box and cat-in-a-polygon terms with tiered payouts, geometry inside
the hashed specification, and a named source revision. It stands beside this
version: nothing below is reinterpreted, and every v1 record verifies as it did.

---

## 1. What problem this addresses

Parametric insurance and catastrophe bonds settle on whether a public
observation crossed a stated number at a stated place and time. In practice
that question is adjudicated from bespoke prose in an offering circular, by a
paid calculation agent, over weeks. Disputes about whether a trigger fired are
expensive, and "did it fire?" remains the most common complaint about
parametric cover.

The underlying observations are public. USGS publishes ShakeMaps; the National
Hurricane Center publishes advisories; NOAA publishes gauge readings. What is
missing is not the data but a **citable, independently checkable record** that a
sponsor and an investor can both point at without either trusting the other.

This document specifies such a record.

## 2. What this is not

A determination record is **not a settlement determination**. It does not
establish that recovery is owed; that is a matter for an indenture and whoever
it appoints. A determination record asserts one narrower thing:

> At this instant, this named public source reported this value, obtained this
> way, and compared against this threshold it stood in this relation — and here
> is everything needed to check that claim.

Every record restates this limitation in its own text, so the qualification
cannot be separated from the number by someone quoting it.

## 2a. How much of the market this can address

Measured against the 244 catastrophe bonds held in this platform's own
`cat_bonds` table, 2026-08-27:

| trigger type | bonds | principal | machine-checkable from public feeds? |
|---|---|---|---|
| indemnity | 161 | $48.4bn | **No.** Settles on the sponsor's own incurred losses, which no third party can observe. |
| industry-loss index | 76 | $15.2bn | Partly. Industry loss is observable, but via a commercial reporting agency (PCS, PERILS), not a free federal feed. |
| parametric | 7 | $1.7bn | Yes. Directly expressible. |

**So the addressable slice is 83 of 244 bonds — 34% by count, 26% by
principal — and the cleanest fit, pure parametric, is only 7 bonds and $1.7bn.**

This is stated up front because the opposite framing is available and would be
false. A standard for verifiable parametric determinations does not address the
majority of the catastrophe bond market, and cannot: indemnity triggers are
structurally outside it, since the settling quantity is private by
construction. Anyone evaluating this should discount accordingly.

Two further caveats from the same data:

- Not one of the 244 bonds has structured trigger terms recorded
  (`trigger_structure_json` is empty on every row). Trigger terms today live in
  prose in an offering circular. That is the gap this standard addresses, and
  also the measure of how far from adoption it is.
- Industry-loss triggers depend on a paid reporting agency's determination. A
  standard can express and check the *comparison*, but it cannot make the
  underlying index independently observable.

## 3. Design constraints

1. **Self-contained.** A reader holding only the record can check it. No
   database access, no API call, no vendor account.
2. **Content-addressed.** The identifier is derived from the content, so it
   cannot be minted for a record that does not exist, and any alteration
   produces a different identifier than the one already published.
3. **Verifiable by an adversary.** The party who benefits from a different
   answer must be able to run the check themselves and get the same result.
4. **Absence recorded explicitly.** A missing observation timestamp is a
   material fact about a reading, not a blank to be filled in.
5. **Verification is not generation.** Checking a record you hold must not let
   you produce records for readings you were never given.

Constraint 5 is what allows the standard to be open while the underlying
hazard data stays metered. It is the same separation OED relies on: OED is a
published *format*, not a published exposure database.

## 4. Canonical form

The identifier is a hash, so two implementations must agree byte-for-byte on
what is hashed. This is the subset of
[RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) (JSON Canonicalization
Scheme) that applies:

| Rule | Requirement |
|---|---|
| Object keys | Sorted ascending by UTF-16 code unit. Not locale-aware collation. |
| Whitespace | None between tokens. |
| Numbers | ECMAScript `Number::toString`. Negative zero serializes as `0`. |
| Strings | UTF-8 literal. Escape only `"` `\` and control characters `U+0000`–`U+001F`. |
| Arrays | Order preserved and significant. |
| `null` | Preserved. Distinct from an absent key. |

### 4.1 What must be refused

An implementation **MUST** reject, rather than coerce:

| Value | Why |
|---|---|
| `NaN`, `Infinity` | `JSON.stringify` writes `null`, so two different records would hash alike. |
| `undefined` inside an array | Written as `null`, changing meaning by position. |
| `BigInt`, `Symbol`, function | No defined canonical form. |
| A date object | Ambiguous precision. Serialize to an explicit ISO 8601 UTC string at the call site. |

An `undefined` **object property** is omitted, not rejected — that is how an
optional field is spelled — but an explicitly `null` property is distinct from
an absent one and the two must not hash alike.

Refusing matters more than it appears to. A hash computed over a silently
coerced value attests to something other than what the issuer meant, which is
worse than an error.

### 4.2 Test vectors

```
canonicalize({b:2, a:"x", c:[1,null,true]})  ->  {"a":"x","b":2,"c":[1,null,true]}
canonicalize({Z:1, a:2, A:3})                ->  {"A":3,"Z":1,"a":2}
canonicalize(-0)                             ->  0
canonicalize({a:1, b:undefined})             ->  {"a":1}
canonicalize({a:1, b:null})                  ->  {"a":1,"b":null}
canonicalize([3,1,2])                        ->  [3,1,2]
```

## 5. The identifier

```
determination_id = "cdt1-" || lowercase_hex( SHA-256( UTF-8( canonicalize( evidence ) ) ) )
```

The identifier is a pure function of the evidence **alone**. Nothing about who
computed it, when they computed it, or where it was stored participates —
otherwise two parties recomputing one reading would disagree, defeating the
purpose. The time of capture is already inside the evidence, so a genuinely
later reading still receives a distinct identifier.

## 6. Record structure

```jsonc
{
  "determination_id": "cdt1-<64 hex>",
  "attestation": {
    "standard": "calchis-determination/1",
    "canonical_form_version": 1,
    "hash_algorithm": "sha-256",
    "content_hash": "<64 hex>"        // SHA-256 of canonicalize(evidence)
  },
  "evidence": { /* section 7 */ }
}
```

## 7. Evidence

The evidence object is self-contained by design, which means it is
deliberately denormalized. A normalized record that joined back to mutable
tables would defeat the entire purpose — this was verified in practice: a gauge
enrichment behind a 2026-07-18 reading was overwritten on 2026-07-19, so the
platform could show what it had said but not prove why it had said it.

| Key | Contents |
|---|---|
| `schema_version` | Integer. Bumped only when a consumer must branch. |
| `captured_at` | ISO 8601 UTC. When the issuer captured this evidence. |
| `evaluation` | `threshold_type`, `current_value`, `threshold_value`, `pct_of_threshold`, `status`, `unit`, and a plain-language `statement`. |
| `event` | Upstream event identity, including `source`, `source_url`, `source_id`, and status **at capture time**. |
| `trigger` | Trigger configuration at capture time, including threshold and alert point. |
| `inputs` | Ordered list of named upstream values that produced `current_value`. |
| `derivation` | `method`, an explicit `formula` where the issuer computed rather than read, and `computed_by_calchis`. |
| `raw_evidence` | Verbatim upstream excerpt and/or structured payload subset, with a `truncated` flag. |
| `timing` | Separates the issuer's clock from the source's clock; carries `observation_time_available` explicitly. |
| `geometry` | How the geographic match was decided, and against which polygon. |
| `provenance`, `disclaimer`, `caveats` | Frozen at capture time. |

Two rules govern the content:

- **Never fabricate precision.** If upstream gave a phrase like
  `"At 8:16 AM CDT Sunday"` with no date, store the verbatim phrase. Do not
  resolve it to a false instant.
- **Record what is missing as explicitly as what is present.**

## 7a. The trigger specification

A determination says what was **observed**. A specification says what would
**count**. They are different documents with different lifetimes: a
specification is agreed once, before anything happens, and is what an offering
circular or policy schedule would reference. Determinations are then issued
against it, repeatedly, as a season runs.

```
spec_id = "cts1-" || lowercase_hex( SHA-256( UTF-8( canonicalize( specification ) ) ) )
```

```jsonc
{
  "standard": "calchis-trigger-spec/1",
  "peril": "hurricane",
  "geography": { "type": "circle", "center_lat": 29.3, "center_lng": -94.79, "radius_km": 50 },
  "metric": {
    "name": "wind_speed",
    "unit": "knots",
    "authority": "NOAA National Hurricane Center public advisory"
  },
  "condition": { "operator": "gte", "value": 64 }
}
```

`operator` is `gte` (fires at or above) for every metric except
`central_pressure`, which is `lte`: a lower central pressure is a stronger
storm, so it fires at or below its value (from 2026-09-30). Status coherence
below reads "met" in that direction.

The metric is named by quantity, and the unit is stated beside it in the
authority's own terms: the threshold reads 64 knots because that is what the
Hurricane Center publishes, whatever unit a monitoring system stores
internally. Records written before 2026-09-05 carry the unit in the metric's
name (`wind_speed_kt`, `storm_surge_ft`, `gauge_stage_ft`). A record is
immutable, and the name is one of the terms, so those specifications keep
their identifiers: a verifier recomputes the id from the name **as recorded**
and only looks up the unit and authority through it. The same terms under the
newer spelling are, by construction, a different specification.

### What the specification deliberately excludes

Identity must depend on the **terms and nothing else**. An implementation
derives a specification by dropping everything operational — the owner, the
surrogate row id, the holder's private name for it, active/inactive state,
free-text notes, and any notification preference.

The last one is the trap worth naming: if an alert threshold participated in
the identifier, changing when a monitoring system sends an email would silently
change the identity of a contract, and two parties who agreed the same terms
but configured different alerting would compute different ids for one
agreement.

### Naming the authority is the point

`wind_speed >= 64 knots` is ambiguous until it says *whose* wind speed. A
forecast, a real-time analysis and a post-storm best track disagree, and that
disagreement is precisely what parties litigate. Every specification names the
authority whose published value settles the metric.

### `unconstrained` is written out

"This fires at any location" is a substantive term. It is spelled as an
explicit geography type rather than an absent field, so it can never be
confused with someone having failed to fill one in.

### Not a legal instrument

This expresses a comparison precisely enough to execute and to check. It does
not express warranties, exclusions, reporting deadlines, or who pays whom. An
indenture does that. This is meant to be referenced **by** one, not to replace
it.

### Linkage

From schema version 2, evidence carries a `specification` block naming the
`spec_id` the reading was evaluated against, so a determination is a complete
statement: *this observation, against these agreed terms.* A verifier
recomputes the `spec_id` from the evidence where the evidence carries enough
to do so — a circle or unconstrained trigger is fully described by the
snapshot. A polygon trigger is not, because the snapshot records only that a
polygon exists and not its geometry; verifiers report `spec_id_unverifiable`
in that case rather than silently skipping the check.

## 7b. Which observation settles it

A hurricane produces many advisories. A gauge produces a reading every few
minutes. **"Wind at or above 64 kt" does not say whether that means any single
reading, the maximum over the event, or the value at landfall** — and real
parametric contracts turn on exactly that distinction. Leaving it implicit is
how two parties read the same terms differently and end up in dispute.

So `condition.observation_rule` is required, and v1 defines exactly one value:

| Rule | Meaning | v1 |
|---|---|---|
| `any_single_observation` | Any one published observation at or above the threshold meets it. | **Defined** |
| maximum over event | The event's peak value settles it. | Not defined |
| at landfall | The value at a determined landfall settles it. | Not defined |
| sustained over window | The value must hold for a stated duration. | Not defined |

**The three undefined rules are common in real contracts.** They are omitted
deliberately rather than stubbed: admitting a rule the evaluator does not
implement would let a party specify it and receive determinations silently
evaluated under a *different* rule. A format that admits a limitation is safer
than one that appears to support something it does not.

This is a real constraint on what v1 can express, and it narrows the addressable
set further than section 2a alone suggests. Extending it is not merely a schema
change — each additional rule needs evaluation logic that can be checked, since
a rule nobody can verify is not part of a verifiable standard.

## 8. Verification

A verifier takes a record and returns `verified` plus a list of findings. It
**MUST** report every finding rather than stopping at the first: a party
adjudicating a disputed reading needs the whole picture, and the first problem
is rarely the most important one.

`verified` is true only when no finding has severity `fatal` or `inconsistent`.

| Severity | Meaning |
|---|---|
| `fatal` | The record is not what its identifier says it is. |
| `inconsistent` | The record is intact but contradicts itself. |
| `advisory` | Worth attention; not a defect. |

### 8.1 Required checks

**Integrity**

| Code | Condition |
|---|---|
| `id_mismatch` | `determination_id` ≠ recomputed identifier. |
| `content_hash_mismatch` | `attestation.content_hash` ≠ hash of evidence. |
| `unknown_hash_algorithm` | Algorithm is not `sha-256`. |
| `unknown_standard` | Advisory. Record declares a standard the verifier does not implement. |

**Internal coherence**

| Code | Condition |
|---|---|
| `pct_arithmetic` | `pct_of_threshold` does not follow from the two values (tolerance 0.01). The percentage is the plain ratio `current / threshold × 100` for linear quantities; for `magnitude` it is the reading's seismic moment as a share of the threshold's, `100 × 10^(1.5 × (M − M_threshold))` (from 2026-09-30); for `central_pressure` it is the pressure deficit below 1013.25 hPa as a share of the threshold's. |
| `status_breached_below_threshold` | Status `breached` while the value is below the threshold. |
| `status_monitoring_at_threshold` | Status `monitoring` while the threshold is met. |
| `status_approaching_below_alert` | Status `approaching` below the configured alert point. |
| `statement_contradicts_values` | The prose asserts a relation the numbers deny. |
| `observation_after_evaluation` | Source observed the value after the issuer evaluated it. |
| `observation_flag_mismatch` | `observation_time_available` disagrees with the timing fields. |
| `no_observation_time_undeclared` | No observation time **and** no caveat saying so. |
| `no_inputs` | No named inputs, so the value traces to nothing. |
| `computed_without_formula` | Marked computed, but no formula to re-run. |
| `no_specification` | Schema version ≥ 2 with no specification block. |
| `spec_id_mismatch` | Cited `spec_id` does not match the terms recorded in the evidence. |
| `malformed_spec_id` | Specification id is not a `cts1-` identifier. |
| `spec_id_unverifiable` | Advisory. Polygon geometry absent, so the id cannot be recomputed. |
| `no_disclaimer` | The record carries no disclaimer distinguishing monitoring from settlement. |

The `statement_contradicts_values` and `no_observation_time_undeclared` checks
exist because the failure they catch is one of presentation, not arithmetic:
a record whose prose or silence misleads a reader is defective even when every
number in it is correct.

### 8.2 Reference implementations

- Offline: `lib/determination/verify.ts` in this repository.
- Hosted: `POST https://calchis.com/api/v1/determinations/verify`, unauthenticated.

The hosted endpoint reads nothing from any database and returns nothing the
caller did not submit. A party who does not wish to trust it should not — the
offline path exists precisely so that trusting the issuer is optional. **An
arbiter you must ask permission to audit is not an arbiter.**

## 8b. The transparency log

Content-addressing stops a record being altered without changing its
identifier. It does **not** stop backdating: `captured_at` is asserted by the
issuer, and an issuer who wanted to could compute a determination after an
outcome was known and stamp an earlier time on it. For a settlement dispute,
*"we said this before the answer was known"* is the entire question.

So every closed UTC day's determination ids are hashed into a Merkle root and
the root is published at `GET /api/v1/determinations/log`. Inclusion proofs
are served at `GET /api/v1/determinations/{id}/proof`. Both are
unauthenticated: a proof is the caller's own identifier plus opaque sibling
hashes, carrying no evidence, and a log readable only by the party it
constrains is not a transparency log.

### Construction

| | |
|---|---|
| Leaf | `SHA-256( 0x00 \|\| utf8(determination_id) )` |
| Node | `SHA-256( 0x01 \|\| left \|\| right )` |
| Odd node | Promoted unchanged. **Not** duplicated. |
| Entry order | Ascending by identifier, de-duplicated. |
| Empty period | Has a defined root, so "nothing was issued" is checkable. |
| Entry | A determination id (`cdt-`, `cdt2-`), or — since 2026-10-31 — a frozen revision-record extract id (`cre1-` + the SHA-256 of the extract's CSV; `docs/revision-record.md` § Citing a month). The leaf rule is the same; the prefix says which. |

The distinct leaf and node prefixes are RFC 6962's, and they close a real
attack: without domain separation an internal node can be presented as a leaf
to produce a valid-looking proof for something never logged.

Odd nodes are promoted rather than duplicated because the duplicate-the-last-leaf
convention admits two distinct entry lists with the same root — a genuine
ambiguity in a log meant to settle disputes.

### What this honestly proves

**It is not a trusted timestamp.** This log is published by the same party it
constrains. We could publish one root and later publish another, and anyone
comparing only against us would not notice.

What it gives is narrower and still worth having:

- Anyone who **recorded a root at the time** can afterwards prove that a
  determination either was or was not covered by it. That is a check the issuer
  cannot pass retroactively.
- Silent retroactive alteration stops being invisible and becomes something the
  issuer must either brazen out or be caught doing.

Proving time to a party who was *not* watching requires an external witness — a
counterparty countersigning roots, another organization mirroring them, or a
public append-only medium. Overstating what the log alone gives is the fastest
way to make the whole standard untrustworthy, so the witness is stated
separately, below, with its own limits.

The advice to a reader is still concrete: **record a root when you see one.**

### The external witness (since 2026-10-31)

Each digest is independently timestamped through OpenTimestamps, which
commits it to a block of the Bitcoin ledger (`docs/digest-anchoring.md`,
migration 222). No cryptocurrency is held, bought or paid: the public ledger
is used only as a clock nobody can edit. What is anchored is one line
binding the period to its root —
`calchis-determination-digest/1 <period_start> <period_end> <root> <size>`,
times as ISO-8601 UTC with milliseconds — hashed with SHA-256. The standard
`.ots` proof is served at `GET /api/v1/determinations/anchors?period=YYYY-MM-DD&format=ots`,
and every proof response and every digest in the log carries `anchor` with
the block height and time once confirmed. An anchor is `confirmed` only when
the Bitcoin attestation matched the block's Merkle root fetched independently;
a proof that fails its own check is never served as confirmed.

What a confirmed anchor proves: that the root, and so every identifier sealed
under it, **existed before that block's time** — evidence of time for a reader
who was not watching. What it does not prove: anything about whether the
determinations were right. That evidence is each record's own (§8).

## 8a. Machine-readable schemas

| Document | Schema |
|---|---|
| Determination record | `docs/determination-record.schema.json` |
| Trigger specification | `docs/trigger-specification.schema.json` |

JSON Schema draft 2020-12. Published for outside implementers; the repository
deliberately does not add a validator dependency to consume its own schemas.
Instead `tests/lib/determination/schema-matches-code.test.ts` holds the two
together — every field the schema marks required is asserted to be produced, and
the shared constants (`standard`, `operator`, the identifier patterns,
the status enumeration) are asserted equal on both sides.

That test exists because a published schema which disagrees with the
implementation is worse than no schema: it tells an outside implementer to build
something that will be rejected.

## 9. Versioning

- The canonical form is versioned separately from the record schema. A change
  to the canonical form invalidates every previously issued identifier and is
  therefore a new standard version, never a patch.
- `schema_version` inside the evidence increments when a consumer must branch.
- An existing version's semantics are never reinterpreted. Add a version.

## 10. Governance

A standard controlled by one company is a vendor format. This document is
published under the repository's license, with the canonical form and
verification algorithm specified above in enough detail to reimplement without
reference to the source.

Two things would materially strengthen it, and neither has happened yet:

1. **Independent reproduction.** No third party has implemented this and
   confirmed matching identifiers. Until one has, "reproducible" is a claim.
2. **Neutral stewardship.** The natural home is the Oasis Loss Modeling
   Framework ecosystem, which already convenes this industry and whose
   exposure format this platform already speaks.

Criticism is more useful than adoption at this stage. Corrections and attacks
on the scheme are welcome.
