# 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 rebuild on an architecture adapted from its sibling project Syto. **`docs/spec/`** (sections 00–10) is the behavioral record: the code leads and the spec is kept rewritten to match — a user-facing change isn't done until its spec section describes it. 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/ # Behavioral specification (00–10) — the WHAT (record; code leads) ├── 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 follows code** — `docs/spec/` is the descriptive record of behavior; the code leads. When in doubt about existing behavior, read the spec. When you ship user-facing behavior, update the matching spec section in the same session; if spec and app disagree, the spec is stale — rewrite it deliberately, never drift silently. - **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 `/` (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`.