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
33 changes: 33 additions & 0 deletions app/js/wingman/wingman.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,25 @@ export function mountWingman(el, ctx) {
`<div class="alert" data-testid="wx-tide-alert">≋ H+${Math.round(exec.simT)} — tide re-assessed: <b>${oldMode} → ${cur.mode}</b> · ${cur.narrative}</div>`;
}

// Capture an in-flight perturbation as a typed, content-addressed store object
// (issue #7), not just a prose log note: structured inputs that survive for the
// Data Analysis monitor and replay (NF3). Re-uses the DEC-61 attributed-delta
// write path (cf. SteeringDelta) — operator wearing the Ops hat. Non-fatal: the
// append-only log entry already records the event, so a failed share never
// breaks the live loop.
async function shareExecutionDelta(/** @type {Partial<import('../../../schema/gen/remit').ExecutionDelta>} */ fields) {
/** @type {import('../../../schema/gen/remit').ExecutionDelta} */
const delta = {
scope: 'execution', by: 'operator', role: 'duty-officer-ops',
at: new Date().toISOString(),
event: 'obstruction',
...fields,
};
await ctx.seam.putObject('ExecutionDelta', delta).catch((/** @type {any} */ err) =>
$('#wx-alerts').innerHTML +=
`<div class="alert"><span class="muted">could not share execution delta (logged locally): ${err?.message ?? err}</span></div>`);
}

async function addObstruction(/** @type {number} */ mins) {
if (exec.complete || !world) return;
const tau = exec.lastTau;
Expand Down Expand Up @@ -274,6 +293,14 @@ export function mountWingman(el, ctx) {
},
source: 'operator', confidence: 'reported',
});
await shareExecutionDelta({
event: 'obstruction',
at_min: Math.round(exec.simT),
cell: { h3: cell.h3, lat: cell.lat, lng: cell.lng },
delay_min: mins,
rv_min: missionEnd,
absorbed_min: absorbed > 0 ? absorbed : 0,
});
await maybeTideAlert(oldMode);
await refreshLog();
await tick(0);
Expand Down Expand Up @@ -318,6 +345,12 @@ export function mountWingman(el, ctx) {
fact_delta: { note: `Cell ${shortH3(next.h3)} blocked — re-routed in flight`, tag: 'track-state' },
source: 'operator', confidence: 'reported',
});
await shareExecutionDelta({
event: 'block',
at_min: Math.round(exec.simT),
cell: { h3: next.h3, lat: next.lat, lng: next.lng },
rv_min: missionEnd,
});
await maybeTideAlert(oldMode);
await refreshLog();
await tick(0);
Expand Down
30 changes: 30 additions & 0 deletions docs/project_notes/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -631,3 +631,33 @@ consequences. Link evidence (e.g. `specs/<feature>/evidence/`) where relevant.
(new typedefs incl. `HexAO`, `Strat`/`Leg`/`RerouteOpts`, `Entity`/`Aspect`). `app/hexviz.js`
(a standalone evidence page) is outside `include`; type-checking the Node-side tooling
(`test`/`e2e`/`run-playwright.mjs`) is a possible follow-up under a second, Node-lib config.

## ADR-0025 (2026-06-12) — Capture execution-phase perturbations as typed `ExecutionDelta` store objects (issue #7)

- **Context:** the Execute phase lets the operator perturb a live run — an obstruction
(`+5`/`+25 min` hold) or a blocked cell forcing a re-route. Both appended a prose
`Observation` to the append-only execution log, but their *structured* inputs (cell,
minutes, resulting RV) lived only in transient wingman UI state. Nothing reached the
content-addressed object store, so — unlike `SteeringDelta` (the first DEC-61 write) — the
perturbations were invisible to the Data Analysis monitor and carried no structured inputs
for replay (NF3). The maintainer raised this as issue #7: *"they aren't captured in the
model. Should they be?"*
- **Decision:** **Yes.** Model the perturbation in LinkML and write it via the existing
DEC-61 attributed-delta path. Added `ExecutionEventKind` (`obstruction`|`block`) + a
`HexCell` value object (`h3` + optional `lat`/`lng`, the hex successor to `Waypoint`) to
`common`, and a concrete `ExecutionDelta (is_a: Delta)` to `records` (`event`, `at_min`,
`cell`, `delay_min`, `rv_min`, `absorbed_min`); regenerated `schema/gen/` + the HTML
reference. The wingman calls `seam.putObject('ExecutionDelta', …)` on each edit, attributed
to the operator wearing the **Ops** hat (`role: 'duty-officer-ops'`).
- **Options considered:** (a) pack the fields into the prose log entry — still invisible to
the store-backed monitor/replay; rejected. (b) a hand-written wingman UI-state object —
violates the LinkML one-source-of-truth rule (ADR-0012) the moment it crosses into the
shared store; rejected. (c) a generated typed delta mirroring `SteeringDelta` — **chosen**.
- **Consequences:** in-flight edits now appear in Data Analysis (with change-glow) and are
structurally preserved for replay. The prose log entry is **kept** (it reads well in the
after-action record) — the delta is its structured sibling, not a replacement; the share is
**non-fatal** (a failed write never breaks the live loop, since the log already recorded
the event). **Not done:** re-running a stamp does not yet *re-apply* the captured
perturbations (they're exogenous operator acts) — capturing the inputs is the prerequisite,
automatic re-application is the follow-up. `HexCell` is now the natural type to migrate the
documented square-vs-hex `Constraint.cells` drift in `SteeringDelta` onto (bugs.md).
2 changes: 2 additions & 0 deletions docs/project_notes/issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,6 @@ evidence (e.g. `specs/<feature>/evidence/`).

| 2026-06-12 | PR #5 | Just-in-time departure (maintainer suggestion). A COA whose exfil would hold *at the exposed bank* for the tide now **leaves base late instead**, reaching the wath exactly at low water — same RV, no exposed wait. The tide-waiting (blue/`tracked`) COA departs ~51 min after the drive-the-road (`direct`) COA, so you watch **two vehicles move at different times** rather than one overlapping marker (`kernel.js materialise()`). Schedule becomes `[hold(delay), transit, visit, exfil]`; plan ids unchanged (they key off the decision inputs, not the materialisation — NF3), so only the golden's schedule/wait assertions moved. 12 unit + 12 e2e green. | ADR-0023 · [#5](https://github.com/DeepBlueCLtd/REMIT/pull/5) |

| 2026-06-12 | issue [#7](https://github.com/DeepBlueCLtd/REMIT/issues/7) | **Capture execution-phase edits in the model (ADR-0025).** In-flight obstructions/blocks during Execute appended a prose `Observation` to the log but never reached the content-addressed store, so their structured inputs were invisible to Data Analysis and absent for replay (NF3). Modelled them in LinkML — `ExecutionEventKind` + a `HexCell` value object (hex successor to `Waypoint`) in `common`, a concrete `ExecutionDelta(is_a Delta)` in `records` (`event`/`at_min`/`cell`/`delay_min`/`rv_min`/`absorbed_min`); regenerated `schema/gen/` + HTML ref (62→64 classes, 20→21 enums). The wingman now `seam.putObject('ExecutionDelta', …)` on each edit (operator wearing the Ops hat), mirroring `SteeringDelta`; prose log kept, share non-fatal. New e2e capture test; 0 typecheck errors, 12 unit green. Replay re-application deferred (follow-up). | ADR-0025 · `specs/004-capture-execution-deltas/` |

| 2026-06-12 | [#8](https://github.com/DeepBlueCLtd/REMIT/pull/8) | **Enforced the JSDoc type-checking the app already declared (ADR-0024).** Every `app/js` file carried `// @ts-check` + JSDoc but nothing ran it; added root `tsconfig.json` (checkJs+strict, noEmit, types:[], DOM/bundler), pinned `typescript`, added `npm run typecheck`, and a `typecheck.yml` CI gate (PR + push-to-main). Drove the app **442 → 0** type errors across 18 files — precise types for DOM/primitives/shared shapes (new typedefs `HexAO`, `Strat`/`Leg`/`RerouteOpts`, `Cell`/`Profile`/`TrajPoint`, `Entity`/`Aspect`…), `any` only for opaque serialisable blobs (plan/requirement/log/viz, per ADR-0012); **zero `@ts-ignore`/`@ts-nocheck`**. Fixed real drift it surfaced: stale pre-hex `wingman.js` annotations (`grid`→`ao`, `{x,y}`→`{h3}` callbacks), two `stateAt` null-guards, an `Entity.domain` tuple, a `@types/node` `setTimeout`→`Timeout` leak, and a documented **SteeringDelta hex-vs-square schema drift** (both in `bugs.md`). No source migrated to `.ts`, no runtime build step added; 12 unit green, e2e in CI. | ADR-0024 · [#8](https://github.com/DeepBlueCLtd/REMIT/pull/8) |
33 changes: 33 additions & 0 deletions e2e/skeleton.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,39 @@ test('the hex grid can be toggled off and on (basemap shows through)', async ({
await expect(map).toHaveAttribute('data-hexes', 'on'); // and back
});

test('execute: an obstruction is captured in the model as a typed ExecutionDelta (issue #7)', async ({ page }) => {
await page.goto('/');
await walkToPlan(page);
await page.getByTestId('continue-compare').click();
await page.getByTestId('pick-direct').check();
await page.getByTestId('cmp-commit').click();
await page.getByTestId('continue-execute').click();

// Step past the visit into the exfil drive (where an obstruction applies — the
// vehicle is on the move), then insert a +25 min obstruction.
for (let i = 0; i < 7; i++) await page.getByTestId('wx-step10').click();
await page.getByTestId('wx-delay').click();

// The perturbation lands in the shared content store as a typed ExecutionDelta —
// structured inputs (event/cell/delay), not just a prose log note (issue #7).
await expect.poll(() => page.evaluate(() => {
const w = (window as any).__remit;
const rec = w.objects.list().reverse().find((o: any) => o.type === 'ExecutionDelta');
if (!rec) return false;
const d = w.objects.get(rec.id).body;
return d.event === 'obstruction' && d.delay_min === 25 && typeof d.cell?.h3 === 'string';
})).toBe(true);

// It surfaces in the Data Analysis monitor like any other store object.
await page.getByTestId('tab-data-analysis').click();
await expect(page.getByTestId('da-index')).toContainText('ExecutionDelta');
await page.locator('.da-group[data-group="ExecutionDelta"] .da-row').first().click();
await page.screenshot({
path: 'specs/004-capture-execution-deltas/evidence/screenshots/01-execution-delta.png',
fullPage: true });
await noFault(page);
});

test('execute: blocking the next hex re-routes in flight', async ({ page }) => {
await page.goto('/');
await walkToPlan(page);
Expand Down
19 changes: 19 additions & 0 deletions schema/common.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ enums:
Observation: {}
Waiver: {}
Replan: {}
ExecutionEventKind:
description: The kind of in-flight operator perturbation applied during Execute (issue #7).
permissible_values:
obstruction:
description: a +N min hold spliced at the vehicle's current cell, re-timed through the tide-aware chooser (ADR-0006/0007)
block:
description: a cell ahead declared impassable, forcing an in-flight re-route around it
classes:
Attribution:
description: Who decided, under what authority, when. Stamped on every committing act — this is universal rule 2 (DEC-15, NF2).
Expand Down Expand Up @@ -202,3 +209,15 @@ classes:
required: true
alias:
description: a human-friendly name, e.g. "OP-21,3"
HexCell:
description: A single H3 hex location — the hex-grid successor to Waypoint (ADR-0014). Identified by its H3 index, with an optional lat/lng centre for rendering.
attributes:
h3:
required: true
description: the H3 cell index (res 9)
lat:
range: float
description: cell-centre latitude
lng:
range: float
description: cell-centre longitude
121 changes: 121 additions & 0 deletions schema/gen/remit.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1290,6 +1290,98 @@
"title": "Excursion",
"type": "object"
},
"ExecutionDelta": {
"additionalProperties": false,
"description": "An in-flight operator perturbation during Execute (issue #7): an obstruction (a +N min hold at the vehicle's current cell) or a blocked cell forcing a re-route. Previously these survived only as a prose Observation note; capturing them as a typed, content-addressed store object preserves their structured inputs for inspection (the Data Analysis monitor) and replay (NF3), and re-uses the DEC-61 attributed-delta write path. Identity is content (DEC-35).",
"properties": {
"absorbed_min": {
"description": "minutes the downstream holds absorbed, so the RV slipped less than delay_min (ADR-0007)",
"type": [
"number",
"null"
]
},
"at": {
"description": "when it was contributed (DEC-15, NF2)",
"format": "date-time",
"type": [
"string",
"null"
]
},
"at_min": {
"description": "mission minutes when applied (sim-time \u2261 plan-time, ADR-0007)",
"type": [
"integer",
"null"
]
},
"by": {
"description": "the contributing author",
"type": [
"string",
"null"
]
},
"cell": {
"anyOf": [
{
"$ref": "#/$defs/HexCell"
},
{
"type": "null"
}
],
"description": "where the perturbation bites \u2014 the vehicle's cell for an obstruction, the blocked cell for a block"
},
"delay_min": {
"description": "the hold added by an obstruction (absent for a block)",
"type": [
"number",
"null"
]
},
"event": {
"$ref": "#/$defs/ExecutionEventKind",
"description": "which perturbation \u2014 obstruction or block"
},
"role": {
"description": "\"the contributing role-hat \u2014 e.g. duty-officer-plans\"",
"type": [
"string",
"null"
]
},
"rv_min": {
"description": "the re-planned RV / mission-end after the perturbation",
"type": [
"number",
"null"
]
},
"scope": {
"description": "\"the write-scope this delta falls under \u2014 e.g. steering\"",
"type": [
"string",
"null"
]
}
},
"required": [
"event"
],
"title": "ExecutionDelta",
"type": "object"
},
"ExecutionEventKind": {
"description": "The kind of in-flight operator perturbation applied during Execute (issue",
"enum": [
"obstruction",
"block"
],
"title": "ExecutionEventKind",
"type": "string"
},
"ExecutionLog": {
"additionalProperties": false,
"description": "The append-only live history of a mission (DEC-23/25/26): alerts when a band is crossed, observations that update truth, waivers, and replans \u2014 never overwritten. Together with rationales and stamps it gives a complete after-action record with perfect replay (F1).",
Expand Down Expand Up @@ -1370,6 +1462,35 @@
"title": "Grid2D",
"type": "object"
},
"HexCell": {
"additionalProperties": false,
"description": "A single H3 hex location \u2014 the hex-grid successor to Waypoint (ADR-0014). Identified by its H3 index, with an optional lat/lng centre for rendering.",
"properties": {
"h3": {
"description": "the H3 cell index (res 9)",
"type": "string"
},
"lat": {
"description": "cell-centre latitude",
"type": [
"number",
"null"
]
},
"lng": {
"description": "cell-centre longitude",
"type": [
"number",
"null"
]
}
},
"required": [
"h3"
],
"title": "HexCell",
"type": "object"
},
"Lineage": {
"additionalProperties": false,
"description": "Hash-link back to the version this one supersedes (universal rule 1).",
Expand Down
42 changes: 42 additions & 0 deletions schema/gen/remit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,16 @@ export enum LogEntryKind {
Waiver = "Waiver",
Replan = "Replan",
};
/**
* The kind of in-flight operator perturbation applied during Execute (issue
*/
export enum ExecutionEventKind {

/** a +N min hold spliced at the vehicle's current cell, re-timed through the tide-aware chooser (ADR-0006/0007) */
obstruction = "obstruction",
/** a cell ahead declared impassable, forcing an in-flight re-route around it */
block = "block",
};


/**
Expand Down Expand Up @@ -264,6 +274,19 @@ export interface Waypoint {
}


/**
* A single H3 hex location — the hex-grid successor to Waypoint (ADR-0014). Identified by its H3 index, with an optional lat/lng centre for rendering.
*/
export interface HexCell {
/** the H3 cell index (res 9) */
h3: string,
/** cell-centre latitude */
lat?: number,
/** cell-centre longitude */
lng?: number,
}


/**
* A commitment's ownership and waiver authority (B3).
*/
Expand Down Expand Up @@ -1011,4 +1034,23 @@ export interface SteeringDelta extends Delta {
}


/**
* An in-flight operator perturbation during Execute (issue #7): an obstruction (a +N min hold at the vehicle's current cell) or a blocked cell forcing a re-route. Previously these survived only as a prose Observation note; capturing them as a typed, content-addressed store object preserves their structured inputs for inspection (the Data Analysis monitor) and replay (NF3), and re-uses the DEC-61 attributed-delta write path. Identity is content (DEC-35).
*/
export interface ExecutionDelta extends Delta {
/** which perturbation — obstruction or block */
event: string,
/** mission minutes when applied (sim-time ≡ plan-time, ADR-0007) */
at_min?: number,
/** where the perturbation bites — the vehicle's cell for an obstruction, the blocked cell for a block */
cell?: HexCell,
/** the hold added by an obstruction (absent for a block) */
delay_min?: number,
/** the re-planned RV / mission-end after the perturbation */
rv_min?: number,
/** minutes the downstream holds absorbed, so the RV slipped less than delay_min (ADR-0007) */
absorbed_min?: number,
}



Loading
Loading