Initial Commit

This commit is contained in:
jb
2026-09-18 23:15:05 -04:00
commit fdcc4863f5
118 changed files with 16868 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
# Architecture — milestone 1
User decisions: assets + estimated valuation; first EVE login creates the app user; single-host Compose. See `milestone.md` for acceptance and explicit non-goals; `research/` for live official-source evidence.
```
Svelte 5 chat / assets / characters
│ same-origin SvelteKit API (opaque HttpOnly session)
├─ convex-svelte subscription: safe character + synchronization state
▼
Bun application services
├─ typed intent router (JEV Choice, or explicit /assets and /value commands)
├─ deterministic capability/ownership gates
├─ shared AssetService / reference valuation
├─ Convex repository ── user state, encrypted tokens, snapshot chunks, executions
├─ centralized ESI transport ── conditional cache / retry / pagination
└─ read-only ReferenceRepository ── ACTIVE immutable bun:sqlite artifact
Dedicated Bun reference worker
source adapters → normalized rows → STAGING database → validation
→ versioned artifact → atomic manifest {ACTIVE, PREVIOUS, generation}
```
## Boundaries
`src/lib/domain` is independent of Svelte, Convex, ESI, SQLite and JEV. Characters/assets belong to an application user. Reference IDs, item names, normalized blueprint relationships and XYZ live in reference SQLite. Mutable ownership, snapshots and assistant executions live in Convex. Market estimates use a separate provider port and never enter the immutable reference DB.
`src/lib/server` is server-only by SvelteKit convention. No EVE tokens or bridge secret may be imported into client modules. Convex public queries expose explicit safe projections. Privileged bridge functions authenticate a dedicated server-only service secret (not the deployment admin key). User-facing queries authenticate an application JWT with issuer/audience and check session revocation plus user ownership.
## Authentication and synchronization
EVE authorization-code + PKCE and encrypted token storage runs in Bun. First login creates an app user keyed to a verified character and owner claim; explicit linking attaches additional characters to the current user. Conflicting ownership never silently merges accounts. Session cookies carry random opaque values; only hashes are stored. Application RS256 JWTs are short-lived and audience-scoped for the Convex browser client. EVE access/refresh tokens are never browser credentials.
Asset sync stages bounded chunks, then atomically changes a character's active generation after all pages succeed. A failed fetch preserves the previous complete generation. Sync metadata records freshness/error, and the domain gate refuses to present missing/stale coverage as complete. ESI page caches are isolated by authorization identity; no partial paginated result is accepted as a full snapshot. Single-host deployment initially runs one web process; token refresh also uses durable compare-and-swap to prevent a stale refresh from overwriting a rotated token.
## Valuation
Only `reference_estimate` is executable now. ESI average prices are estimates, not Jita bids, asks, replacement quotes or liquidation proceeds. Exact accumulation uses integer hundredths of ISK serialized as strings. ESI average unit prices are rounded half-up to 0.01 ISK before quantity multiplication; this deliberately bounded precision is part of the estimate policy, not a claim of exact market execution. Large stacks can magnify sub-cent rounding, so higher-precision reference estimates are a future refinement. Missing/unpriceable items are excluded and counted, never silently zeroed. Prices, assets and reference version carry provenance/freshness. Blueprint-copy valuation is deliberately excluded.
Future order books, depth walking, fees/taxes and location/range matching belong behind `OrderBook`; immediate liquidation cannot be implemented by multiplying the best bid. Industry contracts separate additional cash needed from owned-material opportunity value and economic profit.
## Reference lifecycle
The reference worker owns archive work. Source metadata/fingerprints and import status are independent for Reference Data, Hoboleaks, and the CCP SDE universe complement. CCP records are translated into the existing ESI-shaped universe contract (the compatibility source key remains `esi-universe`). A failed source may retain its verified last successful input while another advances; cross-source validation still gates the resulting database. Explicit production adapters stream/validate archive members and compile real releases, while tiny fixtures continue to exercise offline promotion/rollback. Archive bytes and canonical source payloads have separate hashes; immutable source caches bind provenance to the active artifact. Version identity is the downstream cache key; readers select one version for a request and never mix tables across artifacts. Live readers reject fixture-profile artifacts.
## Development deployment and secrets
The configured development deployment uses Infisical startup injection, not runtime vault requests per user operation. A host-side read-only machine identity fetches allowlisted app secrets; its bootstrap credentials/token never reach the web container. CLI-only Convex administration can derive its key locally from the persistent backend for explicit deployment commands. The browser uses the public HTTPS Convex hostname; internal server traffic stays on the Compose network. Pangolin supplies public TLS/HTTP routing and WebSocket upgrades. See `live-setup.md` for exact origins, ports, and operator boundaries.
## Future domain contracts
- **universe/routing:** normalized systems, finite XYZ, stargate edges; exact EVE light-year constant 9,460,000,000,000,000 metres; future capital search uses ship/skill/eligibility and weighted constraints.
- **structures:** separate visibility from docking permission; timestamped evidence and explicit unknown/allowed/denied with limitations. Live route findings are in `research/eve.md`.
- **blueprints/industry:** normalized activities/materials/products; deterministic quantities/time/economics, candidate generation before any semantic ranking.
- **assistant:** narrow semantic judgments only; deterministic allowlisted dispatch, capability gates, structured connect/sync/clarify/unavailable results. A missing JEV key does not silently simulate natural-language understanding.
## Scaling gate, not premature infrastructure
No Redis, PostgreSQL, search service or market analytics database is introduced. Convex stores bounded snapshot chunks; the live market provider is replaceable. The reviewed full archives have been validated, but projection/cache construction still materializes bounded source maps in memory. Before larger deployments: benchmark transaction/import limits, improve row-level streaming where needed, implement retention/GC and durable job recovery, and validate future release formats rather than assuming compatibility.
+58
View File
@@ -0,0 +1,58 @@
# ESI operations
The server ESI adapter uses the fixed trusted base `https://esi.evetech.net` and pins
`X-Compatibility-Date: 2026-08-18` (`ESI_COMPATIBILITY_DATE`). It exposes only the
character asset GET and global market-price GET needed by the application; callers
cannot provide an arbitrary ESI URL.
## Asset synchronization
`EsiClient.getAssets(characterId, accessToken, authorizationKey)` fetches page one,
reads `X-Pages`, fetches all remaining pages, de-duplicates by `item_id` while
checking for duplicates, and revalidates page one after the other pages. A changed
page count, duplicate ID, malformed page, failed request, or changed first page
fails the operation. The caller must not persist a returned partial result: the
method returns only a complete `{ assets, syncedAt, expiresAt }` snapshot. Asset
IDs are retained as decimal strings, and the response is parsed with
`lossless-json` so an int64 cannot be rounded by JavaScript.
This is a stability check, not a transaction. ESI's paginated endpoint does not
promise a true multi-page snapshot while its cache expires between page requests.
A caller should stage pages and replace the persisted Convex snapshot only after
`getAssets` resolves. Convex persistence is intentionally outside this adapter.
The cache is private to an `EsiClient` and keyed by user/authorization context,
character, and scopes. `authorizationKey` should therefore identify the linked
application user and include the character/scopes when available:
```ts
{ userId, characterId, scopes }
```
A string key is supported for callers that have no scope object, but is treated as
an isolated user key. Tokens are never used as cache keys or emitted in metrics.
Responses retain their raw text, ETag, Last-Modified, and cache metadata. Fresh
responses use the server TTL; stale responses send `If-None-Match` and
`If-Modified-Since`, and a 304 reuses the cached body. `Cache-Control`, `Expires`,
`Date`, and `Age` are honored. `no-store` responses retain neither body nor
validators. Requests have one deadline covering response-body reads and bounded
retries for transient network/5xx/420/429 responses. `Retry-After` (seconds or
HTTP date) and the legacy ESI error-limit reset are respected. Exhausted new
limiter buckets are cooled down per authorization context and group; the legacy
error limiter is cooled down globally. A server delay that exceeds the remaining
request budget returns a typed rate-limit error rather than retrying early. 401
and 403 are never blindly retried.
## Market estimates
`EsiMarketEstimates` reads public `GET /markets/prices`. It uses only
`average_price`; a missing average price is omitted and `adjusted_price` is never
a fallback. The returned `PriceSnapshot.source` includes the pinned compatibility
date, `asOf` is the retrieval time (ESI supplies no source timestamp), and prices
are integer ISK cents represented as validated decimal strings. The route is a
cached global reference estimate, not a regional or structure order book.
Metrics supplied through `EsiClient`'s `observe` callback include endpoint, status,
retry/cache state, and redacted rate-limit headers. They do not include access
tokens, authorization keys, or response bodies.
+80
View File
@@ -0,0 +1,80 @@
# This development deployment
## Addresses
Public configuration is in `config/dev.env` (no credentials):
- App: `https://dev.evebot.app` → this server's **HTTP port 5173**.
- Convex browser API/WebSockets: `https://dev-convex.evebot.app` → this server's **HTTP port 3210**.
- Both Pangolin resources are **HTTP resources**. WebSocket upgrades do not require a separate TCP resource. Pangolin terminates public TLS; the selected upstream ports serve plain HTTP.
- Convex must not have an interactive Pangolin authentication gate; its application JWT/session and server-bridge checks authorize access. The main app may retain the additional Pangolin gate.
- The public Convex endpoint must be reachable by the browser. `127.0.0.1` in browser configuration would mean the browser's machine, not this server.
- Host-side deployment tools use `http://127.0.0.1:3210`; the web container uses `http://backend:3210`.
The operator explicitly selected wildcard binding for web/Convex: `0.0.0.0`. Firewall these ports to the intended ingress network/Pangolin connector; do not assume a proxy protects the directly published ports. Port 3211 (Convex HTTP actions, unused here) and the optional dashboard remain loopback-only. If Newt is containerized, a target of `127.0.0.1` normally refers to that container: configure a reachable address for the app server instead. A 302 to Pangolin login is a proxy-authentication problem; a 502 is an upstream-connection/configuration problem.
## Infisical
The non-secret `.infisical.json` pins:
- Instance: `https://secrets.jb.codes`
- Project: `e176b742-85cd-4af6-96a7-f33ab7c2c709`
- Environment: `dev`
- Path: `/`
A read-only Universal Auth identity lives outside the repository in `~/.config/evebot/infisical.env`, owned by the operator with mode `0600`:
```dotenv
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=...
INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=...
```
Do not commit or paste those values into chat. The helper rejects symlinks, non-regular files, files owned by another user, and group/world-accessible credential files. It authenticates only against the approved instance, keeps tokens out of argv, limits CLI output and execution time, suppresses raw errors, disables CLI telemetry, and disables linked-secret imports, expansion, and personal overrides. It injects only allowlisted application secrets and removes the bootstrap identity/token from the child environment. It never writes fetched app secrets to a dotenv file.
```sh
bun install --frozen-lockfile
bun run secrets:status
bun run secrets:run -- bun scripts/check-live-config.ts
```
Expected application keys: `EVE_CLIENT_ID`, optional `EVE_CLIENT_SECRET` for confidential SSO, `TYPESAFE_API_KEY`, `TOKEN_ENCRYPTION_KEY`, `CONVEX_BRIDGE_SECRET`, `APP_AUTH_PRIVATE_KEY`, and `APP_AUTH_JWKS`. Generate the last four **once** using `bun run keys:generate`; import `.env.keys` into the selected Infisical environment without replacing existing API keys. The generator refuses to overwrite an existing file. After verifying the import and key pairing, remove the temporary copy. Back up the vault: changing/loss of the encryption key makes stored EVE credentials unreadable. Do not regenerate keys casually during deployment.
The helper is an operator launcher, not a sandbox. Commands it launches can read injected secrets: never run `env`, `printenv`, shell tracing, or unrestricted `docker compose config` through it. Use `docker compose config --quiet` for validation. Docker administrators can inspect container environments; Infisical startup injection does not conceal secrets from host administrators. Updating a vault value requires recreating the affected container with fresh injected values. Encryption/signing-key rotation needs a separate migration plan.
## Deploy the local backend
Run from the repository root:
```sh
bun run secrets:run -- docker compose up -d backend
bun run secrets:run --deployment -- bun run convex:configure
bun run secrets:run --deployment -- bun node_modules/convex/bin/main.js dev --once --typecheck disable
```
`--deployment` allows the CLI-only Convex admin key. If it is not stored in the vault, the helper derives it from the local backend container's persistent random instance secret. This requires existing Docker-host administrative access and is restricted to the local deployment URL. The key is captured without being displayed or saved to another file. Compose never passes it to the web container. Preserve/back up the `convex-data` volume, including its instance credentials.
`config/dev.env` is authoritative for the public settings, even if the Convex CLI generates a local environment file. Keep all generated `.env*` files out of version control.
## Reference data and web service
Use the production import procedure in [reference operations](reference-operations.md); never seed this deployment with `--fixture`. A successful production artifact must be installed in the Compose reference volume before enabling asset reports. The immutable artifact and manifest stay pinned to a complete validated generation, and failed imports cannot replace it.
```sh
bun run secrets:run -- docker compose up -d --build web
```
The container listens on 3000 internally but publishes **5173** on the host. This is the production SvelteKit/Bun server, not the Vite development server. The web mounts reference data read-only. The one-shot reference worker has the writable volume mount.
## First live verification
1. Check `https://dev-convex.evebot.app/version` returns a backend version, not a proxy login or 502. Verify a real WSS upgrade using the app Origin.
2. Confirm application secret validation and the deployed Convex functions.
3. Optional paid-provider probe (one generic classification; no character or asset data):
```sh
LIVE_API_SMOKE=1 bun run secrets:run -- bun scripts/smoke-jev.ts
```
4. Open the app, pass the optional Pangolin gate, and connect an EVE character. The exact registered EVE callback must be `https://dev.evebot.app/auth/eve/callback`.
5. Explicitly synchronize assets. Inspect completeness/freshness, exclusions, and the reference-price label; add further characters through the explicit linking flow.
6. Compare conventional browsing and chat `/value`. Live EVE SSO, refresh, and real asset synchronization are not verified merely by validating the client ID/secret or testing the JEV key.
Do not run the write-enabled disposable Convex smoke script against this persistent deployment: its synthetic identities belong only in isolated test backends.
+21
View File
@@ -0,0 +1,21 @@
# Milestone 1 contract
User-selected scope: multi-character assets and **reference/estimated valuation** in both assistant and conventional views; first EVE login creates an application user; single-host Docker Compose deployment.
## Acceptance criteria
- Shared deterministic service aggregates only the authenticated application user's connected characters; browser and assistant do not duplicate valuation logic.
- An unauthenticated request returns a typed authorization-required state and a connect action. Original request can resume after connecting/synchronizing. Missing scopes and incomplete/stale synchronization are not silently treated as empty assets.
- EVE OAuth secrets/refresh tokens never enter browser-visible queries, serialization or logs. Multiple-character connection is explicit and cannot silently transfer a character between application users.
- Reference prices carry source and timestamps. Missing/unpriceable items and partial character coverage are explicit. Estimate is not Jita price, a firm sale offer, immediate liquidation, or an assertion of liquidity.
- The initial scaffolding phase consumed only tiny representative fixtures. The subsequently authorized live-data phase adds explicit full-archive adapters and production-profile validation. Normal queries read normalized indexed SQLite, never runtime fixture/raw archive JSON.
- Compiler makes a new artifact, validates it, atomically publishes it and preserves a rollback target. Independent Reference Data/Hoboleaks source state survives individual failures. Invalid imports cannot replace active data.
- Central ESI transport controls compatibility date, pagination, conditional cache, retry/rate limits and redacted observability. Sync does not replace a complete snapshot with partially fetched pages.
- JEV makes typed semantic routing judgments only. Permissions, capabilities, lookups, arithmetic and execution are deterministic code.
- Tests run offline on Bun. Production build is checked. Live credentials and full archive compatibility are separate validation gates and must not be represented as completed if not exercised.
## Explicitly not delivered in milestone 1
Full manufacturing optimization, blueprint/skills synchronization, wallets, market-depth ingestion, order-book-based liquidation, arbitrage, capital route search, definitive structure docking authorization, full general-purpose EVE conversational answering, and a production-certified full-archive ingestion pipeline.
Interfaces and domain models should make these incremental additions possible, not pretend they already work.
+67
View File
@@ -0,0 +1,67 @@
# Reference data operations
The reference compiler builds immutable SQLite artifacts from checked-in fixtures or explicitly supplied source adapters. It does not download an archive by default.
## Fixture build
```sh
REFERENCE_DIR=data/reference bun workers/reference.ts --fixture
# or
bun run reference:fixture
```
## Production import
Production is explicit and uses the pinned public EVE Ref Reference Data, Hoboleaks, and CCP JSONL SDE releases. `--check` performs only independent HTTP metadata checks (no archive bytes); `--production` streams each archive to a bounded staging file, hashes/counts transport bytes, validates archive members, and publishes only after cross-source SQLite validation.
```sh
bun workers/reference.ts --check --production
bun workers/reference.ts --production
# Re-run an already downloaded, offline import:
bun workers/reference.ts --production --offline --archive-dir data/reference-live
```
The default production archive and artifact directory is `data/reference-live` (ignored by git); production publishes `data/reference-live/manifest.json`, while fixture builds remain under `data/reference`. An explicit `--directory` wins over `REFERENCE_DIR`, which wins over these defaults. `REFERENCE_LIVE_DIR` controls archive storage only. Compose explicitly publishes to `/app/data/reference`, the shared worker/web volume; the production web reader rejects fixture-profile artifacts. Runtime images must provide the `xz` executable (Debian package `xz-utils`) for streaming `.tar.xz` decompression; ZIP deflate uses the Bun runtime. No npm archive dependency is required. Production validation requires more than 5,000 systems and currently imports CCP's `mapSolarSystems`, `mapConstellations`, `mapRegions`, and `mapStargates` JSONL members.
`fixtures/reference/` contains small schema-faithful EVE Ref keyed records for categories, groups, types, and blueprints (including types 34 Tritanium, 35 Pyerite, 587 Rifter, and blueprint 687). The Rifter recipe is synthetic and is labelled as such. Hoboleaks `industryactivities`, `repackagedvolumes`, and `meta` are independent raw maps. `systems.json` is explicitly an ESI-universe fixture: it preserves the official system fields and XYZ coordinate object and is not presented as a Reference Data `/systems` record.
The fixture profile is intentionally small. It uses a one-system threshold and required categories 4, 6, and 9. Those thresholds are never silently used for production: production validation requires more than 5,000 systems, categories 4/6/9, and at least one blueprint. On the inspected 2026-09-13/2026-09-10 releases, the production candidate contained 48 categories, 1,610 groups, 53,001 types, 5,082 blueprints, 8,490 systems, 1,184 constellations, 114 regions, and 13,978 gates. Twenty-one official blueprint material/product IDs are absent from the current Ref type map; those rows are preserved and explicitly listed in `unresolved_references` rather than dropped or fabricated.
## Active artifact and read handles
`manifest.json` is the sole publication pointer. It contains `active` and `previous`, source provenance, and the last attempt. Database files are immutable under `databases/`; source caches are immutable, content-addressed/versioned files under `sources/<source>/<fingerprint>-<importer-key>.json`. Downloaded archives are content-addressed under `archives/<source>/<sha256>.archive` and bound by `.production-archive-receipts.json`; offline replay verifies the receipt, byte count, and SHA-256 before parsing. Each active artifact records the exact source cache path and local payload hash in `sourceInputs`, so rollback selects the old inputs rather than a mutable latest cache. Cache format 2 binds canonical hashes and source-check provenance to the active artifact before reuse or fallback. When a same-identity immutable cache is reused, its verified metadata is adopted before publication, while the latest metadata-check observation remains separate. The manifest stores archive HTTP ETags as opaque validators, not SHA-256 values. `localSha256`/`archiveByteLength` describe the downloaded transport artifact; `canonicalSha256` describes the canonical JSON serialization of the source-cache payload. The local archive hash is computed locally and is not published authenticity evidence.
```ts
import { SqliteReferenceRepository } from '$lib/server/reference';
const repository = new SqliteReferenceRepository(process.env.REFERENCE_DIR ?? 'data/reference');
try {
const [types, version] = await Promise.all([
repository.types([34, 35, 587, 999999]),
repository.version(),
]);
// types contains the three fixture records; unknown IDs are omitted.
} finally {
repository.close();
}
```
Construction snapshots the manifest and opens that database read-only. It performs only a cheap schema/header check; full SQLite integrity and foreign-key scans belong to the compiler and are not repeated on every request. It therefore keeps `types()` and `version()` consistent if another process promotes a build. Type lookups chunk large ID lists into bounded `IN` queries. Create one repository (or `repository.pin()`/`snapshot()`) per request and call `close()` in `finally`; do not retain a closed handle. A new constructor sees the next active artifact.
## Promotion lifecycle
A build acquires `.reference-build.lock`, checks each source independently, fingerprints it, and loads only changed sources. Unchanged sources come from their last successful cache. Inputs are schema-validated and normalized before a fresh staging SQLite file is created. The candidate gets indexes, foreign-key checks, `PRAGMA integrity_check`, cross-source joins, smoke queries, and profile thresholds. The database is fsynced, renamed into `databases/`, then one fsynced manifest rename publishes `active` and retains `previous`.
An invalid candidate never replaces ACTIVE or overwrites a successful source cache. A source-level failure is recorded independently: if that source's last committed cache verifies, another changed source may advance in a newly validated candidate against that fallback; the failed source remains visibly `failed`/`reused` and its active fingerprint stays pinned. Failures are recorded independently in `manifest.sources` and `lastAttempt`; successful-import timestamps are updated only after candidate validation and manifest promotion. A stale Hoboleaks file is visible and rejected by the promotion policy. A failed source does not get hidden by a successful other source.
The lock prevents concurrent builds. A crash can leave a lock, staging directory, or a fully validated database whose manifest rename did not happen; after confirming no compiler is running, remove the lock, then the next build removes `.staging-*` directories and may recover an existing artifact only after rechecking integrity, schema, source provenance hashes, and canonical counts. It never overwrites an existing database. Completed immutable database files are safe to retain. Manifest replacement is a same-filesystem rename with directory fsync; an interrupted build therefore leaves the old manifest active.
## Rollback and checks
```sh
REFERENCE_DIR=data/reference bun workers/reference.ts --rollback
REFERENCE_DIR=data/reference bun workers/reference.ts --check --fixture
bun workers/reference.ts --help
```
`--check` performs source metadata checks only and never archive fetches. With `--fixture` it checks the local fixture adapters; without `--fixture` it performs independent HEAD requests to the official Reference Data, Hoboleaks, and CCP SDE release URLs and reports metadata fingerprints with the top-level `archiveFetch: false` marker. That metadata fingerprint is not a published archive checksum. A default invocation prints help. Production archive provenance is recorded independently for each source: resolved URL, HTTP validators, transport byte count, locally computed archive SHA-256, source build/revision, and canonical payload/cache SHA-256. Offline mode is fail-closed and never falls back to HTTP. TAR headers, XZ memory limits, two-block TAR end markers, ZIP advertised sizes, and CRC-32 values are validated before promotion. Selected Hoboleaks files must match their published MD5, carry a non-future per-file revision (older revisions are valid when the file is unchanged), and be explicitly non-deprecated. An ETag is never treated as a checksum. Archive payloads are content-addressed in immutable source caches; `manifest.json` remains the sole publication pointer. Hoboleaks stale metadata for the selected imported files rejects promotion, while unrelated stale/deprecated files remain visible in the source archive metadata and do not get silently imported.
+18
View File
@@ -0,0 +1,18 @@
HTTP/2 200
date: Fri, 18 Sep 2026 19:44:13 GMT
content-type: application/json
access-control-allow-origin: *
access-control-expose-headers: Etag, Retry-After, X-Compatibility-Date, X-Esi-Error-Limit-Remain, X-Esi-Error-Limit-Reset, X-Pages, X-Ratelimit-Group, X-Ratelimit-Limit, X-Ratelimit-Remaining, X-Ratelimit-Used
cache-control: public, max-age=600
content-language: en
expires: Fri, 18 Sep 2026 19:54:13 GMT
strict-transport-security: max-age=31536000
vary: X-Tenant
vary: Accept-Language
vary: X-Compatibility-Date
x-compatibility-date: 2025-09-26
x-ratelimit-group: meta
x-ratelimit-limit: 150/15m
x-ratelimit-remaining: 130
x-ratelimit-used: 2
File diff suppressed because one or more lines are too long
+19
View File
@@ -0,0 +1,19 @@
HTTP/2 200
date: Fri, 18 Sep 2026 19:44:12 GMT
content-type: application/json
content-length: 208
access-control-allow-origin: *
access-control-expose-headers: Etag, Retry-After, X-Compatibility-Date, X-Esi-Error-Limit-Remain, X-Esi-Error-Limit-Reset, X-Pages, X-Ratelimit-Group, X-Ratelimit-Limit, X-Ratelimit-Remaining, X-Ratelimit-Used
cache-control: public, max-age=600
content-language: en
expires: Fri, 18 Sep 2026 19:54:12 GMT
strict-transport-security: max-age=31536000
vary: X-Tenant
vary: Accept-Language
vary: X-Compatibility-Date
x-compatibility-date: 2020-01-01
x-ratelimit-group: meta
x-ratelimit-limit: 150/15m
x-ratelimit-remaining: 138
x-ratelimit-used: 2
+1
View File
@@ -0,0 +1 @@
{"compatibility_dates":["2026-08-18","2026-08-04","2026-07-21","2026-07-17","2026-06-09","2026-05-19","2025-12-16","2025-11-06","2025-09-30","2025-09-26","2025-08-26","2025-04-02","2025-04-01","2020-01-01"]}
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
HTTP/2 200
date: Fri, 18 Sep 2026 19:44:14 GMT
content-type: application/openapi+json
access-control-allow-origin: *
access-control-expose-headers: Etag, Retry-After, X-Compatibility-Date, X-Esi-Error-Limit-Remain, X-Esi-Error-Limit-Reset, X-Pages, X-Ratelimit-Group, X-Ratelimit-Limit, X-Ratelimit-Remaining, X-Ratelimit-Used
cache-control: public, max-age=600
content-language: en
strict-transport-security: max-age=31536000
vary: X-Tenant
vary: Accept-Language
vary: X-Compatibility-Date
x-compatibility-date: 2026-08-18
x-ratelimit-group: meta
x-ratelimit-limit: 150/15m
x-ratelimit-remaining: 128
x-ratelimit-used: 2
+298
View File
@@ -0,0 +1,298 @@
# EVE ESI/SSO research brief
**Checked:** 2026-09-18 (UTC)
**Scope:** current official ESI/SSO documentation and live ESI OpenAPI/metadata only. No SDE archive was downloaded. The captured OpenAPI evidence is [`eve-openapi-evidence.json`](./eve-openapi-evidence.json); live compatibility/changelog responses are [`eve-compatibility.json`](./eve-compatibility.json) and [`eve-changelog.json`](./eve-changelog.json).
## Executive contract
- Use `https://esi.evetech.net` and send `X-Compatibility-Date: 2026-08-18`. This is the newest value in the live compatibility-date list on this date; `2026-09-18` is not yet a supported value. Pinning the header avoids silently receiving the oldest behavior. [ESI versioning](https://developers.eveonline.com/docs/services/esi/overview/#versioning) · [live compatibility dates](https://esi.evetech.net/meta/compatibility-dates)
- Character-scoped ESI tokens are character-specific. A local EVE app user may link multiple characters, but each character needs its own authorization/token record and the token's granted scopes apply only to that character. [SSO](https://developers.eveonline.com/docs/services/sso/#how-it-works)
- The asset route is paged (`page`, 1-based; `X-Pages`) and has a 3,600-second server/client cache. Market reference prices are one unpaged global array, also cached 3,600 seconds. [Current OpenAPI](https://esi.evetech.net/meta/openapi.json)
- An Access List membership is **not** proof of docking: the current Access List routes return list membership but no structure/profile association. A successful authenticated `GET /universe/structures/{structure_id}` is character-scoped ACL/access evidence, not an unconditional docking guarantee; a `structure_id` in `GET /characters/{character_id}/location` is evidence the character was docked there at the cached observation time. [Resolve Structure IDs](https://docs.esi.evetech.net/docs/scenarios/resolve_structure_ids.html) · [Current OpenAPI](https://esi.evetech.net/meta/openapi.json)
## ESI versioning and current OpenAPI
The live API exposes OpenAPI 3.1 at [`/meta/openapi.json`](https://esi.evetech.net/meta/openapi.json). It was fetched with `X-Compatibility-Date: 2026-08-18`; the response's `X-Compatibility-Date` and `info.version` were both `2026-08-18`. The live [`/meta/compatibility-dates`](https://esi.evetech.net/meta/compatibility-dates) response lists, newest first:
```text
2026-08-18, 2026-08-04, 2026-07-21, 2026-07-17, 2026-06-09,
2026-05-19, 2025-12-16, 2025-11-06, 2025-09-30, 2025-09-26,
2025-08-26, 2025-04-02, 2025-04-01, 2020-01-01
```
ESI documents `X-Compatibility-Date: YYYY-MM-DD` as the reviewed API behavior date; `compatibility_date` is the fallback for clients unable to set headers. It also says the API changes date at 11:00 UTC and a date cannot be future or below the oldest available. The live endpoint is the authoritative check for the value to pin.
The 2026-05-19 changelog release added Access Lists, Mercenary Dens/MTOs, Skyhooks, Sovereignty Hubs, raidable Skyhooks, and `/sovereignty/systems`; it removed `/sovereignty/map` and `/sovereignty/structures` at that compatibility date. [Live changelog](https://esi.evetech.net/meta/changelog) · [Equinox ESI announcement](https://developers.eveonline.com/blog/equinox-on-esi-structures-sovereignty-and-access-lists)
## Assets: exact current contract
### Character assets
```http
GET https://esi.evetech.net/characters/{character_id}/assets?page={page}
Authorization: Bearer <character-token>
X-Compatibility-Date: 2026-08-18
```
- Scope: `esi-assets.read_assets.v1`.
- `page` is optional, integer, minimum `1`; response header `X-Pages` is the total number of pages. Fetch page 1, read `X-Pages`, then pages 2..N. ESI's X-Pages guidance warns that cache expiry between pages can duplicate items; de-duplicate by `item_id` (and consider a stable page-fetch strategy). [X-Pages pagination](https://developers.eveonline.com/docs/services/esi/pagination/x-pages/)
- Cache: `x-cache-age: 3600`, `x-client-cache-ttl: 3600`, `x-server-cache-mode: ttl-based`, `x-server-cache-ttl: 3600`; response supports `Cache-Control`, `ETag`, `Last-Modified`, and requests support `If-None-Match` / `If-Modified-Since`.
- Rate extension: group `char-asset`, `1800` tokens per `15m` (where the new limiter is active).
- Response is an array. Each item has this exact shape:
| Field | Type | Required | Contract |
|---|---|---:|---|
| `is_blueprint_copy` | boolean | no | Whether the asset is a blueprint copy |
| `is_singleton` | boolean | yes | Singleton flag |
| `item_id` | int64 | yes | Asset item ID |
| `location_flag` | enum string | yes | Exact character enum below |
| `location_id` | int64 | yes | Parent/container/location ID |
| `location_type` | enum string | yes | `station`, `solar_system`, `item`, `other` |
| `quantity` | int64 | yes | Quantity |
| `type_id` | int64 | yes | EVE type ID; no type name or capital classification is included |
Character `location_flag` enum (exact OpenAPI values):
```text
AssetSafety, AutoFit, BoosterBay, CapsuleerDeliveries, Cargo,
CorporationGoalDeliveries, CorpseBay, Deliveries, DroneBay, ExpeditionHold,
FighterBay, FighterTube0, FighterTube1, FighterTube2, FighterTube3, FighterTube4,
FleetHangar, FrigateEscapeBay, Hangar, HangarAll, HiSlot0, HiSlot1, HiSlot2,
HiSlot3, HiSlot4, HiSlot5, HiSlot6, HiSlot7, HiddenModifiers, Implant,
InfrastructureHangar, LoSlot0, LoSlot1, LoSlot2, LoSlot3, LoSlot4, LoSlot5,
LoSlot6, LoSlot7, Locked, MedSlot0, MedSlot1, MedSlot2, MedSlot3, MedSlot4,
MedSlot5, MedSlot6, MedSlot7, MobileDepotHold, MoonMaterialBay, QuafeBay,
RigSlot0, RigSlot1, RigSlot2, RigSlot3, RigSlot4, RigSlot5, RigSlot6, RigSlot7,
ShipHangar, Skill, SpecializedAmmoHold, SpecializedAsteroidHold,
SpecializedCommandCenterHold, SpecializedFuelBay, SpecializedGasHold,
SpecializedIceHold, SpecializedIndustrialShipHold, SpecializedLargeShipHold,
SpecializedMaterialBay, SpecializedMediumShipHold, SpecializedMineralHold,
SpecializedOreHold, SpecializedPlanetaryCommoditiesHold, SpecializedSalvageHold,
SpecializedShipHold, SpecializedSmallShipHold, StructureDeedBay, SubSystemBay,
SubSystemSlot0, SubSystemSlot1, SubSystemSlot2, SubSystemSlot3, SubSystemSlot4,
SubSystemSlot5, SubSystemSlot6, SubSystemSlot7, Unlocked, Wardrobe
```
### Corporation assets
```http
GET https://esi.evetech.net/corporations/{corporation_id}/assets?page={page}
```
- Scope: `esi-assets.read_corporation_assets.v1`; current OpenAPI declares required role `Director`.
- Same page/cache/header contract, with rate group `corp-asset`, `1800/15m`.
- Same fields and requiredness as character assets. The exact corporation `location_flag` enum is:
```text
AssetSafety, AutoFit, Bonus, Booster, BoosterBay, Capsule, CapsuleerDeliveries,
Cargo, CorpDeliveries, CorpSAG1, CorpSAG2, CorpSAG3, CorpSAG4, CorpSAG5,
CorpSAG6, CorpSAG7, CorporationGoalDeliveries, CrateLoot, Deliveries, DroneBay,
DustBattle, DustDatabank, ExpeditionHold, FighterBay, FighterTube0, FighterTube1,
FighterTube2, FighterTube3, FighterTube4, FleetHangar, FrigateEscapeBay, Hangar,
HangarAll, HiSlot0, HiSlot1, HiSlot2, HiSlot3, HiSlot4, HiSlot5, HiSlot6, HiSlot7,
HiddenModifiers, Implant, Impounded, InfrastructureHangar, JunkyardReprocessed,
JunkyardTrashed, LoSlot0, LoSlot1, LoSlot2, LoSlot3, LoSlot4, LoSlot5, LoSlot6,
LoSlot7, Locked, MedSlot0, MedSlot1, MedSlot2, MedSlot3, MedSlot4, MedSlot5,
MedSlot6, MedSlot7, MobileDepotHold, MoonMaterialBay, OfficeFolder, Pilot,
PlanetSurface, QuafeBay, QuantumCoreRoom, Reward, RigSlot0, RigSlot1, RigSlot2,
RigSlot3, RigSlot4, RigSlot5, RigSlot6, RigSlot7, SecondaryStorage,
ServiceSlot0, ServiceSlot1, ServiceSlot2, ServiceSlot3, ServiceSlot4, ServiceSlot5,
ServiceSlot6, ServiceSlot7, ShipHangar, ShipOffline, Skill, SkillInTraining,
SpecializedAmmoHold, SpecializedAsteroidHold, SpecializedCommandCenterHold,
SpecializedFuelBay, SpecializedGasHold, SpecializedIceHold,
SpecializedIndustrialShipHold, SpecializedLargeShipHold, SpecializedMaterialBay,
SpecializedMediumShipHold, SpecializedMineralHold, SpecializedOreHold,
SpecializedPlanetaryCommoditiesHold, SpecializedSalvageHold, SpecializedShipHold,
SpecializedSmallShipHold, StructureActive, StructureFuel, StructureInactive,
StructureOffline, SubSystemBay, SubSystemSlot0, SubSystemSlot1, SubSystemSlot2,
SubSystemSlot3, SubSystemSlot4, SubSystemSlot5, SubSystemSlot6, SubSystemSlot7,
Unlocked, Wallet, Wardrobe
```
### Asset helper routes
The same asset scope/rate group applies to the character helper routes:
- `POST /characters/{character_id}/assets/locations` accepts a required JSON array of **1..1000 unique int64 `item_id`s** and returns `{item_id, position:{x,y,z}}`; coordinates for items in hangars/stations are `(0,0,0)`.
- `POST /characters/{character_id}/assets/names` accepts the same 1..1000 unique-ID array and returns `{item_id, name}` for custom-namable items.
- Corporation equivalents use `esi-assets.read_corporation_assets.v1`, require the current OpenAPI's `Director` role, and use the `corp-asset` group.
- These POST responses expose the same cache validators but have no page parameter. [Current OpenAPI evidence](./eve-openapi-evidence.json)
## Reference-price valuation
```http
GET https://esi.evetech.net/markets/prices
X-Compatibility-Date: 2026-08-18
```
- Public; no OAuth scope and no query parameters.
- Response is an unpaged array. Item schema is exactly `{type_id:int64}` plus optional `adjusted_price:number(double)` and optional `average_price:number(double)`; only `type_id` is required by OpenAPI. Do not assume either price field exists for every type.
- Cache: `x-cache-age: 3600`, client/server TTL 3600, server mode `ttl-based`; response advertises `Cache-Control`, `ETag`, and `Last-Modified`, and accepts `If-None-Match` / `If-Modified-Since`. The live response is global rather than region/structure-specific.
- The endpoint does not provide a price timestamp, region, order depth, or valuation semantics. Store fetch time and compatibility date with a valuation snapshot. Choose and document a missing-price policy; the ESI schema does **not** authorize an automatic `adjusted_price` fallback for a missing `average_price`.
- For each asset item with a known price, a transparent reference valuation is `quantity × selected price`; blueprint-copy, fitted/container, and location semantics remain application policy.
For actual structure-market orders, use `GET /markets/structures/{structure_id}?page=N` with `esi-markets.structure_markets.v1`; it is paged by `X-Pages`, cached for 300 seconds, and is not the same data as global reference prices.
## Rate limiting and cache headers
ESI currently documents two mutually exclusive limiter header families:
**New floating-window/token limiter** (only on routes that advertise `x-rate-limit` in OpenAPI):
- `X-Ratelimit-Group`: route bucket, e.g. `char-asset` or `meta`.
- `X-Ratelimit-Limit`: total tokens/window, e.g. `150/15m`.
- `X-Ratelimit-Remaining`: available tokens.
- `X-Ratelimit-Used`: tokens consumed by the request.
- `Retry-After` on `429`, in seconds.
The documented token costs are 2 for 2xx, 1 for 3xx, 5 for 4xx (excluding 429), and 0 for 5xx. Buckets are per rate-limit group and user ID; authenticated user ID is `<applicationID>:<characterID>`, not a global app-only bucket. [Rate limiting](https://developers.eveonline.com/docs/services/esi/rate-limiting/)
**Legacy error limiter** (still active for routes without the new limiter):
- `X-ESI-Error-Limit-Remain`: errors left in the current fixed window.
- `X-ESI-Error-Limit-Reset`: seconds until that window resets.
- It allows at most 100 non-2xx/3xx responses per minute; once reached, ESI returns 420s on routes. [Best practices](https://developers.eveonline.com/docs/services/esi/best-practices/#error-limit)
A live `GET /markets/prices` response on the check date exposed the legacy `X-ESI-*` headers (and ETag/Last-Modified); a live `GET /meta/openapi.json` response with the pinned date exposed the new `X-Ratelimit-*` family (`meta`, `150/15m`). Header values such as remaining/reset are volatile. The ESI `Access-Control-Expose-Headers` response also lists both families, `Retry-After`, `X-Compatibility-Date`, and `X-Pages`.
Use ETag validators first, respect endpoint TTLs, and do not poll at the limiter edge. A 304 is the expected cache-saving path where validators are honored.
## SSO: web login, PKCE, JWT, and refresh
### Discovery and endpoints
Fetch and cache the OAuth metadata document at [`https://login.eveonline.com/.well-known/oauth-authorization-server`](https://login.eveonline.com/.well-known/oauth-authorization-server). On the check date it returned:
```json
{
"issuer": "https://login.eveonline.com",
"authorization_endpoint": "https://login.eveonline.com/v2/oauth/authorize",
"token_endpoint": "https://login.eveonline.com/v2/oauth/token",
"userinfo_endpoint": "https://login.eveonline.com/v2/oauth/verify",
"jwks_uri": "https://login.eveonline.com/oauth/jwks",
"revocation_endpoint": "https://login.eveonline.com/v2/oauth/revoke",
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "client_secret_jwt"]
}
```
Use metadata URLs rather than hard-coding them; the current SSO page explicitly recommends this. The current official deprecation notice says the old `oauth/*` endpoints are obsolete: use `v2/oauth/authorize` and `v2/oauth/token`, send a form-encoded body, and do not send JSON or token parameters in the query string. [SSO docs](https://developers.eveonline.com/docs/services/sso/) · [SSO endpoint deprecations](https://developers.eveonline.com/blog/sso-endpoint-deprecations-2)
### Authorization Code (server/web)
1. Register the app and exact callback URL in the EVE Developers Portal. Generate a random `state` and retain it in the login session.
2. Redirect to `authorization_endpoint` with URL-encoded parameters:
```text
response_type=code
client_id=<client id>
redirect_uri=<registered exact URL>
scope=<space-separated requested scopes>
state=<random CSRF value>
```
3. Verify callback `state`, then POST form-urlencoded to `token_endpoint` with `grant_type=authorization_code`, `code=<one-time-code>`, and the registered `redirect_uri`, using HTTP Basic credentials `client_id:client_secret`. The current strict-conformance notice shows `redirect_uri` in the token body; the current SSO page's simplified Python example omits it, so send the registered value and test against the registered app.
4. The response contains an access token and refresh token. Keep the refresh token server-side; access tokens are time-limited and character/scope-bound.
### Authorization Code + PKCE (native/public client)
- Generate a high-entropy `code_verifier`; send `code_challenge = base64url(SHA-256(code_verifier))` without `=` padding, with `code_challenge_method=S256`.
- Add `code_challenge` and `code_challenge_method=S256` to the authorization query above. Do not use a client secret in this flow.
- Exchange the callback code with a form-urlencoded POST body containing `grant_type=authorization_code`, `code`, `client_id`, `code_verifier`, and the registered `redirect_uri`. Retain the verifier until exchange; verify `state` exactly as in the web flow. The strict-conformance notice is the reason to include `redirect_uri`; its newer SSO-page sample omits that field.
The current ESI OpenAPI OAuth2 security scheme also declares the same `v2` authorize/token URLs and all ESI scopes. For this app's chosen data, the primary scopes are `esi-assets.read_assets.v1`; add `esi-location.read_location.v1` for current docking, `esi-location.read_ship_type.v1` for current ship/capital type ID, and `esi-universe.read_structures.v1` for authenticated structure resolution. [Current OpenAPI](https://esi.evetech.net/meta/openapi.json)
### Refresh
The retained official refresh-token page documents the exact current v2 request shape: web apps POST form-urlencoded `grant_type=refresh_token&refresh_token=<token>` with Basic `client_id:secret`; native/public apps send `grant_type=refresh_token`, `refresh_token`, and `client_id` without Basic. Optional `scope` may narrow to a subset of the original grant. URL-encode the token. The response shape is:
```json
{
"access_token": "<jwt>",
"token_type": "Bearer",
"expires_in": 1200,
"refresh_token": "<possibly-new-token>"
}
```
The current SSO page describes refresh tokens as long-lived while access is not revoked, and the official deprecation notice says refresh-token rotation may change the returned token: always replace the stored refresh token with the returned one. [Official refresh contract (retained page)](https://docs.esi.evetech.net/docs/sso/refreshing_access_tokens.html) · [Rotation notice](https://developers.eveonline.com/blog/sso-endpoint-deprecations-2)
### JWT verification and local user/character identity
The access token is a JWT. Fetch `jwks_uri` from metadata, select the key by JWT header `kid`/algorithm, verify the signature with a standard JWT library, validate expiration, issuer, and audience, and only then trust claims. The current SSO docs specify:
- `iss` should be the EVE issuer (`https://login.eveonline.com/` or, in some cases, `login.eveonline.com`; use metadata and accept only the documented EVE forms).
- `aud` is an array containing the app `client_id` and static `EVE Online`; check both.
- `sub` is `CHARACTER:EVE:<character-id>`; `name` is the character name; `scp` enumerates granted scopes. The official guide describes an array, but singleton scope claims can also be scalar strings. Normalize one valid scope string into a one-element array only after JWT verification; do not infer permissions from missing or malformed claims.
Runtime compatibility follow-up (2026-09-19): a live callback reached verified token processing but failed the original array-only scope parser. The maintained [`eveauth` implementation](https://github.com/ErikKalkoken/eveauth/blob/main/jose.go) explicitly handles both scalar strings and arrays in `extractScopes`, with a [singleton regression test](https://github.com/ErikKalkoken/eveauth/blob/main/jose_test.go). Our signed-token regression reproduced the scalar failure and now verifies both login and refresh normalization. Invalid/missing claims and whitespace-delimited ambiguity remain rejected; all signature, issuer, audience, owner/session and exact scope-membership checks remain in force.
The current JWKS observed `RS256`, but the official deprecation notice warns signature methods can change; do not hard-code the observed key/algorithm. A local app user should be an application identity with a child record per `character_id`, token, refresh token, scopes, and last refresh. SSO selects one character per authorization, so linking another character is another flow. [JWT guidance](https://developers.eveonline.com/docs/services/sso/#validating-jwt-tokens)
## Upwell structures, Access Lists, and docking evidence
### General structure routes
| Route | Auth/scope | Current contract and cache |
|---|---|---|
| `GET /universe/structures?filter=market\|manufacturing_basic` | Public | Unique array of public structure int64 IDs; `filter` only selects structures with that service online; TTL 3600. |
| `GET /universe/structures/{structure_id}` | `esi-universe.read_structures.v1` | 200 returns `name`, `solar_system_id`, `owner_id`, optional `type_id`/position; default error includes 403 when not on ACL; TTL 3600. |
| `GET /corporations/{corporation_id}/structures?page=N` | `esi-corporations.read_structures.v1`; current OpenAPI role `Station_Manager` | Paged array with structure ID/type/corporation/system/profile, name, services, state, fuel/reinforcement fields; `X-Pages`; TTL 3600; `corp-structure` 300/15m. |
| `GET /markets/structures/{structure_id}?page=N` | `esi-markets.structure_markets.v1` | Paged order array (`X-Pages`); TTL 300; no claim that market visibility means docking access. |
The current OpenAPI explicitly describes `/universe/structures/{id}` as returning information only if the caller is “on the ACL”, otherwise “Forbidden”. The retained official structure scenario is more explicit: `/universe/structures` lists only **fully public** structures (an ACL that prevents even one character from docking excludes it), while the authenticated ID route requires a token for a character with docking access. Treat the scenario as corroborating operational guidance because its page warns that it is no longer updated; the pinned OpenAPI is the current field/scope source.
### 2026-05-19 routes
These routes are present in the current (2026-08-18) OpenAPI and have route compatibility date `2026-05-19`:
| Route | Scope | Shape/cache/limit |
|---|---|---|
| `GET /characters/{character_id}/access-lists` | `esi-access.read_lists.v1` | `{access_lists:[{id:int64}]}`; character must manage/admin an ACL; event cache 300; `char-access` 600/15m. |
| `GET /characters/{character_id}/access-lists/{access_list_id}` | same | `{id,name,description,membership}`; event cache 300; same limiter. |
| `GET /characters/{character_id}/structures/mercenary-dens` and `/{mercenary_den_id}` | `esi-structures.read_character.v1` | Character structure data; event cache 3600; `char-structure` 30/15m. |
| `GET /characters/{character_id}/mercenary-tactical-operations` and `/{operation_id}` | `esi-activities.read_character.v1` | MTO data; event cache 300; `char-activity` 150/15m. |
| `GET /corporations/{corporation_id}/structures/skyhooks` and `/{skyhook_id}` | `esi-structures.read_corporation.v1` | Listing wrapper `{skyhooks:[...]}`; detail includes id/planet/state/activity/reagents/timers; event cache 3600; `corp-structure` 300/15m; current OpenAPI says `Station_Manager`. |
| `GET /corporations/{corporation_id}/structures/sovereignty-hubs` and `/{sovereignty_hub_id}` | same | Listing wrapper `{sovereignty_hubs:[...]}`; detail includes id/system/upgrades/reagent bay/resources/transport; event cache 3600; same limiter; detail declares `Station_Manager`. |
| `GET /skyhooks/raidable` | Public | Upcoming/current raidable Skyhooks; TTL cache 300/client, server TTL 60; `activity` 30/15m. |
| `GET /sovereignty/systems` | Public | Combined K-space sovereignty details; TTL 300; `sovereignty` 600/15m. |
### What proves what
1. **Public visibility/discovery only:** an ID in `/universe/structures` proves the structure is in ESI's fully-public list (and, with `filter`, that the selected service is online). It does not enumerate private/ACL structures and does not prove this particular character can dock.
2. **Character-scoped ACL/access evidence:** a successful 200 from `/universe/structures/{structure_id}` using that character's token is the direct ESI structure-access check; a non-authorized caller receives the default error/403. Current OpenAPI says “on the ACL”; the older scenario interprets this as docking access. Preserve that evidence with its timestamp and confidence, but do not promote metadata visibility into an unconditional `canDock` boolean. Ship class, structure state, subsequent ACL changes and cache age still matter, especially for capitals.
3. **Observed docking:** `/characters/{character_id}/location` requires `esi-location.read_location.v1`, is event-based with a 5-second cache, and returns required `solar_system_id` plus optional `station_id` or `structure_id`. A matching `structure_id` is evidence of docking at the observation time, not guaranteed present/future docking permission for a different ship. A mere ACL membership or a 200 on structure metadata is not a docking observation.
4. **ACL membership:** the Access List detail route returns `allow_everyone` plus character/corporation/alliance entries whose access is `Unspecified`, `Allowed`, `Blocked`, `Manager`, or `Admin`. It does not return `structure_id`, structure profile, docking service, or a computed per-character permission. Therefore membership alone cannot prove docking.
5. **Corporation ownership/management:** the corporation-structures route proves the corporation's structures and exposes `profile_id`/operational state to an appropriately authorized corporation character. It does not prove that an arbitrary linked character can dock.
## Capital/intel-specific limits
- `GET /characters/{character_id}/ship` (`esi-location.read_ship_type.v1`) returns only `ship_type_id`, `ship_item_id`, and `ship_name`; it is 5-second TTL-cached and uses `char-location` 1200/15m. Assets likewise expose `type_id`, not a `capital` boolean.
- Capital/supercapital classification therefore requires joining `ship_type_id`/asset `type_id` to official static data; ESI itself does not provide that classification. The official static-data page documents SDE as the static game data source and JSON Lines/YAML formats; no archive was downloaded for this research. [Static Data](https://developers.eveonline.com/docs/services/static-data/)
- A ship `type_id` plus a market price is not proof of current location or docking: use the location route for current presence and structure metadata/ACL check for access.
## Confidence, blockers, and implementation handoff
**High confidence:** current asset/market schemas, scopes, page headers, cache extensions, 2026-08-18 supported compatibility value, 2026-05-19 route additions, SSO endpoints/PKCE parameters, JWT claims, and live rate-header names; all were taken from the pinned OpenAPI, current EVE docs, or live official metadata.
**Medium confidence:** translating a successful structure metadata request into “docking access” relies on the retained official scenario page; current OpenAPI says “on the ACL” and 403 otherwise, but does not name the ACL permission as `docking`.
**Blockers/decisions:**
- There is no current ESI route that joins an Access List to a structure profile or computes a character's docking permission. Do not infer that relationship from Access List membership.
- Asset pages can change while being fetched; use validators/TTL and de-duplicate by `item_id`. Asset `location_id` and `type_id` need separate name/static-data joins.
- `/markets/prices` is a global cached reference snapshot with optional fields, not a region/structure order-book valuation. Define missing-price, blueprint, fitted-ship, and stale-price policy before showing totals.
- Refresh tokens can rotate. Persist the replacement returned by every successful refresh, and store credentials per linked character.
- The current SSO page's simplified token examples omit `redirect_uri` at exchange, while the official strict-conformance notice includes the registered `redirect_uri`; use the latter and verify in a registered test app before launch.
## Sources kept
- [ESI overview/versioning](https://developers.eveonline.com/docs/services/esi/overview/) — compatibility-date and API contract rules.
- [Current ESI OpenAPI](https://esi.evetech.net/meta/openapi.json) — exact route, schema, scope, cache, pagination, and limiter extensions; captured with `X-Compatibility-Date: 2026-08-18`.
- [Compatibility dates](https://esi.evetech.net/meta/compatibility-dates) and [changelog](https://esi.evetech.net/meta/changelog) — live supported-date and 2026 route-release evidence.
- [ESI rate limiting](https://developers.eveonline.com/docs/services/esi/rate-limiting/) and [best practices/error limit](https://developers.eveonline.com/docs/services/esi/best-practices/) — new and legacy headers/costs.
- [X-Pages pagination](https://developers.eveonline.com/docs/services/esi/pagination/x-pages/) — page and cache-expiry behavior.
- [Current SSO docs](https://developers.eveonline.com/docs/services/sso/), [OAuth metadata](https://login.eveonline.com/.well-known/oauth-authorization-server), and [SSO deprecation/rotation notice](https://developers.eveonline.com/blog/sso-endpoint-deprecations-2) — auth code, PKCE, JWT, v2 endpoints, and refresh rotation.
- [Equinox ESI structures/access lists](https://developers.eveonline.com/blog/equinox-on-esi-structures-sovereignty-and-access-lists) — current 2026 structure/access-list release context.
- [Retained structure scenario](https://docs.esi.evetech.net/docs/scenarios/resolve_structure_ids.html) and [retained refresh contract](https://docs.esi.evetech.net/docs/sso/refreshing_access_tokens.html) — exact docking interpretation and refresh body; both pages warn that the old docs site is no longer updated, so current OpenAPI/current SSO docs take precedence.
- [Official static-data docs](https://developers.eveonline.com/docs/services/static-data/) — capital/type classification dependency; no archive downloaded.
+84
View File
@@ -0,0 +1,84 @@
# Data contracts for the remaining-feature roadmap
**Research date:** 2026-09-19 UTC. **Compatibility pin:** `X-Compatibility-Date: 2026-08-18`.
**Scope:** official public documentation/OpenAPI research for planning. No private ESI requests, credentials, user data, archives, or service changes were used. These adapters are not implemented merely because their contracts are listed here.
Primary source: [official ESI OpenAPI](https://esi.evetech.net/meta/openapi.json), requested with the compatibility header above. Additional sources: [versioning](https://developers.eveonline.com/docs/services/esi/overview/), [pagination](https://developers.eveonline.com/docs/services/esi/pagination/x-pages/), [rate limiting](https://developers.eveonline.com/docs/services/esi/rate-limiting/), [best practices](https://developers.eveonline.com/docs/services/esi/best-practices/), and [SSO](https://developers.eveonline.com/docs/services/sso/).
Existing captured evidence in `eve-openapi-evidence.json` covers the first milestone's assets/helpers/structure topics, **not** all expansion routes below. Capture focused, versioned fixtures for each new adapter before implementation. Recheck the pinned contract at that point; advancing the compatibility date is a separate deliberate change.
## Personal-character datasets
All methods in this table are GET. Paths use the trusted `https://esi.evetech.net` base.
| Dataset | Route | OAuth scope | Pagination / historical coverage | Documented cache |
|---|---|---|---|---|
| Wallet balance | `/characters/{character_id}/wallet` | `esi-wallet.read_character_wallet.v1` | Single numeric balance | 120 s |
| Wallet journal | `/characters/{character_id}/wallet/journal` | Same wallet scope | `page` + `X-Pages`; past **30 days** | 3600 s |
| Wallet transactions | `/characters/{character_id}/wallet/transactions` | Same wallet scope | `from_id`, records before the given ID; retention/page size not specified | 3600 s |
| Open orders | `/characters/{character_id}/orders` | `esi-markets.read_character_orders.v1` | Unpaged current orders; includes `is_corporation` | 1200 s |
| Order history | `/characters/{character_id}/orders/history` | Same order scope | `page` + `X-Pages`; cancelled/expired orders up to **90 days** | 3600 s |
| Owned blueprints | `/characters/{character_id}/blueprints` | `esi-characters.read_blueprints.v1` | `page` + `X-Pages`; item/type/location, ME/TE and runs | 3600 s |
| Skills | `/characters/{character_id}/skills` | `esi-skills.read_skills.v1` | Unpaged; documented possibility of stale levels after training completion | 60 s, event-based |
| Skill queue | `/characters/{character_id}/skillqueue` | `esi-skills.read_skillqueue.v1` | Unpaged; needed to reconcile completed training with stale skill responses | 60 s, event-based |
| Industry jobs | `/characters/{character_id}/industry/jobs` | `esi-industry.read_character_jobs.v1` | Unpaged; `include_completed`; completed jobs only **90 days** | 300 s |
Planning consequences:
- Personal wallets share one scope, but balance/journal/transaction freshness and collection strategy differ.
- Do not treat `from_id` as a page number or invent a fixed transaction page size, cursor depth, or retention horizon.
- ESI retention is not application retention. Data may be retained after collection under an approved policy, but missing earlier history cannot be reconstructed by assumption.
- Do not assume order history describes every possible fill or disappearance; reconcile documented state/quantities with transaction evidence and preserve uncertainty.
- Exclude or explicitly partition `is_corporation` orders from personal financial totals. A character endpoint is not proof that every returned economic position is personally owned. Check any similar personal/corporation flags in transaction schemas during adapter implementation.
- Skills and skill queue are separate grants. Reconcile according to the documented completion behavior and timestamps; future queue entries are not trained skills, and conflicting/stale evidence must not manufacture eligibility.
- Owned blueprint instances are distinct from immutable recipe types and from the current asset-copy flag.
## Public and authenticated market feeds
| Dataset | GET route | Access | Pagination / cache |
|---|---|---|---|
| Active region types | `/markets/{region_id}/types` | Public | `page` + `X-Pages`; 600 s |
| Region orders | `/markets/{region_id}/orders` | Public; required `order_type` (`buy`, `sell`, or `all`), optional `type_id` | `page` + `X-Pages`; 300 s |
| Region history | `/markets/{region_id}/history` | Public; required `type_id` | Unpaged daily statistics; daily expiry at 11:05; retention horizon not specified |
| Structure orders | `/markets/structures/{structure_id}` | `esi-markets.structure_markets.v1` and authorized character context | `page` + `X-Pages`; 300 s |
Regional history contains date, volume, order count and high/average/low statistics. It is not a fill forecast, order book, or proof of executable liquidity. Structure-market access is not docking permission and must not become an unrestricted shared private-data cache.
The existing `/markets/prices` provider remains a different, global reference-price dataset; see [the initial ESI research](eve.md#reference-price-valuation).
## Later corporation datasets
Role declarations below come from the pinned contract, not a live corporation-role test. Verify effective role semantics, division restrictions, membership and application-workspace permissions at implementation time. A corporation scope alone is insufficient.
| Dataset | GET route | OAuth scope | Declared role requirement | Pagination / cache / history |
|---|---|---|---|---|
| Assets | `/corporations/{corporation_id}/assets` | `esi-assets.read_corporation_assets.v1` | `Director` | `X-Pages`; 3600 s |
| Wallet balances | `/corporations/{corporation_id}/wallets` | `esi-wallet.read_corporation_wallets.v1` | `Accountant` or `Junior_Accountant` | Division/balance array; 300 s |
| Wallet journal | `/corporations/{corporation_id}/wallets/{division}/journal` | Same wallet scope | Same wallet roles | `X-Pages`; 30 days; 3600 s |
| Wallet transactions | `/corporations/{corporation_id}/wallets/{division}/transactions` | Same wallet scope | Same wallet roles | `from_id`; retention unspecified; 3600 s |
| Open orders | `/corporations/{corporation_id}/orders` | `esi-markets.read_corporation_orders.v1` | `Accountant` or `Trader` | `X-Pages`; 1200 s |
| Order history | `/corporations/{corporation_id}/orders/history` | Same order scope | Same order roles | `X-Pages`; 90 days; 3600 s |
| Blueprints | `/corporations/{corporation_id}/blueprints` | `esi-corporations.read_blueprints.v1` | `Director` | `X-Pages`; 3600 s |
| Industry jobs | `/corporations/{corporation_id}/industry/jobs` | `esi-industry.read_corporation_jobs.v1` | `Factory_Manager` | `X-Pages`; completed jobs 90 days; 300 s |
No corporation-skills equivalent was verified: skills remain character-scoped. Corporation industry-job records must not be confused with the application's own `jobs` orchestration table.
## Scheduling and consistency implications
The pinned extensions identify `char-wallet` as 150 tokens/15 minutes, `char-industry` as 600/15 minutes, `char-detail` as 600/15 minutes, and regional orders as `market-order` 12,000/15 minutes. These are **tokens**, not necessarily one token per request. Confirm route-specific headers and token costs at implementation time.
Character order routes have no route-specific limiter extension in the inspected contract; that does not mean unlimited access. Respect the legacy error limiter and response caching. TTLs are scheduling hints bounded by actual response validators/cache headers, not promises that upstream content is fresh to the second.
X-Pages guidance warns that cache expiry between pages can duplicate records. Do not assume one asset-specific pagination algorithm establishes stable snapshots for every route. Preserve source IDs, idempotent checkpoints, partial/backfill status and coverage windows. Prices, journals, assets, skills and jobs do not form one atomic EVE-wide transaction.
## Concrete code integration points
- `src/lib/server/sso.ts`: authorization currently requests only assets; add server-allowlisted capability scope upgrades and state binding.
- `src/lib/server/esi/index.ts`: only assets and global reference prices currently have public client methods; implement separate typed adapters/strategies for these new routes.
- `src/lib/domain/model.ts`: add dataset/finance/owned-blueprint/skill/job contracts; existing `BlueprintRepository`/`OrderBook` are narrow placeholders.
- `src/convex/schema.ts`: add bounded records, generations/checkpoints and indexes. Its current generic `jobs` placeholder is not ESI industry-job data.
- `src/lib/server/application.ts`: existing synchronization orchestrates assets only; new datasets belong in the durable job/capability design.
## Unresolved and explicitly not promised
Wallet-transaction retention, market-history retention, exact page sizes, maximum cursor depth, complete consistency guarantees across pages/endpoints, real corporation-role behavior, fee formulas, industry economics and dynamic docking authority are not established by this research. Verify those separately; do not infer them from endpoint availability.
+163
View File
@@ -0,0 +1,163 @@
# TypeSafe AI / JEV integration research
**Checked:** 2026-09-18
**Scope:** current official TypeSafe AI/Jev documentation only. Research was gathered with Firecrawl CLI, using sequential scrapes of the official documentation index/full corpus and JavaScript SDK page (global scrape concurrency was 2). No implementation was added.
## Summary
Use JEV as a small, typed intent router in front of the existing asset workflows—not as a chat model, valuation calculator, search engine, authenticator, or function executor. The recommended first call is one JavaScript SDK `Choice` question with exactly `asset_valuation`, `asset_search`, and `unsupported` options. Treat low `Choice.confidence` as an application-level abstention and route to a deterministic clarification/manual path; JEV has no native abstain answer.
For an asset-valuation result, code should call the reference-data/valuation service and perform any arithmetic. For an asset-search result, code should query the asset index. For `unsupported`, code should return a fixed product response. Keep authentication, authorization, tool execution, response text, and all side effects in application code.
## Findings
1. **System One is bounded decision infrastructure, not an agent or chat model.** TypeSafe says System One evaluates a `state` and returns typed answers and probabilities; it does not write replies, generate code, explain reasoning, or choose its own next action. The intended architecture is ordinary software control flow with narrow AI decisions embedded in it. [System One](https://docs.typesafe.ai/concepts/system-one) · [How to build with System One](https://docs.typesafe.ai/concepts/how-to-build-with-system-one)
Implications for this integration:
- **Not generative chat:** use deterministic/canned UI responses or another separately approved component for user-facing text; do not ask JEV to compose the clarification or unsupported message.
- **Not calculation:** do not ask JEV to compute a reference valuation, normalize currency, or perform exact arithmetic. Keep those operations in code. TypeSafe explicitly recommends using code for work it can compute exactly. [Advanced structure/failure modes](https://docs.typesafe.ai/primitives/advanced)
- **Not authentication or authorization:** the SDK API key authenticates calls to TypeSafe; it does not authenticate the end user or authorize an asset action. Perform application identity, entitlements, and permissions before/after routing in code.
- **Not execution:** the function-calling cookbook maps closed-set values to ordinary typed functions, but the functions remain application code. [Function calling cookbook](https://docs.typesafe.ai/cookbooks/function_calling)
2. **State is shared, questions are independent.** A request evaluates one `state` against one or more questions. State may be a string or structured JSON object/array containing the text and relevant application context; JEV currently supports text input and does not support image, audio, or video. Every question sees the same state and one question's answer does not become context for another. [State](https://docs.typesafe.ai/concepts/state) · [API request body](https://docs.typesafe.ai/api#request-body)
For the router, send only the user request plus any text/JSON metadata required to disambiguate it, for example `{ message, asset_context }`. Do not send unrelated records. Current docs warn that context rot lowers accuracy and recommend decomposing both state and questions.
3. **The primitives have distinct, typed semantics.**
- **Choice:** selects exactly one member of a fixed criteria map and returns `choice`, all option `probabilities`, and `confidence`. It is the correct primitive for the three-way router. Include an explicit `unsupported` option rather than forcing every request into a supported route; TypeSafe recommends an `other`/`none of the above` option when a list may not cover every input. A Choice supports up to 255 options. [Choice](https://docs.typesafe.ai/primitives/choice)
- **Noul:** returns `noul`, the probability that a yes/no statement is true. It has no separate `confidence`; values near 0.5 indicate an ambiguous yes/no judgment. Use it for a separate boolean such as “does the request mention a specific asset identifier?”, not as a replacement for the three-way router. [Noul](https://docs.typesafe.ai/primitives/noul)
- **Score:** returns an expected numeric position over an ordered list of 2–10 descriptive levels, the level probabilities, a legend, and confidence. It is appropriate for a dimension such as evidence quality or urgency, not for exact valuation arithmetic. [Score](https://docs.typesafe.ai/primitives/score)
- **Confidence:** is derived from the Choice/Score probability distribution. It is a useful uncertainty signal, not a correctness guarantee. Full probabilities should be retained for auditing and policy decisions. Noul answers do not carry `confidence`. [Confidence](https://docs.typesafe.ai/confidence)
4. **Exact recommended router labels and boundaries.** Use one `Choice` whose criteria are deliberately contrastive:
- `asset_valuation`: the user asks to estimate, compare, look up, or understand the reference worth/valuation of an identified asset or asset candidate. The downstream valuation implementation owns data freshness, methodology, and arithmetic.
- `asset_search`: the user asks to find, browse, list, filter, or retrieve assets or reference records; valuation is not the primary requested operation.
- `unsupported`: the request is neither supported asset browsing nor reference valuation (including unrelated chat, unsupported transactions, or unrelated domains).
Avoid overlapping descriptions such as “anything involving prices” in both supported options. In particular, “find assets priced below X” is primarily `asset_search`; “what is this asset worth?” is `asset_valuation`. A low-confidence result is not a fourth JEV label: it is an application-level abstention.
5. **Confirmed JavaScript package and API.** The official JavaScript SDK docs specify Node.js 20+, package `@typesafe-ai/sdk`, ESM/CommonJS support, and TypeScript declarations. The current SDK reference exposes `VERSION` `0.6.0` and the changelog records v0.6.0 on 2026-09-15. [JavaScript SDK](https://docs.typesafe.ai/sdk/javascript) · [JavaScript changelog](https://docs.typesafe.ai/sdk/javascript/changelog)
Confirmed API/type surface:
- `new TypeSafeClient(config?)`; `TypeSafeClientConfig` includes `apiKey`, `baseURL`, `defaultModel`, `defaultHeaders`, `fetch`, `logger`, `logLevel`, `retry`, `timeout`, and `dangerouslyAllowBrowser`.
- `client.systemOne<Q>(request, options?)` returns `APIPromise<SystemOneResult<Q>>`.
- `systemOne` request fields are `state: EntryType`, `questions: Q`, and optional `model: string`. `EntryType` is a string, JSON object, JSON array, or `null`.
- `choice<T>(instructions, criteria)` returns `ChoiceQuestion<T>`; `noul(instructions?, criteria?)` returns `NoulQuestion`; `score<T>(instructions, criteria)` returns `ScoreQuestion<T>`.
- `SystemOneResult<Q>.answers` maps each question key to `ResultFor<Q[K]>`; `ResultFor` preserves the criteria keys. `ChoiceResponse<T>.choice` is typed as `keyof T & string`, with typed `probabilities`, `confidence`, and `type: "choice"`.
- Environment defaults are `TYPESAFE_API_KEY`, `TYPESAFE_BASE_URL`, `TYPESAFE_DEFAULT_MODEL`, and `TYPESAFE_LOG_LEVEL`; default base URL is `https://api.typesafe.ai`, default model is `jev-latest`, default log level is `warn`, and default per-attempt timeout is 10,000 ms. [TypeSafeClient](https://docs.typesafe.ai/sdk/javascript/api/classes/TypeSafeClient) · [choice()](https://docs.typesafe.ai/sdk/javascript/api/functions/choice) · [SystemOneRequest](https://docs.typesafe.ai/sdk/javascript/api/interfaces/SystemOneRequest) · [SystemOneResult](https://docs.typesafe.ai/sdk/javascript/api/interfaces/SystemOneResult) · [ResultFor](https://docs.typesafe.ai/sdk/javascript/api/type-aliases/ResultFor) · [ChoiceResponse](https://docs.typesafe.ai/sdk/javascript/api/interfaces/ChoiceResponse) · [TypeSafeClientConfig](https://docs.typesafe.ai/sdk/javascript/api/interfaces/TypeSafeClientConfig)
6. **Exact typed-router call (SDK-confirmed).** This is the integration shape to implement later; the snippet uses the confirmed package and methods, not an inferred SDK name:
```ts
import { choice, TypeSafeClient } from "@typesafe-ai/sdk";
const client = new TypeSafeClient({
// Pin while calibrating thresholds; jev-latest is the SDK default.
defaultModel: "jev-1.13.0",
});
const result = await client.systemOne({
state: { message: userMessage },
questions: {
intent: choice("What is the user's primary supported intent?", {
asset_valuation:
"The user wants a reference valuation or wants to understand the worth of an identified asset.",
asset_search:
"The user wants to find, browse, list, filter, or retrieve assets or reference records; valuation is not the primary request.",
unsupported:
"The request is outside asset browsing and reference valuation, including unrelated chat or unsupported actions.",
}),
},
});
const answer = result.answers.intent;
// answer.choice is inferred as the criteria-key union:
// "asset_valuation" | "asset_search" | "unsupported".
// answer.probabilities and answer.confidence are also available.
```
The current model page says `jev-latest` currently resolves to `jev-1.13.0`. Use the pinned ID while calibrating the abstention policy; use the alias only when accepting model movement is intentional. The response `model` field reports the version that actually answered. [Models](https://docs.typesafe.ai/models)
7. **Recommended dispatch and abstention policy.** JEV should return a classification, after which code owns the route:
- if `answer.confidence < CONFIDENCE_FLOOR`, return an application-level `abstain` result and use a deterministic clarification/manual-review flow;
- otherwise, route `asset_valuation` to the reference valuation service, `asset_search` to the asset index, and `unsupported` to a fixed unsupported response.
Start with a configurable `CONFIDENCE_FLOOR = 0.80` for this medium/high-consequence router, then calibrate it against labeled requests and the cost of each routing error. This is an application policy, not a TypeSafe default. The official guidance gives examples of 0.5/0.6 floors for human routing and >0.85/>0.9 for higher-risk actions, and says thresholds must match risk and be tested on the application's data. [Confidence](https://docs.typesafe.ai/confidence) · [Confidence-gated routing](https://docs.typesafe.ai/patterns/confidence-routing) · [Intent routing](https://docs.typesafe.ai/patterns/intent-routing)
Recommended local result shape (not a JEV response shape): `abstain | { route: "asset_valuation" | "asset_search" | "unsupported", confidence, probabilities, model }`. Do not pretend a low-confidence `choice` is a reliable route. Preserve the raw answer, probability map, model version, usage, and request ID for evaluation.
8. **There is no native abstention field.** A Choice always selects one supplied option. Therefore:
- `unsupported` is the explicit semantic escape hatch for out-of-scope input;
- low confidence is the uncertainty escape hatch and must be converted to `abstain` by application code;
- if a future requirement needs a separate “in-scope but missing details” class, add a fourth criteria key or use the same low-confidence abstention path; do not infer that behavior from a missing SDK field.
This is preferable to using an independent Noul for each route: separate Nouls are absolute yes/no judgments and are not guaranteed to be mutually exclusive or comparable. A Choice is the relative one-of-N classifier needed here. [Choice](https://docs.typesafe.ai/primitives/choice) · [Noul](https://docs.typesafe.ai/primitives/noul) · [Confidence](https://docs.typesafe.ai/confidence)
9. **Transport and SDK failure behavior is distinct from semantic abstention.** The HTTP API documents `POST https://api.typesafe.ai/v1/systemone`, bearer authentication, and these error meanings: 401 missing/invalid key, 422 invalid request body, 429 rate limit, and 529 temporary overload. [API reference](https://docs.typesafe.ai/api)
The JavaScript SDK exposes:
- `AuthenticationError` (401), `BadRequestError` (400), `PermissionDeniedError` (403), `NotFoundError` (404), `RateLimitError` (429), `UnprocessableEntityError` (422), and `InternalServerError` (5xx), all carrying the parsed `body`, headers, status, and usually `requestId` through `APIError`.
- `APIConnectionError` for connection/response delivery failures, `APITimeoutError` for a response that exceeds the configured timeout, and `APIUserAbortError` for an `AbortSignal` cancellation.
- Default retry policy: connection errors and timeouts are retryable; HTTP statuses 408, 429, and 500–599 are retried, up to 2 retries after the initial attempt, with exponential backoff starting at 500 ms, capped at 5,000 ms, 25% jitter, and `Retry-After` honored up to 60,000 ms. Per-call `timeout`, `retry`, `headers`, and `signal` are supported via `RequestOptions`. [APIError](https://docs.typesafe.ai/sdk/javascript/api/classes/APIError) · [RateLimitError](https://docs.typesafe.ai/sdk/javascript/api/classes/RateLimitError) · [APITimeoutError](https://docs.typesafe.ai/sdk/javascript/api/classes/APITimeoutError) · [RequestOptions](https://docs.typesafe.ai/sdk/javascript/api/interfaces/RequestOptions) · [RetryPolicy](https://docs.typesafe.ai/sdk/javascript/api/interfaces/RetryPolicy)
Handle 401/403/422/configuration failures as non-retryable integration failures; let the SDK handle transient 408/429/5xx/connection/timeout retries, then fail closed to an application fallback. A transport error is not an `unsupported` classification and must not be converted into one. Never expose the API key in a browser: the SDK defaults `dangerouslyAllowBrowser` to false.
10. **Fan-out is the right composition strategy for independent router features.** TypeSafe recommends sending independent questions about one state together: they are evaluated in parallel, and code may ignore answers that do not apply. If a later question genuinely depends on the first answer or requires newly fetched state, make a second request in code. [Speculative fan-out](https://docs.typesafe.ai/patterns/fan-out) · [Parallel questions cookbook](https://docs.typesafe.ai/cookbooks/parallel_questions)
For the first implementation, keep the request to the single intent Choice. If later needed, add independent questions in the same request—for example a Noul for whether an asset identifier is explicitly present—but do not add an extraction/calculation question merely because it is available. The parallel-questions cookbook reports that batching 13 questions over one document avoids resending the document and, in its dated example, was 12.2× cheaper and 10.0× faster than 13 serial calls with no answer change. The exact benchmark is workload-specific, but the architectural lesson applies.
11. **Composite scoring belongs after classification and remains deterministic.** Break a broad judgment into independent Score dimensions, normalize each score to 0–1, and combine with weights in application code. Preserve each component and its probabilities rather than asking JEV for one opaque “overall valuation.” [Composite scoring](https://docs.typesafe.ai/patterns/composite-scoring) · [Score](https://docs.typesafe.ai/primitives/score)
This can support a later reference-ranking policy (for example evidence quality, recency, and match quality), but it must not replace the authoritative valuation formula. Scores are judgments over descriptive levels; they are not exact financial numbers.
12. **Function-calling cookbook: closed sets only, with code in control.** The official example maps a function selector to a Choice, fixed-list arguments to Choice, `bool` flags to Noul, and set-valued `list[Literal]` arguments to one Noul per member. Optional arguments use a separate “was this argument stated?” Noul so the function default can remain in effect. The cookbook reports call confidence as the least certain judgment rather than a product of probabilities. [Function calling](https://docs.typesafe.ai/cookbooks/function_calling)
This pattern is useful later for deterministic asset-search/valuation handlers, but it is not a JEV tool-execution API. Do not pass open-ended numeric calculations, authorization decisions, or arbitrary generated function arguments to JEV. Validate every route and argument in application code.
13. **Reranking is a candidate-scoring pattern, not an asset search replacement.** The official reranking cookbook first uses a fast search method (BM25) to produce a shortlist, then scores each query/candidate pair with a Noul and sorts by the returned probability. TypeSafe cannot add a candidate omitted by the fast search stage. The dated example reranked 30 candidates for each of 40 legal queries and improved top-1 from 5% to 18% and top-10 from 38% to 62%. [Reranking cookbook](https://docs.typesafe.ai/cookbooks/rerank_typesafe)
If asset search eventually needs semantic ordering, keep the asset index/retrieval system first, then use one Noul per query-candidate pair (or batch independent signals per pair) and sort in code. Do not invoke JEV against the entire asset corpus.
14. **Extraction should be candidate-first and lossless.** The official pre-parsed extraction cookbook uses regex to over-find candidate emails, phone numbers, or amounts; a Choice selects one candidate verbatim; code copies and normalizes it. It adds a `none` Choice option as an escape hatch and notes the 255-option limit. It explicitly says JEV should choose among candidates, not invent an unbounded value or transpose digits. [Pre-parsed value extraction](https://docs.typesafe.ai/cookbooks/pre_parsed_value_extraction_cookbook)
Apply this to asset IDs/tickers only when the candidate finder is owned by code or another approved extractor: provide the candidate strings as Choice options, include `none`, copy the selected identifier exactly, and validate it against the asset index. Do not ask JEV to generate a new identifier or exact valuation.
15. **Routing cookbook evidence supports graceful degradation.** The official classification-with-confidence cookbook classifies into a fine-grained Choice taxonomy, then reports a broader parent label when confidence is below a threshold rather than forcing a specific answer. The intent-routing pattern similarly sends low-confidence requests to a human and routes high-confidence labels to deterministic handlers or specialist systems. [Classification using confidence](https://docs.typesafe.ai/cookbooks/classification_using_confidence) · [Intent routing](https://docs.typesafe.ai/patterns/intent-routing)
For this scope, the equivalent broader fallback is the local `abstain` route, which can request required asset details through a fixed form or send to manual review. There is no official evidence that JEV itself should generate the clarification text.
## Exact integration recommendation
1. Run the router server-side with `@typesafe-ai/sdk` on Node.js 20+.
2. Pin `model: "jev-1.13.0"` (or `defaultModel`) while measuring; log the returned `model`. Move to `jev-latest` only with an explicit threshold-recalibration policy.
3. Send a minimal state containing the incoming user text and only relevant text/JSON context.
4. Ask one `choice()` question with the three contrastive keys `asset_valuation`, `asset_search`, and `unsupported`.
5. Convert `confidence < 0.80` to a local typed `abstain`; validate/recalibrate this floor with a labeled evaluation set. Keep all probabilities, not only the winner.
6. Dispatch high-confidence results in ordinary code: valuation service, asset index, or fixed unsupported response. Keep user auth, authorization, exact calculations, data fetching, and side effects outside JEV.
7. Treat SDK/API failures as failures, not as semantic `unsupported`. Retry only according to the SDK policy; fail closed after retries and retain the request ID/error body for diagnosis.
8. Add fan-out questions only when independent and useful. Add extraction/reranking/composite scoring as separate, deterministic stages—not inside the initial router.
## Gaps and caveats
- **No native abstain result:** the SDK/API always returns a Choice label when the request succeeds. Local `abstain` is required.
- **No official asset-domain rubric or benchmark:** current docs provide generic routing examples, not accuracy for asset browsing/reference valuation. Build a labeled test set containing near-boundary requests, missing asset details, price-search wording, unsupported requests, and adversarial/ambiguous text.
- **Threshold calibration is application-specific:** official docs give guidance and examples but no universal confidence threshold. Validate the proposed 0.80 floor and separately measure false valuation routing versus false search routing.
- **No clarification-label requirement in the chosen scope:** the three requested labels do not include `clarification`. Low-confidence in-domain requests will abstain; if product analytics need a distinct typed `needs_details` outcome, add it deliberately rather than overloading `unsupported`.
- **No JEV-generated response or valuation:** current docs do not provide a chat-generation method, calculator, finance data source, valuation methodology, or authorization system. Those are integration responsibilities.
- **Current SDK documentation is precise, but cookbook code is mostly Python:** the official JavaScript API confirms the client and types, while the requested cookbooks show Python examples. Porting cookbook architecture to TypeScript will require local implementation and tests; do not infer undocumented helper classes from Python cookbook names.
- **Alias movement:** `jev-latest` can point to a future release; thresholds tuned to a pinned model must be retested after any model change.
## Sources
All kept sources below are official TypeSafe AI documentation and were current at the checked date.
- [System One](https://docs.typesafe.ai/concepts/system-one) — model purpose and distinction from LLM agents/generative output.
- [How to build with System One](https://docs.typesafe.ai/concepts/how-to-build-with-system-one) — code-controlled architecture, decomposition, fan-out, confidence gates.
- [State](https://docs.typesafe.ai/concepts/state) — supported state shapes and text-only Jev input.
- [Choice](https://docs.typesafe.ai/primitives/choice), [Noul](https://docs.typesafe.ai/primitives/noul), [Score](https://docs.typesafe.ai/primitives/score), [Confidence](https://docs.typesafe.ai/confidence) — primitive and uncertainty semantics.
- [API reference](https://docs.typesafe.ai/api) and [Models](https://docs.typesafe.ai/models) — HTTP shape, errors, current model ID/alias.
- [JavaScript SDK](https://docs.typesafe.ai/sdk/javascript), [JavaScript changelog](https://docs.typesafe.ai/sdk/javascript/changelog), [TypeSafeClient](https://docs.typesafe.ai/sdk/javascript/api/classes/TypeSafeClient), [choice()](https://docs.typesafe.ai/sdk/javascript/api/functions/choice), [SystemOneRequest](https://docs.typesafe.ai/sdk/javascript/api/interfaces/SystemOneRequest), [SystemOneResult](https://docs.typesafe.ai/sdk/javascript/api/interfaces/SystemOneResult), [ChoiceResponse](https://docs.typesafe.ai/sdk/javascript/api/interfaces/ChoiceResponse), [ResultFor](https://docs.typesafe.ai/sdk/javascript/api/type-aliases/ResultFor) — confirmed package/API/types.
- [Speculative fan-out](https://docs.typesafe.ai/patterns/fan-out), [Composite scoring](https://docs.typesafe.ai/patterns/composite-scoring), [Confidence-gated routing](https://docs.typesafe.ai/patterns/confidence-routing), [Intent routing](https://docs.typesafe.ai/patterns/intent-routing) — required architecture patterns.
- [Function calling](https://docs.typesafe.ai/cookbooks/function_calling), [Parallel questions](https://docs.typesafe.ai/cookbooks/parallel_questions), [Reranking](https://docs.typesafe.ai/cookbooks/rerank_typesafe), [Pre-parsed value extraction](https://docs.typesafe.ai/cookbooks/pre_parsed_value_extraction_cookbook), [Classification using confidence](https://docs.typesafe.ai/cookbooks/classification_using_confidence) — required cookbook coverage.
**Dropped by scope:** third-party articles, reviews, hosted-provider documentation, social posts, and non-official examples were not used as evidence.
+20
View File
@@ -0,0 +1,20 @@
# Platform evidence
Checked 2026-09-18 against live official documentation. Raw working copies are in ignored `.firecrawl/`.
## Convex
- [Self hosting](https://docs.convex.dev/self-hosting) and [official deployment guide](https://github.com/get-convex/convex-backend/blob/main/self-hosted/README.md): backend defaults to SQLite; no PostgreSQL/Redis is necessary. Backend API uses 3210, HTTP actions 3211, dashboard 6791. Persistent `/convex/data` is essential. Generate a deployment admin key with `./generate_admin_key.sh`; CLI uses `CONVEX_SELF_HOSTED_URL` and `CONVEX_SELF_HOSTED_ADMIN_KEY`, not cloud project login. Admin key is deployment-only, never a browser environment value.
- [Official Compose](https://github.com/get-convex/convex-backend/blob/main/self-hosted/docker/docker-compose.yml): health probe `/version`; backend SIGINT shutdown; distinct cloud/site origins. Upstream example uses `latest`; operators should pin tested image digests and back up before upgrades.
- [Svelte quickstart](https://docs.convex.dev/quickstart/svelte), [Svelte overview](https://docs.convex.dev/client/svelte/overview): use the actual `convex-svelte` package, Svelte 5; put Convex functions in `src/convex` via `convex.json`. Root `setupConvex(url)`; declarative `useQuery(api.function, () => args)` for subscriptions. Keep domain calculations in Bun server services, not separate browser/Convex copies.
- [Custom JWT](https://docs.convex.dev/auth/advanced/custom-jwt): supports RS256/ES256, `kid/alg/typ`, `sub/iss/exp` and client refresh `iat`. Enforce application audience. JWKS may be a data URI, avoiding a boot-time callback from Convex to the web container. This permits an EVE-login-backed application identity without handing EVE tokens to the browser. An application JWT is NOT an EVE access token.
## Bun
- [SQLite](https://bun.sh/docs/runtime/sqlite): synchronous `bun:sqlite` driver; parameterized prepared statements, transactions, read-only connections, strict bindings, integrity pragmas. SQLite integers are 64-bit but JS numbers only exact through 2^53−1: external EVE instance/location IDs should be decimal strings at domain boundaries. Reference type/system IDs fit safe integers and are validated.
- Transactions must contain synchronous writes, not awaited parsing/downloads. Immutable artifact compiler can use DELETE journaling to avoid promoting a DB without a WAL sidecar. Complete/close staging file before publishing its manifest. Serving uses read-only handles to a version, not an in-place changing database.
- Local available runtime: Bun 1.3.14. Live docs advertise a newer release; use only APIs verified by repository tests on 1.3.14 and explicitly pin the tested runtime in deployment.
## Evidence caveat
Documentation evolves. URLs above are live sources, not promises of permanent API compatibility. Dependency lockfile, ESI contract evidence and offline regression tests capture the implemented contract. Deployment and live-auth testing need separately configured services/credentials.
+260
View File
@@ -0,0 +1,260 @@
# Production reference-data import plan
**Checked:** 2026-09-18
**Scope:** full real-data import is now authorized, but this phase performed **no archive download**. Only release indexes, HTTP `HEAD` metadata, official documentation/source, and small metadata/API files were inspected. Credentials and Infisical were not touched.
## Decision summary
1. Download the current Reference Data and Hoboleaks archives, then parse their exporter-defined root members. Their current compressed sizes are 14,512,812 bytes and 675,868 bytes.
2. Use CCP's current JSON Lines SDE for the universe complement, specifically `mapSolarSystems.jsonl`. CCP's current build is `3503375` (released 2026-09-10), and the official map guide directly documents reading `_key` and `position.x/y/z` from that member.
3. Do **not** use the EVE Ref ESI-scrape archive as the production universe source: its latest published artifact is dated 2025-11-26, while the CCP SDE is current to 2026-09-10. The ESI scrape is still useful as a shape/reference comparison, not as the freshness authority.
4. The selected compressed download budget is **114,291,615 bytes (~108.997 MiB)** before temporary extraction/database space: Reference Data + Hoboleaks + CCP JSONL SDE.
## 1. Current release metadata (preflight; no archive bytes read)
The values below came from the public JSON indexes and final archive `HEAD` responses. ETags are opaque HTTP validators, not published SHA-256 checksums.
| Input | Immutable/current URL | Bytes | Approx. compressed size | Last-Modified | ETag / version |
| --- | --- | ---: | ---: | --- | --- |
| EVE Ref Reference Data | [`reference-data-latest.tar.xz`](https://data.everef.net/reference-data/reference-data-latest.tar.xz) (dated route: [`reference-data-2026-09-13.tar.xz`](https://data.everef.net/reference-data/history/2026/reference-data-2026-09-13.tar.xz)) | 14,512,812 | 13.84 MiB | 2026-09-13 11:16:26Z | `a4b2dd0325ffac28158f2b93500a0d52` |
| EVE Ref Hoboleaks mirror | [`hoboleaks-sde-latest.tar.xz`](https://data.everef.net/hoboleaks-sde/hoboleaks-sde-latest.tar.xz) (dated route: [`hoboleaks-sde-2026-09-13.tar.xz`](https://data.everef.net/hoboleaks-sde/history/2026/hoboleaks-sde-2026-09-13.tar.xz)) | 675,868 | 660.03 KiB | 2026-09-13 11:05:28Z | `5681550f6fe5f2927b055dfe33883655` |
| EVE Ref ESI scrape (not recommended for production systems) | [`eve-ref-esi-scrape-latest.tar.xz`](https://data.everef.net/esi-scrape/eve-ref-esi-scrape-latest.tar.xz) | 77,038,676 | 73.47 MiB | 2025-11-26 15:42:24Z | `08fe439c74509e521620f588a13cfb37-10` |
| CCP SDE JSONL, build 3503375 | [`eve-online-static-data-3503375-jsonl.zip`](https://developers.eveonline.com/static-data/tranquility/eve-online-static-data-3503375-jsonl.zip) | 99,102,935 | 94.51 MiB | 2026-09-10 11:21:36Z | `fce5dcb01dbe357402ace2848297f556-12` |
The CCP automation endpoint currently returns:
```json
{"_key":"sde","buildNumber":3503375,"releaseDate":"2026-09-10T11:09:04Z"}
```
The EVE Ref archive indexes are [`reference-data/index.json`](https://data.everef.net/reference-data/index.json), [`hoboleaks-sde/index.json`](https://data.everef.net/hoboleaks-sde/index.json), and [`esi-scrape/index.json`](https://data.everef.net/esi-scrape/index.json). CCP's official automation documentation says the latest alias redirects to the build-numbered URL and that all resources support ETag/Last-Modified. Record both the alias and the resolved immutable URL in the import manifest.
EVE Ref's running [`/meta`](https://ref-data.everef.net/meta) currently reports build time `2026-09-13T11:10:03Z` and source hashes:
```json
{
"build_time": "2026-09-13T11:10:03Z",
"esi": {"sha256": "645b2902174fe52561b946a5c6e39e257358803956238f0750798e1cde5e4077"},
"hoboleaks": {"sha256": "477799f37c9275c3ad408117cce8e7913688be41b17fe404cd48e3b501be55cd"},
"sde": {"sha256": "56f5f7eba4b401aeb2fc1090d2db5ab9dd55cc08a396871e764df45017fa70af"}
}
```
These are hashes of EVE Ref's three **input archives**, not checksums of `reference-data-latest.tar.xz`. Current EVE Ref source downloads the YAML SDE input (`SDE_V2_YAML`) when building Reference Data, so `/meta.sde.sha256` must not be presented as the hash of the separately selected CCP JSONL SDE complement.
## 2. Reference Data archive: exporter-defined member layout
The current EVE Ref exporter source is the strongest available archive contract:
- [`BuildRefData.java` at commit 17948f63](https://github.com/autonomouslogic/eve-ref/blob/17948f63ad35b3417ad888a72f6d4fcc49e2b72d/src/main/java/com/autonomouslogic/everef/cli/refdata/BuildRefData.java#L335-L390) writes `meta.json`, then one `.json` member for every entry in `refdata.yaml`, into a TAR and XZ-compresses it.
- [`DataUtil.java`](https://github.com/autonomouslogic/eve-ref/blob/75b17f51e8880b474e44236049b50198a58d0332/src/main/java/com/autonomouslogic/everef/util/DataUtil.java#L1-L85) shows that the builder's SDE input is the configured `SDE_V2_YAML` ZIP, while the separate production universe complement below deliberately uses CCP's current JSONL ZIP.
- [`refdata.yaml` at commit ac663020](https://github.com/autonomouslogic/eve-ref/blob/ac6630205f41404c7635f9efcfd8d6646e45370f/src/main/resources/refdata.yaml) defines the member basenames, source filenames, ID fields, and output-store relationships.
- [`RefDataUtil.java`](https://github.com/autonomouslogic/eve-ref/blob/17948f63ad35b3417ad888a72f6d4fcc49e2b72d/src/main/java/com/autonomouslogic/everef/util/RefDataUtil.java#L103-L136) confirms that `meta.json` is a standalone object and every other JSON member is parsed as an object whose property names are decimal IDs. Its config loader defaults absent `outputFile` to lower-snake-case (`marketGroups` → `market_groups`) and absent `outputStore` to the config ID ([source](https://github.com/autonomouslogic/eve-ref/blob/17948f63ad35b3417ad888a72f6d4fcc49e2b72d/src/main/java/com/autonomouslogic/everef/util/RefDataUtil.java#L177-L200)).
### Exact member names
The exporter emits these 19 root members in config order:
```text
meta.json
categories.json
groups.json
market_groups.json
types.json
type_materials.json
type_bonus.json
masteries.json
type_dogma.json
dogma_attributes.json
dogma_effects.json
meta_groups.json
mutaplasmids.json
skills.json
units.json
blueprints.json
icons.json
regions.json
schematics.json
```
`meta.json` is a `RefDataMeta` object containing `build_time` and `sha256` objects for `sde`, `esi`, and `hoboleaks`. Every other member is written as:
```json
{
"<decimal-id>": {"<normalized record fields>": "..."}
}
```
The writer serializes the map key as a JSON string and the value as the normalized EVE Ref record. It does not write one file per ID inside the archive. A loader should preserve the string key as the canonical ID while validating that it parses as a non-negative 64-bit integer.
### Member/source map
| Member | Config ID / output store | Input members named by the exporter | Import role |
| --- | --- | --- | --- |
| `categories.json` | `categories` / `categories` | SDE `categories.yaml`; ESI `universe/categories` | Canonical category map. |
| `groups.json` | `groups` / `groups` | SDE `groups.yaml`; ESI `universe/groups` | Canonical group map. |
| `market_groups.json` | `marketGroups` / `marketGroups` | SDE `marketGroups.yaml`; ESI `markets/groups` | Canonical market-group map. |
| `types.json` | `types` / `types` | SDE `types.yaml`; ESI `universe/types` | Canonical inventory-type map. |
| `type_materials.json` | `typeMaterials` / `types` | SDE `typeMaterials.yaml`; Hoboleaks `typematerials.json` | Supplemental type merge input; values belong in `types`. |
| `type_bonus.json` | `typeBonus` / `types` | SDE `typeBonus.yaml` | Supplemental type merge input; values belong in `types`. |
| `masteries.json` | `masteries` / `types` | SDE `masteries.yaml` | Supplemental type merge input; values belong in `types`. |
| `type_dogma.json` | `typeDogma` / `types` | SDE `typeDogma.yaml` | Supplemental type merge input; values belong in `types`. |
| `dogma_attributes.json` | `dogmaAttributes` / `dogmaAttributes` | SDE `dogmaAttributes.yaml`; ESI `dogma/attributes` | Canonical dogma-attribute map. |
| `dogma_effects.json` | `dogmaEffects` / `dogmaEffects` | SDE `dogmaEffects.yaml`; ESI `dogma/effects` | Canonical dogma-effect map. |
| `meta_groups.json` | `metaGroups` / `metaGroups` | SDE `metaGroups.yaml` | Canonical meta-group map. |
| `mutaplasmids.json` | `mutaplasmids` / `mutaplasmids` | Hoboleaks `dynamicitemattributes.json` | Canonical mutaplasmid map. |
| `skills.json` | `skills` / `skills` | Derived by EVE Ref decorators | Canonical skill map. |
| `units.json` | `units` / `units` | Hoboleaks `dogmaunits.json` | Canonical unit map. |
| `blueprints.json` | `blueprints` / `blueprints` | SDE `blueprints.yaml`; Hoboleaks `blueprints.json` | Canonical blueprint map. |
| `icons.json` | `icons` / `icons` | SDE `icons.yaml` | Canonical icon map. |
| `regions.json` | `regions` / `regions` | SDE `mapRegions.yaml`; ESI `universe/regions` | Canonical region map. |
| `schematics.json` | `schematics` / `schematics` | SDE `planetSchematics.yaml`; ESI `universe/schematics` | Canonical schematic map. |
The four type supplemental configs have `dedicatedOutput: false` and `outputStore: types` in the current config. The exporter still iterates every config and names a member from `config.outputFile`; therefore, treat `types.json` as the canonical merged type table, and treat those four sidecar members as exporter outputs whose content must be checked after download rather than assumed to be separate canonical tables. Do not invent records from a sidecar if the authoritative merged `types.json` does not contain them.
### Reference Data parser invariants
- Read TAR entries sequentially through XZ decompression; there is no need to materialize an uncompressed archive.
- Require `meta.json` exactly once.
- For each other `.json` member, require a top-level JSON object; every property key must be a decimal ID; reject duplicate IDs and malformed values.
- Skip no member silently except an explicitly documented non-JSON compatibility member. The current EVE Ref parser skips non-JSON entries but the exporter contract above emits JSON only.
- Preserve source/build metadata separately from the local archive URL, byte count, ETag, Last-Modified, and locally computed SHA-256.
## 3. Hoboleaks archive: exact root member layout
The current archive builder is [`ScrapeHoboleaks.java` at commit 17948f63](https://github.com/autonomouslogic/eve-ref/blob/17948f63ad35b3417ad888a72f6d4fcc49e2b72d/src/main/java/com/autonomouslogic/everef/cli/ScrapeHoboleaks.java#L87-L143): it downloads Hoboleaks `meta.json`, writes that as root `meta.json`, iterates `meta.files`, downloads each named file from the Hoboleaks root, and writes that file at the archive root with the same filename. There are no nested `tq/` directories inside the EVE Ref mirror archive.
The current [`tq/meta.json`](https://sde.hoboleaks.space/tq/meta.json) has revision `3503375`, timestamp `2026-09-13T10:41:34`, and 28 file entries. Thus the expected current archive has **29 root members**:
```text
meta.json
accountingentrytypes.json
agenttypes.json
attributeorders.json
clonestates.json
compressibletypes.json
dbuffs.json
dogmaeffectcategories.json
dogmaunits.json
dynamicitemattributes.json
expertsystems.json
graphicmaterialsets.json
industryactivities.json
industryassemblylines.json
industryinstallationtypes.json
industrymodifiersources.json
industrytargetfilters.json
localization_dgmattributes.json
localization_languages.json
repackagedvolumes.json
schoolmap.json
schools.json
skillplans.json
skinmaterialnames.json
skinmaterials.json
skins.json
stationstandingsrestrictions.json
typematerials.json
blueprints.json
```
The list is metadata-driven, not a hard-coded schema promise: if a future `meta.json` adds/removes a file, the archive member set changes with it. The source files retain Hoboleaks casing and shape (for example, `activityID`/`activityName`, not normalized `activity_id`/`activity_name`). The prior [reference contract](https://github.com/autonomouslogic/eve-ref/blob/main/docs/src/datasets/reference-data.md) records small current examples for `industryactivities.json`, `agenttypes.json`, and `repackagedvolumes.json`.
Hoboleaks metadata also carries per-file `md5`, `revision`, `stale`, and `deprecated`. A file can be retrievable but stale. Record this as source state; do not convert it to a transport-successful fresh input.
## 4. Universe XYZ source choice
### Comparison
| Source | Current release | Member/path shape | Freshness and scale | Decision |
| --- | --- | --- | --- | --- |
| EVE Ref ESI scrape | `eve-ref-esi-scrape-latest.tar.xz`, 77,038,676 bytes, last modified 2025-11-26 | EVE Ref's source table names the file `universe/systems.yaml`; ESI universe records use `system_id`, `name`, `position: {x,y,z}`, and related fields | One complete scrape archive, but almost ten months behind the current SDE and not the freshness authority | Do not select for production universe data. |
| CCP SDE JSONL | build `3503375`, 99,102,935 bytes, released 2026-09-10 | Root member `mapSolarSystems.jsonl`; each line has `_key` (solar-system ID) and object fields such as `position.x/y/z` | Current official SDE; JSONL is explicitly documented as streamable and the map guide covers all space types | **Select this source.** |
| ESI live per-system routes | Current API; current ID list observed at 8,490 systems | `/universe/systems/{system_id}` response; one request per system | Fresh but ~8,490 API requests, rate/cache policy, and no single immutable snapshot | Use only for spot checks, not bulk import. |
The official CCP [Map Data guide](https://developers.eveonline.com/docs/guides/map-data/) says map data is in SDE files prefixed `map`, explicitly names `mapSolarSystems`, and its executable example opens `./mapSolarSystems.jsonl`, parses each line, reads `system["_key"]`, and reads `system["position"]["x"]`, `["y"]`, and `["z"]`. It also says SDE/ESI include known, wormhole, and abyssal space. The current [`schema-changelog.yaml`](https://developers.eveonline.com/static-data/tranquility/schema-changelog.yaml) records `mapSolarSystems` as a current file and records `position2D` as added after build `3096543`.
The official [System Security guide](https://developers.eveonline.com/docs/guides/system-security/) independently identifies `mapSolarSystems.jsonl` as the SDE source and `securityStatus` as a full-precision floating-point field. The official [Static Data docs](https://developers.eveonline.com/docs/services/static-data/) define JSONL handling: an integer-keyed object record is represented as one line with `_key`; object values remain object fields (the map guide's `system["position"]` access is the concrete example).
### Minimal exact SDE system line contract
Do not claim an entire system schema from the small guide example. The safe import contract is the fields directly documented by the official examples:
```json
{
"_key": 30000142,
"position": {
"x": -129064861735000000.0,
"y": 60755306910000000.0,
"z": 117469227060000000.0
},
"securityStatus": 0.9459131360054016
}
```
The real line has additional fields. Preserve unknown fields or explicitly ignore them only after validating the fields needed by the application. Use `_key` as `system_id`; do not expect the ESI `system_id` field in this SDE member. Keep XYZ as 64-bit floating-point values; the official guide warns that 32-bit precision is insufficient for interstellar and planetary coordinates.
A current ESI list request returned 8,490 IDs (checked 2026-09-18), confirming that a production profile limited to synthetic fixtures or a handful of systems is not sufficient. The CCP SDE map file is the efficient immutable complement: one current archive download rather than thousands of ESI calls.
## 5. Download and streaming plan (after this phase)
### Preflight
1. Fetch only the small JSON indexes, CCP `latest.jsonl`, Hoboleaks `tq/meta.json`, and `HEAD` metadata as done here.
2. Resolve each `latest` alias and record the final URL, status, content length, ETag, Last-Modified, media type, source revision/build, and retrieval time.
3. Require the expected sizes above for this checked release, but treat a changed release as a new candidate rather than a fatal global condition. Refresh metadata and use the new immutable URL.
4. Never use an ETag as a SHA-256. On the actual download, stream bytes through a local SHA-256 digest and compare byte count with `Content-Length`.
### Selected transfers
| Transfer | Compressed bytes |
| --- | ---: |
| Reference Data | 14,512,812 |
| Hoboleaks SDE | 675,868 |
| CCP SDE JSONL | 99,102,935 |
| **Total** | **114,291,615 (~108.997 MiB)** |
The ESI-scrape alternative would total 92,227,356 bytes with Reference Data + Hoboleaks, but it is stale and is not selected. Do not download both universe complements unless a deliberate cross-check is needed.
### Safe extraction order
1. Create a unique staging directory and `.part` file per source. Stream each HTTP body to disk while hashing and counting; do not place bytes in a JS/Bun `Buffer` for the whole archive.
2. Close and verify the staged file before opening it. Keep the immutable staged artifact until the candidate DB and manifest validate.
3. For Reference Data and Hoboleaks, stream XZ → TAR sequentially. Parse each selected root member as it arrives; close the TAR/XZ reader on any error. These archives are small enough to retain on disk but not to trust as an in-memory blob.
4. For CCP SDE JSONL, keep the completed ZIP staged, inspect the ZIP central directory, and stream only `mapSolarSystems.jsonl` through the JSON Lines parser. Do not extract the whole ~99 MB ZIP merely to build the universe complement. Keep the ZIP hash in provenance even if only one member is consumed.
5. Parse JSONL line-by-line: reject malformed lines, require integer `_key`, require finite numeric `position.x/y/z`, reject duplicate IDs, and count systems. Preserve the full source line or unknown fields if later features may need them.
6. Build a new immutable SQLite candidate, validate all cross-source references and the real universe count (expected to be >5,000), close it, then atomically publish its manifest. Keep the previous manifest/DB untouched on any source or validation failure.
### Independent source status
Track three source outcomes independently:
- `reference_data`: HTTP/download/hash/archive/member validation;
- `hoboleaks`: HTTP/download/hash/archive/member validation plus per-file `stale`/`deprecated` state;
- `ccp_sde_jsonl`: HTTP/download/hash/ZIP member/JSONL validation and build number.
A successful Reference Data archive must not mask a failed SDE universe complement. A stale Hoboleaks file is a distinct policy state from a failed download. Promotion should require the source set selected by the production profile to be successful and policy-acceptable, while retaining the previous published candidate for rollback.
## Gaps and non-claims
- The archives were not downloaded in this phase, so the emitted member list was not independently enumerated from the TAR/ZIP central directories. Reference Data and Hoboleaks member paths are derived from the current EVE Ref exporter source and metadata-driven builder, which is stronger than guessing but remains subject to exporter changes.
- CCP's official docs document the `mapSolarSystems.jsonl` path and fields used by the map example, but do not publish a complete machine-readable schema for every field on that page. The import should not require unlisted optional fields.
- The EVE Ref ESI-scrape archive's internal `universe/systems.yaml` path is established by the current EVE Ref data-source table; its current artifact is stale for a production freshness requirement.
- No credentials, Infisical data, or application secrets were accessed.
## Sources
- [EVE Ref Reference Data docs](https://docs.everef.net/datasets/reference-data) — source combination, archive/API routes, data-source table, and normalization.
- [EVE Ref Reference Data archive index](https://data.everef.net/reference-data/index.json) and [Hoboleaks archive index](https://data.everef.net/hoboleaks-sde/index.json) — current sizes, timestamps, ETags, and URLs.
- [EVE Ref ESI scrape index](https://data.everef.net/esi-scrape/index.json) — current published ESI-scrape size and age.
- [EVE Ref `BuildRefData.java`](https://github.com/autonomouslogic/eve-ref/blob/17948f63ad35b3417ad888a72f6d4fcc49e2b72d/src/main/java/com/autonomouslogic/everef/cli/refdata/BuildRefData.java) — exact Reference Data TAR member writer and `meta.json` behavior.
- [EVE Ref `RefDataUtil.java`](https://github.com/autonomouslogic/eve-ref/blob/17948f63ad35b3417ad888a72f6d4fcc49e2b72d/src/main/java/com/autonomouslogic/everef/util/RefDataUtil.java) — archive parser expectations.
- [EVE Ref `refdata.yaml`](https://github.com/autonomouslogic/eve-ref/blob/ac6630205f41404c7635f9efcfd8d6646e45370f/src/main/resources/refdata.yaml) — current member basenames, source paths, and output stores.
- [EVE Ref `ScrapeHoboleaks.java`](https://github.com/autonomouslogic/eve-ref/blob/17948f63ad35b3417ad888a72f6d4fcc49e2b72d/src/main/java/com/autonomouslogic/everef/cli/ScrapeHoboleaks.java) — metadata-driven root member creation.
- [Hoboleaks `tq/meta.json`](https://sde.hoboleaks.space/tq/meta.json) — current revision, file list, MD5, stale/deprecated state.
- [CCP Static Data docs](https://developers.eveonline.com/docs/services/static-data/) — JSONL format, automation routes, build metadata, HTTP caching.
- [CCP current build metadata](https://developers.eveonline.com/static-data/tranquility/latest.jsonl) and [build 3503375 JSONL release](https://developers.eveonline.com/static-data/tranquility/eve-online-static-data-3503375-jsonl.zip) — current build and release size.
- [CCP Map Data guide](https://developers.eveonline.com/docs/guides/map-data/) — `mapSolarSystems.jsonl`, `_key`, XYZ fields, all-space coverage, and coordinate precision.
- [CCP System Security guide](https://developers.eveonline.com/docs/guides/system-security/) — SDE system source and `securityStatus` field.
- [CCP schema changelog](https://developers.eveonline.com/static-data/tranquility/schema-changelog.yaml) — current `mapSolarSystems` file and `position2D` history.
- [Official ESI OpenAPI](https://esi.evetech.net/meta/openapi.json) and [ESI system ID list](https://esi.evetech.net/latest/universe/systems/) — ESI comparison shape and current system-count sanity check.
+318
View File
@@ -0,0 +1,318 @@
# EVE Ref reference-data and Hoboleaks source contract
**Checked:** 2026-09-18
**Scope:** current EVE Ref documentation/OpenAPI and the two current `data.everef.net` archive release routes named below. No full archive was downloaded. Firecrawl CLI was used for the documentation and representative API pages, sequentially; current machine-readable indexes, metadata, and small Hoboleaks JSON files were fetched directly.
## Summary
EVE Ref combines CCP SDE, an ESI scrape, and Hoboleaks into Reference Data. The Reference Data contract is explicitly still in development and its OpenAPI description is unstable, so the compiler must validate the incoming schema rather than assume that an omitted optional field is present. The current archive artifacts are published through directory `index.json` routes, while the running Reference Data API exposes independent input provenance at `/meta`.
The latest observed artifacts are:
| Artifact | Release route | Bytes | Last modified | ETag (opaque HTTP validator) |
| --- | --- | ---: | --- | --- |
| Reference Data | [`reference-data/index.json`](https://data.everef.net/reference-data/index.json) / [`reference-data-latest.tar.xz`](https://data.everef.net/reference-data/reference-data-latest.tar.xz) | 14,512,812 | 2026-09-13 11:16:26.504Z | `a4b2dd0325ffac28158f2b93500a0d52` |
| Hoboleaks SDE | [`hoboleaks-sde/index.json`](https://data.everef.net/hoboleaks-sde/index.json) / [`hoboleaks-sde-latest.tar.xz`](https://data.everef.net/hoboleaks-sde/hoboleaks-sde-latest.tar.xz) | 675,868 | 2026-09-13 11:05:28.403Z | `5681550f6fe5f2927b055dfe33883655` |
The dated, immutable-looking release routes are listed by [`reference-data/history/index.json`](https://data.everef.net/reference-data/history/index.json) and [`hoboleaks-sde/history/index.json`](https://data.everef.net/hoboleaks-sde/history/index.json). Treat the ETag as an HTTP cache validator, not as a published SHA-256 checksum.
## 1. EVE Ref data model and endpoint contract
The [Reference Data documentation](https://docs.everef.net/datasets/reference-data) says the set merges the latest SDE, ESI scrape, and Hoboleaks export; the complete download is linked from that page. It also states the current normalization rules:
- fields and URLs use `snake_case`;
- the JSON layout is mostly ESI-shaped;
- keyed objects are preferred over arrays to make merging deterministic;
- names and descriptions are language maps, as in the SDE.
The [pinned Reference Data OpenAPI document](https://github.com/autonomouslogic/eve-ref/blob/58dafd29ea55ad78fd62cbf820460cb79eb66202/spec/reference-data.yaml) (commit `58dafd29`, checked 2026-09-18) declares `openapi: 3.0.1`, server `https://ref-data.everef.net`, and says the spec is unstable. The examples below use the documented schema plus the live example endpoints linked by the docs. OpenAPI properties are not marked required for these records; a fixture may therefore be a valid minimal object, but a parser must not turn missing optional fields into invented defaults.
### Categories
`InventoryCategory` has these documented properties:
```text
category_id: int64
name: map<string, string>
published: boolean
icon_id: int64
group_ids: array<int64> # added by EVE Ref
```
A current small representative fixture is the live [category 4 response](https://ref-data.everef.net/categories/4), with the join-bearing fields retained:
```json
{
"category_id": 4,
"name": {"en": "Material"},
"published": true,
"icon_id": 22,
"group_ids": [18]
}
```
The live response currently contains additional language-map entries and additional group IDs; the fixture above is a deliberately small projection, not a claim that the production response contains only one group.
### Groups
`InventoryGroup` has these documented properties:
```text
anchorable: boolean
anchored: boolean
category_id: int64
fittable_non_singleton: boolean
group_id: int64
icon_id: int64
name: map<string, string>
published: boolean
type_ids: array<int64> # added by EVE Ref
use_base_price: boolean
```
A current [group 18 response](https://ref-data.everef.net/groups/18) can be reduced for a join fixture to:
```json
{
"category_id": 4,
"group_id": 18,
"name": {"en": "Mineral"},
"published": true,
"anchorable": false,
"anchored": false,
"fittable_non_singleton": false,
"icon_id": 22,
"use_base_price": true,
"type_ids": [34]
}
```
The production response has more language entries and more type IDs. Preserve the integer IDs as IDs; do not infer them from names.
### Types
`InventoryType` is intentionally broad. Its documented properties include the identity/join fields `type_id`, `group_id`, and EVE Ref-added `category_id`; language maps `name` and `description`; booleans such as `published`, `is_blueprint`, `is_skill`, `is_ore`, and `is_repackable`; numeric physical/economic fields such as `volume`, `packaged_volume`, `portion_size`, `mass`, `capacity`, `radius`, and `base_price`; and keyed maps including `dogma_attributes`, `dogma_effects`, `type_materials`, `required_skills`, and `produced_by_blueprints`.
A minimal current [type 34 response](https://ref-data.everef.net/types/34) sufficient to exercise category/group/type joins is:
```json
{
"type_id": 34,
"name": {"en": "Tritanium"},
"group_id": 18,
"category_id": 4,
"published": true,
"portion_size": 1,
"volume": 0.01,
"packaged_volume": 0.01,
"mass": 0,
"base_price": 2.0
}
```
This is a projection of the current response; fields not needed by this fixture are omitted, not declared unsupported. In particular, do not require every optional `InventoryType` property to exist on every record.
### Blueprints, activities, materials, and products
The OpenAPI schemas are exact about the important nesting:
```text
Blueprint {
activities: map<string, BlueprintActivity>
blueprint_type_id: int64
max_production_limit: int32
}
BlueprintActivity {
materials: map<string, BlueprintMaterial>
products: map<string, BlueprintMaterial>
required_skills: map<string, int32>
time: int64
}
BlueprintMaterial {
probability: double # optional; present for probabilistic products
quantity: int64
type_id: int64
}
```
The activity/material/product maps are keyed objects, not arrays. The key is a serialized ID for the material/product maps and an activity name for `activities`; the schema does not publish a closed activity enum. A current [blueprint 683 response](https://ref-data.everef.net/blueprints/683) demonstrates both deterministic manufacturing and probabilistic invention. A tiny exact manufacturing slice for a fixture is:
```json
{
"blueprint_type_id": 683,
"activities": {
"manufacturing": {
"materials": {
"34": {"quantity": 24000, "type_id": 34}
},
"products": {
"582": {"quantity": 1, "type_id": 582}
},
"required_skills": {"3380": 1},
"time": 6000
}
},
"max_production_limit": 30
}
```
The live object also has other activities and manufacturing materials; the slice above retains only the fields needed to test keyed material/product lookup and a required-skill map. Do not flatten these maps into an array without preserving their IDs.
### Systems and XYZ: explicit scope boundary
The EVE Ref data-source table lists systems under SDE `mapSolarSystems.yaml` and ESI `universe/systems.yaml`; it does **not** list a Reference Data `/systems` endpoint. The Reference Data OpenAPI has `Coordinate {x, y, z}` and uses it for `Region.position`, but it does not define a solar-system schema. Therefore, a systems fixture must not be presented as a Reference Data record.
For a compiler that also ingests the documented ESI system source, the official ESI schema requires this shape:
```json
{
"system_id": 30000142,
"name": "Jita",
"constellation_id": 20000020,
"position": {
"x": -129064861735000000.0,
"y": 60755306910000000.0,
"z": 117469227060000000.0
},
"security_status": 0.9459131360054016
}
```
The `position` object is the requested XYZ fixture. `system_id`, `name`, `position`, `security_status`, and `constellation_id` are required by the current ESI OpenAPI; planets, star, gates, stations, and security class are optional. The [EVE Ref source table](https://docs.everef.net/datasets/reference-data#data-sources) is the authority for the fact that systems come from those SDE/ESI paths; the [official ESI machine-readable schema](https://esi.evetech.net/meta/openapi.json) is the authority for the exact ESI record shape. This distinction avoids inventing a missing Reference Data `/systems` schema.
## 2. Hoboleaks SDE contract
The EVE Ref [Hoboleaks SDE page](https://docs.everef.net/datasets/hoboleaks-sde.html) identifies [`data.everef.net/hoboleaks-sde/`](https://data.everef.net/hoboleaks-sde/) as the archive mirror for files published by [Hoboleaks SDE](https://sde.hoboleaks.space/). The [Hoboleaks landing page](https://sde.hoboleaks.space/) says these are JSON complements for data missing from the official SDE and/or ESI, and warns that they are updated after TQ patches and can temporarily lag the SDE.
There is no formal JSON Schema linked for every Hoboleaks file. The following are exact current small-file structures observed on 2026-09-18; preserve their source casing and top-level map shape when ingesting raw Hoboleaks. EVE Ref's normalized API shape is separate and uses `snake_case`.
### Hoboleaks `tq/meta.json`: per-file provenance and stale state
The landing page explicitly documents `tq/meta.json` as update metadata, including a timestamp, revision, per-file MD5, and a `stale` flag. A current excerpt is:
```json
{
"timestamp": "2026-09-13T10:41:34",
"revision": 3503375,
"files": {
"industryactivities.json": {
"deprecated": false,
"stale": false,
"md5": "bfeffbb198389876e9b7b8f70c81615d",
"revision": 3503375
},
"attributeorders.json": {
"deprecated": false,
"stale": true,
"md5": "72906f5a2415bbdca45fc06936b20096",
"revision": 2911964
}
}
}
```
`stale: true` means Hoboleaks says that file's update was not successful and its contents may be outdated; it is not equivalent to a transport failure. The compiler should record the file as present-but-stale and make the promotion policy explicit. The `md5` is a content-change detector supplied by Hoboleaks; it is not a substitute for a locally computed SHA-256 of an archive.
### Small useful raw maps
[`industryactivities.json`](https://sde.hoboleaks.space/tq/industryactivities.json) is a small keyed object. Its current entries use numeric string keys and **camelCase** fields:
```json
{
"1": {
"activityID": 1,
"description": "Manufacturing of things",
"activityName": "Manufacturing"
},
"9": {
"activityID": 9,
"description": "The process of combining raw and intermediate materials to create advanced components",
"activityName": "Reactions"
}
}
```
[`agenttypes.json`](https://sde.hoboleaks.space/tq/agenttypes.json) is even smaller and maps numeric string keys directly to strings:
```json
{
"1": "NonAgent",
"4": "ResearchAgent",
"13": "HeraldryAgent"
}
```
[`repackagedvolumes.json`](https://sde.hoboleaks.space/tq/repackagedvolumes.json) is a keyed map from a type ID string to a numeric volume:
```json
{
"52907": 1300000.0,
"34828": 5000.0,
"22544": 3750.0
}
```
The EVE Ref data-source table specifically identifies `repackagedvolumes.json` as an addition to inventory types. These files are useful smoke-test inputs because they exercise object maps, string keys, source-specific camelCase, booleans, strings, integers, and floating-point values without requiring a large dataset.
The table also names Hoboleaks `blueprints.json`, `typematerials.json`, and other large files, but no raw schema claim is made here for those files: their full payloads were not downloaded, and the normalized EVE Ref blueprint contract above is the only blueprint/material/product contract asserted in this document.
## 3. Independent provenance and compiler policy
Use independent observations for each layer; do not treat one successful endpoint as proof that all sources succeeded.
| What to record | Route/evidence | Meaning |
| --- | --- | --- |
| Archive artifact identity | `data.everef.net/{reference-data,hoboleaks-sde}/index.json` | URL, byte size, `last_modified`, opaque ETag, and archive type. |
| Immutable release history | `.../history/index.json` and dated year index | Dated artifact URL, `file_time`, byte size, last-modified, and opaque ETag. |
| Reference build/source provenance | [`https://ref-data.everef.net/meta`](https://ref-data.everef.net/meta) | Current `build_time` and SHA-256 values for `esi`, `hoboleaks`, and `sde`. Observed: build `2026-09-13T11:10:03Z`; ESI `645b2902174fe52561b946a5c6e39e257358803956238f0750798e1cde5e4077`; Hoboleaks `477799f37c9275c3ad408117cce8e7913688be41b17fe404cd48e3b501be55cd`; SDE `56f5f7eba4b401aeb2fc1090d2db5ab9dd55cc08a396871e764df45017fa70af`. |
| Hoboleaks per-file state | [`https://sde.hoboleaks.space/tq/meta.json`](https://sde.hoboleaks.space/tq/meta.json) | Per-file revision, MD5, `stale`, and `deprecated`, plus Hoboleaks timestamp/revision. |
| Independent HTTP check | `HEAD` on the exact archive and metadata URLs | Transport status, content length, ETag, and Last-Modified; use to detect a changed artifact before parsing. |
The two archive indexes publish no SHA-256 field. For a future downloader, compute SHA-256 after downloading to a staging path and store it separately from the server ETag and from `/meta`'s *input-source* hashes. Do not label the archive ETag as a checksum.
The `/meta` payload itself is a small useful provenance fixture:
```json
{
"build_time": "2026-09-13T11:10:03Z",
"esi": {"sha256": "645b2902174fe52561b946a5c6e39e257358803956238f0750798e1cde5e4077"},
"hoboleaks": {"sha256": "477799f37c9275c3ad408117cce8e7913688be41b17fe404cd48e3b501be55cd"},
"sde": {"sha256": "56f5f7eba4b401aeb2fc1090d2db5ab9dd55cc08a396871e764df45017fa70af"}
}
```
This is source/build provenance for the running Reference Data service, not a cryptographic checksum of either `.tar.xz` artifact.
### Recommended staged promotion behavior (design guidance, not an EVE schema)
- Build each candidate in a fresh immutable SQLite database; never mutate the currently served database while parsing.
- Capture one source result for Reference Data and one for Hoboleaks: `success`, `stale`, or `failed` with URL, HTTP metadata, bytes, local SHA-256, source revision/build, and an error detail. Keep source results independent so a Hoboleaks failure is not hidden by a successful Reference Data download, and vice versa.
- Validate schema, IDs, keyed-map joins, duplicate keys, numeric ranges, and the expected fixture set before the candidate is eligible for promotion. A transport-successful but `stale: true` Hoboleaks file is visibly stale, not silently “fresh”.
- Write and fsync the candidate database and its manifest before publishing a single atomic pointer/manifest to it. Keep the previous manifest and immutable DB available for immediate rollback; a failed source or validation must leave the previous pointer untouched.
- Store both archive provenance (artifact URL/ETag/Last-Modified/local hash) and EVE Ref provenance (`/meta` build time and input hashes). They answer different questions and must not be merged into one version field.
The local manifest fields in this subsection are implementation recommendations only; they are not claims about a published EVE Ref schema.
## Gaps and non-claims
- Reference Data is explicitly marked “currently in development”; the OpenAPI spec may change without a compatibility promise.
- `reference-data-latest.tar.xz` and `hoboleaks-sde-latest.tar.xz` are release artifacts; their directory indexes do not document an internal tar member manifest or a published archive SHA-256. No full archive was downloaded to infer one.
- Reference Data currently has no documented `/systems` endpoint. Systems XYZ are covered here only as the ESI/SDE source shape identified by EVE Ref, not as a Reference Data record.
- Hoboleaks documents file names and metadata semantics but does not publish a formal JSON Schema for every raw file. No unsupported raw schema is inferred for large `blueprints.json` or `typematerials.json`.
- A `stale` Hoboleaks file can be outdated while still retrievable. Promotion policy must decide whether that is allowed; the source does not define the application's policy.
## Sources
Kept sources are limited to current EVE Ref docs/schema, the linked official ESI machine-readable schema for the explicit systems boundary, and the linked current Hoboleaks metadata/files.
- [EVE Ref Reference Data docs](https://docs.everef.net/datasets/reference-data) — source combination, download/API routes, data-source table, normalization rules, and development warning.
- [EVE Ref Reference Data OpenAPI (pinned commit)](https://github.com/autonomouslogic/eve-ref/blob/58dafd29ea55ad78fd62cbf820460cb79eb66202/spec/reference-data.yaml) — exact `InventoryCategory`, `InventoryGroup`, `InventoryType`, `Blueprint`, `BlueprintActivity`, `BlueprintMaterial`, `Coordinate`, and `RefDataMeta` schemas.
- [Reference Data API metadata](https://ref-data.everef.net/meta) — current build time and independent SHA-256 hashes for ESI, Hoboleaks, and SDE inputs.
- [Reference Data category 4](https://ref-data.everef.net/categories/4), [group 18](https://ref-data.everef.net/groups/18), [type 34](https://ref-data.everef.net/types/34), and [blueprint 683](https://ref-data.everef.net/blueprints/683) — live representative records used for fixture slices.
- [Reference Data archive index](https://data.everef.net/reference-data/index.json) and [Hoboleaks archive index](https://data.everef.net/hoboleaks-sde/index.json) — current archive URL, size, last-modified, and ETag metadata.
- [EVE Ref Hoboleaks SDE docs](https://docs.everef.net/datasets/hoboleaks-sde.html) — archive mirror route and provenance link.
- [Hoboleaks SDE landing page](https://sde.hoboleaks.space/) and [Hoboleaks `tq/meta.json`](https://sde.hoboleaks.space/tq/meta.json) — raw file inventory, update caveats, revision/MD5/stale semantics.
- [Hoboleaks `industryactivities.json`](https://sde.hoboleaks.space/tq/industryactivities.json), [agenttypes.json](https://sde.hoboleaks.space/tq/agenttypes.json), and [repackagedvolumes.json](https://sde.hoboleaks.space/tq/repackagedvolumes.json) — small current raw-map fixtures.
- [Official ESI OpenAPI](https://esi.evetech.net/meta/openapi.json) and [Jita system response](https://esi.evetech.net/latest/universe/systems/30000142/) — exact ESI systems required fields and XYZ example; used only because EVE Ref maps systems to the ESI/SDE sources rather than defining a Reference Data systems endpoint.
+360
View File
@@ -0,0 +1,360 @@
# Implementation roadmap
**Status:** planning proposal, not implementation or a public-launch declaration.
**Baseline:** the operator has verified real EVE sign-in, asset synchronization, and matching Assets/chat reference valuations. The latest implementation gate has 97 passing tests.
**Evidence:** [architecture](architecture.md), [milestone 1](milestone.md), [verification](verification.md), [ESI research](research/eve.md), [expansion data/permission matrix](research/expansion-data-contracts.md), and [reference operations](reference-operations.md).
## 1. Decisions confirmed for this roadmap
1. **Inventory and personal finances deliver the next user-visible value.**
2. **Eventually support open public signup**, not just a private installation. Public signup does not make anybody's character data public.
3. **Keep EVE-only sign-in.** Explicitly linked characters remain attached to one application user; an owner hash is never an EVE-account grouping key.
4. **Include corporation features later**, behind a separate authorization/sharing design.
5. Retain Bun/TypeScript, SvelteKit/Svelte, Convex for application state, immutable reference SQLite, Infisical, and single-host Compose initially. Do not introduce another database or model provider without a measured need and a separate decision.
6. This roadmap is **read-only intelligence and planning**: no automatic market orders, ISK transfers, asset moves, industry-job submission, or ship movement.
New service/table/path names below are proposed implementation boundaries, not existing APIs. Later phases need focused design and current contract verification before coding; they are not one large implementation task.
## 2. What exists versus what still needs building
| Area | Current implementation | Important gap |
|---|---|---|
| Identity | Verified EVE SSO/PKCE, encrypted grants, explicit linking, opaque sessions, app JWTs | Recovery, unlink/delete, transfer lifecycle, incremental scopes, session management, multi-character live tests |
| Assets | Complete staged snapshots, tenant gates, exact reference totals | Real search/filtering, paging, containment, custom names, meaningful locations, history |
| References | 53,001 types; recipes, dogma and system/gate tables; immutable publication | Runtime reader exposes only types/version; station table currently empty; search and domain-specific readers missing |
| Prices | Global ESI average prices, held by the web process | Durable public feed coordination, regional/private books, history, depth allocation, fees |
| Sync | On-demand work inside web requests; generation publication and token CAS | `jobs` is schema-only; no durable dispatcher, crash recovery, shared refresh lease or global worker budgets |
| Finances | Signed integer-cent money helpers | Wallets, journals, transactions, own orders, history/coverage and reconciliation |
| Industry | Recipe data plus placeholder interfaces/economics fields | Owned blueprint instances, skills/jobs, facility assumptions, formulas and optimizer |
| Navigation | Exact light-year distance helper and universe data | Gate-distance service, ship-aware capital graph, dynamic constraints and qualified access evidence |
| Assistant | Two typed intents and exact `/assets` / `/value`; shared deterministic report | Validated parameters, entity clarification, more capabilities, history, grounded explanations |
| Operations | Working development deployment and secure secret injection | Restore-tested backups, key rotation, deletion/retention, capacity/abuse controls, staging/release gates |
**Do not count placeholders as features:** `jobs`, `userPreferences`, and `referenceSyncMetadata` in `src/convex/schema.ts` are not implemented services. `OrderBook`, `BlueprintRepository`, and `CapitalRouter` in `src/lib/domain/model.ts` need richer contracts before use.
## 3. Recommended delivery order
| Milestone | User-visible outcome | Dependencies | Relative size |
|---|---|---|---|
| M0 — Reliable account/data foundation | Clear connection/sync status; safe upgrades and recovery | Current baseline | L |
| M1 — Useful inventory | Find items by character/location; browse containers; saved views and export | M0 dataset/scoping slice | L |
| M2 — Personal finances | Wallets, activity, own orders, and an explained wealth overview | M0 for ingestion; M1 also required for wealth | L |
| P — Public beta and signup gate | Other people can safely use isolated accounts | M0 + M1 + M2, launch checklist | L |
| M3 — Market-aware valuation | Venue-specific replacement, liquidation and sell-order scenarios | M0 + M1; M2 for own orders | L |
| M4 — Manufacturing planner | Feasible build candidates and auditable costs/profit | M1 + M3 + blueprint/skill/job feeds | XL |
| M5A — Advanced production | Multi-stage build/buy, invention/reactions and constrained planning | M4 | XL |
| M5B — Trading/procurement | Depth- and budget-aware arbitrage and shopping plans | M2 + M3 + gate-distance service | L–XL |
| M6 — Capital logistics | Ship-aware jump plans with fuel/assumptions/access evidence | M0 + M1 + character skills; independent of advanced industry | XL |
| M7 — Corporation workspaces | Authorized shared corporation assets/finances/industry | P + reusable personal services + corporation policy | XL |
| M8 — Broader intelligence | Cross-domain assistant, grounded EVE Q&A and alerts | Shipped domain services; incremental throughout | L–XL |
Sizes describe uncertainty and surface area, **not calendar commitments**: S = focused existing surface, M = several layers, L = a new subsystem, XL = multiple separately testable slices. Re-estimate each milestone after its contract/design spike.
Inventory and finance work can proceed privately while the rest of M0/public hardening is completed. Wallet ingestion can run alongside M1 after dataset/scope/job foundations; the combined wealth report waits for both. Define identity/scope/revocation and grant-revision semantics before finalizing the worker model, even if the full lifecycle UX ships later. **P is a release gate, not permission to defer safety until after public signup.** Markets, industry, navigation and corporations do not need to be finished before a limited inventory/finance beta. M5A, M5B and M6 can later progress independently once their inputs exist.
## 4. Shared implementation rules
### One service result, two interfaces
Every feature gets a deterministic service, conventional UI, and typed assistant adapter. Both consume the same validated request/result contracts and provenance. No parallel chat-only finance engine.
Extend the capability registry to declare:
- Supported operation and parameter schema.
- Required datasets/scopes and selected character/resource scope.
- Freshness/completeness policy and explicit missing requirements.
- Server-derived principal and authorization revision.
- Result schema, assumptions, exclusions, source timestamps and versions.
A user/character ID supplied by a browser or JEV is a selector, never authorization. Resolve entities against the caller's allowed set; clarify ambiguous names. Do not broaden an unsupported filtered request into an all-assets answer.
### Immutable generations and honest coverage
Use per-dataset states rather than making the current asset state stand for wallets, skills and everything else. A proposed `datasetSyncStates` record identifies principal, character/resource, dataset, grant revision, active generation or coverage checkpoint, status, timestamps and redacted failure category.
- Snapshot datasets stage and validate before publishing a generation.
- Append-oriented datasets use idempotent source-ID ingestion, bounded pages/checkpoints and explicit coverage windows; they must not pretend to have an atomic unlimited history.
- Reports pin the selected input generations/checkpoints, reference version, price snapshot and assumptions. ESI is not a cross-endpoint transaction: show source skew and unavailable components. Keep fetch/revalidation time separate from an upstream effective timestamp; when the latter is not supplied, leave it unknown.
- New auth/scope state invalidates affected jobs and private caches. Keep old generations for controlled recovery only; do not silently serve them as currently authorized/fresh data.
- Pagination and exports use the **same query scope and pinned inputs** as the summary. Totals cover the selected result set, not just the visible page.
### Money and inventory correctness
Reuse integer-cent arithmetic in `src/lib/domain/money.ts`. Add a shared, lossless **signed decimal ingestion** policy: the current ESI price converter is private and intentionally non-negative, while wallet debits and profit can be negative. Fix the UI money formatter for negative values before finance delivery.
Keep ESI int64 event/order IDs and cursors as decimal strings through requests, persistence and UI. Validate quantity bounds or retain a lossless representation; never silently coerce an out-of-range value through JavaScript `Number`.
Keep valuation methods distinct. Never quietly replace an average price with an adjusted price, best order, or zero. Track exclusion reasons and unit rounding. Wallet cash, open-order commitments, listed goods, industry work-in-progress and inventory can overlap: build a documented reconciliation model before summing them into wealth.
### Data boundaries
- Public reference facts stay in immutable SQLite; extend its schema/readers through versioned compiler builds.
- User state, financial records, jobs, saved reports and permission metadata stay in Convex initially, with indexes and bounded operations.
- Dynamic prices remain outside the immutable reference database. Start with bounded watched-venue/item feeds behind a market repository contract. Benchmark storage/query/write volume before selecting the production order-book backend; no automatic universe-wide ingestion or unapproved database migration.
- Public regional market caches may be shared. Authenticated structure-market/location data must stay authorization-context scoped unless an explicit, valid sharing policy applies.
- Never send grants, cookies, wallet journals or whole asset inventories to JEV/research services. User-entered text sent for classification still needs public-facing provider/privacy disclosure and bounded retention/spend.
## 5. M0 — Reliable account and data foundation
### Deliverables / work packages
**M0.1 — Reproducible release and safety baseline**
- Review secret exclusions and establish an operator-approved version-controlled baseline; no commit is performed by this planning work.
- Add isolated CI for tests/typecheck/build, deployed-artifact checks, migrations and browser flows.
- Add durable browser regression coverage for native POST forms, OAuth redirect/cookie paths, singleton/array scopes, linking, logout and pending-request resume. Use synthetic providers/users only in isolated test deployments, never the persistent live backend.
- Version application images; keep backwards-compatible migrations and an explicit rollback procedure.
- Define and implement server-side rollout gating; it does not exist today. A deployment configuration/capability allowlist may suffice initially, but the mechanism needs approval and tests. Gates must protect routes, assistant dispatch, jobs and subscriptions—not merely hide UI controls.
**M0.2 — Dataset capabilities and incremental consent**
- Extend the model/gate/store contracts for dataset-specific scopes and freshness.
- Replace `EveSso.authorizationUrl`'s hard-coded asset-only request with a server allowlist selected by capability.
- Bind requested scopes, operation/link intent and initiating session to one-time OAuth state; save actual verified grants, not requested grants. Preserve required existing scopes deliberately, and handle denied/reduced grants without discarding the account.
- Add character management: scope/status summaries, explicit upgrade/reconnect actions, per-dataset sync and cancellation.
- Extend the centralized ESI transport with typed endpoint/pagination strategies and explicit read-only POST helpers, not an arbitrary URL/method executor. Asset-name/location lookups need lossless outbound int64 JSON and cache keys including canonical request bodies/auth context. Do not reuse safe-read retry rules for OAuth exchange/refresh.
**M0.3 — Durable Bun sync worker**
- Implement `jobs` rather than adding more long-running work to `POST /api/sync`.
- Add deduplication, typed job states, due times, bounded attempts, cancellation, lease heartbeats/fencing and dataset generation/checkpoint publication. Distinguish server-owned reference/public-feed jobs from user-owned jobs and later corporation jobs; callers cannot promote their own job to system authority.
- Use at-least-once job delivery with fenced, idempotent publication—not an exactly-once promise for remote OAuth calls. Renew leases during long paging/staging work; the current five-minute asset lease can be exceeded by large account syncs.
- Share a per-character credential-refresh lease across web/worker processes; existing CAS alone cannot prevent two remote refresh calls.
- If a refresh/code exchange result is uncertain or lost after rotation, do not blindly replay it. Persist an actionable reauthorization state where safe recovery is impossible.
- Centralize scheduling/cooldowns across workers and share public feeds; respect ESI TTLs, per-character/group limits and the legacy host error budget. Add per-user queue fairness and backpressure.
- Keep heavy reference extraction in its separate bounded worker; publish small reference health metadata to Convex only. Give each worker only its required secrets/capabilities; reference extraction needs no EVE grants, JEV key, JWT signing key or deployment-admin credential.
**M0.4 — EVE-only identity lifecycle**
- Treat each linked character as a login credential under the existing model. Explain that anybody able to authenticate as one of those characters can access the application account: linking is not merely read-only data sharing. Test transfer, revocation, unlinking and loss of access.
- Require fresh proof through an already trusted login character for sensitive account actions; the new character being added is not sufficient step-up proof for adding a login credential. List/revoke sessions and detach credentials safely. Review host-bound cookie naming and test sibling-subdomain cookie injection/session fixation before public launch.
- Make session replacement and prior-session revocation atomic where possible, otherwise explicitly compensating. Revalidate the initiating link/session authority at persistence time. Test failures after grant/session persistence but before old-session revocation or browser-cookie delivery; the current callback spans separate operations.
- Until an approved recovery mechanism exists, reject unlinking the final login character; account deletion is a separate confirmed action. No password/code-only login or recovery is introduced by this plan. If every linked character is inaccessible, explicitly accept/document loss of access or obtain approval for a different recovery policy before P.
- Reauthenticating an existing linked character restores the same application account. Explain that users must first sign into that account before linking a new alt. A signed-out login with an unlinked alt can create a separate application account; matching owner claims must not merge them. Document accidental-duplicate handling: authenticate the accounts separately, explicitly export/delete an unwanted account if chosen, then relink only after its binding is safely released. Do not silently migrate its history.
- A new owner of a transferred character must never inherit the old user's history or account. Quarantine/reconcile ownership changes and invalidate affected credentials/sessions; never merge accounts or trust character names as recovery proof. Test that a verified new owner can eventually obtain a separate active binding without the old row permanently blocking signup, and that unrelated old-user characters remain separate.
- Disable local access/cancel jobs first when removing authorization, then attempt remote revocation and purge credentials. If retry requires retaining token material, use a short-lived encrypted revocation-only outbox with no active-sync access and an explicit deletion deadline. Report unavailable EVE revocation as a cleanup obligation, not successful remote revocation.
**M0.5 — Retention, backup, restore and rotation**
- Define bounded retention for staging data, completed raw generations, journals, interactions, exports, jobs, audit events and deleted accounts. Retained summaries must not silently defeat deletion requests.
- Replace unbounded full-report interaction payloads with versioned, size-limited summaries and input references. Specify what happens when referenced detail expires; do not duplicate entire inventories/journals into conversation history or claim expired detail is reproducible.
- GC may not delete ACTIVE/PREVIOUS artifacts or generations still referenced by valid report cursors/jobs.
- Restore-test Convex data **and instance credentials**, reference manifests/artifacts, Infisical/key backups and configuration in isolation.
- Handle post-backup refresh-token rotation: a restored old token is not necessarily usable. Invalidate restored sessions before reopening traffic and flag grants needing reconnection.
- Design versioned encryption envelopes/key IDs, multi-key migration, fencing with token refresh, signing-key overlap and coordinated bridge-key rotation. Do not regenerate encryption keys over existing ciphertext.
**M0.6 — Reference release operations**
- Schedule metadata checks and bounded rebuilds; keep compatibility-date/schema upgrades reviewed rather than automatically following today's date or a new upstream contract.
- Record release/alias/resolved-source provenance, integrity limits and unsupported members. Locally computed hashes are not upstream signatures; validate published authenticity evidence when available without claiming it exists today.
- Test new release fixtures, offline replay and rollback; measure peak memory/disk/time, set worker limits and reclaim only unreferenced artifacts/archives.
- Require the deployed web reader to verify the actual mounted production artifact after promotion. An HTTP 200 login page or successful worker stdout does not prove catalog readiness.
### Primary code surfaces
`src/lib/domain/{model,capabilities}.ts`, `src/lib/server/{application,sso,security,store,http}.ts`, `src/convex/{schema,functions,status}.ts`, auth/sync/session routes, a proposed `workers/sync.ts`, Compose/operator tooling and tests.
### Acceptance gate
- Two isolated users cannot cross-read/query/export each other's data, including via borrowed cursor/job/report IDs. Same-character reauthentication returns the same account; different characters do not merge merely because owner claims match. Explicit linking and duplicate-account handling are tested.
- Native browser login/refresh works with singleton and array scopes. Changed/revoked initiating sessions cannot hijack linking. Inject failure after login persistence, after old-session revocation, and before cookie delivery; reconcile unissued sessions within a bounded deadline. Retrying after an uncertain callback cannot replay a consumed OAuth code or create duplicate accounts/unbounded active orphan sessions.
- Once transfer/revocation is detected, active private reads/downloads and scheduled jobs stop; credentials are invalidated or restricted to bounded revocation cleanup. The new owner receives none of the old user's snapshots, history, exports or financial data. Define and test the maximum stale-authority window rather than promising instantaneous EVE revocation detection.
- Worker termination/restart, lease expiry, duplicate jobs, scope loss and uncertain token rotation cannot overwrite newer valid state, including through a stale worker's failure report.
- Limits for linked characters, assets/pages/chunks, concurrent jobs and exports are explicit. Convex transaction/read/write limits and existing hard caps are exercised. Reaching a cap fails visibly without truncating and publishing a supposedly complete dataset.
- An isolated restore and migration rollback are demonstrated. Public quota, retention and recovery policies are documented; no unlimited defaults are assumed.
## 6. M1 — Inventory that is useful every day
### Deliverables
1. **Reference and location readers:** indexed normalized type-name search, group/category lookups, systems/regions and NPC stations. Import a verified station source; the current empty table is not coverage. Add private structure-name resolution with explicit scope upgrades and per-character evidence caches.
2. **Inventory read model:** generation-bound indexed rows/projections for queries instead of repeatedly returning all raw asset chunks. Extend ingestion/domain/Convex schemas to retain ESI `is_singleton` as `isSingleton`, alongside parent, location type/flag and blueprint-copy fields. The current model drops it. Preserve validated source facts and stage derived indexes before publication; legacy rows with missing fields remain unknown until refreshed, not guessed from quantity.
3. **Search/filter/grouping:** item or custom name, character, category/group, quantity, location/system/region, priced/unpriced and blueprint-copy state. Provide stable server-side sorting/pagination and selected/all-character coverage.
4. **Containment:** ship/container contents, fitted flags and breadcrumbs. Resolve item-parent relationships without numeric-ID guessing; detect cycles, missing parents and ambiguous/stale locations. Never multiply hull value by contents.
5. **Saved views and exports:** schema-validated filters/preferences instead of arbitrary `v.any()` payloads, CSV and user-private report links. Use unguessable or authenticated identifiers, but never treat possession of an ID as authorization. Recheck tenant, selected resources and authorization revision on every query/download. Keep generated files out of public static storage, bound size/duration/retention, and handle account deletion. Escape spreadsheet formula injection; no public share links by default.
6. **Limited history:** snapshot differences and retained summary checkpoints, labelled as observations rather than inferred trades. Do not fabricate history predating collection.
7. **Assistant parity:** validated item/location/character parameters, deterministic entity resolution, clarification and the same filtered report/summary as the UI.
### Proposed surfaces
Inventory query/result contracts; `InventoryQueryService`/`LocationService`; generation-indexed Convex projections; new reference repository methods; ESI asset-name and structure helpers; `/assets` and shared result components; assistant intent/parameter handling. Extend or wrap `AssetService` rather than cloning its authorization/valuation rules.
### Acceptance gate
Tests cover singleton versus stackable items, legacy missing fields, nested containers, fitted items, blueprint copies, identical custom names, unknown structures, very large IDs and missing parents. Sorting/page boundaries are stable against a pinned snapshot. Selected totals equal aggregation over all matching rows, not the current page; they match the assistant. Forbidden structures remain unresolved rather than leaking another character's cached name. Saved-view validation and borrowed report/export IDs cannot bypass tenant or grant-revision checks. Losing access invalidates future downloads/cursors; already downloaded files cannot be recalled. Oversized or timed-out queries/exports fail visibly within measured Convex/resource budgets without partial-as-complete output.
## 7. M2 — Personal finances and an explained wealth overview
M2.1 can proceed in parallel with M1 once M0's dataset/scope/job foundations exist. M2.2 follows finance ingestion; M2.3 waits for M1 and tested component reconciliation.
### Deliverables
**M2.1 — Wallet balances and activity**
- Character balances, signed journal entries, market transactions and independent freshness indicators.
- Dataset-scoped opt-in authorization, incremental collection, deduplication by documented source identity, bounded historical backfill and explicit gaps. Journal history is limited to 30 days by the pinned contract; transactions use `from_id` and have no verified retention horizon. Do not apply one paging/backfill strategy to both.
- Search/filter cash activity by character, time, reference type and counterparty; classify categories deterministically before optional semantic presentation.
- Use journal entries as the ledger source and transactions as trade detail: do not sum the same cash movement twice. Match internal transfers only with reliable evidence, not merely equal amounts/times.
**M2.2 — Own orders and financial context**
- Active/historical personal orders, remaining quantity, order state and documented escrow/commitment semantics. Character orders can carry `is_corporation`; exclude those from personal totals until the corporation authorization/workspace phase. Open character orders are unpaged; history is paged with a documented 90-day horizon.
- Treat an order disappearing from a snapshot as unknown until history/transactions establish filled, cancelled or expired state.
- Keep cash balance, locked/committed funds and estimated remaining listed goods separate until a tested reconciliation policy says how they combine.
**M2.3 — Wealth dashboard and history**
- Compose wallet cash plus **explicitly labelled reference-valued** inventory and separately explained other components. Show gross components, overlap exclusions, missing inputs and source times. Contracts/collateral, industry work-in-progress and other unmodelled positions must be named exclusions—not silently included in a supposedly complete net worth.
- Record valuation method/reference/price/input versions with historical points. Never backfill past wealth from today's prices/current inventory; later restatements need separate labels. Asset-price appreciation is not realized trading income, and missing journal history prevents claims of lifetime profit or exact reconciliation.
- Budgets/watchlists/summary exports and the same typed finance queries in chat. Default privacy is per application user; no public portfolio or leaderboard.
### Proposed surfaces
Wallet/finance/order repository ports and services; Convex balance snapshots, journal/transaction records and checkpoints, order state/history, financial summaries; ESI wallet/order adapters; a compact finance UI and shared money formatter.
### Acceptance gate
Negative/large balances and debits preserve exact cents through lossless JSON ingestion, signed decimal normalization, totals, charts and export, with explicit rounding and negative-format tests. Journal/transaction/order IDs and cursors above `2^53` remain decimal strings. Re-fetches and overlapping/out-of-order pages are idempotent by principal, dataset and external ID. Finite source history/gaps are visible. Paired internal transfers are not income; ambiguous matches remain unclassified. Inventory, orders and cash cannot be counted twice. Partial datasets do not masquerade as complete wealth/profit. UI and assistant produce matching results for a pinned report.
## 8. P — Public beta, then open signup
Start with a controlled beta; open registration only after the following evidence exists. No public hostname, capacity target, launch date or availability promise is selected by this document.
- **Identity:** EVE-only recovery/last-character policy implemented and tested; sensitive actions reauthenticated; account deletion/export, transfer handling and session revocation work.
- **Isolation:** adversarial two-user tests across every query, mutation, reactive subscription, job, cache, export, history and assistant path; later corporation access is independently gated.
- **Live verification:** record a separate consenting-character matrix for first login, second-character linking, signed-out same-character reauthentication, incremental/reduced scopes, refresh rotation, grant revocation/reconnect, and logout/session revocation. Reuse existing valid first-login/assets evidence rather than needlessly repeating it. Exercise callback/logout backend outages in an isolated staging deployment. Record actual owner-transfer verification only when a legitimate consenting case exists—do not buy/transfer a character just for a test. Any untested provider behavior remains explicitly unverified and needs a conservative policy or launch-blocker decision before P.
- **Abuse/cost:** configurable signup admission, character/dataset/job limits, per-user and global ESI/JEV budgets, request/token/output bounds, concurrency limits and provider kill switches. Gate paid inference before calling JEV; anonymous users can retain deterministic connect/manual-command flows without unrestricted paid classification. Do not rely only on process-local IP windows or trust arbitrary forwarded headers.
- **Deployment:** separate staging/production data and secrets within the operator's existing Infisical project; confirm public hostname/callbacks and data migration explicitly. Keep the current development deployment intact. Never copy encrypted data and then casually replace its key.
- **Ingress:** restrict direct wildcard-bound ports to intended ingress; keep admin endpoints private. The optional dashboard must remain disabled/private; if deployed outside development, pin a tested image digest instead of `latest`. Remove the main-app Pangolin login gate only as an explicit public-launch action. Validate HTTPS, secure cookies, WebSockets, proxy trust and callback/query redaction in proxy logs.
- **Security:** review dependencies, authorization and cryptography; test CSP compatible with Svelte/Convex, referrer policy/native forms, clickjacking, exports and secret-free artifacts/diagnostics. Red-team prompt injection and tenant-bound parameter selection.
- **Operations:** measured load/soak tests, disk/queue/ESI budgets, alerting, tested backup/restore/rotation/deletion, incident and outage procedures. Define capacity, retention, RPO/RTO and maintenance expectations before publishing them.
- **Product/privacy:** permissions explanation, data retention/deletion and external-model disclosure, CCP attribution/terms review, accessible keyboard/mobile flows and safe error/support references. Infrastructure operators remain trusted; this is not zero-knowledge storage.
- **Release:** migration rehearsals, immutable release identifiers, rollback and staged feature flags. Keep fixtures/synthetic write smoke tests on isolated backends; real production checks use explicit consenting accounts.
## 9. M3 — Market data and realistic valuation modes
### Deliverables
- Public regional/venue order books for a **bounded configured universe**, plus authenticated structure books when explicitly authorized. Start narrow; measure before increasing coverage.
- A richer market snapshot contract: order IDs, side, venue/system, range/minimum volume, remaining depth, timestamps, completeness, source/auth context and version.
- Gate-distance/location eligibility needed for ranged buy orders. Market range is not Euclidean light-year distance.
- Depth-consuming replacement and estimated liquidation scenarios. Deduplicate order identity across eligible feeds; allocate a shared book once across all included stacks/characters, never reusing the same remaining volume per row.
- Explicit taxes/fees/standing/skill assumptions, minimum-volume rules, inaccessible/private locations and unfilled residuals. Start with user-confirmed fee-rate assumptions; automated rate derivation waits for the relevant skill/standing adapters, avoiding a dependency cycle with M4. Distinguish local proceeds from a hypothetical destination sale with separately modelled transport cost.
- Sell-order scenarios with evidence-based volume/competition assumptions and uncertainty; no guaranteed fill time or executable quote.
- Reference estimates remain available as their existing, explicitly selected method—not silently upgraded into Jita prices.
### Acceptance gate
Golden scenarios cover insufficient depth, shared-order exhaustion, minimum-volume boundaries, order range, stale/partial pages, own-order interactions, missing fee inputs and private-cache isolation. Every result explains gross amount, costs, eligible/filled quantity, residuals and observation time. A fresh ESI snapshot is still not an execution guarantee.
## 10. M4 — Manufacturing planner
### Deliverables
- Sync owned blueprint **instances** (BPO/BPC, ME/TE, remaining runs), trained skills, skill queue, current industry jobs and input availability. Reference recipes alone do not establish ownership or eligibility. Reconcile documented stale skill levels with completed queue entries; do not count future training as current eligibility. Completed industry-job history is limited to 90 days, not a complete production ledger.
- Extend recipe/skill/dogma readers; explicitly block or exclude recipes with unresolved inputs rather than treating the 21 currently dangling references as zero-cost materials.
- Versioned material/time/job-cost calculations, using verified current rounding rules and independent worked examples. Require facility/system/rig/service/tax assumptions; distinguish verified data from user assumptions.
- Use adjusted prices only where the verified installation-cost formula requires them, never as a fallback asset sale valuation.
- Basic manufacturing feasibility and bounded candidate enumeration; compare using selected market scenarios from M3.
- Separate revenue, additional cash, owned-material opportunity value, economic cost, cash-flow gain, economic profit/margin, production time and ISK/hour. State blueprint acquisition/amortization and consumed-copy cost policies; do not silently price an unknown BPC or other consumed input at zero.
- Plans account for blueprint runs, skills, materials, current jobs, slots, budget and location; projected output is not already-owned inventory.
### Acceptance gate
Known jobs match independently checked material/time/cost examples. BPC run limits, insufficient skills, occupied slots, missing facility assumptions and unsellable outputs produce actionable constraints. Shared materials and market liquidity cannot fund/sell multiple candidates simultaneously in a selected plan. “Owned materials are free” is never used to claim profit.
## 11. M5 — Advanced production and trading branches
### M5A — Production optimization
Add build-versus-buy dependency expansion, component/capital chains, invention/copying/research/reactions, batch/slot scheduling and procurement. Probabilistic outputs need expected values **and variance/assumptions**, not guaranteed quantities. Bound search, detect recipe cycles, report unmodelled mechanics and label heuristic versus proven optima. Evaluate a solver only after benchmarking bounded deterministic approaches behind a planner interface.
Acceptance: no double allocation of shared stock/blueprint runs/slots/cash, correct stochastic accounting, reproducible plans and useful timeout/partial-search results. More recipes in SQLite alone are not an optimizer.
### M5B — Arbitrage and procurement
Add watched-item/venue opportunities, source ask-depth consumption, destination bid-depth consumption, taxes, transport/cargo/budget constraints and opportunity expiry. Gate-route distance can support basic hauling assumptions; capital logistics is a later enhancement, not a prerequisite for all trading.
Acceptance: exclude stale/negative-net opportunities, reserve shared liquidity across recommendations, account for cargo/fees, and never present turnover estimates as guaranteed profit. Outputs are explainable shopping/sale plans, not automatic orders.
## 12. M6 — Capital navigation and qualified access
### Deliverables
- Separate gate and jump legs; index immutable system coordinates for bounded range-neighbour searches. Add verified space/ship-classification metadata where current readers are insufficient; security status or geometric proximity alone does not make a system jump-eligible. Reuse the exact `9,460,000,000,000,000` metre light-year constant.
- Verify hull base range, calibration/eligibility and current jump restrictions; add fuel/cargo, cyno availability/type, cooldown/fatigue and timing assumptions where the available data supports them.
- Avoid systems/regions and unsupported spaces; offer explicit objectives such as fewer jumps, fuel or qualified dockable stops. Return “no feasible route” rather than violating constraints.
- Per-character, per-structure, per-ship-class access evidence with TTLs and limitations. Visibility and ACL membership alone do not establish docking. A Fortizar preference cannot make it suitable for a ship class that cannot dock there.
- Support explicitly entered/verified waypoints where ESI cannot enumerate private structures, cynos or access relationships. Mark coverage gaps; do not claim a complete structure catalogue or real-time safety.
- Enrich the placeholder `CapitalRouter` result with typed legs, distances, fuel, required assumptions, evidence, objective terms and completeness instead of a bare system list/weight.
### Acceptance gate
Reference route fixtures cover boundary distances, unsupported hulls, blocked regions, disconnected space, differing skills, fuel/cyno limits, ship-class docking restrictions and expired access evidence. A geometric route is not presented as executable or safe. Live observation tests use consenting characters and do not move ships.
## 13. M7 — Corporation workspaces (later)
A corporation is a distinct authorization resource, not another character on an application user.
- Design explicit workspace membership/permissions, qualifying credential providers and dataset-level EVE scope/role checks before ingestion.
- Verify corporation membership and current roles; invalidate authority on role loss, transfer, departure or revoked credentials. A valid character JWT alone does not grant corporation access.
- Reuse snapshot/jobs/financial/industry services through an explicit resource-principal contract. Never substitute a browser-provided corporation ID for an authorized principal.
- Add corporation assets, wallet divisions/activity, blueprints/jobs and relevant orders, with endpoint-specific roles and division restrictions verified from current contracts.
- Separate personal portfolios from shared corporation reports. Workspace members do not automatically receive each other's personal wallets/assets.
- Define credential-provider departure, administrator succession, multiple providers, account deletion and corporation data-retention policies; do not leave authority anchored to one departed director.
Acceptance: endpoint/role/division permission matrix, immediate local revocation/fencing once changes are observed, stale-authority behavior, provider failover without credential leakage, personal/corporation isolation and adversarial multi-user workspace tests. Earlier personal feature completion does not waive this gate.
## 14. M8 — Assistant, knowledge and alerts
Ship assistant coverage with each domain milestone rather than leaving chat unsupported until the end:
- M1: typed inventory filters and entity clarification.
- M2: wallet/activity/wealth queries and explicit time windows.
- M3–M6: valuation scenarios, build/trade plans and routes with source-backed explanations.
- M7: explicit personal-versus-corporation context and authorization checks.
Then add user-scoped conversation history, saved questions, reusable scenario inputs, comparisons, and in-app alerts on typed conditions evaluated against completed snapshots. Deduplicate alerts, respect quiet/rate limits, and recheck access at delivery/read time. External notification channels need separate privacy/egress decisions.
Broader EVE Q&A should retrieve curated/versioned public sources with citations and freshness, distinguish game knowledge from private live account facts, and abstain outside evidence. JEV currently supplies typed classification, not an established general-purpose answer generator. Evaluate synthesis capability/provider/privacy/cost before adding a generative model; never give it arbitrary SQL, URLs, credentials or authority over arithmetic/actions.
Acceptance: per-capability adversarial/evaluation corpora, measured routing/clarification accuracy rather than assuming 0.80 is calibrated, shared UI/chat report equality, explicit stale/unknown responses, tenant-safe history and no secret/private-dataset leakage to external models.
## 15. First implementation batch after this plan
Keep the initial branch focused; do not start every milestone at once.
| Work item | Concrete output | Proof of completion |
|---|---|---|
| 1. Baseline/release safety | Isolated test/deploy fixture and backup/migration rehearsal | Existing 97 tests remain green; restore runbook exercised without touching live test identities |
| 2. Dataset, scope and identity contracts | Typed dataset status, scope-upgrade binding, grant revisions and revocation/transfer semantics | Scalar/array grants, reduced scopes, explicit linking, stale-session and job-invalidation tests |
| 3. Durable asset job slice | Queue/worker/lease/refresh fencing for the existing asset endpoint | Kill/restart/duplicate/uncertain-refresh tests preserve good generations |
| 4. Reference/location slice | Runtime search/group/system/station readers + verified station import | Versioned artifact tests; unknown/private locations stay unknown |
| 5. Inventory query slice | User-bound filters, stable paging, complete selected summary | Cross-user cursor tests, large-account tests, UI/chat parity |
| 6. Inventory UX slice | Search, character/location selectors, containment, saved view/export | Native-browser/keyboard tests and operator walkthrough |
| 7. First finance slice | Wallet balance dataset and separately labelled dashboard component | New-scope consent, exact signed money, incomplete-coverage tests |
After the foundations, work items 4–6 (inventory) and 7 (wallet balances) can proceed in parallel; a combined wealth report cannot. Complete the remaining M0 lifecycle/public controls before P even if these private-use slices ship earlier. Journal/transaction history and own orders follow wallet balances as separately reviewed vertical slices.
## 16. Decision and verification register
| Decision / uncertainty | Resolve by | Required evidence |
|---|---|---|
| EVE-only recovery, duplicate accounts and last-character unlink | M0.4, before P | Same-known-character reauthentication; no standalone recovery-code/password authenticator or owner-claim merge. Document duplicate-account handling and all-character-loss policy; changes to recovery proof require separate approval |
| Public admission/capacity and operating budget | M0/P | Target users/characters, sync freshness, ESI/JEV budgets, measured load and disk model |
| Retention, exports/deletion and RPO/RTO | M0/P | Approved policy plus GC/restore/deletion rehearsals |
| Refresh uncertainty, job leases and callback recovery | M0.2/M0.4 design before M0.3 | Distinguish credential versions from grant revisions; fenced success/failure publication; bounded orphan-session/revocation cleanup; crash/retry tests without blind OAuth replay |
| Maximum stale-authority window | M0/P and M7 | Personal and corporation revalidation policy, scheduling budget, outage behavior and immediate local quarantine once changes are observed |
| Rollout gates and dependency images | M0.1/P | Choose/test server-side capability/tenant/config gating; pin deployed production/admin images, including any optional dashboard |
| Proxy/IP identity, JEV disclosure and paid-use policy | M0/P | Document trusted proxy chain; spoofed-header tests; provider/privacy disclosure; durable user/global budgets and no unrestricted anonymous paid inference |
| Authenticated cache sharing | M1/M3/M7 | Default tenant/grant-context isolation for private locations/structures; separately approved sharing authority and revocation tests |
| Finance backfill and incomplete-history semantics | M2 design | Source-specific windows/cursors, decimal-string IDs, deduplication, explicit gaps and no invented opening balances or lifetime profit |
| Production hostname, environment and dev-data migration | P | Operator confirmation, separate secrets/data, safe encryption migration |
| New ESI route scopes/roles/pagination/history | Before each adapter | Use the [verified planning matrix](research/expansion-data-contracts.md), capture pinned fixtures, and opt in to live checks; transaction/market-history retention and real corporation-role behavior remain unresolved |
| Personal contracts/collateral and broader wealth coverage | After M2 core, before extending its completeness claims | Confirm product scope and research the additional routes/grants; keep unsupported positions explicitly excluded meanwhile |
| Market venues, fee inputs and dynamic feed storage | M3 design | User-configured scenarios, verified fee/range semantics and ingestion benchmarks |
| Industry modifiers, rounding and planner scale | M4/M5A | Current formula sources, worked examples, bounded-search benchmarks |
| Capital/cyno/access facts ESI cannot supply | M6 design | Explicit manual/verified inputs and limitations; never invent docking or safety |
| Corporation sharing/provider succession | M7 design | Permission matrix and revocation/transfer/failover tests |
| Broader answer generation / external notifications | M8 design | Explicit provider/egress/privacy/budget approval |
## 17. Definition of done for every slice
- Current source contract and migrations documented; implementations distinguish fixture, production and unavailable data.
- One shared deterministic service with validated inputs, authorization, completeness and provenance.
- Conventional UI plus typed assistant/manual fallback where applicable.
- Unit/property tests for arithmetic and invariants; isolated integration, native-browser and adversarial tenant tests for changed boundaries.
- Bounded resource use, safe errors, appropriate logs/metrics, retention/deletion behavior and rollback.
- No real-user write smoke against the persistent deployment; consented live validation is recorded separately from mocks.
- The server-side rollout/config gate implemented in M0 remains closed to other users until the slice's acceptance gate is met. It is a planned mechanism, not an existing feature. Public signup remains a separate, explicit operator decision.
+28
View File
@@ -0,0 +1,28 @@
# Security invariants and validation gates
These are requirements for the first implementation, not a claim of a production security audit.
- EVE character IDs identify characters, not EVE accounts. Application-user ownership is the aggregation boundary. No EVE account discovery exists here.
- SSO callback must verify OAuth state, browser binding, expiry, one-time use, PKCE, token signature, issuer, audience, authorized client, subject and granted scopes. A character-owner change cannot silently reuse the previous owner's identity.
- Connecting another character is an authenticated, explicit operation. Callback must preserve the initiating user/session binding. Character ownership conflicts fail closed; there is no implicit merge or transfer.
- Refresh tokens are envelope encrypted using a server-held encryption key with authenticated associated data. No raw refresh token or client secret may be a browser property, public Convex query field, URL parameter, log or exception message.
- Browser sessions use high-entropy opaque identifiers, server-side hashes, HttpOnly/SameSite cookies and bounded expiration. Application JWTs use a dedicated issuer/audience/key and short expiry, not EVE bearer tokens. Queries independently enforce the application identity and ownership.
- Server bridge authority is distinct from the deployment administrator credential and browser sessions. All privileged Convex entrypoints require it. Deployment keys are not application runtime defaults.
- State-changing browser requests require an exact allowed origin. Normal app pages use `Referrer-Policy: same-origin`: global `no-referrer` would make native POST navigations send `Origin: null` and fail CSRF verification. Auth responses retain `no-referrer` so callback grant URLs never become referrers. Null/missing/foreign origins remain rejected. No arbitrary user-provided fetch URLs, SQL identifiers, executable function names or tool invocations.
- Market/ESI errors are redacted before display. Observability reports route, status, timings and cache/rate-limit signals, never Authorization headers, OAuth codes or secrets.
- Authenticated ESI caches are keyed by character and authorization context. A partial sync cannot replace a complete snapshot. Token refresh is serialized for a character; stale token rotations cannot overwrite newer credentials.
- Fixture/demo data is explicitly opt-in and visibly marked. Live errors never trigger a silent fallback to demo assets or prices; live reference readers require a production-profile artifact.
- Reference archives are untrusted input. The production importer reads members without extracting arbitrary filesystem paths, rejects unsafe paths and unsupported member types, and enforces compressed/decompressed/member limits, XZ memory limits, archive checksums, and schema validation. Artifact paths/manifests must remain inside the configured reference directory. Cache reuse and fallback verify canonical hashes and the active artifact’s bound provenance; fixture caches cannot satisfy a production import.
- Infisical bootstrap credentials stay in an owner-only local file, separate from application keys. The launcher pins the approved instance and selected project/environment/path, suppresses raw credential-command output, and does not pass bootstrap credentials or the vault token to the application. Application secrets are injected only from an allowlist; deployment admin access requires an explicit operator command. Docker/host administrators can still inspect container environment values.
- No model determines permissions, financial arithmetic, ownership, scope grants, docking rights or whether a synchronization succeeded.
## First-pass operational limits
- Explicit character synchronization revalidates the refresh grant and owner continuity even if ESI asset pages remain fresh in the cache. Ordinary report reads use previously authorized snapshots until their advertised expiry; there is no claim of instantaneous EVE revocation/transfer detection.
- Logout clears the local cookie even if Convex is unavailable, and reports when server-side revocation could not be confirmed. A copied server session may remain valid until normal expiry in that failure case.
- OAuth initiation is origin-checked and rate-limited. Expired OAuth states are reclaimed in bounded batches during subsequent initiation; this is not a full retention service.
- Old asset generations, session tombstones and interaction summaries still need production retention/GC. Monitor disk usage and do not expose this initial deployment to untrusted multi-user traffic before that work. Pending OAuth state is consumed before token exchange deliberately: an uncertain exchange must restart authorization rather than risk replaying a one-time code.
## Deployment gate
Before exposing a host: configure HTTPS and exact origins/callback URL; generate independent high-entropy signing/encryption/bridge secrets; pin backend image digests; back up and restore-test Convex and reference artifact volumes; exercise live SSO/refresh/revocation, account separation and partial-sync recovery. Restrict the dashboard/admin ports to loopback or a management network. Establish secret rotation, account recovery/unlink and retention procedures before inviting other users.
+57
View File
@@ -0,0 +1,57 @@
# Verification record
Initial milestone verified locally on 2026-09-18; live development rollout checked on 2026-09-19 UTC with Bun 1.3.14. Dependency versions are pinned in `package.json`/`bun.lock`.
## Initial offline/disposable-backend milestone
- `bun run check`: Svelte/TypeScript diagnostics pass. TypeScript 6.0.3 is intentional; the installed Svelte checker requires extra compatibility setup for TypeScript 7.
- `bun test`: offline tests exercise domain ownership/freshness, precise integer-cent accumulation, unknown/unpriced/BPC behavior, typed JEV routing and abstention, cryptographic envelopes, PKCE, SSO JWT checks, Convex ownership/session/snapshot rules, ESI cache/pagination/limiter behavior, and reference promotion/rollback/source failure handling. Final run: **66 passed, 0 failed**, across 10 files.
- `bun run build`: production Bun server builds. `svelte-adapter-bun` may emit an unresolved `bun:sqlite` warning while leaving that Bun builtin external. This is not a missing npm dependency: the resulting server and container were executed successfully under Bun.
- `scripts/smoke-web.ts`: executed against both the built production server and the final Docker web container in explicit fixture mode. Assistant, Characters and Assets SSR routes succeeded. `/value` and `/assets` returned the same 208,600,000 integer cents (2,086,000.00 ISK), two characters and two excluded/unpriced stacks. Foreign origins and oversized request bodies were rejected.
- Browser-bundle inspection: no references to `TOKEN_ENCRYPTION_KEY`, `CONVEX_BRIDGE_SECRET`, `APP_AUTH_PRIVATE_KEY`, `refresh_token` or `TYPESAFE_API_KEY` in `build/client`.
- `docker compose config --quiet` and `docker build`: passed. The containerized fixture worker also executed with `--network none`; no archives were fetched.
- Real self-hosted Convex: deployed functions/schema/auth to an isolated disposable backend and executed `scripts/smoke-convex.ts`. Verified session lookup, asset publication, preservation after failed staging, cross-user isolation, safe status projection, correctly signed wrong-audience JWT rejection, and session revocation. This caught and fixed an incorrect attempt to read reserved `aud` as a Convex identity custom claim; audience enforcement belongs in the backend `applicationID` auth configuration.
The disposable backend/web containers were stopped and test-only environment files and keys removed after verification. Typecheck and all 66 offline tests were rerun successfully without those files.
Tested backend image:
```text
ghcr.io/get-convex/convex-backend@sha256:3aa2ccd6697a75de53948b1ee531d7c9688fe09ef360748d75df46dfc888f69a
```
## Reproducing the real Convex smoke
Use a **disposable** backend, set its `CONVEX_SELF_HOSTED_URL` and admin key, generate application keys and configure/deploy as in the README, then run:
```sh
SMOKE_ALLOW_WRITE=1 bun scripts/smoke-convex.ts
```
This writes synthetic users/characters and must not be casually pointed at a production deployment. It needs `CONVEX_BRIDGE_SECRET`, `APP_AUTH_ISSUER` and `APP_AUTH_PRIVATE_KEY` in the calling environment. A wrong-audience test is signed locally; no EVE identity is impersonated and no real EVE account is used.
## Live development rollout — 2026-09-19 UTC
- Latest integrated gate (including native-form, callback diagnostics, and singleton-scope compatibility): **97 tests passed, 0 failed, across 16 files**; Svelte/TypeScript check passed with 0 errors/0 warnings. Docker web build passed.
- Connected the operator-created Infisical project through a read-only Universal Auth identity. Generated app encryption/signing/bridge keys were imported by the operator, compared against the staging file, and locally verified for encryption round-trip and signing/JWKS consistency. The temporary `.env.keys` was then removed. Secret values were not displayed.
- Persistent self-hosted Convex schema/functions/auth configuration deployed. Its administrator credential is derived locally for explicit deployment commands, not stored in the web environment.
- Pangolin/UniFi routing was verified: `https://dev-convex.evebot.app/version` returned HTTP 200 and a real WSS upgrade with `Origin: https://dev.evebot.app` passed. Pangolin authentication remains allowed on the main app but was disabled on the Convex API resource by the operator.
- One real TypeSafe request through `JevIntentRouter` returned `asset_valuation`, model `jev-1.13.0`, confidence 1, in 915 ms. This proves connectivity/routing for that sample, not calibration.
- One real public ESI `/markets/prices` request passed using pinned compatibility `2026-08-18`, returning fresh integer-cent estimates for Tritanium and Rifter. These remain global reference estimates, not Jita bids or executable prices.
- Real Reference Data/Hoboleaks/CCP archives totaling **114,291,615 compressed bytes (~109 MiB)** were parsed. The corrected Docker worker compiled them with `--network none`, a 4 GiB memory limit and two CPUs, explicitly targeting `/app/data/reference` in the shared volume. Peak RSS was not measured. The full online download path has fixture-based receipt/validator tests; the real full rebuild used the already downloaded archives rather than fetching another copy.
- The web container's own read-only repository successfully opened the deployed production artifact, resolved Tritanium/Rifter, matched manifest/table counts, and passed SQLite integrity/foreign-key checks: **53,001 types, 5,082 blueprints, 8,490 systems, 13,978 gates, 21 explicitly recorded unresolved blueprint references**. Deployed version: `b9afed7830756a5e617fec31a2a88c917f07b8f64d2bc719e2176fb628c1e98b`.
- Live anonymous web smoke passed after deployment: SSR routes, no demo fallback, asset/chat authentication gates, session-token protection, and CSRF rejection. A scan of **13 deployed client artifacts plus anonymous SSR output** found no managed secret values; bootstrap tokens and the deployment-admin key were absent from the web environment.
A subsequent operator test exposed `ERR_INVALID_RESPONSE` when submitting Connect Character. The global `no-referrer` response policy made native browser form submissions send `Origin: null`; SvelteKit rejected them before OAuth initiation. An isolated, temporary-profile Firefox test reproduced that behavior and confirmed that `same-origin` preserves the native form Origin. App pages now use `same-origin` while auth responses retain `no-referrer`; null/missing/foreign origins remain rejected. The rebuilt deployment passed header and anonymous endpoint checks. EVE discovery and a server-to-Convex read also passed; diagnostics wrote no OAuth/character records. The operator subsequently reached EVE login but reported a generic failure after callback. Redacted callback-stage diagnostics were added and deployed: only fixed failure categories and allowlisted protocol status/claim names are retained; provider descriptions, JWT payloads, credentials, codes, cookies, and user identifiers are excluded. The next redacted callback diagnostic identified `scopes_invalid` after token verification. The parser now accepts EVE's singleton scope-string representation as well as arrays, with signed-token login/refresh regressions and malformed-claim rejection. The revised image and anonymous smoke passed. After that deployment, the operator confirmed successful character authorization and asset synchronization. Explicit sync exercises refresh-grant verification and owner continuity before asset publication. The operator also confirmed that the live Assets page and chat `/value` return matching totals, without sharing the private amount. This completes the initial live sign-in/sync/reference-valuation verification for one character.
Audit regressions cover native-form header/CSRF boundaries, shared artifact-directory precedence, live readers rejecting fixture profiles, offline no-network behavior, archive receipts/replay, ZIP CRC/length/duplicate records, TAR checksums/end markers, duplicate JSON keys, Hoboleaks file metadata, and immutable cache provenance/rollback/reuse. See [live setup](live-setup.md) and [reference operations](reference-operations.md).
## Not claimed as verified
- Live revocation/character-transfer scenarios and additional-character linking; successful sign-in/sync for one real character is now operator-confirmed.
- Broad TypeSafe inference quality or the proposed 0.80 confidence threshold's calibration on EVE requests.
- Compatibility with arbitrary future archive releases, station-data coverage, large-scale memory/performance benchmarks, or production-size market analytics.
- End-to-end browser automation/visual accessibility audit; the web smoke verifies the built server and endpoints, not all browser interactions.
- A comprehensive TLS/proxy security audit, backup/restore drills, rotation/recovery, unlink/revocation UX, retention/GC, or multi-process refresh/job recovery.
These remain explicit deployment/incremental-development gates, not silent success assumptions.