Add narrative text and a day-significance rank to the interpreter

Each of the day's top significant transits now gets a short sentence
describing what it classically means and which area of life its house
governs, condensed from Sepharial's Transits and Planetary Periods
(1920, public domain) plus a small table of traditional house
significations. The day-level line also shows its rank out of the 4
defined levels, e.g. "Notable (2/4)". Docs (CLAUDE.md, README,
docs/input-output-format.md, docs/reading.md) updated to match,
including a full worked example in docs/reading.md.
This commit is contained in:
ml
2026-07-05 21:05:53 +02:00
parent 1136e3257a
commit 41422adf98
11 changed files with 476 additions and 41 deletions
+53 -15
View File
@@ -71,7 +71,10 @@ deck_in_a_dash/
res/ Tarot card art (22 RWS Major Arcana JPEGs), res/ Tarot card art (22 RWS Major Arcana JPEGs),
Waite's "Pictorial Key to the Tarot" (PDF, Waite's "Pictorial Key to the Tarot" (PDF,
public domain — source of all card/spread public domain — source of all card/spread
text), logo, research notes. text), Sepharial's "Transits and Planetary
Periods" (PDF, public domain — source of
interpreter/src/narrative.c's text), logo,
research notes.
engine/ engine/
third_party/astronomy/ Vendored cosinekitty/astronomy C library third_party/astronomy/ Vendored cosinekitty/astronomy C library
(MIT, pinned commit — see VENDORED.md). (MIT, pinned commit — see VENDORED.md).
@@ -199,11 +202,14 @@ into the Pebble watchapp unchanged later. Only `reading_print_text`/
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* — the first piece of a larger "turn the significant a day's transits are* and, for each of the top items, prints
raw reading into an actual narrative" effort described in project chat a short narrative sentence about what that transit classically means —
history, not yet producing any narrative text. `deck-engine` itself is the first piece of a larger "turn the raw reading into an actual
deliberately unchanged by this beyond gaining `--format json`; the two narrative" effort described in project chat history; the Celtic Cross
binaries compose over that JSON, never by linking together: spread itself isn't woven into that narrative yet (see
`docs/reading.md`). `deck-engine` itself is deliberately unchanged by
this beyond gaining `--format json`; the two binaries compose over that
JSON, never by linking 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
@@ -223,11 +229,15 @@ between the two binaries (see the next bullet).
every aspect in `reading->transits.aspects[]`, keeps the **top 5** by every aspect in `reading->transits.aspects[]`, keeps the **top 5** by
score in `DailyInterpretation.top_items[]` (descending, evicting the score in `DailyInterpretation.top_items[]` (descending, evicting the
rest), and classifies the day into a `DaySignificance` enum (`DAY_QUIET` rest), and classifies the day into a `DaySignificance` enum (`DAY_QUIET`
... `DAY_MAJOR`) from the single highest-scoring item. ... `DAY_MAJOR`) from the single highest-scoring item. The enum also
`interpretation_deserves_framing()` is true only at `DAY_MAJOR` — the carries a trailing `DAY_SIGNIFICANCE_COUNT` sentinel (not a real level)
intended hook for giving the Celtic Cross reading a "this matters" purely so callers can print the level as a rank, e.g. `main.c`'s
intro sentence naming `top_items[0]` on days that earn it; that `"%s (%d/%d)"``"Notable (2/4)"``interp.day_level + 1` out of
rendering doesn't exist yet, `main.c`'s report is plain text only. `DAY_SIGNIFICANCE_COUNT`. `interpretation_deserves_framing()` is true
only at `DAY_MAJOR` — the intended hook for giving the Celtic Cross
reading a "this matters" intro sentence naming `top_items[0]` on days
that earn it; that rendering doesn't exist yet, `main.c`'s report is
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
@@ -235,6 +245,29 @@ between the two binaries (see the next bullet).
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 — expect to retune them once real readings are
compared against how "major" a day actually feels. compared against how "major" a day actually feels.
- **`narrative.c`/`narrative.h`** supplies `main.c`'s one-sentence
description printed under each top item, condensed from Sepharial's
*Transits and Planetary Periods* (1920, public domain —
`res/Transits_and_Planetary_Periods.pdf`), Chapter VIII "Effects of
Transits". Per transiting planet it holds a `base` description plus a
`harmonious`/`discordant` addendum, shown only under a trine/sextile or
square/opposition respectively (either may be `""` if the source gives
none); a conjunction is treated as neutral — base only — since
Sepharial's own text says a conjunction "takes the nature of what it
conjoins," which isn't modelled here. The Moon and Pluto aren't in that
chapter (fast lunar transits were out of scope for a year-ahead
forecasting book; Pluto wasn't discovered until 1930) — their text is
original, written for this project, not drawn from Sepharial. Each
narrative is also framed by *where* it's happening: `narrative_print()`
takes the transiting planet's whole-sign house (1-12, relative to the
natal chart, same as everywhere else in this codebase) and prefixes an
"In matters of ..." line naming that house's traditional area of life,
from a small `k_house_area[12]` table of standard whole-sign house
significations — centuries-old convention, not attributed to any single
source (same status as the sign/aspect names already in `astro.c`).
Passing a house outside 1-12 (the same "no data" sentinel `reading_io.c`
uses elsewhere) omits that framing and prints the planet's narrative on
its own.
- **`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
@@ -261,10 +294,11 @@ between the two binaries (see the next bullet).
it before printing sign/house), since that's supplementary display it before printing sign/house), since that's supplementary display
context rather than something scoring depends on. context rather than something scoring depends on.
- **Deliberately header-only dependency on the engine, throughout**: - **Deliberately header-only dependency on the engine, throughout**:
`significance.h`/`reading_io.h` `#include engine/src/reading.h` for the `significance.h`/`reading_io.h`/`narrative.h` `#include` engine headers
struct/enum *definitions*, but the root `Makefile` never compiles or (`reading.h`/`astro.h`) for the struct/enum *definitions*, but the root
links any engine `.c` file (or the vendored Astronomy Engine) into the `Makefile` never compiles or links any engine `.c` file (or the
interpreter's binary or tests — every vendored Astronomy Engine) into the interpreter's binary or tests —
every
test in `interpreter/tests/` runs against hand-built JSON text or test in `interpreter/tests/` runs against hand-built JSON text or
hand-built `DailyReading` values, with no real ephemeris/tarot-draw hand-built `DailyReading` values, with no real ephemeris/tarot-draw
call involved anywhere. Consequently `reading_io.c` (planet/aspect/sign call involved anywhere. Consequently `reading_io.c` (planet/aspect/sign
@@ -287,3 +321,7 @@ between the two binaries (see the next bullet).
- `engine/i18n/de.lang`'s card/spread text is an original translation of - `engine/i18n/de.lang`'s card/spread text is an original translation of
that same public-domain English text, made for this project — not that same public-domain English text, made for this project — not
taken from any specific published German edition. taken from any specific published German edition.
- Transit narrative text in `interpreter/src/narrative.c`: condensed from
Sepharial's *Transits and Planetary Periods* (1920, public domain —
`res/Transits_and_Planetary_Periods.pdf`), except the Moon and Pluto
entries, which are original (see narrative.c's own comment for why).
+9 -2
View File
@@ -46,7 +46,8 @@ ENGINE_TEST_BINARY := $(BUILD_DIR)/smoke_test
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_SRC_DIR)/reading_io.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_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
@@ -54,8 +55,10 @@ 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
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
.PHONY: all test clean images i18n scripts .PHONY: all test clean images i18n scripts
@@ -106,10 +109,11 @@ 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) test: $(ENGINE_TEST_BINARY) $(SIGNIFICANCE_TEST_BIN) $(JSON_TEST_BIN) $(NARRATIVE_TEST_BIN)
./$(ENGINE_TEST_BINARY) ./$(ENGINE_TEST_BINARY)
./$(SIGNIFICANCE_TEST_BIN) ./$(SIGNIFICANCE_TEST_BIN)
./$(JSON_TEST_BIN) ./$(JSON_TEST_BIN)
./$(NARRATIVE_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)
@@ -120,6 +124,9 @@ $(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)
$(CC) $(CFLAGS) -o $@ $^
$(OBJ_DIR)/%.o: %.c $(OBJ_DIR)/%.o: %.c
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c -o $@ $< $(CC) $(CFLAGS) -c -o $@ $<
+8 -2
View File
@@ -35,7 +35,7 @@ dropping a translated `key=value` file into `engine/i18n/` — see
``` ```
res/ Tarot card art (22 RWS Major Arcana), the res/ Tarot card art (22 RWS Major Arcana), the
Waite PDF, logo, research notes. Waite and Sepharial PDFs, logo, research notes.
engine/ engine/
third_party/astronomy/ Vendored cosinekitty/astronomy C library (MIT). third_party/astronomy/ Vendored cosinekitty/astronomy C library (MIT).
src/ Engine source (rng, tarot, astro, i18n, reading, CLI). src/ Engine source (rng, tarot, astro, i18n, reading, CLI).
@@ -45,7 +45,8 @@ 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,
reading deck-engine's --format json output reading deck-engine's --format json output
(see CLAUDE.md). (see CLAUDE.md).
docs/ Architecture/dataflow diagrams, input/output format reference. docs/ Architecture/dataflow diagrams, input/output format reference.
@@ -118,3 +119,8 @@ unmodified) — see its `VENDORED.md` for the pinned commit.
Card and spread text in `engine/src/tarot_data.c` is condensed from A. E. Card and spread text in `engine/src/tarot_data.c` is condensed from A. E.
Waite's *The Pictorial Key to the Tarot* (1911), which is in the public Waite's *The Pictorial Key to the Tarot* (1911), which is in the public
domain. domain.
Transit narrative text in `interpreter/src/narrative.c` is condensed
from Sepharial's *Transits and Planetary Periods* (1920), also in the
public domain, except the Moon and Pluto entries, which are original
(see the file's own comment).
+14 -6
View File
@@ -248,18 +248,26 @@ dist/run-interpreter.sh
`house`, is silently skipped rather than failing the load (see `house`, is silently skipped rather than failing the load (see
`parse_bodies()` in `reading_io.c`); `spread` is always ignored. `parse_bodies()` in `reading_io.c`); `spread` is always ignored.
- **Output**: a plain-text report — the day's overall significance level - **Output**: a plain-text report — the day's overall significance level
(`Quiet`/`Notable`/`Significant`/`Major`), a "worth a deeper Celtic (`Quiet`/`Notable`/`Significant`/`Major`, plus its rank out of the 4
Cross look" line on `Major` days only, and up to 5 of today's aspects defined levels, e.g. `Notable (2/4)`), a "worth a deeper Celtic Cross
ranked by score, each naming the sign and natal house the transiting look" line on `Major` days only, and up to 5 of today's aspects ranked
planet currently occupies and the sign and house of the natal planet by score, each naming the sign and natal house the transiting planet
it's aspecting: 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:
``` ```
Day significance: Significant Day significance: Significant (3/4)
(a major transit today - worth a deeper Celtic Cross look) (a major transit today - worth a deeper Celtic Cross look)
Top 3 significant transits: Top 3 significant transits:
1. Transiting Saturn in Aries (house 2) Square natal Sun in Taurus (house 3) (orb 0.5°, score 8.10) 1. Transiting Saturn in Aries (house 2) Square natal Sun in Taurus (house 3) (orb 0.5°, score 8.10)
In matters of money, possessions, and personal values (house 2). Brings depression, stagnation, hindrances and obstacles, and some deprivation of the usual benefits.
2. Transiting Pluto in Aquarius (house 12) Opposition natal Moon in Capricorn (house 11) (orb 0.2°, score 7.92) 2. Transiting Pluto in Aquarius (house 12) Opposition natal Moon in Capricorn (house 11) (orb 0.2°, score 7.92)
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.
3. Transiting Jupiter in Leo (house 5) Trine natal Venus in Aries (house 2) (orb 2.1°, score 3.40) 3. Transiting Jupiter in Leo (house 5) Trine natal Venus in Aries (house 2) (orb 2.1°, score 3.40)
In matters of romance, creativity, and children (house 5). Brings increase and expansion - fullness of fortune and health, and a generally fortunate time.
``` ```
+79 -13
View File
@@ -75,26 +75,92 @@ spread — the same source most modern tarot decks trace back to.
Not every day is equally eventful astrologically. A separate, Not every day is equally eventful astrologically. A separate,
optional step can look at today's aspects (part 2, above) and works optional step can look at today's aspects (part 2, above) and works
out an overall **significance level** for the day — Quiet, Notable, out an overall **significance level** for the day — Quiet, Notable,
Significant, or Major — based on which planets are involved, how exact Significant, or Major, shown together with its rank out of those four
the angle is, and whether it touches your Sun or Moon (the two most (for example "Notable (2/4)") — based on which planets are involved,
personally weighted points in a chart). On a genuinely Major day, it how exact the angle is, and whether it touches your Sun or Moon (the
also calls out the single most important thing happening, as a hint two most personally weighted points in a chart). On a genuinely Major
that it's worth paying closer attention to your tarot spread that day day, it also calls out the single most important thing happening, as a
rather than treating it as routine. Quiet days still get a full hint that it's worth paying closer attention to your tarot spread that
day rather than treating it as routine. Quiet days still get a full
reading — this step only adds a "pay attention today" flag, it never reading — this step only adds a "pay attention today" flag, it never
removes anything. removes anything.
For each of the handful of events that make up today's score, you also
get a plain sentence about what it classically means and *which area
of your life* it's playing out in — drawn from the house it falls in
(see "Today's sky", above). For example, a tense aspect from Saturn
landing in your 6th house reads as a day for your daily work and
routine to feel a bit stuck, not just "Saturn is doing something."
That wording is condensed from a public-domain 1920 astrology book
(Sepharial's *Transits and Planetary Periods*), the same way the tarot
card meanings are condensed from Waite's 1911 book.
## 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, and your 10-card Celtic Cross spread for significant today is overall (with a plain-language note on each of the
the day. Right now these sit side by side — the astrology tells you main events driving that), and your 10-card Celtic Cross spread for the
*how eventful* today is and *where* to pay attention, and the tarot day. Right now these sit side by side — the astrology tells you *how
spread gives you the actual guidance to sit with. They don't yet get eventful* today is and *where* to pay attention, and the tarot spread
woven into a single narrative (e.g. a spread explained *in light of* gives you the actual guidance to sit with. They don't yet get woven
today's significant transit) — that combined storytelling is the next into a single narrative (e.g. a spread explained *in light of* today's
piece to build, not something you get today. significant transit) — that combined storytelling is the next piece to
build, not something you get today.
*(This page, like that piece, is still a work in progress — expect it *(This page, like that piece, is still a work in progress — expect it
to grow as the reading itself does.)* to grow as the reading itself does.)*
## A worked example
Here's what an actual day's reading looks like end to end, for someone
born 14 March 1988 in Berlin, checking their reading on 5 July 2026.
**Natal chart (excerpt).** Sun in Pisces, Moon in Aquarius, Ascendant in
Sagittarius.
**Today's sky (excerpt).** Moon phase: Waning Gibbous. Several planets
are forming aspects back to the natal chart above — see below for which
ones actually matter.
**How big is today.**
```
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.
```
In plain terms: today is about as big as this reading gets (Major,
4 out of 4), and the story is squarely about the Moon — a slow-moving
Pluto sitting right on top of it, a chaotic Neptune touching it gently
from the 5th house, and a disruptive-but-possibly-lucky Uranus trine
from the 7th. Communication, romance, and relationships are all
quietly under pressure at once.
**Tarot spread (excerpt).** Drawn independently, using today's date:
- *The Present* — The Emperor, reversed: immaturity or a loss of
authority, rather than confident structure.
- *The Challenge* — The Devil: force, ravage, an obstacle that isn't
easily reasoned with.
- *The Near Future* — The World, reversed: stagnation, things staying
fixed in place rather than resolving.
Read side by side, the astrology and the tarot happen to rhyme here —
both point toward something that wants to shift (Pluto's "hidden
transformation," The Devil's "force") running into something that
feels stuck (a reversed World, an afflicted Moon) — but that's the
reader's own synthesis for now, not something the app does for you (see
above).
+5 -2
View File
@@ -3,6 +3,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "narrative.h"
#include "reading_io.h" #include "reading_io.h"
#include "significance.h" #include "significance.h"
@@ -12,7 +13,7 @@ static void print_usage(const char *prog) {
"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 significance report: the day's overall level and the top\n"
"aspects driving it.\n\n" "aspects driving it, each with a short narrative note.\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",
@@ -115,7 +116,8 @@ int main(int argc, char **argv) {
DailyInterpretation interp; DailyInterpretation interp;
interpret_daily_reading(&reading, &interp); interpret_daily_reading(&reading, &interp);
printf("Day significance: %s\n", day_level_name(interp.day_level)); printf("Day significance: %s (%d/%d)\n", day_level_name(interp.day_level),
interp.day_level + 1, DAY_SIGNIFICANCE_COUNT);
if (interpretation_deserves_framing(&interp)) { if (interpretation_deserves_framing(&interp)) {
printf("(a major transit today - worth a deeper Celtic Cross look)\n"); printf("(a major transit today - worth a deeper Celtic Cross look)\n");
} }
@@ -137,6 +139,7 @@ int main(int argc, char **argv) {
printf(" %s natal %s", aspect_display_name(a->type), body_display_name(a->natal_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]); print_body_in_sign(&reading.natal.bodies[a->natal_planet]);
printf(" (orb %.1f\xc2\xb0, score %.2f)\n", a->orb, item->score); 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;
} }
+133
View File
@@ -0,0 +1,133 @@
#include "narrative.h"
#include <stdbool.h>
typedef struct {
const char *base; /* always relevant, may be "" (see the Sun) */
const char *harmonious; /* shown only under a trine/sextile; may be "" */
const char *discordant; /* shown only under a square/opposition; may be "" */
} TransitNarrative;
/* See narrative.h's doc comment for sourcing (Sepharial, "Transits and
* Planetary Periods", 1920, Chapter VIII) and the Moon/Pluto exception. */
static const TransitNarrative k_narratives[NUM_BODIES] = {
[PLANET_SUN] = {
.base = "",
.harmonious = "Benefits from superiors and advancement in your sphere of "
"life and work - honours, emoluments, and successful new "
"associations.",
.discordant = "Degradation and dishonour, loss of position, and adverse "
"judgement from superiors.",
},
[PLANET_MOON] = {
/* Original text, not from Sepharial - see narrative.h. */
.base = "Colours the everyday and domestic sphere of life, often "
"coinciding with the opening of new avenues.",
.harmonious = "These changes tend to be advantageous.",
.discordant = "These changes tend to be adverse, with some indisposition "
"or domestic friction.",
},
[PLANET_MERCURY] = {
.base = "Affects writings, journeys, commerce, and everyday activities - "
"a neutral messenger whose effect follows the nature of the "
"aspect it makes.",
.harmonious = "",
.discordant = "",
},
[PLANET_VENUS] = {
.base = "Brings domestic and social affairs to the fore - happiness, "
"comforts, and favours.",
.harmonious = "Success in love affairs and artistic pursuits is likely.",
.discordant = "Grief and disappointment are more likely.",
},
[PLANET_MARS] = {
.base = "A strenuous time of quarrels, contention, strife and anger, with "
"some risk of hurts or injuries depending on the sign it "
"occupies.",
.harmonious = "Can bring benefits from doctors, surgeons, or new projects "
"and enterprises.",
.discordant = "",
},
[PLANET_JUPITER] = {
.base = "Brings increase and expansion - fullness of fortune and health, "
"and a generally fortunate time.",
.harmonious = "",
.discordant = "",
},
[PLANET_SATURN] = {
.base = "Brings depression, stagnation, hindrances and obstacles, and "
"some deprivation of the usual benefits.",
.harmonious = "Favours from older connections and past associations are "
"still possible.",
.discordant = "",
},
[PLANET_URANUS] = {
.base = "Brings separations, estrangements, sudden dislocations and "
"violent upsets.",
.harmonious = "Success through official or civic channels, and "
"beneficial changes or appointments, are possible.",
.discordant = "",
},
[PLANET_NEPTUNE] = {
.base = "Brings a state of chaos and confusion - an involved, uncertain "
"condition of affairs, with plots, subtlety, or unseen "
"influences at work.",
.harmonious = "",
.discordant = "",
},
[PLANET_PLUTO] = {
/* Original text, not from Sepharial - see narrative.h. */
.base = "Brings deep, often hidden transformation - the surfacing or "
"dismantling of something that has outgrown its old form.",
.harmonious = "",
.discordant = "",
},
};
static bool aspect_is_harmonious(AspectType type) {
return type == ASPECT_TRINE || type == ASPECT_SEXTILE;
}
static bool aspect_is_discordant(AspectType type) {
return type == ASPECT_SQUARE || type == ASPECT_OPPOSITION;
}
/* Traditional whole-sign house significations - the "area of life" each
* house governs. This is centuries-old, uncredited astrological
* convention (the same status as the sign/aspect names already baked
* into engine/src/astro.c), not text drawn from Sepharial or any other
* single source. Indexed house - 1. */
static const char *const k_house_area[12] = {
"your sense of self, identity, and outward appearance",
"money, possessions, and personal values",
"communication, siblings, and everyday learning",
"home, family, and your roots",
"romance, creativity, and children",
"daily work, routine, and health",
"partnerships and close relationships",
"shared resources, intimacy, and transformation",
"travel, higher learning, and beliefs",
"career, reputation, and public standing",
"friendships, community, and hopes for the future",
"solitude, the subconscious, and hidden matters",
};
void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
int transiting_house) {
const TransitNarrative *n = &k_narratives[aspect->transiting_planet];
const char *extra = "";
if (aspect_is_harmonious(aspect->type)) extra = n->harmonious;
else if (aspect_is_discordant(aspect->type)) extra = n->discordant;
if (n->base[0] == '\0' && extra[0] == '\0') return;
fprintf(out, "%s", indent);
if (transiting_house >= 1 && transiting_house <= 12) {
fprintf(out, "In matters of %s (house %d). ",
k_house_area[transiting_house - 1], transiting_house);
}
if (n->base[0] != '\0') fprintf(out, "%s", n->base);
if (extra[0] != '\0') fprintf(out, "%s%s", n->base[0] != '\0' ? " " : "", extra);
fprintf(out, "\n");
}
+46
View File
@@ -0,0 +1,46 @@
#ifndef DECK_NARRATIVE_H
#define DECK_NARRATIVE_H
#include <stdio.h>
/* Header-only dependency on the engine, same policy as significance.h -
* see its comment for why. */
#include "../../engine/src/astro.h"
/* Condensed, public-domain descriptions of what each transiting planet
* classically signifies, sourced from Sepharial's "Transits and
* Planetary Periods" (1920, public domain - res/Transits_and_Planetary_
* Periods.pdf), Chapter VIII "Effects of Transits" (pp. 60-61). The
* chapter's own structure - a base description of the planet's transit,
* plus a "well aspected"/"badly aspected" addendum - maps directly onto
* transiting-planet + aspect-type, our existing Aspect fields, so no new
* engine data is needed to use it.
*
* The Moon and Pluto aren't covered by that chapter (fast lunar transits
* were outside a year-ahead-forecasting book's scope; Pluto wasn't
* discovered until 1930, a decade after this book) - their text in
* narrative.c is original, written for this project in a matching
* voice, not drawn from Sepharial.
*
* Prints a narrative sentence for `aspect`'s transiting planet to `out`,
* indented with `indent` and followed by a newline - or prints nothing
* if the source material has no text applicable to this planet/aspect
* combination (e.g. an exactly neutral conjunction to a planet whose
* only text is a "well/badly aspected" addendum, like the Sun). A
* conjunction is treated as neutral - neither the "harmonious" nor the
* "discordant" addendum is shown for it - since Sepharial's own text
* says a conjunction "takes the nature of what it conjoins," something
* this data doesn't model.
*
* `transiting_house` is the whole-sign house (1-12) the transiting
* planet currently occupies in the natal chart - i.e.
* reading->transits.bodies[aspect->transiting_planet].house - and names
* *which area of life* the transit is playing out in, the standard,
* 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`
* used elsewhere - see reading_io.c's parse_bodies()) to omit that
* framing and print the planet's narrative on its own. */
void narrative_print(FILE *out, const char *indent, const Aspect *aspect,
int transiting_house);
#endif
+4 -1
View File
@@ -34,7 +34,10 @@ typedef enum {
DAY_QUIET = 0, DAY_QUIET = 0,
DAY_NOTABLE, DAY_NOTABLE,
DAY_SIGNIFICANT, DAY_SIGNIFICANT,
DAY_MAJOR DAY_MAJOR,
DAY_SIGNIFICANCE_COUNT /* not a real level - the number of levels above,
* for callers that want to show day_level as a
* "rank out of N" (e.g. main.c's report). */
} DaySignificance; } DaySignificance;
typedef struct { typedef struct {
+125
View File
@@ -0,0 +1,125 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "../src/narrative.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 void test_hard_aspect_shows_base_but_not_harmonious_bonus(void) {
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON,
.type = ASPECT_SQUARE, .orb = 1.0 };
FILE *f = tmpfile();
narrative_print(f, " ", &a, 0);
const char *text = slurp(f);
assert(strstr(text, "depression") != NULL);
assert(strstr(text, "past associations") == NULL);
fclose(f);
printf("PASS test_hard_aspect_shows_base_but_not_harmonious_bonus\n");
}
static void test_soft_aspect_adds_harmonious_bonus(void) {
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_SUN,
.type = ASPECT_TRINE, .orb = 1.0 };
FILE *f = tmpfile();
narrative_print(f, " ", &a, 0);
const char *text = slurp(f);
assert(strstr(text, "depression") != NULL);
assert(strstr(text, "past associations") != NULL);
fclose(f);
printf("PASS test_soft_aspect_adds_harmonious_bonus\n");
}
static void test_conjunction_is_neutral_prints_base_only(void) {
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_SUN,
.type = ASPECT_CONJUNCTION, .orb = 1.0 };
FILE *f = tmpfile();
narrative_print(f, " ", &a, 0);
const char *text = slurp(f);
assert(strstr(text, "depression") != NULL);
assert(strstr(text, "past associations") == NULL);
fclose(f);
printf("PASS test_conjunction_is_neutral_prints_base_only\n");
}
/* The Sun has no unconditional base text (see narrative.c) - a neutral
* conjunction to it should print nothing at all. */
static void test_empty_base_and_neutral_aspect_prints_nothing(void) {
Aspect a = { .transiting_planet = PLANET_SUN, .natal_planet = PLANET_MOON,
.type = ASPECT_CONJUNCTION, .orb = 1.0 };
FILE *f = tmpfile();
narrative_print(f, " ", &a, 0);
long len = ftell(f);
assert(len == 0);
fclose(f);
printf("PASS test_empty_base_and_neutral_aspect_prints_nothing\n");
}
static void test_discordant_aspect_on_empty_base_planet(void) {
Aspect a = { .transiting_planet = PLANET_SUN, .natal_planet = PLANET_MOON,
.type = ASPECT_OPPOSITION, .orb = 1.0 };
FILE *f = tmpfile();
narrative_print(f, " ", &a, 0);
const char *text = slurp(f);
assert(strstr(text, "Degradation") != NULL);
fclose(f);
printf("PASS test_discordant_aspect_on_empty_base_planet\n");
}
static void test_known_house_adds_area_framing(void) {
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON,
.type = ASPECT_SQUARE, .orb = 1.0 };
FILE *f = tmpfile();
narrative_print(f, " ", &a, 3);
const char *text = slurp(f);
assert(strstr(text, "house 3") != NULL);
assert(strstr(text, "communication") != NULL);
assert(strstr(text, "depression") != NULL);
fclose(f);
printf("PASS test_known_house_adds_area_framing\n");
}
static void test_unknown_house_omits_area_framing(void) {
Aspect a = { .transiting_planet = PLANET_SATURN, .natal_planet = PLANET_MOON,
.type = ASPECT_SQUARE, .orb = 1.0 };
FILE *f = tmpfile();
narrative_print(f, " ", &a, 0); /* 0 = "no data" sentinel, not a real house */
const char *text = slurp(f);
assert(strstr(text, "house") == NULL);
assert(strstr(text, "depression") != NULL);
fclose(f);
printf("PASS test_unknown_house_omits_area_framing\n");
}
int main(void) {
test_hard_aspect_shows_base_but_not_harmonious_bonus();
test_soft_aspect_adds_harmonious_bonus();
test_conjunction_is_neutral_prints_base_only();
test_empty_base_and_neutral_aspect_prints_nothing();
test_discordant_aspect_on_empty_base_planet();
test_known_house_adds_area_framing();
test_unknown_house_omits_area_framing();
printf("All narrative tests passed.\n");
return 0;
}
Binary file not shown.