Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,5 +278,5 @@ branch; allow workflows to write).
<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan:
`specs/004-orbat-red-green-assets/plan.md`
`specs/005-orbat-asset-enrichment/plan.md`
<!-- SPECKIT END -->
28 changes: 24 additions & 4 deletions app/js/entities/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import { stateAt } from '../kernel/kernel.js';
import { TIDE, fordOpenAt } from '../kernel/world.js';
import { assetToEntity, hasTrack, ALLEGIANCE_COLOR } from '../orbat/orbat.js';

/** A tidal-parameter set (the shape of {@link TIDE}). */
/** @typedef {{ period_min: number, low_tide_min: number, open_half_width_min: number }} Tide */
Expand Down Expand Up @@ -101,11 +102,17 @@ export function satOverhead(t, sat = SAT) {
/**
* Build the entity set projected by the Sync Matrix (display-only, DEC-52/53
* v1). Aspects are typed time-functions; `at(plan, t)` is the single read used
* by both the track renderer and the cursor readout (NF1).
* by both the track renderer and the cursor readout (NF1). Authored ORBAT
* assets (DEC-60) are folded in as allegiance-typed entities keyed by asset id.
* @param {import('../../../schema/gen/remit').Asset[]} [assets]
* @returns {Entities}
*/
export function buildEntities() {
export function buildEntities(assets = []) {
/** @type {Entities} */
const orbatEntities = {};
for (const a of assets) orbatEntities[a.id] = /** @type {any} */ (assetToEntity(a));
return {
...orbatEntities,
self: {
id: 'ent-self', label: 'Own force · ROVER-1',
provenance: { kind: 'self' },
Expand Down Expand Up @@ -146,13 +153,26 @@ export function buildEntities() {
* the World step on. A fuller build would gate rows by role view-preset
* (DEC-48/49/50) — here one default preset shows them all.
*/
export function syncCatalogue() {
return [
/**
* @param {import('../../../schema/gen/remit').Asset[]} [assets]
* @returns {import('../views/sync-matrix.js').CatalogueRow[]}
*/
export function syncCatalogue(assets = []) {
/** @type {import('../views/sync-matrix.js').CatalogueRow[]} */
const rows = [
{ key: 'self.phase', entity: 'self', aspect: 'phase', render: 'status', label: 'Own force · phase', needsPlan: true },
{ key: 'self.fuel', entity: 'self', aspect: 'fuel', render: 'line', label: 'Own force · fuel %', needsPlan: true },
{ key: 'tide.height', entity: 'tide', aspect: 'height', render: 'tide', label: 'Tide · height + window' },
{ key: 'sat.pass', entity: 'sat', aspect: 'pass', render: 'band', label: `Recce sat · ${SAT.name} pass` },
];
// Any authored asset carrying a time-varying aspect (a red patrol window, a blue
// availability window) projects as a Sync-Matrix track via a catalogue row (DEC-60/US5).
for (const a of assets) {
if (!hasTrack(a)) continue;
rows.push({ key: `${a.id}.active`, entity: a.id, aspect: 'active', render: 'band',
label: `${a.allegiance} · ${a.label ?? a.id}`, color: ALLEGIANCE_COLOR[/** @type {keyof typeof ALLEGIANCE_COLOR} */ (a.allegiance)] });
}
return rows;
}

// ---------------------------------------------------------------------------
Expand Down
31 changes: 29 additions & 2 deletions app/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { makeMap } from './views/map.js';
import { makeSyncMatrix } from './views/sync-matrix.js';
import { buildEntities, syncCatalogue, satOverhead, coincidenceRules, coincidenceWindows } from './entities/entities.js';
import { contentId, shortId } from './shapes/canonical.js';
import { getDraft, setDraft, subscribeDraft, reconcileOwnForce } from './orbat/orbat.js';
// The shared app context — one store/seam/world/playhead, shared with every
// other role surface (tab) so they all project the same objects (DEC-61).
import { objects, logs, seam, world, playhead } from './shell/context.js';
Expand Down Expand Up @@ -76,9 +77,28 @@ const map = makeMap(mapEl, world.baseline, world.ao, world.places);
/** @type {((cell: {h3: string, id: number}) => void) | null} */ let mapOnCellClick = null; // active map-click handler (set by Plan in no-go mode)
const syncHost = /** @type {HTMLElement} */ (document.getElementById('sync-matrix-host'));
const syncMatrix = makeSyncMatrix(syncHost, playhead);
const entities = buildEntities();
const catalogue = syncCatalogue();
// Authored ORBAT assets (DEC-60) fold into the entity set + Sync-Matrix catalogue. They are
// rebuilt whenever the live draft changes (panel edits, own-force reconciliation), and the
// projection re-renders — display-only, so no asset ever touches the kernel/plan (NF9).
/** @type {any[]} */ let orbatAssets = (getDraft().assets ?? []);
/** @type {string|null} */ let selectedAsset = null;
let entities = buildEntities(orbatAssets);
let catalogue = syncCatalogue(orbatAssets);
const coincRules = coincidenceRules();

/** Rebuild entities/catalogue from the current ORBAT draft and re-project. */
function refreshOrbat(/** @type {any} */ draft) {
orbatAssets = draft?.assets ?? [];
entities = buildEntities(orbatAssets);
catalogue = syncCatalogue(orbatAssets);
renderProjection();
}
subscribeDraft(refreshOrbat);
// Panel → map/Sync-Matrix selection highlight (US5): a selected row glows in both views.
window.addEventListener('remit:orbat-select', (/** @type {any} */ e) => {
selectedAsset = e.detail?.id ?? null;
renderProjection();
});
const slider = /** @type {HTMLInputElement} */ (document.getElementById('playhead-slider'));
const readout = /** @type {HTMLElement} */ (document.getElementById('projection-readout'));

Expand All @@ -102,6 +122,7 @@ function renderProjection() {
target: mapTarget, rv: mapRv,
candidates: mapCandidates, highlight: mapHighlight,
obstructions: mapObstructions, nogo: mapNogo, blocked: mapBlocked,
assets: orbatAssets, selectedAsset, // authored ORBAT roster (display-only, DEC-60)
follow: state.stage === 'execute', // Execute follow-cam: pan to keep the vehicle in view
});
// The Sync Matrix (D6) is the temporal projection — tide + satellite tracks
Expand Down Expand Up @@ -378,6 +399,12 @@ function mountWorld() {
state.ids.configCore = c.id;
state.configCoreHash = await contentId(world.configCore);
worldProvisioned = true;
// Surface the planned own-force (ROVER-1) as the canonical blue ORBAT asset (idempotent,
// display-only): it keeps driving the plan via the pre-existing machinery (NF9/FR-012).
const ownBase = world.places.base;
setDraft(reconcileOwnForce(getDraft(), {
label: 'Own force · ROVER-1', position: { h3: ownBase.h3, lat: ownBase.lat, lng: ownBase.lng },
}));
// With the AO on the map, show the candidate OPs so Capture can point at
// features it can see; the chosen target/RV are set when Capture commits.
mapCandidates = world.places.ops;
Expand Down
Loading
Loading