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
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Generated artefacts (DEC-57 / ADR-0011): produced by schema/generate.sh from the
# LinkML schema and enforced by the schema-regen CI check — never hand-edited.
# Marked generated so GitHub collapses them in diffs and omits them from language stats.
schema/gen/** linguist-generated=true
site/data-model/index.html linguist-generated=true
39 changes: 39 additions & 0 deletions .github/workflows/schema-regen.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: schema-regen

# Guards Principle I (LinkML is the one source of truth, DEC-57/ADR-0011): the
# committed artefacts under schema/gen/ and site/data-model/ are GENERATED, never
# hand-edited. This regenerates them from the schema and fails if anything drifts,
# so the generated files can never silently fall out of step with schema/*.yaml.

on:
pull_request:
push:
branches: [main]

concurrency:
group: schema-regen-${{ github.ref }}
cancel-in-progress: true

jobs:
regen:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

# Match the toolchain generate.sh pins (linkml==1.11.1) was verified against,
# so regeneration is byte-for-byte reproducible.
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Regenerate every derived artefact from the LinkML schema
run: bash schema/generate.sh

- name: Fail if the generated artefacts are stale (schema ≡ generated)
run: |
if ! git diff --exit-code -- schema/gen site/data-model; then
echo "::error::Generated artefacts are out of date. Run 'bash schema/generate.sh' and commit schema/gen/ + site/data-model/index.html."
exit 1
fi
33 changes: 33 additions & 0 deletions .github/workflows/unit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: unit

# Runs the node --test unit suite in CI. Until now only e2e + typecheck ran on PRs,
# so the golden kernel fixtures (NF3), ORBAT model, routing/hexgrid and the new
# schema-adherence guard (ADR-0029) were verified locally only. Build-free.

on:
pull_request:
push:
branches: [main]

concurrency:
group: unit-${{ github.ref }}
cancel-in-progress: true

jobs:
unit:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Unit tests
run: npm run test:unit
19 changes: 19 additions & 0 deletions docs/project_notes/bugs.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,23 @@ Each entry records: date, symptom, root cause, fix, and how to prevent recurrenc
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.

## Schema-adherence guard surfaces the full extent of the schema↔code drift (2026-06-14)

- **Symptom:** the new schema-adherence test (ADR-0029, `test/schema-adherence.test.mjs`) validates real
instances against the generated JSON Schema and found `Asset.position`, `Stamp.start` and
`Materialisation.trajectory` fail `additionalProperties:false` — runtime hex `{h3,lat,lng}` vs the schema's
square-grid `Waypoint`/`StartState`/`TrajectoryPoint` `{x,y}`. It also flagged two non-hex drifts: the kernel's
`appetites` is a `{axis:setting}` map where the schema models `Appetite[] {axis,setting}`, and `TideDecision`
carries runtime fields beyond the schema's.
- **Root cause:** the same drift as the `SteeringDelta`/`Constraint.cells` entry above — the app moved to H3 hex
(ADR-0016) and grew kernel shapes that the LinkML source was never updated to match; the DEC-57 "schema ≡ code"
invariant has drifted on these classes too, not just `SteeringDelta`.
- **Fix (interim):** the adherence test strips these documented fields (its `DRIFT` map) before strict validation,
so the guard is green and still catches any NEW drift. The drift stays visible (here + the test's comments),
never hidden.
- **Real fix:** the Waypoint→HexCell migration (add a hex cell type / `Waypoint.h3`; repoint `Asset.position`,
`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.

<!-- Add new entries above this line. -->
38 changes: 38 additions & 0 deletions docs/project_notes/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -760,3 +760,41 @@ consequences. Link evidence (e.g. `specs/<feature>/evidence/`) where relevant.
- **Remaining skeleton notes:** World-before-Capture tool-order, mock band-calibration, and the `entities/` vs
`views/projection/` module placement were reviewed and **held as-is** (no register change); the mid-stream
coincidence H2→H1-lite edit (ADR-0009/DEC-53) was already reconciled. Closes issue #3.

## ADR-0029 (2026-06-14) — LinkML guardrails: GENERATED banners, regen-no-diff CI, schema-adherence test

- **Context:** ADR-0011/0012 adopted LinkML as the one source of truth (DEC-57) and logged three deferred
guardrails to make Principle I *enforceable* rather than aspirational: GENERATED banners on the derived
artefacts, a regen-no-diff CI check, and a golden-fixtures adherence test (skeleton instances validate against
the generated JSON Schema). This lands all three. (The fourth ADR-0012 note — migrating the app's inline shapes
onto the generated TS — remains its own spec.)
- **Decision:**
- **GENERATED banners.** `schema/generate.sh` now stamps every derived file `@generated — DO NOT EDIT` (a `//`
block on `remit.ts`, a `$comment` first-key on `remit.schema.json` — textual insert, no reformat — and an HTML
comment on `index.html`), plus a `.gitattributes` marking them `linguist-generated`. The banner is re-applied
each run, so it survives regeneration.
- **regen-no-diff CI** (`.github/workflows/schema-regen.yml`): regenerates from the schema and fails on any diff
under `schema/gen/` + `site/data-model/`, so the committed artefacts can never silently fall out of step with
`schema/*.yaml`. Reproducibility pinned — `generate.sh` installs `linkml==1.11.1` + `linkml-runtime==1.11.1`,
CI uses Python 3.11; verified to reproduce the committed bytes exactly (empty `git diff`).
- **Schema-adherence test** (`test/schema-adherence.test.mjs`, `node --test`): builds real instances — a
committed `Orbat` (red/green/own-force) and a kernel `Plan` (Stamp/Scores/Materialisation) — and validates
them against `schema/gen/remit.schema.json` with **ajv** (dev-only; draft-2019-09). Documented pre-existing
drifts are stripped per class (its `DRIFT` map) and the rest validated strictly, so the guard is green yet
still fails on any NEW drift (proven by an undeclared-field assertion). Wired into a new **`unit.yml`** CI job
(`npm run test:unit`) — which also closes the gap that the unit suite (golden fixtures, ORBAT, routing) had
never run in CI, only e2e + typecheck did, so the adherence guard (and all the others) now actually fire on PRs.
- **The guard earned its keep immediately:** it confirmed the documented `Waypoint` square-vs-hex drift (bugs.md)
extends beyond `SteeringDelta` to `Asset.position`, `Stamp.start` and `Materialisation.trajectory`, and surfaced
two more — the kernel carries `appetites` as a `{axis:setting}` map where the schema models `Appetite[]`, and
`TideDecision` shapes differently. Recorded (bugs.md) and stripped via `DRIFT`; the Waypoint→HexCell migration +
appetites/tide reconciliation (then emptying `DRIFT`) is the concrete next follow-up this guard makes visible.
- **Dependency (ADR-0014):** adds `ajv` as a **dev**-only dependency (test-only; never imported by `app/` or the
kernel), maintainer-approved for this guard. No runtime dependency added.
- **Options considered:** (a) ajv vs a hand-rolled validator — ajv, for faithful draft-2019-09 validation (a
hand-rolled check can't honestly resolve `$ref`/`anyOf`/`additionalProperties`); (b) banner via reserialisation
vs textual insert — textual, to leave gen-json-schema's bytes untouched (zero churn); (c) fixing the surfaced
drift now vs strip-and-track — tracked, to keep this PR a pure guardrail, not a schema migration.
- **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.
2 changes: 2 additions & 0 deletions docs/project_notes/issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,5 @@ evidence (e.g. `specs/<feature>/evidence/`).
| 2026-06-14 | `claude/spec-04-implement-0u0s9y` | **ORBAT asset enrichment (spec 005, ADR-0027).** Display-only, additive enrichment of the SME-Int ORBAT (NF9): shared `kind` (`PlatformKind` enum) → allegiance-framed map **symbols** (deck.gl `TextLayer` glyphs via a `SYMBOLS` lookup, no icon atlas) with a per-asset `symbol` override; intel `confidence` (reusing `ConfidenceLevel`) → marker **opacity**; red **dual range rings** (`detection_range_m`/`engagement_range_m`, `engagement ≤ detection`) replacing the single extent for red; descriptive `strength`/`notes` + red `threat_type` / green `category` (`GreenCategory` enum) / blue `role`. All schema-defined + regenerated (Principle I); model/panel/map extended in place. Backward-compatible: idempotent `normalize()` migrates spec-004 red drafts (`extent_m` → detection). 12 new unit (30 total) + 4 new e2e (9 total) green; 0 typecheck errors. Deferred: place-on-map + NATO frame shapes (later slice); routing influence (DEC-51). | ADR-0027 · `specs/005-orbat-asset-enrichment/` |

| 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 |
1 change: 1 addition & 0 deletions docs/project_notes/key_facts.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +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). |

_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._
59 changes: 59 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"devDependencies": {
"@playwright/test": "^1.60.0",
"@sparticuz/chromium": "^149.0.0",
"ajv": "^8.20.0",
"typescript": "^5.7.2",
"vite": "^8.0.16"
},
Expand Down
1 change: 1 addition & 0 deletions schema/gen/remit.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions schema/gen/remit.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading