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
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,5 +277,6 @@ branch; allow workflows to write).

<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan
shell commands, and other important information, read the current plan:
`specs/004-orbat-red-green-assets/plan.md`
<!-- SPECKIT END -->
42 changes: 42 additions & 0 deletions specs/004-orbat-red-green-assets/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Specification Quality Checklist: ORBAT — add & tune blue, red, and green assets

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-06-12
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Notes

- Scope covers all three allegiances — **blue, red, and green** — at the **display-only** authoring
scaffolding level (DEC-56 horizon split, NF9 honest floor). Blue is included for *one ontology,
three stances* symmetry but does **not** drive routing (the existing planned own-force is
reconciled, not re-implemented); reactive-adversary, capability-matched allocation, and
constraint/objective emission are called out as deferred, not in scope.
- The exact per-allegiance parameter field list is left to planning/design (the spec fixes the
shape, not field names) — a candidate for `/speckit-clarify` if the maintainer wants it pinned
before planning.
- All items pass on first iteration. Ready for `/speckit-clarify` (optional) or `/speckit-plan`.
51 changes: 51 additions & 0 deletions specs/004-orbat-red-green-assets/contracts/orbat-store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Contract — ORBAT model & persistence (`app/js/orbat/orbat.js`)

The ORBAT model is the only writer of the authored roster. It is a **pure, deterministic** module
over the LinkML-generated `Orbat`/`Asset` types (imported from `schema/gen/remit.ts`); UI and
rendering read through it. All operations return a **new** draft (no in-place mutation), keeping
identity stable and rendering reproducible (NF3).

## Types

Imported from the generated schema (do not redeclare):
`Orbat`, `Asset`, `BlueParams`, `RedParams`, `GreenParams`, `TimeWindow`, `Allegiance`, `Protection`.

## Operations

| Function | Signature (conceptual) | Guarantees |
|---|---|---|
| `emptyOrbat(name)` | `(string) → Orbat` | version 1, `assets: []`. |
| `addAsset(orbat, { allegiance, position })` | `(Orbat, seed) → { orbat, id }` | mints a fresh unique `id`; seeds default params for the allegiance (`blue`/`red`/`green`); rejects out-of-AO `position`. |
| `duplicateAsset(orbat, id)` | `(Orbat, id) → { orbat, id }` | deep-copies the source's params under a **new** `id` (never copies `canonical_own_force`); source unchanged (FR-002/006). |
| `tuneAsset(orbat, id, patch)` | `(Orbat, id, Partial<Asset>) → Orbat` | applies `patch` to **only** that asset; **clamps/validates** bounds (FR-004); other assets byte-identical. |
| `removeAsset(orbat, id)` | `(Orbat, id) → Orbat` | drops the asset; remaining assets unaffected (FR-006). The canonical own-force blue asset (`canonical_own_force`) is **protected** — removal is refused (FR-012). |
| `reconcileOwnForce(orbat, self)` | `(Orbat, Entity) → Orbat` | ensures exactly one blue asset mirrors the existing planned own-force (ROVER-1), `canonical_own_force = true`; idempotent. |
| `validate(asset)` | `(Asset) → { ok, issues[] }` | bounds + window `start ≤ end` + position-in-AO + allegiance/param-group match. |
| `canonical(orbat)` | `(Orbat) → string` | assets sorted by `id`, canonical JSON (DEC-35) — the persistence & identity form. |

## Persistence

- **Draft**: `saveDraft(orbat)` writes `canonical(orbat)` to `localStorage['remit.orbat.M-001']`;
`loadDraft()` restores it (returns `emptyOrbat` if absent). Mirrored on every mutating op so reload
restores exactly (SC-004).
- **Commit**: `commit(orbat, objects)` PUTs an immutable, content-addressed `Orbat` into the shared
`ObjectStore` with `lineage.previous_version` → the prior committed id (Principle V; idempotent
re-PUT per DEC-35).

## Invariants

- **Determinism (NF3)**: `canonical` is a pure function of asset content (sorted by `id`); equal
rosters ⇒ equal bytes ⇒ equal content id. No timestamps/insertion-order leak in.
- **Honest floor (NF9) + display-only**: the module exposes **no** function that derives adversary
behaviour or mutates a plan; blue `availability`/`capabilities` do not feed routing. The only
kernel-wired own-force is the pre-existing planned `self`, which the canonical blue asset mirrors
(reconciled, not re-implemented). Output is consumed display-only.
- **Isolation (FR-002/SC-003)**: every op returns a new draft touching only the targeted asset.

## Display adapter

`assetToEntity(asset) → Entity` (display-only; the hand-written carve-out) produces the
`buildEntities()`-shaped entity: allegiance-typed, `provenance.kind` = `self` for the canonical
own-force blue asset else `actor`, a `position` for the map marker, and — for any asset with a
time-varying aspect (red `active_windows`, a blue availability window) — a `window`-type aspect for
the Sync Matrix (reusing the satellite-pass render path). Contains no kernel reference.
42 changes: 42 additions & 0 deletions specs/004-orbat-red-green-assets/contracts/orbat-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Contract — ORBAT authoring surface (`app/js/shell/orbat-panel.js`)

A config-declared role-tab (DEC-61 shell seed; natural home: the `sme-int` role — "red/green
entities and threat"). It mounts with the shared context `{ objects, world, playhead, … }` and reads
/ writes the roster **only** through `app/js/orbat/orbat.js` (contract: `orbat-store.md`).

## Affordances (testable contract)

| Affordance | Behaviour | Backing requirement |
|---|---|---|
| **Roster** | Three groups, **Blue (own force)**, **Red (hostile)**, **Green (neutral)**, listing each asset by label + key params. Empty groups show an explicit "none" state. The canonical own-force (ROVER-1) is shown in Blue, marked and protected from removal. | FR-001/012, edge: empty ORBAT |
| **Add blue / Add red / Add green** | Adds a default asset of that allegiance; placement via the map's click-to-pick-hex (spec 003 F5) or a default AO-centre position. Appears immediately in the roster and on the map. | FR-001/005, US1/US2/US3 |
| **Per-row tuners** | Numeric/range/select inputs for `label`, `extent_m`, and the allegiance group: `severity` + `active_windows` (red); `sensitivity` + `protection` (green); `availability` + `capabilities` (blue). Editing re-renders map + Sync-Matrix live. | FR-003, US1/US2/US3 |
| **Duplicate** | One click clones the row under a new id (never the `canonical_own_force` flag); the copy is independently tunable. | FR-006, US4 |
| **Remove** | Removes the row from roster and all views; others unaffected. The canonical own-force asset's remove control is disabled. | FR-006/012, US4 |
| **Validation feedback** | Out-of-range tunes and out-of-AO placements are clamped/rejected with an inline message; the asset never enters an invalid state. | FR-004, edges |
| **Selection** | Selecting a row highlights the asset on the map / Sync-Matrix and vice-versa (shared selection). | US5 |

## Projection contract (display-only)

- **Map** (`map.render`): each asset draws an allegiance-coloured point + faint extent ring +
label, visually distinct across the three allegiances (FR-005). Blue own-force family, red
`#ff7b72`-family, green `#38d39f`-family (palette in `key_facts.md`).
- **Sync Matrix** (`sync-matrix`): any asset with a time-varying aspect (red `active_windows`, a blue
availability window) contributes a `window`-render track via a catalogue row; it stays aligned with
the shared playhead and selection (FR-010/US5).
- **Honest floor + display-only**: nothing the panel shows implies adversary motion/assessment, and
no tune (including blue availability/capability) changes the route or plan (FR-008/NF9).

## Persistence contract

- Every mutating affordance calls `saveDraft` (mirror to localStorage). On mount, `loadDraft`
restores the roster and all tuned values exactly (FR-007/SC-004).
- A **Commit ORBAT** action mints an immutable version in the `ObjectStore` (lineage preserved).

## Non-goals (deferred, asserted by tests where cheap)

- No routing/kernel influence from any asset, including blue tuning (NF9/NF3) — the plan/route is
unchanged by authoring.
- No capability-matched allocation of the blue force-pool (DEC-60 capability — H2): blue is
display-only scaffolding; the existing planned own-force is reconciled, not re-implemented.
- No live ROE constraint / collateral objective emission (DEC-60 J3 capability — H2/H3).
155 changes: 155 additions & 0 deletions specs/004-orbat-red-green-assets/data-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Phase 1 — Data Model: ORBAT red/green assets

**Source of truth = LinkML** (Principle I). The shapes below are authored in the schema modules and
**regenerated** into `schema/gen/remit.schema.json` + `schema/gen/remit.ts` via `schema/generate.sh`.
The app imports the generated TS; it does not hand-author these shapes. Render closures (aspect
time-functions) are the documented behaviour/UI carve-out and stay in `app/js`.

## Schema additions

### `common.yaml` — new enum

```yaml
enums:
Allegiance:
description: >-
Side typing on an entity (DEC-60). Selects the kernel STANCE — plan-for (blue) /
avoid-assess (red) / respect (green). v1 is display-only (NF9 honest floor).
permissible_values:
blue: { description: own force }
red: { description: hostile / adversary (threat source; passive in v1) }
green: { description: neutral / host-nation / civilian (ROE & collateral; inert in v1) }
```

### `entities.yaml` — one attribute on `Entity`

```yaml
attributes:
# ...existing id/label/kind/provenance/aspects...
allegiance:
range: Allegiance
description: side typing (DEC-60); absent ⇒ unaligned/own-context as today
```

> Additive and optional, so existing entities (self/tide/sat) remain valid.

### `orbat.yaml` — NEW module (imported by `remit.yaml`)

```yaml
classes:
Orbat:
description: >-
The roster of participants & potential participants for a scenario — the authoring root
(DEC-60). Versioned & immutable when committed (lineage); the editable working draft mirrors
to localStorage. v1 authors the red & green sides; blue is the existing own force.
attributes:
id: { identifier: true, description: content id of the canonical form (DEC-35) }
name: {}
version: { range: integer }
assets: { range: Asset, multivalued: true, inlined_as_list: true }
lineage: { range: Lineage, inlined: true }

Asset:
description: >-
One ORBAT entry — a first-class located thing (DEC-52) typed by allegiance, with
independently-tunable parameters. Display-only in v1.
attributes:
id: { identifier: true, description: stable per-instance identity (not the label) }
allegiance: { range: Allegiance, required: true }
label: { description: human label; need not be unique }
position: { range: Waypoint, inlined: true, description: AO location (H3 cell / lat-lon) }
extent_m: { range: float, description: reach/footprint radius in metres }
blue: { range: BlueParams, inlined: true, description: present iff allegiance = blue }
red: { range: RedParams, inlined: true, description: present iff allegiance = red }
green: { range: GreenParams, inlined: true, description: present iff allegiance = green }
canonical_own_force:
range: boolean
description: >-
true on the single blue asset reconciled from the existing planned own-force (ROVER-1);
it drives the plan via the existing machinery and is protected from removal.

BlueParams:
description: >-
Own-force pool member (capability-matched ALLOCATION deferred to H2; display-only in v1 — does
not drive routing). The capability vocabulary is the seam a future Scheme matches to a
requirement's activity needs (DEC-59/60).
attributes:
availability: { range: string, description: '"available | down" (own State mirror, DEC-52)' }
capabilities: { range: string, multivalued: true,
description: capability tags a future Scheme matches to activity needs (stub) }

RedParams:
description: Hostile threat picture (threat SOURCE only in v1; reactive behaviour deferred, DEC-51).
attributes:
severity: { range: integer, description: graded threat severity (e.g. 1..5) }
active_windows: { range: TimeWindow, multivalued: true, inlined_as_list: true,
description: mission-minute windows the threat is active (Sync-Matrix track) }

GreenParams:
description: Neutral / collateral picture (ROE & collateral emission deferred; inert in v1, DEC-60 J3).
attributes:
sensitivity: { range: integer, description: graded collateral weight (e.g. 1..5) }
protection: { range: Protection, description: nature of the rule (tagged for the future hard/soft split) }

TimeWindow:
description: A mission-minute interval [start,end].
attributes:
start_min: { range: integer, required: true }
end_min: { range: integer, required: true }

enums:
Protection:
permissible_values:
keep_out: { description: no-go / no-strike area (future HARD constraint) }
minimise_effect: { description: collateral to be minimised (future SOFT objective) }
```

> `Waypoint` and `Lineage` already exist in the schema (reused, not redefined).

## Entities (summary)

| Entity | Key fields | Relationships | Notes |
|---|---|---|---|
| **Orbat** | `id`, `version`, `assets[]`, `lineage` | contains `Asset[]` | versioned/immutable when committed; working draft in localStorage |
| **Asset** | `id`, `allegiance`, `label`, `position`, `extent_m`, `canonical_own_force?` | one of `blue`/`red`/`green` params | first-class Entity (DEC-52); display-only |
| **BlueParams** | `availability`, `capabilities[]` | — | force-pool member; allocation deferred (H2); does not drive routing (NF9) |
| **RedParams** | `severity`, `active_windows[]` | `TimeWindow[]` | threat source only (NF9) |
| **GreenParams** | `sensitivity`, `protection` | `Protection` enum | inert in v1 (DEC-60 J3) |

## Validation rules (from the spec)

- **FR-001/003**: an Asset MUST have `allegiance ∈ {blue, red, green}`, a `position` inside the AO,
and the parameter group matching its allegiance.
- **FR-002**: `id` is unique per instance and never reused; duplicating mints a fresh `id`.
- **FR-004**: `extent_m`, `severity`, `sensitivity` are clamped to their declared bounds; an
`active_window` MUST satisfy `start_min ≤ end_min` (rejected/clamped with feedback otherwise).
- **FR-012 — own-force reconciliation**: exactly one blue asset may carry `canonical_own_force = true`
(the existing planned ROVER-1, surfaced not duplicated); it is protected from removal so the plan
stays valid.
- **Edge — out-of-bounds position**: rejected or clamped to the AO with feedback (never silently lost).
- **Edge — duplicate labels**: permitted; identity is `id`, not `label`.
- **FR-008/NF9**: no field drives kernel behaviour — including blue `availability`/`capabilities`,
which do **not** alter routing in v1; all are display inputs only.

## Lifecycle / state

```text
(draft) add ──► tune ──► duplicate / remove ──► tune … ┐ mirrored to localStorage each change
commit ──► Orbat v_n (immutable, content-addressed, lineage→v_{n-1})
```

- The editable surface is the **draft**; **commit** mints an immutable `Orbat` version in the
`ObjectStore` (Principle V). Reload restores the draft from localStorage (SC-004).
- Canonical/sorted-by-`id` serialisation underpins determinism (NF3) and stable content ids (DEC-35).

## App adapter (display-only, hand-written — carve-out)

`app/js/orbat/orbat.js` maps each committed/draft `Asset` → an `Entity` for `buildEntities()`:
`{ id, label, allegiance, provenance:{kind}, aspects }` (`kind` = `self` for the canonical own-force
blue asset, else `actor`). Red assets with `active_windows` — and any asset given a time-varying
aspect (e.g. a blue asset's availability window) — expose a `window`-type aspect (reusing the
satellite-pass render path) so they appear as Sync-Matrix tracks; position feeds the map marker. The
canonical own-force blue asset reuses the existing planned `self` entity (reconciled, not duplicated)
and is the *only* asset already wired to the kernel via the pre-existing machinery; no `assetToEntity`
closure references the kernel or alters a plan.
Loading
Loading