Feat/genui - #64
Conversation
…HR tool Batches several in-flight features that were sitting uncommitted: - Bodyweight/assisted pullup volume: (BW - assist + extra) * reps - MLService reads the past 3 sessions and recovers from a deload week using the pre-deload baseline instead of the deload trough - PRManager scopes records per handle variation (Rope vs Bar) - CoachToolService.get_sleeping_hr_analytics: p5/p25/mean, stdev, variance and linear trend over the last N nights - GenUI parser tolerates numeric StatCard values, loose trend words and Markdown code fences Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Foundation for the genui refactor: a never-throwing view over raw component prop maps that resolves keys by exact match, normalized match (case/underscore/hyphen/space-insensitive), then semantic alias, and coerces values to typed accessors with documented fallbacks instead of throwing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the four-in-one component contract (A2UiSpec) that lets each UI component name itself, parse its own props, build its own widget and document itself for the LLM prompt on one object, plus the A2UiRegistry lookup table that replaces the old allowedA2UiComponents set and two parallel switch statements. Includes an A2UiTheme skeleton (filled in by Task 4) and A2UiNode, the parsed-tree node type. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code review found that A2UiRegistry's constructor loop silently resolved canonical-name/alias collisions (last-writer-wins for names, first-writer-wins for aliases), which would produce unreachable specs or dropped aliases with no signal as more components are registered in later tasks. The constructor now throws a StateError identifying both colliding specs for any of: two specs sharing a canonical name, an alias colliding with another spec's canonical name, or two specs sharing an alias. Adds three regression tests using a new configurable _NamedFakeSpec fake. Also documents (doc-comment only, no behavior change) that A2UiNode.children is not defensively copied, per the review's Minor finding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the single gate that decides whether an LLM reply is a UI payload or ordinary prose, and turns UI payloads into an A2UiNode tree. Handles markdown fences, prose-wrapped JSON, flat vs props-wrapped shapes, bare-array/envelope auto-wrapping into GridContainer, and recursive children, without ever throwing. Also promotes A2UiProps._asStringKeyed to a public static A2UiProps.stringKeyed so the parser can re-key decoded JSON maps without an awkward part-of coupling between the two libraries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_extractJson previously sliced from the first { to the last }, which
broke on any stray brace in surrounding prose (e.g. "add reps
{optional}"). Replace with a scan that tries jsonDecode on every
balanced {..}/[..] span found via a depth counter that correctly skips
brackets inside string literals, preferring the longest successful
decode as the actual payload.
Also fix _wrap's unconditional single-child collapse: an explicit
envelope key ({"components":[...]}) is a deliberate container request
and must still produce a GridContainer with one child, while a bare
top-level array with one item keeps collapsing since it's ambiguous
between "a list of one" and "just one component."
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds A2UiThemeProvider (InheritedWidget, falls back to A2UiTheme.dark) and the panel/title/empty-state/legend widgets every component spec will share, plus lib/theme/a2ui_app_theme.dart mapping RepForge's real design tokens onto A2UiTheme. This is the only file where the two systems meet - lib/genui/ still imports nothing app-specific. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The injection test compared against repforgeA2UiTheme, which is field-for-field identical to the A2UiThemeProvider.of fallback (A2UiTheme.dark), so it passed even if the InheritedWidget lookup were broken. Inject a fixture with distinct values instead, and assert a sibling context still falls back to the default. Also add direct coverage for A2UiPanel's padding, decoration, and child rendering, previously only exercised indirectly via A2UiEmptyPanel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A2UiSeries.extract() and maxValue() give line/bar/pie and radar chart
components one common {name, values} shape to consume, so a model that
learns {labels, series} once can drive all four components.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ax bug Address code review findings on A2UiSeries: - Add tests pinning down the series->values fallback when every series entry drops to empty/unparseable values, and when series is an empty list — the risky path the brief called out but left untested. - Rename the misleading 'reads the axes alias' test; it only exercised stringified-number coercion inside series values, not alias resolution. - Fix maxValue() to track whether any value has been seen instead of seeding with 0.0, so all-negative series report their true max instead of silently clamping to 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Establishes the pattern for Tasks 7-13: a typed props record, an A2UiSpec bundling name/aliases/doc/parseProps/buildWidget, and never-throwing parsing that degrades to documented fallbacks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes the validator/renderer contradiction where a String value was accepted but cast to num, and the min == max NaN sweep angle bug. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the most-used and most complex A2UI component so far, covering
line/bar/pie rendering over the shared {labels, series} shape with
never-throwing prop parsing and label padding to prevent out-of-range
axis lookups.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds paired x/y observation plotting with an optional correlation badge, following the Task 6-8 A2UiSpec pattern. Malformed points are dropped rather than throwing, and bounds widen degenerate axes so fl_chart never sees a zero-span range. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 10 of the a2ui/genui refactor: RadarChart consumes the same
{labels, series} shape as DynamicChart, with `axes` kept as a
backward-compatible alias for `labels`. Every series is truncated
or zero-padded to labels.length at parse time so fl_chart's radar
never sees a mismatched entry count.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a titled list-of-rows component with a defensive row-extraction fallback chain: named fields, bare scalars, first-stringifiable-value fallback, and silent drop of rows with nothing displayable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Renders a decorative, non-interactive row of scope chips (e.g. "7d / 30d / 90d") and fixes the old renderer's `activeOption as String` crash by matching case-insensitively and falling back to null instead of throwing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 13: assembles all eight leaf components into defaultA2UiRegistry,
adds the GridContainerSpec layout wrapper, the public A2UiRenderer
widget, and the lib/genui/a2ui.dart barrel file that will be the only
import path the rest of the app uses going forward.
fix(genui): make structural children lookup exact, not alias-resolved
Cross-task fix to a2ui_parser.dart (a Task 3 file), discovered during
Task 13 registry integration. A2UiParser._parseChildren and
_declaresChildren resolved the structural `children` key through
A2UiProps' alias-aware lookup(), which treats `items` as an alias for
`children`. That collided with DataListGroupSpec, whose own canonical
data-row key is also `items`: a DataListGroup node's `items` list of
{primaryText, ...} maps was mistaken for child components, none of
them parsed as one, and the whole node was then discarded as an
emptied-out container. Reading the literal `children` key only fixes
this and matches the precision _envelopeKeys already had (it does not
include `items` as a synonym for `children` either).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces hand-written component-schema prose in the coach system prompt with a section generated from defaultA2UiRegistry, so the vocabulary advertised to the model can never drift from what the parser/renderer actually support. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…enderer Replaces private _CoachMessageContent with a public, stateful CoachMessageContent that memoizes parsing per text value and shows a "Building dashboard..." placeholder for partial JSON while streaming, instead of letting raw braces scroll past or losing prose on a mixed reply. Wraps the app root in A2UiThemeProvider(theme: repforgeA2UiTheme) so the renderer picks up RepForge's design tokens. Deletes the superseded lib/genui/a2ui_component.dart and lib/genui/a2ui_renderer.dart, and drops test/new_features_test.dart's GenUI Component Resilience Tests group, whose two cases are already covered more thoroughly by test/genui/a2ui_parser_test.dart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
minY was hardcoded to 0 while maxY derived from the true series max, so an all-negative dataset (e.g. [-10, -5, -3]) produced a visible axis range of [0, 1] with every real data point falling outside it — a silent blank chart despite valid, non-empty data. Adds A2UiSeries.minValue mirroring the existing maxValue, and a shared _yBounds helper used by both _line and _bar so the two renderers can't diverge on axis math. Also covers multi-series label padding, which was previously only exercised through series[0]. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 9 review flagged that ScatterPlotProps.bounds had no regression pin for all-negative-coordinate spreads (same failure class as Task 8's DynamicChartSpec axis bug) and that point-parsing had no test for structurally invalid entries (nested objects, raw lists). Adds both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s/content
Follow-up to the Task 13 a2ui_parser.dart fix: restricting the per-node
_parseChildren/_declaresChildren lookup to the literal 'children' key
was narrower than intended. It regressed 'components'/'elements'/
'content' as per-node child-list keys, which never collided with
anything (only 'items' did, via DataListGroup's own canonical data key).
A payload like {"component":"GridContainer","props":{"columns":1,
"components":[...]}} resolved fine before the original bug and silently
rendered blank (zero children, no null fallback) after the first fix,
since _declaresChildren no longer recognized 'components' as a
children-declaring key either.
Adds a _childKeys constant (children/components/elements/content,
still excluding items) mirroring _envelopeKeys' existing tolerance, and
routes both _parseChildren and _declaresChildren through a shared
_firstChildList literal (non-alias) lookup over that key set.
Adds regression tests in a2ui_renderer_test.dart: per-node
components/elements/content resolve to real children, and items stays
excluded at the per-node level.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…uous memoization test
looksLikeUi only checked whether the text, after stripping a *leading*
fence, started with `{`/`[`. A model that writes a sentence before
opening a fenced payload (e.g. "Here is your data:\n```json\n{...")
fell through undetected, so CoachMessageContent showed the raw partial
JSON instead of the streaming placeholder -- the exact symptom this
task exists to fix. Now also treats an unclosed ``` fence found
anywhere in the streamed-so-far text as a UI signal, while plain prose
with no JSON or fence anywhere still returns false.
Also fixes the memoization regression test in
test/screens/ai_coach_genui_test.dart: the second observation was
taken after a bare `tester.pump()`, which doesn't mark the element
dirty and never actually calls build() again, so the test could not
distinguish memoized parsing from a widget that never rebuilds at all.
It now pumps a second CoachMessageContent instance with identical text
at the same tree location, which reuses the existing State and
genuinely triggers didUpdateWidget/build.
Adds regression tests for both the prose-prefixed-fence case and the
plain-prose-no-json case in test/genui/a2ui_parser_test.dart, plus a
widget-level test in test/screens/ai_coach_genui_test.dart confirming
the placeholder (not raw JSON) renders end-to-end.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… fuzz suites The sleeping-HR analytics tool was hand-constructing an A2UI DynamicChart payload directly, leaking presentation decisions into the data layer. Replace `genui_chart_props` with neutral `labels`/`series` keys so the prompt — not the tool — decides how to present the data. Add two permanent guard suites: a2ui_purity_test.dart proves lib/genui/ never imports app-specific code (theme/models/services/screens) and its component renderers never cast raw model data; a2ui_robustness_test.dart fuzzes the parser and renderer against ~26 hostile/malformed LLM payloads to confirm nothing throws. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sions Review of the previous commit found the purity test's forbidden-import check was depth-blind: its literal needle list only covered one and two ../ hops, but components live three levels below lib/, so a real ../../../theme/... import passed undetected. Replace it with a regex that matches any number of ../ hops (or a package:repforge/ prefix), covering import and export directives alike, and add a self-test that proves the regex catches every relevant depth/form without touching real source files. Also widen the no-raw-casts check to include bool/Object/dynamic, make the components-directory scan recursive, and pin down the two historical silent-visual regressions (Task 8's chart axis-bounds clamp, Task 13's GridContainer child-key aliasing) with positive assertions in the fuzz suite, since neither throws and the existing no-throw checks structurally can't catch either. Reword analyze_health_workout_correlation's tool declaration to drop direct component names, closing the same presentation-leak class this task already fixed for the sleeping-HR tool. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lose review findings Final whole-branch review fix wave for the A2UI genui refactor: - A2UiRenderer's registry override used to be silently dropped past one level of nesting because GridContainerSpec recurses via bare A2UiRenderer(node: ...) calls. Mirror the existing theme-injection pattern with a new A2UiRegistryProvider InheritedWidget so an explicit registry override at any level propagates ambiently to everything below it (explicit param > inherited provider > defaultA2UiRegistry fallback). - Pin the hand-written "WHICH COMPONENT TO REACH FOR" prose in gemini_context_builder.dart against silent drift: every component name it mentions must resolve in defaultA2UiRegistry, and the registry's spec count is asserted directly. - Delete A2UiProps.object()/has() — confirmed zero call sites. - Repurpose the orphaned Task 3 scaffolding test (a2ui_parser_stub_test.dart, redundant with a2ui_parser_test.dart) into a2ui_custom_registry_test.dart, the regression coverage the registry- propagation fix needed. - Add scanned-file-count floors to the purity test's two directory scans so an empty/unreachable directory can't produce a vacuous pass. - Document FilterChips' SizedBox.shrink() as a deliberate exception to the plan's "always A2UiEmptyPanel" rule (decorative chrome, not data). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WalkthroughChangesA2UI dashboard rendering
Workout and AI features
Build and repository configuration
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## r2.1.0 #64 +/- ##
==========================================
+ Coverage 74.90% 75.79% +0.88%
==========================================
Files 88 108 +20
Lines 14491 15839 +1348
==========================================
+ Hits 10855 12005 +1150
- Misses 3636 3834 +198 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 48
🤖 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 @.github/workflows/release.yml:
- Around line 49-51: Update the linker-patching step after flutter pub get to
require and quote PUB_CACHE, restrict find to resolved jni-*/src/CMakeLists.txt
targets, and make the replacement idempotent so existing --build-id=none flags
are not duplicated. Remove the failure suppression and ensure the step exits
nonzero when no target file is found or patching fails.
In `@workout-logger/lib/genui/src/a2ui_panels.dart`:
- Around line 46-65: Update the trailing label Text in the Row alongside the
title to be wrapped with Flexible, and configure it with maxLines: 1 and
TextOverflow.ellipsis so model-provided labels cannot cause a RenderFlex
overflow.
In `@workout-logger/lib/genui/src/a2ui_parser.dart`:
- Around line 208-237: Update _extractJson to first attempt jsonDecode on the
complete stripped text and immediately return the decoded Map or List when
successful. Only run the existing balanced-candidate scan when whole-text
decoding fails, preserving the current fallback behavior for prose-wrapped JSON.
- Around line 78-98: Update the effective-props construction in the parser
around A2UiProps.stringKeyed and _parseChildren so that when json['props'] is a
Map, literal child-related keys from the outer json object are copied into
effective only when those keys are absent from props. Preserve props values when
both levels define the same key, and keep the existing flat-payload behavior
unchanged.
In `@workout-logger/lib/genui/src/a2ui_renderer.dart`:
- Around line 26-35: Update A2UiRenderer.build around the specFor lookup to emit
a debug-only diagnostic containing node.name when spec is null, then preserve
the existing SizedBox.shrink() fallback. Use the project’s existing debug
logging mechanism rather than changing rendering behavior.
In `@workout-logger/lib/genui/src/a2ui_spec.dart`:
- Around line 54-58: Update the A2UiSpec contract methods buildWidget and render
to use named parameters for all three arguments, then update every
implementation and call site consistently, including the renderer and parseProps
flow. Preserve the existing behavior and argument types, or document an explicit
exemption if the Widget build-style API must remain positional.
In `@workout-logger/lib/genui/src/a2ui_theme.dart`:
- Around line 41-42: Guard the public A2UiTheme palette contract so seriesColor
never evaluates a modulo operation with an empty seriesPalette. Update the
A2UiTheme constructor to assert or otherwise enforce a non-empty palette, while
preserving the existing indexed cycling behavior for valid palettes.
In `@workout-logger/lib/genui/src/components/dynamic_chart.dart`:
- Around line 223-249: Update _pie to filter props.series.first.values to
positive entries before calculating total or creating PieChartSectionData,
preserving each entry’s original index so props.labels remains aligned. Return
A2UiEmptyPanel when no positive values remain, and use the filtered values for
percentages and section geometry.
In `@workout-logger/lib/genui/src/components/metric_gauge.dart`:
- Around line 217-221: Update _GaugeArcPainter.shouldRepaint to also compare the
track property from oldDelegate, ensuring the painter repaints when the
background arc color changes while preserving the existing progress, from, and
to comparisons.
In `@workout-logger/lib/genui/src/components/stat_card.dart`:
- Around line 85-94: Update the value-formatting logic around rawValue, unit,
and the contains check to determine whether the unit is already present only
when it matches the trimmed suffix of rawValue. Preserve the existing fallback,
empty-unit handling, and spacing behavior while preventing short units from
matching unrelated text.
In `@workout-logger/lib/main.dart`:
- Around line 141-143: Update the CoachToolService constructor and every call
site, including the shown dependency injection call, to use named parameters for
all three dependencies. Preserve the existing dependency wiring while making
each argument explicit by name.
In `@workout-logger/lib/models/models.dart`:
- Around line 138-151: The calculateVolume method should use final for locals
that are not reassigned: replace the mutable effW declaration and conditional
assignments with a single final conditional expression, and change the drops
iteration variable from var drop to final drop.
- Around line 138-157: Persist assisted-bodyweight semantics so default volume
calculations use effective load rather than assistance weight: update
WorkoutSet.calculateVolume and its persisted fields in
workout-logger/lib/models/models.dart:138-157, aggregate the persisted
assisted-set calculation in ExerciseLog.totalVolume at
workout-logger/lib/models/models.dart:240-243, and update the logging flow at
workout-logger/lib/screens/workout_flow_screen.dart:548-555 to store assistance,
extra load, and a body-weight snapshot (or the effective load) for assisted
exercises.
In `@workout-logger/lib/screens/widgets/exercise_input_section.dart`:
- Around line 75-76: Update the assisted-load display calculations around
isAssistedBW and the equivalent values at the referenced later section to pass
every displayed weight through settings.toDisplay, including effectiveWeight,
settings.userBodyWeight, and currentWeight, while keeping the stored kilogram
values unchanged.
- Around line 75-76: The assisted-exercise classification is inconsistent
between the load panel and _InputRow for dips and push_ups. Reuse the existing
isAssistedBW result when constructing _InputRow, pass it into the widget, and
update _InputRow to use that value for its labeling instead of maintaining a
separate exercise-ID predicate.
- Around line 194-223: The handle selector in build must not display an
unpersisted first handle as selected; require or persist an explicit selection
before logging. In
workout-logger/lib/screens/widgets/exercise_input_section.dart:194-223, update
the active-selection logic accordingly. In
workout-logger/lib/services/workout_provider.dart:506-515, update
setExerciseHandle so existing recorded set handles are never rewritten; lock the
handle after the first set or create a separate log for each variation.
In `@workout-logger/lib/screens/workout_flow_screen.dart`:
- Around line 322-325: Make _loadLastSessionData pass the current log handle
from the onHandleChanged callback. In
workout-logger/lib/services/workout_provider.dart lines 674-696, require an
exact handle match whenever handle is non-empty, excluding logs with null or
different handles. Apply the same rule in lines 699-713 for last-session lookup,
using any legacy fallback only after no exact match exists.
In `@workout-logger/lib/services/ai/coach_tool_service.dart`:
- Around line 414-425: Clamp the model-provided days value in the tool method
before the loop that calls hh.sleepNight, reusing the existing _limitArg helper
and its supported range as other tools do. Keep the default of 14 for missing
input, then iterate using the clamped value.
- Around line 523-546: Update
workout-logger/lib/services/ai/coach_tool_service.dart#L523-L546 so
_getHealthMetrics uses the requested days window rather than always querying one
week, and align the get_health_metrics declaration with the sleep fields
actually returned instead of promising resting HR or readiness data. Update
workout-logger/lib/services/ai/coach_tool_service.dart#L599-L606 by removing the
resting_hr x_metric branch/description unless it is backed by a real data
source; no additional metric should be advertised without implementation
support.
- Around line 625-642: Remove the synthetic sleep fallback block guarded by
xVals.length < 2 in the analysis method, including the generated synthSleep
values and additions to xVals, yVals, and points. When fewer than two real
paired points remain, return the existing insufficient-data error so
correlation, regression, and chart points are never produced from fabricated
health data.
- Around line 714-720: Update get_muscle_group_volume’s matching logic to
resolve each requested group with _resolveMuscleGroup, then compare resolved ids
using _wp.getMuscleGroupName(e.primaryMuscle) instead of raw display-name
substring matching. Preserve valid group requests such as “Quadriceps” and
“Lower Back”, and include secondary muscle activations in the aggregation
alongside each exercise’s primary group.
- Around line 26-28: Update CoachToolService’s constructor to accept the
optional HealthHistoryManager as a named parameter, then update all
instantiations to pass it using healthHistory: while preserving existing
dependency behavior.
In `@workout-logger/lib/services/ai/gemini_ai_service.dart`:
- Around line 91-95: Restrict daily-quota classification in
_isDailyQuotaExhausted within
workout-logger/lib/services/ai/gemini_ai_service.dart:91-95 and
is_daily_quota_exhausted in workout-logger/scripts/test_gemini_api.py:63-64 to
daily-limit identifiers such as GenerateRequestsPerDay or free_tier_requests, or
exact daily metrics; remove generic QuotaExceeded and RESOURCE_EXHAUSTED matches
so minute-scale rate limits continue through retry-delay handling.
- Line 496: Preserve each function-call ID through the response flow: in
workout-logger/lib/services/ai/gemini_ai_service.dart lines 457-496, retain
fc['id'] on each FunctionCall and include the matching ID in every emitted
functionResponse; in workout-logger/scripts/test_gemini_api.py lines 242-255,
copy fc["id"] into each generated functionResponse.
- Around line 98-105: Make the Gemini fallback payload compatible with each
selected model: update _getFallbackModel and the request-building logic around
thinkingConfig so gemini-2.5-flash uses thinkingBudget rather than Gemini 3.x
thinkingLevel, or remove that fallback. Apply the same compatible fallback chain
and per-model thinkingConfig normalization in
workout-logger/lib/services/ai/gemini_ai_service.dart at lines 98-105 and
258-259, and workout-logger/scripts/test_gemini_api.py at lines 67-72 and 221 so
retrying with a new model rebuilds the configuration.
In `@workout-logger/lib/services/interfaces/ml_service_interface.dart`:
- Around line 78-83: Update the documentation for MLService.recommendSets to
state that pastSessions must be ordered most recent first, with index 0 as the
latest prior session and index 1 as the preceding session. Replace the “past 3
sessions” wording with documentation matching the implementation’s two-entry
usage.
In `@workout-logger/lib/services/ml_service.dart`:
- Around line 449-457: Update the post-deload recovery branch in the
recommendation logic to avoid embedding raw set.weight with a hardcoded kg label
in the reasoning string. Either remove the weight value from this message or
route its display through the existing presentation-layer formatting, such as
SettingsProvider.formatWeight, so both units and numeric formatting respect the
user’s settings.
- Around line 375-391: Update deload detection in recommendSets to require the
comparison session to be recent, preventing permanent resets from triggering
recovery from an older workout. Use effective load, consistent with set.volume,
when calculating w0 and w1 for assisted bodyweight exercises instead of raw
set.weight, and cap any recovery recommendations derived from refSets near the
last performed weight rather than allowing high-confidence loads far above
lastSession.
In `@workout-logger/lib/services/settings_provider.dart`:
- Around line 51-53: The body-weight validation in the storage-load path and
setUserBodyWeight must reject non-finite or non-positive values. Update both
paths to accept only finite values greater than zero, persist only valid inputs,
and use 70.0 when the stored userBodyWeight is invalid.
In `@workout-logger/lib/services/workout_provider.dart`:
- Around line 660-671: Update getRecommendations so handle-scoped requests do
not pass the exercise-wide _growthModels[exerciseId] into recommendSets. Key
growth models by both exerciseId and handle when the training contract supports
it; otherwise pass no growth model for handle-scoped recommendations while
preserving the existing exercise-wide behavior for unscoped requests.
In `@workout-logger/lib/theme/a2ui_app_theme.dart`:
- Around line 1-2: Update the imports in the theme adapter to use the public
genui barrel exported by lib/genui/a2ui.dart, importing A2UiTheme through
package:repforge/genui/a2ui.dart instead of the internal src/a2ui_theme.dart
path.
In `@workout-logger/scripts/test_gemini_api.py`:
- Around line 93-98: Update the retry/fallback flow in the test_gemini_api
function so a quota response on the final attempt cannot fall through and return
None after selecting a fallback. Track fallback attempts independently from the
normal retry limit, or raise a clear error when no attempts remain; ensure the
function always returns its expected dict result or an explicit exception before
callers invoke res1.get(...).
In `@workout-logger/test/genui/a2ui_prompt_test.dart`:
- Around line 39-42: Update the example extraction around start and end so the
closing brace search is limited to the region beginning at markerIndex, rather
than using section.lastIndexOf('}') across the entire section. Preserve the
existing substring and validation behavior while ensuring later prompt content
cannot extend the extracted example.
In `@workout-logger/test/genui/a2ui_purity_test.dart`:
- Around line 9-11: Update the _forbiddenPathPattern in the purity test to
include the app-specific data directory alongside theme, models, services, and
screens, ensuring package and relative imports from data are rejected by the
existing guard.
- Around line 95-96: Hoist the cast-matching RegExp used in the loop in the
relevant purity test function into a top-level _castPattern declaration beside
_forbiddenPathPattern and the other shared patterns. Replace the inline RegExp
construction in the per-line check with this shared pattern, preserving the
existing matching expression and behavior.
In `@workout-logger/test/genui/a2ui_registry_test.dart`:
- Around line 133-136: Update the A2UiNode instantiation in the test to use the
const constructor, preserving its existing name and props arguments so
prefer_const_constructors is satisfied.
In `@workout-logger/test/genui/a2ui_renderer_test.dart`:
- Around line 153-158: Update the comment near the GridContainer assertions to
remove the contradictory statement that the node is dropped entirely; document
only that excluding items yields a real zero-children node and the expected
non-crashing behavior, matching the assertions.
In `@workout-logger/test/genui/a2ui_robustness_test.dart`:
- Around line 144-145: Strengthen the minY assertion in the chart axis test to
require the axis minimum to bracket the dataset’s true minimum of -50, rather
than merely being below -10. Keep the existing maxY assertion and the rest of
the chart test unchanged.
- Around line 80-84: Update the test names in both the parser loop and the
“renderer never throws” loop to derive from each payload’s truncated string
instead of the mutable list index. Preserve enough payload content for stable
failure attribution while keeping names concise.
In `@workout-logger/test/genui/a2ui_theme_test.dart`:
- Around line 138-143: Update the Container finders in both assertions around
the panel decoration checks to scope them to the panel widget under test instead
of using the global find.byType(Container). Apply the same scoped finder change
at the second assertion near line 159, preserving the existing padding, color,
and border expectations.
In `@workout-logger/test/genui/components/dynamic_chart_test.dart`:
- Around line 152-161: Add a widget test alongside the existing pie-chart test
that supplies mixed-sign values such as [60, -40], verifies the pie chart
renders without throwing, and confirms the expected fallback behavior from _pie
when percentage calculation receives invalid values.
- Around line 163-175: Extend the testWidgets case for multi-series non-pie
charts to also pump a single-series chart and a multi-series pie chart,
asserting that A2UiLegend is absent in both cases while preserving the existing
positive legend assertions. Add the A2UiLegend import from a2ui_panels.dart and
use it to verify the showLegend condition.
In `@workout-logger/test/genui/components/scatter_plot_test.dart`:
- Around line 89-108: Extend the hostile-input tests around parse to include
points whose x or y coordinates are "NaN" and "Infinity" strings. Assert that
the resulting bounds contain only finite values, ensuring non-finite parsed
doubles are rejected before entering the bounds calculation.
In `@workout-logger/test/genui/components/stat_card_test.dart`:
- Around line 53-56: Add a test case in the “appends a unit that is not already
present” test covering a unit that appears only as a substring, such as value
“10 reps” with unit “s”, and assert the intended formatted result so the
behavior described in stat card parsing is pinned.
- Around line 11-23: Update the test helper pump to pass its props argument into
A2UiProps instead of constructing empty properties, so StatCardSpec.render
receives the caller’s values. Then replace the duplicated pumpWidget setup in
the title/value/subtitle test with a direct pump call using the intended
StatCard properties.
In `@workout-logger/test/new_features_test.dart`:
- Around line 192-231: Extend the CoachToolService test group with cases
covering get_health_metrics, analyze_health_workout_correlation, and
get_muscle_group_volume. Add assertions for the _hh == null error path,
insufficient paired-data behavior, and muscle-group lookup using a display name,
ensuring each test exercises the relevant days window, synthetic-data fallback,
and muscle-id matching defects without changing the existing sleeping-HR test.
- Around line 99-104: Update the assisted pullups test around calculateVolume to
use different values for WorkoutSet.weight and WorkoutSet.assistWeight, while
preserving the expected volume based on bodyweight minus assistWeight plus any
extra weight, multiplied by reps. Choose values that would produce a different
result if weight were incorrectly used in place of assistWeight.
In `@workout-logger/test/settings_provider_test.dart`:
- Line 23: Update SettingsProvider’s _getFallbackModel to use the supported
legacy model gemini-3.5-flash instead of gemini-3.6-flash, while preserving the
existing configuration and AI service behavior.
🪄 Autofix
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: 74411819-f2bf-4d14-a3f4-c07c5d50e60a
📒 Files selected for processing (67)
.github/workflows/release.yml.gitignorefdroid/metadata/com.devasy.repforge.ymlscripts/patch_so.pyworkout-logger/lib/data/exercise_database.dartworkout-logger/lib/genui/a2ui.dartworkout-logger/lib/genui/src/a2ui_node.dartworkout-logger/lib/genui/src/a2ui_panels.dartworkout-logger/lib/genui/src/a2ui_parser.dartworkout-logger/lib/genui/src/a2ui_prompt.dartworkout-logger/lib/genui/src/a2ui_props.dartworkout-logger/lib/genui/src/a2ui_registry.dartworkout-logger/lib/genui/src/a2ui_renderer.dartworkout-logger/lib/genui/src/a2ui_series.dartworkout-logger/lib/genui/src/a2ui_spec.dartworkout-logger/lib/genui/src/a2ui_theme.dartworkout-logger/lib/genui/src/components/data_list_group.dartworkout-logger/lib/genui/src/components/dynamic_chart.dartworkout-logger/lib/genui/src/components/filter_chips.dartworkout-logger/lib/genui/src/components/grid_container.dartworkout-logger/lib/genui/src/components/metric_gauge.dartworkout-logger/lib/genui/src/components/radar_chart.dartworkout-logger/lib/genui/src/components/scatter_plot.dartworkout-logger/lib/genui/src/components/stat_card.dartworkout-logger/lib/genui/src/default_registry.dartworkout-logger/lib/main.dartworkout-logger/lib/models/models.dartworkout-logger/lib/screens/ai_coach_screen.dartworkout-logger/lib/screens/widgets/exercise_input_section.dartworkout-logger/lib/screens/workout_flow_screen.dartworkout-logger/lib/services/ai/coach_tool_service.dartworkout-logger/lib/services/ai/gemini_ai_service.dartworkout-logger/lib/services/gemini_context_builder.dartworkout-logger/lib/services/interfaces/ai_service_interface.dartworkout-logger/lib/services/interfaces/ml_service_interface.dartworkout-logger/lib/services/managers/pr_manager.dartworkout-logger/lib/services/ml_service.dartworkout-logger/lib/services/settings_provider.dartworkout-logger/lib/services/workout_provider.dartworkout-logger/lib/theme/a2ui_app_theme.dartworkout-logger/pubspec.yamlworkout-logger/scripts/test_gemini_api.pyworkout-logger/test/ai_coach_view_model_test.dartworkout-logger/test/gemini_context_builder_test.dartworkout-logger/test/genui/a2ui_custom_registry_test.dartworkout-logger/test/genui/a2ui_parser_test.dartworkout-logger/test/genui/a2ui_prompt_test.dartworkout-logger/test/genui/a2ui_props_test.dartworkout-logger/test/genui/a2ui_purity_test.dartworkout-logger/test/genui/a2ui_registry_test.dartworkout-logger/test/genui/a2ui_renderer_test.dartworkout-logger/test/genui/a2ui_robustness_test.dartworkout-logger/test/genui/a2ui_series_test.dartworkout-logger/test/genui/a2ui_theme_test.dartworkout-logger/test/genui/components/data_list_group_test.dartworkout-logger/test/genui/components/dynamic_chart_test.dartworkout-logger/test/genui/components/filter_chips_test.dartworkout-logger/test/genui/components/metric_gauge_test.dartworkout-logger/test/genui/components/radar_chart_test.dartworkout-logger/test/genui/components/scatter_plot_test.dartworkout-logger/test/genui/components/stat_card_test.dartworkout-logger/test/new_features_test.dartworkout-logger/test/routine_optimizer_screen_test.dartworkout-logger/test/routine_optimizer_view_model_test.dartworkout-logger/test/screens/ai_coach_genui_test.dartworkout-logger/test/settings_provider_test.dartworkout-logger/test/test_utils/mock_ml_service.dart
💤 Files with no reviewable changes (1)
- scripts/patch_so.py
| /// Colour for series index [i], cycling through [seriesPalette]. | ||
| Color seriesColor(int i) => seriesPalette[i % seriesPalette.length]; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Guard seriesColor against an empty seriesPalette.
seriesPalette is a required public field with no non-empty constraint. If a host app injects a theme with an empty palette through A2UiThemeProvider, i % seriesPalette.length evaluates i % 0 and throws. Every chart component calls seriesColor during build, so the whole dashboard fails to render. A2UiTheme.dark and repforgeA2UiTheme both supply five colors today, so the fault is not reachable in-repo, but the public constructor allows it.
Add a fallback color, or assert a non-empty palette in the constructor.
🛡️ Proposed guard
/// Colour for series index [i], cycling through [seriesPalette].
- Color seriesColor(int i) => seriesPalette[i % seriesPalette.length];
+ Color seriesColor(int i) => seriesPalette.isEmpty
+ ? accent
+ : seriesPalette[i % seriesPalette.length];📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /// Colour for series index [i], cycling through [seriesPalette]. | |
| Color seriesColor(int i) => seriesPalette[i % seriesPalette.length]; | |
| /// Colour for series index [i], cycling through [seriesPalette]. | |
| Color seriesColor(int i) => seriesPalette.isEmpty | |
| ? accent | |
| : seriesPalette[i % seriesPalette.length]; |
🤖 Prompt for 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.
In `@workout-logger/lib/genui/src/a2ui_theme.dart` around lines 41 - 42, Guard the
public A2UiTheme palette contract so seriesColor never evaluates a modulo
operation with an empty seriesPalette. Update the A2UiTheme constructor to
assert or otherwise enforce a non-empty palette, while preserving the existing
indexed cycling behavior for valid palettes.
| expect(provider.userName, isNull); | ||
| expect(provider.geminiApiKey, isEmpty); | ||
| expect(provider.geminiModel, equals('gemini-2.5-flash')); | ||
| expect(provider.geminiModel, equals('gemini-3.6-flash')); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Locate the geminiModel default and every hardcoded model id.
set -euo pipefail
rg -n -C4 'geminiModel' workout-logger/lib --type=dart
rg -n 'gemini-[0-9]' workout-logger --type=dart --type=py --type=yamlRepository: Devasy/RepForge
Length of output: 7970
🌐 Web query:
current available Gemini API model names flash
💡 Result:
As of August 7, 2026, the available Gemini "Flash" series models for the Gemini API include the following primary stable and preview versions: Stable Models gemini-3.6-flash: The latest iteration designed for complex, multi-step workflows, improved code generation, and multimodal reasoning [1][2]. gemini-3.5-flash: Designed for sustained frontier performance in agentic and coding tasks [3][4]. gemini-3.5-flash-lite: Optimized as a low-latency, highly cost-effective option for high-volume automation [3]. gemini-3.1-flash-lite: Optimized for high-volume, cost-sensitive tasks [2][3]. gemini-3.1-flash-lite-image: A multimodal model optimized for low-latency, cost-effective image generation and editing [3]. gemini-2.5-flash: A highly capable model balancing intelligence and latency [2]. gemini-2.5-flash-lite: Built for massive scale and high-throughput tasks [2]. gemini-2.5-flash-image: A native image generation model optimized for speed and contextual understanding [5]. Preview Models gemini-3-flash-preview: Features upgraded visual/spatial reasoning and agentic coding capabilities [3][5]. gemini-omni-flash-preview: Currently available in public preview [3]. gemini-3.1-flash-live-preview: Specifically noted for live interaction capabilities [1]. gemini-3.1-flash-tts-preview: Specifically noted for text-to-speech capabilities [1]. gemini-2.5-flash-live-preview: Specifically noted for live interaction [1]. gemini-2.5-flash-tts-preview: Specifically noted for text-to-speech capabilities [1]. Alias Models The Gemini API also provides convenience aliases that automatically point to the latest release of a model series, such as gemini-flash-latest and gemini-flash-lite-latest [5]. Note: Gemini models are typically accessed in the API using the prefix models/ (e.g., models/gemini-3.6-flash) [6]. Versioning can follow stable, preview, or specific dated patterns [1]. You can verify the most current list of available models programmatically using the models.list method in the Gemini API [6].
Citations:
- 1: https://ai.google.dev/gemini-api/docs/models
- 2: https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/google-models
- 3: https://ai.google.dev/gemini-api/docs/changelog
- 4: https://ai.google.dev/gemini-api/docs/models/gemini-3.5-flash
- 5: https://ai.google.dev/api/interactions-api
- 6: https://ai.google.dev/api/models
🌐 Web query:
google_generative_ai 0.4.3 model id gemini-3.6-flash
💡 Result:
The google_generative_ai Dart SDK (version 0.4.3) and the gemini-3.6-flash model are distinct components with different statuses within the Google AI ecosystem as of August 7, 2026. google_generative_ai (SDK) The google_generative_ai package is a legacy Dart SDK that has been deprecated [1][2]. Version 0.4.3 was released approximately two years ago and contained only internal changes to support the Vertex SDK, with no user-visible changes [3]. Because this library is no longer actively maintained and has been deprecated as of November 30, 2025, it does not receive updates to support newer models like Gemini 3.6 Flash [1][2]. Developers are encouraged to migrate to modern alternatives such as Genkit for Dart or Firebase AI Logic [1][2]. gemini-3.6-flash (Model) Gemini 3.6 Flash is a generally available (GA), production-ready model released in July 2026 [4][5][6]. It is part of the Gemini 3 model family and is designed for high efficiency and performance in coding and agentic tasks [7][6]. This model is accessed through the modern Google GenAI SDKs, which replaced the legacy generative AI libraries [8][1]. Using the legacy google_generative_ai SDK to attempt to call gemini-3.6-flash is not the recommended or supported development path [1].
Citations:
- 1: https://ai.google.dev/gemini-api/docs/libraries
- 2: https://pub.dev/documentation/google_generative_ai/latest/
- 3: https://github.com/google-gemini/deprecated-generative-ai-dart/blob/main/pkgs/google_generative_ai/CHANGELOG.md
- 4: https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/3-6-flash
- 5: https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-3-6-flash-3-5-flash-lite-3-5-flash-cyber/
- 6: https://ai.google.dev/gemini-api/docs/changelog
- 7: https://deepmind.google/models/model-cards/gemini-3-6-flash/
- 8: https://ai.google.dev/gemini-api/docs
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== pubspec dependency =="
rg -n 'google_generative_ai|google_ai' workout-logger/pubspec.yaml workout-logger/pubspec.lock -C2 || true
echo
echo "== google_generative_ai usages =="
rg -n 'google_generative_ai|Generative|GenerativeModel|googleAI|models\.|generate' workout-logger/lib --type=dart --context=2Repository: Devasy/RepForge
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== AI service model constants/constructor =="
sed -n '1,120p' workout-logger/lib/services/ai/gemini_ai_service.dart
echo
echo "== Settings model conversion around kGeminiModels =="
sed -n '860,920p' workout-logger/lib/screens/widgets/profile_sections.dartRepository: Devasy/RepForge
Length of output: 7532
Fallback before relying on gemini-3.6-flash with this SDK.
SettingsProvider and the AI service now default to gemini-3.6-flash, but google_generative_ai 0.4.3 predates that model. Since the actual call does not construct GenerativeModel and instead posts directly to v1beta/models/gemini-3.6-flash:generateContent, API support may be sufficient, but the local model fallback in _getFallbackModel should use a supported legacy default such as gemini-3.5-flash until the new SDK model is safely available through GenerativeModel.
🤖 Prompt for 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.
In `@workout-logger/test/settings_provider_test.dart` at line 23, Update
SettingsProvider’s _getFallbackModel to use the supported legacy model
gemini-3.5-flash instead of gemini-3.6-flash, while preserving the existing
configuration and AI service behavior.
| if (RegExp(r"\bas (String|num|int|double|List|Map|bool|Object|dynamic)\b") | ||
| .hasMatch(lines[i])) { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Hoist the cast pattern out of the loop.
The RegExp is constructed for every line of every component file. Declare it once beside _forbiddenPathPattern, as done for the other two patterns in this file.
♻️ Proposed refactor
Add beside the other patterns:
final RegExp _castPattern =
RegExp(r'\bas (String|num|int|double|List|Map|bool|Object|dynamic)\b');Then:
- if (RegExp(r"\bas (String|num|int|double|List|Map|bool|Object|dynamic)\b")
- .hasMatch(lines[i])) {
+ if (_castPattern.hasMatch(lines[i])) {
violations.add('${entity.path}:${i + 1}: ${lines[i].trim()}');
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (RegExp(r"\bas (String|num|int|double|List|Map|bool|Object|dynamic)\b") | |
| .hasMatch(lines[i])) { | |
| if (_castPattern.hasMatch(lines[i])) { |
🤖 Prompt for 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.
In `@workout-logger/test/genui/a2ui_purity_test.dart` around lines 95 - 96, Hoist
the cast-matching RegExp used in the loop in the relevant purity test function
into a top-level _castPattern declaration beside _forbiddenPathPattern and the
other shared patterns. Replace the inline RegExp construction in the per-line
check with this shared pattern, preserving the existing matching expression and
behavior.
| // GridContainer has no other content, so with `items` correctly | ||
| // excluded from child resolution it has zero children and is dropped | ||
| // entirely rather than silently rendered blank — `_declaresChildren` | ||
| // does not fire for `items`, so this actually returns a real | ||
| // zero-children node here (GridContainer doesn't declare `items` as | ||
| // its own data key), which is the expected non-crashing behavior. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
The comment contradicts itself and the assertions.
The comment states the node "is dropped entirely rather than silently rendered blank", then states it "actually returns a real zero-children node here". The assertions at lines 159-160 confirm the second statement. Remove the first clause so the comment describes the pinned behavior only.
📝 Proposed fix
- // GridContainer has no other content, so with `items` correctly
- // excluded from child resolution it has zero children and is dropped
- // entirely rather than silently rendered blank — `_declaresChildren`
- // does not fire for `items`, so this actually returns a real
- // zero-children node here (GridContainer doesn't declare `items` as
- // its own data key), which is the expected non-crashing behavior.
+ // `_declaresChildren` does not fire for `items`, so the node is not
+ // rejected: it parses into a real GridContainer with zero children.
+ // That is the expected non-crashing behavior — `items` stays
+ // DataListGroup's own data key and is never read as child components.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // GridContainer has no other content, so with `items` correctly | |
| // excluded from child resolution it has zero children and is dropped | |
| // entirely rather than silently rendered blank — `_declaresChildren` | |
| // does not fire for `items`, so this actually returns a real | |
| // zero-children node here (GridContainer doesn't declare `items` as | |
| // its own data key), which is the expected non-crashing behavior. | |
| // `_declaresChildren` does not fire for `items`, so the node is not | |
| // rejected: it parses into a real GridContainer with zero children. | |
| // That is the expected non-crashing behavior — `items` stays | |
| // DataListGroup's own data key and is never read as child components. |
🤖 Prompt for 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.
In `@workout-logger/test/genui/a2ui_renderer_test.dart` around lines 153 - 158,
Update the comment near the GridContainer assertions to remove the contradictory
statement that the node is dropped entirely; document only that excluding items
yields a real zero-children node and the expected non-crashing behavior,
matching the assertions.
| for (var i = 0; i < _payloads.length; i++) { | ||
| test('payload $i', () { | ||
| expect(() => _parser.parse(_payloads[i]), returnsNormally); | ||
| }); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Derive stable test names from the payload.
The names are payload $i. If a payload is inserted in the middle of _payloads, every later test is renamed. That breaks failure attribution across CI runs. Use a truncated payload string in the name instead.
♻️ Proposed refactor
- for (var i = 0; i < _payloads.length; i++) {
- test('payload $i', () {
- expect(() => _parser.parse(_payloads[i]), returnsNormally);
- });
- }
+ for (final payload in _payloads) {
+ final label = payload.isEmpty
+ ? '<empty>'
+ : payload.substring(0, payload.length.clamp(0, 60));
+ test('payload $label', () {
+ expect(() => _parser.parse(payload), returnsNormally);
+ });
+ }Apply the same change to the renderer never throws loop.
🤖 Prompt for 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.
In `@workout-logger/test/genui/a2ui_robustness_test.dart` around lines 80 - 84,
Update the test names in both the parser loop and the “renderer never throws”
loop to derive from each payload’s truncated string instead of the mutable list
index. Preserve enough payload content for stable failure attribution while
keeping names concise.
…scoping - WorkoutSet now snapshots bodyweight/assist/extra at logging time instead of recomputing effective load from the CURRENT profile bodyweight on every read, which was silently corrupting historical volume whenever a user updated their weight. ExerciseLog.totalVolume and the workout_flow_screen logging path thread the snapshot through. - Exercise-handle matching (workout_provider) now requires an exact handle match whenever a handle is set, falling back to legacy behavior only when no exact match exists — a null-handle log was previously matching ANY requested handle, surfacing the wrong variation's "last session" data. - Handle selector no longer visually pre-selects an unpersisted handle, and setExerciseHandle no longer retroactively relabels already-logged sets. - Assisted-load display values now respect the user's unit preference; the assisted-exercise classification is computed once and shared instead of drifting between two separate predicates. - Body-weight input (settings_provider) now rejects non-finite/non-positive values on both the load and set paths, falling back to 70.0 when invalid. - ml_service: deload-recovery reasoning no longer hardcodes "kg" regardless of unit settings; recovery detection now requires the comparison session to be recent and uses effective (not raw) load for assisted exercises. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…le groups by id - get_sleeping_hr_analytics clamps the model-provided days window instead of looping unbounded; get_health_metrics now honors the requested days window instead of always querying one week, and both its and the correlation tool's declarations no longer advertise fields (resting HR, readiness) that aren't actually backed by implementation. - analyze_health_workout_correlation no longer fabricates synthetic sleep data points to pad out insufficient real pairs — returns the existing insufficient-data error instead, so correlation/regression/chart output is never partly made up. - get_muscle_group_volume now resolves requested names to ids via _resolveMuscleGroup and compares ids (also aggregating secondary muscle activations) instead of raw display-name substring matching. - CoachToolService's optional HealthHistoryManager is now a named parameter. - gemini_ai_service: daily-quota classification narrowed to actual daily-limit identifiers so minute-scale rate limits go through normal retry-delay handling instead of being misclassified as daily exhaustion; function-call ids are now preserved and matched into their responses; the fallback path now builds a thinkingConfig compatible with whichever model was actually selected. Mirrored in scripts/test_gemini_api.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nit match - DynamicChart's pie mode now filters to positive values before computing percentages/sections (preserving original index alignment with labels and series colors), falling back to an empty panel when nothing positive remains, instead of rendering a nonsense chart from negative/zero data. - A2UiPanelTitle's trailing label is now Flexible with maxLines/ellipsis so a long model-provided string can't overflow the row. - StatCard's unit-already-present check now requires a trailing-suffix match instead of any substring, fixing a false positive like unit "s" matching inside value "10 reps". - MetricGauge's arc painter now also compares `track` in shouldRepaint, so a background-color-only change still triggers a repaint. - A2UiTheme.seriesColor asserts a non-empty palette before the modulo index that would otherwise throw on one. - A2UiParser: props/outer-children now merge (props wins on conflict) so a model writing children as a sibling of props isn't silently dropped; adds a whole-text jsonDecode fast path ahead of the balanced-span scan. - A2UiRenderer logs the unresolved component name via the app's existing debugPrint/kDebugMode convention before falling back to an empty widget. - a2ui_app_theme now imports A2UiTheme via the public genui barrel instead of an internal src path. - CI: the release workflow's linker-patch step now requires and quotes PUB_CACHE, restricts the patch to resolved jni-*/src/CMakeLists.txt targets, is idempotent against re-runs, and fails the build instead of silently continuing when no target is found or patching fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes tests that would pass identically whether the behavior they claim to verify was correct or broken: - stat_card_test's pump() helper now actually threads its props argument into the rendered node (it previously always rendered empty props). - new_features_test's assisted-pullups case now uses distinguishable weight/assistWeight values, so the test fails if the wrong field is used. Tightens two guardrail-class tests to actually detect what they claim to: - a2ui_prompt_test's worked-example extraction is now bounded to the region after the "WORKED EXAMPLE:" marker via balanced-brace matching, instead of the last '}' anywhere in the whole prompt. - a2ui_purity_test's forbidden-import regex now also guards lib/data/. - a2ui_robustness_test's negative-axis assertion now requires minY to actually bracket the dataset's true minimum, not just be below -10. - a2ui_theme_test's panel-decoration finders are scoped to the panel under test rather than the first Container anywhere in the tree. Adds regression coverage pinning fixes already shipped in prior commits: DynamicChart pie's negative-value filtering, StatCard's unit-suffix match, and CoachToolService's days-window/insufficient-data/muscle-id fixes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
workout-logger/lib/services/workout_provider.dart (1)
526-535: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winPreserve the log handle when removing a set.
addSetpreservescurrentLog.handle, butremoveLastSetrebuildsExerciseLogwithouthandleat Line 548. After a removal, the completed log can lose its handle even when recorded sets remain. Handle-scoped history then cannot match this log.Proposed fix
_currentExerciseLogs[_currentExerciseIndex] = ExerciseLog( exerciseId: currentLog.exerciseId, sets: newSets, notes: currentLog.notes, + handle: currentLog.handle, );🤖 Prompt for 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. In `@workout-logger/lib/services/workout_provider.dart` around lines 526 - 535, Update removeLastSet to preserve the current ExerciseLog handle when rebuilding the log, matching the handle retention already implemented in addSet. Pass currentLog.handle into the replacement ExerciseLog so completed logs with remaining sets remain associated with handle-scoped history.workout-logger/lib/screens/widgets/exercise_input_section.dart (1)
129-177: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winLabel assisted dropsets in
_DropsetSection.When dropsets are enabled for pull-ups, chin-ups, dips, or push-ups,
_DropsetSectiondoes not passisAssistedBodyweightExercise(exerciseId)to its rows, so each weight field is labeled only as the unit, notAssist. Pass the assisted-bodyweight state into_DropsetSection, label each drop weight asAssist (<unit>), and add help text that shows how each assist value maps to effective load.🤖 Prompt for 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. In `@workout-logger/lib/screens/widgets/exercise_input_section.dart` around lines 129 - 177, Update the `_DropsetSection` call and implementation to receive the assisted-bodyweight state from `isAssistedBodyweightExercise(exerciseId)`. Use that state when rendering each drop weight field so assisted exercises display “Assist (<unit>)” instead of only the unit, and add help text explaining the assist value’s effective-load calculation.workout-logger/lib/services/ai/coach_tool_service.dart (3)
560-604: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winUse the requested correlation window for sleep data.
The default window is 60 days, but Line 604 always requests
HealthGranularity.week. Requests above seven days silently discard older paired days. The rawdaysvalue is also not bounded.Use
_limitArgwith a supported maximum. Select and trim the sleep-bar granularity as_getHealthMetricsdoes, or reduce the declared window to seven days. This finding is related to the earlier unbounded health-window finding.🤖 Prompt for 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. In `@workout-logger/lib/services/ai/coach_tool_service.dart` around lines 560 - 604, The correlation flow around the sleepBars call must honor the requested days window instead of always using HealthGranularity.week. Bound days with _limitArg using the supported health-window maximum, then select the appropriate sleep-bar granularity and trim/filter returned sleep data consistently with _getHealthMetrics before pairing it with workout data.
405-524: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winAlign the sleeping-HR response with its declared output contract.
The tool declaration promises median and P75 values. This response never returns either value. It also does not return the declared aggregate percentile set.
Return the declared statistics, or remove unsupported fields from the tool description. Otherwise the model can report unavailable analysis as if it were present.
🤖 Prompt for 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. In `@workout-logger/lib/services/ai/coach_tool_service.dart` around lines 405 - 524, Update _getSleepingHrAnalytics to match the tool’s declared output contract by calculating and returning median, P75, and the complete declared aggregate percentile statistics, including corresponding daily or series values where required. Reuse the collected sleeping-HR samples and preserve existing fields, or revise the tool declaration to remove any statistics that cannot be computed; do not leave declared metrics absent from the response.
574-599: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winAggregate workout values for sessions on the same date.
dayDatahas one entry per date, but each session assignsm['y']again. Volume and duration retain only the last session.exercise_max_weightcan also replace a higher earlier value with a lower later value.Sum volume and duration per date. Keep the maximum weight per date.
Proposed fix
- m['y'] = vol; + m['y'] = (m['y'] ?? 0.0) + vol; ... - m['y'] = s.duration.toDouble(); + m['y'] = (m['y'] ?? 0.0) + s.duration; ... - if (maxW > 0) m['y'] = maxW; + if (maxW > (m['y'] ?? 0.0)) m['y'] = maxW;🤖 Prompt for 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. In `@workout-logger/lib/services/ai/coach_tool_service.dart` around lines 574 - 599, Update the session aggregation loop in the dayData-building logic to combine values for sessions sharing the same date instead of overwriting m['y']. Accumulate workout_volume and session_duration into the existing date value, while exercise_max_weight must retain the maximum of the existing value and the current session’s max weight. Preserve the existing exercise filtering and omit zero/unresolved maximum-weight results.workout-logger/lib/services/ml_service.dart (1)
375-407: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse history when
lastSessionis empty.If
lastSessionis empty andpastSessionscontains a valid session, this method returns an empty list.refSetsstarts empty and changes only after a detected deload. InitializerefSetsfrom the first non-empty historical session when no last session exists. Add a regression test for this input.Proposed fix
- List<WorkoutSet> refSets = lastSession; + List<WorkoutSet> refSets = lastSession.isNotEmpty + ? lastSession + : pastSessions?.firstWhere( + (session) => session.isNotEmpty, + orElse: () => const <WorkoutSet>[], + ) ?? + const <WorkoutSet>[];🤖 Prompt for 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. In `@workout-logger/lib/services/ml_service.dart` around lines 375 - 407, Update the refSets initialization in the surrounding method so that when lastSession is empty, it falls back to the first non-empty session in pastSessions before the existing deload detection and empty-result return. Preserve lastSession as the preferred source when available, and add a regression test covering an empty lastSession with a valid historical session.
🤖 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
`@docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md`:
- Around line 189-191: Extend the migration flow in AppInitializer to validate
SQLite data before setting storage_migrated_v1: read back the migrated records
through exportAllData() and compare normalized data, identifiers, counts, nested
sets, JSON blobs, and settings against the Hive source. Set the Hive migration
flag only when validation succeeds; otherwise leave it unset and preserve the
existing migration retry behavior.
- Around line 128-133: Update the personal_records schema so records are keyed
by both exercise_id and handle, and define how existing legacy records without
handles are mapped during migration. Ensure migration preserves separate records
for multiple handles, and add coverage validating that no records collide,
overwrite, or get discarded.
- Around line 186-192: The migration flow in AppInitializer must be atomic and
safely retryable: execute all Hive reads and SQLite writes within one SQLite
transaction, or stage them in a fresh database and replace the active database
only after complete success. Ensure any failure discards all partial writes so
the next launch can rerun from current Hive data without stale rows or
conflicts, and add failure-injection coverage for retries after each entity
boundary.
- Around line 193-194: Update the Hive backup policy in the migration design to
define explicit retention and deletion behavior. Choose either removal after
verified migration with a documented recovery window, or encryption with
inclusion in deletion, reset, and export flows; replace the indefinite-retention
statement and apply the same policy to the related backup lifecycle section.
- Around line 172-180: Add an explicit schema version to
SqliteStorageService.init() by opening the database with version 1 and an
onUpgrade migration callback. Define the migration structure for future schema
changes, and include supported downgrade handling or an explicit data-preserving
down-revision path if sqflite provides downgrade support.
- Around line 204-206: Update the model query execution design to validate
row_limit within 1..500 before applying the outer LIMIT, and add tight bounds
for query length/shape and SQLite execution time to limit unbounded work from
scans, sorts, joins, or recursive CTEs. Preserve the existing error contract by
returning {'error': message} for cap or execution violations.
- Around line 68-72: Update the schema definitions for
exercise_muscle_activations.muscle_group_id, routine_exercises.exercise_id,
sessions.routine_id, exercise_logs.exercise_id, and targets.exercise_id to
include references to their corresponding tables. Also update onConfigure for
every writable connection to execute PRAGMA foreign_keys = ON.
- Around line 203-205: The connection design must not rely on
openReadOnlyDatabase as an independent safety boundary when it can reuse a
writable same-path instance. Update the read-only connection design to open with
singleInstance: false and require an Android integration test confirming writes
fail, or instead use an immutable snapshot/read-only-only handle enforced by
native SQLite.
In `@workout-logger/lib/services/ai/coach_tool_service.dart`:
- Around line 324-330: Remove readiness_score from the metric schema and all
related metric-handling paths in the coach tool service, including the synthetic
calculation near the readiness metric construction. Keep only measured health
metrics such as sleep_hours and deep_sleep_min, and update descriptions or
validation so readiness_score is no longer accepted or presented as independent
health data.
- Around line 1343-1347: Update _limitArg so the clamped value is explicitly
converted to int before returning it, while preserving the existing fallback and
1-to-max bounds.
---
Outside diff comments:
In `@workout-logger/lib/screens/widgets/exercise_input_section.dart`:
- Around line 129-177: Update the `_DropsetSection` call and implementation to
receive the assisted-bodyweight state from
`isAssistedBodyweightExercise(exerciseId)`. Use that state when rendering each
drop weight field so assisted exercises display “Assist (<unit>)” instead of
only the unit, and add help text explaining the assist value’s effective-load
calculation.
In `@workout-logger/lib/services/ai/coach_tool_service.dart`:
- Around line 560-604: The correlation flow around the sleepBars call must honor
the requested days window instead of always using HealthGranularity.week. Bound
days with _limitArg using the supported health-window maximum, then select the
appropriate sleep-bar granularity and trim/filter returned sleep data
consistently with _getHealthMetrics before pairing it with workout data.
- Around line 405-524: Update _getSleepingHrAnalytics to match the tool’s
declared output contract by calculating and returning median, P75, and the
complete declared aggregate percentile statistics, including corresponding daily
or series values where required. Reuse the collected sleeping-HR samples and
preserve existing fields, or revise the tool declaration to remove any
statistics that cannot be computed; do not leave declared metrics absent from
the response.
- Around line 574-599: Update the session aggregation loop in the
dayData-building logic to combine values for sessions sharing the same date
instead of overwriting m['y']. Accumulate workout_volume and session_duration
into the existing date value, while exercise_max_weight must retain the maximum
of the existing value and the current session’s max weight. Preserve the
existing exercise filtering and omit zero/unresolved maximum-weight results.
In `@workout-logger/lib/services/ml_service.dart`:
- Around line 375-407: Update the refSets initialization in the surrounding
method so that when lastSession is empty, it falls back to the first non-empty
session in pastSessions before the existing deload detection and empty-result
return. Preserve lastSession as the preferred source when available, and add a
regression test covering an empty lastSession with a valid historical session.
In `@workout-logger/lib/services/workout_provider.dart`:
- Around line 526-535: Update removeLastSet to preserve the current ExerciseLog
handle when rebuilding the log, matching the handle retention already
implemented in addSet. Pass currentLog.handle into the replacement ExerciseLog
so completed logs with remaining sets remain associated with handle-scoped
history.
🪄 Autofix
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: 41c03bb5-3016-4f57-a26a-90de8fe188b3
📒 Files selected for processing (29)
.github/workflows/release.ymldocs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.mdworkout-logger/lib/genui/src/a2ui_panels.dartworkout-logger/lib/genui/src/a2ui_parser.dartworkout-logger/lib/genui/src/a2ui_renderer.dartworkout-logger/lib/genui/src/a2ui_spec.dartworkout-logger/lib/genui/src/a2ui_theme.dartworkout-logger/lib/genui/src/components/dynamic_chart.dartworkout-logger/lib/genui/src/components/metric_gauge.dartworkout-logger/lib/genui/src/components/stat_card.dartworkout-logger/lib/main.dartworkout-logger/lib/models/models.dartworkout-logger/lib/screens/widgets/exercise_input_section.dartworkout-logger/lib/screens/workout_flow_screen.dartworkout-logger/lib/services/ai/coach_tool_service.dartworkout-logger/lib/services/ai/gemini_ai_service.dartworkout-logger/lib/services/interfaces/ml_service_interface.dartworkout-logger/lib/services/ml_service.dartworkout-logger/lib/services/settings_provider.dartworkout-logger/lib/services/workout_provider.dartworkout-logger/lib/theme/a2ui_app_theme.dartworkout-logger/scripts/test_gemini_api.pyworkout-logger/test/genui/a2ui_prompt_test.dartworkout-logger/test/genui/a2ui_purity_test.dartworkout-logger/test/genui/a2ui_robustness_test.dartworkout-logger/test/genui/a2ui_theme_test.dartworkout-logger/test/genui/components/dynamic_chart_test.dartworkout-logger/test/genui/components/stat_card_test.dartworkout-logger/test/new_features_test.dart
| CREATE TABLE exercise_muscle_activations ( | ||
| exercise_id TEXT NOT NULL REFERENCES exercises(id), | ||
| muscle_group_id TEXT NOT NULL, | ||
| activation_percentage INTEGER NOT NULL | ||
| ); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files matching SQLite migration/coach SQL spec:"
fd -i '2026-08-08-sqlite-migration-and-coach-sql-tool-design.md|sqflite|sqlite' . | sed -n '1,120p'
echo
echo "Target file excerpt:"
if [ -f "docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md" ]; then
nl -ba docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md | sed -n '55,130p'
fi
echo
echo "Search for foreign key declarations/pragmas/onConfigure:"
rg -n "FOREIGN KEY|PRAGMA foreign_keys|foreign_keys|onConfigure|REFERENCES\s*\(" -S . \
--glob '!**/.git/**' \
--glob '!**/build/**' \
--glob '!**/dist/**' \
| sed -n '1,240p'Repository: Devasy/RepForge
Length of output: 346
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Target file excerpt:"
if [ -f "docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md" ]; then
awk 'NR>=55 && NR<=130 {printf "%6d\t%s\n", NR, $0}' docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md
fi
echo
echo "Search for foreign key declarations/pragmas/onConfigure:"
rg -n --fixed-strings "FOREIGN KEY" "PRAGMA foreign_keys" "foreign_keys" "onConfigure" "REFERENCES (" "REFERENCES(" . \
--glob '!**/.git/**' \
--glob '!**/build/**' \
--glob '!**/dist/**' \
|| true
echo
echo "Candidate database files:"
fd -i 'database|db|sqlite|sqflite' . \
--glob '!**/.git/**' \
--glob '!**/build/**' \
--glob '!**/dist/**' \
| sed -n '1,80p'Repository: Devasy/RepForge
Length of output: 3203
🌐 Web query:
SQLite foreign key constraints enabled per database connection PRAGMA foreign_keys ON documentation
💡 Result:
In SQLite, foreign key constraints must be enabled for each database connection using the PRAGMA foreign_keys = ON; command [1][2]. Key details regarding foreign key enforcement include: 1. Default Setting: Foreign key enforcement is disabled by default for backward compatibility [1][2]. Applications should explicitly enable or disable this setting as needed rather than relying on the default, as future releases may change this behavior [3][1]. 2. Connection-Specific: The setting applies only to the current database connection [1][4]. 3. Constraints on Use: The PRAGMA foreign_keys command is a no-op if executed within a transaction (i.e., when a BEGIN or SAVEPOINT is pending) [3][5]. 4. Compilation Requirements: For foreign key constraints to function, the SQLite library must be compiled without the SQLITE_OMIT_FOREIGN_KEY or SQLITE_OMIT_TRIGGER preprocessor macros [1][2]. 5. Alternative Configuration: Developers can also use the sqlite3_db_config C interface with the SQLITE_DBCONFIG_ENABLE_FKEY option to enable or disable foreign key enforcement programmatically [6]. To verify the current status of foreign key enforcement, you can execute the command PRAGMA foreign_keys;, which returns 1 if enabled or 0 if disabled [1][2]. If the command returns no data, it indicates that the version of SQLite being used does not support foreign key enforcement [1][2].
Citations:
- 1: http://sqlite.org/foreignkeys.html
- 2: https://sqlite.org/draft/foreignkeys.html
- 3: https://sqlite.org/pragma.html
- 4: https://www2.sqlite.org/matrix/foreignkeys.html
- 5: https://sqlite.org/draft/pragma.html
- 6: https://www.sqlite.org/draft/c3ref/c_dbconfig_enable_fkey.html
Add the missing foreign keys and enable enforcement.
Add foreign keys for muscle_group_id, routine_exercises.exercise_id, sessions.routine_id, exercise_logs.exercise_id, and targets.exercise_id.
Also execute PRAGMA foreign_keys = ON in onConfigure for every writable connection. SQLite disables foreign-key enforcement by default and requires per-connection enablement.
🤖 Prompt for 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.
In
`@docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md`
around lines 68 - 72, Update the schema definitions for
exercise_muscle_activations.muscle_group_id, routine_exercises.exercise_id,
sessions.routine_id, exercise_logs.exercise_id, and targets.exercise_id to
include references to their corresponding tables. Also update onConfigure for
every writable connection to execute PRAGMA foreign_keys = ON.
Source: MCP tools
| CREATE TABLE personal_records ( | ||
| exercise_id TEXT PRIMARY KEY, | ||
| best_weight REAL NOT NULL, | ||
| best_reps INTEGER NOT NULL, | ||
| best_volume REAL NOT NULL, | ||
| achieved_at TEXT NOT NULL |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Preserve the handle dimension in personal records.
personal_records.exercise_id permits only one record per exercise. The current handle-aware workout flow requires separate records for different handles. Migration will therefore collide, overwrite, or discard data when one exercise has multiple handles.
Add handle to the key or unique constraint. Define the legacy-record mapping and test migration with multiple handles.
🤖 Prompt for 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.
In
`@docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md`
around lines 128 - 133, Update the personal_records schema so records are keyed
by both exercise_id and handle, and define how existing legacy records without
handles are mapped during migration. Ensure migration preserves separate records
for multiple handles, and add coverage validating that no records collide,
overwrite, or get discarded.
| ## 5. `SqliteStorageService` | ||
|
|
||
| New file: `lib/services/sqlite_storage_service.dart`, `class SqliteStorageService implements IStorageService`. | ||
|
|
||
| - `init()`: opens the database (`openDatabase`), runs `onCreate` (schema above) on first creation. | ||
| - Every `IStorageService` method gets a real implementation: entity writes that touch multiple tables (e.g. `saveWorkoutSession` → `sessions` + `exercise_logs` + `sets`) run inside a single `db.transaction()` — delete-then-reinsert child rows for the given parent id, so updates and inserts share one code path. | ||
| - `exportAllData()` / `importData()` keep their existing JSON contract (used by the migration below and by the user-facing export/import feature) — implemented by reading/writing through the same model `toJson()`/`fromJson()` methods already used elsewhere. | ||
|
|
||
| No changes to `IStorageService`'s method signatures. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the reviewed file and nearby sqlite storage related files.
fd -a '2026-08-08-sqlite-migration-and-coach-sql-tool-design.md|sqlite_storage_service|IStorageService|storage_service' . | sed 's#^\./##' | sort
echo '--- reviewed file excerpts ---'
file='docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md'
if [ -f "$file" ]; then
sed -n '130,210p' "$file"
fi
echo '--- git diff stat ---'
git diff --stat || true
echo '--- search openDatabase versions in tracked files ---'
rg -n "openDatabase|onCreate|onUpgrade|version:\s*[0-9]+|version:" --glob '*.dart' . || trueRepository: Devasy/RepForge
Length of output: 6778
Add an explicit SQLite schema version and upgrade path.
SqliteStorageService.init() only describes onCreate; later app versions must also get schema changes. Add version: 1 and define onUpgrade migrations, including downgrade behavior if sqflite supports it or an explicit data-preserving down-revision path.
🤖 Prompt for 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.
In
`@docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md`
around lines 172 - 180, Add an explicit schema version to
SqliteStorageService.init() by opening the database with version 1 and an
onUpgrade migration callback. Define the migration structure for future schema
changes, and include supported downgrade handling or an explicit data-preserving
down-revision path if sqflite provides downgrade support.
Source: MCP tools
| **Goal:** existing installs upgrade from Hive to SQLite exactly once, safely, with no possibility of a half-migrated state. | ||
|
|
||
| 1. On app start, `AppInitializer` (in `main.dart`) checks `settings['storage_migrated_v1']` **in the existing Hive settings box** (the migration hasn't happened yet at this point, so Hive is still authoritative for this check). | ||
| 2. If unset: instantiate both the existing `StorageService` (Hive) and a fresh `SqliteStorageService`. For every entity type, read via the existing, already-correct Hive read methods (`getAllWorkoutSessions()`, `getAllRoutines()`, `getAllTargets()`, `getAllMuscleGroups()`, `getCustomExercises()`, `getAllTrainingPrograms()`, `getAllPersonalRecords()`, `getAllConversations()`, plus raw settings keys) and write each into `SqliteStorageService` through its normal write methods. This trusts only the new write path — reads reuse logic that already works. | ||
| 3. Only if every entity type migrates without throwing: write `storage_migrated_v1 = true` into the Hive settings box. | ||
| 4. From that point on (this launch and all future launches), `AppInitializer` hands `WorkoutProvider` a `SqliteStorageService` instead of `StorageService`. | ||
| 5. If migration throws partway through anything, the flag is never set. The app falls back to `StorageService` (Hive) for that launch, and retries the full migration on the next app start. There is no partial-migration state a user can get stuck in. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Make migration retries atomic and idempotent.
A failure after some entity writes leaves partial SQLite state. The next attempt then writes against that state. If Hive changed during the fallback launch, stale rows and primary-key conflicts can remain.
Wrap the complete migration in one SQLite transaction, or migrate into a fresh staging database and replace it only after success. Add a failure-injection test that retries after each entity boundary.
Also applies to: 214-214
🤖 Prompt for 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.
In
`@docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md`
around lines 186 - 192, The migration flow in AppInitializer must be atomic and
safely retryable: execute all Hive reads and SQLite writes within one SQLite
transaction, or stage them in a fresh database and replace the active database
only after complete success. Ensure any failure discards all partial writes so
the next launch can rerun from current Hive data without stale rows or
conflicts, and add failure-injection coverage for retries after each entity
boundary.
| 2. If unset: instantiate both the existing `StorageService` (Hive) and a fresh `SqliteStorageService`. For every entity type, read via the existing, already-correct Hive read methods (`getAllWorkoutSessions()`, `getAllRoutines()`, `getAllTargets()`, `getAllMuscleGroups()`, `getCustomExercises()`, `getAllTrainingPrograms()`, `getAllPersonalRecords()`, `getAllConversations()`, plus raw settings keys) and write each into `SqliteStorageService` through its normal write methods. This trusts only the new write path — reads reuse logic that already works. | ||
| 3. Only if every entity type migrates without throwing: write `storage_migrated_v1 = true` into the Hive settings box. | ||
| 4. From that point on (this launch and all future launches), `AppInitializer` hands `WorkoutProvider` a `SqliteStorageService` instead of `StorageService`. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Validate migrated data before setting storage_migrated_v1.
A migration that completes without throwing can still lose fields during model-to-table conversion. After the flag is set, SQLite becomes authoritative.
Read back the migrated data and compare a normalized exportAllData() result, entity identifiers, counts, nested sets, JSON blobs, and settings. Set the Hive flag only after validation succeeds.
🤖 Prompt for 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.
In
`@docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md`
around lines 189 - 191, Extend the migration flow in AppInitializer to validate
SQLite data before setting storage_migrated_v1: read back the migrated records
through exportAllData() and compare normalized data, identifiers, counts, nested
sets, JSON blobs, and settings against the Hive source. Set the Hive migration
flag only when validation succeeds; otherwise leave it unset and preserve the
existing migration retry behavior.
| 6. **Hive boxes are never deleted.** They remain on disk indefinitely as a passive backup — the data volume for a personal fitness log is small, so the disk cost is negligible next to the safety value. | ||
|
|
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Define retention and deletion behavior for the Hive backup.
Keeping Hive indefinitely retains stale sessions, conversations, and settings after SQLite deletes or changes them. A user reset or data-deletion flow can therefore leave sensitive data on disk.
Either remove Hive after verified migration with a documented recovery window, or encrypt it and include it in deletion, reset, and export lifecycle handling.
Also applies to: 220-224
🤖 Prompt for 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.
In
`@docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md`
around lines 193 - 194, Update the Hive backup policy in the migration design to
define explicit retention and deletion behavior. Choose either removal after
verified migration with a documented recovery window, or encryption with
inclusion in deletion, reset, and export flows; replace the indefinite-retention
statement and apply the same policy to the related backup lifecycle section.
| - **Connection:** a dedicated **read-only** `sqflite` connection (`openReadOnlyDatabase`) to the same database file used by `SqliteStorageService`. This is the real safety boundary — the OS/SQLite layer itself refuses writes on this connection, regardless of what SQL text is submitted. | ||
| - **Text validation (defense-in-depth, not the primary guard):** trim the query, strip a single trailing `;`, reject if a second `;` remains (multi-statement), reject case-insensitively if it doesn't start with `SELECT` or `WITH`, reject if it contains `insert|update|delete|drop|alter|create|attach|detach|pragma|vacuum|replace|trigger` as a keyword. | ||
| - **Row cap:** wrap the model's query as `SELECT * FROM (<query>) LIMIT ?` with a default of 200, model-adjustable up to 500 — never trusts a `LIMIT` the model wrote itself. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
rg -n 'openReadOnlyDatabase|singleInstance|readOnly' workout-logger/lib workout-logger/testRepository: Devasy/RepForge
Length of output: 153
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== repo files matching sqlite/spec docs =="
git ls-files | rg '(^|/)(specs|.*sqlite.*|sql.*|storage|.*storage.*|.*Sql|.*sql.*)' | head -200
echo
echo "== locate target file =="
fd -a '2026-08-08-sqlite-migration-and-coach-sql-tool-design\.md|sqflite|SqliteStorageService|storage_service|storage' . | head -200
echo
echo "== target context if present =="
if [ -f "docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md" ]; then
nl -ba docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md | sed -n '170,230p'
fiRepository: Devasy/RepForge
Length of output: 1270
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== search any openReadOnlyDatabase in repo (broad) =="
rg -n 'openReadOnlyDatabase|openDatabase.*readOnly|readOnlyDatabase|singleInstance|readOnly' . --glob '!**/.git/**' || true
echo
echo "== search SqliteStorageService =="
rg -n 'SqliteStorageService|sqliteStorage|sqlite' workout-logger/lib workout-logger/test workout-logger/flutter_runner workout-logger/ios workout-logger/android 2>/dev/null || trueRepository: Devasy/RepForge
Length of output: 615
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md')
lines = p.read_text(encoding='utf-8').splitlines()
for i in range(202, 217):
print(f"{i+1}: {lines[i]}")
PY
echo
echo "== current sqflite declarations in pubspecs =="
git ls-files '*pubspec.yaml' | xargs -r sh -c 'echo "--- $0"; grep -n -i "sqflite\|sqlite" "$0" || true'Repository: Devasy/RepForge
Length of output: 2525
🌐 Web query:
sqflite openReadOnlyDatabase singleInstance readOnly openDatabase documentation
💡 Result:
In the sqflite package, there are two primary ways to open a database in read-only mode: using the dedicated openReadOnlyDatabase function or the standard openDatabase function with the readOnly parameter [1][2][3]. openReadOnlyDatabase The openReadOnlyDatabase function is a convenience method specifically designed to open a database at a given path in read-only mode [1][4]. Its implementation internally calls openDatabase with readOnly set to true [1]. Signature: Future openReadOnlyDatabase(String path, {bool? singleInstance = true}) openDatabase Alternatively, you can use the standard openDatabase function by setting the readOnly parameter to true [3][5]. Signature: Future openDatabase(String path, {bool? readOnly = false, bool? singleInstance = true,...}) When readOnly is set to true, the database is opened as-is, and all other configuration parameters (such as version, onCreate, or onUpgrade callbacks) are ignored [3][5]. The singleInstance Parameter The singleInstance parameter, which defaults to true, determines how the database instance is managed [3][5]. When singleInstance is true, sqflite returns a shared database instance for a given path [3][5]. If you call openDatabase or openReadOnlyDatabase multiple times with the same path, the library returns this same existing instance [3][5]. If singleInstance is set to false, a new connection/instance is created each time, which can be useful in specific scenarios like multi-isolate access, though developers are generally advised to manage their own global reference to avoid lock issues [6]. Summary of Behavior When you set readOnly: true (or use the helper function), you cannot perform write operations, and database initialization callbacks like onCreate or onUpgrade will not be executed [3][7][5]. This is commonly used for checking if a file is a valid database file by attempting to open it and read its version [8].
Citations:
- 1: https://pub.dev/documentation/sqflite/latest/sqflite/openReadOnlyDatabase.html
- 2: https://pub.dev/documentation/sqflite/latest/sqflite/
- 3: https://pub.dev/documentation/sqflite/latest/sqflite/openDatabase.html
- 4: https://pub.dev/documentation/sqflite_common/latest/sqflite/openReadOnlyDatabase.html
- 5: https://pub.dev/documentation/sqflite_common/latest/sqflite/openDatabase.html
- 6: https://github.com/tekartik/sqflite/blob/master/sqflite/doc/usage_recommendations.md
- 7: https://pub.dev/documentation/sqflite/latest/sqflite/OpenDatabaseOptions-class.html
- 8: https://github.com/tekartik/sqflite/blob/master/sqflite/doc/opening_db.md
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md')
text = p.read_text(encoding='utf-8')
checks = {
'contains_openReadOnlyDatabase': 'openReadOnlyDatabase' in text,
'claims_primary_safety_boundary': 'real safety boundary' in text,
'mentions_single_instance_arg': 'singleInstance' in text,
'uses_same_file': 'same database file' in text,
}
for name, ok in checks.items():
print(f"{name}: {ok}")
PY
echo
echo "== inspect current sqflite declaration in pubspec =="
python3 - <<'PY'
from pathlib import Path
p = Path('workout-logger/pubspec.yaml')
print(p.read_text(encoding='utf-8'))
PYRepository: Devasy/RepForge
Length of output: 4424
Do not present openReadOnlyDatabase on the same file as an independent safety boundary.
Because openReadOnlyDatabase forwards singleInstance: true, a later same-path open can return the existing writable database instance, so the SQL tool’s readonly option can be discarded. Open with singleInstance: false and require an Android integration test that write operations fail, or use an immutable snapshot/read-only-only handle that is enforced by the native SQLite layer.
🤖 Prompt for 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.
In
`@docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md`
around lines 203 - 205, The connection design must not rely on
openReadOnlyDatabase as an independent safety boundary when it can reuse a
writable same-path instance. Update the read-only connection design to open with
singleInstance: false and require an Android integration test confirming writes
fail, or instead use an immutable snapshot/read-only-only handle enforced by
native SQLite.
Source: MCP tools
| - **Text validation (defense-in-depth, not the primary guard):** trim the query, strip a single trailing `;`, reject if a second `;` remains (multi-statement), reject case-insensitively if it doesn't start with `SELECT` or `WITH`, reject if it contains `insert|update|delete|drop|alter|create|attach|detach|pragma|vacuum|replace|trigger` as a keyword. | ||
| - **Row cap:** wrap the model's query as `SELECT * FROM (<query>) LIMIT ?` with a default of 200, model-adjustable up to 500 — never trusts a `LIMIT` the model wrote itself. | ||
| - **Error handling:** any exception (syntax error, cap violation, etc.) returns `{'error': message}`, matching every other tool's contract — a bad query is a recoverable turn, not a crash. |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file excerpt =="
sed -n '160,240p' docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md || true
echo
echo "== related mentions =="
rg -n "Row cap|LIMIT|validate|timeout|query length|500|200" docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md . --glob '!.git/**' | head -120
echo
echo "== local sqlite behavior probe: outer LIMIT does not limit work and negative LIMIT has no upper bound =="
python3 - <<'PY'
import sqlite3
stmt = [
"""CREATE TABLE IF NOT EXISTS t(i INTEGER PRIMARY KEY, v TEXT);""",
"""INSERT INTO t(i, v) SELECT i, printf('%d', i) FROM (WITH RECURSIVE c(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM c WHERE x < :n) SELECT x FROM c)""",
"""SELECT * FROM (SELECT v FROM t ORDER BY v, i DESC) LIMIT ?;""",
]
conn = sqlite3.connect(":memory:")
conn.execute("""CREATE TABLE t(i INTEGER PRIMARY KEY, v TEXT);""")
n = 8000
queries = []
for i, q in enumerate(stmt, start=1):
s = q[:2000]
print(f"q{i}: {s[:160].replace(chr(10), ' ')}...")
PYRepository: Devasy/RepForge
Length of output: 20188
🌐 Web query:
SQLite LIMIT clause negative value -1 no limit behavior documentation
💡 Result:
In SQLite, providing a negative value to the LIMIT clause is interpreted as "no limit," meaning there is no upper bound on the number of rows returned or affected [1][2][3]. This behavior applies consistently across SELECT statements [1][2], as well as UPDATE and DELETE statements (when those statements are used in configurations that support LIMIT clauses, such as when SQLite is compiled with SQLITE_ENABLE_UPDATE_DELETE_LIMIT) [3]. If a negative value is provided, the query proceeds as if the LIMIT clause were absent, returning or processing the entire result set [2][3].
Citations:
- 1: https://system.data.sqlite.org/src/doc/dc206da59f/Doc/Extra/lang_select.html
- 2: https://stackoverflow.com/questions/6329032/sqlite3-and-limiting-the-number-of-results
- 3: https://system.data.sqlite.org/home/doc/0a3d6229a7425242/Doc/Extra/Core/lang_update.html
Bound accepted row caps and SQL execution budgets.
The outer LIMIT only limits rows returned, not the work SQLite does for scans, sorts, cross joins, or recursive CTEs. SQLite also treats negative LIMIT values as no upper bound. Require row_limit in 1..500 before wrapping the query, keep query length/shape/execution time tight, or snapshot the database for read-heavy tool output.
🤖 Prompt for 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.
In
`@docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md`
around lines 204 - 206, Update the model query execution design to validate
row_limit within 1..500 before applying the outer LIMIT, and add tight bounds
for query length/shape and SQLite execution time to limit unbounded work from
scans, sorts, joins, or recursive CTEs. Preserve the existing error contract by
returning {'error': message} for cap or execution violations.
Source: MCP tools
| '(sleep_hours, deep_sleep_min, readiness_score) and a workout metric ' | ||
| '(workout_volume, session_duration, exercise_max_weight). Returns analytical stats ' | ||
| 'and paired coordinates ready to visualize.', | ||
| Schema.object( | ||
| properties: { | ||
| 'x_metric': Schema.string( | ||
| description: 'Health metric, e.g. "sleep_hours", "deep_sleep_min", "readiness_score".', |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Remove the synthetic readiness_score metric.
At Line 613, the implementation creates readiness_score from sleep duration with an arbitrary 70–100 formula. This is not a measured readiness value. A correlation result can therefore present a sleep-derived proxy as independent health data.
Remove readiness_score from the schema, or rename it as an explicitly documented sleep-derived proxy. This finding is related to the earlier health-metric contract finding.
🤖 Prompt for 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.
In `@workout-logger/lib/services/ai/coach_tool_service.dart` around lines 324 -
330, Remove readiness_score from the metric schema and all related
metric-handling paths in the coach tool service, including the synthetic
calculation near the readiness metric construction. Keep only measured health
metrics such as sleep_hours and deep_sleep_min, and update descriptions or
validation so readiness_score is no longer accepted or presented as independent
health data.
| int _limitArg(Map<String, Object?> args, int fallback, | ||
| {String key = 'limit', int max = 40}) { | ||
| final n = (args[key] as num?)?.toInt(); | ||
| if (n == null) return fallback; | ||
| return n.clamp(1, 40); | ||
| return n.clamp(1, max); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C3 'int _limitArg|return n\.clamp' \
workout-logger/lib/services/ai/coach_tool_service.dartRepository: Devasy/RepForge
Length of output: 647
🌐 Web query:
Dart num.clamp int static return type documentation
💡 Result:
The Dart num.clamp method is defined in the dart:core library with the static return type num [1][2][3]. While its formal signature in the num class is num clamp(num lowerLimit, num upperLimit), the method is designed to return the value of the receiver or the specified limits, ensuring the result stays within the inclusive range [lowerLimit, upperLimit] [2][3]. It is important to note the following regarding its return type and behavior: 1. Static Type: The declared return type is num [1][3]. Consequently, when using this method, the static analyzer will treat the result as a num, even if the runtime value is specifically an int or a double [4]. 2. Runtime Behavior: Because num is a common supertype for both int and double, the actual runtime type of the returned object will depend on the types of the receiver, the lowerLimit, and the upperLimit [4]. If all inputs are integers, the result will typically be an int; if any input is a double, the result will typically be a double [4]. 3. Special Handling: There have been discussions within the Dart community regarding the limitations of this signature—specifically that it does not provide more precise static return type information (e.g., returning int when only integers are involved) [4]. However, the signature remains num for backward compatibility and to maintain the flexibility of the num type [4]. When working with clamp, you may need to perform explicit type casting or use methods like toInt if you require a specific numeric type for subsequent operations [2][3].
Citations:
- 1: https://api.dart.dev/dev/2.0.0-dev.67.0/dart-core/num-class.html
- 2: https://github.com/dart-lang/sdk/blob/796d6cc344b93c0432b404173438e0343fc6c2b8/sdk/lib/core/num.dart
- 3: https://github.com/dart-lang/sdk/blob/c42a304ce78f37b717cc055abc503023871a7013/sdk/lib/core/num.dart
- 4: double.clamp should return a double dart-lang/sdk#39652
Return an int from _limitArg.
n.clamp(1, max) has static type num, but _limitArg returns int, so static analysis reports a return-type mismatch.
Proposed fix
- return n.clamp(1, max);
+ if (n < 1) return 1;
+ if (n > max) return max;
+ return n;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| int _limitArg(Map<String, Object?> args, int fallback, | |
| {String key = 'limit', int max = 40}) { | |
| final n = (args[key] as num?)?.toInt(); | |
| if (n == null) return fallback; | |
| return n.clamp(1, 40); | |
| return n.clamp(1, max); | |
| int _limitArg(Map<String, Object?> args, int fallback, | |
| {String key = 'limit', int max = 40}) { | |
| final n = (args[key] as num?)?.toInt(); | |
| if (n == null) return fallback; | |
| if (n < 1) return 1; | |
| if (n > max) return max; | |
| return n; |
🤖 Prompt for 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.
In `@workout-logger/lib/services/ai/coach_tool_service.dart` around lines 1343 -
1347, Update _limitArg so the clamped value is explicitly converted to int
before returning it, while preserving the existing fallback and 1-to-max bounds.
Summary by CodeRabbit
New Features
Bug Fixes