9.9 KiB
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 insrc/app/infrastructure/adapters (IndexedDB / localStorage / URL hash). The rest of the app never toucheswindow/indexedDBdirectly.- 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 reusessrc/coreand thechart-rendererservice 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/ 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
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/pushunless explicitly invited. - Verify — run
npm run typecheckandnpm testafter 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.apito drop unused languages also stripped every editor feature — seedocs/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-rangemeans the browser only downloads what a glyph needs anyway.)
- 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;
- 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-updatein-session first (flush unrecorded rationale), then/alignment— and/eng-councilreview 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 withdocs/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 underreference/) 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+10decide./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 becomedocs/architecture/rules and new/alignmentchecks./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.