Files
astrolabe/AGENTS.md
T

173 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 0010) 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 (0010) — the WHAT (record; code leads)
├── architecture/ # Architecture playbook (0011) — 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 (M0M6)
└── 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`.