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:
ml
2026-07-05 15:55:43 +02:00
parent 57deb62b51
commit bd739ccfdf
25 changed files with 1497 additions and 11 deletions
+123
View File
@@ -0,0 +1,123 @@
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "reading_io.h"
#include "significance.h"
static void print_usage(const char *prog) {
fprintf(stderr,
"Usage: %s [reading.json]\n\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"
"prints a significance report: the day's overall level and the top\n"
"aspects driving it.\n\n"
"Typical use:\n"
" dist/deck-engine ... --format json | %s\n"
" dist/deck-engine ... --format json > reading.json && %s reading.json\n",
prog, prog, prog);
}
static char *read_all(FILE *f, size_t *out_length) {
size_t cap = 4096;
size_t len = 0;
char *buf = malloc(cap);
size_t n;
while ((n = fread(buf + len, 1, cap - len, f)) > 0) {
len += n;
if (len == cap) {
cap *= 2;
buf = realloc(buf, cap);
}
}
buf = realloc(buf, len + 1);
buf[len] = '\0';
*out_length = len;
return buf;
}
/* Display names for the report - deliberately a small local table rather
* than linking engine/src/astro.c's astro_body_name()/astro_aspect_name()
* (which would pull in the vendored Astronomy Engine just for cosmetic
* text). Same policy as reading_io.c's slug tables. */
static const char *body_display_name(Body body) {
static const char *const names[NUM_BODIES] = {
"Sun", "Moon", "Mercury", "Venus", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto",
};
return names[body];
}
static const char *aspect_display_name(AspectType type) {
static const char *const names[5] = {
"Conjunction", "Sextile", "Square", "Trine", "Opposition",
};
return names[type];
}
static const char *day_level_name(DaySignificance level) {
switch (level) {
case DAY_QUIET: return "Quiet";
case DAY_NOTABLE: return "Notable";
case DAY_SIGNIFICANT: return "Significant";
case DAY_MAJOR: return "Major";
}
return "Unknown";
}
int main(int argc, char **argv) {
if (argc > 2 || (argc == 2 && (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0))) {
print_usage(argv[0]);
return argc > 2 ? 1 : 0;
}
FILE *in = stdin;
bool opened_file = false;
if (argc == 2 && strcmp(argv[1], "-") != 0) {
in = fopen(argv[1], "r");
if (!in) {
fprintf(stderr, "error: could not open %s\n", argv[1]);
return 1;
}
opened_file = true;
}
size_t length;
char *text = read_all(in, &length);
if (opened_file) fclose(in);
DailyReading reading;
bool loaded = reading_load_json(text, length, &reading);
free(text);
if (!loaded) {
fprintf(stderr, "error: could not parse input as deck-engine --format json output\n");
return 1;
}
DailyInterpretation interp;
interpret_daily_reading(&reading, &interp);
printf("Day significance: %s\n", day_level_name(interp.day_level));
if (interpretation_deserves_framing(&interp)) {
printf("(a major transit today - worth a deeper Celtic Cross look)\n");
}
printf("\n");
if (interp.top_item_count == 0) {
printf("No notable transits today.\n");
return 0;
}
printf("Top %d significant transit%s:\n", interp.top_item_count,
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);
}
return 0;
}