Give the interpreter real Celtic Cross meanings and --format/--lang support
- guidance.c now prints last, after the full spread; both it and main.c's new Celtic Cross readout pull real Waite card meanings via tarot_data.c instead of just naming cards. - interpreter-cli gains --format text|html|json and --lang en|de, matching deck-engine - required linking engine/src/tarot_data.c and i18n.c into the interpreter binary (a narrow, documented exception to its earlier "no engine .c files" policy) and writing German translations for narrative.c's/guidance.c's own text.
This commit is contained in:
@@ -5,13 +5,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
## Project goal
|
## Project goal
|
||||||
|
|
||||||
A Pebble smartwatch app that produces a daily personalized tarot (Celtic
|
A Pebble smartwatch app that produces a daily personalized tarot (Celtic
|
||||||
Cross) and astrology (natal chart + transits) reading. Currently only the
|
Cross) and astrology (natal chart + transits) reading. This repository
|
||||||
**core engine** exists: a plain-C, dependency-free library plus a
|
holds the **core engine**: a plain-C, dependency-free library plus a
|
||||||
standalone CLI binary (`dist/deck-engine`) that can be built and run
|
standalone CLI binary (`dist/deck-engine`) that can be built and run
|
||||||
without any Pebble SDK or emulator. The watchapp itself (Pebble C/UI,
|
without any Pebble SDK or emulator, deliberately built so it links
|
||||||
resource packs) has not been started yet — the engine is deliberately
|
straight into Pebble C/UI watch code without rework (see "Portability to
|
||||||
built so it can be linked straight into it later without rework (see
|
the watch" below).
|
||||||
"Portability to the watch" below).
|
|
||||||
|
|
||||||
## Build & run
|
## Build & run
|
||||||
|
|
||||||
@@ -99,12 +98,11 @@ deck_in_a_dash/
|
|||||||
### Data flow / the real API
|
### Data flow / the real API
|
||||||
|
|
||||||
`reading_generate(seed, birth, utc_moment, &DailyReading)` in
|
`reading_generate(seed, birth, utc_moment, &DailyReading)` in
|
||||||
`reading.h` is the single entry point that matters — it's what the
|
`reading.h` is the single entry point that matters — the real API
|
||||||
watchapp will call once it exists. It populates a plain struct
|
surface. It populates a plain struct (`NatalChart` + `DailyTransits` +
|
||||||
(`NatalChart` + `DailyTransits` + `CelticCrossSpread`) that the caller
|
`CelticCrossSpread`) that a caller walks directly to build a UI.
|
||||||
walks directly to build a UI. `reading_print_text`/`reading_print_html`
|
`reading_print_text`/`reading_print_html` are desktop-only text/HTML
|
||||||
are desktop-only conveniences for inspecting a reading (used by the CLI);
|
renderers of that same struct, used by the CLI.
|
||||||
the watchapp will never call them.
|
|
||||||
|
|
||||||
### Determinism (`rng.c`)
|
### Determinism (`rng.c`)
|
||||||
|
|
||||||
@@ -186,44 +184,55 @@ translate the values — no source changes needed, `make` picks up any
|
|||||||
|
|
||||||
`i18n_load` uses stdio (`fopen`/`fgets`), so — like `main.c` and
|
`i18n_load` uses stdio (`fopen`/`fgets`), so — like `main.c` and
|
||||||
`reading_print_*` — it's a desktop-only entry point; `i18n_get` itself
|
`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
|
is a pure fixed-size-array lookup with no heap allocation, so it links
|
||||||
to port to the watch once it has its own (non-file-based) way to
|
into the watch as-is given a non-file-based way to populate the catalog,
|
||||||
populate the catalog, e.g. from a compiled-in resource.
|
e.g. from a compiled-in resource.
|
||||||
|
|
||||||
### Portability to the watch
|
### Portability to the watch
|
||||||
|
|
||||||
`rng`, `tarot`, `tarot_data`, `astro`, `i18n_get`, and `reading_generate`
|
`rng`, `tarot`, `tarot_data`, `astro`, `i18n_get`, and `reading_generate`
|
||||||
are kept free of `stdio`/CLI assumptions specifically so they can link
|
are kept free of `stdio`/CLI assumptions specifically so they link into
|
||||||
into the Pebble watchapp unchanged later. Only `reading_print_text`/
|
the Pebble watchapp unchanged. Only `reading_print_text`/`_html` (text
|
||||||
`_html` (text formatting), `main.c` (CLI arg parsing), and `i18n_load`
|
formatting), `main.c` (CLI arg parsing), and `i18n_load` (reads a file
|
||||||
(reads a file from disk) are desktop-only and won't port as-is.
|
from disk) are desktop-only and don't port as-is.
|
||||||
|
|
||||||
## Interpretation (`interpreter/`)
|
## Interpretation (`interpreter/`)
|
||||||
|
|
||||||
A second, separate top-level module and **its own binary**
|
A second, separate top-level module and **its own binary**
|
||||||
(`dist/interpreter-cli`, sibling to `dist/deck-engine`) that scores *how
|
(`dist/interpreter-cli`, sibling to `dist/deck-engine`) that scores *how
|
||||||
significant a day's transits are* and, for each of the top items, prints
|
significant a day's transits are*, prints a short narrative sentence for
|
||||||
a short narrative sentence about what that transit classically means —
|
each of the top items about what that transit classically means, reads
|
||||||
the first piece of a larger "turn the raw reading into an actual
|
out the full Celtic Cross spread with every card's meaning, and finally
|
||||||
narrative" effort described in project chat history; the Celtic Cross
|
prints guidance tying the day's top transit to the spread's
|
||||||
spread itself isn't woven into that narrative yet (see
|
Attitude/Outcome cards; see `docs/reading.md`. Like `deck-engine`, it
|
||||||
`docs/reading.md`). `deck-engine` itself is deliberately unchanged by
|
supports `--format text|html|json` and `--lang <code>`/`--i18n-dir
|
||||||
this beyond gaining `--format json`; the two binaries compose over that
|
<path>` for its own output - see `main.c`'s bullets below for both.
|
||||||
JSON, never by linking together:
|
`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
|
```bash
|
||||||
make # -> dist/deck-engine (unchanged; --format json is new) and dist/interpreter-cli
|
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
|
make test # builds and runs both engine/tests/*_test.c and interpreter/tests/*_test.c
|
||||||
|
|
||||||
dist/deck-engine ... --format json | dist/interpreter-cli
|
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: 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):
|
# or, for daily use (OS clock + user.properties, same as run-engine.sh):
|
||||||
dist/run-interpreter.sh
|
dist/run-interpreter.sh [text|html|json] [lang]
|
||||||
```
|
```
|
||||||
|
|
||||||
The root `Makefile` builds both from one invocation, but keeps them as
|
Note `--lang` means something different on each side of the pipe:
|
||||||
separate compilation units throughout — no object file is ever shared
|
`deck-engine`'s own `--lang` (if passed at all) is irrelevant here, since
|
||||||
between the two binaries (see the next bullet).
|
`--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
|
- `interpret_daily_reading(reading, &out)` (`significance.h/.c`) scores
|
||||||
every aspect in `reading->transits.aspects[]`, keeps the **top 5** by
|
every aspect in `reading->transits.aspects[]`, keeps the **top 5** by
|
||||||
@@ -234,17 +243,15 @@ between the two binaries (see the next bullet).
|
|||||||
purely so callers can print the level as a rank, e.g. `main.c`'s
|
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
|
`"%s (%d/%d)"` → `"Notable (2/4)"` — `interp.day_level + 1` out of
|
||||||
`DAY_SIGNIFICANCE_COUNT`. `interpretation_deserves_framing()` is true
|
`DAY_SIGNIFICANCE_COUNT`. `interpretation_deserves_framing()` is true
|
||||||
only at `DAY_MAJOR` — the intended hook for giving the Celtic Cross
|
only at `DAY_MAJOR` — `main.c` uses it solely to decide whether to
|
||||||
reading a "this matters" intro sentence naming `top_items[0]` on days
|
print an extra "worth a deeper Celtic Cross look" line; it's not a
|
||||||
that earn it; that rendering doesn't exist yet, `main.c`'s report is
|
gate on `guidance.c` (below), which runs on every day level.
|
||||||
plain text only.
|
|
||||||
- Score = `transiting-planet weight (slow/outer planets score higher) ×
|
- Score = `transiting-planet weight (slow/outer planets score higher) ×
|
||||||
orb tightness (1.0 = exact, ~0 = at the edge) × natal-luminary bonus
|
orb tightness (1.0 = exact, ~0 = at the edge) × natal-luminary bonus
|
||||||
(transits to natal Sun/Moon score higher than to other planets)`. The
|
(transits to natal Sun/Moon score higher than to other planets)`. The
|
||||||
weights and the `DAY_QUIET`/`DAY_NOTABLE`/`DAY_SIGNIFICANT`/`DAY_MAJOR`
|
weights and the `DAY_QUIET`/`DAY_NOTABLE`/`DAY_SIGNIFICANT`/`DAY_MAJOR`
|
||||||
thresholds in `significance.c` are hand-tuned, not derived from
|
thresholds in `significance.c` are hand-tuned, not derived from
|
||||||
anything physical — expect to retune them once real readings are
|
anything physical.
|
||||||
compared against how "major" a day actually feels.
|
|
||||||
- **`narrative.c`/`narrative.h`** supplies `main.c`'s one-sentence
|
- **`narrative.c`/`narrative.h`** supplies `main.c`'s one-sentence
|
||||||
description printed under each top item, condensed from Sepharial's
|
description printed under each top item, condensed from Sepharial's
|
||||||
*Transits and Planetary Periods* (1920, public domain —
|
*Transits and Planetary Periods* (1920, public domain —
|
||||||
@@ -267,7 +274,81 @@ between the two binaries (see the next bullet).
|
|||||||
source (same status as the sign/aspect names already in `astro.c`).
|
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`
|
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
|
uses elsewhere) omits that framing and prints the planet's narrative on
|
||||||
its own.
|
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:
|
- **`reading_print_json`** (`engine/src/reading.c`) is the wire format:
|
||||||
the full `DailyReading` (natal, transits, spread), using the
|
the full `DailyReading` (natal, transits, spread), using the
|
||||||
language-independent `astro_*_slug()`/`tarot_*_slug()` accessors
|
language-independent `astro_*_slug()`/`tarot_*_slug()` accessors
|
||||||
@@ -283,31 +364,37 @@ between the two binaries (see the next bullet).
|
|||||||
and fills a `DailyReading` with those (used for scoring), plus
|
and fills a `DailyReading` with those (used for scoring), plus
|
||||||
`"natal"."bodies"`/`"transits"."bodies"` (each body's sign + whole-sign
|
`"natal"."bodies"`/`"transits"."bodies"` (each body's sign + whole-sign
|
||||||
house, via `parse_bodies()` — used only for the sign/house context
|
house, via `parse_bodies()` — used only for the sign/house context
|
||||||
`main.c` prints alongside each significant event, never for scoring).
|
`main.c` prints alongside each significant event, never for scoring)
|
||||||
`"spread"` is left zeroed entirely - nothing reads it. An aspect naming
|
and `"spread"."positions"` (each position's card + reversed flag, via
|
||||||
a planet/aspect-type slug it doesn't recognize, or a document missing
|
`parse_spread()` — used by `main.c`'s full Celtic Cross readout and by
|
||||||
`"transits"."aspects"` entirely (as opposed to a present-but-empty
|
`guidance.c`'s Attitude/Outcome framing, never for scoring). An
|
||||||
array, which is a valid "no aspects today"), makes the whole load fail;
|
aspect naming a planet/aspect-type slug it doesn't recognize, or a
|
||||||
by contrast an unrecognized body slug or missing `house` inside
|
document missing `"transits"."aspects"` entirely (as opposed to a
|
||||||
`"bodies"` is silently skipped (that body's `house` stays `0`, an
|
present-but-empty array, which is a valid "no aspects today"), makes
|
||||||
invalid whole-sign house number used as "no data" - `main.c` checks for
|
the whole load fail; by contrast an unrecognized body/card/position
|
||||||
it before printing sign/house), since that's supplementary display
|
slug, a missing `house` inside `"bodies"`, or a missing `"spread"` key
|
||||||
context rather than something scoring depends on.
|
entirely is silently skipped/zeroed (`main.c` checks for the body
|
||||||
- **Deliberately header-only dependency on the engine, throughout**:
|
"no data" sentinel before printing sign/house), since all of that is
|
||||||
`significance.h`/`reading_io.h`/`narrative.h` `#include` engine headers
|
supplementary display context rather than something scoring depends
|
||||||
(`reading.h`/`astro.h`) for the struct/enum *definitions*, but the root
|
on.
|
||||||
`Makefile` never compiles or links any engine `.c` file (or the
|
- **Mostly header-only dependency on the engine**: `significance.h`/
|
||||||
vendored Astronomy Engine) into the interpreter's binary or tests —
|
`reading_io.h`/`narrative.h`/`guidance.h` `#include` engine headers
|
||||||
every
|
(`reading.h`/`astro.h`/`tarot.h`) for the struct/enum *definitions*,
|
||||||
test in `interpreter/tests/` runs against hand-built JSON text or
|
and the root `Makefile` never compiles or links `astro.c`, `tarot.c`,
|
||||||
hand-built `DailyReading` values, with no real ephemeris/tarot-draw
|
`rng.c`, `reading.c`, or the vendored Astronomy Engine into the
|
||||||
call involved anywhere. Consequently `reading_io.c` (planet/aspect/sign
|
interpreter's binary or tests — every test in `interpreter/tests/`
|
||||||
slugs) and `main.c` (planet/aspect/sign display names) each duplicate a
|
runs against hand-built JSON text or hand-built `DailyReading`/
|
||||||
small local table rather than linking `astro.c` to reuse its own -
|
`DailyInterpretation` values, with no real ephemeris/tarot-draw call
|
||||||
keep those in sync if the engine's slugs/names ever change.
|
involved anywhere. Consequently `reading_io.c` (planet/aspect/sign/
|
||||||
- Only `Aspect`s compete for the top 5 so far (`SignificantItemKind` is
|
card/position slugs) and `main.c` (planet/aspect/sign display names)
|
||||||
currently just `ITEM_ASPECT`); moon phase and house ingresses are
|
each duplicate a small local table rather than linking `astro.c` to
|
||||||
candidate future item kinds but aren't scored yet.
|
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
|
## Licensing
|
||||||
|
|
||||||
@@ -325,3 +412,14 @@ between the two binaries (see the next bullet).
|
|||||||
Sepharial's *Transits and Planetary Periods* (1920, public domain —
|
Sepharial's *Transits and Planetary Periods* (1920, public domain —
|
||||||
`res/Transits_and_Planetary_Periods.pdf`), except the Moon and Pluto
|
`res/Transits_and_Planetary_Periods.pdf`), except the Moon and Pluto
|
||||||
entries, which are original (see narrative.c's own comment for why).
|
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).
|
||||||
|
|||||||
@@ -36,29 +36,45 @@ ENGINE_TEST_BINARY := $(BUILD_DIR)/smoke_test
|
|||||||
|
|
||||||
# ---- interpreter ----
|
# ---- interpreter ----
|
||||||
|
|
||||||
# Only ever compiles this module's own sources - significance.h/
|
# Mostly compiles only this module's own sources - significance.h/
|
||||||
# reading_io.h reach into engine/src/ for struct/enum *definitions*
|
# reading_io.h/narrative.h reach into engine/src/ for struct/enum
|
||||||
# (plain #includes), but no engine/src/*.c (or the vendored Astronomy
|
# *definitions* (plain #includes) without linking any engine/src/*.c (or
|
||||||
# Engine) is compiled or linked here. That's what keeps this module (and
|
# the vendored Astronomy Engine). That's what keeps those modules (and
|
||||||
# its tests) independent of the engine's actual ephemeris/tarot-draw
|
# their tests) independent of the engine's actual ephemeris/tarot-draw
|
||||||
# implementation - see significance.c's and reading_io.c's own comments
|
# implementation - see significance.c's and reading_io.c's own comments
|
||||||
# for why each duplicates a small table instead of linking astro.c.
|
# for why each duplicates a small table instead of linking astro.c.
|
||||||
|
#
|
||||||
|
# The one deliberate exception is engine/src/tarot_data.c (card/position
|
||||||
|
# names and Waite meaning text) and its own i18n.c dependency: both are
|
||||||
|
# pure, deterministic lookup tables with no ephemeris, no randomness, and
|
||||||
|
# (since the interpreter never calls i18n_load) no file I/O either, so
|
||||||
|
# linking them doesn't compromise interpreter/tests/'s "no real
|
||||||
|
# ephemeris/tarot-draw call" independence - it just avoids duplicating
|
||||||
|
# ~40 short strings of Waite's own card text a second time for
|
||||||
|
# main.c's/guidance.c's full-spread readout. See CLAUDE.md.
|
||||||
INTERP_SRC_DIR := interpreter/src
|
INTERP_SRC_DIR := interpreter/src
|
||||||
INTERP_TEST_DIR := interpreter/tests
|
INTERP_TEST_DIR := interpreter/tests
|
||||||
|
|
||||||
INTERP_LIB_SRCS := $(INTERP_SRC_DIR)/significance.c $(INTERP_SRC_DIR)/json.c \
|
INTERP_LIB_SRCS := $(INTERP_SRC_DIR)/significance.c $(INTERP_SRC_DIR)/json.c \
|
||||||
$(INTERP_SRC_DIR)/reading_io.c $(INTERP_SRC_DIR)/narrative.c
|
$(INTERP_SRC_DIR)/reading_io.c $(INTERP_SRC_DIR)/narrative.c \
|
||||||
|
$(INTERP_SRC_DIR)/guidance.c
|
||||||
INTERP_LIB_OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(INTERP_LIB_SRCS))
|
INTERP_LIB_OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(INTERP_LIB_SRCS))
|
||||||
INTERP_MAIN_OBJ := $(OBJ_DIR)/$(INTERP_SRC_DIR)/main.o
|
INTERP_MAIN_OBJ := $(OBJ_DIR)/$(INTERP_SRC_DIR)/main.o
|
||||||
|
|
||||||
|
# Shared with the engine build (same source, same flags - see the
|
||||||
|
# comment above); reused as-is rather than compiled twice.
|
||||||
|
INTERP_TAROT_TEXT_OBJS := $(OBJ_DIR)/$(ENGINE_SRC_DIR)/tarot_data.o $(OBJ_DIR)/$(ENGINE_SRC_DIR)/i18n.o
|
||||||
|
|
||||||
INTERP_BINARY := $(DIST_DIR)/interpreter-cli
|
INTERP_BINARY := $(DIST_DIR)/interpreter-cli
|
||||||
|
|
||||||
SIGNIFICANCE_TEST_OBJ := $(OBJ_DIR)/$(INTERP_TEST_DIR)/significance_test.o
|
SIGNIFICANCE_TEST_OBJ := $(OBJ_DIR)/$(INTERP_TEST_DIR)/significance_test.o
|
||||||
JSON_TEST_OBJ := $(OBJ_DIR)/$(INTERP_TEST_DIR)/json_test.o
|
JSON_TEST_OBJ := $(OBJ_DIR)/$(INTERP_TEST_DIR)/json_test.o
|
||||||
NARRATIVE_TEST_OBJ := $(OBJ_DIR)/$(INTERP_TEST_DIR)/narrative_test.o
|
NARRATIVE_TEST_OBJ := $(OBJ_DIR)/$(INTERP_TEST_DIR)/narrative_test.o
|
||||||
|
GUIDANCE_TEST_OBJ := $(OBJ_DIR)/$(INTERP_TEST_DIR)/guidance_test.o
|
||||||
SIGNIFICANCE_TEST_BIN := $(BUILD_DIR)/significance_test
|
SIGNIFICANCE_TEST_BIN := $(BUILD_DIR)/significance_test
|
||||||
JSON_TEST_BIN := $(BUILD_DIR)/json_test
|
JSON_TEST_BIN := $(BUILD_DIR)/json_test
|
||||||
NARRATIVE_TEST_BIN := $(BUILD_DIR)/narrative_test
|
NARRATIVE_TEST_BIN := $(BUILD_DIR)/narrative_test
|
||||||
|
GUIDANCE_TEST_BIN := $(BUILD_DIR)/guidance_test
|
||||||
|
|
||||||
.PHONY: all test clean images i18n scripts
|
.PHONY: all test clean images i18n scripts
|
||||||
|
|
||||||
@@ -68,7 +84,7 @@ $(ENGINE_BINARY): $(ENGINE_OBJS) $(ENGINE_MAIN_OBJ)
|
|||||||
@mkdir -p $(DIST_DIR)
|
@mkdir -p $(DIST_DIR)
|
||||||
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
|
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
|
||||||
|
|
||||||
$(INTERP_BINARY): $(INTERP_LIB_OBJS) $(INTERP_MAIN_OBJ)
|
$(INTERP_BINARY): $(INTERP_LIB_OBJS) $(INTERP_MAIN_OBJ) $(INTERP_TAROT_TEXT_OBJS)
|
||||||
@mkdir -p $(DIST_DIR)
|
@mkdir -p $(DIST_DIR)
|
||||||
$(CC) $(CFLAGS) -o $@ $^
|
$(CC) $(CFLAGS) -o $@ $^
|
||||||
|
|
||||||
@@ -109,11 +125,12 @@ scripts:
|
|||||||
cp $(ENGINE_SCRIPTS_DIR)/user.properties.template $(DIST_DIR)/user.properties; \
|
cp $(ENGINE_SCRIPTS_DIR)/user.properties.template $(DIST_DIR)/user.properties; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
test: $(ENGINE_TEST_BINARY) $(SIGNIFICANCE_TEST_BIN) $(JSON_TEST_BIN) $(NARRATIVE_TEST_BIN)
|
test: $(ENGINE_TEST_BINARY) $(SIGNIFICANCE_TEST_BIN) $(JSON_TEST_BIN) $(NARRATIVE_TEST_BIN) $(GUIDANCE_TEST_BIN)
|
||||||
./$(ENGINE_TEST_BINARY)
|
./$(ENGINE_TEST_BINARY)
|
||||||
./$(SIGNIFICANCE_TEST_BIN)
|
./$(SIGNIFICANCE_TEST_BIN)
|
||||||
./$(JSON_TEST_BIN)
|
./$(JSON_TEST_BIN)
|
||||||
./$(NARRATIVE_TEST_BIN)
|
./$(NARRATIVE_TEST_BIN)
|
||||||
|
./$(GUIDANCE_TEST_BIN)
|
||||||
|
|
||||||
$(ENGINE_TEST_BINARY): $(ENGINE_OBJS) $(ENGINE_TEST_OBJ)
|
$(ENGINE_TEST_BINARY): $(ENGINE_OBJS) $(ENGINE_TEST_OBJ)
|
||||||
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
|
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
|
||||||
@@ -124,7 +141,11 @@ $(SIGNIFICANCE_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/significance.o $(SIGNIFIC
|
|||||||
$(JSON_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/json.o $(OBJ_DIR)/$(INTERP_SRC_DIR)/reading_io.o $(JSON_TEST_OBJ)
|
$(JSON_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/json.o $(OBJ_DIR)/$(INTERP_SRC_DIR)/reading_io.o $(JSON_TEST_OBJ)
|
||||||
$(CC) $(CFLAGS) -o $@ $^
|
$(CC) $(CFLAGS) -o $@ $^
|
||||||
|
|
||||||
$(NARRATIVE_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/narrative.o $(NARRATIVE_TEST_OBJ)
|
$(NARRATIVE_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/narrative.o $(INTERP_TAROT_TEXT_OBJS) $(NARRATIVE_TEST_OBJ)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^
|
||||||
|
|
||||||
|
$(GUIDANCE_TEST_BIN): $(OBJ_DIR)/$(INTERP_SRC_DIR)/guidance.o $(OBJ_DIR)/$(INTERP_SRC_DIR)/significance.o \
|
||||||
|
$(INTERP_TAROT_TEXT_OBJS) $(GUIDANCE_TEST_OBJ)
|
||||||
$(CC) $(CFLAGS) -o $@ $^
|
$(CC) $(CFLAGS) -o $@ $^
|
||||||
|
|
||||||
$(OBJ_DIR)/%.o: %.c
|
$(OBJ_DIR)/%.o: %.c
|
||||||
|
|||||||
@@ -7,11 +7,10 @@
|
|||||||
A Pebble smartwatch app that produces a daily personalized tarot (Celtic
|
A Pebble smartwatch app that produces a daily personalized tarot (Celtic
|
||||||
Cross) and astrology (natal chart + transits) reading.
|
Cross) and astrology (natal chart + transits) reading.
|
||||||
|
|
||||||
**Status:** only the core engine exists so far — a plain-C, dependency-free
|
This repository holds the core engine: a plain-C, dependency-free
|
||||||
library and standalone CLI (`dist/deck-engine`) that computes a full
|
library and standalone CLI (`dist/deck-engine`) that computes a full
|
||||||
reading without needing a Pebble device, emulator, or SDK. The watchapp
|
reading without needing a Pebble device, emulator, or SDK, designed so
|
||||||
itself hasn't been built yet; the engine is designed so it can be linked
|
it links straight into Pebble watch code without rework.
|
||||||
into it later without rework.
|
|
||||||
|
|
||||||
## What it computes
|
## What it computes
|
||||||
|
|
||||||
@@ -45,10 +44,14 @@ engine/
|
|||||||
the user.properties template.
|
the user.properties template.
|
||||||
dist/ Build output (gitignored) — see below.
|
dist/ Build output (gitignored) — see below.
|
||||||
interpreter/ Separate module + binary (dist/interpreter-cli):
|
interpreter/ Separate module + binary (dist/interpreter-cli):
|
||||||
scores how significant a day's transits are
|
scores how significant a day's transits are,
|
||||||
and prints a short narrative note for each,
|
reads out the full Celtic Cross spread with
|
||||||
reading deck-engine's --format json output
|
every card's meaning, and closes with guidance
|
||||||
(see CLAUDE.md).
|
(every day, tailored to how significant it is)
|
||||||
|
tying the top transit to the spread, reading
|
||||||
|
deck-engine's --format json output. Supports
|
||||||
|
--format text|html|json and --lang en|de for
|
||||||
|
its own output too (see CLAUDE.md).
|
||||||
docs/ Architecture/dataflow diagrams, input/output format reference.
|
docs/ Architecture/dataflow diagrams, input/output format reference.
|
||||||
Makefile Single root Makefile, builds both engine/ and interpreter/.
|
Makefile Single root Makefile, builds both engine/ and interpreter/.
|
||||||
```
|
```
|
||||||
@@ -94,16 +97,23 @@ dist/deck-engine --seed "2026-07-03" \
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Interpreter** (`interpreter/`, built by the same root `make`):
|
**Interpreter** (`interpreter/`, built by the same root `make`):
|
||||||
scores how significant a day's transits are, reading `deck-engine`'s
|
scores how significant a day's transits are, reads out the full Celtic
|
||||||
`--format json` output — the two binaries compose over that JSON, they're
|
Cross spread with every card's meaning, and closes with guidance tying
|
||||||
never linked together. `dist/run-interpreter.sh` is the daily-use
|
the day's biggest transit to the Attitude/Outcome cards - reading
|
||||||
equivalent of `run-engine.sh` (same OS clock + `user.properties` inputs),
|
`deck-engine`'s `--format json` output. Its own output supports the same
|
||||||
piping straight into `interpreter-cli`:
|
`--format text|html|json` and `--lang en|de` as `deck-engine` (note this
|
||||||
|
`--lang` is independent of whatever language `deck-engine` was run
|
||||||
|
with - `--format json` is always language-independent, see
|
||||||
|
`docs/input-output-format.md`). `dist/run-interpreter.sh` is the
|
||||||
|
daily-use equivalent of `run-engine.sh` (same OS clock + `user.properties`
|
||||||
|
inputs, including `user.properties`' `lang=`), piping straight into
|
||||||
|
`interpreter-cli`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dist/deck-engine ... --format json | dist/interpreter-cli
|
dist/deck-engine ... --format json | dist/interpreter-cli [--format text|html|json] [--lang en|de]
|
||||||
# or, for daily use:
|
# or, for daily use:
|
||||||
dist/run-interpreter.sh
|
dist/run-interpreter.sh # text output, language from user.properties
|
||||||
|
dist/run-interpreter.sh html de # HTML report with card art, override the language
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
@@ -124,3 +134,10 @@ Transit narrative text in `interpreter/src/narrative.c` is condensed
|
|||||||
from Sepharial's *Transits and Planetary Periods* (1920), also in the
|
from Sepharial's *Transits and Planetary Periods* (1920), also in the
|
||||||
public domain, except the Moon and Pluto entries, which are original
|
public domain, except the Moon and Pluto entries, which are original
|
||||||
(see the file's own comment).
|
(see the file's own comment).
|
||||||
|
|
||||||
|
Guidance text in `interpreter/src/guidance.c` is original, written for
|
||||||
|
this project.
|
||||||
|
|
||||||
|
`engine/i18n/de.lang`'s translations of the above two files' text
|
||||||
|
(`narrative.*`/`guidance.*` keys) are original German translations made
|
||||||
|
for this project, same status as its card/spread text.
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ know before changing the engine. This document is the visual overview.
|
|||||||
(MIT), pinned to a specific commit — see its `VENDORED.md`.
|
(MIT), pinned to a specific commit — see its `VENDORED.md`.
|
||||||
- **`engine/src/`** is the core engine. Everything except `main.c`,
|
- **`engine/src/`** is the core engine. Everything except `main.c`,
|
||||||
`reading.c`'s `reading_print_text`/`reading_print_html` functions, and
|
`reading.c`'s `reading_print_text`/`reading_print_html` functions, and
|
||||||
`i18n.c`'s file-loading half is free of `stdio`/CLI assumptions,
|
`i18n.c`'s file-loading half is free of `stdio`/CLI assumptions, so it
|
||||||
specifically so it can be linked into the Pebble watchapp later without
|
links straight into a Pebble watchapp without rework.
|
||||||
rework.
|
|
||||||
- `rng.c` — deterministic string-seeded PRNG (FNV-1a + splitmix64).
|
- `rng.c` — deterministic string-seeded PRNG (FNV-1a + splitmix64).
|
||||||
- `tarot.c` / `tarot_data.c` — the Celtic Cross draw and its content.
|
- `tarot.c` / `tarot_data.c` — the Celtic Cross draw and its content.
|
||||||
- `astro.c` — natal chart + daily transits, built on the vendored
|
- `astro.c` — natal chart + daily transits, built on the vendored
|
||||||
@@ -41,9 +40,6 @@ know before changing the engine. This document is the visual overview.
|
|||||||
`engine/scripts/user.properties.local` (gitignored) once it's filled
|
`engine/scripts/user.properties.local` (gitignored) once it's filled
|
||||||
in, and restored from there if `dist/` is ever wiped — see
|
in, and restored from there if `dist/` is ever wiped — see
|
||||||
`CLAUDE.md`'s "Build & run".
|
`CLAUDE.md`'s "Build & run".
|
||||||
- **The Pebble watchapp does not exist yet.** When it's built, it will
|
|
||||||
call `reading_generate()` directly and walk the returned struct to lay
|
|
||||||
out its own screens — it has no reason to touch `reading_print_*`.
|
|
||||||
|
|
||||||
## Dataflow
|
## Dataflow
|
||||||
|
|
||||||
@@ -64,9 +60,10 @@ computations and combines their results:
|
|||||||
with position and upright/reversed orientation, fully determined by
|
with position and upright/reversed orientation, fully determined by
|
||||||
the seed string alone.
|
the seed string alone.
|
||||||
|
|
||||||
These three results are combined into one `DailyReading` struct, which is
|
These three results are combined into one `DailyReading` struct — the
|
||||||
either rendered by `reading_print_text`/`reading_print_html` (used by the
|
real API surface. `reading_print_text`/`reading_print_html` (used by the
|
||||||
CLI) or, in the future, walked directly by watchapp UI code. See
|
CLI) are just one way of consuming it; any caller, including watch UI
|
||||||
|
code, can walk the struct's fields directly. See
|
||||||
[`input-output-format.md`](input-output-format.md) for the exact fields
|
[`input-output-format.md`](input-output-format.md) for the exact fields
|
||||||
and output formats.
|
and output formats.
|
||||||
|
|
||||||
|
|||||||
@@ -55,12 +55,6 @@ digraph architecture {
|
|||||||
{ rank=same; binary; distimg; disti18n; runsh; props; }
|
{ rank=same; binary; distimg; disti18n; runsh; props; }
|
||||||
}
|
}
|
||||||
|
|
||||||
subgraph cluster_watch {
|
|
||||||
label="Pebble watchapp — not built yet";
|
|
||||||
style="dashed"; color=firebrick; fontsize=11; fontcolor=firebrick;
|
|
||||||
watch [label="watchapp C / UI\n(future)", fillcolor=white, color=firebrick, fontcolor=firebrick, style="rounded,dashed"];
|
|
||||||
}
|
|
||||||
|
|
||||||
pdf -> tarot_data [label="sourced from", style=dotted, color=gray40];
|
pdf -> tarot_data [label="sourced from", style=dotted, color=gray40];
|
||||||
|
|
||||||
tarot -> rng;
|
tarot -> rng;
|
||||||
@@ -78,6 +72,4 @@ digraph architecture {
|
|||||||
images -> distimg [label="`make images`\ncopies", style=dotted, color=gray40];
|
images -> distimg [label="`make images`\ncopies", style=dotted, color=gray40];
|
||||||
langfiles -> disti18n [label="`make i18n`\ncopies", style=dotted, color=gray40];
|
langfiles -> disti18n [label="`make i18n`\ncopies", style=dotted, color=gray40];
|
||||||
runsh -> props [label="reads at\nrun time", style=dotted, color=gray40];
|
runsh -> props [label="reads at\nrun time", style=dotted, color=gray40];
|
||||||
|
|
||||||
watch -> reading [label="will call\nreading_generate()\ndirectly", style=dashed, color=firebrick, fontcolor=firebrick];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,11 +33,9 @@ digraph dataflow {
|
|||||||
|
|
||||||
print_text [label="reading_print_text()", fillcolor="#cfe8fb"];
|
print_text [label="reading_print_text()", fillcolor="#cfe8fb"];
|
||||||
print_html [label="reading_print_html()", fillcolor="#cfe8fb"];
|
print_html [label="reading_print_html()", fillcolor="#cfe8fb"];
|
||||||
watch_fn [label="watchapp UI code\n(future — walks the struct\ndirectly, no print_* call)", fillcolor=white, color=firebrick, fontcolor=firebrick, style="rounded,dashed"];
|
|
||||||
|
|
||||||
out_text [label="stdout: plain-text report", shape=parallelogram, fillcolor="#c8f0c8"];
|
out_text [label="stdout: plain-text report", shape=parallelogram, fillcolor="#c8f0c8"];
|
||||||
out_html [label="stdout: reading.html\n(<img src=\"img/...\"> per card)", shape=parallelogram, fillcolor="#c8f0c8"];
|
out_html [label="stdout: reading.html\n(<img src=\"img/...\"> per card)", shape=parallelogram, fillcolor="#c8f0c8"];
|
||||||
out_watch [label="watch screens\n(future)", shape=parallelogram, fillcolor=white, color=firebrick, fontcolor=firebrick, style="dashed"];
|
|
||||||
|
|
||||||
cli -> main;
|
cli -> main;
|
||||||
runsh -> main;
|
runsh -> main;
|
||||||
@@ -59,9 +57,7 @@ digraph dataflow {
|
|||||||
|
|
||||||
daily_reading -> print_text;
|
daily_reading -> print_text;
|
||||||
daily_reading -> print_html;
|
daily_reading -> print_html;
|
||||||
daily_reading -> watch_fn [style=dashed, color=firebrick];
|
|
||||||
|
|
||||||
print_text -> out_text;
|
print_text -> out_text;
|
||||||
print_html -> out_html;
|
print_html -> out_html;
|
||||||
watch_fn -> out_watch [style=dashed, color=firebrick];
|
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 238 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 236 KiB |
+225
-36
@@ -4,9 +4,10 @@ See [`architecture.md`](architecture.md) for how these fit together.
|
|||||||
There are two ways to provide input (direct CLI flags, or `run-engine.sh`
|
There are two ways to provide input (direct CLI flags, or `run-engine.sh`
|
||||||
+ `user.properties`) and three output formats (`text`, `html`, `json`); the
|
+ `user.properties`) and three output formats (`text`, `html`, `json`); the
|
||||||
`DailyReading` struct is the programmatic form all three are rendered
|
`DailyReading` struct is the programmatic form all three are rendered
|
||||||
from, and the one the future watchapp will consume directly. `json` is
|
from, and the one watch UI code consumes directly. `json` is also
|
||||||
also `interpreter-cli`'s input format — see "The interpreter" at the
|
`interpreter-cli`'s input format — see "The interpreter" at the bottom
|
||||||
bottom of this file.
|
of this file, which supports the same three output formats (and the
|
||||||
|
same `--lang`) for its own report, independently of `deck-engine`.
|
||||||
|
|
||||||
## Input
|
## Input
|
||||||
|
|
||||||
@@ -90,8 +91,8 @@ build, like `dist/img/`). Adding a language is just dropping another
|
|||||||
### `DailyReading` (the real output — `reading.h`)
|
### `DailyReading` (the real output — `reading.h`)
|
||||||
|
|
||||||
`reading_generate()` is the actual API; `text`/`html` below are just two
|
`reading_generate()` is the actual API; `text`/`html` below are just two
|
||||||
ways of rendering the struct it fills in. This is what the watchapp will
|
ways of rendering the struct it fills in. This is what watch UI code
|
||||||
read directly once it exists.
|
reads directly.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -227,16 +228,39 @@ read.
|
|||||||
`dist/interpreter-cli` is a separate binary — see `CLAUDE.md`'s
|
`dist/interpreter-cli` is a separate binary — see `CLAUDE.md`'s
|
||||||
"Interpretation" section for the full architecture — that reads a
|
"Interpretation" section for the full architecture — that reads a
|
||||||
`--format json` document and scores how significant today's transits
|
`--format json` document and scores how significant today's transits
|
||||||
are. It never links the engine; it only depends on the JSON shape
|
are. It never links `astro.c`/`tarot.c`/`reading.c` (or the vendored
|
||||||
above.
|
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.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dist/deck-engine ... --format json | dist/interpreter-cli
|
dist/deck-engine ... --format json | dist/interpreter-cli --format html --lang de
|
||||||
# or:
|
# or:
|
||||||
dist/deck-engine ... --format json > reading.json
|
dist/deck-engine ... --format json > reading.json
|
||||||
dist/interpreter-cli reading.json
|
dist/interpreter-cli --format json reading.json
|
||||||
# or, for daily use (OS clock + user.properties, same inputs as run-engine.sh):
|
# or, for daily use (OS clock + user.properties, same inputs as run-engine.sh,
|
||||||
dist/run-interpreter.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
|
- **Input**: a file path argument, or stdin if no argument (or `-`) is
|
||||||
@@ -244,30 +268,195 @@ dist/run-interpreter.sh
|
|||||||
means "no notable transits today"; a missing key is treated as invalid
|
means "no notable transits today"; a missing key is treated as invalid
|
||||||
input) and drives scoring. `"natal"."bodies"`/`"transits"."bodies"` are
|
input) and drives scoring. `"natal"."bodies"`/`"transits"."bodies"` are
|
||||||
read too, for the sign/house context printed alongside each event below
|
read too, for the sign/house context printed alongside each event below
|
||||||
(not for scoring) — an entry naming an unrecognized body, or a missing
|
(not for scoring), and `"spread"."positions"` is read for the full
|
||||||
`house`, is silently skipped rather than failing the load (see
|
Celtic Cross readout and the guidance paragraph below — an entry naming
|
||||||
`parse_bodies()` in `reading_io.c`); `spread` is always ignored.
|
an unrecognized body/card/position, a missing `house`, or a missing
|
||||||
- **Output**: a plain-text report — the day's overall significance level
|
`"spread"` key entirely is silently skipped/zeroed rather than failing
|
||||||
(`Quiet`/`Notable`/`Significant`/`Major`, plus its rank out of the 4
|
the load (see `parse_bodies()`/`parse_spread()` in `reading_io.c`).
|
||||||
defined levels, e.g. `Notable (2/4)`), a "worth a deeper Celtic Cross
|
- **Input**: same for all three output formats above - only how the
|
||||||
look" line on `Major` days only, and up to 5 of today's aspects ranked
|
parsed data gets rendered differs.
|
||||||
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`'s `narrative.c` bullet) —
|
|
||||||
omitted only if the source material has nothing to say for that
|
|
||||||
planet/aspect combination:
|
|
||||||
|
|
||||||
```
|
### `--format text`
|
||||||
Day significance: Significant (3/4)
|
|
||||||
(a major transit today - worth a deeper Celtic Cross look)
|
|
||||||
|
|
||||||
Top 3 significant transits:
|
A plain-text report, printed in a fixed order —
|
||||||
1. Transiting Saturn in Aries (house 2) Square natal Sun in Taurus (house 3) (orb 0.5°, score 8.10)
|
1. the day's overall significance level (`Quiet`/`Notable`/
|
||||||
In matters of money, possessions, and personal values (house 2). Brings depression, stagnation, hindrances and obstacles, and some deprivation of the usual benefits.
|
`Significant`/`Major`, plus its rank out of the 4 defined levels,
|
||||||
2. Transiting Pluto in Aquarius (house 12) Opposition natal Moon in Capricorn (house 11) (orb 0.2°, score 7.92)
|
e.g. `Notable (2/4)`), with a "worth a deeper Celtic Cross look"
|
||||||
In matters of solitude, the subconscious, and hidden matters (house 12). Brings deep, often hidden transformation - the surfacing or dismantling of something that has outgrown its old form.
|
line on `Major` days only;
|
||||||
3. Transiting Jupiter in Leo (house 5) Trine natal Venus in Aries (house 2) (orb 2.1°, score 3.40)
|
2. up to 5 of today's aspects ranked by score, each naming the sign
|
||||||
In matters of romance, creativity, and children (house 5). Brings increase and expansion - fullness of fortune and health, and a generally fortunate time.
|
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`'s `narrative.c` bullet) — omitted only if
|
||||||
|
the source material has nothing to say for that planet/aspect
|
||||||
|
combination;
|
||||||
|
3. 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;
|
||||||
|
4. 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 on `Major`, softer on
|
||||||
|
`Quiet`/`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 — see `CLAUDE.md`'s `guidance.c` bullet.
|
||||||
|
This is printed last, as the closing takeaway after the reader has
|
||||||
|
seen the full spread it references.
|
||||||
|
|
||||||
|
```
|
||||||
|
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 him: the general influence affecting the matter.
|
||||||
|
Benevolence, compassion, credit; also confusion to enemies, obstruction, immaturity.
|
||||||
|
2. The Challenge: The Devil
|
||||||
|
This crosses him: the nature of the obstacle in the matter.
|
||||||
|
Ravage, violence, vehemence, extraordinary efforts, force, fatality.
|
||||||
|
...
|
||||||
|
7. Himself: The Magician
|
||||||
|
His 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):
|
||||||
|
|
||||||
|
```
|
||||||
|
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 ihn: 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:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<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 him: 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.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `significant_transits` has `interp.top_item_count` entries (0-5, same
|
||||||
|
ranking as `--format text`); `celtic_cross` always has exactly 10, in
|
||||||
|
`CelticCrossPosition` enum order (same order as `deck-engine`'s own
|
||||||
|
`spread.positions`).
|
||||||
|
- `orb`/`score` are `%.4f`, matching `deck-engine`'s own JSON precision
|
||||||
|
for angles.
|
||||||
|
- `guidance` is always present and non-empty (unlike
|
||||||
|
`significant_transits`, which can be `[]` on a genuinely quiet day) -
|
||||||
|
see `guidance.c`'s no-aspects-at-all fallback in `CLAUDE.md`.
|
||||||
|
- With `--lang de`, every string value above except the slugs
|
||||||
|
(`"level"`, `"transiting_planet"`, `"aspect"`, `"card"`, `"position"`,
|
||||||
|
etc.) changes language; the slugs never do.
|
||||||
|
|||||||
+132
-21
@@ -18,6 +18,12 @@ exactly where the planets were, and which zodiac sign was rising, at the
|
|||||||
moment you were born — your **natal chart**. Everything else the app
|
moment you were born — your **natal chart**. Everything else the app
|
||||||
does is compared against that one fixed reference point.
|
does is compared against that one fixed reference point.
|
||||||
|
|
||||||
|
You can also choose a **language** for your reading - English or German.
|
||||||
|
Every part of the reading described below, including the guidance
|
||||||
|
paragraph at the end, is available in either; switching languages
|
||||||
|
doesn't change any of the astrology or the tarot draw itself, only how
|
||||||
|
it's worded.
|
||||||
|
|
||||||
## What happens every day
|
## What happens every day
|
||||||
|
|
||||||
Each day, the app does two independent things and then combines them:
|
Each day, the app does two independent things and then combines them:
|
||||||
@@ -95,21 +101,51 @@ That wording is condensed from a public-domain 1920 astrology book
|
|||||||
(Sepharial's *Transits and Planetary Periods*), the same way the tarot
|
(Sepharial's *Transits and Planetary Periods*), the same way the tarot
|
||||||
card meanings are condensed from Waite's 1911 book.
|
card meanings are condensed from Waite's 1911 book.
|
||||||
|
|
||||||
|
Every day — Quiet or Major — the app also reads out your full 10-card
|
||||||
|
Celtic Cross spread, position by position, each with that card's actual
|
||||||
|
meaning (drawn from the same Waite text as the rest of the reading), so
|
||||||
|
you get one complete, readable account of the spread rather than just
|
||||||
|
a list of card names.
|
||||||
|
|
||||||
|
After that spread, you get a short paragraph of actual guidance: how to
|
||||||
|
meet the day, given both the single biggest transit happening and your
|
||||||
|
own tarot spread. It looks at that transit's character (is it an easy
|
||||||
|
angle or a tense one?) together with your *Attitude* card — the
|
||||||
|
position Waite describes as "his position or attitude in the
|
||||||
|
circumstances", i.e. how you're currently approaching things — upright
|
||||||
|
or reversed, and gives a concrete stance to take today. It then names
|
||||||
|
your *Attitude* and *Outcome* cards again, this time spelling out what
|
||||||
|
each one actually means for the orientation it landed in — so instead
|
||||||
|
of just "your Outcome card is the Wheel of Fortune, reversed" you get
|
||||||
|
that card's real meaning alongside it. This is the one place the app
|
||||||
|
actually combines the astrology and the tarot into a single piece of
|
||||||
|
advice, rather than leaving you to read them side by side yourself, and
|
||||||
|
it's printed last, as the closing takeaway once you've seen the full
|
||||||
|
spread it's referring to.
|
||||||
|
|
||||||
|
The wording flexes with how big the day is: on a Major day it opens
|
||||||
|
with something like "With Pluto as today's dominant influence"; on a
|
||||||
|
quieter day with only a mild aspect in play it's softer, e.g. "With a
|
||||||
|
mild touch from Jupiter today" or, fainter still, "Mercury is only
|
||||||
|
faintly active today, but for what it's worth"; and on a genuinely
|
||||||
|
uneventful day with no aspects at all, there's no planet to name, so it
|
||||||
|
falls back to a plain read of your Attitude card's orientation alone
|
||||||
|
("There's no standout transit today - it's an astrologically quiet
|
||||||
|
day..."). The underlying stance advice itself doesn't change with
|
||||||
|
intensity — only how insistently it's introduced.
|
||||||
|
|
||||||
## What a day's reading actually gives you
|
## What a day's reading actually gives you
|
||||||
|
|
||||||
Put together, one day's reading contains: your natal chart (for
|
Put together, one day's reading contains: your natal chart (for
|
||||||
reference), today's sky and how it's speaking to your chart, how
|
reference), today's sky and how it's speaking to your chart, how
|
||||||
significant today is overall (with a plain-language note on each of the
|
significant today is overall (with a plain-language note on each of the
|
||||||
main events driving that), and your 10-card Celtic Cross spread for the
|
main events driving that), your full 10-card Celtic Cross spread read
|
||||||
day. Right now these sit side by side — the astrology tells you *how
|
out card by card with each card's meaning, and — every day, not just
|
||||||
eventful* today is and *where* to pay attention, and the tarot spread
|
Major ones — a closing paragraph of guidance tying the day's biggest
|
||||||
gives you the actual guidance to sit with. They don't yet get woven
|
transit to your Attitude and Outcome cards specifically. Attitude and
|
||||||
into a single narrative (e.g. a spread explained *in light of* today's
|
Outcome get that treatment in the closing guidance; the other eight
|
||||||
significant transit) — that combined storytelling is the next piece to
|
positions are read in light of your natal chart and today's sky, the
|
||||||
build, not something you get today.
|
way the worked example below does by hand.
|
||||||
|
|
||||||
*(This page, like that piece, is still a work in progress — expect it
|
|
||||||
to grow as the reading itself does.)*
|
|
||||||
|
|
||||||
## A worked example
|
## A worked example
|
||||||
|
|
||||||
@@ -123,7 +159,7 @@ Sagittarius.
|
|||||||
are forming aspects back to the natal chart above — see below for which
|
are forming aspects back to the natal chart above — see below for which
|
||||||
ones actually matter.
|
ones actually matter.
|
||||||
|
|
||||||
**How big is today.**
|
**How big is today, and the significant transits.**
|
||||||
|
|
||||||
```
|
```
|
||||||
Day significance: Major (4/4)
|
Day significance: Major (4/4)
|
||||||
@@ -149,18 +185,93 @@ from the 5th house, and a disruptive-but-possibly-lucky Uranus trine
|
|||||||
from the 7th. Communication, romance, and relationships are all
|
from the 7th. Communication, romance, and relationships are all
|
||||||
quietly under pressure at once.
|
quietly under pressure at once.
|
||||||
|
|
||||||
**Tarot spread (excerpt).** Drawn independently, using today's date:
|
**The Celtic Cross, read out in full** (drawn independently, using
|
||||||
|
today's date):
|
||||||
|
|
||||||
- *The Present* — The Emperor, reversed: immaturity or a loss of
|
```
|
||||||
authority, rather than confident structure.
|
Celtic Cross:
|
||||||
- *The Challenge* — The Devil: force, ravage, an obstacle that isn't
|
1. The Present: The Emperor (Reversed)
|
||||||
easily reasoned with.
|
This covers him: the general influence affecting the matter.
|
||||||
- *The Near Future* — The World, reversed: stagnation, things staying
|
Benevolence, compassion, credit; also confusion to enemies, obstruction, immaturity.
|
||||||
fixed in place rather than resolving.
|
2. The Challenge: The Devil
|
||||||
|
This crosses him: the nature of the obstacle in the matter.
|
||||||
|
Ravage, violence, vehemence, extraordinary efforts, force, fatality.
|
||||||
|
3. The Crown: The Empress
|
||||||
|
This crowns him: the aim or ideal, the best that can be achieved.
|
||||||
|
Fruitfulness, action, initiative, length of days; also difficulty, doubt, ignorance.
|
||||||
|
4. The Foundation: Justice
|
||||||
|
This is beneath him: the basis of the matter, already actual.
|
||||||
|
Equity, rightness, probity, executive; triumph of the deserving side in law.
|
||||||
|
5. The Recent Past: Temperance
|
||||||
|
This is behind him: the influence that is just passing away.
|
||||||
|
Economy, moderation, frugality, management, accommodation.
|
||||||
|
6. The Near Future: The World (Reversed)
|
||||||
|
This is before him: the influence now coming into action.
|
||||||
|
Inertia, fixity, stagnation, permanence.
|
||||||
|
7. Himself: The Magician
|
||||||
|
His position or attitude in the circumstances.
|
||||||
|
Skill, diplomacy, address, subtlety; self-confidence, will.
|
||||||
|
8. His House: The Hermit (Reversed)
|
||||||
|
His environment and the tendencies at work therein.
|
||||||
|
Concealment, disguise, policy, fear, unreasoned caution.
|
||||||
|
9. Hopes and Fears: The Moon (Reversed)
|
||||||
|
His hopes or fears in the matter.
|
||||||
|
Instability, inconstancy, silence, lesser degrees of deception and error.
|
||||||
|
10. The Outcome: Strength
|
||||||
|
What will come: the final result of the matter.
|
||||||
|
Power, energy, action, courage, magnanimity; complete success and honours.
|
||||||
|
```
|
||||||
|
|
||||||
|
**The closing guidance**, tying the day's biggest transit (Pluto,
|
||||||
|
above) to the Attitude and Outcome cards from the spread just shown:
|
||||||
|
|
||||||
|
```
|
||||||
|
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.
|
||||||
|
```
|
||||||
|
|
||||||
Read side by side, the astrology and the tarot happen to rhyme here —
|
Read side by side, the astrology and the tarot happen to rhyme here —
|
||||||
both point toward something that wants to shift (Pluto's "hidden
|
both point toward something that wants to shift (Pluto's "hidden
|
||||||
transformation," The Devil's "force") running into something that
|
transformation," The Devil's "force") running into something that
|
||||||
feels stuck (a reversed World, an afflicted Moon) — but that's the
|
feels stuck (a reversed World, an afflicted Moon). The closing guidance
|
||||||
reader's own synthesis for now, not something the app does for you (see
|
picks up part of that thread automatically — an upright Attitude card
|
||||||
above).
|
(The Magician) paired with this neutral, concentrating Pluto transit
|
||||||
|
becomes "make sure your current approach is the one you want" — reading
|
||||||
|
every other position (Challenge, Near Future, and the rest) into that
|
||||||
|
same story the way this paragraph just did by hand is the reader's own
|
||||||
|
synthesis (see above).
|
||||||
|
|
||||||
|
**A quieter day, for contrast.** Someone else — born 10 October 1990 in
|
||||||
|
Sydney, checking their reading on 15 January 2026 — gets a much lower-key
|
||||||
|
version of the same guidance, because the biggest thing happening that
|
||||||
|
day is a loose, single Jupiter aspect rather than an exact Pluto hit:
|
||||||
|
|
||||||
|
```
|
||||||
|
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.
|
||||||
|
```
|
||||||
|
|
||||||
|
Same stance logic (a tense aspect, an upright Attitude card), but the
|
||||||
|
opening line matches how much the day actually asks of you.
|
||||||
|
|
||||||
|
**The same Major day, in German.** Switching languages doesn't change
|
||||||
|
anything about the reading itself - same cards, same transits, same
|
||||||
|
guidance logic - only the words:
|
||||||
|
|
||||||
|
```
|
||||||
|
Bedeutung des Tages: Einschneidend (4/4)
|
||||||
|
(ein einschneidender Transit heute - ein genauerer Blick auf das Keltische Kreuz lohnt sich)
|
||||||
|
|
||||||
|
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.
|
||||||
|
```
|
||||||
|
|||||||
@@ -154,3 +154,107 @@ card.judgement.reversed=Schwäche, Kleinmut, Einfalt; auch Überlegung, Entschei
|
|||||||
card.world.name=Die Welt
|
card.world.name=Die Welt
|
||||||
card.world.upright=Gesicherter Erfolg, Belohnung, Reise, Weg, Auswanderung, Flucht, Ortswechsel.
|
card.world.upright=Gesicherter Erfolg, Belohnung, Reise, Weg, Auswanderung, Flucht, Ortswechsel.
|
||||||
card.world.reversed=Trägheit, Erstarrung, Stillstand, Beständigkeit.
|
card.world.reversed=Trägheit, Erstarrung, Stillstand, Beständigkeit.
|
||||||
|
|
||||||
|
# --- UI-Bezeichnungen des Interpreters (interpreter/src/main.c) ---
|
||||||
|
interp.day_significance=Bedeutung des Tages:
|
||||||
|
interp.day_level.quiet=Ruhig
|
||||||
|
interp.day_level.notable=Bemerkenswert
|
||||||
|
interp.day_level.significant=Bedeutend
|
||||||
|
interp.day_level.major=Einschneidend
|
||||||
|
interp.major_framing=(ein einschneidender Transit heute - ein genauerer Blick auf das Keltische Kreuz lohnt sich)
|
||||||
|
interp.top_transits.one=Wichtigster Transit:
|
||||||
|
interp.top_transits.many=Top %d wichtige Transits:
|
||||||
|
interp.no_notable_transits=Heute keine nennenswerten Transits.
|
||||||
|
interp.in_sign_house=in %s (Haus %d)
|
||||||
|
interp.score=Punktzahl
|
||||||
|
interp.page_title=Deck in a Dash - Interpretation
|
||||||
|
interp.heading=Tägliche Interpretation
|
||||||
|
interp.heading_day_significance=Bedeutung des Tages
|
||||||
|
interp.heading_transits=Wichtige Transits
|
||||||
|
interp.heading_guidance=Rat für heute
|
||||||
|
|
||||||
|
# --- Transit-Deutungen (interpreter/src/narrative.c), sinngemäße
|
||||||
|
# Übertragung aus Sepharials Transits and Planetary Periods (1920,
|
||||||
|
# gemeinfrei), Kapitel VIII "Effects of Transits" - eigene Übersetzung
|
||||||
|
# für dieses Projekt, keine Übernahme aus einer veröffentlichten
|
||||||
|
# deutschen Ausgabe. Mond und Pluto sind eigener, englischsprachig
|
||||||
|
# verfasster Text (siehe narrative.c) und hier ebenfalls frei übertragen. ---
|
||||||
|
narrative.house_frame=In Fragen von %s (Haus %d).
|
||||||
|
narrative.house.1=deines Selbstbilds, deiner Identität und deines äußeren Auftretens
|
||||||
|
narrative.house.2=Geld, Besitz und persönlicher Werte
|
||||||
|
narrative.house.3=Kommunikation, Geschwistern und alltäglichem Lernen
|
||||||
|
narrative.house.4=Zuhause, Familie und deiner Herkunft
|
||||||
|
narrative.house.5=Romantik, Kreativität und Kindern
|
||||||
|
narrative.house.6=täglicher Arbeit, Routine und Gesundheit
|
||||||
|
narrative.house.7=Partnerschaften und engen Beziehungen
|
||||||
|
narrative.house.8=gemeinsamen Ressourcen, Nähe und Wandel
|
||||||
|
narrative.house.9=Reisen, höherer Bildung und Überzeugungen
|
||||||
|
narrative.house.10=Karriere, Ansehen und öffentlichem Auftreten
|
||||||
|
narrative.house.11=Freundschaften, Gemeinschaft und Zukunftshoffnungen
|
||||||
|
narrative.house.12=Rückzug, dem Unbewussten und verborgenen Dingen
|
||||||
|
|
||||||
|
narrative.sun.base=
|
||||||
|
narrative.sun.harmonious=Vorteile durch Vorgesetzte und Aufstieg in deinem Lebens- und Arbeitsbereich - Ehrungen, Vergütungen und erfolgreiche neue Verbindungen.
|
||||||
|
narrative.sun.discordant=Herabsetzung und Unehre, Verlust der Stellung und ungünstige Beurteilung durch Vorgesetzte.
|
||||||
|
narrative.moon.base=Färbt den alltäglichen und häuslichen Lebensbereich, oft verbunden mit der Eröffnung neuer Wege.
|
||||||
|
narrative.moon.harmonious=Diese Veränderungen fallen eher vorteilhaft aus.
|
||||||
|
narrative.moon.discordant=Diese Veränderungen fallen eher ungünstig aus, mit etwas Unwohlsein oder häuslichen Reibereien.
|
||||||
|
narrative.mercury.base=Betrifft Schriftliches, Reisen, Handel und alltägliche Angelegenheiten - ein neutraler Bote, dessen Wirkung sich nach der Art des Aspekts richtet, den er bildet.
|
||||||
|
narrative.mercury.harmonious=
|
||||||
|
narrative.mercury.discordant=
|
||||||
|
narrative.venus.base=Rückt häusliche und gesellschaftliche Angelegenheiten in den Vordergrund - Glück, Annehmlichkeiten und Gunstbezeigungen.
|
||||||
|
narrative.venus.harmonious=Erfolg in Liebesangelegenheiten und künstlerischen Unternehmungen ist wahrscheinlich.
|
||||||
|
narrative.venus.discordant=Kummer und Enttäuschung sind wahrscheinlicher.
|
||||||
|
narrative.mars.base=Eine anstrengende Zeit voller Streit, Auseinandersetzungen und Ärger, mit einem gewissen Verletzungsrisiko je nach dem Zeichen, in dem er steht.
|
||||||
|
narrative.mars.harmonious=Kann Vorteile durch Ärzte, Chirurgen oder neue Projekte und Unternehmungen bringen.
|
||||||
|
narrative.mars.discordant=
|
||||||
|
narrative.jupiter.base=Bringt Zuwachs und Ausdehnung - Fülle an Glück und Gesundheit und eine insgesamt günstige Zeit.
|
||||||
|
narrative.jupiter.harmonious=
|
||||||
|
narrative.jupiter.discordant=
|
||||||
|
narrative.saturn.base=Bringt Niedergeschlagenheit, Stillstand, Hindernisse und Erschwernisse sowie eine gewisse Einbuße der gewohnten Vorteile.
|
||||||
|
narrative.saturn.harmonious=Gunstbezeigungen aus älteren Verbindungen und vergangenen Beziehungen sind weiterhin möglich.
|
||||||
|
narrative.saturn.discordant=
|
||||||
|
narrative.uranus.base=Bringt Trennungen, Entfremdungen, plötzliche Verschiebungen und heftige Erschütterungen.
|
||||||
|
narrative.uranus.harmonious=Erfolg auf amtlichem oder öffentlichem Weg sowie günstige Veränderungen oder Berufungen sind möglich.
|
||||||
|
narrative.uranus.discordant=
|
||||||
|
narrative.neptune.base=Bringt einen Zustand von Chaos und Verwirrung - eine verworrene, unsichere Lage der Dinge, mit Intrigen, Heimlichkeiten oder unsichtbaren Einflüssen am Werk.
|
||||||
|
narrative.neptune.harmonious=
|
||||||
|
narrative.neptune.discordant=
|
||||||
|
narrative.pluto.base=Bringt tiefgreifenden, oft verborgenen Wandel - das Auftauchen oder die Auflösung von etwas, das seine alte Form überwachsen hat.
|
||||||
|
narrative.pluto.harmonious=
|
||||||
|
narrative.pluto.discordant=
|
||||||
|
|
||||||
|
# --- Guidance (interpreter/src/guidance.c), eigener Text für dieses
|
||||||
|
# Projekt - siehe guidance.c. Bewusst in direkter "Du"-Anrede, anders
|
||||||
|
# als das "er/ihn" aus Waites eigenem Text oben - entspricht dem
|
||||||
|
# eigenen Registerwechsel des englischen Originaltexts für diesen
|
||||||
|
# neuen, unmittelbareren Inhalt. ---
|
||||||
|
guidance.stance.harmonious.upright=Du begegnest dem Tag bereits mit der richtigen Einstellung - vertraue darauf, statt eine günstige Lage in Frage zu stellen.
|
||||||
|
guidance.stance.harmonious.reversed=Der Tag selbst spielt dir in die Hände, auch wenn du dich noch nicht gefestigt fühlst - vertraue der Dynamik mehr als deinen derzeitigen Zweifeln.
|
||||||
|
guidance.stance.discordant.upright=Dein Gespür ist richtig, doch der Tag stellt es auf die Probe - halte deine Position, ohne die Sache zu erzwingen.
|
||||||
|
guidance.stance.discordant.reversed=Sowohl der Tag als auch dein eigener Stand sind gerade unsicher - jetzt ist der Moment, dich zu sammeln, bevor du weitergehst.
|
||||||
|
guidance.stance.neutral.upright=Der heutige Tag verstärkt, was du bereits mitbringst - deine derzeitige Herangehensweise wird verstärkt, also stelle sicher, dass es die richtige ist.
|
||||||
|
guidance.stance.neutral.reversed=Der heutige Tag verstärkt vieles, auch das, was in deiner eigenen Herangehensweise noch ungeklärt ist - kläre es lieber, bevor du handelst.
|
||||||
|
guidance.intro.quiet=%s ist heute nur schwach aktiv, aber immerhin:
|
||||||
|
guidance.intro.notable=%s wirkt heute nur leicht:
|
||||||
|
guidance.intro.significant=%s ist heute deutlich aktiv:
|
||||||
|
guidance.intro.major=%s ist heute der bestimmende Einfluss:
|
||||||
|
guidance.no_transit.upright=Heute gibt es keinen herausragenden Transit - astrologisch ist es ein ruhiger Tag. Das macht ihn vor allem zu einer Frage der Standfestigkeit: vertraue deinem jetzigen Stand und nutze die Ruhe, um voranzukommen.
|
||||||
|
guidance.no_transit.reversed=Heute gibt es keinen herausragenden Transit - astrologisch ist es ein ruhiger Tag. Das ist ein guter Moment, um in aller Ruhe deinen eigenen Stand neu zu finden, da nichts von außen das Tempo vorgibt.
|
||||||
|
guidance.attitude_intro=Deine Haltungskarte ist
|
||||||
|
guidance.outcome_intro=Das wahrscheinliche Ergebnis des Blatts ist
|
||||||
|
|
||||||
|
# Planetennamen für guidance.intro.* oben - eine eigene Tabelle
|
||||||
|
# getrennt von body.*, da Sonne und Mond im Deutschen (wie im
|
||||||
|
# Englischen) einen bestimmten Artikel erhalten, die übrigen acht
|
||||||
|
# Planeten aber nicht.
|
||||||
|
guidance.planet.sun=die Sonne
|
||||||
|
guidance.planet.moon=der Mond
|
||||||
|
guidance.planet.mercury=Merkur
|
||||||
|
guidance.planet.venus=Venus
|
||||||
|
guidance.planet.mars=Mars
|
||||||
|
guidance.planet.jupiter=Jupiter
|
||||||
|
guidance.planet.saturn=Saturn
|
||||||
|
guidance.planet.uranus=Uranus
|
||||||
|
guidance.planet.neptune=Neptun
|
||||||
|
guidance.planet.pluto=Pluto
|
||||||
|
|||||||
@@ -163,3 +163,104 @@ card.judgement.reversed=Weakness, pusillanimity, simplicity; also deliberation,
|
|||||||
card.world.name=The World
|
card.world.name=The World
|
||||||
card.world.upright=Assured success, recompense, voyage, route, emigration, flight, change of place.
|
card.world.upright=Assured success, recompense, voyage, route, emigration, flight, change of place.
|
||||||
card.world.reversed=Inertia, fixity, stagnation, permanence.
|
card.world.reversed=Inertia, fixity, stagnation, permanence.
|
||||||
|
|
||||||
|
# --- Interpreter UI labels (interpreter/src/main.c) ---
|
||||||
|
interp.day_significance=Day significance:
|
||||||
|
interp.day_level.quiet=Quiet
|
||||||
|
interp.day_level.notable=Notable
|
||||||
|
interp.day_level.significant=Significant
|
||||||
|
interp.day_level.major=Major
|
||||||
|
interp.major_framing=(a major transit today - worth a deeper Celtic Cross look)
|
||||||
|
interp.top_transits.one=Top significant transit:
|
||||||
|
interp.top_transits.many=Top %d significant transits:
|
||||||
|
interp.no_notable_transits=No notable transits today.
|
||||||
|
interp.in_sign_house=in %s (house %d)
|
||||||
|
interp.score=score
|
||||||
|
interp.page_title=Deck in a Dash - Interpretation
|
||||||
|
interp.heading=Daily Interpretation
|
||||||
|
interp.heading_day_significance=Day Significance
|
||||||
|
interp.heading_transits=Significant Transits
|
||||||
|
interp.heading_guidance=Guidance
|
||||||
|
|
||||||
|
# --- Transit narratives (interpreter/src/narrative.c), condensed from
|
||||||
|
# Sepharial's Transits and Planetary Periods (1920, public domain),
|
||||||
|
# Chapter VIII "Effects of Transits" - except Moon and Pluto, which are
|
||||||
|
# original (see narrative.c's own comment for why) ---
|
||||||
|
narrative.house_frame=In matters of %s (house %d).
|
||||||
|
narrative.house.1=your sense of self, identity, and outward appearance
|
||||||
|
narrative.house.2=money, possessions, and personal values
|
||||||
|
narrative.house.3=communication, siblings, and everyday learning
|
||||||
|
narrative.house.4=home, family, and your roots
|
||||||
|
narrative.house.5=romance, creativity, and children
|
||||||
|
narrative.house.6=daily work, routine, and health
|
||||||
|
narrative.house.7=partnerships and close relationships
|
||||||
|
narrative.house.8=shared resources, intimacy, and transformation
|
||||||
|
narrative.house.9=travel, higher learning, and beliefs
|
||||||
|
narrative.house.10=career, reputation, and public standing
|
||||||
|
narrative.house.11=friendships, community, and hopes for the future
|
||||||
|
narrative.house.12=solitude, the subconscious, and hidden matters
|
||||||
|
|
||||||
|
narrative.sun.base=
|
||||||
|
narrative.sun.harmonious=Benefits from superiors and advancement in your sphere of life and work - honours, emoluments, and successful new associations.
|
||||||
|
narrative.sun.discordant=Degradation and dishonour, loss of position, and adverse judgement from superiors.
|
||||||
|
narrative.moon.base=Colours the everyday and domestic sphere of life, often coinciding with the opening of new avenues.
|
||||||
|
narrative.moon.harmonious=These changes tend to be advantageous.
|
||||||
|
narrative.moon.discordant=These changes tend to be adverse, with some indisposition or domestic friction.
|
||||||
|
narrative.mercury.base=Affects writings, journeys, commerce, and everyday activities - a neutral messenger whose effect follows the nature of the aspect it makes.
|
||||||
|
narrative.mercury.harmonious=
|
||||||
|
narrative.mercury.discordant=
|
||||||
|
narrative.venus.base=Brings domestic and social affairs to the fore - happiness, comforts, and favours.
|
||||||
|
narrative.venus.harmonious=Success in love affairs and artistic pursuits is likely.
|
||||||
|
narrative.venus.discordant=Grief and disappointment are more likely.
|
||||||
|
narrative.mars.base=A strenuous time of quarrels, contention, strife and anger, with some risk of hurts or injuries depending on the sign it occupies.
|
||||||
|
narrative.mars.harmonious=Can bring benefits from doctors, surgeons, or new projects and enterprises.
|
||||||
|
narrative.mars.discordant=
|
||||||
|
narrative.jupiter.base=Brings increase and expansion - fullness of fortune and health, and a generally fortunate time.
|
||||||
|
narrative.jupiter.harmonious=
|
||||||
|
narrative.jupiter.discordant=
|
||||||
|
narrative.saturn.base=Brings depression, stagnation, hindrances and obstacles, and some deprivation of the usual benefits.
|
||||||
|
narrative.saturn.harmonious=Favours from older connections and past associations are still possible.
|
||||||
|
narrative.saturn.discordant=
|
||||||
|
narrative.uranus.base=Brings separations, estrangements, sudden dislocations and violent upsets.
|
||||||
|
narrative.uranus.harmonious=Success through official or civic channels, and beneficial changes or appointments, are possible.
|
||||||
|
narrative.uranus.discordant=
|
||||||
|
narrative.neptune.base=Brings a state of chaos and confusion - an involved, uncertain condition of affairs, with plots, subtlety, or unseen influences at work.
|
||||||
|
narrative.neptune.harmonious=
|
||||||
|
narrative.neptune.discordant=
|
||||||
|
narrative.pluto.base=Brings deep, often hidden transformation - the surfacing or dismantling of something that has outgrown its old form.
|
||||||
|
narrative.pluto.harmonious=
|
||||||
|
narrative.pluto.discordant=
|
||||||
|
|
||||||
|
# --- Guidance (interpreter/src/guidance.c), original text written for
|
||||||
|
# this project - see guidance.c's own comment for why. Deliberately
|
||||||
|
# direct "you/your" address, unlike the "him/his" register used for
|
||||||
|
# Waite's own text above - matches the English original's own register
|
||||||
|
# shift for this new, more conversational content. ---
|
||||||
|
guidance.stance.harmonious.upright=You're already meeting the day in the right spirit - lean into it rather than second-guessing a favorable position.
|
||||||
|
guidance.stance.harmonious.reversed=The day itself is working in your favor even though you don't feel settled yet - trust the momentum more than your current doubts.
|
||||||
|
guidance.stance.discordant.upright=Your instincts are sound, but the day is testing them - hold your position without forcing the issue.
|
||||||
|
guidance.stance.discordant.reversed=Both the day and your own footing are unsettled right now - this is a moment to steady yourself before pushing forward.
|
||||||
|
guidance.stance.neutral.upright=Today concentrates whatever you already bring to it - your current approach will be amplified, so make sure it's the one you want.
|
||||||
|
guidance.stance.neutral.reversed=Today intensifies things, including whatever is currently unresolved in your own approach - worth sorting out before acting.
|
||||||
|
guidance.intro.quiet=%s is only faintly active today, but for what it's worth:
|
||||||
|
guidance.intro.notable=With a mild touch from %s today:
|
||||||
|
guidance.intro.significant=With %s clearly active today:
|
||||||
|
guidance.intro.major=With %s as today's dominant influence:
|
||||||
|
guidance.no_transit.upright=There's no standout transit today - it's an astrologically quiet day. That leaves things mostly about steadiness: trust your current footing and use the calm to make headway.
|
||||||
|
guidance.no_transit.reversed=There's no standout transit today - it's an astrologically quiet day. That's a good moment to quietly re-settle your own footing, since nothing external is forcing the pace.
|
||||||
|
guidance.attitude_intro=Your Attitude card is
|
||||||
|
guidance.outcome_intro=The spread's likely Outcome is
|
||||||
|
|
||||||
|
# Planet names as used in guidance.intro.* above - a separate table from
|
||||||
|
# body.* because English (and German) give the Sun/Moon a definite
|
||||||
|
# article that the other eight planets don't take.
|
||||||
|
guidance.planet.sun=the Sun
|
||||||
|
guidance.planet.moon=the Moon
|
||||||
|
guidance.planet.mercury=Mercury
|
||||||
|
guidance.planet.venus=Venus
|
||||||
|
guidance.planet.mars=Mars
|
||||||
|
guidance.planet.jupiter=Jupiter
|
||||||
|
guidance.planet.saturn=Saturn
|
||||||
|
guidance.planet.uranus=Uranus
|
||||||
|
guidance.planet.neptune=Neptune
|
||||||
|
guidance.planet.pluto=Pluto
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
# "today": same OS-clock seed/transit-moment and user.properties birth
|
# "today": same OS-clock seed/transit-moment and user.properties birth
|
||||||
# data as run-engine.sh, but reports how significant today's transits are
|
# data as run-engine.sh, but reports how significant today's transits are
|
||||||
# instead of printing the full reading. Usage: ./run-interpreter.sh
|
# instead of printing the full reading. Usage: ./run-interpreter.sh
|
||||||
|
# [text|html|json] [lang]
|
||||||
|
#
|
||||||
|
# [lang] overrides user.properties' lang= for this run only, same as
|
||||||
|
# run-engine.sh - but note it only affects interpreter-cli's own output;
|
||||||
|
# deck-engine is always run without --lang here, since --format json is
|
||||||
|
# deliberately language-independent (see docs/input-output-format.md).
|
||||||
#
|
#
|
||||||
# Requires both dist/deck-engine (built by engine/'s Makefile) and
|
# Requires both dist/deck-engine (built by engine/'s Makefile) and
|
||||||
# dist/interpreter-cli (built by interpreter/'s own Makefile) to already
|
# dist/interpreter-cli (built by interpreter/'s own Makefile) to already
|
||||||
@@ -35,6 +41,7 @@ birth_time=""
|
|||||||
birth_utc_offset=""
|
birth_utc_offset=""
|
||||||
birth_lat=""
|
birth_lat=""
|
||||||
birth_lon=""
|
birth_lon=""
|
||||||
|
lang="en"
|
||||||
|
|
||||||
while IFS='=' read -r key value || [ -n "$key" ]; do
|
while IFS='=' read -r key value || [ -n "$key" ]; do
|
||||||
key="$(printf '%s' "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
key="$(printf '%s' "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
||||||
@@ -48,6 +55,7 @@ while IFS='=' read -r key value || [ -n "$key" ]; do
|
|||||||
birth_utc_offset) birth_utc_offset="$value" ;;
|
birth_utc_offset) birth_utc_offset="$value" ;;
|
||||||
birth_lat) birth_lat="$value" ;;
|
birth_lat) birth_lat="$value" ;;
|
||||||
birth_lon) birth_lon="$value" ;;
|
birth_lon) birth_lon="$value" ;;
|
||||||
|
lang) [ -n "$value" ] && lang="$value" ;;
|
||||||
esac
|
esac
|
||||||
done <"$PROPERTIES_FILE"
|
done <"$PROPERTIES_FILE"
|
||||||
|
|
||||||
@@ -67,10 +75,13 @@ check_set birth_lon "$birth_lon"
|
|||||||
|
|
||||||
# OS-provided inputs: today's date is the tarot seed (stable all day, a
|
# OS-provided inputs: today's date is the tarot seed (stable all day, a
|
||||||
# new spread each day), current UTC time drives today's transits. The
|
# new spread each day), current UTC time drives today's transits. The
|
||||||
# tarot seed doesn't matter for --format json (only "transits" is read
|
# tarot seed still matters for --format json now that "spread" is read
|
||||||
# downstream) but it's kept identical to run-engine.sh's for consistency.
|
# downstream too (guidance.c's Attitude/Outcome framing) - kept identical
|
||||||
|
# to run-engine.sh's for consistency either way.
|
||||||
seed="$(date +%Y-%m-%d)"
|
seed="$(date +%Y-%m-%d)"
|
||||||
transit_date="$(date -u +%Y-%m-%dT%H:%M)"
|
transit_date="$(date -u +%Y-%m-%dT%H:%M)"
|
||||||
|
format="${1:-text}"
|
||||||
|
lang="${2:-$lang}"
|
||||||
|
|
||||||
"$ENGINE_BINARY" \
|
"$ENGINE_BINARY" \
|
||||||
--seed "$seed" \
|
--seed "$seed" \
|
||||||
@@ -81,4 +92,4 @@ transit_date="$(date -u +%Y-%m-%dT%H:%M)"
|
|||||||
--birth-lon "$birth_lon" \
|
--birth-lon "$birth_lon" \
|
||||||
--date "$transit_date" \
|
--date "$transit_date" \
|
||||||
--format json \
|
--format json \
|
||||||
| exec "$INTERPRETER_BINARY"
|
| exec "$INTERPRETER_BINARY" --format "$format" --lang "$lang" --i18n-dir "$SCRIPT_DIR/i18n"
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
#include "guidance.h"
|
||||||
|
|
||||||
|
/* i18n_get() only - see narrative.c's own comment on why this one extra
|
||||||
|
* engine header is needed despite guidance.h's otherwise header-only
|
||||||
|
* engine dependency. */
|
||||||
|
#include "../../engine/src/i18n.h"
|
||||||
|
|
||||||
|
/* Slugs for building "guidance.planet.<slug>"/i18n keys - mirrors
|
||||||
|
* reading_io.c's own k_body_slug, same duplication policy (see
|
||||||
|
* significance.c's comment for why each independently-testable module
|
||||||
|
* keeps its own small table rather than linking astro.c). */
|
||||||
|
static const char *const k_body_slug[NUM_BODIES] = {
|
||||||
|
"sun", "moon", "mercury", "venus", "mars",
|
||||||
|
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
||||||
|
};
|
||||||
|
|
||||||
|
/* English fallback only - "the Sun"/"the Moon" take a definite article
|
||||||
|
* that the other eight planets don't; see engine/i18n/en.lang's own
|
||||||
|
* comment on the guidance.planet.* keys this backs. */
|
||||||
|
static const char *const k_planet_name_fallback[NUM_BODIES] = {
|
||||||
|
"the Sun", "the Moon", "Mercury", "Venus", "Mars",
|
||||||
|
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *planet_display_name(Body body) {
|
||||||
|
char key[32];
|
||||||
|
snprintf(key, sizeof key, "guidance.planet.%s", k_body_slug[body]);
|
||||||
|
return i18n_get(key, k_planet_name_fallback[body]);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CHAR_HARMONIOUS = 0,
|
||||||
|
CHAR_DISCORDANT,
|
||||||
|
CHAR_NEUTRAL,
|
||||||
|
NUM_TRANSIT_CHARACTERS
|
||||||
|
} TransitCharacter;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *slug; /* builds "guidance.stance.<slug>.*" keys */
|
||||||
|
const char *upright; /* Attitude card fell upright (English fallback) */
|
||||||
|
const char *reversed; /* Attitude card fell reversed (English fallback) */
|
||||||
|
} AttitudeStance;
|
||||||
|
|
||||||
|
/* Original guidance text for this project - see guidance.h's doc
|
||||||
|
* comment for why. Fallback (English) text only; German translations
|
||||||
|
* live in engine/i18n/de.lang under the matching "guidance.*" keys.
|
||||||
|
* Indexed by TransitCharacter. */
|
||||||
|
static const AttitudeStance k_stance[NUM_TRANSIT_CHARACTERS] = {
|
||||||
|
[CHAR_HARMONIOUS] = {
|
||||||
|
"harmonious",
|
||||||
|
"You're already meeting the day in the right spirit - lean into "
|
||||||
|
"it rather than second-guessing a favorable position.",
|
||||||
|
"The day itself is working in your favor even though you don't "
|
||||||
|
"feel settled yet - trust the momentum more than your current "
|
||||||
|
"doubts.",
|
||||||
|
},
|
||||||
|
[CHAR_DISCORDANT] = {
|
||||||
|
"discordant",
|
||||||
|
"Your instincts are sound, but the day is testing them - hold "
|
||||||
|
"your position without forcing the issue.",
|
||||||
|
"Both the day and your own footing are unsettled right now - "
|
||||||
|
"this is a moment to steady yourself before pushing forward.",
|
||||||
|
},
|
||||||
|
[CHAR_NEUTRAL] = {
|
||||||
|
"neutral",
|
||||||
|
"Today concentrates whatever you already bring to it - your "
|
||||||
|
"current approach will be amplified, so make sure it's the one "
|
||||||
|
"you want.",
|
||||||
|
"Today intensifies things, including whatever is currently "
|
||||||
|
"unresolved in your own approach - worth sorting out before "
|
||||||
|
"acting.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static TransitCharacter classify_transit(AspectType type) {
|
||||||
|
if (type == ASPECT_TRINE || type == ASPECT_SEXTILE) return CHAR_HARMONIOUS;
|
||||||
|
if (type == ASPECT_SQUARE || type == ASPECT_OPPOSITION) return CHAR_DISCORDANT;
|
||||||
|
return CHAR_NEUTRAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *stance_text(TransitCharacter character, bool reversed) {
|
||||||
|
const AttitudeStance *s = &k_stance[character];
|
||||||
|
char key[48];
|
||||||
|
snprintf(key, sizeof key, "guidance.stance.%s.%s", s->slug, reversed ? "reversed" : "upright");
|
||||||
|
return i18n_get(key, reversed ? s->reversed : s->upright);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* How to introduce the top transit, tailored to how strongly it's
|
||||||
|
* scoring (interp->day_level) - independent of the stance sentence
|
||||||
|
* itself (k_stance, above), which is written to stay true regardless of
|
||||||
|
* intensity. Each entry takes exactly one %s: the transiting planet's
|
||||||
|
* display name, always as the sentence's grammatical subject (matters
|
||||||
|
* for German, where the other three day levels put the planet name in
|
||||||
|
* non-nominative position in a naive word-for-word translation - see
|
||||||
|
* engine/i18n/de.lang's guidance.intro.* entries, which are phrased to
|
||||||
|
* avoid that). Fallback (English) text only. */
|
||||||
|
static const char *const k_intro_slug[DAY_SIGNIFICANCE_COUNT] = {
|
||||||
|
[DAY_QUIET] = "quiet",
|
||||||
|
[DAY_NOTABLE] = "notable",
|
||||||
|
[DAY_SIGNIFICANT] = "significant",
|
||||||
|
[DAY_MAJOR] = "major",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *const k_intro_fallback[DAY_SIGNIFICANCE_COUNT] = {
|
||||||
|
[DAY_QUIET] = "%s is only faintly active today, but for what it's worth:",
|
||||||
|
[DAY_NOTABLE] = "With a mild touch from %s today:",
|
||||||
|
[DAY_SIGNIFICANT] = "With %s clearly active today:",
|
||||||
|
[DAY_MAJOR] = "With %s as today's dominant influence:",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *intro_template(DaySignificance level) {
|
||||||
|
char key[32];
|
||||||
|
snprintf(key, sizeof key, "guidance.intro.%s", k_intro_slug[level]);
|
||||||
|
return i18n_get(key, k_intro_fallback[level]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback for a day with no aspects in orb at all (top_item_count ==
|
||||||
|
* 0, always DAY_QUIET) - there's no transiting planet to name, so the
|
||||||
|
* guidance falls back to the Attitude card's orientation alone. */
|
||||||
|
static const char *no_transit_text(bool reversed) {
|
||||||
|
static const char *const upright =
|
||||||
|
"There's no standout transit today - it's an astrologically quiet "
|
||||||
|
"day. That leaves things mostly about steadiness: trust your "
|
||||||
|
"current footing and use the calm to make headway.";
|
||||||
|
static const char *const reversed_text =
|
||||||
|
"There's no standout transit today - it's an astrologically quiet "
|
||||||
|
"day. That's a good moment to quietly re-settle your own "
|
||||||
|
"footing, since nothing external is forcing the pace.";
|
||||||
|
return i18n_get(reversed ? "guidance.no_transit.reversed" : "guidance.no_transit.upright",
|
||||||
|
reversed ? reversed_text : upright);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *reversed_marker(bool reversed) {
|
||||||
|
static char buf[32];
|
||||||
|
if (!reversed) return "";
|
||||||
|
snprintf(buf, sizeof buf, " %s", i18n_get("ui.reversed", "(Reversed)"));
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
void guidance_print(FILE *out, const char *indent, const DailyInterpretation *interp,
|
||||||
|
const CelticCrossSpread *spread) {
|
||||||
|
const TarotDraw *attitude = &spread->positions[POSITION_ATTITUDE];
|
||||||
|
const TarotDraw *outcome = &spread->positions[POSITION_OUTCOME];
|
||||||
|
|
||||||
|
if (interp->top_item_count == 0) {
|
||||||
|
fprintf(out, "%s%s\n", indent, no_transit_text(attitude->reversed));
|
||||||
|
} else {
|
||||||
|
const Aspect *top = &interp->top_items[0].aspect;
|
||||||
|
TransitCharacter character = classify_transit(top->type);
|
||||||
|
const char *stance = stance_text(character, attitude->reversed);
|
||||||
|
|
||||||
|
char intro[192];
|
||||||
|
snprintf(intro, sizeof(intro), intro_template(interp->day_level),
|
||||||
|
planet_display_name(top->transiting_planet));
|
||||||
|
fprintf(out, "%s%s %s\n", indent, intro, stance);
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(out, "%s%s %s%s: %s\n", indent, i18n_get("guidance.attitude_intro", "Your Attitude card is"),
|
||||||
|
tarot_card_name(attitude->card), reversed_marker(attitude->reversed),
|
||||||
|
tarot_card_meaning(attitude->card, attitude->reversed));
|
||||||
|
fprintf(out, "%s%s %s%s: %s\n", indent, i18n_get("guidance.outcome_intro", "The spread's likely Outcome is"),
|
||||||
|
tarot_card_name(outcome->card), reversed_marker(outcome->reversed),
|
||||||
|
tarot_card_meaning(outcome->card, outcome->reversed));
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#ifndef DECK_GUIDANCE_H
|
||||||
|
#define DECK_GUIDANCE_H
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* Header-only dependency on the engine for struct/enum *definitions*,
|
||||||
|
* same policy as narrative.h/significance.h - see their comments for
|
||||||
|
* why. Unlike those, though, the root Makefile *does* link
|
||||||
|
* engine/src/tarot_data.c's *implementation* (tarot_card_name()/
|
||||||
|
* tarot_card_meaning()) into the interpreter binary - a deliberate,
|
||||||
|
* narrow exception; see the Makefile's own comment on
|
||||||
|
* INTERP_TAROT_TEXT_OBJS for why that doesn't compromise this module's
|
||||||
|
* independent testability. */
|
||||||
|
#include "../../engine/src/tarot.h"
|
||||||
|
#include "significance.h"
|
||||||
|
|
||||||
|
/* Ties the day's single most significant transit
|
||||||
|
* (interp->top_items[0]) to the Celtic Cross spread, and prints a short
|
||||||
|
* paragraph of concrete guidance on how to meet the day - the "combined
|
||||||
|
* storytelling" piece docs/reading.md flags as future work, now built.
|
||||||
|
* Prints on every day, quiet or major, tailored to interp->day_level -
|
||||||
|
* callers can invoke it unconditionally after every
|
||||||
|
* interpret_daily_reading() call.
|
||||||
|
*
|
||||||
|
* The core guidance keys off two things: the character of the top
|
||||||
|
* transit's aspect (harmonious/discordant/neutral, same classification
|
||||||
|
* narrative.c uses) and 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. That crossing (3 x 2 = 6 combinations) is
|
||||||
|
* original text written for this project, not drawn from Waite or
|
||||||
|
* Sepharial, since neither source discusses combining astrology and
|
||||||
|
* tarot; it stays valid regardless of the day's intensity. What *does*
|
||||||
|
* vary with interp->day_level is only the sentence introducing the top
|
||||||
|
* transit (e.g. "With Saturn as today's dominant influence" on a Major
|
||||||
|
* day vs. "Saturn is only faintly active today, but for what it's
|
||||||
|
* worth" on a Quiet one). A day with no aspects in orb at all
|
||||||
|
* (interp->top_item_count == 0, always DAY_QUIET) has no transiting
|
||||||
|
* planet to introduce, so it falls back to a stance keyed on the
|
||||||
|
* Attitude card's orientation alone.
|
||||||
|
*
|
||||||
|
* The paragraph also names the Attitude and Outcome cards and, via
|
||||||
|
* tarot_card_meaning(), states their actual Waite meaning for the
|
||||||
|
* orientation they landed in (e.g. what Wheel of Fortune reversed
|
||||||
|
* means) - not a duplicated table, the real thing, since tarot_data.c
|
||||||
|
* is linked (see above).
|
||||||
|
*
|
||||||
|
* Every piece of this module's own text (stance/intro/no-transit/label
|
||||||
|
* strings) is looked up via i18n_get() under "guidance.*" keys
|
||||||
|
* (engine/i18n's .lang files) before falling back to the English text baked
|
||||||
|
* into guidance.c, so it respects whatever --lang the caller loaded
|
||||||
|
* with i18n_load() (main.c does so before calling this). 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 - see guidance.c's own
|
||||||
|
* comment on k_intro_fallback. */
|
||||||
|
void guidance_print(FILE *out, const char *indent, const DailyInterpretation *interp,
|
||||||
|
const CelticCrossSpread *spread);
|
||||||
|
|
||||||
|
#endif
|
||||||
+399
-51
@@ -1,25 +1,63 @@
|
|||||||
|
#define _POSIX_C_SOURCE 200809L /* for open_memstream */
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "../../engine/src/i18n.h"
|
||||||
|
#include "guidance.h"
|
||||||
#include "narrative.h"
|
#include "narrative.h"
|
||||||
#include "reading_io.h"
|
#include "reading_io.h"
|
||||||
#include "significance.h"
|
#include "significance.h"
|
||||||
|
|
||||||
|
typedef enum { FORMAT_TEXT, FORMAT_HTML, FORMAT_JSON } OutputFormat;
|
||||||
|
|
||||||
static void print_usage(const char *prog) {
|
static void print_usage(const char *prog) {
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"Usage: %s [reading.json]\n\n"
|
"Usage: %s [--format text|html|json] [--lang <code>] [--i18n-dir <path>]\n"
|
||||||
|
" [reading.json]\n\n"
|
||||||
"Reads a DailyReading in deck-engine's --format json shape - from the\n"
|
"Reads a DailyReading in deck-engine's --format json shape - from the\n"
|
||||||
"given file, or from stdin if no file is given (or it is \"-\") - and\n"
|
"given file, or from stdin if no file is given (or it is \"-\") - and\n"
|
||||||
"prints a significance report: the day's overall level and the top\n"
|
"prints a full report: the day's overall significance level, the\n"
|
||||||
"aspects driving it, each with a short narrative note.\n\n"
|
"top aspects driving it (each with a short narrative note), a full\n"
|
||||||
|
"readout of the Celtic Cross spread with every card's meaning, and\n"
|
||||||
|
"finally guidance tying the day's top transit to the spread's\n"
|
||||||
|
"Attitude/Outcome cards.\n\n"
|
||||||
|
" --format Output format, defaults to text. html links card art via\n"
|
||||||
|
" img/<file>, same convention as deck-engine's own html\n"
|
||||||
|
" output - save/run this next to the img/ directory\n"
|
||||||
|
" `make images` copies into dist/. json embeds the same\n"
|
||||||
|
" narrative/guidance prose as text (in whatever --lang was\n"
|
||||||
|
" loaded) alongside stable, language-independent slugs.\n"
|
||||||
|
" --lang Reading language code, defaults to en. Matches a file\n"
|
||||||
|
" named <code>.lang in --i18n-dir (see engine/i18n/) -\n"
|
||||||
|
" note this is this binary's OWN output language, not\n"
|
||||||
|
" related to whatever --lang deck-engine was run with to\n"
|
||||||
|
" produce its --format json input (which is always\n"
|
||||||
|
" language-independent - see docs/input-output-format.md).\n"
|
||||||
|
" --i18n-dir Directory to look up <lang>.lang in. Defaults to the\n"
|
||||||
|
" i18n/ directory next to this binary (`make i18n`\n"
|
||||||
|
" copies engine/i18n/ there as dist/i18n/).\n\n"
|
||||||
"Typical use:\n"
|
"Typical use:\n"
|
||||||
" dist/deck-engine ... --format json | %s\n"
|
" dist/deck-engine ... --format json | %s\n"
|
||||||
" dist/deck-engine ... --format json > reading.json && %s reading.json\n",
|
" dist/deck-engine ... --format json > reading.json && %s reading.json\n",
|
||||||
prog, prog, prog);
|
prog, prog, prog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Same convention as engine/src/main.c's own default_i18n_path() -
|
||||||
|
* duplicated rather than shared, since the two binaries are never
|
||||||
|
* linked together (see CLAUDE.md's "Interpretation" section). */
|
||||||
|
static void default_i18n_path(const char *argv0, const char *lang, char *out, size_t out_size) {
|
||||||
|
const char *slash = strrchr(argv0, '/');
|
||||||
|
if (slash) {
|
||||||
|
int dir_len = (int)(slash - argv0);
|
||||||
|
snprintf(out, out_size, "%.*s/i18n/%s.lang", dir_len, argv0, lang);
|
||||||
|
} else {
|
||||||
|
snprintf(out, out_size, "i18n/%s.lang", lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static char *read_all(FILE *f, size_t *out_length) {
|
static char *read_all(FILE *f, size_t *out_length) {
|
||||||
size_t cap = 4096;
|
size_t cap = 4096;
|
||||||
size_t len = 0;
|
size_t len = 0;
|
||||||
@@ -39,63 +77,391 @@ static char *read_all(FILE *f, size_t *out_length) {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Display names for the report - deliberately a small local table rather
|
static const char *ui(const char *key, const char *fallback) { return i18n_get(key, fallback); }
|
||||||
* than linking engine/src/astro.c's astro_body_name()/astro_aspect_name()
|
|
||||||
* (which would pull in the vendored Astronomy Engine just for cosmetic
|
/* Slugs for i18n key building (body.<slug>/sign.<slug>/aspect.<slug>,
|
||||||
* text). Same policy as reading_io.c's slug tables. */
|
* mirroring astro.c's own tables) and reused as-is for --format json's
|
||||||
|
* language-independent fields - deliberately a small local table rather
|
||||||
|
* than linking astro.c (would pull in the vendored Astronomy Engine just
|
||||||
|
* for cosmetic text/slugs). Same policy as reading_io.c's own tables. */
|
||||||
|
static const char *const k_body_slug[NUM_BODIES] = {
|
||||||
|
"sun", "moon", "mercury", "venus", "mars",
|
||||||
|
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
||||||
|
};
|
||||||
|
static const char *const k_sign_slug[12] = {
|
||||||
|
"aries", "taurus", "gemini", "cancer", "leo", "virgo",
|
||||||
|
"libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
|
||||||
|
};
|
||||||
|
static const char *const k_aspect_slug[5] = {
|
||||||
|
"conjunction", "sextile", "square", "trine", "opposition",
|
||||||
|
};
|
||||||
|
static const char *const k_day_level_slug[DAY_SIGNIFICANCE_COUNT] = {
|
||||||
|
"quiet", "notable", "significant", "major",
|
||||||
|
};
|
||||||
|
|
||||||
static const char *body_display_name(Body body) {
|
static const char *body_display_name(Body body) {
|
||||||
static const char *const names[NUM_BODIES] = {
|
static const char *const fallback[NUM_BODIES] = {
|
||||||
"Sun", "Moon", "Mercury", "Venus", "Mars",
|
"Sun", "Moon", "Mercury", "Venus", "Mars",
|
||||||
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto",
|
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto",
|
||||||
};
|
};
|
||||||
return names[body];
|
char key[32];
|
||||||
|
snprintf(key, sizeof key, "body.%s", k_body_slug[body]);
|
||||||
|
return ui(key, fallback[body]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char *aspect_display_name(AspectType type) {
|
static const char *aspect_display_name(AspectType type) {
|
||||||
static const char *const names[5] = {
|
static const char *const fallback[5] = {
|
||||||
"Conjunction", "Sextile", "Square", "Trine", "Opposition",
|
"Conjunction", "Sextile", "Square", "Trine", "Opposition",
|
||||||
};
|
};
|
||||||
return names[type];
|
char key[32];
|
||||||
|
snprintf(key, sizeof key, "aspect.%s", k_aspect_slug[type]);
|
||||||
|
return ui(key, fallback[type]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char *sign_display_name(ZodiacSign sign) {
|
static const char *sign_display_name(ZodiacSign sign) {
|
||||||
static const char *const names[12] = {
|
static const char *const fallback[12] = {
|
||||||
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||||||
};
|
};
|
||||||
return names[sign];
|
char key[32];
|
||||||
|
snprintf(key, sizeof key, "sign.%s", k_sign_slug[sign]);
|
||||||
|
return ui(key, fallback[sign]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *day_level_name(DaySignificance level) {
|
||||||
|
switch (level) {
|
||||||
|
case DAY_QUIET: return ui("interp.day_level.quiet", "Quiet");
|
||||||
|
case DAY_NOTABLE: return ui("interp.day_level.notable", "Notable");
|
||||||
|
case DAY_SIGNIFICANT: return ui("interp.day_level.significant", "Significant");
|
||||||
|
case DAY_MAJOR: return ui("interp.day_level.major", "Major");
|
||||||
|
default: return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *reversed_marker(bool reversed) {
|
||||||
|
static char buf[32];
|
||||||
|
if (!reversed) return "";
|
||||||
|
snprintf(buf, sizeof buf, " %s", ui("ui.reversed", "(Reversed)"));
|
||||||
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* pos->house is 0 when reading_load_json had no sign/house data for this
|
/* pos->house is 0 when reading_load_json had no sign/house data for this
|
||||||
* body (see parse_bodies() in reading_io.c) - a valid whole-sign house
|
* body (see parse_bodies() in reading_io.c) - a valid whole-sign house
|
||||||
* is always 1-12, so this is a safe "unknown, say nothing" sentinel. */
|
* is always 1-12, so this is a safe "unknown, say nothing" sentinel. */
|
||||||
static void print_body_in_sign(const PlanetPosition *pos) {
|
static void print_body_in_sign_text(FILE *out, const PlanetPosition *pos) {
|
||||||
if (pos->house < 1 || pos->house > 12) return;
|
if (pos->house < 1 || pos->house > 12) return;
|
||||||
printf(" in %s (house %d)", sign_display_name(pos->sign), pos->house);
|
char buf[64];
|
||||||
|
snprintf(buf, sizeof buf, ui("interp.in_sign_house", "in %s (house %d)"),
|
||||||
|
sign_display_name(pos->sign), pos->house);
|
||||||
|
fprintf(out, " %s", buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char *day_level_name(DaySignificance level) {
|
/* Captures narrative_print()/guidance_print()'s output (they only know
|
||||||
switch (level) {
|
* how to write to a FILE*) into a heap string, for embedding into JSON -
|
||||||
case DAY_QUIET: return "Quiet";
|
* avoids changing either module's public API just for this one caller.
|
||||||
case DAY_NOTABLE: return "Notable";
|
* Strips a single trailing newline; caller must free() the result. */
|
||||||
case DAY_SIGNIFICANT: return "Significant";
|
static char *capture_narrative(const Aspect *aspect, int house) {
|
||||||
case DAY_MAJOR: return "Major";
|
char *buf = NULL;
|
||||||
|
size_t size = 0;
|
||||||
|
FILE *mem = open_memstream(&buf, &size);
|
||||||
|
narrative_print(mem, "", aspect, house);
|
||||||
|
fclose(mem);
|
||||||
|
if (size > 0 && buf[size - 1] == '\n') buf[size - 1] = '\0';
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *capture_guidance(const DailyInterpretation *interp, const CelticCrossSpread *spread) {
|
||||||
|
char *buf = NULL;
|
||||||
|
size_t size = 0;
|
||||||
|
FILE *mem = open_memstream(&buf, &size);
|
||||||
|
guidance_print(mem, "", interp, spread);
|
||||||
|
fclose(mem);
|
||||||
|
if (size > 0 && buf[size - 1] == '\n') buf[size - 1] = '\0';
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_top_transits_header(FILE *out, int count) {
|
||||||
|
if (count == 1) {
|
||||||
|
fprintf(out, "%s\n", ui("interp.top_transits.one", "Top significant transit:"));
|
||||||
|
} else {
|
||||||
|
char buf[64];
|
||||||
|
snprintf(buf, sizeof buf, ui("interp.top_transits.many", "Top %d significant transits:"), count);
|
||||||
|
fprintf(out, "%s\n", buf);
|
||||||
}
|
}
|
||||||
return "Unknown";
|
}
|
||||||
|
|
||||||
|
/* ===== --format text ===== */
|
||||||
|
|
||||||
|
static void print_celtic_cross_text(FILE *out, const CelticCrossSpread *spread) {
|
||||||
|
fprintf(out, "%s:\n", ui("ui.celtic_cross", "Celtic Cross"));
|
||||||
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
|
const TarotDraw *draw = &spread->positions[i];
|
||||||
|
fprintf(out, " %d. %s: %s%s\n", i + 1, tarot_position_name((CelticCrossPosition)i),
|
||||||
|
tarot_card_name(draw->card), reversed_marker(draw->reversed));
|
||||||
|
fprintf(out, " %s\n", tarot_position_description((CelticCrossPosition)i));
|
||||||
|
fprintf(out, " %s\n", tarot_card_meaning(draw->card, draw->reversed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void interp_print_text(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
|
||||||
|
fprintf(out, "%s %s (%d/%d)\n", ui("interp.day_significance", "Day significance:"),
|
||||||
|
day_level_name(interp->day_level), interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
|
||||||
|
if (interpretation_deserves_framing(interp)) {
|
||||||
|
fprintf(out, "%s\n", ui("interp.major_framing",
|
||||||
|
"(a major transit today - worth a deeper Celtic Cross look)"));
|
||||||
|
}
|
||||||
|
fprintf(out, "\n");
|
||||||
|
|
||||||
|
if (interp->top_item_count == 0) {
|
||||||
|
fprintf(out, "%s\n", ui("interp.no_notable_transits", "No notable transits today."));
|
||||||
|
} else {
|
||||||
|
print_top_transits_header(out, interp->top_item_count);
|
||||||
|
for (int i = 0; i < interp->top_item_count; i++) {
|
||||||
|
const SignificantItem *item = &interp->top_items[i];
|
||||||
|
const Aspect *a = &item->aspect;
|
||||||
|
|
||||||
|
fprintf(out, " %d. %s %s", i + 1, ui("ui.transiting", "Transiting"),
|
||||||
|
body_display_name(a->transiting_planet));
|
||||||
|
print_body_in_sign_text(out, &reading->transits.bodies[a->transiting_planet]);
|
||||||
|
fprintf(out, " %s %s %s", aspect_display_name(a->type), ui("ui.natal", "natal"),
|
||||||
|
body_display_name(a->natal_planet));
|
||||||
|
print_body_in_sign_text(out, &reading->natal.bodies[a->natal_planet]);
|
||||||
|
fprintf(out, " (%s %.1f\xc2\xb0, %s %.2f)\n", ui("ui.orb", "orb"), a->orb,
|
||||||
|
ui("interp.score", "score"), item->score);
|
||||||
|
narrative_print(out, " ", a, reading->transits.bodies[a->transiting_planet].house);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fprintf(out, "\n");
|
||||||
|
|
||||||
|
print_celtic_cross_text(out, &reading->spread);
|
||||||
|
fprintf(out, "\n");
|
||||||
|
|
||||||
|
guidance_print(out, "", interp, &reading->spread);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== --format html ===== */
|
||||||
|
|
||||||
|
static void interp_print_html(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
|
||||||
|
fprintf(out,
|
||||||
|
"<!doctype html>\n<html><head><meta charset=\"utf-8\">\n"
|
||||||
|
"<title>%s</title>\n"
|
||||||
|
"<style>\n"
|
||||||
|
"body { font-family: sans-serif; max-width: 900px; margin: 2em auto; }\n"
|
||||||
|
"table { border-collapse: collapse; margin-bottom: 1.5em; }\n"
|
||||||
|
"td { padding: 2px 10px 2px 0; }\n"
|
||||||
|
"h1, h2 { border-bottom: 1px solid #ccc; }\n"
|
||||||
|
".spread { display: flex; flex-wrap: wrap; gap: 1.5em; }\n"
|
||||||
|
".card { width: 160px; }\n"
|
||||||
|
".card img { width: 140px; display: block; }\n"
|
||||||
|
".card img.reversed { transform: rotate(180deg); }\n"
|
||||||
|
".card .position { font-weight: bold; }\n"
|
||||||
|
".card .name { font-style: italic; }\n"
|
||||||
|
".guidance { background: #f6f6f6; padding: 1em; border-radius: 6px; }\n"
|
||||||
|
"</style></head><body>\n",
|
||||||
|
ui("interp.page_title", "Deck in a Dash - Interpretation"));
|
||||||
|
|
||||||
|
fprintf(out, "<h1>%s</h1>\n", ui("interp.heading", "Daily Interpretation"));
|
||||||
|
|
||||||
|
fprintf(out, "<h2>%s</h2>\n<p>%s %s (%d/%d)",
|
||||||
|
ui("interp.heading_day_significance", "Day Significance"),
|
||||||
|
ui("interp.day_significance", "Day significance:"), day_level_name(interp->day_level),
|
||||||
|
interp->day_level + 1, DAY_SIGNIFICANCE_COUNT);
|
||||||
|
if (interpretation_deserves_framing(interp)) {
|
||||||
|
fprintf(out, "<br>%s", ui("interp.major_framing",
|
||||||
|
"(a major transit today - worth a deeper Celtic Cross look)"));
|
||||||
|
}
|
||||||
|
fprintf(out, "</p>\n");
|
||||||
|
|
||||||
|
fprintf(out, "<h2>%s</h2>\n", ui("interp.heading_transits", "Significant Transits"));
|
||||||
|
if (interp->top_item_count == 0) {
|
||||||
|
fprintf(out, "<p>%s</p>\n", ui("interp.no_notable_transits", "No notable transits today."));
|
||||||
|
} else {
|
||||||
|
fprintf(out, "<table>\n");
|
||||||
|
for (int i = 0; i < interp->top_item_count; i++) {
|
||||||
|
const SignificantItem *item = &interp->top_items[i];
|
||||||
|
const Aspect *a = &item->aspect;
|
||||||
|
char *text = capture_narrative(a, reading->transits.bodies[a->transiting_planet].house);
|
||||||
|
|
||||||
|
fprintf(out, "<tr><td>%d.</td><td>%s %s %s %s %s</td>"
|
||||||
|
"<td>%s %.1f°, %s %.2f</td></tr>\n",
|
||||||
|
i + 1, ui("ui.transiting", "Transiting"), body_display_name(a->transiting_planet),
|
||||||
|
aspect_display_name(a->type), ui("ui.natal", "natal"),
|
||||||
|
body_display_name(a->natal_planet), ui("ui.orb", "orb"), a->orb,
|
||||||
|
ui("interp.score", "score"), item->score);
|
||||||
|
if (text[0] != '\0') fprintf(out, "<tr><td></td><td colspan=\"2\">%s</td></tr>\n", text);
|
||||||
|
free(text);
|
||||||
|
}
|
||||||
|
fprintf(out, "</table>\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(out, "<h2>%s</h2>\n<div class=\"spread\">\n", ui("ui.celtic_cross", "Celtic Cross"));
|
||||||
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
|
const TarotDraw *draw = &reading->spread.positions[i];
|
||||||
|
fprintf(out,
|
||||||
|
"<div class=\"card\">\n"
|
||||||
|
" <div class=\"position\">%s</div>\n"
|
||||||
|
" <img class=\"%s\" src=\"img/%s\" alt=\"%s\">\n"
|
||||||
|
" <div class=\"name\">%s%s</div>\n"
|
||||||
|
" <div class=\"desc\">%s</div>\n"
|
||||||
|
" <div class=\"meaning\">%s</div>\n"
|
||||||
|
"</div>\n",
|
||||||
|
tarot_position_name((CelticCrossPosition)i), draw->reversed ? "reversed" : "",
|
||||||
|
tarot_card_image_file(draw->card), tarot_card_name(draw->card), tarot_card_name(draw->card),
|
||||||
|
reversed_marker(draw->reversed), tarot_position_description((CelticCrossPosition)i),
|
||||||
|
tarot_card_meaning(draw->card, draw->reversed));
|
||||||
|
}
|
||||||
|
fprintf(out, "</div>\n");
|
||||||
|
|
||||||
|
char *guidance = capture_guidance(interp, &reading->spread);
|
||||||
|
fprintf(out, "<h2>%s</h2>\n<div class=\"guidance\">\n", ui("interp.heading_guidance", "Guidance"));
|
||||||
|
const char *start = guidance;
|
||||||
|
for (const char *p = guidance; ; p++) {
|
||||||
|
if (*p == '\n' || *p == '\0') {
|
||||||
|
if (p > start) fprintf(out, "<p>%.*s</p>\n", (int)(p - start), start);
|
||||||
|
if (*p == '\0') break;
|
||||||
|
start = p + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(guidance);
|
||||||
|
fprintf(out, "</div>\n");
|
||||||
|
|
||||||
|
fprintf(out, "</body></html>\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== --format json ===== */
|
||||||
|
|
||||||
|
static void json_string(FILE *out, const char *s) {
|
||||||
|
fputc('"', out);
|
||||||
|
for (; *s; s++) {
|
||||||
|
unsigned char c = (unsigned char)*s;
|
||||||
|
switch (c) {
|
||||||
|
case '"': fputs("\\\"", out); break;
|
||||||
|
case '\\': fputs("\\\\", out); break;
|
||||||
|
case '\n': fputs("\\n", out); break;
|
||||||
|
case '\r': fputs("\\r", out); break;
|
||||||
|
case '\t': fputs("\\t", out); break;
|
||||||
|
default:
|
||||||
|
if (c < 0x20) fprintf(out, "\\u%04x", c);
|
||||||
|
else fputc((char)c, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fputc('"', out);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void interp_print_json(FILE *out, const DailyReading *reading, const DailyInterpretation *interp) {
|
||||||
|
fprintf(out, "{\n");
|
||||||
|
|
||||||
|
fprintf(out, " \"day_significance\": {\n");
|
||||||
|
fprintf(out, " \"level\": \"%s\",\n", k_day_level_slug[interp->day_level]);
|
||||||
|
fprintf(out, " \"rank\": %d,\n", interp->day_level + 1);
|
||||||
|
fprintf(out, " \"count\": %d,\n", DAY_SIGNIFICANCE_COUNT);
|
||||||
|
fprintf(out, " \"deserves_framing\": %s\n", interpretation_deserves_framing(interp) ? "true" : "false");
|
||||||
|
fprintf(out, " },\n");
|
||||||
|
|
||||||
|
fprintf(out, " \"significant_transits\": [\n");
|
||||||
|
for (int i = 0; i < interp->top_item_count; i++) {
|
||||||
|
const SignificantItem *item = &interp->top_items[i];
|
||||||
|
const Aspect *a = &item->aspect;
|
||||||
|
const PlanetPosition *tp = &reading->transits.bodies[a->transiting_planet];
|
||||||
|
const PlanetPosition *np = &reading->natal.bodies[a->natal_planet];
|
||||||
|
char *text = capture_narrative(a, tp->house);
|
||||||
|
|
||||||
|
fprintf(out, " {\n");
|
||||||
|
fprintf(out, " \"transiting_planet\": \"%s\",\n", k_body_slug[a->transiting_planet]);
|
||||||
|
fprintf(out, " \"transiting_sign\": \"%s\",\n", k_sign_slug[tp->sign]);
|
||||||
|
fprintf(out, " \"transiting_house\": %d,\n", tp->house);
|
||||||
|
fprintf(out, " \"natal_planet\": \"%s\",\n", k_body_slug[a->natal_planet]);
|
||||||
|
fprintf(out, " \"natal_sign\": \"%s\",\n", k_sign_slug[np->sign]);
|
||||||
|
fprintf(out, " \"natal_house\": %d,\n", np->house);
|
||||||
|
fprintf(out, " \"aspect\": \"%s\",\n", k_aspect_slug[a->type]);
|
||||||
|
fprintf(out, " \"orb\": %.4f,\n", a->orb);
|
||||||
|
fprintf(out, " \"score\": %.4f,\n", item->score);
|
||||||
|
fprintf(out, " \"narrative\": ");
|
||||||
|
json_string(out, text);
|
||||||
|
fprintf(out, "\n }%s\n", i + 1 < interp->top_item_count ? "," : "");
|
||||||
|
free(text);
|
||||||
|
}
|
||||||
|
fprintf(out, " ],\n");
|
||||||
|
|
||||||
|
fprintf(out, " \"celtic_cross\": [\n");
|
||||||
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
|
const TarotDraw *draw = &reading->spread.positions[i];
|
||||||
|
fprintf(out, " {\"position\": \"%s\", \"card\": \"%s\", \"reversed\": %s, \"card_name\": ",
|
||||||
|
tarot_position_slug((CelticCrossPosition)i), tarot_card_slug(draw->card),
|
||||||
|
draw->reversed ? "true" : "false");
|
||||||
|
json_string(out, tarot_card_name(draw->card));
|
||||||
|
fprintf(out, ", \"meaning\": ");
|
||||||
|
json_string(out, tarot_card_meaning(draw->card, draw->reversed));
|
||||||
|
fprintf(out, "}%s\n", i + 1 < TAROT_SPREAD_SIZE ? "," : "");
|
||||||
|
}
|
||||||
|
fprintf(out, " ],\n");
|
||||||
|
|
||||||
|
char *guidance = capture_guidance(interp, &reading->spread);
|
||||||
|
fprintf(out, " \"guidance\": ");
|
||||||
|
json_string(out, guidance);
|
||||||
|
fprintf(out, "\n");
|
||||||
|
free(guidance);
|
||||||
|
|
||||||
|
fprintf(out, "}\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
if (argc > 2 || (argc == 2 && (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0))) {
|
const char *format_str = "text";
|
||||||
|
const char *lang = "en", *i18n_dir_arg = NULL;
|
||||||
|
const char *input_path = NULL;
|
||||||
|
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
const char *arg = argv[i];
|
||||||
|
if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") == 0) {
|
||||||
print_usage(argv[0]);
|
print_usage(argv[0]);
|
||||||
return argc > 2 ? 1 : 0;
|
return 0;
|
||||||
|
} else if (strcmp(arg, "--format") == 0) {
|
||||||
|
if (++i >= argc) { print_usage(argv[0]); return 1; }
|
||||||
|
format_str = argv[i];
|
||||||
|
} else if (strcmp(arg, "--lang") == 0) {
|
||||||
|
if (++i >= argc) { print_usage(argv[0]); return 1; }
|
||||||
|
lang = argv[i];
|
||||||
|
} else if (strcmp(arg, "--i18n-dir") == 0) {
|
||||||
|
if (++i >= argc) { print_usage(argv[0]); return 1; }
|
||||||
|
i18n_dir_arg = argv[i];
|
||||||
|
} else if (arg[0] == '-' && strcmp(arg, "-") != 0) {
|
||||||
|
fprintf(stderr, "Unknown argument: %s\n", arg);
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
} else if (input_path == NULL) {
|
||||||
|
input_path = arg;
|
||||||
|
} else {
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutputFormat format;
|
||||||
|
if (strcmp(format_str, "text") == 0) format = FORMAT_TEXT;
|
||||||
|
else if (strcmp(format_str, "html") == 0) format = FORMAT_HTML;
|
||||||
|
else if (strcmp(format_str, "json") == 0) format = FORMAT_JSON;
|
||||||
|
else {
|
||||||
|
fprintf(stderr, "Invalid --format, expected text, html, or json\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char i18n_path[512];
|
||||||
|
if (i18n_dir_arg) {
|
||||||
|
snprintf(i18n_path, sizeof i18n_path, "%s/%s.lang", i18n_dir_arg, lang);
|
||||||
|
} else {
|
||||||
|
default_i18n_path(argv[0], lang, i18n_path, sizeof i18n_path);
|
||||||
|
}
|
||||||
|
if (!i18n_load(i18n_path) && strcmp(lang, "en") != 0) {
|
||||||
|
fprintf(stderr, "warning: could not load translations for '%s' from %s - "
|
||||||
|
"falling back to built-in English\n", lang, i18n_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
FILE *in = stdin;
|
FILE *in = stdin;
|
||||||
bool opened_file = false;
|
bool opened_file = false;
|
||||||
if (argc == 2 && strcmp(argv[1], "-") != 0) {
|
if (input_path && strcmp(input_path, "-") != 0) {
|
||||||
in = fopen(argv[1], "r");
|
in = fopen(input_path, "r");
|
||||||
if (!in) {
|
if (!in) {
|
||||||
fprintf(stderr, "error: could not open %s\n", argv[1]);
|
fprintf(stderr, "error: could not open %s\n", input_path);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
opened_file = true;
|
opened_file = true;
|
||||||
@@ -116,30 +482,12 @@ int main(int argc, char **argv) {
|
|||||||
DailyInterpretation interp;
|
DailyInterpretation interp;
|
||||||
interpret_daily_reading(&reading, &interp);
|
interpret_daily_reading(&reading, &interp);
|
||||||
|
|
||||||
printf("Day significance: %s (%d/%d)\n", day_level_name(interp.day_level),
|
if (format == FORMAT_HTML) {
|
||||||
interp.day_level + 1, DAY_SIGNIFICANCE_COUNT);
|
interp_print_html(stdout, &reading, &interp);
|
||||||
if (interpretation_deserves_framing(&interp)) {
|
} else if (format == FORMAT_JSON) {
|
||||||
printf("(a major transit today - worth a deeper Celtic Cross look)\n");
|
interp_print_json(stdout, &reading, &interp);
|
||||||
}
|
} else {
|
||||||
printf("\n");
|
interp_print_text(stdout, &reading, &interp);
|
||||||
|
|
||||||
if (interp.top_item_count == 0) {
|
|
||||||
printf("No notable transits today.\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("Top %d significant transit%s:\n", interp.top_item_count,
|
|
||||||
interp.top_item_count == 1 ? "" : "s");
|
|
||||||
for (int i = 0; i < interp.top_item_count; i++) {
|
|
||||||
const SignificantItem *item = &interp.top_items[i];
|
|
||||||
const Aspect *a = &item->aspect;
|
|
||||||
|
|
||||||
printf(" %d. Transiting %s", i + 1, body_display_name(a->transiting_planet));
|
|
||||||
print_body_in_sign(&reading.transits.bodies[a->transiting_planet]);
|
|
||||||
printf(" %s natal %s", aspect_display_name(a->type), body_display_name(a->natal_planet));
|
|
||||||
print_body_in_sign(&reading.natal.bodies[a->natal_planet]);
|
|
||||||
printf(" (orb %.1f\xc2\xb0, score %.2f)\n", a->orb, item->score);
|
|
||||||
narrative_print(stdout, " ", a, reading.transits.bodies[a->transiting_planet].house);
|
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-40
@@ -1,86 +1,103 @@
|
|||||||
#include "narrative.h"
|
#include "narrative.h"
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* i18n_get() only - narrative.h already pulls in astro.h; this is the
|
||||||
|
* one additional engine header needed to route k_narratives/
|
||||||
|
* k_house_area through the same translation catalog tarot_data.c uses
|
||||||
|
* (see the Makefile's INTERP_TAROT_TEXT_OBJS comment for why i18n.c is
|
||||||
|
* linked into this binary). */
|
||||||
|
#include "../../engine/src/i18n.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
const char *slug; /* builds "narrative.<slug>.*" i18n keys */
|
||||||
const char *base; /* always relevant, may be "" (see the Sun) */
|
const char *base; /* always relevant, may be "" (see the Sun) */
|
||||||
const char *harmonious; /* shown only under a trine/sextile; may be "" */
|
const char *harmonious; /* shown only under a trine/sextile; may be "" */
|
||||||
const char *discordant; /* shown only under a square/opposition; may be "" */
|
const char *discordant; /* shown only under a square/opposition; may be "" */
|
||||||
} TransitNarrative;
|
} TransitNarrative;
|
||||||
|
|
||||||
/* See narrative.h's doc comment for sourcing (Sepharial, "Transits and
|
/* Fallback (English) text, also the source of truth when no translation
|
||||||
* Planetary Periods", 1920, Chapter VIII) and the Moon/Pluto exception. */
|
* catalog is loaded - see narrative.h's doc comment for sourcing
|
||||||
|
* (Sepharial, "Transits and Planetary Periods", 1920, Chapter VIII) and
|
||||||
|
* the Moon/Pluto exception. German translations live in
|
||||||
|
* engine/i18n/de.lang under the same "narrative.<slug>.*" keys. */
|
||||||
static const TransitNarrative k_narratives[NUM_BODIES] = {
|
static const TransitNarrative k_narratives[NUM_BODIES] = {
|
||||||
[PLANET_SUN] = {
|
[PLANET_SUN] = {
|
||||||
.base = "",
|
"sun", "",
|
||||||
.harmonious = "Benefits from superiors and advancement in your sphere of "
|
"Benefits from superiors and advancement in your sphere of "
|
||||||
"life and work - honours, emoluments, and successful new "
|
"life and work - honours, emoluments, and successful new "
|
||||||
"associations.",
|
"associations.",
|
||||||
.discordant = "Degradation and dishonour, loss of position, and adverse "
|
"Degradation and dishonour, loss of position, and adverse "
|
||||||
"judgement from superiors.",
|
"judgement from superiors.",
|
||||||
},
|
},
|
||||||
[PLANET_MOON] = {
|
[PLANET_MOON] = {
|
||||||
/* Original text, not from Sepharial - see narrative.h. */
|
/* Original text, not from Sepharial - see narrative.h. */
|
||||||
.base = "Colours the everyday and domestic sphere of life, often "
|
"moon",
|
||||||
|
"Colours the everyday and domestic sphere of life, often "
|
||||||
"coinciding with the opening of new avenues.",
|
"coinciding with the opening of new avenues.",
|
||||||
.harmonious = "These changes tend to be advantageous.",
|
"These changes tend to be advantageous.",
|
||||||
.discordant = "These changes tend to be adverse, with some indisposition "
|
"These changes tend to be adverse, with some indisposition "
|
||||||
"or domestic friction.",
|
"or domestic friction.",
|
||||||
},
|
},
|
||||||
[PLANET_MERCURY] = {
|
[PLANET_MERCURY] = {
|
||||||
.base = "Affects writings, journeys, commerce, and everyday activities - "
|
"mercury",
|
||||||
|
"Affects writings, journeys, commerce, and everyday activities - "
|
||||||
"a neutral messenger whose effect follows the nature of the "
|
"a neutral messenger whose effect follows the nature of the "
|
||||||
"aspect it makes.",
|
"aspect it makes.",
|
||||||
.harmonious = "",
|
"", "",
|
||||||
.discordant = "",
|
|
||||||
},
|
},
|
||||||
[PLANET_VENUS] = {
|
[PLANET_VENUS] = {
|
||||||
.base = "Brings domestic and social affairs to the fore - happiness, "
|
"venus",
|
||||||
|
"Brings domestic and social affairs to the fore - happiness, "
|
||||||
"comforts, and favours.",
|
"comforts, and favours.",
|
||||||
.harmonious = "Success in love affairs and artistic pursuits is likely.",
|
"Success in love affairs and artistic pursuits is likely.",
|
||||||
.discordant = "Grief and disappointment are more likely.",
|
"Grief and disappointment are more likely.",
|
||||||
},
|
},
|
||||||
[PLANET_MARS] = {
|
[PLANET_MARS] = {
|
||||||
.base = "A strenuous time of quarrels, contention, strife and anger, with "
|
"mars",
|
||||||
|
"A strenuous time of quarrels, contention, strife and anger, with "
|
||||||
"some risk of hurts or injuries depending on the sign it "
|
"some risk of hurts or injuries depending on the sign it "
|
||||||
"occupies.",
|
"occupies.",
|
||||||
.harmonious = "Can bring benefits from doctors, surgeons, or new projects "
|
"Can bring benefits from doctors, surgeons, or new projects "
|
||||||
"and enterprises.",
|
"and enterprises.",
|
||||||
.discordant = "",
|
"",
|
||||||
},
|
},
|
||||||
[PLANET_JUPITER] = {
|
[PLANET_JUPITER] = {
|
||||||
.base = "Brings increase and expansion - fullness of fortune and health, "
|
"jupiter",
|
||||||
|
"Brings increase and expansion - fullness of fortune and health, "
|
||||||
"and a generally fortunate time.",
|
"and a generally fortunate time.",
|
||||||
.harmonious = "",
|
"", "",
|
||||||
.discordant = "",
|
|
||||||
},
|
},
|
||||||
[PLANET_SATURN] = {
|
[PLANET_SATURN] = {
|
||||||
.base = "Brings depression, stagnation, hindrances and obstacles, and "
|
"saturn",
|
||||||
|
"Brings depression, stagnation, hindrances and obstacles, and "
|
||||||
"some deprivation of the usual benefits.",
|
"some deprivation of the usual benefits.",
|
||||||
.harmonious = "Favours from older connections and past associations are "
|
"Favours from older connections and past associations are "
|
||||||
"still possible.",
|
"still possible.",
|
||||||
.discordant = "",
|
"",
|
||||||
},
|
},
|
||||||
[PLANET_URANUS] = {
|
[PLANET_URANUS] = {
|
||||||
.base = "Brings separations, estrangements, sudden dislocations and "
|
"uranus",
|
||||||
|
"Brings separations, estrangements, sudden dislocations and "
|
||||||
"violent upsets.",
|
"violent upsets.",
|
||||||
.harmonious = "Success through official or civic channels, and "
|
"Success through official or civic channels, and "
|
||||||
"beneficial changes or appointments, are possible.",
|
"beneficial changes or appointments, are possible.",
|
||||||
.discordant = "",
|
"",
|
||||||
},
|
},
|
||||||
[PLANET_NEPTUNE] = {
|
[PLANET_NEPTUNE] = {
|
||||||
.base = "Brings a state of chaos and confusion - an involved, uncertain "
|
"neptune",
|
||||||
|
"Brings a state of chaos and confusion - an involved, uncertain "
|
||||||
"condition of affairs, with plots, subtlety, or unseen "
|
"condition of affairs, with plots, subtlety, or unseen "
|
||||||
"influences at work.",
|
"influences at work.",
|
||||||
.harmonious = "",
|
"", "",
|
||||||
.discordant = "",
|
|
||||||
},
|
},
|
||||||
[PLANET_PLUTO] = {
|
[PLANET_PLUTO] = {
|
||||||
/* Original text, not from Sepharial - see narrative.h. */
|
/* Original text, not from Sepharial - see narrative.h. */
|
||||||
.base = "Brings deep, often hidden transformation - the surfacing or "
|
"pluto",
|
||||||
|
"Brings deep, often hidden transformation - the surfacing or "
|
||||||
"dismantling of something that has outgrown its old form.",
|
"dismantling of something that has outgrown its old form.",
|
||||||
.harmonious = "",
|
"", "",
|
||||||
.discordant = "",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,7 +113,8 @@ static bool aspect_is_discordant(AspectType type) {
|
|||||||
* house governs. This is centuries-old, uncredited astrological
|
* house governs. This is centuries-old, uncredited astrological
|
||||||
* convention (the same status as the sign/aspect names already baked
|
* convention (the same status as the sign/aspect names already baked
|
||||||
* into engine/src/astro.c), not text drawn from Sepharial or any other
|
* into engine/src/astro.c), not text drawn from Sepharial or any other
|
||||||
* single source. Indexed house - 1. */
|
* single source. Indexed house - 1; fallback (English) text only, keyed
|
||||||
|
* as "narrative.house.<1-12>" in engine/i18n's .lang files. */
|
||||||
static const char *const k_house_area[12] = {
|
static const char *const k_house_area[12] = {
|
||||||
"your sense of self, identity, and outward appearance",
|
"your sense of self, identity, and outward appearance",
|
||||||
"money, possessions, and personal values",
|
"money, possessions, and personal values",
|
||||||
@@ -112,22 +130,35 @@ static const char *const k_house_area[12] = {
|
|||||||
"solitude, the subconscious, and hidden matters",
|
"solitude, the subconscious, and hidden matters",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static const char *narrative_field(const char *slug, const char *field, const char *fallback) {
|
||||||
|
char key[48];
|
||||||
|
snprintf(key, sizeof key, "narrative.%s.%s", slug, field);
|
||||||
|
return i18n_get(key, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
|
void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
|
||||||
int transiting_house) {
|
int transiting_house) {
|
||||||
const TransitNarrative *n = &k_narratives[aspect->transiting_planet];
|
const TransitNarrative *n = &k_narratives[aspect->transiting_planet];
|
||||||
|
const char *base = narrative_field(n->slug, "base", n->base);
|
||||||
|
|
||||||
const char *extra = "";
|
const char *extra = "";
|
||||||
if (aspect_is_harmonious(aspect->type)) extra = n->harmonious;
|
if (aspect_is_harmonious(aspect->type)) extra = narrative_field(n->slug, "harmonious", n->harmonious);
|
||||||
else if (aspect_is_discordant(aspect->type)) extra = n->discordant;
|
else if (aspect_is_discordant(aspect->type)) extra = narrative_field(n->slug, "discordant", n->discordant);
|
||||||
|
|
||||||
if (n->base[0] == '\0' && extra[0] == '\0') return;
|
if (base[0] == '\0' && extra[0] == '\0') return;
|
||||||
|
|
||||||
fprintf(out, "%s", indent);
|
fprintf(out, "%s", indent);
|
||||||
if (transiting_house >= 1 && transiting_house <= 12) {
|
if (transiting_house >= 1 && transiting_house <= 12) {
|
||||||
fprintf(out, "In matters of %s (house %d). ",
|
char house_key[24];
|
||||||
k_house_area[transiting_house - 1], transiting_house);
|
snprintf(house_key, sizeof house_key, "narrative.house.%d", transiting_house);
|
||||||
|
const char *area = i18n_get(house_key, k_house_area[transiting_house - 1]);
|
||||||
|
|
||||||
|
char frame[256];
|
||||||
|
snprintf(frame, sizeof frame, i18n_get("narrative.house_frame", "In matters of %s (house %d)."),
|
||||||
|
area, transiting_house);
|
||||||
|
fprintf(out, "%s ", frame);
|
||||||
}
|
}
|
||||||
if (n->base[0] != '\0') fprintf(out, "%s", n->base);
|
if (base[0] != '\0') fprintf(out, "%s", base);
|
||||||
if (extra[0] != '\0') fprintf(out, "%s%s", n->base[0] != '\0' ? " " : "", extra);
|
if (extra[0] != '\0') fprintf(out, "%s%s", base[0] != '\0' ? " " : "", extra);
|
||||||
fprintf(out, "\n");
|
fprintf(out, "\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,13 @@
|
|||||||
* personalized way transits are read (see astro.h's own house-field
|
* personalized way transits are read (see astro.h's own house-field
|
||||||
* comment). Pass a value outside 1-12 (e.g. the "no data" sentinel `0`
|
* comment). Pass a value outside 1-12 (e.g. the "no data" sentinel `0`
|
||||||
* used elsewhere - see reading_io.c's parse_bodies()) to omit that
|
* used elsewhere - see reading_io.c's parse_bodies()) to omit that
|
||||||
* framing and print the planet's narrative on its own. */
|
* framing and print the planet's narrative on its own.
|
||||||
|
*
|
||||||
|
* Every string here is looked up via i18n_get() under "narrative.*"
|
||||||
|
* keys (engine/i18n's .lang files) before falling back to the English text
|
||||||
|
* baked into narrative.c, the same catalog tarot_data.c uses - so this
|
||||||
|
* text respects whatever --lang the caller loaded with i18n_load()
|
||||||
|
* (main.c does so before calling this). */
|
||||||
void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
|
void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
|
||||||
int transiting_house);
|
int transiting_house);
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,20 @@ static const char *const k_sign_slug[12] = {
|
|||||||
"libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
|
"libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Mirrors tarot_data.c's own k_card_slug/k_position_slug, same
|
||||||
|
* duplication policy as k_body_slug above. */
|
||||||
|
static const char *const k_card_slug[TAROT_DECK_SIZE] = {
|
||||||
|
"fool", "magician", "high_priestess", "empress", "emperor", "hierophant",
|
||||||
|
"lovers", "chariot", "strength", "hermit", "wheel_of_fortune", "justice",
|
||||||
|
"hanged_man", "death", "temperance", "devil", "tower", "star", "moon",
|
||||||
|
"sun", "judgement", "world",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *const k_position_slug[TAROT_SPREAD_SIZE] = {
|
||||||
|
"present", "challenge", "crown", "foundation", "recent_past",
|
||||||
|
"near_future", "attitude", "environment", "hopes_and_fears", "outcome",
|
||||||
|
};
|
||||||
|
|
||||||
static bool body_from_slug(const char *slug, Body *out) {
|
static bool body_from_slug(const char *slug, Body *out) {
|
||||||
for (int i = 0; i < NUM_BODIES; i++) {
|
for (int i = 0; i < NUM_BODIES; i++) {
|
||||||
if (strcmp(slug, k_body_slug[i]) == 0) {
|
if (strcmp(slug, k_body_slug[i]) == 0) {
|
||||||
@@ -77,6 +91,48 @@ static bool aspect_type_from_slug(const char *slug, AspectType *out) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool card_from_slug(const char *slug, TarotCard *out) {
|
||||||
|
for (int i = 0; i < TAROT_DECK_SIZE; i++) {
|
||||||
|
if (strcmp(slug, k_card_slug[i]) == 0) {
|
||||||
|
*out = (TarotCard)i;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool position_from_slug(const char *slug, CelticCrossPosition *out) {
|
||||||
|
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||||
|
if (strcmp(slug, k_position_slug[i]) == 0) {
|
||||||
|
*out = (CelticCrossPosition)i;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fills out->positions[] from a "spread"."positions" JSON array. Used only
|
||||||
|
* for guidance_print()'s Attitude/Outcome framing, never for scoring, so
|
||||||
|
* this is deliberately lenient like parse_bodies(): an entry with an
|
||||||
|
* unrecognized/missing "position" or "card" slug is skipped, and a
|
||||||
|
* missing "spread" key entirely just leaves every position zeroed
|
||||||
|
* (card = the first enum value, reversed = false) rather than failing
|
||||||
|
* the whole load. */
|
||||||
|
static void parse_spread(const JsonValue *positions, CelticCrossSpread *out) {
|
||||||
|
int count = json_array_count(positions);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
const JsonValue *item = json_array_get(positions, i);
|
||||||
|
CelticCrossPosition position;
|
||||||
|
TarotCard card;
|
||||||
|
if (!position_from_slug(json_as_string(json_object_get(item, "position")), &position)) continue;
|
||||||
|
if (!card_from_slug(json_as_string(json_object_get(item, "card")), &card)) continue;
|
||||||
|
|
||||||
|
out->positions[position].card = card;
|
||||||
|
const JsonValue *reversed = json_object_get(item, "reversed");
|
||||||
|
out->positions[position].reversed = reversed && reversed->type == JSON_BOOL && reversed->as.boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
||||||
memset(out, 0, sizeof(*out));
|
memset(out, 0, sizeof(*out));
|
||||||
|
|
||||||
@@ -122,6 +178,9 @@ bool reading_load_json(const char *text, size_t length, DailyReading *out) {
|
|||||||
parse_bodies(natal ? json_object_get(natal, "bodies") : NULL, out->natal.bodies);
|
parse_bodies(natal ? json_object_get(natal, "bodies") : NULL, out->natal.bodies);
|
||||||
parse_bodies(json_object_get(transits, "bodies"), out->transits.bodies);
|
parse_bodies(json_object_get(transits, "bodies"), out->transits.bodies);
|
||||||
|
|
||||||
|
const JsonValue *spread = json_object_get(root, "spread");
|
||||||
|
parse_spread(spread ? json_object_get(spread, "positions") : NULL, &out->spread);
|
||||||
|
|
||||||
json_free(root);
|
json_free(root);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,18 +10,21 @@
|
|||||||
|
|
||||||
/* Parses deck-engine's `--format json` output (text, null-terminated at
|
/* Parses deck-engine's `--format json` output (text, null-terminated at
|
||||||
* text[length]) and fills *out with just enough of a DailyReading for
|
* text[length]) and fills *out with just enough of a DailyReading for
|
||||||
* interpret_daily_reading() to work on and for reporting sign/house
|
* interpret_daily_reading() to work on, for reporting sign/house context
|
||||||
* context around each significant event: out->transits.aspects[]/
|
* around each significant event, and for guidance_print()'s tarot
|
||||||
* aspect_count (used for scoring - see significance.c), and
|
* framing: out->transits.aspects[]/aspect_count (used for scoring - see
|
||||||
* out->natal.bodies[]/out->transits.bodies[] (sign + house per body,
|
* significance.c), out->natal.bodies[]/out->transits.bodies[] (sign +
|
||||||
* used only for display). out->natal.ascendant_longitude/houses[] and
|
* house per body, used only for display), and out->spread.positions[]
|
||||||
* out->spread are left zeroed - nothing reads them yet.
|
* (card + reversed per Celtic Cross position, used only by
|
||||||
|
* guidance.c). out->natal.ascendant_longitude/houses[] are left zeroed -
|
||||||
|
* nothing reads them yet.
|
||||||
*
|
*
|
||||||
* Returns false on malformed JSON, or if "transits"."aspects" isn't
|
* Returns false on malformed JSON, or if "transits"."aspects" isn't
|
||||||
* present as an array (an empty array is fine - that's a real "no
|
* present as an array (an empty array is fine - that's a real "no
|
||||||
* aspects today" reading, not an error). A missing/unrecognized entry
|
* aspects today" reading, not an error). A missing/unrecognized entry in
|
||||||
* in "natal"."bodies"/"transits"."bodies" is not an error - see
|
* "natal"."bodies"/"transits"."bodies"/"spread"."positions", or a
|
||||||
* parse_bodies() in reading_io.c. */
|
* missing "spread" key entirely, is not an error - see parse_bodies()/
|
||||||
|
* parse_spread() in reading_io.c. */
|
||||||
bool reading_load_json(const char *text, size_t length, DailyReading *out);
|
bool reading_load_json(const char *text, size_t length, DailyReading *out);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
#define _POSIX_C_SOURCE 200809L /* for mkstemp/fdopen */
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "../../engine/src/i18n.h"
|
||||||
|
#include "../src/guidance.h"
|
||||||
|
|
||||||
|
static const char *slurp(FILE *f) {
|
||||||
|
static char buf[4096];
|
||||||
|
long len = ftell(f);
|
||||||
|
rewind(f);
|
||||||
|
size_t n = fread(buf, 1, (size_t)len, f);
|
||||||
|
buf[n] = '\0';
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
static DailyInterpretation make_interp(DaySignificance level, AspectType type, Body transiting) {
|
||||||
|
DailyInterpretation interp;
|
||||||
|
interp.top_item_count = 1;
|
||||||
|
interp.day_level = level;
|
||||||
|
interp.top_items[0].kind = ITEM_ASPECT;
|
||||||
|
interp.top_items[0].score = 9.5;
|
||||||
|
interp.top_items[0].aspect.transiting_planet = transiting;
|
||||||
|
interp.top_items[0].aspect.natal_planet = PLANET_MOON;
|
||||||
|
interp.top_items[0].aspect.type = type;
|
||||||
|
interp.top_items[0].aspect.orb = 0.5;
|
||||||
|
return interp;
|
||||||
|
}
|
||||||
|
|
||||||
|
static DailyInterpretation make_empty_interp(void) {
|
||||||
|
DailyInterpretation interp;
|
||||||
|
interp.top_item_count = 0;
|
||||||
|
interp.day_level = DAY_QUIET;
|
||||||
|
return interp;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CelticCrossSpread make_spread(TarotCard attitude_card, bool attitude_reversed,
|
||||||
|
TarotCard outcome_card, bool outcome_reversed) {
|
||||||
|
CelticCrossSpread spread;
|
||||||
|
spread.positions[POSITION_ATTITUDE].card = attitude_card;
|
||||||
|
spread.positions[POSITION_ATTITUDE].reversed = attitude_reversed;
|
||||||
|
spread.positions[POSITION_OUTCOME].card = outcome_card;
|
||||||
|
spread.positions[POSITION_OUTCOME].reversed = outcome_reversed;
|
||||||
|
return spread;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_harmonious_upright(void) {
|
||||||
|
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_TRINE, PLANET_JUPITER);
|
||||||
|
CelticCrossSpread spread = make_spread(CARD_SUN, false, CARD_WORLD, false);
|
||||||
|
FILE *f = tmpfile();
|
||||||
|
guidance_print(f, "", &interp, &spread);
|
||||||
|
const char *text = slurp(f);
|
||||||
|
|
||||||
|
assert(strstr(text, "already meeting the day in the right spirit") != NULL);
|
||||||
|
assert(strstr(text, "Jupiter") != NULL);
|
||||||
|
assert(strstr(text, "The Sun: Material happiness") != NULL);
|
||||||
|
assert(strstr(text, "The World: Assured success") != NULL);
|
||||||
|
assert(strstr(text, "(Reversed)") == NULL);
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
printf("PASS test_harmonious_upright\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_discordant_reversed(void) {
|
||||||
|
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_SQUARE, PLANET_SATURN);
|
||||||
|
CelticCrossSpread spread = make_spread(CARD_DEVIL, true, CARD_WORLD, false);
|
||||||
|
FILE *f = tmpfile();
|
||||||
|
guidance_print(f, "", &interp, &spread);
|
||||||
|
const char *text = slurp(f);
|
||||||
|
|
||||||
|
assert(strstr(text, "Both the day and your own footing are unsettled") != NULL);
|
||||||
|
assert(strstr(text, "Saturn") != NULL);
|
||||||
|
assert(strstr(text, "The Devil (Reversed): Evil fatality") != NULL);
|
||||||
|
assert(strstr(text, "The World: Assured success") != NULL);
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
printf("PASS test_discordant_reversed\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_neutral_conjunction_upright(void) {
|
||||||
|
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_CONJUNCTION, PLANET_PLUTO);
|
||||||
|
CelticCrossSpread spread = make_spread(CARD_EMPEROR, false, CARD_MOON, true);
|
||||||
|
FILE *f = tmpfile();
|
||||||
|
guidance_print(f, "", &interp, &spread);
|
||||||
|
const char *text = slurp(f);
|
||||||
|
|
||||||
|
assert(strstr(text, "Today concentrates whatever you already bring to it") != NULL);
|
||||||
|
assert(strstr(text, "Pluto") != NULL);
|
||||||
|
assert(strstr(text, "The Emperor: Stability, power") != NULL);
|
||||||
|
assert(strstr(text, "The Moon (Reversed): Instability, inconstancy") != NULL);
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
printf("PASS test_neutral_conjunction_upright\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The stance sentence itself doesn't change with intensity - only the
|
||||||
|
* sentence introducing the transiting planet does (see k_intro in
|
||||||
|
* guidance.c). These three cover the remaining day levels below Major
|
||||||
|
* (which test_harmonious_upright already covers). */
|
||||||
|
static void test_significant_day_uses_significant_intro(void) {
|
||||||
|
DailyInterpretation interp = make_interp(DAY_SIGNIFICANT, ASPECT_SEXTILE, PLANET_VENUS);
|
||||||
|
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
||||||
|
FILE *f = tmpfile();
|
||||||
|
guidance_print(f, "", &interp, &spread);
|
||||||
|
const char *text = slurp(f);
|
||||||
|
|
||||||
|
assert(strstr(text, "clearly active today") != NULL);
|
||||||
|
assert(strstr(text, "Venus") != NULL);
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
printf("PASS test_significant_day_uses_significant_intro\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_notable_day_uses_notable_intro(void) {
|
||||||
|
DailyInterpretation interp = make_interp(DAY_NOTABLE, ASPECT_SEXTILE, PLANET_MARS);
|
||||||
|
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
||||||
|
FILE *f = tmpfile();
|
||||||
|
guidance_print(f, "", &interp, &spread);
|
||||||
|
const char *text = slurp(f);
|
||||||
|
|
||||||
|
assert(strstr(text, "mild touch from Mars") != NULL);
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
printf("PASS test_notable_day_uses_notable_intro\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_quiet_day_with_a_faint_item_still_names_it(void) {
|
||||||
|
DailyInterpretation interp = make_interp(DAY_QUIET, ASPECT_SEXTILE, PLANET_MERCURY);
|
||||||
|
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
||||||
|
FILE *f = tmpfile();
|
||||||
|
guidance_print(f, "", &interp, &spread);
|
||||||
|
const char *text = slurp(f);
|
||||||
|
|
||||||
|
assert(strstr(text, "only faintly active today") != NULL);
|
||||||
|
assert(strstr(text, "Mercury") != NULL);
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
printf("PASS test_quiet_day_with_a_faint_item_still_names_it\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_no_aspects_at_all_falls_back_to_attitude_only(void) {
|
||||||
|
DailyInterpretation interp = make_empty_interp();
|
||||||
|
CelticCrossSpread spread = make_spread(CARD_STAR, false, CARD_SUN, false);
|
||||||
|
FILE *f = tmpfile();
|
||||||
|
guidance_print(f, "", &interp, &spread);
|
||||||
|
const char *text = slurp(f);
|
||||||
|
|
||||||
|
assert(strstr(text, "astrologically quiet day") != NULL);
|
||||||
|
assert(strstr(text, "trust your current footing") != NULL);
|
||||||
|
assert(strstr(text, "The Star: Loss, theft") != NULL);
|
||||||
|
assert(strstr(text, "The Sun: Material happiness") != NULL);
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
printf("PASS test_no_aspects_at_all_falls_back_to_attitude_only\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_no_aspects_at_all_respects_reversed_attitude(void) {
|
||||||
|
DailyInterpretation interp = make_empty_interp();
|
||||||
|
CelticCrossSpread spread = make_spread(CARD_STAR, true, CARD_SUN, false);
|
||||||
|
FILE *f = tmpfile();
|
||||||
|
guidance_print(f, "", &interp, &spread);
|
||||||
|
const char *text = slurp(f);
|
||||||
|
|
||||||
|
assert(strstr(text, "quietly re-settle your own footing") != NULL);
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
printf("PASS test_no_aspects_at_all_respects_reversed_attitude\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Proves guidance_print() actually routes through i18n_get() rather
|
||||||
|
* than just printing the hardcoded fallback. Run last: i18n_load()
|
||||||
|
* replaces the process-global catalog for the rest of the binary's
|
||||||
|
* lifetime. */
|
||||||
|
static void test_translation_catalog_overrides_fallback_text(void) {
|
||||||
|
char path[] = "/tmp/deck_guidance_test_XXXXXX";
|
||||||
|
int fd = mkstemp(path);
|
||||||
|
assert(fd >= 0);
|
||||||
|
FILE *f = fdopen(fd, "w");
|
||||||
|
fprintf(f, "guidance.stance.discordant.upright=UEBERSETZTE HALTUNG\n");
|
||||||
|
fprintf(f, "guidance.intro.major=UEBERSETZTE EINLEITUNG %%s\n");
|
||||||
|
fprintf(f, "guidance.planet.saturn=UEBERSETZTER SATURN\n");
|
||||||
|
fprintf(f, "guidance.attitude_intro=UEBERSETZTE HALTUNGSKARTE\n");
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
assert(i18n_load(path));
|
||||||
|
unlink(path);
|
||||||
|
|
||||||
|
DailyInterpretation interp = make_interp(DAY_MAJOR, ASPECT_SQUARE, PLANET_SATURN);
|
||||||
|
CelticCrossSpread spread = make_spread(CARD_SUN, false, CARD_WORLD, false);
|
||||||
|
FILE *out = tmpfile();
|
||||||
|
guidance_print(out, "", &interp, &spread);
|
||||||
|
const char *text = slurp(out);
|
||||||
|
|
||||||
|
assert(strstr(text, "UEBERSETZTE HALTUNG") != NULL);
|
||||||
|
assert(strstr(text, "UEBERSETZTE EINLEITUNG UEBERSETZTER SATURN") != NULL);
|
||||||
|
assert(strstr(text, "UEBERSETZTE HALTUNGSKARTE") != NULL);
|
||||||
|
assert(strstr(text, "Your instincts are sound") == NULL); /* English fallback must not leak through */
|
||||||
|
|
||||||
|
fclose(out);
|
||||||
|
printf("PASS test_translation_catalog_overrides_fallback_text\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
test_harmonious_upright();
|
||||||
|
test_discordant_reversed();
|
||||||
|
test_neutral_conjunction_upright();
|
||||||
|
test_significant_day_uses_significant_intro();
|
||||||
|
test_notable_day_uses_notable_intro();
|
||||||
|
test_quiet_day_with_a_faint_item_still_names_it();
|
||||||
|
test_no_aspects_at_all_falls_back_to_attitude_only();
|
||||||
|
test_no_aspects_at_all_respects_reversed_attitude();
|
||||||
|
test_translation_catalog_overrides_fallback_text();
|
||||||
|
printf("All guidance tests passed.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -41,11 +41,11 @@ static void test_json_parse_rejects_malformed(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* A trimmed-but-structurally-faithful fixture matching deck-engine's real
|
/* A trimmed-but-structurally-faithful fixture matching deck-engine's real
|
||||||
* --format json shape: "spread" is present with decoy content the loader
|
* --format json shape. "natal"/"transits" bodies are real (if partial) -
|
||||||
* must skip over without understanding its schema, to prove it navigates
|
* reading_load_json parses sign/house from them for display, alongside
|
||||||
* by key path rather than assuming any position. "natal"/"transits"
|
* the aspects used for scoring. "spread" only fills in the Attitude and
|
||||||
* bodies are real (if partial) - reading_load_json now parses sign/house
|
* Outcome positions (the two guidance.c reads); the other eight are
|
||||||
* from them for display, alongside the aspects used for scoring. */
|
* absent entirely, to prove partial spread data doesn't fail the load. */
|
||||||
static const char *k_fixture =
|
static const char *k_fixture =
|
||||||
"{"
|
"{"
|
||||||
" \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"taurus\", \"house\": 3}], \"houses\": []},"
|
" \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"taurus\", \"house\": 3}], \"houses\": []},"
|
||||||
@@ -57,7 +57,10 @@ static const char *k_fixture =
|
|||||||
" {\"transiting_planet\": \"saturn\", \"natal_planet\": \"sun\", \"type\": \"square\", \"orb\": 0.5}"
|
" {\"transiting_planet\": \"saturn\", \"natal_planet\": \"sun\", \"type\": \"square\", \"orb\": 0.5}"
|
||||||
" ]"
|
" ]"
|
||||||
" },"
|
" },"
|
||||||
" \"spread\": {\"positions\": [{\"position\": \"present\", \"card\": \"devil\", \"reversed\": false}]}"
|
" \"spread\": {\"positions\": ["
|
||||||
|
" {\"position\": \"attitude\", \"card\": \"devil\", \"reversed\": true},"
|
||||||
|
" {\"position\": \"outcome\", \"card\": \"world\", \"reversed\": false}"
|
||||||
|
" ]}"
|
||||||
"}";
|
"}";
|
||||||
|
|
||||||
static void test_reading_load_json_extracts_aspects(void) {
|
static void test_reading_load_json_extracts_aspects(void) {
|
||||||
@@ -95,6 +98,35 @@ static void test_reading_load_json_extracts_body_sign_and_house(void) {
|
|||||||
printf("PASS test_reading_load_json_extracts_body_sign_and_house\n");
|
printf("PASS test_reading_load_json_extracts_body_sign_and_house\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_reading_load_json_extracts_spread_positions(void) {
|
||||||
|
DailyReading reading;
|
||||||
|
bool ok = reading_load_json(k_fixture, strlen(k_fixture), &reading);
|
||||||
|
|
||||||
|
assert(ok);
|
||||||
|
assert(reading.spread.positions[POSITION_ATTITUDE].card == CARD_DEVIL);
|
||||||
|
assert(reading.spread.positions[POSITION_ATTITUDE].reversed == true);
|
||||||
|
assert(reading.spread.positions[POSITION_OUTCOME].card == CARD_WORLD);
|
||||||
|
assert(reading.spread.positions[POSITION_OUTCOME].reversed == false);
|
||||||
|
|
||||||
|
/* Positions absent from the fixture (e.g. Present) get the zeroed
|
||||||
|
* default (CARD_FOOL, upright) rather than garbage. */
|
||||||
|
assert(reading.spread.positions[POSITION_PRESENT].card == CARD_FOOL);
|
||||||
|
assert(reading.spread.positions[POSITION_PRESENT].reversed == false);
|
||||||
|
|
||||||
|
printf("PASS test_reading_load_json_extracts_spread_positions\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_reading_load_json_missing_spread_is_not_fatal(void) {
|
||||||
|
const char *text = "{\"transits\": {\"aspects\": []}}";
|
||||||
|
DailyReading reading;
|
||||||
|
bool ok = reading_load_json(text, strlen(text), &reading);
|
||||||
|
|
||||||
|
assert(ok);
|
||||||
|
assert(reading.spread.positions[POSITION_OUTCOME].card == CARD_FOOL);
|
||||||
|
|
||||||
|
printf("PASS test_reading_load_json_missing_spread_is_not_fatal\n");
|
||||||
|
}
|
||||||
|
|
||||||
static void test_reading_load_json_unknown_body_slug_is_skipped_not_fatal(void) {
|
static void test_reading_load_json_unknown_body_slug_is_skipped_not_fatal(void) {
|
||||||
const char *text =
|
const char *text =
|
||||||
"{\"natal\": {\"bodies\": [{\"body\": \"xenu\", \"sign\": \"taurus\", \"house\": 3}]},"
|
"{\"natal\": {\"bodies\": [{\"body\": \"xenu\", \"sign\": \"taurus\", \"house\": 3}]},"
|
||||||
@@ -147,6 +179,8 @@ int main(void) {
|
|||||||
test_json_parse_rejects_malformed();
|
test_json_parse_rejects_malformed();
|
||||||
test_reading_load_json_extracts_aspects();
|
test_reading_load_json_extracts_aspects();
|
||||||
test_reading_load_json_extracts_body_sign_and_house();
|
test_reading_load_json_extracts_body_sign_and_house();
|
||||||
|
test_reading_load_json_extracts_spread_positions();
|
||||||
|
test_reading_load_json_missing_spread_is_not_fatal();
|
||||||
test_reading_load_json_unknown_body_slug_is_skipped_not_fatal();
|
test_reading_load_json_unknown_body_slug_is_skipped_not_fatal();
|
||||||
test_reading_load_json_empty_aspects_is_valid();
|
test_reading_load_json_empty_aspects_is_valid();
|
||||||
test_reading_load_json_rejects_missing_transits();
|
test_reading_load_json_rejects_missing_transits();
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
#define _POSIX_C_SOURCE 200809L /* for mkstemp/fdopen */
|
||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "../../engine/src/i18n.h"
|
||||||
#include "../src/narrative.h"
|
#include "../src/narrative.h"
|
||||||
|
|
||||||
static const char *slurp(FILE *f) {
|
static const char *slurp(FILE *f) {
|
||||||
@@ -112,6 +117,37 @@ static void test_unknown_house_omits_area_framing(void) {
|
|||||||
printf("PASS test_unknown_house_omits_area_framing\n");
|
printf("PASS test_unknown_house_omits_area_framing\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Proves narrative_print() actually routes through i18n_get() rather
|
||||||
|
* than just printing the hardcoded fallback - loads a translation file
|
||||||
|
* that overrides one planet's base text and one house's area text, and
|
||||||
|
* checks the override wins. Run last: i18n_load() replaces the
|
||||||
|
* process-global catalog for the rest of the test binary's lifetime. */
|
||||||
|
static void test_translation_catalog_overrides_fallback_text(void) {
|
||||||
|
char path[] = "/tmp/deck_narrative_test_XXXXXX";
|
||||||
|
int fd = mkstemp(path);
|
||||||
|
assert(fd >= 0);
|
||||||
|
FILE *f = fdopen(fd, "w");
|
||||||
|
fprintf(f, "narrative.saturn.base=UEBERSETZTER SATURN TEXT\n");
|
||||||
|
fprintf(f, "narrative.house.3=UEBERSETZTES HAUS DREI\n");
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
assert(i18n_load(path));
|
||||||
|
unlink(path);
|
||||||
|
|
||||||
|
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON,
|
||||||
|
.type = ASPECT_CONJUNCTION, .orb = 1.0 };
|
||||||
|
FILE *out = tmpfile();
|
||||||
|
narrative_print(out, "", &a, 3);
|
||||||
|
const char *text = slurp(out);
|
||||||
|
|
||||||
|
assert(strstr(text, "UEBERSETZTER SATURN TEXT") != NULL);
|
||||||
|
assert(strstr(text, "UEBERSETZTES HAUS DREI") != NULL);
|
||||||
|
assert(strstr(text, "depression") == NULL); /* English fallback must not leak through */
|
||||||
|
|
||||||
|
fclose(out);
|
||||||
|
printf("PASS test_translation_catalog_overrides_fallback_text\n");
|
||||||
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
test_hard_aspect_shows_base_but_not_harmonious_bonus();
|
test_hard_aspect_shows_base_but_not_harmonious_bonus();
|
||||||
test_soft_aspect_adds_harmonious_bonus();
|
test_soft_aspect_adds_harmonious_bonus();
|
||||||
@@ -120,6 +156,7 @@ int main(void) {
|
|||||||
test_discordant_aspect_on_empty_base_planet();
|
test_discordant_aspect_on_empty_base_planet();
|
||||||
test_known_house_adds_area_framing();
|
test_known_house_adds_area_framing();
|
||||||
test_unknown_house_omits_area_framing();
|
test_unknown_house_omits_area_framing();
|
||||||
|
test_translation_catalog_overrides_fallback_text();
|
||||||
printf("All narrative tests passed.\n");
|
printf("All narrative tests passed.\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user