- Pass 1, stitch: one
Noulquestion (a yes/no question whose answer is the probability that yes is correct) per adjacent pair of lines, asking whether the line break split a sentence across the two. All the pairs go in a single request, and lines that continue a split sentence get merged back into blocks. - Pass 2, classify: one
Choicequestion (pick one option from a list, with a probability for every option) per merged block, choosing among heading, paragraph, list item, quote, code, or callout (a note, tip, or warning set apart from the main text). The blocks only exist once pass 1 has answered, so this is a second request; it also carries companion questions for every block (heading level, step order, callout kind) whose answers are read only when the block’s type makes them relevant. - Direct evidence stays in code. Blank lines and explicit markers (
-,1.,#) are read directly and never sent to the model to reconsider; this memo kept its blank lines but lost every marker. The model gets only the questions code cannot answer from the text.
Setup
TYPESAFE_API_KEY.
The document: a team memo that lost its formatting
The test document is a memo about a build-system migration, in the state it arrives in a plain-text inbox: paragraphs hard-wrapped mid-sentence, a shell command sitting on a bare line, two lists with no bullets or numbers, a warning with nothing marking it as one.L014| ). The model reads the ids as
ordinary text in the state, and questions and answers refer to lines by them. The
semantic search cookbook
uses the same scheme.
Pass 1: stitching split sentences
One Noul question per adjacent pair of lines, all in one request; pairs separated by a blank line are skipped. The question is deliberately narrow (“does this line pick up mid-sentence?”), which is close to an objective fact about the text. It also quotes both lines, so the model judges the exact text in question rather than finding it by id among 28 lines.. ! ? : ;), the cutoff rises to 0.5.
Pass 2: classifying blocks
Each stitched block gets aChoice question: what kind of content is this? These three
dicts, plus the step question’s true/false criteria inside classify_questions below,
are the entire specification of the classifier. To adapt the pipeline to your own
documents, edit these descriptions.
heading, the renderer needs a heading level; if list_item,
whether order matters; if callout, which kind. The types are not known yet, and waiting
for them would mean a third round trip, so the companion questions are asked up front in
the same request. Most of these answers are never read: the step probability of a
paragraph goes unused. An extra question adds little, since the state is most of the
tokens and is sent once either way, while an extra round trip adds a full request of
latency.
warning.
Rendering
Code assembles the page from the judgments. Consecutive list items become one list, numbered when the mean of the items’ step probabilities is at least 0.5. That threshold is a group-level decision no single question asked directly.Open it in the playground
This share link holds the stitched blocks and the full pass-2 question set. Open it to re-run the classification live.Appendix
Where the join thresholds come from
The per-line join probabilities from pass 1:- After a dangling line (one with no sentence-ending punctuation), the sentence may not be finished, so a probability of 0.2 or above is enough to join the lines.
- After terminal punctuation (a character that ends a sentence or clause:
.!?:;), the cutoff rises to 0.5.L015| The platform teamfollows a colon, and a colon can lead into either a list or the rest of a sentence. It scores 0.16 here; a higher score would still need to clear 0.5 before the list merged into the sentence introducing it.

