Merge main into the product branch, and finish the Ask markdown fix - #32

Merged
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product
Aug 17, 2026
Merged

Merge main into the product branch, and finish the Ask markdown fix#32
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

Brings all 10 commits from main onto feat/product-in-the-atmosphere, including every Ask/AI fix, and lands the markdown rendering the bug report was about. Supersedes #31.

What arrives from main

  • b84728c - Ask renders Markdown (markdown.tsx) and ask.ts asks the model for structure. Both halves; neither works alone.
  • c78131c - stops Ask truncating now that answers are longer (max_tokens was cutting a 264-page review's answer at 56s).
  • 84bc19c - opening a prepared case brings its document with it.
  • 3e6bc88, 2ae6907, plus the refusal-panel, archive and header work.

The four conflicts, and why they resolved the way they did

All in files both branches had changed independently.

  • read.tsx / read.test.tsx / App.tsx - both branches had independently fixed the same pdf.js 401 (the bearer token never reached /raw). Your branch's version is a superset: it carries that fix and the citation-attribution feature (citers/blind, who cited which finding after reveal). Kept yours, so nothing is lost either way - main's only unique content there was one redundant token test.
  • One genuine merge: main had made token optional because two of its layout tests render <Read> without one. Your side had it required, which would have failed those tests. Resolved by taking main's optional handling plus your citation feature - the header is omitted rather than sent as Bearer undefined.
  • Backdrop.tsx - both branches made the identical two fixes (register from STATES, guard transitionTo); only the comment wording differed.

Markers were stripped hunk-by-hunk keeping the right side, notcheckout --ours, which would have discarded main's auto-merged changes in those same files.

Two follow-on fixes

  • A real hole in the renderer. If an answer arrives with no newlines, the parser turned the whole thing into one giant <h4> - worse than the wall of text reported. Reconstruction is now anchored only to unambiguous markers: a bare - stays a dash, 1. stays part of a number, and it runs only when the answer has no newline anywhere, so well-formed markdown takes an unchanged path. A test carries the reported answer verbatim.
  • A test timeout that only appeared on the merge. Five seeding tests copy a real PDF and write hash-chain links; the two-opener one takes ~3.5s against a 5s default. It passed on either branch alone and failed on the merge because the suite grew to 962 tests and the files compete for cores. Given an explicit budget on the five tests that seed, rather than raised globally.

Verification

  • 971 tests pass across 66 files; three consecutive full runs green before the markdown commit went on top (962/962 each)
  • tsc --noEmit clean on the deliberation app
  • All three workspaces build
  • Confirmed present after merge: markdown.tsx, the ask.ts markdown prompt, the max_tokens fix, citationsFor/citers, and the committed regulatory documents

🤖 Generated with Claude Code

Darkest-Teddyand others added 18 commits August 16, 2026 01:36
Reading Trails, phase 1: a Read & mark tab, and seats that hold still
Pressing Read & mark went dark and stayed dark until a reload.
Backdrop registered five scenes by hand - dashboard, new, library, ask, record
- while packages/atmosphere publishes seven. When Section arrived and sceneFor
started returning "read" for the reading surface, nothing added it here. So
transitionTo was called with an id the engine had never heard of, which throws
by contract (core/Atmosphere.ts). The throw happened inside an effect, and React
unmounts a tree whose effect threw, so a fault in the DECORATION took the whole
product with it.
The same id on a deep link went through mount() instead, inside the loader's
try, and was swallowed - a working product with no scene behind it. That is why
reloading appeared to fix it, and why it looked intermittent: three different
outcomes from one missing table entry, depending on how you arrived.
Registration now reads STATES, which is what apps/atmosphere has always done and
where a new scene actually gets added. That makes the drift impossible rather
than merely repaired. Registering is a map insert and the factory does not run
until something mounts it, so carrying "landing" here costs nothing.
The transition effect is also guarded now. Fixing the catalogue removed this
throw, not the class of them - a driver refusing a shader mid-session arrives at
the same line - and this file's own header says a background may not take the
product down with it. The loader honoured that and the scene change did not.
The fake engine in the test keeps the real class's contract: an unregistered id
throws. A forgiving one would have passed against the broken code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both surfaces put their prose directly on a live WebGL field. Reading is the
longest stretch of text in the product - a document strip, a findings rail, and
the paragraph explaining why a citation could not be placed - and the position
form is a heading, four labels, three explanations and a basis line. Neither had
a ground under any of it.
.glass is the one surface in this product that carries one, and app.css says why
it exists in those words: a low-alpha wash plus a blur that stays readable over a
lit colony without hiding what is behind it. The evidence stage has used it from
the start and reads cleanly over every scene because of it. These two were the
screens that needed it most and were going without.
Waiting is plated as well as the form. That tab BECOMES Waiting once you seal, so
a ground on only one of them would disappear at the moment you submit.
ONE PLATE PER SCREEN, not one per part - app.css puts the blur on containers
only, because a dozen stacked blur layers buy nothing the parent has not already
bought.
The empty reading state is plated too. It is one paragraph and nothing else,
which makes it the state most exposed to the scene: there is no other object on
the screen for the type to sit on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header is fixed and the work scrolls under it, which is the arrangement
Chrome.tsx argues for: a case table with three hundred rows is a document, and a
product that hijacks the wheel to fly a camera is a product nobody can use. The
cost of that arrangement is that the rows pass behind the navigation. On a long
position form the wordmark and the sign-out button sit across the sentence you
are reading.
So it leaves going down and comes back the moment you turn around. DIRECTION,
NOT POSITION: "hide below 400px" would take the navigation away and keep it away
for the length of the table, and scrolling up is the gesture that means give me
the chrome back.
A 6px floor, because a trackpad does not emit one scroll event per gesture - it
emits a stream of one- and two-pixel moves, some of them in the wrong direction,
and without a floor the header flickers the whole way down. Always showing above
96px, roughly its own height plus its inset, where it is not covering anything
yet and hiding would be motion for no reason.
Inert while it is away rather than merely transparent. This holds the main menu
and the only sign-out control, and a tab stop on something translated off the top
of the viewport sends focus somewhere the reader cannot see. One attribute takes
the subtree out of the tab order and swallows pointer events with it.
Transform and opacity only - the two a compositor animates without laying out
again, and there is a WebGL scene drawing behind this. Reduced motion keeps the
behaviour and drops the transition: a reader who asked for less motion still
wants the navigation off their paragraph.
No rAF throttle. The listener is passive and does two comparisons against a
number the browser already has, scroll events are frame-aligned as it is, and the
state only changes on the turn - so React re-renders when the answer changes, not
when the page moves.
Verified with real wheel input rather than scrollTo, which fires no scroll event
at all under CDP - worth knowing before anyone writes an e2e test for this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects on the one screen where being unable to read the interface costs a
reviewer their answer rather than their patience.
THE CALL BUTTONS WERE NOT BUTTONS. `.rail` and `.persona` were dropped by the
redesign and nothing replaced them, so the three-way call - the single most
important control in the product - rendered as "AdvanceDo not advanceCannot
conclude": three words run together, no gap, no border, no pressed state. Agree
and Override on the verdict screen had the same pair and the same result. Both
now use `.choice` and `button.ghost`, which is what every other form here is
built from and already carries hover and aria-pressed.
THE TEXTAREA HAD VANISHED INTO ITS OWN PANEL. The form rule says a field is
"darker than paper", and it was not: paper carries 0.50 alpha and the field
carried 0.46, so the field covered LESS of what was behind it than the plate did.
Over a scene that was survivable by accident. On a plate the two composite to
within a couple of values of each other and there was no box on the screen at all
- only a slightly different dark where the caret was supposed to go. 0.78 now,
and the rim goes from 0.15 to 0.26, because a field has to be an object somebody
can see the extent of before they click into it.
The prose moves onto the field rhythm the rest of the product uses: labels,
controls and their explanations in `.field` with `.hint`, the two halves of an
outside claim spaced by rule rather than by an inline style no stylesheet could
reach, and the basis line ruled off above the button because it summarises every
answer rather than asking for one more.
A GUARD FOR THE CLASS OF BUG. A className with no rule does not throw, does not
warn, and does not fail a type check - it renders as an unstyled element that
looks like a mistake nobody made. The stylesheet test now reads every plain
className out of the components and fails on any the stylesheet has no rule for.
Run against the old markup it names `rail` and `persona` exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read & mark could never open a PDF. It showed "Failed to fetch", and behind that
was a 401 on /api/cases/:id/documents/:docId/raw.
pdf.js does not go through api.ts. It is handed a URL and issues its OWN request,
so the bearer token this app keeps in memory never reached the one endpoint that
serves a document's bytes - the endpoint every other call authenticates against
without trouble.
It hid for as long as it did because no case had a document on it. The screen
said "No documents on this case yet" and nothing ever reached the fetch, so the
first person to attach a file was always going to be the first person to find
this.
httpHeaders rather than a query parameter: a token in the URL is a token in the
browser history, in the referrer, and in any log the request crosses, and this is
the endpoint that streams unpublished safety data.
Verified against a real 178-page EMA assessment report - the page renders, a
finding in the rail jumps to the page it cites, and the page says what the
finding says it does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A prepared case arrived with findings and no document. The case file carries a
page number for every finding, and the library manifest knows which document
those pages are in, but the two facts never met - so Read & mark said "No
documents on this case yet" on every case anybody opened, and the reader, which
joins a finding to a page THROUGH a document id, had nothing to join to. Making
one usable meant uploading the file by hand and re-adding every finding against
it.
The source now comes with the case. It goes through the same door a person's own
upload does - documents.upload measures before it accepts - so a scanned or
off-topic source is refused here exactly as it would be on the Evidence stage, by
the same code, with the same reason. Nothing about shipping a file with the
product makes it readable.
A FAILURE HERE DOES NOT LOSE THE CASE. The approval packages are not committed,
so a checkout without them is the normal case, not the exception; a missing file
or a refusal leaves the case opening exactly as it did before, which is still a
working case. There is a test for that path specifically.
DEDUPLICATION HAD TO STOP BEING GLOBAL, and this is what proved it. DocumentStore
keyed uploads by content hash across the whole store, which is right inside a case
- re-sending a file you already sent must not become a second document a second
position can cite - and wrong across them. With the review arriving at open time,
the second person to open the same case uploaded identical bytes and got back the
FIRST person's document, scoped to a case they cannot see: forCase found nothing
for them, and their findings named a document that is not on the case they were
filed under. The key is per case now. That is the same argument the caseId suffix
already makes one level up - a prepared case is a starting point, not a shared
room - and the note at the top of documents.ts, which justified the global
version, says so instead.
The attach test injects the manifest rather than reading disk: the approval
packages are deliberately untracked, so a test that needed the real file would
pass on a developer machine and fail in CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
services/api shells out to measure_pdf.py and extract_pdf_text.py for every
document, and both need PyMuPDF. The interpreter comes from PYTHON and otherwise
defaults to whatever `python` is on PATH - which, on a machine whose system
Python has no pip and cannot get one, is an interpreter that will never have
PyMuPDF in it.
The failure was quiet and pointed at the wrong thing. Every upload came back 422
"unreadable - PyMuPDF is not installed", which reads as a bad DOCUMENT rather
than a missing dependency, and two tests in services/api failed the same way on
any machine without a global install. A green suite and a working upload both
depended on knowing to set an environment variable that nothing mentions.
So both entry points now look for a repo-local .venv and use it when nothing has
already named an interpreter. An explicit PYTHON still wins, and a checkout with
no virtualenv behaves exactly as before - CI installs the requirements globally
and is untouched.
dev-all prints the interpreter it chose, because a silently-chosen one is how the
last one hid.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ask returned `truncated: max_tokens too low` on three of four attempts. The route
answered 502 and the composer showed a bare `upstream` - no answer at all, for a
question retrieval had already served the right pages for.
SHAPE_ASK's 16000 was measured, and it was right for the prompt it was measured
against. What changed is the answer: ask.ts now asks for Markdown, and headings,
bullets and blank lines are more tokens of answer for the same question. Thinking
shares that budget, so the pair crossed the ceiling on any question with a broad
answer - "What liver findings are reported, and at what doses?" against the
264-page Turalio review, every time but one.
This is the fifth time on this project that an output ceiling has been too small
once thinking was counted, and the first where the ceiling did not move but the
output did. The number is SHAPE_SUMMARY's for SHAPE_SUMMARY's reason: the answer
is bounded by the prompt, so the only job left for this ceiling is to stop being
the binding constraint on thinking. A cap is not a reservation - nothing is spent
by raising one, only by generating into it - which is why the fix is always to
raise it and never to tune it.
SHAPE_ADJUDICATION stays at 16000. It has its own measurement - zero truncation
across 10 runs on the probe case - and its input is a bounded case rather than
however many pages retrieval returned.
Measured after: six of six on the question that failed three of four, and the
same question answered in the UI with its headings and citations intact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The library's bodies had a floor in the code and none in the frame: an
additive plane of light bands with no depth write, which over a near-black
ground is very nearly nothing. A rank of standing volumes read as a rank of
floating ones.
It is a heightfield now, ported from the landing page's own library frame
and for the same reason that one has it - an object glowing over nothing is
a shape, and the same object with ground catching its light is a place. Each
case throws a pool on the ground it stands on, azure if it is usable and a
smaller red one if it was refused, so the refusals are legible on the floor
as well as in the object.
Three things the port had to learn on the way across, all in terrain.ts:
emitters carry a colour, because a refused body is the one red thing in this
palette; eight slots rather than four, because the library holds six; and a
dissolve, because the camera flies inside a body and the world outside has
to stop existing. The old floor's sweeping bands moved into the same shader,
so the light now runs over the landform instead of through it.
The bodies stand ON the surface rather than sunk into it, and that is forced
rather than chosen: Atmosphere builds rtC with depthBuffer false and renders
into it whenever a transition is not running, so outside a scene swap nothing
here is depth tested and a buried foot would simply be drawn. Same reason the
ground draws first. Noted in the file, since the scene's own header claims a
depth test it does not get.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two refused entries opened their one line with "REFUSED - " in capitals,
two rows under a card already drawing a red REFUSED badge off the same
usable flag. The badge said twice, and it was the only shouted lead-in on a
page where the other four entries open on a sentence.
What is left does the job every other shape does - what the document is,
then what that costs the reader - so the six read as one set again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
h1-h4 and p are all margin 0 in app.css: this app takes its vertical rhythm
from a container, never from the type. Refused rendered a bare <section> and
is mounted straight into the shell, so it had no container at all - the
title, the red refusal box and all six headings and paragraphs stacked with
literally no space between them, the box wedged against the heading above
and the one below.
.section restores the 16px between blocks. Each heading is paired with its
body in a .stack-s as well, because .section alone spaces every child
equally and that leaves an h3 floating exactly as far from its own paragraph
as from the block before it - the panel reads as three groups now rather
than six loose lines. No new CSS; both classes already existed.
Five more bare sections in this file have the same gap, and the inline
marginTop hacks at 664 and 722 are what standing in for it looks like.
Left for their own change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Developer API's response_schema proto does not carry
additionalProperties, so sending it rejects the REQUEST outright - 400,
Unknown name "additionalProperties" at generation_config.response_schema -
before a model is ever selected. Every AI surface writes that key, so
adjudicate, ask, interpret, extract and navigate all died together on that
host, and handleAdjudicate reporting every upstream fault as a bare 502
made a request that never reached a model look like a model that failed.
Stripped in transport rather than in the schema builders. The constraint is
real where it is honoured: Vertex enforces it, it is what stops a model
returning a field nobody asked for, and the committed numbers were measured
with it in force. Loosening the shared schemas to suit the weaker host would
weaken the stronger one to buy nothing, so the schemas stay strict and the
transport adapts.
Known red: services/api/test/server.test.ts "runs the rest of the flow for
the owner" fails on adjudicate returning 502. It reproduces on origin/main
without this change, so it is not introduced here, but it is landing red and
is worth chasing next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	apps/deliberation/src/App.tsx
#	apps/deliberation/src/read.tsx
#	apps/deliberation/src/shell/Backdrop.tsx
#	apps/deliberation/test/read.test.tsx
A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a714b01)
`POST /api/demo` opens a real library case: it copies the regulatory review off disk,
transcribes its findings and writes the opening links of the hash chain. One of those
runs about 1.4s alone, and "gives each opener their own copy" does the whole thing
TWICE - 3.5s before the machine is doing anything else, against a 5s default.
It passed on either side and failed on the merge of the two, which is the tell: the
suite grew from 881 and 948 tests to 962, the files compete for the same cores, and a
test sitting 1.5s under the limit went 2s over it. Nothing on the path changed - the
failure moved with the SIZE OF THE RUN, which is a limit set too close rather than a
slow path worth chasing.
Stated on the five tests that seed, rather than raised globally: a timeout is a claim
about what a test does, and only these five copy a PDF. Three consecutive full runs
green at 962, then 971 with the markdown tests on top.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7cdf48e4-510d-489b-8a7b-884a5c1cb888

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

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

@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Updated: merged origin/main again to pick up 347c87f (strip additionalProperties for the Developer API host) — the conflict in gemini.ts was two independent additions at the same spot, resolved as a union so both geminiCredentialAdvice and responseSchemaFor survive. 976 tests across 67 files, three builds, CI green.

The merge resolution deleted the ======= and >>>>>>> lines but left the
opening <<<<<<< HEAD. It sat inside the JSDoc for geminiCredentialAdvice,
so it was legal comment text - which is why typecheck, lint and CI all
stayed green on it.
@AndresL230
AndresL230 merged commit f0ff440 into feat/product-in-the-atmosphereAug 17, 2026
3 checks passed
AndresL230 added a commit that referenced this pull request Aug 17, 2026
PR #32 reconciled main into this branch, but its last main merge predated
dd140aa by 26 minutes, so the two newest commits on main never rode along:
dd140aa Serve the verdict from the record, not from the tab that ran it
53e9a5b Give the four remaining bare sections the rhythm the rest of the app has
Without dd140aa the product branch kept the pre-fix adjudication path: the
verdict lived only in the browser of whoever pressed Adjudicate, so Reveal &
verdict was empty for every signed case, every refresh, and every non-owner -
and the Adjudicate button still offered itself on cases that could only answer
409, spending three model calls to find that out. Nine regression tests come
with the fix.
One conflict, in the services/api/server.ts import block, resolved as the union
of both sides: this branch's geminiCredentialAdvice and main's CallKind/Complete
- the seam that keeps server.test.ts from making live paid adjudication calls.
Both symbols are used; dropping either breaks the build.
Verified: typecheck clean, lint clean, 985/985 tests passing across 67 files.
AndresL230 added a commit that referenced this pull request Aug 17, 2026
main moved 42 commits under this branch - #33 (Postgres/Supabase), #24, #29,
#32 and the logo - and rewrote the three files this touches most.
THE RECONCILIATION #33 DEFERRED. `ShareStore` was written against the pattern
this branch had: a synchronous `new ShareStore(path)` beside `AuthStore` and
`InviteStore`. That layer is gone. So the store joins the new one the way the
other four did:
- `ShareStoreApi` in postgres-share.ts, where `AuthStoreApi` and
`InviteStoreApi` live, for the reason stated there: the second
implementation is what needs a name for the shape.
- `ShareStore.open(path)`, async, private constructor.
- `PostgresShareStore`, and supabase/migrations/0002_share_links.sql.
- Both branches of `buildStores`, and `Stores.shares`.
- The behaviour is ONE suite over both implementations
(test/share-store-contract.ts), as with auth and invites.
`shareSecret` deliberately stayed OUT of `buildStores`: which backing holds the
links is a storage decision, whether the deployment can publish at all is not.
THE TABLE HOLDS NO TOKEN, and the migration says so at length because that is
the feature's whole security argument - the URL is
HMAC(secret, "caseId:version"), derivable from two columns that are not secret,
so a stolen dump yields nothing. `publish` omits `version` from its upsert's
SET list on purpose: assigning it would reset a revoked case to 1 and re-mint
the token revoke had just killed. A test reads the column list back out of
information_schema, because that property fails silently.
WHAT THE CONFLICTS COST. server.ts kept main's static serving whole and its
`/api/health` route; the branch's "NO STATIC-FILE SERVING HERE" comment was
true when written and is now a lie, so it is replaced by an accurate one:
`/r/:caseId/:token` is still not served in production, a scanned QR 404s on a
deployed host, and the two decisions that has to wait on are written down
beside `staticRoot()`. Adding it here was out of scope and stayed out.
`handleReport` and `handleShare` became async; `handleReport` fetches the people
in one `auth.list()` rather than a lookup per name, because `auth.get` is
asynchronous now and `buildCaseReport` takes a synchronous `person`.
ONE THING THE MERGE HAD TO DECIDE. docs/HANDOFF-open-prs.md predicted that #30
and main each carried a provenance rule and that they would auto-merge without
conflict. They did, and both were live: `view`'s read any unrecognised actor as
`live`, the report's read only "model" as `live`. Two surfaces describing one
signed safety adjudication, disagreeing about whether a model produced it.
Closed with one module-level `sourceOf`, failing toward `stub`; every writer
passes "stub" or "model" so nothing observable changed, and the test pins that
both readers agree on a third actor neither was written for.
The postgres test fixture now applies every migration in filename order. Pinned
to 0001, it built a database that was correct on the day it was written and
silently lacked share_links.
Verified: lint, typecheck, 1267 tests with DATABASE_URL set (1179 without),
both builds, harness + metrics + golden + an unchanged verdict-manifest, e2e.
The public bundle still carries no auth code: only main's own entry chunk
matches AUTO_PASSWORD or /api/auth/login, and public.html references neither.
HANDOFF-pr34.md is deleted - the job it describes is this commit, and what
happened is recorded in docs/HANDOFF-open-prs.md and the spec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Merge main into the product branch, and finish the Ask markdown fix - #32

Merged
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product
Aug 17, 2026
Merged

Merge main into the product branch, and finish the Ask markdown fix#32
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

Brings all 10 commits from main onto feat/product-in-the-atmosphere, including every Ask/AI fix, and lands the markdown rendering the bug report was about. Supersedes #31.

What arrives from main

  • b84728c - Ask renders Markdown (markdown.tsx) and ask.ts asks the model for structure. Both halves; neither works alone.
  • c78131c - stops Ask truncating now that answers are longer (max_tokens was cutting a 264-page review's answer at 56s).
  • 84bc19c - opening a prepared case brings its document with it.
  • 3e6bc88, 2ae6907, plus the refusal-panel, archive and header work.

The four conflicts, and why they resolved the way they did

All in files both branches had changed independently.

  • read.tsx / read.test.tsx / App.tsx - both branches had independently fixed the same pdf.js 401 (the bearer token never reached /raw). Your branch's version is a superset: it carries that fix and the citation-attribution feature (citers/blind, who cited which finding after reveal). Kept yours, so nothing is lost either way - main's only unique content there was one redundant token test.
  • One genuine merge: main had made token optional because two of its layout tests render <Read> without one. Your side had it required, which would have failed those tests. Resolved by taking main's optional handling plus your citation feature - the header is omitted rather than sent as Bearer undefined.
  • Backdrop.tsx - both branches made the identical two fixes (register from STATES, guard transitionTo); only the comment wording differed.

Markers were stripped hunk-by-hunk keeping the right side, notcheckout --ours, which would have discarded main's auto-merged changes in those same files.

Two follow-on fixes

  • A real hole in the renderer. If an answer arrives with no newlines, the parser turned the whole thing into one giant <h4> - worse than the wall of text reported. Reconstruction is now anchored only to unambiguous markers: a bare - stays a dash, 1. stays part of a number, and it runs only when the answer has no newline anywhere, so well-formed markdown takes an unchanged path. A test carries the reported answer verbatim.
  • A test timeout that only appeared on the merge. Five seeding tests copy a real PDF and write hash-chain links; the two-opener one takes ~3.5s against a 5s default. It passed on either branch alone and failed on the merge because the suite grew to 962 tests and the files compete for cores. Given an explicit budget on the five tests that seed, rather than raised globally.

Verification

  • 971 tests pass across 66 files; three consecutive full runs green before the markdown commit went on top (962/962 each)
  • tsc --noEmit clean on the deliberation app
  • All three workspaces build
  • Confirmed present after merge: markdown.tsx, the ask.ts markdown prompt, the max_tokens fix, citationsFor/citers, and the committed regulatory documents

🤖 Generated with Claude Code

Darkest-Teddyand others added 18 commits August 16, 2026 01:36
Reading Trails, phase 1: a Read & mark tab, and seats that hold still
Pressing Read & mark went dark and stayed dark until a reload.
Backdrop registered five scenes by hand - dashboard, new, library, ask, record
- while packages/atmosphere publishes seven. When Section arrived and sceneFor
started returning "read" for the reading surface, nothing added it here. So
transitionTo was called with an id the engine had never heard of, which throws
by contract (core/Atmosphere.ts). The throw happened inside an effect, and React
unmounts a tree whose effect threw, so a fault in the DECORATION took the whole
product with it.
The same id on a deep link went through mount() instead, inside the loader's
try, and was swallowed - a working product with no scene behind it. That is why
reloading appeared to fix it, and why it looked intermittent: three different
outcomes from one missing table entry, depending on how you arrived.
Registration now reads STATES, which is what apps/atmosphere has always done and
where a new scene actually gets added. That makes the drift impossible rather
than merely repaired. Registering is a map insert and the factory does not run
until something mounts it, so carrying "landing" here costs nothing.
The transition effect is also guarded now. Fixing the catalogue removed this
throw, not the class of them - a driver refusing a shader mid-session arrives at
the same line - and this file's own header says a background may not take the
product down with it. The loader honoured that and the scene change did not.
The fake engine in the test keeps the real class's contract: an unregistered id
throws. A forgiving one would have passed against the broken code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both surfaces put their prose directly on a live WebGL field. Reading is the
longest stretch of text in the product - a document strip, a findings rail, and
the paragraph explaining why a citation could not be placed - and the position
form is a heading, four labels, three explanations and a basis line. Neither had
a ground under any of it.
.glass is the one surface in this product that carries one, and app.css says why
it exists in those words: a low-alpha wash plus a blur that stays readable over a
lit colony without hiding what is behind it. The evidence stage has used it from
the start and reads cleanly over every scene because of it. These two were the
screens that needed it most and were going without.
Waiting is plated as well as the form. That tab BECOMES Waiting once you seal, so
a ground on only one of them would disappear at the moment you submit.
ONE PLATE PER SCREEN, not one per part - app.css puts the blur on containers
only, because a dozen stacked blur layers buy nothing the parent has not already
bought.
The empty reading state is plated too. It is one paragraph and nothing else,
which makes it the state most exposed to the scene: there is no other object on
the screen for the type to sit on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header is fixed and the work scrolls under it, which is the arrangement
Chrome.tsx argues for: a case table with three hundred rows is a document, and a
product that hijacks the wheel to fly a camera is a product nobody can use. The
cost of that arrangement is that the rows pass behind the navigation. On a long
position form the wordmark and the sign-out button sit across the sentence you
are reading.
So it leaves going down and comes back the moment you turn around. DIRECTION,
NOT POSITION: "hide below 400px" would take the navigation away and keep it away
for the length of the table, and scrolling up is the gesture that means give me
the chrome back.
A 6px floor, because a trackpad does not emit one scroll event per gesture - it
emits a stream of one- and two-pixel moves, some of them in the wrong direction,
and without a floor the header flickers the whole way down. Always showing above
96px, roughly its own height plus its inset, where it is not covering anything
yet and hiding would be motion for no reason.
Inert while it is away rather than merely transparent. This holds the main menu
and the only sign-out control, and a tab stop on something translated off the top
of the viewport sends focus somewhere the reader cannot see. One attribute takes
the subtree out of the tab order and swallows pointer events with it.
Transform and opacity only - the two a compositor animates without laying out
again, and there is a WebGL scene drawing behind this. Reduced motion keeps the
behaviour and drops the transition: a reader who asked for less motion still
wants the navigation off their paragraph.
No rAF throttle. The listener is passive and does two comparisons against a
number the browser already has, scroll events are frame-aligned as it is, and the
state only changes on the turn - so React re-renders when the answer changes, not
when the page moves.
Verified with real wheel input rather than scrollTo, which fires no scroll event
at all under CDP - worth knowing before anyone writes an e2e test for this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects on the one screen where being unable to read the interface costs a
reviewer their answer rather than their patience.
THE CALL BUTTONS WERE NOT BUTTONS. `.rail` and `.persona` were dropped by the
redesign and nothing replaced them, so the three-way call - the single most
important control in the product - rendered as "AdvanceDo not advanceCannot
conclude": three words run together, no gap, no border, no pressed state. Agree
and Override on the verdict screen had the same pair and the same result. Both
now use `.choice` and `button.ghost`, which is what every other form here is
built from and already carries hover and aria-pressed.
THE TEXTAREA HAD VANISHED INTO ITS OWN PANEL. The form rule says a field is
"darker than paper", and it was not: paper carries 0.50 alpha and the field
carried 0.46, so the field covered LESS of what was behind it than the plate did.
Over a scene that was survivable by accident. On a plate the two composite to
within a couple of values of each other and there was no box on the screen at all
- only a slightly different dark where the caret was supposed to go. 0.78 now,
and the rim goes from 0.15 to 0.26, because a field has to be an object somebody
can see the extent of before they click into it.
The prose moves onto the field rhythm the rest of the product uses: labels,
controls and their explanations in `.field` with `.hint`, the two halves of an
outside claim spaced by rule rather than by an inline style no stylesheet could
reach, and the basis line ruled off above the button because it summarises every
answer rather than asking for one more.
A GUARD FOR THE CLASS OF BUG. A className with no rule does not throw, does not
warn, and does not fail a type check - it renders as an unstyled element that
looks like a mistake nobody made. The stylesheet test now reads every plain
className out of the components and fails on any the stylesheet has no rule for.
Run against the old markup it names `rail` and `persona` exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read & mark could never open a PDF. It showed "Failed to fetch", and behind that
was a 401 on /api/cases/:id/documents/:docId/raw.
pdf.js does not go through api.ts. It is handed a URL and issues its OWN request,
so the bearer token this app keeps in memory never reached the one endpoint that
serves a document's bytes - the endpoint every other call authenticates against
without trouble.
It hid for as long as it did because no case had a document on it. The screen
said "No documents on this case yet" and nothing ever reached the fetch, so the
first person to attach a file was always going to be the first person to find
this.
httpHeaders rather than a query parameter: a token in the URL is a token in the
browser history, in the referrer, and in any log the request crosses, and this is
the endpoint that streams unpublished safety data.
Verified against a real 178-page EMA assessment report - the page renders, a
finding in the rail jumps to the page it cites, and the page says what the
finding says it does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A prepared case arrived with findings and no document. The case file carries a
page number for every finding, and the library manifest knows which document
those pages are in, but the two facts never met - so Read & mark said "No
documents on this case yet" on every case anybody opened, and the reader, which
joins a finding to a page THROUGH a document id, had nothing to join to. Making
one usable meant uploading the file by hand and re-adding every finding against
it.
The source now comes with the case. It goes through the same door a person's own
upload does - documents.upload measures before it accepts - so a scanned or
off-topic source is refused here exactly as it would be on the Evidence stage, by
the same code, with the same reason. Nothing about shipping a file with the
product makes it readable.
A FAILURE HERE DOES NOT LOSE THE CASE. The approval packages are not committed,
so a checkout without them is the normal case, not the exception; a missing file
or a refusal leaves the case opening exactly as it did before, which is still a
working case. There is a test for that path specifically.
DEDUPLICATION HAD TO STOP BEING GLOBAL, and this is what proved it. DocumentStore
keyed uploads by content hash across the whole store, which is right inside a case
- re-sending a file you already sent must not become a second document a second
position can cite - and wrong across them. With the review arriving at open time,
the second person to open the same case uploaded identical bytes and got back the
FIRST person's document, scoped to a case they cannot see: forCase found nothing
for them, and their findings named a document that is not on the case they were
filed under. The key is per case now. That is the same argument the caseId suffix
already makes one level up - a prepared case is a starting point, not a shared
room - and the note at the top of documents.ts, which justified the global
version, says so instead.
The attach test injects the manifest rather than reading disk: the approval
packages are deliberately untracked, so a test that needed the real file would
pass on a developer machine and fail in CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
services/api shells out to measure_pdf.py and extract_pdf_text.py for every
document, and both need PyMuPDF. The interpreter comes from PYTHON and otherwise
defaults to whatever `python` is on PATH - which, on a machine whose system
Python has no pip and cannot get one, is an interpreter that will never have
PyMuPDF in it.
The failure was quiet and pointed at the wrong thing. Every upload came back 422
"unreadable - PyMuPDF is not installed", which reads as a bad DOCUMENT rather
than a missing dependency, and two tests in services/api failed the same way on
any machine without a global install. A green suite and a working upload both
depended on knowing to set an environment variable that nothing mentions.
So both entry points now look for a repo-local .venv and use it when nothing has
already named an interpreter. An explicit PYTHON still wins, and a checkout with
no virtualenv behaves exactly as before - CI installs the requirements globally
and is untouched.
dev-all prints the interpreter it chose, because a silently-chosen one is how the
last one hid.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ask returned `truncated: max_tokens too low` on three of four attempts. The route
answered 502 and the composer showed a bare `upstream` - no answer at all, for a
question retrieval had already served the right pages for.
SHAPE_ASK's 16000 was measured, and it was right for the prompt it was measured
against. What changed is the answer: ask.ts now asks for Markdown, and headings,
bullets and blank lines are more tokens of answer for the same question. Thinking
shares that budget, so the pair crossed the ceiling on any question with a broad
answer - "What liver findings are reported, and at what doses?" against the
264-page Turalio review, every time but one.
This is the fifth time on this project that an output ceiling has been too small
once thinking was counted, and the first where the ceiling did not move but the
output did. The number is SHAPE_SUMMARY's for SHAPE_SUMMARY's reason: the answer
is bounded by the prompt, so the only job left for this ceiling is to stop being
the binding constraint on thinking. A cap is not a reservation - nothing is spent
by raising one, only by generating into it - which is why the fix is always to
raise it and never to tune it.
SHAPE_ADJUDICATION stays at 16000. It has its own measurement - zero truncation
across 10 runs on the probe case - and its input is a bounded case rather than
however many pages retrieval returned.
Measured after: six of six on the question that failed three of four, and the
same question answered in the UI with its headings and citations intact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The library's bodies had a floor in the code and none in the frame: an
additive plane of light bands with no depth write, which over a near-black
ground is very nearly nothing. A rank of standing volumes read as a rank of
floating ones.
It is a heightfield now, ported from the landing page's own library frame
and for the same reason that one has it - an object glowing over nothing is
a shape, and the same object with ground catching its light is a place. Each
case throws a pool on the ground it stands on, azure if it is usable and a
smaller red one if it was refused, so the refusals are legible on the floor
as well as in the object.
Three things the port had to learn on the way across, all in terrain.ts:
emitters carry a colour, because a refused body is the one red thing in this
palette; eight slots rather than four, because the library holds six; and a
dissolve, because the camera flies inside a body and the world outside has
to stop existing. The old floor's sweeping bands moved into the same shader,
so the light now runs over the landform instead of through it.
The bodies stand ON the surface rather than sunk into it, and that is forced
rather than chosen: Atmosphere builds rtC with depthBuffer false and renders
into it whenever a transition is not running, so outside a scene swap nothing
here is depth tested and a buried foot would simply be drawn. Same reason the
ground draws first. Noted in the file, since the scene's own header claims a
depth test it does not get.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two refused entries opened their one line with "REFUSED - " in capitals,
two rows under a card already drawing a red REFUSED badge off the same
usable flag. The badge said twice, and it was the only shouted lead-in on a
page where the other four entries open on a sentence.
What is left does the job every other shape does - what the document is,
then what that costs the reader - so the six read as one set again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
h1-h4 and p are all margin 0 in app.css: this app takes its vertical rhythm
from a container, never from the type. Refused rendered a bare <section> and
is mounted straight into the shell, so it had no container at all - the
title, the red refusal box and all six headings and paragraphs stacked with
literally no space between them, the box wedged against the heading above
and the one below.
.section restores the 16px between blocks. Each heading is paired with its
body in a .stack-s as well, because .section alone spaces every child
equally and that leaves an h3 floating exactly as far from its own paragraph
as from the block before it - the panel reads as three groups now rather
than six loose lines. No new CSS; both classes already existed.
Five more bare sections in this file have the same gap, and the inline
marginTop hacks at 664 and 722 are what standing in for it looks like.
Left for their own change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Developer API's response_schema proto does not carry
additionalProperties, so sending it rejects the REQUEST outright - 400,
Unknown name "additionalProperties" at generation_config.response_schema -
before a model is ever selected. Every AI surface writes that key, so
adjudicate, ask, interpret, extract and navigate all died together on that
host, and handleAdjudicate reporting every upstream fault as a bare 502
made a request that never reached a model look like a model that failed.
Stripped in transport rather than in the schema builders. The constraint is
real where it is honoured: Vertex enforces it, it is what stops a model
returning a field nobody asked for, and the committed numbers were measured
with it in force. Loosening the shared schemas to suit the weaker host would
weaken the stronger one to buy nothing, so the schemas stay strict and the
transport adapts.
Known red: services/api/test/server.test.ts "runs the rest of the flow for
the owner" fails on adjudicate returning 502. It reproduces on origin/main
without this change, so it is not introduced here, but it is landing red and
is worth chasing next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	apps/deliberation/src/App.tsx
#	apps/deliberation/src/read.tsx
#	apps/deliberation/src/shell/Backdrop.tsx
#	apps/deliberation/test/read.test.tsx
A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a714b01)
`POST /api/demo` opens a real library case: it copies the regulatory review off disk,
transcribes its findings and writes the opening links of the hash chain. One of those
runs about 1.4s alone, and "gives each opener their own copy" does the whole thing
TWICE - 3.5s before the machine is doing anything else, against a 5s default.
It passed on either side and failed on the merge of the two, which is the tell: the
suite grew from 881 and 948 tests to 962, the files compete for the same cores, and a
test sitting 1.5s under the limit went 2s over it. Nothing on the path changed - the
failure moved with the SIZE OF THE RUN, which is a limit set too close rather than a
slow path worth chasing.
Stated on the five tests that seed, rather than raised globally: a timeout is a claim
about what a test does, and only these five copy a PDF. Three consecutive full runs
green at 962, then 971 with the markdown tests on top.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7cdf48e4-510d-489b-8a7b-884a5c1cb888

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

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

@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Updated: merged origin/main again to pick up 347c87f (strip additionalProperties for the Developer API host) — the conflict in gemini.ts was two independent additions at the same spot, resolved as a union so both geminiCredentialAdvice and responseSchemaFor survive. 976 tests across 67 files, three builds, CI green.

The merge resolution deleted the ======= and >>>>>>> lines but left the
opening <<<<<<< HEAD. It sat inside the JSDoc for geminiCredentialAdvice,
so it was legal comment text - which is why typecheck, lint and CI all
stayed green on it.
@AndresL230
AndresL230 merged commit f0ff440 into feat/product-in-the-atmosphereAug 17, 2026
3 checks passed
AndresL230 added a commit that referenced this pull request Aug 17, 2026
PR #32 reconciled main into this branch, but its last main merge predated
dd140aa by 26 minutes, so the two newest commits on main never rode along:
dd140aa Serve the verdict from the record, not from the tab that ran it
53e9a5b Give the four remaining bare sections the rhythm the rest of the app has
Without dd140aa the product branch kept the pre-fix adjudication path: the
verdict lived only in the browser of whoever pressed Adjudicate, so Reveal &
verdict was empty for every signed case, every refresh, and every non-owner -
and the Adjudicate button still offered itself on cases that could only answer
409, spending three model calls to find that out. Nine regression tests come
with the fix.
One conflict, in the services/api/server.ts import block, resolved as the union
of both sides: this branch's geminiCredentialAdvice and main's CallKind/Complete
- the seam that keeps server.test.ts from making live paid adjudication calls.
Both symbols are used; dropping either breaks the build.
Verified: typecheck clean, lint clean, 985/985 tests passing across 67 files.
AndresL230 added a commit that referenced this pull request Aug 17, 2026
main moved 42 commits under this branch - #33 (Postgres/Supabase), #24, #29,
#32 and the logo - and rewrote the three files this touches most.
THE RECONCILIATION #33 DEFERRED. `ShareStore` was written against the pattern
this branch had: a synchronous `new ShareStore(path)` beside `AuthStore` and
`InviteStore`. That layer is gone. So the store joins the new one the way the
other four did:
- `ShareStoreApi` in postgres-share.ts, where `AuthStoreApi` and
`InviteStoreApi` live, for the reason stated there: the second
implementation is what needs a name for the shape.
- `ShareStore.open(path)`, async, private constructor.
- `PostgresShareStore`, and supabase/migrations/0002_share_links.sql.
- Both branches of `buildStores`, and `Stores.shares`.
- The behaviour is ONE suite over both implementations
(test/share-store-contract.ts), as with auth and invites.
`shareSecret` deliberately stayed OUT of `buildStores`: which backing holds the
links is a storage decision, whether the deployment can publish at all is not.
THE TABLE HOLDS NO TOKEN, and the migration says so at length because that is
the feature's whole security argument - the URL is
HMAC(secret, "caseId:version"), derivable from two columns that are not secret,
so a stolen dump yields nothing. `publish` omits `version` from its upsert's
SET list on purpose: assigning it would reset a revoked case to 1 and re-mint
the token revoke had just killed. A test reads the column list back out of
information_schema, because that property fails silently.
WHAT THE CONFLICTS COST. server.ts kept main's static serving whole and its
`/api/health` route; the branch's "NO STATIC-FILE SERVING HERE" comment was
true when written and is now a lie, so it is replaced by an accurate one:
`/r/:caseId/:token` is still not served in production, a scanned QR 404s on a
deployed host, and the two decisions that has to wait on are written down
beside `staticRoot()`. Adding it here was out of scope and stayed out.
`handleReport` and `handleShare` became async; `handleReport` fetches the people
in one `auth.list()` rather than a lookup per name, because `auth.get` is
asynchronous now and `buildCaseReport` takes a synchronous `person`.
ONE THING THE MERGE HAD TO DECIDE. docs/HANDOFF-open-prs.md predicted that #30
and main each carried a provenance rule and that they would auto-merge without
conflict. They did, and both were live: `view`'s read any unrecognised actor as
`live`, the report's read only "model" as `live`. Two surfaces describing one
signed safety adjudication, disagreeing about whether a model produced it.
Closed with one module-level `sourceOf`, failing toward `stub`; every writer
passes "stub" or "model" so nothing observable changed, and the test pins that
both readers agree on a third actor neither was written for.
The postgres test fixture now applies every migration in filename order. Pinned
to 0001, it built a database that was correct on the day it was written and
silently lacked share_links.
Verified: lint, typecheck, 1267 tests with DATABASE_URL set (1179 without),
both builds, harness + metrics + golden + an unchanged verdict-manifest, e2e.
The public bundle still carries no auth code: only main's own entry chunk
matches AUTO_PASSWORD or /api/auth/login, and public.html references neither.
HANDOFF-pr34.md is deleted - the job it describes is this commit, and what
happened is recorded in docs/HANDOFF-open-prs.md and the spec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Merge main into the product branch, and finish the Ask markdown fix - #32

Merged
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product
Aug 17, 2026
Merged

Merge main into the product branch, and finish the Ask markdown fix#32
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

Brings all 10 commits from main onto feat/product-in-the-atmosphere, including every Ask/AI fix, and lands the markdown rendering the bug report was about. Supersedes #31.

What arrives from main

  • b84728c - Ask renders Markdown (markdown.tsx) and ask.ts asks the model for structure. Both halves; neither works alone.
  • c78131c - stops Ask truncating now that answers are longer (max_tokens was cutting a 264-page review's answer at 56s).
  • 84bc19c - opening a prepared case brings its document with it.
  • 3e6bc88, 2ae6907, plus the refusal-panel, archive and header work.

The four conflicts, and why they resolved the way they did

All in files both branches had changed independently.

  • read.tsx / read.test.tsx / App.tsx - both branches had independently fixed the same pdf.js 401 (the bearer token never reached /raw). Your branch's version is a superset: it carries that fix and the citation-attribution feature (citers/blind, who cited which finding after reveal). Kept yours, so nothing is lost either way - main's only unique content there was one redundant token test.
  • One genuine merge: main had made token optional because two of its layout tests render <Read> without one. Your side had it required, which would have failed those tests. Resolved by taking main's optional handling plus your citation feature - the header is omitted rather than sent as Bearer undefined.
  • Backdrop.tsx - both branches made the identical two fixes (register from STATES, guard transitionTo); only the comment wording differed.

Markers were stripped hunk-by-hunk keeping the right side, notcheckout --ours, which would have discarded main's auto-merged changes in those same files.

Two follow-on fixes

  • A real hole in the renderer. If an answer arrives with no newlines, the parser turned the whole thing into one giant <h4> - worse than the wall of text reported. Reconstruction is now anchored only to unambiguous markers: a bare - stays a dash, 1. stays part of a number, and it runs only when the answer has no newline anywhere, so well-formed markdown takes an unchanged path. A test carries the reported answer verbatim.
  • A test timeout that only appeared on the merge. Five seeding tests copy a real PDF and write hash-chain links; the two-opener one takes ~3.5s against a 5s default. It passed on either branch alone and failed on the merge because the suite grew to 962 tests and the files compete for cores. Given an explicit budget on the five tests that seed, rather than raised globally.

Verification

  • 971 tests pass across 66 files; three consecutive full runs green before the markdown commit went on top (962/962 each)
  • tsc --noEmit clean on the deliberation app
  • All three workspaces build
  • Confirmed present after merge: markdown.tsx, the ask.ts markdown prompt, the max_tokens fix, citationsFor/citers, and the committed regulatory documents

🤖 Generated with Claude Code

Darkest-Teddyand others added 18 commits August 16, 2026 01:36
Reading Trails, phase 1: a Read & mark tab, and seats that hold still
Pressing Read & mark went dark and stayed dark until a reload.
Backdrop registered five scenes by hand - dashboard, new, library, ask, record
- while packages/atmosphere publishes seven. When Section arrived and sceneFor
started returning "read" for the reading surface, nothing added it here. So
transitionTo was called with an id the engine had never heard of, which throws
by contract (core/Atmosphere.ts). The throw happened inside an effect, and React
unmounts a tree whose effect threw, so a fault in the DECORATION took the whole
product with it.
The same id on a deep link went through mount() instead, inside the loader's
try, and was swallowed - a working product with no scene behind it. That is why
reloading appeared to fix it, and why it looked intermittent: three different
outcomes from one missing table entry, depending on how you arrived.
Registration now reads STATES, which is what apps/atmosphere has always done and
where a new scene actually gets added. That makes the drift impossible rather
than merely repaired. Registering is a map insert and the factory does not run
until something mounts it, so carrying "landing" here costs nothing.
The transition effect is also guarded now. Fixing the catalogue removed this
throw, not the class of them - a driver refusing a shader mid-session arrives at
the same line - and this file's own header says a background may not take the
product down with it. The loader honoured that and the scene change did not.
The fake engine in the test keeps the real class's contract: an unregistered id
throws. A forgiving one would have passed against the broken code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both surfaces put their prose directly on a live WebGL field. Reading is the
longest stretch of text in the product - a document strip, a findings rail, and
the paragraph explaining why a citation could not be placed - and the position
form is a heading, four labels, three explanations and a basis line. Neither had
a ground under any of it.
.glass is the one surface in this product that carries one, and app.css says why
it exists in those words: a low-alpha wash plus a blur that stays readable over a
lit colony without hiding what is behind it. The evidence stage has used it from
the start and reads cleanly over every scene because of it. These two were the
screens that needed it most and were going without.
Waiting is plated as well as the form. That tab BECOMES Waiting once you seal, so
a ground on only one of them would disappear at the moment you submit.
ONE PLATE PER SCREEN, not one per part - app.css puts the blur on containers
only, because a dozen stacked blur layers buy nothing the parent has not already
bought.
The empty reading state is plated too. It is one paragraph and nothing else,
which makes it the state most exposed to the scene: there is no other object on
the screen for the type to sit on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header is fixed and the work scrolls under it, which is the arrangement
Chrome.tsx argues for: a case table with three hundred rows is a document, and a
product that hijacks the wheel to fly a camera is a product nobody can use. The
cost of that arrangement is that the rows pass behind the navigation. On a long
position form the wordmark and the sign-out button sit across the sentence you
are reading.
So it leaves going down and comes back the moment you turn around. DIRECTION,
NOT POSITION: "hide below 400px" would take the navigation away and keep it away
for the length of the table, and scrolling up is the gesture that means give me
the chrome back.
A 6px floor, because a trackpad does not emit one scroll event per gesture - it
emits a stream of one- and two-pixel moves, some of them in the wrong direction,
and without a floor the header flickers the whole way down. Always showing above
96px, roughly its own height plus its inset, where it is not covering anything
yet and hiding would be motion for no reason.
Inert while it is away rather than merely transparent. This holds the main menu
and the only sign-out control, and a tab stop on something translated off the top
of the viewport sends focus somewhere the reader cannot see. One attribute takes
the subtree out of the tab order and swallows pointer events with it.
Transform and opacity only - the two a compositor animates without laying out
again, and there is a WebGL scene drawing behind this. Reduced motion keeps the
behaviour and drops the transition: a reader who asked for less motion still
wants the navigation off their paragraph.
No rAF throttle. The listener is passive and does two comparisons against a
number the browser already has, scroll events are frame-aligned as it is, and the
state only changes on the turn - so React re-renders when the answer changes, not
when the page moves.
Verified with real wheel input rather than scrollTo, which fires no scroll event
at all under CDP - worth knowing before anyone writes an e2e test for this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects on the one screen where being unable to read the interface costs a
reviewer their answer rather than their patience.
THE CALL BUTTONS WERE NOT BUTTONS. `.rail` and `.persona` were dropped by the
redesign and nothing replaced them, so the three-way call - the single most
important control in the product - rendered as "AdvanceDo not advanceCannot
conclude": three words run together, no gap, no border, no pressed state. Agree
and Override on the verdict screen had the same pair and the same result. Both
now use `.choice` and `button.ghost`, which is what every other form here is
built from and already carries hover and aria-pressed.
THE TEXTAREA HAD VANISHED INTO ITS OWN PANEL. The form rule says a field is
"darker than paper", and it was not: paper carries 0.50 alpha and the field
carried 0.46, so the field covered LESS of what was behind it than the plate did.
Over a scene that was survivable by accident. On a plate the two composite to
within a couple of values of each other and there was no box on the screen at all
- only a slightly different dark where the caret was supposed to go. 0.78 now,
and the rim goes from 0.15 to 0.26, because a field has to be an object somebody
can see the extent of before they click into it.
The prose moves onto the field rhythm the rest of the product uses: labels,
controls and their explanations in `.field` with `.hint`, the two halves of an
outside claim spaced by rule rather than by an inline style no stylesheet could
reach, and the basis line ruled off above the button because it summarises every
answer rather than asking for one more.
A GUARD FOR THE CLASS OF BUG. A className with no rule does not throw, does not
warn, and does not fail a type check - it renders as an unstyled element that
looks like a mistake nobody made. The stylesheet test now reads every plain
className out of the components and fails on any the stylesheet has no rule for.
Run against the old markup it names `rail` and `persona` exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read & mark could never open a PDF. It showed "Failed to fetch", and behind that
was a 401 on /api/cases/:id/documents/:docId/raw.
pdf.js does not go through api.ts. It is handed a URL and issues its OWN request,
so the bearer token this app keeps in memory never reached the one endpoint that
serves a document's bytes - the endpoint every other call authenticates against
without trouble.
It hid for as long as it did because no case had a document on it. The screen
said "No documents on this case yet" and nothing ever reached the fetch, so the
first person to attach a file was always going to be the first person to find
this.
httpHeaders rather than a query parameter: a token in the URL is a token in the
browser history, in the referrer, and in any log the request crosses, and this is
the endpoint that streams unpublished safety data.
Verified against a real 178-page EMA assessment report - the page renders, a
finding in the rail jumps to the page it cites, and the page says what the
finding says it does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A prepared case arrived with findings and no document. The case file carries a
page number for every finding, and the library manifest knows which document
those pages are in, but the two facts never met - so Read & mark said "No
documents on this case yet" on every case anybody opened, and the reader, which
joins a finding to a page THROUGH a document id, had nothing to join to. Making
one usable meant uploading the file by hand and re-adding every finding against
it.
The source now comes with the case. It goes through the same door a person's own
upload does - documents.upload measures before it accepts - so a scanned or
off-topic source is refused here exactly as it would be on the Evidence stage, by
the same code, with the same reason. Nothing about shipping a file with the
product makes it readable.
A FAILURE HERE DOES NOT LOSE THE CASE. The approval packages are not committed,
so a checkout without them is the normal case, not the exception; a missing file
or a refusal leaves the case opening exactly as it did before, which is still a
working case. There is a test for that path specifically.
DEDUPLICATION HAD TO STOP BEING GLOBAL, and this is what proved it. DocumentStore
keyed uploads by content hash across the whole store, which is right inside a case
- re-sending a file you already sent must not become a second document a second
position can cite - and wrong across them. With the review arriving at open time,
the second person to open the same case uploaded identical bytes and got back the
FIRST person's document, scoped to a case they cannot see: forCase found nothing
for them, and their findings named a document that is not on the case they were
filed under. The key is per case now. That is the same argument the caseId suffix
already makes one level up - a prepared case is a starting point, not a shared
room - and the note at the top of documents.ts, which justified the global
version, says so instead.
The attach test injects the manifest rather than reading disk: the approval
packages are deliberately untracked, so a test that needed the real file would
pass on a developer machine and fail in CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
services/api shells out to measure_pdf.py and extract_pdf_text.py for every
document, and both need PyMuPDF. The interpreter comes from PYTHON and otherwise
defaults to whatever `python` is on PATH - which, on a machine whose system
Python has no pip and cannot get one, is an interpreter that will never have
PyMuPDF in it.
The failure was quiet and pointed at the wrong thing. Every upload came back 422
"unreadable - PyMuPDF is not installed", which reads as a bad DOCUMENT rather
than a missing dependency, and two tests in services/api failed the same way on
any machine without a global install. A green suite and a working upload both
depended on knowing to set an environment variable that nothing mentions.
So both entry points now look for a repo-local .venv and use it when nothing has
already named an interpreter. An explicit PYTHON still wins, and a checkout with
no virtualenv behaves exactly as before - CI installs the requirements globally
and is untouched.
dev-all prints the interpreter it chose, because a silently-chosen one is how the
last one hid.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ask returned `truncated: max_tokens too low` on three of four attempts. The route
answered 502 and the composer showed a bare `upstream` - no answer at all, for a
question retrieval had already served the right pages for.
SHAPE_ASK's 16000 was measured, and it was right for the prompt it was measured
against. What changed is the answer: ask.ts now asks for Markdown, and headings,
bullets and blank lines are more tokens of answer for the same question. Thinking
shares that budget, so the pair crossed the ceiling on any question with a broad
answer - "What liver findings are reported, and at what doses?" against the
264-page Turalio review, every time but one.
This is the fifth time on this project that an output ceiling has been too small
once thinking was counted, and the first where the ceiling did not move but the
output did. The number is SHAPE_SUMMARY's for SHAPE_SUMMARY's reason: the answer
is bounded by the prompt, so the only job left for this ceiling is to stop being
the binding constraint on thinking. A cap is not a reservation - nothing is spent
by raising one, only by generating into it - which is why the fix is always to
raise it and never to tune it.
SHAPE_ADJUDICATION stays at 16000. It has its own measurement - zero truncation
across 10 runs on the probe case - and its input is a bounded case rather than
however many pages retrieval returned.
Measured after: six of six on the question that failed three of four, and the
same question answered in the UI with its headings and citations intact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The library's bodies had a floor in the code and none in the frame: an
additive plane of light bands with no depth write, which over a near-black
ground is very nearly nothing. A rank of standing volumes read as a rank of
floating ones.
It is a heightfield now, ported from the landing page's own library frame
and for the same reason that one has it - an object glowing over nothing is
a shape, and the same object with ground catching its light is a place. Each
case throws a pool on the ground it stands on, azure if it is usable and a
smaller red one if it was refused, so the refusals are legible on the floor
as well as in the object.
Three things the port had to learn on the way across, all in terrain.ts:
emitters carry a colour, because a refused body is the one red thing in this
palette; eight slots rather than four, because the library holds six; and a
dissolve, because the camera flies inside a body and the world outside has
to stop existing. The old floor's sweeping bands moved into the same shader,
so the light now runs over the landform instead of through it.
The bodies stand ON the surface rather than sunk into it, and that is forced
rather than chosen: Atmosphere builds rtC with depthBuffer false and renders
into it whenever a transition is not running, so outside a scene swap nothing
here is depth tested and a buried foot would simply be drawn. Same reason the
ground draws first. Noted in the file, since the scene's own header claims a
depth test it does not get.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two refused entries opened their one line with "REFUSED - " in capitals,
two rows under a card already drawing a red REFUSED badge off the same
usable flag. The badge said twice, and it was the only shouted lead-in on a
page where the other four entries open on a sentence.
What is left does the job every other shape does - what the document is,
then what that costs the reader - so the six read as one set again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
h1-h4 and p are all margin 0 in app.css: this app takes its vertical rhythm
from a container, never from the type. Refused rendered a bare <section> and
is mounted straight into the shell, so it had no container at all - the
title, the red refusal box and all six headings and paragraphs stacked with
literally no space between them, the box wedged against the heading above
and the one below.
.section restores the 16px between blocks. Each heading is paired with its
body in a .stack-s as well, because .section alone spaces every child
equally and that leaves an h3 floating exactly as far from its own paragraph
as from the block before it - the panel reads as three groups now rather
than six loose lines. No new CSS; both classes already existed.
Five more bare sections in this file have the same gap, and the inline
marginTop hacks at 664 and 722 are what standing in for it looks like.
Left for their own change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Developer API's response_schema proto does not carry
additionalProperties, so sending it rejects the REQUEST outright - 400,
Unknown name "additionalProperties" at generation_config.response_schema -
before a model is ever selected. Every AI surface writes that key, so
adjudicate, ask, interpret, extract and navigate all died together on that
host, and handleAdjudicate reporting every upstream fault as a bare 502
made a request that never reached a model look like a model that failed.
Stripped in transport rather than in the schema builders. The constraint is
real where it is honoured: Vertex enforces it, it is what stops a model
returning a field nobody asked for, and the committed numbers were measured
with it in force. Loosening the shared schemas to suit the weaker host would
weaken the stronger one to buy nothing, so the schemas stay strict and the
transport adapts.
Known red: services/api/test/server.test.ts "runs the rest of the flow for
the owner" fails on adjudicate returning 502. It reproduces on origin/main
without this change, so it is not introduced here, but it is landing red and
is worth chasing next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	apps/deliberation/src/App.tsx
#	apps/deliberation/src/read.tsx
#	apps/deliberation/src/shell/Backdrop.tsx
#	apps/deliberation/test/read.test.tsx
A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a714b01)
`POST /api/demo` opens a real library case: it copies the regulatory review off disk,
transcribes its findings and writes the opening links of the hash chain. One of those
runs about 1.4s alone, and "gives each opener their own copy" does the whole thing
TWICE - 3.5s before the machine is doing anything else, against a 5s default.
It passed on either side and failed on the merge of the two, which is the tell: the
suite grew from 881 and 948 tests to 962, the files compete for the same cores, and a
test sitting 1.5s under the limit went 2s over it. Nothing on the path changed - the
failure moved with the SIZE OF THE RUN, which is a limit set too close rather than a
slow path worth chasing.
Stated on the five tests that seed, rather than raised globally: a timeout is a claim
about what a test does, and only these five copy a PDF. Three consecutive full runs
green at 962, then 971 with the markdown tests on top.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7cdf48e4-510d-489b-8a7b-884a5c1cb888

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

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

@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Updated: merged origin/main again to pick up 347c87f (strip additionalProperties for the Developer API host) — the conflict in gemini.ts was two independent additions at the same spot, resolved as a union so both geminiCredentialAdvice and responseSchemaFor survive. 976 tests across 67 files, three builds, CI green.

The merge resolution deleted the ======= and >>>>>>> lines but left the
opening <<<<<<< HEAD. It sat inside the JSDoc for geminiCredentialAdvice,
so it was legal comment text - which is why typecheck, lint and CI all
stayed green on it.
@AndresL230
AndresL230 merged commit f0ff440 into feat/product-in-the-atmosphereAug 17, 2026
3 checks passed
AndresL230 added a commit that referenced this pull request Aug 17, 2026
PR #32 reconciled main into this branch, but its last main merge predated
dd140aa by 26 minutes, so the two newest commits on main never rode along:
dd140aa Serve the verdict from the record, not from the tab that ran it
53e9a5b Give the four remaining bare sections the rhythm the rest of the app has
Without dd140aa the product branch kept the pre-fix adjudication path: the
verdict lived only in the browser of whoever pressed Adjudicate, so Reveal &
verdict was empty for every signed case, every refresh, and every non-owner -
and the Adjudicate button still offered itself on cases that could only answer
409, spending three model calls to find that out. Nine regression tests come
with the fix.
One conflict, in the services/api/server.ts import block, resolved as the union
of both sides: this branch's geminiCredentialAdvice and main's CallKind/Complete
- the seam that keeps server.test.ts from making live paid adjudication calls.
Both symbols are used; dropping either breaks the build.
Verified: typecheck clean, lint clean, 985/985 tests passing across 67 files.
AndresL230 added a commit that referenced this pull request Aug 17, 2026
main moved 42 commits under this branch - #33 (Postgres/Supabase), #24, #29,
#32 and the logo - and rewrote the three files this touches most.
THE RECONCILIATION #33 DEFERRED. `ShareStore` was written against the pattern
this branch had: a synchronous `new ShareStore(path)` beside `AuthStore` and
`InviteStore`. That layer is gone. So the store joins the new one the way the
other four did:
- `ShareStoreApi` in postgres-share.ts, where `AuthStoreApi` and
`InviteStoreApi` live, for the reason stated there: the second
implementation is what needs a name for the shape.
- `ShareStore.open(path)`, async, private constructor.
- `PostgresShareStore`, and supabase/migrations/0002_share_links.sql.
- Both branches of `buildStores`, and `Stores.shares`.
- The behaviour is ONE suite over both implementations
(test/share-store-contract.ts), as with auth and invites.
`shareSecret` deliberately stayed OUT of `buildStores`: which backing holds the
links is a storage decision, whether the deployment can publish at all is not.
THE TABLE HOLDS NO TOKEN, and the migration says so at length because that is
the feature's whole security argument - the URL is
HMAC(secret, "caseId:version"), derivable from two columns that are not secret,
so a stolen dump yields nothing. `publish` omits `version` from its upsert's
SET list on purpose: assigning it would reset a revoked case to 1 and re-mint
the token revoke had just killed. A test reads the column list back out of
information_schema, because that property fails silently.
WHAT THE CONFLICTS COST. server.ts kept main's static serving whole and its
`/api/health` route; the branch's "NO STATIC-FILE SERVING HERE" comment was
true when written and is now a lie, so it is replaced by an accurate one:
`/r/:caseId/:token` is still not served in production, a scanned QR 404s on a
deployed host, and the two decisions that has to wait on are written down
beside `staticRoot()`. Adding it here was out of scope and stayed out.
`handleReport` and `handleShare` became async; `handleReport` fetches the people
in one `auth.list()` rather than a lookup per name, because `auth.get` is
asynchronous now and `buildCaseReport` takes a synchronous `person`.
ONE THING THE MERGE HAD TO DECIDE. docs/HANDOFF-open-prs.md predicted that #30
and main each carried a provenance rule and that they would auto-merge without
conflict. They did, and both were live: `view`'s read any unrecognised actor as
`live`, the report's read only "model" as `live`. Two surfaces describing one
signed safety adjudication, disagreeing about whether a model produced it.
Closed with one module-level `sourceOf`, failing toward `stub`; every writer
passes "stub" or "model" so nothing observable changed, and the test pins that
both readers agree on a third actor neither was written for.
The postgres test fixture now applies every migration in filename order. Pinned
to 0001, it built a database that was correct on the day it was written and
silently lacked share_links.
Verified: lint, typecheck, 1267 tests with DATABASE_URL set (1179 without),
both builds, harness + metrics + golden + an unchanged verdict-manifest, e2e.
The public bundle still carries no auth code: only main's own entry chunk
matches AUTO_PASSWORD or /api/auth/login, and public.html references neither.
HANDOFF-pr34.md is deleted - the job it describes is this commit, and what
happened is recorded in docs/HANDOFF-open-prs.md and the spec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Merge main into the product branch, and finish the Ask markdown fix - #32

Merged
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product
Aug 17, 2026
Merged

Merge main into the product branch, and finish the Ask markdown fix#32
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

Brings all 10 commits from main onto feat/product-in-the-atmosphere, including every Ask/AI fix, and lands the markdown rendering the bug report was about. Supersedes #31.

What arrives from main

  • b84728c - Ask renders Markdown (markdown.tsx) and ask.ts asks the model for structure. Both halves; neither works alone.
  • c78131c - stops Ask truncating now that answers are longer (max_tokens was cutting a 264-page review's answer at 56s).
  • 84bc19c - opening a prepared case brings its document with it.
  • 3e6bc88, 2ae6907, plus the refusal-panel, archive and header work.

The four conflicts, and why they resolved the way they did

All in files both branches had changed independently.

  • read.tsx / read.test.tsx / App.tsx - both branches had independently fixed the same pdf.js 401 (the bearer token never reached /raw). Your branch's version is a superset: it carries that fix and the citation-attribution feature (citers/blind, who cited which finding after reveal). Kept yours, so nothing is lost either way - main's only unique content there was one redundant token test.
  • One genuine merge: main had made token optional because two of its layout tests render <Read> without one. Your side had it required, which would have failed those tests. Resolved by taking main's optional handling plus your citation feature - the header is omitted rather than sent as Bearer undefined.
  • Backdrop.tsx - both branches made the identical two fixes (register from STATES, guard transitionTo); only the comment wording differed.

Markers were stripped hunk-by-hunk keeping the right side, notcheckout --ours, which would have discarded main's auto-merged changes in those same files.

Two follow-on fixes

  • A real hole in the renderer. If an answer arrives with no newlines, the parser turned the whole thing into one giant <h4> - worse than the wall of text reported. Reconstruction is now anchored only to unambiguous markers: a bare - stays a dash, 1. stays part of a number, and it runs only when the answer has no newline anywhere, so well-formed markdown takes an unchanged path. A test carries the reported answer verbatim.
  • A test timeout that only appeared on the merge. Five seeding tests copy a real PDF and write hash-chain links; the two-opener one takes ~3.5s against a 5s default. It passed on either branch alone and failed on the merge because the suite grew to 962 tests and the files compete for cores. Given an explicit budget on the five tests that seed, rather than raised globally.

Verification

  • 971 tests pass across 66 files; three consecutive full runs green before the markdown commit went on top (962/962 each)
  • tsc --noEmit clean on the deliberation app
  • All three workspaces build
  • Confirmed present after merge: markdown.tsx, the ask.ts markdown prompt, the max_tokens fix, citationsFor/citers, and the committed regulatory documents

🤖 Generated with Claude Code

Darkest-Teddyand others added 18 commits August 16, 2026 01:36
Reading Trails, phase 1: a Read & mark tab, and seats that hold still
Pressing Read & mark went dark and stayed dark until a reload.
Backdrop registered five scenes by hand - dashboard, new, library, ask, record
- while packages/atmosphere publishes seven. When Section arrived and sceneFor
started returning "read" for the reading surface, nothing added it here. So
transitionTo was called with an id the engine had never heard of, which throws
by contract (core/Atmosphere.ts). The throw happened inside an effect, and React
unmounts a tree whose effect threw, so a fault in the DECORATION took the whole
product with it.
The same id on a deep link went through mount() instead, inside the loader's
try, and was swallowed - a working product with no scene behind it. That is why
reloading appeared to fix it, and why it looked intermittent: three different
outcomes from one missing table entry, depending on how you arrived.
Registration now reads STATES, which is what apps/atmosphere has always done and
where a new scene actually gets added. That makes the drift impossible rather
than merely repaired. Registering is a map insert and the factory does not run
until something mounts it, so carrying "landing" here costs nothing.
The transition effect is also guarded now. Fixing the catalogue removed this
throw, not the class of them - a driver refusing a shader mid-session arrives at
the same line - and this file's own header says a background may not take the
product down with it. The loader honoured that and the scene change did not.
The fake engine in the test keeps the real class's contract: an unregistered id
throws. A forgiving one would have passed against the broken code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both surfaces put their prose directly on a live WebGL field. Reading is the
longest stretch of text in the product - a document strip, a findings rail, and
the paragraph explaining why a citation could not be placed - and the position
form is a heading, four labels, three explanations and a basis line. Neither had
a ground under any of it.
.glass is the one surface in this product that carries one, and app.css says why
it exists in those words: a low-alpha wash plus a blur that stays readable over a
lit colony without hiding what is behind it. The evidence stage has used it from
the start and reads cleanly over every scene because of it. These two were the
screens that needed it most and were going without.
Waiting is plated as well as the form. That tab BECOMES Waiting once you seal, so
a ground on only one of them would disappear at the moment you submit.
ONE PLATE PER SCREEN, not one per part - app.css puts the blur on containers
only, because a dozen stacked blur layers buy nothing the parent has not already
bought.
The empty reading state is plated too. It is one paragraph and nothing else,
which makes it the state most exposed to the scene: there is no other object on
the screen for the type to sit on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header is fixed and the work scrolls under it, which is the arrangement
Chrome.tsx argues for: a case table with three hundred rows is a document, and a
product that hijacks the wheel to fly a camera is a product nobody can use. The
cost of that arrangement is that the rows pass behind the navigation. On a long
position form the wordmark and the sign-out button sit across the sentence you
are reading.
So it leaves going down and comes back the moment you turn around. DIRECTION,
NOT POSITION: "hide below 400px" would take the navigation away and keep it away
for the length of the table, and scrolling up is the gesture that means give me
the chrome back.
A 6px floor, because a trackpad does not emit one scroll event per gesture - it
emits a stream of one- and two-pixel moves, some of them in the wrong direction,
and without a floor the header flickers the whole way down. Always showing above
96px, roughly its own height plus its inset, where it is not covering anything
yet and hiding would be motion for no reason.
Inert while it is away rather than merely transparent. This holds the main menu
and the only sign-out control, and a tab stop on something translated off the top
of the viewport sends focus somewhere the reader cannot see. One attribute takes
the subtree out of the tab order and swallows pointer events with it.
Transform and opacity only - the two a compositor animates without laying out
again, and there is a WebGL scene drawing behind this. Reduced motion keeps the
behaviour and drops the transition: a reader who asked for less motion still
wants the navigation off their paragraph.
No rAF throttle. The listener is passive and does two comparisons against a
number the browser already has, scroll events are frame-aligned as it is, and the
state only changes on the turn - so React re-renders when the answer changes, not
when the page moves.
Verified with real wheel input rather than scrollTo, which fires no scroll event
at all under CDP - worth knowing before anyone writes an e2e test for this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects on the one screen where being unable to read the interface costs a
reviewer their answer rather than their patience.
THE CALL BUTTONS WERE NOT BUTTONS. `.rail` and `.persona` were dropped by the
redesign and nothing replaced them, so the three-way call - the single most
important control in the product - rendered as "AdvanceDo not advanceCannot
conclude": three words run together, no gap, no border, no pressed state. Agree
and Override on the verdict screen had the same pair and the same result. Both
now use `.choice` and `button.ghost`, which is what every other form here is
built from and already carries hover and aria-pressed.
THE TEXTAREA HAD VANISHED INTO ITS OWN PANEL. The form rule says a field is
"darker than paper", and it was not: paper carries 0.50 alpha and the field
carried 0.46, so the field covered LESS of what was behind it than the plate did.
Over a scene that was survivable by accident. On a plate the two composite to
within a couple of values of each other and there was no box on the screen at all
- only a slightly different dark where the caret was supposed to go. 0.78 now,
and the rim goes from 0.15 to 0.26, because a field has to be an object somebody
can see the extent of before they click into it.
The prose moves onto the field rhythm the rest of the product uses: labels,
controls and their explanations in `.field` with `.hint`, the two halves of an
outside claim spaced by rule rather than by an inline style no stylesheet could
reach, and the basis line ruled off above the button because it summarises every
answer rather than asking for one more.
A GUARD FOR THE CLASS OF BUG. A className with no rule does not throw, does not
warn, and does not fail a type check - it renders as an unstyled element that
looks like a mistake nobody made. The stylesheet test now reads every plain
className out of the components and fails on any the stylesheet has no rule for.
Run against the old markup it names `rail` and `persona` exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read & mark could never open a PDF. It showed "Failed to fetch", and behind that
was a 401 on /api/cases/:id/documents/:docId/raw.
pdf.js does not go through api.ts. It is handed a URL and issues its OWN request,
so the bearer token this app keeps in memory never reached the one endpoint that
serves a document's bytes - the endpoint every other call authenticates against
without trouble.
It hid for as long as it did because no case had a document on it. The screen
said "No documents on this case yet" and nothing ever reached the fetch, so the
first person to attach a file was always going to be the first person to find
this.
httpHeaders rather than a query parameter: a token in the URL is a token in the
browser history, in the referrer, and in any log the request crosses, and this is
the endpoint that streams unpublished safety data.
Verified against a real 178-page EMA assessment report - the page renders, a
finding in the rail jumps to the page it cites, and the page says what the
finding says it does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A prepared case arrived with findings and no document. The case file carries a
page number for every finding, and the library manifest knows which document
those pages are in, but the two facts never met - so Read & mark said "No
documents on this case yet" on every case anybody opened, and the reader, which
joins a finding to a page THROUGH a document id, had nothing to join to. Making
one usable meant uploading the file by hand and re-adding every finding against
it.
The source now comes with the case. It goes through the same door a person's own
upload does - documents.upload measures before it accepts - so a scanned or
off-topic source is refused here exactly as it would be on the Evidence stage, by
the same code, with the same reason. Nothing about shipping a file with the
product makes it readable.
A FAILURE HERE DOES NOT LOSE THE CASE. The approval packages are not committed,
so a checkout without them is the normal case, not the exception; a missing file
or a refusal leaves the case opening exactly as it did before, which is still a
working case. There is a test for that path specifically.
DEDUPLICATION HAD TO STOP BEING GLOBAL, and this is what proved it. DocumentStore
keyed uploads by content hash across the whole store, which is right inside a case
- re-sending a file you already sent must not become a second document a second
position can cite - and wrong across them. With the review arriving at open time,
the second person to open the same case uploaded identical bytes and got back the
FIRST person's document, scoped to a case they cannot see: forCase found nothing
for them, and their findings named a document that is not on the case they were
filed under. The key is per case now. That is the same argument the caseId suffix
already makes one level up - a prepared case is a starting point, not a shared
room - and the note at the top of documents.ts, which justified the global
version, says so instead.
The attach test injects the manifest rather than reading disk: the approval
packages are deliberately untracked, so a test that needed the real file would
pass on a developer machine and fail in CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
services/api shells out to measure_pdf.py and extract_pdf_text.py for every
document, and both need PyMuPDF. The interpreter comes from PYTHON and otherwise
defaults to whatever `python` is on PATH - which, on a machine whose system
Python has no pip and cannot get one, is an interpreter that will never have
PyMuPDF in it.
The failure was quiet and pointed at the wrong thing. Every upload came back 422
"unreadable - PyMuPDF is not installed", which reads as a bad DOCUMENT rather
than a missing dependency, and two tests in services/api failed the same way on
any machine without a global install. A green suite and a working upload both
depended on knowing to set an environment variable that nothing mentions.
So both entry points now look for a repo-local .venv and use it when nothing has
already named an interpreter. An explicit PYTHON still wins, and a checkout with
no virtualenv behaves exactly as before - CI installs the requirements globally
and is untouched.
dev-all prints the interpreter it chose, because a silently-chosen one is how the
last one hid.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ask returned `truncated: max_tokens too low` on three of four attempts. The route
answered 502 and the composer showed a bare `upstream` - no answer at all, for a
question retrieval had already served the right pages for.
SHAPE_ASK's 16000 was measured, and it was right for the prompt it was measured
against. What changed is the answer: ask.ts now asks for Markdown, and headings,
bullets and blank lines are more tokens of answer for the same question. Thinking
shares that budget, so the pair crossed the ceiling on any question with a broad
answer - "What liver findings are reported, and at what doses?" against the
264-page Turalio review, every time but one.
This is the fifth time on this project that an output ceiling has been too small
once thinking was counted, and the first where the ceiling did not move but the
output did. The number is SHAPE_SUMMARY's for SHAPE_SUMMARY's reason: the answer
is bounded by the prompt, so the only job left for this ceiling is to stop being
the binding constraint on thinking. A cap is not a reservation - nothing is spent
by raising one, only by generating into it - which is why the fix is always to
raise it and never to tune it.
SHAPE_ADJUDICATION stays at 16000. It has its own measurement - zero truncation
across 10 runs on the probe case - and its input is a bounded case rather than
however many pages retrieval returned.
Measured after: six of six on the question that failed three of four, and the
same question answered in the UI with its headings and citations intact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The library's bodies had a floor in the code and none in the frame: an
additive plane of light bands with no depth write, which over a near-black
ground is very nearly nothing. A rank of standing volumes read as a rank of
floating ones.
It is a heightfield now, ported from the landing page's own library frame
and for the same reason that one has it - an object glowing over nothing is
a shape, and the same object with ground catching its light is a place. Each
case throws a pool on the ground it stands on, azure if it is usable and a
smaller red one if it was refused, so the refusals are legible on the floor
as well as in the object.
Three things the port had to learn on the way across, all in terrain.ts:
emitters carry a colour, because a refused body is the one red thing in this
palette; eight slots rather than four, because the library holds six; and a
dissolve, because the camera flies inside a body and the world outside has
to stop existing. The old floor's sweeping bands moved into the same shader,
so the light now runs over the landform instead of through it.
The bodies stand ON the surface rather than sunk into it, and that is forced
rather than chosen: Atmosphere builds rtC with depthBuffer false and renders
into it whenever a transition is not running, so outside a scene swap nothing
here is depth tested and a buried foot would simply be drawn. Same reason the
ground draws first. Noted in the file, since the scene's own header claims a
depth test it does not get.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two refused entries opened their one line with "REFUSED - " in capitals,
two rows under a card already drawing a red REFUSED badge off the same
usable flag. The badge said twice, and it was the only shouted lead-in on a
page where the other four entries open on a sentence.
What is left does the job every other shape does - what the document is,
then what that costs the reader - so the six read as one set again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
h1-h4 and p are all margin 0 in app.css: this app takes its vertical rhythm
from a container, never from the type. Refused rendered a bare <section> and
is mounted straight into the shell, so it had no container at all - the
title, the red refusal box and all six headings and paragraphs stacked with
literally no space between them, the box wedged against the heading above
and the one below.
.section restores the 16px between blocks. Each heading is paired with its
body in a .stack-s as well, because .section alone spaces every child
equally and that leaves an h3 floating exactly as far from its own paragraph
as from the block before it - the panel reads as three groups now rather
than six loose lines. No new CSS; both classes already existed.
Five more bare sections in this file have the same gap, and the inline
marginTop hacks at 664 and 722 are what standing in for it looks like.
Left for their own change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Developer API's response_schema proto does not carry
additionalProperties, so sending it rejects the REQUEST outright - 400,
Unknown name "additionalProperties" at generation_config.response_schema -
before a model is ever selected. Every AI surface writes that key, so
adjudicate, ask, interpret, extract and navigate all died together on that
host, and handleAdjudicate reporting every upstream fault as a bare 502
made a request that never reached a model look like a model that failed.
Stripped in transport rather than in the schema builders. The constraint is
real where it is honoured: Vertex enforces it, it is what stops a model
returning a field nobody asked for, and the committed numbers were measured
with it in force. Loosening the shared schemas to suit the weaker host would
weaken the stronger one to buy nothing, so the schemas stay strict and the
transport adapts.
Known red: services/api/test/server.test.ts "runs the rest of the flow for
the owner" fails on adjudicate returning 502. It reproduces on origin/main
without this change, so it is not introduced here, but it is landing red and
is worth chasing next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	apps/deliberation/src/App.tsx
#	apps/deliberation/src/read.tsx
#	apps/deliberation/src/shell/Backdrop.tsx
#	apps/deliberation/test/read.test.tsx
A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a714b01)
`POST /api/demo` opens a real library case: it copies the regulatory review off disk,
transcribes its findings and writes the opening links of the hash chain. One of those
runs about 1.4s alone, and "gives each opener their own copy" does the whole thing
TWICE - 3.5s before the machine is doing anything else, against a 5s default.
It passed on either side and failed on the merge of the two, which is the tell: the
suite grew from 881 and 948 tests to 962, the files compete for the same cores, and a
test sitting 1.5s under the limit went 2s over it. Nothing on the path changed - the
failure moved with the SIZE OF THE RUN, which is a limit set too close rather than a
slow path worth chasing.
Stated on the five tests that seed, rather than raised globally: a timeout is a claim
about what a test does, and only these five copy a PDF. Three consecutive full runs
green at 962, then 971 with the markdown tests on top.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7cdf48e4-510d-489b-8a7b-884a5c1cb888

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

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

@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Updated: merged origin/main again to pick up 347c87f (strip additionalProperties for the Developer API host) — the conflict in gemini.ts was two independent additions at the same spot, resolved as a union so both geminiCredentialAdvice and responseSchemaFor survive. 976 tests across 67 files, three builds, CI green.

The merge resolution deleted the ======= and >>>>>>> lines but left the
opening <<<<<<< HEAD. It sat inside the JSDoc for geminiCredentialAdvice,
so it was legal comment text - which is why typecheck, lint and CI all
stayed green on it.
@AndresL230
AndresL230 merged commit f0ff440 into feat/product-in-the-atmosphereAug 17, 2026
3 checks passed
AndresL230 added a commit that referenced this pull request Aug 17, 2026
PR #32 reconciled main into this branch, but its last main merge predated
dd140aa by 26 minutes, so the two newest commits on main never rode along:
dd140aa Serve the verdict from the record, not from the tab that ran it
53e9a5b Give the four remaining bare sections the rhythm the rest of the app has
Without dd140aa the product branch kept the pre-fix adjudication path: the
verdict lived only in the browser of whoever pressed Adjudicate, so Reveal &
verdict was empty for every signed case, every refresh, and every non-owner -
and the Adjudicate button still offered itself on cases that could only answer
409, spending three model calls to find that out. Nine regression tests come
with the fix.
One conflict, in the services/api/server.ts import block, resolved as the union
of both sides: this branch's geminiCredentialAdvice and main's CallKind/Complete
- the seam that keeps server.test.ts from making live paid adjudication calls.
Both symbols are used; dropping either breaks the build.
Verified: typecheck clean, lint clean, 985/985 tests passing across 67 files.
AndresL230 added a commit that referenced this pull request Aug 17, 2026
main moved 42 commits under this branch - #33 (Postgres/Supabase), #24, #29,
#32 and the logo - and rewrote the three files this touches most.
THE RECONCILIATION #33 DEFERRED. `ShareStore` was written against the pattern
this branch had: a synchronous `new ShareStore(path)` beside `AuthStore` and
`InviteStore`. That layer is gone. So the store joins the new one the way the
other four did:
- `ShareStoreApi` in postgres-share.ts, where `AuthStoreApi` and
`InviteStoreApi` live, for the reason stated there: the second
implementation is what needs a name for the shape.
- `ShareStore.open(path)`, async, private constructor.
- `PostgresShareStore`, and supabase/migrations/0002_share_links.sql.
- Both branches of `buildStores`, and `Stores.shares`.
- The behaviour is ONE suite over both implementations
(test/share-store-contract.ts), as with auth and invites.
`shareSecret` deliberately stayed OUT of `buildStores`: which backing holds the
links is a storage decision, whether the deployment can publish at all is not.
THE TABLE HOLDS NO TOKEN, and the migration says so at length because that is
the feature's whole security argument - the URL is
HMAC(secret, "caseId:version"), derivable from two columns that are not secret,
so a stolen dump yields nothing. `publish` omits `version` from its upsert's
SET list on purpose: assigning it would reset a revoked case to 1 and re-mint
the token revoke had just killed. A test reads the column list back out of
information_schema, because that property fails silently.
WHAT THE CONFLICTS COST. server.ts kept main's static serving whole and its
`/api/health` route; the branch's "NO STATIC-FILE SERVING HERE" comment was
true when written and is now a lie, so it is replaced by an accurate one:
`/r/:caseId/:token` is still not served in production, a scanned QR 404s on a
deployed host, and the two decisions that has to wait on are written down
beside `staticRoot()`. Adding it here was out of scope and stayed out.
`handleReport` and `handleShare` became async; `handleReport` fetches the people
in one `auth.list()` rather than a lookup per name, because `auth.get` is
asynchronous now and `buildCaseReport` takes a synchronous `person`.
ONE THING THE MERGE HAD TO DECIDE. docs/HANDOFF-open-prs.md predicted that #30
and main each carried a provenance rule and that they would auto-merge without
conflict. They did, and both were live: `view`'s read any unrecognised actor as
`live`, the report's read only "model" as `live`. Two surfaces describing one
signed safety adjudication, disagreeing about whether a model produced it.
Closed with one module-level `sourceOf`, failing toward `stub`; every writer
passes "stub" or "model" so nothing observable changed, and the test pins that
both readers agree on a third actor neither was written for.
The postgres test fixture now applies every migration in filename order. Pinned
to 0001, it built a database that was correct on the day it was written and
silently lacked share_links.
Verified: lint, typecheck, 1267 tests with DATABASE_URL set (1179 without),
both builds, harness + metrics + golden + an unchanged verdict-manifest, e2e.
The public bundle still carries no auth code: only main's own entry chunk
matches AUTO_PASSWORD or /api/auth/login, and public.html references neither.
HANDOFF-pr34.md is deleted - the job it describes is this commit, and what
happened is recorded in docs/HANDOFF-open-prs.md and the spec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Merge main into the product branch, and finish the Ask markdown fix - #32

Merged
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product
Aug 17, 2026
Merged

Merge main into the product branch, and finish the Ask markdown fix#32
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

Brings all 10 commits from main onto feat/product-in-the-atmosphere, including every Ask/AI fix, and lands the markdown rendering the bug report was about. Supersedes #31.

What arrives from main

  • b84728c - Ask renders Markdown (markdown.tsx) and ask.ts asks the model for structure. Both halves; neither works alone.
  • c78131c - stops Ask truncating now that answers are longer (max_tokens was cutting a 264-page review's answer at 56s).
  • 84bc19c - opening a prepared case brings its document with it.
  • 3e6bc88, 2ae6907, plus the refusal-panel, archive and header work.

The four conflicts, and why they resolved the way they did

All in files both branches had changed independently.

  • read.tsx / read.test.tsx / App.tsx - both branches had independently fixed the same pdf.js 401 (the bearer token never reached /raw). Your branch's version is a superset: it carries that fix and the citation-attribution feature (citers/blind, who cited which finding after reveal). Kept yours, so nothing is lost either way - main's only unique content there was one redundant token test.
  • One genuine merge: main had made token optional because two of its layout tests render <Read> without one. Your side had it required, which would have failed those tests. Resolved by taking main's optional handling plus your citation feature - the header is omitted rather than sent as Bearer undefined.
  • Backdrop.tsx - both branches made the identical two fixes (register from STATES, guard transitionTo); only the comment wording differed.

Markers were stripped hunk-by-hunk keeping the right side, notcheckout --ours, which would have discarded main's auto-merged changes in those same files.

Two follow-on fixes

  • A real hole in the renderer. If an answer arrives with no newlines, the parser turned the whole thing into one giant <h4> - worse than the wall of text reported. Reconstruction is now anchored only to unambiguous markers: a bare - stays a dash, 1. stays part of a number, and it runs only when the answer has no newline anywhere, so well-formed markdown takes an unchanged path. A test carries the reported answer verbatim.
  • A test timeout that only appeared on the merge. Five seeding tests copy a real PDF and write hash-chain links; the two-opener one takes ~3.5s against a 5s default. It passed on either branch alone and failed on the merge because the suite grew to 962 tests and the files compete for cores. Given an explicit budget on the five tests that seed, rather than raised globally.

Verification

  • 971 tests pass across 66 files; three consecutive full runs green before the markdown commit went on top (962/962 each)
  • tsc --noEmit clean on the deliberation app
  • All three workspaces build
  • Confirmed present after merge: markdown.tsx, the ask.ts markdown prompt, the max_tokens fix, citationsFor/citers, and the committed regulatory documents

🤖 Generated with Claude Code

Darkest-Teddyand others added 18 commits August 16, 2026 01:36
Reading Trails, phase 1: a Read & mark tab, and seats that hold still
Pressing Read & mark went dark and stayed dark until a reload.
Backdrop registered five scenes by hand - dashboard, new, library, ask, record
- while packages/atmosphere publishes seven. When Section arrived and sceneFor
started returning "read" for the reading surface, nothing added it here. So
transitionTo was called with an id the engine had never heard of, which throws
by contract (core/Atmosphere.ts). The throw happened inside an effect, and React
unmounts a tree whose effect threw, so a fault in the DECORATION took the whole
product with it.
The same id on a deep link went through mount() instead, inside the loader's
try, and was swallowed - a working product with no scene behind it. That is why
reloading appeared to fix it, and why it looked intermittent: three different
outcomes from one missing table entry, depending on how you arrived.
Registration now reads STATES, which is what apps/atmosphere has always done and
where a new scene actually gets added. That makes the drift impossible rather
than merely repaired. Registering is a map insert and the factory does not run
until something mounts it, so carrying "landing" here costs nothing.
The transition effect is also guarded now. Fixing the catalogue removed this
throw, not the class of them - a driver refusing a shader mid-session arrives at
the same line - and this file's own header says a background may not take the
product down with it. The loader honoured that and the scene change did not.
The fake engine in the test keeps the real class's contract: an unregistered id
throws. A forgiving one would have passed against the broken code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both surfaces put their prose directly on a live WebGL field. Reading is the
longest stretch of text in the product - a document strip, a findings rail, and
the paragraph explaining why a citation could not be placed - and the position
form is a heading, four labels, three explanations and a basis line. Neither had
a ground under any of it.
.glass is the one surface in this product that carries one, and app.css says why
it exists in those words: a low-alpha wash plus a blur that stays readable over a
lit colony without hiding what is behind it. The evidence stage has used it from
the start and reads cleanly over every scene because of it. These two were the
screens that needed it most and were going without.
Waiting is plated as well as the form. That tab BECOMES Waiting once you seal, so
a ground on only one of them would disappear at the moment you submit.
ONE PLATE PER SCREEN, not one per part - app.css puts the blur on containers
only, because a dozen stacked blur layers buy nothing the parent has not already
bought.
The empty reading state is plated too. It is one paragraph and nothing else,
which makes it the state most exposed to the scene: there is no other object on
the screen for the type to sit on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header is fixed and the work scrolls under it, which is the arrangement
Chrome.tsx argues for: a case table with three hundred rows is a document, and a
product that hijacks the wheel to fly a camera is a product nobody can use. The
cost of that arrangement is that the rows pass behind the navigation. On a long
position form the wordmark and the sign-out button sit across the sentence you
are reading.
So it leaves going down and comes back the moment you turn around. DIRECTION,
NOT POSITION: "hide below 400px" would take the navigation away and keep it away
for the length of the table, and scrolling up is the gesture that means give me
the chrome back.
A 6px floor, because a trackpad does not emit one scroll event per gesture - it
emits a stream of one- and two-pixel moves, some of them in the wrong direction,
and without a floor the header flickers the whole way down. Always showing above
96px, roughly its own height plus its inset, where it is not covering anything
yet and hiding would be motion for no reason.
Inert while it is away rather than merely transparent. This holds the main menu
and the only sign-out control, and a tab stop on something translated off the top
of the viewport sends focus somewhere the reader cannot see. One attribute takes
the subtree out of the tab order and swallows pointer events with it.
Transform and opacity only - the two a compositor animates without laying out
again, and there is a WebGL scene drawing behind this. Reduced motion keeps the
behaviour and drops the transition: a reader who asked for less motion still
wants the navigation off their paragraph.
No rAF throttle. The listener is passive and does two comparisons against a
number the browser already has, scroll events are frame-aligned as it is, and the
state only changes on the turn - so React re-renders when the answer changes, not
when the page moves.
Verified with real wheel input rather than scrollTo, which fires no scroll event
at all under CDP - worth knowing before anyone writes an e2e test for this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects on the one screen where being unable to read the interface costs a
reviewer their answer rather than their patience.
THE CALL BUTTONS WERE NOT BUTTONS. `.rail` and `.persona` were dropped by the
redesign and nothing replaced them, so the three-way call - the single most
important control in the product - rendered as "AdvanceDo not advanceCannot
conclude": three words run together, no gap, no border, no pressed state. Agree
and Override on the verdict screen had the same pair and the same result. Both
now use `.choice` and `button.ghost`, which is what every other form here is
built from and already carries hover and aria-pressed.
THE TEXTAREA HAD VANISHED INTO ITS OWN PANEL. The form rule says a field is
"darker than paper", and it was not: paper carries 0.50 alpha and the field
carried 0.46, so the field covered LESS of what was behind it than the plate did.
Over a scene that was survivable by accident. On a plate the two composite to
within a couple of values of each other and there was no box on the screen at all
- only a slightly different dark where the caret was supposed to go. 0.78 now,
and the rim goes from 0.15 to 0.26, because a field has to be an object somebody
can see the extent of before they click into it.
The prose moves onto the field rhythm the rest of the product uses: labels,
controls and their explanations in `.field` with `.hint`, the two halves of an
outside claim spaced by rule rather than by an inline style no stylesheet could
reach, and the basis line ruled off above the button because it summarises every
answer rather than asking for one more.
A GUARD FOR THE CLASS OF BUG. A className with no rule does not throw, does not
warn, and does not fail a type check - it renders as an unstyled element that
looks like a mistake nobody made. The stylesheet test now reads every plain
className out of the components and fails on any the stylesheet has no rule for.
Run against the old markup it names `rail` and `persona` exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read & mark could never open a PDF. It showed "Failed to fetch", and behind that
was a 401 on /api/cases/:id/documents/:docId/raw.
pdf.js does not go through api.ts. It is handed a URL and issues its OWN request,
so the bearer token this app keeps in memory never reached the one endpoint that
serves a document's bytes - the endpoint every other call authenticates against
without trouble.
It hid for as long as it did because no case had a document on it. The screen
said "No documents on this case yet" and nothing ever reached the fetch, so the
first person to attach a file was always going to be the first person to find
this.
httpHeaders rather than a query parameter: a token in the URL is a token in the
browser history, in the referrer, and in any log the request crosses, and this is
the endpoint that streams unpublished safety data.
Verified against a real 178-page EMA assessment report - the page renders, a
finding in the rail jumps to the page it cites, and the page says what the
finding says it does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A prepared case arrived with findings and no document. The case file carries a
page number for every finding, and the library manifest knows which document
those pages are in, but the two facts never met - so Read & mark said "No
documents on this case yet" on every case anybody opened, and the reader, which
joins a finding to a page THROUGH a document id, had nothing to join to. Making
one usable meant uploading the file by hand and re-adding every finding against
it.
The source now comes with the case. It goes through the same door a person's own
upload does - documents.upload measures before it accepts - so a scanned or
off-topic source is refused here exactly as it would be on the Evidence stage, by
the same code, with the same reason. Nothing about shipping a file with the
product makes it readable.
A FAILURE HERE DOES NOT LOSE THE CASE. The approval packages are not committed,
so a checkout without them is the normal case, not the exception; a missing file
or a refusal leaves the case opening exactly as it did before, which is still a
working case. There is a test for that path specifically.
DEDUPLICATION HAD TO STOP BEING GLOBAL, and this is what proved it. DocumentStore
keyed uploads by content hash across the whole store, which is right inside a case
- re-sending a file you already sent must not become a second document a second
position can cite - and wrong across them. With the review arriving at open time,
the second person to open the same case uploaded identical bytes and got back the
FIRST person's document, scoped to a case they cannot see: forCase found nothing
for them, and their findings named a document that is not on the case they were
filed under. The key is per case now. That is the same argument the caseId suffix
already makes one level up - a prepared case is a starting point, not a shared
room - and the note at the top of documents.ts, which justified the global
version, says so instead.
The attach test injects the manifest rather than reading disk: the approval
packages are deliberately untracked, so a test that needed the real file would
pass on a developer machine and fail in CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
services/api shells out to measure_pdf.py and extract_pdf_text.py for every
document, and both need PyMuPDF. The interpreter comes from PYTHON and otherwise
defaults to whatever `python` is on PATH - which, on a machine whose system
Python has no pip and cannot get one, is an interpreter that will never have
PyMuPDF in it.
The failure was quiet and pointed at the wrong thing. Every upload came back 422
"unreadable - PyMuPDF is not installed", which reads as a bad DOCUMENT rather
than a missing dependency, and two tests in services/api failed the same way on
any machine without a global install. A green suite and a working upload both
depended on knowing to set an environment variable that nothing mentions.
So both entry points now look for a repo-local .venv and use it when nothing has
already named an interpreter. An explicit PYTHON still wins, and a checkout with
no virtualenv behaves exactly as before - CI installs the requirements globally
and is untouched.
dev-all prints the interpreter it chose, because a silently-chosen one is how the
last one hid.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ask returned `truncated: max_tokens too low` on three of four attempts. The route
answered 502 and the composer showed a bare `upstream` - no answer at all, for a
question retrieval had already served the right pages for.
SHAPE_ASK's 16000 was measured, and it was right for the prompt it was measured
against. What changed is the answer: ask.ts now asks for Markdown, and headings,
bullets and blank lines are more tokens of answer for the same question. Thinking
shares that budget, so the pair crossed the ceiling on any question with a broad
answer - "What liver findings are reported, and at what doses?" against the
264-page Turalio review, every time but one.
This is the fifth time on this project that an output ceiling has been too small
once thinking was counted, and the first where the ceiling did not move but the
output did. The number is SHAPE_SUMMARY's for SHAPE_SUMMARY's reason: the answer
is bounded by the prompt, so the only job left for this ceiling is to stop being
the binding constraint on thinking. A cap is not a reservation - nothing is spent
by raising one, only by generating into it - which is why the fix is always to
raise it and never to tune it.
SHAPE_ADJUDICATION stays at 16000. It has its own measurement - zero truncation
across 10 runs on the probe case - and its input is a bounded case rather than
however many pages retrieval returned.
Measured after: six of six on the question that failed three of four, and the
same question answered in the UI with its headings and citations intact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The library's bodies had a floor in the code and none in the frame: an
additive plane of light bands with no depth write, which over a near-black
ground is very nearly nothing. A rank of standing volumes read as a rank of
floating ones.
It is a heightfield now, ported from the landing page's own library frame
and for the same reason that one has it - an object glowing over nothing is
a shape, and the same object with ground catching its light is a place. Each
case throws a pool on the ground it stands on, azure if it is usable and a
smaller red one if it was refused, so the refusals are legible on the floor
as well as in the object.
Three things the port had to learn on the way across, all in terrain.ts:
emitters carry a colour, because a refused body is the one red thing in this
palette; eight slots rather than four, because the library holds six; and a
dissolve, because the camera flies inside a body and the world outside has
to stop existing. The old floor's sweeping bands moved into the same shader,
so the light now runs over the landform instead of through it.
The bodies stand ON the surface rather than sunk into it, and that is forced
rather than chosen: Atmosphere builds rtC with depthBuffer false and renders
into it whenever a transition is not running, so outside a scene swap nothing
here is depth tested and a buried foot would simply be drawn. Same reason the
ground draws first. Noted in the file, since the scene's own header claims a
depth test it does not get.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two refused entries opened their one line with "REFUSED - " in capitals,
two rows under a card already drawing a red REFUSED badge off the same
usable flag. The badge said twice, and it was the only shouted lead-in on a
page where the other four entries open on a sentence.
What is left does the job every other shape does - what the document is,
then what that costs the reader - so the six read as one set again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
h1-h4 and p are all margin 0 in app.css: this app takes its vertical rhythm
from a container, never from the type. Refused rendered a bare <section> and
is mounted straight into the shell, so it had no container at all - the
title, the red refusal box and all six headings and paragraphs stacked with
literally no space between them, the box wedged against the heading above
and the one below.
.section restores the 16px between blocks. Each heading is paired with its
body in a .stack-s as well, because .section alone spaces every child
equally and that leaves an h3 floating exactly as far from its own paragraph
as from the block before it - the panel reads as three groups now rather
than six loose lines. No new CSS; both classes already existed.
Five more bare sections in this file have the same gap, and the inline
marginTop hacks at 664 and 722 are what standing in for it looks like.
Left for their own change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Developer API's response_schema proto does not carry
additionalProperties, so sending it rejects the REQUEST outright - 400,
Unknown name "additionalProperties" at generation_config.response_schema -
before a model is ever selected. Every AI surface writes that key, so
adjudicate, ask, interpret, extract and navigate all died together on that
host, and handleAdjudicate reporting every upstream fault as a bare 502
made a request that never reached a model look like a model that failed.
Stripped in transport rather than in the schema builders. The constraint is
real where it is honoured: Vertex enforces it, it is what stops a model
returning a field nobody asked for, and the committed numbers were measured
with it in force. Loosening the shared schemas to suit the weaker host would
weaken the stronger one to buy nothing, so the schemas stay strict and the
transport adapts.
Known red: services/api/test/server.test.ts "runs the rest of the flow for
the owner" fails on adjudicate returning 502. It reproduces on origin/main
without this change, so it is not introduced here, but it is landing red and
is worth chasing next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	apps/deliberation/src/App.tsx
#	apps/deliberation/src/read.tsx
#	apps/deliberation/src/shell/Backdrop.tsx
#	apps/deliberation/test/read.test.tsx
A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a714b01)
`POST /api/demo` opens a real library case: it copies the regulatory review off disk,
transcribes its findings and writes the opening links of the hash chain. One of those
runs about 1.4s alone, and "gives each opener their own copy" does the whole thing
TWICE - 3.5s before the machine is doing anything else, against a 5s default.
It passed on either side and failed on the merge of the two, which is the tell: the
suite grew from 881 and 948 tests to 962, the files compete for the same cores, and a
test sitting 1.5s under the limit went 2s over it. Nothing on the path changed - the
failure moved with the SIZE OF THE RUN, which is a limit set too close rather than a
slow path worth chasing.
Stated on the five tests that seed, rather than raised globally: a timeout is a claim
about what a test does, and only these five copy a PDF. Three consecutive full runs
green at 962, then 971 with the markdown tests on top.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7cdf48e4-510d-489b-8a7b-884a5c1cb888

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

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

@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Updated: merged origin/main again to pick up 347c87f (strip additionalProperties for the Developer API host) — the conflict in gemini.ts was two independent additions at the same spot, resolved as a union so both geminiCredentialAdvice and responseSchemaFor survive. 976 tests across 67 files, three builds, CI green.

The merge resolution deleted the ======= and >>>>>>> lines but left the
opening <<<<<<< HEAD. It sat inside the JSDoc for geminiCredentialAdvice,
so it was legal comment text - which is why typecheck, lint and CI all
stayed green on it.
@AndresL230
AndresL230 merged commit f0ff440 into feat/product-in-the-atmosphereAug 17, 2026
3 checks passed
AndresL230 added a commit that referenced this pull request Aug 17, 2026
PR #32 reconciled main into this branch, but its last main merge predated
dd140aa by 26 minutes, so the two newest commits on main never rode along:
dd140aa Serve the verdict from the record, not from the tab that ran it
53e9a5b Give the four remaining bare sections the rhythm the rest of the app has
Without dd140aa the product branch kept the pre-fix adjudication path: the
verdict lived only in the browser of whoever pressed Adjudicate, so Reveal &
verdict was empty for every signed case, every refresh, and every non-owner -
and the Adjudicate button still offered itself on cases that could only answer
409, spending three model calls to find that out. Nine regression tests come
with the fix.
One conflict, in the services/api/server.ts import block, resolved as the union
of both sides: this branch's geminiCredentialAdvice and main's CallKind/Complete
- the seam that keeps server.test.ts from making live paid adjudication calls.
Both symbols are used; dropping either breaks the build.
Verified: typecheck clean, lint clean, 985/985 tests passing across 67 files.
AndresL230 added a commit that referenced this pull request Aug 17, 2026
main moved 42 commits under this branch - #33 (Postgres/Supabase), #24, #29,
#32 and the logo - and rewrote the three files this touches most.
THE RECONCILIATION #33 DEFERRED. `ShareStore` was written against the pattern
this branch had: a synchronous `new ShareStore(path)` beside `AuthStore` and
`InviteStore`. That layer is gone. So the store joins the new one the way the
other four did:
- `ShareStoreApi` in postgres-share.ts, where `AuthStoreApi` and
`InviteStoreApi` live, for the reason stated there: the second
implementation is what needs a name for the shape.
- `ShareStore.open(path)`, async, private constructor.
- `PostgresShareStore`, and supabase/migrations/0002_share_links.sql.
- Both branches of `buildStores`, and `Stores.shares`.
- The behaviour is ONE suite over both implementations
(test/share-store-contract.ts), as with auth and invites.
`shareSecret` deliberately stayed OUT of `buildStores`: which backing holds the
links is a storage decision, whether the deployment can publish at all is not.
THE TABLE HOLDS NO TOKEN, and the migration says so at length because that is
the feature's whole security argument - the URL is
HMAC(secret, "caseId:version"), derivable from two columns that are not secret,
so a stolen dump yields nothing. `publish` omits `version` from its upsert's
SET list on purpose: assigning it would reset a revoked case to 1 and re-mint
the token revoke had just killed. A test reads the column list back out of
information_schema, because that property fails silently.
WHAT THE CONFLICTS COST. server.ts kept main's static serving whole and its
`/api/health` route; the branch's "NO STATIC-FILE SERVING HERE" comment was
true when written and is now a lie, so it is replaced by an accurate one:
`/r/:caseId/:token` is still not served in production, a scanned QR 404s on a
deployed host, and the two decisions that has to wait on are written down
beside `staticRoot()`. Adding it here was out of scope and stayed out.
`handleReport` and `handleShare` became async; `handleReport` fetches the people
in one `auth.list()` rather than a lookup per name, because `auth.get` is
asynchronous now and `buildCaseReport` takes a synchronous `person`.
ONE THING THE MERGE HAD TO DECIDE. docs/HANDOFF-open-prs.md predicted that #30
and main each carried a provenance rule and that they would auto-merge without
conflict. They did, and both were live: `view`'s read any unrecognised actor as
`live`, the report's read only "model" as `live`. Two surfaces describing one
signed safety adjudication, disagreeing about whether a model produced it.
Closed with one module-level `sourceOf`, failing toward `stub`; every writer
passes "stub" or "model" so nothing observable changed, and the test pins that
both readers agree on a third actor neither was written for.
The postgres test fixture now applies every migration in filename order. Pinned
to 0001, it built a database that was correct on the day it was written and
silently lacked share_links.
Verified: lint, typecheck, 1267 tests with DATABASE_URL set (1179 without),
both builds, harness + metrics + golden + an unchanged verdict-manifest, e2e.
The public bundle still carries no auth code: only main's own entry chunk
matches AUTO_PASSWORD or /api/auth/login, and public.html references neither.
HANDOFF-pr34.md is deleted - the job it describes is this commit, and what
happened is recorded in docs/HANDOFF-open-prs.md and the spec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Merge main into the product branch, and finish the Ask markdown fix - #32

Merged
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product
Aug 17, 2026
Merged

Merge main into the product branch, and finish the Ask markdown fix#32
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

Brings all 10 commits from main onto feat/product-in-the-atmosphere, including every Ask/AI fix, and lands the markdown rendering the bug report was about. Supersedes #31.

What arrives from main

  • b84728c - Ask renders Markdown (markdown.tsx) and ask.ts asks the model for structure. Both halves; neither works alone.
  • c78131c - stops Ask truncating now that answers are longer (max_tokens was cutting a 264-page review's answer at 56s).
  • 84bc19c - opening a prepared case brings its document with it.
  • 3e6bc88, 2ae6907, plus the refusal-panel, archive and header work.

The four conflicts, and why they resolved the way they did

All in files both branches had changed independently.

  • read.tsx / read.test.tsx / App.tsx - both branches had independently fixed the same pdf.js 401 (the bearer token never reached /raw). Your branch's version is a superset: it carries that fix and the citation-attribution feature (citers/blind, who cited which finding after reveal). Kept yours, so nothing is lost either way - main's only unique content there was one redundant token test.
  • One genuine merge: main had made token optional because two of its layout tests render <Read> without one. Your side had it required, which would have failed those tests. Resolved by taking main's optional handling plus your citation feature - the header is omitted rather than sent as Bearer undefined.
  • Backdrop.tsx - both branches made the identical two fixes (register from STATES, guard transitionTo); only the comment wording differed.

Markers were stripped hunk-by-hunk keeping the right side, notcheckout --ours, which would have discarded main's auto-merged changes in those same files.

Two follow-on fixes

  • A real hole in the renderer. If an answer arrives with no newlines, the parser turned the whole thing into one giant <h4> - worse than the wall of text reported. Reconstruction is now anchored only to unambiguous markers: a bare - stays a dash, 1. stays part of a number, and it runs only when the answer has no newline anywhere, so well-formed markdown takes an unchanged path. A test carries the reported answer verbatim.
  • A test timeout that only appeared on the merge. Five seeding tests copy a real PDF and write hash-chain links; the two-opener one takes ~3.5s against a 5s default. It passed on either branch alone and failed on the merge because the suite grew to 962 tests and the files compete for cores. Given an explicit budget on the five tests that seed, rather than raised globally.

Verification

  • 971 tests pass across 66 files; three consecutive full runs green before the markdown commit went on top (962/962 each)
  • tsc --noEmit clean on the deliberation app
  • All three workspaces build
  • Confirmed present after merge: markdown.tsx, the ask.ts markdown prompt, the max_tokens fix, citationsFor/citers, and the committed regulatory documents

🤖 Generated with Claude Code

Darkest-Teddyand others added 18 commits August 16, 2026 01:36
Reading Trails, phase 1: a Read & mark tab, and seats that hold still
Pressing Read & mark went dark and stayed dark until a reload.
Backdrop registered five scenes by hand - dashboard, new, library, ask, record
- while packages/atmosphere publishes seven. When Section arrived and sceneFor
started returning "read" for the reading surface, nothing added it here. So
transitionTo was called with an id the engine had never heard of, which throws
by contract (core/Atmosphere.ts). The throw happened inside an effect, and React
unmounts a tree whose effect threw, so a fault in the DECORATION took the whole
product with it.
The same id on a deep link went through mount() instead, inside the loader's
try, and was swallowed - a working product with no scene behind it. That is why
reloading appeared to fix it, and why it looked intermittent: three different
outcomes from one missing table entry, depending on how you arrived.
Registration now reads STATES, which is what apps/atmosphere has always done and
where a new scene actually gets added. That makes the drift impossible rather
than merely repaired. Registering is a map insert and the factory does not run
until something mounts it, so carrying "landing" here costs nothing.
The transition effect is also guarded now. Fixing the catalogue removed this
throw, not the class of them - a driver refusing a shader mid-session arrives at
the same line - and this file's own header says a background may not take the
product down with it. The loader honoured that and the scene change did not.
The fake engine in the test keeps the real class's contract: an unregistered id
throws. A forgiving one would have passed against the broken code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both surfaces put their prose directly on a live WebGL field. Reading is the
longest stretch of text in the product - a document strip, a findings rail, and
the paragraph explaining why a citation could not be placed - and the position
form is a heading, four labels, three explanations and a basis line. Neither had
a ground under any of it.
.glass is the one surface in this product that carries one, and app.css says why
it exists in those words: a low-alpha wash plus a blur that stays readable over a
lit colony without hiding what is behind it. The evidence stage has used it from
the start and reads cleanly over every scene because of it. These two were the
screens that needed it most and were going without.
Waiting is plated as well as the form. That tab BECOMES Waiting once you seal, so
a ground on only one of them would disappear at the moment you submit.
ONE PLATE PER SCREEN, not one per part - app.css puts the blur on containers
only, because a dozen stacked blur layers buy nothing the parent has not already
bought.
The empty reading state is plated too. It is one paragraph and nothing else,
which makes it the state most exposed to the scene: there is no other object on
the screen for the type to sit on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header is fixed and the work scrolls under it, which is the arrangement
Chrome.tsx argues for: a case table with three hundred rows is a document, and a
product that hijacks the wheel to fly a camera is a product nobody can use. The
cost of that arrangement is that the rows pass behind the navigation. On a long
position form the wordmark and the sign-out button sit across the sentence you
are reading.
So it leaves going down and comes back the moment you turn around. DIRECTION,
NOT POSITION: "hide below 400px" would take the navigation away and keep it away
for the length of the table, and scrolling up is the gesture that means give me
the chrome back.
A 6px floor, because a trackpad does not emit one scroll event per gesture - it
emits a stream of one- and two-pixel moves, some of them in the wrong direction,
and without a floor the header flickers the whole way down. Always showing above
96px, roughly its own height plus its inset, where it is not covering anything
yet and hiding would be motion for no reason.
Inert while it is away rather than merely transparent. This holds the main menu
and the only sign-out control, and a tab stop on something translated off the top
of the viewport sends focus somewhere the reader cannot see. One attribute takes
the subtree out of the tab order and swallows pointer events with it.
Transform and opacity only - the two a compositor animates without laying out
again, and there is a WebGL scene drawing behind this. Reduced motion keeps the
behaviour and drops the transition: a reader who asked for less motion still
wants the navigation off their paragraph.
No rAF throttle. The listener is passive and does two comparisons against a
number the browser already has, scroll events are frame-aligned as it is, and the
state only changes on the turn - so React re-renders when the answer changes, not
when the page moves.
Verified with real wheel input rather than scrollTo, which fires no scroll event
at all under CDP - worth knowing before anyone writes an e2e test for this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects on the one screen where being unable to read the interface costs a
reviewer their answer rather than their patience.
THE CALL BUTTONS WERE NOT BUTTONS. `.rail` and `.persona` were dropped by the
redesign and nothing replaced them, so the three-way call - the single most
important control in the product - rendered as "AdvanceDo not advanceCannot
conclude": three words run together, no gap, no border, no pressed state. Agree
and Override on the verdict screen had the same pair and the same result. Both
now use `.choice` and `button.ghost`, which is what every other form here is
built from and already carries hover and aria-pressed.
THE TEXTAREA HAD VANISHED INTO ITS OWN PANEL. The form rule says a field is
"darker than paper", and it was not: paper carries 0.50 alpha and the field
carried 0.46, so the field covered LESS of what was behind it than the plate did.
Over a scene that was survivable by accident. On a plate the two composite to
within a couple of values of each other and there was no box on the screen at all
- only a slightly different dark where the caret was supposed to go. 0.78 now,
and the rim goes from 0.15 to 0.26, because a field has to be an object somebody
can see the extent of before they click into it.
The prose moves onto the field rhythm the rest of the product uses: labels,
controls and their explanations in `.field` with `.hint`, the two halves of an
outside claim spaced by rule rather than by an inline style no stylesheet could
reach, and the basis line ruled off above the button because it summarises every
answer rather than asking for one more.
A GUARD FOR THE CLASS OF BUG. A className with no rule does not throw, does not
warn, and does not fail a type check - it renders as an unstyled element that
looks like a mistake nobody made. The stylesheet test now reads every plain
className out of the components and fails on any the stylesheet has no rule for.
Run against the old markup it names `rail` and `persona` exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read & mark could never open a PDF. It showed "Failed to fetch", and behind that
was a 401 on /api/cases/:id/documents/:docId/raw.
pdf.js does not go through api.ts. It is handed a URL and issues its OWN request,
so the bearer token this app keeps in memory never reached the one endpoint that
serves a document's bytes - the endpoint every other call authenticates against
without trouble.
It hid for as long as it did because no case had a document on it. The screen
said "No documents on this case yet" and nothing ever reached the fetch, so the
first person to attach a file was always going to be the first person to find
this.
httpHeaders rather than a query parameter: a token in the URL is a token in the
browser history, in the referrer, and in any log the request crosses, and this is
the endpoint that streams unpublished safety data.
Verified against a real 178-page EMA assessment report - the page renders, a
finding in the rail jumps to the page it cites, and the page says what the
finding says it does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A prepared case arrived with findings and no document. The case file carries a
page number for every finding, and the library manifest knows which document
those pages are in, but the two facts never met - so Read & mark said "No
documents on this case yet" on every case anybody opened, and the reader, which
joins a finding to a page THROUGH a document id, had nothing to join to. Making
one usable meant uploading the file by hand and re-adding every finding against
it.
The source now comes with the case. It goes through the same door a person's own
upload does - documents.upload measures before it accepts - so a scanned or
off-topic source is refused here exactly as it would be on the Evidence stage, by
the same code, with the same reason. Nothing about shipping a file with the
product makes it readable.
A FAILURE HERE DOES NOT LOSE THE CASE. The approval packages are not committed,
so a checkout without them is the normal case, not the exception; a missing file
or a refusal leaves the case opening exactly as it did before, which is still a
working case. There is a test for that path specifically.
DEDUPLICATION HAD TO STOP BEING GLOBAL, and this is what proved it. DocumentStore
keyed uploads by content hash across the whole store, which is right inside a case
- re-sending a file you already sent must not become a second document a second
position can cite - and wrong across them. With the review arriving at open time,
the second person to open the same case uploaded identical bytes and got back the
FIRST person's document, scoped to a case they cannot see: forCase found nothing
for them, and their findings named a document that is not on the case they were
filed under. The key is per case now. That is the same argument the caseId suffix
already makes one level up - a prepared case is a starting point, not a shared
room - and the note at the top of documents.ts, which justified the global
version, says so instead.
The attach test injects the manifest rather than reading disk: the approval
packages are deliberately untracked, so a test that needed the real file would
pass on a developer machine and fail in CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
services/api shells out to measure_pdf.py and extract_pdf_text.py for every
document, and both need PyMuPDF. The interpreter comes from PYTHON and otherwise
defaults to whatever `python` is on PATH - which, on a machine whose system
Python has no pip and cannot get one, is an interpreter that will never have
PyMuPDF in it.
The failure was quiet and pointed at the wrong thing. Every upload came back 422
"unreadable - PyMuPDF is not installed", which reads as a bad DOCUMENT rather
than a missing dependency, and two tests in services/api failed the same way on
any machine without a global install. A green suite and a working upload both
depended on knowing to set an environment variable that nothing mentions.
So both entry points now look for a repo-local .venv and use it when nothing has
already named an interpreter. An explicit PYTHON still wins, and a checkout with
no virtualenv behaves exactly as before - CI installs the requirements globally
and is untouched.
dev-all prints the interpreter it chose, because a silently-chosen one is how the
last one hid.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ask returned `truncated: max_tokens too low` on three of four attempts. The route
answered 502 and the composer showed a bare `upstream` - no answer at all, for a
question retrieval had already served the right pages for.
SHAPE_ASK's 16000 was measured, and it was right for the prompt it was measured
against. What changed is the answer: ask.ts now asks for Markdown, and headings,
bullets and blank lines are more tokens of answer for the same question. Thinking
shares that budget, so the pair crossed the ceiling on any question with a broad
answer - "What liver findings are reported, and at what doses?" against the
264-page Turalio review, every time but one.
This is the fifth time on this project that an output ceiling has been too small
once thinking was counted, and the first where the ceiling did not move but the
output did. The number is SHAPE_SUMMARY's for SHAPE_SUMMARY's reason: the answer
is bounded by the prompt, so the only job left for this ceiling is to stop being
the binding constraint on thinking. A cap is not a reservation - nothing is spent
by raising one, only by generating into it - which is why the fix is always to
raise it and never to tune it.
SHAPE_ADJUDICATION stays at 16000. It has its own measurement - zero truncation
across 10 runs on the probe case - and its input is a bounded case rather than
however many pages retrieval returned.
Measured after: six of six on the question that failed three of four, and the
same question answered in the UI with its headings and citations intact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The library's bodies had a floor in the code and none in the frame: an
additive plane of light bands with no depth write, which over a near-black
ground is very nearly nothing. A rank of standing volumes read as a rank of
floating ones.
It is a heightfield now, ported from the landing page's own library frame
and for the same reason that one has it - an object glowing over nothing is
a shape, and the same object with ground catching its light is a place. Each
case throws a pool on the ground it stands on, azure if it is usable and a
smaller red one if it was refused, so the refusals are legible on the floor
as well as in the object.
Three things the port had to learn on the way across, all in terrain.ts:
emitters carry a colour, because a refused body is the one red thing in this
palette; eight slots rather than four, because the library holds six; and a
dissolve, because the camera flies inside a body and the world outside has
to stop existing. The old floor's sweeping bands moved into the same shader,
so the light now runs over the landform instead of through it.
The bodies stand ON the surface rather than sunk into it, and that is forced
rather than chosen: Atmosphere builds rtC with depthBuffer false and renders
into it whenever a transition is not running, so outside a scene swap nothing
here is depth tested and a buried foot would simply be drawn. Same reason the
ground draws first. Noted in the file, since the scene's own header claims a
depth test it does not get.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two refused entries opened their one line with "REFUSED - " in capitals,
two rows under a card already drawing a red REFUSED badge off the same
usable flag. The badge said twice, and it was the only shouted lead-in on a
page where the other four entries open on a sentence.
What is left does the job every other shape does - what the document is,
then what that costs the reader - so the six read as one set again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
h1-h4 and p are all margin 0 in app.css: this app takes its vertical rhythm
from a container, never from the type. Refused rendered a bare <section> and
is mounted straight into the shell, so it had no container at all - the
title, the red refusal box and all six headings and paragraphs stacked with
literally no space between them, the box wedged against the heading above
and the one below.
.section restores the 16px between blocks. Each heading is paired with its
body in a .stack-s as well, because .section alone spaces every child
equally and that leaves an h3 floating exactly as far from its own paragraph
as from the block before it - the panel reads as three groups now rather
than six loose lines. No new CSS; both classes already existed.
Five more bare sections in this file have the same gap, and the inline
marginTop hacks at 664 and 722 are what standing in for it looks like.
Left for their own change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Developer API's response_schema proto does not carry
additionalProperties, so sending it rejects the REQUEST outright - 400,
Unknown name "additionalProperties" at generation_config.response_schema -
before a model is ever selected. Every AI surface writes that key, so
adjudicate, ask, interpret, extract and navigate all died together on that
host, and handleAdjudicate reporting every upstream fault as a bare 502
made a request that never reached a model look like a model that failed.
Stripped in transport rather than in the schema builders. The constraint is
real where it is honoured: Vertex enforces it, it is what stops a model
returning a field nobody asked for, and the committed numbers were measured
with it in force. Loosening the shared schemas to suit the weaker host would
weaken the stronger one to buy nothing, so the schemas stay strict and the
transport adapts.
Known red: services/api/test/server.test.ts "runs the rest of the flow for
the owner" fails on adjudicate returning 502. It reproduces on origin/main
without this change, so it is not introduced here, but it is landing red and
is worth chasing next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	apps/deliberation/src/App.tsx
#	apps/deliberation/src/read.tsx
#	apps/deliberation/src/shell/Backdrop.tsx
#	apps/deliberation/test/read.test.tsx
A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a714b01)
`POST /api/demo` opens a real library case: it copies the regulatory review off disk,
transcribes its findings and writes the opening links of the hash chain. One of those
runs about 1.4s alone, and "gives each opener their own copy" does the whole thing
TWICE - 3.5s before the machine is doing anything else, against a 5s default.
It passed on either side and failed on the merge of the two, which is the tell: the
suite grew from 881 and 948 tests to 962, the files compete for the same cores, and a
test sitting 1.5s under the limit went 2s over it. Nothing on the path changed - the
failure moved with the SIZE OF THE RUN, which is a limit set too close rather than a
slow path worth chasing.
Stated on the five tests that seed, rather than raised globally: a timeout is a claim
about what a test does, and only these five copy a PDF. Three consecutive full runs
green at 962, then 971 with the markdown tests on top.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7cdf48e4-510d-489b-8a7b-884a5c1cb888

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

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

@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Updated: merged origin/main again to pick up 347c87f (strip additionalProperties for the Developer API host) — the conflict in gemini.ts was two independent additions at the same spot, resolved as a union so both geminiCredentialAdvice and responseSchemaFor survive. 976 tests across 67 files, three builds, CI green.

The merge resolution deleted the ======= and >>>>>>> lines but left the
opening <<<<<<< HEAD. It sat inside the JSDoc for geminiCredentialAdvice,
so it was legal comment text - which is why typecheck, lint and CI all
stayed green on it.
@AndresL230
AndresL230 merged commit f0ff440 into feat/product-in-the-atmosphereAug 17, 2026
3 checks passed
AndresL230 added a commit that referenced this pull request Aug 17, 2026
PR #32 reconciled main into this branch, but its last main merge predated
dd140aa by 26 minutes, so the two newest commits on main never rode along:
dd140aa Serve the verdict from the record, not from the tab that ran it
53e9a5b Give the four remaining bare sections the rhythm the rest of the app has
Without dd140aa the product branch kept the pre-fix adjudication path: the
verdict lived only in the browser of whoever pressed Adjudicate, so Reveal &
verdict was empty for every signed case, every refresh, and every non-owner -
and the Adjudicate button still offered itself on cases that could only answer
409, spending three model calls to find that out. Nine regression tests come
with the fix.
One conflict, in the services/api/server.ts import block, resolved as the union
of both sides: this branch's geminiCredentialAdvice and main's CallKind/Complete
- the seam that keeps server.test.ts from making live paid adjudication calls.
Both symbols are used; dropping either breaks the build.
Verified: typecheck clean, lint clean, 985/985 tests passing across 67 files.
AndresL230 added a commit that referenced this pull request Aug 17, 2026
main moved 42 commits under this branch - #33 (Postgres/Supabase), #24, #29,
#32 and the logo - and rewrote the three files this touches most.
THE RECONCILIATION #33 DEFERRED. `ShareStore` was written against the pattern
this branch had: a synchronous `new ShareStore(path)` beside `AuthStore` and
`InviteStore`. That layer is gone. So the store joins the new one the way the
other four did:
- `ShareStoreApi` in postgres-share.ts, where `AuthStoreApi` and
`InviteStoreApi` live, for the reason stated there: the second
implementation is what needs a name for the shape.
- `ShareStore.open(path)`, async, private constructor.
- `PostgresShareStore`, and supabase/migrations/0002_share_links.sql.
- Both branches of `buildStores`, and `Stores.shares`.
- The behaviour is ONE suite over both implementations
(test/share-store-contract.ts), as with auth and invites.
`shareSecret` deliberately stayed OUT of `buildStores`: which backing holds the
links is a storage decision, whether the deployment can publish at all is not.
THE TABLE HOLDS NO TOKEN, and the migration says so at length because that is
the feature's whole security argument - the URL is
HMAC(secret, "caseId:version"), derivable from two columns that are not secret,
so a stolen dump yields nothing. `publish` omits `version` from its upsert's
SET list on purpose: assigning it would reset a revoked case to 1 and re-mint
the token revoke had just killed. A test reads the column list back out of
information_schema, because that property fails silently.
WHAT THE CONFLICTS COST. server.ts kept main's static serving whole and its
`/api/health` route; the branch's "NO STATIC-FILE SERVING HERE" comment was
true when written and is now a lie, so it is replaced by an accurate one:
`/r/:caseId/:token` is still not served in production, a scanned QR 404s on a
deployed host, and the two decisions that has to wait on are written down
beside `staticRoot()`. Adding it here was out of scope and stayed out.
`handleReport` and `handleShare` became async; `handleReport` fetches the people
in one `auth.list()` rather than a lookup per name, because `auth.get` is
asynchronous now and `buildCaseReport` takes a synchronous `person`.
ONE THING THE MERGE HAD TO DECIDE. docs/HANDOFF-open-prs.md predicted that #30
and main each carried a provenance rule and that they would auto-merge without
conflict. They did, and both were live: `view`'s read any unrecognised actor as
`live`, the report's read only "model" as `live`. Two surfaces describing one
signed safety adjudication, disagreeing about whether a model produced it.
Closed with one module-level `sourceOf`, failing toward `stub`; every writer
passes "stub" or "model" so nothing observable changed, and the test pins that
both readers agree on a third actor neither was written for.
The postgres test fixture now applies every migration in filename order. Pinned
to 0001, it built a database that was correct on the day it was written and
silently lacked share_links.
Verified: lint, typecheck, 1267 tests with DATABASE_URL set (1179 without),
both builds, harness + metrics + golden + an unchanged verdict-manifest, e2e.
The public bundle still carries no auth code: only main's own entry chunk
matches AUTO_PASSWORD or /api/auth/login, and public.html references neither.
HANDOFF-pr34.md is deleted - the job it describes is this commit, and what
happened is recorded in docs/HANDOFF-open-prs.md and the spec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Merge main into the product branch, and finish the Ask markdown fix - #32

Merged
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product
Aug 17, 2026
Merged

Merge main into the product branch, and finish the Ask markdown fix#32
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

Brings all 10 commits from main onto feat/product-in-the-atmosphere, including every Ask/AI fix, and lands the markdown rendering the bug report was about. Supersedes #31.

What arrives from main

  • b84728c - Ask renders Markdown (markdown.tsx) and ask.ts asks the model for structure. Both halves; neither works alone.
  • c78131c - stops Ask truncating now that answers are longer (max_tokens was cutting a 264-page review's answer at 56s).
  • 84bc19c - opening a prepared case brings its document with it.
  • 3e6bc88, 2ae6907, plus the refusal-panel, archive and header work.

The four conflicts, and why they resolved the way they did

All in files both branches had changed independently.

  • read.tsx / read.test.tsx / App.tsx - both branches had independently fixed the same pdf.js 401 (the bearer token never reached /raw). Your branch's version is a superset: it carries that fix and the citation-attribution feature (citers/blind, who cited which finding after reveal). Kept yours, so nothing is lost either way - main's only unique content there was one redundant token test.
  • One genuine merge: main had made token optional because two of its layout tests render <Read> without one. Your side had it required, which would have failed those tests. Resolved by taking main's optional handling plus your citation feature - the header is omitted rather than sent as Bearer undefined.
  • Backdrop.tsx - both branches made the identical two fixes (register from STATES, guard transitionTo); only the comment wording differed.

Markers were stripped hunk-by-hunk keeping the right side, notcheckout --ours, which would have discarded main's auto-merged changes in those same files.

Two follow-on fixes

  • A real hole in the renderer. If an answer arrives with no newlines, the parser turned the whole thing into one giant <h4> - worse than the wall of text reported. Reconstruction is now anchored only to unambiguous markers: a bare - stays a dash, 1. stays part of a number, and it runs only when the answer has no newline anywhere, so well-formed markdown takes an unchanged path. A test carries the reported answer verbatim.
  • A test timeout that only appeared on the merge. Five seeding tests copy a real PDF and write hash-chain links; the two-opener one takes ~3.5s against a 5s default. It passed on either branch alone and failed on the merge because the suite grew to 962 tests and the files compete for cores. Given an explicit budget on the five tests that seed, rather than raised globally.

Verification

  • 971 tests pass across 66 files; three consecutive full runs green before the markdown commit went on top (962/962 each)
  • tsc --noEmit clean on the deliberation app
  • All three workspaces build
  • Confirmed present after merge: markdown.tsx, the ask.ts markdown prompt, the max_tokens fix, citationsFor/citers, and the committed regulatory documents

🤖 Generated with Claude Code

Darkest-Teddyand others added 18 commits August 16, 2026 01:36
Reading Trails, phase 1: a Read & mark tab, and seats that hold still
Pressing Read & mark went dark and stayed dark until a reload.
Backdrop registered five scenes by hand - dashboard, new, library, ask, record
- while packages/atmosphere publishes seven. When Section arrived and sceneFor
started returning "read" for the reading surface, nothing added it here. So
transitionTo was called with an id the engine had never heard of, which throws
by contract (core/Atmosphere.ts). The throw happened inside an effect, and React
unmounts a tree whose effect threw, so a fault in the DECORATION took the whole
product with it.
The same id on a deep link went through mount() instead, inside the loader's
try, and was swallowed - a working product with no scene behind it. That is why
reloading appeared to fix it, and why it looked intermittent: three different
outcomes from one missing table entry, depending on how you arrived.
Registration now reads STATES, which is what apps/atmosphere has always done and
where a new scene actually gets added. That makes the drift impossible rather
than merely repaired. Registering is a map insert and the factory does not run
until something mounts it, so carrying "landing" here costs nothing.
The transition effect is also guarded now. Fixing the catalogue removed this
throw, not the class of them - a driver refusing a shader mid-session arrives at
the same line - and this file's own header says a background may not take the
product down with it. The loader honoured that and the scene change did not.
The fake engine in the test keeps the real class's contract: an unregistered id
throws. A forgiving one would have passed against the broken code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both surfaces put their prose directly on a live WebGL field. Reading is the
longest stretch of text in the product - a document strip, a findings rail, and
the paragraph explaining why a citation could not be placed - and the position
form is a heading, four labels, three explanations and a basis line. Neither had
a ground under any of it.
.glass is the one surface in this product that carries one, and app.css says why
it exists in those words: a low-alpha wash plus a blur that stays readable over a
lit colony without hiding what is behind it. The evidence stage has used it from
the start and reads cleanly over every scene because of it. These two were the
screens that needed it most and were going without.
Waiting is plated as well as the form. That tab BECOMES Waiting once you seal, so
a ground on only one of them would disappear at the moment you submit.
ONE PLATE PER SCREEN, not one per part - app.css puts the blur on containers
only, because a dozen stacked blur layers buy nothing the parent has not already
bought.
The empty reading state is plated too. It is one paragraph and nothing else,
which makes it the state most exposed to the scene: there is no other object on
the screen for the type to sit on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header is fixed and the work scrolls under it, which is the arrangement
Chrome.tsx argues for: a case table with three hundred rows is a document, and a
product that hijacks the wheel to fly a camera is a product nobody can use. The
cost of that arrangement is that the rows pass behind the navigation. On a long
position form the wordmark and the sign-out button sit across the sentence you
are reading.
So it leaves going down and comes back the moment you turn around. DIRECTION,
NOT POSITION: "hide below 400px" would take the navigation away and keep it away
for the length of the table, and scrolling up is the gesture that means give me
the chrome back.
A 6px floor, because a trackpad does not emit one scroll event per gesture - it
emits a stream of one- and two-pixel moves, some of them in the wrong direction,
and without a floor the header flickers the whole way down. Always showing above
96px, roughly its own height plus its inset, where it is not covering anything
yet and hiding would be motion for no reason.
Inert while it is away rather than merely transparent. This holds the main menu
and the only sign-out control, and a tab stop on something translated off the top
of the viewport sends focus somewhere the reader cannot see. One attribute takes
the subtree out of the tab order and swallows pointer events with it.
Transform and opacity only - the two a compositor animates without laying out
again, and there is a WebGL scene drawing behind this. Reduced motion keeps the
behaviour and drops the transition: a reader who asked for less motion still
wants the navigation off their paragraph.
No rAF throttle. The listener is passive and does two comparisons against a
number the browser already has, scroll events are frame-aligned as it is, and the
state only changes on the turn - so React re-renders when the answer changes, not
when the page moves.
Verified with real wheel input rather than scrollTo, which fires no scroll event
at all under CDP - worth knowing before anyone writes an e2e test for this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects on the one screen where being unable to read the interface costs a
reviewer their answer rather than their patience.
THE CALL BUTTONS WERE NOT BUTTONS. `.rail` and `.persona` were dropped by the
redesign and nothing replaced them, so the three-way call - the single most
important control in the product - rendered as "AdvanceDo not advanceCannot
conclude": three words run together, no gap, no border, no pressed state. Agree
and Override on the verdict screen had the same pair and the same result. Both
now use `.choice` and `button.ghost`, which is what every other form here is
built from and already carries hover and aria-pressed.
THE TEXTAREA HAD VANISHED INTO ITS OWN PANEL. The form rule says a field is
"darker than paper", and it was not: paper carries 0.50 alpha and the field
carried 0.46, so the field covered LESS of what was behind it than the plate did.
Over a scene that was survivable by accident. On a plate the two composite to
within a couple of values of each other and there was no box on the screen at all
- only a slightly different dark where the caret was supposed to go. 0.78 now,
and the rim goes from 0.15 to 0.26, because a field has to be an object somebody
can see the extent of before they click into it.
The prose moves onto the field rhythm the rest of the product uses: labels,
controls and their explanations in `.field` with `.hint`, the two halves of an
outside claim spaced by rule rather than by an inline style no stylesheet could
reach, and the basis line ruled off above the button because it summarises every
answer rather than asking for one more.
A GUARD FOR THE CLASS OF BUG. A className with no rule does not throw, does not
warn, and does not fail a type check - it renders as an unstyled element that
looks like a mistake nobody made. The stylesheet test now reads every plain
className out of the components and fails on any the stylesheet has no rule for.
Run against the old markup it names `rail` and `persona` exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read & mark could never open a PDF. It showed "Failed to fetch", and behind that
was a 401 on /api/cases/:id/documents/:docId/raw.
pdf.js does not go through api.ts. It is handed a URL and issues its OWN request,
so the bearer token this app keeps in memory never reached the one endpoint that
serves a document's bytes - the endpoint every other call authenticates against
without trouble.
It hid for as long as it did because no case had a document on it. The screen
said "No documents on this case yet" and nothing ever reached the fetch, so the
first person to attach a file was always going to be the first person to find
this.
httpHeaders rather than a query parameter: a token in the URL is a token in the
browser history, in the referrer, and in any log the request crosses, and this is
the endpoint that streams unpublished safety data.
Verified against a real 178-page EMA assessment report - the page renders, a
finding in the rail jumps to the page it cites, and the page says what the
finding says it does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A prepared case arrived with findings and no document. The case file carries a
page number for every finding, and the library manifest knows which document
those pages are in, but the two facts never met - so Read & mark said "No
documents on this case yet" on every case anybody opened, and the reader, which
joins a finding to a page THROUGH a document id, had nothing to join to. Making
one usable meant uploading the file by hand and re-adding every finding against
it.
The source now comes with the case. It goes through the same door a person's own
upload does - documents.upload measures before it accepts - so a scanned or
off-topic source is refused here exactly as it would be on the Evidence stage, by
the same code, with the same reason. Nothing about shipping a file with the
product makes it readable.
A FAILURE HERE DOES NOT LOSE THE CASE. The approval packages are not committed,
so a checkout without them is the normal case, not the exception; a missing file
or a refusal leaves the case opening exactly as it did before, which is still a
working case. There is a test for that path specifically.
DEDUPLICATION HAD TO STOP BEING GLOBAL, and this is what proved it. DocumentStore
keyed uploads by content hash across the whole store, which is right inside a case
- re-sending a file you already sent must not become a second document a second
position can cite - and wrong across them. With the review arriving at open time,
the second person to open the same case uploaded identical bytes and got back the
FIRST person's document, scoped to a case they cannot see: forCase found nothing
for them, and their findings named a document that is not on the case they were
filed under. The key is per case now. That is the same argument the caseId suffix
already makes one level up - a prepared case is a starting point, not a shared
room - and the note at the top of documents.ts, which justified the global
version, says so instead.
The attach test injects the manifest rather than reading disk: the approval
packages are deliberately untracked, so a test that needed the real file would
pass on a developer machine and fail in CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
services/api shells out to measure_pdf.py and extract_pdf_text.py for every
document, and both need PyMuPDF. The interpreter comes from PYTHON and otherwise
defaults to whatever `python` is on PATH - which, on a machine whose system
Python has no pip and cannot get one, is an interpreter that will never have
PyMuPDF in it.
The failure was quiet and pointed at the wrong thing. Every upload came back 422
"unreadable - PyMuPDF is not installed", which reads as a bad DOCUMENT rather
than a missing dependency, and two tests in services/api failed the same way on
any machine without a global install. A green suite and a working upload both
depended on knowing to set an environment variable that nothing mentions.
So both entry points now look for a repo-local .venv and use it when nothing has
already named an interpreter. An explicit PYTHON still wins, and a checkout with
no virtualenv behaves exactly as before - CI installs the requirements globally
and is untouched.
dev-all prints the interpreter it chose, because a silently-chosen one is how the
last one hid.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ask returned `truncated: max_tokens too low` on three of four attempts. The route
answered 502 and the composer showed a bare `upstream` - no answer at all, for a
question retrieval had already served the right pages for.
SHAPE_ASK's 16000 was measured, and it was right for the prompt it was measured
against. What changed is the answer: ask.ts now asks for Markdown, and headings,
bullets and blank lines are more tokens of answer for the same question. Thinking
shares that budget, so the pair crossed the ceiling on any question with a broad
answer - "What liver findings are reported, and at what doses?" against the
264-page Turalio review, every time but one.
This is the fifth time on this project that an output ceiling has been too small
once thinking was counted, and the first where the ceiling did not move but the
output did. The number is SHAPE_SUMMARY's for SHAPE_SUMMARY's reason: the answer
is bounded by the prompt, so the only job left for this ceiling is to stop being
the binding constraint on thinking. A cap is not a reservation - nothing is spent
by raising one, only by generating into it - which is why the fix is always to
raise it and never to tune it.
SHAPE_ADJUDICATION stays at 16000. It has its own measurement - zero truncation
across 10 runs on the probe case - and its input is a bounded case rather than
however many pages retrieval returned.
Measured after: six of six on the question that failed three of four, and the
same question answered in the UI with its headings and citations intact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The library's bodies had a floor in the code and none in the frame: an
additive plane of light bands with no depth write, which over a near-black
ground is very nearly nothing. A rank of standing volumes read as a rank of
floating ones.
It is a heightfield now, ported from the landing page's own library frame
and for the same reason that one has it - an object glowing over nothing is
a shape, and the same object with ground catching its light is a place. Each
case throws a pool on the ground it stands on, azure if it is usable and a
smaller red one if it was refused, so the refusals are legible on the floor
as well as in the object.
Three things the port had to learn on the way across, all in terrain.ts:
emitters carry a colour, because a refused body is the one red thing in this
palette; eight slots rather than four, because the library holds six; and a
dissolve, because the camera flies inside a body and the world outside has
to stop existing. The old floor's sweeping bands moved into the same shader,
so the light now runs over the landform instead of through it.
The bodies stand ON the surface rather than sunk into it, and that is forced
rather than chosen: Atmosphere builds rtC with depthBuffer false and renders
into it whenever a transition is not running, so outside a scene swap nothing
here is depth tested and a buried foot would simply be drawn. Same reason the
ground draws first. Noted in the file, since the scene's own header claims a
depth test it does not get.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two refused entries opened their one line with "REFUSED - " in capitals,
two rows under a card already drawing a red REFUSED badge off the same
usable flag. The badge said twice, and it was the only shouted lead-in on a
page where the other four entries open on a sentence.
What is left does the job every other shape does - what the document is,
then what that costs the reader - so the six read as one set again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
h1-h4 and p are all margin 0 in app.css: this app takes its vertical rhythm
from a container, never from the type. Refused rendered a bare <section> and
is mounted straight into the shell, so it had no container at all - the
title, the red refusal box and all six headings and paragraphs stacked with
literally no space between them, the box wedged against the heading above
and the one below.
.section restores the 16px between blocks. Each heading is paired with its
body in a .stack-s as well, because .section alone spaces every child
equally and that leaves an h3 floating exactly as far from its own paragraph
as from the block before it - the panel reads as three groups now rather
than six loose lines. No new CSS; both classes already existed.
Five more bare sections in this file have the same gap, and the inline
marginTop hacks at 664 and 722 are what standing in for it looks like.
Left for their own change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Developer API's response_schema proto does not carry
additionalProperties, so sending it rejects the REQUEST outright - 400,
Unknown name "additionalProperties" at generation_config.response_schema -
before a model is ever selected. Every AI surface writes that key, so
adjudicate, ask, interpret, extract and navigate all died together on that
host, and handleAdjudicate reporting every upstream fault as a bare 502
made a request that never reached a model look like a model that failed.
Stripped in transport rather than in the schema builders. The constraint is
real where it is honoured: Vertex enforces it, it is what stops a model
returning a field nobody asked for, and the committed numbers were measured
with it in force. Loosening the shared schemas to suit the weaker host would
weaken the stronger one to buy nothing, so the schemas stay strict and the
transport adapts.
Known red: services/api/test/server.test.ts "runs the rest of the flow for
the owner" fails on adjudicate returning 502. It reproduces on origin/main
without this change, so it is not introduced here, but it is landing red and
is worth chasing next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	apps/deliberation/src/App.tsx
#	apps/deliberation/src/read.tsx
#	apps/deliberation/src/shell/Backdrop.tsx
#	apps/deliberation/test/read.test.tsx
A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a714b01)
`POST /api/demo` opens a real library case: it copies the regulatory review off disk,
transcribes its findings and writes the opening links of the hash chain. One of those
runs about 1.4s alone, and "gives each opener their own copy" does the whole thing
TWICE - 3.5s before the machine is doing anything else, against a 5s default.
It passed on either side and failed on the merge of the two, which is the tell: the
suite grew from 881 and 948 tests to 962, the files compete for the same cores, and a
test sitting 1.5s under the limit went 2s over it. Nothing on the path changed - the
failure moved with the SIZE OF THE RUN, which is a limit set too close rather than a
slow path worth chasing.
Stated on the five tests that seed, rather than raised globally: a timeout is a claim
about what a test does, and only these five copy a PDF. Three consecutive full runs
green at 962, then 971 with the markdown tests on top.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7cdf48e4-510d-489b-8a7b-884a5c1cb888

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

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

@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Updated: merged origin/main again to pick up 347c87f (strip additionalProperties for the Developer API host) — the conflict in gemini.ts was two independent additions at the same spot, resolved as a union so both geminiCredentialAdvice and responseSchemaFor survive. 976 tests across 67 files, three builds, CI green.

The merge resolution deleted the ======= and >>>>>>> lines but left the
opening <<<<<<< HEAD. It sat inside the JSDoc for geminiCredentialAdvice,
so it was legal comment text - which is why typecheck, lint and CI all
stayed green on it.
@AndresL230
AndresL230 merged commit f0ff440 into feat/product-in-the-atmosphereAug 17, 2026
3 checks passed
AndresL230 added a commit that referenced this pull request Aug 17, 2026
PR #32 reconciled main into this branch, but its last main merge predated
dd140aa by 26 minutes, so the two newest commits on main never rode along:
dd140aa Serve the verdict from the record, not from the tab that ran it
53e9a5b Give the four remaining bare sections the rhythm the rest of the app has
Without dd140aa the product branch kept the pre-fix adjudication path: the
verdict lived only in the browser of whoever pressed Adjudicate, so Reveal &
verdict was empty for every signed case, every refresh, and every non-owner -
and the Adjudicate button still offered itself on cases that could only answer
409, spending three model calls to find that out. Nine regression tests come
with the fix.
One conflict, in the services/api/server.ts import block, resolved as the union
of both sides: this branch's geminiCredentialAdvice and main's CallKind/Complete
- the seam that keeps server.test.ts from making live paid adjudication calls.
Both symbols are used; dropping either breaks the build.
Verified: typecheck clean, lint clean, 985/985 tests passing across 67 files.
AndresL230 added a commit that referenced this pull request Aug 17, 2026
main moved 42 commits under this branch - #33 (Postgres/Supabase), #24, #29,
#32 and the logo - and rewrote the three files this touches most.
THE RECONCILIATION #33 DEFERRED. `ShareStore` was written against the pattern
this branch had: a synchronous `new ShareStore(path)` beside `AuthStore` and
`InviteStore`. That layer is gone. So the store joins the new one the way the
other four did:
- `ShareStoreApi` in postgres-share.ts, where `AuthStoreApi` and
`InviteStoreApi` live, for the reason stated there: the second
implementation is what needs a name for the shape.
- `ShareStore.open(path)`, async, private constructor.
- `PostgresShareStore`, and supabase/migrations/0002_share_links.sql.
- Both branches of `buildStores`, and `Stores.shares`.
- The behaviour is ONE suite over both implementations
(test/share-store-contract.ts), as with auth and invites.
`shareSecret` deliberately stayed OUT of `buildStores`: which backing holds the
links is a storage decision, whether the deployment can publish at all is not.
THE TABLE HOLDS NO TOKEN, and the migration says so at length because that is
the feature's whole security argument - the URL is
HMAC(secret, "caseId:version"), derivable from two columns that are not secret,
so a stolen dump yields nothing. `publish` omits `version` from its upsert's
SET list on purpose: assigning it would reset a revoked case to 1 and re-mint
the token revoke had just killed. A test reads the column list back out of
information_schema, because that property fails silently.
WHAT THE CONFLICTS COST. server.ts kept main's static serving whole and its
`/api/health` route; the branch's "NO STATIC-FILE SERVING HERE" comment was
true when written and is now a lie, so it is replaced by an accurate one:
`/r/:caseId/:token` is still not served in production, a scanned QR 404s on a
deployed host, and the two decisions that has to wait on are written down
beside `staticRoot()`. Adding it here was out of scope and stayed out.
`handleReport` and `handleShare` became async; `handleReport` fetches the people
in one `auth.list()` rather than a lookup per name, because `auth.get` is
asynchronous now and `buildCaseReport` takes a synchronous `person`.
ONE THING THE MERGE HAD TO DECIDE. docs/HANDOFF-open-prs.md predicted that #30
and main each carried a provenance rule and that they would auto-merge without
conflict. They did, and both were live: `view`'s read any unrecognised actor as
`live`, the report's read only "model" as `live`. Two surfaces describing one
signed safety adjudication, disagreeing about whether a model produced it.
Closed with one module-level `sourceOf`, failing toward `stub`; every writer
passes "stub" or "model" so nothing observable changed, and the test pins that
both readers agree on a third actor neither was written for.
The postgres test fixture now applies every migration in filename order. Pinned
to 0001, it built a database that was correct on the day it was written and
silently lacked share_links.
Verified: lint, typecheck, 1267 tests with DATABASE_URL set (1179 without),
both builds, harness + metrics + golden + an unchanged verdict-manifest, e2e.
The public bundle still carries no auth code: only main's own entry chunk
matches AUTO_PASSWORD or /api/auth/login, and public.html references neither.
HANDOFF-pr34.md is deleted - the job it describes is this commit, and what
happened is recorded in docs/HANDOFF-open-prs.md and the spec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Merge main into the product branch, and finish the Ask markdown fix - #32

Merged
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product
Aug 17, 2026
Merged

Merge main into the product branch, and finish the Ask markdown fix#32
AndresL230 merged 20 commits into
feat/product-in-the-atmospherefrom
merge-main-into-product

Conversation

@Darkest-Teddy

Copy link
Copy Markdown
Contributor

Brings all 10 commits from main onto feat/product-in-the-atmosphere, including every Ask/AI fix, and lands the markdown rendering the bug report was about. Supersedes #31.

What arrives from main

  • b84728c - Ask renders Markdown (markdown.tsx) and ask.ts asks the model for structure. Both halves; neither works alone.
  • c78131c - stops Ask truncating now that answers are longer (max_tokens was cutting a 264-page review's answer at 56s).
  • 84bc19c - opening a prepared case brings its document with it.
  • 3e6bc88, 2ae6907, plus the refusal-panel, archive and header work.

The four conflicts, and why they resolved the way they did

All in files both branches had changed independently.

  • read.tsx / read.test.tsx / App.tsx - both branches had independently fixed the same pdf.js 401 (the bearer token never reached /raw). Your branch's version is a superset: it carries that fix and the citation-attribution feature (citers/blind, who cited which finding after reveal). Kept yours, so nothing is lost either way - main's only unique content there was one redundant token test.
  • One genuine merge: main had made token optional because two of its layout tests render <Read> without one. Your side had it required, which would have failed those tests. Resolved by taking main's optional handling plus your citation feature - the header is omitted rather than sent as Bearer undefined.
  • Backdrop.tsx - both branches made the identical two fixes (register from STATES, guard transitionTo); only the comment wording differed.

Markers were stripped hunk-by-hunk keeping the right side, notcheckout --ours, which would have discarded main's auto-merged changes in those same files.

Two follow-on fixes

  • A real hole in the renderer. If an answer arrives with no newlines, the parser turned the whole thing into one giant <h4> - worse than the wall of text reported. Reconstruction is now anchored only to unambiguous markers: a bare - stays a dash, 1. stays part of a number, and it runs only when the answer has no newline anywhere, so well-formed markdown takes an unchanged path. A test carries the reported answer verbatim.
  • A test timeout that only appeared on the merge. Five seeding tests copy a real PDF and write hash-chain links; the two-opener one takes ~3.5s against a 5s default. It passed on either branch alone and failed on the merge because the suite grew to 962 tests and the files compete for cores. Given an explicit budget on the five tests that seed, rather than raised globally.

Verification

  • 971 tests pass across 66 files; three consecutive full runs green before the markdown commit went on top (962/962 each)
  • tsc --noEmit clean on the deliberation app
  • All three workspaces build
  • Confirmed present after merge: markdown.tsx, the ask.ts markdown prompt, the max_tokens fix, citationsFor/citers, and the committed regulatory documents

🤖 Generated with Claude Code

Darkest-Teddyand others added 18 commits August 16, 2026 01:36
Reading Trails, phase 1: a Read & mark tab, and seats that hold still
Pressing Read & mark went dark and stayed dark until a reload.
Backdrop registered five scenes by hand - dashboard, new, library, ask, record
- while packages/atmosphere publishes seven. When Section arrived and sceneFor
started returning "read" for the reading surface, nothing added it here. So
transitionTo was called with an id the engine had never heard of, which throws
by contract (core/Atmosphere.ts). The throw happened inside an effect, and React
unmounts a tree whose effect threw, so a fault in the DECORATION took the whole
product with it.
The same id on a deep link went through mount() instead, inside the loader's
try, and was swallowed - a working product with no scene behind it. That is why
reloading appeared to fix it, and why it looked intermittent: three different
outcomes from one missing table entry, depending on how you arrived.
Registration now reads STATES, which is what apps/atmosphere has always done and
where a new scene actually gets added. That makes the drift impossible rather
than merely repaired. Registering is a map insert and the factory does not run
until something mounts it, so carrying "landing" here costs nothing.
The transition effect is also guarded now. Fixing the catalogue removed this
throw, not the class of them - a driver refusing a shader mid-session arrives at
the same line - and this file's own header says a background may not take the
product down with it. The loader honoured that and the scene change did not.
The fake engine in the test keeps the real class's contract: an unregistered id
throws. A forgiving one would have passed against the broken code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both surfaces put their prose directly on a live WebGL field. Reading is the
longest stretch of text in the product - a document strip, a findings rail, and
the paragraph explaining why a citation could not be placed - and the position
form is a heading, four labels, three explanations and a basis line. Neither had
a ground under any of it.
.glass is the one surface in this product that carries one, and app.css says why
it exists in those words: a low-alpha wash plus a blur that stays readable over a
lit colony without hiding what is behind it. The evidence stage has used it from
the start and reads cleanly over every scene because of it. These two were the
screens that needed it most and were going without.
Waiting is plated as well as the form. That tab BECOMES Waiting once you seal, so
a ground on only one of them would disappear at the moment you submit.
ONE PLATE PER SCREEN, not one per part - app.css puts the blur on containers
only, because a dozen stacked blur layers buy nothing the parent has not already
bought.
The empty reading state is plated too. It is one paragraph and nothing else,
which makes it the state most exposed to the scene: there is no other object on
the screen for the type to sit on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header is fixed and the work scrolls under it, which is the arrangement
Chrome.tsx argues for: a case table with three hundred rows is a document, and a
product that hijacks the wheel to fly a camera is a product nobody can use. The
cost of that arrangement is that the rows pass behind the navigation. On a long
position form the wordmark and the sign-out button sit across the sentence you
are reading.
So it leaves going down and comes back the moment you turn around. DIRECTION,
NOT POSITION: "hide below 400px" would take the navigation away and keep it away
for the length of the table, and scrolling up is the gesture that means give me
the chrome back.
A 6px floor, because a trackpad does not emit one scroll event per gesture - it
emits a stream of one- and two-pixel moves, some of them in the wrong direction,
and without a floor the header flickers the whole way down. Always showing above
96px, roughly its own height plus its inset, where it is not covering anything
yet and hiding would be motion for no reason.
Inert while it is away rather than merely transparent. This holds the main menu
and the only sign-out control, and a tab stop on something translated off the top
of the viewport sends focus somewhere the reader cannot see. One attribute takes
the subtree out of the tab order and swallows pointer events with it.
Transform and opacity only - the two a compositor animates without laying out
again, and there is a WebGL scene drawing behind this. Reduced motion keeps the
behaviour and drops the transition: a reader who asked for less motion still
wants the navigation off their paragraph.
No rAF throttle. The listener is passive and does two comparisons against a
number the browser already has, scroll events are frame-aligned as it is, and the
state only changes on the turn - so React re-renders when the answer changes, not
when the page moves.
Verified with real wheel input rather than scrollTo, which fires no scroll event
at all under CDP - worth knowing before anyone writes an e2e test for this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects on the one screen where being unable to read the interface costs a
reviewer their answer rather than their patience.
THE CALL BUTTONS WERE NOT BUTTONS. `.rail` and `.persona` were dropped by the
redesign and nothing replaced them, so the three-way call - the single most
important control in the product - rendered as "AdvanceDo not advanceCannot
conclude": three words run together, no gap, no border, no pressed state. Agree
and Override on the verdict screen had the same pair and the same result. Both
now use `.choice` and `button.ghost`, which is what every other form here is
built from and already carries hover and aria-pressed.
THE TEXTAREA HAD VANISHED INTO ITS OWN PANEL. The form rule says a field is
"darker than paper", and it was not: paper carries 0.50 alpha and the field
carried 0.46, so the field covered LESS of what was behind it than the plate did.
Over a scene that was survivable by accident. On a plate the two composite to
within a couple of values of each other and there was no box on the screen at all
- only a slightly different dark where the caret was supposed to go. 0.78 now,
and the rim goes from 0.15 to 0.26, because a field has to be an object somebody
can see the extent of before they click into it.
The prose moves onto the field rhythm the rest of the product uses: labels,
controls and their explanations in `.field` with `.hint`, the two halves of an
outside claim spaced by rule rather than by an inline style no stylesheet could
reach, and the basis line ruled off above the button because it summarises every
answer rather than asking for one more.
A GUARD FOR THE CLASS OF BUG. A className with no rule does not throw, does not
warn, and does not fail a type check - it renders as an unstyled element that
looks like a mistake nobody made. The stylesheet test now reads every plain
className out of the components and fails on any the stylesheet has no rule for.
Run against the old markup it names `rail` and `persona` exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read & mark could never open a PDF. It showed "Failed to fetch", and behind that
was a 401 on /api/cases/:id/documents/:docId/raw.
pdf.js does not go through api.ts. It is handed a URL and issues its OWN request,
so the bearer token this app keeps in memory never reached the one endpoint that
serves a document's bytes - the endpoint every other call authenticates against
without trouble.
It hid for as long as it did because no case had a document on it. The screen
said "No documents on this case yet" and nothing ever reached the fetch, so the
first person to attach a file was always going to be the first person to find
this.
httpHeaders rather than a query parameter: a token in the URL is a token in the
browser history, in the referrer, and in any log the request crosses, and this is
the endpoint that streams unpublished safety data.
Verified against a real 178-page EMA assessment report - the page renders, a
finding in the rail jumps to the page it cites, and the page says what the
finding says it does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A prepared case arrived with findings and no document. The case file carries a
page number for every finding, and the library manifest knows which document
those pages are in, but the two facts never met - so Read & mark said "No
documents on this case yet" on every case anybody opened, and the reader, which
joins a finding to a page THROUGH a document id, had nothing to join to. Making
one usable meant uploading the file by hand and re-adding every finding against
it.
The source now comes with the case. It goes through the same door a person's own
upload does - documents.upload measures before it accepts - so a scanned or
off-topic source is refused here exactly as it would be on the Evidence stage, by
the same code, with the same reason. Nothing about shipping a file with the
product makes it readable.
A FAILURE HERE DOES NOT LOSE THE CASE. The approval packages are not committed,
so a checkout without them is the normal case, not the exception; a missing file
or a refusal leaves the case opening exactly as it did before, which is still a
working case. There is a test for that path specifically.
DEDUPLICATION HAD TO STOP BEING GLOBAL, and this is what proved it. DocumentStore
keyed uploads by content hash across the whole store, which is right inside a case
- re-sending a file you already sent must not become a second document a second
position can cite - and wrong across them. With the review arriving at open time,
the second person to open the same case uploaded identical bytes and got back the
FIRST person's document, scoped to a case they cannot see: forCase found nothing
for them, and their findings named a document that is not on the case they were
filed under. The key is per case now. That is the same argument the caseId suffix
already makes one level up - a prepared case is a starting point, not a shared
room - and the note at the top of documents.ts, which justified the global
version, says so instead.
The attach test injects the manifest rather than reading disk: the approval
packages are deliberately untracked, so a test that needed the real file would
pass on a developer machine and fail in CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
services/api shells out to measure_pdf.py and extract_pdf_text.py for every
document, and both need PyMuPDF. The interpreter comes from PYTHON and otherwise
defaults to whatever `python` is on PATH - which, on a machine whose system
Python has no pip and cannot get one, is an interpreter that will never have
PyMuPDF in it.
The failure was quiet and pointed at the wrong thing. Every upload came back 422
"unreadable - PyMuPDF is not installed", which reads as a bad DOCUMENT rather
than a missing dependency, and two tests in services/api failed the same way on
any machine without a global install. A green suite and a working upload both
depended on knowing to set an environment variable that nothing mentions.
So both entry points now look for a repo-local .venv and use it when nothing has
already named an interpreter. An explicit PYTHON still wins, and a checkout with
no virtualenv behaves exactly as before - CI installs the requirements globally
and is untouched.
dev-all prints the interpreter it chose, because a silently-chosen one is how the
last one hid.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE PAGE WAS INERT ON ARRIVAL, and that is the defect underneath the rest. The
selection was seeded by a `useState` initialiser, which runs on the FIRST render -
and on that render `library` is `[]`, because App.tsx fetches it after mount. So
`source` was fixed at "" for the life of the page while the `<select>` displayed
Turalio: a select whose React value matches no option falls back to displaying
option zero, and reading `.value` off the DOM returns that option's value. Every
readout agreed and the state underneath was empty. `send` and `summarise` both
open with `if (... || source === "") return`, so every suggestion chip, the summary
button and Ask did nothing at all - no request, no error, no pending turn - until
the dropdown was changed by hand. It is derived from the library now, so a pick
that names no real document falls back on its own rather than sticking at "".
THE DOCUMENT IS THE SUBJECT, SO IT GETS A ROW. The picker was a `.field` inside
`.pagehead .actions`, which with `margin-left: auto` meant it took whatever width
the title did not - a 565px native select floating below the lede, aligned to
nothing, in the slot a page uses for its actions. It is not an action: every
question, answer and citation below it is about ONE document, and changing it
clears the thread. The summary moves onto that row for the same reason - it acts
on the document, not on the conversation.
ONE COMPOSER, ONE ACTION. The box held six full-sentence suggestions wrapped to
three rows, a primary-styled summary button, and the send button - which
`button.primary:disabled` draws as a transparent hairline, the state it is in every
time the box is empty. The loudest control in the composer was Summarise and the
quietest was the one the box exists for. Suggestions are a way in before there is a
thread, so they sit above it and leave when spent.
MARKDOWN NEEDED BOTH HALVES. A summary measured off this deployment is 5,953
characters containing ZERO newlines, with "Animal findings (rats):" and "Human
clinical findings:" as run-on labels inside one paragraph - nothing had ever asked
the model for structure, so there was nothing to render and a `<p>` was not wrong.
ask.ts asks for it now; markdown.tsx renders it. Neither alone changes the screen.
Inline emphasis is fenced off on purpose: ask-eval scores `statedFact` with
patterns like `30[06]\s*mg/kg`, and `**300** mg/kg` puts asterisks where that `\s*`
expects whitespace, scoring a correct answer as a miss. Structure is free; a marker
between a number and its unit is not.
The renderer builds React elements and never touches innerHTML, so HTML in a
model's answer is text on the page. Links are not a construct: an answer is drawn
from a PDF page and has nowhere legitimate to point, and provenance is the citation
rows the server resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ask returned `truncated: max_tokens too low` on three of four attempts. The route
answered 502 and the composer showed a bare `upstream` - no answer at all, for a
question retrieval had already served the right pages for.
SHAPE_ASK's 16000 was measured, and it was right for the prompt it was measured
against. What changed is the answer: ask.ts now asks for Markdown, and headings,
bullets and blank lines are more tokens of answer for the same question. Thinking
shares that budget, so the pair crossed the ceiling on any question with a broad
answer - "What liver findings are reported, and at what doses?" against the
264-page Turalio review, every time but one.
This is the fifth time on this project that an output ceiling has been too small
once thinking was counted, and the first where the ceiling did not move but the
output did. The number is SHAPE_SUMMARY's for SHAPE_SUMMARY's reason: the answer
is bounded by the prompt, so the only job left for this ceiling is to stop being
the binding constraint on thinking. A cap is not a reservation - nothing is spent
by raising one, only by generating into it - which is why the fix is always to
raise it and never to tune it.
SHAPE_ADJUDICATION stays at 16000. It has its own measurement - zero truncation
across 10 runs on the probe case - and its input is a bounded case rather than
however many pages retrieval returned.
Measured after: six of six on the question that failed three of four, and the
same question answered in the UI with its headings and citations intact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The library's bodies had a floor in the code and none in the frame: an
additive plane of light bands with no depth write, which over a near-black
ground is very nearly nothing. A rank of standing volumes read as a rank of
floating ones.
It is a heightfield now, ported from the landing page's own library frame
and for the same reason that one has it - an object glowing over nothing is
a shape, and the same object with ground catching its light is a place. Each
case throws a pool on the ground it stands on, azure if it is usable and a
smaller red one if it was refused, so the refusals are legible on the floor
as well as in the object.
Three things the port had to learn on the way across, all in terrain.ts:
emitters carry a colour, because a refused body is the one red thing in this
palette; eight slots rather than four, because the library holds six; and a
dissolve, because the camera flies inside a body and the world outside has
to stop existing. The old floor's sweeping bands moved into the same shader,
so the light now runs over the landform instead of through it.
The bodies stand ON the surface rather than sunk into it, and that is forced
rather than chosen: Atmosphere builds rtC with depthBuffer false and renders
into it whenever a transition is not running, so outside a scene swap nothing
here is depth tested and a buried foot would simply be drawn. Same reason the
ground draws first. Noted in the file, since the scene's own header claims a
depth test it does not get.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two refused entries opened their one line with "REFUSED - " in capitals,
two rows under a card already drawing a red REFUSED badge off the same
usable flag. The badge said twice, and it was the only shouted lead-in on a
page where the other four entries open on a sentence.
What is left does the job every other shape does - what the document is,
then what that costs the reader - so the six read as one set again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
h1-h4 and p are all margin 0 in app.css: this app takes its vertical rhythm
from a container, never from the type. Refused rendered a bare <section> and
is mounted straight into the shell, so it had no container at all - the
title, the red refusal box and all six headings and paragraphs stacked with
literally no space between them, the box wedged against the heading above
and the one below.
.section restores the 16px between blocks. Each heading is paired with its
body in a .stack-s as well, because .section alone spaces every child
equally and that leaves an h3 floating exactly as far from its own paragraph
as from the block before it - the panel reads as three groups now rather
than six loose lines. No new CSS; both classes already existed.
Five more bare sections in this file have the same gap, and the inline
marginTop hacks at 664 and 722 are what standing in for it looks like.
Left for their own change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Developer API's response_schema proto does not carry
additionalProperties, so sending it rejects the REQUEST outright - 400,
Unknown name "additionalProperties" at generation_config.response_schema -
before a model is ever selected. Every AI surface writes that key, so
adjudicate, ask, interpret, extract and navigate all died together on that
host, and handleAdjudicate reporting every upstream fault as a bare 502
made a request that never reached a model look like a model that failed.
Stripped in transport rather than in the schema builders. The constraint is
real where it is honoured: Vertex enforces it, it is what stops a model
returning a field nobody asked for, and the committed numbers were measured
with it in force. Loosening the shared schemas to suit the weaker host would
weaken the stronger one to buy nothing, so the schemas stay strict and the
transport adapts.
Known red: services/api/test/server.test.ts "runs the rest of the flow for
the owner" fails on adjudicate returning 502. It reproduces on origin/main
without this change, so it is not introduced here, but it is landing red and
is worth chasing next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	apps/deliberation/src/App.tsx
#	apps/deliberation/src/read.tsx
#	apps/deliberation/src/shell/Backdrop.tsx
#	apps/deliberation/test/read.test.tsx
A `<p>` COLLAPSES NEWLINES, which is why the reported screenshot looks the way it
does: the model's markdown was well-formed - `### Reported Studies`, `- **General
Toxicology:**` - and the old `<p>{answer}` rendered it as one run-on line with the
markers still in it. markdown.tsx fixes that case and this commit does not change it.
WHAT IT DOES FIX is the case underneath. `answer` is one JSON string, and a model
writing six thousand characters into a string field does sometimes emit the MARKERS
without the newlines. Fed to `parse` that is a single line beginning with `###`, so
the whole answer became ONE heading - not a wall of text any more but a wall of
heading, which is worse than what was reported. A test carrying the reported answer
verbatim, with its newlines removed, now asserts two headings and three list items.
CONFINED TO THE DEGENERATE CASE, and the test is the whole string rather than a
per-line judgement. An answer that broke ANY of its lines was formatted by a model
that knew how, and reconstructing over the top of that would be this file inventing
structure where real structure already exists. Only an answer with no newline at all
is repaired, so nothing that works today takes a different path.
EVERY RULE IS ANCHORED TO SOMETHING UNAMBIGUOUS. A mid-line `###` is not prose. A
mid-line bullet is recognised only by the `**` label the ask prompt asks for, because
a bare ` - ` is a dash in a reviewer's prose and splitting on it would cut a sentence
of transcribed evidence in half with nothing on screen to show it happened. `1.` stays
part of a number. A heading that ran into its paragraph is cut at a sentence opener,
never mid-title: `### Studies In Rats` keeps "In" because "Rats" is capitalised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a714b01)
`POST /api/demo` opens a real library case: it copies the regulatory review off disk,
transcribes its findings and writes the opening links of the hash chain. One of those
runs about 1.4s alone, and "gives each opener their own copy" does the whole thing
TWICE - 3.5s before the machine is doing anything else, against a 5s default.
It passed on either side and failed on the merge of the two, which is the tell: the
suite grew from 881 and 948 tests to 962, the files compete for the same cores, and a
test sitting 1.5s under the limit went 2s over it. Nothing on the path changed - the
failure moved with the SIZE OF THE RUN, which is a limit set too close rather than a
slow path worth chasing.
Stated on the five tests that seed, rather than raised globally: a timeout is a claim
about what a test does, and only these five copy a PDF. Three consecutive full runs
green at 962, then 971 with the markdown tests on top.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7cdf48e4-510d-489b-8a7b-884a5c1cb888

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

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

@Darkest-Teddy

Copy link
Copy Markdown
ContributorAuthor

Updated: merged origin/main again to pick up 347c87f (strip additionalProperties for the Developer API host) — the conflict in gemini.ts was two independent additions at the same spot, resolved as a union so both geminiCredentialAdvice and responseSchemaFor survive. 976 tests across 67 files, three builds, CI green.

The merge resolution deleted the ======= and >>>>>>> lines but left the
opening <<<<<<< HEAD. It sat inside the JSDoc for geminiCredentialAdvice,
so it was legal comment text - which is why typecheck, lint and CI all
stayed green on it.
@AndresL230
AndresL230 merged commit f0ff440 into feat/product-in-the-atmosphereAug 17, 2026
3 checks passed
AndresL230 added a commit that referenced this pull request Aug 17, 2026
PR #32 reconciled main into this branch, but its last main merge predated
dd140aa by 26 minutes, so the two newest commits on main never rode along:
dd140aa Serve the verdict from the record, not from the tab that ran it
53e9a5b Give the four remaining bare sections the rhythm the rest of the app has
Without dd140aa the product branch kept the pre-fix adjudication path: the
verdict lived only in the browser of whoever pressed Adjudicate, so Reveal &
verdict was empty for every signed case, every refresh, and every non-owner -
and the Adjudicate button still offered itself on cases that could only answer
409, spending three model calls to find that out. Nine regression tests come
with the fix.
One conflict, in the services/api/server.ts import block, resolved as the union
of both sides: this branch's geminiCredentialAdvice and main's CallKind/Complete
- the seam that keeps server.test.ts from making live paid adjudication calls.
Both symbols are used; dropping either breaks the build.
Verified: typecheck clean, lint clean, 985/985 tests passing across 67 files.
AndresL230 added a commit that referenced this pull request Aug 17, 2026
main moved 42 commits under this branch - #33 (Postgres/Supabase), #24, #29,
#32 and the logo - and rewrote the three files this touches most.
THE RECONCILIATION #33 DEFERRED. `ShareStore` was written against the pattern
this branch had: a synchronous `new ShareStore(path)` beside `AuthStore` and
`InviteStore`. That layer is gone. So the store joins the new one the way the
other four did:
- `ShareStoreApi` in postgres-share.ts, where `AuthStoreApi` and
`InviteStoreApi` live, for the reason stated there: the second
implementation is what needs a name for the shape.
- `ShareStore.open(path)`, async, private constructor.
- `PostgresShareStore`, and supabase/migrations/0002_share_links.sql.
- Both branches of `buildStores`, and `Stores.shares`.
- The behaviour is ONE suite over both implementations
(test/share-store-contract.ts), as with auth and invites.
`shareSecret` deliberately stayed OUT of `buildStores`: which backing holds the
links is a storage decision, whether the deployment can publish at all is not.
THE TABLE HOLDS NO TOKEN, and the migration says so at length because that is
the feature's whole security argument - the URL is
HMAC(secret, "caseId:version"), derivable from two columns that are not secret,
so a stolen dump yields nothing. `publish` omits `version` from its upsert's
SET list on purpose: assigning it would reset a revoked case to 1 and re-mint
the token revoke had just killed. A test reads the column list back out of
information_schema, because that property fails silently.
WHAT THE CONFLICTS COST. server.ts kept main's static serving whole and its
`/api/health` route; the branch's "NO STATIC-FILE SERVING HERE" comment was
true when written and is now a lie, so it is replaced by an accurate one:
`/r/:caseId/:token` is still not served in production, a scanned QR 404s on a
deployed host, and the two decisions that has to wait on are written down
beside `staticRoot()`. Adding it here was out of scope and stayed out.
`handleReport` and `handleShare` became async; `handleReport` fetches the people
in one `auth.list()` rather than a lookup per name, because `auth.get` is
asynchronous now and `buildCaseReport` takes a synchronous `person`.
ONE THING THE MERGE HAD TO DECIDE. docs/HANDOFF-open-prs.md predicted that #30
and main each carried a provenance rule and that they would auto-merge without
conflict. They did, and both were live: `view`'s read any unrecognised actor as
`live`, the report's read only "model" as `live`. Two surfaces describing one
signed safety adjudication, disagreeing about whether a model produced it.
Closed with one module-level `sourceOf`, failing toward `stub`; every writer
passes "stub" or "model" so nothing observable changed, and the test pins that
both readers agree on a third actor neither was written for.
The postgres test fixture now applies every migration in filename order. Pinned
to 0001, it built a database that was correct on the day it was written and
silently lacked share_links.
Verified: lint, typecheck, 1267 tests with DATABASE_URL set (1179 without),
both builds, harness + metrics + golden + an unchanged verdict-manifest, e2e.
The public bundle still carries no auth code: only main's own entry chunk
matches AUTO_PASSWORD or /api/auth/login, and public.html references neither.
HANDOFF-pr34.md is deleted - the job it describes is this commit, and what
happened is recorded in docs/HANDOFF-open-prs.md and the spec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Darkest-Teddy@AndresL230