11 KiB
T02 contract record: datasets, authority and pinned reports
Status: proposed contracts for the first implementation batch (2026-09-19). This record freezes shapes and invariants, not persistence or worker behavior. It does not approve stale-authority windows, recovery guarantees, retention, quotas or public release policy.
Invariants
- A
ResourcePrincipalis resolved by the server. A character, corporation or location ID supplied by a browser, assistant or job payload is a selector and never authorization. grantRevision(what scopes/resources are authorized) is distinct fromcredentialVersion(which encrypted token/credential generation is current). A revision change invalidates affected jobs, cursors, caches and private reports.DatasetCoveragedistinguishes an atomic snapshotgenerationfrom an append datasetcheckpointand coverage window.fetchedAtis the local observation time.upstreamEffectiveAtis nullable because ESI often supplies no effective timestamp.- A dataset read is authorized only when its principal is selected and user-owned, its
authorizationStatusis active, and itsgrantRevisionandauthorizationEpochexactly match the selected resource. Ready cached data from an earlier authority epoch is stale and cannot satisfy a gate. - Snapshot data must stage and validate before publishing a generation. Append data is idempotent by source identity and reports finite coverage/gaps rather than pretending to be an unlimited atomic history.
PublicationFenceis attached to both success and failure publication. User jobs require a non-empty resource fence; system/reference/public-feed jobs require an empty one. Corporation-owned jobs require every fenced resource to be the exact corporation principal (no foreign-resource mixing). It covers every selected resource with grant, credential and monotonic authorization-epoch fences; a worker cannot publish after its grant, credential, generation or quarantine fence is stale. This is a contract for later implementation, not an implementation in T02.JobAuthorityis explicit and duplicated job-level principal/revision fields are avoided; the fence is the authority source. A user-owned job cannot promote itself tosystem; system/reference/public-feed jobs have no user dataset authority.- A
ConsentBindingbinds one OAuth attempt to one initiating session, intent, capability and a non-empty list of per-resource requested/approved-preservation scopes. Accepted callbacks require a pending, unexpired binding; grants echo the exact bound request, are cardinality-matched, and are validated for finite counters, required scopes, server allowlists, grant revision and authorization epoch before persistence. Callback failure with an unknown consumed-code outcome returnsrecovery_required; it must not blindly replay a code. Exact orphan cleanup and recovery UX remain an operator decision. - Transfer, revocation and quarantine are distinct authorization states. The contract carries an unresolved
StaleAuthorityObservation.maximumWindowMs; no stale grace period is invented here. - An inventory cursor is opaque to the caller but carries a server-side pin of selected resources, authorization revisions, epochs, snapshot generations, reference version, price version and expiry plus continuation offset/last-item state. Server state marks cursors
activeorexhausted; every sort uses canonicalitem_idtie-breaking; anafter_endresult always hasnextCursor: null. Possession of a cursor/report ID is not authorization. - Inventory summaries use
scope: all_matching_rows, never the visible page. An emptyafter_endpage is a valid terminal response and may retain a non-empty selected summary;nextCursor: nullandpageState: 'after_end'make this explicit. An empty initial page means no matching rows. Clients must not substitute page totals. - Wallet results contain labelled
wallet_balancecomponents. They do not expose a combined wealth total or imply journal, transaction, order or profit coverage; each component pins credential version and wallet coverage. Asset.isSingletonis optional at the legacy boundary; query rows must normalize absence toisSingleton: null, never infer singleton state from quantity. T02 only freezes this contract; retaining the ESI fact during ingestion remains a downstream T06/T07 gate.- A cursor token is opaque. Its unsigned pin is stored server-side and includes server-derived caller identity, query fingerprint, selected resources, revisions, epochs and input versions; every use revalidates that pin and current authorization. Possessing or editing a token cannot authorize or retarget a cursor, and no cursor-signing crypto is selected here.
- Capability consent separates required scopes from the explicit server allowlist and approved-existing scopes preserved during an upgrade. Arbitrary requested scopes are rejected; declarative metadata does not itself replace runtime authorization checks.
- Reports carry one dataset pin per selected resource/dataset, including coverage, authorization revision, authorization epoch and fetch/effective times; compatibility maps are indexes, not the authority. Ready-result provenance requires complete snapshot/append pins, while partial results use explicit partial states.
Frozen request/result surfaces
InventoryQuery is the first query surface: resolved resource selection, validated filters (character/type/name/category/group/quantity/location/system/region/blueprint state), stable sort, bounded page size and a pinned cursor. InventoryQueryResult has explicit ready, partial, stale, authorization-required and unavailable states.
ReferenceReaderPort mirrors T03’s indexed ReferenceReader: type search, category/group, region/constellation/system and station methods use its bounded selection/record shapes. pin() returns a ReferenceReaderHandle with immutable pinnedVersion and explicit close ownership, so multiple calls share one artifact. LocationReader is only a narrow adapter over that pinned reader and synthesizes explicit kind: 'unknown' facts for empty ID lookups; it is not a competing reference implementation. Private structure names/evidence are not public reference facts.
WalletBalanceResult is opt-in and resource-scoped. Each component carries exact Isk, local fetch time, nullable upstream effective time, coverage and authorization revision. Partial character coverage is visible.
Public market_data and reference_data gates accept only active system/public provenance; an active character or corporation dataset cannot satisfy another user’s public-data requirement.
ReportProvenance pins report inputs, authorization revisions, reference/price versions, fetch/effective times and assumptions. Complete reports require the exact selected-resource × required-dataset pin set; each pin echoes its selected resource and complete coverage. It is intentionally smaller than a copied inventory and contains no credentials, cookies or provider payloads.
Approved money policy boundary
The baseline remains unchanged: existing Isk values are integer hundredths serialized as decimal text; market-price validation remains non-negative; numeric quantities remain non-negative safe integers and bigint quantities remain supported. The currently verified ESI market-price converter accepts the unsigned JSON-number decimal subset, permits exponents in [-1000, 1000], and rounds non-negative ties half-up. Its legacy regex may accept a terminal newline; the chosen strict boundary below intentionally tightens that edge rather than claiming zero grammar differences. That baseline is retained as a separate policy; negative market estimates remain invalid.
The approved shared signed extension accepts ASCII decimal text only, with strict full-input matching against:
^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$
This is the chosen JSON-number grammar for the decimal boundary: no trimming, whitespace, leading +, leading-zero integer, .5, 1., separators, NaN, Infinity, or non-ASCII digits. The raw input is capped at 8192 characters, checked before regex or parsing; accepted ASCII input has the same character and UTF-8 byte length. Strict matching must explicitly reject a final newline because JavaScript $ can otherwise accept one.
After syntax validation, count all mantissa digits (excluding sign and decimal point) and reject more than 4096 coefficient digits. Inspect the exponent lexically and boundedly before converting it or allocating powers/BigInts. The cents shift after scaling (centsShift = exponent + 2 - fractionDigits) must be within [-1000, 1000]; any excess is rejected, never truncated. For example, 1e998 has shift 1000 and is allowed, while 1e999 has shift 1001 and is rejected. 1. followed by 1002 zeros has shift -1000 and is allowed under the coefficient/raw-input limits. Normalize to integer hundredths using fixed half-away-from-zero rounding and canonicalize negative zero to 0. There is no selectable rounding-mode API. Examples: 1.005 → 101, -1.005 → -101 cents.
T02 records this policy only; T04 owns parser/formatter implementation. Newly rejected over-cap, over-coefficient, over-exponent/shift, whitespace, non-ASCII and otherwise invalid inputs are intentional compatibility changes at the new signed boundary. Strict final-newline rejection also tightens the legacy ESI regex edge; preserve the established unsigned non-negative validation and half-up rounding semantics, plus existing quantity behavior.
Explicit decision gates
- Callback/recovery: exact behavior after a provider code may have been consumed is unresolved; use recovery/new-binding states and never blind replay.
- Stale authority: maximum stale window and remote-revocation retry/deletion deadline are unresolved; quarantine/local fail-closed behavior needs the owning auth/worker decision.
- Retention: generation/checkpoint/report/export retention and GC are unresolved; valid pins must be retained, but no duration or quota is selected here.
- Public admission/quotas: no public limits or signup policy are selected by these contracts.
- Money implementation compatibility: the signed policy is approved and fixed; T04 must preserve the exact grammar, bounds, reject-before-allocation ordering, half-away rounding, negative-zero canonicalization, and baseline unsigned ESI validation/half-up behavior.
- Reference source coverage: T03 must verify station source coverage; the reader contract does not claim stations exist.
Compatibility and migration
T02 adds type-only contracts, declarative capability metadata, synthetic fixtures and tests. It does not add Convex tables, indexes, migrations, OAuth behavior, workers, schema changes or production routes. Existing Character, Asset, AssetReport, capability gates and repository ports remain source-compatible. AssetReport.provenance is optional until downstream services can pin it.