From fef68fe90d7f00d7d831c335b03119f11c2c927b Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 28 Jun 2026 08:57:44 +0300 Subject: or0: encapsulate STATS_* maps behind aggregator accessors The stats reader modules (stats-render.source.sh, stats-filter-album.source.sh) indexed stats-aggregate.source.sh's private STATS_* associative arrays directly, so a key-convention change in the aggregator would silently break both readers. Add a read API owned by stats-aggregate.source.sh (the data owner), mirroring album-render's ALBUM_VIEW_PAGE_BY_PHOTO / album_view_page_for_photo split: stats_total_photos - STATS_TOTALS[photos] denominator stats_filter_pagebase - (prefix,label) -> pagebase, hiding the STATS_FILTER_KEYSEP catkey encoding stats_filter_title - STATS_FILTER_TITLE[pagebase] stats_filter_photos - STATS_FILTER_PHOTOS[pagebase] list stats_filter_count - number of filter mini-albums stats_filter_pagebases - pagebases, LC_ALL=C-sorted (order owned here) stats_category_count/size/max/keys_by_count_desc - per-category count-array reads Route every cross-module STATS_* read through these accessors. The render module's _stats_max_count / _stats_keys_by_count_desc were duplicates of the new stats_category_max / stats_category_keys_by_count_desc, so they are removed. Behavior preserved exactly: missing-key semantics, iteration order (ordered ladders, calendar months, count-desc with LC_ALL=C tie-break, sorted pagebase enqueue) and generated HTML are byte-identical. Header docs updated to describe the accessor boundary. The arrays stay the backing store; only the cross-module READ path is encapsulated. Co-Authored-By: Claude Opus 4.8 --- bin/shuriken | 259 +++++++++++++++++++++++++---------- src/lib/stats-aggregate.source.sh | 136 +++++++++++++++++- src/lib/stats-filter-album.source.sh | 29 ++-- src/lib/stats-render.source.sh | 94 +++++-------- 4 files changed, 376 insertions(+), 142 deletions(-) diff --git a/bin/shuriken b/bin/shuriken index 49412e6..1076527 100755 --- a/bin/shuriken +++ b/bin/shuriken @@ -4045,8 +4045,13 @@ refresh_splash() { # Public API / handoff contract # ---------------------------------------------------------------------------- # collect_photo_exif_stats iterates the album's photos and fills these globals -# (all declared with `declare -gA` so the render tasks can read them after the -# call without namerefs): +# (all declared with `declare -gA`). They are this module's PRIVATE backing +# store: the reader modules (stats-render.source.sh, stats-filter-album.source.sh) +# do NOT index them directly -- they go through the accessor functions at the end +# of this file (stats_total_photos, stats_filter_pagebase, stats_filter_title, +# stats_filter_photos, stats_filter_count, stats_filter_pagebases, and the +# stats_category_* count accessors). That keeps the readers decoupled from the +# key conventions used below. The arrays filled here are: # # STATS_CAMERAS[] = count (camera leaderboard) # STATS_LENSES[] = count (sparse; may be empty) @@ -4696,15 +4701,146 @@ collect_photo_exif_stats() { done < <(incoming_image_files) } +# ---------------------------------------------------------------------------- +# Public read API (task or0) +# ---------------------------------------------------------------------------- +# The STATS_* maps above are this module's PRIVATE backing store. The reader +# modules (stats-render.source.sh, stats-filter-album.source.sh) must NOT index +# them directly: they call the accessors below instead. Keeping the maps private +# behind documented functions decouples the readers from the aggregator's key +# conventions (the "\x1f