# Map — Codex (per FVW v8 §22 + §43)

**Module ID:** `oscar.map`
**Version:** 0.2.0
**Status:** Constitutional (per §43 + latent doctrine, locally ratified).

## C1 — Behaviour Inventory

- `app/src/map/MapView.tsx` — Leaflet container + tile layer + drawn layer group + pending layer group + vertex-drag handlers + selection state bridge
- `app/src/map/SidePanel.tsx` — selection side panel (added v0.2.0)
- `app/src/map/FilterChips.tsx` — kind/date/name filter chips (added v0.2.0)
- `app/src/map/ExportMenu.tsx` — export format picker + per-format generators (added v0.2.0)
- `app/src/views/Documents.tsx` — saved documents index view (added v0.2.0)
- `app/src/data/models/MapLayer.ts` — MapLayer data shape (extended v0.2.0)
- `app/src/data/models/Document.ts` — Document data shape (added v0.2.0)
- `app/src/data/models/Annotation.ts` — Annotation data shape (added v0.2.0)
- `app/src/data/persistence/idb.ts` — IDB store registration (DB v19, adds `documents` and `annotations` stores)

**Drift note (v0.2.0, resolving pre-existing drift):** v0.1.0 of this codex declared "SVG-based renderer, no Leaflet dependency". The runtime has used Leaflet since the original v1 implementation. v0.2.0 explicitly lifts the SVG-only constraint and codifies Leaflet as the runtime renderer. No runtime change is required for this correction — only the doctrine. Going forward, Leaflet is the authoritative renderer; if a future revision migrates away from Leaflet, this codex must be amended with a new fragment.

## C2 — State Machine

### Drawing state (point/line/polygon)
```
idle → drawing → pending → save | auto-commit-on-tool-switch | cancel
```

- `idle`: no tool armed, no pending shape
- `drawing`: tool armed, operator can click on map to add vertices
- `pending`: shape has vertices; save bar is open
- `save`: operator clicks Save / presses Enter — layer persisted
- `auto-commit-on-tool-switch`: operator clicks a different tool — previous pending shape is committed silently with name (or "Untitled <kind>")
- `cancel`: operator clicks Cancel / presses Esc — pending shape discarded

### Selection state (v0.2.0)
```
none → selected → none
```

- `none`: no layer selected
- `selected`: a single layer is selected; side panel is open; the shape on the map and the row in the layer list are both highlighted
- Transition `selected → none` on: Esc press, click on empty map area, click on a different layer (replaces selection), click on the selected row a second time

### Vertex-edit state (v0.2.0)
```
idle → dragging → committed | cancelled
```

- `idle`: no vertex drag in progress
- `dragging`: operator has mouse-down on a vertex circle
- `committed`: operator releases mouse outside any cancel target → new coordinate persisted, version bumped
- `cancelled`: operator presses Esc during drag → coordinate restored to pre-drag value

## C3 — Side Effect List

- IDB write to `map_layers` (DB v19) on save / vertex commit / rename / delete / linked-tree change / kind change
- IDB write to `documents` (DB v19) on every export
- IDB write to `annotations` (DB v19) on annotation store / status change (manual only in v0.2.0)
- React state update on selection change (shared between map and layer list)
- React state update on filter change (re-derives visible layer set; no IDB write)
- Map viewport change (pan/zoom) on selection; no IDB write

## C4 — Input Validation Contract

- Drawing:
  - At least 1 point required to save
  - Line requires ≥ 2 points to save
  - Polygon requires ≥ 3 points to save
  - Geometry type matches count (validated at save time)
- Selection:
  - Selected layer must exist in `map_layers` store; stale selection cleared on read miss
- Filters:
  - Date range "Custom" requires valid ISO start/end
  - Name substring is case-insensitive, trimmed
- Export:
  - PDF + GeoJSON require ≥ 1 layer in selection
  - Title block fields validated for non-empty strings where present

## C5 — Failure Modes

- No project → save disabled (v0.1.0, retained)
- Vertex drag outside map bounds → drag commits with clamped coordinate (no layer corruption)
- Document export with zero layers → export disabled, user prompted to select layers first
- Annotation store with missing parent document → orphan annotation rejected at write time
- Stale selection (layer deleted out from under panel) → panel auto-closes, state returns to `none`

## C6 — User Simulation List

1. Open `/map` → see layers for current project, no selection, all filters empty
2. Click Point → drawing state armed → click map → pending point → save bar shows "Save (1 pt)"
3. Type name + Enter → save committed, layer appears in list
4. Click Line → previous pending point (if any) auto-committed as "Untitled point"; line tool armed
5. Click 3 places on map → pending line, save bar shows "Save (3 pts)"
6. Click Polygon → previous line auto-committed; polygon tool armed
7. Click 3 places on map → pending polygon, save bar shows "Save (3 pts)"
8. Click on a shape in the layer list → that layer is selected, side panel opens, map zooms to it
9. Click on a different shape on the map → previous selection cleared, new layer selected
10. Press Esc → selection cleared, side panel closes
11. Click on a vertex of a line → drag → release → new coord persisted, version bumped
12. Press Esc mid-drag → drag cancelled, coord restored
13. Click "Rename" in side panel → name input shows → type → Enter → name updated, version bumped
14. Click "Delete" in side panel → confirm → layer removed, selection cleared
15. Apply filter chips: select "Point" only → list + map filtered to point layers only
16. Apply date filter "Today" → list + map filtered to layers created today
17. Type "boundary" in name filter → list + map filtered to layers with "boundary" in name
18. Clear all filters → list + map show all layers
19. Open Export Menu → select PDF + layers + extent + title → generate → PDF stored in Documents view
20. Open Documents view → see saved export with type, layers, timestamp, version numbers → re-download

## C7 — Reproducibility Test

Deterministic. All IDs are deterministic strings (no random per-session IDs). All version timestamps are ISO8601 UTC. Filter logic is pure (no side effects in filter evaluation).

## C8 — Shape Matrix

| Surface | Modes | Chips | States |
|---|---|---|---|
| Map canvas | browse, draw-point, draw-line, draw-polygon, drag-vertex | point, line, polygon, save, cancel, locate | idle, drawing, dragging |
| Layer list | browse | point, line, polygon, today, week, month, custom, name-search | filtered, unfiltered |
| Side panel | view, edit-name, edit-notes, edit-vertices | rename, delete, duplicate, export, edit-vertices | selected, editing |
| Filter chips | multi-select kind, single-select date, free-text name | (per chip) | active, inactive |
| Export menu | format picker, options form | pdf, png, svg, geojson, kml, print | idle, generating, done, error |
| Documents view | browse | (per document type) | empty, populated |

## C9 — State Shape Inventory

### C9.1 localStorage
None

### C9.2 In-memory
- `selectedLayerId: string | null` — shared selection state
- `filterKind: Set<'point' | 'line' | 'polygon'>` — active kind filters (empty set = no filter)
- `filterDate: 'today' | 'week' | 'month' | 'custom' | null` — active date filter (null = no filter)
- `filterDateRange: { start: ISO8601, end: ISO8601 } | null` — populated when filterDate === 'custom'
- `filterName: string` — substring search (empty = no filter)
- `pending: PendingShape | null` — drawing-in-progress
- `pendingKind: LayerKind | null` — kind of pending shape
- `pendingName: string` — typed name for pending shape
- `drawMode: LayerKind | null` — armed tool (separate from pendingKind because Point is one-shot)
- `vertexDrag: { layerId, vertexIndex, originalCoord } | null` — in-progress vertex drag
- `currentPath: coords` — retained from v0.1.0 for backward compat

### C9.3 IDB
- `map_layers` store (DB v19) — `MapLayer` records; extended fields: `version` (number, default 1), `syncState`, `syncedAt`, `serverVersion`
- `documents` store (DB v19, added v0.2.0) — `Document` records
- `annotations` store (DB v19, added v0.2.0) — `Annotation` records
- `layer_versions` store (DB v19, added v0.2.0) — `LayerVersion` ledger records (one entry per significant edit)

## C10 — Forward-Compat Fields

v0.2.0 adds the following fields to existing record shapes. They are write-empty, read-default. No behaviour reads or writes these fields in v0.2.0; they exist so a future cloud-sync doctrine pass can populate them without a schema migration.

- `MapLayer.syncState: 'local' | 'syncing' | 'synced' | 'conflict'` — default `'local'`
- `MapLayer.syncedAt: ISO8601 | null` — default `null`
- `MapLayer.serverVersion: number | null` — default `null`
- `Document.syncState: 'local' | 'syncing' | 'synced' | 'conflict'` — default `'local'`
- `Document.syncedAt: ISO8601 | null` — default `null`
- `Document.serverVersion: number | null` — default `null`

---

## END OF MAP CODEX (v0.2.0)