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:
Lumen Stage1 2026-08-30 23:25:35 -05:00
commit c0bfd413ff
100 changed files with 9863 additions and 0 deletions

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