57deb62b51
Every planet position now carries its whole-sign house (1-12); transits report the natal house each transiting planet currently occupies rather than a fresh "houses for right now" chart, matching how transits are normally read. Surfaced in both text and HTML output, fully translated.
196 lines
9.7 KiB
Markdown
196 lines
9.7 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## Project goal
|
||
|
||
A Pebble smartwatch app that produces a daily personalized tarot (Celtic
|
||
Cross) and astrology (natal chart + transits) reading. Currently only the
|
||
**core engine** exists: a plain-C, dependency-free library plus a
|
||
standalone CLI binary (`dist/deck-engine`) that can be built and run
|
||
without any Pebble SDK or emulator. The watchapp itself (Pebble C/UI,
|
||
resource packs) has not been started yet — the engine is deliberately
|
||
built so it can be linked straight into it later without rework (see
|
||
"Portability to the watch" below).
|
||
|
||
## Build & run
|
||
|
||
```bash
|
||
cd engine
|
||
make # builds dist/deck-engine, dist/img/, dist/i18n/, dist/run.sh, dist/user.properties
|
||
make test # builds and runs engine/tests/smoke_test.c
|
||
make clean # removes build/ and the generated binary/img/i18n (never touches dist/user.properties)
|
||
```
|
||
|
||
Build output goes to `dist/`, a sibling of `engine/` and `res/` (not
|
||
inside `engine/`). There is no `make install`; `dist/` is meant to be run
|
||
in place.
|
||
|
||
Two ways to run a reading:
|
||
|
||
```bash
|
||
# 1. Direct CLI, all inputs explicit:
|
||
dist/deck-engine --seed "2026-07-03" \
|
||
--birth-date 1990-05-14 --birth-time 14:32 --birth-utc-offset 2 \
|
||
--birth-lat 52.5200 --birth-lon 13.4050 \
|
||
[--date YYYY-MM-DDTHH:MM] [--format text|html] [--lang en|de] [--i18n-dir <path>]
|
||
|
||
# 2. dist/run.sh [text|html] [lang] — wraps the binary for daily use: seed
|
||
# and --date come from the OS clock (today's date / current UTC time),
|
||
# birth data + default language are read from dist/user.properties
|
||
# next to the script; the optional [lang] arg overrides that language
|
||
# for a single run.
|
||
```
|
||
|
||
`dist/user.properties` is seeded once from `engine/scripts/user.properties.template`
|
||
with placeholder values (`YOUR_BIRTH_DATE_HERE`, etc.) and is **never
|
||
overwritten by later builds** — `run.sh` refuses to run (with a clear
|
||
error) until the placeholders are replaced with real values.
|
||
|
||
Run a single smoke test by editing `engine/tests/smoke_test.c`'s `main()`
|
||
temporarily, or just read its assertions — there's no test filter flag,
|
||
the whole suite runs in milliseconds.
|
||
|
||
## Architecture
|
||
|
||
```
|
||
deck_in_a_dash/
|
||
res/ Tarot card art (22 RWS Major Arcana JPEGs),
|
||
Waite's "Pictorial Key to the Tarot" (PDF,
|
||
public domain — source of all card/spread
|
||
text), logo, research notes.
|
||
engine/
|
||
third_party/astronomy/ Vendored cosinekitty/astronomy C library
|
||
(MIT, pinned commit — see VENDORED.md).
|
||
Unmodified from upstream.
|
||
src/
|
||
rng.h/.c Deterministic string-seeded PRNG.
|
||
tarot.h/.c Celtic Cross spread draw logic.
|
||
tarot_data.c 22 cards' names/meanings + 10 position
|
||
names/descriptions (Waite's own text).
|
||
astro.h/.c Natal chart + daily transits.
|
||
i18n.h/.c key=value translation catalog + English fallback.
|
||
reading.h/.c Ties tarot + astro together; text/HTML output.
|
||
main.c CLI entry point.
|
||
i18n/en.lang, i18n/de.lang Translation source files, one per language.
|
||
tests/smoke_test.c
|
||
scripts/run.sh, scripts/user.properties.template
|
||
Makefile
|
||
dist/ Build output (gitignored-style; see Build & run).
|
||
```
|
||
|
||
### Data flow / the real API
|
||
|
||
`reading_generate(seed, birth, utc_moment, &DailyReading)` in
|
||
`reading.h` is the single entry point that matters — it's what the
|
||
watchapp will call once it exists. It populates a plain struct
|
||
(`NatalChart` + `DailyTransits` + `CelticCrossSpread`) that the caller
|
||
walks directly to build a UI. `reading_print_text`/`reading_print_html`
|
||
are desktop-only conveniences for inspecting a reading (used by the CLI);
|
||
the watchapp will never call them.
|
||
|
||
### Determinism (`rng.c`)
|
||
|
||
The Celtic Cross draw must be exactly reproducible from a seed string, on
|
||
any platform (including the watch's ARM core later), so `rng.c` is pure
|
||
integer arithmetic: FNV-1a hashes the seed string, splitmix64 streams
|
||
from it, and a partial Fisher–Yates shuffle (`tarot.c`) draws the 10
|
||
cards plus a reversal bit per card from that same stream, in one pass.
|
||
|
||
**Known-sharp-edge**: `rng_next_bounded`'s rejection-sampling limit must
|
||
stay a `uint64_t`. When `bound` evenly divides 2^32 (true for `bound ==
|
||
2`, used by every reversed-card coin flip), the true rejection threshold
|
||
*is* 2^32, which silently truncates to 0 in a `uint32_t` and turns the
|
||
loop into an infinite one. This exact bug shipped once already — if
|
||
you touch this function, keep the intermediate type 64-bit.
|
||
|
||
### Astrology (`astro.c`)
|
||
|
||
- Bodies: Sun, Moon, Mercury–Pluto (`Body` enum in `astro.h`, prefixed
|
||
`PLANET_*` — deliberately *not* `BODY_*`, because Astronomy Engine's
|
||
own `astro_body_t` already defines `BODY_SUN`, `BODY_MOON`, etc., and
|
||
both headers are included together in `astro.c`).
|
||
- **Positions must go through `Astronomy_GeoVector` + `Astronomy_Ecliptic`,
|
||
not `Astronomy_EclipticLongitude`.** The latter computes *heliocentric*
|
||
longitude and outright rejects `BODY_SUN` (`ASTRO_INVALID_BODY`) —
|
||
wrong for astrology, which needs apparent geocentric position. This
|
||
also shipped once and was caught by the natal-Sun-sign smoke test.
|
||
- Houses use the **Whole Sign** system (house 1 = the Ascendant's sign,
|
||
houses follow zodiacally) — no Placidus/Koch cusp trig. Astronomy
|
||
Engine has no Ascendant function; it's computed directly in `astro.c`
|
||
from sidereal time + a standard low-precision obliquity polynomial +
|
||
the RAMC/obliquity/latitude identity (Duffett-Smith & Zwart).
|
||
- Every `PlanetPosition` (`astro.h`) carries a whole-sign `house` (1-12),
|
||
via `assign_houses()`/`whole_sign_house()`. **`DailyTransits.bodies[*].house`
|
||
is always relative to the *natal* Ascendant**, not a fresh "houses for
|
||
right now" chart — that's the standard, personalized way transits are
|
||
read (e.g. "transiting Jupiter is in your 5th house"), and it's why
|
||
`astro_compute_daily_transits` takes the natal chart as a parameter in
|
||
the first place (also used for aspect detection).
|
||
- Aspects (conjunction/sextile/square/trine/opposition) use an 8° orb
|
||
when a luminary (Sun/Moon) is involved, 6° otherwise.
|
||
|
||
### Tarot (`tarot.c`, `tarot_data.c`)
|
||
|
||
Only the 22 Major Arcana are modelled (`TAROT_DECK_SIZE`) — matches the
|
||
art actually in `res/img/`. The 10 Celtic Cross positions
|
||
(`CelticCrossPosition` in `tarot.h`) follow **Waite's own original
|
||
drawing order** from *The Pictorial Key to the Tarot*, Part III §7 ("An
|
||
Ancient Celtic Method of Divination"): Present → Challenge → **Crown →
|
||
Foundation → Recent Past** → Near Future → Attitude → Environment →
|
||
Hopes/Fears → Outcome. Note this differs from the reordering used by
|
||
most modern tutorials (which typically put Foundation/Recent Past before
|
||
Crown) — `res/2026_07_03_celtic_cross_spread.txt` has an example of that
|
||
more common (but less original) ordering; don't use it as a reference for
|
||
the position enum order. Card and position text in `tarot_data.c` is
|
||
condensed from Waite's own wording in Part III §3 (public domain).
|
||
|
||
### Translations (`i18n.c`, `engine/i18n/*.lang`)
|
||
|
||
All display text (planet/sign/moon-phase/aspect names, tarot card names
|
||
and meanings, Celtic Cross position names, and `reading.c`'s section
|
||
headers) is looked up in a small process-global catalog loaded by
|
||
`i18n_load(path)` from a `key=value` file, via `i18n_get(key, fallback)`.
|
||
Every call site passes the hardcoded English string as `fallback`, so a
|
||
missing file, a missing key, or an untranslated language degrades to
|
||
English rather than showing a raw key or nothing — this is what lets
|
||
`engine/i18n/de.lang` (or any future language) be filled in incrementally.
|
||
|
||
Keys are namespaced by the *slug* tables in `tarot_data.c`/`astro.c`
|
||
(`card.<slug>.name`, `card.<slug>.upright/reversed`,
|
||
`position.<slug>.name/desc`, `body.<slug>`, `sign.<slug>`,
|
||
`moonphase.<slug>`, `aspect.<slug>`), plus `ui.*` keys for `reading.c`'s
|
||
own labels — deliberately keyed off separate slug strings rather than
|
||
the C enum names, so renaming an enumerator can never silently break a
|
||
`.lang` file. **To add a language**: copy `engine/i18n/en.lang` to
|
||
`<code>.lang` in the same directory (keys must match exactly) and
|
||
translate the values — no source changes needed, `make` picks up any
|
||
`*.lang` file there automatically.
|
||
|
||
`i18n_load` uses stdio (`fopen`/`fgets`), so — like `main.c` and
|
||
`reading_print_*` — it's a desktop-only entry point; `i18n_get` itself
|
||
is a pure fixed-size-array lookup with no heap allocation, so it's fine
|
||
to port to the watch once it has its own (non-file-based) way to
|
||
populate the catalog, e.g. from a compiled-in resource.
|
||
|
||
### Portability to the watch
|
||
|
||
`rng`, `tarot`, `tarot_data`, `astro`, `i18n_get`, and `reading_generate`
|
||
are kept free of `stdio`/CLI assumptions specifically so they can link
|
||
into the Pebble watchapp unchanged later. Only `reading_print_text`/
|
||
`_html` (text formatting), `main.c` (CLI arg parsing), and `i18n_load`
|
||
(reads a file from disk) are desktop-only and won't port as-is.
|
||
|
||
## Licensing
|
||
|
||
- Engine code (`engine/src/`, `engine/scripts/`, `engine/Makefile`):
|
||
MIT, per the repo's top-level `LICENSE`.
|
||
- `engine/third_party/astronomy/`: vendored MIT code, unmodified — see
|
||
`VENDORED.md` for the pinned upstream commit.
|
||
- Card/spread text in `tarot_data.c`/`engine/i18n/en.lang`: condensed
|
||
from A. E. Waite's *The Pictorial Key to the Tarot* (1911), public
|
||
domain.
|
||
- `engine/i18n/de.lang`'s card/spread text is an original translation of
|
||
that same public-domain English text, made for this project — not
|
||
taken from any specific published German edition.
|