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:
@@ -3,6 +3,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "narrative.h"
|
||||
#include "reading_io.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"
|
||||
"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"
|
||||
"aspects driving it.\n\n"
|
||||
"aspects driving it, each with a short narrative note.\n\n"
|
||||
"Typical use:\n"
|
||||
" dist/deck-engine ... --format json | %s\n"
|
||||
" dist/deck-engine ... --format json > reading.json && %s reading.json\n",
|
||||
@@ -115,7 +116,8 @@ int main(int argc, char **argv) {
|
||||
DailyInterpretation 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)) {
|
||||
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));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
@@ -34,7 +34,10 @@ typedef enum {
|
||||
DAY_QUIET = 0,
|
||||
DAY_NOTABLE,
|
||||
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;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user