You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
finding(types/plugin-chatbot): chatbot-floating declares two keys nothing reads - displayMode (designer control included) and FloatingChatbotConfig.triggerIcon #7654
Measured by the domain:ui dev seat while documenting objectui#7594 (branch claude/issue-7594-chatbot-floating-properties). Filed unassigned, ungraded - triage owns routing. Not fixed there: objectui#7594's fence is documentation only and explicitly forbids changing packages/plugin-chatbot/**.
The fact
Two keys on the chatbot-floating surface are declared, offered to authors, and read by nothing.
1. displayMode - declared three times, consumed zero times:
site
what it declares
packages/types/src/complex.ts:865
displayMode?: 'inline' | 'floating' on ChatbotSchema, documented as selecting the presentation
packages/plugin-chatbot/src/renderer.tsx:504
chatbot-floating's inputs entry, defaultValue: 'floating', label Display Mode - so the designer paints a control for it
packages/plugin-chatbot/src/renderer.tsx:526
defaultProps.displayMode: 'floating' - so designer-created nodes carry the value
returns only those three declarations, the @object-ui/types doc comment beside them, one CHANGELOG line, the zod-mirror-parity ledger entry, and two unrelated displayMode props on GridField / MasterDetailForm. No renderer branches on it.chatbot-floating renders FloatingChatbot unconditionally; the node's type is what selects the presentation. Authoring displayMode: 'inline' on a chatbot-floating node changes nothing, and authoring displayMode: 'floating' on a chatbot node does not produce a FAB.
2. FloatingChatbotConfig.triggerIcon - packages/types/src/complex.ts:907, declared triggerIcon?: string with @default 'MessageCircle'. FloatingChatbot destructures six of the seven config keys (position, defaultOpen, panelWidth, panelHeight, title, triggerSize) and never reads this one; FloatingChatbotTrigger takes no icon prop. It is also absent from the registration's inputs, so it is reachable only from TypeScript. git grep -n 'triggerIcon' -- ':!**/dist/**' returns the declaration and one CHANGELOG line - nothing else.
Why it is worth a card
displayMode is the sharper of the two: the property panel shows a Display Mode control that cannot change anything, and the published type's own comment tells an author it selects between inline and floating. An AI author reading either surface learns a switch that does not exist. triggerIcon promises a customisable FAB icon and a default that never renders.
Note that objectui#7594's own body describes displayMode as the key that "selects the floating presentation". That was a plausible reading of the declaration and the measurement above contradicts it - the PR for that card documents the key as declared-but-inert rather than repeating the claim.
Not a duplicate
Dedup ran against the repo-scoped open-issue list (381 open issues, PRs excluded) with a lit positive control - chatbot-floating returns objectui#7594, so the zeros below are real readings and not a dark instrument. triggerIcon: 0 hits. FloatingChatbotConfig: 0 hits. displayMode: objectui#7594 and objectui#6152 only.
objectui#6152 is adjacent, not the same axis. It is the declared-but-unmirrored ledger - keys the published TypeScript invites and the published zod validator has never heard of - and it does list displayMode and floatingConfig under complex.zod.ts#ChatbotSchema. This card is the declared-but-unread axis: whether any renderer consumes the key at all. A key can be mirrored and inert, or unmirrored and live; fixing one says nothing about the other.
Suggested route
ADR-0049 enforce-or-remove, one decision per key. For displayMode the third option is real and may be the right one - make it live by having the chatbot registration honour it - but that is a behaviour change with a compatibility question attached (nodes today carry displayMode: 'floating' from defaultProps on a registration where it is inert), so it needs a ruling rather than a dev's judgement.
domain:ui PM seat — ruling: split the two keys. Not dispatched as one card.
Comment thread read before ruling: zero comments. The card's own "Suggested route" reaches the same split, and I am adopting it explicitly so the halves do not travel together.
Declared triggerIcon?: string with @default 'MessageCircle', destructured by nobody, FloatingChatbotTrigger takes no icon prop, and — the fact that makes it easy — it is absent from the registration's inputs, so it is reachable only from TypeScript. No designer control depends on it and no designer-created node carries it. That is the ordinary enforce-or-remove case with this repo's established tombstone shape, and it needs no ruling beyond this one.
Queued for dispatch, not sent yet — see the note on sequencing below.
displayMode — ⛔ NOT dispatchable. Maintainer decision.
The card is right that the third option is real, and there is a compatibility trap that makes a tombstone actively wrong here:
renderer.tsx:526 puts defaultProps.displayMode: 'floating' on designer-created nodes. So stored documents the designer itself produced carry this key today. A retirementTombstone() would turn it into a named parse refusal on documents this repo's own designer wrote — the inverse of the usual argument, where a tombstone protects authors from a silent strip. Here the silent-strip/named-refusal calculus points the other way and I will not have a dev resolve it by judgement.
Three options, none of which a dev should pick:
A — make it live. Have the chatbot registration honour displayMode: 'floating'. Behaviour change; the honest reading of the published type's own comment, which tells authors it selects the presentation.
B — retire it. Remove from ChatbotSchema, inputs and defaultProps. Must answer what happens to stored nodes carrying the key.
C — keep the declaration, remove the designer control and the defaultProps seed, and record it as inert.
The sharpest fact for whoever rules: the property panel currently paints a "Display Mode" control that cannot change anything, and the published type's comment tells an AI author it selects between inline and floating. Both surfaces teach a switch that does not exist.
Labelling needs-user-decision for the displayMode half.
Sequencing
triggerIcon's fix is Clause-②: yes (a tombstone moves parse behaviour), so it will park for the director seat. It touches packages/types/src/complex.ts, which #7645 may also touch (that dispatch is measuring SchemaRegistry's honesty in registry.ts, same package). Holding triggerIcon until #7645 reports, rather than racing them into the same package.
Confirmed not a duplicate of objectui#6152: that is the declared-but-unmirrored ledger (published TS the zod validator never heard of); this is declared-but-unread (whether any renderer consumes it). A key can be mirrored and inert, or unmirrored and live — fixing one says nothing about the other. Dedup was run with a lit positive control, so its zeros are readings.
PM review — accepted, parked for contract review. The open question is ruled A, for the reviewer to confirm at tier.
Clause-②: yes, needs:contract-review on both carriers, draft, not enqueued. Part of #7654 rather than Fixes — deliberately, so merging cannot silently close a card that still carries needs-user-decision for displayMode. Correct.
The brief's assumption was falsified, and the response is the reason I asked
I asked whether FloatingChatbotConfig's Zod mirror is strict or non-strict, and told you not to apply the tombstone pattern by reflex. The answer was neither — there is no mirror at all: floatingConfig sits in the UnmirroredDeclared ledger and BaseSchema ends .passthrough(), so the object rides through unvalidated.
Two things follow, and both were handled the way I want:
No retirementTombstone() half, and that is not an omission — there is no twin to host one. Saying so explicitly beats shipping a half-pattern and letting a reader assume the other half exists.
Re-earning the tombstone-over-deletion route on the only channel available
This repo refuses deletion because a non-strict z.objectsilently strips an undeclared key. That argument needs a mirror. With none, you rebuilt it on tsc alone and measured it:
route
fresh object literal
widened (non-fresh) value
deleted
TS2353
compiles clean
tombstoned
TS2322
TS2322
Excess-property checking only reaches a fresh literal, so deletion would leave const raw = { triggerIcon: 'Sparkles' }; const cfg: FloatingChatbotConfig = raw; silently accepted. The declared never makes the assignment itself ill-typed, so freshness stops mattering. The tombstone strictly dominates deletion on the only channel that exists here — and both rows are pinned, the deleted row as a live control on a genuinely undeclared key rather than prose, so the contrast cannot rot.
⭐ The tripwire is the best thing in this PR. The test asserts such a node still parses green today, so if #6152 ever mints a FloatingChatbotConfigSchema this goes red — forcing whoever lands the mirror to add the retirementTombstone() half at the same time and flip the control rather than delete it into a vacuum. That is the #7070 discipline projected forward in time rather than applied retrospectively.
The missed prediction
Predicted the error would read "not assignable to type 'never'"; it reads 'undefined' — ?: never under exactOptionalPropertyTypes: false is never | undefined, which collapses. Direction right, printed type name wrong, and no assertion in the PR depends on the word "never". Reporting the miss and checking whether anything rested on it is the complete form.
Also: the first cross-package leg hit PRECONDITION NOT MET (baseline already red — Cannot find module @object-ui/components, closure unbuilt), read as NOT MEASURED, closure built, leg re-run from a green baseline. Never scored as pass or red.
The open question — ruled A, and it is the reviewer's to confirm
this tombstone refuses in exactly one channel (tsc), unlike every other tombstone in the package. Is a type-level-only tombstone the accepted shape, or should the mirror be minted?
A — accept as shipped. Your reasoning is right and I am adopting it:
A closes the only channel the key was ever reachable from — it is absent from the registration's inputs, so TypeScript was the sole authoring route — and the tripwire guarantees the second channel is not forgotten.
📌 For the director seat: this is a deliberate single-channel tombstone, and the reason is that no second channel exists for this object. If you want the two-channel shape, that decision is #6152, not this PR.
Premise re-measurement
All three card claims re-verified with a lit control — triggerIcon 2 hits (declaration + one CHANGELOG line) against triggerSize's 10 across 4 files, so the zeros are readings. Line numbers drifted (complex.ts:928, card said :907); the facts did not.
⚠️ Note for the record: displayMode remains untouched and #7654 stays open with needs-user-decision. renderer.tsx seeds it into defaultProps, so a tombstone there would be a named refusal on documents this repo's own designer wrote — the calculus is inverted and it is a maintainer call.
Added measurement for the parked displayMode decision — the designer SEEDS it
From the domain:ui PM seat. Measured while triaging objectui#7655, whose body lists displayMode among the keys the chatbot registrations read. It is not read; this card is right. The reading below is posted here because it enlarges the blast radius of whichever route is chosen.
Word-boundary grep over packages/plugin-chatbot/src/** on origin/main, tests excluded — two hits, both renderer.tsx, neither a read:
Lit controls in the same population: floatingConfig 3 + 8, surface 13 files, processVisibility 11 + 2.
So the key is not merely declared-and-unread — the designer offers it as a control and seeds it into every document it emits. Every stored document the designer writes for chatbot-floating carries a key nothing consumes, and an author who sets displayMode: 'inline' gets floating regardless, silently.
What this changes about the two routes
Either route now has to touch renderer.tsx:504 and :526, not just the type:
Make it live — the renderer honours it, and chatbot-floating can render inline. Product question: is that what the registration split is for? If the two registrations are meant to be the discriminator, a key that duplicates them is a second dialect for one decision (AGENTS.md #0.1).
Retire it — and remove the designer control and the defaultProps seed with it, or the designer keeps emitting a key the schema refuses (objectui#7070: a control is restated, never deleted into a vacuum).
One thing the retire arm no longer needs to argue
The retire-vs-remove discriminator was settled and amended this round (objectui#7519 comment 5548726381, objectui#7678 comment 5548730153): a tombstone is available only where the retired thing has a surviving carrier. ChatbotSchema survives, so the prongs apply here, and prong 2 is met by the sameCHANGELOG.md:578 line that licensed the triggerIcon tombstone in PR #7669 — it advertised displayMode and floatingConfig together. So if the decision is to retire, the route is a tombstone, matching its sibling key, and no fresh discriminator argument is owed.
⚠️ This does not decide the card. Whether chatbot-floating should be able to render inline is not a contract question and is not the seat's to answer.
Maintainer ruling recorded — B: retire displayMode. Tombstone on ChatbotSchema, designer control and defaultProps seed removed. needs-user-decision cleared; card stays pm:queue for the ui seat.
Director seat (objectstack #12708), summon #14, session session_01LsEjuNMPitCHwEfYftZ1um, 2026-09-05T02:5xZ.
Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, replying to decision batch #44 in which this card was item 4 (the displayMode half only; triggerIcon landed in PR #7669 under the ui seat's own review). The batch stated the options as 「A 做活:chatbot 注册项尊重 displayMode: 'floating' ⇒ 同一件事两种拼写,且是行为变更。B 退役:类型 tombstone(?: never)+ 删注册项 inputs 里的控件 + 删 defaultProps 种子;存量文档因无镜像而不受影响,changeset 写明。C 保留声明、只摘控件和种子——类型里留着一句谎话」 and recommended 「推荐 B。ui 席,Clause-② yes(公开类型成员退役),@object-ui/typesminor + BREAKING,plugin-chatbot patch;#6152 铸镜像时的 tripwire 与 #7669 同款」. Verbatim reply: 「同意」 — B is adopted as recommended.
Ruled: B. The node type (chatbot vs chatbot-floating) is the one selector of presentation; displayMode is a second spelling of that choice that no renderer has ever read. Retire it:
packages/types/src/complex.tsChatbotSchema.displayMode → retirement tombstone (?: never) with the JSDoc pointing at type as the replacement, citing discriminator prong 2 (advertised at CHANGELOG.md:578, 3.3.0) exactly as the ui seat's 02:32Z measurement at 5548764449 established.
Not taken: A (two spellings for one decision, and a behaviour change on a key that shipped inert); C (the published type keeps teaching an AI author a switch that does not exist).
Execution: domain:ui seat. Clause-② yes (public type member retired); @object-ui/typesminor + BREAKING with the launch-window banner, @object-ui/plugin-chatbotpatch. Contract review at tier is the ui seat's, in-seat, per the 2026-08-31 ownership ruling — director audits after the fact only. Once this PR merges the card has no open half and closes on it; a closing keyword is fine this time.
State transition, same stroke: needs-user-decision stripped; pm:queue, finding, domain:ui retained. Ledger: objectstack director seat post #12708, summon #14.
⛔ The dispatch fence this PR was built on was STALE, and that is the seat's error, not the dev seat's
The contract review of PR #7705 (comment to follow on that PR) found it first. Recording it here because the dev seat followed the brief exactly and the brief was wrong.
What happened
My claim comment and both dispatch briefs said displayMode was "parked on a maintainer decision" and told the dev to carry it across unchanged because objectui#7654 owned its fate.
objectui#7654 was ruled at 2026-09-05T02:48:39Z — comment 5548848737, maintainer verbatim 「同意」, decision batch #44. Ruling B: retire displayMode — ?: never tombstone on ChatbotSchema, the designer control and the defaultProps seed removed. needs-user-decision was cleared in the same stroke; the card has read pm:queue since.
The first dispatch of this card was killed by a rate limit. When I re-issued the brief after the quota reset — more than four hours after that ruling — I copied the fence forward without re-reading the card. The stale sentence then propagated into eight lines across five tree carriers, the changeset, the PR body and the dev report.
Why it is worse than an ordinary stale read
The ruling cites this seat's own measurement as its ground: "citing discriminator prong 2 (advertised at CHANGELOG.md:578, 3.3.0) exactly as the ui seat's 02:32Z measurement at 5548764449 established."
So the sequence was: I measured displayMode, the maintainer ruled on that measurement, and I then dispatched a brief telling a dev the question was still open. ⛔ There is no version of "the base moved under me" that covers this — I am the one who moved it.
The rule this breaks is the one this seat has been enforcing all day
Every dispatch brief and every review brief issued today carries: ⛔ read the current state of every card and PR you name before writing a tense for it. Two contract-review REFUSEs earlier today were issued on exactly that defect in other seats' work. This is the same defect in mine, and it is the fifth instance I have recorded today of ruling or briefing from material I had not re-read.
⭐ The instruction is not enough on its own. Re-issuing a brief after an interruption is the specific moment it fails, because the brief looks already-verified. Any brief re-sent after a gap gets its cards re-read first — that is the concrete change, and it goes into this seat's check-in.
Which is why the review's other blocking item follows: moving displayMode off ChatbotSchema hollows the exact member ruling B names. Option B — keep both keys on ChatbotSchema — is now not merely the reviewer's preference; it is what leaves the ruling executable as written.
Claim: PM loop round 25 — domain:ui execution seat
Session: session_01KbJQ1y1J12nZxYzFWhP8Q3
Branch: claude/issue-7654-display-mode-retire
Worktree: objectui-issue-7654
Domain: domain:ui
File surface: packages/types/src/complex.ts (the ChatbotSchema.displayMode member only), packages/plugin-chatbot/src/renderer.tsx (the chatbot-floatinginputs control and the defaultProps seed), the affected pin tests, content/docs/** chatbot pages (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: fable — node scripts/pm/dispatch-gates.mjs --tier this round returns no path-derived mandate; fable is the clause-② CONTENT mandate per the maintainer's own ruling, which states Clause-② yes explicitly.
Clause-②: yes
Serial constraints cleared: Two live contentions on the same region, declared.#7732 (claude/issue-7295-chat-message-avatar-keys) edits packages/types/src/complex.ts, zod/complex.zod.ts and content/docs/plugins/plugin-chatbot.mdx — the same file and the same docs page, though its subject is the ChatMessage interface, not ChatbotSchema. #7726 edits packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts, which this card's tombstone will touch. Merge the base branch, ⛔ never rebase, and re-read both files before editing. Cleared by measurement: #7705merged4ce14f125 — it restored displayMode on ChatbotSchema byte-identically to base precisely so this ruling stays executable, and it left the designer control at renderer.tsx:498 and the seed at :520 in place; #7669 merged, its triggerIcon tombstone is the shape this one follows.
{
"issue": 7654,
"status": "done",
"branch": "claude/issue-7654-display-mode-retire",
"pr": "https://github.com/objectstack-ai/objectui/pull/7739",
"premise_still_valid": true,
"summary": "Text-only land-prep on PR #7739, commit 6b254df25 (fast-forward push 5a0059834..6b254df25; PR still draft, the card-7654 closing line intact, needs:contract-review still on both carriers). main had moved to 81a2eb1fb and was MERGED in at c8e5f66fb (no rebase, no force-push). EDIT 1: the changeset and the PR's Clause-2 section now carry an accept-set list with one line per face, including the previously absent manifest / author-time-validator / generated-JSX-props face, worded from my own measurements (inputs 20 to 19, defaultProps 9 keys to 8, validateTree 0 to 1 unknown-prop WARNING, generated props interface 20 members to 19) and explicitly bounded to author time - no stored document stops parsing and nothing at render moves. EDIT 2: the false objectui#7678-item-2 transposition is gone from both in-tree sites (the shape-pin comment in chatbot-display-mode-retired.test.ts and the TRIPWIRE comment in chatbot-registration-authoring-faces-7655.test.ts); each now states the measured fact that a scalar-union enum arm reddens BOTH pins. I also corrected the same false sentence in the PR body, and made the shape-pin test TITLE honest (it had called itself 'the tripwire that fires'). The correction was ALSO filed on card #7678 as item 3 (comment 5552764383) with the shape-conditional rule. ONE DIVERGENCE FROM THE BRIEF, flagged rather than softened: three of the four manifest-face readings reproduced EXACTLY; the fourth reproduced only in its mechanism. codegen.ts does derive a closed JSX-intrinsic props interface from inputs and it does go 20 to 19 members - but the stated consequence, that a .tsx page 'loses the attribute', does not reproduce in either direction I could measure: (a) compile() KEEPS the value, returning a tree carrying displayMode 'floating' on BOTH legs, so nothing is silently dropped - the consequence is a TYPE error, not a value loss; and (b) the shipped sdui-intrinsics.d.ts is generated from getPublicConfigs() (PUBLIC tier, 31 components), which does not contain chatbot-floating on either leg, so that published artifact is untouched. I also could not confirm the review's 'os validate' half: os validate runs safeValidateSchema (the Zod path) and is silent on this key before and after. The changeset and PR body say only what I measured, and name those three unmoved neighbours. State note: the brief said the killed prior run left nothing behind. The REMOTE tip was indeed unchanged at 5a0059834, but the local worktree carried an unpushed clean merge commit (055b6ce2d, older main) plus an untracked scratch test; I reset the local branch to the reviewed 5a0059834, re-merged current origin/main, and moved the scratch file out of the tree.",
"tests": "All at head 6b254df25 unless stated; exit codes captured by redirect BEFORE any pipe; verdicts read from each tool's own line; heavy runs through the container's shared verify lock. UNION: tsc -p packages/types/tsconfig.test.json --noEmit exit 0, 0 'error TS' lines | pnpm --filter @object-ui/types build exit 0, dist/complex.d.ts carries 2 'displayMode?: never' tombstones | pnpm exec vitest run packages/types/ packages/plugin-chatbot/ = 156 files / 2497 tests passed | pnpm lint (repo-wide eslint) = 'Tasks: 47 successful, 47 total', exit 0, 0 errors, NO narrowing | check-changeset-presence, check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite, check:control-bytes, check:shell-escape-residue, check:doc-fences, check:doc-types all exit 0. NOT MEASURED: check:doc-snippets returned its own 'PRECONDITION NOT MET (exit 2)' (26 packages unbuilt in this worktree) - that is 'could not run', not a failure, and this round touches no document; left to CI. ABLATION 1 (manifest faces, both legs): LIVE leg = packages/plugin-chatbot/src/renderer.tsx rewritten to origin/main's content. Mutation proven on disk: blob 8529f2cb to 3dbd2634 and equal to the origin/main blob, anchored greps on the removed control line and the removed seed line 0 to 1, head-only comment marker 1 to 0. Restore under trap EXIT INT TERM with an absolute path built from git rev-parse --show-toplevel, git checkout HEAD -- ABSPATH (never a bare checkout, which reads from the polluted index): blob back to 8529f2cb, git diff HEAD empty, porcelain clean. REBUILD PRECONDITION does not apply and is stated rather than skipped: the root vitest config aliases every @object-ui/* specifier to src, and the harness imports the registration relatively, so both legs read the mutated SOURCE - and the readings DIFFER, which is itself the proof no stale dist was consulted. Readings LIVE vs branch: inputs 20 / 19 (displayMode present / absent); defaultProps 9 keys with displayMode 'floating' / 8 keys, absent; validateTree on a node carrying displayMode = 0 diagnostics / exactly 1 (code unknown-prop, severity warning); control bogus key = 1 unknown-prop warning on BOTH legs; control live key enableMarkdown = clean on BOTH legs; generateDts props interface = 20 members with 'displayMode?: string' / 19 members, that line gone; compile() tree keys = ['type','displayMode'] on BOTH legs (value kept); PUBLIC-tier manifest = 31 components, chatbot-floating absent on BOTH legs. Fourth reading (author-time only) read directly off source: page.tsx:552 'const { tree, diagnostics } = compile(src, getJsxManifest());' and :553 'const errors = diagnostics.filter((d) => d.severity === 'error');' - warnings are discarded, only error blocks. ABLATION 2 (z.enum arm, run twice - once pre-edit and once again at 6b254df25): an optional z.enum over 'inline' | 'floating' injected on ChatbotSchema's twin in complex.zod.ts at a counted anchor asserted to occur exactly once. Mutation proven on disk: marker 0 to 1, blob 53fd0ed0 to 86793dd7. Result - tsc gives EXACTLY 1 error, zod-mirror-parity.test.ts(1586,14): error TS2322: Type '\"complex.zod.ts#ChatbotSchema\"' is not assignable to type 'never'; vitest gives EXACTLY 2 failures, both in chatbot-display-mode-retired.test.ts - the parse-green test AND the shape pin (42 passed). Baseline on the same tree: tsc exit 0, vitest 44/44. Restore under trap: blob back to 53fd0ed0, marker 1 to 0, git diff HEAD empty, porcelain clean. ABLATION 3 (parity ratchet still lit, run twice - once pre-edit and once again at 6b254df25): 'displayMode' removed from the ChatbotSchema UnmirroredDeclared entry, blob 681ed828 to 1f7a2b33, anchored counts old spelling 1 to 0 and new spelling 0 to 1. tsc gives EXACTLY 1 error at zod-mirror-parity.test.ts(1586,14). Under the SAME mutation vitest on the parity file stays 15/15 PASSED - the runtime census is blind to a per-key mismatch, so the instrument is tsc (CI's Type Check); vitest is NOT cited as parity evidence anywhere in the tree, the changeset or the PR body. Restore under trap: blob back to 681ed828, git diff HEAD empty. BYTES: control-byte self-scan (grep -naP over every file in the PR diff) clean; PR body and both comments scanned for tag-shaped fragments (none) and for closing keywords near an issue number (only the intended line-1 closing line for card 7654); PR body PATCHed via repo-scoped REST with body only, then READ BACK - stored is byte-identical to sent plus exactly the +58-byte appended bare footer, one footer total, draft:true and all four labels preserved; the #7678 comment likewise read back byte-identical plus its footer.",
"mcp_calls": "0 — every GitHub read and write went through the container's repo-scoped REST channel (probe green: GET /repos/objectstack-ai/objectui/pulls/7739 = 200); no mcp__github__* call was made this run",
"open_questions": [],
"out_of_scope_findings": []
}
Activity
claude commentedon Sep 4, 2026
domain:uiPM seat — ruling: split the two keys. Not dispatched as one card.Comment thread read before ruling: zero comments. The card's own "Suggested route" reaches the same split, and I am adopting it explicitly so the halves do not travel together.
triggerIcon— dispatchable, straightforward ADR-0049Declared
triggerIcon?: stringwith@default 'MessageCircle', destructured by nobody,FloatingChatbotTriggertakes no icon prop, and — the fact that makes it easy — it is absent from the registration'sinputs, so it is reachable only from TypeScript. No designer control depends on it and no designer-created node carries it. That is the ordinary enforce-or-remove case with this repo's established tombstone shape, and it needs no ruling beyond this one.Queued for dispatch, not sent yet — see the note on sequencing below.
displayMode— ⛔ NOT dispatchable. Maintainer decision.The card is right that the third option is real, and there is a compatibility trap that makes a tombstone actively wrong here:
renderer.tsx:526putsdefaultProps.displayMode: 'floating'on designer-created nodes. So stored documents the designer itself produced carry this key today. AretirementTombstone()would turn it into a named parse refusal on documents this repo's own designer wrote — the inverse of the usual argument, where a tombstone protects authors from a silent strip. Here the silent-strip/named-refusal calculus points the other way and I will not have a dev resolve it by judgement.Three options, none of which a dev should pick:
chatbotregistration honourdisplayMode: 'floating'. Behaviour change; the honest reading of the published type's own comment, which tells authors it selects the presentation.ChatbotSchema,inputsanddefaultProps. Must answer what happens to stored nodes carrying the key.defaultPropsseed, and record it as inert.The sharpest fact for whoever rules: the property panel currently paints a "Display Mode" control that cannot change anything, and the published type's comment tells an AI author it selects between inline and floating. Both surfaces teach a switch that does not exist.
Labelling
needs-user-decisionfor thedisplayModehalf.Sequencing
triggerIcon's fix isClause-②: yes(a tombstone moves parse behaviour), so it will park for the director seat. It touchespackages/types/src/complex.ts, which #7645 may also touch (that dispatch is measuringSchemaRegistry's honesty inregistry.ts, same package). HoldingtriggerIconuntil #7645 reports, rather than racing them into the same package.Confirmed not a duplicate of objectui#6152: that is the declared-but-unmirrored ledger (published TS the zod validator never heard of); this is declared-but-unread (whether any renderer consumes it). A key can be mirrored and inert, or unmirrored and live — fixing one says nothing about the other. Dedup was run with a lit positive control, so its zeros are readings.
Generated by Claude Code
claude commentedon Sep 4, 2026
PM review — accepted, parked for contract review. The open question is ruled A, for the reviewer to confirm at tier.
Clause-②: yes,needs:contract-reviewon both carriers, draft, not enqueued.Part of #7654rather thanFixes— deliberately, so merging cannot silently close a card that still carriesneeds-user-decisionfordisplayMode. Correct.The brief's assumption was falsified, and the response is the reason I asked
I asked whether
FloatingChatbotConfig's Zod mirror is strict or non-strict, and told you not to apply the tombstone pattern by reflex. The answer was neither — there is no mirror at all:floatingConfigsits in theUnmirroredDeclaredledger andBaseSchemaends.passthrough(), so the object rides through unvalidated.Two things follow, and both were handled the way I want:
retirementTombstone()half, and that is not an omission — there is no twin to host one. Saying so explicitly beats shipping a half-pattern and letting a reader assume the other half exists.Re-earning the tombstone-over-deletion route on the only channel available
This repo refuses deletion because a non-strict
z.objectsilently strips an undeclared key. That argument needs a mirror. With none, you rebuilt it ontscalone and measured it:TS2353TS2322TS2322Excess-property checking only reaches a fresh literal, so deletion would leave
const raw = { triggerIcon: 'Sparkles' }; const cfg: FloatingChatbotConfig = raw;silently accepted. The declarednevermakes the assignment itself ill-typed, so freshness stops mattering. The tombstone strictly dominates deletion on the only channel that exists here — and both rows are pinned, the deleted row as a live control on a genuinely undeclared key rather than prose, so the contrast cannot rot.⭐ The tripwire is the best thing in this PR. The test asserts such a node still parses green today, so if #6152 ever mints a
FloatingChatbotConfigSchemathis goes red — forcing whoever lands the mirror to add theretirementTombstone()half at the same time and flip the control rather than delete it into a vacuum. That is the #7070 discipline projected forward in time rather than applied retrospectively.The missed prediction
Predicted the error would read "not assignable to type
'never'"; it reads'undefined'—?: neverunderexactOptionalPropertyTypes: falseisnever | undefined, which collapses. Direction right, printed type name wrong, and no assertion in the PR depends on the word "never". Reporting the miss and checking whether anything rested on it is the complete form.Also: the first cross-package leg hit
PRECONDITION NOT MET(baseline already red —Cannot find module @object-ui/components, closure unbuilt), read as NOT MEASURED, closure built, leg re-run from a green baseline. Never scored as pass or red.The open question — ruled A, and it is the reviewer's to confirm
A — accept as shipped. Your reasoning is right and I am adopting it:
inputs, so TypeScript was the sole authoring route — and the tripwire guarantees the second channel is not forgotten.📌 For the director seat: this is a deliberate single-channel tombstone, and the reason is that no second channel exists for this object. If you want the two-channel shape, that decision is #6152, not this PR.
Premise re-measurement
All three card claims re-verified with a lit control —
triggerIcon2 hits (declaration + one CHANGELOG line) againsttriggerSize's 10 across 4 files, so the zeros are readings. Line numbers drifted (complex.ts:928, card said:907); the facts did not.displayModeremains untouched and #7654 stays open withneeds-user-decision.renderer.tsxseeds it intodefaultProps, so a tombstone there would be a named refusal on documents this repo's own designer wrote — the calculus is inverted and it is a maintainer call.Generated by Claude Code
claude commentedon Sep 5, 2026
Added measurement for the parked
displayModedecision — the designer SEEDS itFrom the
domain:uiPM seat. Measured while triaging objectui#7655, whose body listsdisplayModeamong the keys the chatbot registrations read. It is not read; this card is right. The reading below is posted here because it enlarges the blast radius of whichever route is chosen.Word-boundary grep over
packages/plugin-chatbot/src/**onorigin/main, tests excluded — two hits, bothrenderer.tsx, neither a read::504— designerinputsentry,{ name: 'displayMode', type: 'string', label: 'Display Mode', defaultValue: 'floating', … }:526—defaultProps,displayMode: 'floating'Lit controls in the same population:
floatingConfig3 + 8,surface13 files,processVisibility11 + 2.So the key is not merely declared-and-unread — the designer offers it as a control and seeds it into every document it emits. Every stored document the designer writes for
chatbot-floatingcarries a key nothing consumes, and an author who setsdisplayMode: 'inline'gets floating regardless, silently.What this changes about the two routes
Either route now has to touch
renderer.tsx:504and:526, not just the type:chatbot-floatingcan render inline. Product question: is that what the registration split is for? If the two registrations are meant to be the discriminator, a key that duplicates them is a second dialect for one decision (AGENTS.md#0.1).defaultPropsseed with it, or the designer keeps emitting a key the schema refuses (objectui#7070: a control is restated, never deleted into a vacuum).One thing the retire arm no longer needs to argue
The retire-vs-remove discriminator was settled and amended this round (objectui#7519 comment 5548726381, objectui#7678 comment 5548730153): a tombstone is available only where the retired thing has a surviving carrier.
ChatbotSchemasurvives, so the prongs apply here, and prong 2 is met by the sameCHANGELOG.md:578line that licensed thetriggerIcontombstone in PR #7669 — it advertiseddisplayModeandfloatingConfigtogether. So if the decision is to retire, the route is a tombstone, matching its sibling key, and no fresh discriminator argument is owed.chatbot-floatingshould be able to render inline is not a contract question and is not the seat's to answer.Generated by Claude Code
os-warren commentedon Sep 5, 2026
Maintainer ruling recorded — B: retire
displayMode. Tombstone onChatbotSchema, designer control anddefaultPropsseed removed.needs-user-decisioncleared; card stayspm:queuefor the ui seat.Director seat (objectstack #12708), summon #14, session
session_01LsEjuNMPitCHwEfYftZ1um, 2026-09-05T02:5xZ.Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, replying to decision batch #44 in which this card was item 4 (the
displayModehalf only;triggerIconlanded in PR #7669 under the ui seat's own review). The batch stated the options as 「A 做活:chatbot注册项尊重displayMode: 'floating'⇒ 同一件事两种拼写,且是行为变更。B 退役:类型 tombstone(?: never)+ 删注册项inputs里的控件 + 删defaultProps种子;存量文档因无镜像而不受影响,changeset 写明。C 保留声明、只摘控件和种子——类型里留着一句谎话」 and recommended 「推荐 B。ui 席,Clause-② yes(公开类型成员退役),@object-ui/typesminor+ BREAKING,plugin-chatbotpatch;#6152 铸镜像时的 tripwire 与 #7669 同款」. Verbatim reply: 「同意」 — B is adopted as recommended.Ruled: B. The node
type(chatbotvschatbot-floating) is the one selector of presentation;displayModeis a second spelling of that choice that no renderer has ever read. Retire it:packages/types/src/complex.tsChatbotSchema.displayMode→ retirement tombstone (?: never) with the JSDoc pointing attypeas the replacement, citing discriminator prong 2 (advertised atCHANGELOG.md:578, 3.3.0) exactly as the ui seat's 02:32Z measurement at5548764449established.packages/plugin-chatbot/src/renderer.tsx— remove thedisplayModeentry fromchatbot-floating'sinputs(the "Display Mode" control) and thedefaultProps.displayMode: 'floating'seed. A control is restated, never deleted into a vacuum (finding(views): the gantt and timeline branches still fabricate date-axis field names on all three faces — and app-shell's own #3129 note certifies them as already fixed #7070): the restatement is the tombstone plus the changeset text.displayModeis in the 121 declared-but-unmirrored keys across 16 schema pairs — the lane #6058's new UnmirroredDeclared ledger made visible #6152 unmirrored ledger andBaseSchemais passthrough, so runtime validation of this key is unchanged (zero, before and after). The changeset must say so in one sentence, and the PR carries the same tripwire shape as feat(types)!: retire FloatingChatbotConfig.triggerIcon as an ADR-0049 tombstone (objectui#7654) #7669 so that minting a mirror under 121 declared-but-unmirrored keys across 16 schema pairs — the lane #6058's new UnmirroredDeclared ledger made visible #6152 forces theretirementTombstone()half at that time.Not taken: A (two spellings for one decision, and a behaviour change on a key that shipped inert); C (the published type keeps teaching an AI author a switch that does not exist).
Execution:
domain:uiseat. Clause-② yes (public type member retired);@object-ui/typesminor+ BREAKING with the launch-window banner,@object-ui/plugin-chatbotpatch. Contract review at tier is the ui seat's, in-seat, per the 2026-08-31 ownership ruling — director audits after the fact only. Once this PR merges the card has no open half and closes on it; a closing keyword is fine this time.State transition, same stroke:
needs-user-decisionstripped;pm:queue,finding,domain:uiretained. Ledger: objectstack director seat post #12708, summon #14.Generated by Claude Code
claude commentedon Sep 5, 2026
⛔ The dispatch fence this PR was built on was STALE, and that is the seat's error, not the dev seat's
The contract review of PR #7705 (comment to follow on that PR) found it first. Recording it here because the dev seat followed the brief exactly and the brief was wrong.
What happened
My claim comment and both dispatch briefs said
displayModewas "parked on a maintainer decision" and told the dev to carry it across unchanged because objectui#7654 owned its fate.objectui#7654 was ruled at 2026-09-05T02:48:39Z — comment 5548848737, maintainer verbatim 「同意」, decision batch #44. Ruling B: retire
displayMode—?: nevertombstone onChatbotSchema, the designer control and thedefaultPropsseed removed.needs-user-decisionwas cleared in the same stroke; the card has readpm:queuesince.The first dispatch of this card was killed by a rate limit. When I re-issued the brief after the quota reset — more than four hours after that ruling — I copied the fence forward without re-reading the card. The stale sentence then propagated into eight lines across five tree carriers, the changeset, the PR body and the dev report.
Why it is worse than an ordinary stale read
The ruling cites this seat's own measurement as its ground: "citing discriminator prong 2 (advertised at
CHANGELOG.md:578, 3.3.0) exactly as the ui seat's 02:32Z measurement at5548764449established."So the sequence was: I measured
displayMode, the maintainer ruled on that measurement, and I then dispatched a brief telling a dev the question was still open. ⛔ There is no version of "the base moved under me" that covers this — I am the one who moved it.The rule this breaks is the one this seat has been enforcing all day
Every dispatch brief and every review brief issued today carries: ⛔ read the current state of every card and PR you name before writing a tense for it. Two contract-review REFUSEs earlier today were issued on exactly that defect in other seats' work. This is the same defect in mine, and it is the fifth instance I have recorded today of ruling or briefing from material I had not re-read.
⭐ The instruction is not enough on its own. Re-issuing a brief after an interruption is the specific moment it fails, because the brief looks already-verified. Any brief re-sent after a gap gets its cards re-read first — that is the concrete change, and it goes into this seat's check-in.
What this changes for PR #7705
Nothing about the ruled shape (one interface per registration stands, #6169). What changes:
displayModetense in the tree, the changeset and the PR body must read ruled B, not parked.displayModestill stays untouched in this PR — but for a different reason. Not "pending a decision": the decision exists and its execution is finding(types/plugin-chatbot):chatbot-floatingdeclares two keys nothing reads -displayMode(designer control included) andFloatingChatbotConfig.triggerIcon#7654's own PR, which must tombstone the member onChatbotSchemaand strip the control and the seed. This PR must leave that member executable for it.displayModeoffChatbotSchemahollows the exact member ruling B names. Option B — keep both keys onChatbotSchema— is now not merely the reviewer's preference; it is what leaves the ruling executable as written.Remediation dispatched with the corrected fence.
Generated by Claude Code
claude commentedon Sep 5, 2026
Claim: PM loop round 25 —
domain:uiexecution seatSession:
session_01KbJQ1y1J12nZxYzFWhP8Q3Branch:
claude/issue-7654-display-mode-retireWorktree:
objectui-issue-7654Domain:
domain:uiFile surface:
packages/types/src/complex.ts(theChatbotSchema.displayModemember only),packages/plugin-chatbot/src/renderer.tsx(thechatbot-floatinginputscontrol and thedefaultPropsseed), the affected pin tests,content/docs/**chatbot pages (stop on breach; explain in the report)Container & model: M,
mode:subagent,model: fable—node scripts/pm/dispatch-gates.mjs --tierthis round returns no path-derived mandate; fable is the clause-② CONTENT mandate per the maintainer's own ruling, which statesClause-② yesexplicitly.Clause-②: yes
Serial constraints cleared: Two live contentions on the same region, declared.
#7732(claude/issue-7295-chat-message-avatar-keys) editspackages/types/src/complex.ts,zod/complex.zod.tsandcontent/docs/plugins/plugin-chatbot.mdx— the same file and the same docs page, though its subject is theChatMessageinterface, notChatbotSchema.#7726editspackages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts, which this card's tombstone will touch. Merge the base branch, ⛔ never rebase, and re-read both files before editing. Cleared by measurement:#7705merged4ce14f125— it restoreddisplayModeonChatbotSchemabyte-identically to base precisely so this ruling stays executable, and it left the designer control atrenderer.tsx:498and the seed at:520in place;#7669merged, itstriggerIcontombstone is the shape this one follows.Generated by Claude Code
claude commentedon Sep 5, 2026
os-dev-report
{ "issue": 7654, "status": "done", "branch": "claude/issue-7654-display-mode-retire", "pr": "https://github.com/objectstack-ai/objectui/pull/7739", "premise_still_valid": true, "summary": "Text-only land-prep on PR #7739, commit 6b254df25 (fast-forward push 5a0059834..6b254df25; PR still draft, the card-7654 closing line intact, needs:contract-review still on both carriers). main had moved to 81a2eb1fb and was MERGED in at c8e5f66fb (no rebase, no force-push). EDIT 1: the changeset and the PR's Clause-2 section now carry an accept-set list with one line per face, including the previously absent manifest / author-time-validator / generated-JSX-props face, worded from my own measurements (inputs 20 to 19, defaultProps 9 keys to 8, validateTree 0 to 1 unknown-prop WARNING, generated props interface 20 members to 19) and explicitly bounded to author time - no stored document stops parsing and nothing at render moves. EDIT 2: the false objectui#7678-item-2 transposition is gone from both in-tree sites (the shape-pin comment in chatbot-display-mode-retired.test.ts and the TRIPWIRE comment in chatbot-registration-authoring-faces-7655.test.ts); each now states the measured fact that a scalar-union enum arm reddens BOTH pins. I also corrected the same false sentence in the PR body, and made the shape-pin test TITLE honest (it had called itself 'the tripwire that fires'). The correction was ALSO filed on card #7678 as item 3 (comment 5552764383) with the shape-conditional rule. ONE DIVERGENCE FROM THE BRIEF, flagged rather than softened: three of the four manifest-face readings reproduced EXACTLY; the fourth reproduced only in its mechanism. codegen.ts does derive a closed JSX-intrinsic props interface from inputs and it does go 20 to 19 members - but the stated consequence, that a .tsx page 'loses the attribute', does not reproduce in either direction I could measure: (a) compile() KEEPS the value, returning a tree carrying displayMode 'floating' on BOTH legs, so nothing is silently dropped - the consequence is a TYPE error, not a value loss; and (b) the shipped sdui-intrinsics.d.ts is generated from getPublicConfigs() (PUBLIC tier, 31 components), which does not contain chatbot-floating on either leg, so that published artifact is untouched. I also could not confirm the review's 'os validate' half: os validate runs safeValidateSchema (the Zod path) and is silent on this key before and after. The changeset and PR body say only what I measured, and name those three unmoved neighbours. State note: the brief said the killed prior run left nothing behind. The REMOTE tip was indeed unchanged at 5a0059834, but the local worktree carried an unpushed clean merge commit (055b6ce2d, older main) plus an untracked scratch test; I reset the local branch to the reviewed 5a0059834, re-merged current origin/main, and moved the scratch file out of the tree.", "tests": "All at head 6b254df25 unless stated; exit codes captured by redirect BEFORE any pipe; verdicts read from each tool's own line; heavy runs through the container's shared verify lock. UNION: tsc -p packages/types/tsconfig.test.json --noEmit exit 0, 0 'error TS' lines | pnpm --filter @object-ui/types build exit 0, dist/complex.d.ts carries 2 'displayMode?: never' tombstones | pnpm exec vitest run packages/types/ packages/plugin-chatbot/ = 156 files / 2497 tests passed | pnpm lint (repo-wide eslint) = 'Tasks: 47 successful, 47 total', exit 0, 0 errors, NO narrowing | check-changeset-presence, check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite, check:control-bytes, check:shell-escape-residue, check:doc-fences, check:doc-types all exit 0. NOT MEASURED: check:doc-snippets returned its own 'PRECONDITION NOT MET (exit 2)' (26 packages unbuilt in this worktree) - that is 'could not run', not a failure, and this round touches no document; left to CI. ABLATION 1 (manifest faces, both legs): LIVE leg = packages/plugin-chatbot/src/renderer.tsx rewritten to origin/main's content. Mutation proven on disk: blob 8529f2cb to 3dbd2634 and equal to the origin/main blob, anchored greps on the removed control line and the removed seed line 0 to 1, head-only comment marker 1 to 0. Restore under trap EXIT INT TERM with an absolute path built from git rev-parse --show-toplevel, git checkout HEAD -- ABSPATH (never a bare checkout, which reads from the polluted index): blob back to 8529f2cb, git diff HEAD empty, porcelain clean. REBUILD PRECONDITION does not apply and is stated rather than skipped: the root vitest config aliases every @object-ui/* specifier to src, and the harness imports the registration relatively, so both legs read the mutated SOURCE - and the readings DIFFER, which is itself the proof no stale dist was consulted. Readings LIVE vs branch: inputs 20 / 19 (displayMode present / absent); defaultProps 9 keys with displayMode 'floating' / 8 keys, absent; validateTree on a node carrying displayMode = 0 diagnostics / exactly 1 (code unknown-prop, severity warning); control bogus key = 1 unknown-prop warning on BOTH legs; control live key enableMarkdown = clean on BOTH legs; generateDts props interface = 20 members with 'displayMode?: string' / 19 members, that line gone; compile() tree keys = ['type','displayMode'] on BOTH legs (value kept); PUBLIC-tier manifest = 31 components, chatbot-floating absent on BOTH legs. Fourth reading (author-time only) read directly off source: page.tsx:552 'const { tree, diagnostics } = compile(src, getJsxManifest());' and :553 'const errors = diagnostics.filter((d) => d.severity === 'error');' - warnings are discarded, only error blocks. ABLATION 2 (z.enum arm, run twice - once pre-edit and once again at 6b254df25): an optional z.enum over 'inline' | 'floating' injected on ChatbotSchema's twin in complex.zod.ts at a counted anchor asserted to occur exactly once. Mutation proven on disk: marker 0 to 1, blob 53fd0ed0 to 86793dd7. Result - tsc gives EXACTLY 1 error, zod-mirror-parity.test.ts(1586,14): error TS2322: Type '\"complex.zod.ts#ChatbotSchema\"' is not assignable to type 'never'; vitest gives EXACTLY 2 failures, both in chatbot-display-mode-retired.test.ts - the parse-green test AND the shape pin (42 passed). Baseline on the same tree: tsc exit 0, vitest 44/44. Restore under trap: blob back to 53fd0ed0, marker 1 to 0, git diff HEAD empty, porcelain clean. ABLATION 3 (parity ratchet still lit, run twice - once pre-edit and once again at 6b254df25): 'displayMode' removed from the ChatbotSchema UnmirroredDeclared entry, blob 681ed828 to 1f7a2b33, anchored counts old spelling 1 to 0 and new spelling 0 to 1. tsc gives EXACTLY 1 error at zod-mirror-parity.test.ts(1586,14). Under the SAME mutation vitest on the parity file stays 15/15 PASSED - the runtime census is blind to a per-key mismatch, so the instrument is tsc (CI's Type Check); vitest is NOT cited as parity evidence anywhere in the tree, the changeset or the PR body. Restore under trap: blob back to 681ed828, git diff HEAD empty. BYTES: control-byte self-scan (grep -naP over every file in the PR diff) clean; PR body and both comments scanned for tag-shaped fragments (none) and for closing keywords near an issue number (only the intended line-1 closing line for card 7654); PR body PATCHed via repo-scoped REST with body only, then READ BACK - stored is byte-identical to sent plus exactly the +58-byte appended bare footer, one footer total, draft:true and all four labels preserved; the #7678 comment likewise read back byte-identical plus its footer.", "mcp_calls": "0 — every GitHub read and write went through the container's repo-scoped REST channel (probe green: GET /repos/objectstack-ai/objectui/pulls/7739 = 200); no mcp__github__* call was made this run", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code