# Astrolabe Project

> **Purpose**: Onboarding document for AI agents (and humans) working on Astrolabe.

---

## Project Overview

**Astrolabe** is a browser-based snippet manager for Vega-Lite visualizations. A user keeps
a local library of **snippets** (saved Vega-Lite specs), edits each as JSON with live
validation and a live chart preview, and reuses **datasets** across many snippets. Fully
local, offline-capable, no account.

It is a **spec-driven rebuild** on an architecture adapted from its sibling project Syto.
The authoritative behavioral contract is **`docs/spec/`** (sections 00–10). Implement _to
the spec_; do not port legacy code.

### Technical Stack

| Layer   | Technology                                                                           |
| ------- | ------------------------------------------------------------------------------------ |
| Build   | Vite, TypeScript, Vitest (happy-dom)                                                 |
| UI      | React, Zustand, CSS Modules                                                          |
| Editor  | Monaco (JSON + Vega-Lite schema service)                                             |
| Charts  | Vega-Lite + vega-embed                                                               |
| Storage | IndexedDB (snippets, datasets), localStorage (settings/prefs), URL hash (view state) |
| Offline | `vite-plugin-pwa` (Workbox), `registerType: 'prompt'`                                |

---

## Architecture (non-negotiable)

- **`src/core/` is portable** — no browser APIs, no React, no Monaco. All spec operations
  live here and are tested hardest.
- **`src/app/`** — React + Zustand UI. State in Zustand **stores**; browser specifics in
  **`src/app/infrastructure/`** adapters (IndexedDB / localStorage / URL hash). The rest of
  the app never touches `window`/`indexedDB` directly.
- **Modals** go through a registry + coordinator + shell, not ad-hoc rendering.
- **CSS Modules + design tokens** (`styles/tokens.css`); themes flip `[data-theme]`.
- **No shared library with Syto** — patterns are adapted, never imported.
- **The app is served at `/app/`; `/` is a standalone marketing landing** (`src/landing/`).
  Multi-page Vite build — `index.html` → the landing, `app/index.html` → the app (hash
  view-state routing is unchanged by the base path). The landing reuses `src/core` and the
  `chart-renderer` service only — never stores, orchestration, modals, or components — and
  lazy-loads Vega, so `/` stays light. The PWA service worker and manifest are scoped to
  `/app/`, leaving the landing uncontrolled and always-fresh. **`/learn/` is a second such
  entry** (`src/learn/`) — the markdown-authored deep-dive section, under the same rules
  (see architecture 11).

See [`docs/architecture/`](docs/architecture/00-overview.md) for the patterns behind each
layer (state, persistence, modals, routing, rendering, inference, relationships) and
`docs/IMPLEMENTATION-PLAN.md` for the milestone sequence. Both are **self-contained** — no
external repo is needed to work from them.

---

## Directory Structure

```
index.html           # Landing entry (served at /)
app/index.html       # App entry (served at /app/)
learn/index.html     # Learning-section entry (served at /learn/)
src/
├── main.tsx         # App bootstrap (font wiring, startup, render)
├── landing/         # Marketing landing at / — standalone page; reuses core + chart-renderer
├── learn/           # /learn/ deep-dive — markdown lessons + the SpecProgression engine (arch 11)
├── core/            # Portable spec engine (no browser/React/Monaco)
├── app/
│   ├── components/  # React UI (CSS Modules co-located)
│   ├── hooks/       # Reusable React hooks (e.g. useFocusTrap — shared by overlays)
│   ├── stores/      # Zustand stores (incl. ConfirmStore — in-app confirm dialogs)
│   ├── services/    # Business logic
│   ├── orchestration/   # Startup wiring: store↔adapter subscribers (persistence)
│   └── infrastructure/  # IndexedDB, localStorage, Monaco, settings adapters
styles/              # Global CSS (tokens, base)
docs/
├── spec/            # Authoritative behavioral specification (00–10) — the WHAT
├── architecture/    # Architecture playbook (00–11) — the HOW (self-contained)
│   └── visual-specimen.html  # Standalone token sandbox + reusable-primitive catalog
├── exploration/     # Point-in-time records (research, reviews, scope memos) — not maintained
├── IMPLEMENTATION-PLAN.md    # Milestone sequence (M0–M6)
└── WHY-A-SEPARATE-REBUILD.md
```

---

## Development

```bash
npm run dev        # Dev server
npm run build      # Typecheck + production build (+ PWA)
npm run typecheck  # tsc --noEmit
npm test           # Vitest (run once)
npm run test:watch # Vitest watch
npm run format     # Prettier
```

### AI Developer Protocol

- **No git on your own initiative** — don't `add`/`commit`/`push` unless explicitly invited.
- **Verify** — run `npm run typecheck` and `npm test` after changes. A green build and a
  smaller bundle prove nothing about behavior: when a change touches what the user sees or
  does, confirm it by exercising the feature, not by the compiler alone.
- **Trim content, not capability** — when narrowing a third-party import or build to cut
  size, remove optional _content_, never the library's _features_. The smallest/lowest-level
  entry point is seldom the right one — it often drops capabilities you meant to keep. Prefer
  the entry that excludes the unwanted content while retaining behavior, and validate the
  behavior survived. (This bit us once: importing Monaco's `editor.api` to drop unused
  languages also stripped every editor feature — see `docs/architecture/08`.)
  - The line isn't "content vs. capability" by category — it's **"does any real user path
    depend on this?"** Safe to drop: data no user path exercises (a date formatter's unused
    locale tables, an icon set you never render, themes you don't ship). _Not_ safe, even
    though it looks like "content": **human-language coverage** — font script subsets,
    translatable strings — which is capability the moment the app is meant to be usable in
    that language. Treat dropping it like dropping a feature. (This bit us a second time:
    trimming IBM Plex to the latin subsets dropped Cyrillic — capability for an
    internationally-usable app. We ship every script subset and precache them for offline;
    `unicode-range` means the browser only downloads what a glyph needs anyway.)
- **Spec is the contract** — when in doubt, read `docs/spec/`. If the spec is wrong or
  silent, raise it; change the spec deliberately rather than drifting from it.
- **Core-first** — for each feature, build the pure `src/core/` logic with tests before UI.
- **Session wrap-up** — when the user signals the session is wrapping, run the review
  pass before any commit: `/doc-update` in-session first (flush unrecorded rationale),
  then `/alignment` — and `/eng-council` review when the diff is structural — each as a
  **clean-context subagent** that reports back. Full protocol in CLAUDE.md.

### Project skills

Invoke with `/<name>` (defined in `.claude/skills/`):

- **`/alignment`** — review uncommitted/staged changes for quality, test coverage, and
  alignment with `docs/spec/` + `docs/architecture/`. Fixes issues directly and leaves
  `// TODO:` breadcrumbs at code sites for out-of-scope observations.
- **`/doc-update`** — capture session-discovered knowledge gaps into the right doc layer
  (`docs/spec/` for behavior, `docs/architecture/` for patterns).
- **`/council`** — consult the design council (Carbon, GOV.UK, WAI-ARIA APG, Nielsen
  Norman, cloned under `reference/`) before a user-facing interaction/content/a11y
  decision. Auto-fires on error/empty-state copy and new interactive-widget keyboard/focus
  work; on demand otherwise. It advises; `docs/architecture/09`+`10` decide.
- **`/eng-council`** — convene the engineering council: an evidence-grounded review of
  codebase structure, consistency, layering altitude, and subtraction (what to delete).
  Modes: whole-codebase sweep, refactor review, new-functionality review, and a pre-build
  consult (only the consult auto-fires — before building a new instance of a kind).
  Recurring findings become `docs/architecture/` rules and new `/alignment` checks.
- **`/release`** — bump version, update the changelog, prepare a git tag.

### Versioning

Simplified semver `0.x.y` (pre-1.0): minor for features/behavior, patch for fixes. Single
source of truth is `version` in `package.json`, injected as `__APP_VERSION__`.

### Testing Philosophy

High coverage on `src/core/` (parsing, detection, profiling, reference resolution, fit
transforms, import normalization). Lighter on components. Extract testable logic out of
components into core/stores where practical.

Don't test static presentational components — copy, markup, and links with no logic behind
them. Content assertions ("renders the heading X", "this link exists") are change-detectors:
they break on intentional copy edits and catch no real bug. Test the logic a component
carries — platform branches, state transitions, config-path writes, render serialization —
not the strings it renders; if that logic is worth guarding, lift it into core/stores and
test it there.

Component tests (happy-dom) share a harness shape: `createRoot` + `act` with
`IS_REACT_ACT_ENVIRONMENT = true` set at module level, stores reset in `beforeEach`, and
`vi.mock('../services/chart-renderer', …)` for anything that embeds a chart (vega-embed is
integration-heavy; a resolved no-op handle suffices) — see any `components/*.test.tsx`.
Infrastructure tests that touch IndexedDB run against `fake-indexeddb`.
