Skip to content

Architecture deepening: reader collapse 037-040 + TimerTicker 044 + ADRs - #51

Merged
archae0pteryx merged 10 commits into
mainfrom
feat/architecture-deepening
May 11, 2026
Merged

Architecture deepening: reader collapse 037-040 + TimerTicker 044 + ADRs#51
archae0pteryx merged 10 commits into
mainfrom
feat/architecture-deepening

Conversation

@archae0pteryx

@archae0pteryx archae0pteryx commented May 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Architecture deepening pass on the frontend reader layer and the domain timer module, plus issue-queue hygiene.

Frontend reader collapse (037 → 038 → 039 → 040):

  • FocusReader, ProposalReader, CapsReader all merged into PolledReader<T>; the corresponding useFocuses / useProposals / useCaps rename hops are deleted (their behaviors are covered by usePolledReader.test.tsx).
  • One generic createTauriReader<Raw, Out>({ invokeKey, eventKey?, map }) factory in src/api/tauriReader.ts backs all three Tauri-side readers. Per-domain files contain only channel name + optional event name + Raw → Out mapping.

Domain TimerTicker (044):

  • New crates/domain/src/timer_ticker.rs exposing pure tick(now_secs, &[Focus]) -> Vec<TimerTransition>. Six table-driven tests cover the acceptance grid (no timer, not-yet-expired, exactly-at-expiry, past expiry, already-Expired, multiple in input order).
  • Issue 029 (the upcoming timer-expiry + NotificationSource slice) is updated to consume tick — its imperative shell shrinks to read store → tick → for each transition: store.update_timer + Tauri emit + notification.

Issue queue + ADRs:

  • 029 amended (consume TimerTicker); 043 step-list tightened against actual lifecycle.rs shape; 044 analogy corrected (OverCapMonitor is stateful via Mutex<State>; TimerTicker is stateless because dedup lives in persisted timer.status == Expired); 045 factually rewritten.
  • 042 closed (ADR-0001) — cap_state is already the single projection both tray.rs and CapEvaluator consume; no refactor warranted.
  • 045 closed (ADR-0002) — fixture readers are 3-line Promise.resolve(value) factories; a generic helper would deduplicate one line per use site with zero leverage gain. Test-wrapper deletion piece already folded into 039.

Net code: roughly −274 LOC across the 9 commits (collapse + close).

Test plan

  • task check green at each commit boundary
  • Frontend Vitest: 69 / 69 passing (2 fewer than baseline — the two deleted useFocuses tests were already covered by usePolledReader.test.tsx against a generic <number> reader)
  • Rust cargo test: 90 / 90 passing across adhd-ranch-domain, adhd-ranch-storage, adhd-ranch-commands, adhd-ranch-http-api
  • Biome lint / TypeScript build clean
  • Manual sanity: launch the Tauri app, confirm pigs render + tray badge updates + caps still flow (recommended before merge — the reader-collapse touches main UI wiring)

Issue trace

Issue Status
037 collapse FocusReader ✅ done
038 collapse ProposalReader ✅ done
039 unify useCaps on usePolledReader ✅ done
040 generic tauriReader factory ✅ done
044 TimerTicker pure module ✅ done
042 unify cap-state projection closed → ADR-0001
045 symmetric fixtureReader closed → ADR-0002
029 scope expansion + consume TimerTicker issue file updated only
043 ProposalLifecycle atomicity (HITL) issue file tightened only
041 FocusWriter WriteOutcome not in this PR

Summary by CodeRabbit

  • New Features

    • App now detects timer expiries and emits notifications (configurable per-source).
    • Notifications unified under a single settings toggle set (timer, focus-cap, task-cap).
  • Documentation

    • Added ADRs and spec documents describing notification, timer ticker, and reader design decisions.
  • Refactor

    • Consolidated polling reader pattern across data loading and updated app state to use the unified polling API.

Review Change Stack

Add architecture queue alongside priority queue. Two tracks:

Frontend reader collapse (037-040, 045) — drafted previously plus new
045 closing the caps fixture gap so 040's generic Tauri factory has a
symmetric fixture/Tauri pair.

Domain seams (041-044) from this session's deepening audit:
- 041 FocusWriter returns WriteOutcome (no more swallowed rejections)
- 042 Unify cap-state behind one CapStatus projection (tray + notifier
  agree by construction)
- 043 ProposalLifecycle::accept atomicity — HITL design, produces ADR
- 044 Carve domain TimerTicker pure module ahead of 029 wiring

Why: shallow modules and split error/transaction responsibility surfaced
during architecture review. Each slice is independent and AFK except
043 which needs a design decision recorded as an ADR.

029 scope-expansion edit deliberately left unstaged — separate commit.
`crates/domain/src/timer_ticker.rs` — `tick(now_secs, focuses) ->
Vec<TimerTransition>`. Pure: no I/O, no async, no clock read. Returns
one transition per Focus with `timer.status == Running` and elapsed
>= duration. Reuses `timer_remaining_secs` to avoid parallel arithmetic.

`TimerStatus::Running` gate is the dedup lock against persisted Expired
focuses — without it every tick after the first expiry would re-fire.

Six unit tests cover the acceptance grid (no timer, not-yet, exactly-at,
past, already-Expired, multiple-in-order). No `app/` wiring; issue 029
will consume `tick`.
029 — scope expansion already drafted pre-session: unify cap notifier
onto the NotificationSource trait, delete `Alerts` struct, single
notifications.sources toggle surface. Plus this session: require expiry
detection consumes `adhd_ranch_domain::tick(...)` from 044; declare 044
as a blocker.

043 — tighten the 3-step transactional description against the actual
shape of `accept()` at lifecycle.rs:41-52 + helper `apply()` at :64-90.
Was conflating six calls with three writes; now names each write and
the file it touches.

044 — drop misleading "mirrors `OverCapMonitor`'s shape: stateless
detection" claim. `OverCapMonitor` is stateful (`Mutex<State>` for
in-memory dedup). `TimerTicker` is stateless because dedup lives in
persisted `timer.status == Expired`. Replace analogy with the contrast.

045 — factual rewrite. `createFixtureCapsReader` already exists at
`src/api/caps.ts:23`. Real gap: `useAppState.test.tsx:14` reimplements
the body locally instead of importing, AND no generic helper exists.
Acceptance criteria updated to delete the local wrapper, not "add a
factory that doesn't exist".
ADR-0001 records why. Verified against caps.rs:17 and both consumers
(tray.rs:34,104 + commands/caps.rs:39-40): both derive from the same
`cap_state(focuses, caps)` function. No two-path divergence to fix.

042 file deleted; README architecture queue trimmed; future audits
should verify the projection function before asserting "two read paths".

Also marks 044 as done in the queue (merged this session).
…ocus[]>

`FocusReader` was the same shape as `PolledReader<readonly Focus[]>`
minus a `list`/`read` rename. `useFocuses` was a 22-line rename hop
(`value` → `focuses`). Two interfaces, one rename, no leverage.

Delete:
- `src/api/focuses.ts` (FocusReader + duplicate Unsubscribe export)
- `src/hooks/useFocuses.ts`
- `src/hooks/useFocuses.test.tsx` (its 2 behaviors are already covered
  by `src/hooks/usePolledReader.test.tsx` against a generic `<number>`
  reader)

Rename:
- `createTauriFocusReader` returns `PolledReader<readonly Focus[]>`,
  method `list` → `read`
- `createFixtureFocusReader` same

Update consumers (`App.tsx`, `useAppState.ts`, `useAppState.test.tsx`):
- prop type → `PolledReader<readonly Focus[]>`
- `useFocuses(reader)` → `usePolledReader(reader)`
- ready state → `state.value` (was `state.focuses`)
- `failingFocusReader` builder rewritten to the new shape

Collateral: `Unsubscribe` was exported from both `src/api/focuses.ts`
(the deleted file) and `src/hooks/usePolledReader.ts`. Redirected the
two non-focus consumers (`src/api/proposals.ts`,
`src/api/tauriProposalReader.ts`) to import from `usePolledReader`.
This is not 038 scope — just keeping the proposal side compiling.

`task check` green. 69/69 tests pass.
…ader<Proposal[]>

Mirror of 037 for the proposal catalog. `ProposalReader` was the same
shape as `PolledReader<readonly Proposal[]>` minus a `list`/`read`
rename. `useProposals` was a 22-line rename hop (`value` → `proposals`).

Delete:
- `src/hooks/useProposals.ts` (no test file existed; `usePolledReader`
  tests already cover loading/ready/error/subscribe behaviors)
- `ProposalReader` interface in `src/api/proposals.ts` (file retained
  for `ProposalDecisionResult`, `ProposalEdit`, `ProposalWriter`)

Rename:
- `createTauriProposalReader` returns `PolledReader<readonly Proposal[]>`,
  method `list` → `read`
- `createFixtureProposalReader` same

Update consumers (`useAppState.ts`, `useAppState.test.tsx`):
- prop type → `PolledReader<readonly Proposal[]>`
- `useProposals(reader)` → `usePolledReader(reader)`
- ready state → `state.value` (was `state.proposals`)
- `failingProposalReader` builder rewritten to the new shape

`task check` green. 69/69 tests pass.
…aps>

`useCaps` returned `Caps` directly with a silent `DEFAULT_CAPS` fallback
on error and no loading state — a different pattern than the focus and
proposal readers (now both `PolledReader<T>`-shaped via 037/038). Two
patterns for the same job.

Unify on `usePolledReader<Caps>`. UX preserved (silent default fallback,
no loading flash) by mapping the polled state at the call site:

  const caps = capsState.status === "ready" ? capsState.value : DEFAULT_CAPS;

Delete:
- `src/hooks/useCaps.ts`
- `CapsReader` interface in `src/api/caps.ts` (factories now return
  `PolledReader<Caps>` directly; `subscribe?` omitted because caps are
  not pushed by Tauri)

Rename:
- `createTauriCapsReader` / `createFixtureCapsReader` return
  `PolledReader<Caps>`; method `get` → `read`

Collateral (folds part of 045):
- `src/hooks/useAppState.test.tsx:14` local `fixtureCapsReader` wrapper
  deleted; tests now use `createFixtureCapsReader(fixtureCaps)` directly
- `CapsReader` import removed (type no longer exists)

`task check` green. 69/69 tests pass.
After 037/038/039 unified the three Tauri readers on `PolledReader<T>`,
each `createTauri*Reader` repeated the same plumbing: `invoke<Raw>(key)`,
optionally `listen(event)`, optionally a `Raw → Out` map. Three copies.

Consolidate into one factory at `src/api/tauriReader.ts`:

  createTauriReader<Raw, Out>({ invokeKey, eventKey?, map })
    -> PolledReader<Out>

Per-domain files now contain only the channel name, optional event name,
the `Raw → Out` mapping, and any Rust shape interfaces. No `invoke`,
`listen`, or `Unsubscribe` plumbing duplicated.

- `tauriFocusReader.ts` — `Raw = readonly RustFocus[]`, map calls
  `fromRust` per element; keeps `RustFocus` + `fromRust` co-located
- `tauriProposalReader.ts` — `Raw = readonly Proposal[]`, identity map;
  `createTauriProposalWriter` unchanged
- `caps.ts` — `Raw = Caps`, identity map; no `eventKey`

`task check` green. 69/69 tests pass.
ADR-0002 records why. Each fixture factory is 3 lines of
`{ read: () => Promise.resolve(value) }`; a generic helper would
deduplicate one line across three call sites — zero leverage gain
per CLAUDE.md ("No abstractions for single-use code").

Issue 040's own "Out of scope" note already documented this. The
second piece of 045 — deleting the test wrapper at
useAppState.test.tsx:14 and importing createFixtureCapsReader
directly — was folded into the 039 commit.

045 file deleted; README queue updated; 037-040 marked done.
@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4eba8c02-f6fa-438b-a807-63864744ac64

📥 Commits

Reviewing files that changed from the base of the PR and between 1927c27 and 8c4409e.

📒 Files selected for processing (13)
  • issues/044-domain-timer-ticker.md
  • src/api/caps.ts
  • src/api/fixtureFocusReader.ts
  • src/api/fixtureProposalReader.ts
  • src/api/polledReader.ts
  • src/api/tauriFocusReader.ts
  • src/api/tauriProposalReader.ts
  • src/api/tauriReader.ts
  • src/components/App.tsx
  • src/hooks/useAppState.test.tsx
  • src/hooks/useAppState.ts
  • src/hooks/usePolledReader.test.tsx
  • src/hooks/usePolledReader.ts

📝 Walkthrough

Walkthrough

This PR consolidates three separate reader/hook pairs (FocusReader/useFocuses, ProposalReader/useProposals, CapsReader/useCaps) into a unified PolledReader abstraction, introduces a generic Tauri reader factory, adds a pure domain timer-expiry detection module, and documents architecture decisions via ADRs and issue specifications.

Changes

Frontend Reader Consolidation (037–040)

Layer / File(s) Summary
PolledReader type
src/api/polledReader.ts, src/hooks/usePolledReader.ts
Adds exported PolledReader<T> and Unsubscribe, and moves local hook type declarations into the shared api module.
Generic Tauri Reader Factory
src/api/tauriReader.ts
Introduces TauriReaderConfig interface and createTauriReader helper that handles invoke+listen plumbing, conditionally adding subscribe when eventKey is provided.
Tauri Reader Implementations
src/api/tauriFocusReader.ts, src/api/tauriProposalReader.ts, src/api/caps.ts
Refactors readers to delegate to createTauriReader with appropriate config (invokeKey, eventKey, map); caps reader now returns PolledReader<Caps>.
Fixture Reader Updates
src/api/fixtureFocusReader.ts, src/api/fixtureProposalReader.ts, src/api/fixtureFocusReader.test.ts
Converts fixture readers to return PolledReader with read() method; updates test assertions to call read().
API Surface Cleanup
src/api/focuses.ts, src/api/proposals.ts
Removes exported FocusReader, ProposalReader, and Unsubscribe interfaces; adds explicit export type { Proposal };.
Hook Deletions & Test Updates
src/hooks/useFocuses.ts, src/hooks/useFocuses.test.tsx, src/hooks/useProposals.ts, src/hooks/useCaps.ts
Removes reader-specific hooks and their tests; tests and call-sites updated to use usePolledReader and PolledReader fixtures.
useAppState Consolidation
src/hooks/useAppState.ts, src/hooks/useAppState.test.tsx
Refactors to call usePolledReader directly for focuses, proposals, and caps; derives caps from polled state with DEFAULT_CAPS fallback; updates tests to use createFixtureCapsReader.
App.tsx Wiring
src/components/App.tsx
Updates AppProps to declare focusReader: PolledReader<readonly Focus[]>; uses usePolledReader(focusReader) and accesses focusState.value when ready.

Timer Ticker Domain Module (044)

Layer / File(s) Summary
Timer Ticker Implementation
crates/domain/src/timer_ticker.rs
Introduces TimerTransition struct and tick(now_secs, focuses) function that emits expiry transitions for Running timers past their duration; includes unit tests for ordering and edge cases.
Module Exports
crates/domain/src/lib.rs
Declares pub mod timer_ticker; and re-exports tick and TimerTransition for external use.

Architecture Documentation & Decisions

Layer / File(s) Summary
ADR Records
docs/adr/0001-cap-state-is-already-a-single-projection.md, docs/adr/0002-fixture-readers-are-too-thin-for-a-generic-helper.md
Documents decisions to not implement issue 042 (cap projection unification) and issue 045 (generic fixture reader helper).
Reader Consolidation Specs
issues/037-collapse-focus-reader.md, issues/038-collapse-proposal-reader.md, issues/039-unify-caps-on-polled-reader.md, issues/040-generic-tauri-reader-factory.md
Issue specifications describing the planned refactor to collapse FocusReader/ProposalReader/CapsReader into PolledReader, plus the generic Tauri factory.
Additional Issue Specs
issues/041-focus-writer-write-outcome.md, issues/043-proposal-lifecycle-atomicity-design.md, issues/044-domain-timer-ticker.md, issues/029-timer-expiry-and-notification-interface.md
Specs for FocusWriter write-outcome API, ProposalLifecycle atomicity design, TimerTicker domain module, and unified NotificationSource interface with timer integration.
Architecture Queue
issues/README.md
Updates issues README with new "Architecture queue" section listing completed/refined items and ADR links.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • killallgit/adhd-ranch#32: The new timer_ticker module depends on timer domain types and semantics introduced in PR #32.

Poem

🐰 A reader hook hops into one bright burrow,
One PolledReader now gathers data neat and true;
The Tauri factory listens, maps, and calls with care,
Timers tick in domain land — a single transition there.
Hooray — the rabbit nibbles a carrot and says whoo!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main changes: architecture deepening work spanning frontend reader consolidation (issues 037-040), domain TimerTicker module (044), and ADR documentation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/architecture-deepening

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@issues/044-domain-timer-ticker.md`:
- Line 21: The spec and implementation disagree on tick's timestamp type: change
the spec signature pub fn tick(now_secs: u64, focuses: &[Focus]) ->
Vec<TimerTransition>; to use i64 (pub fn tick(now_secs: i64, focuses: &[Focus])
-> Vec<TimerTransition>), keeping the implementation's signed timestamp; update
any references or docs that rely on the spec signature to use now_secs: i64 and
ensure types align with the implementation of tick.

In `@src/api/fixtureProposalReader.ts`:
- Line 1: Move the PolledReader type out of the hooks layer into a new shared
boundary module (e.g., create a shared types module and export PolledReader from
it), then update the import sites in fixtureProposalReader.ts, caps.ts,
tauriFocusReader.ts, and tauriProposalReader.ts to import PolledReader from that
shared module instead of from ../hooks/usePolledReader; also update the hook
implementation to import PolledReader from the same shared module so that
src/hooks handles state/effects only and api files no longer depend on hooks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 38e3ba56-ea5f-4b3b-a84a-1fcef50a922c

📥 Commits

Reviewing files that changed from the base of the PR and between 1ff458d and 1927c27.

📒 Files selected for processing (29)
  • crates/domain/src/lib.rs
  • crates/domain/src/timer_ticker.rs
  • docs/adr/0001-cap-state-is-already-a-single-projection.md
  • docs/adr/0002-fixture-readers-are-too-thin-for-a-generic-helper.md
  • issues/029-timer-expiry-and-notification-interface.md
  • issues/037-collapse-focus-reader.md
  • issues/038-collapse-proposal-reader.md
  • issues/039-unify-caps-on-polled-reader.md
  • issues/040-generic-tauri-reader-factory.md
  • issues/041-focus-writer-write-outcome.md
  • issues/043-proposal-lifecycle-atomicity-design.md
  • issues/044-domain-timer-ticker.md
  • issues/README.md
  • src/api/caps.ts
  • src/api/fixtureFocusReader.test.ts
  • src/api/fixtureFocusReader.ts
  • src/api/fixtureProposalReader.ts
  • src/api/focuses.ts
  • src/api/proposals.ts
  • src/api/tauriFocusReader.ts
  • src/api/tauriProposalReader.ts
  • src/api/tauriReader.ts
  • src/components/App.tsx
  • src/hooks/useAppState.test.tsx
  • src/hooks/useAppState.ts
  • src/hooks/useCaps.ts
  • src/hooks/useFocuses.test.tsx
  • src/hooks/useFocuses.ts
  • src/hooks/useProposals.ts
💤 Files with no reviewable changes (5)
  • src/hooks/useProposals.ts
  • src/hooks/useCaps.ts
  • src/hooks/useFocuses.test.tsx
  • src/api/focuses.ts
  • src/hooks/useFocuses.ts

Comment thread issues/044-domain-timer-ticker.md Outdated
Comment thread src/api/fixtureProposalReader.ts Outdated
Resolves two CodeRabbit findings on PR #51.

1. (Major) `src/api/*` was importing `PolledReader` and `Unsubscribe`
   from `src/hooks/usePolledReader`, inverting the CLAUDE.md dependency
   direction (`hooks/` calls `api/`, not the other way). The contract
   the api/ readers implement belongs in api/.

   - New `src/api/polledReader.ts` owns `PolledReader<T>` + `Unsubscribe`
   - `src/hooks/usePolledReader.ts` keeps `usePolledReader<T>` hook and
     `PolledState<T>` discriminated union; imports `PolledReader` from
     api/polledReader
   - All 6 api/ files now import from `./polledReader`; no api/ file
     references hooks/
   - Components and the hook tests import the type from `api/polledReader`
     and the hook from `hooks/usePolledReader`

2. (Minor) Issue 044's spec block showed `now_secs: u64`; implementation
   uses `i64`. Unix timestamps are signed; matched the spec to the impl
   (no code change).

`task check` green. 69/69 frontend tests pass.
@archae0pteryx
archae0pteryx merged commit dd43677 into main May 11, 2026
2 checks passed
@archae0pteryx
archae0pteryx deleted the feat/architecture-deepening branch May 11, 2026 22:33
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