From 080ea27ff4ff038d38395145b8905a53814db37a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 19:46:23 +0000 Subject: [PATCH 1/5] =?UTF-8?q?refactor(types):=20P3+P4=20=E2=80=94=20type?= =?UTF-8?q?=20globals=20&=20timer=20handles=20at=20origin=20(drop=20any-ca?= =?UTF-8?q?sts)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P3: a single app/js/globals.d.ts declares the debug/test window handles and the shared cross-window context (DEC-61) — __remit, __remitFault, __remitShell, __REMIT_SAMPLE, __map. Removes ~11 `/** @type {any} */ (window)` / (globalThis) / (window.opener) casts across context/main/shell/popout/map/data-analysis; the opener and popIn now type-check through the augmented Window. P4: timer handles typed at declaration with ReturnType (matching data-analysis.js) instead of casting the setTimeout/setInterval result to any — main.js steeringShareTimer, wingman.js playTimer. Also: shell.js error helper uses `err instanceof Error` instead of two any-casts. tsconfig includes app/js/**/*.d.ts. 0 typecheck errors; 32 unit green; type-level only (no runtime behaviour change). https://claude.ai/code/session_01EhtBoKXg6bHnKdquacyknf --- app/js/analysis/data-analysis.js | 2 +- app/js/globals.d.ts | 26 ++++++++++++++++++++++++++ app/js/main.js | 8 ++++---- app/js/shell/context.js | 2 +- app/js/shell/popout.js | 6 +++--- app/js/shell/shell.js | 6 +++--- app/js/views/map.js | 4 ++-- app/js/wingman/wingman.js | 4 ++-- tsconfig.json | 2 +- 9 files changed, 43 insertions(+), 17 deletions(-) create mode 100644 app/js/globals.d.ts diff --git a/app/js/analysis/data-analysis.js b/app/js/analysis/data-analysis.js index 309b432..4c0c9ad 100644 --- a/app/js/analysis/data-analysis.js +++ b/app/js/analysis/data-analysis.js @@ -154,7 +154,7 @@ export function mountDataAnalysis(container, ctx) { ]); } catch (err) { const msg = err instanceof Error ? err.message : String(err); - /** @type {any} */ (window).__remitFault?.(`provisioning: ${msg}`); + window.__remitFault?.(`provisioning: ${msg}`); } refresh(); } diff --git a/app/js/globals.d.ts b/app/js/globals.d.ts new file mode 100644 index 0000000..cbd2f47 --- /dev/null +++ b/app/js/globals.d.ts @@ -0,0 +1,26 @@ +// Ambient declarations for the app's debug/test global handles and the shared +// cross-window context (DEC-61). Typing them here — at their origin — is what +// lets the use sites drop their `/** @type {any} */ (window)` casts. + +type RemitContext = typeof import('./shell/context.js').context; + +declare global { + interface Window { + /** The shared app context (DEC-61); the Overview lap attaches `.state`. Debug/test handle. */ + __remit: RemitContext & { state?: unknown }; + /** Surface a failure in the banner — nothing fails silently. Seeded by the Overview lap. */ + __remitFault?: (msg: string) => void; + /** Minimal API a popped-out child calls to pop itself back in on close. */ + __remitShell?: { popIn: (id: string, fromClose: boolean) => void }; + /** Author-time terrain-sampling flag (views/map.js + tools/sample-terrain.mjs). */ + __REMIT_SAMPLE?: boolean; + /** The live MapLibre map, exposed only while sampling terrain. */ + __map?: unknown; + } + + // context.js seeds globalThis.__remit so it exists regardless of which tab boots first. + // eslint-disable-next-line no-var + var __remit: RemitContext & { state?: unknown }; +} + +export {}; diff --git a/app/js/main.js b/app/js/main.js index c61d73b..9af6250 100644 --- a/app/js/main.js +++ b/app/js/main.js @@ -62,7 +62,7 @@ const state = { // Debug/test handle (read-only use; not part of any contract). The context // module seeds window.__remit = {objects, logs, seam, world, playhead}; attach // the Overview's mission state so existing tooling can read window.__remit.state. -/** @type {any} */ (window).__remit.state = state; +window.__remit.state = state; // --- projection surface (map + timeline + playhead) ------------------------ const mapEl = /** @type {HTMLElement} */ (document.getElementById('map')); @@ -260,7 +260,7 @@ function showStage(/** @type {string} */ key) { /** Surface a failure in the banner — nothing fails silently. */ function showFault(/** @type {string} */ msg) { - /** @type {any} */ (window).__remitFault?.(msg); + window.__remitFault?.(msg); } /** @@ -448,7 +448,7 @@ function mountCaptureStage() { // `constraints` payload is the schema's Constraint shape (DEC-24); the delta // envelope (scope + attribution) is the DEC-61 stamped-delta scaffolding that // becomes a first-class Delta type in the writes phase. -let steeringShareTimer = 0; +/** @type {ReturnType | undefined} */ let steeringShareTimer; let lastSharedNogoKey = ''; function shareSteering() { // Hex world: no-go cells are H3 ids (the kernel reads `cell.h3`), not square x/y. @@ -472,7 +472,7 @@ function shareSteering() { } function scheduleShareSteering() { clearTimeout(steeringShareTimer); - steeringShareTimer = /** @type {any} */ (setTimeout(shareSteering, 450)); + steeringShareTimer = setTimeout(shareSteering, 450); } function mountPlan() { diff --git a/app/js/shell/context.js b/app/js/shell/context.js index 31d7116..a05fcbe 100644 --- a/app/js/shell/context.js +++ b/app/js/shell/context.js @@ -30,4 +30,4 @@ export const context = { objects, logs, seam, world, playhead }; // Debug/test handle (read-only use; not part of any contract). Seeded here so // it exists regardless of which tab boots first; main.js attaches `.state`. -/** @type {any} */ (globalThis).__remit = context; +globalThis.__remit = context; diff --git a/app/js/shell/popout.js b/app/js/shell/popout.js index 2287512..89aee9c 100644 --- a/app/js/shell/popout.js +++ b/app/js/shell/popout.js @@ -10,12 +10,12 @@ import { roles } from './roles.js'; /** @param {string} m */ -const fault = (m) => /** @type {any} */ (window).__remitFault?.(m); +const fault = (m) => window.__remitFault?.(m); export function boot() { const root = /** @type {HTMLElement} */ (document.getElementById('popout-root')); const id = location.hash.match(/tab=([\w-]+)/)?.[1] ?? null; - const opener = /** @type {any} */ (window.opener); + const opener = window.opener; const ctx = opener && opener.__remit; if (!ctx) { @@ -42,7 +42,7 @@ export function boot() { // Ask the opener to pop us back in when this window closes. window.addEventListener('beforeunload', () => { - try { opener.__remitShell?.popIn?.(id, true); } catch (_) { /* opener gone */ } + try { if (id) opener?.__remitShell?.popIn?.(id, true); } catch (_) { /* opener gone */ } }); } diff --git a/app/js/shell/shell.js b/app/js/shell/shell.js index 5622254..439b97b 100644 --- a/app/js/shell/shell.js +++ b/app/js/shell/shell.js @@ -17,9 +17,9 @@ const hashTab = () => location.hash.match(HASH_RE)?.[1] ?? null; /** @param {string} id */ const setHash = (id) => history.replaceState(null, '', `#tab=${id}`); /** @param {string} m */ -const fault = (m) => /** @type {any} */ (window).__remitFault?.(m); +const fault = (m) => window.__remitFault?.(m); /** @param {unknown} err */ -const msg = (err) => (err && /** @type {any} */ (err).message ? /** @type {any} */ (err).message : String(err)); +const msg = (err) => (err instanceof Error ? err.message : String(err)); const defs = roles(); const mounted = new Set(); @@ -36,7 +36,7 @@ export function boot() { tabsHost.addEventListener('keydown', onKeydown); window.addEventListener('hashchange', () => { const id = hashTab(); if (id) activate(id, false); }); // Minimal API a popped-out child calls to pop itself back in on close. - /** @type {any} */ (window).__remitShell = { popIn }; + window.__remitShell = { popIn }; activate(hashTab() ?? 'overview', false); } diff --git a/app/js/views/map.js b/app/js/views/map.js index b59e4b0..ccf2819 100644 --- a/app/js/views/map.js +++ b/app/js/views/map.js @@ -53,7 +53,7 @@ export function makeMap(el, baseline, ao, places) { // Author-time terrain sampling (tools/sample-terrain.mjs) sets window.__REMIT_SAMPLE // before load: keep the basemap framebuffer readable and expose the map. Inert otherwise. - const sampling = typeof window !== 'undefined' && /** @type {any} */ (window).__REMIT_SAMPLE; + const sampling = typeof window !== 'undefined' && window.__REMIT_SAMPLE; // maplibre's MapOptions type is stricter/narrower than the runtime options // (e.g. it omits preserveDrawingBuffer); cast the options blob at this library // boundary — the map instance itself stays fully typed. @@ -63,7 +63,7 @@ export function makeMap(el, baseline, ao, places) { preserveDrawingBuffer: sampling, })); map.on('error', () => {}); - if (sampling) /** @type {any} */ (window).__map = map; + if (sampling) window.__map = map; // Hovering an ORBAT asset marker surfaces its descriptive detail (spec 005 / T023) — a // display-only readout (kind, confidence, strength, notes, threat-type/category/role). const assetTooltip = (/** @type {any} */ info) => { diff --git a/app/js/wingman/wingman.js b/app/js/wingman/wingman.js index ca58c1d..61a84f4 100644 --- a/app/js/wingman/wingman.js +++ b/app/js/wingman/wingman.js @@ -170,7 +170,7 @@ export function mountWingman(el, ctx) { } } - /** @type {number | undefined} */ + /** @type {ReturnType | undefined} */ let playTimer; /** Sim-minutes per real second from the slider: 2 … 512 (powers of two). * Labelled "N min/s", not "×N" — 2 min/s is 120× real time. */ @@ -190,7 +190,7 @@ export function mountWingman(el, ctx) { $('#wx-play').addEventListener('click', () => { if (playTimer) { stopPlay(); return; } // 100 ms ticks; speed read per tick, so dragging the slider mid-play works. - playTimer = /** @type {any} */ (setInterval(() => tick(speedOf() / 10), 100)); + playTimer = setInterval(() => tick(speedOf() / 10), 100); refreshSpeedUI(); }); $('#wx-restart').addEventListener('click', async () => { diff --git a/tsconfig.json b/tsconfig.json index 4c58293..996b2a8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,5 +17,5 @@ "types": [], "skipLibCheck": true }, - "include": ["app/js/**/*.js"] + "include": ["app/js/**/*.js", "app/js/**/*.d.ts"] } From 719b78e043dfe87a1bbca89746428c427776cd19 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 19:54:12 +0000 Subject: [PATCH 2/5] =?UTF-8?q?refactor(types):=20P2=20=E2=80=94=20type=20?= =?UTF-8?q?the=20tuneAsset/validate=20paths=20in=20orbat.js=20(drop=20any-?= =?UTF-8?q?casts)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit orbat.js leaned on `/** @type {any} */ (patch).x` to read nested patch/next fields even though patch is Partial and the generated Red/Green/BlueParams carry every field — vestigial casts. Removed them all: - tuneAsset reads patch.kind/symbol/.../red/green/blue + availability_window through the real types; the availability_window assignment/delete is typed (BlueParams.availability_window: TimeWindow) - validate(): a small `inVocab(vocab, string)` helper replaces `VOCAB.includes(/** @type {any} */ (x))` (LinkML emits enum slots as `string`, which trips the readonly-tuple literal narrowing); the param-group check is an explicit blue/red/green branch instead of a dynamic-index cast; the Omit `rest` returns without a cast - position/inAO/self params typed HexCell (not any); sanitizeWindows takes TimeWindow[]; cleanStr takes unknown orbat.js any-casts 18 → 0. 0 typecheck errors; 32 unit green; behaviour identical. https://claude.ai/code/session_01EhtBoKXg6bHnKdquacyknf --- app/js/orbat/orbat.js | 67 ++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/app/js/orbat/orbat.js b/app/js/orbat/orbat.js index 1c5eabd..49c5e24 100644 --- a/app/js/orbat/orbat.js +++ b/app/js/orbat/orbat.js @@ -17,6 +17,8 @@ import { canonicalJSON, contentId } from '../shapes/canonical.js'; /** @typedef {import('../../../schema/gen/remit').BlueParams} BlueParams */ /** @typedef {import('../../../schema/gen/remit').RedParams} RedParams */ /** @typedef {import('../../../schema/gen/remit').GreenParams} GreenParams */ +/** @typedef {import('../../../schema/gen/remit').HexCell} HexCell */ +/** @typedef {import('../../../schema/gen/remit').TimeWindow} TimeWindow */ /** @typedef {import('../entities/entities.js').Entity} Entity */ /** localStorage key for the working draft (per mission). */ @@ -85,7 +87,7 @@ const clamp = (v, [lo, hi]) => Math.min(hi, Math.max(lo, v)); const clampInt = (v, bounds) => Math.round(clamp(v, bounds)); /** Trim a free-text value; empty ⇒ undefined (so empties are dropped, not stored blank, FR-014). */ -const cleanStr = (/** @type {any} */ v) => { const s = String(v ?? '').trim(); return s || undefined; }; +const cleanStr = (/** @type {unknown} */ v) => { const s = String(v ?? '').trim(); return s || undefined; }; /** Set `obj[key]` to a free-text `value` when non-empty, else delete the key — so cleared * fields are dropped, never stored blank (FR-014). @param {any} obj @param {string} key @param {any} value */ @@ -101,6 +103,10 @@ function setVocab(obj, key, value, vocab) { if (vocab.includes(value)) obj[key] = value; // else: ignore } +/** Vocabulary membership that accepts any string — avoids the readonly-tuple + * `.includes()` literal-narrowing without a cast. @param {readonly string[]} vocab @param {string} v */ +const inVocab = (vocab, v) => vocab.some((x) => x === v); + /** Default per-allegiance parameter group. * @param {'blue'|'red'|'green'} allegiance */ function defaultParams(allegiance) { @@ -170,8 +176,8 @@ function replaceAsset(orbat, id, fn) { * Add a default asset of an allegiance at `position`, with a fresh unique id. * Rejects an out-of-AO position when an `inAO` predicate is supplied (FR-001/003). * @param {Orbat} orbat - * @param {{ allegiance: 'blue'|'red'|'green', position?: any, label?: string }} seed - * @param {{ inAO?: (pos: any) => boolean }} [opts] + * @param {{ allegiance: 'blue'|'red'|'green', position?: HexCell, label?: string }} seed + * @param {{ inAO?: (pos: HexCell) => boolean }} [opts] * @returns {{ orbat: Orbat, id: string }} */ export function addAsset(orbat, { allegiance, position, label }, opts = {}) { @@ -207,7 +213,7 @@ export function duplicateAsset(orbat, id) { * Apply `patch` to ONLY the targeted asset, clamping/validating its bounds (FR-004). * Other assets stay byte-identical (SC-003). Returns a new draft. * @param {Orbat} orbat @param {string} id @param {Partial} patch - * @param {{ inAO?: (pos: any) => boolean }} [opts] + * @param {{ inAO?: (pos: HexCell) => boolean }} [opts] * @returns {Orbat} */ export function tuneAsset(orbat, id, patch, opts = {}) { @@ -221,14 +227,14 @@ export function tuneAsset(orbat, id, patch, opts = {}) { if (patch.extent_m !== undefined) next.extent_m = clamp(Number(patch.extent_m), BOUNDS.extent_m); // Enrichment (spec 005): kind/symbol/confidence + descriptive strength/notes. Vocab-checked // where applicable; free-text is trimmed and empties are dropped (FR-014), never stored blank. - if ('kind' in patch) setVocab(next, 'kind', /** @type {any} */ (patch).kind, PLATFORM_KINDS); - if ('symbol' in patch) setOrDrop(next, 'symbol', cleanStr(/** @type {any} */ (patch).symbol)); - if ('confidence' in patch) setVocab(next, 'confidence', /** @type {any} */ (patch).confidence, CONFIDENCE_LEVELS); - if ('strength' in patch) setOrDrop(next, 'strength', cleanStr(/** @type {any} */ (patch).strength)); - if ('notes' in patch) setOrDrop(next, 'notes', cleanStr(/** @type {any} */ (patch).notes)); + if ('kind' in patch) setVocab(next, 'kind', patch.kind, PLATFORM_KINDS); + if ('symbol' in patch) setOrDrop(next, 'symbol', cleanStr(patch.symbol)); + if ('confidence' in patch) setVocab(next, 'confidence', patch.confidence, CONFIDENCE_LEVELS); + if ('strength' in patch) setOrDrop(next, 'strength', cleanStr(patch.strength)); + if ('notes' in patch) setOrDrop(next, 'notes', cleanStr(patch.notes)); if (patch.red && next.allegiance === 'red') { next.red = { ...next.red }; - const pr = /** @type {any} */ (patch.red); + const pr = patch.red; if (pr.severity !== undefined) next.red.severity = clampInt(Number(pr.severity), BOUNDS.severity); if (pr.active_windows !== undefined) next.red.active_windows = sanitizeWindows(pr.active_windows); if (pr.detection_range_m !== undefined) next.red.detection_range_m = clamp(Number(pr.detection_range_m), BOUNDS.extent_m); @@ -240,24 +246,24 @@ export function tuneAsset(orbat, id, patch, opts = {}) { } if (patch.green && next.allegiance === 'green') { next.green = { ...next.green }; - const pg = /** @type {any} */ (patch.green); + const pg = patch.green; if (pg.sensitivity !== undefined) next.green.sensitivity = clampInt(Number(pg.sensitivity), BOUNDS.sensitivity); - if (pg.protection !== undefined && PROTECTIONS.includes(pg.protection)) next.green.protection = pg.protection; + if (pg.protection !== undefined && inVocab(PROTECTIONS, pg.protection)) next.green.protection = pg.protection; if ('category' in pg) setVocab(next.green, 'category', pg.category, GREEN_CATEGORIES); } if (patch.blue && next.allegiance === 'blue') { next.blue = { ...next.blue }; if (patch.blue.availability !== undefined) next.blue.availability = String(patch.blue.availability); - if ('role' in patch.blue) setOrDrop(next.blue, 'role', cleanStr(/** @type {any} */ (patch.blue).role)); + if ('role' in patch.blue) setOrDrop(next.blue, 'role', cleanStr(patch.blue.role)); if (patch.blue.capabilities !== undefined) next.blue.capabilities = (patch.blue.capabilities ?? []).map(String).filter(Boolean); if ('availability_window' in patch.blue) { - const w = /** @type {any} */ (patch.blue).availability_window; + const w = patch.blue.availability_window; if (w && w.start_min != null && w.end_min != null) { const s = Math.round(Number(w.start_min)), e = Math.round(Number(w.end_min)); - /** @type {any} */ (next.blue).availability_window = { start_min: Math.min(s, e), end_min: Math.max(s, e) }; + next.blue.availability_window = { start_min: Math.min(s, e), end_min: Math.max(s, e) }; } else { - delete (/** @type {any} */ (next.blue).availability_window); + delete next.blue.availability_window; } } } @@ -266,7 +272,7 @@ export function tuneAsset(orbat, id, patch, opts = {}) { } /** Clamp a list of time windows to start ≤ end, dropping malformed entries (FR-004). - * @param {any[]} windows */ + * @param {TimeWindow[]} windows */ function sanitizeWindows(windows) { return (windows ?? []) .map((w) => ({ start_min: Math.round(Number(w.start_min)), end_min: Math.round(Number(w.end_min)) })) @@ -291,7 +297,7 @@ export function removeAsset(orbat, id) { * `canonical_own_force = true` (FR-012). The asset is reconciled, never duplicated — the * plan keeps driving from the pre-existing machinery. * @param {Orbat} orbat - * @param {{ label?: string, position?: any }} self the planned own-force entity/place + * @param {{ label?: string, position?: HexCell }} self the planned own-force entity/place * @returns {Orbat} */ export function reconcileOwnForce(orbat, self) { @@ -299,7 +305,7 @@ export function reconcileOwnForce(orbat, self) { // Strip any stray canonical flag from non-canonical rows (exactly one survives). if (a.canonical_own_force && a.id !== OWN_FORCE_ID) { const { canonical_own_force, ...rest } = a; - return /** @type {Asset} */ (rest); + return rest; } return a; }); @@ -324,15 +330,18 @@ export function reconcileOwnForce(orbat, self) { /** * Validate one asset: allegiance ∈ {blue,red,green}, the matching param group present, * bounds in range, window start ≤ end, and (when `inAO` is supplied) position in the AO. - * @param {Asset} asset @param {{ inAO?: (pos: any) => boolean }} [opts] + * @param {Asset} asset @param {{ inAO?: (pos: HexCell) => boolean }} [opts] * @returns {{ ok: boolean, issues: string[] }} */ export function validate(asset, opts = {}) { const issues = []; - if (!ALLEGIANCES.includes(/** @type {any} */ (asset.allegiance))) + if (!inVocab(ALLEGIANCES, asset.allegiance)) issues.push(`allegiance must be one of ${ALLEGIANCES.join('/')}`); - else if (!asset[/** @type {'blue'|'red'|'green'} */ (asset.allegiance)]) - issues.push(`${asset.allegiance} asset is missing its ${asset.allegiance} parameter group`); + else { + const group = asset.allegiance === 'blue' ? asset.blue + : asset.allegiance === 'red' ? asset.red : asset.green; + if (!group) issues.push(`${asset.allegiance} asset is missing its ${asset.allegiance} parameter group`); + } if (asset.extent_m !== undefined && (asset.extent_m < BOUNDS.extent_m[0] || asset.extent_m > BOUNDS.extent_m[1])) issues.push(`extent_m out of bounds ${BOUNDS.extent_m.join('..')}`); if (asset.red?.severity !== undefined && (asset.red.severity < BOUNDS.severity[0] || asset.red.severity > BOUNDS.severity[1])) @@ -342,9 +351,9 @@ export function validate(asset, opts = {}) { for (const w of asset.red?.active_windows ?? []) if (Number(w.start_min) > Number(w.end_min)) issues.push('active window start_min must be ≤ end_min'); // Enrichment (spec 005): vocab + red dual-range bounds + engagement ≤ detection. - if (asset.kind !== undefined && !PLATFORM_KINDS.includes(/** @type {any} */ (asset.kind))) issues.push('unknown platform kind'); - if (asset.confidence !== undefined && !CONFIDENCE_LEVELS.includes(/** @type {any} */ (asset.confidence))) issues.push('unknown confidence level'); - if (asset.green?.category !== undefined && !GREEN_CATEGORIES.includes(/** @type {any} */ (asset.green.category))) issues.push('unknown green category'); + if (asset.kind !== undefined && !inVocab(PLATFORM_KINDS, asset.kind)) issues.push('unknown platform kind'); + if (asset.confidence !== undefined && !inVocab(CONFIDENCE_LEVELS, asset.confidence)) issues.push('unknown confidence level'); + if (asset.green?.category !== undefined && !inVocab(GREEN_CATEGORIES, asset.green.category)) issues.push('unknown green category'); const dr = asset.red?.detection_range_m, er = asset.red?.engagement_range_m; if (dr !== undefined && (dr < BOUNDS.extent_m[0] || dr > BOUNDS.extent_m[1])) issues.push(`detection_range_m out of bounds ${BOUNDS.extent_m.join('..')}`); if (er !== undefined && (er < BOUNDS.extent_m[0] || er > BOUNDS.extent_m[1])) issues.push(`engagement_range_m out of bounds ${BOUNDS.extent_m.join('..')}`); @@ -457,7 +466,7 @@ export function subscribeDraft(fn) { * Any asset with a time-varying aspect (red `active_windows`, a blue availability window) * exposes a `window`-type aspect (reusing the satellite-pass render path) so it appears as * a Sync-Matrix track. Contains NO kernel reference and alters no plan (NF9). - * @param {Asset} asset @returns {Entity & { allegiance: string, position?: any, asset: Asset }} + * @param {Asset} asset @returns {Entity & { allegiance: string, position?: HexCell, asset: Asset }} */ export function assetToEntity(asset) { const windows = activeWindowsOf(asset); @@ -466,7 +475,7 @@ export function assetToEntity(asset) { if (windows.length) { aspects.active = { type: 'window', - at: (/** @type {any} */ _plan, /** @type {number} */ t) => windows.some((w) => t >= w.start && t <= w.end), + at: (/** @type {unknown} */ _plan, /** @type {number} */ t) => windows.some((w) => t >= w.start && t <= w.end), windows: (/** @type {number} */ h) => windows .filter((w) => w.end >= 0 && w.start <= h) .map((w) => ({ start: Math.max(0, w.start), end: Math.min(h, w.end), center: (w.start + w.end) / 2 })), @@ -489,7 +498,7 @@ function activeWindowsOf(asset) { if (asset.allegiance === 'red') return (asset.red?.active_windows ?? []).map((w) => ({ start: Number(w.start_min), end: Number(w.end_min) })); if (asset.allegiance === 'blue') { - const w = /** @type {any} */ (asset.blue)?.availability_window; + const w = asset.blue?.availability_window; if (w && w.start_min != null && w.end_min != null) return [{ start: Number(w.start_min), end: Number(w.end_min) }]; } return []; From 33a437e8918e20200c17776c4906176c0263ddff Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 16 Jun 2026 20:02:14 +0000 Subject: [PATCH 3/5] =?UTF-8?q?refactor(types):=20P1=20=E2=80=94=20main.js?= =?UTF-8?q?=20consumes=20generated=20types;=20declare=20real=20invariants?= =?UTF-8?q?=20in=20schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Types the Overview lap's serialisable state with the LinkML-generated types instead of `/** @type {any} */ (null)` casts: requirement: Requirement|null, handful: Plan[], selected/preview/execPlan: Plan|null, steering: Constraint[]. Reading those surfaced that the generated types marked always-present fields optional (LinkML declares few required), which would have forced read-site casts/guards — the very smell we're removing. The principled fix is to declare the real invariants at the origin (the schema): - Requirement.commitments, Plan.strategy, Plan.scores, Scores.satisfaction, Materialisation.schedule, Materialisation.trajectory → required: true (regenerated) Plus genuine null-safety (no casts): `if (!state.requirement) return` guards in the compare/execute stages (requirement is genuinely null pre-capture), `?? null` on the COA `.find()`, and optional-chaining on the observe-band lookup. tsconfig lib → ES2023 (the cards already use Array.findLast at runtime). main.js any-casts 23 → 12 (the rest are deck.gl view-state, P5). schema-required is a faithful reconciliation (schema ≡ code, DEC-57); golden plan ids unchanged (runtime untouched); adherence still validates whole; 32 unit green; 0 typecheck errors. https://claude.ai/code/session_01EhtBoKXg6bHnKdquacyknf --- app/js/main.js | 28 +++++++++++-------- schema/gen/remit.schema.json | 52 +++++++++++++----------------------- schema/gen/remit.ts | 12 ++++----- schema/plan.yaml | 5 ++++ schema/requirement.yaml | 1 + site/data-model/index.html | 12 ++++----- tsconfig.json | 2 +- 7 files changed, 54 insertions(+), 58 deletions(-) diff --git a/app/js/main.js b/app/js/main.js index 9af6250..800407d 100644 --- a/app/js/main.js +++ b/app/js/main.js @@ -19,6 +19,10 @@ import { getDraft, setDraft, subscribeDraft, reconcileOwnForce } from './orbat/o // other role surface (tab) so they all project the same objects (DEC-61). import { objects, logs, seam, world, playhead } from './shell/context.js'; +/** @typedef {import('../../schema/gen/remit').Requirement} Requirement */ +/** @typedef {import('../../schema/gen/remit').Plan} Plan */ +/** @typedef {import('../../schema/gen/remit').Constraint} Constraint */ + const MISSION_ID = 'M-001'; const STRATEGY_SEED = 1337; @@ -43,19 +47,19 @@ const state = { stage: 'world', unlocked: new Set(['world']), done: new Set(), - requirement: /** @type {any} */ (null), + requirement: /** @type {Requirement | null} */ (null), ids: { requirement: '', baseline: '', profile: '', configCore: '', stamp: '', rationale: '' }, configCoreHash: '', bandUnit: bandUnitFor(world.baseline.channels[0]), appetites: { tempo: 'balanced', exposure: 'balanced' }, - steering: /** @type {any[]} */ ([]), // operator no-go constraints (Plan) - handful: /** @type {any[]} */ ([]), - selectedPlan: /** @type {any} */ (null), - previewPlan: /** @type {any} */ (null), // COA highlighted (radio) in Compare, before commit - execPlan: /** @type {any} */ (null), // live clone played back (and re-routed) in Execute - execSummary: /** @type {any} */ (null), + steering: /** @type {Constraint[]} */ ([]), // operator no-go constraints (Plan) + handful: /** @type {Plan[]} */ ([]), + selectedPlan: /** @type {Plan | null} */ (null), + previewPlan: /** @type {Plan | null} */ (null), // COA highlighted (radio) in Compare, before commit + execPlan: /** @type {Plan | null} */ (null), // live clone played back (and re-routed) in Execute + execSummary: /** @type {unknown} */ (null), horizonMin: 180, - lastPlanRequest: /** @type {any} */ (null), + lastPlanRequest: /** @type {unknown} */ (null), nextHint: /** @type {string|null} */ (null), }; @@ -308,16 +312,17 @@ function mountStage(/** @type {string} */ key) { if (key === 'capture') mountCaptureStage(); if (key === 'plan') mountPlan(); if (key === 'compare') { + if (!state.requirement) return; mountCompare(panel('compare'), { seam, handful: state.handful, commitments: state.requirement.commitments, onPreview(planId) { // Highlighting a COA (radio) previews it everywhere — map + Sync Matrix // own-force tracks — before the rationale is committed. - state.previewPlan = state.handful.find((p) => p.id === planId); + state.previewPlan = state.handful.find((p) => p.id === planId) ?? null; renderProjection(); }, onSelected(planId, _rationale, rationaleId) { - state.selectedPlan = state.handful.find((p) => p.id === planId); + state.selectedPlan = state.handful.find((p) => p.id === planId) ?? null; state.ids.rationale = rationaleId; renderProjection(); advance('compare'); @@ -325,6 +330,7 @@ function mountStage(/** @type {string} */ key) { }); } if (key === 'execute') { + if (!state.requirement) return; // The wingman plays back (and may re-route) a live clone, so the committed // plan stays immutable. Reset the shared playhead to H+0 (the removed Views // interstitial used to do this on the way through). @@ -553,7 +559,7 @@ function mountPlan() {

${p.strategy.label}

${p.strategy.blurb}
${p.tide_decision ? `
≋ ${p.tide_decision.narrative}
` : ''} -
observe ${obs.margin_band} ${obs.margin_min}m
+
observe ${obs?.margin_band ?? 'n/a'}${obs ? ` ${obs.margin_min}m` : ''}
exfil ${rv ? `H+${rv.end_min} ` : ''}${exf?.margin_band ?? 'n/a'}${exf ? ` ${exf.margin_min}m` : ''} cost ${p.scores.cost_band}
id ${shortId(p.id)} = hash(stamp ⊕ strategy)
diff --git a/schema/gen/remit.schema.json b/schema/gen/remit.schema.json index f526037..34c3ae1 100644 --- a/schema/gen/remit.schema.json +++ b/schema/gen/remit.schema.json @@ -1805,10 +1805,7 @@ "items": { "$ref": "#/$defs/ScheduleLeg" }, - "type": [ - "array", - "null" - ] + "type": "array" }, "state_curves": { "anyOf": [ @@ -1835,10 +1832,7 @@ "items": { "$ref": "#/$defs/TrajectoryPoint" }, - "type": [ - "array", - "null" - ] + "type": "array" }, "verified": { "type": [ @@ -1847,6 +1841,10 @@ ] } }, + "required": [ + "schedule", + "trajectory" + ], "title": "Materialisation", "type": "object" }, @@ -2027,14 +2025,7 @@ "description": "cached, regenerable; null when infeasible" }, "scores": { - "anyOf": [ - { - "$ref": "#/$defs/Scores" - }, - { - "type": "null" - } - ] + "$ref": "#/$defs/Scores" }, "stamp": { "anyOf": [ @@ -2048,14 +2039,7 @@ "description": "authoritative \u2014 the plan's identity basis" }, "strategy": { - "anyOf": [ - { - "$ref": "#/$defs/Strategy" - }, - { - "type": "null" - } - ] + "$ref": "#/$defs/Strategy" }, "tide_decision": { "anyOf": [ @@ -2070,7 +2054,9 @@ } }, "required": [ - "id" + "id", + "strategy", + "scores" ], "title": "Plan", "type": "object" @@ -2288,10 +2274,7 @@ "items": { "$ref": "#/$defs/Commitment" }, - "type": [ - "array", - "null" - ] + "type": "array" }, "id": { "description": "content id (sha256:\u2026) of the canonical form (DEC-35)", @@ -2332,7 +2315,8 @@ }, "required": [ "id", - "intent" + "intent", + "commitments" ], "title": "Requirement", "type": "object" @@ -2434,12 +2418,12 @@ "items": { "$ref": "#/$defs/Satisfaction" }, - "type": [ - "array", - "null" - ] + "type": "array" } }, + "required": [ + "satisfaction" + ], "title": "Scores", "type": "object" }, diff --git a/schema/gen/remit.ts b/schema/gen/remit.ts index 8a346ef..a69e64a 100644 --- a/schema/gen/remit.ts +++ b/schema/gen/remit.ts @@ -382,7 +382,7 @@ export interface Requirement { /** issuing role/authority + time */ provenance?: Attribution, /** the promises it decomposes into */ - commitments?: Commitment[], + commitments: Commitment[], lineage?: Lineage, } @@ -949,12 +949,12 @@ export interface Strategy { export interface Plan { /** "= hash(Stamp, strategy)" */ id: string, - strategy?: Strategy, + strategy: Strategy, /** authoritative — the plan's identity basis */ stamp?: Stamp, /** cached, regenerable; null when infeasible */ materialisation?: Materialisation, - scores?: Scores, + scores: Scores, /** the exfil wait-vs-detour weighing (ADR-0006); null when no ford */ tide_decision?: TideDecision, /** first-class, named clashes (C1) */ @@ -966,8 +966,8 @@ export interface Plan { * The cached, regenerable working-out of a plan; absent when the plan is infeasible. */ export interface Materialisation { - schedule?: ScheduleLeg[], - trajectory?: TrajectoryPoint[], + schedule: ScheduleLeg[], + trajectory: TrajectoryPoint[], state_curves?: StateCurves, /** the tide decision at plan time */ tide?: TideDecision, @@ -1018,7 +1018,7 @@ export interface StateCurves { * A plan's comparable scores, read under the comparability guard (A2/C2/C6, NF10). */ export interface Scores { - satisfaction?: Satisfaction[], + satisfaction: Satisfaction[], cost_band?: string, robustness_band?: string, } diff --git a/schema/plan.yaml b/schema/plan.yaml index 69aa6ca..18d5311 100644 --- a/schema/plan.yaml +++ b/schema/plan.yaml @@ -107,6 +107,7 @@ classes: strategy: range: Strategy inlined: true + required: true stamp: range: Stamp inlined: true @@ -118,6 +119,7 @@ classes: scores: range: Scores inlined: true + required: true tide_decision: range: TideDecision inlined: true @@ -134,10 +136,12 @@ classes: range: ScheduleLeg multivalued: true inlined_as_list: true + required: true trajectory: range: TrajectoryPoint multivalued: true inlined_as_list: true + required: true state_curves: range: StateCurves inlined: true @@ -193,6 +197,7 @@ classes: range: Satisfaction multivalued: true inlined_as_list: true + required: true cost_band: range: Band robustness_band: diff --git a/schema/requirement.yaml b/schema/requirement.yaml index 3fd55ff..0b131ee 100644 --- a/schema/requirement.yaml +++ b/schema/requirement.yaml @@ -45,6 +45,7 @@ classes: range: Commitment multivalued: true inlined_as_list: true + required: true description: the promises it decomposes into lineage: range: Lineage diff --git a/site/data-model/index.html b/site/data-model/index.html index f16efad..314f108 100644 --- a/site/data-model/index.html +++ b/site/data-model/index.html @@ -291,10 +291,10 @@

REMIT — v1 Data Model

The serialisable object core of Stamp ||--o{ Appetite : "appetites" Stamp ||--o{ Constraint : "steering" Constraint ||--o{ HexCell : "cells" - Plan ||--o| Strategy : "strategy" + Plan ||--|| Strategy : "strategy" Plan ||--o| Stamp : "stamp" Plan ||--o| Materialisation : "materialisation" - Plan ||--o| Scores : "scores" + Plan ||--|| Scores : "scores" Plan ||--o| TideDecision : "tide_decision" Plan ||--o{ Conflict : "conflicts" Materialisation ||--o{ ScheduleLeg : "schedule" @@ -368,7 +368,7 @@

REMIT — v1 Data Model

The serialisable object core of Activity ||--o{ Effect : "effects" Activity ||..o{ Channel : "relevant_channels" CellPredicate ||--o{ CellPredicate : "children" - TimingConstraint ||--o| TimeWindow : "window"

contains (inlined)  ·  references (by id)  ·  o{ many  || one  o| optional. Boxes outside the module are classes defined elsewhere.

CommitmentProvenance

A commitment's ownership and waiver authority (B3).

fieldtypecarddescription
issuing_rolestring0..1
authoritystring0..1
ownerstring0..1
waiver_authoritystring0..1who may waive this commitment
expirydatetime0..1
rationalestring0..1

Requirement

identified

The command's narrative intent, owned as a versioned immutable object (DEC-5/18). Amend it and you get a new version with lineage — the original stands. Content-addressed: its id is the hash of its canonical form.

fieldtypecarddescription
ididstring1content id (sha256:…) of the canonical form (DEC-35)
versioninteger0..1amendment → new version (B5)
intentstring1the command's narrative, in their words
provenanceAttribution0..1issuing role/authority + time
commitmentsCommitment0..*the promises it decomposes into
lineageLineage0..1

Commitment

identified

One thing the plan must achieve (DEC-16/17/22). Either hard (must hold) or soft with a priority. Carries the negotiated contract text and any unresolved ambiguities, and walks a lifecycle of states.

fieldtypecarddescription
ididstring1e.g. "cmt-1"
activityActivity1
criticalityCriticality1
provenanceCommitmentProvenance0..1
captureCapture0..1the negotiated contract record (DEC-17)
stateCommitmentState0..1

Capture

The negotiation record behind a commitment — answers, the canonical contract text, open ambiguities (DEC-17).

fieldtypecarddescription
answersAnswer0..*
echo_backstring0..1the canonical contract text read back to the operator
ambiguitiesAmbiguity0..*resolvable-later

Answer

fieldtypecarddescription
slotstring0..1which question slot
valuestring0..1
statusAnswerStatus0..1
bystring0..1
atdatetime0..1

Ambiguity

fieldtypecarddescription
questionstring0..1
statusAmbiguityStatus0..1
consequencestring0..1what hinges on resolving it

Activity

The verb of a commitment — visit, loiter, avoid, transit, maintain (DEC-16/21). `where` is a Waypoint or a boolean predicate over map cells (DEC-33); `when` is a window, before/after, or recurring. Always inlined in its Commitment (no independent identity).

fieldtypecarddescription
typeActivityType1
whereWaypoint0..1the location — a map cell (skeleton uses {x, y, alias})
where_predicateCellPredicate0..1alternative to `where` — a boolean predicate tree over cells (DEC-33)
whenTimingConstraint0..1
durationDurationBound0..1
modifiersActivityModifiers0..1
effectsEffect0..*"v1: self-state delta / persistent marker (always empty in the skeleton)"
outcome_modelstring0..1"v1: boolean | duration; pluggable (probabilistic = H3)"
relevant_channelsChannel0..*channel ids to sample / trigger on (DEC-21)

CellPredicate

A boolean tree over map cells (DEC-33): an atom, or and/or/not of sub-predicates. Atoms are a pluggable registry (land-cover, elevation, slope, static-LOS, near(feature, dist)). Conceptual — the skeleton uses Waypoints only.

fieldtypecarddescription
opstring0..1"atom | and | or | not"
atomstring0..1the atom name when op = atom (e.g. land-cover, slope)
paramsstring0..1atom parameters
childrenCellPredicate0..*sub-predicates for and/or/not

TimingConstraint

When an activity must happen — exactly one form is used (DEC-16).

fieldtypecarddescription
windowTimeWindow0..1an absolute or anchored interval
before_mininteger0..1deadline, in mission minutes
after_mininteger0..1earliest start, in mission minutes
recurringstring0..1"recurring(period, anchor) — conceptual"

TimeWindow

fieldtypecarddescription
start_mininteger0..1
end_mininteger0..1

DurationBound

fieldtypecarddescription
min_mininteger0..1minimum dwell, in minutes
max_mininteger0..1

ActivityModifiers

fieldtypecarddescription
stationaryboolean0..1
be_at_rolestring0..1"entry | exit | visit"
afterstring0..1sequencing — id of a commitment this must follow (e.g. "cmt-1")

World 14 classes

The world the plan moves through: baseline, channels, movement, excursions, effects (DEC-7/19/20/21).

erDiagram
+  TimingConstraint ||--o| TimeWindow : "window"

contains (inlined)  ·  references (by id)  ·  o{ many  || one  o| optional. Boxes outside the module are classes defined elsewhere.

CommitmentProvenance

A commitment's ownership and waiver authority (B3).

fieldtypecarddescription
issuing_rolestring0..1
authoritystring0..1
ownerstring0..1
waiver_authoritystring0..1who may waive this commitment
expirydatetime0..1
rationalestring0..1

Requirement

identified

The command's narrative intent, owned as a versioned immutable object (DEC-5/18). Amend it and you get a new version with lineage — the original stands. Content-addressed: its id is the hash of its canonical form.

fieldtypecarddescription
ididstring1content id (sha256:…) of the canonical form (DEC-35)
versioninteger0..1amendment → new version (B5)
intentstring1the command's narrative, in their words
provenanceAttribution0..1issuing role/authority + time
commitmentsCommitment1..*the promises it decomposes into
lineageLineage0..1

Commitment

identified

One thing the plan must achieve (DEC-16/17/22). Either hard (must hold) or soft with a priority. Carries the negotiated contract text and any unresolved ambiguities, and walks a lifecycle of states.

fieldtypecarddescription
ididstring1e.g. "cmt-1"
activityActivity1
criticalityCriticality1
provenanceCommitmentProvenance0..1
captureCapture0..1the negotiated contract record (DEC-17)
stateCommitmentState0..1

Capture

The negotiation record behind a commitment — answers, the canonical contract text, open ambiguities (DEC-17).

fieldtypecarddescription
answersAnswer0..*
echo_backstring0..1the canonical contract text read back to the operator
ambiguitiesAmbiguity0..*resolvable-later

Answer

fieldtypecarddescription
slotstring0..1which question slot
valuestring0..1
statusAnswerStatus0..1
bystring0..1
atdatetime0..1

Ambiguity

fieldtypecarddescription
questionstring0..1
statusAmbiguityStatus0..1
consequencestring0..1what hinges on resolving it

Activity

The verb of a commitment — visit, loiter, avoid, transit, maintain (DEC-16/21). `where` is a Waypoint or a boolean predicate over map cells (DEC-33); `when` is a window, before/after, or recurring. Always inlined in its Commitment (no independent identity).

fieldtypecarddescription
typeActivityType1
whereWaypoint0..1the location — a map cell (skeleton uses {x, y, alias})
where_predicateCellPredicate0..1alternative to `where` — a boolean predicate tree over cells (DEC-33)
whenTimingConstraint0..1
durationDurationBound0..1
modifiersActivityModifiers0..1
effectsEffect0..*"v1: self-state delta / persistent marker (always empty in the skeleton)"
outcome_modelstring0..1"v1: boolean | duration; pluggable (probabilistic = H3)"
relevant_channelsChannel0..*channel ids to sample / trigger on (DEC-21)

CellPredicate

A boolean tree over map cells (DEC-33): an atom, or and/or/not of sub-predicates. Atoms are a pluggable registry (land-cover, elevation, slope, static-LOS, near(feature, dist)). Conceptual — the skeleton uses Waypoints only.

fieldtypecarddescription
opstring0..1"atom | and | or | not"
atomstring0..1the atom name when op = atom (e.g. land-cover, slope)
paramsstring0..1atom parameters
childrenCellPredicate0..*sub-predicates for and/or/not

TimingConstraint

When an activity must happen — exactly one form is used (DEC-16).

fieldtypecarddescription
windowTimeWindow0..1an absolute or anchored interval
before_mininteger0..1deadline, in mission minutes
after_mininteger0..1earliest start, in mission minutes
recurringstring0..1"recurring(period, anchor) — conceptual"

TimeWindow

fieldtypecarddescription
start_mininteger0..1
end_mininteger0..1

DurationBound

fieldtypecarddescription
min_mininteger0..1minimum dwell, in minutes
max_mininteger0..1

ActivityModifiers

fieldtypecarddescription
stationaryboolean0..1
be_at_rolestring0..1"entry | exit | visit"
afterstring0..1sequencing — id of a commitment this must follow (e.g. "cmt-1")

World 14 classes

The world the plan moves through: baseline, channels, movement, excursions, effects (DEC-7/19/20/21).

erDiagram
   Baseline {
   }
   Changepoint {
@@ -470,10 +470,10 @@ 

REMIT — v1 Data Model

The serialisable object core of Stamp ||--o{ Appetite : "appetites" Stamp ||--o{ Constraint : "steering" Constraint ||--o{ HexCell : "cells" - Plan ||--o| Strategy : "strategy" + Plan ||--|| Strategy : "strategy" Plan ||--o| Stamp : "stamp" Plan ||--o| Materialisation : "materialisation" - Plan ||--o| Scores : "scores" + Plan ||--|| Scores : "scores" Plan ||--o| TideDecision : "tide_decision" Plan ||--o{ Conflict : "conflicts" Materialisation ||--o{ ScheduleLeg : "schedule" @@ -483,7 +483,7 @@

REMIT — v1 Data Model

The serialisable object core of ScheduleLeg ||..o| Commitment : "commitment_id" Scores ||--o{ Satisfaction : "satisfaction" Satisfaction ||..o| Commitment : "commitment_id" - Conflict ||..o{ Commitment : "parties"

contains (inlined)  ·  references (by id)  ·  o{ many  || one  o| optional. Boxes outside the module are classes defined elsewhere.

Stamp

Every input that determines a plan, bundled into one identity (DEC-23/24/29): which requirement, which world, which config core, what appetites and steering, which kernel and seed. A plan's id IS the hash of its Stamp, so two plans are comparable only when their stamps share a basis (the comparability guard). Skeleton additions (DEC-47): `profile_version` and `start` are part of identity because the plan depends on the platform and the starting state.

fieldtypecarddescription
requirement_versionRequirement0..1id of the requirement version
baseline_versionBaseline0..1id of the baseline version
excursionsExcursion0..*ids of the excursion versions (empty in v1)
config_core_hashstring0..1hash of the world-defining config core — medium/channels/movement-model/providers/vocabulary; the instance shell is excluded (DEC-48)
profile_versionProfile0..1id of the profile version (skeleton addition, DEC-47)
startStartState0..1the starting state (skeleton addition, DEC-47)
appetitesAppetite0..*the implementer's risk dials (DEC-6), as an axis→setting map
steeringConstraint0..*interpreted operator gestures / no-go constraints (DEC-24)
kernel_versionstring0..1e.g. "mock-0.1" — part of identity (DEC-29)
strategy_seedinteger0..1RNG seed for strategy ordering — part of identity (DEC-29)

StartState

The starting position (H3 hex) and clock baked into a Stamp (skeleton, DEC-47; hex per ADR-0016).

fieldtypecarddescription
h3string1the H3 cell index (res 9) of the start cell
latfloat0..1cell-centre latitude (optional, for rendering)
lngfloat0..1cell-centre longitude (optional, for rendering)
clock_mininteger0..1mission clock at start (minutes)

Appetite

identified

One risk-appetite dial setting (DEC-6) — e.g. tempo = rapid, exposure = cautious. Keyed by axis, so a Stamp's appetites serialise as an axis→setting map.

fieldtypecarddescription
axisidstring1"e.g. tempo, exposure"
settingstring0..1"e.g. deliberate/balanced/rapid, bold/balanced/cautious"

Constraint

One interpreted operator steering gesture (DEC-24) — e.g. a no-go region.

fieldtypecarddescription
typestring0..1"e.g. no-go"
cellsHexCell0..*

Strategy

One candidate strategy in the plan handful (skeleton).

fieldtypecarddescription
keyStrategyKey1
labelstring0..1
axisstring0..1the axis it optimises, e.g. "time/speed"
blurbstring0..1a one-line description

Plan

identified

A candidate solution whose id IS the hash of its Stamp (DEC-5/22/29). Its materialisation (schedule, trajectory, state curves) is cached and regenerable; its scores and first-class conflicts make it comparable. The skeleton also carries the chosen `strategy` and the `tide_decision` weighing.

fieldtypecarddescription
ididstring1"= hash(Stamp, strategy)"
strategyStrategy0..1
stampStamp0..1authoritative — the plan's identity basis
materialisationMaterialisation0..1cached, regenerable; null when infeasible
scoresScores0..1
tide_decisionTideDecision0..1the exfil wait-vs-detour weighing (ADR-0006); null when no ford
conflictsConflict0..*first-class, named clashes (C1)

Materialisation

The cached, regenerable working-out of a plan; absent when the plan is infeasible.

fieldtypecarddescription
scheduleScheduleLeg0..*
trajectoryTrajectoryPoint0..*
state_curvesStateCurves0..1
tideTideDecision0..1the tide decision at plan time
verifiedboolean0..1
kernel_version_verifiedstring0..1the kernel version that verified the materialisation

ScheduleLeg

One leg of a plan's schedule. Exfil legs may carry a tide hold (ADR-0006).

fieldtypecarddescription
kindScheduleLegKind1
labelstring0..1
start_minfloat0..1
end_minfloat0..1
commitment_idCommitment0..1the commitment this leg serves (visit/exfil legs)

TrajectoryPoint

One sampled point on the platform's path (H3 hex per ADR-0016); time/fuel rounded to 1 dp for IEEE stability.

fieldtypecarddescription
h3string1the H3 cell index (res 9)
latfloat0..1cell-centre latitude
lngfloat0..1cell-centre longitude
tfloat0..1mission minutes
fuel_pctfloat0..1

StateCurves

The end-state of the platform's curves over the plan (v1 carries fuel only).

fieldtypecarddescription
fuel_end_pctfloat0..1

Scores

A plan's comparable scores, read under the comparability guard (A2/C2/C6, NF10).

fieldtypecarddescription
satisfactionSatisfaction0..*
cost_bandBand0..1
robustness_bandBand0..1

Satisfaction

One commitment's verdict and slack within a plan.

fieldtypecarddescription
commitment_idCommitment0..1
labelstring0..1
margin_minfloat0..1slack in minutes; may be negative
margin_bandMarginBand0..1
verdictVerdict0..1

TideDecision

The result of the tidal-ford wait-vs-detour weighing (ADR-0006): the kernel materialises the exfil both ways — wait at the bank vs a ford-free detour — and commits to the earlier RV arrival, publishing the choice here.

fieldtypecarddescription
modeTideMode1
wait_minfloat0..1minutes held at the bank before crossing
ford_rvfloat0..1mission minutes the ford route reaches the RV
detour_rvfloat0..1mission minutes the detour route reaches the RV; null when no detour exists
rv_minfloat0..1mission minutes the chosen route reaches the RV (the committed arrival)
narrativestring0..1the choice, in words

Conflict

identified

A first-class, named clash when commitments cannot all be kept (C1) — not a silent failure.

fieldtypecarddescription
ididstring1
kindConflictKind1
partiesCommitment0..*the commitments in tension
narrativestring0..1what the clash is, in words

Records 13 classes

The decision and execution record: SelectionRationale and the append-only ExecutionLog (DEC-23/25/26).

erDiagram
+  Conflict ||..o{ Commitment : "parties"

contains (inlined)  ·  references (by id)  ·  o{ many  || one  o| optional. Boxes outside the module are classes defined elsewhere.

Stamp

Every input that determines a plan, bundled into one identity (DEC-23/24/29): which requirement, which world, which config core, what appetites and steering, which kernel and seed. A plan's id IS the hash of its Stamp, so two plans are comparable only when their stamps share a basis (the comparability guard). Skeleton additions (DEC-47): `profile_version` and `start` are part of identity because the plan depends on the platform and the starting state.

fieldtypecarddescription
requirement_versionRequirement0..1id of the requirement version
baseline_versionBaseline0..1id of the baseline version
excursionsExcursion0..*ids of the excursion versions (empty in v1)
config_core_hashstring0..1hash of the world-defining config core — medium/channels/movement-model/providers/vocabulary; the instance shell is excluded (DEC-48)
profile_versionProfile0..1id of the profile version (skeleton addition, DEC-47)
startStartState0..1the starting state (skeleton addition, DEC-47)
appetitesAppetite0..*the implementer's risk dials (DEC-6), as an axis→setting map
steeringConstraint0..*interpreted operator gestures / no-go constraints (DEC-24)
kernel_versionstring0..1e.g. "mock-0.1" — part of identity (DEC-29)
strategy_seedinteger0..1RNG seed for strategy ordering — part of identity (DEC-29)

StartState

The starting position (H3 hex) and clock baked into a Stamp (skeleton, DEC-47; hex per ADR-0016).

fieldtypecarddescription
h3string1the H3 cell index (res 9) of the start cell
latfloat0..1cell-centre latitude (optional, for rendering)
lngfloat0..1cell-centre longitude (optional, for rendering)
clock_mininteger0..1mission clock at start (minutes)

Appetite

identified

One risk-appetite dial setting (DEC-6) — e.g. tempo = rapid, exposure = cautious. Keyed by axis, so a Stamp's appetites serialise as an axis→setting map.

fieldtypecarddescription
axisidstring1"e.g. tempo, exposure"
settingstring0..1"e.g. deliberate/balanced/rapid, bold/balanced/cautious"

Constraint

One interpreted operator steering gesture (DEC-24) — e.g. a no-go region.

fieldtypecarddescription
typestring0..1"e.g. no-go"
cellsHexCell0..*

Strategy

One candidate strategy in the plan handful (skeleton).

fieldtypecarddescription
keyStrategyKey1
labelstring0..1
axisstring0..1the axis it optimises, e.g. "time/speed"
blurbstring0..1a one-line description

Plan

identified

A candidate solution whose id IS the hash of its Stamp (DEC-5/22/29). Its materialisation (schedule, trajectory, state curves) is cached and regenerable; its scores and first-class conflicts make it comparable. The skeleton also carries the chosen `strategy` and the `tide_decision` weighing.

fieldtypecarddescription
ididstring1"= hash(Stamp, strategy)"
strategyStrategy1
stampStamp0..1authoritative — the plan's identity basis
materialisationMaterialisation0..1cached, regenerable; null when infeasible
scoresScores1
tide_decisionTideDecision0..1the exfil wait-vs-detour weighing (ADR-0006); null when no ford
conflictsConflict0..*first-class, named clashes (C1)

Materialisation

The cached, regenerable working-out of a plan; absent when the plan is infeasible.

fieldtypecarddescription
scheduleScheduleLeg1..*
trajectoryTrajectoryPoint1..*
state_curvesStateCurves0..1
tideTideDecision0..1the tide decision at plan time
verifiedboolean0..1
kernel_version_verifiedstring0..1the kernel version that verified the materialisation

ScheduleLeg

One leg of a plan's schedule. Exfil legs may carry a tide hold (ADR-0006).

fieldtypecarddescription
kindScheduleLegKind1
labelstring0..1
start_minfloat0..1
end_minfloat0..1
commitment_idCommitment0..1the commitment this leg serves (visit/exfil legs)

TrajectoryPoint

One sampled point on the platform's path (H3 hex per ADR-0016); time/fuel rounded to 1 dp for IEEE stability.

fieldtypecarddescription
h3string1the H3 cell index (res 9)
latfloat0..1cell-centre latitude
lngfloat0..1cell-centre longitude
tfloat0..1mission minutes
fuel_pctfloat0..1

StateCurves

The end-state of the platform's curves over the plan (v1 carries fuel only).

fieldtypecarddescription
fuel_end_pctfloat0..1

Scores

A plan's comparable scores, read under the comparability guard (A2/C2/C6, NF10).

fieldtypecarddescription
satisfactionSatisfaction1..*
cost_bandBand0..1
robustness_bandBand0..1

Satisfaction

One commitment's verdict and slack within a plan.

fieldtypecarddescription
commitment_idCommitment0..1
labelstring0..1
margin_minfloat0..1slack in minutes; may be negative
margin_bandMarginBand0..1
verdictVerdict0..1

TideDecision

The result of the tidal-ford wait-vs-detour weighing (ADR-0006): the kernel materialises the exfil both ways — wait at the bank vs a ford-free detour — and commits to the earlier RV arrival, publishing the choice here.

fieldtypecarddescription
modeTideMode1
wait_minfloat0..1minutes held at the bank before crossing
ford_rvfloat0..1mission minutes the ford route reaches the RV
detour_rvfloat0..1mission minutes the detour route reaches the RV; null when no detour exists
rv_minfloat0..1mission minutes the chosen route reaches the RV (the committed arrival)
narrativestring0..1the choice, in words

Conflict

identified

A first-class, named clash when commitments cannot all be kept (C1) — not a silent failure.

fieldtypecarddescription
ididstring1
kindConflictKind1
partiesCommitment0..*the commitments in tension
narrativestring0..1what the clash is, in words

Records 13 classes

The decision and execution record: SelectionRationale and the append-only ExecutionLog (DEC-23/25/26).

erDiagram
   SelectionRationale {
   }
   ChosenBands {
diff --git a/tsconfig.json b/tsconfig.json
index 996b2a8..1162680 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,7 +3,7 @@
     "target": "ES2022",
     "module": "ESNext",
     "moduleResolution": "bundler",
-    "lib": ["ES2022", "DOM", "DOM.Iterable"],
+    "lib": ["ES2023", "DOM", "DOM.Iterable"],
 
     "allowJs": true,
     "checkJs": true,

From 44061c02c68df85d4f2a60119bf08a510510f453 Mon Sep 17 00:00:00 2001
From: Claude 
Date: Tue, 16 Jun 2026 20:10:35 +0000
Subject: [PATCH 4/5] =?UTF-8?q?refactor(types):=20P5=20=E2=80=94=20type=20?=
 =?UTF-8?q?the=20map's=20data-in=20+=20accessor=20params=20(drop=20any-cas?=
 =?UTF-8?q?ts)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

views/map.js took its render options as `any`. Added a RenderOpts typedef (plans:
Plan[], selected: Plan|null, assets: Asset[] — the serialisable data — plus typed
view-state fields) and typed buildLayers/setData/render against it, so the plan/
asset/candidate/obstruction iterations infer real types instead of casting each
callback param to any. Accessor params (col/alpha/path/…) are annotated with real
types (Asset/Plan/HexCell), not any.

The residual casts in map.js are the genuine library boundary: deck.gl's typed
accessor props (Accessor) don't accept a plain JSDoc callback, so the get*
props are cast there (documented, like the existing MapOptions cast) — the accessor
BODIES stay type-checked. Plus idiomatic `keyof typeof` index narrowing.

Also marked TrajectoryPoint.lat/lng/t/fuel_pct required (always present on a rendered
point) so route coords type as number, not number|undefined — regenerated.

map.js any-casts 39 → 25. 0 typecheck errors; 32 unit green; golden ids unchanged.

https://claude.ai/code/session_01EhtBoKXg6bHnKdquacyknf
---
 app/js/views/map.js          | 54 ++++++++++++++++++++++++++----------
 schema/gen/remit.schema.json | 26 ++++++-----------
 schema/gen/remit.ts          |  8 +++---
 schema/plan.yaml             |  4 +++
 site/data-model/index.html   |  2 +-
 5 files changed, 58 insertions(+), 36 deletions(-)

diff --git a/app/js/views/map.js b/app/js/views/map.js
index ccf2819..d68da21 100644
--- a/app/js/views/map.js
+++ b/app/js/views/map.js
@@ -15,6 +15,28 @@ import { latLngToId } from '../kernel/hexgrid.js';
 import { STRAT_COLORS } from './render.js';
 import { ALLEGIANCE_COLOR, symbolOf, confidenceOpacity } from '../orbat/orbat.js';
 
+/** @typedef {import('../../../schema/gen/remit').Plan} Plan */
+/** @typedef {import('../../../schema/gen/remit').Asset} Asset */
+/** @typedef {import('../../../schema/gen/remit').HexCell} HexCell */
+/**
+ * The projection inputs (display-only; DEC-24/NF1). `plans`/`selected`/`assets`
+ * are the serialisable data (generated types); the rest are transient view-state.
+ * @typedef {object} RenderOpts
+ * @property {Plan[]} [plans]
+ * @property {Plan|null} [selected]
+ * @property {number} [t]
+ * @property {(HexCell & {id?: number})|null} [target]
+ * @property {(HexCell & {id?: number})|null} [rv]
+ * @property {(HexCell & {key?: string})[]|null} [candidates]
+ * @property {HexCell|null} [highlight]
+ * @property {HexCell[]} [obstructions]
+ * @property {{h3: string, id?: number}[]} [nogo]
+ * @property {({h3?: string, id?: number}|number)[]} [blocked]
+ * @property {Asset[]} [assets]
+ * @property {string|null} [selectedAsset]
+ * @property {boolean} [follow]
+ */
+
 const ALLEGIANCE_RGB = Object.fromEntries(Object.entries(ALLEGIANCE_COLOR).map(([k, v]) => [k, [parseInt(v.slice(1, 3), 16), parseInt(v.slice(3, 5), 16), parseInt(v.slice(5, 7), 16)]]));
 
 const rgb = (/** @type {string} */ h) => [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];
@@ -109,7 +131,7 @@ export function makeMap(el, baseline, ao, places) {
     return null;
   };
 
-  function buildLayers(/** @type {any} */ opts) {
+  function buildLayers(/** @type {RenderOpts} */ opts) {
     const { plans = [], selected = null, t = 0, target = null, rv = null,
             candidates = null, highlight = null, obstructions = [], nogo = [], blocked = [],
             assets = [], selectedAsset = null } = opts;
@@ -125,6 +147,10 @@ export function makeMap(el, baseline, ao, places) {
     // Hex-grid overlay (terrain + situational no-go / blocked highlights). Toggleable:
     // when hidden, the basemap shows through and the operational overlay (routes,
     // markers, vehicle ghost) below still renders.
+    // deck.gl's typed accessor props (Accessor) don't accept a plain
+    // JSDoc-typed callback, so the get* functions below are cast at this library
+    // boundary (as with the MapOptions cast above). Their PARAMETERS are typed, so
+    // the accessor bodies stay checked — only the deck.gl prop shape is asserted.
     const layers = [];
     if (showHexes) {
       layers.push(new H3HexagonLayer({
@@ -137,13 +163,13 @@ export function makeMap(el, baseline, ao, places) {
     }
 
     // Routes — non-selected faint, selected bold (or all bold in compare mode).
-    const path = (/** @type {any} */ p) => p.materialisation.trajectory.map((/** @type {any} */ q) => [q.lng, q.lat]);
+    const path = (/** @type {Plan} */ p) => (p.materialisation?.trajectory ?? []).map((q) => [q.lng, q.lat]);
     for (const p of plans) {
       if (!p.materialisation) continue;
       const isSel = selected && p.id === selected.id;
       const base = STRAT_RGB[p.strategy.key] || [200, 200, 200];
       layers.push(new PathLayer({
-        id: 'route-' + p.strategy.key, data: [p], getPath: path,
+        id: 'route-' + p.strategy.key, data: [p], getPath: /** @type {any} */ (path),
         getColor: /** @type {any} */ (selected && !isSel ? [base[0], base[1], base[2], 110] : base),
         getWidth: isSel ? 3.6 : (selected ? 2 : 3), widthUnits: 'pixels', capRounded: true, jointRounded: true,
       }));
@@ -171,10 +197,10 @@ export function makeMap(el, baseline, ao, places) {
     // marker, range ring(s) (red: faint detection + bold engagement; others: one extent), and a
     // label. Intel CONFIDENCE sets marker/symbol emphasis (opacity). Display-only (NF9): drawn
     // from the authored roster, never derived. The selected asset's marker is enlarged/outlined.
-    const placed = assets.map((/** @type {any} */ a) => ({ a, pos: assetLngLat(a) })).filter((/** @type {any} */ x) => x.pos);
+    const placed = assets.map((a) => ({ a, pos: assetLngLat(a) })).filter((x) => x.pos);
     if (placed.length) {
-      const col = (/** @type {any} */ a) => ALLEGIANCE_RGB[a.allegiance] ?? [200, 210, 220];
-      const alpha = (/** @type {any} */ a, /** @type {number} */ base) => Math.round(base * confidenceOpacity(a));
+      const col = (/** @type {Asset} */ a) => ALLEGIANCE_RGB[a.allegiance] ?? [200, 210, 220];
+      const alpha = (/** @type {Asset} */ a, /** @type {number} */ base) => Math.round(base * confidenceOpacity(a));
 
       // Range rings: red draws an outer (faint) detection ring + an inner (bold) engagement ring;
       // green/blue draw their single extent ring.
@@ -222,19 +248,19 @@ export function makeMap(el, baseline, ao, places) {
     return layers;
   }
 
-  function setData(/** @type {any} */ opts) {
+  function setData(/** @type {RenderOpts} */ opts) {
     const { selected = null, t = 0, plans = [], highlight = null, nogo = [], blocked = [], obstructions = [], assets = [] } = opts;
     el.dataset.fordState = fordOpenAt(t) ? 'open' : 'closed';
     // Expose the placed ORBAT roster to the e2e suite: "id:allegiance:kind:confidence" per asset.
-    const placedAssets = assets.filter((/** @type {any} */ a) => assetLngLat(a));
-    el.dataset.assets = placedAssets.map((/** @type {any} */ a) => `${a.id}:${a.allegiance}:${a.kind ?? ''}:${a.confidence ?? ''}`).join('|');
+    const placedAssets = assets.filter((a) => assetLngLat(a));
+    el.dataset.assets = placedAssets.map((a) => `${a.id}:${a.allegiance}:${a.kind ?? ''}:${a.confidence ?? ''}`).join('|');
     el.dataset.assetCount = String(placedAssets.length);
     if (selected) { const g = stateAt(selected, t); el.dataset.ghost = g ? `${g.lng.toFixed(4)},${g.lat.toFixed(4)},${g.phase}` : ''; }
-    else el.dataset.ghost = plans.filter((/** @type {any} */ p) => p.materialisation).map((/** @type {any} */ p) => { const g = stateAt(p, t); return g ? `${p.strategy.key}:${g.lng.toFixed(4)},${g.lat.toFixed(4)}` : ''; }).filter(Boolean).join('|');
+    else el.dataset.ghost = plans.filter((p) => p.materialisation).map((p) => { const g = stateAt(p, t); return g ? `${p.strategy.key}:${g.lng.toFixed(4)},${g.lat.toFixed(4)}` : ''; }).filter(Boolean).join('|');
     el.dataset.highlight = highlight ? shortH3(highlight.h3) : '';
-    el.dataset.nogo = nogo.map((/** @type {any} */ c) => shortH3(c.h3)).join('|');
-    el.dataset.blocked = blocked.map((/** @type {any} */ c) => shortH3(c.h3 ?? ao.indexes[c.id ?? c])).join('|');
-    el.dataset.obstructions = obstructions.map((/** @type {any} */ o) => shortH3(o.h3)).join('|');
+    el.dataset.nogo = nogo.map((c) => shortH3(c.h3)).join('|');
+    el.dataset.blocked = blocked.map((c) => shortH3(typeof c === 'number' ? ao.indexes[c] : c.h3 ?? ao.indexes[c.id ?? 0])).join('|');
+    el.dataset.obstructions = obstructions.map((o) => shortH3(o.h3)).join('|');
   }
 
   // Execute-mode follow-cam: keep the live vehicle comfortably on screen (esp. when zoomed
@@ -251,7 +277,7 @@ export function makeMap(el, baseline, ao, places) {
     }
   }
 
-  function render(/** @type {any} */ opts = {}) {
+  function render(/** @type {RenderOpts} */ opts = {}) {
     lastOpts = opts;
     overlay.setProps({ layers: buildLayers(opts) });   // deck overlay renders above the basemap
     setData(opts);
diff --git a/schema/gen/remit.schema.json b/schema/gen/remit.schema.json
index 34c3ae1..d482827 100644
--- a/schema/gen/remit.schema.json
+++ b/schema/gen/remit.schema.json
@@ -2973,10 +2973,7 @@
             "description": "One sampled point on the platform's path (H3 hex per ADR-0016); time/fuel rounded to 1 dp for IEEE stability.",
             "properties": {
                 "fuel_pct": {
-                    "type": [
-                        "number",
-                        "null"
-                    ]
+                    "type": "number"
                 },
                 "h3": {
                     "description": "the H3 cell index (res 9)",
@@ -2984,28 +2981,23 @@
                 },
                 "lat": {
                     "description": "cell-centre latitude",
-                    "type": [
-                        "number",
-                        "null"
-                    ]
+                    "type": "number"
                 },
                 "lng": {
                     "description": "cell-centre longitude",
-                    "type": [
-                        "number",
-                        "null"
-                    ]
+                    "type": "number"
                 },
                 "t": {
                     "description": "mission minutes",
-                    "type": [
-                        "number",
-                        "null"
-                    ]
+                    "type": "number"
                 }
             },
             "required": [
-                "h3"
+                "h3",
+                "lat",
+                "lng",
+                "t",
+                "fuel_pct"
             ],
             "title": "TrajectoryPoint",
             "type": "object"
diff --git a/schema/gen/remit.ts b/schema/gen/remit.ts
index a69e64a..9644b94 100644
--- a/schema/gen/remit.ts
+++ b/schema/gen/remit.ts
@@ -997,12 +997,12 @@ export interface TrajectoryPoint {
     /** the H3 cell index (res 9) */
     h3: string,
     /** cell-centre latitude */
-    lat?: number,
+    lat: number,
     /** cell-centre longitude */
-    lng?: number,
+    lng: number,
     /** mission minutes */
-    t?: number,
-    fuel_pct?: number,
+    t: number,
+    fuel_pct: number,
 }
 
 
diff --git a/schema/plan.yaml b/schema/plan.yaml
index 18d5311..32e0b11 100644
--- a/schema/plan.yaml
+++ b/schema/plan.yaml
@@ -176,15 +176,19 @@ classes:
         description: the H3 cell index (res 9)
       lat:
         range: float
+        required: true
         description: cell-centre latitude
       lng:
         range: float
+        required: true
         description: cell-centre longitude
       t:
         range: float
+        required: true
         description: mission minutes
       fuel_pct:
         range: float
+        required: true
   StateCurves:
     description: The end-state of the platform's curves over the plan (v1 carries fuel only).
     attributes:
diff --git a/site/data-model/index.html b/site/data-model/index.html
index 314f108..c98a027 100644
--- a/site/data-model/index.html
+++ b/site/data-model/index.html
@@ -483,7 +483,7 @@ 

REMIT — v1 Data Model

The serialisable object core of ScheduleLeg ||..o| Commitment : "commitment_id" Scores ||--o{ Satisfaction : "satisfaction" Satisfaction ||..o| Commitment : "commitment_id" - Conflict ||..o{ Commitment : "parties"

contains (inlined)  ·  references (by id)  ·  o{ many  || one  o| optional. Boxes outside the module are classes defined elsewhere.

Stamp

Every input that determines a plan, bundled into one identity (DEC-23/24/29): which requirement, which world, which config core, what appetites and steering, which kernel and seed. A plan's id IS the hash of its Stamp, so two plans are comparable only when their stamps share a basis (the comparability guard). Skeleton additions (DEC-47): `profile_version` and `start` are part of identity because the plan depends on the platform and the starting state.

fieldtypecarddescription
requirement_versionRequirement0..1id of the requirement version
baseline_versionBaseline0..1id of the baseline version
excursionsExcursion0..*ids of the excursion versions (empty in v1)
config_core_hashstring0..1hash of the world-defining config core — medium/channels/movement-model/providers/vocabulary; the instance shell is excluded (DEC-48)
profile_versionProfile0..1id of the profile version (skeleton addition, DEC-47)
startStartState0..1the starting state (skeleton addition, DEC-47)
appetitesAppetite0..*the implementer's risk dials (DEC-6), as an axis→setting map
steeringConstraint0..*interpreted operator gestures / no-go constraints (DEC-24)
kernel_versionstring0..1e.g. "mock-0.1" — part of identity (DEC-29)
strategy_seedinteger0..1RNG seed for strategy ordering — part of identity (DEC-29)

StartState

The starting position (H3 hex) and clock baked into a Stamp (skeleton, DEC-47; hex per ADR-0016).

fieldtypecarddescription
h3string1the H3 cell index (res 9) of the start cell
latfloat0..1cell-centre latitude (optional, for rendering)
lngfloat0..1cell-centre longitude (optional, for rendering)
clock_mininteger0..1mission clock at start (minutes)

Appetite

identified

One risk-appetite dial setting (DEC-6) — e.g. tempo = rapid, exposure = cautious. Keyed by axis, so a Stamp's appetites serialise as an axis→setting map.

fieldtypecarddescription
axisidstring1"e.g. tempo, exposure"
settingstring0..1"e.g. deliberate/balanced/rapid, bold/balanced/cautious"

Constraint

One interpreted operator steering gesture (DEC-24) — e.g. a no-go region.

fieldtypecarddescription
typestring0..1"e.g. no-go"
cellsHexCell0..*

Strategy

One candidate strategy in the plan handful (skeleton).

fieldtypecarddescription
keyStrategyKey1
labelstring0..1
axisstring0..1the axis it optimises, e.g. "time/speed"
blurbstring0..1a one-line description

Plan

identified

A candidate solution whose id IS the hash of its Stamp (DEC-5/22/29). Its materialisation (schedule, trajectory, state curves) is cached and regenerable; its scores and first-class conflicts make it comparable. The skeleton also carries the chosen `strategy` and the `tide_decision` weighing.

fieldtypecarddescription
ididstring1"= hash(Stamp, strategy)"
strategyStrategy1
stampStamp0..1authoritative — the plan's identity basis
materialisationMaterialisation0..1cached, regenerable; null when infeasible
scoresScores1
tide_decisionTideDecision0..1the exfil wait-vs-detour weighing (ADR-0006); null when no ford
conflictsConflict0..*first-class, named clashes (C1)

Materialisation

The cached, regenerable working-out of a plan; absent when the plan is infeasible.

fieldtypecarddescription
scheduleScheduleLeg1..*
trajectoryTrajectoryPoint1..*
state_curvesStateCurves0..1
tideTideDecision0..1the tide decision at plan time
verifiedboolean0..1
kernel_version_verifiedstring0..1the kernel version that verified the materialisation

ScheduleLeg

One leg of a plan's schedule. Exfil legs may carry a tide hold (ADR-0006).

fieldtypecarddescription
kindScheduleLegKind1
labelstring0..1
start_minfloat0..1
end_minfloat0..1
commitment_idCommitment0..1the commitment this leg serves (visit/exfil legs)

TrajectoryPoint

One sampled point on the platform's path (H3 hex per ADR-0016); time/fuel rounded to 1 dp for IEEE stability.

fieldtypecarddescription
h3string1the H3 cell index (res 9)
latfloat0..1cell-centre latitude
lngfloat0..1cell-centre longitude
tfloat0..1mission minutes
fuel_pctfloat0..1

StateCurves

The end-state of the platform's curves over the plan (v1 carries fuel only).

fieldtypecarddescription
fuel_end_pctfloat0..1

Scores

A plan's comparable scores, read under the comparability guard (A2/C2/C6, NF10).

fieldtypecarddescription
satisfactionSatisfaction1..*
cost_bandBand0..1
robustness_bandBand0..1

Satisfaction

One commitment's verdict and slack within a plan.

fieldtypecarddescription
commitment_idCommitment0..1
labelstring0..1
margin_minfloat0..1slack in minutes; may be negative
margin_bandMarginBand0..1
verdictVerdict0..1

TideDecision

The result of the tidal-ford wait-vs-detour weighing (ADR-0006): the kernel materialises the exfil both ways — wait at the bank vs a ford-free detour — and commits to the earlier RV arrival, publishing the choice here.

fieldtypecarddescription
modeTideMode1
wait_minfloat0..1minutes held at the bank before crossing
ford_rvfloat0..1mission minutes the ford route reaches the RV
detour_rvfloat0..1mission minutes the detour route reaches the RV; null when no detour exists
rv_minfloat0..1mission minutes the chosen route reaches the RV (the committed arrival)
narrativestring0..1the choice, in words

Conflict

identified

A first-class, named clash when commitments cannot all be kept (C1) — not a silent failure.

fieldtypecarddescription
ididstring1
kindConflictKind1
partiesCommitment0..*the commitments in tension
narrativestring0..1what the clash is, in words

Records 13 classes

The decision and execution record: SelectionRationale and the append-only ExecutionLog (DEC-23/25/26).

erDiagram
+  Conflict ||..o{ Commitment : "parties"

contains (inlined)  ·  references (by id)  ·  o{ many  || one  o| optional. Boxes outside the module are classes defined elsewhere.

Stamp

Every input that determines a plan, bundled into one identity (DEC-23/24/29): which requirement, which world, which config core, what appetites and steering, which kernel and seed. A plan's id IS the hash of its Stamp, so two plans are comparable only when their stamps share a basis (the comparability guard). Skeleton additions (DEC-47): `profile_version` and `start` are part of identity because the plan depends on the platform and the starting state.

fieldtypecarddescription
requirement_versionRequirement0..1id of the requirement version
baseline_versionBaseline0..1id of the baseline version
excursionsExcursion0..*ids of the excursion versions (empty in v1)
config_core_hashstring0..1hash of the world-defining config core — medium/channels/movement-model/providers/vocabulary; the instance shell is excluded (DEC-48)
profile_versionProfile0..1id of the profile version (skeleton addition, DEC-47)
startStartState0..1the starting state (skeleton addition, DEC-47)
appetitesAppetite0..*the implementer's risk dials (DEC-6), as an axis→setting map
steeringConstraint0..*interpreted operator gestures / no-go constraints (DEC-24)
kernel_versionstring0..1e.g. "mock-0.1" — part of identity (DEC-29)
strategy_seedinteger0..1RNG seed for strategy ordering — part of identity (DEC-29)

StartState

The starting position (H3 hex) and clock baked into a Stamp (skeleton, DEC-47; hex per ADR-0016).

fieldtypecarddescription
h3string1the H3 cell index (res 9) of the start cell
latfloat0..1cell-centre latitude (optional, for rendering)
lngfloat0..1cell-centre longitude (optional, for rendering)
clock_mininteger0..1mission clock at start (minutes)

Appetite

identified

One risk-appetite dial setting (DEC-6) — e.g. tempo = rapid, exposure = cautious. Keyed by axis, so a Stamp's appetites serialise as an axis→setting map.

fieldtypecarddescription
axisidstring1"e.g. tempo, exposure"
settingstring0..1"e.g. deliberate/balanced/rapid, bold/balanced/cautious"

Constraint

One interpreted operator steering gesture (DEC-24) — e.g. a no-go region.

fieldtypecarddescription
typestring0..1"e.g. no-go"
cellsHexCell0..*

Strategy

One candidate strategy in the plan handful (skeleton).

fieldtypecarddescription
keyStrategyKey1
labelstring0..1
axisstring0..1the axis it optimises, e.g. "time/speed"
blurbstring0..1a one-line description

Plan

identified

A candidate solution whose id IS the hash of its Stamp (DEC-5/22/29). Its materialisation (schedule, trajectory, state curves) is cached and regenerable; its scores and first-class conflicts make it comparable. The skeleton also carries the chosen `strategy` and the `tide_decision` weighing.

fieldtypecarddescription
ididstring1"= hash(Stamp, strategy)"
strategyStrategy1
stampStamp0..1authoritative — the plan's identity basis
materialisationMaterialisation0..1cached, regenerable; null when infeasible
scoresScores1
tide_decisionTideDecision0..1the exfil wait-vs-detour weighing (ADR-0006); null when no ford
conflictsConflict0..*first-class, named clashes (C1)

Materialisation

The cached, regenerable working-out of a plan; absent when the plan is infeasible.

fieldtypecarddescription
scheduleScheduleLeg1..*
trajectoryTrajectoryPoint1..*
state_curvesStateCurves0..1
tideTideDecision0..1the tide decision at plan time
verifiedboolean0..1
kernel_version_verifiedstring0..1the kernel version that verified the materialisation

ScheduleLeg

One leg of a plan's schedule. Exfil legs may carry a tide hold (ADR-0006).

fieldtypecarddescription
kindScheduleLegKind1
labelstring0..1
start_minfloat0..1
end_minfloat0..1
commitment_idCommitment0..1the commitment this leg serves (visit/exfil legs)

TrajectoryPoint

One sampled point on the platform's path (H3 hex per ADR-0016); time/fuel rounded to 1 dp for IEEE stability.

fieldtypecarddescription
h3string1the H3 cell index (res 9)
latfloat1cell-centre latitude
lngfloat1cell-centre longitude
tfloat1mission minutes
fuel_pctfloat1

StateCurves

The end-state of the platform's curves over the plan (v1 carries fuel only).

fieldtypecarddescription
fuel_end_pctfloat0..1

Scores

A plan's comparable scores, read under the comparability guard (A2/C2/C6, NF10).

fieldtypecarddescription
satisfactionSatisfaction1..*
cost_bandBand0..1
robustness_bandBand0..1

Satisfaction

One commitment's verdict and slack within a plan.

fieldtypecarddescription
commitment_idCommitment0..1
labelstring0..1
margin_minfloat0..1slack in minutes; may be negative
margin_bandMarginBand0..1
verdictVerdict0..1

TideDecision

The result of the tidal-ford wait-vs-detour weighing (ADR-0006): the kernel materialises the exfil both ways — wait at the bank vs a ford-free detour — and commits to the earlier RV arrival, publishing the choice here.

fieldtypecarddescription
modeTideMode1
wait_minfloat0..1minutes held at the bank before crossing
ford_rvfloat0..1mission minutes the ford route reaches the RV
detour_rvfloat0..1mission minutes the detour route reaches the RV; null when no detour exists
rv_minfloat0..1mission minutes the chosen route reaches the RV (the committed arrival)
narrativestring0..1the choice, in words

Conflict

identified

A first-class, named clash when commitments cannot all be kept (C1) — not a silent failure.

fieldtypecarddescription
ididstring1
kindConflictKind1
partiesCommitment0..*the commitments in tension
narrativestring0..1what the clash is, in words

Records 13 classes

The decision and execution record: SelectionRationale and the append-only ExecutionLog (DEC-23/25/26).

erDiagram
   SelectionRationale {
   }
   ChosenBands {

From 4a9d90fe9a0265804e9f942e82002972b01c4c0a Mon Sep 17 00:00:00 2001
From: Claude 
Date: Tue, 16 Jun 2026 20:13:30 +0000
Subject: [PATCH 5/5] docs: record the type-from-origin refactor (ADR-0031) +
 work-log
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

ADR-0031 documents P1–P5 (the maintainer-directed cast elimination), the schema
`required`-invariant declarations that let main.js consume generated types without
read-site casts, the assertion-vs-annotation distinction, and the consumer-ctx
follow-up. Work-log row added.

https://claude.ai/code/session_01EhtBoKXg6bHnKdquacyknf
---
 docs/project_notes/decisions.md | 37 +++++++++++++++++++++++++++++++++
 docs/project_notes/issues.md    |  2 ++
 2 files changed, 39 insertions(+)

diff --git a/docs/project_notes/decisions.md b/docs/project_notes/decisions.md
index a10f317..accb77d 100644
--- a/docs/project_notes/decisions.md
+++ b/docs/project_notes/decisions.md
@@ -827,3 +827,40 @@ consequences. Link evidence (e.g. `specs//evidence/`) where relevant.
   strict (nothing stripped). Out of scope (unchanged): the broader "app imports the generated TS" migration
   (ADR-0012, its own spec) — the app still hand-writes most shapes; only the `SteeringDelta` binding consumes
   generated types today.
+
+## ADR-0031 (2026-06-16) — Type from origin: eliminate `any`/cast smells (maintainer-directed)
+
+- **Context:** the maintainer flagged type-casting as a code smell — "data should be correctly typed from origin."
+  An audit found the loudest casts already absent (zero `@type {unknown}` double-casts, zero `@ts-ignore`/`@ts-nocheck`,
+  zero `as` casts), leaving ~133 `@type` assertions: a large idiomatic bucket (DOM narrowing, `const`/tuple literals
+  — not smells) and a real-smell bucket dominated by `any`. Five fixes (P1–P5).
+- **Decision — type the data at its source, never assert over it:**
+  - **P1 (main.js serialisable state):** typed `requirement`/`handful`/`selected|preview|execPlan`/`steering` with
+    the generated `Requirement`/`Plan`/`Constraint`. Reading them revealed the generated types marked always-present
+    fields optional (LinkML declares few required), which would have forced read-site casts — so the real invariants
+    were declared **`required` at the origin (the schema):** `Requirement.commitments`, `Plan.strategy`, `Plan.scores`,
+    `Scores.satisfaction`, `Materialisation.schedule`/`trajectory`, `TrajectoryPoint.lat`/`lng`/`t`/`fuel_pct`
+    (regenerated). Genuine null-safety via guards / `?? null` / optional-chaining — no casts. `tsconfig` lib → ES2023
+    (the cards use `Array.findLast`).
+  - **P2 (orbat.js):** removed the `(patch).x` / `(next.blue).y` casts (vestigial — `Partial` + the generated
+    `*Params` carry every field); an `inVocab(vocab, string)` helper replaces `VOCAB.includes(/** any */ (x))`
+    (LinkML emits enum slots as `string`); explicit allegiance branch instead of a dynamic-index cast. 18 → 0.
+  - **P3 (globals):** one `app/js/globals.d.ts` declares the debug/test window handles + shared context (DEC-61),
+    dropping ~11 `(window)`/`(globalThis)`/`(window.opener)` casts across six files.
+  - **P4 (timers):** `ReturnType` declaration annotations instead of casting the
+    handle to `any` (the `@types/node` leak in bugs.md persists).
+  - **P5 (views/map.js):** a `RenderOpts` typedef types the data into the view (plans/assets/selected), so the
+    plan/asset iterations infer real types; accessor **parameters** annotated with `Asset`/`Plan`/`HexCell`.
+- **The acceptable residue (documented, not chased):** DOM element narrowing (`getElementById` → `HTMLElement`) is the
+  required idiom, not a smell; deck.gl's typed accessor **props** (`Accessor`) won't accept a plain JSDoc
+  callback, so the `get*` props are cast at that library boundary (like the existing MapOptions cast) — the accessor
+  bodies stay checked; `keyof typeof` index narrowing is idiomatic.
+- **Distinction honoured:** a `/** @type {T} */ (expr)` **assertion** (overriding inference) is the smell removed; a
+  `/** @type {T} */ name` **parameter/field annotation** (declaring a type) is the fix and stays.
+- **Verification:** 0 typecheck errors; 32 unit green; **golden plan ids unchanged** (the schema is validation-only —
+  marking fields required reflects the real shapes, never touches the runtime canonical form, NF3); adherence still
+  validates whole; regen idempotent. App-wide `any`-casts ~133 → ~78.
+- **Out of scope (clean follow-up):** the **consumer ctx** of `compare`/`wingman`/`learn`/`entities` still receives
+  plans via loosely-typed `ctx`, so they cast `(s) => s.label` over `scores.satisfaction` etc. Typing those ctx params
+  with the generated types (now unblocked by the `required` invariants) is the same pattern, one layer out — the
+  remaining slice of ADR-0012's "app imports the generated TS."
diff --git a/docs/project_notes/issues.md b/docs/project_notes/issues.md
index 2e859a9..b5cfd07 100644
--- a/docs/project_notes/issues.md
+++ b/docs/project_notes/issues.md
@@ -64,3 +64,5 @@ evidence (e.g. `specs//evidence/`).
 | 2026-06-14 | `claude/linkml-guardrails` | **LinkML guardrails — ADR-0011/0012 deferred follow-ups (ADR-0029).** Made Principle I (LinkML = source of truth, DEC-57) *enforceable*: (1) **GENERATED banners** on every derived artefact via `schema/generate.sh` (`remit.ts` `//` block, `remit.schema.json` `$comment` first-key, `index.html` HTML comment) + `.gitattributes linguist-generated`; (2) **regen-no-diff CI** (`.github/workflows/schema-regen.yml`) — regenerates from the schema (pinned `linkml`/`linkml-runtime==1.11.1`, Python 3.11, byte-reproducible) and fails on any `schema/gen/`+`site/data-model/` diff; (3) **schema-adherence test** (`test/schema-adherence.test.mjs`, `ajv` dev-only, draft-2019-09) validating a committed `Orbat` + a kernel `Plan` against the generated JSON Schema, wired into a new **`unit.yml`** CI job (also closing the gap that `test:unit` had never run in CI — only e2e + typecheck did). The guard immediately surfaced the full extent of the Waypoint hex/square drift (`Asset.position`/`Stamp.start`/`Materialisation.trajectory`) + appetites map-vs-list + `TideDecision` (bugs.md) — stripped+tracked via its `DRIFT` map; the Waypoint→HexCell migration is the surfaced follow-up. 32 unit (+2) green; 0 typecheck errors. Dev-dep `ajv` (ADR-0014-approved, test-only). | ADR-0029 |
 
 | 2026-06-14 | `claude/waypoint-hexcell` | **Waypoint→HexCell migration — restore schema ≡ code (ADR-0030).** Closed the drift the ADR-0029 adherence guard surfaced: repointed `Asset.position` / `Constraint.cells` / `StartState` / `TrajectoryPoint` onto hex (`HexCell` / `h3`, the ADR-0016 successor to `Waypoint`), added `TideDecision.rv_min`, and modelled `Stamp.appetites` as an `axis→setting` map (LinkML inlined dict, `Appetite.axis` identifier) — matching the runtime *without* changing the kernel, so **golden plan ids are unchanged** (NF3; the schema is validation-only). Deleted the documented interim `unknown`→`Waypoint[]` cast in `main.js` (bugs.md resolved). The adherence test's `DRIFT` map is now **empty** — it validates ORBAT + Plan instances whole. Regenerated (idempotent, regen-no-diff green); 32 unit green; 0 typecheck errors. Stacked on #13. | ADR-0030 |
+
+| 2026-06-16 | `claude/type-from-origin` | **Type from origin — eliminate `any`/cast smells (ADR-0031, maintainer-directed).** Scrutinised the codebase for casts (the loud ones — `unknown` double-casts, `@ts-ignore`, `as` — were already absent) and applied five fixes: **P1** main.js serialisable state typed with generated `Requirement`/`Plan`/`Constraint` (+ declared the real invariants `required` in the schema — `Requirement.commitments`, `Plan.strategy`/`scores`, `Scores.satisfaction`, `Materialisation.schedule`/`trajectory`, `TrajectoryPoint.lat`/`lng`/`t`/`fuel_pct` — so reads need no casts; guards/`?? null` for genuine nullability; lib→ES2023); **P2** orbat.js `tuneAsset`/`validate` (18→0, `inVocab` helper); **P3** `app/js/globals.d.ts` for window/context handles (~11 casts); **P4** timer handles via `ReturnType<…>`; **P5** map.js `RenderOpts` + typed accessor params. Distinction held: type *assertions* removed, *parameter/field annotations* kept. App-wide `any`-casts ~133→~78; the residue is idiomatic DOM-narrowing + the documented deck.gl accessor boundary. Golden ids unchanged (schema validation-only, NF3); 32 unit green; 0 typecheck errors. **Follow-up:** type the consumer `ctx` of compare/wingman/learn/entities (same pattern, one layer out). | ADR-0031 |