From b07419b3d1ed42bc05152f702af9f548a8c84946 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 11:09:46 +0000 Subject: [PATCH] =?UTF-8?q?feat(schema):=20Waypoint->HexCell=20migration?= =?UTF-8?q?=20=E2=80=94=20restore=20schema=20=E2=89=A1=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the drift the ADR-0029 adherence guard surfaced. The schema still modelled several persisted shapes as square-grid Waypoint{x,y} though the app went hex (H3) at ADR-0016. Repoint to the existing HexCell and fix two non-hex drifts, so the schema describes the real runtime shapes (DEC-57): - Asset.position, Constraint.cells -> HexCell; StartState, TrajectoryPoint rebuilt on h3 (+lat/lng) instead of x/y - TideDecision gains rv_min (the chosen route's RV arrival) - Stamp.appetites modelled as an axis->setting map (Appetite.axis identifier + inlined dict), matching the runtime map WITHOUT changing the kernel — so the golden plan ids are unchanged (NF3; the schema is validation-only) Payoff: deletes the documented interim unknown->Waypoint[] cast in main.js (bugs.md resolved), and the adherence test drops its DRIFT/strip machinery — it now validates ORBAT + Plan instances whole. Regenerated (idempotent, regen-no-diff green); 32 unit green; golden ids unchanged; 0 typecheck errors. ADR-0030. Stacked on #13. https://claude.ai/code/session_01EhtBoKXg6bHnKdquacyknf --- app/js/main.js | 9 ++-- docs/project_notes/bugs.md | 7 +++ docs/project_notes/decisions.md | 29 ++++++++++ docs/project_notes/issues.md | 2 + docs/project_notes/key_facts.md | 2 +- docs/remit-data-model.md | 4 +- schema/gen/remit.schema.json | 93 ++++++++++++++++++++++++++------- schema/gen/remit.ts | 38 +++++++++----- schema/orbat.yaml | 4 +- schema/plan.yaml | 39 +++++++++----- site/data-model/index.html | 8 +-- test/schema-adherence.test.mjs | 68 ++++++++---------------- 12 files changed, 199 insertions(+), 104 deletions(-) diff --git a/app/js/main.js b/app/js/main.js index 2cf40aa..c61d73b 100644 --- a/app/js/main.js +++ b/app/js/main.js @@ -461,12 +461,9 @@ function shareSteering() { /** @type {import('../../schema/gen/remit').SteeringDelta} */ const delta = { scope: 'steering', - // Schema drift (DEC-57): the app moved to hex H3 ids, but the generated - // Constraint.cells is still square-grid Waypoint{x,y}. Cast until the LinkML - // schema grows a hex cell type and is regenerated. - constraints: cells.length - ? [{ type: 'no-go', cells: /** @type {import('../../schema/gen/remit').Waypoint[]} */ (/** @type {unknown} */ (cells)) }] - : [], + // The generated Constraint.cells is HexCell[] (h3) since the Waypoint→HexCell + // migration (ADR-0030), so the app's hex no-go cells fit directly — no cast. + constraints: cells.length ? [{ type: 'no-go', cells }] : [], by: 'operator', role: 'duty-officer-plans', at: new Date().toISOString(), diff --git a/docs/project_notes/bugs.md b/docs/project_notes/bugs.md index a1cbc16..c053992 100644 --- a/docs/project_notes/bugs.md +++ b/docs/project_notes/bugs.md @@ -249,6 +249,8 @@ Each entry records: date, symptom, root cause, fix, and how to prevent recurrenc - **Prevention / real fix:** update the LinkML source (a hex cell type, or `Waypoint.h3`) and re-run `schema/generate.sh`, then drop the cast. Enforced type-checking (ADR-0024) now catches this class of schema/code drift at build time instead of silently. +- **Resolved (2026-06-14, ADR-0030):** done — `Constraint.cells` repointed to `HexCell`, regenerated, and the + `unknown`→`Waypoint[]` cast deleted from `main.js` (typecheck stays green without it). ## Schema-adherence guard surfaces the full extent of the schema↔code drift (2026-06-14) @@ -268,5 +270,10 @@ Each entry records: date, symptom, root cause, fix, and how to prevent recurrenc `Constraint.cells`, `StartState`, `TrajectoryPoint`), reconcile `appetites`→`Appetite[]` and `TideDecision`, re-run `schema/generate.sh`, then empty the test's `DRIFT` map. The regen-no-diff + adherence checks (ADR-0029) now make this class of drift impossible to reintroduce silently. +- **Resolved (2026-06-14, ADR-0030):** done — `Asset.position`/`Constraint.cells`/`StartState`/`TrajectoryPoint` + repointed to hex (`HexCell` / `h3`), `TideDecision` gained `rv_min`, and `appetites` is now modelled as an + `axis→setting` map (LinkML inlined dict, `Appetite.axis` identifier) — so it matches the runtime *without* + changing the kernel (golden plan ids unchanged, NF3). The adherence test's `DRIFT` map is now **empty**: it + validates the instances whole. diff --git a/docs/project_notes/decisions.md b/docs/project_notes/decisions.md index affa94e..a10f317 100644 --- a/docs/project_notes/decisions.md +++ b/docs/project_notes/decisions.md @@ -798,3 +798,32 @@ consequences. Link evidence (e.g. `specs//evidence/`) where relevant. - **Consequences:** Principle I is now enforced, not just stated — generated files are labelled, drift fails CI two ways (regen + adherence), and the schema's real gaps are visible and tracked. Scope: tooling/CI/test only; the sole schema-output change is the banner text. No `app/`/kernel code changed. + +## ADR-0030 (2026-06-14) — Waypoint→HexCell migration: restore schema ≡ code, empty the adherence DRIFT map + +- **Context:** the ADR-0029 adherence guard surfaced that the LinkML schema still modelled several persisted + shapes as the **square-grid `Waypoint{x,y}`** even though the app went hex (H3) at ADR-0016 — `Asset.position`, + `Constraint.cells`, `StartState`, `Materialisation.trajectory` — plus two non-hex drifts: `appetites` is a + runtime `{axis:setting}` map vs the schema's `Appetite[]`, and `TideDecision` carried a runtime `rv_min` the + schema lacked. The guard had to *strip* these (its `DRIFT` map). This closes them so the schema describes reality. +- **Decision:** migrate the schema source (the modules, then regenerate) to match the real shapes — the DEC-57 + direction is *schema follows the skeleton's real code* (the runtime is authoritative for v1): + - **Hex coordinates →** repoint `Asset.position` (orbat) and `Constraint.cells` (plan) to the existing + **`HexCell`** (`{h3, lat?, lng?}`, the ADR-0016 successor to `Waypoint`); rebuild `StartState` and + `TrajectoryPoint` on `h3`(+`lat`/`lng`) instead of `x`/`y`. + - **`TideDecision`** gains `rv_min` (the chosen route's RV arrival the runtime publishes). + - **`appetites`** is modelled as an **`axis→setting` map** — `Appetite.axis` made the LinkML `identifier` and + `Stamp.appetites` `inlined` (not `inlined_as_list`), so gen-json-schema emits the compact dict form that + validates `{tempo:'balanced', exposure:'balanced'}` directly. Chosen over changing the kernel to emit + `Appetite[]`, which would have altered the Stamp's canonical bytes → **moved every golden plan id** (NF3). +- **Payoff:** the documented interim **cast is deleted** — `main.js`'s `SteeringDelta` write no longer casts hex + cells `as unknown as Waypoint[]` (bugs.md); they're `HexCell`s now. The adherence test drops its `DRIFT`/strip + machinery and **validates the instances whole**; an undeclared-field assertion still proves it catches new drift. +- **Verification:** regenerate is idempotent + byte-reproducible (the ADR-0029 regen-no-diff check passes); 32 unit + tests green; **golden plan ids unchanged** (the schema is validation-only — it never touches the runtime canonical + form, so NF3 holds); 0 typecheck errors. `Waypoint` itself is retained in the schema (still the generic grid + location type) but is no longer referenced by these persisted shapes. +- **Consequences:** Principle I's "schema ≡ code" is restored for the serialisable core; the adherence guard is now + 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. diff --git a/docs/project_notes/issues.md b/docs/project_notes/issues.md index 73978df..2e859a9 100644 --- a/docs/project_notes/issues.md +++ b/docs/project_notes/issues.md @@ -62,3 +62,5 @@ evidence (e.g. `specs//evidence/`). | 2026-06-14 | issue [#3](https://github.com/DeepBlueCLtd/REMIT/issues/3) | **Walking-skeleton gate reconciliation (DEC-47 → register DEC-62).** Closed the three held skeleton deviations at the skeleton-complete gate: (A) the stamp gains `profile_version`+`start` identity axes (refines DEC-29/35); (B) `Plan.id = hash(Stamp ⊕ strategy)` within-handful discriminator (clarifies DEC-29); (C) the no-build `// @ts-check`+JSDoc approach ratified as DEC-41's TypeScript realisation (ADR-0024 typecheck + DEC-57 generated TS), a caveat not a reversal. All three were already baked into the LinkML schema (DEC-57); here recorded in the Doc-owned register (DEC-62, v28) + prose spine §6/§7 + skeleton spec gate note. Remaining notes (tool-order, band-calibration, module placement) held as-is. **Docs/governance only — no schema or code change.** | ADR-0028 · DEC-62 · [#3](https://github.com/DeepBlueCLtd/REMIT/issues/3) | | 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 | diff --git a/docs/project_notes/key_facts.md b/docs/project_notes/key_facts.md index 921a8c9..b503e7f 100644 --- a/docs/project_notes/key_facts.md +++ b/docs/project_notes/key_facts.md @@ -41,7 +41,7 @@ need a value. | ORBAT allegiance palette (004) | blue (own force) `#4493f8` · red (hostile) `#ff7b72` · green (neutral) `#38d39f` (`ALLEGIANCE_COLOR` in `orbat.js`; mirrored in `map.js` markers + Sync-Matrix tracks). | | ORBAT bounds / persistence (004) | `extent_m` 100..20000 m · `severity`/`sensitivity` 1..5 · `protection` ∈ {`keep_out`,`minimise_effect`}. Working draft mirrors to `localStorage['remit.orbat.M-001']` (canonical JSON, survives reload); commit mints an immutable content-addressed `Orbat` in the `ObjectStore` with lineage. | | ORBAT enrichment (005) | Display-only, additive (ADR-0027): `Asset.kind` (`PlatformKind`: infantry/vehicle/aircraft/vessel/sensor/emplacement/structure) → map **symbol** (`SYMBOLS` glyph lookup in `orbat.js`, deck.gl `TextLayer`, no icon atlas) + per-asset `symbol` override; `Asset.confidence` (`ConfidenceLevel`) → marker **opacity** `{high:1, medium:0.6, low:0.35}` (absent ⇒ 1); `Asset.strength`/`notes` (free text); red `RedParams.detection_range_m`/`engagement_range_m` (dual rings, `engagement ≤ detection`) + `threat_type`; green `GreenParams.category` (`GreenCategory`: hospital/school/utility/place_of_worship/residential/other); blue `BlueParams.role`. `normalize()` (in `loadDraft`) migrates spec-004 red drafts `extent_m`→`detection_range_m`. Vocab fields ignore invalid values; free-text trims + drops-empty. | -| Schema guardrails (ADR-0029) | Generated artefacts are enforced, not just labelled: `schema/generate.sh` stamps `@generated` banners on `schema/gen/*` + `site/data-model/index.html` (+ `.gitattributes linguist-generated`) and pins `linkml`/`linkml-runtime==1.11.1`; **regen-no-diff CI** (`.github/workflows/schema-regen.yml`, Python 3.11) fails on any `schema/gen/`+`site/data-model/` drift; **schema-adherence test** (`test/schema-adherence.test.mjs`, `ajv` dev-only, draft-2019-09) validates a committed `Orbat` + a kernel `Plan` against `remit.schema.json`. Known drifts stripped via the test's `DRIFT` map (Waypoint→HexCell; appetites map/list; `TideDecision` — bugs.md). The whole `test:unit` suite now runs in CI via **`.github/workflows/unit.yml`** (previously only e2e + typecheck ran). | +| Schema guardrails (ADR-0029) | Generated artefacts are enforced, not just labelled: `schema/generate.sh` stamps `@generated` banners on `schema/gen/*` + `site/data-model/index.html` (+ `.gitattributes linguist-generated`) and pins `linkml`/`linkml-runtime==1.11.1`; **regen-no-diff CI** (`.github/workflows/schema-regen.yml`, Python 3.11) fails on any `schema/gen/`+`site/data-model/` drift; **schema-adherence test** (`test/schema-adherence.test.mjs`, `ajv` dev-only, draft-2019-09) validates a committed `Orbat` + a kernel `Plan` against `remit.schema.json`. Validates ORBAT + Plan instances **whole** — no stripping (the Waypoint→HexCell migration, ADR-0030, closed the earlier drifts: `HexCell` positions/cells/start/trajectory, `TideDecision.rv_min`, `appetites` as an `axis→setting` map). The whole `test:unit` suite runs in CI via **`.github/workflows/unit.yml`** (previously only e2e + typecheck ran). | _Pages URLs resolve once GitHub Pages is enabled (served from `gh-pages`). Add anything else worth remembering (service URLs, IDs, constants) as it comes up._ diff --git a/docs/remit-data-model.md b/docs/remit-data-model.md index 9203eeb..94999d8 100644 --- a/docs/remit-data-model.md +++ b/docs/remit-data-model.md @@ -174,8 +174,8 @@ Stamp { baseline_version, excursions: [excursion_version] config_core_hash // DEC-48: world-defining config core (medium/channels/ // movement-model/providers/vocabulary); instance shell excluded - profile_version, start: { x, y, clock_min } // DEC-62: own-force profile (DEC-19) + start state — the plan - // depends on both, so both are identity inputs (NF3) + profile_version, start: { h3, clock_min } // DEC-62: own-force profile (DEC-19) + start state (H3 hex, + // ADR-0030) — the plan depends on both, so both are identity inputs (NF3) appetites: { axis → setting } // implementer's, DEC-6 steering: [Constraint] // interpreted gestures, DEC-24 kernel_version, strategy_seed // DEC-29: part of identity diff --git a/schema/gen/remit.schema.json b/schema/gen/remit.schema.json index 393b702..f526037 100644 --- a/schema/gen/remit.schema.json +++ b/schema/gen/remit.schema.json @@ -353,14 +353,33 @@ }, "Appetite": { "additionalProperties": false, - "description": "One risk-appetite dial setting (DEC-6) \u2014 e.g. tempo = rapid, exposure = cautious.", + "description": "One risk-appetite dial setting (DEC-6) \u2014 e.g. tempo = rapid, exposure = cautious. Keyed by axis, so a Stamp's appetites serialise as an axis\u2192setting map.", "properties": { "axis": { "description": "\"e.g. tempo, exposure\"", + "type": "string" + }, + "setting": { + "description": "\"e.g. deliberate/balanced/rapid, bold/balanced/cautious\"", "type": [ "string", "null" ] + } + }, + "required": [ + "axis" + ], + "title": "Appetite", + "type": "object" + }, + "Appetite__identifier_optional": { + "additionalProperties": false, + "description": "One risk-appetite dial setting (DEC-6) \u2014 e.g. tempo = rapid, exposure = cautious. Keyed by axis, so a Stamp's appetites serialise as an axis\u2192setting map.", + "properties": { + "axis": { + "description": "\"e.g. tempo, exposure\"", + "type": "string" }, "setting": { "description": "\"e.g. deliberate/balanced/rapid, bold/balanced/cautious\"", @@ -370,6 +389,7 @@ ] } }, + "required": [], "title": "Appetite", "type": "object" }, @@ -499,13 +519,13 @@ "position": { "anyOf": [ { - "$ref": "#/$defs/Waypoint" + "$ref": "#/$defs/HexCell" }, { "type": "null" } ], - "description": "AO location (H3 cell / lat-lon)" + "description": "AO location (H3 hex cell + lat-lon centre, ADR-0016)" }, "red": { "anyOf": [ @@ -1188,7 +1208,7 @@ "properties": { "cells": { "items": { - "$ref": "#/$defs/Waypoint" + "$ref": "#/$defs/HexCell" }, "type": [ "array", @@ -2551,12 +2571,23 @@ "description": "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.", "properties": { "appetites": { - "description": "the implementer's risk dials (DEC-6)", - "items": { - "$ref": "#/$defs/Appetite" + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/Appetite__identifier_optional" + }, + { + "description": "\"e.g. deliberate/balanced/rapid, bold/balanced/cautious\"", + "type": "string" + }, + { + "type": "null" + } + ] }, + "description": "the implementer's risk dials (DEC-6), as an axis\u2192setting map", "type": [ - "array", + "object", "null" ] }, @@ -2639,27 +2670,37 @@ }, "StartState": { "additionalProperties": false, - "description": "The starting position and clock baked into a Stamp (skeleton, DEC-47).", + "description": "The starting position (H3 hex) and clock baked into a Stamp (skeleton, DEC-47; hex per ADR-0016).", "properties": { "clock_min": { + "description": "mission clock at start (minutes)", "type": [ "integer", "null" ] }, - "x": { + "h3": { + "description": "the H3 cell index (res 9) of the start cell", + "type": "string" + }, + "lat": { + "description": "cell-centre latitude (optional, for rendering)", "type": [ - "integer", + "number", "null" ] }, - "y": { + "lng": { + "description": "cell-centre longitude (optional, for rendering)", "type": [ - "integer", + "number", "null" ] } }, + "required": [ + "h3" + ], "title": "StartState", "type": "object" }, @@ -2851,6 +2892,13 @@ "null" ] }, + "rv_min": { + "description": "mission minutes the chosen route reaches the RV (the committed arrival)", + "type": [ + "number", + "null" + ] + }, "wait_min": { "description": "minutes held at the bank before crossing", "type": [ @@ -2938,7 +2986,7 @@ }, "TrajectoryPoint": { "additionalProperties": false, - "description": "One sampled point on the platform's path; coordinates may be fractional, time/fuel rounded to 1 dp for IEEE stability.", + "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": [ @@ -2946,26 +2994,35 @@ "null" ] }, - "t": { - "description": "mission minutes", + "h3": { + "description": "the H3 cell index (res 9)", + "type": "string" + }, + "lat": { + "description": "cell-centre latitude", "type": [ "number", "null" ] }, - "x": { + "lng": { + "description": "cell-centre longitude", "type": [ "number", "null" ] }, - "y": { + "t": { + "description": "mission minutes", "type": [ "number", "null" ] } }, + "required": [ + "h3" + ], "title": "TrajectoryPoint", "type": "object" }, diff --git a/schema/gen/remit.ts b/schema/gen/remit.ts index 2bda767..8a346ef 100644 --- a/schema/gen/remit.ts +++ b/schema/gen/remit.ts @@ -13,6 +13,7 @@ export type EntityId = string; export type AspectName = string; export type OrbatId = string; export type AssetId = string; +export type AppetiteAxis = string; export type PlanId = string; export type ConflictId = string; export type SelectionRationaleId = string; @@ -796,8 +797,8 @@ export interface Asset { allegiance: string, /** human label; need not be unique */ label?: string, - /** AO location (H3 cell / lat-lon) */ - position?: Waypoint, + /** AO location (H3 hex cell + lat-lon centre, ADR-0016) */ + position?: HexCell, /** reach / footprint radius in metres */ extent_m?: number, /** present iff allegiance = blue */ @@ -882,8 +883,8 @@ export interface Stamp { profile_version?: ProfileId, /** the starting state (skeleton addition, DEC-47) */ start?: StartState, - /** the implementer's risk dials (DEC-6) */ - appetites?: Appetite[], + /** the implementer's risk dials (DEC-6), as an axis→setting map */ + appetites?: {[index: AppetiteAxis]: Appetite }, /** interpreted operator gestures / no-go constraints (DEC-24) */ steering?: Constraint[], /** e.g. "mock-0.1" — part of identity (DEC-29) */ @@ -894,21 +895,26 @@ export interface Stamp { /** - * The starting position and clock baked into a Stamp (skeleton, DEC-47). + * The starting position (H3 hex) and clock baked into a Stamp (skeleton, DEC-47; hex per ADR-0016). */ export interface StartState { - x?: number, - y?: number, + /** the H3 cell index (res 9) of the start cell */ + h3: string, + /** cell-centre latitude (optional, for rendering) */ + lat?: number, + /** cell-centre longitude (optional, for rendering) */ + lng?: number, + /** mission clock at start (minutes) */ clock_min?: number, } /** - * One risk-appetite dial setting (DEC-6) — e.g. tempo = rapid, exposure = cautious. + * 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. */ export interface Appetite { /** "e.g. tempo, exposure" */ - axis?: string, + axis: string, /** "e.g. deliberate/balanced/rapid, bold/balanced/cautious" */ setting?: string, } @@ -920,7 +926,7 @@ export interface Appetite { export interface Constraint { /** "e.g. no-go" */ type?: string, - cells?: Waypoint[], + cells?: HexCell[], } @@ -985,11 +991,15 @@ export interface ScheduleLeg { /** - * One sampled point on the platform's path; coordinates may be fractional, time/fuel rounded to 1 dp for IEEE stability. + * One sampled point on the platform's path (H3 hex per ADR-0016); time/fuel rounded to 1 dp for IEEE stability. */ export interface TrajectoryPoint { - x?: number, - y?: number, + /** the H3 cell index (res 9) */ + h3: string, + /** cell-centre latitude */ + lat?: number, + /** cell-centre longitude */ + lng?: number, /** mission minutes */ t?: number, fuel_pct?: number, @@ -1038,6 +1048,8 @@ export interface TideDecision { ford_rv?: number, /** mission minutes the detour route reaches the RV; null when no detour exists */ detour_rv?: number, + /** mission minutes the chosen route reaches the RV (the committed arrival) */ + rv_min?: number, /** the choice, in words */ narrative?: string, } diff --git a/schema/orbat.yaml b/schema/orbat.yaml index 0373243..ff4dfcd 100644 --- a/schema/orbat.yaml +++ b/schema/orbat.yaml @@ -48,9 +48,9 @@ classes: label: description: human label; need not be unique position: - range: Waypoint + range: HexCell inlined: true - description: AO location (H3 cell / lat-lon) + description: AO location (H3 hex cell + lat-lon centre, ADR-0016) extent_m: range: float description: reach / footprint radius in metres diff --git a/schema/plan.yaml b/schema/plan.yaml index 2dd9af3..69aa6ca 100644 --- a/schema/plan.yaml +++ b/schema/plan.yaml @@ -43,8 +43,8 @@ classes: appetites: range: Appetite multivalued: true - inlined_as_list: true - description: the implementer's risk dials (DEC-6) + inlined: true + description: the implementer's risk dials (DEC-6), as an axis→setting map steering: range: Constraint multivalued: true @@ -56,18 +56,25 @@ classes: range: integer description: RNG seed for strategy ordering — part of identity (DEC-29) StartState: - description: The starting position and clock baked into a Stamp (skeleton, DEC-47). + description: The starting position (H3 hex) and clock baked into a Stamp (skeleton, DEC-47; hex per ADR-0016). attributes: - x: - range: integer - y: - range: integer + h3: + required: true + description: the H3 cell index (res 9) of the start cell + lat: + range: float + description: cell-centre latitude (optional, for rendering) + lng: + range: float + description: cell-centre longitude (optional, for rendering) clock_min: range: integer + description: mission clock at start (minutes) Appetite: - description: One risk-appetite dial setting (DEC-6) — e.g. tempo = rapid, exposure = cautious. + description: 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. attributes: axis: + identifier: true description: '"e.g. tempo, exposure"' setting: description: '"e.g. deliberate/balanced/rapid, bold/balanced/cautious"' @@ -77,7 +84,7 @@ classes: type: description: '"e.g. no-go"' cells: - range: Waypoint + range: HexCell multivalued: true inlined_as_list: true Strategy: @@ -158,12 +165,17 @@ classes: inlined: false description: the commitment this leg serves (visit/exfil legs) TrajectoryPoint: - description: One sampled point on the platform's path; coordinates may be fractional, time/fuel rounded to 1 dp for IEEE stability. + description: One sampled point on the platform's path (H3 hex per ADR-0016); time/fuel rounded to 1 dp for IEEE stability. attributes: - x: + h3: + required: true + description: the H3 cell index (res 9) + lat: range: float - y: + description: cell-centre latitude + lng: range: float + description: cell-centre longitude t: range: float description: mission minutes @@ -214,6 +226,9 @@ classes: detour_rv: range: float description: mission minutes the detour route reaches the RV; null when no detour exists + rv_min: + range: float + description: mission minutes the chosen route reaches the RV (the committed arrival) narrative: description: the choice, in words Conflict: diff --git a/site/data-model/index.html b/site/data-model/index.html index 26650de..f16efad 100644 --- a/site/data-model/index.html +++ b/site/data-model/index.html @@ -277,7 +277,7 @@

REMIT — v1 Data Model

The serialisable object core of Aspect ||..o| Channel : "channel_ref" Orbat ||--o{ Asset : "assets" Orbat ||--o| Lineage : "lineage" - Asset ||--o| Waypoint : "position" + Asset ||--o| HexCell : "position" Asset ||--o| BlueParams : "blue" Asset ||--o| RedParams : "red" Asset ||--o| GreenParams : "green" @@ -290,7 +290,7 @@

REMIT — v1 Data Model

The serialisable object core of Stamp ||--o| StartState : "start" Stamp ||--o{ Appetite : "appetites" Stamp ||--o{ Constraint : "steering" - Constraint ||--o{ Waypoint : "cells" + Constraint ||--o{ HexCell : "cells" Plan ||--o| Strategy : "strategy" Plan ||--o| Stamp : "stamp" Plan ||--o| Materialisation : "materialisation" @@ -469,7 +469,7 @@

REMIT — v1 Data Model

The serialisable object core of Stamp ||--o| StartState : "start" Stamp ||--o{ Appetite : "appetites" Stamp ||--o{ Constraint : "steering" - Constraint ||--o{ Waypoint : "cells" + Constraint ||--o{ HexCell : "cells" Plan ||--o| Strategy : "strategy" Plan ||--o| Stamp : "stamp" Plan ||--o| Materialisation : "materialisation" @@ -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)
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 and clock baked into a Stamp (skeleton, DEC-47).

fieldtypecarddescription
xinteger0..1
yinteger0..1
clock_mininteger0..1

Appetite

One risk-appetite dial setting (DEC-6) — e.g. tempo = rapid, exposure = cautious.

fieldtypecarddescription
axisstring0..1"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"
cellsWaypoint0..*

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; coordinates may be fractional, time/fuel rounded to 1 dp for IEEE stability.

fieldtypecarddescription
xfloat0..1
yfloat0..1
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
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)"
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
   SelectionRationale {
   }
   ChosenBands {
diff --git a/test/schema-adherence.test.mjs b/test/schema-adherence.test.mjs
index 1476127..822affd 100644
--- a/test/schema-adherence.test.mjs
+++ b/test/schema-adherence.test.mjs
@@ -1,20 +1,15 @@
 // Schema-adherence guard (ADR-0011/0012, Principle I / DEC-57).
 //
-// Real skeleton instances — a committed ORBAT and a kernel Plan — are validated
-// against the GENERATED JSON Schema (schema/gen/remit.schema.json). This proves
-// the constitution's "schema ≡ code" for the serialisable object core, and fails
-// the build the moment code or schema drift apart (the gap a hand-written type
-// would hide). Build-free: imports the pure app modules directly, like the other
-// node --test suites.
+// Real skeleton instances — a committed ORBAT and the kernel's Plan handful — are
+// validated against the GENERATED JSON Schema (schema/gen/remit.schema.json). This
+// proves the constitution's "schema ≡ code" for the serialisable object core and
+// fails the build the moment code or schema drift apart (the gap a hand-written
+// type would hide). Build-free: imports the pure app modules directly, like the
+// other node --test suites.
 //
-// Known, pre-existing drifts are listed in DRIFT (each tracked in
-// docs/project_notes/bugs.md) and stripped before strict validation, so the guard
-// stays green while still catching any NEW drift in the surrounding fields. Drop
-// an entry here when its schema fix lands — the two open ones are:
-//   • Waypoint/StartState/TrajectoryPoint are still square-grid {x,y}; the app is
-//     hex {h3,lat,lng} since ADR-0016 (the Waypoint→HexCell migration).
-//   • the kernel carries appetites as a {axis:setting} map; the schema models
-//     Appetite[] {axis,setting}. TideDecision likewise shapes differently.
+// History: the first cut of this guard had to strip documented drifts (square-grid
+// Waypoint vs hex; appetites map-vs-list; TideDecision). The Waypoint→HexCell
+// migration (ADR-0030) closed them, so it now validates the instances whole.
 
 import { test } from 'node:test';
 import assert from 'node:assert/strict';
@@ -37,25 +32,10 @@ function validatorFor(cls) {
   return v;
 }
 
-// Documented schema↔code drifts (docs/project_notes/bugs.md), stripped per class.
-const DRIFT = {
-  Asset: ['position'],
-  Stamp: ['start', 'appetites'],
-  Materialisation: ['trajectory', 'tide'],
-  Plan: ['tide_decision'],
-};
-
-const clone = (x) => JSON.parse(JSON.stringify(x));
-function strip(cls, inst) {
-  const c = clone(inst);
-  for (const f of DRIFT[cls] ?? []) delete c[f];
-  return c;
-}
-
-/** Assert `instance` validates against `#/$defs/` once documented drift is stripped. */
+/** Assert `instance` validates against `#/$defs/`. */
 function adheres(cls, instance) {
   const v = validatorFor(cls);
-  const ok = v(strip(cls, instance));
+  const ok = v(instance);
   const detail = ok ? '' : '\n' + v.errors.map((e) => `  ${e.instancePath || '(root)'} ${e.message}`).join('\n');
   assert.ok(ok, `${cls} does not adhere to the generated JSON Schema:${detail}`);
 }
@@ -69,11 +49,8 @@ test('ORBAT — a committed Orbat (red + green + own-force blue) adheres to the
   const { id } = await commit(o, new ObjectStore());
 
   // The full Orbat (content id reattached to the id-free canonical body, DEC-35),
-  // with each asset's drifting position stripped, must validate strictly.
-  const orbat = { id, ...JSON.parse(canonical(o)) };
-  orbat.assets = orbat.assets.map((a) => strip('Asset', a));
-  const v = validatorFor('Orbat');
-  assert.ok(v(orbat), 'Orbat: ' + (v.errors || []).map((e) => `${e.instancePath} ${e.message}`).join(' | '));
+  // hex positions and all.
+  adheres('Orbat', { id, ...JSON.parse(canonical(o)) });
 
   assert.equal(o.assets.length, 3);
   for (const a of o.assets) adheres('Asset', a); // red, green, blue individually
@@ -81,7 +58,7 @@ test('ORBAT — a committed Orbat (red + green + own-force blue) adheres to the
 
 // ---- Plan / kernel output (spec 002/003) ----------------------------------------
 
-test('Plan — a kernel plan, its Stamp, Scores and Materialisation adhere to the schema', async () => {
+test('Plan — the kernel handful, its Stamp, Scores and Materialisation adhere to the schema', async () => {
   const world = buildWorld();
   const OP = world.places.ops[0];
   const RV = world.places.rvEast;
@@ -96,18 +73,17 @@ test('Plan — a kernel plan, its Stamp, Scores and Materialisation adhere to th
     state: world.state, config_core: await contentId(world.configCore),
     appetites: { tempo: 'balanced', exposure: 'balanced' }, steering: [], strategy_seed: 1337, ao: world.ao,
   });
-  const p = plans[0];
 
-  adheres('Stamp', p.stamp);
+  // Every plan in the handful — exercises both tide modes (detour + wait).
+  for (const p of plans) adheres('Plan', p);
+
+  const p = plans[0];
+  adheres('Stamp', p.stamp);                     // hex start + axis→setting appetites map
   adheres('Scores', p.scores);
-  adheres('Materialisation', p.materialisation);
+  adheres('Materialisation', p.materialisation); // hex trajectory
 
-  // The whole Plan, with drift stripped from its nested Stamp + Materialisation.
-  const plan = strip('Plan', p);
-  plan.stamp = strip('Stamp', plan.stamp);
-  plan.materialisation = strip('Materialisation', plan.materialisation);
-  const v = validatorFor('Plan');
-  assert.ok(v(plan), 'Plan: ' + (v.errors || []).map((e) => `${e.instancePath} ${e.message}`).join(' | '));
+  // A no-go steering constraint — its cells are HexCells since ADR-0030.
+  adheres('Constraint', { type: 'no-go', cells: [{ h3: '89195436313ffff' }] });
 
   // The guard guards: corrupting a clean object with an undeclared field must fail
   // (additionalProperties:false), so NEW drift cannot slip past.