Add JSON output format and a separate interpreter binary
deck-engine gains --format json (language-independent, slug-based) so a new dist/interpreter-cli can score how significant a day's transits are without linking the engine itself — it depends only on the JSON shape, via its own minimal parser. Also documents the full reading pipeline end to end (CLAUDE.md, README, docs/) and adds docs/reading.md, a non-technical explanation of what a daily reading contains and means.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../src/json.h"
|
||||
#include "../src/reading_io.h"
|
||||
|
||||
static void test_json_parse_basic_shapes(void) {
|
||||
const char *text = "{\"a\": 1, \"b\": [true, false, null, \"text\"], \"c\": {\"nested\": 2.5}}";
|
||||
JsonValue *root = json_parse(text, strlen(text));
|
||||
assert(root && root->type == JSON_OBJECT);
|
||||
|
||||
assert(json_as_number(json_object_get(root, "a")) == 1.0);
|
||||
|
||||
const JsonValue *b = json_object_get(root, "b");
|
||||
assert(json_array_count(b) == 4);
|
||||
assert(json_array_get(b, 0)->type == JSON_BOOL && json_array_get(b, 0)->as.boolean == true);
|
||||
assert(json_array_get(b, 1)->type == JSON_BOOL && json_array_get(b, 1)->as.boolean == false);
|
||||
assert(json_array_get(b, 2)->type == JSON_NULL);
|
||||
assert(strcmp(json_as_string(json_array_get(b, 3)), "text") == 0);
|
||||
|
||||
const JsonValue *c = json_object_get(root, "c");
|
||||
assert(json_as_number(json_object_get(c, "nested")) == 2.5);
|
||||
|
||||
json_free(root);
|
||||
printf("PASS test_json_parse_basic_shapes\n");
|
||||
}
|
||||
|
||||
static void test_json_parse_rejects_malformed(void) {
|
||||
const char *bad_inputs[] = {
|
||||
"{not valid json",
|
||||
"[1, 2,]",
|
||||
"{\"a\": }",
|
||||
"",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(bad_inputs) / sizeof(bad_inputs[0]); i++) {
|
||||
JsonValue *root = json_parse(bad_inputs[i], strlen(bad_inputs[i]));
|
||||
assert(root == NULL);
|
||||
}
|
||||
printf("PASS test_json_parse_rejects_malformed\n");
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
static const char *k_fixture =
|
||||
"{"
|
||||
" \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"taurus\"}], \"houses\": []},"
|
||||
" \"transits\": {"
|
||||
" \"bodies\": [{\"body\": \"sun\", \"sign\": \"cancer\"}],"
|
||||
" \"moon_phase\": \"full\","
|
||||
" \"aspects\": ["
|
||||
" {\"transiting_planet\": \"pluto\", \"natal_planet\": \"moon\", \"type\": \"opposition\", \"orb\": 0.2},"
|
||||
" {\"transiting_planet\": \"saturn\", \"natal_planet\": \"sun\", \"type\": \"square\", \"orb\": 0.5}"
|
||||
" ]"
|
||||
" },"
|
||||
" \"spread\": {\"positions\": [{\"position\": \"present\", \"card\": \"devil\", \"reversed\": false}]}"
|
||||
"}";
|
||||
|
||||
static void test_reading_load_json_extracts_aspects(void) {
|
||||
DailyReading reading;
|
||||
bool ok = reading_load_json(k_fixture, strlen(k_fixture), &reading);
|
||||
|
||||
assert(ok);
|
||||
assert(reading.transits.aspect_count == 2);
|
||||
assert(reading.transits.aspects[0].transiting_planet == PLANET_PLUTO);
|
||||
assert(reading.transits.aspects[0].natal_planet == PLANET_MOON);
|
||||
assert(reading.transits.aspects[0].type == ASPECT_OPPOSITION);
|
||||
assert(reading.transits.aspects[0].orb == 0.2);
|
||||
assert(reading.transits.aspects[1].transiting_planet == PLANET_SATURN);
|
||||
assert(reading.transits.aspects[1].natal_planet == PLANET_SUN);
|
||||
assert(reading.transits.aspects[1].type == ASPECT_SQUARE);
|
||||
|
||||
printf("PASS test_reading_load_json_extracts_aspects\n");
|
||||
}
|
||||
|
||||
static void test_reading_load_json_empty_aspects_is_valid(void) {
|
||||
const char *text = "{\"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_empty_aspects_is_valid\n");
|
||||
}
|
||||
|
||||
static void test_reading_load_json_rejects_missing_transits(void) {
|
||||
const char *text = "{\"natal\": {}, \"spread\": {}}";
|
||||
DailyReading reading;
|
||||
bool ok = reading_load_json(text, strlen(text), &reading);
|
||||
|
||||
assert(!ok);
|
||||
|
||||
printf("PASS test_reading_load_json_rejects_missing_transits\n");
|
||||
}
|
||||
|
||||
static void test_reading_load_json_rejects_unknown_slug(void) {
|
||||
const char *text =
|
||||
"{\"transits\": {\"aspects\": ["
|
||||
" {\"transiting_planet\": \"xenu\", \"natal_planet\": \"moon\", \"type\": \"square\", \"orb\": 1.0}"
|
||||
"]}}";
|
||||
DailyReading reading;
|
||||
bool ok = reading_load_json(text, strlen(text), &reading);
|
||||
|
||||
assert(!ok);
|
||||
|
||||
printf("PASS test_reading_load_json_rejects_unknown_slug\n");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_json_parse_basic_shapes();
|
||||
test_json_parse_rejects_malformed();
|
||||
test_reading_load_json_extracts_aspects();
|
||||
test_reading_load_json_empty_aspects_is_valid();
|
||||
test_reading_load_json_rejects_missing_transits();
|
||||
test_reading_load_json_rejects_unknown_slug();
|
||||
printf("All JSON tests passed.\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "../src/significance.h"
|
||||
|
||||
static Aspect make_aspect(Body transiting, Body natal, AspectType type, double orb) {
|
||||
Aspect a;
|
||||
a.transiting_planet = transiting;
|
||||
a.natal_planet = natal;
|
||||
a.type = type;
|
||||
a.orb = orb;
|
||||
return a;
|
||||
}
|
||||
|
||||
static void test_empty_day_is_quiet(void) {
|
||||
DailyReading reading = {0};
|
||||
DailyInterpretation interp;
|
||||
|
||||
interpret_daily_reading(&reading, &interp);
|
||||
|
||||
assert(interp.top_item_count == 0);
|
||||
assert(interp.day_level == DAY_QUIET);
|
||||
assert(!interpretation_deserves_framing(&interp));
|
||||
|
||||
printf("PASS test_empty_day_is_quiet\n");
|
||||
}
|
||||
|
||||
static void test_exact_outer_planet_to_luminary_is_major(void) {
|
||||
DailyReading reading = {0};
|
||||
reading.transits.aspect_count = 1;
|
||||
reading.transits.aspects[0] = make_aspect(PLANET_PLUTO, PLANET_SUN, ASPECT_SQUARE, 0.1);
|
||||
|
||||
DailyInterpretation interp;
|
||||
interpret_daily_reading(&reading, &interp);
|
||||
|
||||
assert(interp.top_item_count == 1);
|
||||
assert(interp.day_level == DAY_MAJOR);
|
||||
assert(interpretation_deserves_framing(&interp));
|
||||
assert(interp.top_items[0].aspect.transiting_planet == PLANET_PLUTO);
|
||||
|
||||
printf("PASS test_exact_outer_planet_to_luminary_is_major\n");
|
||||
}
|
||||
|
||||
static void test_top_five_selected_and_ranked(void) {
|
||||
/* 7 candidates spanning very tight/major to very loose/minor. The two
|
||||
* weakest (a fast Moon transit and a wide-orb Venus-Mars trine, both
|
||||
* with no luminary/slow-planet weight behind them) should be evicted,
|
||||
* and the strongest (near-exact outer-planet opposition to natal Moon)
|
||||
* should end up first. */
|
||||
DailyReading reading = {0};
|
||||
reading.transits.aspect_count = 7;
|
||||
reading.transits.aspects[0] = make_aspect(PLANET_PLUTO, PLANET_MOON, ASPECT_OPPOSITION, 0.2);
|
||||
reading.transits.aspects[1] = make_aspect(PLANET_SATURN, PLANET_SUN, ASPECT_SQUARE, 0.5);
|
||||
reading.transits.aspects[2] = make_aspect(PLANET_URANUS, PLANET_MERCURY, ASPECT_TRINE, 1.0);
|
||||
reading.transits.aspects[3] = make_aspect(PLANET_JUPITER, PLANET_VENUS, ASPECT_SEXTILE, 2.0);
|
||||
reading.transits.aspects[4] = make_aspect(PLANET_MARS, PLANET_JUPITER, ASPECT_CONJUNCTION, 1.5);
|
||||
reading.transits.aspects[5] = make_aspect(PLANET_MOON, PLANET_MERCURY, ASPECT_SEXTILE, 5.9);
|
||||
reading.transits.aspects[6] = make_aspect(PLANET_VENUS, PLANET_MARS, ASPECT_TRINE, 5.8);
|
||||
|
||||
DailyInterpretation interp;
|
||||
interpret_daily_reading(&reading, &interp);
|
||||
|
||||
assert(interp.top_item_count == 5);
|
||||
|
||||
for (int i = 1; i < interp.top_item_count; i++) {
|
||||
assert(interp.top_items[i - 1].score >= interp.top_items[i].score);
|
||||
}
|
||||
|
||||
for (int i = 0; i < interp.top_item_count; i++) {
|
||||
Body t = interp.top_items[i].aspect.transiting_planet;
|
||||
Body n = interp.top_items[i].aspect.natal_planet;
|
||||
assert(!(t == PLANET_MOON && n == PLANET_MERCURY));
|
||||
assert(!(t == PLANET_VENUS && n == PLANET_MARS));
|
||||
}
|
||||
|
||||
assert(interp.top_items[0].aspect.transiting_planet == PLANET_PLUTO);
|
||||
assert(interp.top_items[0].aspect.natal_planet == PLANET_MOON);
|
||||
|
||||
printf("PASS test_top_five_selected_and_ranked\n");
|
||||
}
|
||||
|
||||
static void test_fewer_than_five_aspects_reports_actual_count(void) {
|
||||
DailyReading reading = {0};
|
||||
reading.transits.aspect_count = 2;
|
||||
reading.transits.aspects[0] = make_aspect(PLANET_VENUS, PLANET_MARS, ASPECT_TRINE, 3.0);
|
||||
reading.transits.aspects[1] = make_aspect(PLANET_MERCURY, PLANET_VENUS, ASPECT_SEXTILE, 4.0);
|
||||
|
||||
DailyInterpretation interp;
|
||||
interpret_daily_reading(&reading, &interp);
|
||||
|
||||
assert(interp.top_item_count == 2);
|
||||
|
||||
printf("PASS test_fewer_than_five_aspects_reports_actual_count\n");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_empty_day_is_quiet();
|
||||
test_exact_outer_planet_to_luminary_is_major();
|
||||
test_top_five_selected_and_ranked();
|
||||
test_fewer_than_five_aspects_reports_actual_count();
|
||||
printf("All significance tests passed.\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user