hero_slides bucket C: slide linking + staleness + folder picker (closes 5/5 remaining undispatched methods) #54

Closed
opened 2026-05-10 12:48:31 +00:00 by mik-tf · 1 comment
Owner

Status

Design locked 2026-05-10 by mik-tf. Implementation queued for session 89; will land as 3 mini-PRs (folder.pick → staleness pair → linking pair). The four "Recommendations needing approval" below are now Decisions — no further design discussion required before code.

TL;DR

Three small features close out the hero_slides JSON-RPC dispatcher (the last 5 of 15 undispatched methods surfaced by #49's schema audit). ~280 LOC across 3 mini-PRs. Design locked 2026-05-10 by mik-tf — recommendations below stand as-decided. Implementation queued for session 89.

Feature User impact Methods Effort
Slide linking Edit a slide once, propagate to every deck that includes it slide.setLink, slide.clearLink ~150 LOC
Staleness tracking Dashboard shows which slides need re-generation, so "Generate All" only re-renders what changed slide.getStaleness, deck.staleness ~80 LOC
Folder picker Wizard can target a folder of markdown files as a deck source folder.pick ~30 LOC

Why now

Buckets A + B closed via PR #52 + PR #53. These 5 methods are the only ones still returning -32000 "Method not found" to the dashboard's silent-catch JS callsites. Closing this finishes the hero_slides_server schema-vs-dispatcher alignment arc that started at #49.

Each is a real user need, not just a tracker box-tick:

  • Linking removes "I have 5 decks with the same intro slide and now I have to edit it 5 times."
  • Staleness turns Generate All from a $-burning sledgehammer into a precision tool — important now that Generate All is wired to billable Gemini image models via hero_aibroker PR #67.
  • Folder picker unblocks the Slide Wizard's "import a folder of markdown" flow already in the JS at dashboard.js:4139.

Decisions (locked 2026-05-10)

Slide linking — 4 design calls

Call Recommendation Why
Where to store the link metadata.toml field on the destination slide: linked_to = "<col>::<deck>::<slug>" Single SSOT per slide (already where image-model overrides live); survives renames; trivial to grep/audit; no orphan symlinks
Cycle detection Forbid A→B→A at setLink time (walk the chain, error on cycle) Avoids generate-time infinite loops; chains will be 1-2 deep so the walk is negligible; operator gets a clear error immediately, not a confusing partial result
Generate-time semantics Reuse the source slide's image bytes verbatim This is what "linked" means to the operator — edit once, propagate. Themed copies want a different feature. Bonus: one image generation, two slides served
Source-disappears behavior Render placeholder + warning surfaced in dashboard Don't brick a live presentation; let the operator fix at their pace

Staleness — 1 design call

A slide is stale if either:

  • its source .md mtime > its rendered .png mtime, or
  • the deck's theme.md mtime > its rendered .png mtime.

StaleSlide carries per-cause attribution (content_edited / theme_edited) so the dashboard badge can say which.

Context-fingerprint drift (per ADR-0007) is intentionally not in the staleness signal yet — start simple, add later if user feedback demands it. Keeps the per-deck scan O(n) on filesystem mtimes only.

folder.pick — 1 architectural call

Curated allowlist read from a hero_proc secret ([folder.pick] allowed_roots = [...]). Default when unconfigured: ~/Documents.

Native OS file dialog (rfd / nfd2 crates) is ruled out — it would open on the server's display, not the user's browser. Doesn't make sense over JSON-RPC at all.

Sequencing — 3 mini-PRs after approval

  1. folder.pick (~30 LOC) — smallest, no contention. Can land first regardless of design calls above.
  2. Staleness pair (~80 LOC) — independent of linking; can run in parallel with #1.
  3. Linking pair (~150 LOC) — most LOC, persistence layer + dispatcher arms + small UI follow-up if dashboard.js doesn't already speak the new metadata shape.

Acceptance

  • All 5 methods dispatched against real lib implementations (no stubs left in hero_slides_lib/src/deck.rs:1423-1448).
  • scripts/smoke_openrpc.py METHOD_MISSING count drops 5 → 0.
  • Dashboard.js callsites at lines 3188, 3916, 4945, 4974, 5056 (slide.setLink), 603, 648, 1090, 3566, 3628, 5330 (deck.staleness + slide.getStaleness), 4139 (folder.pick) stop catching -32000.
  • Hero Browser MCP smoke (linking): create a link between two slides in the example deck → editing the source updates both at next generate.
  • Hero Browser MCP smoke (staleness): edit a slide's .mddeck.staleness flags it within one call → dashboard's stale-badge updates.
  • folder.pick returns the configured allowlist; unconfigured server returns ~/Documents.

Out of scope

  • Bucket A + B (already shipped via PR #52 + PR #53).
  • Context-fingerprint-aware staleness (deferred until user feedback demands it).
  • Native OS file-dialog for folder.pick (architecturally wrong over JSON-RPC).
  • Themed-copy variant of slide linking (different feature; not asked for).
  • UI redesign for stale badges / link indicators (existing affordances assumed adequate).

Refs

  • hero_slides#51 — parent (bucket A + B closed)
  • hero_slides#49 — origin of the schema audit
  • hero_slides PR #50legacy_param_shim (bucket C handlers can rely on it)
  • lhumina_code/hero_slides/crates/hero_slides_lib/src/deck.rs:1423-1448 — the three lib stubs
  • lhumina_code/hero_slides/crates/hero_slides_server/openrpc.json — schema declarations

Signed-off-by: mik-tf

## Status **Design locked 2026-05-10** by mik-tf. Implementation queued for session 89; will land as 3 mini-PRs (folder.pick → staleness pair → linking pair). The four "Recommendations needing approval" below are now **Decisions** — no further design discussion required before code. ## TL;DR Three small features close out the hero_slides JSON-RPC dispatcher (the last 5 of 15 undispatched methods surfaced by [#49](https://forge.ourworld.tf/lhumina_code/hero_slides/issues/49)'s schema audit). **~280 LOC across 3 mini-PRs. Design locked 2026-05-10 by mik-tf — recommendations below stand as-decided. Implementation queued for session 89.** | Feature | User impact | Methods | Effort | |---|---|---|---| | Slide linking | Edit a slide once, propagate to every deck that includes it | `slide.setLink`, `slide.clearLink` | ~150 LOC | | Staleness tracking | Dashboard shows which slides need re-generation, so "Generate All" only re-renders what changed | `slide.getStaleness`, `deck.staleness` | ~80 LOC | | Folder picker | Wizard can target a folder of markdown files as a deck source | `folder.pick` | ~30 LOC | ## Why now Buckets A + B closed via [PR #52](https://forge.ourworld.tf/lhumina_code/hero_slides/pulls/52) + [PR #53](https://forge.ourworld.tf/lhumina_code/hero_slides/pulls/53). These 5 methods are the only ones still returning `-32000 "Method not found"` to the dashboard's silent-catch JS callsites. Closing this finishes the hero_slides_server schema-vs-dispatcher alignment arc that started at [#49](https://forge.ourworld.tf/lhumina_code/hero_slides/issues/49). Each is a real user need, not just a tracker box-tick: - **Linking** removes "I have 5 decks with the same intro slide and now I have to edit it 5 times." - **Staleness** turns Generate All from a $-burning sledgehammer into a precision tool — important now that Generate All is wired to billable Gemini image models via [hero_aibroker PR #67](https://forge.ourworld.tf/lhumina_code/hero_aibroker/pulls/67). - **Folder picker** unblocks the Slide Wizard's "import a folder of markdown" flow already in the JS at `dashboard.js:4139`. ## Decisions (locked 2026-05-10) ### Slide linking — 4 design calls | Call | Recommendation | Why | |---|---|---| | Where to store the link | `metadata.toml` field on the destination slide: `linked_to = "<col>::<deck>::<slug>"` | Single SSOT per slide (already where image-model overrides live); survives renames; trivial to grep/audit; no orphan symlinks | | Cycle detection | Forbid `A→B→A` at `setLink` time (walk the chain, error on cycle) | Avoids generate-time infinite loops; chains will be 1-2 deep so the walk is negligible; operator gets a clear error immediately, not a confusing partial result | | Generate-time semantics | Reuse the source slide's image bytes verbatim | This is what "linked" means to the operator — edit once, propagate. Themed copies want a different feature. Bonus: one image generation, two slides served | | Source-disappears behavior | Render placeholder + warning surfaced in dashboard | Don't brick a live presentation; let the operator fix at their pace | ### Staleness — 1 design call A slide is **stale** if either: - its source `.md` mtime > its rendered `.png` mtime, or - the deck's `theme.md` mtime > its rendered `.png` mtime. `StaleSlide` carries per-cause attribution (`content_edited` / `theme_edited`) so the dashboard badge can say which. Context-fingerprint drift (per ADR-0007) is intentionally **not** in the staleness signal yet — start simple, add later if user feedback demands it. Keeps the per-deck scan O(n) on filesystem mtimes only. ### `folder.pick` — 1 architectural call Curated allowlist read from a hero_proc secret (`[folder.pick] allowed_roots = [...]`). Default when unconfigured: `~/Documents`. Native OS file dialog (`rfd` / `nfd2` crates) is ruled out — it would open on the server's display, not the user's browser. Doesn't make sense over JSON-RPC at all. ## Sequencing — 3 mini-PRs after approval 1. **`folder.pick`** (~30 LOC) — smallest, no contention. Can land first regardless of design calls above. 2. **Staleness pair** (~80 LOC) — independent of linking; can run in parallel with #1. 3. **Linking pair** (~150 LOC) — most LOC, persistence layer + dispatcher arms + small UI follow-up if `dashboard.js` doesn't already speak the new metadata shape. ## Acceptance - [ ] All 5 methods dispatched against real lib implementations (no stubs left in `hero_slides_lib/src/deck.rs:1423-1448`). - [ ] `scripts/smoke_openrpc.py` METHOD_MISSING count drops 5 → 0. - [ ] Dashboard.js callsites at lines 3188, 3916, 4945, 4974, 5056 (`slide.setLink`), 603, 648, 1090, 3566, 3628, 5330 (`deck.staleness` + `slide.getStaleness`), 4139 (`folder.pick`) stop catching `-32000`. - [ ] **Hero Browser MCP smoke (linking):** create a link between two slides in the example deck → editing the source updates both at next generate. - [ ] **Hero Browser MCP smoke (staleness):** edit a slide's `.md` → `deck.staleness` flags it within one call → dashboard's stale-badge updates. - [ ] `folder.pick` returns the configured allowlist; unconfigured server returns `~/Documents`. ## Out of scope - Bucket A + B (already shipped via [PR #52](https://forge.ourworld.tf/lhumina_code/hero_slides/pulls/52) + [PR #53](https://forge.ourworld.tf/lhumina_code/hero_slides/pulls/53)). - Context-fingerprint-aware staleness (deferred until user feedback demands it). - Native OS file-dialog for `folder.pick` (architecturally wrong over JSON-RPC). - Themed-copy variant of slide linking (different feature; not asked for). - UI redesign for stale badges / link indicators (existing affordances assumed adequate). ## Refs - [hero_slides#51](https://forge.ourworld.tf/lhumina_code/hero_slides/issues/51) — parent (bucket A + B closed) - [hero_slides#49](https://forge.ourworld.tf/lhumina_code/hero_slides/issues/49) — origin of the schema audit - [hero_slides PR #50](https://forge.ourworld.tf/lhumina_code/hero_slides/pulls/50) — `legacy_param_shim` (bucket C handlers can rely on it) - `lhumina_code/hero_slides/crates/hero_slides_lib/src/deck.rs:1423-1448` — the three lib stubs - `lhumina_code/hero_slides/crates/hero_slides_server/openrpc.json` — schema declarations Signed-off-by: mik-tf
mik-tf changed title from hero_slides_server: implement bucket C — slide.setLink/clearLink, slide.getStaleness, deck.staleness, folder.pick (5 methods blocked on missing lib code) to hero_slides bucket C: slide linking + staleness + folder picker (closes 5/5 remaining undispatched methods) 2026-05-10 13:13:17 +00:00
Author
Owner

Closed via hero_slides PR #55 (squash-merged 2026-05-10).

All 5 methods land against real hero_slides_lib implementations — no stubs left in deck.rs:1423-1448.

Method Backing
folder.pick new handler reading hero_proc secret folder_pick_allowed_roots (context hero_slides, newline-separated, ~/Documents fallback)
deck.staleness new deck_staleness() using ADR-0007 DeckInputsContext::for_deck + compose_inputs_hash, per-component reasons via comparison against last_* snapshot fields
slide.getStaleness new slide_staleness() wrapper
slide.setLink new deck_slide_link_set writes typed SlideLink on SlideMetaEntry.source_link; cycle detection rejects A→B→A and self-link; source PNG copied verbatim
slide.clearLink new deck_slide_link_clear clears source_link, leaves PNG

Verified locally end-to-end via the live hero_slides_admin dashboard. Driving window.rpc() directly with the freshly-built binaries:

  • Real AI generate produced a 1.3 MB Gemini Flash Image PNG into examples/sample_deck/output/01_intro.png
  • metadata.toml now records the full ADR-0007 snapshot (last_generated, last_theme_hash, last_context_fingerprint, last_prompts_hash, last_image_model)
  • slide.getStaleness right after generate: is_stale=false
  • Edit 01_intro.mdslide.getStaleness: is_stale=true, reasons=["content_edited"] ← canonical positive case fires correctly
  • setLink of two slides → md5 of source/dest PNGs match byte-for-byte
  • Self-link rejected: "cannot link a slide to itself"
  • Cycle rejected: "linking 01_title → 01_intro would form a cycle"

Three reconciliations vs the original issue body captured in workspace decision file D-09-slide-linking-and-staleness.md:

  1. Staleness is hash-based (ADR-0007) not mtime-based — the existing StaleSlide type and compute_inputs_hash infrastructure was richer than the issue assumed.
  2. Linking persists via typed SlideLink + SlideMetaEntry.source_link, not a raw linked_to string field.
  3. folder.pick schema description flipped from "native OS dialog" to allowlist semantics; params/result shape unchanged.

Coupled fix: hero_aibroker PR #68 was required for AI generate to work — the broker silently dropped message.images from OpenRouter responses because the Message struct had no such field. Without that fix, every slide.generate failed with "No images in response" (a regression unrelated to bucket C but on the critical path for validation).

Also fixes a pre-existing bug in slide_generate_with_selection and slide_generate_with_context that only persisted SlideMetaEntry.hash on AI render, never the ADR-0007 inputs-hash snapshot. Without those fields, every generated slide stayed in the back-fill rule forever and content_edited never fired.

Closed via [hero_slides PR #55](https://forge.ourworld.tf/lhumina_code/hero_slides/pulls/55) (squash-merged 2026-05-10). **All 5 methods land against real `hero_slides_lib` implementations** — no stubs left in `deck.rs:1423-1448`. | Method | Backing | |---|---| | `folder.pick` | new handler reading hero_proc secret `folder_pick_allowed_roots` (context `hero_slides`, newline-separated, `~/Documents` fallback) | | `deck.staleness` | new `deck_staleness()` using ADR-0007 `DeckInputsContext::for_deck` + `compose_inputs_hash`, per-component `reasons` via comparison against `last_*` snapshot fields | | `slide.getStaleness` | new `slide_staleness()` wrapper | | `slide.setLink` | new `deck_slide_link_set` writes typed `SlideLink` on `SlideMetaEntry.source_link`; cycle detection rejects `A→B→A` and self-link; source PNG copied verbatim | | `slide.clearLink` | new `deck_slide_link_clear` clears `source_link`, leaves PNG | **Verified locally end-to-end via the live `hero_slides_admin` dashboard.** Driving `window.rpc()` directly with the freshly-built binaries: - Real AI generate produced a 1.3 MB Gemini Flash Image PNG into `examples/sample_deck/output/01_intro.png` - `metadata.toml` now records the full ADR-0007 snapshot (`last_generated`, `last_theme_hash`, `last_context_fingerprint`, `last_prompts_hash`, `last_image_model`) - `slide.getStaleness` right after generate: `is_stale=false` - Edit `01_intro.md` → `slide.getStaleness`: `is_stale=true, reasons=["content_edited"]` ← canonical positive case fires correctly - `setLink` of two slides → md5 of source/dest PNGs match byte-for-byte - Self-link rejected: `"cannot link a slide to itself"` - Cycle rejected: `"linking 01_title → 01_intro would form a cycle"` **Three reconciliations vs the original issue body** captured in workspace decision file `D-09-slide-linking-and-staleness.md`: 1. Staleness is hash-based (ADR-0007) not mtime-based — the existing `StaleSlide` type and `compute_inputs_hash` infrastructure was richer than the issue assumed. 2. Linking persists via typed `SlideLink` + `SlideMetaEntry.source_link`, not a raw `linked_to` string field. 3. `folder.pick` schema description flipped from `"native OS dialog"` to allowlist semantics; params/result shape unchanged. **Coupled fix:** [hero_aibroker PR #68](https://forge.ourworld.tf/lhumina_code/hero_aibroker/pulls/68) was required for AI generate to work — the broker silently dropped `message.images` from OpenRouter responses because the `Message` struct had no such field. Without that fix, every `slide.generate` failed with `"No images in response"` (a regression unrelated to bucket C but on the critical path for validation). **Also fixes a pre-existing bug** in `slide_generate_with_selection` and `slide_generate_with_context` that only persisted `SlideMetaEntry.hash` on AI render, never the ADR-0007 inputs-hash snapshot. Without those fields, every generated slide stayed in the back-fill rule forever and `content_edited` never fired.
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#54
No description provided.