# GoVT Open Civic Data Contract

**Schema:** `govt.open-data/1` (response `meta.contract`) · **Envelope schema:** `govt.provenance/1` (per-record `provenance`)
**Status:** Phase 0 — read-only, unauthenticated, license-enforced open data. No API key, no consumer registry, no bespoke connector.

This document is the canonical reference for consuming Go Vermont's (`govermont.co`) public `/api/v1` civic data. It is linked from every v1 response via `meta.contract.docs`.

---

## 1. The envelope (`govt.provenance/1`)

Every record on every `/api/v1` collection route (`officials`, `events`, `towns`, `counties`) carries a **non-optional** `provenance` block. There is no "trust me" path: if a record is served, it carries its own envelope.

```ts
type Provenance = {
  schema: "govt.provenance/1";
  type: "T0" | "T1" | "T2" | "T3" | "T4" | "T5" | "T6"; // artifact KIND — not a trust ranking
  trust_tier: 0 | 1 | 2 | 3 | 4 | 5;                    // computed trust — 5 = highest
  authority_rank: 1 | 2 | 3 | 4 | 5 | 6 | 7;             // source-authority ladder — 1 = strongest
  source: { url?: string; note?: string };
  verification: {
    verifiedBy?: "scraper" | "agent" | "manual" | "phone";
    lastVerified?: string;                                // ISO date
    freshness: "recent" | "current" | "aging" | "outdated" | "unknown";
  };
  correction_pending: boolean;                            // true if a correction is under human review
  derivation: string[];                                   // input types for derived artifacts; [] for primary records
  label: string;                                           // REQUIRED — pre-composed display string (see §2)
};
```

**Load-bearing properties:**
- **TYPE and `trust_tier` are orthogonal.** TYPE describes the artifact's shape; `trust_tier` describes how much trust it actually carries. A licensee must never infer trust from TYPE alone.
- **`trust_tier` is computed server-side at serve time, on every request** — it is never stored, so a stale-served-as-fresh record is impossible by construction.
- **Consumers may re-derive `trust_tier` downward, never upward.** If you compose GoVT records into a new artifact, your output's trust ceiling is the *minimum* `trust_tier` among your inputs.

Every collection response also carries a response-level open-data contract block:

```ts
type OpenDataContract = {
  schema: "govt.open-data/1";
  license: { name: string; url: string; termsVersion: string };
  docs: string;    // this document
  verify: string;  // the repudiation endpoint, §3
};
```

### Response metadata: build time vs. data age

These are two different clocks and the contract keeps them apart. Conflating them is
how a dataset reports itself as fresh while serving months-old records.

```ts
meta: {
  total?: number;
  responseGeneratedAt: string;   // when THIS RESPONSE was built. Says nothing about data age.
  dataFreshness: {               // when the DATA was last checked, derived from the records
    latestCheckedAt: string | null;   // newest verification stamp among served records
    oldestCheckedAt: string | null;   // oldest verification stamp among served records
    missingCheckedCount: number;      // served records carrying no usable stamp
    missingSourceCount?: number;      // served records carrying no source URL
    note: string;                     // plain-English scope of the above
  };
  contract: OpenDataContract;
}
```

**`responseGeneratedAt` is not a freshness signal.** Do not display it as "last updated" —
it is the moment we answered you, and it advances on every request no matter how stale the
underlying data is. Use `dataFreshness`, or the per-record `provenance` stamps, for anything
a reader will interpret as currency.

**A `null` timestamp means unknown, not recent.** `latestCheckedAt: null` with a non-zero
`missingCheckedCount` means those records carry no usable verification stamp at all. Render
that as unknown; never substitute the request time, and never fall back to a default date.

**Stamps are strictly validated before they count.** A calendar-impossible date
(`2026-02-30`) or a stamp more than one day in the future is treated as missing everywhere —
aggregate metadata, per-record `trust_tier`, and labels alike — rather than being normalized
into a plausible-looking date. Aggregate freshness never claims more than per-record
provenance does.

> **History:** `meta.lastUpdated` was removed on 2026-08-19. It carried request time, so
> every response asserted freshness regardless of data age — including during a six-month
> window when production served a February export. It is gone from the type rather than
> deprecated, so a regression fails to compile, and its absence is asserted at runtime by
> INV-B. Consumers that read `meta.lastUpdated` should migrate to `dataFreshness`.

---

## 2. The label rule (§D1.4 — the enforcement keystone)

`provenance.label` is a **pre-composed, ready-to-render string**. Breach detection for "did this consumer preserve provenance" reduces to a verbatim string-presence check — auditable by anyone from a screenshot, no cryptography required.

**Required-label rule:** any surface (page, card, export, screenshot, social post) that displays a GoVT record must render `provenance.label` **verbatim, in the same visual unit** as the fact itself. Do not paraphrase it, truncate it, or compose your own label from the envelope's individual fields.

The exact strings that can appear (pinned by test in the GoVT codebase — treat any deviation from these shapes as a bug to report, not a license to improvise):

| Condition | Label shape |
|---|---|
| Verified, fresh stamp | `Verified — {source}, checked {YYYY-MM-DD}` |
| Human-verified, but stamp aged past 90 days | `Verified {YYYY-MM-DD} — may be outdated ({N} days since last check)` |
| Sourced/scraped, unreviewed, with a stamp | `Reported — {source}, last checked {YYYY-MM-DD}; not independently verified` |
| Sourced, no per-record stamp recorded (e.g. towns/counties interim) | `Reported — {source}; no per-record check recorded` |
| AI-checked, not human-verified | `AI-checked — not human-verified; may be inaccurate` |
| Raw citizen report | `Citizen report — unverified` |

If `correction_pending` is `true`, every label above is suffixed with ` · correction under review` — a consumer must not present a record as settled while GoVT has an open correction against it.

---

## 3. The verify endpoint (repudiation without cryptography)

`GET /api/v1/verify?kind={official|event|town|county}&id={id}`

Returns `{ served: true, record, provenance }` if GoVT is currently serving that exact record (byte-identical to what the matching collection route serves), or `{ served: false }` otherwise. This is GoVT's repudiation mechanism: anyone — a journalist, a citizen, an auditor — can check whether a claim attributed to GoVT is one GoVT actually serves, without any authentication or shared secret. There is no per-record signature scheme; the verify endpoint against GoVT's own live data *is* the audit trail, backed by git history as the archive of record.

---

## 4. The license (§D1.5)

`meta.contract.license.url` points to the published **GoVT Open Civic Data License**, currently `/open-data-license`. In summary, the license binds every consumer to:

1. **Attribution** — the exact string `Data: GoVT (govermont.co)`, plain text. No GoVT logo, no co-branding, no "powered by GoVT."
2. **Envelope preservation** — the `provenance.label` renders verbatim wherever the fact is shown; a machine re-serving GoVT data carries the full `provenance` block byte-intact.
3. **No tier raising** — you may present a record at a *lower* trust posture than its envelope states, never higher.
4. **No endorsement** — you may not state or imply GoVT endorses, partners with, or is affiliated with your product.
5. **No fabrication or misattribution** — never attribute to GoVT a record or value GoVT does not actually serve.
6. **No stale-as-fresh** — a record whose envelope says `aging`, `outdated`, or `unknown` must not be presented without that state visible.
7. **Audit + termination** — GoVT may audit public consumer surfaces; material breach terminates the license and the right to attribute.

Read the full term sheet at `/open-data-license`. **The published license is a draft term sheet pending final legal-language review; `termsVersion` in every response tells you which version you are bound by.**

---

## 5. Deterministic resolution recipe (§D2.1)

This is the exact, deterministic (no LLM inference) algorithm the govermont.co site itself uses to resolve "who represents this citizen" — reproduce it exactly; do not substitute a model-guessed lookup for any part of it.

1. **Resolve the citizen's town.** Match the citizen's town name to a `townSlug` via an exact (case-insensitive) match against `GET /api/v1/towns` — do not fuzzy-match or infer.
2. **Municipal officials.** Fetch `GET /api/v1/officials?town={townSlug}` (all levels for the town, filtered server-side by the API), then keep records where `level === "municipal"` **OR** `title` (case-insensitive) contains `"selectboard"` or `"council"` — this second condition matches a small number of officials whose `level` field is miscategorized but whose title makes their role unambiguous. This is the exact filter the site itself applies (not a `level=municipal`-only query, which would silently miss those records).
3. **Legislators — fetch-then-filter, not a per-district query.** The public API does not expose a `?district=` filter. Instead:
   - Fetch `GET /api/v1/officials?level=legislature` (all state legislators).
   - Fetch (or reuse) the citizen's town record from `/api/v1/towns`, which carries `houseDistricts: string[]` and `senateDistricts: string[]` (arrays, because a small number of Vermont towns are split across more than one district).
   - Filter: keep a legislator if (`official.chamber === "house"` AND `official.district` is a member of `town.houseDistricts`) OR (`official.chamber === "senate"` AND `official.district` is a member of `town.senateDistricts`).
4. **Carry the envelope forward.** Every resolved official record already carries its own `provenance`. Store `official.id` + `provenance.verification.lastVerified` + your own `retrieved_at` timestamp alongside any downstream use of that record, so a later dispute or staleness question is traceable back to the exact GoVT record and moment you consumed it.
5. **No fabrication on a miss.** If step 2 or step 3 return no match — a genuine coverage gap, or a request outside Vermont — do not synthesize an address or an official. Present the target as unresolved/unverified to your own user; never construct a `name@municipality.gov`-shaped guess.

---

## 6. What this contract deliberately does NOT include

Per the ratified trust-architecture design (`FABLE_GOVT_FEDUP_TRUST_DESIGN.md` §D1.7): no authenticated consumer channel, no API key, no per-record cryptographic signature, no consumer registry. Enforcement is the license (§4) + the verify endpoint (§3) + GoVT's own git history as the audit archive — not a bespoke trust mechanism a licensee must integrate against. If you preserve the envelope and the label, you are compliant by construction.

---

*This document is versioned alongside the govermont.co codebase. `meta.contract.license.termsVersion` in any given API response tells you which revision of the license terms governed that response.*
