The pass announces the verdict it just decided

Storing doc_lang on the document row is not enough on its own. The
editor sees that row when the document is opened or saved, and the pass
that decides the verdict runs after a save — so the client was always
one save behind, and read-aloud is reached for precisely when she has
stopped typing and no further save is coming. Heard in a browser: a
Portuguese paragraph read in an American voice, twice, until another
keystroke went in.

/check, /voice and /collocation now answer with X-Petal-Doc-Lang. A
header rather than a wider body: all three answer with a bare array of
the unified pending set and every caller reads it as one, and a verdict
is metadata about the pass rather than another suggestion. It reaches
the app through the same handler shape onUnauthorized already uses.

Claude-Session: https://claude.ai/code/session_01GJHNvirh7Hzhc9RL3HAvz7
This commit is contained in:
prosolis
2026-07-29 00:21:15 -07:00
parent c719effe1d
commit e67f77eb05
4 changed files with 114 additions and 5 deletions
+27 -4
View File
@@ -276,11 +276,34 @@ function signedOut(): UnauthorizedError {
return new UnauthorizedError()
}
async function req<T>(path: string, init?: RequestInit): Promise<T> {
// The document-language verdict is decided by the checkpoint pass, so the pass's
// own response is the first moment the client can know it. It rides on a header
// (the pass answers with a bare array of suggestions, and every caller reads it
// as one), and reaches the app through a handler registered here — the same
// shape onUnauthorized already uses, for the same reason: it is one fact from
// deep inside a request that a component several layers up needs.
//
// Without it the editor learns the verdict only from a document save, which is
// always one save behind the pass — and read-aloud is reached for precisely when
// she has stopped typing and no further save is coming.
let docLangHandler: ((docId: string, lang: DocLang) => void) | null = null
export function onDocLang(handler: (docId: string, lang: DocLang) => void) {
docLangHandler = handler
}
// `verdictFor` names the document whose language this response may announce.
// Only the three pass endpoints pass it; everything else has no verdict to carry
// and never touches the handler.
async function req<T>(path: string, init?: RequestInit, verdictFor?: string): Promise<T> {
const res = await fetch(`/api${path}`, {
headers: { 'Content-Type': 'application/json' },
...init,
})
if (verdictFor && res.ok) {
const lang = res.headers.get('X-Petal-Doc-Lang')
if (lang === '' || lang === 'en' || lang === 'pair') docLangHandler?.(verdictFor, lang)
}
if (res.status === 401) throw signedOut()
if (!res.ok) {
const detail = await res.text().catch(() => '')
@@ -322,14 +345,14 @@ export const api = {
// Rate-limited per document server-side (returns the existing set if too soon).
// Both passes return the UNIFIED pending set (grammar + voice), so the client
// never drops one family's highlights when the other refreshes.
checkDoc: (id: string) => req<Suggestion[]>(`/docs/${id}/check`, { method: 'POST' }),
checkDoc: (id: string) => req<Suggestion[]>(`/docs/${id}/check`, { method: 'POST' }, id),
// Voice-consistency pass: whole-document, explicit-action, slower. Returns the
// unified pending set too. Rate-limited per document server-side.
voiceDoc: (id: string) => req<Suggestion[]>(`/docs/${id}/voice`, { method: 'POST' }),
voiceDoc: (id: string) => req<Suggestion[]>(`/docs/${id}/voice`, { method: 'POST' }, id),
// Collocation coach: whole-document, explicit-action pass flagging non-native
// word pairings ("do a decision" → "make a decision"). Returns the unified
// pending set too. Rate-limited per document server-side.
collocationDoc: (id: string) => req<Suggestion[]>(`/docs/${id}/collocation`, { method: 'POST' }),
collocationDoc: (id: string) => req<Suggestion[]>(`/docs/${id}/collocation`, { method: 'POST' }, id),
// Mechanics pass: persist the client-detected deterministic fixes as the
// 'mechanics' family and return the unified pending set. Not rate-limited (it's
// free, local detection); runs alongside the grammar checkpoint.