mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
173 lines
9.9 KiB
Markdown
173 lines
9.9 KiB
Markdown
# 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 `/<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`.
|