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

9.3 KiB
Raw Permalink Blame History

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)

  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).