Lumen/SPIKE-04-FESTIVAL-DATA-PACKAGE.md
Lumen Stage1 c0bfd413ff Stage 1: project foundation (strict TS, lint boundaries B-1..B-7, directory structure, CI, boundary tests)
- dedicated git repo at /home/avi/Projects/Lumen (main)
- TypeScript strict (target ES2022, bundler, exactOptionalPropertyTypes, noUncheckedIndexedAccess)
- ESLint 9 + typescript-eslint strictTypeChecked + eslint-plugin-boundaries for B-1..B-7, no-restricted-globals/syntax for B-1/B-7
- Prettier 3.5
- Structure per IMPLEMENTATION-CONTRACT.md §4 (src/platform/idb|cache|sw, storage, data, sync/{transport,verifier}, domain/{emergency,schedule,map,festival,readiness,clock,favorites}, ui/{components,views,router,render}, app, emergency-baseline, assets, public, content, pipeline, tests, scripts)
- CI: .github/workflows/ci.yml (typecheck + lint + format + test)
- Boundary tests: tests/unit/boundaries.test.ts (4 tests) + scripts/check-boundaries.ts
- No feature code, no PWA/IDB/sync/mesh/accounts per contract Stage 1
2026-08-30 23:25:35 -05:00

218 lines
9.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SPIKE-04 — Festival Data Package (v1 Schema Concept)
- **Phase:** Architecture Validation
- **Date:** 2026-08-30
- **Decision under test:** ADR-005 (JSON sections + assets + signed manifest).
- **Status:** schema concept validated against all questions below; normative
schema fragments included. This is a design artifact, not an implementation.
## 1. Direct answers
**What is the package?** An immutable, versioned directory of files: one
`manifest.json`, one `signature.json`, N section documents (JSON), an asset
inventory (`assets.json`), and asset binaries. Nothing executable.
**What identifies it?** `edition` + `packageVersion` (identity for humans and
update logic) and the SHA-256 of the manifest bytes (content identity). The
URL `/editions/<edition>/packages/<packageVersion>/` is the location, not the
identity — a mirror must serve byte-identical files or fail verification.
**What version does it have?** `packageVersion`: a strictly monotonic integer
per edition. `generatedAt` is informational only and never gates behavior.
**What application versions are compatible?** `appCompatibility.minAppVersion`
/ `maxAppVersion` (declared by the package) intersected with the shell's
supported `schemaVersion` range (declared by the app). Both directions are
checked at boot and before staging (ARCH §18.5, SPIKE-02 §2 step 4).
**What data sections exist?** V1 mandatory sections: `emergency`, `schedule`,
`map`, `info`, plus the `assets` inventory. Optional sections (V1: none
shipped; reserved): `announcements`, `i18n.<lang>`, `extras.*`.
**How are assets represented?** Files under `assets/`, listed in `assets.json`
with `id`, `file`, `kind`, `role`, `sha256`, `bytes`. Assets are referenced by
`id` from section documents (never by path), so re-encoding/re-naming assets
does not ripple into sections.
**How are assets hashed?** Each asset carries SHA-256 over exact file bytes +
`bytes` length, verified at staging before activation.
**How is integrity represented?** Two layers: per-file SHA-256+size for every
file listed in the manifest; and manifest completeness (every listed file
present and matching). A dataset is *complete* iff the manifest says so and
spot/full verification confirms it.
**How is authenticity represented?** `signature.json`: Ed25519 signature over
SHA-256 of the exact manifest bytes, made with the festival's offline signing
key; includes `publicKeyFingerprint` which must match a key in the app's
embedded key set. Unverifiable authenticity ⇒ reject, keep current dataset.
**How is expiration represented?** **It isn't — deliberately.** Datasets and
sections carry no expiry that can disable them: an offline device must never
watch its own data "expire" (a wrong clock could brick the app — see
SPIKE-05's time-independence rule). Only *display-only* future content
(announcements/notices, when implemented) may carry `expiresAtUtc`, and expiry
hides the notice, never critical data.
**How are schema migrations handled?** Datasets are immutable per version, so
there is no in-place migration. Migration = the publisher emits a new package
version in the new schema; clients accept it iff within their supported
`schemaVersion` range. The shell supports a *range* of schemas so app and data
can ship independently (ordering rule ARCH §18.6). **User data** has its own
separate `schemaVersion` with small idempotent migrations at boot (favorites
must survive both dataset updates and app updates).
**How are optional sections represented?** `sections[*].required: bool`
(default `true`). Readiness gates on required sections only; optional sections
report their own presence without affecting READY (SPIKE-05). V1: all four
content sections are required; the flag exists for future `announcements` etc.
**How are emergency data versions represented?** The `emergency` section
carries its own `emergencySchemaVersion` (structure) and `contentVersion`
(content revision, monotonic) + `updatedAt`. The floor (embedded baseline)
declares which `emergencySchemaVersion`s it can fall back for; the shell
declares which it can render from the dataset. This keeps floor/dataset
compatibility explicit and independent of the package's overall `schemaVersion`.
## 2. Normative fragments (illustrative, not final field-for-field)
### manifest.json
```json
{
"format": "lumen.package/1",
"edition": "lumen-2026",
"packageVersion": 7,
"schemaVersion": 1,
"generatedAt": "2026-08-28T14:02:11Z",
"festival": {
"name": "Lumen Festival 2026",
"timezone": "America/Chicago",
"startUtc": 1789362000000,
"endUtc": 1789635600000
},
"appCompatibility": { "minAppVersion": "1.0.0", "maxAppVersion": null },
"sections": {
"emergency": { "file": "emergency.json", "sha256": "…", "bytes": 41233, "required": true },
"schedule": { "file": "schedule.json", "sha256": "…", "bytes": 412201, "required": true },
"map": { "file": "map.json", "sha256": "…", "bytes": 38122, "required": true },
"info": { "file": "info.json", "sha256": "…", "bytes": 96710, "required": true },
"assets": { "file": "assets.json", "sha256": "…", "bytes": 7111, "required": true }
},
"counts": { "events": 312, "pois": 87, "assets": 14 },
"limits": { "totalBytes": 31240012 }
}
```
### signature.json
```json
{
"algorithm": "ed25519",
"over": "sha256(manifest.json exact bytes)",
"manifestSha256": "…",
"publicKeyFingerprint": "sha256:…",
"signature": "<base64>"
}
```
### emergency.json (dataset section, schema `emergency/1`)
```json
{
"section": "emergency",
"emergencySchemaVersion": 1,
"contentVersion": 3,
"updatedAt": "2026-08-27T09:00:00Z",
"services": {
"emergencyNumber": "911",
"security": { "phone": "+1-555-0142", "location": "Main Gate Kiosk" },
"firstAid": { "location": "Behind Stage B", "hours": "10:00–02:00" }
},
"locations": {
"musterPoints": [ { "id": "mp-1", "name": "North Field", "poi": "poi-muster-n" } ],
"exits": [ { "id": "ex-1", "name": "East Gate", "poi": "poi-exit-e" } ],
"aeds": [ { "poi": "poi-aed-1" }, { "poi": "poi-aed-2" } ]
},
"address": { "lines": ["…"], "coordinates": { "lat": 41.88, "lon": -87.63 } },
"procedures": [ { "id": "weather", "title": "Severe Weather", "steps": ["…"] } ],
"notices": [ { "id": "n-1", "severity": "info", "title": "…", "body": [], "expiresAtUtc": null } ]
}
```
### schedule.json (schema `schedule/1`) — event shape
```json
{
"id": "evt-0113",
"title": "…",
"stageId": "stage-b",
"artistIds": ["art-007"],
"startUtc": 1789459200000,
"endUtc": 1789462800000,
"dayKey": "2026-09-11",
"tags": ["live"],
"status": "scheduled"
}
```
Stable `id`s are contractual (favorites survive updates). `dayKey` is
precomputed in the festival zone at publish time (SPIKE-08).
### map.json (schema `map/1`)
```json
{
"section": "map",
"base": {
"levels": [
{ "id": "overview", "assetId": "map-base-overview", "width": 1600, "height": 1200 },
{ "id": "detail", "assetId": "map-base-detail", "width": 3072, "height": 2304,
"nightAssetId": "map-base-detail-night" }
]
},
"pois": [
{ "id": "poi-aed-1", "name": "AED — Info Tent", "category": "aed",
"x": 0.412, "y": 0.633, "description": "…", "lat": null, "lng": null }
],
"categories": ["stage","restroom","water","food","first-aid","aed","security",
"entrance","exit","parking","camping","vip","muster","info","vendor","other"]
}
```
### assets.json
```json
{
"assets": [
{ "id": "map-base-overview", "file": "assets/map-base-overview.webp",
"kind": "map-base", "role": "overview", "sha256": "…", "bytes": 402113 },
{ "id": "map-base-detail", "file": "assets/map-base-detail.webp",
"kind": "map-base", "role": "detail", "sha256": "…", "bytes": 2118004 }
]
}
```
## 3. Validation pipeline gates (publisher side)
1. Schema-validate every section; reject unknown required fields policy:
unknown fields are *allowed forward-compat*, missing required fields fail.
2. Stable-ID check: event/POI IDs stable vs previous package (warn on removals;
require explicit `status: cancelled` rather than deletion).
3. Time sanity: no zero-length events; `dayKey` matches festival-zone date of
`startUtc`; all events inside festival window ± 1 day.
4. Budget enforcement: per-file ≤ 6 MB, section totals, image dimension caps
(SPIKE-03 F-1), total ≤ 40 MB target.
5. Hash → sign → upload immutable files → flip `latest.json` → smoke-fetch and
re-verify from the CDN.
## 4. Findings
- **F-1 (add):** `sections[*].required` flag + readiness coupling (SPIKE-05).
- **F-2 (add):** no-expiration rule for datasets (above) — explicit because a
naive "validUntil" field would be an offline-brick hazard.
- **F-3 (add):** emergency section gets independent `emergencySchemaVersion` +
`contentVersion` for floor/dataset compatibility reasoning (SPIKE-06).
- **F-4 (add):** user-data schema versioning/migrations declared separately
from package schema (cross-check with SPIKE-01 P protocol and ARCH §9).
- **F-5 (confirmed):** JSON + per-file hashes + Ed25519 manifest signature is
sufficient; no binary formats needed at this scale.
## 5. Verdict
**ACCEPT WITH CHANGES.** Package concept stands; schema gains `required`
flags, the no-expiration rule, emergency sub-versioning, and explicit
user-data schema separation (ADR-005 addendum).