Report sign/house in significant transits; back up user.properties outside dist/

This commit is contained in:
ml
2026-07-05 17:18:20 +02:00
parent 28558b4343
commit 1136e3257a
9 changed files with 197 additions and 50 deletions
+23 -4
View File
@@ -57,6 +57,22 @@ static const char *aspect_display_name(AspectType type) {
return names[type];
}
static const char *sign_display_name(ZodiacSign sign) {
static const char *const names[12] = {
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
};
return names[sign];
}
/* 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
* is always 1-12, so this is a safe "unknown, say nothing" sentinel. */
static void print_body_in_sign(const PlanetPosition *pos) {
if (pos->house < 1 || pos->house > 12) return;
printf(" in %s (house %d)", sign_display_name(pos->sign), pos->house);
}
static const char *day_level_name(DaySignificance level) {
switch (level) {
case DAY_QUIET: return "Quiet";
@@ -114,10 +130,13 @@ int main(int argc, char **argv) {
interp.top_item_count == 1 ? "" : "s");
for (int i = 0; i < interp.top_item_count; i++) {
const SignificantItem *item = &interp.top_items[i];
printf(" %d. Transiting %s %s natal %s (orb %.1f\xc2\xb0, score %.2f)\n", i + 1,
body_display_name(item->aspect.transiting_planet),
aspect_display_name(item->aspect.type),
body_display_name(item->aspect.natal_planet), item->aspect.orb, item->score);
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);
}
return 0;
}
+44
View File
@@ -17,6 +17,11 @@ static const char *const k_aspect_slug[5] = {
"conjunction", "sextile", "square", "trine", "opposition",
};
static const char *const k_sign_slug[12] = {
"aries", "taurus", "gemini", "cancer", "leo", "virgo",
"libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces",
};
static bool body_from_slug(const char *slug, Body *out) {
for (int i = 0; i < NUM_BODIES; i++) {
if (strcmp(slug, k_body_slug[i]) == 0) {
@@ -27,6 +32,41 @@ static bool body_from_slug(const char *slug, Body *out) {
return false;
}
static bool sign_from_slug(const char *slug, ZodiacSign *out) {
for (int i = 0; i < 12; i++) {
if (strcmp(slug, k_sign_slug[i]) == 0) {
*out = (ZodiacSign)i;
return true;
}
}
return false;
}
/* Fills out[NUM_BODIES] from a "bodies" JSON array, the shape shared by
* both "natal" and "transits" (see reading_print_json in engine/src/
* reading.c). Used only for sign/house context in significant-event
* reporting, not for scoring, so this is deliberately lenient: an entry
* with an unrecognized/missing "body" slug is skipped, and a missing
* "house" leaves that body's house at 0 - not a valid whole-sign house
* number (always 1-12), so callers use house == 0 as "no position data
* available" rather than this failing the whole load. */
static void parse_bodies(const JsonValue *bodies, PlanetPosition out[NUM_BODIES]) {
int count = json_array_count(bodies);
for (int i = 0; i < count; i++) {
const JsonValue *item = json_array_get(bodies, i);
Body body;
if (!body_from_slug(json_as_string(json_object_get(item, "body")), &body)) continue;
ZodiacSign sign;
if (sign_from_slug(json_as_string(json_object_get(item, "sign")), &sign)) {
out[body].sign = sign;
}
out[body].degree_in_sign = json_as_number(json_object_get(item, "degree_in_sign"));
out[body].ecliptic_longitude = json_as_number(json_object_get(item, "ecliptic_longitude"));
out[body].house = (int)json_as_number(json_object_get(item, "house"));
}
}
static bool aspect_type_from_slug(const char *slug, AspectType *out) {
for (int i = 0; i < 5; i++) {
if (strcmp(slug, k_aspect_slug[i]) == 0) {
@@ -78,6 +118,10 @@ bool reading_load_json(const char *text, size_t length, DailyReading *out) {
}
out->transits.aspect_count = filled;
const JsonValue *natal = json_object_get(root, "natal");
parse_bodies(natal ? json_object_get(natal, "bodies") : NULL, out->natal.bodies);
parse_bodies(json_object_get(transits, "bodies"), out->transits.bodies);
json_free(root);
return ok;
}
+9 -5
View File
@@ -10,14 +10,18 @@
/* Parses deck-engine's `--format json` output (text, null-terminated at
* text[length]) and fills *out with just enough of a DailyReading for
* interpret_daily_reading() to work on: out->transits.aspects[]/
* aspect_count. out->natal and out->spread are left zeroed - nothing
* here reads them (see significance.c), so there's no reason to parse
* the rest of the document yet.
* interpret_daily_reading() to work on and for reporting sign/house
* context around each significant event: out->transits.aspects[]/
* aspect_count (used for scoring - see significance.c), and
* out->natal.bodies[]/out->transits.bodies[] (sign + house per body,
* used only for display). out->natal.ascendant_longitude/houses[] and
* out->spread are left zeroed - nothing reads them yet.
*
* 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
* aspects today" reading, not an error). */
* aspects today" reading, not an error). A missing/unrecognized entry
* in "natal"."bodies"/"transits"."bodies" is not an error - see
* parse_bodies() in reading_io.c. */
bool reading_load_json(const char *text, size_t length, DailyReading *out);
#endif
+40 -5
View File
@@ -41,14 +41,16 @@ static void test_json_parse_rejects_malformed(void) {
}
/* A trimmed-but-structurally-faithful fixture matching deck-engine's real
* --format json shape: natal/spread sections are present with decoy
* content the loader must skip over without understanding their schema,
* to prove it navigates by key path rather than assuming any position. */
* --format json shape: "spread" is present with decoy content the loader
* must skip over without understanding its schema, to prove it navigates
* by key path rather than assuming any position. "natal"/"transits"
* bodies are real (if partial) - reading_load_json now parses sign/house
* from them for display, alongside the aspects used for scoring. */
static const char *k_fixture =
"{"
" \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"taurus\"}], \"houses\": []},"
" \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"taurus\", \"house\": 3}], \"houses\": []},"
" \"transits\": {"
" \"bodies\": [{\"body\": \"sun\", \"sign\": \"cancer\"}],"
" \"bodies\": [{\"body\": \"sun\", \"sign\": \"cancer\", \"house\": 5}],"
" \"moon_phase\": \"full\","
" \"aspects\": ["
" {\"transiting_planet\": \"pluto\", \"natal_planet\": \"moon\", \"type\": \"opposition\", \"orb\": 0.2},"
@@ -75,6 +77,37 @@ static void test_reading_load_json_extracts_aspects(void) {
printf("PASS test_reading_load_json_extracts_aspects\n");
}
static void test_reading_load_json_extracts_body_sign_and_house(void) {
DailyReading reading;
bool ok = reading_load_json(k_fixture, strlen(k_fixture), &reading);
assert(ok);
assert(reading.natal.bodies[PLANET_SUN].sign == SIGN_TAURUS);
assert(reading.natal.bodies[PLANET_SUN].house == 3);
assert(reading.transits.bodies[PLANET_SUN].sign == SIGN_CANCER);
assert(reading.transits.bodies[PLANET_SUN].house == 5);
/* Bodies missing from the fixture (e.g. the Moon) get the "no data"
* sentinel rather than a garbage house number. */
assert(reading.natal.bodies[PLANET_MOON].house == 0);
assert(reading.transits.bodies[PLANET_MOON].house == 0);
printf("PASS test_reading_load_json_extracts_body_sign_and_house\n");
}
static void test_reading_load_json_unknown_body_slug_is_skipped_not_fatal(void) {
const char *text =
"{\"natal\": {\"bodies\": [{\"body\": \"xenu\", \"sign\": \"taurus\", \"house\": 3}]},"
" \"transits\": {\"aspects\": []}}";
DailyReading reading;
bool ok = reading_load_json(text, strlen(text), &reading);
assert(ok);
assert(reading.transits.aspect_count == 0);
printf("PASS test_reading_load_json_unknown_body_slug_is_skipped_not_fatal\n");
}
static void test_reading_load_json_empty_aspects_is_valid(void) {
const char *text = "{\"transits\": {\"aspects\": []}}";
DailyReading reading;
@@ -113,6 +146,8 @@ int main(void) {
test_json_parse_basic_shapes();
test_json_parse_rejects_malformed();
test_reading_load_json_extracts_aspects();
test_reading_load_json_extracts_body_sign_and_house();
test_reading_load_json_unknown_body_slug_is_skipped_not_fatal();
test_reading_load_json_empty_aspects_is_valid();
test_reading_load_json_rejects_missing_transits();
test_reading_load_json_rejects_unknown_slug();