Commit Graph
5968 Commits
Author SHA1 Message Date
Marc-André Bombeck 6d98a67751 Say which knip entry is kept on purpose, and re-measure the count
The production pass reports 22 files, and the comment said 21 — a small
drift, but the paragraph reads as a triage nobody has redone, so the
number is dated now and carries an instruction to re-measure.

Two things the block did not say:

The standing exception. `signals/adapters/metric-status.ts` is reported
unreachable because its only importer is its own registry-invariant test.
That is what the module is for: it projects the signal registry onto the
MetricStatusMeta shape so the test can assert the two agree field for
field. Deleting it to clear the line removes a check rather than ballast.
Someone triaging the list without that written down has to rediscover it,
and the obvious move is the wrong one.

The healed example. The false-positive slot cited a module reported
because scripts/ sits outside the tsconfig that defines the @/ alias.
It no longer appears — module and importing script both still exist, so
resolution improved under us. Replaced with the case that does hold
today, the service-worker policy that public/sw.js mirrors by hand, and
the old one kept as a note: an example that heals silently makes the
reasoning around it look wrong to whoever checks it next.
2026-08-31 21:04:22 +02:00
Marc-André Bombeck e6751f0f0c Merge pull request #882 from MBombeck/fix/transposed-reference-window-v2
Drop a reference window whose floor sits above its ceiling
2026-08-31 20:35:14 +02:00
Marc-André Bombeck 963f61cf0f Drop a reference window whose floor sits above its ceiling
A lab report's reference range reaches the record by four paths. Three of
them validate that the floor does not exceed the ceiling; the document
path did not, and the catalog store writes what it is handed. An
integration run against a real Postgres confirmed the consequence: a
100/30 pair landed on the row and on the freshly minted biomarker, so
every later reading of that analyte inherited a window that cannot be
true and a high value came back reassuring.

Dropping beats swapping. A transposed pair says the transcription is
wrong, not which of the two numbers is, and inventing an order would
state something the report never printed. The verbatim string survives
either way, so nothing the document actually said is lost.

Three points, answering according to what each one is:
- the extraction schema drops the pair, because every field there is
  catch-guarded so one bad value cannot sink a 120-fact document
- the review-screen edit refuses with the wording the lab schemas
  already use, because a person can see both numbers and fix them
- the commit path drops it as the last gate, which is what covers facts
  staged before this rule existed and confirmed afterwards

parseReferenceRange has answered a transposed PRINTED window this way
since it was written, and the commit path already counts a window it
could not read at all. This extends both to the case that was readable
and cannot be true.
2026-08-31 19:38:08 +02:00
Marc-André Bombeck a852860654 Merge pull request #881 from Antiheld86/feat/measurements-write-scope
Feat/measurements write scope
2026-08-31 16:03:44 +02:00
Antiheld86 a778acac4e chore: regenerate OpenAPI spec 2026-08-31 12:48:18 +00:00
Antiheld86 7c23b14053 Teach the arrival-wiring mock about isScopedCredential
That file stubs @/lib/api-handler wholesale, so it lists the exports it
knew about when it was written. /api/measurements now reaches for
isScopedCredential to decide source attribution and whether the sync
checkpoint may move, the mock does not provide it, and every test in the
file dies on import rather than on an assertion.

`false` is the honest stub: those cases resolve as a cookie session,
which is exactly what unscoped means, so the route takes its ordinary
path through both rules.

Checked the rest of the tree rather than fixing only the file that went
red — this is the only suite mocking that module for a route which
reaches the new export. The batch route's tests mock at a different
seam and are unaffected.

Not verified here: this environment cannot generate the Prisma client,
so that file is one of the suites which fail to import for an unrelated
reason and the fix could not be run.
2026-08-31 12:14:51 +00:00
Antiheld86 19b69dee21 Fix the B8 delegation cases to actually claim a record
Three failures in the integration run, all mine, none in the product.

The two delegation cases set the account selector as a header on the
NextRequest. Nothing reads it there. `readSelectorHeader()` resolves
through `next/headers`, which this suite mocks against the shared
`headerJar`, so the carrier came back `none` and both cases quietly
exercised the ordinary own-record path instead of the one they name. The
scoped case wrote to its own record and returned 201; the wildcard
control wrote to the caller rather than the owner. Neither was testing
delegation at all.

That is worse than a broken test. The refusal these two cover is the
regression this change could most easily have shipped in silence, and
the tests standing in front of it were not looking at it.

They now set the selector on the jar, through the exported
ACCOUNT_SELECTOR_HEADER rather than a literal, so a rename cannot
reintroduce the same gap.

The third failure is unrelated and smaller: the read-leg case read the
audit row once, racing a fire-and-forget write, and reported `undefined`
for a row about to exist. It now polls, matching B1, which carries a
comment about this exact hazard that I did not follow.

Not verified here: this environment has no Docker daemon, so the
integration suite cannot be run against a real Postgres. The diagnosis
is from the mock and the resolver, not from a green run.
2026-08-31 11:57:26 +00:00
Antiheld86 56d6fb180a Make the measurement-token mint cookie-only
The mint used requireAuth() with no scope, which refuses a narrow token
but still admits a cookie-equivalent one. The reach is not the problem —
a wildcard token can already do everything this endpoint does. The
lifetimes are: a native access token lives a day, and what it mints here
lives a year. So a credential that leaked for an afternoon could leave
behind one that outlives revoking it, and the revocation would look like
it had worked.

requireCookieAuth() closes that. It is the helper the passkey
registration and trusted-device routes already use for the same reason,
and the sharing guard recognises it as cookie-only.

The prose said more than the code did, in three places: the route header
and the OpenAPI description both claimed the endpoint "cannot mint
another token", which held only for narrow tokens, and the 403 arm
described a scope refusal that a Bearer never reaches. All three now say
what is true — no Bearer credential reaches this endpoint at any scope,
and a Bearer is refused 401 before the operator switch is read.

Tests follow the status change: the integration case asserting a scoped
token cannot mint now expects 401 rather than 403, since the refusal is
authentication and not scope, and a companion case pins the wildcard —
the credential class this change is actually about — and asserts no
token row is created.
2026-08-31 11:38:16 +00:00
Antiheld86 8f61e0bc6a Plainer copy on the measurement token card
The card read like API documentation sitting in a settings page: it named
the Authorization header, spelled out both endpoint paths, and listed the
scope's limits as four clauses. None of that is what someone deciding
whether to mint a token needs, and the endpoint paths were already in the
API endpoints card immediately above it.

Rewrite to the shape the MCP connector card next door uses — a short
title, one line on what the token is for, one line saying it is shown
once, and one sentence on what it can and cannot do. Drop the endpoints
note entirely rather than rephrase it, since the card above owns that.

Say the useful consequence instead of the mechanism: readings arrive as
if entered by hand, so they can be corrected. That is the thing a person
will actually notice.

Translations were revised across all seven locales — the previous German
in particular read as translated English rather than as the rest of the
app, which uses short informal sentences.
2026-08-31 09:50:47 +00:00
Antiheld86 e79709379b Fix duplicate status keys in the new OpenAPI response blocks
`stdResponses` already carries 401, 422 and 429. The three response
blocks added with the measurements:write scope each declared their own
422 (and, on the mint route, a 429) BEFORE spreading it, so the generic
wording overwrote the specific wording and TypeScript refused the
duplicate key. The build failed at auth.ts with "'422' is specified more
than once".

Move the explicit keys after the spread, which is what the MCP mint next
door already does and the only order in which a per-route description
survives.

Worth stating because the failure mode is quiet: had TypeScript allowed
it, the published contract would simply have carried "Request validation
failed" everywhere instead of naming
measurement.create.source_not_permitted and its siblings.
2026-08-31 09:22:38 +00:00
Antiheld86 cba7a0fce5 Add a self-service measurements:write API token scope
Since v1.30.17 there has been no self-service path to a token that can
write a measurement. That release made the Bearer scope default
fail-closed, which closed a real privilege-escalation hole, and removed
the generic mint at POST /api/tokens along with it. The generic mint had
been the only way to get a token that could reach /api/measurements, so
a local sensor pushed through Home Assistant or Node-RED lost its door
while the OAuth integrations kept theirs.

Add a narrow scope accepted by exactly two routes, both writes, both on
the holder's own record.

The scope constant is a leaf module beside the surface that consumes it,
matching FHIR_READ_SCOPE and the MCP scopes. requireRecordAuth grows an
options object rather than a sibling function, because the sharing
surface guard keys every leg on that one identifier and a second name
would mean two copies of the delegable declaration. AuthContext carries
the admitting scope as a single nullable string rather than the token's
permission array, so nothing downstream can re-derive an authorisation
decision from it.

A scoped credential is refused a record selector before any grant is
read. Placing the refusal above the sharing check rather than beside it
means no future widening of what a grant permits can reach one of these
tokens.

Rows are attributed MANUAL. An entry that explicitly names APPLE_HEALTH
is refused 422 rather than relabelled: that source is half the
(userId, type, source, externalId) dedup key the iOS client writes into,
it participates in the cross-source merge, and it gates the HealthKit
sync fields. For the same reason a scoped write no longer advances
User.lastSyncedAt, which the phone reads to size its undelivered window.

Also closes a narrower gap found while building this. The idempotency
wrapper resolved a replayed delegated response against a live grant
without considering which credential was presenting it, so a cached
response about a shared record could be returned to a token the handler
itself would refuse. No write was reachable that way; the response body
was.

Guards: T3 gains the mint site, T4's vocabulary gains the scope and
learns to read the requireRecordAuth form (without which this change
would open a way to widen a route that the closed vocabulary could not
see), and a new T5 freezes the set of delegable routes naming a scope.
The sharing surface guard's AST matcher reads only the first two
arguments, so it needed prose but no code.
2026-08-31 08:46:16 +00:00
Marc-André Bombeck 4308114c5b Merge pull request #879 from MBombeck/fix/lab-outlier-range-assertion
test(labs): assert the outlier's distance from the edge, not one string
2026-08-31 00:22:15 +02:00
Marc-André Bombeck 5e89a201f7 test(labs): assert the outlier's distance from the edge, not one string
Follow-up to #860, promised in review rather than asked of the
contributor. `not.toContain("left: 4%")" passes for a marker at 4.02%,
which is the same defect one rounding away, so the test now reads the
marker position and asserts it sits inside the padding at both ends.

EDGE_PADDING_PERCENT moves to module scope and is exported, so the
assertion uses the number the component clamps with instead of a copy
that can drift. It was being reallocated on every render besides.

Verified by mutation: dropping the outlier expansion puts the marker at
exactly 4 and turns this test red.
2026-08-31 00:06:50 +02:00
Marc-André Bombeck 7cca0a0710 Merge pull request #860 from TimonBed/feat/labs-ocr-openai-fix
rework of range bar ind lab list
2026-08-31 00:04:14 +02:00
Timon_bed f30a202a67 fix: keep lab outliers clear of range bar edges 2026-08-30 23:45:47 +02:00
Marc-André Bombeck b1d6765790 Merge pull request #875 from MBombeck/dependabot/npm_and_yarn/vitejs/plugin-react-6.1.0
chore(deps-dev): bump @vitejs/plugin-react from 6.0.4 to 6.1.0
2026-08-30 22:24:11 +02:00
Marc-André Bombeck 78921399fb Merge pull request #877 from MBombeck/dependabot/npm_and_yarn/radix-ui-1.6.7
chore(deps): bump radix-ui from 1.6.2 to 1.6.7
2026-08-30 22:07:32 +02:00
Marc-André Bombeck e7d40e22b0 Merge pull request #876 from MBombeck/dependabot/npm_and_yarn/lucide-react-1.34.0
chore(deps): bump lucide-react from 1.27.0 to 1.34.0
2026-08-30 22:05:58 +02:00
Marc-André Bombeck 68f33dad5f Merge pull request #874 from MBombeck/dependabot/npm_and_yarn/next/bundle-analyzer-16.3.3
chore(deps-dev): bump @next/bundle-analyzer from 16.3.1 to 16.3.3
2026-08-30 22:00:18 +02:00
Marc-André Bombeck 0be1663989 Merge pull request #873 from MBombeck/dependabot/npm_and_yarn/hookform/resolvers-5.9.1
chore(deps): bump @hookform/resolvers from 5.4.0 to 5.9.1
2026-08-30 21:58:27 +02:00
Marc-André Bombeck a4b2a1541d Merge pull request #872 from MBombeck/fix/pg-runtime-dependency
fix: declare pg as the runtime dependency it already was
2026-08-30 21:57:26 +02:00
dependabot[bot] c532f60c57 chore(deps): bump radix-ui from 1.6.2 to 1.6.7
Bumps [radix-ui](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radix-ui) from 1.6.2 to 1.6.7.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radix-ui/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/1.6.7/packages/react/radix-ui)

---
updated-dependencies:
- dependency-name: radix-ui
  dependency-version: 1.6.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 19:17:23 +00:00
dependabot[bot] 07b32617d2 chore(deps): bump lucide-react from 1.27.0 to 1.34.0
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.27.0 to 1.34.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.34.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 19:02:53 +00:00
dependabot[bot] 098cc9d5d4 chore(deps-dev): bump @vitejs/plugin-react from 6.0.4 to 6.1.0
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 6.0.4 to 6.1.0.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.1.0/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 19:02:16 +00:00
dependabot[bot] 5898742fa3 chore(deps-dev): bump @next/bundle-analyzer from 16.3.1 to 16.3.3
Bumps [@next/bundle-analyzer](https://github.com/vercel/next.js/tree/HEAD/packages/next-bundle-analyzer) from 16.3.1 to 16.3.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.3/packages/next-bundle-analyzer)

---
updated-dependencies:
- dependency-name: "@next/bundle-analyzer"
  dependency-version: 16.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 19:01:40 +00:00
dependabot[bot] 2e6e133409 chore(deps): bump @hookform/resolvers from 5.4.0 to 5.9.1
Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 5.4.0 to 5.9.1.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v5.4.0...v5.9.1)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 19:00:56 +00:00
Marc-André Bombeck b4c18d131a Merge pull request #871 from MBombeck/fix/codeql-action-4.37.9
ci: move every codeql-action pin to v4.37.9 in one commit
2026-08-30 20:50:25 +02:00
Marc-André Bombeck 9f07a46633 fix: declare pg as the runtime dependency it already was
The Dockerfile asserts that the standalone image can resolve pg, because
pg-boss loads it through native Node resolution. That assertion has been
passing on a coincidence: pg sat in devDependencies, and the version our own
entry pinned happened to match the one @prisma/adapter-pg pulled in, so both
names resolved to a single copy in the store and Next's file tracer packed it
whole.

Bumping the adapter to 7.10.0 moves its transitive pg to 8.23.0 while ours
stayed at 8.22.0. The tracer packs only the copy the adapter reaches, the
top-level link points at the other one, and the image fails to build on both
architectures:

    Error: Cannot find module '/app/node_modules/pg/lib/index.js'

Move pg into dependencies at ^8.23.0. The lockfile now carries one pg version
instead of two, so the two resolutions cannot drift apart again on the next
adapter release. Carries the adapter bump from #865, which cannot build alone.
2026-08-30 20:49:46 +02:00
Marc-André Bombeck eb49580271 Merge pull request #868 from MBombeck/dependabot/npm_and_yarn/zod-openapi-6.0.1
chore(deps): bump zod-openapi from 6.0.0 to 6.0.1
2026-08-30 20:47:35 +02:00
Marc-André Bombeck 95a75cd9e0 Merge pull request #862 from MBombeck/dependabot/npm_and_yarn/knip-6.32.3
chore(deps-dev): bump knip from 6.32.2 to 6.32.3
2026-08-30 20:47:27 +02:00
Marc-André Bombeck 314f35b2ee ci: move every codeql-action pin to v4.37.9 in one commit
init, analyze and upload-sarif are the same release pinned in three
workflows. Dependabot opened them as three separate pull requests, so each
run mixed two versions and analyze rejected the config init had written:
"Loaded a configuration file for version 4.37.7, but running version
4.37.9". None of the three could go green alone, and the code-scanning
analysis stayed red while they waited on each other.

Bump all five references together and group the action in dependabot.yml so
the next release arrives as one pull request.
2026-08-30 20:20:31 +02:00
Marc-André Bombeck 58de37cf52 Merge pull request #869 from MBombeck/dependabot/npm_and_yarn/simplewebauthn/server-13.3.3
chore(deps): bump @simplewebauthn/server from 13.3.2 to 13.3.3
2026-08-30 20:19:17 +02:00
dependabot[bot] e87d44952c chore(deps-dev): bump knip from 6.32.2 to 6.32.3
Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) from 6.32.2 to 6.32.3.
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.32.3/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-version: 6.32.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 18:16:04 +00:00
Marc-André Bombeck d67115b115 Merge pull request #867 from MBombeck/dependabot/npm_and_yarn/node-rs/argon2-2.1.0
chore(deps): bump @node-rs/argon2 from 2.0.2 to 2.1.0
2026-08-30 20:12:57 +02:00
Marc-André Bombeck 2326ca585b Merge pull request #861 from TimonBed/feat/openai-ocr-only
fix: fixes ocr for newer open ai models
2026-08-30 20:02:24 +02:00
Marc-André Bombeck fe0c248d5c Merge pull request #864 from MBombeck/release/v1.38.0
chore(release): v1.38.0 — the score works with the record you keep
v1.38.0
2026-08-30 18:50:21 +02:00
Timon_bed d1251634b3 prettier fix 2026-08-30 18:07:19 +02:00
Timon_bed 34e37fe850 prettier fix 2026-08-30 18:01:47 +02:00
dependabot[bot] a425cf58bf chore(deps): bump @simplewebauthn/server from 13.3.2 to 13.3.3
Bumps [@simplewebauthn/server](https://github.com/MasterKale/SimpleWebAuthn/tree/HEAD/packages/server) from 13.3.2 to 13.3.3.
- [Release notes](https://github.com/MasterKale/SimpleWebAuthn/releases)
- [Changelog](https://github.com/MasterKale/SimpleWebAuthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/MasterKale/SimpleWebAuthn/commits/v13.3.3/packages/server)

---
updated-dependencies:
- dependency-name: "@simplewebauthn/server"
  dependency-version: 13.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 15:56:00 +00:00
dependabot[bot] 89b630060d chore(deps): bump zod-openapi from 6.0.0 to 6.0.1
Bumps [zod-openapi](https://github.com/samchungy/zod-openapi) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/samchungy/zod-openapi/releases)
- [Changelog](https://github.com/samchungy/zod-openapi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/samchungy/zod-openapi/compare/zod-openapi@6.0.0...zod-openapi@6.0.1)

---
updated-dependencies:
- dependency-name: zod-openapi
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 15:55:30 +00:00
dependabot[bot] b16f5f9947 chore(deps): bump @node-rs/argon2 from 2.0.2 to 2.1.0
Bumps [@node-rs/argon2](https://github.com/napi-rs/node-rs) from 2.0.2 to 2.1.0.
- [Release notes](https://github.com/napi-rs/node-rs/releases)
- [Commits](https://github.com/napi-rs/node-rs/compare/@node-rs/argon2@2.0.2...@node-rs/argon2@2.1.0)

---
updated-dependencies:
- dependency-name: "@node-rs/argon2"
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-30 15:55:00 +00:00
Marc-André Bombeck f71b0142a8 chore(release): v1.38.0 — the score works with the record you keep
Three areas of health become a recommendation rather than a gate. A one
or two area record now scores, the card says what the number rests on,
and refusal narrows to the one honest case: nothing scoreable yet.

Carries the composition notice, the reference-citation guard that keeps
invented targets out, @TimonBed's OAuth callback warning, and the fix
for a geometry check that had never measured in CI.
2026-08-30 17:54:02 +02:00
Marc-André Bombeck 28af7c13d3 Merge pull request #859 from MBombeck/feat/health-score-target-guards
feat: pin the rule that keeps invented targets dead
2026-08-30 17:50:57 +02:00
Timon_bed 6ab3b02935 fixes ocr for newer open ai models 2026-08-30 17:37:52 +02:00
Marc-André Bombeck b4ebd6ffce test: drop the fixture's unused module-path export
The constant was left over from an earlier shape of the guard, which
located the fixture by path before it imported it directly. Nothing reads
it now, and the dead-code gate says so.
2026-08-30 17:33:07 +02:00
Timon_bed de9a16b69d rework of range bar ind lab list 2026-08-30 17:24:46 +02:00
Marc-André Bombeck f29995a18c docs(api): correct the score descriptions the adaptive rule outdated
Three descriptions in the OpenAPI registry still describe the floor that
v1.38 removed, and one of them contradicts its own error-response
description two dozen lines below it.

- The score-config PATCH still said the write is refused below three
  distinct areas and one physical measurement. The route refuses the
  empty selection and nothing else; its 422 description already said so.
- The dashboard snapshot's healthScore still said the block is null until
  three eligible domains include a physiological one. It is null only
  when no pillar is readable at all.
- The pillar domain field still pointed at a three-distinct-domains rule
  that no longer exists, where what a client needs to hear is that domain
  count comes resolved on scoreBasis and must not be counted out of
  composition.

Regenerated docs/api/openapi.yaml.

The user-facing description of the score lives in the docs site's own
repository, not here; nothing under docs/ describes the composite.
2026-08-30 17:08:39 +02:00
Marc-André Bombeck 81be61b6d1 fix(seed): give the demo every area of health the score can span
The demo seeds no waist measurement and no wellbeing screener, so two of
the seven pillars have never had data and the score rests on three areas
instead of five. Adiposity and wellbeing render as empty rows on the card
that is meant to show the feature.

Both are seeded now: a fortnightly waist that walks 96 cm down to 88 cm
over the window (0.527 to 0.484 at the demo profile's height, so the
ratio crosses NICE 2022's threshold inside the seeded span rather than
sitting flat green), and six WHO-5 / PHQ-9 / GAD-7 administrations with
their item answers encrypted the way the check-in route writes them, each
with the COMPUTED projection measurement so the history view and the
trend chart agree. Totals sit in the calm bands and item 9 is answered
zero throughout, which also keeps the pillar clear of its
crisis-signposting arm.

Verified against a throwaway Postgres: before, five ok pillars over three
areas; after, seven over five, tier full.

The dates were never the problem. Every timestamp the seed writes is
already relative to the run, and the header says so. What ages is the
seeded DATA — the pillar windows keep moving after a seed, so four weeks
on, activity and sleep fall out of theirs and the score narrows to
whatever cardiometabolic data is still fresh. Since v1.38 that is a
labelled partial score rather than a blank card, but it is still not the
demo at its best, and the header now says that too.
2026-08-30 17:08:29 +02:00
Marc-André Bombeck d98d714ee7 feat(score): pin the rule that keeps invented targets dead
The score once graded weight against 22 x height squared, widened by 2 kg
either side: a band nobody set, no surface named, and the only producer
of that pillar's verdict. v1.34 removed it and the composite rebuild left
a pillar set where every scored band cites somebody. Nothing but review
has kept it that way since, and review is what let the original in.

So the rule is written down. PillarReference.kind becomes a runtime list
carrying the reason there is no fourth kind, reference.source is
documented as a citation a reader could go and look up, and a guard
beside the pillars checks all seven modules three ways.

The arm worth arguing about is the second. An invented target is a
continuous function of a profile field; a guideline band is a step
function into a published table. So the guard sweeps every pillar across
a matrix of heights and ages and requires the scored bands it emits to
land inside a small enumerated set transcribed from the guideline that
pillar cites. No finite enumeration can hold a per-person curve, which is
what makes the arm able to fail rather than merely able to pass. The
other two arms cover the citation string and freeze which pillar may read
which profile field, with the reason written beside it and stale entries
failing as loudly as missing ones.

The proof is in the file: the pre-v1.34 weight target is restored as a
fixture in today's shape and each arm is shown refusing it.
2026-08-30 17:08:16 +02:00
Marc-André Bombeck a2a743c9e1 Merge pull request #858 from MBombeck/feat/health-score-composition-notice
feat: the score says when a pillar stopped counting
2026-08-30 16:42:42 +02:00