- 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
9.3 KiB
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 emergencySchemaVersions 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
{
"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
{
"algorithm": "ed25519",
"over": "sha256(manifest.json exact bytes)",
"manifestSha256": "…",
"publicKeyFingerprint": "sha256:…",
"signature": "<base64>"
}
emergency.json (dataset section, schema emergency/1)
{
"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
{
"id": "evt-0113",
"title": "…",
"stageId": "stage-b",
"artistIds": ["art-007"],
"startUtc": 1789459200000,
"endUtc": 1789462800000,
"dayKey": "2026-09-11",
"tags": ["live"],
"status": "scheduled"
}
Stable ids are contractual (favorites survive updates). dayKey is
precomputed in the festival zone at publish time (SPIKE-08).
map.json (schema map/1)
{
"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
{
"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)
- Schema-validate every section; reject unknown required fields policy: unknown fields are allowed forward-compat, missing required fields fail.
- Stable-ID check: event/POI IDs stable vs previous package (warn on removals;
require explicit
status: cancelledrather than deletion). - Time sanity: no zero-length events;
dayKeymatches festival-zone date ofstartUtc; all events inside festival window ± 1 day. - Budget enforcement: per-file ≤ 6 MB, section totals, image dimension caps (SPIKE-03 F-1), total ≤ 40 MB target.
- Hash → sign → upload immutable files → flip
latest.json→ smoke-fetch and re-verify from the CDN.
4. Findings
- F-1 (add):
sections[*].requiredflag + 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+contentVersionfor 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).