Commit Graph
566 Commits
Author SHA1 Message Date
Marc-André Bombeck 8dc34c7691 fix(coach): keep GLP-1 dose changes before the lookback limit out
The GLP-1 block sent every dose change with its decrypted note whatever
the lookback limit said. Dose changes are entries, so only those inside
the limit reach the prompt; the dose in force stays as the current dose.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 1158ae9ffd fix(coach): keep earlier tables beyond the lookback limit out of the context
The earlier-tables block names each stored table with its window and row
count. The list was filtered by module and exclusion only, so a table
stored before the limit was set or narrowed still told the model how much
history it covered. It now answers to the same range test show_result
uses, and the history-reach guard checks both the filter and that the
call site passes the limit.
2026-10-05 07:47:31 +02:00
Marc-André Bombeck 2a556b47c3 fix(coach): offer only providers that can answer and say when a switch does not take
The provider-chain summary marked every entry available, so the Coach's
provider select offered entries without a key or sign-in. Choosing one
saved it to the front of the chain, the resolver skipped it, and the
select jumped back without a word.

Availability now comes from the presence probe, which names every chain
type whose credential is present. After a switch the picker reads the
chain back and says so when the server still answers with another
provider.
2026-10-05 07:45:05 +02:00
Marc-André Bombeck 929c0ebaa4 Merge remote-tracking branch 'origin/main' into feat/coach-lookback-limit
# Conflicts:
#	src/lib/ai/coach/snapshot.ts
#	src/lib/ai/coach/tools/executor.ts
#	src/lib/insights/features.ts
2026-10-05 07:31:03 +02:00
Marc-André Bombeck 393ac955f6 Make the Coach lookback a limit on every read
The saved Coach window only seeded the snapshot and each tool's default.
A tool could still read back to all time, the availability probe reported
counts and means for history outside the window, and several blocks read
fixed windows of their own (labs 12 months, correlations 180 days, the
coarse tail, the all-time extremes) whatever the setting said.

coachPrefs.defaultWindow is now enforced as a limit through one module,
history-reach.ts: the turn context clamps the client window, the snapshot
clamps its window and gives every fixed-window block either the shorter
window or, when the block's own window cannot fit, leaves it out. Every
retrieval tool clamps its window, an earlier period or a stored table that
reaches past the limit answers outside_reach, and the probe reads only
inside the limit and names older history as outside_reach without any
figure. Follow-up chips that would read past the limit are not offered.
The default (allTime) is no limit and reads as before. MCP clients pass no
limit and keep their own window arguments.

Adds the twelve-month option, rewrites the picker text to state what is
read with the numbers the readers use, shows the same options in the
managed-profile settings, counts cluster members with a singular, and
drops the opacity on switched-off rows that put their text below WCAG AA.
2026-10-04 20:45:21 +02:00
Marc-André Bombeck 546e67475f perf(mcp): build single-metric reads from the metric's own rows
get_metric_series (and get_metrics, the metric resources and the review
prompts, which fan out over it) answered with one section of a Coach
snapshot, but built the whole snapshot to get it: the all-type feature
extraction, the derived wellness layer, trajectory, correlations, memory,
GLP-1, cycle, illness, labs and visits. On a seeded account with about
215k measurements a resting heart rate read ran 208 SQL statements and
blocked the event loop for 175 ms to return under 2 KB.

The MCP surface now asks the executor for a source snapshot. It runs the
same prelude (prefs, module gates, admitted sources, cutoffs, now shared in
snapshot-prelude.ts) and the same block builders over the requested
source's reads only. The core clinical aggregates come from extractFeatures
scoped to the one block, whose bulk read keeps its all-type cap so the rows
a block sees do not change. A section large enough for the full builder's
prompt-budget pass to trim, and the environment cluster, go to the full
builder, so their results are the old ones by construction.

The integration test compares the tool's serialised result with the full
builder's section and grounding for nine sources over four windows, the
get_metrics batch and the metric resource, on a seeded account dense enough
to put the feature row cap into play.
2026-10-04 18:17:24 +02:00
Marc-André Bombeck 13b8920dd5 Merge remote-tracking branch 'origin/main' into feat/pulse-hourly-mean-complete 2026-10-04 14:54:53 +02:00
Marc-André Bombeck 8f730484e0 Average pulse by day in the comparison snapshot and Coach inventory
The insights comparison snapshot took its current and baseline pulse figures
from a reading AVG, and the Coach's data inventory told the model a reading
mean of the whole pulse history. Both now read the mean of the user's local
days, each the mean of its hours' means, through the day-mean helper. Every
other type keeps its aggregate.
2026-10-04 11:37:57 +02:00
Marc-André Bombeck 11e35a6ba1 Average pulse by day in the insights, Coach and report trend
The feature extraction, the signals of the day, the status cards, the graded
series, the Coach snapshot, the mood correlations and the report's trend arrow
each still averaged pulse over its readings, so a dense workout hour decided
the day and a workout day the window. They now read a pulse day as the mean of
its hours' means and a window as the mean of its days, through the day-mean
helper. Counts, extremes and spreads stay over every reading; every other type
is unchanged.
2026-10-04 11:33:45 +02:00
Marc-André Bombeck a0fa4783c0 fix(ai): keep JSON mode when an endpoint rejects only reasoning_effort
An endpoint that does not know reasoning_effort answers with an unknown
parameter error, which the JSON-mode probe read as a refusal of
response_format and switched that base URL to plain prompts.
2026-10-04 10:48:35 +02:00
Marc-André Bombeck befac59576 feat(ai): send a per-entry reasoning effort to Local and gateway providers
Reasoning models on Ollama and similar servers think by default and can
spend the whole max_tokens budget before the visible answer starts, so
the call comes back empty or times out. The Local and OpenAI-compatible
chain entries now carry an optional reasoning setting: Default sends
nothing, Off sends reasoning_effort "none", Low / Medium / High send
their own value. It is stored on the entry in aiProviderChain, so a
chain can mix a thinking model with one that does not know the field.

- Every exported resolver binds the entry's value onto the client, the
  connection test included, keyed on the instance type so the operator
  provider, OpenAI, Anthropic and Codex never carry it.
- PATCH /api/user/ai-provider accepts localReasoningEffort and
  compatReasoningEffort and GET reports them. A provider absent from
  the chain holds its value on a disabled entry; restating Default on a
  chain that was never customised writes nothing.
- PUT /api/insights/provider-chain accepts reasoningEffort on local and
  openai-compatible entries, refuses it on any other, and keeps the
  stored value when the body leaves it out. Its schema is now shared
  with the OpenAPI registry.
- A Default request body is byte-identical to the one sent before.

Refs #1126
2026-10-04 10:42:59 +02:00
Marc-André Bombeck 1494409209 Merge remote-tracking branch 'origin/main' into wip/v140-H
# Conflicts:
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/ko.json
#	messages/pl.json
#	src/__tests__/sharing-surface-guard.test.ts
#	src/__tests__/v137-wave-zero-fixtures.test.ts
#	tests/integration/sharing-manage-handler-matrix.test.ts
2026-10-03 22:52:49 +02:00
Marc-André Bombeck a14174fe4d Let a medication have several courses
A medication taken now and then (#1024) keeps one row and gains course
rows: calendar spans, both ends inclusive, an optional encrypted note, no
schedule or dose of their own. Courses never overlap and every course but
the latest has ended, so the latest course is always the one the
medication's startsOn/endsOn project. src/lib/medications/courses.ts is
the only writer of those two columns for a medication with courses; it
writes under a per-medication advisory lock and re-projects in the same
transaction, so reminders, the next-due line and intakeActionable keep
reading the columns and stay right.

- Migration 0368 adds medication_courses and backfills one course per
  medication window (an end-only window starts on the creation day, or
  on its end day when that lies earlier); a medication without a window
  stays course-less. Idempotent, data-preserving.
- GET/POST /api/medications/{id}/courses and PATCH/DELETE
  .../courses/{courseId} (owner-only); create seeds the first course
  from a window; the medication PUT edits the latest course and refuses
  a window that would break the rules (medications.course.*).
- List, detail, create and update carry courses, courseCount,
  previousCourseEndedOn and canStartCourse, resolved on the server.
- Compliance expands over the span of all courses and drops the slots
  between them; every compliance read selects the courses. A rolling
  cadence is not anchored by an intake from a previous course.
- The card names the last course's end and the running course's number;
  the detail Lifecycle group lists the courses with start, edit and
  delete. The Coach compliance block, the MCP resource, the doctor
  report window and table, the clinician view and FHIR (one
  MedicationStatement per course with effectivePeriod and status) read
  the courses.
- Backup carries courses per medication and the restore writes them and
  re-projects; an older file derives the one course its window
  describes. Wipe plan, encrypted columns, rotation and OpenAPI follow.
2026-10-03 22:39:25 +02:00
Marc-André Bombeck bcbfd87ae9 feat(coach): name the person's own symptoms in the illness block
The Coach illness block now carries the last 14 days of the person's own
symptoms (name sanitised for the prompt, count, highest intensity, last
occurrence; at most ten, most frequent first), and exists when there are
symptoms but no episode. The prompt describes the field in English and
German. The MCP get_illness_recovery result leaves it out in this
release.
2026-10-03 21:24:28 +02:00
Marc-André Bombeck 9d9e1f153c feat(insights): make each person-defined symptom its own correlation channel
Every active symptom definition enters pattern discovery as an outcome
channel SYMPTOM:<id>, labelled with its decrypted name (the data label
the interpretation already prefers over the key). The daily value is the
day's highest intensity in the person's zone; days without an occurrence
count as 0 only between the first and the last logged day. At most eight,
the active-definition cap. The SYMPTOM: prefix resolves to the illness
module in the surface map, so the family leaves the matrix with the
module, and the discovery guard lists the new fetcher.
2026-10-03 21:14:08 +02:00
Marc-André Bombeck 7022b698e7 fix(ai): forget the remembered working provider when the chain is saved
The in-memory memo of the last provider that answered reorders the
chain for up to an hour. Saving a new chain now drops it for that
account, so a provider the person removed or disabled no longer leads
the chain they just arranged.
2026-10-03 18:04:09 +02:00
Marc-André Bombeck 5ee1b3e28f fix(ai): run every model call under the response-timeout setting
The response-timeout setting reached the briefing, the status notes and
the Coach, which threaded it by hand. Document reads, document
extraction, lab OCR and lab staging called the provider without a
timeout, so every client fell back to its own 60 s literal and a slow
self-hosted model was cut off however high the setting stood.

The setting now rides on the provider instance. resolveProvider,
resolveProviderChain and resolveProviderForTest stamp the record's
aiResponseTimeoutSeconds on everything they return, and each wire
client reads its ceiling through callTimeoutMs: the setting, else the
surface's own timeoutMs, else 60 s. A call site no longer has to pass
it, so a new one cannot forget it. The connection test runs under the
same ceiling a read does.

The Coach nudge tick and the reaction line keep their short ceilings
(timeoutPolicy: "surface-ceiling"): the tick runs accounts one after
another under a shared budget, the reaction line's claim lease is
shorter than the largest setting, and both have deterministic copy
that stands in.

/api/auth/me publishes the effective value as
ai.provider.responseTimeoutMs, so a client can size its own request
timeout from it instead of guessing.

Refs #1090
2026-10-02 23:01:36 +02:00
Marc-André Bombeck 7cb1b5ba05 fix(insights): write the weight note and its thresholds in the reader's mass unit
The weight status note had its own generator and never took the unit
preference: its signal, summary, graded series, latest-day figure,
target band and correlation pairs were all in kilograms, and the card
prompt stated the normal day-to-day swing, the plateau line and the
loss pace in kilograms too. A reader on pounds was handed both units.

The generator now converts every weight figure once after the canonical
arithmetic (the correlations are scale-free and stay as computed), the
card prompt states its thresholds in the reader's unit, and the unit
preference joins the input fingerprint so a switch is never answered
with the old note. The shared status-card examples and the briefing's
plateau pattern no longer show a weight in kilograms for the model to
copy.

Refs #1067
2026-10-01 13:23:17 +02:00
Marc-André Bombeck d11c165675 fix(coach): read the cycle contrast and the metric table in the reader's units
The cycle block's headline contrast labelled weight in kg and the
temperatures in °C, and left a glucose contrast without any unit. The
metric table, which the person sees and whose summary the model reads,
listed weight in kilograms, temperatures in degrees Celsius, walking
speed in metres per second and distance in metres.

The cycle contrast now converts both means and the gap into the
reader's units (the gap by the factor alone) and names the unit in its
sentence. The table resolves each column's unit and decimals from the
reader's preferences and converts every folded period value once; the
snapshot carries the preferences it was written in, so the table and
the snapshot of a turn always agree.

Refs #1067
2026-10-01 13:23:17 +02:00
Marc-André Bombeck ca5ce518d8 fix(coach): state the snapshot's weight, body and distance figures in the reader's units
The Coach snapshot carried weight, body composition, temperatures and
walking speed and distance as stored: kilograms, degrees Celsius, metres
per second and metres, with no unit on the block. The evidence-block
rules then listed kg and both glucose units as the units to cite. A
reader on pounds, Fahrenheit or miles was answered in the units they do
not use.

Every value block now converts its timeline (and the weight block its
aggregate and coarse tail) into the reader's units after aggregating on
the canonical value, and names the unit on the block; the grounding
scalars stay canonical. Workout distances arrive in kilometres or miles
with the unit named once. The prompt in every locale tells the model
that a block's figures are already in the reader's unit and takes the
evidence-block unit from the block instead of a fixed list.

Refs #1067
2026-10-01 13:23:17 +02:00
Marc-André Bombeck d1b2539b1f fix(coach): state reference and time-in-range bands in the reader's units
The reference grounding printed the fasting glucose bands in mg/dL and
the body-temperature band in °C, and the system prompt described time
in range as 70-180 mg/dL. For a reader on mmol/L or Fahrenheit the
model was handed one quantity in two units: the glucose block in
mmol/L, the band beside it in mg/dL.

The grounding block now takes both unit preferences and prints the
general, the diabetes goal and the temperature band in them, while the
placement is still classified on the canonical value. The glucose
block carries the time-in-range band in its own unit, and the prompt
points the model at it instead of naming a unit.

Refs #1067
2026-10-01 13:23:17 +02:00
Marc-André Bombeck 6dbf57f4f4 fix(ai): name the acute glucose and fever floors in both units
The acute red-flag clause stated the glucose floors only in mg/dL and the
sustained-fever floor only in °C. The clause is matched against what the
person writes, so a reading of 2.9 mmol/L or 103 °F did not meet any of
its numbers. Each floor now carries both units, mg/dL and °C first, in
the English and German contracts.

Refs #1067
2026-10-01 13:23:17 +02:00
Marc-André Bombeck 383323b542 Merge remote-tracking branch 'origin/main' into fix/v1397-meta 2026-10-01 11:29:08 +02:00
Marc-André Bombeck b4577f5c31 test: give every scanning guard a floor on what it found
A guard that compares a sweep against an allowlist, or against "none of
these may exist", passes when the sweep comes back empty. walkSourceFiles
already refuses a short walk; this adds the same refusal for what a guard
does with the walk.

- requireFloor(label, found, floor) throws below the floor and refuses a
  floor of zero.
- scanSourceMatches(root, pattern, { fileFloor, matchFloor }) walks, matches
  and floors both the files read and the hits found.

Floors added, pinned at the current counts:

- coach_usage owner columns: the clauses it actually examined (6).
- measurement reference index: the foreign keys it extracted (2).
- job outcome fact inventory: job files (130, floor 120) and fact keys
  (399, floor 350).
- tone harness: assessment prompt modules (11).
- coach locale default: turn pipeline modules (15, floor 12).
- locale integrity: message bundles (7).
- settings card header coverage and settings anatomy: the walked cards.
- coach metric scope inventory: insight route sources.
- side-effect drift guard: both enums populated, since two empty keysets
  are equal.

The pill-mapper guard walked with globSync, which skips dot-prefixed
directories; it now walks through walkSourceFiles like the rest.
2026-10-01 10:52:48 +02:00
Marc-André Bombeck 27ac4c1d63 Merge origin/main into fix/v1397-tz 2026-10-01 10:39:26 +02:00
Marc-André Bombeck 7c5ff89def feat(lint): refuse UTC day keys outside the time zone module
`instant.toISOString().slice(0, 10)` and parsing a key back as UTC
midnight are the idioms behind every day-boundary fix this year: the
evening west of UTC and the small hours east of it land on the
neighbouring day. A new rule, healthlog/no-utc-day-key, refuses them in
src/ outside src/lib/tz and tests, and refuses a disable directive for
itself that is not next-line or gives no reason.

The time zone module now names every legitimate form: userDayKey for a
person's day, dateOnlyKey / dateOnlyAtNoonUtc / dayKeyAsUtcMidnight for
date-only values and day labels, daysBetweenDateKeys / weekdayOfDateKey /
isCalendarDateKey / statedDateKey for key arithmetic and parsing.

Of the 133 sites the rule found, 84 are gone. Most moved onto a named
helper with no change in behaviour; these changed behaviour:

- cycle calendar, insights, Coach snapshot and prediction refresh start
  their lookbacks at the first instant of the user's day
- a model-transcribed date like "June 10, 2026" no longer lands a day
  early in a process east of UTC (documents and lab scans)
- a custom report range covers exactly the picked days in the profile
  zone instead of starting the evening before west of UTC
- date-of-birth pickers cap at the user's today, not the UTC one
- an allergy onset date is stored at noon UTC like other date-only values
- the report's cycle summary, the Coach's usual BP range and the intake
  projector's rollup recompute use the user's day
- vaccination dates render as dates instead of the previous evening

49 remain, each with its reason on the line above: 39 UTC by design (15
file name stamps, rollup and object keys, the UTC-day score engines) and
10 marked baseline. A guard test requires a classified reason on every
directive and holds the baseline to a ceiling that only goes down.
2026-10-01 09:59:57 +02:00
Marc-André Bombeck 4ac05364fe fix(coach): count GLP-1 injection days on the user's calendar
The snapshot cut the last injection, the dose history and the projected
next injection on UTC days and compared getUTCDay() with the schedule's
local weekday. An evening injection west of UTC landed on the next day,
and east of UTC 'tomorrow' was today. Day keys now come from the user's
zone, which the Coach snapshot already resolves and now passes in.
2026-10-01 09:29:01 +02:00
Marc-André Bombeck 6c6d363bba fix(insights): write metric status notes in the reader's units
The per-metric status note quoted glucose in mg/dL, body composition in
kg, temperatures in °C and walking distance in metres, whatever the
reader had chosen; the deterministic floor without a provider did the
same. The generator now converts the series, the reference band and the
signal into the reader's units before they reach the prompt or the
floor, and prints the guideline band table in that unit while still
classifying on the canonical value.

A changed glucose unit or metric/imperial preference (own account or a
managed record) now re-warms the stored notes and narratives through
the hash-gated refill, so today's text does not keep the old unit.

Refs #1067
2026-10-01 08:52:44 +02:00
Marc-André Bombeck 26352001b9 chore(coach): drop an unused type import from the method line 2026-09-27 13:43:39 +02:00
Marc-André Bombeck 21a2e2fcef fix(coach): keep the chart on "as a table" so the chart is offered back
"As a table" used to drop the table's chart, so the reply offered no way
back to it. The copied table now keeps its chart and carries view: table,
which the panel opens on the table (the chart one tap away on the toggle)
and the chips read to offer "as a chart". show_result with view table
does the same. The field is optional on the result metadata; a client
that ignores it shows the chart first as before.
2026-09-27 13:36:48 +02:00
Marc-André Bombeck 14bc609712 test(coach): follow the comparison read and the question pointer in the dialog scenarios 2026-09-27 13:32:59 +02:00
Marc-André Bombeck b1629e2ad3 refactor(coach): keep the exclusion gate in its own module beside the snapshot builder 2026-09-27 13:32:58 +02:00
Marc-André Bombeck 5173049130 Merge branch 'release/v1.39.4' into fix/v1394-review-correctness 2026-09-27 13:27:59 +02:00
Marc-André Bombeck ee5c44d9d7 fix(coach): name a night in the prose by the key the sleep table uses
The Coach's recent nights carried the wake instant in UTC as their date,
while the sleep table under the answer keys a night by the local day it
ends on. East of UTC the two named different days for the same night.
The snapshot now gives the night's own key.
2026-09-27 13:27:51 +02:00
Marc-André Bombeck 7e91596f02 perf(coach): send the table rules only when there is a table
The recheck and follow-up rules now ride the tool-mode prompt only once
the conversation holds a table, or from the round after the turn produced
its first one. The clarifying-question rules are shorter and English for
every locale, like the other instructions to the model; the German copy
of them and of the recheck rule is gone. A turn without tables sends
about 1 300 characters less on every round.

The golden transcripts change only in the system prompt's hash and
length (4 509 to 3 187 characters; 4 728 to 3 280 for the German turn).
2026-09-27 13:26:16 +02:00
Marc-André Bombeck 42e658f775 test(coach): pin the stored dialog id shapes 2026-09-27 13:22:29 +02:00
Marc-André Bombeck 6a458ac5b0 fix(coach): hold restored dialog ids to the shapes the server mints
Table refs, step ids, chip ids, a chip's anchor ref, clarification choice
ids and a reused table's source are now validated against their exact
server-minted patterns when a stored reply is read back. Some of them are
written into the model's context on a later turn (the earlier-tables list,
the "keep looking" fetched list), so a stored row edited by hand can no
longer carry text into a prompt through them.
2026-09-27 13:22:09 +02:00
Marc-André Bombeck d6ac4acad2 refactor(coach): count the assistant messages before the window inside the conversation read 2026-09-27 13:22:09 +02:00
Marc-André Bombeck 4e47670f25 refactor(coach): import the prior-table gate from the executor, not the tools barrel 2026-09-27 13:22:09 +02:00
Marc-André Bombeck bd3aa3ddb1 fix(coach): hold an excluded metric's earlier tables back from the model
show_result now passes the same scope gate a fresh read does before it
decrypts a stored table: a metric the person has since excluded from the
Coach, or one outside the conversation's scope, answers
unavailable_in_scope and nothing is sent. The list of earlier tables in
the context drops those metrics too, so the model is not told they exist.

The exclusion (the person's own list plus switched-off modules, and no
adherence when medications are excluded) is the snapshot builder's, now
one exported helper both paths use.
2026-09-27 13:18:26 +02:00
Marc-André Bombeck 2d9443b0b7 fix(coach): keep the person's question out of the system role, read the latest messages once
"Keep looking" no longer copies the unfinished question into the
server-written context lines. The question stays in the transcript as the
person's own turn; the line only points at it. Lifted into the system
role, whatever the question said would have carried the system's
authority.

A turn now reads its conversation's newest messages once and shares the
rows between the chip, clarification, "keep looking" and repeat-question
checks, instead of three identical reads.

Earlier tables keep their m<k> names once a conversation outgrows the
window a turn loads: the index counts the assistant messages before the
window too, so a name the model read on an earlier turn still points at
the same table.
2026-09-27 13:16:08 +02:00
Marc-André Bombeck eeda21c7a1 refactor(coach): keep the table storage budget in the pure refs module
The backup validation now reads the per-message table ceiling from
results/refs, which imports nothing but types. Reading it from
results/project pulled the tool executor and the message store into the
snapshot builder's import graph, and the free-text sanitiser guard then
walked files that never build a prompt.
2026-09-27 13:15:48 +02:00
Marc-André Bombeck 4558271b3e fix(coach): let a correct comparison with an earlier period stand
The grounding ledger derives differences only between figures of one
payload, so a correct "up 4 mmHg on last week" drawn from two separate
table reads was withheld as unverified. A table of the period before or of
a year ago now reads the current window too, and its summary carries the
current figures and the change (delta and percent change) beside the
earlier ones. A wrong delta is still flagged.
2026-09-27 13:08:36 +02:00
Marc-André Bombeck c2ab9cda11 test(coach): put the dialog scenarios to a real model nightly
The live evaluation now captures the tool calls a generation makes, with
their arguments, and hands them to the judge. runScenarioLive puts a
scenario to the model in the chat turn's own context (system prompt,
tool-mode and dialog addenda, DATA INVENTORY, EARLIER TABLES, transcript)
over the real tool catalogue, answers each call with a stand-in result,
and reads the reply through the turn's clarification and chip parsers.
runScenarioJudge grades each scenario a few times with the same
evaluateScenario the deterministic suite uses and reports a pass rate per
scenario; the server-only scenarios and checks are left out.

Like the existing judge it runs only with COACH_EVAL_API_KEY, from the
nightly workflow's non-blocking step, and never throws.
2026-09-27 13:07:25 +02:00
Marc-André Bombeck 1dd74bfa20 test(coach): follow the related-metric chip copy 2026-09-27 13:07:18 +02:00
Marc-André Bombeck 9892088620 fix(coach): account for every stored table and keep table controls on screen
A withheld table now leaves a line saying why (module off, or the stored
tables could not be read) and a failed read shows an error row with a
retry, so the Data used count matches what is listed. A table copied from
an earlier answer names that answer's date under its title. The preview
shows the latest twelve periods of a time series, and the truncation note
says the latest rows are kept, which is what the server keeps. The title,
copy and view controls sit above the scrolling table with the title kept
as a hidden caption, and a reply with tables takes the full column width.

The step headers and the related-metric chip read as label: value in all
seven locales, so a metric name no longer sits capitalised mid-sentence.
2026-09-27 13:07:07 +02:00
Marc-André Bombeck 25ad1810c2 fix(coach): offer the period before only when the record reaches back
The "compare with the period before" chip now needs a first reading
before the window's start, the same history check the other history chips
use, and is never offered on an all-time table, which has no period
before it: the table tool reads all time as the current period whatever
it is asked.
2026-09-27 13:06:55 +02:00
Marc-André Bombeck 203068d5ba fix(coach): keep a billed reply when its tables fail, and fit tables once
Tables are now projected outside the provider's failure path: a
projection defect costs the turn its tables and is annotated, instead of
refunding tokens already billed and dropping the reply.

The at-rest ceiling for a message's tables is applied once, when the
dialog is assembled, before anything streams. A table trimmed only at
write time used to show live and read back as withheld after a reload.
2026-09-27 13:06:02 +02:00
Marc-André Bombeck ec281c02e2 test(coach): grade the dialog scenarios through the real pipeline
Ten scenarios, each in German and English, describe what a turn must do
with a question: which tool it calls and with which arguments, whether it
may read anything new, whether it asks back, declines a dose, offers
chips only for what the record holds, answers a chip without a provider,
offers to keep looking after a forced answer, and keeps an earlier
table's title out of the model's context. evaluateScenario grades a run
against them.

The deterministic suite drives POST /api/insights/chat with a scripted
provider; the tool loop, executor, table tool, chips, clarification
filter, method line, reply guards and stream run for real. Every check
also runs against a misbehaving model or a broken record and must fail
there.

route-followups.test.ts pins the front door for chips and answered
questions: validation, the rate limit and capability gate on a reuse
turn, the stored chip winning over anything the request adds, stale
chips and answers degrading to a plain message, and the continue line.
2026-09-27 13:04:12 +02:00
Marc-André Bombeck de2200bd5c fix(coach): make table weeks and months mean one thing per metric
A total's weeks and months are now the sum of its days, labelled as a
total, and a level's stay a mean. All time is read as UTC calendar months
from the rollup tier's MONTH fold, or the same fold of the live table when
the tier has no rows, so a month no longer reads as a mean of daily totals
for a short history and a monthly total for a long one. Months are keyed
by their UTC start instead of being re-read in the user's zone, which
named every month one early west of UTC.

The chart's long ranges had the same shift: tier rows are now stamped at
noon UTC of the bucket's first day, which falls inside the bucket's own
week and month in every zone.

Step length and walking speed keep two decimals, loud-sound events count
as a total in whole events, and HRV counts a night measured by both
estimators once and charts the estimator the person mostly has. The model
summary names what a period's value is and gives a total's range total.
2026-09-27 13:03:31 +02:00