fuzzyworld

when-am-i / files / plan.md

plan.md

22.9 KB · history · blame

Plan — When am I? (formerly "What Year Is It?")

Status as of 2026-06-16. Owner: Pavel.

Goal of this milestone

Ship an end-to-end MVP: a user picks a location on the map and a full date (year + month + day, BCE/CE), and the app resolves the matching region and era and displays the local calendar year using real calendar math (not just the era's metadata). Build the engine with Vitest tests and add ESLint.

Boston is the proof-of-architecture region; the loop must generalize to any region file.


Where we are today (baseline)

Working: - App.tsx: header + intro + map; echoes clicked lat/lng. - src/components/MapPicker.tsx: the live map (center=[20,0], zoom=2). - Data: region files massachusett.json (Massachusett reckoning) and colonial-america.json (empire-wide Julian/Gregorian); schema docs in SCHEMA.json / SCHEMA.md. - Deploy: .fw-ci.yml builds and publishes dist/ to GitLab Pages.

Missing / debt: - No date picker. - No resolution engine: nothing loads the calendar JSON, runs point-in-polygon, matches an era, or converts/displays a local year. - ~~No build-time data validation (README claims it exists — it does not).~~ Resolved 2026-06-20: npm run validate:data (scripts/validate-data.mts) now validates every region file and runs in prebuild + CI, so the README claim is true. See the data-pipeline note below. - Dead code: src/MapPicker.tsx is an unused, broken duplicate (no center/zoom) of the real component. - Empty stubs: src/data/index.ts, src/hooks/index.ts, src/types/index.ts, src/utils/index.ts. - No tests, no lint.


Architecture for the MVP

Core framing (from Q4): "When am I?" is addressed to a person, so the answer is plural. The engine does not pick one era — it collects every valid answer a local could give. Ask a Boston settler and an Algonquian person in 1700 and you get two answers; both are valid and we show both.

Data flow: click (lat,lng) + date → find all region(s) containing the point → within each, find all eras whose date range covers the date (eras may overlap) → convert the date into each era's calendar (or fall back to descriptive-only) → render the full list of answers.

1. Types — src/types/index.ts

Mirror the JSON schema and add result types: - Geometry (GeoJSON Polygon/MultiPolygon; coords are [lng, lat]). - Region { name; geo: Geometry }. - Era { name; start; end; calendar; usedBy?; description?; sources? } — start/end are YYYY-MM-DD | null; new optional usedBy labels whose answer it is (e.g. "British colonists", "Algonquian peoples"), which the plural model needs. - CalendarRegion { region: Region; eras: Era[] }. - HistoricalDate — see "Date representation" below. - Answer { region: Region; era: Era; conversion: CalendarConversion } — one possible reply a local could give. - Resolution { answers: Answer[]; missReason?: 'no-region' | 'no-era' } — the engine returns the list of answers (possibly empty, with a reason). - CalendarConversion { system: string; localYearLabel: string; localDateLabel?: string; convertible: boolean } — convertible: false ⇒ descriptive-only (see §5).

2. Data loading — src/data/index.ts

3. Geometry — src/utils/geo.ts

4. Era matching — src/utils/eras.ts

5. Calendar conversion — src/utils/calendars.ts

6. UI

7. Cleanup


Data pipeline (lightweight, to make the README honest)

Shipped 2026-06-20 as scripts/validate-data.mts (npm run validate:data), wired into prebuild and CI. Decision: rather than promote SCHEMA.json to a real JSON Schema / zod schema, the validator itself is the authoritative, machine-checkable schema. A hand-rolled validator covers semantic checks a plain JSON Schema cannot express — dates that actually parse, geometry rings that are closed with in-range [lng, lat], and [start, end) ordering — and avoids a second spec that could drift from the validator. SCHEMA.json / SCHEMA.md remain the human-readable field docs and now point at the validator. Overlapping eras are intentionally not flagged (Q4).


Verify tooling (adopt from ~/code/share)

Reuse Pavel's established pattern (reference: ~/code/share/web/scripts/*.mts): - A .mts Playwright script run via tsx that auto-starts the Vite dev server, forces deterministic app state via URL query params (here: a ?demo= that pre-sets picked location + date so the answers panel renders without manual clicking), stabilizes any volatile content, screenshots to screenshots/<category>/, and checks them in. - ImageMagick magick composites for documentation imagery. - Most valuable once the ResultPanel exists → schedule in Phase 4 (UI). The ?demo= state hook should be designed into App during Phase 4, not bolted on later.

Per-calendar PR screenshots

A screenshot:calendar -- <file.json> script that renders one calendar's answers so a calendar PR can include illustrative screenshots showing any major issues: - Compute a point inside the region (polygon centroid / point-on-surface) and force the app there via ?demo= with a representative date per era (e.g. midpoint of each era's range). Screenshot the answers panel for each. - Optionally a single composite per calendar (map + answers) via magick. - This makes review concrete: a wrong region polygon, an off-by-a-century era boundary, a miscategorized "convertible vs descriptive-only", or a broken conversion all show up visually in the PR. Reviewer (Pavel) checks the screenshot against Wikipedia/facts. - Document this in the contributor section so each calendar PR attaches its screenshots. Depends on the ?demo= hook + ResultPanel → Phase 4+.

Testing & lint


Documentation gate (required before "done")


Phased execution

  1. Foundations — types, data loader, delete dead MapPicker, Vitest + ESLint scaffolding, CI.
  2. Engine — geo + era matching + conversion registry, with tests. Gregorian/Julian first.
  3. Library-backed calendars — wire the chosen conversion lib (Q1), add tests.
  4. UI — DatePicker + ResultPanel + App wiring; handle all miss states.
  5. Data pipeline — ✅ validate:data + prebuild/CI gate (see note above; the validator is the schema, no separate JSON Schema file).
  6. Docs — README + SCHEMA sync. ✅ Generalization proven 2026-06-20 with three region files beyond Boston: iran.json (Solar Hijri + lunar Hijri, exercising the Temporal-backed Persian and Islamic converters), russian-america.json (a second Julian→Gregorian cutover, at the 1867 Alaska transfer), and oregon-country.json (Hudson's Bay Company, covering the Seattle area). Note: the Temporal chinese calendar was the first pick for the convertible-library proof but is broken in @js-temporal/polyfill@0.5.1 (throws on every date — "Unexpected leap month suffix"; no fixed version available), so Persian/Iran was used instead.

The five most-populous calendars added 2026-06-20 (covering the most humans × time, per Pavel's request): china.json (Chinese sexagenary cycle — descriptive-only, hand-rolled stem-branch/zodiac naming, since the Temporal lunisolar chinese is broken), india.json (Saka, existing Temporal converter), japan.json (a pre-1873 lunisolar descriptive era + the modern Japanese nengō era, Temporal japanese using era/eraYear, scoped to Meiji+), thailand.json (Buddhist Era, existing converter), and ethiopia.json (Ethiopian calendar, hand-rolled via JDN because the Temporal ethiopic and coptic calendars also throw in this polyfill — same class of bug as chinese). New converters live in src/utils/calendars.ts with tests.


Stretch goals (post-MVP)

AI-assisted dataset authoring (requested 2026-06-20)

✅ Shipped 2026-06-20 as the new-calendar skill (.claude/skills/new-calendar/SKILL.md): it walks an author through researching a place/people's calendar(s) with sourced citations, drafting the GeoJSON polygon and eras, picking the right converter (or descriptive-only), and running validate:data / gen:badges / screenshot:calendar so the result is PR-ready — with the project's "never invent, always source" rule front and centre. A review/QA skill remains a possible follow-up. Original sketch of what the skill does: - Take a place + people/polity as input (e.g. "the Mughal Empire", "Coast Salish around Puget Sound") and research the calendar system(s), era boundaries, and whose reckoning it is (usedBy), gathering citable sources (the project's acceptance gate — never invent events or moon names; see "Data accuracy" below). - Draft the region polygon (GeoJSON [lng, lat], closed rings) and the eras with proleptic-Gregorian start/end, choosing the right calendar string so it hits an existing converter (or flagging it as descriptive-only). - Decide convertible vs descriptive-only correctly, and if a new convertible system is needed, note the converter work in src/utils/calendars.ts (with the @js-temporal/polyfill caveat — e.g. chinese is currently broken). - Run npm run validate:data, npm run gen:badges, and npm run screenshot:calendar -- <file> so the output is PR-ready with review screenshots. - Possibly a second, smaller skill to review/QA an existing dataset against its sources. This is a tooling/authoring aid, not app runtime; keep it in .claude/, not shipped to users.

In-browser map creation/editing (requested 2026-06-20)

Let contributors draw and edit region polygons directly in the app, instead of hand-editing GeoJSON or round-tripping through geojson.io. Lowers the contribution bar a lot (the current flow asks people to author raw [lng, lat] rings by hand). Initially felt heavy-handed (a full path-editing UI), but it now pairs naturally with the other authoring/build-time tooling being considered: - A Leaflet drawing layer (e.g. leaflet-draw / react-leaflet editing) to add/move/delete polygon vertices on the existing map, emitting schema-shaped GeoJSON. - An export path: produce a ready-to-commit region JSON (or a prefilled GitLab merge-request / issue) so a drawn map becomes a contribution without local tooling. - Naturally combines with land-clipping (below) so a roughly-drawn coastal polygon is auto-trimmed to land, and with AI-assisted authoring (above) so the AI can draft a polygon the user then nudges by hand. - Open question: purely a contributor/authoring tool vs. a playful end-user feature ("draw your own region"). Keep authoring output reproducible either way.

Scalable calendar colors (requested 2026-06-20)

✅ Shipped 2026-06-20 in src/utils/calendarColor.ts. The new calendarColorScale(names) assigns colors to the set of calendars on screen at once: curated calendars keep their PALETTE color, the rest are walked in a stable (sorted) order and handed golden-angle-spaced hues so they fan out around the wheel instead of colliding on a raw name hash. App.tsx builds one scale from every calendar in the dataset, so a calendar keeps the same color across the map, the timeline, and as you scrub dates (legend ↔ map ↔ timeline agree). calendarColor(name) is retained for a lone name / fallback (now also golden-angle-based). Possible later polish: contrast/accessibility tuning so no generated color vanishes on the map tiles. Original framing of the problem: - Space hues apart (e.g. golden-angle increments) instead of a raw name hash, and/or assign colors per render so the calendars actually on screen are maximally distinct. - Keep determinism where it matters (legend ↔ map agreement within a view) and keep the curated palette for the handful of well-known calendars (or drop it if a good generator makes it redundant — that would touch calendarColor.test.ts). - Consider contrast/accessibility (avoid colors that vanish on the map tiles).

Rich descriptive-only answers

For non-convertible calendars, replace the plain "no year count" note with the kind of answer a real local would give. Example target for Boston, 1700-04-01, asked of a Massachusett person:

"We don't count years that way — but it is the [early-spring] moon, ~3 days after a new moon, and about N winters since [a remembered event]."

This is authentic in structure. Its pieces split sharply by cost:

Data accuracy


Resolved decisions

Source
1# Plan — When am I? (formerly "What Year Is It?")
2
3Status as of 2026-06-16. Owner: Pavel.
4
5## Goal of this milestone
6
7Ship an **end-to-end MVP**: a user picks a location on the map and a **full date**
8(year + month + day, BCE/CE), and the app resolves the matching region and era and
9displays the **local calendar year** using **real calendar math** (not just the era's
10metadata). Build the engine with **Vitest tests** and add **ESLint**.
11
12Boston is the proof-of-architecture region; the loop must generalize to any region file.
13
14---
15
16## Where we are today (baseline)
17
18Working:
19- [App.tsx](src/App.tsx): header + intro + map; echoes clicked lat/lng.
20- [src/components/MapPicker.tsx](src/components/MapPicker.tsx): the live map (`center=[20,0]`, `zoom=2`).
21- Data: region files [massachusett.json](src/data/calendars/massachusett.json)
22 (Massachusett reckoning) and [colonial-america.json](src/data/calendars/colonial-america.json)
23 (empire-wide Julian/Gregorian); schema docs in
24 [SCHEMA.json](src/data/calendars/SCHEMA.json) / [SCHEMA.md](src/data/calendars/SCHEMA.md).
25- Deploy: [.fw-ci.yml](.fw-ci.yml) builds and publishes `dist/` to GitLab Pages.
26
27Missing / debt:
28- **No date picker.**
29- **No resolution engine**: nothing loads the calendar JSON, runs point-in-polygon,
30 matches an era, or converts/displays a local year.
31- ~~**No build-time data validation** (README claims it exists — it does not).~~
32 **Resolved 2026-06-20:** `npm run validate:data`
33 ([scripts/validate-data.mts](scripts/validate-data.mts)) now validates every
34 region file and runs in `prebuild` + CI, so the README claim is true. See the
35 data-pipeline note below.
36- **Dead code**: [src/MapPicker.tsx](src/MapPicker.tsx) is an unused, broken duplicate
37 (no `center`/`zoom`) of the real component.
38- **Empty stubs**: [src/data/index.ts](src/data/index.ts), [src/hooks/index.ts](src/hooks/index.ts),
39 [src/types/index.ts](src/types/index.ts), [src/utils/index.ts](src/utils/index.ts).
40- **No tests, no lint.**
41
42---
43
44## Architecture for the MVP
45
46Core framing (from Q4): *"When am I?"* is addressed to **a person**, so the answer
47is **plural**. The engine does not pick one era — it **collects every valid answer** a local
48could give. Ask a Boston settler and an Algonquian person in 1700 and you get two answers;
49both are valid and we show both.
50
51Data flow: `click (lat,lng)` + `date` → **find all region(s)** containing the point →
52within each, **find all eras** whose date range covers the date (eras may overlap) →
53**convert** the date into each era's calendar (or fall back to descriptive-only) →
54**render the full list of answers**.
55
56### 1. Types — [src/types/index.ts](src/types/index.ts)
57Mirror the JSON schema and add result types:
58- `Geometry` (GeoJSON Polygon/MultiPolygon; coords are `[lng, lat]`).
59- `Region { name; geo: Geometry }`.
60- `Era { name; start; end; calendar; usedBy?; description?; sources? }` — `start`/`end` are
61 `YYYY-MM-DD | null`; new optional **`usedBy`** labels *whose* answer it is
62 (e.g. "British colonists", "Algonquian peoples"), which the plural model needs.
63- `CalendarRegion { region: Region; eras: Era[] }`.
64- `HistoricalDate` — see "Date representation" below.
65- `Answer { region: Region; era: Era; conversion: CalendarConversion }` — one possible
66 reply a local could give.
67- `Resolution { answers: Answer[]; missReason?: 'no-region' | 'no-era' }` — the engine
68 returns the **list** of answers (possibly empty, with a reason).
69- `CalendarConversion { system: string; localYearLabel: string; localDateLabel?: string; convertible: boolean }`
70 — `convertible: false` ⇒ descriptive-only (see §5).
71
72### 2. Data loading — [src/data/index.ts](src/data/index.ts)
73- Load all region files with `import.meta.glob('./calendars/*.json', { eager: true })`,
74 excluding `SCHEMA.json`.
75- Export a typed `regions: CalendarRegion[]`.
76- Runtime shape-guard so a malformed file fails loudly (complements build-time validation).
77
78### 3. Geometry — `src/utils/geo.ts`
79- `pointInPolygon(lat, lng, geometry): boolean` — ray-casting, supports `Polygon` and
80 `MultiPolygon`, respects GeoJSON `[lng, lat]` ordering and ring holes.
81- `findRegions(lat, lng, regions): CalendarRegion[]` — return **all** matches (regions
82 can overlap; UI must handle 0, 1, or many).
83
84### 4. Era matching — `src/utils/eras.ts`
85- `findEras(date, eras): Era[]` — return **all** eras whose half-open interval `[start, end)`
86 covers the date (`null` start = open past, `null` end = open present). **Eras may overlap**
87 (Q4): parallel calendars coexisting at the same place and time is expected, not an error.
88- Compare via Julian Day Number (see Date representation) so BCE/CE and proleptic-Gregorian
89 boundaries compare correctly.
90
91### 5. Calendar conversion — `src/utils/calendars.ts`
92- `convert(date, calendarName): CalendarConversion`.
93- Registry keyed by the era's `calendar` string. Two independent gates (Q3):
94 - **Acceptance** of a calendar into the app is a *data* decision (your rule: a wiki page
95 with enough sourced info). Not enforced here.
96 - **Convertibility** is whether a deterministic epoch+rule exists to compute a numeric
97 local year. Observational calendars with no continuous count (e.g. "Algonquian lunar
98 calendar") are **descriptive-only**: return `convertible: false`, still show the era,
99 its name/description/sources and the Gregorian year for reference, with a clear
100 "no continuous numeric year to convert to" note — **never silently mislabel**.
101- Convertible set (Q1): Gregorian + hand-rolled Julian, plus `@js-temporal/polyfill`
102 systems (Islamic, Hebrew, Persian, Japanese, Chinese, …).
103- **MVP scope for descriptive-only** (decided): render `description` + `sources` links
104 (e.g. a Wikipedia explanation) + Gregorian reference year. **No extra per-calendar data
105 needed** — the schema already has `sources`. The rich answer's **lunar phase** has since
106 shipped (computed, no data needed); the remaining pieces (moon *name* +
107 "winters since event") are still a **stretch goal**, see below.
108
109### 6. UI
110- `src/components/DatePicker.tsx`: year + month + day inputs + BCE/CE toggle; emits a
111 `HistoricalDate`. Validate ranges (days-in-month, no year 0 in historical numbering).
112- `src/components/ResultPanel.tsx`: renders **the list of answers** — for each, *whose*
113 answer (`usedBy`), the calendar system, the local year/date (or descriptive-only note),
114 description, and sources. Framing: "Ask a British colonist → …; ask an Algonquian
115 person → …". Plus miss states (no region / no era covering the date).
116- [App.tsx](src/App.tsx): hold `{ lat, lng }` + `HistoricalDate`, call the engine, render the panel.
117
118### 7. Cleanup
119- Delete [src/MapPicker.tsx](src/MapPicker.tsx) (dead duplicate).
120- Replace empty `index.ts` stubs with real exports or delete the unused ones.
121
122---
123
124## Data pipeline (lightweight, to make the README honest)
125
126**Shipped 2026-06-20** as [scripts/validate-data.mts](scripts/validate-data.mts)
127(`npm run validate:data`), wired into `prebuild` and CI.
128**Decision:** rather than promote [SCHEMA.json](src/data/calendars/SCHEMA.json)
129to a real JSON Schema / zod schema, the validator itself is the authoritative,
130machine-checkable schema. A hand-rolled validator covers semantic checks a plain
131JSON Schema cannot express — dates that actually parse, geometry rings that are
132closed with in-range `[lng, lat]`, and `[start, end)` ordering — and avoids a
133second spec that could drift from the validator. SCHEMA.json / SCHEMA.md remain
134the human-readable field docs and now point at the validator. Overlapping eras
135are intentionally **not** flagged (Q4).
136
137- ~~Promote [SCHEMA.json](src/data/calendars/SCHEMA.json) to a real **JSON Schema** (or a
138 zod schema) and~~ add `npm run validate:data` that checks every region file:
139 required fields, valid geometry, parseable proleptic-Gregorian dates. **Overlapping eras
140 are allowed** (Q4) — do not flag them.
141- Add the optional `usedBy` field to the schema (back-compatible).
142- Model **coexistence**: around colonization the colonial calendars and the Massachusett
143 lunar reckoning apply at the same place and time, each tagged with `usedBy`. (Now split
144 across two overlapping regions — [massachusett.json](src/data/calendars/massachusett.json)
145 and [colonial-america.json](src/data/calendars/colonial-america.json) — rather than one
146 Boston file.)
147- Wire validation into `prebuild` and into CI so bad data can't deploy.
148
149---
150
151## Verify tooling (adopt from `~/code/share`)
152
153Reuse Pavel's established pattern (reference: `~/code/share/web/scripts/*.mts`):
154- A `.mts` Playwright script run via `tsx` that **auto-starts the Vite dev server**,
155 forces deterministic app state via **URL query params** (here: a `?demo=` that pre-sets
156 picked location + date so the answers panel renders without manual clicking), stabilizes
157 any volatile content, screenshots to `screenshots/<category>/`, and **checks them in**.
158- ImageMagick `magick` composites for documentation imagery.
159- Most valuable once the ResultPanel exists → schedule in **Phase 4** (UI). The `?demo=`
160 state hook should be designed into App during Phase 4, not bolted on later.
161
162### Per-calendar PR screenshots
163A `screenshot:calendar -- <file.json>` script that renders **one calendar's** answers so a
164**calendar PR can include illustrative screenshots showing any major issues**:
165- Compute a point **inside the region** (polygon centroid / point-on-surface) and force the
166 app there via `?demo=` with a **representative date per era** (e.g. midpoint of each era's
167 range). Screenshot the answers panel for each.
168- Optionally a single composite per calendar (map + answers) via `magick`.
169- This makes review concrete: a wrong region polygon, an off-by-a-century era boundary, a
170 miscategorized "convertible vs descriptive-only", or a broken conversion all show up
171 **visually** in the PR. Reviewer (Pavel) checks the screenshot against Wikipedia/facts.
172- Document this in the contributor section so each calendar PR attaches its screenshots.
173 Depends on the `?demo=` hook + ResultPanel → **Phase 4+**.
174
175## Testing & lint
176
177- **Vitest** + config; unit tests for: `pointInPolygon` (inside/outside/edge/MultiPolygon),
178 `findEras` (boundaries, open intervals, **overlapping eras**, BCE), `convert` (known
179 date↔year pairs per system, descriptive-only fallback), and a data-validation test that
180 loads every region file.
181- **ESLint** (flat config, TS + react-hooks) + `npm run lint`.
182- Add `test`, `lint`, `validate:data` scripts to [package.json](package.json).
183- Update [.fw-ci.yml](.fw-ci.yml): run `lint` + `test` before `build`.
184
185---
186
187## Documentation gate (required before "done")
188
189- README: document the date picker, the resolution engine, the plural-answers model, the
190 list of **convertible vs descriptive-only** calendars, the new scripts (`test`, `lint`,
191 `validate:data`), and the real validation step (replacing the current aspirational claim).
192- Keep [SCHEMA.md](src/data/calendars/SCHEMA.md) in sync with the promoted JSON Schema.
193- Note any data-format changes back into the contributor section.
194
195---
196
197## Phased execution
198
1991. **Foundations** — types, data loader, delete dead `MapPicker`, Vitest + ESLint scaffolding, CI.
2002. **Engine** — geo + era matching + conversion registry, with tests. Gregorian/Julian first.
2013. **Library-backed calendars** — wire the chosen conversion lib (Q1), add tests.
2024. **UI** — DatePicker + ResultPanel + App wiring; handle all miss states.
2035. **Data pipeline** — ✅ `validate:data` + `prebuild`/CI gate (see note above; the
204 validator is the schema, no separate JSON Schema file).
2056. **Docs** — README + SCHEMA sync. ✅ Generalization proven 2026-06-20 with three
206 region files beyond Boston:
207 [iran.json](src/data/calendars/iran.json) (Solar Hijri + lunar Hijri, exercising the
208 Temporal-backed Persian and Islamic converters),
209 [russian-america.json](src/data/calendars/russian-america.json) (a second Julian→Gregorian
210 cutover, at the 1867 Alaska transfer), and
211 [oregon-country.json](src/data/calendars/oregon-country.json) (Hudson's Bay Company,
212 covering the Seattle area). Note: the Temporal **`chinese`** calendar was the
213 first pick for the convertible-library proof but is broken in
214 `@js-temporal/polyfill@0.5.1` (throws on every date — "Unexpected leap month
215 suffix"; no fixed version available), so **Persian/Iran** was used instead.
216
217 **The five most-populous calendars added 2026-06-20** (covering the most
218 humans × time, per Pavel's request): [china.json](src/data/calendars/china.json)
219 (Chinese sexagenary cycle — descriptive-only, hand-rolled stem-branch/zodiac
220 naming, since the Temporal lunisolar `chinese` is broken),
221 [india.json](src/data/calendars/india.json) (Saka, existing Temporal converter),
222 [japan.json](src/data/calendars/japan.json) (a pre-1873 lunisolar
223 descriptive era + the modern Japanese **nengō** era, Temporal `japanese` using
224 `era`/`eraYear`, scoped to Meiji+),
225 [thailand.json](src/data/calendars/thailand.json) (Buddhist Era, existing
226 converter), and [ethiopia.json](src/data/calendars/ethiopia.json) (Ethiopian
227 calendar, **hand-rolled via JDN** because the Temporal `ethiopic` *and*
228 `coptic` calendars also throw in this polyfill — same class of bug as
229 `chinese`). New converters live in
230 [src/utils/calendars.ts](src/utils/calendars.ts) with tests.
231
232---
233
234## Stretch goals (post-MVP)
235
236### AI-assisted dataset authoring (requested 2026-06-20)
237**✅ Shipped 2026-06-20** as the `new-calendar` skill
238([.claude/skills/new-calendar/SKILL.md](.claude/skills/new-calendar/SKILL.md)):
239it walks an author through researching a place/people's calendar(s) with sourced
240citations, drafting the GeoJSON polygon and eras, picking the right converter (or
241descriptive-only), and running `validate:data` / `gen:badges` /
242`screenshot:calendar` so the result is PR-ready — with the project's "never
243invent, always source" rule front and centre. A review/QA skill remains a
244possible follow-up. Original sketch of what the skill does:
245- Take a place + people/polity as input (e.g. "the Mughal Empire", "Coast Salish around
246 Puget Sound") and **research** the calendar system(s), era boundaries, and *whose*
247 reckoning it is (`usedBy`), gathering **citable sources** (the project's acceptance gate
248 — never invent events or moon names; see "Data accuracy" below).
249- Draft the **region polygon** (GeoJSON `[lng, lat]`, closed rings) and the eras with
250 proleptic-Gregorian `start`/`end`, choosing the right `calendar` string so it hits an
251 existing converter (or flagging it as descriptive-only).
252- Decide **convertible vs descriptive-only** correctly, and if a new convertible system is
253 needed, note the converter work in [src/utils/calendars.ts](src/utils/calendars.ts)
254 (with the `@js-temporal/polyfill` caveat — e.g. `chinese` is currently broken).
255- Run `npm run validate:data`, `npm run gen:badges`, and
256 `npm run screenshot:calendar -- <file>` so the output is PR-ready with review screenshots.
257- Possibly a second, smaller skill to **review/QA** an existing dataset against its sources.
258This is a tooling/authoring aid, not app runtime; keep it in `.claude/`, not shipped to users.
259
260### In-browser map creation/editing (requested 2026-06-20)
261Let contributors **draw and edit region polygons directly in the app**, instead of
262hand-editing GeoJSON or round-tripping through geojson.io. Lowers the contribution bar
263a lot (the current flow asks people to author raw `[lng, lat]` rings by hand). Initially
264felt heavy-handed (a full path-editing UI), but it now pairs naturally with the other
265authoring/build-time tooling being considered:
266- A Leaflet drawing layer (e.g. `leaflet-draw` / `react-leaflet` editing) to add/move/delete
267 polygon vertices on the existing map, emitting schema-shaped GeoJSON.
268- An **export** path: produce a ready-to-commit region JSON (or a prefilled GitLab
269 merge-request / issue) so a drawn map becomes a contribution without local tooling.
270- Naturally combines with **land-clipping** (below) so a roughly-drawn coastal polygon is
271 auto-trimmed to land, and with **AI-assisted authoring** (above) so the AI can draft a
272 polygon the user then nudges by hand.
273- Open question: purely a contributor/authoring tool vs. a playful end-user feature
274 ("draw your own region"). Keep authoring output reproducible either way.
275
276### Scalable calendar colors (requested 2026-06-20)
277**✅ Shipped 2026-06-20** in [src/utils/calendarColor.ts](src/utils/calendarColor.ts).
278The new `calendarColorScale(names)` assigns colors to the *set* of calendars on
279screen at once: curated calendars keep their `PALETTE` color, the rest are walked
280in a stable (sorted) order and handed **golden-angle-spaced** hues so they fan out
281around the wheel instead of colliding on a raw name hash. [App.tsx](src/App.tsx)
282builds one scale from every calendar in the dataset, so a calendar keeps the same
283color across the map, the timeline, and as you scrub dates (legend ↔ map ↔ timeline
284agree). `calendarColor(name)` is retained for a lone name / fallback (now also
285golden-angle-based). Possible later polish: contrast/accessibility tuning so no
286generated color vanishes on the map tiles. Original framing of the problem:
287- Space hues apart (e.g. golden-angle increments) instead of a raw name hash, and/or
288 assign colors **per render** so the calendars actually on screen are maximally distinct.
289- Keep determinism where it matters (legend ↔ map agreement within a view) and keep the
290 curated palette for the handful of well-known calendars (or drop it if a good generator
291 makes it redundant — that would touch [calendarColor.test.ts](src/utils/calendarColor.test.ts)).
292- Consider contrast/accessibility (avoid colors that vanish on the map tiles).
293
294### Rich descriptive-only answers
295For non-convertible calendars, replace the plain "no year count" note with the kind of
296answer a real local would give. Example target for **Boston, 1700-04-01**, asked of a
297**Massachusett person**:
298
299> "We don't count years that way — but it is the **[early-spring] moon**, **~3 days after a
300> new moon**, and about **N winters since [a remembered event]**."
301
302This is authentic *in structure*. Its pieces split sharply by cost:
303
304- **Computable for free (no per-region data):**
305 - **Lunar phase / days since new moon** — *shipped*. `lunarPhaseLabel` in
306 `src/utils/calendars.ts` computes the night-of-the-moon + phase from the Julian Day
307 Number; the "Algonquian lunar calendar" answer now leads with it (still
308 `convertible: false`, no year) instead of the bare "no year count" note.
309 - **Eclipse-anchored "winters since"** — superseded by the per-region
310 `anchorEvents` approach below (eclipses turned out to need either a heavy
311 ephemeris or the same curated data, so they're just one *kind* of anchor).
312- **"Winters since" via per-region anchor events — ✅ Shipped 2026-06-20.** Pavel's
313 call (the most reliable option): each anchor must be a **documented, sourced**
314 event supplied in the data; the engine only counts the winters, never invents
315 one. Implemented as an optional `Era.anchorEvents` (`{ name, date, sources }[]`),
316 with `wintersSince()` in [src/utils/calendars.ts](src/utils/calendars.ts)
317 picking the most recent anchor at/before the date and reporting "about N winters
318 since &lt;event&gt;" (counted as completed calendar years so anniversaries aren't
319 off-by-one). Wired through [resolve.ts](src/utils/resolve.ts) →
320 `CalendarConversion.reckoning` → a sourced line in
321 [ResultPanel.tsx](src/components/ResultPanel.tsx); validated in
322 `validate-data.mts` and the data loader; documented in SCHEMA.md/README.
323 [massachusett.json](src/data/calendars/massachusett.json) carries the first
324 anchor (the 1616–1619 epidemic → "about 84 winters since" in 1700).
325- **Still needs curated, sourced per-region data (the expensive part — hence stretch):**
326 - **Moon names** — vary by nation, inconsistently attested; must be cited, not invented.
327 - More **historical-event anchors** for other descriptive calendars — must be real,
328 dateable, locally remembered events, **not** invented ones (the "asteroid crash" in
329 the brainstorm was illustrative only — no such event occurred).
330- **Schema:** the `anchorEvents` part shipped (above); optional `reckoning` text and a
331 `moons` list remain optional + back-compatible future additions.
332
333### Data accuracy
334- Refine Boston's generic `"Algonquian lunar calendar"` to the specific **Massachusett**
335 people (via `usedBy`), with sources. "Algonquian" is a language family, not one calendar.
336
337---
338
339## Resolved decisions
340
341- **Q1 — Conversion library.** ✅ `@js-temporal/polyfill` for library-backed systems
342 (Islamic, Hebrew, Persian, Japanese, Chinese, …) **plus a hand-rolled Julian↔Gregorian**
343 (Temporal has no Julian calendar).
344- **Q2 — Historical date representation.** ✅ JSON `start`/`end` are **proleptic Gregorian**
345 (rule written into SCHEMA.md). Internally normalize to a **Julian Day Number** for all
346 comparisons so BCE/CE and pre-1582 dates sort correctly. **UTC is not involved** — it is a
347 time-of-day/timezone standard, not a calendar; we resolve dates, not clock time, and avoid
348 JS `Date` in the engine (it mishandles BCE and drags in timezones).
349- **Q3 — Descriptive-only calendars.** ✅ Two gates: *acceptance* (data decision: a sourced
350 wiki page is enough) vs *convertibility* (a deterministic epoch+rule exists). Calendars
351 that pass acceptance but not convertibility (e.g. Algonquian lunar) are shown with their
352 name/description/sources + the Gregorian year for reference and a clear "no continuous
353 numeric year to convert to" note. Never hidden, never mislabeled. (See "Algonquian
354 answers" below for the nuance the UI should convey.)
355- **Q4 — Plural answers / overlap.** ✅ The engine returns **all** valid answers.
356 Geographic region overlap → show all matched regions. Eras within a region **may overlap
357 in time** → show all concurrent calendars, because "When am I?" is asked of a person
358 and different people legitimately answer differently.
359- **Q5 — Modern global default.** ✅ (implemented 2026-06-20, [src/utils/resolve.ts](src/utils/resolve.ts))
360 The Gregorian calendar is the world's near-universal *civil* calendar today, so for dates
361 from **1927** onward (chosen cutoff — just after the last major national adoption, Turkey
362 1926) the engine appends a synthetic **"most of the world" Gregorian answer** anywhere on
363 Earth, unless a region already supplies a Gregorian answer for that date (dedup, no double
364 Gregorian). So most modern clicks resolve even with no specific region, and a place with a
365 local calendar shows both (e.g. modern Iran → Persian **and** Gregorian). It is engine-only:
366 **not** a region polygon, so it never shades the map (a world polygon would flood it); a
367 matching band is mirrored onto the year timeline. A single global cutoff is a deliberate
368 simplification (adoption was staggered: 1582/1752/1918/1926/1949/2016…), hence "most of
369 the world", not a universal claim. The cutoff is one constant (`MODERN_DEFAULT_START_YEAR`).
370 Open follow-up: the default currently applies over oceans too — could be land-bounded once
371 the land-clipping work (stretch goals) lands.