From 464c8e8d8b4e0080119b0aad5405392c263db0c0 Mon Sep 17 00:00:00 2001 From: Alexander Rulkens Date: Mon, 8 Jun 2026 01:06:41 +0200 Subject: [PATCH 01/27] docs(spec): poi-free codebase design Dissolve PoiCategory into registry-derived label/marker axes, fold #poi= into #focus=, and rename the Concept-A poi holdouts to structure. Captures the 2026-06-08 entanglement-radar findings + brainstorm decisions. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-08-poi-free-codebase-design.md | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-08-poi-free-codebase-design.md diff --git a/docs/superpowers/specs/2026-06-08-poi-free-codebase-design.md b/docs/superpowers/specs/2026-06-08-poi-free-codebase-design.md new file mode 100644 index 00000000..35909694 --- /dev/null +++ b/docs/superpowers/specs/2026-06-08-poi-free-codebase-design.md @@ -0,0 +1,302 @@ +# Make the codebase `poi`-free — design + +**Date:** 2026-06-08 +**Status:** design (awaiting plan) +**Worktree/branch:** `worktree-make-codebase-poi-free` +**Backlog item:** "Make the codebase `poi`-free (`poi*` → `structure*`)" (Priority: high) + +## Motivation + +The data layer is already `StructureRecord` / `StructureStore` and the InfoCard +family is `StructureDetailCard` / `CompactStructureCard`. The legacy `poi` +("point of interest") vocabulary survives at the engine / identity / URL edges. +An `entanglement-radar` pass (2026-06-08) found the wart is sharper than a +straight rename: **`poi` is one name for two genuinely different concepts**, and +a blind `poi → structure` rename would silently merge them. + +### Concept A — `poi` already _means_ "structure" + +These sites are about structures specifically; renaming to `structure` makes the +name **more** accurate: + +- `isPoi(t): t is StructureRecord` — the predicate literally returns `StructureRecord`. +- `resolvePoiFromPick → StructureRecord`, `PickPoiInput`, `poiIndex` (ring-pick decode). +- `poiUrl.ts` / `parsePoiHash` / `poiIdToHash` / `#poi=` — encodes `StructureRecord.id`. +- `FocusState.poiId` — the focused structure id (membership cache key). +- `selectedPoi` engine param (`selectionRingPass`, `EngineSubsystemHandles`). +- `STRUCTURE_POI_STYLES` / `structurePoiStyles.ts` — already keyed by `StructureCategory`. + +### Concept B — `poi` means "the focusable category superset" + +`PoiCategory = StructureCategory | 'famousGalaxy'` is **not** a synonym for +`structure` — it is a strict superset including `famousGalaxy`. A mechanical +`Poi → Structure` rename here is a type collision (`StructureCategory` exists) +**and** a semantic lie (drops `famousGalaxy`). This type keys the SettingsPanel +**label/marker visibility toggles**. + +Tracing why `famousGalaxy` is a member exposes the actual braid +(`engine.ts:261-323`): + +- **Labels:** `famousGalaxy` _does_ bear labels (routed to the `galaxyNames` + fade layer); structures bear labels (routed to the structure label layer). +- **Markers:** `famousGalaxy` has **no** ring/halo marker — the marker toggle + for it is a dead no-op; only structures bear markers. + +So the two real axes are different sets: + +- **label-bearing** = `{famousGalaxy, cluster, supercluster, void, group}` +- **marker-bearing** = `{cluster, supercluster, void, group}` (structures only) + +The current code crushes both into one `PoiCategory` union and pays for it with +`if (category === 'famousGalaxy')` branches and a **phantom `famousGalaxy` key** +in the marker visibility record. "Having a label / having a marker" is a +_property of the thing_, not a category that singles out famous galaxies — which +is exactly what makes `famousGalaxy` look out of place in the union. + +## Goals + +1. No `poi`/`Poi`/`POI` identifier, filename, or doc-comment survives in `src/` + or the mirrored `tests/`. (`tools/` is already clean.) +2. `PoiCategory` and the `famousGalaxy` special-casing **dissolve** into + registry-derived data — `famousGalaxy` becomes "just a row" with capability + flags, not a hand-listed exception. +3. The `#poi=` deep-link folds entirely into `#focus=`; the `poiUrl` codec is + deleted. + +## Non-goals + +- No back-compat for old `#poi=` shared links — they intentionally fall through + to "no deep link" (decided 2026-06-08). +- No change to the structure data layer, `.ccat` format, or the `Source.Cluster` + /`'cluster'` category vocabulary (already `Structure*`-clean). +- No render/visual behaviour change. The label & marker toggles behave + identically; only the marker record sheds its dead `famousGalaxy` key. + +--- + +## Part A — Dissolve `PoiCategory` into two registry-derived axes + +### A1. Registry capability flags + +Add to `SOURCE_REGISTRY` rows (via `SourceEntryBase` or per-variant): + +```ts +// SourceEntryBase additions +/** True if this category carries toggleable on-screen text labels. */ +readonly bearsLabel: boolean; +/** True if this category carries a ring/halo marker (structures only today). */ +readonly bearsMarker: boolean; +/** + * Which fade layer this category's labels live on. Present iff bearsLabel. + * - 'galaxyNames' — the shared galaxy-name layer (famousGalaxy) + * - 'structure' — the per-structure-category label layer + */ +readonly labelLayer?: 'galaxyNames' | 'structure'; +``` + +Settings per row: + +| row | bearsLabel | bearsMarker | labelLayer | +| ----------------------------------- | ---------- | ----------- | ------------- | +| famousGalaxy | true | false | 'galaxyNames' | +| cluster | true | true | 'structure' | +| supercluster | true | true | 'structure' | +| void | true | true | 'structure' | +| group | true | true | 'structure' | +| sdss/glade/2mrs/milliquas/synthetic | false | false | — | + +### A2. Display metadata folded into the registry + +The standalone `data/poiCategoryInfo.ts` (`POI_CATEGORY_INFO`, `PoiCategoryInfo`) +is **removed**. Its three fields move onto the label-bearing registry rows. The +existing `SourceEntryBase.label` already holds the short UI name +(`'Cluster'`, `'Famous'`), so only the longer + plural forms are new: + +```ts +// SourceEntryBase additions (present iff bearsLabel) +/** Long form for detail surfaces ("Galaxy Cluster"). */ +readonly detailLabel?: string; +/** Plural for list/toggle headers ("Clusters"). */ +readonly plural?: string; +``` + +Mapping from the old table: old `shortLabel` ≡ existing `label`; old `label` +→ `detailLabel`; old `plural` → `plural`. (Note `famousGalaxy`'s existing +registry `label` is `'Famous'` but its old `shortLabel` was `'Galaxy'` — the +detail/short copy is reconciled in the plan; default to the old display-table +strings since those are what the UI renders today.) + +A derived accessor (e.g. `categoryDisplayInfo(id)` or a derived +`CATEGORY_DISPLAY_INFO` map built from the registry) replaces every +`POI_CATEGORY_INFO[cat]` read. + +### A3. Derived category sets + types + +Derive from the registry (companion to `structureCategories.ts`): + +```ts +// data/labelCategories.ts (new) — label-bearing rows, registry order +export const LABEL_CATEGORIES = /* registry rows where bearsLabel */; +export type LabelCategory = (typeof LABEL_CATEGORIES)[number]; +// = 'famousGalaxy' | 'cluster' | 'supercluster' | 'void' | 'group' +``` + +- `LabelCategory` **replaces `PoiCategory`** (honest name; still contains + `famousGalaxy` because famous galaxies genuinely _are_ the labelled galaxies — + that is data, not a special case). `@types/engine/data/PoiCategory.d.ts` is + deleted. +- The **marker** axis narrows to `StructureCategory` (markers are structure-only). + +### A4. Visibility records re-typed + +- `labelCategoryVisibility: Readonly>` +- `markerCategoryVisibility: Readonly>` + — **drops the phantom `famousGalaxy` key.** + +Touches `EngineSettingsState`, `UseEngineSettingsState`, `EngineCallbacks`, +`SettingsCallbackSeed`, `Selection`, `EngineLabelsHandle`, `useEngineSettings`, +`SettingsPanel`, `engine.ts` defaults, the 4 test fixtures. The hand-written +`{ famousGalaxy: true, cluster: true, ... }` default literals derive from the +category sets instead (also retires the backlog's "copy-pasted 8× +DEFAULT_CATEGORY_VISIBILITY" sub-item for these records). + +### A5. Setters de-special-cased + +- `setCategoryMarkerVisible(category: StructureCategory, …)` — the + `if (category !== 'famousGalaxy')` no-op guard is **deleted**. +- `setCategoryLabelVisible(category: LabelCategory, …)` — fires the fade on the + layer named by the row's `labelLayer` field (A1), replacing the + `if (category === 'famousGalaxy')` branch. `galaxyNames` → fade + `{kind:'labelLayer', layer:'galaxyNames'}` (plus `setFamousLabelsVisible`); + `structure` → fade `{kind:'labelLayer', layer:'', category}`. + +--- + +## Part B — Fold `#poi=` into `#focus=` + +`camera.focusOn()` is already unified (accepts `GalaxyInfo | StructureRecord`), +so only the URL codec layer is split. + +### B1. Single codec + +- **Delete** `services/url/poiUrl.ts` (`parsePoiHash`, `poiIdToHash`) and its + test. +- `FocusTarget` gains a structure variant: + + ```ts + export type FocusTarget = + | { kind: 'famous'; id: string } + | { kind: 'pgc'; pgc: bigint } + | { kind: 'sdss'; objID: bigint } + | { kind: 'pos'; raDeg: number; decDeg: number } + | { kind: 'structure'; id: string }; // NEW + ``` + +- `parseFocusHash`: a `focus=` body whose id begins with a structure-category + prefix (`cluster-` / `supercluster-` / `void-` / `group-`) parses to + `{ kind: 'structure', id }`; otherwise the existing galaxy ladder is unchanged. + (Galaxy famous-ids never use those prefixes, so no collision.) +- `selectionToFocusId`: for a structure selection, return `info.id` verbatim + (structure ids are already curated + stable, e.g. `cluster-virgo-m87`); the + galaxy ladder is unchanged. + +### B2. `hasDeepLink` + +Drop the `#poi=` branch. The `#focus=` branch now also covers structures. + +### B3. `useUrlSync` + +Keep the **two pending slots** — galaxy resolution is async against loaded +catalogs; structure resolution is a synchronous table lookup. They are genuinely +different resolution sources and must not be complected. The fold is purely at +the codec/parse-routing level: + +- `initialPendingFromHash` parses one `#focus=` hash; a `kind:'structure'` + result routes to the structure slot, everything else to the galaxy slot. The + second parser call (`parsePoiHash`) is removed. +- `InitialPending`'s `{ kind: 'poi'; poiId }` → `{ kind: 'structure'; id }`. +- Slot/state rename: `pendingPoiId` → `pendingStructureId`. +- `computeDesiredHash`: a structure target writes `focus=` (via + `selectionToFocusId`), not `poi=`. + +--- + +## Part C — Mechanical `poi → structure` renames + +All sites where `poi` already means "structure": + +| Now | Becomes | +| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `services/engine/isPoi.ts` `isPoi()` | `isStructure.ts` `isStructure()` | +| `resolvePoiFromPick.ts`, `resolvePoiFromPick`, `PickPoiInput`, `poiIndex` | `resolveStructureFromPick.ts`, `resolveStructureFromPick`, `PickStructureInput`, `structureIndex` | +| `structurePoiStyles.ts`, `STRUCTURE_POI_STYLES` | `structureMarkerStyles.ts`, `STRUCTURE_MARKER_STYLES` | +| `FocusState.poiId` | `FocusState.structureId` | +| `FocusState.category: 'cluster'\|'supercluster'\|'void'` | `StructureCategory` (also picks up the missing `group`) | +| `selectedPoi` param (`selectionRingPass`, `EngineSubsystemHandles`, `engine.ts`) | `selectedStructure` | +| `structureMarkerRenderer` `poiIndex` packing / `StructureMarkerRenderer` `poiIndex` | `structureIndex` | +| `produceStructureMarkers` `poiIndex` | `structureIndex` | +| `CreateClickResolverInput` `poiIndex` | `structureIndex` | + +Plus all `poi`/`POI` in doc-comments (`commitGalaxyFocus`, `commitStructureFocus`, +`clearAll`, `selectionSubsystem`, `buildStaticAnchorStructures`, +`structureMembership`, `assetWiring`, `FocusState`, …). + +Test files renamed to match: `isPoi.test.ts` → `isStructure.test.ts`, +`resolvePoiFromPick.test.ts` → `resolveStructureFromPick.test.ts`, +`wireInput.poi.test.ts` → `wireInput.structure.test.ts`, +`pickRenderer.poi.test.ts` → `pickRenderer.structure.test.ts`, +`EngineCameraHandle.poi.test.ts` → `EngineCameraHandle.structure.test.ts`, +`poiCategories.test.ts` → `labelCategories.test.ts` (re-scoped to the derived +sets). `poiUrl.test.ts` is **deleted**; its structure-id round-trip cases move +into `focusUrl.test.ts`. + +--- + +## Type changes (contract) + +```ts +// @types/engine/data/LabelCategory.d.ts (replaces PoiCategory.d.ts) +export type LabelCategory = (typeof LABEL_CATEGORIES)[number]; + +// @types/camera/FocusTarget.d.ts — add the structure variant (B1) + +// @types/engine/state/FocusState.d.ts +readonly structureId: string; // was poiId +readonly category: StructureCategory; // was 'cluster' | 'supercluster' | 'void' + +// @types/settings/EngineSettingsState.d.ts +readonly labelCategoryVisibility: Readonly>; +readonly markerCategoryVisibility: Readonly>; + +// @types/data/SourceEntryBase.d.ts — add bearsLabel, bearsMarker, labelLayer?, +// detailLabel?, plural? (A1, A2) +``` + +## Testing + +- The rename is type-driven: a missed Concept-A/B site won't compile. + `npm run typecheck` + the full `npm test` suite are the safety net. +- New `focusUrl` tests: `#focus=cluster-virgo-m87` ↔ `{kind:'structure'}` + round-trip; each structure-category prefix; galaxy ladder regression + unaffected; a structure id is never mis-parsed as a famous id. +- New test: a `#poi=…` hash now yields no deep link (documents the intentional + break) — added to `hasDeepLink` / `focusUrl` tests. +- New `labelCategories` test: `LABEL_CATEGORIES` / `MARKER_CATEGORIES` derive + the expected sets from the registry; the marker set excludes `famousGalaxy`. +- `setCategoryMarkerVisible` / `setCategoryLabelVisible` tests updated for the + narrowed types and the `labelLayer`-driven routing. + +## Execution + +Per project workflow: `subagent-driven-development`. Edits delegated to +background implementer subagents; the main thread runs `npm test` / +`typecheck`, prettiers touched files, and commits. Branch + PR (squash-merge). +No data rebuild or R2 sync — this is code-only (the `cluster*→structure*` +artifact rename from #280 already handled the deploy side). + +## Out of scope (follow-ups) + +- The broader `STRUCTURE_CATEGORY_META` DRY consolidation beyond the display-info + fold done here (the `hasBulkCatalog` flag, the `POI_CATEGORIES_WITH_MARKERS` + derivation) — see `docs/BACKLOG.md`. +- Promoting the Milky Way to a first-class `Source` — separate backlog item. From 1413f145058c618501e89fc9ba7e7a0c38aaafc3 Mon Sep 17 00:00:00 2001 From: Alexander Rulkens Date: Mon, 8 Jun 2026 01:59:18 +0200 Subject: [PATCH 02/27] docs(plan): poi-free codebase implementation plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase A (dissolve PoiCategory → registry-derived label/marker axes), Phase B (#poi= → #focus= fold), Phase C (mechanical poi→structure renames). 20 TDD tasks; rides the same PR as the implementation. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plans/2026-06-08-poi-free-codebase.md | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-08-poi-free-codebase.md diff --git a/docs/superpowers/plans/2026-06-08-poi-free-codebase.md b/docs/superpowers/plans/2026-06-08-poi-free-codebase.md new file mode 100644 index 00000000..d996b814 --- /dev/null +++ b/docs/superpowers/plans/2026-06-08-poi-free-codebase.md @@ -0,0 +1,338 @@ +# Make the codebase `poi`-free — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Remove all `poi`/`Poi`/`POI` vocabulary from `src/` + mirrored `tests/`, dissolving the two distinct concepts it hid (the label/marker category superset, and "structure") into honest, registry-derived names. + +**Architecture:** Three phases. **A** dissolves `PoiCategory` into registry-derived capability flags (`bearsLabel`/`bearsMarker`/`labelLayer`) + a derived `LabelCategory` type, narrows the marker axis to `StructureCategory`, folds the display-metadata table into `SOURCE_REGISTRY`, and de-special-cases the visibility setters. **B** folds the `#poi=` deep-link into `#focus=` (deletes the `poiUrl` codec, adds a `structure` variant to `FocusTarget`). **C** is the mechanical `poi → structure` rename of every site where `poi` already means "structure". Keep build + typecheck green at every commit. + +**Tech Stack:** TypeScript, Vite, React, Vitest. Source of truth: `docs/superpowers/specs/2026-06-08-poi-free-codebase-design.md`. Plan style: `docs/superpowers/conventions/plan-style.md` (contract code only — signatures + test names; no implementation bodies; cite files, don't paste). + +**Execution notes:** Executing subagents cannot run `npm`/`npx`; the main thread runs `npm test -- ` / `npm run typecheck` and commits. Stage specific paths only (never `git add -A`/`.`). Prettier only touched files. Commit as the user's git identity with a `Co-Authored-By: Claude Opus 4.8 (1M context) ` trailer. + +--- + +## Phase A — Dissolve `PoiCategory` into registry-derived axes + +### Task A1: Add capability + display fields to the registry rows + +**Files:** + +- Modify: `src/@types/data/SourceEntryBase.d.ts` +- Modify: `src/data/sources.ts` (every `SOURCE_ENTRIES` row) +- Test: `tests/data/sources.test.ts` + +**Contract** — add to `SourceEntryBase`: + +```ts +/** True if this category carries toggleable on-screen text labels. */ +readonly bearsLabel: boolean; +/** True if this category carries a ring/halo marker (structures only today). */ +readonly bearsMarker: boolean; +/** Fade layer the labels live on. Present iff bearsLabel. */ +readonly labelLayer?: 'galaxyNames' | 'structure'; +/** Long form for detail surfaces ("Galaxy Cluster"). Present iff bearsLabel. */ +readonly detailLabel?: string; +/** Plural for list/toggle headers ("Clusters"). Present iff bearsLabel. */ +readonly plural?: string; +``` + +Per-row values (see spec §A1/§A2 table): + +- `famousGalaxy`: `bearsLabel: true, bearsMarker: false, labelLayer: 'galaxyNames', detailLabel: 'Famous Galaxy', plural: 'Famous galaxies'`. (Keep its existing `label: 'Famous'`; the old display table's short form was `'Galaxy'` — see judgement note at end of plan.) +- `cluster/supercluster/void/group`: `bearsLabel: true, bearsMarker: true, labelLayer: 'structure'`, with `detailLabel`/`plural` copied from the old `POI_CATEGORY_INFO` rows (`src/data/poiCategoryInfo.ts:24-50`). +- `sdss/glade/2mrs/milliquas/synthetic`: `bearsLabel: false, bearsMarker: false` (omit `labelLayer`/`detailLabel`/`plural`). + +- [ ] Add tests: `famousGalaxy row bears a label but no marker` (assert `bearsLabel` true, `bearsMarker` false, `labelLayer === 'galaxyNames'`); `structure rows bear both a label and a marker` (loop cluster/supercluster/void/group); `bulk survey rows bear neither` (assert sdss/glade false/false). +- [ ] Run: `npm test -- sources` → new tests FAIL (fields absent). +- [ ] Add the five fields to `SourceEntryBase`; populate every row in `sources.ts`. +- [ ] Run: `npm test -- sources` and `npm run typecheck` → PASS. +- [ ] Commit (`git add src/@types/data/SourceEntryBase.d.ts src/data/sources.ts tests/data/sources.test.ts`). + +### Task A2: Derive `LABEL_CATEGORIES` + `LabelCategory` from the registry + +**Files:** + +- Create: `src/data/labelCategories.ts` +- Create: `src/@types/engine/data/LabelCategory.d.ts` +- Test: `tests/data/labelCategories.test.ts` + +**Contract** (mirror `src/data/structureCategories.ts` + `src/@types/engine/data/StructureCategory.d.ts`): + +```ts +// src/data/labelCategories.ts +export const LABEL_CATEGORIES = /* SOURCE_ENTRIES filtered by bearsLabel, mapped to .id, registry order */; +// src/@types/engine/data/LabelCategory.d.ts +export type LabelCategory = (typeof LABEL_CATEGORIES)[number]; +``` + +- [ ] Add tests in `tests/data/labelCategories.test.ts`: `LABEL_CATEGORIES contains famousGalaxy and the four structure categories` (assert sorted equals `['cluster','famousGalaxy','group','supercluster','void']`); `LABEL_CATEGORIES is a superset of STRUCTURE_CATEGORIES`; `LABEL_CATEGORIES excludes bulk surveys` (no `'sdss'`/`'glade'`). +- [ ] Run: `npm test -- labelCategories` → FAIL (module absent). +- [ ] Implement `labelCategories.ts` (filter `SOURCE_ENTRIES` by `bearsLabel`) and the `LabelCategory` type alias. +- [ ] Run: `npm test -- labelCategories` + `npm run typecheck` → PASS. +- [ ] Commit. + +### Task A3: Fold the display table into a registry-derived accessor + +**Files:** + +- Create: `src/data/categoryDisplayInfo.ts` (derived `CATEGORY_DISPLAY_INFO` keyed by `LabelCategory`, fields `{ label, shortLabel, plural }` sourced from the registry rows' `detailLabel`/`label`/`plural`) +- Modify: every `POI_CATEGORY_INFO` consumer (enumerate with `rg -l 'POI_CATEGORY_INFO|poiCategoryInfo|PoiCategoryInfo' src` — expect InfoCard / SettingsPanel family) +- Delete: `src/data/poiCategoryInfo.ts` +- Test: `tests/data/categoryDisplayInfo.test.ts` (rename from any `poiCategoryInfo` test if one exists; else new) + +**Contract:** + +```ts +export type CategoryDisplayInfo = { label: string; shortLabel: string; readonly plural: string }; +export const CATEGORY_DISPLAY_INFO: Readonly>; +``` + +Mapping: `shortLabel ← row.label`; `label ← row.detailLabel`; `plural ← row.plural`. + +- [ ] Add tests: `CATEGORY_DISPLAY_INFO has a row per LabelCategory`; `cluster renders 'Galaxy Cluster' / 'Cluster' / 'Clusters'`; `famousGalaxy renders the famous display copy`. +- [ ] Run → FAIL. +- [ ] Implement `categoryDisplayInfo.ts` deriving from the registry; repoint every `POI_CATEGORY_INFO[x]` read to `CATEGORY_DISPLAY_INFO[x]`; delete `poiCategoryInfo.ts`. +- [ ] Run the touched tests + `npm run typecheck` → PASS. Confirm `rg 'POI_CATEGORY_INFO|poiCategoryInfo' src` is empty. +- [ ] Commit. + +### Task A4: Rename `PoiCategory` → `LabelCategory`; retype the visibility records + +**Files (enumerate first with `rg -l 'PoiCategory' src`):** `@types/settings/EngineSettingsState.d.ts`, `@types/settings/UseEngineSettingsState.d.ts`, `@types/engine/EngineCallbacks.d.ts`, `@types/engine/wiring/SettingsCallbackSeed.d.ts`, `@types/engine/subsystems/Selection.d.ts`, `@types/engine/handles/EngineLabelsHandle.d.ts`, `@types/engine/UseEngineReturn.d.ts`, `src/hooks/useEngine.ts`, `src/hooks/useEngineSettings.ts`, `src/components/SettingsPanel/SettingsPanel.tsx`, `src/services/engine/engine.ts`, `src/services/engine/labelStyleOverride.ts`, `src/services/engine/wiring/assetWiring.ts`, the 4 visibility-default fixtures under `tests/`. + +- Delete: `src/@types/engine/data/PoiCategory.d.ts` + +**Contract changes:** + +```ts +readonly labelCategoryVisibility: Readonly>; +readonly markerCategoryVisibility: Readonly>; // narrowed — no famousGalaxy +// labelStyleOverride.ts: +export type LabelStyleOverrideTarget = 'youAreHere' | LabelCategory; +``` + +Default-visibility literals (`engine.ts:451-465`, `useEngineSettings.ts:174-188`) derive from `LABEL_CATEGORIES` / `STRUCTURE_CATEGORIES` instead of hand-listed `{ famousGalaxy: true, cluster: true, ... }`. + +- [ ] Update/add fixture + type tests first where assertions exist (`tests/@types/engineSettingsState.labelCategoryVisibility.test.ts`): assert `markerCategoryVisibility` has no `famousGalaxy` key and `labelCategoryVisibility` does. +- [ ] Run → FAIL. +- [ ] Replace `PoiCategory` with `LabelCategory` import-by-import; narrow the marker record key type to `StructureCategory`; derive the default literals from the category sets; delete `PoiCategory.d.ts`. +- [ ] Run `npm run typecheck` + the touched tests → PASS. `rg 'PoiCategory' src tests` empty. +- [ ] Commit. + +### Task A5: De-special-case the visibility setters + +**Files:** + +- Modify: `src/services/engine/engine.ts:261-323` (`setCategoryLabelVisible`, `setCategoryMarkerVisible`) +- Test: existing `tests/services/engine/setCategoryVisibleFade.test.ts`, `tests/services/engine/setSourceVisibleFade.test.ts` + +**Contract:** + +```ts +function setCategoryMarkerVisible(state, cb, category: StructureCategory, visible: boolean): void; +function setCategoryLabelVisible(state, cb, category: LabelCategory, visible: boolean): void; +``` + +- `setCategoryMarkerVisible`: drop the `if (category !== 'famousGalaxy')` guard entirely — every `StructureCategory` fires a `markerLayer` fade. +- `setCategoryLabelVisible`: replace the `if (category === 'famousGalaxy')` branch with a dispatch on the row's `labelLayer` field — `'galaxyNames'` fires the `galaxyNames` labelLayer fade (+ `setFamousLabelsVisible`); `'structure'` fires the structure labelLayer fade keyed by `category`. + +- [ ] Update tests: `setCategoryMarkerVisible fires a markerLayer fade for every structure category`; `setCategoryLabelVisible routes famousGalaxy to the galaxyNames layer`; `...routes a structure category to the structure label layer`. +- [ ] Run → FAIL (guards still present / wrong signatures). +- [ ] Rewrite both setters per the contract. +- [ ] Run the two test files + `npm run typecheck` → PASS. +- [ ] Commit. + +--- + +## Phase B — Fold `#poi=` into `#focus=` + +### Task B1: Add the `structure` variant to the focus codec + +**Files:** + +- Modify: `src/@types/camera/FocusTarget.d.ts` +- Modify: `src/services/url/focusUrl.ts` (`parseFocusHash`, `selectionToFocusId`) +- Test: `tests/services/url/focusUrl.test.ts` + +**Contract:** + +```ts +export type FocusTarget = + | { kind: 'famous'; id: string } + | { kind: 'pgc'; pgc: bigint } + | { kind: 'sdss'; objID: bigint } + | { kind: 'pos'; raDeg: number; decDeg: number } + | { kind: 'structure'; id: string }; // NEW +``` + +- `parseFocusHash`: a `focus=` body whose id starts with `cluster-` / `supercluster-` / `void-` / `group-` → `{ kind: 'structure', id }`; the existing pgc/sdss/pos/famous ladder is otherwise unchanged. Derive the prefix set from `STRUCTURE_CATEGORIES` (no hardcoded list). +- `selectionToFocusId(info: GalaxyInfo)` is galaxy-only; add a sibling path so a structure selection encodes to its `id` verbatim. (Check how `computeDesiredHash` calls it in B3 — the structure branch may bypass `selectionToFocusId` and use `record.id` directly. Pick whichever keeps the codec galaxy/structure split clean.) + +- [ ] Add tests: `parseFocusHash routes cluster-virgo-m87 to kind structure`; one per category prefix; `a famous id without a structure prefix still routes to kind famous`; `pgc-/sdss-/pos@ ladder unchanged` (regression). +- [ ] Run: `npm test -- focusUrl` → FAIL. +- [ ] Implement the variant + prefix routing. +- [ ] Run → PASS + `npm run typecheck`. +- [ ] Commit. + +### Task B2: Drop `#poi=` from `hasDeepLink` + +**Files:** + +- Modify: `src/utils/url/hasDeepLink.ts:44-58` +- Test: `tests/utils/url/hasDeepLink.test.ts` + +- [ ] Add/adjust tests: `#focus=cluster-virgo-m87 is a deep link`; `#poi=... is NOT a deep link` (documents the intentional break); `#focus=m31` regression still true. +- [ ] Run → the `#poi=` test FAILs (still treated as deep link). +- [ ] Remove the `#poi=` branch (`hasDeepLink.ts:48`); update the docblock (drop the `#poi=` bullet). +- [ ] Run → PASS. +- [ ] Commit. + +### Task B3: Route structures through the single focus codec in `useUrlSync` + +**Files:** + +- Modify: `src/hooks/useUrlSync.ts` (imports, `InitialPending`, `computeDesiredHash`, `initialPendingFromHash`, hook body slot rename) +- Test: `tests/hooks/useUrlSync.test.ts` + +**Contract:** + +```ts +export type InitialPending = + | { kind: 'galaxy'; target: FocusTarget } + | { kind: 'structure'; id: string } // was { kind: 'poi'; poiId } + | { kind: null }; +``` + +- Remove the `parsePoiHash` import + call. `initialPendingFromHash` parses one `#focus=` hash; a `kind:'structure'` `FocusTarget` → `{ kind:'structure', id }`, everything else → `{ kind:'galaxy', target }`. +- `computeDesiredHash`: a structure focus writes `focus=` (not `poi=`). A galaxy writes `focus=` as today. +- Rename state slot `pendingPoiId` → `pendingStructureId` (and its setter); update the popstate handler + effect 4 (the structure drain) + the `UrlSyncReturn` shape (`@types/engine/UrlSyncReturn.d.ts`). +- Keep BOTH pending slots and both drains — the resolution sources differ (async catalogs vs synchronous structure table); do not merge them. + +- [ ] Update tests: `initialPendingFromHash routes #focus=cluster-... to kind structure`; `computeDesiredHash writes focus= for a structure target`; `galaxy hashes unchanged`; rename existing `poi`-named cases. +- [ ] Run: `npm test -- useUrlSync` → FAIL. +- [ ] Apply the changes (the `camera.focusOn` drain already accepts structures — no logic change there, just the slot/parse rename). +- [ ] Run → PASS + `npm run typecheck`. +- [ ] Commit. + +### Task B4: Delete the `poiUrl` codec + +**Files:** + +- Delete: `src/services/url/poiUrl.ts`, `tests/services/url/poiUrl.test.ts` + +- [ ] Confirm `rg 'poiUrl|parsePoiHash|poiIdToHash' src tests` shows only the files to delete (B1/B3 removed all consumers). +- [ ] Delete both files. +- [ ] Run `npm run typecheck` + `npm test -- url` → PASS. +- [ ] Commit (`git rm` the two paths). + +--- + +## Phase C — Mechanical `poi → structure` renames + +> For each task: enumerate sites with `rg`, rename via edits + `git mv` for files, update the mirrored test, keep typecheck green. These are pure renames — no behaviour change. + +### Task C1: `isPoi` → `isStructure` + +**Files:** `git mv src/services/engine/isPoi.ts src/services/engine/isStructure.ts`; `git mv tests/services/engine/isPoi.test.ts tests/services/engine/isStructure.test.ts`; consumers via `rg -l '\bisPoi\b' src tests` (expect `pickToSelection`/`useUrlSync`/`commitFocus`/`engine.ts` + several `@types` JSDoc refs). + +**Contract:** `export function isStructure(target: FocusableTarget): target is StructureRecord` + +- [ ] `git mv` both files; rename the function + all call sites + the test `describe`/imports. +- [ ] Run `npm test -- isStructure` + `npm run typecheck` → PASS. `rg '\bisPoi\b' src tests` empty. +- [ ] Commit. + +### Task C2: `resolvePoiFromPick` → `resolveStructureFromPick` + +**Files:** `git mv src/services/engine/helpers/resolvePoiFromPick.ts → resolveStructureFromPick.ts`; `git mv` its test; modify `src/services/engine/helpers/pickToSelection.ts` (consumer). + +**Contract:** + +```ts +export type PickStructureInput = { + readonly category: StructureCategory; + readonly structureIndex: number; +}; +export function resolveStructureFromPick( + structures: PickStructureStore, + input: PickStructureInput, +): StructureRecord | null; +``` + +Note: the input `category` can narrow to `StructureCategory` now (the `famousGalaxy` early-return guard at `resolvePoiFromPick.ts:60` becomes unnecessary once the caller passes a `StructureCategory` — verify the caller's type; keep a defensive guard only if the caller still supplies a wider type). + +- [ ] Rename file + fn + type + the `poiIndex` field → `structureIndex`; update `pickToSelection.ts` + the test. +- [ ] Run the test + `npm run typecheck` → PASS. +- [ ] Commit. + +### Task C3: `structurePoiStyles` → `structureMarkerStyles` + +**Files:** `git mv src/services/engine/presentation/structurePoiStyles.ts → structureMarkerStyles.ts`; `git mv tests/data/poiCategories.test.ts → tests/data/structureMarkerStyles.test.ts`; consumers via `rg -l 'STRUCTURE_POI_STYLES|structurePoiStyles' src tests` (expect `produceStructureMarkers`/`produceStructureLabels`). + +**Contract:** export `STRUCTURE_MARKER_STYLES` (type `StructureMarkerStyle` keeps its name); `SIG_MIN_ALPHA` unchanged. + +- [ ] `git mv` both; rename the export + consumers; in the test, drop the `PoiCategory` import (use `StructureCategory`) and rename the `describe`. +- [ ] Run `npm test -- structureMarkerStyles` + `npm run typecheck` → PASS. `rg 'STRUCTURE_POI_STYLES|structurePoiStyles' src tests` empty. +- [ ] Commit. + +### Task C4: `poiIndex` → `structureIndex` in the marker/pick path + +**Files (sites not covered by C2):** `src/services/gpu/renderers/structureMarkerRenderer.ts`, `src/@types/rendering/StructureMarkerRenderer.d.ts`, `src/services/engine/presentation/produceStructureMarkers.ts`, `src/@types/engine/CreateClickResolverInput.d.ts`; mirrored tests (`structureMarkerRenderer.*.test.ts`, `ringPick.test.ts`). + +- [ ] `rg -n '\bpoiIndex\b' src tests` → rename every remaining occurrence to `structureIndex` (field names, locals, comments). +- [ ] Run the affected renderer tests + `npm run typecheck` → PASS. `rg '\bpoiIndex\b' src tests` empty. +- [ ] Commit. + +### Task C5: `FocusState.poiId` → `structureId`; widen `category` + +**Files:** `src/@types/engine/state/FocusState.d.ts`; consumers via `rg -l 'poiId' src` (expect `structureFocusSubsystem`, `commitStructureFocus`, `structureMembership`, the membership-cache key); mirrored tests. + +**Contract:** + +```ts +readonly structureId: string; // was poiId +readonly category: StructureCategory; // was 'cluster' | 'supercluster' | 'void' (now includes group) +``` + +- [ ] Update tests asserting `FocusState` shape / the membership cache key first. +- [ ] Rename `poiId` → `structureId` everywhere; widen `category` to `StructureCategory`; check the membership-cache key string (`structureMembership.ts:12`) and any focus-framing switch handles `group` (it should, since group is already a focusable structure). +- [ ] Run the focus/membership tests + `npm run typecheck` → PASS. +- [ ] Commit. + +### Task C6: `selectedPoi` → `selectedStructure` + +**Files:** `rg -ln 'selectedPoi' src` → `src/services/engine/frame/passes/selectionRingPass.ts`, `src/@types/engine/handles/EngineSubsystemHandles.d.ts`, `src/services/engine/engine.ts` (mostly param names + comments). + +- [ ] Rename the param/comment occurrences to `selectedStructure`. +- [ ] Run `npm run typecheck` + `npm test -- selectionRingPass` → PASS. +- [ ] Commit. + +### Task C7: Doc-comment + remaining-token sweep + +**Files:** every `poi`/`POI` left in comments. Enumerate: `rg -n 'poi|POI' src tests -g '*.ts' -g '*.tsx' -g '*.wesl' | rg -iv 'point|poisson|poison'`. + +- [ ] For each remaining hit (docblocks in `commitGalaxyFocus`, `commitStructureFocus`, `clearAll`, `selectionSubsystem`, `buildStaticAnchorStructures`, `assetWiring`, `useUrlSync`, `useSplash`, `App.tsx`, etc.) reword "POI"/"poi" to "structure" (or "deep link" where it described the URL generically). Follow `feedback_comment_style` — timeless + terse, no history notes. +- [ ] `rg 'poi|POI' src tests -g '*.ts' -g '*.tsx' -g '*.wesl' | rg -iv 'point|poisson|poison'` → empty. +- [ ] Run `npm run typecheck` → PASS. +- [ ] Commit. + +### Task C8: Rename the remaining `*.poi.test.ts` files + final verification + +**Files:** `git mv tests/services/engine/phases/wireInput.poi.test.ts → wireInput.structure.test.ts`; `git mv tests/services/gpu/renderers/pickRenderer.poi.test.ts → pickRenderer.structure.test.ts`; `git mv tests/@types/engine/EngineCameraHandle.poi.test.ts → EngineCameraHandle.structure.test.ts`. Update each `describe`/imports if they reference renamed symbols. + +- [ ] `git mv` the three files; fix any internal `poi` references. +- [ ] **Entanglement-radar verification:** run the `entanglement-radar` lens over `git diff main...HEAD` — confirm the spec's un-braided choices survived: `PoiCategory` gone (not re-spelled), marker record narrowed (no phantom `famousGalaxy`), the two URL resolution paths NOT merged, the famousGalaxy `if`-branches dissolved into `labelLayer` data. Note any regressions and fix before finishing. +- [ ] **Full sweep:** `find src tests -iname '*poi*'` empty; `rg 'poi|POI' src tests -g '*.ts' -g '*.tsx' -g '*.wesl' | rg -iv 'point|poisson|poison'` empty. +- [ ] Run full `npm test` + `npm run typecheck` + `npm run build` → all green. +- [ ] Prettier all touched files; commit. + +--- + +## Self-review notes + +**Spec coverage:** A1–A5 cover spec Part A (capability flags A1, display fold A2/A3, derived types A2, record retype A4, setter de-special-case A5). B1–B4 cover Part B (codec variant B1, hasDeepLink B2, useUrlSync B3, delete B4). C1–C8 cover Part C (every rename row in the spec's Part C table + the test renames + the final entanglement-radar gate). + +**Judgement calls (flag for review):** + +1. **`famousGalaxy` display copy:** the old table had `shortLabel: 'Galaxy'` but the registry `label` is `'Famous'`. A3 maps `shortLabel ← row.label`, so the famous chip short form would read `'Famous'`, not `'Galaxy'`. If the UI must keep `'Galaxy'`, set the famousGalaxy row's `label` to `'Galaxy'` in A1 (and check no other consumer of that row's `label` regresses) — confirm during A1. +2. **`tests/data/poiCategories.test.ts` fate:** the spec suggested re-scoping it to the derived sets, but it currently tests the _style table_. This plan instead renames it to `structureMarkerStyles.test.ts` (C3, style assertions) and creates a fresh `labelCategories.test.ts` (A2, derived sets). Two clear tests beats one re-scoped one. +3. **`resolveStructureFromPick` defensive guard:** the `famousGalaxy` early-return may become dead once the caller passes `StructureCategory` (C2). Drop it only if the caller's type genuinely narrows; otherwise keep it. From b0ff7860b10aed472bc5a2d3e2e2816ecf2791dd Mon Sep 17 00:00:00 2001 From: Alexander Rulkens Date: Mon, 8 Jun 2026 02:12:25 +0200 Subject: [PATCH 03/27] docs(plan): explicit category display fields; famous shows "Famous Galaxies" Decouple category display copy from the source label so famousGalaxy source stays 'Famous' while the category reads 'Famous Galaxies'. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plans/2026-06-08-poi-free-codebase.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/superpowers/plans/2026-06-08-poi-free-codebase.md b/docs/superpowers/plans/2026-06-08-poi-free-codebase.md index d996b814..f73db613 100644 --- a/docs/superpowers/plans/2026-06-08-poi-free-codebase.md +++ b/docs/superpowers/plans/2026-06-08-poi-free-codebase.md @@ -33,17 +33,21 @@ readonly bearsMarker: boolean; readonly labelLayer?: 'galaxyNames' | 'structure'; /** Long form for detail surfaces ("Galaxy Cluster"). Present iff bearsLabel. */ readonly detailLabel?: string; +/** Compact form for chips/previews ("Cluster"). Present iff bearsLabel. */ +readonly shortLabel?: string; /** Plural for list/toggle headers ("Clusters"). Present iff bearsLabel. */ readonly plural?: string; ``` +These three display fields are **explicit** on each label-bearing row — deliberately NOT derived from the source `label`, because the famous-galaxy _source_ label (`'Famous'`) differs from the _category_ display copy. Decoupling them is what keeps the source name and the category name independent. + Per-row values (see spec §A1/§A2 table): -- `famousGalaxy`: `bearsLabel: true, bearsMarker: false, labelLayer: 'galaxyNames', detailLabel: 'Famous Galaxy', plural: 'Famous galaxies'`. (Keep its existing `label: 'Famous'`; the old display table's short form was `'Galaxy'` — see judgement note at end of plan.) -- `cluster/supercluster/void/group`: `bearsLabel: true, bearsMarker: true, labelLayer: 'structure'`, with `detailLabel`/`plural` copied from the old `POI_CATEGORY_INFO` rows (`src/data/poiCategoryInfo.ts:24-50`). -- `sdss/glade/2mrs/milliquas/synthetic`: `bearsLabel: false, bearsMarker: false` (omit `labelLayer`/`detailLabel`/`plural`). +- `famousGalaxy`: `bearsLabel: true, bearsMarker: false, labelLayer: 'galaxyNames', detailLabel: 'Famous Galaxy', shortLabel: 'Galaxy', plural: 'Famous Galaxies'`. (Keep its existing source `label: 'Famous'` unchanged — only the category display fields are added.) +- `cluster/supercluster/void/group`: `bearsLabel: true, bearsMarker: true, labelLayer: 'structure'`, with `detailLabel`/`shortLabel`/`plural` copied verbatim from the old `POI_CATEGORY_INFO` rows (`src/data/poiCategoryInfo.ts:24-50`). +- `sdss/glade/2mrs/milliquas/synthetic`: `bearsLabel: false, bearsMarker: false` (omit `labelLayer`/`detailLabel`/`shortLabel`/`plural`). -- [ ] Add tests: `famousGalaxy row bears a label but no marker` (assert `bearsLabel` true, `bearsMarker` false, `labelLayer === 'galaxyNames'`); `structure rows bear both a label and a marker` (loop cluster/supercluster/void/group); `bulk survey rows bear neither` (assert sdss/glade false/false). +- [ ] Add tests: `famousGalaxy row bears a label but no marker` (assert `bearsLabel` true, `bearsMarker` false, `labelLayer === 'galaxyNames'`); `famousGalaxy category copy is detail 'Famous Galaxy' / short 'Galaxy' / plural 'Famous Galaxies'`; `structure rows bear both a label and a marker` (loop cluster/supercluster/void/group); `bulk survey rows bear neither` (assert sdss/glade false/false). - [ ] Run: `npm test -- sources` → new tests FAIL (fields absent). - [ ] Add the five fields to `SourceEntryBase`; populate every row in `sources.ts`. - [ ] Run: `npm test -- sources` and `npm run typecheck` → PASS. @@ -88,9 +92,9 @@ export type CategoryDisplayInfo = { label: string; shortLabel: string; readonly export const CATEGORY_DISPLAY_INFO: Readonly>; ``` -Mapping: `shortLabel ← row.label`; `label ← row.detailLabel`; `plural ← row.plural`. +Mapping: `label ← row.detailLabel`; `shortLabel ← row.shortLabel`; `plural ← row.plural` (all explicit registry fields — see A1). -- [ ] Add tests: `CATEGORY_DISPLAY_INFO has a row per LabelCategory`; `cluster renders 'Galaxy Cluster' / 'Cluster' / 'Clusters'`; `famousGalaxy renders the famous display copy`. +- [ ] Add tests: `CATEGORY_DISPLAY_INFO has a row per LabelCategory`; `cluster renders 'Galaxy Cluster' / 'Cluster' / 'Clusters'`; `famousGalaxy renders 'Famous Galaxy' / 'Galaxy' / 'Famous Galaxies'`. - [ ] Run → FAIL. - [ ] Implement `categoryDisplayInfo.ts` deriving from the registry; repoint every `POI_CATEGORY_INFO[x]` read to `CATEGORY_DISPLAY_INFO[x]`; delete `poiCategoryInfo.ts`. - [ ] Run the touched tests + `npm run typecheck` → PASS. Confirm `rg 'POI_CATEGORY_INFO|poiCategoryInfo' src` is empty. @@ -333,6 +337,6 @@ readonly category: StructureCategory; // was 'cluster' | 'supercluster' | 'void **Judgement calls (flag for review):** -1. **`famousGalaxy` display copy:** the old table had `shortLabel: 'Galaxy'` but the registry `label` is `'Famous'`. A3 maps `shortLabel ← row.label`, so the famous chip short form would read `'Famous'`, not `'Galaxy'`. If the UI must keep `'Galaxy'`, set the famousGalaxy row's `label` to `'Galaxy'` in A1 (and check no other consumer of that row's `label` regresses) — confirm during A1. +1. **`famousGalaxy` display copy — RESOLVED (2026-06-08):** the category display fields are explicit on the row (not derived from the source `label`), so the famous _source_ keeps `label: 'Famous'` while the _category_ reads detail `'Famous Galaxy'` / short `'Galaxy'` / plural **`'Famous Galaxies'`** (user's call: toggle/header shows "Famous Galaxies"). Baked into A1/A3 above. 2. **`tests/data/poiCategories.test.ts` fate:** the spec suggested re-scoping it to the derived sets, but it currently tests the _style table_. This plan instead renames it to `structureMarkerStyles.test.ts` (C3, style assertions) and creates a fresh `labelCategories.test.ts` (A2, derived sets). Two clear tests beats one re-scoped one. 3. **`resolveStructureFromPick` defensive guard:** the `famousGalaxy` early-return may become dead once the caller passes `StructureCategory` (C2). Drop it only if the caller's type genuinely narrows; otherwise keep it. From 0e1a4a8333dacd80530d5e79ea7640227ff4ef0b Mon Sep 17 00:00:00 2001 From: Alexander Rulkens Date: Mon, 8 Jun 2026 02:12:41 +0200 Subject: [PATCH 04/27] docs(plan): fix A3 display-field source reference Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/superpowers/plans/2026-06-08-poi-free-codebase.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/superpowers/plans/2026-06-08-poi-free-codebase.md b/docs/superpowers/plans/2026-06-08-poi-free-codebase.md index f73db613..c1012e00 100644 --- a/docs/superpowers/plans/2026-06-08-poi-free-codebase.md +++ b/docs/superpowers/plans/2026-06-08-poi-free-codebase.md @@ -80,7 +80,7 @@ export type LabelCategory = (typeof LABEL_CATEGORIES)[number]; **Files:** -- Create: `src/data/categoryDisplayInfo.ts` (derived `CATEGORY_DISPLAY_INFO` keyed by `LabelCategory`, fields `{ label, shortLabel, plural }` sourced from the registry rows' `detailLabel`/`label`/`plural`) +- Create: `src/data/categoryDisplayInfo.ts` (derived `CATEGORY_DISPLAY_INFO` keyed by `LabelCategory`, fields `{ label, shortLabel, plural }` sourced from the registry rows' `detailLabel`/`shortLabel`/`plural`) - Modify: every `POI_CATEGORY_INFO` consumer (enumerate with `rg -l 'POI_CATEGORY_INFO|poiCategoryInfo|PoiCategoryInfo' src` — expect InfoCard / SettingsPanel family) - Delete: `src/data/poiCategoryInfo.ts` - Test: `tests/data/categoryDisplayInfo.test.ts` (rename from any `poiCategoryInfo` test if one exists; else new) From 2e55d10592a1c5d5b94af14c8cf0f78d8452465f Mon Sep 17 00:00:00 2001 From: Alexander Rulkens Date: Mon, 8 Jun 2026 02:18:39 +0200 Subject: [PATCH 05/27] feat(sources): add bearsLabel/bearsMarker + category display fields to registry Capability flags (bearsLabel/bearsMarker/labelLayer) and per-category display copy (detailLabel/shortLabel/plural) on SOURCE_REGISTRY rows, so the label and marker category axes can derive from the registry instead of a PoiCategory union + a separate POI_CATEGORY_INFO table. famousGalaxy: label-only; structures: label + marker. Task A1 of the poi-free plan. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/@types/data/SourceEntryBase.d.ts | 40 +++++++++++++++++++++ src/data/sources.ts | 52 ++++++++++++++++++++++++++++ tests/data/sources.test.ts | 36 +++++++++++++++++++ 3 files changed, 128 insertions(+) diff --git a/src/@types/data/SourceEntryBase.d.ts b/src/@types/data/SourceEntryBase.d.ts index c36478f7..8f49bc35 100644 --- a/src/@types/data/SourceEntryBase.d.ts +++ b/src/@types/data/SourceEntryBase.d.ts @@ -23,4 +23,44 @@ export type SourceEntryBase = { * off at runtime — this is purely the default. */ readonly visible: boolean; + /** + * True if this source carries toggleable on-screen text labels. + * Drives the label-visibility record and the fade-layer routing in the + * label subsystem. The two real label sets are: + * - galaxyNames layer: famousGalaxy + * - structure layer: cluster, supercluster, void, group + * All bulk surveys (sdss, glade, 2mrs, milliquas, synthetic) are false. + */ + readonly bearsLabel: boolean; + /** + * True if this source carries a ring/halo marker around its anchor point. + * Today this is exactly the structure category set (cluster, supercluster, + * void, group). famousGalaxy has no ring — only a name label — so the + * marker-visibility record excludes it entirely. + */ + readonly bearsMarker: boolean; + /** + * Which fade layer this source's labels live on. Present iff bearsLabel. + * - 'galaxyNames' — the shared galaxy-name layer (famousGalaxy rows) + * - 'structure' — the per-structure-category label layer + * Absent on non-label-bearing rows (bearsLabel === false). + */ + readonly labelLayer?: 'galaxyNames' | 'structure'; + /** + * Long-form label for detail surfaces (e.g. 'Galaxy Cluster', 'Famous Galaxy'). + * Present iff bearsLabel. Replaces the old POI_CATEGORY_INFO.label field. + */ + readonly detailLabel?: string; + /** + * Compact label for chips and previews (e.g. 'Cluster', 'Galaxy'). + * Present iff bearsLabel. The existing `label` field carries the shortest + * UI name ('Cluster', 'Famous') — shortLabel is the one step longer form + * the InfoCard chips use. + */ + readonly shortLabel?: string; + /** + * Plural label for list and toggle headers (e.g. 'Clusters', 'Famous Galaxies'). + * Present iff bearsLabel. Replaces the old POI_CATEGORY_INFO.plural field. + */ + readonly plural?: string; }; diff --git a/src/data/sources.ts b/src/data/sources.ts index a816058e..87b5b639 100644 --- a/src/data/sources.ts +++ b/src/data/sources.ts @@ -156,6 +156,8 @@ export const SOURCE_REGISTRY = { binBaseName: null, // generated at runtime; no file allSky: true, // uniform-in-sphere by construction visible: true, + bearsLabel: false, + bearsMarker: false, maxDistMpc: 1000, // matches the radius in synthetic.ts bandLabels: { u: 'u', g: 'g', r: 'r', i: 'i', z: 'z' }, colourSpec: { slotA: 'u', slotB: 'g', rangeMin: 0.5, rangeMax: 2.0, kPerZ: 3.0 }, @@ -182,6 +184,8 @@ export const SOURCE_REGISTRY = { binBaseName: 'sdss', allSky: false, visible: true, + bearsLabel: false, + bearsMarker: false, // Main galaxy sample reaches z ~ 0.7+ for luminous red galaxies; // rounded up generously. maxDistMpc: 3000, @@ -211,6 +215,8 @@ export const SOURCE_REGISTRY = { binBaseName: '2mrs', allSky: true, visible: true, + bearsLabel: false, + bearsMarker: false, // Flux-limited at K_s ≈ 11.75; effective z ≲ 0.06. maxDistMpc: 250, bandLabels: { u: '—', g: 'J', r: 'H', i: 'K', z: '—' }, @@ -237,6 +243,8 @@ export const SOURCE_REGISTRY = { binBaseName: 'glade', allSky: true, visible: true, + bearsLabel: false, + bearsMarker: false, // Covers most of the GLADE distance distribution. GLADE has a long // sparse tail past 1 Gpc that the default framing deliberately clips. maxDistMpc: 1500, @@ -268,6 +276,12 @@ export const SOURCE_REGISTRY = { binBaseName: 'famous', allSky: true, // hand-picked entries from across the sky visible: true, + bearsLabel: true, + bearsMarker: false, + labelLayer: 'galaxyNames', + detailLabel: 'Famous Galaxy', + shortLabel: 'Galaxy', + plural: 'Famous Galaxies', maxDistMpc: 200, // covers the curated set: M31 → NGC 4889 // Famous entries don't carry per-row photometry — the source survey // already measured it. The SDSS-mirroring labels are cosmetic so the @@ -295,6 +309,12 @@ export const SOURCE_REGISTRY = { label: 'Cluster', allSky: true, visible: true, + bearsLabel: true, + bearsMarker: true, + labelLayer: 'structure', + detailLabel: 'Galaxy Cluster', + shortLabel: 'Cluster', + plural: 'Clusters', }, [Source.Supercluster]: { type: 'structure', @@ -303,6 +323,12 @@ export const SOURCE_REGISTRY = { label: 'Supercluster', allSky: true, visible: true, + bearsLabel: true, + bearsMarker: true, + labelLayer: 'structure', + detailLabel: 'Supercluster', + shortLabel: 'Supercluster', + plural: 'Superclusters', }, [Source.Void]: { type: 'structure', @@ -311,6 +337,12 @@ export const SOURCE_REGISTRY = { label: 'Void', allSky: true, visible: true, + bearsLabel: true, + bearsMarker: true, + labelLayer: 'structure', + detailLabel: 'Cosmic Void', + shortLabel: 'Void', + plural: 'Voids', }, [Source.Group]: { type: 'structure', @@ -319,6 +351,12 @@ export const SOURCE_REGISTRY = { label: 'Group', allSky: true, visible: true, + bearsLabel: true, + bearsMarker: true, + labelLayer: 'structure', + detailLabel: 'Galaxy Group', + shortLabel: 'Group', + plural: 'Groups', }, [Source.Milliquas]: { type: 'survey', @@ -327,6 +365,8 @@ export const SOURCE_REGISTRY = { label: 'Milliquas', binBaseName: 'milliquas', allSky: true, + bearsLabel: false, + bearsMarker: false, // Visible by default: the quasar source is stable enough to ship on. // It renders with the shared galaxy-billboard path (no quasar-specific // visuals yet), which is acceptable for the bright low-z tail the @@ -387,6 +427,8 @@ export const SOURCE_REGISTRY = { // and most users want the points-only view first. They can flip it // on in the SettingsPanel. visible: false, + bearsLabel: false, + bearsMarker: false, binBaseName: 'filaments', // 1.0 is the unit baseline; user scales it down for a subtler overlay // or up for emphasis via the (future) Filaments slider. @@ -401,6 +443,8 @@ export const SOURCE_REGISTRY = { // Default-off: the ~32 MB voxel payload is demand-loaded the first // time the user enables the field in the Volumes panel — not at boot. visible: false, + bearsLabel: false, + bearsMarker: false, handle: 'cf4-density', // Underscore in the filename for legacy reasons; `handle` mirrors it // in kebab-case for UI / settings keys. @@ -429,6 +473,8 @@ export const SOURCE_REGISTRY = { // Default-on: this is the headline cosmic-web overlay; the global // intensity of 1.0 (set on this entry) gives it presence on first paint. visible: true, + bearsLabel: false, + bearsMarker: false, handle: 'mcpm', binBaseName: 'mcpm', tiered: true, // small / medium / large `.scfd` variants @@ -454,6 +500,8 @@ export const SOURCE_REGISTRY = { label: 'Gaussian (debug)', allSky: true, visible: false, + bearsLabel: false, + bearsMarker: false, handle: 'debug-gaussian', binBaseName: null, tiered: false, @@ -475,6 +523,8 @@ export const SOURCE_REGISTRY = { label: 'Cartesian grid (debug)', allSky: true, visible: false, + bearsLabel: false, + bearsMarker: false, handle: 'debug-cartesian', binBaseName: null, tiered: false, @@ -496,6 +546,8 @@ export const SOURCE_REGISTRY = { label: 'Spherical grid (debug)', allSky: true, visible: false, + bearsLabel: false, + bearsMarker: false, handle: 'debug-spherical', binBaseName: null, tiered: false, diff --git a/tests/data/sources.test.ts b/tests/data/sources.test.ts index 19e19259..de04ca20 100644 --- a/tests/data/sources.test.ts +++ b/tests/data/sources.test.ts @@ -105,6 +105,42 @@ describe('Source enum — POI codes (cluster/supercluster/void)', () => { }); }); +describe('Registry capability flags — bearsLabel / bearsMarker', () => { + it('famousGalaxy row bears a label but no marker', () => { + const entry = SOURCE_REGISTRY[Source.FamousGalaxy]; + expect(entry.bearsLabel).toBe(true); + expect(entry.bearsMarker).toBe(false); + expect(entry.labelLayer).toBe('galaxyNames'); + }); + + it('famousGalaxy category copy is detail "Famous Galaxy" / short "Galaxy" / plural "Famous Galaxies"', () => { + const entry = SOURCE_REGISTRY[Source.FamousGalaxy]; + expect(entry.detailLabel).toBe('Famous Galaxy'); + expect(entry.shortLabel).toBe('Galaxy'); + expect(entry.plural).toBe('Famous Galaxies'); + }); + + it('structure rows bear both a label and a marker', () => { + // The four structure source ids — verified correct in STRUCTURE_CATEGORIES test above. + const structureIds = [Source.Cluster, Source.Supercluster, Source.Void, Source.Group] as const; + for (const id of structureIds) { + const entry = SOURCE_REGISTRY[id]; + expect(entry.bearsLabel).toBe(true); + expect(entry.bearsMarker).toBe(true); + expect(entry.labelLayer).toBe('structure'); + } + }); + + it('bulk survey rows bear neither a label nor a marker', () => { + const surveyIds = [Source.SDSS, Source.Glade] as const; + for (const id of surveyIds) { + const entry = SOURCE_REGISTRY[id]; + expect(entry.bearsLabel).toBe(false); + expect(entry.bearsMarker).toBe(false); + } + }); +}); + describe('Famous-galaxy hi-res LOD constants', () => { it('HI_RES_LAYER_SIDE_BY_TIER pegs small to 512 and medium/large to 1024', () => { // Mobile (small) halves the layer dim to keep the GPU footprint at From a2190564bb61f11d7c2dd9ff34f311d28fbf90d0 Mon Sep 17 00:00:00 2001 From: Alexander Rulkens Date: Mon, 8 Jun 2026 02:21:28 +0200 Subject: [PATCH 06/27] feat(data): derive LABEL_CATEGORIES + LabelCategory from the registry The label-visibility axis (famousGalaxy + the four structure categories), mirroring structureCategories.ts. Honest registry-derived replacement for the hand-written PoiCategory union. Task A2 of the poi-free plan. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/@types/engine/data/LabelCategory.d.ts | 3 +++ src/data/labelCategories.ts | 21 ++++++++++++++++++ tests/data/labelCategories.test.ts | 26 +++++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 src/@types/engine/data/LabelCategory.d.ts create mode 100644 src/data/labelCategories.ts create mode 100644 tests/data/labelCategories.test.ts diff --git a/src/@types/engine/data/LabelCategory.d.ts b/src/@types/engine/data/LabelCategory.d.ts new file mode 100644 index 00000000..cddb1b01 --- /dev/null +++ b/src/@types/engine/data/LabelCategory.d.ts @@ -0,0 +1,3 @@ +import { LABEL_CATEGORIES } from '../../../data/labelCategories'; + +export type LabelCategory = (typeof LABEL_CATEGORIES)[number]; diff --git a/src/data/labelCategories.ts b/src/data/labelCategories.ts new file mode 100644 index 00000000..676f63f6 --- /dev/null +++ b/src/data/labelCategories.ts @@ -0,0 +1,21 @@ +import { SOURCE_ENTRIES } from './sourceEntries'; + +/** + * LABEL_CATEGORIES — every source that renders a text label in the 3D scene, + * in registry order. + * + * Two kinds of sources bear labels: + * - 'famousGalaxy' — the curated atlas entry carries a name that floats above + * the dot on a leader line (the 'galaxyNames' layer). + * - Structure categories (cluster, supercluster, void, group) — each ring + * carries a name on the 'structure' label layer. + * + * This is the label-visibility axis: settings toggles, fade registration, and + * the SettingsPanel label-on/off rows all iterate this set. Bulk surveys + * (SDSS, GLADE, 2MRS, Milliquas) do not bear labels and are excluded. + * + * Companion to STRUCTURE_CATEGORIES, which is the narrower structure-only axis + * (used where 'famousGalaxy' is irrelevant, e.g. the structure store and + * marker producer). + */ +export const LABEL_CATEGORIES = SOURCE_ENTRIES.filter((e) => e.bearsLabel).map((e) => e.id); diff --git a/tests/data/labelCategories.test.ts b/tests/data/labelCategories.test.ts new file mode 100644 index 00000000..43a6104d --- /dev/null +++ b/tests/data/labelCategories.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; +import { LABEL_CATEGORIES } from '../../src/data/labelCategories'; +import { STRUCTURE_CATEGORIES } from '../../src/data/structureCategories'; + +describe('LABEL_CATEGORIES', () => { + it('contains famousGalaxy and the four structure categories', () => { + expect([...LABEL_CATEGORIES].sort()).toEqual([ + 'cluster', + 'famousGalaxy', + 'group', + 'supercluster', + 'void', + ]); + }); + + it('is a superset of STRUCTURE_CATEGORIES', () => { + for (const cat of STRUCTURE_CATEGORIES) { + expect(LABEL_CATEGORIES).toContain(cat); + } + }); + + it('excludes bulk surveys', () => { + expect(LABEL_CATEGORIES).not.toContain('sdss'); + expect(LABEL_CATEGORIES).not.toContain('glade'); + }); +}); From 595e6b35ee7d4e73c880939207a0b06f9b3b28ba Mon Sep 17 00:00:00 2001 From: Alexander Rulkens Date: Mon, 8 Jun 2026 02:25:23 +0200 Subject: [PATCH 07/27] feat(data): fold category display copy into registry-derived CATEGORY_DISPLAY_INFO Replace the standalone POI_CATEGORY_INFO table with CATEGORY_DISPLAY_INFO derived from the label-bearing registry rows; repoint InfoCard + SettingsPanel consumers; delete poiCategoryInfo.ts. Task A3 of the poi-free plan. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../InfoCard/CompactStructureCard.tsx | 6 +- .../InfoCard/StructureDetailCard.tsx | 6 +- .../SettingsPanel/SettingsPanel.tsx | 6 +- src/data/categoryDisplayInfo.ts | 55 +++++++++++++++++++ src/data/poiCategoryInfo.ts | 50 ----------------- tests/data/categoryDisplayInfo.test.ts | 23 ++++++++ 6 files changed, 89 insertions(+), 57 deletions(-) create mode 100644 src/data/categoryDisplayInfo.ts delete mode 100644 src/data/poiCategoryInfo.ts create mode 100644 tests/data/categoryDisplayInfo.test.ts diff --git a/src/components/InfoCard/CompactStructureCard.tsx b/src/components/InfoCard/CompactStructureCard.tsx index 6d51b311..3a8c2f3c 100644 --- a/src/components/InfoCard/CompactStructureCard.tsx +++ b/src/components/InfoCard/CompactStructureCard.tsx @@ -7,7 +7,7 @@ import type { ReactNode } from 'react'; import type { StructureRecord } from '../../@types/engine/data/StructureRecord'; import { formatDistance } from '../../utils/format/distance'; -import { POI_CATEGORY_INFO } from '../../data/poiCategoryInfo'; +import { CATEGORY_DISPLAY_INFO } from '../../data/categoryDisplayInfo'; import styles from './CompactStructureCard.module.css'; export type CompactStructureCardProps = { @@ -28,7 +28,9 @@ export function CompactStructureCard({ structure }: CompactStructureCardProps):
{structure.name}
- {POI_CATEGORY_INFO[structure.category].shortLabel} + + {CATEGORY_DISPLAY_INFO[structure.category].shortLabel} +
{formatDistance(distanceMpc)} diff --git a/src/components/InfoCard/StructureDetailCard.tsx b/src/components/InfoCard/StructureDetailCard.tsx index e983b287..b1415c5b 100644 --- a/src/components/InfoCard/StructureDetailCard.tsx +++ b/src/components/InfoCard/StructureDetailCard.tsx @@ -8,7 +8,7 @@ import type { ReactNode } from 'react'; import type { StructureRecord } from '../../@types/engine/data/StructureRecord'; import { formatDistance } from '../../utils/format/distance'; import { formatAbellDesignation } from '../../utils/format/formatAbellDesignation'; -import { POI_CATEGORY_INFO } from '../../data/poiCategoryInfo'; +import { CATEGORY_DISPLAY_INFO } from '../../data/categoryDisplayInfo'; import { CardHeader } from './CardHeader'; import { CardRow } from './CardRow'; import { DescriptionBlock } from './DescriptionBlock'; @@ -55,7 +55,9 @@ export function StructureDetailCard({
{structure.name}
- {POI_CATEGORY_INFO[structure.category].label} + + {CATEGORY_DISPLAY_INFO[structure.category].label} +
diff --git a/src/components/SettingsPanel/SettingsPanel.tsx b/src/components/SettingsPanel/SettingsPanel.tsx index b19a9f53..291faf1c 100644 --- a/src/components/SettingsPanel/SettingsPanel.tsx +++ b/src/components/SettingsPanel/SettingsPanel.tsx @@ -88,7 +88,7 @@ import type { BiasMode as BiasModeT } from '../../@types/data/BiasMode'; import { ALL_TONE_MAP_CURVES, toneMapCurveLabel } from '../../data/toneMapCurve'; import type { ToneMapCurve as ToneMapCurveT } from '../../@types/data/ToneMapCurve'; import type { PoiCategory } from '../../@types/engine/data/PoiCategory'; -import { POI_CATEGORY_INFO } from '../../data/poiCategoryInfo'; +import { CATEGORY_DISPLAY_INFO } from '../../data/categoryDisplayInfo'; import { STRUCTURE_CATEGORIES } from '../../data/structureCategories'; import type { ScalarFieldPaletteId } from '../../@types/data/ScalarFieldPaletteId'; import type { VolumeFieldRowData } from '../../@types/settings/VolumeFieldRowData'; @@ -818,7 +818,7 @@ export function SettingsPanel({ return (