466 lines
27 KiB
Markdown
466 lines
27 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. 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
|
||
|
||
```bash
|
||
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
|
||
```
|
||
|
||
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:
|
||
|
||
```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|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).
|
||
|
||
## 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 (`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 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:
|
||
|
||
```bash
|
||
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 in `reading->transits.aspects[]`, keeps the **top 5** by
|
||
score in `DailyInterpretation.top_items[]` (descending, evicting the
|
||
rest), and classifies the day into a `DaySignificance` enum (`DAY_QUIET`
|
||
... `DAY_MAJOR`) from the single highest-scoring item. The enum also
|
||
carries a trailing `DAY_SIGNIFICANCE_COUNT` sentinel (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 + 1` out of
|
||
`DAY_SIGNIFICANCE_COUNT`. `interpretation_deserves_framing()` is true
|
||
only at `DAY_MAJOR` — `main.c` uses it solely to decide whether to
|
||
print an extra "worth a deeper Celtic Cross look" line; it's not a
|
||
gate on `guidance.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 the `DAY_QUIET`/`DAY_NOTABLE`/`DAY_SIGNIFICANT`/`DAY_MAJOR`
|
||
thresholds in `significance.c` are hand-tuned, not derived from
|
||
anything physical.
|
||
- **`narrative.c`/`narrative.h`** supplies `main.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 a `base` description plus a
|
||
`harmonious`/`discordant` addendum, 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 small `k_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 in `astro.c`).
|
||
Passing a house outside 1-12 (the same "no data" sentinel `reading_io.c`
|
||
uses elsewhere) omits that framing and prints the planet's narrative on
|
||
its own. Every string here (`k_narratives[]`'s `base`/`harmonious`/
|
||
`discordant`, `k_house_area[]`, and the "In matters of ..." framing
|
||
template itself) is looked up via `i18n_get()` under `narrative.*` keys
|
||
before falling back to this English text - `engine/i18n/de.lang` has a
|
||
full German translation under the same keys.
|
||
- `main.c` calls `i18n_load()` once at startup (same pattern as
|
||
`engine/src/main.c`, including the same `default_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 just
|
||
`tarot_data.c`'s, but `narrative.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 its
|
||
`narrative.c` sentence) → 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 real `tarot_position_name()`/`tarot_card_name()`/
|
||
`tarot_position_description()`/`tarot_card_meaning()` accessors, not a
|
||
duplicated table, since `tarot_data.c` is 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 html` mirrors `deck-engine`'s own HTML styling
|
||
and reuses the same `img/<file>` card-art convention (`tarot_card_
|
||
image_file()`); `--format json` embeds the same localized narrative/
|
||
guidance prose as `--format text` (in whatever `--lang` was loaded)
|
||
*alongside* stable, language-independent slugs (`transiting_planet`,
|
||
`aspect`, `card`, `position`, etc., reusing the same small local slug
|
||
tables `reading_io.c` needs for parsing) — a deliberate departure from
|
||
`reading_print_json`'s "slugs only, never prose" policy (next bullet),
|
||
justified because rendering that prose *is* this module's job, unlike
|
||
`deck-engine`'s JSON which is purely a machine interchange format.
|
||
`narrative_print()`/`guidance_print()` only know how to write to a
|
||
`FILE *`, so `--format json` captures their output into a heap string
|
||
via POSIX `open_memstream()` (`capture_narrative()`/`capture_guidance()`)
|
||
rather than changing either module's public API just for this one
|
||
caller.
|
||
- **`guidance.c`/`guidance.h`** is the combined-storytelling piece: it
|
||
ties `interp->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.c` calls it unconditionally
|
||
after every `interpret_daily_reading()`, with no `day_level` gate. The
|
||
core guidance keys off two things: the top transit's aspect character
|
||
(harmonious/discordant/neutral, same classification `narrative.c`
|
||
uses) 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 with
|
||
`interp->day_level` is only the sentence introducing the top transit
|
||
(`k_intro[DAY_SIGNIFICANCE_COUNT]`, one `%s` each) — e.g. "With Saturn
|
||
as today's dominant influence" on `DAY_MAJOR` vs. "Saturn is only
|
||
faintly active today, but for what it's worth" on `DAY_QUIET`. A day
|
||
with no aspects in orb at all (`top_item_count == 0`, always
|
||
`DAY_QUIET`) has no transiting planet to introduce, so it falls back
|
||
to `k_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 via `tarot_card_meaning()` (e.g. what
|
||
Wheel of Fortune reversed means) — the real Waite text, not a
|
||
duplicated table, for the same reason as `print_celtic_cross_text()`
|
||
above. Every string here (stance/intro/no-transit/label text, plus a
|
||
`guidance.planet.*` table used only for the intro sentence's planet
|
||
name - separate from `body.*` because "the Sun"/"the Moon" take a
|
||
definite article the other eight planets don't, in both English and
|
||
German) is looked up via `i18n_get()` under `guidance.*` keys before
|
||
falling back to this English text - `engine/i18n/de.lang` has a full
|
||
German translation. The German `guidance.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 full `DailyReading` (natal, transits, spread), using the
|
||
language-independent `astro_*_slug()`/`tarot_*_slug()` accessors
|
||
(`astro.h`/`tarot.h`) rather than `astro_*_name()`/`tarot_*_name()` —
|
||
the JSON must stay identical regardless of `--lang`, since it's a
|
||
machine interchange format, not display text.
|
||
- **`json.c`/`json.h`** is a small hand-rolled recursive-descent JSON
|
||
parser (object/array/string/number/bool/null) - not a general-purpose
|
||
validating library, just enough to parse `deck-engine`'s own output.
|
||
Unlike the core engine, it uses `malloc`; `interpreter-cli` was never
|
||
meant to run on the watch itself.
|
||
- **`reading_io.c`** walks the parsed JSON down to `"transits"."aspects"`
|
||
and fills a `DailyReading` with those (used for scoring), plus
|
||
`"natal"."bodies"`/`"transits"."bodies"` (each body's sign + whole-sign
|
||
house, via `parse_bodies()` — used only for the sign/house context
|
||
`main.c` prints alongside each significant event, never for scoring)
|
||
and `"spread"."positions"` (each position's card + reversed flag, via
|
||
`parse_spread()` — used by `main.c`'s full Celtic Cross readout and by
|
||
`guidance.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 missing `house` inside `"bodies"`, or a missing `"spread"` key
|
||
entirely is silently skipped/zeroed (`main.c` checks 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` `#include` engine headers
|
||
(`reading.h`/`astro.h`/`tarot.h`) for the struct/enum *definitions*,
|
||
and the root `Makefile` never compiles or links `astro.c`, `tarot.c`,
|
||
`rng.c`, `reading.c`, or the vendored Astronomy Engine into the
|
||
interpreter's binary or tests — every test in `interpreter/tests/`
|
||
runs against hand-built JSON text or hand-built `DailyReading`/
|
||
`DailyInterpretation` values, with no real ephemeris/tarot-draw call
|
||
involved anywhere. Consequently `reading_io.c` (planet/aspect/sign/
|
||
card/position slugs) and `main.c` (planet/aspect/sign display names)
|
||
each duplicate a small local table rather than linking `astro.c` to
|
||
reuse its own - keep those in sync if the engine's slugs ever change.
|
||
`tarot_data.c`/`i18n.c` are the one exception, actually linked in (see
|
||
above) - so card/position *names and meanings* are never duplicated;
|
||
only the slugs `reading_io.c` needs for JSON parsing (which
|
||
`tarot_data.c` doesn't expose a reverse lookup for) still are.
|
||
- Only `Aspect`s compete for the top 5 (`SignificantItemKind` is
|
||
currently just `ITEM_ASPECT`).
|
||
|
||
## Licensing
|
||
|
||
- Engine code (`engine/src/`, `engine/scripts/`) and the root `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.
|
||
- 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 via
|
||
`tarot_card_meaning()`, covered by the `tarot_data.c` bullet above,
|
||
not original text.)
|
||
- `engine/i18n/de.lang`'s `narrative.*`/`guidance.*` entries are original
|
||
German translations of the above two files' text, made for this
|
||
project — same status as `de.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).
|