docs(plugin-chatbot): compile the README's snippets against the shipped surface - #8228
Conversation
…ed surface
Burns down `packages/plugin-chatbot/README.md`'s `UNGATED_DOCS` row in
`scripts/check-doc-snippet-types.mjs` (objectui#5174 batch 30). Ten of the
page's eleven `tsx` fences now compile in the gate's covered tier; the
eleventh is a declared fragment naming objectui#8214.
Two real defects the census found, both decided against the shipped surface:
- The Quick Start taught an unannotated `useState([{ role: 'assistant' }])`
for a typed prop, so `role` widened to `string` and `Chatbot` refused the
array. Annotated `useState` with `ChatMessage[]`, and the new-message
literal with `ChatMessage`.
- The mapper example passed `onSend`, which `ChatbotEnhancedProps` does not
declare and `ChatbotEnhanced.tsx` never reads — `onSend` is a
`useObjectChat` option and an SDUI schema key, not a component prop. It
also called `useChat({ api })`, a spelling `@ai-sdk/react@4.0.68` retired
in favour of a transport. Both repaired to what the package itself writes.
The residual is objectui#8214: `uiMessagesToChatMessages` declares its
parameter as the package's own `AnyUIMessage[]`, whose `AnyPart.state` is
the tool-invocation state union, so `@ai-sdk/react`'s `UIMessage` is refused
at the call that export exists for. `src/**` is out of this batch's scope,
so the block carries a fragment marker with the measured reason instead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
…30-plugin-chatbot No overlap with either path in this branch's diff; merged rather than rebased per the repo's no-force-push rule. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
Standing down on That check is informational (not in the Generated by Claude Code |
|
Armed —
Landing stroke follows the merge: content probe on re-fetched Generated by Claude Code |
|
LANDED — Merged at 2026-09-07T02:41:36Z as Batch 31 ( Generated by Claude Code |
Part of #5174 (batch 30:
packages/plugin-chatbot/README.md)Base sha
590158122(batch 29, PR #8206).origin/mainadvanced to580b0fdf4mid-task; merged in with a merge commit (never rebased) and every reading below is re-taken on the merged headb1a383c75.Spelling note: generic type arguments are written as WORDS below, and element names bare, because GitHub's body sanitizer eats tag-shaped fragments — backticks and fenced code blocks included — and half of this page's story is about a generic.
Census — two readings, both with the gate's own analyzer
Both readings use
analyze/compileSnippets/scanFencesexported fromscripts/check-doc-snippet-types.mjs, with the target's row removed fromUNGATED_DOCSand every other row left in place. No hand-written regex. Controls healthy on every run:@object-ui/typesresolved topackages/types/dist/index.d.ts, sentinel 1, positive 0, undeclared 1, root-declared 1, 0 findings, 0 src leaks.Eleven
tsxfences on the base at lines 24, 61, 82, 145, 202, 271, 292, 316, 347, 377, 396.Reading 1 — ledger-literal, on the base
9 diagnostics, not the 8 the row claims. By code: TS2304 x3, TS2552 x2, TS2353 x1, TS2345 x1, TS2322 x1, TS17000 x1.
useStateapinot inUseChatOptions; TS2345 mapper parameter; TS2322onSend; TS17000 empty expressionChatbotEnhanced; TS2552messagesChatbotEnhanced; TS2552messagesAgainst the row's own wording:
5 undefined-name— exact. TS2304 x3 plus TS2552 x2 (Did you mean 'onmessage'is the same class, just with a suggestion).1 unresolved-module— falsified, it is zero. See the next section.TS17000 x1,TS2322 x1— present, but the TS2322 is not the one the brief predicted, and the count is a lower bound. See "Blind spot".Reading 2 — after adding only the missing imports and
declarestand-ins, in memory5 diagnostics. By code: TS2322 x2, TS2353 x1, TS2345 x1, TS17000 x1. This is the real debt, and the decisions below are made on it.
rolewidens to string (new — was hidden)What was added:
import { useState } from 'react'to fence 24;import type ChatMessageplusimport ChatbotEnhancedplusdeclare const messages: ChatMessage[]to fences 377 and 396. Nothing else — no prop touched, no type widened.Blind spot: 1
Batch 27's mechanism fired once, in a form neither batch 27 nor 28 had seen. Fence 24's
Chatbottag name was bound, so the element's prop check ran — butmessagescame from an unresolveduseState(...)call, so its binding carried an error type and the check found nothing to complain about. ImportuseStateand the element's real TS2322 appears.⇒ A mixed ledger row understates type debt not only when a JSX tag name is unresolved (batch 27) but whenever an errored binding FEEDS a checked element. Real TS2322 count was 2, the row said 1.
Also note the row is an upper bound in the total direction and a lower bound in the type-code direction at the same time: 9 diagnostics collapsed to 5 real ones, and one type code went up.
The unresolved module: the mechanism, measured
There is no TS2307 on this page, and the gate can see the package's third-party dependency types. Nothing to widen, no gate gap, nothing filed on this axis.
Measured with
derivePackageTypePaths/deriveDeclaredDependencyPathson the merged head:@object-ui/plugin-chatbotresolves frompackages/plugin-chatbot/dist/index.d.ts(built, not source-typed).neededPackages, soderiveDeclaredDependencyPathsreads its manifest and probes each declared specifier frompackages/plugin-chatbot/.@ai-sdk/reactmaps tonode_modules/.pnpm/@ai-sdk+react@4.0.68_react@19.2.8_zod@4.4.3/node_modules/@ai-sdk/react/dist/index.d.ts,declaredBy: @object-ui/plugin-chatbot. It is not hoisted to the rootnode_modules(ls node_modules/@ai-sdk/reactfails) and it does not need to be: pnpm links it underpackages/plugin-chatbot/node_modules/, which is exactly where the probe file sits.boundFailuresfor this document: 0.@ai-sdk/reactis inpaths, soresolvesOnlyThroughRootManifestreturns false for it.ai(7.0.65), added by this PR to fence 347's imports: declared by@object-ui/plugin-chatbot, mapped, not bounded.⇒ PM mechanism assumption A2 is falsified in its second half. A workspace package's third-party dependency types resolve whether or not they are hoisted, because the resolution probe runs from inside the owning package's directory. The whole run maps 91 specifiers from the declared dependencies of 34 imported packages; only 3 declared specifiers ship no types and stay unresolvable, and none of them is on this page.
Where the ledger's
1 unresolved-modulecame from is not recoverable from this tree — it predates the current dependency-path derivation. What replaced it is real: the two diagnostics the row never named.TS17000 and TS2322, decided against the shipped surface
TS2322 in fence 347 —
onSendis a phantom prop. README defect, repaired.Declarations read:
ChatbotEnhancedPropsatpackages/plugin-chatbot/src/ChatbotEnhanced.tsx:450extendsReact.HTMLAttributesofHTMLDivElement. It declaresonSendMessage?: (message: string, files?: File[]) =a void return at:457. NoonSend.ChatbotEnhanced.tsxnever READSonSendeither — a grep foronSendnot followed byMessagein that file returns nothing.onSendis real, but somewhere else: it isUseObjectChatOptions['onSend'](useObjectChat.ts:335,(content: string, messages: ObjectChatMessage[]) =void) and an SDUI schema key the three renderers forward (renderer.tsx:92,:281,:419).So this is not batch 28's other branch: the component does not read at runtime a prop the type omits. The type refuses a key nothing implements. README defect ⇒ repaired to
onSendMessage, the prop that exists.The page's own prose one section up already said
onSend(content, messages)is "the callback fed from"useObjectChat's messages — correct there, and the fence had promoted it to a component prop.TS17000 in fence 347 — elided body. Typed binding, no marker.
onSend={/* … */}is an empty JSX expression container. The brief's prescription wasdeclare const onSend: ChatbotEnhancedProps['onSend']; that member does not exist, so the binding is spelled against the member that does:One edit removes both the TS17000 and the TS2322: the attribute becomes
onSendMessage={handleSend}. Typed binding, not a fragment marker, per the brief's ordering.TS2322 in fence 24 — the Quick Start's untyped state. README defect, repaired.
This is the one the brief predicted, and it was invisible on the base (see "Blind spot"):
ChatbotProps.messagesisChatMessage[](src/index.tsx:21), and thatChatMessageis the runtime one re-exported from./ChatbotEnhanced, whoseroleis the closed union'user' | 'assistant' | 'system'(ChatbotEnhanced.tsx:23).What the page used to teach: that an unannotated
useState([{ id, role: 'assistant', content }])is a fine backing store for a typed prop. It is not — the array literal'srolewidens tostring, and the element seven lines down refuses it. A reader copying the Quick Start whole got a red squiggle at the very firstChatbotelement, which is the flagship example of the package. The same trap sat inhandleSend'snewMessageliteral.Repaired with
useStateofChatMessage[]plus aChatMessageannotation onnewMessage, and the imports both needed (useStatefrom react,type ChatMessagefrom the package). Two sentences of prose above the fence now say why the annotation is there.TS2353 in fence 347 — a retired call shape. README defect, repaired.
useChat({ api: '/api/chat' })is refused: the installed@ai-sdk/react@4.0.68typesUseChatOptionsas achatorChatInitunion intersected with throttle/resume options, andChatInit(fromai@7.0.65) carriestransport, notapi. The package itself already writes the current shape —useObjectChat.ts:12importsDefaultChatTransportfromaiand:539constructs one. Repaired to match, which is also what makesaian import of this fence.TS2345 in fence 347 — package type defect. Not repairable here; filed as #8214.
uiMessagesToChatMessagesdeclares its parameter as the package's ownAnyUIMessage[].mapMessages.ts:19writesAnyPartto be permissive but types one member tightly,state?: ChatToolInvocation['state']— the TOOL-invocation state set. A text part in the AI SDK'sUIMessagePartunion carriesstateof'streaming' | 'done', which is not in that set, so the whole assignment fails. The interface written to be loose is, on that one property, stricter than the union it absorbs.This is batch 28's second branch and it points at
src/, which this batch may not touch:mapMessages.ts's docblock says it is "Shared betweenuseObjectChat… and apps that driveuseChatthemselves (e.g. Studio …)", and this README section documents the same use.useObjectChat.ts:683destructureschatResult as any, and__tests__/mapMessages.test.ts:135calls the mapper withmsgs as never. This README block is the first uncast caller in the repository.p.type === 'text'.Filed as #8214 with the mechanism, the three options and a recommendation. ⛔ Not repaired here, and no cast added to the page: a documented cast is the tolerant fallback AGENTS.md #0.1 refuses.
Key-surface bound (objectui#7927)
Verified rather than assumed, per fence:
ChatbotProps(src/index.tsx:20) extendsReact.HTMLAttributesofHTMLDivElement, no index signature, notBaseSchema-derived ⇒ the compiler judges keys itself. Fence 29 (Quick Start):messages,onSendMessage,placeholderall declared; zero after repair.ChatbotEnhancedProps(ChatbotEnhanced.tsx:450) same shape, same verdict ⇒ this is exactly how theonSendphantom was caught (JSX excess-property checking). Fences 363, 399, 422:messages,surface,hideClearBar,processVisibility,onSendMessageall declared; zero after repair.ChatMessage(ChatbotEnhanced.tsx:21) is a plain interface, no index signature, not BaseSchema-derived — so theroleunion is really enforced, which is why fence 29's TS2322 is a true positive rather than an artefact.ChatMessagefrom@object-ui/types(complex.d.ts:785) is likewise a plain interface with no index signature. A5 confirmed: two distinct types by design — the authoring one adds a'tool'role and allows a Date timestamp; fence 322 imports it under the alias the page already used.constobject literals with no annotation, so nothing is judged about their keys. Stated so the green is not read as more than it is.A4 — phantom exports
check:readme-exportsis green on the base and on the head (exit 0 both times, package built). Both names the brief flagged are real exports ofpackages/plugin-chatbot/dist/index.d.ts:uiMessagesToChatMessagesat:98,toRuntimeMessagesat:100. So are the two type imports this PR adds,ChatbotEnhancedProps(:63) andChatMessage(:89). No phantom, nothing to repair on this axis.The unlabeled fence at 251 (now 257)
It is an ASCII box-drawing architecture diagram. Both gates read it and both correctly decline it, for stated reasons:
check-doc-snippet-typescollects onlyts/tsx/typescriptinfo strings, so it never enters the compiled set.check-doc-fence-languagesDOES read it:scanFencesthere filters nothing, its language is the empty string which is inUNHIGHLIGHTED_SPELLINGS, andclassifiesAsTypeScriptreads line 1 of the body literally — a box-drawing character, so it returns false andclassifyFencereturns null: "the block is nothing to this gate".packages/plugin-chatbot/README.mdis not in that gate'sKNOWN_UNHIGHLIGHTED_TS_FENCESledger and does not belong there.⛔ Not relabelled. The two
bashfences (16, 183) are likewise correctly out of scope.Per-fence decision
useStateimport,useStateofChatMessage[],ChatMessageon the new-message literal, prosedeclare const messagesdeclare const messagesTen fences compile in the covered tier. One is a declared fragment.
Ledger decision
UNGATED_DOCSrow deleted — the file reads zero, by the two honest routes the gate's own header names ("a block that should compile was made self-contained … and a block that genuinely cannot compile got aFRAGMENT_MARKERdeclaration with a written reason").If you would rather keep the page in the ledger with
1 TS2345, say so and it is a two-line change back.Gate diff is exactly the two lines of the row, deletions only. Counters: 229 documents both ways; covered 221 ⇒ 222; ungated 8 ⇒ 7; blocks to compile 601 ⇒ 611 (this page's ten); declared fragments 158 ⇒ 159 (this page's one).
No pin enumerates this row:
scripts/__tests__/check-doc-snippet-types.test.ts:600pins only the rootREADME.mdrow and itsobjectui#7417reason, so no test file needed editing (batches 28 and 29 measured the same).Strictness region
The region from the
Fence scanningbanner to EOF, 83711 bytes:590158122:2749d53ae3a8df033a53b8d7a354fa7e22ee2d1a17f6ad0c3d61122e904e084bb1a383c75:2749d53ae3a8df033a53b8d7a354fa7e22ee2d1a17f6ad0c3d61122e904e084bIdentical. The deleted row sits above the banner. Nothing about this gate's strictness moved.
Positive control
Run twice — on
a3cc79575and again on the merged headb1a383c75— against a committed tree, under atrap ... EXIT INT TERMwith absolute paths.placeholder="Type your message..."becomesplaceholder={42}in the repaired Quick StartChatbotelement.0b607af3508916004f1d68cab40b8db81624e92c; mutated blob74a0f4fdba6c47689099c11b3047d48bc8c2d896.[semantic] packages/plugin-chatbot/README.md:55:7 TS2322: Type 'number' is not assignable to type 'string'. (611 of 611 judged, 1 failed)0b607af3508916004f1d68cab40b8db81624e92c,git diff HEADon the path 0 bytes,git status --porcelainempty.Gates — all pinned to
b1a383c75pnpm check:doc-snippetspnpm exec vitest run scripts/__tests__/check-doc-snippet-types.test.ts scripts/__tests__/check-doc-fence-languages.test.ts(123 tests)pnpm exec vitest run scripts/__tests__/(115 files, 3415 tests)pnpm check:doc-typespnpm check:readme-exports(package built)pnpm check:doc-fencespnpm type-check:scriptspnpm lint:rootpnpm check:control-bytesgrep -naPcontrol-byte self-scan of both changed pathsnode scripts/check-changeset-presence.mjsnode scripts/check-governed-queue-guard.mjs --teston both pathspnpm check:entry-guardturbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter) --concurrency=2Re-derived beyond the dispatched list, all 0:
check:doc-example-readers,lint:coverage,type-check:coverage,check:self-import,check:unreferenced-sources,check:esm-specifiers,check:shell-escape-residue,check:governed-queue-guardself-test.check:node-esm-loadfirst exited 1 for the environmental reason the brief predicted — turbo shares one cache across worktrees and replayed 12 packages built in the siblingobjectui-issue-5174-b29tree, which the provenance leg correctly refuses to grade. Re-run with--force-buildas its own message instructs: exit 0, 34 of 39 published ESM entries evaluated.Changeset:
check-changeset-presence.mjsreports 2 files changed, 0 of them published source of a released package and 0 a manifest whose contract moved ⇒ none owed. No label applied, and no label written at all.Live E2E (informational)is red on every branch today for the upstream reason tracked in #7990 / objectstack#16186 — not this branch's.Generated by Claude Code