refactor: collapse per-component staleness snapshots into a mirrored struct #62

Open
opened 2026-05-13 10:55:09 +00:00 by casper-stevens · 3 comments
Member

Problem

SlideMetaEntry maintains two parallel representations of the same staleness data:

Purpose Fields
Coarse "is stale?" last_generated (composite hash)
Fine "why stale?" (attribution) last_theme_hash, last_context_fingerprint, last_prompts_hash, last_image_model

The attribution fields in SlideMetaEntry manually mirror DeckInputsContext. Adding any new input component requires touching 4 places: DeckInputsContext, SlideMetaEntry (current + last_*), compose_inputs_hash, and compute_stale_reasons. The #[allow(clippy::too_many_arguments)] on compute_stale_reasons (8 args) is the surface symptom.

Note: staleness is checked live without a page reload, so the fix must not change the shape of the JSON returned by the staleness RPC — only the internal Rust data model and metadata.toml serialization need to be updated.

Fix

Introduce a ComponentSnapshots struct that mirrors DeckInputsContext:

#[derive(Default, Serialize, Deserialize)]
pub struct ComponentSnapshots {
    pub theme_hash: Option<String>,
    pub context_fingerprint: Option<String>,
    pub prompts_hash: Option<String>,
    pub image_model: Option<String>,
}

Replace the four flat last_* fields in SlideMetaEntry with last_components: Option<ComponentSnapshots>. Then compute_stale_reasons takes (current: &DeckInputsContext, last: Option<&ComponentSnapshots>) instead of 8 loose arguments.

Adding a new input component then becomes a 2-place change (add field to DeckInputsContext + ComponentSnapshots) instead of 4.

Also fix

  • compose_inputs_hash has a dead hasher.update(b"") placeholder reserved for a future layout hash — remove or replace with a real value when the feature lands.
  • slide_staleness calls deck_staleness and filters — fine for now, but worth noting as a future perf concern for large decks.

Files

  • crates/hero_slides_lib/src/hashing.rsSlideMetaEntry, DeckInputsContext, compose_inputs_hash
  • crates/hero_slides_lib/src/deck.rscompute_stale_reasons, deck_staleness, slide_staleness
## Problem `SlideMetaEntry` maintains two parallel representations of the same staleness data: | Purpose | Fields | |---|---| | Coarse "is stale?" | `last_generated` (composite hash) | | Fine "why stale?" (attribution) | `last_theme_hash`, `last_context_fingerprint`, `last_prompts_hash`, `last_image_model` | The attribution fields in `SlideMetaEntry` manually mirror `DeckInputsContext`. Adding any new input component requires touching 4 places: `DeckInputsContext`, `SlideMetaEntry` (current + last_*), `compose_inputs_hash`, and `compute_stale_reasons`. The `#[allow(clippy::too_many_arguments)]` on `compute_stale_reasons` (8 args) is the surface symptom. Note: staleness is checked live without a page reload, so the fix must not change the shape of the JSON returned by the staleness RPC — only the internal Rust data model and `metadata.toml` serialization need to be updated. ## Fix Introduce a `ComponentSnapshots` struct that mirrors `DeckInputsContext`: ```rust #[derive(Default, Serialize, Deserialize)] pub struct ComponentSnapshots { pub theme_hash: Option<String>, pub context_fingerprint: Option<String>, pub prompts_hash: Option<String>, pub image_model: Option<String>, } ``` Replace the four flat `last_*` fields in `SlideMetaEntry` with `last_components: Option<ComponentSnapshots>`. Then `compute_stale_reasons` takes `(current: &DeckInputsContext, last: Option<&ComponentSnapshots>)` instead of 8 loose arguments. **Adding a new input component then becomes a 2-place change** (add field to `DeckInputsContext` + `ComponentSnapshots`) instead of 4. ## Also fix - `compose_inputs_hash` has a dead `hasher.update(b"")` placeholder reserved for a future layout hash — remove or replace with a real value when the feature lands. - `slide_staleness` calls `deck_staleness` and filters — fine for now, but worth noting as a future perf concern for large decks. ## Files - `crates/hero_slides_lib/src/hashing.rs` — `SlideMetaEntry`, `DeckInputsContext`, `compose_inputs_hash` - `crates/hero_slides_lib/src/deck.rs` — `compute_stale_reasons`, `deck_staleness`, `slide_staleness`
Author
Member

Implementation Spec for Issue #62

Objective

Replace the four flat last_* attribution fields in SlideMetaEntry (last_theme_hash, last_context_fingerprint, last_prompts_hash, last_image_model) with a single nested last_components: Option<ComponentSnapshots> struct that mirrors DeckInputsContext. Simplify compute_stale_reasons to accept meta: Option<&SlideMetaEntry> and read snapshot fields via last_components instead of 8 loose arguments. Remove the dead hasher.update(b"") placeholder in compose_inputs_hash. The JSON shape returned by staleness RPCs must not change.


Requirements

  • ComponentSnapshots must derive Default, Clone, Serialize, Deserialize so Option<ComponentSnapshots> round-trips through metadata.toml correctly.
  • Serde must use #[serde(default, skip_serializing_if = "Option::is_none")] on last_components in SlideMetaEntry so legacy metadata.toml files (no last_components key) deserialize without error and fall back to None.
  • The StaleSlide type and all RPC response shapes must not change.
  • #[allow(clippy::too_many_arguments)] on compute_stale_reasons must be removed.
  • Dead hasher.update(b"") placeholder in compose_inputs_hash must be removed.
  • ComponentSnapshots must be re-exported from lib.rs.

Files to Modify

  • crates/hero_slides_lib/src/hashing.rs — add ComponentSnapshots, replace four flat last_* fields in SlideMetaEntry, remove dead hasher.update(b"") in compose_inputs_hash
  • crates/hero_slides_lib/src/deck.rs — update compute_stale_reasons body, update all four SlideMetaEntry construction sites, remove #[allow(clippy::too_many_arguments)]
  • crates/hero_slides_lib/src/lib.rs — add ComponentSnapshots to public re-exports
  • examples/hero_slides_intro/metadata.toml — migrate flat keys to TOML subtable format

Implementation Plan

Step 1: Add ComponentSnapshots struct to hashing.rs

File: crates/hero_slides_lib/src/hashing.rs

  • After the closing brace of DeckInputsContext, insert a new struct:
/// Snapshot of all `DeckInputsContext` components at the time a slide was last generated.
/// Stored as `last_components` in `SlideMetaEntry`; mirrors `DeckInputsContext` field-for-field.
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct ComponentSnapshots {
    pub theme_hash: Option<String>,
    pub context_fingerprint: Option<String>,
    pub prompts_hash: Option<String>,
    pub image_model: Option<String>,
}

Dependencies: none

Step 2: Replace four flat fields in SlideMetaEntry with last_components

File: crates/hero_slides_lib/src/hashing.rs

  • Remove last_theme_hash, last_context_fingerprint, last_prompts_hash, last_image_model from SlideMetaEntry.
  • Add: #[serde(default, skip_serializing_if = "Option::is_none")] pub last_components: Option<ComponentSnapshots>,

Dependencies: Step 1

Step 3: Remove dead hasher.update(b"") in compose_inputs_hash

File: crates/hero_slides_lib/src/hashing.rs

  • In compose_inputs_hash, remove the layout-hash placeholder lines (comment + hasher.update(b"") + hasher.update(b"\0")).

Dependencies: Step 2

Step 4: Update compute_stale_reasons body in deck.rs

File: crates/hero_slides_lib/src/deck.rs

  • Remove #[allow(clippy::too_many_arguments)].
  • In the body, replace all m.last_theme_hash.as_deref() with m.last_components.as_ref().and_then(|c| c.theme_hash.as_deref()) and similarly for the other three fields.

Dependencies: Step 2

Step 5: Update four SlideMetaEntry construction sites in deck.rs

File: crates/hero_slides_lib/src/deck.rs

At each of the four sites that currently set four flat last_* fields, replace with:

last_components: Some(ComponentSnapshots {
    theme_hash: Some(inputs_ctx.theme_hash.clone()),
    context_fingerprint: Some(inputs_ctx.context_fingerprint.clone()),
    prompts_hash: Some(inputs_ctx.prompts_hash.clone()),
    image_model: Some(model_id.to_string()),
}),

(Adjust field sources per site — see Notes below for the linked-slide fast path.)

Dependencies: Step 2, Step 4

Step 6: Add ComponentSnapshots to lib.rs re-exports

File: crates/hero_slides_lib/src/lib.rs

  • Add ComponentSnapshots to the existing hashing re-export line alongside SlideMetaEntry.

Dependencies: Step 1

Step 7: Migrate examples/hero_slides_intro/metadata.toml

File: examples/hero_slides_intro/metadata.toml

  • Replace flat last_theme_hash, last_context_fingerprint, last_prompts_hash keys under each slide section with the TOML subtable [slides.<id>.last_components].

Dependencies: Step 2


Acceptance Criteria

  • SlideMetaEntry has no fields named last_theme_hash, last_context_fingerprint, last_prompts_hash, or last_image_model
  • ComponentSnapshots struct exists in hashing.rs with the four Option<String> fields
  • SlideMetaEntry has last_components: Option<ComponentSnapshots> with correct serde attributes
  • compose_inputs_hash has no hasher.update(b"") placeholder
  • compute_stale_reasons has no #[allow(clippy::too_many_arguments)]
  • All four write sites in deck.rs construct a ComponentSnapshots value
  • ComponentSnapshots is in lib.rs re-exports
  • examples/hero_slides_intro/metadata.toml uses the TOML subtable format
  • cargo check and cargo test pass with no new warnings
  • StaleSlide RPC type and its JSON shape are unchanged

Notes

  • SlideMetaEntry.image_model (the current per-slide model override) stays as a flat field — only the snapshot (last_image_model) moves into ComponentSnapshots as image_model.
  • Legacy metadata.toml files without last_components deserialize as None, which falls through to the existing "inputs_changed" fallback — no migration code needed.
  • Removing hasher.update(b"") changes the composite hash formula, causing a one-time regeneration of all slides on next staleness check — this is deliberate and acceptable.
  • slide_copy_to_deck uses SlideMetaEntry { hash, ..Default::default() } and requires no change.
  • No changes needed outside hero_slides_lib; no other crate references the flat last_* fields.
## Implementation Spec for Issue #62 ### Objective Replace the four flat `last_*` attribution fields in `SlideMetaEntry` (`last_theme_hash`, `last_context_fingerprint`, `last_prompts_hash`, `last_image_model`) with a single nested `last_components: Option<ComponentSnapshots>` struct that mirrors `DeckInputsContext`. Simplify `compute_stale_reasons` to accept `meta: Option<&SlideMetaEntry>` and read snapshot fields via `last_components` instead of 8 loose arguments. Remove the dead `hasher.update(b"")` placeholder in `compose_inputs_hash`. The JSON shape returned by staleness RPCs must not change. --- ### Requirements - `ComponentSnapshots` must derive `Default`, `Clone`, `Serialize`, `Deserialize` so `Option<ComponentSnapshots>` round-trips through `metadata.toml` correctly. - Serde must use `#[serde(default, skip_serializing_if = "Option::is_none")]` on `last_components` in `SlideMetaEntry` so legacy `metadata.toml` files (no `last_components` key) deserialize without error and fall back to `None`. - The `StaleSlide` type and all RPC response shapes must not change. - `#[allow(clippy::too_many_arguments)]` on `compute_stale_reasons` must be removed. - Dead `hasher.update(b"")` placeholder in `compose_inputs_hash` must be removed. - `ComponentSnapshots` must be re-exported from `lib.rs`. --- ### Files to Modify - `crates/hero_slides_lib/src/hashing.rs` — add `ComponentSnapshots`, replace four flat `last_*` fields in `SlideMetaEntry`, remove dead `hasher.update(b"")` in `compose_inputs_hash` - `crates/hero_slides_lib/src/deck.rs` — update `compute_stale_reasons` body, update all four `SlideMetaEntry` construction sites, remove `#[allow(clippy::too_many_arguments)]` - `crates/hero_slides_lib/src/lib.rs` — add `ComponentSnapshots` to public re-exports - `examples/hero_slides_intro/metadata.toml` — migrate flat keys to TOML subtable format --- ### Implementation Plan #### Step 1: Add `ComponentSnapshots` struct to `hashing.rs` File: `crates/hero_slides_lib/src/hashing.rs` - After the closing brace of `DeckInputsContext`, insert a new struct: ```rust /// Snapshot of all `DeckInputsContext` components at the time a slide was last generated. /// Stored as `last_components` in `SlideMetaEntry`; mirrors `DeckInputsContext` field-for-field. #[derive(Default, Clone, Serialize, Deserialize)] pub struct ComponentSnapshots { pub theme_hash: Option<String>, pub context_fingerprint: Option<String>, pub prompts_hash: Option<String>, pub image_model: Option<String>, } ``` Dependencies: none #### Step 2: Replace four flat fields in `SlideMetaEntry` with `last_components` File: `crates/hero_slides_lib/src/hashing.rs` - Remove `last_theme_hash`, `last_context_fingerprint`, `last_prompts_hash`, `last_image_model` from `SlideMetaEntry`. - Add: `#[serde(default, skip_serializing_if = "Option::is_none")] pub last_components: Option<ComponentSnapshots>,` Dependencies: Step 1 #### Step 3: Remove dead `hasher.update(b"")` in `compose_inputs_hash` File: `crates/hero_slides_lib/src/hashing.rs` - In `compose_inputs_hash`, remove the layout-hash placeholder lines (comment + `hasher.update(b"")` + `hasher.update(b"\0")`). Dependencies: Step 2 #### Step 4: Update `compute_stale_reasons` body in `deck.rs` File: `crates/hero_slides_lib/src/deck.rs` - Remove `#[allow(clippy::too_many_arguments)]`. - In the body, replace all `m.last_theme_hash.as_deref()` with `m.last_components.as_ref().and_then(|c| c.theme_hash.as_deref())` and similarly for the other three fields. Dependencies: Step 2 #### Step 5: Update four `SlideMetaEntry` construction sites in `deck.rs` File: `crates/hero_slides_lib/src/deck.rs` At each of the four sites that currently set four flat `last_*` fields, replace with: ```rust last_components: Some(ComponentSnapshots { theme_hash: Some(inputs_ctx.theme_hash.clone()), context_fingerprint: Some(inputs_ctx.context_fingerprint.clone()), prompts_hash: Some(inputs_ctx.prompts_hash.clone()), image_model: Some(model_id.to_string()), }), ``` (Adjust field sources per site — see Notes below for the linked-slide fast path.) Dependencies: Step 2, Step 4 #### Step 6: Add `ComponentSnapshots` to `lib.rs` re-exports File: `crates/hero_slides_lib/src/lib.rs` - Add `ComponentSnapshots` to the existing `hashing` re-export line alongside `SlideMetaEntry`. Dependencies: Step 1 #### Step 7: Migrate `examples/hero_slides_intro/metadata.toml` File: `examples/hero_slides_intro/metadata.toml` - Replace flat `last_theme_hash`, `last_context_fingerprint`, `last_prompts_hash` keys under each slide section with the TOML subtable `[slides.<id>.last_components]`. Dependencies: Step 2 --- ### Acceptance Criteria - [ ] `SlideMetaEntry` has no fields named `last_theme_hash`, `last_context_fingerprint`, `last_prompts_hash`, or `last_image_model` - [ ] `ComponentSnapshots` struct exists in `hashing.rs` with the four `Option<String>` fields - [ ] `SlideMetaEntry` has `last_components: Option<ComponentSnapshots>` with correct serde attributes - [ ] `compose_inputs_hash` has no `hasher.update(b"")` placeholder - [ ] `compute_stale_reasons` has no `#[allow(clippy::too_many_arguments)]` - [ ] All four write sites in `deck.rs` construct a `ComponentSnapshots` value - [ ] `ComponentSnapshots` is in `lib.rs` re-exports - [ ] `examples/hero_slides_intro/metadata.toml` uses the TOML subtable format - [ ] `cargo check` and `cargo test` pass with no new warnings - [ ] `StaleSlide` RPC type and its JSON shape are unchanged --- ### Notes - `SlideMetaEntry.image_model` (the current per-slide model override) stays as a flat field — only the snapshot (`last_image_model`) moves into `ComponentSnapshots` as `image_model`. - Legacy `metadata.toml` files without `last_components` deserialize as `None`, which falls through to the existing `"inputs_changed"` fallback — no migration code needed. - Removing `hasher.update(b"")` changes the composite hash formula, causing a one-time regeneration of all slides on next staleness check — this is deliberate and acceptable. - `slide_copy_to_deck` uses `SlideMetaEntry { hash, ..Default::default() }` and requires no change. - No changes needed outside `hero_slides_lib`; no other crate references the flat `last_*` fields.
Author
Member

Test Results

  • Total: 119
  • Passed: 119
  • Failed: 0

All 111 unit tests, 6 integration tests, and 2 doc tests passed.

Also fixed two pre-existing test compilation errors in deck.rs (test-only Theme struct literals missing source_pdf_file and source_pdf_pages fields added in a prior commit).

## Test Results - Total: 119 - Passed: 119 - Failed: 0 All 111 unit tests, 6 integration tests, and 2 doc tests passed. Also fixed two pre-existing test compilation errors in `deck.rs` (test-only `Theme` struct literals missing `source_pdf_file` and `source_pdf_pages` fields added in a prior commit).
Author
Member

Implementation Summary

All changes are in crates/hero_slides_lib.

Changes made

src/hashing.rs

  • Added ComponentSnapshots struct (derives Debug, Default, Clone, Serialize, Deserialize) with four Option<String> fields mirroring DeckInputsContext: theme_hash, context_fingerprint, prompts_hash, image_model
  • Replaced four flat fields in SlideMetaEntry (last_theme_hash, last_context_fingerprint, last_prompts_hash, last_image_model) with a single last_components: Option<ComponentSnapshots> field with #[serde(default, skip_serializing_if = "Option::is_none")]
  • Removed dead hasher.update(b"") layout-hash placeholder and its associated comment from compose_inputs_hash

src/deck.rs

  • Removed #[allow(clippy::too_many_arguments)] from compute_stale_reasons
  • Updated compute_stale_reasons body to read staleness attribution from m.last_components instead of four flat fields
  • Updated all four SlideMetaEntry construction sites (in deck_generate, slide_generate_with_selection, slide_generate_with_context, and the linked-slide fast path) to write last_components: Some(ComponentSnapshots { ... }) instead of four flat fields
  • Fixed two pre-existing test compilation errors (missing Theme struct fields)

src/lib.rs

  • Added ComponentSnapshots to the public re-export list

examples/hero_slides_intro/metadata.toml

  • Migrated flat last_theme_hash, last_context_fingerprint, last_prompts_hash keys to the TOML subtable format [slides.05_themes.last_components]

Acceptance criteria

  • SlideMetaEntry has no flat last_* fields
  • ComponentSnapshots exists with four Option<String> fields
  • compute_stale_reasons has no #[allow(clippy::too_many_arguments)]
  • All four write sites produce a ComponentSnapshots value
  • ComponentSnapshots is publicly exported
  • compose_inputs_hash has no dead placeholder
  • Example metadata.toml uses TOML subtable format
  • All 119 tests pass
  • StaleSlide RPC type and JSON shape unchanged
## Implementation Summary All changes are in `crates/hero_slides_lib`. ### Changes made **`src/hashing.rs`** - Added `ComponentSnapshots` struct (derives `Debug`, `Default`, `Clone`, `Serialize`, `Deserialize`) with four `Option<String>` fields mirroring `DeckInputsContext`: `theme_hash`, `context_fingerprint`, `prompts_hash`, `image_model` - Replaced four flat fields in `SlideMetaEntry` (`last_theme_hash`, `last_context_fingerprint`, `last_prompts_hash`, `last_image_model`) with a single `last_components: Option<ComponentSnapshots>` field with `#[serde(default, skip_serializing_if = "Option::is_none")]` - Removed dead `hasher.update(b"")` layout-hash placeholder and its associated comment from `compose_inputs_hash` **`src/deck.rs`** - Removed `#[allow(clippy::too_many_arguments)]` from `compute_stale_reasons` - Updated `compute_stale_reasons` body to read staleness attribution from `m.last_components` instead of four flat fields - Updated all four `SlideMetaEntry` construction sites (in `deck_generate`, `slide_generate_with_selection`, `slide_generate_with_context`, and the linked-slide fast path) to write `last_components: Some(ComponentSnapshots { ... })` instead of four flat fields - Fixed two pre-existing test compilation errors (missing `Theme` struct fields) **`src/lib.rs`** - Added `ComponentSnapshots` to the public re-export list **`examples/hero_slides_intro/metadata.toml`** - Migrated flat `last_theme_hash`, `last_context_fingerprint`, `last_prompts_hash` keys to the TOML subtable format `[slides.05_themes.last_components]` ### Acceptance criteria - `SlideMetaEntry` has no flat `last_*` fields - `ComponentSnapshots` exists with four `Option<String>` fields - `compute_stale_reasons` has no `#[allow(clippy::too_many_arguments)]` - All four write sites produce a `ComponentSnapshots` value - `ComponentSnapshots` is publicly exported - `compose_inputs_hash` has no dead placeholder - Example `metadata.toml` uses TOML subtable format - All 119 tests pass - `StaleSlide` RPC type and JSON shape unchanged
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_slides#62
No description provided.