Files
astrolabe/docs/IMPLEMENTATION-PLAN.md
T

14 KiB
Raw Blame History

Astrolabe — Incremental Implementation Plan

A spec-driven rebuild of Astrolabe on Syto's architecture. The authoritative behavioral contract is docs/spec/ (sections 0010). This document sequences the build into the quickest path to a usable MVP, then layers the rest.

Method per milestone: build core-first (portable, pure, tested) → wire UI → cover with tests → manual smoke check against the spec's acceptance points. "Test the Core, Trust the UI": high coverage on src/core/, lighter on components.


Architectural ground rules

These are decided and apply to every milestone. The how behind each is written up self-containedly in docs/architecture/ — read the matching doc before implementing.

  • src/core/ is portable — no browser APIs, no React, no Monaco. Pure spec operations (detection, profiling, reference resolution, fit transforms, validation, import normalization). This is what we test hardest and what could power a future headless renderer/CLI.
  • src/app/ holds React + Zustand UI. State lives in Zustand stores (useAppStore, plus per-feature stores); browser specifics live in src/app/infrastructure/ adapters (IndexedDB, localStorage, URL hash) so the rest of the app never touches window/indexedDB directly.
  • Modals via a registry + coordinator + shell (see Architecture 03), not ad-hoc conditional rendering.
  • CSS Modules + design tokens (styles/tokens.css); themes flip [data-theme]. Vega theme follows the UI theme.
  • Editor: Monaco, self-hosted from npm + raw monaco-editor API (not the CDN loader / @monaco-editor/react wrapper — decided; rationale in Architecture 08). Workers are wired explicitly via Vite ?worker. The Vega-Lite JSON-schema service is what gives autocomplete + validation; mine vega-editor for how it wires the schema.
  • No shared library with Syto. Patterns are copied/adapted, never imported.

Milestone map

# Milestone Outcome Spec
M0 Skeleton Repo builds, tests run, empty shell renders
M1 MVP core loop Author a Vega-Lite snippet, see it render live, it persists §02, §03AC, §04, §09A
M2 Editor robustness Draft/Published, validation, schema autocomplete, fit modes §03DE, §04, §07(editor)
M3 Datasets Named reusable data + reference resolution in preview §05, §03F, §09B
M4 Chart Builder No-JSON chart composition from a dataset §06
M5 Settings + Import/Export Preferences + workspace backup/transfer §07, §08, §09C
M6 Shell polish Resize/toggle panes, routing, shortcuts, toasts, a11y, offline §01, §10

MVP boundary = end of M1 (a genuinely usable single-user chart authoring loop). M2 makes it robust; M3M6 make it complete. Ship/dogfood after M1, iterate.


M0 · Skeleton (done)

Vite + React + Zustand + TypeScript + Vitest (happy-dom) + vite-plugin-pwa. src/coresrc/app split, useAppStore, design tokens, three-pane placeholder shell, first core module (format-detection) with tests.

Verified: npm run typecheck, npm test (14 passing), npm run build (PWA SW generated).


M1 · MVP core loop → the quickest usable Astrolabe

Goal: select/create a snippet, edit its spec JSON, watch a live Vega-Lite preview, and have it survive reload. Single source kind: inline-data specs only (datasets come in M3). No draft/published yet — edits save directly.

Core (src/core/)

  • snippet.ts — the Snippet type (spec §09A) + factory (createSnippet, default sample bar-chart template, auto-generated date/time name).
  • rendering.tsprepareSpecForRender(spec, { fitMode }) skeleton; in M1 it's near pass-through (reference resolution is a no-op until M3, fit-mode is M2). Establish the "transform a copy, never mutate stored spec" contract now.

Infrastructure (src/app/infrastructure/)

  • idb.ts — thin IndexedDB wrapper (open, get/put/delete/getAll by store). (see Architecture 02 · Persistence)
  • snippet-store.ts — persist snippets (object store snippets).

App

  • stores/SnippetStore.tsuseSnippetStore with snippets, activeSnippetId, selector-derived activeSnippet; load-on-startup; create/select/delete/update actions (debounced auto-save of edits, spec §03B). Seed one sample snippet on first run.
  • components/SnippetLibrary.tsx — list + "Create New" pinned item + select/delete.
  • components/SpecEditor.tsx — Monaco JSON editor bound to active snippet's spec; debounced write-back to the store. (Worker wiring via Vite ?worker imports — mine vega-editor's Monaco setup.)
  • components/LivePreview.tsx — render current spec via vega-embed (actions: false), debounced; clean empty pane when no/blank spec; basic error text.
  • Fill the three panes in App.tsx with these.

Tests (core-first)

  • snippet.test.ts — factory defaults, sample template validity, unique naming.
  • rendering.test.ts — copy-not-mutate invariant; pass-through shape.
  • A store test for create/select/delete/auto-save reducer logic (logic extracted from the component so it's testable without DOM).

Manual checks

  • Fresh load seeds a sample snippet that renders a bar chart.
  • Type in the editor → preview updates after the debounce; bad JSON → editor keeps working, preview shows an error, recovers when fixed.
  • Reload → snippets and selection persist.

M2 · Editor robustness

Goal: the editor becomes trustworthy — draft vs published, schema-aware assistance, and the fit-mode rendering contract.

Core

  • rendering.ts — implement fit-mode transform (Original/Width/Height/Full → Vega-Lite "container"), recursing into layered/concat/child specs (spec §04 Rendering Contract, step 2).
  • vega-lite-schema.ts — provide the Vega-Lite JSON schema for Monaco's validation/autocomplete (mine vega-editor for sourcing/versioning the schema).

App

  • Snippet gains spec (published) + draftSpec (working) per §09A; editing touches draftSpec only.
  • SpecEditor header: Draft/Published toggle; Publish (promotes draft, recomputes dataset refs — refs land in M3) + Revert (confirm dialog).
  • Library list item: draft-vs-published status indicator.
  • Monaco wired with the Vega-Lite schema → squiggles + autocomplete; inline error surface in the editor pane (§03E).
  • Preview Fit control (4 modes), persisted (previewFitMode).

Tests

  • Fit-mode transforms for each mode incl. nested specs; copy-not-mutate.
  • Draft/publish/revert reducer logic; "has unpublished changes" derivation.

Manual checks

  • Edit draft, see status flip to "draft"; Publish → status clears; Revert → draft restored with confirmation.
  • Invalid spec shows inline error; autocomplete suggests Vega-Lite properties.
  • Each fit mode resizes the chart as specified; choice survives reload.

M3 · Datasets

Goal: named, reusable data that snippets reference by name; preview resolves the reference.

Core

  • profiling.ts — row/column counts, column names, per-column type inference (number/text/date/boolean). (see Architecture 06 · Type Inference)
  • rendering.ts — implement dataset reference resolution (§04 Rendering Contract, step 1): {data:{name}} → inline values / raw text+format / URL+format, recursing into sub-specs; "dataset not found" error.
  • dataset.ts — Dataset type (§09B); name uniqueness helpers; rename-propagation into referencing specs. (see Architecture 07 · Naming & Relationships)

Infrastructure

  • dataset-store.ts — separate high-capacity IndexedDB store (§09E).

App

  • stores/DatasetStore.ts + Datasets modal (list/detail panes, create form, edit, delete, copy-reference) via the modal registry/coordinator.
  • Snippet datasetRefs maintained on publish; library shows dataset icon + Linked Datasets; dataset detail shows Linked Snippets (bidirectional name link, §09F).
  • Extract-to-Dataset flow from the editor (§03F).
  • URL-sourced datasets fetched at render time.

Tests

  • Reference resolution per source/format incl. nested; not-found error.
  • Profiling/type inference across mixed columns, nulls, booleans.
  • Rename propagation; name-uniqueness + import-style auto-suffix.

Manual checks

  • Create a dataset, reference it by name in a snippet → preview renders.
  • Extract inline data → spec rewritten to a reference, dataset appears, links show both ways.
  • Delete/rename a referenced dataset behaves per spec.

M4 · Chart Builder

Goal: no-JSON chart composition from a dataset → a new snippet.

Core

  • chart-builder.ts — pure spec assembler: (mark ∈ Bar/Line/Point/Area/Circle) + channels (X/Y/Color/Size) with field types (Quantitative/Nominal/Ordinal/Temporal)
    • optional width/height → complete Vega-Lite spec with tooltips + named data ref (§06 Output). Field-type defaults from inferred column type.

App

  • Chart Builder modal (config pane + live preview pane), launched from a selected dataset; default pre-population (first col→X, second→Y); validation (≥1 channel); Create Snippet → new linked snippet becomes active.

Tests

  • Spec assembly: mark/channel/type permutations, unmapped channels omitted, width/height inclusion, field-type derivation, validation gate.

Manual checks

  • Build a bar chart from a dataset in a few clicks; preview live-updates; Create → new snippet opens and renders.

M5 · Settings + Import/Export

Goal: preferences and whole-workspace backup/transfer.

Core

  • settings.ts — UserSettings shape + defaults + load-with-fallback (§07, §09C); unknown/missing values fall back silently.
  • import-normalize.ts — accept envelope / bare array / single snippet / foreign shapes; field mapping (content→spec, draft→draftSpec, createdAt→created); tag "imported"; merge rules (append, id-collision reassign, dataset-name auto-suffix, datasets-before-snippets) (§08).
  • export-envelope.ts — build the {version, exportedAt, exportedBy, snippets, datasets} envelope.

Infrastructure

  • settings-store.ts (localStorage); ux-prefs for sort + panel layout (§09D).

App

  • Settings modal (Appearance/Editor/Performance/Formatting), Apply/Cancel/Reset, dirty indicator; wire render-debounce + theme + date-format through to the app.
  • Header Import/Export (direct file dialog / download, no modal).
  • Date formatting util (smart/iso/custom) used by the library list.

Tests

  • Import normalization across all accepted shapes; merge/collision/rename logic; quota-overage messaging path. Envelope round-trip (export→import idempotence).
  • Settings load-with-fallback for partial/unknown records.

Manual checks

  • Change theme/debounce/date-format → takes effect; Cancel reverts; Reset confirms.
  • Export → reimport into a populated workspace merges without overwrite; renames reported.

M6 · Shell polish & non-functional

Goal: the workspace feels finished and meets §10.

  • Panes: drag-resize handles with min widths; per-pane show/hide toggle strip; widths + visibility persist (§01A, §09D).
  • Routing: URL hash view-state (#snippet-<id>, #datasets/...) with Back/Forward; restore on load (§01E). (see Architecture 04 · Routing & Events)
  • Shortcuts: Cmd/Ctrl+Shift+N / +K / +S / +, / Esc via a single key router (§01D). (see Architecture 04 · Routing & Events)
  • Toasts: success/error/warning/info, stacking, auto-dismiss, reduced-motion (§01F).
  • Storage monitor for the snippet tier (§02).
  • A11y: modal focus trap + return, labelled icon buttons, contrast in both themes (§10).
  • Offline/installable: verify the SW + manifest give a working offline + installed app.
  • About & Privacy and Donate modals.

Manual checks: keyboard-only run-through; reload restores view from URL; offline reload works; install as standalone; reduced-motion honored.


Cross-cutting, do-as-you-go

  • i18n (optional, deferred): if translation is wanted, split a portable i18n registry (no React) from the app-layer bindings, mirroring the coreapp boundary. M1M6 can ship English-only with date formatting locale-aware (§10). Don't retrofit later if avoidable — keep user-facing strings centralized from M1.
  • Versioning: simplified semver 0.x.y, package.json__APP_VERSION__ (already wired). Bump per shippable milestone.
  • Docs trio: keep SOUL.md / AGENTS.md / CLAUDE.md current as the app grows.

Architecture reference

The how behind each milestone is documented self-containedly in docs/architecture/ — no external repo needed:

Need Doc
Zustand stores, selector derivations, debounced auto-save 01 · State & Stores
IndexedDB wrapper, lazy loading, migrations, localStorage prefs, storage tiers 02 · Persistence
Modal registry + coordinator + shell, unsaved-change detection, focus trap 03 · Modal System
URL hash view-state, keyboard routing, interactive-context detection 04 · Routing & Events
vega-embed integration, theming, debounced preview, error display 05 · Rendering, Theming & Preview
Column type inference + dataset profiling 06 · Type Inference & Profiling
Unique names + import auto-suffix, snippet↔dataset links, rename propagation 07 · Naming & Relationships
Monaco setup, Vega-Lite schema service, editor patterns mined from vega/editor 08 · Vega Editor Techniques