diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 1d2e28a..8201eee 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -1,7 +1,11 @@ name: build -# Build a versioned Linux CLI release tarball (see `make package`) on -# every push/PR, plus on-demand via the Gitea "Run workflow" button. +# Build a versioned Linux CLI release tarball (see `make package`) and the +# Pebble watchapp (see `make watchapp`) on every push/PR, plus on-demand +# via the Gitea "Run workflow" button. Runs inside build-image (see +# ../../build-image/Dockerfile, built/pushed via ../../build-image.sh and +# ../../upload-image.sh), which bundles the Pebble SDK alongside the C +# toolchain. on: push: pull_request: @@ -17,12 +21,18 @@ jobs: # Must match a label your act_runner is registered with. runs-on: ubuntu-latest + container: + image: cr.ladkau.de/deck-in-a-dash/builder:latest + credentials: + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_PASSWORD }} + steps: - uses: actions/checkout@v4 with: - # `make package`'s version comes from `git describe --tags` (see - # the Makefile's `package` target) - needs full history/tags, - # not actions/checkout's default shallow single-commit clone. + # `make package`/`make watchapp`'s version comes from `git + # describe --tags` - needs full history/tags, not + # actions/checkout's default shallow single-commit clone. fetch-depth: 0 - name: Preflight @@ -30,12 +40,14 @@ jobs: set -euo pipefail command -v cc >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: no C compiler (cc) in PATH" >&2; exit 1; } command -v make >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: make not in PATH" >&2; exit 1; } + command -v pebble >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: pebble not in PATH" >&2; exit 1; } - name: Build and test run: | set -euo pipefail make test make package + make watchapp - name: Upload build artifact # v4 uses the newer @actions/artifact backend, which this Gitea @@ -45,14 +57,21 @@ jobs: name: deck-in-a-dash path: dist/deck-in-a-dash-*-linux-*.tar.gz + - name: Upload watchapp artifact + uses: actions/upload-artifact@v3 + with: + name: deck-in-a-dash-watchapp + path: dist/deck-in-a-dash-*.pbw + - name: Publish to dl.ladkau.de - # Uploads the tarball over SFTP instead of using + # Uploads the tarball and .pbw over SFTP instead of using # actions/upload-artifact (whose zip wrapping can't be disabled). # Only runs on push so PR builds don't publish. if: gitea.event_name == 'push' run: | set -euo pipefail - FILE="$(ls dist/deck-in-a-dash-*-linux-*.tar.gz)" + TARBALL="$(ls dist/deck-in-a-dash-*-linux-*.tar.gz)" + PBW="$(ls dist/deck-in-a-dash-*.pbw)" mkdir -p ~/.ssh echo "${{ secrets.DL_SFTP_KEY }}" > ~/.ssh/dl_sftp_key chmod 600 ~/.ssh/dl_sftp_key @@ -60,5 +79,6 @@ jobs: -o StrictHostKeyChecking=accept-new \ uploader@dl.ladkau.de <transiting_planet), aspect_disp - +lay_name(a->type), - 191 + ui("ui.natal", "natal"), body_display_name(a->natal_ - +planet)); - 192 +} - 193 + - 194 static void print_top_transits_header(FILE *out, int count) { - 195 if (count == 1) { - 196 fprintf(out, "%s\n", ui("interp.top_transits.one", "Top si - gnificant transit:")); - -● Now the text format reordering: - -● Update(interpreter/src/main.c) ⎿  Added 9 lines, removed 5 lines - 223 } - 224 fprintf(out, "\n"); - 225 - 226 + fprintf(out, "%s\n", ui("interp.heading_guidance", "Guidance - +")); - 227 + if (interp->top_item_count > 0) { - 228 + print_top_transit_line(out, &interp->top_items[0].aspect); - 229 + } - 230 + char guidance_buf[GUIDANCE_TEXT_MAX]; - 231 + guidance_write(guidance_buf, sizeof guidance_buf, "", interp - +, &reading->spread); - 232 + fputs(guidance_buf, out); - 233 + fprintf(out, "\n"); - 234 + - 235 if (interp->top_item_count == 0) { - 236 fprintf(out, "%s\n", ui("interp.no_notable_transits", "No - notable transits today.")); - 237 } else { - ... - 258 fprintf(out, "\n"); - 259 - 260 print_celtic_cross_text(out, &reading->spread); - 252 - fprintf(out, "\n"); - 253 - - 254 - char guidance_buf[GUIDANCE_TEXT_MAX]; - 255 - guidance_write(guidance_buf, sizeof guidance_buf, "", interp - -, &reading->spread); - 256 - fputs(guidance_buf, out); - 261 } - 262 - 263 /* ===== --format html ===== */ - -● Now the HTML format: - -● Update(interpreter/src/main.c) - ⎿  Added 19 lines - 293 } - 294 fprintf(out, "

\n"); - 295 - 296 + char *guidance = capture_guidance(interp, &reading->spread); - 297 + fprintf(out, "

%s

\n
\n", ui(" - +interp.heading_guidance", "Guidance")); - 298 + if (interp->top_item_count > 0) { - 299 + const Aspect *top_a = &interp->top_items[0].aspect; - 300 + fprintf(out, "

%s %s %s %s %s

\n", u - +i("ui.transiting", "Transiting"), - 301 + body_display_name(top_a->transiting_planet), aspec - +t_display_name(top_a->type), - 302 + ui("ui.natal", "natal"), body_display_name(top_a-> - +natal_planet)); - 303 + } - 304 + const char *start = guidance; - 305 + for (const char *p = guidance; ; p++) { - 306 + if (*p == '\n' || *p == '\0') { - 307 + if (p > start) fprintf(out, "

%.*s

\n", (int)(p - s - +tart), start); - 308 + if (*p == '\0') break; - 309 + start = p + 1; - 310 + } - 311 + } - 312 + free(guidance); - 313 + fprintf(out, "
\n"); - 314 + - 315 fprintf(out, "

%s

\n", ui("interp.heading_transits", - "Significant Transits")); - 316 if (interp->top_item_count == 0) { - 317 fprintf(out, "

%s

\n", ui("interp.no_notable_transits - ", "No notable transits today.")); - -● Now remove the old guidance block that used to sit at the end of the HTML - output: - - Read 1 file (ctrl+o to expand) - -● Update(interpreter/src/main.c) - ⎿  Removed 13 lines - 352 } - 353 fprintf(out, "\n"); - 354 - 355 - char *guidance = capture_guidance(interp, &reading->spread); - 356 - fprintf(out, "

%s

\n
\n", ui(" - -interp.heading_guidance", "Guidance")); - 357 - const char *start = guidance; - 358 - for (const char *p = guidance; ; p++) { - 359 - if (*p == '\n' || *p == '\0') { - 360 - if (p > start) fprintf(out, "

%.*s

\n", (int)(p - s - -tart), start); - 361 - if (*p == '\0') break; - 362 - start = p + 1; - 363 - } - 364 - } - 365 - free(guidance); - 366 - fprintf(out, "
\n"); - 367 - - 355 fprintf(out, "\n"); - 356 } - 357 - -● Now let's verify the full HTML function reads correctly, then move to the - watch UI file. - - Read 1 file (ctrl+o to expand) - -● This looks correct. Now let's update the watch's on-device report page to - match: - -● Update(watch/src/c/ui_report_window.c) - ⎿  Added 21 lines, removed 4 lines - 144 interp->day_level + 1, DAY_SIGNIFICANCE_COUNT); - 145 y = add_text(width, y, s_day_title, s_font_body); - 146 - 147 - y = add_text(width, y, i18n_get("interp.heading_transits", " - -Significant Transits"), s_font_section); - 148 - if (interp->top_item_count == 0) { - 149 - y = add_text(width, y, i18n_get("interp.no_notable_transit - -s", "No notable transits today."), s_font_body); - 150 - } else { - 147 + /* Build every transit title up front (used both for the rep - +eated - 148 + * top-item line below and the full list further down), sinc - +e - 149 + * snprintf-ing s_transit_titles[0] twice would be redundant - + work for - 150 + * no benefit - the buffer is static/reused either way. */ - 151 + if (interp->top_item_count > 0) { - 152 for (int i = 0; i < interp->top_item_count; i++) { - 153 const Aspect *a = &interp->top_items[i].aspect; - 154 snprintf(s_transit_titles[i], sizeof s_transit_titles[i] - , "%s %s %s %s %s", - 155 i18n_get("ui.transiting", "Transiting"), astro_ - body_name(a->transiting_planet), - 156 astro_aspect_name(a->type), i18n_get("ui.natal" - , "natal"), - 157 astro_body_name(a->natal_planet)); - 158 + } - 159 + } - 160 + - 161 + y = add_text(width, y, i18n_get("interp.heading_guidance", " - +Guidance"), s_font_section); - 162 + if (interp->top_item_count > 0) { - 163 + y = add_text(width, y, s_transit_titles[0], s_font_subhead - +); - 164 + } - 165 + guidance_write(s_guidance, sizeof s_guidance, "", interp, &r - +eading->spread); - 166 + y = add_text(width, y, s_guidance, s_font_body); - 167 + - 168 + y = add_text(width, y, i18n_get("interp.heading_transits", " - +Significant Transits"), s_font_section); - 169 + if (interp->top_item_count == 0) { - 170 + y = add_text(width, y, i18n_get("interp.no_notable_transit - +s", "No notable transits today."), s_font_body); - 171 + } else { - 172 + for (int i = 0; i < interp->top_item_count; i++) { - 173 + const Aspect *a = &interp->top_items[i].aspect; - 174 y = add_text(width, y, s_transit_titles[i], s_font_subhe - ad); - 175 - 176 narrative_write(s_narratives[i], sizeof s_narratives[i], - "", a, - -● Now remove the old guidance block at the bottom of build_content: - - Read 1 file (ctrl+o to expand) - -● Update(watch/src/c/ui_report_window.c) - ⎿  Removed 4 lines - 201 y = add_text(width, y, tarot_card_meaning(draw->card, draw - ->reversed), s_font_body); - 202 } - 203 - 204 - y = add_text(width, y, i18n_get("interp.heading_guidance", " - -Guidance"), s_font_section); - 205 - guidance_write(s_guidance, sizeof s_guidance, "", interp, &r - -eading->spread); - 206 - y = add_text(width, y, s_guidance, s_font_body); - 207 - - 204 scroll_layer_set_content_size(s_scroll_layer, GSize(width, y - )); - 205 } - 206 - -● Let me view the full function to verify correctness: - - Read 1 file (ctrl+o to expand) - -● This looks correct. Now let's build both the desktop interpreter-cli and the - watch app to verify everything compiles and behaves as expected. - - Ran 1 shell command (ctrl+o to expand) - -● Build succeeds cleanly. Let's run the CLI end-to-end and confirm the new - ordering, in both English and German: - - Ran 1 shell command (ctrl+o to expand) - -● Output order is exactly as requested: "Bedeutung des Tages" → "Rat für heute" - (with the top transit repeated right above the guidance text) → "Wichtige - Transits" → "Keltisches Kreuz". Let's also spot-check the HTML output and run - the interpreter test suite. - - Ran 2 shell commands (ctrl+o to expand) - -✻ Crunched for 5m 27s - -❯ Transit Saturn Quadrat natal Mond should be the full line: Transit Saturn in - Widder (Haus 2) Quadrat natal Mond in Steinbock (Haus 11) (Orbis 0.5°, - Punktzahl 8.41) - In Fragen von Geld, Besitz und persönlicher Werte (Haus 2). Bringt - Niedergeschlagenheit, Stillstand, Hindernisse und Erschwernisse sowie eine - gewisse Einbuße der gewohnten Vorteile. - - Read 1 file (ctrl+o to expand) - -● Update(interpreter/src/main.c) - ⎿  Added 24 lines, removed 10 lines - 179 return buf; - 180 } - 181 - 182 -/* Short "Transiting X Aspect natal Y" restatement of the day' - -s single - 183 - * most significant transit (no sign/house/orb/score - that de - -tail is - 184 - * still shown in full in the "Significant Transits" section f - -urther - 185 - * down), printed just above the guidance paragraph so the rea - -der has - 186 - * that context even though guidance.c's own intro sentence on - -ly names - 187 - * the planet, not the full aspect. */ - 188 -static void print_top_transit_line(FILE *out, const Aspect *a) - - { - 189 - fprintf(out, "%s %s %s %s %s\n", ui("ui.transiting", "Transi - -ting"), - 190 - body_display_name(a->transiting_planet), aspect_disp - -lay_name(a->type), - 191 - ui("ui.natal", "natal"), body_display_name(a->natal_ - -planet)); - 182 +/* Prints one significant-transit item's full detail (title li - +ne with - 183 + * sign/house/orb/score, plus its narrative sentence) - shared - + between - 184 + * the "Significant Transits" list and the day's top item, whi - +ch is - 185 + * repeated in full just above the guidance paragraph (see - 186 + * interp_print_text) so the reader has that same detail in vi - +ew right - 187 + * next to the guidance text it informs. `index` is 1-based an - +d prefixes - 188 + * the line with "N. "; pass 0 to omit it, for the repeated to - +p item, - 189 + * which isn't part of the numbered list. */ - 190 +static void print_transit_item_text(FILE *out, const DailyRead - +ing *reading, - 191 + const SignificantItem *it - +em, int index) { - 192 + const Aspect *a = &item->aspect; - 193 + if (index > 0) fprintf(out, " %d. ", index); - 194 + fprintf(out, "%s %s", ui("ui.transiting", "Transiting"), bod - +y_display_name(a->transiting_planet)); - 195 + print_body_in_sign_text(out, &reading->transits.bodies[a->tr - +ansiting_planet]); - 196 + fprintf(out, " %s %s %s", aspect_display_name(a->type), ui(" - +ui.natal", "natal"), - 197 + body_display_name(a->natal_planet)); - 198 + print_body_in_sign_text(out, &reading->natal.bodies[a->natal - +_planet]); - 199 + fprintf(out, " (%s %.1f\xc2\xb0, %s %.2f)\n", ui("ui.orb", " - +orb"), a->orb, - 200 + ui("interp.score", "score"), item->score); - 201 + - 202 + char narrative_buf[NARRATIVE_TEXT_MAX]; - 203 + narrative_write(narrative_buf, sizeof narrative_buf, " " - +, a, - 204 + reading->transits.bodies[a->transiting_plan - +et].house); - 205 + fputs(narrative_buf, out); - 206 } - 207 - 208 static void print_top_transits_header(FILE *out, int count) { - -● Update(interpreter/src/main.c) - ⎿  Added 2 lines, removed 17 lines - 239 - 240 fprintf(out, "%s\n", ui("interp.heading_guidance", "Guidance - ")); - 241 if (interp->top_item_count > 0) { - 242 - print_top_transit_line(out, &interp->top_items[0].aspect); - 242 + print_transit_item_text(out, reading, &interp->top_items[0 - +], 0); - 243 } - 244 char guidance_buf[GUIDANCE_TEXT_MAX]; - 245 guidance_write(guidance_buf, sizeof guidance_buf, "", interp - , &reading->spread); - ... - 251 } else { - 252 print_top_transits_header(out, interp->top_item_count); - 253 for (int i = 0; i < interp->top_item_count; i++) { - 254 - const SignificantItem *item = &interp->top_items[i]; - 255 - const Aspect *a = &item->aspect; - 256 - - 257 - fprintf(out, " %d. %s %s", i + 1, ui("ui.transiting", " - -Transiting"), - 258 - body_display_name(a->transiting_planet)); - 259 - print_body_in_sign_text(out, &reading->transits.bodies[a - -->transiting_planet]); - 260 - fprintf(out, " %s %s %s", aspect_display_name(a->type), - -ui("ui.natal", "natal"), - 261 - body_display_name(a->natal_planet)); - 262 - print_body_in_sign_text(out, &reading->natal.bodies[a->n - -atal_planet]); - 263 - fprintf(out, " (%s %.1f\xc2\xb0, %s %.2f)\n", ui("ui.orb - -", "orb"), a->orb, - 264 - ui("interp.score", "score"), item->score); - 265 - - 266 - char narrative_buf[NARRATIVE_TEXT_MAX]; - 267 - narrative_write(narrative_buf, sizeof narrative_buf, " - - ", a, - 268 - reading->transits.bodies[a->transiting_ - -planet].house); - 269 - fputs(narrative_buf, out); - 254 + print_transit_item_text(out, reading, &interp->top_items - +[i], i + 1); - 255 } - 256 } - 257 fprintf(out, "\n"); - - Read 1 file (ctrl+o to expand) - -● Update(interpreter/src/main.c) - ⎿  Added 20 lines - 261 - 262 /* ===== --format html ===== */ - 263 - 264 +/* HTML counterpart of print_transit_item_text() - same shared - +-between- - 265 + * the-repeated-top-item-and-the-full-list purpose, one table - +row (title - 266 + * + orb/score) plus a narrative row if non-empty. `index` is - +1-based; - 267 + * pass 0 to omit the "N." cell, for the repeated top item. */ - 268 +static void print_transit_item_html(FILE *out, const DailyRead - +ing *reading, - 269 + const SignificantItem *it - +em, int index) { - 270 + const Aspect *a = &item->aspect; - 271 + char index_buf[8] = ""; - 272 + if (index > 0) snprintf(index_buf, sizeof index_buf, "%d.", - +index); - 273 + char *text = capture_narrative(a, reading->transits.bodies[a - +->transiting_planet].house); - 274 + - 275 + fprintf(out, "%s%s %s %s %s %s" - 276 + "%s %.1f°, %s %.2f\n", - 277 + index_buf, ui("ui.transiting", "Transiting"), body_d - +isplay_name(a->transiting_planet), - 278 + aspect_display_name(a->type), ui("ui.natal", "natal" - +), body_display_name(a->natal_planet), - 279 + ui("ui.orb", "orb"), a->orb, ui("interp.score", "sco - +re"), item->score); - 280 + if (text[0] != '\0') fprintf(out, "%s\n", text); - 281 + free(text); - 282 +} - 283 + - 284 static void interp_print_html(FILE *out, const DailyReading *r - eading, const DailyInterpretation *interp) { - 285 fprintf(out, - 286 "\n\n" - -● Now update the guidance block and the transits-list loop in interp_print_html - to use this helper: - Read 1 file (ctrl+o to expand) - -● Update(interpreter/src/main.c) - ⎿  Added 3 lines, removed 4 lines - 315 char *guidance = capture_guidance(interp, &reading->spread); - 316 fprintf(out, "

%s

\n
\n", ui(" - interp.heading_guidance", "Guidance")); - 317 if (interp->top_item_count > 0) { - 318 - const Aspect *top_a = &interp->top_items[0].aspect; - 319 - fprintf(out, "

%s %s %s %s %s

\n", u - -i("ui.transiting", "Transiting"), - 320 - body_display_name(top_a->transiting_planet), aspec - -t_display_name(top_a->type), - 321 - ui("ui.natal", "natal"), body_display_name(top_a-> - -natal_planet)); - 318 + fprintf(out, "\n"); - 319 + print_transit_item_html(out, reading, &interp->top_items[0 - +], 0); - 320 + fprintf(out, "
\n"); - 321 } - 322 const char *start = guidance; - 323 for (const char *p = guidance; ; p++) { - -● Update(interpreter/src/main.c) - ⎿  Added 1 line, removed 12 lines - 336 } else { - 337 fprintf(out, "\n"); - 338 for (int i = 0; i < interp->top_item_count; i++) { - 339 - const SignificantItem *item = &interp->top_items[i]; - 340 - const Aspect *a = &item->aspect; - 341 - char *text = capture_narrative(a, reading->transits.bodi - -es[a->transiting_planet].house); - 342 - - 343 - fprintf(out, "" - 344 - "\n", - 345 - i + 1, ui("ui.transiting", "Transiting"), body_d - -isplay_name(a->transiting_planet), - 346 - aspect_display_name(a->type), ui("ui.natal", "na - -tal"), - 347 - body_display_name(a->natal_planet), ui("ui.orb", - - "orb"), a->orb, - 348 - ui("interp.score", "score"), item->score); - 349 - if (text[0] != '\0') fprintf(out, "\n", text); - 350 - free(text); - 339 + print_transit_item_html(out, reading, &interp->top_items - +[i], i + 1); - 340 } - 341 fprintf(out, "
%d.%s %s %s %s %s%s %.1f°, %s %.2f
%s
\n"); - 342 } - - Read 1 file (ctrl+o to expand) - -● Update(watch/src/c/ui_report_window.c) - ⎿  Added 6 lines, removed 8 lines - 144 interp->day_level + 1, DAY_SIGNIFICANCE_COUNT); - 145 y = add_text(width, y, s_day_title, s_font_body); - 146 - 147 - /* Build every transit title up front (used both for the rep - -eated - 148 - * top-item line below and the full list further down), sinc - -e - 149 - * snprintf-ing s_transit_titles[0] twice would be redundant - - work for - 150 - * no benefit - the buffer is static/reused either way. */ - 147 + /* Build every transit's title + narrative up front (used bo - +th for the - 148 + * repeated top-item line below and the full list further do - +wn), so - 149 + * neither buffer is computed twice for item 0. */ - 150 if (interp->top_item_count > 0) { - 151 for (int i = 0; i < interp->top_item_count; i++) { - 152 const Aspect *a = &interp->top_items[i].aspect; - ... - 154 i18n_get("ui.transiting", "Transiting"), astro_ - body_name(a->transiting_planet), - 155 astro_aspect_name(a->type), i18n_get("ui.natal" - , "natal"), - 156 astro_body_name(a->natal_planet)); - 157 + narrative_write(s_narratives[i], sizeof s_narratives[i], - + "", a, - 158 + reading->transits.bodies[a->transiting_ - +planet].house); - 159 } - 160 } - 161 - 162 y = add_text(width, y, i18n_get("interp.heading_guidance", " - Guidance"), s_font_section); - 163 if (interp->top_item_count > 0) { - 164 y = add_text(width, y, s_transit_titles[0], s_font_subhead - ); - 165 + y = add_text(width, y, s_narratives[0], s_font_body); - 166 } - 167 guidance_write(s_guidance, sizeof s_guidance, "", interp, &r - eading->spread); - 168 y = add_text(width, y, s_guidance, s_font_body); - ... - 172 y = add_text(width, y, i18n_get("interp.no_notable_transit - s", "No notable transits today."), s_font_body); - 173 } else { - 174 for (int i = 0; i < interp->top_item_count; i++) { - 173 - const Aspect *a = &interp->top_items[i].aspect; - 175 y = add_text(width, y, s_transit_titles[i], s_font_subhe - ad); - 175 - - 176 - narrative_write(s_narratives[i], sizeof s_narratives[i], - - "", a, - 177 - reading->transits.bodies[a->transiting_ - -planet].house); - 176 y = add_text(width, y, s_narratives[i], s_font_body); - 177 } - 178 } - -● Now let's rebuild and verify both the desktop CLI output and the watch build - compile cleanly. - - Ran 2 shell commands (ctrl+o to expand) - -● This is a real -Wformat-truncation warning even though the actual index is - always small (≤5). Let's fix it cleanly to keep the build warning-free. - - Read 1 file (ctrl+o to expand) - -● index here is always MAX_SIGNIFICANT_ITEMS (5) at most, so char index_buf[8] - is safe in practice, but GCC can't prove it since the parameter is a plain - int. Simplest fix: size the buffer to fit any int. - -● Update(interpreter/src/main.c) - ⎿  Added 1 line, removed 1 line - 268 static void print_transit_item_html(FILE *out, const DailyRead - ing *reading, - 269 const SignificantItem *it - em, int index) { - 270 const Aspect *a = &item->aspect; - 271 - char index_buf[8] = ""; - 271 + char index_buf[16] = ""; - 272 if (index > 0) snprintf(index_buf, sizeof index_buf, "%d.", - index); - 273 char *text = capture_narrative(a, reading->transits.bodies[a - ->transiting_planet].house); - 274 - - Ran 1 shell command (ctrl+o to expand) - -● Clean. Now a full rebuild and end-to-end check: - - Ran 1 shell command (ctrl+o to expand) - -● Those two warnings are pre-existing, from the vendored (unmodified) - third-party astronomy library — unrelated to this change. - - Ran 1 shell command (ctrl+o to expand) - -● Matches the requested output exactly. Let's also check HTML output and run the - test suite. - - Ran 1 shell command (ctrl+o to expand) - -● HTML order and content also match. Now the test suite and a full - multi-platform watch build: - - Ran 1 shell command (ctrl+o to expand) - -● All tests pass (main.c isn't unit-tested directly, but these cover the - underlying significance/narrative/guidance logic it calls). Now let's build - the watch app to confirm ui_report_window.c compiles cleanly across all - platforms: - - Ran 3 shell commands (ctrl+o to expand) - -● Watch build succeeds cleanly across all six platforms with the new - ui_report_window.c. Both the desktop interpreter-cli (text and HTML) and the - on-watch report page now show: - - 1. Bedeutung des Tages (day significance) - 2. Rat für heute (guidance), with the full most-important-transit line - (sign/house/orb/score + narrative) repeated right above the guidance text - 3. Wichtige Transits (full ranked list, unchanged) - 4. Keltisches Kreuz - - The watch app is ready to install if you want to test it on hardware. - -✻ Sautéed for 4m 59s - -※ recap: Working on the Deck in a Dash Pebble app: I moved "Rat für heute" guidance right after the day significance section and repeated the top transit's full detail before it, across CLI and watch UI. Next: install and verify on the watch itself. (disable recaps in /config) - -❯ for the html and the watch display the attitude card and outcome should be - displayed next to each other right after guidance. - - Searched for 4 patterns, read 2 files, ran 2 shell commands (ctrl+o to expand) - -● Now let's look at the full spread rendering block and the #include list to - plan the shared-helper refactor: - Read 1 file (ctrl+o to expand) - -● Update(interpreter/src/main.c) - ⎿  Added 18 lines - 281 free(text); - 282 } - 283 - 284 +/* One Celtic Cross card as a ".card" div - shared between the - 285 + * Attitude/Outcome preview (right after guidance) and the ful - +l spread - 286 + * further down, so both render identically. */ - 287 +static void print_card_html(FILE *out, CelticCrossPosition pos - +ition, const TarotDraw *draw) { - 288 + fprintf(out, - 289 + "
\n" - 290 + "
%s
\n" - 291 + " \"%s\"\n" - 292 + "
%s%s
\n" - 293 + "
%s
\n" - 294 + "
%s
\n" - 295 + "
\n", - 296 + tarot_position_name(position), draw->reversed ? "reversed" - + : "", - 297 + tarot_card_image_file(draw->card), tarot_card_name(draw->c - +ard), tarot_card_name(draw->card), - 298 + reversed_marker(draw->reversed), tarot_position_descriptio - +n(position), - 299 + tarot_card_meaning(draw->card, draw->reversed)); - 300 +} - 301 + - 302 static void interp_print_html(FILE *out, const DailyReading *r - eading, const DailyInterpretation *interp) { - 303 fprintf(out, - 304 "\n\n" - -● Now insert the Attitude/Outcome preview right after guidance, and simplify the - main spread loop to reuse the helper: - -● Update(interpreter/src/main.c) - ⎿  Added 5 lines - 348 free(guidance); - 349 fprintf(out, "
\n"); - 350 - 351 + fprintf(out, "
\n"); - 352 + print_card_html(out, POSITION_ATTITUDE, &reading->spread.pos - +itions[POSITION_ATTITUDE]); - 353 + print_card_html(out, POSITION_OUTCOME, &reading->spread.posi - +tions[POSITION_OUTCOME]); - 354 + fprintf(out, "
\n"); - 355 + - 356 fprintf(out, "

%s

\n", ui("interp.heading_transits", - "Significant Transits")); - 357 if (interp->top_item_count == 0) { - 358 fprintf(out, "

%s

\n", ui("interp.no_notable_transits - ", "No notable transits today.")); - -● Update(interpreter/src/main.c) - ⎿  Added 1 line, removed 13 lines - 366 - 367 fprintf(out, "

%s

\n
\n", ui("ui - .celtic_cross", "Celtic Cross")); - 368 for (int i = 0; i < TAROT_SPREAD_SIZE; i++) { - 369 - const TarotDraw *draw = &reading->spread.positions[i]; - 370 - fprintf(out, - 371 - "
\n" - 372 - "
%s
\n" - 373 - " \"%s\"\n" - 374 - "
%s%s
\n" - 375 - "
%s
\n" - 376 - "
%s
\n" - 377 - "
\n", - 378 - tarot_position_name((CelticCrossPosition)i), draw->rever - -sed ? "reversed" : "", - 379 - tarot_card_image_file(draw->card), tarot_card_name(draw- - ->card), tarot_card_name(draw->card), - 380 - reversed_marker(draw->reversed), tarot_position_descript - -ion((CelticCrossPosition)i), - 381 - tarot_card_meaning(draw->card, draw->reversed)); - 369 + print_card_html(out, (CelticCrossPosition)i, &reading->spr - +ead.positions[i]); - 370 } - 371 fprintf(out, "
\n"); - 372 - - Ran 1 shell command (ctrl+o to expand) - -● No warnings (the two pre-existing vendored-library warnings didn't reappear - since object files were cached — let's verify with a clean build too): - - Ran 2 shell commands (ctrl+o to expand) - -● It's there — my earlier sed filter just happened to cut it out. Let's view the - actual content directly: - - Read 1 file (ctrl+o to expand) - -● Confirmed correct: Attitude and Outcome cards render side by side, right after - guidance, before "Wichtige Transits". Let's verify visually in a browser-like - view and check the Outcome card too: - - Read 1 file (ctrl+o to expand) - -● Now let's implement the same for the watch. I'll add a second image-slot pool - for the Attitude/Outcome preview row and a width-aware text helper for two - side-by-side columns. - - Read 1 file (ctrl+o to expand) - -● Update(watch/src/c/ui_report_window.c) - ⎿  Added 5 lines - 37 static char s_position_titles[TAROT_SPREAD_SIZE][96]; - 38 static char s_guidance[GUIDANCE_TEXT_MAX]; - 39 - 40 +/* Attitude/Outcome preview row, shown side by side right after - + guidance - 41 + * (same two cards reappear in the full spread further down - s - +ee - 42 + * REPEAT_SLOT_COUNT's own comment). */ - 43 +static char s_repeat_titles[2][96]; - 44 + - 45 /* Loads/frees each position's GBitmap as scroll_offset_changed - () sees it - 46 * enter/leave the visible viewport, rather than decoding all t - en Celtic - 47 * Cross images up front - a defensive habit left over from an - earlier, - -● Update(watch/src/c/ui_report_window.c) - ⎿  Added 8 lines - 58 - 59 static ImageSlot s_slots[TAROT_SPREAD_SIZE]; - 60 - 61 +/* Attitude (0) and Outcome (1) preview slots, drawn at half sc - +reen - 62 + * width each so the two sit side by side - the BitmapLayer's o - +wn bounds - 63 + * (narrower than IMAGE_WIDTH on 144px-wide platforms) clip the - + centered - 64 + * image automatically, same as GAlignCenter already does for t - +he - 65 + * full-width slots above, no separate scaling logic needed. */ - 66 +#define REPEAT_SLOT_COUNT 2 - 67 +static ImageSlot s_repeat_slots[REPEAT_SLOT_COUNT]; - 68 + - 69 #define MAX_TEXT_LAYERS 50 - 70 static TextLayer *s_text_layers[MAX_TEXT_LAYERS]; - 71 static int s_text_layer_count; - -● Now update add_text to support an x-offset variant, and update - teardown_content/update_visible_images to include the repeat slots: - -● Update(watch/src/c/ui_report_window.c) - ⎿  Added 6 lines, removed 2 lines - 88 return reversed ? i18n_get("ui.reversed", "(Reversed)") : ""; - 89 } - 90 - 91 -static int16_t add_text(int16_t width, int16_t y, const char *t - -ext, GFont font) { - 92 - TextLayer *tl = text_layer_create(GRect(0, y, width, 4000)); - 91 +static int16_t add_text_at(int16_t x, int16_t width, int16_t y, - + const char *text, GFont font) { - 92 + TextLayer *tl = text_layer_create(GRect(x, y, width, 4000)); - 93 text_layer_set_font(tl, font); - 94 text_layer_set_text(tl, text); - 95 text_layer_set_overflow_mode(tl, GTextOverflowModeWordWrap); - ... - 104 return y + size.h + MARGIN; - 105 } - 106 - 107 +static int16_t add_text(int16_t width, int16_t y, const char * - +text, GFont font) { - 108 + return add_text_at(0, width, y, text, font); - 109 +} - 110 + - 111 static void teardown_content(void) { - 112 for (int i = 0; i < s_text_layer_count; i++) { - 113 text_layer_destroy(s_text_layers[i]); - -❯ hmm on the html the images are now below the guidance text they should be - above so between the heading and the guidance text - - Read 1 file (ctrl+o to expand) - -● Update(interpreter/src/main.c) - ⎿  Added 8 lines, removed 1 line - 331 fprintf(out, "

\n"); - 332 - 333 char *guidance = capture_guidance(interp, &reading->spread); - 334 - fprintf(out, "

%s

\n
\n", ui(" - -interp.heading_guidance", "Guidance")); - 334 + fprintf(out, "

%s

\n", ui("interp.heading_guidance", - +"Guidance")); - 335 + - 336 + fprintf(out, "
\n"); - 337 + print_card_html(out, POSITION_ATTITUDE, &reading->spread.pos - +itions[POSITION_ATTITUDE]); - 338 + print_card_html(out, POSITION_OUTCOME, &reading->spread.posi - +tions[POSITION_OUTCOME]); - 339 + fprintf(out, "
\n"); - 340 + - 341 + fprintf(out, "
\n"); - 342 if (interp->top_item_count > 0) { - 343 fprintf(out, "\n"); - 344 print_transit_item_html(out, reading, &interp->top_items[0 - ], 0); - -● Now remove the old attitude-outcome block that used to sit after the guidance - : - - Read 1 file (ctrl+o to expand) - -● Update(interpreter/src/main.c) - ⎿  Removed 5 lines - 355 free(guidance); - 356 fprintf(out, "\n"); - 357 - 358 - fprintf(out, "
\n"); - 359 - print_card_html(out, POSITION_ATTITUDE, &reading->spread.pos - -itions[POSITION_ATTITUDE]); - 360 - print_card_html(out, POSITION_OUTCOME, &reading->spread.posi - -tions[POSITION_OUTCOME]); - 361 - fprintf(out, "
\n"); - 362 - - 358 fprintf(out, "

%s

\n", ui("interp.heading_transits", - "Significant Transits")); - 359 if (interp->top_item_count == 0) { - 360 fprintf(out, "

%s

\n", ui("interp.no_notable_transits - ", "No notable transits today.")); - -● Let's verify the final HTML structure and rebuild: - - Read 1 file (ctrl+o to expand) -● This matches: heading → attitude/outcome images → guidance text (top-transit - repeat + paragraphs). Let's rebuild and verify. - - Ran 2 shell commands (ctrl+o to expand) - -● Order confirmed: heading (line 20) → attitude/outcome images (21) → guidance - div with top-transit-repeat + text (37) → transits list (46). Now let's finish - the watch-side implementation, keeping the same heading → images → - guidance-text order there too. - - Read 1 file (ctrl+o to expand) - -● Now let's update teardown_content and update_visible_images to also handle - s_repeat_slots: - -● Update(watch/src/c/ui_report_window.c) - ⎿  Added 11 lines - 124 s_slots[i].bitmap_layer = NULL; - 125 } - 126 } - 127 + - 128 + for (int i = 0; i < REPEAT_SLOT_COUNT; i++) { - 129 + if (s_repeat_slots[i].bitmap) { - 130 + gbitmap_destroy(s_repeat_slots[i].bitmap); - 131 + s_repeat_slots[i].bitmap = NULL; - 132 + } - 133 + if (s_repeat_slots[i].bitmap_layer) { - 134 + bitmap_layer_destroy(s_repeat_slots[i].bitmap_layer); - 135 + s_repeat_slots[i].bitmap_layer = NULL; - 136 + } - 137 + } - 138 } - 139 - 140 static void update_visible_images(void) { - -● Update(watch/src/c/ui_report_window.c) - ⎿  Added 17 lines, removed 11 lines - 137 } - 138 } - 139 - 140 +static void update_slot_visibility(ImageSlot *slot, int16_t vi - +sible_top, int16_t visible_bottom) { - 141 + bool visible = slot->y < visible_bottom && slot->y + slot->h - +eight > visible_top; - 142 + - 143 + if (visible && !slot->bitmap) { - 144 + slot->bitmap = gbitmap_create_with_resource(card_resource_ - +id(slot->card)); - 145 + bitmap_layer_set_bitmap(slot->bitmap_layer, slot->bitmap); - 146 + } else if (!visible && slot->bitmap) { - 147 + bitmap_layer_set_bitmap(slot->bitmap_layer, NULL); - 148 + gbitmap_destroy(slot->bitmap); - 149 + slot->bitmap = NULL; - 150 + } - 151 +} - 152 + - 153 static void update_visible_images(void) { - 154 GPoint offset = scroll_layer_get_content_offset(s_scroll_lay - er); - 155 int16_t visible_top = -offset.y; - 156 int16_t visible_bottom = visible_top + s_screen_height; - 157 - 158 for (int i = 0; i < TAROT_SPREAD_SIZE; i++) { - 146 - ImageSlot *slot = &s_slots[i]; - 147 - bool visible = slot->y < visible_bottom && slot->y + slot- - ->height > visible_top; - 148 - - 149 - if (visible && !slot->bitmap) { - 150 - slot->bitmap = gbitmap_create_with_resource(card_resourc - -e_id(slot->card)); - 151 - bitmap_layer_set_bitmap(slot->bitmap_layer, slot->bitmap - -); - 152 - } else if (!visible && slot->bitmap) { - 153 - bitmap_layer_set_bitmap(slot->bitmap_layer, NULL); - 154 - gbitmap_destroy(slot->bitmap); - 155 - slot->bitmap = NULL; - 156 - } - 159 + update_slot_visibility(&s_slots[i], visible_top, visible_b - +ottom); - 160 } - 161 + for (int i = 0; i < REPEAT_SLOT_COUNT; i++) { - 162 + update_slot_visibility(&s_repeat_slots[i], visible_top, vi - +sible_bottom); - 163 + } - 164 } - 165 - 166 static void scroll_offset_changed(ScrollLayer *scroll_layer, v - oid *context) { - -● Now let's update build_content to place the two-card preview between the - "Guidance" heading and the guidance text (matching the corrected HTML order): - - Read 1 file (ctrl+o to expand) - -● Update(watch/src/c/ui_report_window.c) - ⎿  Added 42 lines - 194 } - 195 - 196 y = add_text(width, y, i18n_get("interp.heading_guidance", " - Guidance"), s_font_section); - 197 + - 198 + /* Attitude/Outcome preview, side by side - the same two car - +ds guidance - 199 + * text below names and quotes, shown here so the reader see - +s them - 200 + * before reading why they matter. Reappear again, with thei - +r full - 201 + * description/meaning, in the Celtic Cross walkthrough furt - +her down. */ - 202 + { - 203 + int16_t half_w = width / 2; - 204 + const TarotDraw *attitude = &reading->spread.positions[POS - +ITION_ATTITUDE]; - 205 + const TarotDraw *outcome = &reading->spread.positions[POSI - +TION_OUTCOME]; - 206 + - 207 + snprintf(s_repeat_titles[0], sizeof s_repeat_titles[0], "% - +s: %s%s", - 208 + tarot_position_name(POSITION_ATTITUDE), tarot_car - +d_name(attitude->card), - 209 + reversed_marker(attitude->reversed)); - 210 + snprintf(s_repeat_titles[1], sizeof s_repeat_titles[1], "% - +s: %s%s", - 211 + tarot_position_name(POSITION_OUTCOME), tarot_card - +_name(outcome->card), - 212 + reversed_marker(outcome->reversed)); - 213 + - 214 + int16_t y_left = add_text_at(0, half_w, y, s_repeat_titles - +[0], s_font_subhead); - 215 + int16_t y_right = add_text_at(half_w, width - half_w, y, s - +_repeat_titles[1], s_font_subhead); - 216 + - 217 + int16_t img_height = card_image_height_for_width(IMAGE_WID - +TH); - 218 + - 219 + s_repeat_slots[0].card = attitude->card; - 220 + s_repeat_slots[0].y = y_left; - 221 + s_repeat_slots[0].height = img_height; - 222 + s_repeat_slots[0].bitmap_layer = bitmap_layer_create(GRect - +(0, y_left, half_w, img_height)); - 223 + bitmap_layer_set_alignment(s_repeat_slots[0].bitmap_layer, - + GAlignCenter); - 224 + scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_la - +yer(s_repeat_slots[0].bitmap_layer)); - 225 + y_left += img_height + MARGIN; - 226 + - 227 + s_repeat_slots[1].card = outcome->card; - 228 + s_repeat_slots[1].y = y_right; - 229 + s_repeat_slots[1].height = img_height; - 230 + s_repeat_slots[1].bitmap_layer = - 231 + bitmap_layer_create(GRect(half_w, y_right, width - hal - +f_w, img_height)); - 232 + bitmap_layer_set_alignment(s_repeat_slots[1].bitmap_layer, - + GAlignCenter); - 233 + scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_la - +yer(s_repeat_slots[1].bitmap_layer)); - 234 + y_right += img_height + MARGIN; - 235 + - 236 + y = (y_left > y_right) ? y_left : y_right; - 237 + } - 238 + - 239 if (interp->top_item_count > 0) { - 240 y = add_text(width, y, s_transit_titles[0], s_font_subhead - ); - 241 y = add_text(width, y, s_narratives[0], s_font_body); - -● Now let's build the watch app to confirm everything compiles across all - platforms: - - Ran 2 shell commands (ctrl+o to expand) - -● All tests pass, and both HTML and watch builds are clean. Summary of what - changed: - - - HTML (interpreter/src/main.c): the Attitude/Outcome cards now render side by - side (
, reusing the same .card markup as - the full spread via a new shared print_card_html() helper) directly under the - "Rat für heute"/"Guidance" heading, before the guidance text itself. - - Watch (watch/src/c/ui_report_window.c): same ordering — two half-width - BitmapLayers for the Attitude/Outcome card art sit side by side right after - the "Guidance" heading and before the repeated top-transit line + guidance - text. Added a second lazy-load slot pool (s_repeat_slots) alongside the - existing 10-slot spread pool, wired into - teardown_content()/update_visible_images() (factored into a shared - update_slot_visibility() helper). - - Both card images still also appear again later in the full Celtic Cross - walkthrough, same as before. - -✻ Worked for 9m 17s - -※ recap: Working on the Deck in a Dash Pebble watch app's interpreter output layout. Just finished moving the Attitude/Outcome card preview above the guidance text in both HTML and watch display; next step is to sideload and visually verify on the actual watch. (disable recaps in /config) - -❯ on the watch the headings of the two cards do not look good - -● User answered Claude's questions: - ⎿  · What specifically looks bad about the Attitude/Outcome card headings on - the watch? This will help me target the right fix instead of guessing. → - Misaligned/overlapping - -● User answered Claude's questions: - ⎿  · Could you paste a screenshot of the misaligned/overlapping headings? I - want to see exactly what's colliding before changing the layout. → Describe - instead - -● Go ahead and describe what's overlapping with what — that'll help me pinpoint - the fix. - -✻ Cooked for 1m 25s - -❯ the text reads Er selbst: Die Das Ergebnis Liebenden (U-Die mgekehrt) - Herrscherin - - Read 1 file (ctrl+o to expand) - -● Update(watch/src/c/ui_report_window.c) - ⎿  Added 16 lines, removed 22 lines - 195 - 196 y = add_text(width, y, i18n_get("interp.heading_guidance", " - Guidance"), s_font_section); - 197 - 198 - /* Attitude/Outcome preview, side by side - the same two car - -ds guidance - 199 - * text below names and quotes, shown here so the reader see - -s them - 200 - * before reading why they matter. Reappear again, with thei - -r full - 201 - * description/meaning, in the Celtic Cross walkthrough furt - -her down. */ - 198 + /* Attitude/Outcome preview, side by side, image only - no p - +er-card - 199 + * caption text. A "Position: Card Name (Reversed)" label do - +esn't fit - 200 + * a half-width column without wrapping into 3+ short lines, - + and with - 201 + * both columns wrapping independently the reader ends up re - +ading - 202 + * fragments across columns rather than down one column at a - + time - 203 + * (confirmed on-device: it reads as one garbled interleaved - + sentence, - 204 + * not two). Captions are unnecessary anyway - the guidance - +text - 205 + * printed immediately below already names and quotes both c - +ards in - 206 + * full ("Deine Haltungskarte ist ..." / "Das wahrscheinlich - +e Ergebnis - 207 + * ... ist ..."), and both reappear with their full position - +/name/ - 208 + * meaning again in the Celtic Cross walkthrough further dow - +n. */ - 209 { - 210 int16_t half_w = width / 2; - 211 const TarotDraw *attitude = &reading->spread.positions[POS - ITION_ATTITUDE]; - 212 const TarotDraw *outcome = &reading->spread.positions[POSI - TION_OUTCOME]; - 206 - - 207 - snprintf(s_repeat_titles[0], sizeof s_repeat_titles[0], "% - -s: %s%s", - 208 - tarot_position_name(POSITION_ATTITUDE), tarot_car - -d_name(attitude->card), - 209 - reversed_marker(attitude->reversed)); - 210 - snprintf(s_repeat_titles[1], sizeof s_repeat_titles[1], "% - -s: %s%s", - 211 - tarot_position_name(POSITION_OUTCOME), tarot_card - -_name(outcome->card), - 212 - reversed_marker(outcome->reversed)); - 213 - - 214 - int16_t y_left = add_text_at(0, half_w, y, s_repeat_titles - -[0], s_font_subhead); - 215 - int16_t y_right = add_text_at(half_w, width - half_w, y, s - -_repeat_titles[1], s_font_subhead); - 216 - - 213 int16_t img_height = card_image_height_for_width(IMAGE_WID - TH); - 214 - 215 s_repeat_slots[0].card = attitude->card; - 220 - s_repeat_slots[0].y = y_left; - 216 + s_repeat_slots[0].y = y; - 217 s_repeat_slots[0].height = img_height; - 222 - s_repeat_slots[0].bitmap_layer = bitmap_layer_create(GRect - -(0, y_left, half_w, img_height)); - 218 + s_repeat_slots[0].bitmap_layer = bitmap_layer_create(GRect - +(0, y, half_w, img_height)); - 219 bitmap_layer_set_alignment(s_repeat_slots[0].bitmap_layer, - GAlignCenter); - 220 scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_la - yer(s_repeat_slots[0].bitmap_layer)); - 225 - y_left += img_height + MARGIN; - 221 - 222 s_repeat_slots[1].card = outcome->card; - 228 - s_repeat_slots[1].y = y_right; - 223 + s_repeat_slots[1].y = y; - 224 s_repeat_slots[1].height = img_height; - 225 s_repeat_slots[1].bitmap_layer = - 231 - bitmap_layer_create(GRect(half_w, y_right, width - hal - -f_w, img_height)); - 226 + bitmap_layer_create(GRect(half_w, y, width - half_w, i - +mg_height)); - 227 bitmap_layer_set_alignment(s_repeat_slots[1].bitmap_layer, - GAlignCenter); - 228 scroll_layer_add_child(s_scroll_layer, bitmap_layer_get_la - yer(s_repeat_slots[1].bitmap_layer)); - 234 - y_right += img_height + MARGIN; - 229 - 236 - y = (y_left > y_right) ? y_left : y_right; - 230 + y += img_height + MARGIN; - 231 } - 232 - 233 if (interp->top_item_count > 0) { - -● Now remove the now-unused s_repeat_titles buffer: - - Searched for 1 pattern (ctrl+o to expand) - -● Unused now — let's remove it: - - Read 1 file (ctrl+o to expand) -● Update(watch/src/c/ui_report_window.c) - ⎿  Removed 5 lines - 37 static char s_position_titles[TAROT_SPREAD_SIZE][96]; - 38 static char s_guidance[GUIDANCE_TEXT_MAX]; - 39 - 40 -/* Attitude/Outcome preview row, shown side by side right after - - guidance - 41 - * (same two cards reappear in the full spread further down - s - -ee - 42 - * REPEAT_SLOT_COUNT's own comment). */ - 43 -static char s_repeat_titles[2][96]; - 44 - - 40 /* Loads/frees each position's GBitmap as scroll_offset_changed - () sees it - 41 * enter/leave the visible viewport, rather than decoding all t - en Celtic - 42 * Cross images up front - a defensive habit left over from an - earlier, - - Ran 1 shell command (ctrl+o to expand) - -● Clean build across all platforms, no unused-variable warnings. Let's view the - final block to double-check correctness: - - Read 1 file (ctrl+o to expand) - -● Good — this is correct and matches the fix. Let's run the full desktop test - suite once more to confirm nothing else regressed, then summarize. - - Ran 1 shell command (ctrl+o to expand) - -● Fixed. The two card headings on the watch were causing the garbled reading you - described because each half-width column was wrapping "Position: Card Name - (Reversed)" into 3+ short lines, and with both columns wrapping independently, - reading across the row picks up interleaved fragments from both columns - instead of one coherent line at a time. - - I removed the per-card captions on the watch entirely — the row now just shows - the two card images side by side with no text. That's not a loss of - information: the guidance text printed immediately below already names and - quotes both cards in full ("Deine Haltungskarte ist..." / "Das wahrscheinliche - Ergebnis... ist..."), and both cards reappear with their full - position/name/meaning again later in the Celtic Cross walkthrough. The HTML - output is untouched — it has enough width for the full captions to render - cleanly there. - - Rebuilt cleanly across all six watch platforms and reran the desktop test - suite; everything passes. Ready to install on hardware whenever you want to - verify. - -✻ Cogitated for 2m 52s - -❯ looks good the app on the watch does it take into consideration the current - day as part of the seed? - - Searched for 1 pattern, read 1 file (ctrl+o to expand) - -● Yes — reading_state_recompute() in watch/src/c/reading_state.c:14-15 builds - the seed as "%04d-%02d-%02d" from the current UTC date (time(NULL) + - gmtime()), e.g. "2026-07-16", and passes that straight into - reading_generate(). That's the exact same seed convention dist/run-engine.sh - uses on desktop, so the Celtic Cross draw is deterministic per calendar day — - same day always reproduces the same spread, and it changes at UTC midnight. - - Searched for 1 pattern, read 1 file (ctrl+o to expand) - -● Yes. reading_state_recompute() (watch/src/c/reading_state.c:9-15) builds the - seed from the device's current UTC date — time(NULL) + gmtime() → "YYYY-MM-DD" - — and passes it straight into reading_generate(), same convention as - dist/run-engine.sh on desktop. So the Celtic Cross draw (and everything - downstream) is deterministic per calendar day: same day always reproduces the - same spread, and it changes at UTC midnight. - - It's recomputed at two points (main.c:28,44): once on app launch, and once - whenever the config page pushes updated birth/settings data. There's no timer - that re-checks the date while the app stays open across a midnight rollover — - you'd need to relaunch (or change settings) to pick up a new day mid-session. - That's also exactly where the still-pending daily notification (task #44, - wakeup_schedule()) would naturally hook in, since a scheduled wakeup could - trigger a recompute at the same time each day rather than relying on the user - reopening the app. - -✻ Sautéed for 1m 2s - -❯ ok one last addition - after the daily interpretation we should write the day - which is being interpreted. so the date which is passed to reading generate. - - Searched for 4 patterns, read 6 files (ctrl+o to expand) - -● Update(engine/src/reading.h) - ⎿  Added 6 lines - 15 #include "tarot.h" - 16 - 17 typedef struct { - 18 + /* The moment passed to reading_generate() - the day this rea - +ding is - 19 + * for, not when it happened to be generated. Set by reading_ - +generate() - 20 + * itself, so every caller (CLI, watch, reading_load_json() o - +n the - 21 + * interpreter side) gets it for free without threading it th - +rough - 22 + * separately. */ - 23 + time_t utc_moment; - 24 NatalChart natal; - 25 DailyTransits transits; - 26 CelticCrossSpread spread; - -● Now update reading_generate() in reading.c, and add the date field to - reading_print_json: - -● Update(engine/src/reading.c) - ⎿  Added 1 line - 5 - 6 void reading_generate(const char *tarot_seed, const BirthData * - birth, - 7 time_t utc_moment, DailyReading *out) { - 8 + out->utc_moment = utc_moment; - 9 astro_compute_natal_chart(birth, &out->natal); - 10 astro_compute_daily_transits(utc_moment, &out->natal, &out->t - ransits); - 11 tarot_draw_celtic_cross(tarot_seed, &out->spread); - -● Update(engine/src/reading.c) - ⎿  Added 3 lines - 161 void reading_print_json(const DailyReading *r, FILE *out) { - 162 fprintf(out, "{\n"); - 163 - 164 + struct tm *utc = gmtime(&r->utc_moment); - 165 + fprintf(out, " \"date\": \"%04d-%02d-%02d\",\n", utc->tm_ye - +ar + 1900, utc->tm_mon + 1, utc->tm_mday); - 166 + - 167 fprintf(out, " \"natal\": {\n \"bodies\": [\n"); - 168 for (int b = 0; b < NUM_BODIES; b++) { - 169 print_body_position_json(out, " ", (Body)b, &r->natal - .bodies[b]); - -● Now let's update reading_io.c to parse the new date field: - Read 2 files (ctrl+o to expand) - -● Update(interpreter/src/reading_io.c) - ⎿  Added 3 lines - 1 +#define _DEFAULT_SOURCE /* for timegm, parsing "date" back into - +utc_moment */ - 2 + - 3 #include "reading_io.h" - 4 #include "json.h" - 5 - 6 +#include - 7 #include - 8 - 9 /* Mirrors astro.c's own k_body_slug/k_aspect_slug (its i18n loo - kup-key - -● Now add the date parsing logic in reading_load_json: - - Read 1 file (ctrl+o to expand) - -● Update(interpreter/src/reading_io.c) - ⎿  Added 19 lines - 136 } - 137 } - 138 - 139 +/* Parses the top-level "date" field ("YYYY-MM-DD", see readin - +g_print_json - 140 + * in engine/src/reading.c) back into out->utc_moment, as midn - +ight UTC of - 141 + * that date - reading_print_json only ever wrote the calendar - + date, not a - 142 + * time of day, so that's the only value that round-trips thro - +ugh - 143 + * gmtime() to the same date. Lenient like parse_bodies()/pars - +e_spread(): - 144 + * a missing or malformed "date" just leaves out->utc_moment a - +t 0 (from - 145 + * reading_load_json's own memset), not a load failure - nothi - +ng here - 146 + * depends on it for scoring, only for display. */ - 147 +static void parse_date(const JsonValue *date, DailyReading *ou - +t) { - 148 + const char *date_str = date ? json_as_string(date) : NULL; - 149 + if (!date_str) return; - 150 + - 151 + struct tm tm_date = {0}; - 152 + if (sscanf(date_str, "%d-%d-%d", &tm_date.tm_year, &tm_date. - +tm_mon, &tm_date.tm_mday) != 3) return; - 153 + tm_date.tm_year -= 1900; - 154 + tm_date.tm_mon -= 1; - 155 + out->utc_moment = timegm(&tm_date); - 156 +} - 157 + - 158 bool reading_load_json(const char *text, size_t length, DailyR - eading *out) { - 159 memset(out, 0, sizeof(*out)); - 160 - -● Update(interpreter/src/reading_io.c) - ⎿  Added 2 lines - 196 } - 197 out->transits.aspect_count = filled; - 198 - 199 + parse_date(json_object_get(root, "date"), out); - 200 + - 201 const JsonValue *natal = json_object_get(root, "natal"); - 202 parse_bodies(natal ? json_object_get(natal, "bodies") : NULL - , out->natal.bodies); - 203 parse_bodies(json_object_get(transits, "bodies"), out->trans - its.bodies); - -● Update(interpreter/src/reading_io.h) - ⎿  Added 5 lines, removed 3 lines - 14 * around each significant event, and for guidance_print()'s ta - rot - 15 * framing: out->transits.aspects[]/aspect_count (used for scor - ing - see - 16 * significance.c), out->natal.bodies[]/out->transits.bodies[] - (sign + - 17 - * house per body, used only for display), and out->spread.posi - -tions[] - 17 + * house per body, used only for display), out->spread.position - +s[] - 18 * (card + reversed per Celtic Cross position, used only by - 19 - * guidance.c). out->natal.ascendant_longitude/houses[] are lef - -t zeroed - - 20 - * nothing reads them yet. - 19 + * guidance.c), and out->utc_moment (the top-level "date" field - +, parsed - 20 + * back to midnight UTC of that date - used only for display, e - +.g. main.c - 21 + * printing which day a reading is for). out->natal.ascendant_l - +ongitude/ - 22 + * houses[] are left zeroed - nothing reads them yet. - 23 * - 24 * Returns false on malformed JSON, or if "transits"."aspects" - isn't - 25 * present as an array (an empty array is fine - that's a real - "no - - Read 1 file (ctrl+o to expand) - -● Update(interpreter/src/main.c) - ⎿  Added 9 lines - 126 return ui(key, fallback[sign]); - 127 } - 128 - 129 +/* "YYYY-MM-DD" for reading->utc_moment - the day this reading - + is for - 130 + * (see reading_load_json()/parse_date() in reading_io.c), not - + today's - 131 + * real date if the two ever differ (e.g. reading.json was gen - +erated - 132 + * earlier and interpreted later). */ - 133 +static void format_date(time_t utc_moment, char *out, size_t o - +ut_size) { - 134 + struct tm *utc = gmtime(&utc_moment); - 135 + snprintf(out, out_size, "%04d-%02d-%02d", utc->tm_year + 190 - +0, utc->tm_mon + 1, utc->tm_mday); - 136 +} - 137 + - 138 static const char *day_level_name(DaySignificance level) { - 139 switch (level) { - 140 case DAY_QUIET: return ui("interp.day_level.quiet", "Quiet - "); - -● Now the text format — add the date line at the very top: - -● Update(interpreter/src/main.c) - ⎿  Added 4 lines - 238 } - 239 - 240 static void interp_print_text(FILE *out, const DailyReading *r - eading, const DailyInterpretation *interp) { - 241 + char date_buf[16]; - 242 + format_date(reading->utc_moment, date_buf, sizeof date_buf); - 243 + fprintf(out, "%s %s\n\n", ui("interp.reading_date", "Reading - + for:"), date_buf); - 244 + - 245 fprintf(out, "%s %s (%d/%d)\n", ui("interp.day_significance" - , "Day significance:"), - 246 day_level_name(interp->day_level), interp->day_level - + 1, DAY_SIGNIFICANCE_COUNT); - 247 if (interpretation_deserves_framing(interp)) { - -● Now the HTML format — right after the

Daily Interpretation

heading: - - Read 1 file (ctrl+o to expand) - -● Update(interpreter/src/main.c) - ⎿  Added 5 lines - 333 - 334 fprintf(out, "

%s

\n", ui("interp.heading", "Daily In - terpretation")); - 335 - 336 + char date_buf[16]; - 337 + format_date(reading->utc_moment, date_buf, sizeof date_buf); - 338 + fprintf(out, "

%s %s

\n", - 339 + ui("interp.reading_date", "Reading for:"), date_buf) - +; - 340 + - 341 fprintf(out, "

%s

\n

%s %s (%d/%d)", - 342 ui("interp.heading_day_significance", "Day Significa - nce"), - 343 ui("interp.day_significance", "Day significance:"), - day_level_name(interp->day_level), - -● Now the JSON format: - - Searched for 1 pattern, read 1 file (ctrl+o to expand) - -● Update(interpreter/src/main.c) - ⎿  Added 4 lines - 416 static void interp_print_json(FILE *out, const DailyReading *r - eading, const DailyInterpretation *interp) { - 417 fprintf(out, "{\n"); - 418 - 419 + char date_buf[16]; - 420 + format_date(reading->utc_moment, date_buf, sizeof date_buf); - 421 + fprintf(out, " \"date\": \"%s\",\n", date_buf); - 422 + - 423 fprintf(out, " \"day_significance\": {\n"); - 424 fprintf(out, " \"level\": \"%s\",\n", k_day_level_slug[in - terp->day_level]); - 425 fprintf(out, " \"rank\": %d,\n", interp->day_level + 1); - -● Now add the i18n key to both language files: - - Read 1 file (ctrl+o to expand) - -● Update(engine/i18n/en.lang) - ⎿  Added 1 line - 171 card.world.reversed=Inertia, fixity, stagnation, permanence. - 172 - 173 # --- Interpreter UI labels (interpreter/src/main.c) --- - 174 +interp.reading_date=Reading for: - 175 interp.day_significance=Day significance: - 176 interp.day_level.quiet=Quiet - 177 interp.day_level.notable=Notable - - Read 1 file (ctrl+o to expand) - -● Update(engine/i18n/de.lang) - ⎿  Added 1 line - 162 card.world.reversed=Trägheit, Erstarrung, Stillstand, Beständi - gkeit. - 163 - 164 # --- UI-Bezeichnungen des Interpreters (interpreter/src/main. - c) --- - 165 +interp.reading_date=Lesung für: - 166 interp.day_significance=Bedeutung des Tages: - 167 interp.day_level.quiet=Ruhig - 168 interp.day_level.notable=Bemerkenswert - Read 1 file (ctrl+o to expand) - -● Update(watch/src/c/ui_report_window.c) - ⎿  Added 1 line - 31 * Static rather than malloc'd - fixed, known-in-advance sizes, - and it - 32 * keeps report_window_reload() from having to reason about fre - eing the - 33 * previous run's buffers before reusing them. */ - 34 +static char s_reading_date[48]; - 35 static char s_day_title[64]; - 36 static char s_transit_titles[MAX_SIGNIFICANT_ITEMS][96]; - 37 static char s_narratives[MAX_SIGNIFICANT_ITEMS][NARRATIVE_TEXT_ - MAX]; - - Read 1 file (ctrl+o to expand) - -● Update(watch/src/c/ui_report_window.c) - ⎿  Added 11 lines - 169 - 170 int16_t y = MARGIN; - 171 - 172 + /* The day this reading is for (reading->utc_moment, set by - 173 + * reading_generate() - see reading_state_recompute()), not - +necessarily - 174 + * "today" if the report is viewed right at a UTC-midnight b - +oundary - 175 + * before the next recompute. Plain gmtime(), same justifica - +tion as - 176 + * reading_state.c's own use of it. */ - 177 + struct tm *utc = gmtime(&reading->utc_moment); - 178 + snprintf(s_reading_date, sizeof s_reading_date, "%s %04d-%02 - +d-%02d", - 179 + i18n_get("interp.reading_date", "Reading for:"), ut - +c->tm_year + 1900, - 180 + utc->tm_mon + 1, utc->tm_mday); - 181 + y = add_text(width, y, s_reading_date, s_font_body); - 182 + - 183 y = add_text(width, y, i18n_get("interp.heading_day_signific - ance", "Day Significance"), s_font_section); - 184 snprintf(s_day_title, sizeof s_day_title, "%s (%d/%d)", day_ - level_name(interp->day_level), - 185 interp->day_level + 1, DAY_SIGNIFICANCE_COUNT); - - Read 1 file, ran 1 shell command (ctrl+o to expand) - -● Update(engine/tests/smoke_test.c) - ⎿  Added 1 line - 120 * a stray astro_*_name()/tarot_*_name() call leaking transl - ated text - 121 * into what must stay language-independent regardless of -- - lang). */ - 122 assert(buf[0] == '{'); - 123 + assert(strstr(buf, "\"date\": \"2025-07-02\"") != NULL); /* - +UTC date of the fixed 1751500000 seed */ - 124 assert(strstr(buf, "\"natal\"") != NULL); - 125 assert(strstr(buf, "\"transits\"") != NULL); - 126 assert(strstr(buf, "\"spread\"") != NULL); - - Read 1 file (ctrl+o to expand) - -● Update(interpreter/tests/json_test.c) - ⎿  Added 1 line - 1 #include - 2 #include - 3 #include - 4 +#include - 5 - 6 #include "../src/json.h" - 7 #include "../src/reading_io.h" - -● Update(interpreter/tests/json_test.c) - ⎿  Added 1 line - 49 * absent entirely, to prove partial spread data doesn't fail t - he load. */ - 50 static const char *k_fixture = - 51 "{" - 52 + " \"date\": \"2026-07-16\"," - 53 " \"natal\": {\"bodies\": [{\"body\": \"sun\", \"sign\": \"t - aurus\", \"house\": 3}], \"houses\": []}," - 54 " \"transits\": {" - 55 " \"bodies\": [{\"body\": \"sun\", \"sign\": \"cancer\", \ - "house\": 5}]," - -● Update(interpreter/tests/json_test.c) - ⎿  Added 24 lines - 82 printf("PASS test_reading_load_json_extracts_aspects\n"); - 83 } - 84 - 85 +static void test_reading_load_json_extracts_date(void) { - 86 + DailyReading reading; - 87 + bool ok = reading_load_json(k_fixture, strlen(k_fixture), &r - +eading); - 88 + - 89 + assert(ok); - 90 + struct tm *utc = gmtime(&reading.utc_moment); - 91 + assert(utc->tm_year + 1900 == 2026); - 92 + assert(utc->tm_mon + 1 == 7); - 93 + assert(utc->tm_mday == 16); - 94 + - 95 + printf("PASS test_reading_load_json_extracts_date\n"); - 96 +} - 97 + - 98 +static void test_reading_load_json_missing_date_defaults_to_ze - +ro(void) { - 99 + const char *text = "{\"transits\": {\"aspects\": []}}"; - 100 + DailyReading reading; - 101 + bool ok = reading_load_json(text, strlen(text), &reading); - 102 + - 103 + assert(ok); - 104 + assert(reading.utc_moment == 0); - 105 + - 106 + printf("PASS test_reading_load_json_missing_date_defaults_to - +_zero\n"); - 107 +} - 108 + - 109 static void test_reading_load_json_extracts_body_sign_and_hous - e(void) { - 110 DailyReading reading; - 111 bool ok = reading_load_json(k_fixture, strlen(k_fixture), &r - eading); - -● Update(interpreter/tests/json_test.c) - ⎿  Added 2 lines - 204 test_json_parse_basic_shapes(); - 205 test_json_parse_rejects_malformed(); - 206 test_reading_load_json_extracts_aspects(); - 207 + test_reading_load_json_extracts_date(); - 208 + test_reading_load_json_missing_date_defaults_to_zero(); - 209 test_reading_load_json_extracts_body_sign_and_house(); - 210 test_reading_load_json_extracts_spread_positions(); - 211 test_reading_load_json_missing_spread_is_not_fatal(); - -● Now let's update the documented JSON schema in docs/input-output-format.md to - match: - - Read 1 file (ctrl+o to expand) - -● Update(docs/input-output-format.md) - ⎿  Added 2 lines, removed 1 line - 171 - 172 ### `--format json` - 173 - 174 -A single JSON object mirroring `DailyReading` exactly — `natal - -`, - 174 +A single JSON object mirroring `DailyReading` exactly — `date` - +, `natal`, - 175 `transits`, `spread` — using the **slug** accessors - 176 (`astro_body_slug()`, `astro_sign_slug()`, `astro_moon_phase_s - lug()`, - 177 `astro_aspect_slug()`, `tarot_card_slug()`, `tarot_position_sl - ug()`) - ... - 182 - 183 ```json - 184 { - 185 + "date": "2026-07-16", - 186 "natal": { - 187 "bodies": [ - 188 {"body": "sun", "sign": "taurus", "degree_in_sign": 23.4 - 926, "ecliptic_longitude": 53.4926, "house": 3}, - - Read 1 file (ctrl+o to expand) - -● Update(docs/input-output-format.md) - ⎿  Added 7 lines - 211 } - 212 ``` - 213 - 214 +- `date` is the UTC calendar date of `--date` (or the current - +system time - 215 + if `--date` was omitted) — the day this reading is *for*, no - +t - 216 + necessarily the day it was generated on. `reading_generate() - +` stores - 217 + the full moment in `DailyReading.utc_moment`; only the date - +part is - 218 + serialized here since that's the only thing the rest of this - + format - 219 + (and the interpreter's own reports, which read this field ba - +ck — see - 220 + "The interpreter" below) ever display. - 221 - `bodies` is always `NUM_BODIES` (10) entries, Sun..Pluto in - `Body` enum - 222 order; `spread.positions` is always `TAROT_SPREAD_SIZE` (10) - entries in - 223 `CelticCrossPosition` enum order (Present, Challenge, Crown, - - Read 1 file (ctrl+o to expand) - ⎿  Interrupted · What should Claude do instead? - -❯ nothing please continue - -● Update(docs/input-output-format.md) - ⎿  Added 9 lines, removed 4 lines - 287 ### `--format text` - 288 - 289 A plain-text report, printed in a fixed order — - 290 - 1. the day's overall significance level (`Quiet`/`Notable`/ - 290 + 1. the day this reading is for (`Reading for: 2026-07-16`), - +from the - 291 + input JSON's top-level `date` field (see `--format json` - +above) — - 292 + not necessarily the day the report is actually being read - + on, if - 293 + `reading.json` was generated earlier and piped into - 294 + `interpreter-cli` later; - 295 + 2. the day's overall significance level (`Quiet`/`Notable`/ - 296 `Significant`/`Major`, plus its rank out of the 4 defined - levels, - 297 e.g. `Notable (2/4)`), with a "worth a deeper Celtic Cros - s look" - 298 line on `Major` days only; - 294 - 2. up to 5 of today's aspects ranked by score, each naming t - -he sign - 299 + 3. up to 5 of today's aspects ranked by score, each naming t - +he sign - 300 and natal house the transiting planet currently occupies - and the - 301 sign and house of the natal planet it's aspecting, follow - ed by a - 302 short narrative sentence about what that transiting plane - t - ... - 304 governs* (see `CLAUDE.md`'s `narrative.c` bullet) — omitt - ed only if - 305 the source material has nothing to say for that planet/as - pect - 306 combination; - 302 - 3. the full 10-position Celtic Cross spread, in Waite's own - -drawing - 307 + 4. the full 10-position Celtic Cross spread, in Waite's own - +drawing - 308 order, each with its card (and orientation), the position - 's own - 309 description, and that card's actual meaning; - 305 - 4. finally, a guidance paragraph tying the day's top transit - - to the - 310 + 5. finally, a guidance paragraph tying the day's top transit - + to the - 311 spread's Attitude/Outcome cards, with its opening sentenc - e tailored - 312 to `day_level` (stronger wording on `Major`, softer on - 313 `Quiet`/`Notable`, falling back to a transit-free reading - of the - - Read 1 file (ctrl+o to expand) - -● Update(docs/input-output-format.md) ⎿  Added 2 lines - 318 seen the full spread it references. - 319 - 320 ``` - 321 +Reading for: 2026-07-16 - 322 + - 323 Day significance: Major (4/4) - 324 (a major transit today - worth a deeper Celtic Cross look) - 325 - -● Update(docs/input-output-format.md) - ⎿  Added 2 lines - 369 above): - 370 - 371 ``` - 372 +Lesung für: 2026-07-16 - 373 + - 374 Bedeutung des Tages: Einschneidend (4/4) - 375 (ein einschneidender Transit heute - ein genauerer Blick auf d - as Keltische Kreuz lohnt sich) - 376 - -● Now let's find and update the interpreter's own --format json example to - include the date field: - - Searched for 1 pattern, read 1 file (ctrl+o to expand) - -● Update(docs/input-output-format.md) - ⎿  Added 1 line - 432 - 433 ```json - 434 { - 435 + "date": "2026-07-16", - 436 "day_significance": { - 437 "level": "major", - 438 "rank": 4, - - Read 1 file (ctrl+o to expand) - -● Update(docs/input-output-format.md) - ⎿  Added 3 lines - 466 } - 467 ``` - 468 - 469 +- `date` is copied straight through from the input JSON's own - +top-level - 470 + `date` field (see `--format json` above) - always language-i - +ndependent - 471 + regardless of `--lang`, same as every other slug in this doc - +ument. - 472 - `significant_transits` has `interp.top_item_count` entries ( - 0-5, same - 473 ranking as `--format text`); `celtic_cross` always has exact - ly 10, in - 474 `CelticCrossPosition` enum order (same order as `deck-engine - `'s own - -● Now let's rebuild everything and run the full test suite to confirm - correctness end-to-end. - - Searched for 1 pattern, read 1 file, ran 2 shell commands (ctrl+o to expand) - -● Warning cleared. Let's rebuild fully and run all tests: - - Ran 1 shell command (ctrl+o to expand) - -● All tests pass. Now let's verify the actual end-to-end output: - - Ran 1 shell command (ctrl+o to expand) - -● All working. Let's check HTML too, then build the watch app: - - Ran 1 shell command (ctrl+o to expand) - -● Now let's build the watch app across all platforms: - - Ran 1 shell command (ctrl+o to expand) - -● Clean build. Let's do a final review of the watch code to make sure the date - display looks right: - - Read 1 file (ctrl+o to expand) - -● This all looks correct. Summary of the change: - - - DailyReading (engine/src/reading.h) gained a time_t utc_moment field, set - automatically by reading_generate() — the single source of truth for "which - day is this reading for," available to every consumer (CLI, interpreter, - watch) without threading a separate parameter around. - - reading_print_json now emits a top-level "date": "YYYY-MM-DD" field (UTC - calendar date), and reading_io.c parses it back on the interpreter side. - - Displayed everywhere: interpreter-cli's text output ("Reading for: - 2026-07-16" / "Lesung für: 2026-07-16" as the very first line), HTML (right - under the "Daily Interpretation" heading), JSON ("date" field), and the - watch's on-device report (first line, above "Day Significance"). - - Added test coverage (smoke_test.c, json_test.c) and updated - docs/input-output-format.md to document the new field. - - Full desktop test suite passes, and the watch app builds cleanly across all - six platforms. Ready to install on hardware whenever you'd like to verify. - -✻ Churned for 5m 13s - -※ recap: You're building the Deck in a Dash Pebble watch app; just added a "Reading for: " line to the interpreter output and watch display, and it builds cleanly. Next: install the updated app on your watch to verify it looks right. - -❯ what would be a good commit message - - Ran 4 shell commands (ctrl+o to expand) - -● Based on the full staged diff (icons/logo, interpreter reordering, - Attitude/Outcome preview, and the new date field), here's a commit message in - the style your recent commits use (descriptive subject, then paragraphs - grouped by sub-topic with file references): - - Read 1 file (ctrl+o to expand) - -● That's everything currently staged, covering three logically distinct pieces - of work in one commit (matching how your last two commits each bundled a - themed set of changes). Let me know if you'd rather split it into separate - commits (e.g., icons/logo vs. report restructuring vs. date field) instead — - happy to do either. Just say the word and I'll actually run the commit. - -✻ Cooked for 1m 55s - -❯ /export ⎿ Conversation exported to: /home/ml/krotik/deck_in_a_dash/2026-07-16-185651- - this-session-is-being-continued-from-a-previous-c.txt - -❯ /compact - -❯ /compact - ⎿ Compacted (ctrl+o to see full summary) - ⎿  Referenced file interpreter/src/main.c - ⎿  Referenced file docs/input-output-format.md - ⎿  Read interpreter/tests/json_test.c (219 lines) - ⎿  Read engine/tests/smoke_test.c (182 lines) - ⎿  Referenced file watch/src/c/ui_report_window.c - ⎿  Plan file referenced (~/.claude/plans/woolly-fluttering-naur.md) - -❯ next should be to introduce gender to the configuration and to interpret the - reading with appropriate pronounce - ⎿  1 skill available - -● I'll start by exploring where the reading text currently references the - person, to see where pronouns would need to vary by gender. - - Searched for 5 patterns, read 8 files, ran 3 shell commands (ctrl+o to expand) - -● Now let's look at app_message.c (how config page data is received) and - pkjs/index.js (the config page form). - - Searched for 4 patterns, read 2 files (ctrl+o to expand) - -● Now let's check reading_state.c/.h for where reading_generate() is called on - the watch, and ui_report_window.c's usage of tarot_position_name/description. - - Searched for 3 patterns, read 2 files, listed 1 directory, ran 1 shell command (ctrl+o to expand) -● Now I have a full picture. Let me set up tracking for this multi-file feature. - -● Starting with the core engine: adding a Gender enum and threading it through - the position-text lookups in tarot.h/tarot_data.c. - -● Update(engine/src/tarot.h) - ⎿  Added 11 lines - 52 bool reversed; - 53 } TarotDraw; - 54 - 55 +/* Who the reading is for - affects only the pronouns in the Ce - +ltic - 56 + * Cross position text below (Waite's original wording addresse - +s the - 57 + * querent as "him"), never anything about the tarot draw or as - +trology - 58 + * math itself. GENDER_UNSPECIFIED is the default and renders - 59 + * pronoun-neutral phrasing (singular "they" in English). */ - 60 +typedef enum { - 61 + GENDER_UNSPECIFIED = 0, - 62 + GENDER_MALE, - 63 + GENDER_FEMALE - 64 +} Gender; - 65 + - 66 typedef struct { - 67 /* Indexed by CelticCrossPosition. */ - 68 TarotDraw positions[TAROT_SPREAD_SIZE]; - -● Update(engine/src/tarot.h) - ⎿  Added 3 lines, removed 2 lines - 78 const char *tarot_card_image_file(TarotCard card); /* matches r - es/img/ */ - 79 const char *tarot_card_meaning(TarotCard card, bool reversed); - 80 - 81 -const char *tarot_position_name(CelticCrossPosition position); - 82 -const char *tarot_position_description(CelticCrossPosition posi - -tion); - 81 +const char *tarot_position_name(CelticCrossPosition position, G - +ender gender); - 82 +const char *tarot_position_description(CelticCrossPosition posi - +tion, Gender gender); - 83 - 84 /* Stable, language-independent identifiers (e.g. "high_prieste - ss", - 85 * "wheel_of_fortune", "recent_past") - the same strings i18n.c - 's lookup - ... - 87 * (reading_print_json) that must stay identical regardless of - --lang. */ - 88 const char *tarot_card_slug(TarotCard card); - 89 const char *tarot_position_slug(CelticCrossPosition position); - 90 +const char *tarot_gender_slug(Gender gender); /* "unspecified"/ - +"male"/"female" */ - 91 - 92 #endif - - Ran 1 shell command (ctrl+o to expand) - -● Update(engine/src/tarot_data.c) - ⎿  Added 68 lines, removed 23 lines - 130 - 131 /* Position names and one-line descriptions follow Waite's own - account - 132 * of "An Ancient Celtic Method of Divination" (Part III §7), - in his - 133 - * original drawing order. */ - 133 + * original drawing order. Waite addresses the querent as "him - +" - 134 + * throughout; that wording is kept verbatim as the GENDER_MAL - +E variant - 135 + * below, and GENDER_FEMALE/GENDER_UNSPECIFIED are original pa - +raphrases - 136 + * written for this project (not Waite's own text) so the read - +ing can - 137 + * address the person it's actually for. Each array is indexed - + by - 138 + * Gender (tarot.h): [GENDER_UNSPECIFIED], [GENDER_MALE], [GEN - +DER_FEMALE]. */ - 139 typedef struct { - 135 - const char *name; - 136 - const char *description; - 140 + const char *name[3]; - 141 + const char *description[3]; - 142 } PositionInfo; - 143 - 144 static const PositionInfo k_positions[TAROT_SPREAD_SIZE] = { - 145 [POSITION_PRESENT] = { - 141 - "The Present", - 142 - "This covers him: the general influence affecting the matt - -er." - 146 + { "The Present", "The Present", "The Present" }, - 147 + { - 148 + "This covers them: the general influence affecting the m - +atter.", - 149 + "This covers him: the general influence affecting the ma - +tter.", - 150 + "This covers her: the general influence affecting the ma - +tter.", - 151 + } - 152 }, - 153 [POSITION_CHALLENGE] = { - 145 - "The Challenge", - 146 - "This crosses him: the nature of the obstacle in the matte - -r." - 154 + { "The Challenge", "The Challenge", "The Challenge" }, - 155 + { - 156 + "This crosses them: the nature of the obstacle in the ma - +tter.", - 157 + "This crosses him: the nature of the obstacle in the mat - +ter.", - 158 + "This crosses her: the nature of the obstacle in the mat - +ter.", - 159 + } - 160 }, - 161 [POSITION_CROWN] = { - 149 - "The Crown", - 150 - "This crowns him: the aim or ideal, the best that can be a - -chieved." - 162 + { "The Crown", "The Crown", "The Crown" }, - 163 + { - 164 + "This crowns them: the aim or ideal, the best that can b - +e achieved.", - 165 + "This crowns him: the aim or ideal, the best that can be - + achieved.", - 166 + "This crowns her: the aim or ideal, the best that can be - + achieved.", - 167 + } - 168 }, - 169 [POSITION_FOUNDATION] = { - 153 - "The Foundation", - 154 - "This is beneath him: the basis of the matter, already act - -ual." - 170 + { "The Foundation", "The Foundation", "The Foundation" }, - 171 + { - 172 + "This is beneath them: the basis of the matter, already - +actual.", - 173 + "This is beneath him: the basis of the matter, already a - +ctual.", - 174 + "This is beneath her: the basis of the matter, already a - +ctual.", - 175 + } - 176 }, - 177 [POSITION_RECENT_PAST] = { - 157 - "The Recent Past", - 158 - "This is behind him: the influence that is just passing aw - -ay." - 178 + { "The Recent Past", "The Recent Past", "The Recent Past" - +}, - 179 + { - 180 + "This is behind them: the influence that is just passing - + away.", - 181 + "This is behind him: the influence that is just passing - +away.", - 182 + "This is behind her: the influence that is just passing - +away.", - 183 + } - 184 }, - 185 [POSITION_NEAR_FUTURE] = { - 161 - "The Near Future", - 162 - "This is before him: the influence now coming into action. - -" - 186 + { "The Near Future", "The Near Future", "The Near Future" - +}, - 187 + { - 188 + "This is before them: the influence now coming into acti - +on.", - 189 + "This is before him: the influence now coming into actio - +n.", - 190 + "This is before her: the influence now coming into actio - +n.", - 191 + } - 192 }, - 193 [POSITION_ATTITUDE] = { - 165 - "Himself", - 166 - "His position or attitude in the circumstances." - 194 + { "Themself", "Himself", "Herself" }, - 195 + { - 196 + "Their position or attitude in the circumstances.", - 197 + "His position or attitude in the circumstances.", - 198 + "Her position or attitude in the circumstances.", - 199 + } - 200 }, - 201 [POSITION_ENVIRONMENT] = { - 169 - "His House", - 170 - "His environment and the tendencies at work therein." - 202 + { "Their House", "His House", "Her House" }, - 203 + { - 204 + "Their environment and the tendencies at work therein.", - 205 + "His environment and the tendencies at work therein.", - 206 + "Her environment and the tendencies at work therein.", - 207 + } - 208 }, - 209 [POSITION_HOPES_AND_FEARS] = { - 173 - "Hopes and Fears", - 174 - "His hopes or fears in the matter." - 210 + { "Hopes and Fears", "Hopes and Fears", "Hopes and Fears" - +}, - 211 + { - 212 + "Their hopes or fears in the matter.", - 213 + "His hopes or fears in the matter.", - 214 + "Her hopes or fears in the matter.", - 215 + } - 216 }, - 217 [POSITION_OUTCOME] = { - 177 - "The Outcome", - 178 - "What will come: the final result of the matter." - 218 + { "The Outcome", "The Outcome", "The Outcome" }, - 219 + { - 220 + "What will come: the final result of the matter.", - 221 + "What will come: the final result of the matter.", - 222 + "What will come: the final result of the matter.", - 223 + } - 224 }, - 225 }; - 226 - -● Now update the slug tables, position_field(), and the public accessor - functions: - -● Update(engine/src/tarot_data.c) - ⎿  Added 21 lines, removed 2 lines - 274 return i18n_get(key, fallback); - 275 } - 276 - 277 -static const char *position_field(const char *slug, const char - - *field, const char *fallback) { - 278 - char key[64]; - 277 +/* Suffix appended to the i18n key for the male/female variant - +s - the - 278 + * unspecified/neutral variant uses the plain, unsuffixed key - +so it - 279 + * matches the field name callers already know (e.g. "position - +. - 280 + * attitude.name"), same convention as card_field()'s upright/ - +reversed - 281 + * field names above. */ - 282 +static const char *const k_gender_suffix[3] = { - 283 + [GENDER_UNSPECIFIED] = "", - 284 + [GENDER_MALE] = ".male", - 285 + [GENDER_FEMALE] = ".female", - 286 +}; - 287 + - 288 +static const char *const k_gender_slug[3] = { - 289 + [GENDER_UNSPECIFIED] = "unspecified", - 290 + [GENDER_MALE] = "male", - 291 + [GENDER_FEMALE] = "female", - 292 +}; - 293 + - 294 +static const char *position_field(const char *slug, const char - + *field, Gender gender, - 295 + const char *fallback) { - 296 + char key[80]; - 297 strcpy(key, "position."); - 298 strcat(key, slug); - 299 strcat(key, "."); - 300 strcat(key, field); - 301 + strcat(key, k_gender_suffix[gender]); - 302 return i18n_get(key, fallback); - 303 } - 304 - -● Update(engine/src/tarot_data.c) - ⎿  Added 6 lines, removed 4 lines - 316 reversed ? k_cards[card].reversed : k_car - ds[card].upright); - 317 } - 318 - 319 -const char *tarot_position_name(CelticCrossPosition position) - -{ - 320 - return position_field(k_position_slug[position], "name", k_p - -ositions[position].name); - 319 +const char *tarot_position_name(CelticCrossPosition position, - +Gender gender) { - 320 + return position_field(k_position_slug[position], "name", gen - +der, k_positions[position].name[gender]); - 321 } - 322 - 323 -const char *tarot_position_description(CelticCrossPosition pos - -ition) { - 324 - return position_field(k_position_slug[position], "desc", k_p - -ositions[position].description); - 323 +const char *tarot_position_description(CelticCrossPosition pos - +ition, Gender gender) { - 324 + return position_field(k_position_slug[position], "desc", gen - +der, - 325 + k_positions[position].description[gen - +der]); - 326 } - 327 - 328 const char *tarot_card_slug(TarotCard card) { return k_card_sl - ug[card]; } - 329 const char *tarot_position_slug(CelticCrossPosition position) - { return k_position_slug[position]; } - 330 +const char *tarot_gender_slug(Gender gender) { return k_gender - +_slug[gender]; } - -● Task #47 done. Now task #48: thread Gender through - DailyReading/reading_generate and the CLI. -● Update(engine/src/reading.h) - ⎿  Added 5 lines, removed 1 line - 21 * interpreter side) gets it for free without threading it th - rough - 22 * separately. */ - 23 time_t utc_moment; - 24 + /* Who the reading is for - only affects the pronouns in the - +Celtic - 25 + * Cross position text (see tarot.h's Gender), set by reading - +_generate() - 26 + * the same way utc_moment is, so every caller gets it for fr - +ee. */ - 27 + Gender gender; - 28 NatalChart natal; - 29 DailyTransits transits; - 30 CelticCrossSpread spread; - ... - 32 - 33 /* The real API: this is the only function the watchapp needs t - o call. - 34 * It walks the returned struct directly to lay out its own scr - eens. */ - 31 -void reading_generate(const char *tarot_seed, const BirthData * - -birth, - 35 +void reading_generate(const char *tarot_seed, const BirthData * - +birth, Gender gender, - 36 time_t utc_moment, DailyReading *out); - 37 - 38 /* Desktop-only output formats for inspecting a reading before - any watch - -● Update(engine/src/reading.c) - ⎿  Added 2 lines, removed 1 line - 3 - 4 #include - 5 - 6 -void reading_generate(const char *tarot_seed, const BirthData * - -birth, - 6 +void reading_generate(const char *tarot_seed, const BirthData * - +birth, Gender gender, - 7 time_t utc_moment, DailyReading *out) { - 8 out->utc_moment = utc_moment; - 9 + out->gender = gender; - 10 astro_compute_natal_chart(birth, &out->natal); - 11 astro_compute_daily_transits(utc_moment, &out->natal, &out->t - ransits); - 12 tarot_draw_celtic_cross(tarot_seed, &out->spread); - -● Update(engine/src/reading.c) - ⎿  Added 2 lines, removed 2 lines - 67 fprintf(out, "===== %s =====\n", ui("ui.celtic_cross", "Celti - c Cross")); - 68 for (int i = 0; i < TAROT_SPREAD_SIZE; i++) { - 69 const TarotDraw *draw = &r->spread.positions[i]; - 70 - fprintf(out, "%-16s %s%s%s\n", tarot_position_name((CelticC - -rossPosition)i), - 70 + fprintf(out, "%-16s %s%s%s\n", tarot_position_name((CelticC - +rossPosition)i, r->gender), - 71 tarot_card_name(draw->card), draw->reversed ? " " : - "", - 72 draw->reversed ? ui("ui.reversed", "(Reversed)") : - ""); - 73 - fprintf(out, " %s\n", tarot_position_description((CelticCr - -ossPosition)i)); - 73 + fprintf(out, " %s\n", tarot_position_description((CelticCr - +ossPosition)i, r->gender)); - 74 fprintf(out, " %s\n", tarot_card_meaning(draw->card, draw- - >reversed)); - 75 } - 76 } - -● Update(engine/src/reading.c) - ⎿  Added 2 lines, removed 2 lines - 139 "

%s
\n" - 140 "
%s
\n" - 141 "
\n", - 142 - tarot_position_name((CelticCrossPosition)i), - 142 + tarot_position_name((CelticCrossPosition)i, r->gender), - 143 draw->reversed ? "reversed" : "", - 144 tarot_card_image_file(draw->card), tarot_card_name(draw- - >card), - 145 tarot_card_name(draw->card), draw->reversed ? " " : "", - 146 draw->reversed ? ui("ui.reversed", "(Reversed)") : "", - 147 - tarot_position_description((CelticCrossPosition)i), - 147 + tarot_position_description((CelticCrossPosition)i, r->ge - +nder), - 148 tarot_card_meaning(draw->card, draw->reversed)); - 149 } - 150 fprintf(out, "\n\n"); - -● Now the JSON output — add a "gender" field: - -● Update(engine/src/reading.c) - ⎿  Added 1 line - 164 - 165 struct tm *utc = gmtime(&r->utc_moment); - 166 fprintf(out, " \"date\": \"%04d-%02d-%02d\",\n", utc->tm_ye - ar + 1900, utc->tm_mon + 1, utc->tm_mday); - 167 + fprintf(out, " \"gender\": \"%s\",\n", tarot_gender_slug(r- - +>gender)); - 168 - 169 fprintf(out, " \"natal\": {\n \"bodies\": [\n"); - 170 for (int b = 0; b < NUM_BODIES; b++) { - -● Now the CLI main.c — add --gender: - -● Update(engine/src/main.c) - ⎿  Added 4 lines - 15 "Usage: %s --seed --birth-date YYYY-MM-DD --birth- - time HH:MM\n" - 16 " --birth-utc-offset --birth-lat --b - irth-lon \n" - 17 " [--date YYYY-MM-DDTHH:MM] [--format text|html|js - on]\n" - 18 + " [--gender male|female|unspecified]\n" - 19 " [--lang ] [--i18n-dir ]\n\n" - 20 " --seed Tarot seed string; same seed -> same - Celtic Cross spread.\n" - 21 " --birth-date/time Birth date/time in local clock time. - \n" - 22 " --birth-utc-offset Hours to subtract from local birth t - ime to get UTC (e.g. 2 for CEST).\n" - 23 " --birth-lat/lon Birth location in decimal degrees (n - orth/east positive).\n" - 24 " --date UTC moment for today's transits/taro - t draw. Defaults to now.\n" - 25 + " --gender Who the reading is for - only affect - +s the Celtic Cross\n" - 26 + " position text's pronouns, never the - +chart/draw itself.\n" - 27 + " Defaults to unspecified (pronoun-neu - +tral phrasing).\n" - 28 " --format Output format, defaults to text. htm - l links card art via\n" - 29 " img/, i.e. it expects to be sa - ved next to the img/\n" - 30 " directory that `make images` copies - into dist/. json uses\n" - -● Update(engine/src/main.c) - ⎿  Added 1 line - 67 const char *birth_utc_offset = NULL, *birth_lat = NULL, *birt - h_lon = NULL; - 68 const char *date_str = NULL, *format_str = "text"; - 69 const char *lang = "en", *i18n_dir_arg = NULL; - 70 + const char *gender_str = "unspecified"; - 71 - 72 for (int i = 1; i < argc; i++) { - 73 const char *arg = argv[i]; - -● Update(engine/src/main.c) - ⎿  Added 1 line - 80 else if (strcmp(arg, "--birth-lon") == 0) rc = require_arg( - argc, argv, &i, arg, &birth_lon); - 81 else if (strcmp(arg, "--date") == 0) rc = require_arg(argc, - argv, &i, arg, &date_str); - 82 else if (strcmp(arg, "--format") == 0) rc = require_arg(arg - c, argv, &i, arg, &format_str); - 83 + else if (strcmp(arg, "--gender") == 0) rc = require_arg(arg - +c, argv, &i, arg, &gender_str); - 84 else if (strcmp(arg, "--lang") == 0) rc = require_arg(argc, - argv, &i, arg, &lang); - 85 else if (strcmp(arg, "--i18n-dir") == 0) rc = require_arg(a - rgc, argv, &i, arg, &i18n_dir_arg); - 86 else if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") == - 0) { - -● Now parse gender_str into the Gender enum and pass it to reading_generate(): - -● Update(engine/src/main.c) - ⎿  Added 9 lines - 142 return 1; - 143 } - 144 - 145 + Gender gender; - 146 + if (strcmp(gender_str, "unspecified") == 0) gender = GENDER_ - +UNSPECIFIED; - 147 + else if (strcmp(gender_str, "male") == 0) gender = GENDER_MA - +LE; - 148 + else if (strcmp(gender_str, "female") == 0) gender = GENDER_ - +FEMALE; - 149 + else { - 150 + fprintf(stderr, "Invalid --gender, expected male, female, - +or unspecified\n"); - 151 + return 1; - 152 + } - 153 + - 154 char i18n_path[512]; - 155 if (i18n_dir_arg) { - 156 snprintf(i18n_path, sizeof i18n_path, "%s/%s.lang", i18n_d - ir_arg, lang); - -● Update(engine/src/main.c) - ⎿  Added 1 line, removed 1 line - 163 } - 164 - 165 DailyReading reading; - 166 - reading_generate(seed, &birth, utc_moment, &reading); - 166 + reading_generate(seed, &birth, gender, utc_moment, &reading) - +; - 167 - 168 if (format == FORMAT_HTML) { - 169 reading_print_html(&reading, stdout); - -● Task #48 done. Now let's add the gendered i18n text (task #49). - -● Update(engine/i18n/en.lang) - ⎿  Added 39 lines, removed 12 lines - 79 aspect.opposition=Opposition - 80 - 81 # --- Celtic Cross position names/descriptions (tarot_data.c), - Waite's - 82 -# own wording from "An Ancient Celtic Method of Divination" -- - -- - 82 +# own wording from "An Ancient Celtic Method of Divination". W - +aite - 83 +# addresses the querent as "him" throughout; the unsuffixed ke - +y is a - 84 +# pronoun-neutral paraphrase (singular "they") used when no ge - +nder is - 85 +# configured, the ".male" key is Waite's own wording verbatim, - + and the - 86 +# ".female" key is an original paraphrase - see tarot_data.c's - + own - 87 +# comment on Gender. --- - 88 position.present.name=The Present - 84 -position.present.desc=This covers him: the general influence a - -ffecting the matter. - 89 +position.present.desc=This covers them: the general influence - +affecting the matter. - 90 +position.present.desc.male=This covers him: the general influe - +nce affecting the matter. - 91 +position.present.desc.female=This covers her: the general infl - +uence affecting the matter. - 92 position.challenge.name=The Challenge - 86 -position.challenge.desc=This crosses him: the nature of the ob - -stacle in the matter. - 93 +position.challenge.desc=This crosses them: the nature of the o - +bstacle in the matter. - 94 +position.challenge.desc.male=This crosses him: the nature of t - +he obstacle in the matter. - 95 +position.challenge.desc.female=This crosses her: the nature of - + the obstacle in the matter. - 96 position.crown.name=The Crown - 88 -position.crown.desc=This crowns him: the aim or ideal, the bes - -t that can be achieved. - 97 +position.crown.desc=This crowns them: the aim or ideal, the be - +st that can be achieved. - 98 +position.crown.desc.male=This crowns him: the aim or ideal, th - +e best that can be achieved. - 99 +position.crown.desc.female=This crowns her: the aim or ideal, - +the best that can be achieved. - 100 position.foundation.name=The Foundation - 90 -position.foundation.desc=This is beneath him: the basis of the - - matter, already actual. - 101 +position.foundation.desc=This is beneath them: the basis of th - +e matter, already actual. - 102 +position.foundation.desc.male=This is beneath him: the basis o - +f the matter, already actual. - 103 +position.foundation.desc.female=This is beneath her: the basis - + of the matter, already actual. - 104 position.recent_past.name=The Recent Past - 92 -position.recent_past.desc=This is behind him: the influence th - -at is just passing away. - 105 +position.recent_past.desc=This is behind them: the influence t - +hat is just passing away. - 106 +position.recent_past.desc.male=This is behind him: the influen - +ce that is just passing away. - 107 +position.recent_past.desc.female=This is behind her: the influ - +ence that is just passing away. - 108 position.near_future.name=The Near Future - 94 -position.near_future.desc=This is before him: the influence no - -w coming into action. - 95 -position.attitude.name=Himself - 96 -position.attitude.desc=His position or attitude in the circums - -tances. - 97 -position.environment.name=His House - 98 -position.environment.desc=His environment and the tendencies a - -t work therein. - 109 +position.near_future.desc=This is before them: the influence n - +ow coming into action. - 110 +position.near_future.desc.male=This is before him: the influen - +ce now coming into action. - 111 +position.near_future.desc.female=This is before her: the influ - +ence now coming into action. - 112 +position.attitude.name=Themself - 113 +position.attitude.name.male=Himself - 114 +position.attitude.name.female=Herself - 115 +position.attitude.desc=Their position or attitude in the circu - +mstances. - 116 +position.attitude.desc.male=His position or attitude in the ci - +rcumstances. - 117 +position.attitude.desc.female=Her position or attitude in the - +circumstances. - 118 +position.environment.name=Their House - 119 +position.environment.name.male=His House - 120 +position.environment.name.female=Her House - 121 +position.environment.desc=Their environment and the tendencies - + at work therein. - 122 +position.environment.desc.male=His environment and the tendenc - +ies at work therein. - 123 +position.environment.desc.female=Her environment and the tende - +ncies at work therein. - 124 position.hopes_and_fears.name=Hopes and Fears - 100 -position.hopes_and_fears.desc=His hopes or fears in the matter - -. - 125 +position.hopes_and_fears.desc=Their hopes or fears in the matt - +er. - 126 +position.hopes_and_fears.desc.male=His hopes or fears in the m - +atter. - 127 +position.hopes_and_fears.desc.female=Her hopes or fears in the - + matter. - 128 position.outcome.name=The Outcome - 129 position.outcome.desc=What will come: the final result of the - matter. - 130 - -● Update(engine/i18n/de.lang) - ⎿  Added 40 lines, removed 12 lines - 71 aspect.trine=Trigon - 72 aspect.opposition=Opposition - 73 - 74 -# --- Positionen des Keltischen Kreuzes (tarot_data.c) --- - 74 +# --- Positionen des Keltischen Kreuzes (tarot_data.c). Waite - +spricht die - 75 +# fragende Person durchgehend mit "ihn/ihm/sein" an; der unsuf - +figierte - 76 +# Schlüssel ist eine geschlechtsneutrale Umschreibung (über "d - +ie fragende - 77 +# Person"/"die eigene", ohne Pronomen), die bei nicht angegebe - +nem - 78 +# Geschlecht verwendet wird, ".male" ist Waites eigener Wortla - +ut - 79 +# unverändert, ".female" eine für dieses Projekt geschriebene - +Umschreibung - - 80 +# siehe tarot_data.c's Kommentar zu Gender. --- - 81 position.present.name=Die Gegenwart - 76 -position.present.desc=Dies bedeckt ihn: der allgemeine Einflus - -s, der die Angelegenheit betrifft. - 82 +position.present.desc=Dies bedeckt die fragende Person: der al - +lgemeine Einfluss, der die Angelegenheit betrifft. - 83 +position.present.desc.male=Dies bedeckt ihn: der allgemeine Ei - +nfluss, der die Angelegenheit betrifft. - 84 +position.present.desc.female=Dies bedeckt sie: der allgemeine - +Einfluss, der die Angelegenheit betrifft. - 85 position.challenge.name=Die Herausforderung - 78 -position.challenge.desc=Dies kreuzt ihn: die Art des Hindernis - -ses in der Angelegenheit. - 86 +position.challenge.desc=Dies kreuzt die fragende Person: die A - +rt des Hindernisses in der Angelegenheit. - 87 +position.challenge.desc.male=Dies kreuzt ihn: die Art des Hind - +ernisses in der Angelegenheit. - 88 +position.challenge.desc.female=Dies kreuzt sie: die Art des Hi - +ndernisses in der Angelegenheit. - 89 position.crown.name=Die Krone - 80 -position.crown.desc=Dies krönt ihn: das Ziel oder Ideal, das B - -este, was erreicht werden kann. - 90 +position.crown.desc=Dies krönt die fragende Person: das Ziel o - +der Ideal, das Beste, was erreicht werden kann. - 91 +position.crown.desc.male=Dies krönt ihn: das Ziel oder Ideal, - +das Beste, was erreicht werden kann. - 92 +position.crown.desc.female=Dies krönt sie: das Ziel oder Ideal - +, das Beste, was erreicht werden kann. - 93 position.foundation.name=Das Fundament - 82 -position.foundation.desc=Dies liegt unter ihm: die Grundlage d - -er Angelegenheit, bereits Wirklichkeit. - 94 +position.foundation.desc=Dies liegt unter der fragenden Person - +: die Grundlage der Angelegenheit, bereits Wirklichkeit. - 95 +position.foundation.desc.male=Dies liegt unter ihm: die Grundl - +age der Angelegenheit, bereits Wirklichkeit. - 96 +position.foundation.desc.female=Dies liegt unter ihr: die Grun - +dlage der Angelegenheit, bereits Wirklichkeit. - 97 position.recent_past.name=Die jüngste Vergangenheit - 84 -position.recent_past.desc=Dies liegt hinter ihm: der Einfluss, - - der gerade vergeht. - 98 +position.recent_past.desc=Dies liegt hinter der fragenden Pers - +on: der Einfluss, der gerade vergeht. - 99 +position.recent_past.desc.male=Dies liegt hinter ihm: der Einf - +luss, der gerade vergeht. - 100 +position.recent_past.desc.female=Dies liegt hinter ihr: der Ei - +nfluss, der gerade vergeht. - 101 position.near_future.name=Die nahe Zukunft - 86 -position.near_future.desc=Dies liegt vor ihm: der Einfluss, de - -r jetzt wirksam wird. - 87 -position.attitude.name=Er selbst - 88 -position.attitude.desc=Seine Haltung oder Einstellung in den g - -egebenen Umständen. - 89 -position.environment.name=Sein Haus - 90 -position.environment.desc=Sein Umfeld und die darin wirkenden - -Tendenzen. - 102 +position.near_future.desc=Dies liegt vor der fragenden Person: - + der Einfluss, der jetzt wirksam wird. - 103 +position.near_future.desc.male=Dies liegt vor ihm: der Einflus - +s, der jetzt wirksam wird. - 104 +position.near_future.desc.female=Dies liegt vor ihr: der Einfl - +uss, der jetzt wirksam wird. - 105 +position.attitude.name=Die fragende Person selbst - 106 +position.attitude.name.male=Er selbst - 107 +position.attitude.name.female=Sie selbst - 108 +position.attitude.desc=Die eigene Haltung oder Einstellung in - +den gegebenen Umständen. - 109 +position.attitude.desc.male=Seine Haltung oder Einstellung in - +den gegebenen Umständen. - 110 +position.attitude.desc.female=Ihre Haltung oder Einstellung in - + den gegebenen Umständen. - 111 +position.environment.name=Das eigene Haus - 112 +position.environment.name.male=Sein Haus - 113 +position.environment.name.female=Ihr Haus - 114 +position.environment.desc=Das eigene Umfeld und die darin wirk - +enden Tendenzen. - 115 +position.environment.desc.male=Sein Umfeld und die darin wirke - +nden Tendenzen. - 116 +position.environment.desc.female=Ihr Umfeld und die darin wirk - +enden Tendenzen. - 117 position.hopes_and_fears.name=Hoffnungen und Ängste - 92 -position.hopes_and_fears.desc=Seine Hoffnungen oder Ängste in - -der Angelegenheit. - 118 +position.hopes_and_fears.desc=Die eigenen Hoffnungen oder Ängs - +te in der Angelegenheit. - 119 +position.hopes_and_fears.desc.male=Seine Hoffnungen oder Ängst - +e in der Angelegenheit. - 120 +position.hopes_and_fears.desc.female=Ihre Hoffnungen oder Ängs - +te in der Angelegenheit. - 121 position.outcome.name=Das Ergebnis - 122 position.outcome.desc=Was kommen wird: das endgültige Ergebnis - der Angelegenheit. - 123 - -● Task #49 done. Now onto the interpreter side (task #50). - Read 1 file (ctrl+o to expand) - -● Update(interpreter/src/reading_io.c) - ⎿  Added 6 lines - 39 "near_future", "attitude", "environment", "hopes_and_fears", - "outcome", - 40 }; - 41 - 42 +/* Mirrors tarot_data.c's own k_gender_slug, same duplication p - +olicy as - 43 + * k_card_slug/k_position_slug above (no reverse lookup exposed - + there). */ - 44 +static const char *const k_gender_slug[3] = { - 45 + "unspecified", "male", "female", - 46 +}; - 47 + - 48 static bool body_from_slug(const char *slug, Body *out) { - 49 for (int i = 0; i < NUM_BODIES; i++) { - 50 if (strcmp(slug, k_body_slug[i]) == 0) { - -● Update(interpreter/src/reading_io.c) - ⎿  Added 10 lines - 110 return false; - 111 } - 112 - 113 +static bool gender_from_slug(const char *slug, Gender *out) { - 114 + for (int i = 0; i < 3; i++) { - 115 + if (strcmp(slug, k_gender_slug[i]) == 0) { - 116 + *out = (Gender)i; - 117 + return true; - 118 + } - 119 + } - 120 + return false; - 121 +} - 122 + - 123 static bool position_from_slug(const char *slug, CelticCrossPo - sition *out) { - 124 for (int i = 0; i < TAROT_SPREAD_SIZE; i++) { - 125 if (strcmp(slug, k_position_slug[i]) == 0) { - -● Update(interpreter/src/reading_io.c) - ⎿  Added 12 lines - 171 out->utc_moment = timegm(&tm_date); - 172 } - 173 - 174 +/* Parses the top-level "gender" field (see reading_print_json - + in - 175 + * engine/src/reading.c) into out->gender. Lenient like parse_ - +date(): a - 176 + * missing or unrecognized value just leaves out->gender at - 177 + * GENDER_UNSPECIFIED (0, from reading_load_json's own memset) - +, not a - 178 + * load failure - it only affects Celtic Cross position text p - +ronouns, - 179 + * nothing scoring depends on it. */ - 180 +static void parse_gender(const JsonValue *gender, DailyReading - + *out) { - 181 + const char *gender_str = gender ? json_as_string(gender) : N - +ULL; - 182 + if (!gender_str) return; - 183 + gender_from_slug(gender_str, &out->gender); - 184 +} - 185 + - 186 bool reading_load_json(const char *text, size_t length, DailyR - eading *out) { - 187 memset(out, 0, sizeof(*out)); - 188 - -● Update(interpreter/src/reading_io.c) - ⎿  Added 1 line - 225 out->transits.aspect_count = filled; - 226 - 227 parse_date(json_object_get(root, "date"), out); - 228 + parse_gender(json_object_get(root, "gender"), out); - 229 - 230 const JsonValue *natal = json_object_get(root, "natal"); - 231 parse_bodies(natal ? json_object_get(natal, "bodies") : NULL - , out->natal.bodies); - -● Now update interpreter/src/reading_io.h's doc comment, and - interpreter/src/main.c's two call sites. - - Read 1 file (ctrl+o to expand) - -● Update(interpreter/src/reading_io.h) - ⎿  Added 6 lines, removed 3 lines - 16 * significance.c), out->natal.bodies[]/out->transits.bodies[] - (sign + - 17 * house per body, used only for display), out->spread.position - s[] - 18 * (card + reversed per Celtic Cross position, used only by - 19 - * guidance.c), and out->utc_moment (the top-level "date" field - -, parsed - 19 + * guidance.c), out->utc_moment (the top-level "date" field, pa - +rsed - 20 * back to midnight UTC of that date - used only for display, e - .g. main.c - 21 - * printing which day a reading is for). out->natal.ascendant_l - -ongitude/ - 22 - * houses[] are left zeroed - nothing reads them yet. - 21 + * printing which day a reading is for), and out->gender (the t - +op-level - 22 + * "gender" field - used only for the Celtic Cross position tex - +t's - 23 + * pronouns, e.g. tarot_position_name()/tarot_position_descript - +ion()). - 24 + * out->natal.ascendant_longitude/houses[] are left zeroed - no - +thing - 25 + * reads them yet. - 26 * - 27 * Returns false on malformed JSON, or if "transits"."aspects" - isn't - 28 * present as an array (an empty array is fine - that's a real - "no - -● Now let's find the two call sites in interpreter/src/main.c. - - Searched for 1 pattern, read 1 file (ctrl+o to expand) - -● I'll change print_celtic_cross_text to take Gender and update its call site. - -● Update(interpreter/src/main.c) - ⎿  Added 3 lines, removed 3 lines - 226 - 227 /* ===== --format text ===== */ - 228 - 229 -static void print_celtic_cross_text(FILE *out, const CelticCro - -ssSpread *spread) { - 229 +static void print_celtic_cross_text(FILE *out, const CelticCro - +ssSpread *spread, Gender gender) { - 230 fprintf(out, "%s:\n", ui("ui.celtic_cross", "Celtic Cross")) - ; - 231 for (int i = 0; i < TAROT_SPREAD_SIZE; i++) { - 232 const TarotDraw *draw = &spread->positions[i]; - 233 - fprintf(out, " %d. %s: %s%s\n", i + 1, tarot_position_nam - -e((CelticCrossPosition)i), - 233 + fprintf(out, " %d. %s: %s%s\n", i + 1, tarot_position_nam - +e((CelticCrossPosition)i, gender), - 234 tarot_card_name(draw->card), reversed_marker(draw- - >reversed)); - 235 - fprintf(out, " %s\n", tarot_position_description((Celt - -icCrossPosition)i)); - 235 + fprintf(out, " %s\n", tarot_position_description((Celt - +icCrossPosition)i, gender)); - 236 fprintf(out, " %s\n", tarot_card_meaning(draw->card, d - raw->reversed)); - 237 } - 238 } - -● Update(interpreter/src/main.c) - ⎿  Added 1 line, removed 1 line - 269 } - 270 fprintf(out, "\n"); - 271 - 272 - print_celtic_cross_text(out, &reading->spread); - 272 + print_celtic_cross_text(out, &reading->spread, reading->gend - +er); - 273 } - 274 - 275 /* ===== --format html ===== */ - -● Now let's view the HTML side (print_card_html and its call sites) plus the - JSON format section. - - Read 1 file (ctrl+o to expand) - -● Update(interpreter/src/main.c) - ⎿  Added 4 lines, removed 3 lines - 297 /* One Celtic Cross card as a ".card" div - shared between the - 298 * Attitude/Outcome preview (right after guidance) and the ful - l spread - 299 * further down, so both render identically. */ - 300 -static void print_card_html(FILE *out, CelticCrossPosition pos - -ition, const TarotDraw *draw) { - 300 +static void print_card_html(FILE *out, CelticCrossPosition pos - +ition, const TarotDraw *draw, - 301 + Gender gender) { - 302 fprintf(out, - 303 "
\n" - 304 "
%s
\n" - ... - 307 "
%s
\n" - 308 "
%s
\n" - 309 "
\n", - 309 - tarot_position_name(position), draw->reversed ? "reversed" - - : "", - 310 + tarot_position_name(position, gender), draw->reversed ? "r - +eversed" : "", - 311 tarot_card_image_file(draw->card), tarot_card_name(draw->c - ard), tarot_card_name(draw->card), - 311 - reversed_marker(draw->reversed), tarot_position_descriptio - -n(position), - 312 + reversed_marker(draw->reversed), tarot_position_descriptio - +n(position, gender), - 313 tarot_card_meaning(draw->card, draw->reversed)); - 314 } - 315 - -● Update(interpreter/src/main.c) - ⎿  Added 2 lines, removed 2 lines - 353 fprintf(out, "

%s

\n", ui("interp.heading_guidance", - "Guidance")); - 354 - 355 fprintf(out, "
\n"); - 356 - print_card_html(out, POSITION_ATTITUDE, &reading->spread.pos - -itions[POSITION_ATTITUDE]); - 357 - print_card_html(out, POSITION_OUTCOME, &reading->spread.posi - -tions[POSITION_OUTCOME]); - 356 + print_card_html(out, POSITION_ATTITUDE, &reading->spread.pos - +itions[POSITION_ATTITUDE], reading->gender); - 357 + print_card_html(out, POSITION_OUTCOME, &reading->spread.posi - +tions[POSITION_OUTCOME], reading->gender); - 358 fprintf(out, "
\n"); - 359 - 360 fprintf(out, "
\n"); - -● Update(interpreter/src/main.c) - ⎿  Added 1 line, removed 1 line - 387 - 388 fprintf(out, "

%s

\n
\n", ui("ui - .celtic_cross", "Celtic Cross")); - 389 for (int i = 0; i < TAROT_SPREAD_SIZE; i++) { - 390 - print_card_html(out, (CelticCrossPosition)i, &reading->spr - -ead.positions[i]); - 390 + print_card_html(out, (CelticCrossPosition)i, &reading->spr - +ead.positions[i], reading->gender); - 391 } - 392 fprintf(out, "
\n"); - 393 - - Searched for 1 pattern, read 1 file (ctrl+o to expand) - -● Task #50 complete. Now the watch — config persistence, config page, and - reading pipeline (task #51). -● Update(watch/src/c/config.h) - ⎿  Added 3 lines - 13 typedef struct { - 14 bool configured; /* false until the config page has been subm - itted once */ - 15 BirthData birth; - 16 + Gender gender; /* who the reading is for - only affects Celti - +c Cross - 17 + * position text pronouns, see engine/src/taro - +t.h. - 18 + * Defaults to GENDER_UNSPECIFIED (0). */ - 19 char lang[8]; /* e.g. "en", "de" - matches engine/i18n/ - .lang's own code */ - 20 bool notify_enabled; - 21 int notify_hour; /* 0-23, the watch's own local wall-clock - time (i.e. - - Searched for 1 pattern (ctrl+o to expand) - -● astro.h doesn't include tarot.h. Let's add that include to config.h. - -● Update(watch/src/c/config.h) - ⎿  Added 1 line - 4 #include - 5 - 6 #include "../../../engine/src/astro.h" - 7 +#include "../../../engine/src/tarot.h" /* Gender */ - 8 - 9 /* Everything the watch needs that isn't computed fresh each da - y: birth - 10 * data (the same fields as dist/user.properties), display lang - uage, and - -● Now config.c — persist/load/log the gender field: - -● Update(watch/src/c/config.c) - ⎿  Added 1 line - 17 PKEY_BIRTH_UTC_OFFSET_MINUTES, - 18 PKEY_BIRTH_LAT_MICRODEG, - 19 PKEY_BIRTH_LON_MICRODEG, - 20 + PKEY_GENDER, - 21 PKEY_LANG, - 22 PKEY_NOTIFY_ENABLED, - 23 PKEY_NOTIFY_HOUR, - -● Update(watch/src/c/config.c) - ⎿  Added 1 line - 41 out->birth.utc_offset_hours = (double)(int32_t)persist_read_i - nt(PKEY_BIRTH_UTC_OFFSET_MINUTES) / 60.0; - 42 out->birth.latitude = (double)(int32_t)persist_read_int(PKEY_ - BIRTH_LAT_MICRODEG) / 1000000.0; - 43 out->birth.longitude = (double)(int32_t)persist_read_int(PKEY - _BIRTH_LON_MICRODEG) / 1000000.0; - 44 + out->gender = (Gender)persist_read_int(PKEY_GENDER); /* defau - +lts to 0 = GENDER_UNSPECIFIED */ - 45 - 46 if (persist_read_string(PKEY_LANG, out->lang, sizeof out->lan - g) <= 0) { - 47 strcpy(out->lang, "en"); - -● Update(watch/src/c/config.c) - ⎿  Added 1 line - 63 persist_write_int(PKEY_BIRTH_UTC_OFFSET_MINUTES, (int32_t)(cf - g->birth.utc_offset_hours * 60.0)); - 64 persist_write_int(PKEY_BIRTH_LAT_MICRODEG, (int32_t)(cfg->bir - th.latitude * 1000000.0)); - 65 persist_write_int(PKEY_BIRTH_LON_MICRODEG, (int32_t)(cfg->bir - th.longitude * 1000000.0)); - 66 + persist_write_int(PKEY_GENDER, (int32_t)cfg->gender); - 67 - 68 persist_write_string(PKEY_LANG, cfg->lang); - 69 - -● Update(watch/src/c/config.c) - ⎿  Added 3 lines, removed 2 lines - 82 APP_LOG(APP_LOG_LEVEL_INFO, "config: birth %04d-%02d-%02d %02 - d:%02d UTC offset %dmin", - 83 cfg->birth.year, cfg->birth.month, cfg->birth.day, cf - g->birth.hour, cfg->birth.minute, - 84 (int)(cfg->birth.utc_offset_hours * 60)); - 85 - APP_LOG(APP_LOG_LEVEL_INFO, "config: lat=%ld/1e6 lon=%ld/1e6 - -lang=%s", - 86 - (long)(cfg->birth.latitude * 1000000), (long)(cfg->bi - -rth.longitude * 1000000), cfg->lang); - 85 + APP_LOG(APP_LOG_LEVEL_INFO, "config: lat=%ld/1e6 lon=%ld/1e6 - +gender=%d lang=%s", - 86 + (long)(cfg->birth.latitude * 1000000), (long)(cfg->bi - +rth.longitude * 1000000), - 87 + (int)cfg->gender, cfg->lang); - 88 APP_LOG(APP_LOG_LEVEL_INFO, "config: notify_enabled=%d at %02 - d:%02d", - 89 cfg->notify_enabled, cfg->notify_hour, cfg->notify_mi - nute); - 90 } - -● Now watch/package.json — add GENDER to messageKeys: - - Read 1 file (ctrl+o to expand) - -● Update(watch/package.json) - ⎿  Added 1 line - 35 "BIRTH_UTC_OFFSET_MINUTES", - 36 "BIRTH_LAT_MICRODEG", - 37 "BIRTH_LON_MICRODEG", - 38 + "GENDER", - 39 "LANG", - 40 "NOTIFY_ENABLED", - 41 "NOTIFY_HOUR", - -● Update(watch/src/c/app_message.c) - ⎿  Added 1 line - 44 cfg.birth.longitude = v / 1000000.0; - 45 got_birth = true; - 46 } - 47 + if (read_int(iter, MESSAGE_KEY_GENDER, &v)) cfg.gender = (Gen - +der)v; - 48 - 49 Tuple *lang_tuple = dict_find(iter, MESSAGE_KEY_LANG); - 50 if (lang_tuple) { - - Ran 1 shell command (ctrl+o to expand) - -● Update(watch/src/pkjs/index.js) - ⎿  Added 8 lines - 20 { code: 'en', label: 'English' }, - 21 { code: 'de', label: 'Deutsch' } - 22 ]; - 23 +// Matches engine/src/tarot.h's Gender enum (GENDER_UNSPECIFIED - + = 0, - 24 +// GENDER_MALE = 1, GENDER_FEMALE = 2) - only affects the Celti - +c Cross - 25 +// position text's pronouns, never the chart/draw itself. - 26 +var GENDERS = [ - 27 + { code: 0, label: 'Prefer not to say' }, - 28 + { code: 1, label: 'Male' }, - 29 + { code: 2, label: 'Female' } - 30 +]; - 31 - 32 // 120x120 PNG of res/logo-small.png, inlined as base64 - this - page is a - 33 // bare `data:` URI with no server, so there's nowhere else to - load an - -● Now let's add buildGenderOptions() next to buildLanguageOptions(): - -● Update(watch/src/pkjs/index.js) - ⎿  Added 10 lines - 73 return html; - 74 } - 75 - 76 +function buildGenderOptions(selected) { - 77 + var html = ''; - 78 + for (var i = 0; i < GENDERS.length; i++) { - 79 + var gender = GENDERS[i]; - 80 + var isSelected = gender.code === selected ? ' selected' : ' - +'; - 81 + html += ''; - 82 + } - 83 + return html; - 84 +} - 85 + - 86 // Runs *inside* the webview (embedded as a