Skip to content

Commit 660aa4e

Browse files
authored
Merge pull request #13 from DeepBlueCLtd/claude/linkml-guardrails
LinkML guardrails: GENERATED banners, regen-no-diff CI, schema-adherence test
2 parents 6e8bbfe + 987a526 commit 660aa4e

14 files changed

Lines changed: 353 additions & 1 deletion

File tree

.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Generated artefacts (DEC-57 / ADR-0011): produced by schema/generate.sh from the
2+
# LinkML schema and enforced by the schema-regen CI check — never hand-edited.
3+
# Marked generated so GitHub collapses them in diffs and omits them from language stats.
4+
schema/gen/** linguist-generated=true
5+
site/data-model/index.html linguist-generated=true

.github/workflows/schema-regen.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: schema-regen
2+
3+
# Guards Principle I (LinkML is the one source of truth, DEC-57/ADR-0011): the
4+
# committed artefacts under schema/gen/ and site/data-model/ are GENERATED, never
5+
# hand-edited. This regenerates them from the schema and fails if anything drifts,
6+
# so the generated files can never silently fall out of step with schema/*.yaml.
7+
8+
on:
9+
pull_request:
10+
push:
11+
branches: [main]
12+
13+
concurrency:
14+
group: schema-regen-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
regen:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v4
23+
24+
# Match the toolchain generate.sh pins (linkml==1.11.1) was verified against,
25+
# so regeneration is byte-for-byte reproducible.
26+
- name: Set up Python
27+
uses: actions/setup-python@v5
28+
with:
29+
python-version: '3.11'
30+
31+
- name: Regenerate every derived artefact from the LinkML schema
32+
run: bash schema/generate.sh
33+
34+
- name: Fail if the generated artefacts are stale (schema ≡ generated)
35+
run: |
36+
if ! git diff --exit-code -- schema/gen site/data-model; then
37+
echo "::error::Generated artefacts are out of date. Run 'bash schema/generate.sh' and commit schema/gen/ + site/data-model/index.html."
38+
exit 1
39+
fi

.github/workflows/unit.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: unit
2+
3+
# Runs the node --test unit suite in CI. Until now only e2e + typecheck ran on PRs,
4+
# so the golden kernel fixtures (NF3), ORBAT model, routing/hexgrid and the new
5+
# schema-adherence guard (ADR-0029) were verified locally only. Build-free.
6+
7+
on:
8+
pull_request:
9+
push:
10+
branches: [main]
11+
12+
concurrency:
13+
group: unit-${{ github.ref }}
14+
cancel-in-progress: true
15+
16+
jobs:
17+
unit:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Set up Node
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: 22
27+
cache: npm
28+
29+
- name: Install dependencies
30+
run: npm ci
31+
32+
- name: Unit tests
33+
run: npm run test:unit

docs/project_notes/bugs.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,23 @@ Each entry records: date, symptom, root cause, fix, and how to prevent recurrenc
250250
re-run `schema/generate.sh`, then drop the cast. Enforced type-checking (ADR-0024) now
251251
catches this class of schema/code drift at build time instead of silently.
252252

253+
## Schema-adherence guard surfaces the full extent of the schema↔code drift (2026-06-14)
254+
255+
- **Symptom:** the new schema-adherence test (ADR-0029, `test/schema-adherence.test.mjs`) validates real
256+
instances against the generated JSON Schema and found `Asset.position`, `Stamp.start` and
257+
`Materialisation.trajectory` fail `additionalProperties:false` — runtime hex `{h3,lat,lng}` vs the schema's
258+
square-grid `Waypoint`/`StartState`/`TrajectoryPoint` `{x,y}`. It also flagged two non-hex drifts: the kernel's
259+
`appetites` is a `{axis:setting}` map where the schema models `Appetite[] {axis,setting}`, and `TideDecision`
260+
carries runtime fields beyond the schema's.
261+
- **Root cause:** the same drift as the `SteeringDelta`/`Constraint.cells` entry above — the app moved to H3 hex
262+
(ADR-0016) and grew kernel shapes that the LinkML source was never updated to match; the DEC-57 "schema ≡ code"
263+
invariant has drifted on these classes too, not just `SteeringDelta`.
264+
- **Fix (interim):** the adherence test strips these documented fields (its `DRIFT` map) before strict validation,
265+
so the guard is green and still catches any NEW drift. The drift stays visible (here + the test's comments),
266+
never hidden.
267+
- **Real fix:** the Waypoint→HexCell migration (add a hex cell type / `Waypoint.h3`; repoint `Asset.position`,
268+
`Constraint.cells`, `StartState`, `TrajectoryPoint`), reconcile `appetites``Appetite[]` and `TideDecision`,
269+
re-run `schema/generate.sh`, then empty the test's `DRIFT` map. The regen-no-diff + adherence checks (ADR-0029)
270+
now make this class of drift impossible to reintroduce silently.
271+
253272
<!-- Add new entries above this line. -->

docs/project_notes/decisions.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,3 +760,41 @@ consequences. Link evidence (e.g. `specs/<feature>/evidence/`) where relevant.
760760
- **Remaining skeleton notes:** World-before-Capture tool-order, mock band-calibration, and the `entities/` vs
761761
`views/projection/` module placement were reviewed and **held as-is** (no register change); the mid-stream
762762
coincidence H2→H1-lite edit (ADR-0009/DEC-53) was already reconciled. Closes issue #3.
763+
764+
## ADR-0029 (2026-06-14) — LinkML guardrails: GENERATED banners, regen-no-diff CI, schema-adherence test
765+
766+
- **Context:** ADR-0011/0012 adopted LinkML as the one source of truth (DEC-57) and logged three deferred
767+
guardrails to make Principle I *enforceable* rather than aspirational: GENERATED banners on the derived
768+
artefacts, a regen-no-diff CI check, and a golden-fixtures adherence test (skeleton instances validate against
769+
the generated JSON Schema). This lands all three. (The fourth ADR-0012 note — migrating the app's inline shapes
770+
onto the generated TS — remains its own spec.)
771+
- **Decision:**
772+
- **GENERATED banners.** `schema/generate.sh` now stamps every derived file `@generated — DO NOT EDIT` (a `//`
773+
block on `remit.ts`, a `$comment` first-key on `remit.schema.json` — textual insert, no reformat — and an HTML
774+
comment on `index.html`), plus a `.gitattributes` marking them `linguist-generated`. The banner is re-applied
775+
each run, so it survives regeneration.
776+
- **regen-no-diff CI** (`.github/workflows/schema-regen.yml`): regenerates from the schema and fails on any diff
777+
under `schema/gen/` + `site/data-model/`, so the committed artefacts can never silently fall out of step with
778+
`schema/*.yaml`. Reproducibility pinned — `generate.sh` installs `linkml==1.11.1` + `linkml-runtime==1.11.1`,
779+
CI uses Python 3.11; verified to reproduce the committed bytes exactly (empty `git diff`).
780+
- **Schema-adherence test** (`test/schema-adherence.test.mjs`, `node --test`): builds real instances — a
781+
committed `Orbat` (red/green/own-force) and a kernel `Plan` (Stamp/Scores/Materialisation) — and validates
782+
them against `schema/gen/remit.schema.json` with **ajv** (dev-only; draft-2019-09). Documented pre-existing
783+
drifts are stripped per class (its `DRIFT` map) and the rest validated strictly, so the guard is green yet
784+
still fails on any NEW drift (proven by an undeclared-field assertion). Wired into a new **`unit.yml`** CI job
785+
(`npm run test:unit`) — which also closes the gap that the unit suite (golden fixtures, ORBAT, routing) had
786+
never run in CI, only e2e + typecheck did, so the adherence guard (and all the others) now actually fire on PRs.
787+
- **The guard earned its keep immediately:** it confirmed the documented `Waypoint` square-vs-hex drift (bugs.md)
788+
extends beyond `SteeringDelta` to `Asset.position`, `Stamp.start` and `Materialisation.trajectory`, and surfaced
789+
two more — the kernel carries `appetites` as a `{axis:setting}` map where the schema models `Appetite[]`, and
790+
`TideDecision` shapes differently. Recorded (bugs.md) and stripped via `DRIFT`; the Waypoint→HexCell migration +
791+
appetites/tide reconciliation (then emptying `DRIFT`) is the concrete next follow-up this guard makes visible.
792+
- **Dependency (ADR-0014):** adds `ajv` as a **dev**-only dependency (test-only; never imported by `app/` or the
793+
kernel), maintainer-approved for this guard. No runtime dependency added.
794+
- **Options considered:** (a) ajv vs a hand-rolled validator — ajv, for faithful draft-2019-09 validation (a
795+
hand-rolled check can't honestly resolve `$ref`/`anyOf`/`additionalProperties`); (b) banner via reserialisation
796+
vs textual insert — textual, to leave gen-json-schema's bytes untouched (zero churn); (c) fixing the surfaced
797+
drift now vs strip-and-track — tracked, to keep this PR a pure guardrail, not a schema migration.
798+
- **Consequences:** Principle I is now enforced, not just stated — generated files are labelled, drift fails CI
799+
two ways (regen + adherence), and the schema's real gaps are visible and tracked. Scope: tooling/CI/test only;
800+
the sole schema-output change is the banner text. No `app/`/kernel code changed.

docs/project_notes/issues.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,5 @@ evidence (e.g. `specs/<feature>/evidence/`).
6060
| 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/` |
6161

6262
| 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) |
63+
64+
| 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 |

docs/project_notes/key_facts.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ need a value.
4141
| 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). |
4242
| 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. |
4343
| 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. |
44+
| 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). |
4445

4546
_Pages URLs resolve once GitHub Pages is enabled (served from `gh-pages`). Add
4647
anything else worth remembering (service URLs, IDs, constants) as it comes up._

package-lock.json

Lines changed: 59 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"devDependencies": {
1919
"@playwright/test": "^1.60.0",
2020
"@sparticuz/chromium": "^149.0.0",
21+
"ajv": "^8.20.0",
2122
"typescript": "^5.7.2",
2223
"vite": "^8.0.16"
2324
},

schema/gen/remit.schema.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)