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
This commit is contained in:
commit
c0bfd413ff
100 changed files with 9863 additions and 0 deletions
218
SPIKE-04-FESTIVAL-DATA-PACKAGE.md
Normal file
218
SPIKE-04-FESTIVAL-DATA-PACKAGE.md
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
# 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).
|
||||
Loading…
Add table
Add a link
Reference in a new issue