# Analyse an object: the pinned record, at your own exposure

`POST /objects/{id}/analyse` runs **named analyses** over a canonical risk
object and records what they found on that object's provenance. It is a job:
the call returns `202` with a job id, you poll `GET /jobs/{id}`, and the
finished job carries the artifacts.

Two analyses today:

| `analyses` name | What it does |
| --- | --- |
| `point_hazard` | Looks the pinned record up at **each of the object's own coordinates** — the same document [`GET /hazard`](hazard.md) serves, once per location. |
| `event_intersection` | Selects the **catalogue events that fall in the box the exposure spans** — the same document [`GET /events/{peril}`](events.md) serves, filtered by the same selector a backtest filters with. |

Nothing here is new arithmetic. Both analyses are the routes above aimed at an
object instead of at coordinates you typed, which is the point: the numbers on
an artifact and the numbers you get by calling those routes yourself are the
same numbers, read from the same pinned bytes.

Read [the quickstart](quickstart.md) first for the two values you export
(`CEDE_BASE_URL`, `CEDE_API_KEY`) and how to get a key from `POST /signup`.

## What it is for

A price is a number computed from a record on a particular day. Six months
later, the question a reader has is not "what is the price" but "what did the
data say when this was priced" — and by then the catalogue has a new vintage
and the answer is unrecoverable unless somebody wrote it down. This verb writes
it down, on the object, in `provenance.analysis_runs`:

* the run id, and a link to the job holding the full artifacts;
* per artifact, a one-line summary, the **assumptions** behind it, and the
  **data vintages** it was computed from — feed id, version, vintage, and the
  SHA-256 of the exact bytes.

Provenance is append-only. A second run adds a second entry; nothing already
recorded is rewritten, including a run that said something different about the
same coordinates.

## What the object needs

| The object must carry | Because |
| --- | --- |
| `peril.code` | Every analysis here is a reading of one peril. `PATCH /objects/{id}` writes it. |
| `exposure.locations[]` with `latitude` and `longitude` | Every analysis here is a reading at a place. Ingest never geocodes an address, so a schedule of addresses reaches this route with nowhere to look. |
| at most 25 located locations | A run embeds a full hazard document per location. Above the cap the run is **refused**, never trimmed: an artifact that quietly described part of an exposure would be read as describing all of it. |

Refusals are structured and name what would have worked: `unknown_analysis`
(422) lists every analysis that can be asked for, `unanalysable_object` (422)
names the block to write and the route that writes it. Both are in [the error
reference](errors.md).

---

## Walk it

The block below stores a two-location Kanto schedule, runs both analyses over
it, reads the artifacts out of the finished job, reads the provenance back off
the object, and then asks for an analysis that does not exist so you can see
the refusal.

<!-- cede:runnable -->

```bash
set -euo pipefail

: "${CEDE_BASE_URL:?export CEDE_BASE_URL first — see the quickstart}"
: "${CEDE_API_KEY:?export CEDE_API_KEY first — see the quickstart}"

# 1 — An object to analyse: two located sites, one peril.
cat > schedule.json <<'JSON'
{
  "schema_version": "0.1.0",
  "status": "draft",
  "exposure": {
    "kind": "location_schedule",
    "currency": "JPY",
    "locations": [
      {
        "ref": "LOC-0001",
        "name": "Shinagawa distribution centre",
        "latitude": 35.6284,
        "longitude": 139.7387,
        "geocode": {"resolution": "rooftop", "confidence": 0.96},
        "values": {"building": {"amount": 8400000000, "currency": "JPY"}}
      },
      {
        "ref": "LOC-0002",
        "name": "Chiba assembly plant",
        "latitude": 35.5687,
        "longitude": 140.1247,
        "geocode": {"resolution": "street", "confidence": 0.83},
        "values": {"building": {"amount": 5100000000, "currency": "JPY"}}
      }
    ],
    "source_fidelity": {
      "unmapped_columns": [],
      "guessed_units": [],
      "ambiguous_rows": []
    }
  },
  "peril": {"code": "earthquake"}
}
JSON

curl -sS "$CEDE_BASE_URL/objects" \
  -H "Authorization: Bearer $CEDE_API_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @schedule.json > object.json

OBJECT_ID="$(python3 -c 'import json; print(json.load(open("object.json"))["id"])')"
echo "object: $OBJECT_ID"

# 2 — Run both analyses. A job comes back.
curl -sS "$CEDE_BASE_URL/objects/$OBJECT_ID/analyse" \
  -H "Authorization: Bearer $CEDE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"analyses": ["point_hazard", "event_intersection"]}' > accepted.json

JOB_ID="$(python3 -c 'import json; print(json.load(open("accepted.json"))["id"])')"
echo "job:    $JOB_ID"

# 3 — Poll it to a terminal state.
for _ in $(seq 1 60); do
  curl -sS "$CEDE_BASE_URL/jobs/$JOB_ID" \
    -H "Authorization: Bearer $CEDE_API_KEY" > job.json
  STATUS="$(python3 -c 'import json; print(json.load(open("job.json"))["status"])')"
  [ "$STATUS" = "queued" ] || [ "$STATUS" = "running" ] || break
  sleep 1
done
echo "status: $STATUS"
test "$STATUS" = "succeeded"

# 4 — The artifacts, each with its assumptions and the vintages behind it.
python3 -c 'import json
run = json.load(open("job.json"))["result"]["analysis"]
print("run:      ", run["run_id"], "at", run["completed_at"])
print("analyses: ", ", ".join(run["analyses"]))
for artifact in run["artifacts"]:
    print()
    print("==", artifact["analysis"])
    print("  ", artifact["summary"])
    for vintage in artifact["data_vintages"]:
        print("   read:", vintage["id"], vintage["version"],
              "vintage", vintage["vintage"], vintage["sha256"][:12] + "…")
    if artifact["analysis"] == "point_hazard":
        for reading in artifact["locations"]:
            hazard = reading["hazard"]
            print("   %-22s %s,%s -> %s over %s years"
                  % (reading.get("ref", reading["location_index"]),
                     reading["point"]["latitude"], reading["point"]["longitude"],
                     hazard["metrics"]["basis"],
                     hazard["assumptions"]["period_of_record"]["years"]))
        for missed in artifact["not_analysed"]:
            print("   not read:", missed["location_index"], missed["reason"])
    else:
        box = artifact["box"]
        print("   box:  %s-%sN %s-%sE (exposure extent + %s deg)"
              % (box["min_latitude"], box["max_latitude"],
                 box["min_longitude"], box["max_longitude"],
                 box["cell_half_width_degrees"]))
        print("   events:", artifact["counts"]["intersecting"], "of",
              artifact["counts"]["events_in_snapshot"], "in the snapshot")
        for event in artifact["catalogue"]["events"][:5]:
            print("     ", event["time"], "M" + str(event["magnitude"]),
                  event["event_id"])
print()
print(run["disclosure"])'

# 5 — The record on the object itself. This is what a reader finds months later.
curl -sS "$CEDE_BASE_URL/objects/$OBJECT_ID" \
  -H "Authorization: Bearer $CEDE_API_KEY" > analysed.json

python3 -c 'import json
document = json.load(open("analysed.json"))
print("status:", document["status"])
for entry in document["provenance"]["analysis_runs"]:
    print("run", entry["run_id"], "->", entry["links"]["job"])
    for artifact in entry["artifacts"]:
        print("  ", artifact["analysis"] + ":", artifact["summary"])
        print("     method:", artifact["assumptions"]["method"][:90] + "…")
        for vintage in artifact["data_vintages"]:
            print("     vintage:", vintage["id"], vintage["version"],
                  vintage["sha256"][:12] + "…")'

# 6 — An analysis that does not exist. The refusal names the whole valid set,
#     and this block demands the status rather than just printing it: if the
#     route ever answered something else, this example would stop exiting 0.
status=$(curl -sS -o refused.json -w '%{http_code}' \
  "$CEDE_BASE_URL/objects/$OBJECT_ID/analyse" \
  -H "Authorization: Bearer $CEDE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"analyses": ["ground_motion"]}')

python3 -c 'import json
error = json.load(open("refused.json"))["error"]
print(error["status"], error["code"])
print(error["message"])'

test "$status" = 422
```

## Reading the artifacts

**`point_hazard`** holds one entry per location that could be read, each
carrying the full hazard document for that coordinate — its metrics, its
method, its period of record and the snapshot it came from. Locations that
carried coordinates no pinned snapshot covers are listed under `not_analysed`
with the edge they fell outside, rather than dropped: an exposure half of which
was read must never read as an exposure that was read.

**`event_intersection`** holds the box (the exposure's own extent, grown by the
half-degree cell a point lookup counts in), the counts, and a page of the
catalogue events inside it. The box is *not* a structure's trigger box: a
cat-in-a-box trigger carries its own region on `trigger.index`, and a backtest
of that structure selects against that region instead.

## What this is, and is not

* **A measurement, not a view of risk.** Every run carries that sentence in its
  `disclosure`. No loading, no premium, no statement about whether the risk
  should be carried or on what terms.
* **An intersection is not a loss.** An event inside the box says the catalogue
  put a solution there. What happened to anything at the locations needs a
  vulnerability model this build does not have — and would card in the registry
  if it did.
* **Nothing reaches the network.** Every number is arithmetic over a pinned
  snapshot whose bytes are servable in full at
  `/feeds/{id}/versions/{version}/data`, with the digest on the artifact.
* **Free.** Craton bills per object and per model run; this run mints no object
  and runs no registry model, and every number in it is already free at
  `GET /hazard` and `GET /events/{peril}`. `GET /usage` will not show it. A
  cancelled account is still refused, though — the run *writes* to an object,
  and every route that writes to an object refuses one.

The published schema is `analysis.v0.schema.json`, listed in the API contract
document with every other response shape; the run travels in the job at
`result.analysis`. Validate against it while holding no Craton code.

## Where to go next

* [The point hazard lookup](hazard.md) — the same reading, at one coordinate.
* [Events: the hazard record behind a result](events.md) — the same catalogue,
  addressable event by event.
* [A technical price, from nothing](price.md) — the verb that reads the object
  this one annotated.
