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:
@@ -231,3 +231,8 @@ const char *astro_moon_phase_name(MoonPhaseName phase) {
|
||||
const char *astro_aspect_name(AspectType type) {
|
||||
return lookup("aspect.", k_aspect_slug[type], k_aspect_names[type]);
|
||||
}
|
||||
|
||||
const char *astro_body_slug(Body body) { return k_body_slug[body]; }
|
||||
const char *astro_sign_slug(ZodiacSign sign) { return k_sign_slug[sign]; }
|
||||
const char *astro_moon_phase_slug(MoonPhaseName phase) { return k_moon_phase_slug[phase]; }
|
||||
const char *astro_aspect_slug(AspectType type) { return k_aspect_slug[type]; }
|
||||
|
||||
@@ -104,4 +104,13 @@ const char *astro_sign_name(ZodiacSign sign);
|
||||
const char *astro_moon_phase_name(MoonPhaseName phase);
|
||||
const char *astro_aspect_name(AspectType type);
|
||||
|
||||
/* Stable, language-independent identifiers (e.g. "sun", "waxing_crescent",
|
||||
* "square") - the same strings i18n.c's lookup keys are built from. Used
|
||||
* for machine-readable output (reading_print_json) that must stay
|
||||
* identical regardless of --lang, unlike astro_*_name() above. */
|
||||
const char *astro_body_slug(Body body);
|
||||
const char *astro_sign_slug(ZodiacSign sign);
|
||||
const char *astro_moon_phase_slug(MoonPhaseName phase);
|
||||
const char *astro_aspect_slug(AspectType type);
|
||||
|
||||
#endif
|
||||
|
||||
+9
-4
@@ -8,13 +8,13 @@
|
||||
#include "reading.h"
|
||||
#include "i18n.h"
|
||||
|
||||
typedef enum { FORMAT_TEXT, FORMAT_HTML } OutputFormat;
|
||||
typedef enum { FORMAT_TEXT, FORMAT_HTML, FORMAT_JSON } OutputFormat;
|
||||
|
||||
static void print_usage(const char *prog) {
|
||||
fprintf(stderr,
|
||||
"Usage: %s --seed <string> --birth-date YYYY-MM-DD --birth-time HH:MM\n"
|
||||
" --birth-utc-offset <hours> --birth-lat <deg> --birth-lon <deg>\n"
|
||||
" [--date YYYY-MM-DDTHH:MM] [--format text|html]\n"
|
||||
" [--date YYYY-MM-DDTHH:MM] [--format text|html|json]\n"
|
||||
" [--lang <code>] [--i18n-dir <path>]\n\n"
|
||||
" --seed Tarot seed string; same seed -> same Celtic Cross spread.\n"
|
||||
" --birth-date/time Birth date/time in local clock time.\n"
|
||||
@@ -23,7 +23,9 @@ static void print_usage(const char *prog) {
|
||||
" --date UTC moment for today's transits/tarot draw. Defaults to now.\n"
|
||||
" --format Output format, defaults to text. html links card art via\n"
|
||||
" img/<file>, i.e. it expects to be saved next to the img/\n"
|
||||
" directory that `make images` copies into dist/.\n"
|
||||
" directory that `make images` copies into dist/. json uses\n"
|
||||
" stable, language-independent identifiers (not --lang's\n"
|
||||
" display text) - it's the interpreter/ module's input format.\n"
|
||||
" --lang Reading language code, defaults to en. Matches a file\n"
|
||||
" named <code>.lang in --i18n-dir (see engine/i18n/).\n"
|
||||
" --i18n-dir Directory to look up <lang>.lang in. Defaults to the\n"
|
||||
@@ -128,8 +130,9 @@ int main(int argc, char **argv) {
|
||||
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 or html\n");
|
||||
fprintf(stderr, "Invalid --format, expected text, html, or json\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -149,6 +152,8 @@ int main(int argc, char **argv) {
|
||||
|
||||
if (format == FORMAT_HTML) {
|
||||
reading_print_html(&reading, stdout);
|
||||
} else if (format == FORMAT_JSON) {
|
||||
reading_print_json(&reading, stdout);
|
||||
} else {
|
||||
reading_print_text(&reading, stdout);
|
||||
}
|
||||
|
||||
@@ -142,3 +142,55 @@ void reading_print_html(const DailyReading *r, FILE *out) {
|
||||
}
|
||||
fprintf(out, "</div>\n</body></html>\n");
|
||||
}
|
||||
|
||||
static void print_body_position_json(FILE *out, const char *indent, Body body,
|
||||
const PlanetPosition *p) {
|
||||
fprintf(out,
|
||||
"%s{\"body\": \"%s\", \"sign\": \"%s\", \"degree_in_sign\": %.4f, "
|
||||
"\"ecliptic_longitude\": %.4f, \"house\": %d}",
|
||||
indent, astro_body_slug(body), astro_sign_slug(p->sign),
|
||||
p->degree_in_sign, p->ecliptic_longitude, p->house);
|
||||
}
|
||||
|
||||
void reading_print_json(const DailyReading *r, FILE *out) {
|
||||
fprintf(out, "{\n");
|
||||
|
||||
fprintf(out, " \"natal\": {\n \"bodies\": [\n");
|
||||
for (int b = 0; b < NUM_BODIES; b++) {
|
||||
print_body_position_json(out, " ", (Body)b, &r->natal.bodies[b]);
|
||||
fprintf(out, "%s\n", b + 1 < NUM_BODIES ? "," : "");
|
||||
}
|
||||
fprintf(out, " ],\n \"ascendant_longitude\": %.4f,\n \"houses\": [",
|
||||
r->natal.ascendant_longitude);
|
||||
for (int h = 0; h < 12; h++) {
|
||||
fprintf(out, "\"%s\"%s", astro_sign_slug(r->natal.houses[h]), h + 1 < 12 ? ", " : "");
|
||||
}
|
||||
fprintf(out, "]\n },\n");
|
||||
|
||||
fprintf(out, " \"transits\": {\n \"bodies\": [\n");
|
||||
for (int b = 0; b < NUM_BODIES; b++) {
|
||||
print_body_position_json(out, " ", (Body)b, &r->transits.bodies[b]);
|
||||
fprintf(out, "%s\n", b + 1 < NUM_BODIES ? "," : "");
|
||||
}
|
||||
fprintf(out, " ],\n \"moon_phase\": \"%s\",\n \"aspects\": [\n",
|
||||
astro_moon_phase_slug(r->transits.moon_phase));
|
||||
for (int i = 0; i < r->transits.aspect_count; i++) {
|
||||
const Aspect *a = &r->transits.aspects[i];
|
||||
fprintf(out,
|
||||
" {\"transiting_planet\": \"%s\", \"natal_planet\": \"%s\", "
|
||||
"\"type\": \"%s\", \"orb\": %.4f}%s\n",
|
||||
astro_body_slug(a->transiting_planet), astro_body_slug(a->natal_planet),
|
||||
astro_aspect_slug(a->type), a->orb,
|
||||
i + 1 < r->transits.aspect_count ? "," : "");
|
||||
}
|
||||
fprintf(out, " ]\n },\n");
|
||||
|
||||
fprintf(out, " \"spread\": {\n \"positions\": [\n");
|
||||
for (int i = 0; i < TAROT_SPREAD_SIZE; i++) {
|
||||
const TarotDraw *draw = &r->spread.positions[i];
|
||||
fprintf(out, " {\"position\": \"%s\", \"card\": \"%s\", \"reversed\": %s}%s\n",
|
||||
tarot_position_slug((CelticCrossPosition)i), tarot_card_slug(draw->card),
|
||||
draw->reversed ? "true" : "false", i + 1 < TAROT_SPREAD_SIZE ? "," : "");
|
||||
}
|
||||
fprintf(out, " ]\n }\n}\n");
|
||||
}
|
||||
|
||||
@@ -23,4 +23,10 @@ void reading_generate(const char *tarot_seed, const BirthData *birth,
|
||||
void reading_print_text(const DailyReading *r, FILE *out);
|
||||
void reading_print_html(const DailyReading *r, FILE *out);
|
||||
|
||||
/* Machine-readable serialization of the full struct, using the
|
||||
* language-independent *_slug() accessors (never astro_*_name()/
|
||||
* tarot_*_name()) so the output is identical regardless of --lang. This
|
||||
* is the interchange format interpreter/ reads - see its json.c. */
|
||||
void reading_print_json(const DailyReading *r, FILE *out);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -70,4 +70,11 @@ const char *tarot_card_meaning(TarotCard card, bool reversed);
|
||||
const char *tarot_position_name(CelticCrossPosition position);
|
||||
const char *tarot_position_description(CelticCrossPosition position);
|
||||
|
||||
/* Stable, language-independent identifiers (e.g. "high_priestess",
|
||||
* "wheel_of_fortune", "recent_past") - the same strings i18n.c's lookup
|
||||
* keys are built from. Used for machine-readable output
|
||||
* (reading_print_json) that must stay identical regardless of --lang. */
|
||||
const char *tarot_card_slug(TarotCard card);
|
||||
const char *tarot_position_slug(CelticCrossPosition position);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -259,3 +259,6 @@ const char *tarot_position_name(CelticCrossPosition position) {
|
||||
const char *tarot_position_description(CelticCrossPosition position) {
|
||||
return position_field(k_position_slug[position], "desc", k_positions[position].description);
|
||||
}
|
||||
|
||||
const char *tarot_card_slug(TarotCard card) { return k_card_slug[card]; }
|
||||
const char *tarot_position_slug(CelticCrossPosition position) { return k_position_slug[position]; }
|
||||
|
||||
Reference in New Issue
Block a user