31 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. This repository
holds the core engine: 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, deliberately built so it links
straight into Pebble C/UI watch code without rework (see "Portability to
the watch" below).
Build & run
make # builds dist/{deck-engine,interpreter-cli}, dist/img/, dist/i18n/,
# dist/run-engine.sh, dist/run-interpreter.sh, dist/user.properties
make test # builds and runs engine/tests/smoke_test.c and interpreter/tests/*_test.c
make clean # removes build/ and the generated binaries/img/i18n (never touches dist/user.properties)
make package # builds a versioned Linux CLI tarball at dist/ - see "Packaging" below
make watchapp # builds the Pebble watchapp and copies a versioned .pbw to
# dist/ - see "Packaging" below. Requires the Pebble SDK
# (not needed for anything else above); not run by CI.
A single Makefile at the repo root builds both engine/ and
interpreter/ — there is no per-module Makefile, and no cd needed
before running make. Build output goes to dist/, a sibling of
engine/, interpreter/, and res/. 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|json] [--lang en|de] [--i18n-dir <path>]
# 2. dist/run-engine.sh [text|html|json] [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.) — run-engine.sh/
run-interpreter.sh refuse to run (with a clear error) until the
placeholders are replaced with real values. Because dist/ itself is
disposable (make clean, or just deleting the directory, wipes it), the
scripts Makefile target also keeps a durable backup outside dist/:
once dist/user.properties looks filled in (no leftover YOUR_...), every
build copies it out to engine/scripts/user.properties.local
(gitignored); if dist/user.properties is ever missing when a build
runs, it's restored from that backup instead of being reseeded from the
placeholder template. dist/run-interpreter.sh is the equivalent
daily-use wrapper for the interpreter: it runs run-engine.sh's same
OS-clock seed/date and user.properties birth data through deck-engine --format json, piped straight into dist/interpreter-cli — see
"Interpretation" below.
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.
Packaging
make package (a Makefile target, not a separate script) builds a
fresh all and bundles it into a versioned, self-contained Linux CLI
tarball at dist/deck-in-a-dash-<version>-linux-<arch>.tar.gz -
deck-engine, interpreter-cli, run-engine.sh/run-interpreter.sh,
img/, i18n/, LICENSE, README.md, docs/reading.md (renamed
READING.md), and a placeholder user.properties (from
engine/scripts/user.properties.template, never
dist/user.properties itself - see below). Extracting the tarball
anywhere and running the scripts/binaries from inside it works exactly
like running them from dist/ in place, since run-engine.sh/
run-interpreter.sh already resolve every path relative to their own
location (SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"), not the
caller's working directory.
Version: defaults to the current commit's git tag, matching
vX.Y.Z (the v is stripped); if HEAD isn't exactly on such a tag,
it falls back to a 0.0.0-dev+<short-sha> placeholder with a warning on
stderr. Override either with make package VERSION=1.2.3. Push a
vX.Y.Z tag to drive a real release version.
Never packages dist/user.properties: the staging step copies
files into the package by explicit name only - never a wildcard or
recursive copy of dist/ itself - specifically so a filled-in
dist/user.properties (real birth data, gitignored, private) can never
end up in a distributable package. The package always gets the
placeholder template instead, identical to what a first-time make
seeds dist/user.properties with.
CI (.gitea/workflows/build.yml): runs make test && make package
on every push/PR (plus manual dispatch). The resulting tarball is both
kept as a Gitea Actions run artifact and, on push only (not PRs),
published over SFTP to dl.ladkau.de under files/deck-in-a-dash/ -
requires a DL_SFTP_KEY secret configured on this repo (or inherited
from the org/instance level).
Watch app packaging (make watchapp)
A separate target, deliberately not a prerequisite of all - it
shells out to pebble build inside watch/ (via watch/wscript's own
Pebble SDK/waf tooling, not this Makefile's plain-cc rules) and copies
the resulting watch/build/watch.pbw to
dist/deck-in-a-dash-<version>.pbw, using the exact same version
resolution as make package above (current git tag, or make watchapp VERSION=1.2.3 to override - see that section, not duplicated here).
Requires the Pebble SDK (pebble on PATH, plus the Python 3 + Pillow
watch/wscript needs for its resource-generation scripts) - a machine
without it can still build/test/package the CLI via every other target
in this file.
.gitea/workflows/build.yml does run make watchapp on every
push/PR, alongside make test/make package - the whole job runs
inside a container image (build-image/, built and pushed with
build-image.sh/upload-image.sh, see "Build image" below) that has
the Pebble SDK baked in, so the CI runner itself needs no toolchain
beyond Docker access to pull that image. Both the CLI tarball and the
.pbw are uploaded as build artifacts and published to
dl.ladkau.de/files/deck-in-a-dash/ on push.
Build image
build-image/Dockerfile pins the exact toolchain versions (Pebble
Tool, Pebble SDK core, Node.js) needed for make test, make package,
and make watchapp, so builds are reproducible independent of whatever
happens to be installed on a given machine. Three root-level scripts
drive it, each reading registry settings from a gitignored
registry.env (copy registry.env.example to create it - never commit
the real file, since it holds registry credentials) and the image tag
from build-image/VERSION:
build-image.sh- builds and locally tags the image (:VERSIONand:latest), no push.run-image.sh [VERSION]- runsmake test package watchappinside the locally-built image against a live bind-mount of the repo, wiping generated build artifacts first for CI parity. Use this to validate abuild-image/Dockerfilechange before pushing the image anywhere.upload-image.sh- logs in and pushes the image (:VERSIONand:latest) to the registry;.gitea/workflows/build.ymlpulls:latestto run its build job (see "Watch app packaging" above).
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), Sepharial's "Transits and Planetary
Periods" (PDF, public domain — source of
interpreter/src/narrative.c's 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-engine.sh, scripts/run-interpreter.sh, scripts/user.properties.template
interpreter/ Separate module + binary; see "Interpretation" below.
dist/ Build output (gitignored-style; see Build & run) and,
after `make package`, versioned release tarballs.
Makefile Single root Makefile, builds both engine/ and
interpreter/, and packages a release (see Packaging).
Data flow / the real API
reading_generate(seed, birth, utc_moment, &DailyReading) in
reading.h is the single entry point that matters — the real API
surface. It populates a plain struct (NatalChart + DailyTransits +
CelticCrossSpread) that a caller walks directly to build a UI.
reading_print_text/reading_print_html are desktop-only text/HTML
renderers of that same struct, used by the CLI.
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).
Gender (Gender in tarot.h: GENDER_UNSPECIFIED/GENDER_MALE/
GENDER_FEMALE) is who the reading is for — it only affects the
pronouns in the 9 Celtic Cross position names/descriptions that have one
(Waite addresses the querent as "him" throughout; every other position
text in this codebase — card meanings, narrative, guidance — is already
pronoun-neutral or direct "you" address, so nothing else varies with
it). DailyReading.gender is set by reading_generate() the same way
utc_moment is, deck-engine's --gender male|female|unspecified
flag/dist/user.properties' gender= field/the watch config page's
Gender field all feed it, and reading_print_json/reading_load_json
carry it across the deck-engine → interpreter-cli JSON boundary
(tarot_gender_slug()) the same way date does. GENDER_UNSPECIFIED
is the default and renders pronoun-neutral phrasing (singular "they" in
English; German rephrases around a noun like "die fragende Person"
rather than a pronoun, since German has no equivalent singular-they
construction in common use).
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. position.<slug>.name/desc additionally take an optional
.male/.female suffix (e.g. position.attitude.name.male) for the
Gender-varying wording (see "Tarot" above); the unsuffixed key is
looked up first as the fallback for a missing gendered key, so a
position whose wording doesn't vary by gender only needs the one
unsuffixed key. 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 links
into the watch as-is given a 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 link into
the Pebble watchapp unchanged. Only reading_print_text/_html (text
formatting), main.c (CLI arg parsing), and i18n_load (reads a file
from disk) are desktop-only and don't port as-is.
Interpretation (interpreter/)
A second, separate top-level module and its own binary
(dist/interpreter-cli, sibling to dist/deck-engine) that scores how
significant a day's transits are, prints a short narrative sentence for
each of the top items about what that transit classically means, reads
out the full Celtic Cross spread with every card's meaning, and finally
prints guidance tying the day's top transit to the spread's
Attitude/Outcome cards; see docs/reading.md. Like deck-engine, it
supports --format text|html|json and --lang <code>/--i18n-dir <path> for its own output - see main.c's bullets below for both.
deck-engine itself is deliberately unchanged beyond gaining --format json; the two binaries compose over that JSON, never by linking
reading_generate() or any I/O function together:
make # -> dist/deck-engine (unchanged; --format json is new) and dist/interpreter-cli
make test # builds and runs both engine/tests/*_test.c and interpreter/tests/*_test.c
dist/deck-engine ... --format json | dist/interpreter-cli [--format text|html|json] [--lang <code>]
# or: dist/deck-engine ... --format json > reading.json && dist/interpreter-cli reading.json
# or, for daily use (OS clock + user.properties, same as run-engine.sh):
dist/run-interpreter.sh [text|html|json] [lang]
Note --lang means something different on each side of the pipe:
deck-engine's own --lang (if passed at all) is irrelevant here, since
--format json is deliberately language-independent (see
docs/input-output-format.md); dist/interpreter-cli --lang controls
its own text/html/json output language, independently.
The root Makefile builds both from one invocation, and keeps them
independent compilation units for everything ephemeris/tarot-draw/
random-related — the one exception is engine/src/tarot_data.c and its
own i18n.c dependency (pure, deterministic card/position lookup text,
see INTERP_TAROT_TEXT_OBJS in the Makefile), whose object files are
built once and linked into both binaries.
interpret_daily_reading(reading, &out)(significance.h/.c) scores every aspect inreading->transits.aspects[], keeps the top 5 by score inDailyInterpretation.top_items[](descending, evicting the rest), and classifies the day into aDaySignificanceenum (DAY_QUIET...DAY_MAJOR) from the single highest-scoring item. The enum also carries a trailingDAY_SIGNIFICANCE_COUNTsentinel (not a real level) purely so callers can print the level as a rank, e.g.main.c's"%s (%d/%d)"→"Notable (2/4)"—interp.day_level + 1out ofDAY_SIGNIFICANCE_COUNT.interpretation_deserves_framing()is true only atDAY_MAJOR—main.cuses it solely to decide whether to print an extra "worth a deeper Celtic Cross look" line; it's not a gate onguidance.c(below), which runs on every day level.- Score =
transiting-planet weight (slow/outer planets score higher) × orb tightness (1.0 = exact, ~0 = at the edge) × natal-luminary bonus (transits to natal Sun/Moon score higher than to other planets). The weights and theDAY_QUIET/DAY_NOTABLE/DAY_SIGNIFICANT/DAY_MAJORthresholds insignificance.care hand-tuned, not derived from anything physical. narrative.c/narrative.hsuppliesmain.c's one-sentence description printed under each top item, condensed from Sepharial's Transits and Planetary Periods (1920, public domain —res/Transits_and_Planetary_Periods.pdf), Chapter VIII "Effects of Transits". Per transiting planet it holds abasedescription plus aharmonious/discordantaddendum, shown only under a trine/sextile or square/opposition respectively (either may be""if the source gives none); a conjunction is treated as neutral — base only — since Sepharial's own text says a conjunction "takes the nature of what it conjoins," which isn't modelled here. The Moon and Pluto aren't in that chapter (fast lunar transits were out of scope for a year-ahead forecasting book; Pluto wasn't discovered until 1930) — their text is original, written for this project, not drawn from Sepharial. Each narrative is also framed by where it's happening:narrative_print()takes the transiting planet's whole-sign house (1-12, relative to the natal chart, same as everywhere else in this codebase) and prefixes an "In matters of ..." line naming that house's traditional area of life, from a smallk_house_area[12]table of standard whole-sign house significations — centuries-old convention, not attributed to any single source (same status as the sign/aspect names already inastro.c). Passing a house outside 1-12 (the same "no data" sentinelreading_io.cuses elsewhere) omits that framing and prints the planet's narrative on its own. Every string here (k_narratives[]'sbase/harmonious/discordant,k_house_area[], and the "In matters of ..." framing template itself) is looked up viai18n_get()undernarrative.*keys before falling back to this English text -engine/i18n/de.langhas a full German translation under the same keys.main.ccallsi18n_load()once at startup (same pattern asengine/src/main.c, including the samedefault_i18n_path()helper, duplicated rather than shared since the two binaries are never linked- see the compilation-units bullet above) before producing any output,
so every
i18n_get()call anywhere in the interpreter - not justtarot_data.c's, butnarrative.c's/guidance.c's own (below) - respects--lang.--format text's report is printed in a fixed order: significance level → top significant transits (each with itsnarrative.csentence) → the full Celtic Cross spread (print_celtic_cross_text(), every position in Waite's own drawing order, each with its card, orientation, position description, and card meaning — via the realtarot_position_name()/tarot_card_name()/tarot_position_description()/tarot_card_meaning()accessors, not a duplicated table, sincetarot_data.cis linked into this binary) →guidance.c's paragraph, deliberately printed last, so it reads as the closing takeaway after the reader has seen the full spread it references.--format htmlmirrorsdeck-engine's own HTML styling and reuses the sameimg/<file>card-art convention (tarot_card_ image_file());--format jsonembeds the same localized narrative/ guidance prose as--format text(in whatever--langwas loaded) alongside stable, language-independent slugs (transiting_planet,aspect,card,position, etc., reusing the same small local slug tablesreading_io.cneeds for parsing) — a deliberate departure fromreading_print_json's "slugs only, never prose" policy (next bullet), justified because rendering that prose is this module's job, unlikedeck-engine's JSON which is purely a machine interchange format.narrative_print()/guidance_print()only know how to write to aFILE *, so--format jsoncaptures their output into a heap string via POSIXopen_memstream()(capture_narrative()/capture_guidance()) rather than changing either module's public API just for this one caller.
- see the compilation-units bullet above) before producing any output,
so every
guidance.c/guidance.his the combined-storytelling piece: it tiesinterp->top_items[0](the day's single most significant transit) to the Celtic Cross spread and prints a short "how to meet the day" paragraph on every day —main.ccalls it unconditionally after everyinterpret_daily_reading(), with noday_levelgate. The core guidance keys off two things: the top transit's aspect character (harmonious/discordant/neutral, same classificationnarrative.cuses) crossed with whether the spread's Attitude position — Waite's "Himself: his position or attitude in the circumstances", the position most directly about how the reader is meeting the day — fell upright or reversed (3 × 2 = 6 combinations); this text stays valid regardless of the day's intensity. What does vary withinterp->day_levelis only the sentence introducing the top transit (k_intro[DAY_SIGNIFICANCE_COUNT], one%seach) — e.g. "With Saturn as today's dominant influence" onDAY_MAJORvs. "Saturn is only faintly active today, but for what it's worth" onDAY_QUIET. A day with no aspects in orb at all (top_item_count == 0, alwaysDAY_QUIET) has no transiting planet to introduce, so it falls back tok_no_transit_stance, keyed on the Attitude card's orientation alone. All of this guidance text is original, written for this project, since neither Waite nor Sepharial discuss combining astrology and tarot. The paragraph also names the Attitude and Outcome cards and states their actual meaning viatarot_card_meaning()(e.g. what Wheel of Fortune reversed means) — the real Waite text, not a duplicated table, for the same reason asprint_celtic_cross_text()above. Every string here (stance/intro/no-transit/label text, plus aguidance.planet.*table used only for the intro sentence's planet name - separate frombody.*because "the Sun"/"the Moon" take a definite article the other eight planets don't, in both English and German) is looked up viai18n_get()underguidance.*keys before falling back to this English text -engine/i18n/de.langhas a full German translation. The Germanguidance.intro.*templates are deliberately phrased so the planet name placeholder is always the sentence's grammatical subject (nominative case) across all four day levels, sidestepping the case-agreement problems a naive word-for-word translation of the English templates would hit (e.g. "with Saturn" needs dative in German, but "Saturn is only faintly active" needs nominative - the four German templates are worded so the placeholder never needs to change case).reading_print_json(engine/src/reading.c) is the wire format: the fullDailyReading(natal, transits, spread), using the language-independentastro_*_slug()/tarot_*_slug()accessors (astro.h/tarot.h) rather thanastro_*_name()/tarot_*_name()— the JSON must stay identical regardless of--lang, since it's a machine interchange format, not display text.json.c/json.his a small hand-rolled recursive-descent JSON parser (object/array/string/number/bool/null) - not a general-purpose validating library, just enough to parsedeck-engine's own output. Unlike the core engine, it usesmalloc;interpreter-cliwas never meant to run on the watch itself.reading_io.cwalks the parsed JSON down to"transits"."aspects"and fills aDailyReadingwith those (used for scoring), plus"natal"."bodies"/"transits"."bodies"(each body's sign + whole-sign house, viaparse_bodies()— used only for the sign/house contextmain.cprints alongside each significant event, never for scoring) and"spread"."positions"(each position's card + reversed flag, viaparse_spread()— used bymain.c's full Celtic Cross readout and byguidance.c's Attitude/Outcome framing, never for scoring). An aspect naming a planet/aspect-type slug it doesn't recognize, or a document missing"transits"."aspects"entirely (as opposed to a present-but-empty array, which is a valid "no aspects today"), makes the whole load fail; by contrast an unrecognized body/card/position slug, a missinghouseinside"bodies", or a missing"spread"key entirely is silently skipped/zeroed (main.cchecks for the body "no data" sentinel before printing sign/house), since all of that is supplementary display context rather than something scoring depends on.- Mostly header-only dependency on the engine:
significance.h/reading_io.h/narrative.h/guidance.h#includeengine headers (reading.h/astro.h/tarot.h) for the struct/enum definitions, and the rootMakefilenever compiles or linksastro.c,tarot.c,rng.c,reading.c, or the vendored Astronomy Engine into the interpreter's binary or tests — every test ininterpreter/tests/runs against hand-built JSON text or hand-builtDailyReading/DailyInterpretationvalues, with no real ephemeris/tarot-draw call involved anywhere. Consequentlyreading_io.c(planet/aspect/sign/ card/position slugs) andmain.c(planet/aspect/sign display names) each duplicate a small local table rather than linkingastro.cto reuse its own - keep those in sync if the engine's slugs ever change.tarot_data.c/i18n.care the one exception, actually linked in (see above) - so card/position names and meanings are never duplicated; only the slugsreading_io.cneeds for JSON parsing (whichtarot_data.cdoesn't expose a reverse lookup for) still are. - Only
Aspects compete for the top 5 (SignificantItemKindis currently justITEM_ASPECT).
Licensing
- Engine code (
engine/src/,engine/scripts/) and the rootMakefile: 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. TheGENDER_MALEvariant of the Celtic Cross position text keeps Waite's own "him"/"his" wording verbatim; theGENDER_UNSPECIFIED/GENDER_FEMALEvariants are original paraphrases written for this project, not Waite's own text (see "Tarot" above). 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. ItsGender-varying position text follows the same policy as the English original:.maleis a direct translation of Waite's own wording,.female/unsuffixed are original paraphrases for this project.- Transit narrative text in
interpreter/src/narrative.c: condensed from Sepharial's Transits and Planetary Periods (1920, public domain —res/Transits_and_Planetary_Periods.pdf), except the Moon and Pluto entries, which are original (see narrative.c's own comment for why). - Guidance text in
interpreter/src/guidance.c: original, written for this project — no source to condense from, since neither Waite nor Sepharial discuss combining astrology and tarot. (The Attitude/Outcome card meanings that same paragraph quotes are Waite's own text viatarot_card_meaning(), covered by thetarot_data.cbullet above, not original text.) engine/i18n/de.lang'snarrative.*/guidance.*entries are original German translations of the above two files' text, made for this project — same status asde.lang's card/spread text (not taken from a specific published German edition of Sepharial, since none exists for this condensed, project-specific wording anyway).