feat(report): daily narrative on every today surface (Phase 4 M-A2)#14
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Wires the LLM-written today, in a paragraph decoration onto every
today-shaped surface — the studio /today page, the CLI
fluxmirror todaytext report, and the HTML day card all now lead with a2-3 sentence narrative pulled from the existing M-A1 synthesis pipeline.
Pieces
DTO + helpers (fluxmirror-core)
TodayDatagains anarrative: DailyNarrativefield with a saneDefault so older callers and the JSON wire shape stay invariant.
DailyNarrativecarriesparagraph+source(Llm|Heuristic)model+cost_usd+cache_hit.report::data::build_daily_summary_input(today)returns the JSONcontext object that fills every
{placeholder}inprompts/daily.txtplus 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-ondeterministic fallback. Properties: never empty, never panics on
degenerate input, byte-stable for the same
TodayData.Synthesis wrapper (fluxmirror-ai)
daily_narrativewithsynthesise_daily(store, config, today) -> DailyNarrative.Lives in
fluxmirror-aifor the same reasonsession_intentandanomaly_storydo —synthesise()depends on budget / cache /provider modules; pushing the wrapper down to core would
re-introduce the cycle.
heuristic_paragraph: provider=off,no writable store, empty window, budget hit, network error, empty
completion text. The
narrativefield is therefore alwayspopulated regardless of provider state.
Surface wiring
today.rs— pullsTodayDataviacore::data::collect_today,populates narrative via
synthesise_daily, prepends a## Narrativesection above the existing stats. Italic_(estimate)_footnote whensource = Heuristic.html_day.rs—render_day_cardnow takesOption<&DailyNarrative>; injects a styled<section class="narrative">at the top of the summary block plusthe same estimate footnote.
api/today.rs— overlays narrative viafluxmirror_ai::synthesise_dailybefore responding.Today.tsx+Home.tsx— render the paragraph in asmall 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-fastbash scripts/test-rust-hook.shbash scripts/check-report-commands.shbash scripts/build-manifests.sh --checkbash scripts/check-extension-shape.shpnpm install --frozen-lockfile && pnpm buildinstudio-web/crates/fluxmirror-core/tests/daily_narrative.rs— heuristicparagraph non-empty for busy + empty fixtures, snapshot pin,
build_daily_summary_inputkey contract, zero-reads ratiofallback to
"n/a".crates/fluxmirror-studio/tests/api_today_narrative.rs—fixture DB + provider=
offreturnsnarrative.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 newunit tests pin the narrative section + estimate footnote
markup.