Skip to content

feat(report): daily narrative on every today surface (Phase 4 M-A2)#14

Merged
rojae merged 2 commits into
feature/phase4/basefrom
feature/phase4/daily-narrative
May 5, 2026
Merged

feat(report): daily narrative on every today surface (Phase 4 M-A2)#14
rojae merged 2 commits into
feature/phase4/basefrom
feature/phase4/daily-narrative

Conversation

@rojae

@rojae rojae commented May 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Wires the LLM-written today, in a paragraph decoration onto every
today-shaped surface — the studio /today page, the CLI fluxmirror today text report, and the HTML day card all now lead with a
2-3 sentence narrative pulled from the existing M-A1 synthesis pipeline.

Pieces

DTO + helpers (fluxmirror-core)

  • TodayData gains a narrative: DailyNarrative field with a sane
    Default so older callers and the JSON wire shape stay invariant.
  • DailyNarrative carries paragraph + source (Llm | Heuristic)
    • optional model + cost_usd + cache_hit.
  • report::data::build_daily_summary_input(today) returns the JSON
    context object that fills every {placeholder} in
    prompts/daily.txt plus richer keys for future prompt revisions
    (lifecycle hint, top files, agents, shell signature buckets,
    primary languages from file extensions).
  • report::ai_narrative::heuristic_paragraph(today) is the always-on
    deterministic fallback. Properties: never empty, never panics on
    degenerate input, byte-stable for the same TodayData.

Synthesis wrapper (fluxmirror-ai)

  • New module daily_narrative with
    synthesise_daily(store, config, today) -> DailyNarrative.
    Lives in fluxmirror-ai for the same reason session_intent and
    anomaly_story do — synthesise() depends on budget / cache /
    provider modules; pushing the wrapper down to core would
    re-introduce the cycle.
  • Every error leg drops into heuristic_paragraph: provider=off,
    no writable store, empty window, budget hit, network error, empty
    completion text. The narrative field is therefore always
    populated regardless of provider state.

Surface wiring

  • CLI today.rs — pulls TodayData via core::data::collect_today,
    populates narrative via synthesise_daily, prepends a
    ## Narrative section above the existing stats. Italic
    _(estimate)_ footnote when source = Heuristic.
  • CLI html_day.rsrender_day_card now takes
    Option<&DailyNarrative>; injects a styled
    <section class="narrative"> at the top of the summary block plus
    the same estimate footnote.
  • Studio api/today.rs — overlays narrative via
    fluxmirror_ai::synthesise_daily before responding.
  • Frontend Today.tsx + Home.tsx — render the paragraph in a
    small panel at the top of each page with an estimate pill when
    source = 'heuristic'. Home also links the panel to /today.

Test plan

  • cargo test --workspace --no-fail-fast
  • bash scripts/test-rust-hook.sh
  • bash scripts/check-report-commands.sh
  • bash scripts/build-manifests.sh --check
  • bash scripts/check-extension-shape.sh
  • pnpm install --frozen-lockfile && pnpm build in studio-web/
  • New tests:
    • crates/fluxmirror-core/tests/daily_narrative.rs — heuristic
      paragraph non-empty for busy + empty fixtures, snapshot pin,
      build_daily_summary_input key contract, zero-reads ratio
      fallback to "n/a".
    • crates/fluxmirror-studio/tests/api_today_narrative.rs
      fixture DB + provider=off returns
      narrative.source = "heuristic" with non-empty paragraph;
      empty DB still emits the heuristic empty-window paragraph.
    • crates/fluxmirror-cli/src/cmd/report/html_day.rs — two new
      unit tests pin the narrative section + estimate footnote
      markup.

rojae added 2 commits May 5, 2026 11:35
Wires the LLM-written 'today, in a paragraph' decoration onto every
today-shaped surface — the studio /today page, the CLI 'fluxmirror
today' text report, and the HTML day card all now lead with a
2-3 sentence narrative pulled from the existing synthesis pipeline.

DTO + helpers (fluxmirror-core):

- TodayData gains a `narrative: DailyNarrative` field with a sane
  Default so older callers + the JSON wire shape stay invariant.
- DailyNarrative carries paragraph + source (Llm | Heuristic) +
  optional model + cost_usd + cache_hit so callers can decide
  whether to render an 'estimate' footnote.
- report::data::build_daily_summary_input(today) returns the json
  context object that fills every {placeholder} in
  prompts/daily.txt plus richer keys for future prompt revisions
  (lifecycle hint, top files, agents, shell signature buckets,
  primary languages from file extensions).
- report::ai_narrative::heuristic_paragraph(today) is the
  always-on deterministic fallback. Properties: never empty,
  never panics on degenerate input, byte-stable for the same
  TodayData. Lives in core (not ai) so the fallback is testable
  with a plain `cargo test -p fluxmirror-core` run.

Synthesis wrapper (fluxmirror-ai):

- New module daily_narrative with synthesise_daily(store, config,
  today) -> DailyNarrative. Lives here for the same reason
  session_intent + anomaly_story do — synthesise() depends on the
  budget / cache / provider modules, so pushing the wrapper down
  to core would re-introduce the cycle.
- Every error leg drops into heuristic_paragraph: provider="off",
  no writable store, empty window, budget hit, network error,
  empty completion text. The narrative field is therefore always
  populated regardless of provider state.

Surface wiring:

- crates/fluxmirror-cli/src/cmd/report/today.rs: pulls TodayData
  via core::data::collect_today, populates narrative via
  synthesise_daily, prepends a `## Narrative` section above the
  existing stats. `_(estimate)_` footnote when source=Heuristic.
- crates/fluxmirror-cli/src/cmd/report/html_day.rs: render_day_card
  now takes Option<&DailyNarrative>; injects a styled
  <section class="narrative"> at the top of the summary block plus
  an italic estimate footnote when heuristic.
- crates/fluxmirror-studio/src/api/today.rs: overlays narrative
  via fluxmirror_ai::synthesise_daily before responding. Existing
  api_today.rs shape test still passes (extra key tolerated).
- studio-web/src/routes/Today.tsx + Home.tsx: render the paragraph
  in a small panel at the top of each page with an 'estimate' pill
  when source=heuristic. Home also links the panel to /today.

Tests:

- crates/fluxmirror-core/tests/daily_narrative.rs — heuristic
  paragraph non-empty for busy + empty fixtures, snapshot pin,
  build_daily_summary_input key contract, zero-reads ratio
  fallback to "n/a".
- crates/fluxmirror-studio/tests/api_today_narrative.rs — fixture
  DB + provider="off" returns narrative.source = "heuristic"
  with non-empty paragraph; empty DB still emits the heuristic
  empty-window paragraph.
- crates/fluxmirror-cli/src/cmd/report/html_day.rs — two new unit
  tests pin the narrative section + estimate footnote markup.

Heuristic-only path is unconditional fallback; no panic legs.
The init_demo_row sandbox was overriding HOME and removing
USERPROFILE but leaving XDG_CONFIG_HOME / XDG_DATA_HOME /
XDG_CACHE_HOME inherited from the test runner. On Linux,
paths::config_dir() honours XDG_CONFIG_HOME first, so the
GitHub Actions runner's literal /home/runner/.config path
won out over the per-test tempdir, and init then wrote into
a directory it never had permission to create. macOS doesn't
read XDG, which is why the regression hid for so long.

Three subprocess calls in this test file now scrub all three
XDG variables alongside the existing HOME / USERPROFILE
fixtures. No production code touched.
@rojae rojae merged commit 3f32cee into feature/phase4/base May 5, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant