24 KiB
Input & output formats
See architecture.md for how these fit together.
There are two ways to provide input (direct CLI flags, or run-engine.sh
user.properties) and three output formats (text,html,json); theDailyReadingstruct is the programmatic form all three are rendered from, and the one watch UI code consumes directly.jsonis alsointerpreter-cli's input format — see "The interpreter" at the bottom of this file, which supports the same three output formats (and the same--lang) for its own report, independently ofdeck-engine.
Input
Direct CLI flags (dist/deck-engine)
deck-engine --seed <string> --birth-date YYYY-MM-DD --birth-time HH:MM
--birth-utc-offset <hours> --birth-lat <deg> --birth-lon <deg>
[--date YYYY-MM-DDTHH:MM] [--format text|html|json]
[--gender male|female|unspecified]
| Flag | Required | Format | Meaning |
|---|---|---|---|
--seed |
yes | any string | Tarot seed. The same seed always produces the same 10-card spread, positions, and orientations (see rng.c/tarot.c). |
--birth-date |
yes | YYYY-MM-DD |
Birth date, local calendar. |
--birth-time |
yes | HH:MM (24h) |
Birth time, local clock. |
--birth-utc-offset |
yes | decimal hours, e.g. 2 or -5.5 |
Hours to subtract from local birth time to get UTC (e.g. 2 for CEST). |
--birth-lat |
yes | decimal degrees | Birth latitude, north positive. |
--birth-lon |
yes | decimal degrees | Birth longitude, east positive. |
--date |
no | YYYY-MM-DDTHH:MM[:SS] |
UTC moment used for today's transits and as the moment the tarot seed is drawn against. Defaults to the current system time. |
--format |
no | text | html | json |
Output format, defaults to text. json always uses language-independent identifiers, ignoring --lang. |
--gender |
no | male | female | unspecified |
Who the reading is for. Only affects the Celtic Cross position text's pronouns (e.g. "His House" vs. "Her House" vs. "Their House") - never the chart/draw itself. Defaults to unspecified (pronoun-neutral phrasing). |
--lang |
no | language code, e.g. en, de |
Reading language, defaults to en. Matches a file named <code>.lang in --i18n-dir (see "Translations" below). Unknown codes or missing files fall back to English with a warning on stderr. |
--i18n-dir |
no | path | Directory to look for <lang>.lang in. Defaults to an i18n/ directory next to the binary itself (resolved from argv[0], so dist/deck-engine finds dist/i18n/ regardless of the caller's working directory). |
Birth latitude/longitude only affect the Ascendant/houses — planet signs are geocentric and location-independent.
run-engine.sh + user.properties
dist/run-engine.sh [text|html|json] [lang] wraps the binary for daily
use:
-
Seed and
--datecome from the OS clock:date +%Y-%m-%d(stable all day) anddate -u +%Y-%m-%dT%H:%M. -
Birth data and language are read from
dist/user.properties, akey=valueproperties file next to the script (#-prefixed lines and blank lines are comments; whitespace around=is trimmed):birth_date=1990-05-14 birth_time=14:32 birth_utc_offset=2 birth_lat=52.5200 birth_lon=13.4050 gender=unspecified lang=enThe file is seeded once from
engine/scripts/user.properties.templatewith placeholder values (YOUR_BIRTH_DATE_HERE, etc.).run-engine.shchecks every birth field for an empty value or a leftoverYOUR_placeholder and refuses to run with a clear error until the file is filled in;genderandlanghave no such check since they always have a usable default (unspecified/en). Sincedist/itself can be wiped (make clean, or deleting the directory), the build also keeps a durable backup atengine/scripts/user.properties.local(gitignored) once the file looks filled in, and restores from it ifdist/user.propertiesis ever missing — seeCLAUDE.md's "Build & run". -
The optional second argument overrides
lang=for a single run without editing the file, e.g.dist/run-engine.sh html de. -
dist/run-interpreter.sh(no arguments) is the equivalent wrapper for the interpreter: same OS clock +user.propertiesinputs, but callsdeck-engine --format jsonand pipes it straight intodist/interpreter-cli— see "The interpreter" below.
Translations (--lang, dist/i18n/)
Every piece of display text — planet/sign/moon-phase/aspect names, tarot
card names and meanings, Celtic Cross position names, and the section
headers in the text/html output — is looked up in a translation
catalog with the built-in English text as the fallback for any key the
catalog doesn't have. See CLAUDE.md's "Translations" section for the
key-naming convention and how to add a new language.
Celtic Cross position text additionally varies with --gender: the
plain key (e.g. position.present.desc) is the pronoun-neutral wording
used for unspecified, and .male/.female-suffixed keys (e.g.
position.present.desc.male) hold the gendered wording where it
differs. A position whose wording doesn't vary by gender (e.g. "The
Outcome") simply has no .male/.female keys - the neutral key's
translation is used for every --gender value in that case.
engine/i18n/en.lang and de.lang are the shipped translation files;
make copies engine/i18n/*.lang to dist/i18n/ (refreshed on every
build, like dist/img/). Adding a language is just dropping another
<code>.lang file into engine/i18n/ — no code changes needed.
Output
DailyReading (the real output — reading.h)
reading_generate() is the actual API; text/html below are just two
ways of rendering the struct it fills in. This is what watch UI code
reads directly.
typedef struct {
NatalChart natal;
DailyTransits transits;
CelticCrossSpread spread;
} DailyReading;
NatalChart(astro.h):bodies[10](Sun..Pluto, each a sign + degree-in-sign + raw ecliptic longitude + whole-signhouse1-12),ascendant_longitude,houses[12](whole-sign:houses[0]is the Ascendant's sign).DailyTransits(astro.h):bodies[10](today's positions, same shape as above —househere is the natal chart's house the transiting planet currently occupies, not a fresh "houses for right now" chart),moon_phase(one of 8 named phases),aspects[]+aspect_count(each aspect names a transiting planet, a natal planet, anAspectType— conjunction/sextile/square/trine/opposition — and the orb in degrees).CelticCrossSpread(tarot.h):positions[10], indexed byCelticCrossPosition(Present, Challenge, Crown, Foundation, Recent Past, Near Future, Attitude, Environment, Hopes and Fears, Outcome — Waite's own drawing order), each holding aTarotCardand areversedbool.
--format text
Four =====-delimited sections, in order: natal chart (each body's sign
- degree + house, then the Ascendant), today's sky (Moon phase, then each
body's transiting position + the natal house it currently occupies),
today's aspects to the natal chart (one line per aspect, with orb), and
the Celtic Cross (one block per position: name, card,
(Reversed)if applicable, the position's meaning, then the card's meaning).
===== Natal Chart =====
Sun 23.5° Taurus (House 3)
Moon 15.2° Capricorn (House 11)
...
Ascendant 18.4° Pisces
===== Today's Sky =====
Moon phase: Waning Gibbous
Sun 11.5° Cancer (House 5)
...
===== Today's Aspects to Natal Chart =====
Transiting Sun Opposition natal Moon (orb 3.7°)
...
===== Celtic Cross =====
The Present The High Priestess (Reversed)
This covers them: the general influence affecting the matter.
Passion, moral or physical ardour, conceit, surface knowledge.
...
--format html
A single self-contained HTML page (inline <style>, no JS, no external
requests) with the same four sections, plus each of the 10 Celtic Cross
cards rendered as an image:
<img class="reversed" src="img/RWS_Tarot_02_High_Priestess.jpeg" alt="The High Priestess">
src="img/<file>"is relative to the HTML file's own location — it expects to sit next to animg/directory, exactly likedist/reading.htmlnext todist/img/(whichmake imagespopulates fromres/img/). Saving the HTML output anywhere else breaks the image links.- Reversed cards get
class="reversed", styled withtransform: rotate(180deg)in the page's inline CSS.
--format json
A single JSON object mirroring DailyReading exactly — date, gender,
natal, transits, spread — using the slug accessors
(astro_body_slug(), astro_sign_slug(), astro_moon_phase_slug(),
astro_aspect_slug(), tarot_card_slug(), tarot_position_slug())
rather than the display-name ones, so the output is identical regardless
of --lang — this is the one format meant for another program to parse
(dist/interpreter-cli, see "The interpreter" below), not for a human to
read.
{
"date": "2026-07-16",
"gender": "unspecified",
"natal": {
"bodies": [
{"body": "sun", "sign": "taurus", "degree_in_sign": 23.4926, "ecliptic_longitude": 53.4926, "house": 3},
...
],
"ascendant_longitude": 348.4123,
"houses": ["pisces", "aries", ...]
},
"transits": {
"bodies": [
{"body": "sun", "sign": "cancer", "degree_in_sign": 11.5012, "ecliptic_longitude": 101.5012, "house": 5},
...
],
"moon_phase": "waning_gibbous",
"aspects": [
{"transiting_planet": "sun", "natal_planet": "moon", "type": "opposition", "orb": 3.7021},
...
]
},
"spread": {
"positions": [
{"position": "present", "card": "high_priestess", "reversed": true},
...
]
}
}
dateis the UTC calendar date of--date(or the current system time if--datewas omitted) — the day this reading is for, not necessarily the day it was generated on.reading_generate()stores the full moment inDailyReading.utc_moment; only the date part is serialized here since that's the only thing the rest of this format (and the interpreter's own reports, which read this field back — see "The interpreter" below) ever display.genderis--gender's value (male/female/unspecified,tarot_gender_slug()) — who the reading is for. Only affects the Celtic Cross position text's pronouns, nothing else in this document. The interpreter reads this field back the same way it readsdate.bodiesis alwaysNUM_BODIES(10) entries, Sun..Pluto inBodyenum order;spread.positionsis alwaysTAROT_SPREAD_SIZE(10) entries inCelticCrossPositionenum order (Present, Challenge, Crown, Foundation, Recent Past, Near Future, Attitude, Environment, Hopes and Fears, Outcome — seeCLAUDE.md's "Tarot" section for why that's not the order most tutorials use).natal.houses[i]is the sign occupying whole-sign housei + 1;houses[0]is the Ascendant's own sign.transits.bodies[*].houseis a house number (1-12) into the natal chart, not a fresh "houses for right now" chart — same rule as thetext/htmloutput.- All angles are decimal degrees,
%.4f.aspectsmay be an empty array (a valid "no aspects today"), but the key itself is always present.
The interpreter (interpreter/, dist/interpreter-cli)
dist/interpreter-cli is a separate binary — see CLAUDE.md's
"Interpretation" section for the full architecture — that reads a
--format json document and scores how significant today's transits
are. It never links astro.c/tarot.c/reading.c (or the vendored
Astronomy Engine) — only engine/src/tarot_data.c/i18n.c are linked
in, for real card/position names and Waite meaning text (see
CLAUDE.md's INTERP_TAROT_TEXT_OBJS note). Otherwise it depends only
on the JSON shape above.
Like deck-engine, its own output supports --format text|html|json
and --lang <code>/--i18n-dir <path>:
| Flag | Required | Format | Meaning |
|---|---|---|---|
[reading.json] |
no | file path or - |
Input file; stdin (or -) if omitted. |
--format |
no | text | html | json |
This binary's own output format, defaults to text. Entirely independent of the --format json that produced its input - see below. |
--lang |
no | language code, e.g. en, de |
This binary's own output language, defaults to en. Same <code>.lang/--i18n-dir convention as deck-engine (see "Translations" above) - main.c calls i18n_load() itself, so narrative.c's/guidance.c's own text (in engine/i18n/*.lang under narrative.*/guidance.* keys) is translated too, not just the card/position/body/sign/aspect vocabulary it shares with deck-engine. |
--i18n-dir |
no | path | Same convention as deck-engine's: defaults to an i18n/ directory next to this binary. |
This --lang is unrelated to whatever --lang deck-engine was run
with to produce the --format json input - that JSON is always
language-independent (slugs only, see above), so it doesn't matter what
language deck-engine printed anything in, or whether it was even asked
to print anything at all (--format json never touches --lang
either). The two --lang flags, on the two sides of the pipe, are
independent settings.
dist/deck-engine ... --format json | dist/interpreter-cli --format html --lang de
# or:
dist/deck-engine ... --format json > reading.json
dist/interpreter-cli --format json reading.json
# or, for daily use (OS clock + user.properties, same inputs as run-engine.sh,
# including user.properties' lang=):
dist/run-interpreter.sh # text output, language from user.properties
dist/run-interpreter.sh html de # override format/language for this run
- Input: a file path argument, or stdin if no argument (or
-) is given."transits"."aspects"is required (an empty array is valid and means "no notable transits today"; a missing key is treated as invalid input) and drives scoring."natal"."bodies"/"transits"."bodies"are read too, for the sign/house context printed alongside each event below (not for scoring), and"spread"."positions"is read for the full Celtic Cross readout and the guidance paragraph below — an entry naming an unrecognized body/card/position, a missinghouse, or a missing"spread"key entirely is silently skipped/zeroed rather than failing the load (seeparse_bodies()/parse_spread()inreading_io.c). - Input: same for all three output formats above - only how the parsed data gets rendered differs.
--format text
A plain-text report, printed in a fixed order —
- the day this reading is for (
Reading for: 2026-07-16), from the input JSON's top-leveldatefield (see--format jsonabove) — not necessarily the day the report is actually being read on, ifreading.jsonwas generated earlier and piped intointerpreter-clilater; - the day's overall significance level (
Quiet/Notable/Significant/Major, plus its rank out of the 4 defined levels, e.g.Notable (2/4)), with a "worth a deeper Celtic Cross look" line onMajordays only; - up to 5 of today's aspects ranked by score, each naming the sign
and natal house the transiting planet currently occupies and the
sign and house of the natal planet it's aspecting, followed by a
short narrative sentence about what that transiting planet
classically signifies and which area of life its current house
governs (see
CLAUDE.md'snarrative.cbullet) — omitted only if the source material has nothing to say for that planet/aspect combination; - the full 10-position Celtic Cross spread, in Waite's own drawing order, each with its card (and orientation), the position's own description, and that card's actual meaning;
- finally, a guidance paragraph tying the day's top transit to the
spread's Attitude/Outcome cards, with its opening sentence tailored
to
day_level(stronger wording onMajor, softer onQuiet/Notable, falling back to a transit-free reading of the Attitude card alone on a day with no aspects at all) and, for the Attitude/Outcome cards it names, their actual Waite meaning for the orientation they landed in — seeCLAUDE.md'sguidance.cbullet. This is printed last, as the closing takeaway after the reader has seen the full spread it references.
Reading for: 2026-07-16
Day significance: Major (4/4)
(a major transit today - worth a deeper Celtic Cross look)
Top 5 significant transits:
1. Transiting Pluto in Aquarius (house 3) Conjunction natal Moon in Aquarius (house 3) (orb 2.9°, score 9.60)
In matters of communication, siblings, and everyday learning (house 3). Brings deep, often hidden transformation - the surfacing or dismantling of something that has outgrown its old form.
2. Transiting Neptune in Aries (house 5) Sextile natal Moon in Aquarius (house 3) (orb 2.5°, score 9.25)
In matters of romance, creativity, and children (house 5). Brings a state of chaos and confusion - an involved, uncertain condition of affairs, with plots, subtlety, or unseen influences at work.
3. Transiting Uranus in Gemini (house 7) Trine natal Moon in Aquarius (house 3) (orb 2.0°, score 8.96)
In matters of partnerships and close relationships (house 7). Brings separations, estrangements, sudden dislocations and violent upsets. Success through official or civic channels, and beneficial changes or appointments, are possible.
Celtic Cross:
1. The Present: The Emperor (Reversed)
This covers them: the general influence affecting the matter.
Benevolence, compassion, credit; also confusion to enemies, obstruction, immaturity.
2. The Challenge: The Devil
This crosses them: the nature of the obstacle in the matter.
Ravage, violence, vehemence, extraordinary efforts, force, fatality.
...
7. Themself: The Magician
Their position or attitude in the circumstances.
Skill, diplomacy, address, subtlety; self-confidence, will.
...
10. The Outcome: Strength
What will come: the final result of the matter.
Power, energy, action, courage, magnanimity; complete success and honours.
With Pluto as today's dominant influence: Today concentrates whatever you already
bring to it - your current approach will be amplified, so make sure it's the one
you want.
Your Attitude card is The Magician: Skill, diplomacy, address, subtlety; self-confidence, will.
The spread's likely Outcome is Strength: Power, energy, action, courage, magnanimity; complete success and honours.
On a quieter day, only the opening of the guidance paragraph changes:
Day significance: Notable (2/4)
...
With a mild touch from Jupiter today: Your instincts are sound, but the day is
testing them - hold your position without forcing the issue.
Your Attitude card is The Sun: Material happiness, fortunate marriage, contentment.
The spread's likely Outcome is Justice: Equity, rightness, probity, executive; triumph of the deserving side in law.
The same report with --lang de (same input as the Major example
above):
Lesung für: 2026-07-16
Bedeutung des Tages: Einschneidend (4/4)
(ein einschneidender Transit heute - ein genauerer Blick auf das Keltische Kreuz lohnt sich)
Top 5 wichtige Transits:
1. Transit Pluto in Wassermann (Haus 3) Konjunktion natal Mond in Wassermann (Haus 3) (Orbis 2.9°, Punktzahl 9.60)
In Fragen von Kommunikation, Geschwistern und alltäglichem Lernen (Haus 3). Bringt tiefgreifenden, oft verborgenen Wandel - das Auftauchen oder die Auflösung von etwas, das seine alte Form überwachsen hat.
...
Keltisches Kreuz:
1. Die Gegenwart: Der Herrscher (Umgekehrt)
Dies bedeckt die fragende Person: der allgemeine Einfluss, der die Angelegenheit betrifft.
Wohlwollen, Mitgefühl, Ansehen; auch Verwirrung der Feinde, Behinderung, Unreife.
...
Pluto ist heute der bestimmende Einfluss: Der heutige Tag verstärkt, was du bereits
mitbringst - deine derzeitige Herangehensweise wird verstärkt, also stelle sicher,
dass es die richtige ist.
Deine Haltungskarte ist Der Magier: Geschick, Diplomatie, Gewandtheit, Feinsinn; Selbstvertrauen, Wille.
Das wahrscheinliche Ergebnis des Blatts ist Kraft: Macht, Energie, Tatkraft, Mut, Großmut; voller Erfolg und Ehren.
--format html
A single self-contained HTML page, same inline-CSS/no-JS approach as
deck-engine's own --format html, and the same three sections as text
(significant transits as a table, the full Celtic Cross as an image
grid, guidance as a closing callout) - reusing the exact same
img/<file> card-art convention (tarot_card_image_file()), so it
expects to sit next to an img/ directory just like deck-engine's
HTML output:
<div class="card">
<div class="position">The Present</div>
<img class="reversed" src="img/RWS_Tarot_04_Emperor.jpeg" alt="The Emperor">
<div class="name">The Emperor (Reversed)</div>
<div class="desc">This covers them: the general influence affecting the matter.</div>
<div class="meaning">Benevolence, compassion, credit; also confusion to enemies, obstruction, immaturity.</div>
</div>
The guidance section renders as one <p> per line of the same text
--format text prints last (intro sentence, Attitude line, Outcome
line), inside a <div class="guidance">.
--format json
Unlike deck-engine's own --format json - which is deliberately
language-independent (slugs only, no --lang-dependent prose, since
it's a machine interchange format between the two binaries) -
interpreter-cli's --format json embeds the same localized
narrative/guidance prose --format text prints, in whatever --lang
was loaded, alongside stable, language-independent slug fields. This
is a deliberate difference: rendering that prose is this module's whole
job, so a JSON consumer that wants it doesn't have to reimplement
narrative.c/guidance.c's logic itself - but a consumer that only
wants structured data (e.g. watch UI code with its own rendering) still
has the slugs to work with directly.
{
"date": "2026-07-16",
"day_significance": {
"level": "major",
"rank": 4,
"count": 4,
"deserves_framing": true
},
"significant_transits": [
{
"transiting_planet": "pluto",
"transiting_sign": "aquarius",
"transiting_house": 3,
"natal_planet": "moon",
"natal_sign": "aquarius",
"natal_house": 3,
"aspect": "conjunction",
"orb": 2.8821,
"score": 9.5961,
"narrative": "In matters of communication, siblings, and everyday learning (house 3). Brings deep, often hidden transformation - the surfacing or dismantling of something that has outgrown its old form."
}
],
"celtic_cross": [
{
"position": "present",
"card": "emperor",
"reversed": true,
"card_name": "The Emperor",
"meaning": "Benevolence, compassion, credit; also confusion to enemies, obstruction, immaturity."
}
],
"guidance": "With Pluto as today's dominant influence: Today concentrates whatever you already bring to it - your current approach will be amplified, so make sure it's the one you want.\nYour Attitude card is The Magician: Skill, diplomacy, address, subtlety; self-confidence, will.\nThe spread's likely Outcome is Strength: Power, energy, action, courage, magnanimity; complete success and honours."
}
dateis copied straight through from the input JSON's own top-leveldatefield (see--format jsonabove) - always language-independent regardless of--lang, same as every other slug in this document.significant_transitshasinterp.top_item_countentries (0-5, same ranking as--format text);celtic_crossalways has exactly 10, inCelticCrossPositionenum order (same order asdeck-engine's ownspread.positions).orb/scoreare%.4f, matchingdeck-engine's own JSON precision for angles.guidanceis always present and non-empty (unlikesignificant_transits, which can be[]on a genuinely quiet day) - seeguidance.c's no-aspects-at-all fallback inCLAUDE.md.- With
--lang de, every string value above except the slugs ("level","transiting_planet","aspect","card","position", etc.) changes language; the slugs never do.