26a02b71 2026-06-19 Pavel Safronov | 1 | # Plan — When am I? (formerly "What Year Is It?") |
d96c425a 2026-06-17 Pavel Safronov | 2 | |
| 3 | Status as of 2026-06-16. Owner: Pavel. |
| 4 | |
| 5 | ## Goal of this milestone |
| 6 | |
| 7 | Ship 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 |
| 9 | displays the **local calendar year** using **real calendar math** (not just the era's |
| 10 | metadata). Build the engine with **Vitest tests** and add **ESLint**. |
| 11 | |
| 12 | Boston is the proof-of-architecture region; the loop must generalize to any region file. |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## Where we are today (baseline) |
| 17 | |
| 18 | Working: |
| 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`). |
26a02b71 2026-06-19 Pavel Safronov | 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 |
d96c425a 2026-06-17 Pavel Safronov | 24 | [SCHEMA.json](src/data/calendars/SCHEMA.json) / [SCHEMA.md](src/data/calendars/SCHEMA.md). |
a6030ee4 2026-10-27 Pavel Safronov | 25 | - Deploy: [.fw-ci.yml](.fw-ci.yml) builds and publishes `dist/` to GitLab Pages. |
d96c425a 2026-06-17 Pavel Safronov | 26 | |
| 27 | Missing / 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. |
30de6faf 2026-06-20 Pavel Safronov | 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. |
d96c425a 2026-06-17 Pavel Safronov | 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 | |
26a02b71 2026-06-19 Pavel Safronov | 46 | Core framing (from Q4): *"When am I?"* is addressed to **a person**, so the answer |
d96c425a 2026-06-17 Pavel Safronov | 47 | is **plural**. The engine does not pick one era — it **collects every valid answer** a local |
| 48 | could give. Ask a Boston settler and an Algonquian person in 1700 and you get two answers; |
| 49 | both are valid and we show both. |
| 50 | |
| 51 | Data flow: `click (lat,lng)` + `date` → **find all region(s)** containing the point → |
| 52 | within 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) |
| 57 | Mirror 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 |
8961f70b 2026-06-20 Pavel Safronov | 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. |
d96c425a 2026-06-17 Pavel Safronov | 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 | |
30de6faf 2026-06-20 Pavel Safronov | 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) |
| 129 | to a real JSON Schema / zod schema, the validator itself is the authoritative, |
| 130 | machine-checkable schema. A hand-rolled validator covers semantic checks a plain |
| 131 | JSON Schema cannot express — dates that actually parse, geometry rings that are |
| 132 | closed with in-range `[lng, lat]`, and `[start, end)` ordering — and avoids a |
| 133 | second spec that could drift from the validator. SCHEMA.json / SCHEMA.md remain |
| 134 | the human-readable field docs and now point at the validator. Overlapping eras |
| 135 | are 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: |
d96c425a 2026-06-17 Pavel Safronov | 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). |
26a02b71 2026-06-19 Pavel Safronov | 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.) |
d96c425a 2026-06-17 Pavel Safronov | 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 | |
| 153 | Reuse 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 |
| 163 | A `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). |
a6030ee4 2026-10-27 Pavel Safronov | 183 | - Update [.fw-ci.yml](.fw-ci.yml): run `lint` + `test` before `build`. |
d96c425a 2026-06-17 Pavel Safronov | 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 | |
| 199 | 1. **Foundations** — types, data loader, delete dead `MapPicker`, Vitest + ESLint scaffolding, CI. |
| 200 | 2. **Engine** — geo + era matching + conversion registry, with tests. Gregorian/Julian first. |
| 201 | 3. **Library-backed calendars** — wire the chosen conversion lib (Q1), add tests. |
| 202 | 4. **UI** — DatePicker + ResultPanel + App wiring; handle all miss states. |
c7d226e1 2026-06-20 Pavel Safronov | 203 | 5. **Data pipeline** — ✅ `validate:data` + `prebuild`/CI gate (see note above; the |
| 204 | validator is the schema, no separate JSON Schema file). |
| 205 | 6. **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. |
d96c425a 2026-06-17 Pavel Safronov | 216 | |
483f1c57 2026-06-21 Pavel Safronov | 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 | |
d96c425a 2026-06-17 Pavel Safronov | 232 | --- |
| 233 | |
| 234 | ## Stretch goals (post-MVP) |
| 235 | |
c7d226e1 2026-06-20 Pavel Safronov | 236 | ### AI-assisted dataset authoring (requested 2026-06-20) |
483f1c57 2026-06-21 Pavel Safronov | 237 | **✅ Shipped 2026-06-20** as the `new-calendar` skill |
| 238 | ([.claude/skills/new-calendar/SKILL.md](.claude/skills/new-calendar/SKILL.md)): |
| 239 | it walks an author through researching a place/people's calendar(s) with sourced |
| 240 | citations, drafting the GeoJSON polygon and eras, picking the right converter (or |
| 241 | descriptive-only), and running `validate:data` / `gen:badges` / |
| 242 | `screenshot:calendar` so the result is PR-ready — with the project's "never |
| 243 | invent, always source" rule front and centre. A review/QA skill remains a |
| 244 | possible follow-up. Original sketch of what the skill does: |
c7d226e1 2026-06-20 Pavel Safronov | 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. |
| 258 | This is a tooling/authoring aid, not app runtime; keep it in `.claude/`, not shipped to users. |
| 259 | |
73912a47 2026-06-20 Pavel Safronov | 260 | ### In-browser map creation/editing (requested 2026-06-20) |
| 261 | Let contributors **draw and edit region polygons directly in the app**, instead of |
| 262 | hand-editing GeoJSON or round-tripping through geojson.io. Lowers the contribution bar |
| 263 | a lot (the current flow asks people to author raw `[lng, lat]` rings by hand). Initially |
| 264 | felt heavy-handed (a full path-editing UI), but it now pairs naturally with the other |
| 265 | authoring/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) |
483f1c57 2026-06-21 Pavel Safronov | 277 | **✅ Shipped 2026-06-20** in [src/utils/calendarColor.ts](src/utils/calendarColor.ts). |
| 278 | The new `calendarColorScale(names)` assigns colors to the *set* of calendars on |
| 279 | screen at once: curated calendars keep their `PALETTE` color, the rest are walked |
| 280 | in a stable (sorted) order and handed **golden-angle-spaced** hues so they fan out |
| 281 | around the wheel instead of colliding on a raw name hash. [App.tsx](src/App.tsx) |
| 282 | builds one scale from every calendar in the dataset, so a calendar keeps the same |
| 283 | color across the map, the timeline, and as you scrub dates (legend ↔ map ↔ timeline |
| 284 | agree). `calendarColor(name)` is retained for a lone name / fallback (now also |
| 285 | golden-angle-based). Possible later polish: contrast/accessibility tuning so no |
| 286 | generated color vanishes on the map tiles. Original framing of the problem: |
73912a47 2026-06-20 Pavel Safronov | 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 | |
d96c425a 2026-06-17 Pavel Safronov | 294 | ### Rich descriptive-only answers |
| 295 | For non-convertible calendars, replace the plain "no year count" note with the kind of |
| 296 | answer 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 | |
| 302 | This is authentic *in structure*. Its pieces split sharply by cost: |
| 303 | |
| 304 | - **Computable for free (no per-region data):** |
8961f70b 2026-06-20 Pavel Safronov | 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. |
483f1c57 2026-06-21 Pavel Safronov | 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 <event>" (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):** |
d96c425a 2026-06-17 Pavel Safronov | 326 | - **Moon names** — vary by nation, inconsistently attested; must be cited, not invented. |
483f1c57 2026-06-21 Pavel Safronov | 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. |
d96c425a 2026-06-17 Pavel Safronov | 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 |
26a02b71 2026-06-19 Pavel Safronov | 357 | in time** → show all concurrent calendars, because "When am I?" is asked of a person |
d96c425a 2026-06-17 Pavel Safronov | 358 | and different people legitimately answer differently. |
b92a4280 2026-06-20 Pavel Safronov | 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. |