Document reading no longer treats a signed-in subscription model as unable to
see images, so 'read with AI' works on that path instead of erroring; scanned
PDFs and photos are read the same way. Drops two now-redundant blurbs from the
document panel and, on the recovery insights page, moves each metric's
explanation into its own card instead of repeating the heading above it.
Remove the two per-document AI copy blocks in the document detail sheet:
the external-egress notice and the "read this document" explainer. The
settings-toggle honesty confirm already states the egress trade, so the
per-document repeat is redundant. Drop the now-unused `egressExternal`
prop and the `documents.ai.egressNotice` + `documents.ai.available` keys
across all six locales. Keep the capability `egress` DTO field — the API
still reports it.
On /insights/recovery, the metric block rendered an outer heading plus an
explainer above a tile whose own header repeated the same title. Move the
explainer inside the tile as a `description` beneath its `TileHeader` and
promote the tile title to a real `<h2>` (new `titleAs` affordance), so each
metric shows one heading with the blurb under it and the h1->h2 outline
stays nested. Applies to every metric the block renders.
The document/lab-OCR vision gate excluded every `-codex` slug from the
GPT-5.x line as text-only. That is stale: the `codex/responses` wire reads
an `input_image` block on these slugs (docs/codex-protocol-spec.md plus the
current model lineup), and the 5.2-Codex line is vision-tuned. An operator
pinned to a `-codex` slug therefore resolved no vision-capable provider and
hit a 422 "No vision-capable AI provider is configured" when reading a
document with AI.
Drop the `-codex` exclusion from the gpt-5 branch so a codex-only chain
resolves a document-vision provider; keep the `-mini`/`-preview`
reasoning-slug guard. Lab-OCR on a `-codex` pin gains vision the same way.
Per-medication effect view: plots the target metric/lab with start, dose
changes, and pauses marked, a before/after-start comparison, and adherence
overlaid — descriptive only, never a verdict or dose advice. Compact summary on
the medication insights area. Target resolved from ATC class or name, with a
per-user override. Migration 0232.
Add the per-medication "Wirkung" tab (hidden for one-shot and no-target meds)
rendering the target series with a start marker, dose-change markers, shaded
pause bands and a faint reference band, an adherence lane, a neutral
before/after-start card with honest empty states, an optional level-shift note,
and a retarget chooser. Weave a compact, read-only efficacy summary into the
Insights medication surface that reads the same DTO and links through to the
tab. All copy is descriptive; add a banned-verdict-verb guard test over the
medications.efficacy namespace across all six locales.
Add the migration (0232), Prisma model MedicationEfficacyTarget (persists ONLY
the user's explicit target override; everything else derives each read), and
the server builder buildMedicationEfficacy that composes the target's series,
a before/after-start comparison, the cadence-aware adherence lane (never
recomputed), an optional conservative level-shift note, honest data-floor
states and the retarget options into one strictly-descriptive DTO — no
verdict/score field by construction. Expose GET /api/medications/[id]/efficacy
and PUT /api/medications/[id]/efficacy/target, regenerate the OpenAPI contract.
Add an additive efficacy-target resolver alongside the existing metric-only
class map: a discriminated metric|lab target type, a WHO ATC class-prefix
table (consuming Medication.atcCode), and the statin/thyroid/supplement lab
classes the v1.22 map documented as a follow-on. Resolution order is ATC
prefix then whole-word name inference, conservative-fail to no target. The
existing MED_TARGET_MAP / inferMedTargetClass path stays byte-identical so the
adherence-storyline safety posture is untouched.
A layout-audit-driven consistency pass: one shared left edge for insight cards
(headings and body align, worst on mobile), no scroll past content on settings
and admin pages, Settings/Notifications only in the account menu on mobile, the
dashboard action beside its greeting text, and preventive-care/cycle/onboarding
cards on the shared card anatomy. The spacing and alignment rules are recorded
in the standards. Also stages the document rasterization native binding in the
image so a scanned PDF is read on an image-only AI provider.
Bring the structural card-anatomy offenders onto the spacing scale. The
cycle wellness-tile family (ring tile, phase-education, insight summary,
cycle view) and the insights wellness-scores tile drop banned p-5/px-5
py-5 for p-4 md:p-6 and keep their premium hue surface; their header and
info glyphs move off text-primary to foreground, spinners to muted. The
onboarding hand-rolled shells (anamnesis card, baseline form,
getting-started checklist, tour tooltip) drop p-5/space-y-5/rounded-2xl
for p-4 md:p-6/space-y-4/rounded-xl. settings/anamnesis-section moves its
three raw Card+CardTitle shells onto SettingsCard + SettingsCardHeader.
Establish one inset system on the Insights surface. SubPageShell's flat
px-6 header sat 8 px right of every card body on mobile — track the card
slot inset (px-4 md:px-6) so the heading-vs-body step disappears.
DeviceScoreTile and ResilienceTile hand-rolled a muted h-4 header row;
route them through TileHeader size=sm (foreground icon, shared gutter),
latest reading in the right slot. ScoreAnatomyView dropped its banned p-5
+ centred uppercase title for a left-aligned TileHeader on p-4 md:p-6, so
the ring hero stays a centred identity block while the header shares the
reading edge with the stacked assessment card. SparklineDeltaTile swaps
its hand-copied header row for the TileHeader primitive so it can't
drift, and its contributor label rounds off the sub-xs size.
SettingsShell's content column carried pb-24 md:pb-0 that double-counted
the bottom-nav clearance AuthShell's <main> already reserves, stacking
~96 px of dead scroll below the last card on mobile. AdminShell still
carried the pre-#154 min-h-[calc(100dvh-12rem)] column reserve on <main>,
over-reserving on short sections at every breakpoint.
Delete the redundant SettingsShell gutter, move AdminShell's floor off
<main> onto its grid, and factor the budget-derived grid floor into one
shared SUB_SHELL_GRID_FLOOR constant so the two shells can never drift.
Extend the guard to /admin/* and measure against each element's
content-box bottom so a redundant sub-shell pb-* now fails instead of
inflating the allowed budget.
The overdue-dose CTA sat in a flex-wrap row, so a full verdict sentence
pushed the take/log button onto its own line — wasted vertical space.
Adopt the PageHeader inline-action pattern: items-start justify-between,
the sentence gets min-w-0 flex-1 and wraps internally, the button gets
shrink-0 and stays beside the text. Tighten the greeting to verdict gap
from space-y-3 to space-y-2.
The mobile bottom-nav More overflow appended the shared utility tail
(Settings, Notifications) after the feature destinations. Those two are
already reachable from the user/avatar menu, so the hub duplicated them
across two menus on the same screen. Return feature destinations only;
the utilities stay in the user menu. Sidebar footer is unaffected.
Flip the three hub assertions to lock the feature-only contract.
The auto-read vault specs mocked only the document capability probe, leaving
the usage endpoint (assist + content-index flags) unmocked — so the read
action rendered but stayed disabled and the click timed out. Mock the same
three endpoints the working document-AI e2e does (usage, labs OCR capability,
document capability) so the action is enabled.
One opt-in in AI settings reads every uploaded document with the configured
provider — no per-document tap. Off by default; the vault stays local-first
otherwise. Scanned PDFs and photos are read too (a PDF's pages are rendered to
images so an image-only provider can read them). The background indexer now
follows the same opt-in, so nothing leaves the machine automatically unless it
is enabled.
- ai-providers.md: an off-by-default 'Automatic document reading' section
(what ON sends where, the subscription-training / no-BAA honesty, local =
no egress, regulated users → BYOK no-train API or local) and a vault
local-first caveat
- e2e: the settings honesty-confirm flow (switch → confirm once → write) and
the vault per-egress-notice / explicit-read-action contract, with the
document capability endpoint mocked on every path
Add a single switch in AI settings — off by default — that turns on ambient AI
reading of uploaded vault documents. Turning it on reveals a once-shown,
vendor-blind honesty confirm (subscription providers may use the content to
improve their models; a self-hosted local model never egresses) that must be
acknowledged before the setting is written. Copy is translated across all six
locales.
Only Anthropic accepts a native PDF block; codex and other image-only-wire
providers dropped it. Add a bounded, best-effort server-side rasterizer that
renders a PDF's pages to JPEG images those providers read as ordinary image
input, so a scanned/image-only PDF is legible on the auto-read path.
- @napi-rs/canvas promoted to a direct dependency; its musl prebuilts + the
pdfjs loader pinned into the standalone trace (next.config.ts)
- rasterize-pdf.ts: lazy pdfjs import (keeps DOMMatrix out of the eager chunk),
first 10 pages, 1600px long edge, JPEG q80, never throws
- prepareVisionInput rasterizes a PDF for a non-Anthropic vision provider and
keeps the native PDF block for Anthropic; the document capability reports
PDFs readable via raster so the UI offers the read
The auto-index-on-upload job resolved over the cost-first app-wide chain
(codex priority 1) and gated with the chain-based consent check, so a
pure-codex chain could silently egress a freshly uploaded document to the
subscription backend on upload. Reconcile it onto resolveDocumentVisionProvider
(local-first, codex last) and govern external egress with the
documentsAutoAiRead opt-in: a local pick always runs (no egress); an external
pick runs only when the toggle is ON, otherwise the job stays strictly local.
This tightens the default and makes the opt-in path deliberate.
Add the documentsAutoAiRead per-user Boolean (migration 0231, default
false) that authorises ambient AI reading of uploaded vault documents.
OFF by default keeps the vault local-first for every self-hoster.
- schema column + migration 0231_documents_auto_ai_read
- documentAutoReadEnabled(userId) read helper
- GET/PATCH /api/auth/me/documents-auto-ai-read toggle route; the ON
write mints an ai_full consent receipt for the audit trail
- the document consent gate short-circuits an external pick when the
toggle is ON; a local pick stays ungated and OFF still requires a
receipt for any external egress
The finding is npm's OWN bundled sigstore (v3.1.0) inside the Node base
image, used for publish provenance at build time only. It is not in our
application dependency tree and is unreachable at runtime — same class as the
already-suppressed picomatch entry. Re-check when the base image ships an npm
with sigstore >= 4.1.1.
The local content-extraction reader imported pdf-parse at module top level.
pdf-parse pulls in pdfjs, whose module evaluation references the browser-only
DOMMatrix global; on the server that reference throws during chunk
instantiation and takes down every route sharing the chunk — the document
list, upload, and search among them, so old and new documents alike fail to
load. Move the parser to a lazy import() inside the extraction function
(surface type kept as a type-only import). Verified under plain Node that the
lazy path still extracts a real PDF text layer, so local indexing is
unaffected.
Three local Playwright screenshot harnesses at the repo root were picked up
by a wide stage during the v1.28.0 assembly. They are never imported by the
app build; remove them and ignore the .audit-*.mjs pattern so a root-level
add can't recatch them.
Marks the document line as one coherent milestone: the vault becomes a
place to understand files, not just store them — read, indexed, searched,
shared, and answered — with a local-first / consented AI posture and a
vendor-blind egress notice held at every step. Ships alongside a full
correctness, resilience, and quality sweep down to the low-priority findings.
fullyParallel spreads a spec's tests across workers and each re-runs the
file's beforeAll; two workers seeding the fixed-id vault corpus at once
raced on the document_condition_links primary key (ON CONFLICT only
suppresses its named arbiter index). Guard both seeders with a Postgres
advisory lock so concurrent workers seed one-at-a-time and every insert
re-runs as a clean idempotent upsert.
Add an e2e suite mocking the capability probe, the chat history GET, and the
streaming chat POST so the panel renders (v1.27.31 lesson): the entry opens an
empty safety-noted surface, a turn streams a reply that settles to one assistant
bubble, a non-indexed document shows the calm hint, and no provider leaves no
chat surface. Clear the streamed tail + optimistic bubble once the persisted
twin lands so the reply never double-renders.
Extract DocumentChatConversation (pure) from the chat panel so the open
conversation surface is statically renderable, and pin: composer + always-on
safety note, plain-text/XSS-safe assistant render (raw HTML escaped), the
streaming tail vs thinking placeholder, mapped error keys, and the container's
indexed vs not-indexed gating. Assert the AI section renders the chat slot only
in the provider-available branch.
Add a scoped 'Chat about this document' panel beneath the Read-with-AI
actions. Opens a conversation grounded only in the document's stored text:
a message list + input streaming the assistant reply token-by-token over
the SSE route, with prior turns loaded via GET.
- Availability: the panel renders in the provider-available branch of the
AI section (a pure chatSlot keeps the section statically renderable), and
gates internally on whether the document is content-indexed. Not indexed
shows a calm read-it-first hint, never an error.
- Plain-text only: assistant prose renders via ProseBlocks / StreamedProse
as React children (no markdown library, no dangerouslySetInnerHTML).
- Streaming reuses the Coach SSE frame parser; the wire frames are a subset.
- A one-line safety note states answers describe the document and are not
medical advice, consistent with the summary panel's not-a-diagnosis.
- Query keys via the central factory; history invalidated on the done frame.
- i18n x6 for every new string.