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.
9.7 KiB
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
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:
# 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 (
Bodyenum inastro.h, prefixedPLANET_*— deliberately notBODY_*, because Astronomy Engine's ownastro_body_talready definesBODY_SUN,BODY_MOON, etc., and both headers are included together inastro.c). - Positions must go through
Astronomy_GeoVector+Astronomy_Ecliptic, notAstronomy_EclipticLongitude. The latter computes heliocentric longitude and outright rejectsBODY_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.cfrom sidereal time + a standard low-precision obliquity polynomial + the RAMC/obliquity/latitude identity (Duffett-Smith & Zwart). - Every
PlanetPosition(astro.h) carries a whole-signhouse(1-12), viaassign_houses()/whole_sign_house().DailyTransits.bodies[*].houseis 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 whyastro_compute_daily_transitstakes 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-levelLICENSE. engine/third_party/astronomy/: vendored MIT code, unmodified — seeVENDORED.mdfor 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.