Skip to content

finding(plugin-chatbot): per-message avatar / avatarFallback work end to end but no authoring-facing type declares them — #4424's RuntimeOnlyMessageKeys picked only three keys #7295

Description

@yinlianghui

Found while covering content/docs/plugins/plugin-chatbot.mdx for #5174 batch 5 (PR #7294). Filed unassigned and not fixed there — that PR's surface is the UNGATED_DOCS ledger plus three doc pages, and this is a package type.

What

Authoring a chatbot node with a per-message avatar override works, all the way through:

  • Chatbot resolves it: message.avatarFallback || userAvatarFallback and message.avatar || userAvatarUrl — packages/plugin-chatbot/src/index.tsx:177-178.
  • The authoring-to-runtime seam carries it: authoredToRuntimeMessage destructures only role / timestamp / toolInvocations and spreads the rest (...passthrough) — packages/plugin-chatbot/src/chatMessageAdapter.ts:256-262.
  • The SDUI renderer feeds it: toRuntimeMessages(messages) into Chatbot — packages/plugin-chatbot/src/renderer.tsx.

But no type an author can name declares those two keys.

type declares avatar / avatarFallback?
ChatMessage from @object-ui/types (what ChatbotSchema.messages is typed as) no
SeamChatMessage from @object-ui/plugin-chatbot no — RuntimeOnlyMessageKeys is Pick(RuntimeChatMessage, 'buildProgress' or 'blueprintProgress' or 'charts')
ChatMessage from @object-ui/plugin-chatbot (the runtime shape) yes

So ChatbotSchema.messages is ChatMessage[] from @object-ui/types, and that interface has no index signature — an author who annotates their schema gets TS2353: 'avatarFallback' does not exist in type 'ChatMessage' on a value that renders correctly.

Measured

With the four example schemas on content/docs/plugins/plugin-chatbot.mdx annotated ChatbotSchema, the doc-snippet gate reports exactly five of these, one per documented per-message avatar:

plugin-chatbot.mdx:326:7  TS2353: Object literal may only specify known properties, and 'avatarFallback' does not exist in type 'ChatMessage'.
plugin-chatbot.mdx:350:7  TS2353: ... 'avatarFallback' ...
plugin-chatbot.mdx:376:7  TS2353: ... 'avatarFallback' ...
plugin-chatbot.mdx:382:7  TS2353: ... 'avatarFallback' ...
plugin-chatbot.mdx:393:7  TS2353: ... 'avatarFallback' ...

PR #7294 therefore leaves those three example blocks unannotated rather than deleting working, documented behaviour to satisfy a checker. That is a deliberate hole in that page's gate coverage, and it closes when this is decided.

Why it is worth a card rather than a shrug

This is the residue of #4424, which is the card that introduced SeamChatMessage and named the render-only keys. It picked buildProgress, blueprintProgress and charts — the keys API mode lifts out of the stream — and did not consider the two keys a human author writes by hand. The seam is otherwise exactly the mechanism that is supposed to make this expressible.

Related: #4383 (the barrel used to export two different ChatMessage types), #4399 (the as any this seam replaced), #6169 (the chatbot authoring surface becoming one named type).

Two readings, not resolved here

  1. The authoring contract should carry them. Add avatar?: string and avatarFallback?: string to @object-ui/types' ChatMessage, since a metadata author demonstrably writes them and the runtime honours them. Cheapest, and makes declared = enforced true again for this key pair.
  2. They are render-only and the seam should carry them. Widen RuntimeOnlyMessageKeys to include them, leaving the pure authoring contract narrow. This keeps @object-ui/types minimal but does not help the ChatbotSchema.messages author, who never names SeamChatMessage — so on its own it does not close the hole above.

Reading 1 is what the measured usage argues for; reading 2 is what the existing seam vocabulary argues for. Someone who owns the authoring contract should pick.

Reachability

Nothing breaks at runtime — the keys work today. The cost is type-level: a TypeScript author annotating their chatbot metadata is told a correct value is wrong, and a documentation page cannot be fully gate-covered.

Activity

  1. added
    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
    on Sep 5, 2026
  2. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 — domain:spec / priority:p3 / pm:queue

    锚定 (anchoring):卡片的读法 1(把两个键加进 @object-ui/types 的 ChatMessage)落在 packages/types ⇒ domain:spec;读法 2(放宽 RuntimeOnlyMessageKeys)落在 packages/plugin-chatbot ⇒ domain:ui。按"谁拥有 authoring contract"锚定 domain:spec —— 卡片自己就是这么问的("Someone who owns the authoring contract should pick"),而且 ChatbotSchema.messages 的类型来自 types,裁决权在那一侧。⚠️ 若裁决走读法 2,这张卡要交接给 domain:ui,见下文。

    在 origin/main a472b07 上复核的锚点 —— 全部成立,行号未漂

    packages/plugin-chatbot/src/index.tsx:173       ? (message.avatar || userAvatarUrl)
    packages/plugin-chatbot/src/index.tsx:177       ? (message.avatarFallback || userAvatarFallback)
    packages/plugin-chatbot/src/index.tsx:178       : (message.avatarFallback || assistantAvatarFallback)
    
    packages/plugin-chatbot/src/chatMessageAdapter.ts:91    type RuntimeOnlyMessageKeys = Pick<
    packages/plugin-chatbot/src/chatMessageAdapter.ts:92      RuntimeChatMessage,
    packages/plugin-chatbot/src/chatMessageAdapter.ts:93      'buildProgress' | 'blueprintProgress' | 'charts'
    packages/plugin-chatbot/src/chatMessageAdapter.ts:94    >;
    packages/plugin-chatbot/src/chatMessageAdapter.ts:256   const { role, timestamp, toolInvocations, ...passthrough } = message;
    

    三个键、...passthrough、message.avatar* 的读取,全对上。

    @object-ui/types 侧的负结果带正控制:packages/types/src/complex.ts:594 export interface ChatMessage,声明体到 :642 共 49 行(这是分母),在其中 grep avatar 与 [key: string] —— 0 命中。所以"没有 avatar 键、也没有 index signature,注解后必然 TS2353"是实测的缺席,不是读错文件。

    定级理由

    priority:p3:运行期无损,卡片自己写清楚了("the keys work today")。代价是类型面 + 一个门禁覆盖洞(PR #7294 在 plugin-chatbot.mdx 上故意留了三块示例不加注解,5 处 TS2353 都是同一对键)。没有静默错答,没有安全面,有明确绕法(不加注解 / as any)。

    定型:两条读法都不是 Feature,不踩 manual floor

    用机械边界测试逐条过:

    • 读法 1 = 把一对已经被消费、已经被文档教授的键补进声明 ⇒ 恢复 declared = enforced ⇒ Bug/tidy。它不加宽可授权面:作者今天就能写、就能渲染出来,只是类型骗他说不行。
    • 读法 2 = 把同两个键补进 seam 词表 ⇒ 同理,也是恢复。

    所以不需要人工底线,domain:spec 席可以直接裁并落地。⚠️ 但两者都动已发布的 TS 类型,必须带 changeset。

    ⛔ 给执行席的边界

    1. 卡片对读法 2 的自我批评要当真、不要绕过:RuntimeOnlyMessageKeys 放宽了,并不能帮到写 ChatbotSchema.messages 的作者 —— 他从头到尾不会写出 SeamChatMessage 这个名字。单做读法 2 不关闭上面那个门禁洞。若裁决选它,必须同时说明门禁洞怎么办(否则这张卡不算做完)。
    2. ⛔ 不要用给 ChatMessage 加 index signature 的办法收工 —— 那正是 finding(types): BaseSchema's [key: string]: any leaves every component schema open, so a "declare the surface" fix can never reject a misspelled TOP-LEVEL key #5155 记录的、让"声明表面"永远无法拒绝拼错键的那个形状,是在这张卡上还债的同时新欠一笔。
    3. 落地后回 content/docs/plugins/plugin-chatbot.mdx 把 PR docs(guides,plugins,tooling): clear three .mdx pages off the doc-snippet ledger (#5174 batch 5) #7294 留下的三块示例补上注解,并从 UNGATED_DOCS 里销账 —— 这是本卡的完成判据之一,不是可选尾巴。
    4. 若裁决是读法 2,请把 domain:spec 改成 domain:ui 并说明理由,别让 lane 与落点脱节。

    ⛔ 分诊席不认领、不派单、不写码、不裁决读法。


    Generated by Claude Code

  3. self-assigned this
    on Sep 5, 2026
  4. os-justin commented on Sep 5, 2026

    @os-justin
    Collaborator

    Claim: PM loop round R1 (third wave)
    Session: session_01BAZFhALsQsGqxui8sNqM8s
    Branch: claude/issue-7295-chat-message-avatar-keys
    Worktree: objectui-issue-7295
    Domain: domain:spec
    File surface (region level): packages/types/src/complex.ts — the ChatMessage interface only (:594–642 on 446d93d) · packages/types/src/zod/complex.zod.ts — the ChatMessageSchema object only (:360…) · one pin under packages/types/src/__tests__/ · content/docs/plugins/plugin-chatbot.mdx — annotate the three example blocks PR #7294 left unannotated (no prose rewrite) · scripts/check-doc-snippet-types.mjs — only if an UNGATED_DOCS row for that page still exists (one row removal; a triage-declared completion criterion, comment 5548376176, so the cross-domain touch is declared here) · .changeset/* (@object-ui/types) · ⛔ packages/plugin-chatbot/** read-only (RuntimeOnlyMessageKeys untouched; reading 2 rejected as incomplete by triage's own boundary 1) · ⛔ no index signature on ChatMessage (triage boundary 2, #5155)
    Container & model: S, mode:subagent, model: fable = CONTRACT_REVIEW_TIER (two new declared keys on a published payload type)
    Clause-②: yes
    Serial constraints cleared (file / region granularity per #5734 comment 5551274423): complex.ts / complex.zod.ts are held by no in-flight branch (#5741: packages/core/src/evaluator/**; #6751: one fixture; PR #7726 queued for landing: base.ts / base.zod.ts / form.zod.ts top / expression.*; PR #7723 queued: form.ts / form.zod.ts CheckboxSchema) · plugin-chatbot.mdx untouched by every open branch · the dev merges origin/main before opening the PR and again if a sibling lands; the queue arbitrates.

    Premise re-verified by this seat on origin/main = 446d93d (12:22Z): packages/types/src/complex.ts:594 export interface ChatMessage with zero avatar / index-signature hits inside it; zod/complex.zod.ts:360 export const ChatMessageSchema = z.object({ with no avatar member; packages/plugin-chatbot/src/index.tsx:173–178 reads message.avatar / message.avatarFallback; chatMessageAdapter.ts:91–94 RuntimeOnlyMessageKeys = 'buildProgress' | 'blueprintProgress' | 'charts', :256 spreads ...passthrough. Last commits on the two type files: 4ce14f1 (#7655), neither touched the ChatMessage block.

    Disposition — reading 1, under a declared veto window (⛔ not a maintainer ruling). Triage (5548376176) graded both readings as Bug-side restoration of declared = enforced, outside the manual floor, and delegated the choice to this seat ("domain:spec 席可以直接裁并落地"); its boundary 1 records that reading 2 alone does not close the doc-gate hole, so reading 1 is the only complete fix: declare avatar?: string and avatarFallback?: string on ChatMessage on both faces, with docblocks citing the read sites. Reversible by a two-line re-declaration if vetoed. Completion criteria carried from triage: the three plugin-chatbot.mdx example blocks get their ChatbotSchema annotations back and the doc-snippet gate stays green on them; no index signature; changeset present.


    Generated by Claude Code

  5. os-justin commented on Sep 5, 2026

    @os-justin
    Collaborator

    Claim (dev seat): implementing under the domain:spec seat's dispatch (PM claim comment 5551795779, session session_01BAZFhALsQsGqxui8sNqM8s, PM loop round R1 third wave).

    Branch: claude/issue-7295-chat-message-avatar-keys (pushed empty first as the write-route probe; base 446d93d)
    Worktree: objectui-issue-7295

    Disposition executed as dispatched — reading 1: avatar?: string and avatarFallback?: string declared on ChatMessage in packages/types/src/complex.ts and on ChatMessageSchema in packages/types/src/zod/complex.zod.ts, each docblock citing the read site (packages/plugin-chatbot/src/index.tsx:173–178). No index signature. packages/plugin-chatbot/** stays read-only (RuntimeOnlyMessageKeys untouched; SeamChatMessage inheritance verified by that package's type-check against the rebuilt types dist). The three content/docs/plugins/plugin-chatbot.mdx example blocks PR #7294 left unannotated (supportChat, salesBot, multiAgentChat) get their ChatbotSchema annotation back. No UNGATED_DOCS row for that page exists on main — PR #7294 already removed it — so nothing under scripts/ is touched.

    Zone-2 "before" measured on 446d93d (built packages/types/dist): ChatMessageSchema is a plain strip-mode z.object with no catchall. An authored per-message avatar / avatarFallback parses green through ChatMessageSchema, ChatbotSchema and safeValidateSchema and is DROPPED from the parsed output on all three paths; avatar: 42 is admitted-and-stripped, never refused. The runtime honours the key only because the renderer receives the un-parsed document. Declaring the keys makes the parse keep them and refuse a wrong-typed value at the key — recorded in the PR body as a measured behaviour change (a restoration: the renderer already honours the key). The TS-face "before" reproduces the card's count exactly: the three blocks annotated ChatbotSchema against the base dist give 5 x TS2353 on avatarFallback and nothing else. Both faces of ChatbotSchema's own avatar config keys are string, and the runtime ChatMessage in ChatbotEnhanced.tsx declares both per-message keys as string, so the declared type is not narrower than what the runtime accepts.

    Assignee untouched.


    Generated by Claude Code

  6. os-justin commented on Sep 5, 2026

    @os-justin
    Collaborator

    os-dev-report

    {
      "issue": 7295,
      "status": "done",
      "branch": "claude/issue-7295-chat-message-avatar-keys",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7732",
      "premise_still_valid": true,
      "clause_2": "yes — two new declared keys on a published payload type (ChatMessage / ChatMessageSchema); needs:contract-review attached, PR stays draft for the in-seat review",
      "line_budget": "n/a",
      "summary": "Reading 1 executed as dispatched: avatar?: string and avatarFallback?: string declared on ChatMessage (packages/types/src/complex.ts) and on ChatMessageSchema (packages/types/src/zod/complex.zod.ts), each docblock citing packages/plugin-chatbot/src/index.tsx:173-178; no index signature; packages/plugin-chatbot/** untouched, SeamChatMessage inheritance proven by type-check plus a positive scratch probe against the rebuilt dist (red against the ablated dist). Premise re-verified on 446d93d: zero avatar / index-signature hits inside ChatMessage on either face, read sites and the seam spread at :256 present. Zone-2 measured before: ChatMessageSchema is a plain strip-mode z.object (no catchall) — an authored per-message avatar parses green through ChatMessageSchema, ChatbotSchema and safeValidateSchema and is DROPPED from the parsed output on all three paths; avatar: 42 was admitted-and-stripped. After: the parse keeps the value and refuses a non-string at the key (messages.0.avatar through the entry point) — recorded in the PR body and the changeset as a measured behaviour change, still a restoration; patch level with the argument stated (objectui#6938 / #6150 precedent). Value type string matches ChatbotSchema's own userAvatarUrl / userAvatarFallback (pinned by invariant equality) and the runtime ChatbotEnhanced.tsx:92-93 declaration. Docs: the three plugin-chatbot.mdx example blocks PR #7294 left unannotated (supportChat, salesBot, multiAgentChat; measured from that PR's landed diff 9bf0abfe) are annotated ChatbotSchema again, no prose rewrite; no UNGATED_DOCS row for that page exists on main (PR #7294 removed it), so scripts/ untouched. origin/main merged before the PR (b74a859, PR #7723 landed after dispatch; clean merge, diff vs main is exactly the five files). Assignee untouched; claim comment 5551863889.",
      "files_changed": [
        "packages/types/src/complex.ts",
        "packages/types/src/zod/complex.zod.ts",
        "packages/types/src/__tests__/chat-message-avatar-keys-7295.test.ts",
        "content/docs/plugins/plugin-chatbot.mdx",
        ".changeset/7295-chat-message-avatar-keys.md"
      ],
      "gates": [
        { "command": "pnpm --filter @object-ui/types build", "exit": 0, "head": "4b0f366c", "verdict": "dist completeness: 1 package(s) complete (120 emitted files verified); dist/complex.d.ts carries avatar?: string (count 1)" },
        { "command": "pnpm exec vitest run --maxWorkers=2 packages/types/", "exit": 0, "head": "4b0f366c", "verdict": "Test Files 115 passed (115) · Tests 1985 passed (1985); pin alone 25/25" },
        { "command": "pnpm --filter @object-ui/types type-check", "exit": 0, "head": "4b0f366c", "verdict": "tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json — 0 diagnostics; --listFiles on tsconfig.test.json lists the pin (count 1)" },
        { "command": "pnpm --filter @object-ui/plugin-chatbot type-check (against rebuilt types dist)", "exit": 0, "head": "4b0f366c", "verdict": "tsc --noEmit && tsc -p tsconfig.test.json — 0 diagnostics" },
        { "command": "scratch tsc probe: Equal of NonNullable SeamChatMessage avatar / avatarFallback with string + literal carrying both", "exit": 0, "head": "4b0f366c", "verdict": "0 diagnostics; same probe against the ABLATED dist: exit 2, TS2339 on SeamChatMessage avatar and avatarFallback, TS2353 on the literal" },
        { "command": "pnpm exec vitest run --maxWorkers=2 packages/plugin-chatbot/", "exit": 0, "head": "63ece2c8", "verdict": "Test Files 37 passed (37) · Tests 456 passed (456) — run before the main merge; the merge (PR #7723: form.ts, form.zod.ts, one test, one changeset) touches no plugin-chatbot input" },
        { "command": "node scripts/check-changeset-presence.mjs", "exit": 0, "head": "4b0f366c", "verdict": "3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/7295-chat-message-avatar-keys.md" },
        { "command": "node scripts/check-changeset-fixed.mjs", "exit": 0, "head": "4b0f366c", "verdict": "All workspace packages are in the changeset fixed group" },
        { "command": "node scripts/check-changeset-no-major.mjs", "exit": 0, "head": "4b0f366c", "verdict": "No changeset declares a major bump" },
        { "command": "node scripts/check-control-bytes.mjs (re-run after the merge commit)", "exit": 0, "head": "4b0f366c", "verdict": "OK (scanned 6300 tracked text file(s); skipped 85 binary)" },
        { "command": "node scripts/check-spec-symbol-derivation.mjs (pnpm check:spec-symbols)", "exit": 0, "head": "63ece2c8 content", "verdict": "spec member citations: no comment cites a key its spec symbol does not declare" },
        { "command": "node scripts/check-doc-component-types.mjs (pnpm check:doc-types)", "exit": 0, "head": "4b0f366c", "verdict": "Every documented component type is registered" },
        { "command": "node scripts/check-doc-fence-languages.mjs (pnpm check:doc-fences)", "exit": 0, "head": "4b0f366c", "verdict": "every TypeScript block in 227 document(s) is fenced ts/tsx/typescript (declared exceptions unchanged)" },
        { "command": "node scripts/check-doc-links.mjs", "exit": 0, "head": "63ece2c8 content", "verdict": "Links are valid across 17 scan roots" },
        { "command": "node /home/user/objectstack/scripts/pm/check-governed-merges.mjs --test (five paths)", "exit": 0, "head": "file list unchanged by the merge", "verdict": "0 of 5 path(s) hit the register — NOT governed" },
        { "command": "pnpm check:doc-snippets", "exit": "NOT MEASURED", "head": "4b0f366c", "verdict": "--build-filter names the whole plugin + app-shell closure (26 packages), beyond the foreground cap on a shared box. Narrowed measurement declared: the three annotated blocks extracted from the mdx as on disk, tsc --strict against the rebuilt packages/types/dist → exit 0, 0 diagnostics (before, base dist: exit 2, exactly 5 x TS2353 on avatarFallback). CI Doc Snippet Type Check is the measurement." },
        { "command": "CI on PR #7732", "exit": "in_progress", "head": "4b0f366c", "verdict": "report delivered at draft-PR time per contract; not waited on" }
      ],
      "tests": "Pin packages/types/src/__tests__/chat-message-avatar-keys-7295.test.ts (form of checkbox-wrapper-class-6938 / undeclared-but-consumed-keys-6150, adapted for a STRIP-mode mirror and a TS face with NO index signature): 25 tests — read-site text and message.KEY read set derived off index.tsx (both keys present, control avatarUrl absent), seam spread text, shape membership, value survives through ChatMessageSchema / ChatbotSchema / safeValidateSchema, refusal at the key and at messages.0.KEY through the entry point, invariant type-level pins (string, optional, not any, equal to ChatbotSchema userAvatarUrl / userAvatarFallback, string extends keyof ChatMessage is false, control key not in keyof, @ts-expect-error control literal), control key absent from shape and still admitted-and-stripped, and the three doc blocks' annotations with their five per-message avatarFallback overrides. Ablation (fix committed first at 63ece2c8; trap-guarded, absolute paths, under the verify lock): both faces checked out from 446d93d — mutation proven by blob hash (fd59df10 / b1cd5ac0 = BASE blobs) and marker counts 1/1 → 0/0; pin 12 failed | 13 passed (25): red = membership x2, survive x6, refusal x4; green = read-site, read-set, seam, control-key, doc legs; tsc -p tsconfig.test.json exit 2 with 13 diagnostics all inside the pin file (TS2339 / TS2344 / TS2353), 0 elsewhere; types dist rebuilt from the mutated source (marker 0) turned the seam probe red (exit 2). Restore git checkout HEAD on both paths: blobs e3bb6a4e / 08ab25c4 = HEAD blobs, markers 1/1, git diff HEAD empty, git status empty; dist rebuilt from the restored source (marker 1). Before-state measurements through the base dist: ChatMessageSchema / ChatbotSchema / safeValidateSchema all success with messages[0] keys id, role, content only (avatar dropped); avatar: 42 success; control avatarUrl: 42 success and stripped.",
      "mcp_calls": "7 — add_issue_comment (claim), create_pull_request, pull_request_read get (labels + body read-back), issue_write update (label union), pull_request_read get (label read-back), add_issue_comment (this report), issue_read get_comments (this report's read-back)",
      "deviations": [
        "REST reads are 403 from this seat too (measured on /repos/objectstack-ai/objectui/issues/7295: GitHub access is not enabled for this session); issue #7295 body and both comments, #4424 and #5155 were read through the public issue page's embedded JSON payload (front timeline hasNextPage false for #7295); PR #7294's diff read from git after deepening the shallow clone by 400 (commit 9bf0abfe); PR #7723's changeset read from a git scratch ref (refs/scratch/7295/6938, dropped afterwards) — it had already landed on main as b74a859.",
        "needs:contract-review attached via MCP issue_write update with the union of the auto-labeler's set (package: types, tests) — read back equal to the union, nothing stripped.",
        "pnpm check:doc-snippets NOT MEASURED locally (26-package build closure); narrowed tsc measurement of the three blocks declared above. CI Doc Snippet Type Check is the measurement.",
        "vitest packages/plugin-chatbot/ was run on 63ece2c8 (pre-merge); not re-run on 4b0f366c because the merge touches no plugin-chatbot input (form.ts / form.zod.ts / a types test / a changeset). types build, types suite and both type-checks WERE re-run on 4b0f366c under the lock (VERDICT command-exit 0).",
        "PR body footer: written in the harness-instructed form (Generated with Claude Code line + session URL); create_pull_request appended its own session-URL _Generated by_ block after it (read back verbatim, nothing eaten). No PATCH edit was made to the body.",
        "The pin reads content/docs/plugins/plugin-chatbot.mdx off disk from the types package (annotation + five-override count) — precedent in that directory reads docs the same way; it keeps triage's completion criterion pinned.",
        "The mdx Per-message Avatars block still imports ChatMessage from @object-ui/plugin-chatbot (PR #7294's choice when the types face lacked the keys); it is not one of the three blocks and was left as-is — handed back below rather than filed."
      ],
      "open_questions": [
        { "question": "Should the Per-message Avatars block on plugin-chatbot.mdx now import ChatMessage from @object-ui/types (the authoring face, which declares the keys after this PR) instead of @object-ui/plugin-chatbot (the runtime shape)?", "options": ["A — leave it (runtime import is correct too; not one of the three blocks the card names)", "B — one-line import swap as a rider on PR #7732 during contract review"], "recommendation": "A for this PR; B only if the reviewer wants the page to teach the authoring face uniformly — a one-line doc change, no contract movement." }
      ],
      "out_of_scope_findings": []
    }

    Worktree removed (node_modules deleted, git worktree remove exit 0); shared checkout untouched. Placeholders spelled as uppercase words because the body sanitizer eats angle-bracket shapes.


    Generated by Claude Code

  7. os-justin commented on Sep 5, 2026

    @os-justin
    Collaborator

    Landing paused — PR #7732 red on head 4b0f366c (reading 2026-09-05T12:58Z, session session_01BAZFhALsQsGqxui8sNqM8s): Lint completed: failure (job 101309392962) — @object-ui/types#lint exits 1 on exactly one error, in the PR's own pin: packages/types/src/__tests__/chat-message-avatar-keys-7295.test.ts:118:41 @typescript-eslint/no-empty-object-type (the {} type); the 268 warnings are pre-existing no-explicit-any. Doc Snippet Type Check is green on this head (the three re-annotated plugin-chatbot.mdx blocks compile — triage's completion criterion). A real failure in code the PR adds, not a flake; no re-run.

    The contract increment was read by ref against origin/main (complex.ts ChatMessage block +2 optional string members with read-site docblocks; complex.zod.ts ChatMessageSchema +2 z.string().optional(); changeset @object-ui/types: patch with the measured strip-mode behaviour change stated; docs = three ChatbotSchema annotations only; not governed on all 5 paths) — the in-seat verdict at tier is deferred to the patched head so the contract increment and the delta are judged once. needs:contract-review stays on the PR. Patch round 1 routed to the dev (SendMessage, 12:59Z): fix the type at :118, run the PACKAGE lint (0 errors), types suite + type-check, merge origin/main (a00db9ef), push, report. Review re-check armed.


    Generated by Claude Code

  8. os-justin commented on Sep 5, 2026

    @os-justin
    Collaborator

    os-dev-report

    {
      "issue": 7295,
      "patch_round": 1,
      "status": "done",
      "branch": "claude/issue-7295-chat-message-avatar-keys",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7732",
      "head": "fdd09f70",
      "premise_still_valid": true,
      "clause_2": "yes — unchanged; needs:contract-review left on the PR, the seat reviews the contract increment and this delta together on fdd09f70",
      "line_budget": "n/a",
      "summary": "Patch round 1 from the PM seat (2026-09-05T12:59Z): CI Lint was red on 4b0f366c with exactly one error — @typescript-eslint/no-empty-object-type at chat-message-avatar-keys-7295.test.ts:118. Fixed in one commit (fdd09f70): the IsOptional helper now spells the empty object as Record of string to never (an object with no keys is assignable to the pick only when the key is optional), with the docblock line above it (117) re-worded to match; the rule is not disabled and no other line moved. origin/main merged first (a00db9ef, PR #7729: one fixture, one test, one changeset — no overlap; clean merge 58f5f85b). Package lint back to 0 errors (268 pre-existing warnings untouched); the pin's type-level legs still compile; a scratch tsc probe confirms the helper still discriminates (true for avatar / avatarFallback, false for the required id / content). Pushed; gate table for the new head posted as PR comment 5551996152 (body not edited). Diff against origin/main is still exactly the five files.",
      "files_changed": [
        "packages/types/src/complex.ts",
        "packages/types/src/zod/complex.zod.ts",
        "packages/types/src/__tests__/chat-message-avatar-keys-7295.test.ts",
        "content/docs/plugins/plugin-chatbot.mdx",
        ".changeset/7295-chat-message-avatar-keys.md"
      ],
      "files_changed_this_round": [
        "packages/types/src/__tests__/chat-message-avatar-keys-7295.test.ts"
      ],
      "gates": [
        { "command": "pnpm --filter @object-ui/types lint", "exit": 0, "head": "fdd09f70 (content, run pre-commit)", "verdict": "268 problems (0 errors, 268 warnings) — the one error gone, warnings pre-existing and untouched" },
        { "command": "pnpm --filter @object-ui/types type-check", "exit": 0, "head": "fdd09f70 (content, run pre-commit)", "verdict": "tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json — 0 diagnostics" },
        { "command": "pnpm exec vitest run --maxWorkers=2 packages/types/ (verify lock, VERDICT command-exit 0)", "exit": 0, "head": "fdd09f70", "verdict": "Test Files 116 passed (116) · Tests 1991 passed (1991)" },
        { "command": "node scripts/check-control-bytes.mjs (after staging)", "exit": 0, "head": "fdd09f70 (content)", "verdict": "OK (scanned 6302 tracked text file(s); skipped 85 binary)" },
        { "command": "scratch tsc --strict probe of IsOptional over ChatMessage", "exit": 0, "head": "fdd09f70 (content)", "verdict": "true for avatar / avatarFallback, false for id / content — 0 diagnostics" },
        { "command": "CI on PR #7732", "exit": "in_progress", "head": "fdd09f70", "verdict": "not waited on, per contract" }
      ],
      "not_rerun_this_round": "types build, plugin-chatbot type-check and suite, changeset gates, doc gates, governed-surface test — inputs unchanged since the 4b0f366c table in the PR body (file list unchanged: 0 of 5 paths governed)",
      "tests": "Same pin, same 25 tests, same ablation evidence as the round-0 report (comment 5551953202); this round changed only the spelling of one type-level helper, verified by the package type-check plus the probe above.",
      "mcp_calls": "11 — 7 in round 0 (see comment 5551953202) + this round: add_issue_comment (PR gate table), add_issue_comment (this report), issue_read get_comments on the PR (gate-table read-back), issue_read get_comments on #7295 (this report's read-back)",
      "deviations": [
        "The fix touched lines 117-118 (the helper and the docblock sentence directly above it), not line 118 alone — the comment named the empty-object idiom and would otherwise have described the old spelling.",
        "Lint, type-check, control-bytes and the probe ran on the working tree immediately before the commit; the committed content is byte-identical (git status clean after commit). The suite ran on the committed head under the lock.",
        "No lint entry was in the round-0 gate table: the dispatch's gate list did not name the package lint and I did not derive it from the repo's Lint workflow — recorded here so the omission is visible; the round-0 table's other entries stand."
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Worktree removed again after this round (node_modules deleted, git worktree remove); shared checkout untouched. Placeholders spelled as words because the body sanitizer eats angle-bracket shapes.


    Generated by Claude Code

  9. os-justin commented on Sep 5, 2026

    @os-justin
    Collaborator

    Contract review at tier — PASS on PR #7732 head fdd09f70 (reading 2026-09-05T13:05Z)

    Implemented-by: dev subagent of session_01BAZFhALsQsGqxui8sNqM8s, mode:subagent, branch claude/issue-7295-chat-message-avatar-keys (claim 5551863889; reports 5551953202 and 5551998296)
    Reviewed-by: session_01BAZFhALsQsGqxui8sNqM8s (domain:spec execution seat); served tier read at 13:03Z via get_session: session_context.model = last_served_model = claude-fable-5-1 = CONTRACT_REVIEW_TIER; no fallback evidence in this round.

    Judged on the PR diff against main = a00db9ef (five files, 410/6) — the contract increment and the round-1 delta together, as the paused note (5551976750) said they would be.

    ① Derived judgments, per item

    • TS face, packages/types/src/complex.ts ChatMessage: avatar?: string, avatarFallback?: string — CORRECT. Both are read at packages/plugin-chatbot/src/index.tsx:173–178 in a || fold with the chatbot-level userAvatarUrl / userAvatarFallback (both string), and the runtime shape at ChatbotEnhanced.tsx:92–93 declares them string; optional is right (the fold falls back). Public surface: two additive optional members on an exported interface; no index signature added (triage boundary 2, objectui#5155) and pinned type-level (string extends keyof ChatMessage is false).
    • zod face, packages/types/src/zod/complex.zod.ts ChatMessageSchema: two z.string().optional() — CORRECT. Strip mode is preserved (control key avatarUrl still admitted-and-stripped, pinned). Accept-set movement as measured on 446d93d and re-derived from the diff: (a) an authored string at either key now SURVIVES ChatMessageSchema / ChatbotSchema / safeValidateSchema where it was dropped before — widening toward what the renderer already does; (b) a non-string at either key is now REFUSED (messages.0.avatar through the entry point) where it was admitted-and-stripped — enforcement of the declared type. Same verdict shape as objectui#6938 (wrapperClass) and the objectui#6150 batch.
    • packages/plugin-chatbot/** untouched; SeamChatMessage inherits through its ChatMessage half, shown by that package's type-check against the rebuilt dist and by the positive/ablated scratch probe (round-0 report). CORRECT — reading 2 was rejected by triage's own boundary 1, and nothing here re-opens it.
    • Docs: the three plugin-chatbot.mdx blocks PR docs(guides,plugins,tooling): clear three .mdx pages off the doc-snippet ledger (#5174 batch 5) #7294 left unannotated are annotated ChatbotSchema (import line, annotation, semicolon; no prose change). Doc Snippet Type Check is completed: success on fdd09f70 (job 101310586371, 13:04Z), so triage's completion criterion is measured, not asserted. No UNGATED_DOCS row existed on main for the page; scripts/ untouched. CORRECT.
    • Pin chat-message-avatar-keys-7295.test.ts: 25 tests; ablation of both faces reds membership x2, survive x6, refusal x4 and leaves the control legs green. Round-1 delta (IsOptional helper spelled Record<string, never>, docblock line above re-worded; nothing else moved) keeps the discriminator (probe: true for the two keys, false for id / content) and clears the one no-empty-object-type error; package lint 0 errors.
    • Describe strings citing the read-site line and the issue number: house style on main (26 such describes under packages/types/src/zod, form.zod.ts:264 for wrapperClass among them). Not a finding.

    ② Semver
    @object-ui/types: patch — consistent with the precedent already on main (.changeset/6938-checkbox-wrapper-class.md, .changeset/6150-undeclared-but-consumed-keys.md) for exactly this verdict shape. Wording note, non-blocking: the bolded sentence "no document that validated before stops validating" over-claims — a document with avatar: 42 validated before (admitted-and-stripped) and now refuses; the same paragraph discloses that refusal two bullets later, and the identical wording sits in the #6938 changeset on main. Left as is for consistency; recorded here for the central release-note writer, who reads the whole changeset.

    ③ Flags and open questions

    • Open question (Per-message Avatars block importing ChatMessage from @object-ui/plugin-chatbot): A — leave it. The runtime import is correct for that block, it is not one of the three blocks the card names, and a rider would be doc churn with no contract movement.
    • Round-0 deviation "no lint entry in the gate table": acknowledged; the Lint gate is in the round-1 table and CI Lint is the measurement on this head.
    • pnpm check:doc-snippets NOT MEASURED locally: CI Doc Snippet Type Check green on this head is the measurement.

    Verdict: PASS. Clearing needs:contract-review on PR #7732 in the same round with a provenance comment citing this PASS; this card never carried the label. Landing then follows the pre-landing three when every check run on fdd09f70 is completed: success (skipped allowed).


    Generated by Claude Code

  10. os-justin commented on Sep 5, 2026

    @os-justin
    Collaborator

    Landing note — PR #7732 enqueued (2026-09-05T13:20Z)

    Pre-landing three:

    1. In-seat contract-tier PASS on record: comment 5552020607 on this card, head fdd09f70, carrying the Implemented-by: / Reviewed-by: pair and the three-item judgment (served tier read at 13:03Z).
    2. Both carriers cleared: this card never carried needs:contract-review; the PR-side label was cleared with provenance comment 5552022295 and read back at 13:06Z (package: types, tests). node scripts/pm/check-clause2-carriers.mjs --pair 7732 exits 3 from this seat (REST session-gated, seat post §4) — the three limbs were verified by hand instead.
    3. Check runs on fdd09f70: 29 completed: success + 3 skipped, none failed, none pending (full farm read by name at 13:19Z; Lint green after patch round 1, Doc Snippet Type Check green — triage's completion criterion on the three plugin-chatbot.mdx blocks).

    Landing actions:

    • draft:false at 13:19Z.
    • Auto-merge enabled with mergeMethod: SQUASH explicitly at 13:20Z (tool echoes empty method/time — known reading, seat post §4).
    • Positive probe at 13:20Z: git ls-remote origin 'refs/heads/gh-readonly-queue/main/*' shows refs/heads/gh-readonly-queue/main/pr-7732-3bde19ba07276e5eb963a27bb13dbd05a1990c9d at 858cd72e, queued behind PR test(types): derive the zod-mirror-parity population instead of writing it down #7731 (objectui#7433); main = 9587fc95.

    Veto window: this card's reading 1 was a seat disposition under a declared veto window (claim 5551795779), not a maintainer ruling; the landing keeps that window open — a two-line re-declaration reverses it.

    Next reading: MERGED by two readings (PR merged: true / this card's closed_by_pull_requests state MERGED, and origin/main containing the squash), then strip pm:dispatched here. The stale finding label is triage's to drop.


    Generated by Claude Code

  11. os-justin commented on Sep 5, 2026

    @os-justin
    Collaborator

    MERGED — two readings (2026-09-05T15:25Z, session session_01BAZFhALsQsGqxui8sNqM8s): (1) PR #7732 reads merged: true, merged_at 13:35:51Z, and this card's closed_by_pull_requests names PR #7732 in state MERGED (card closed 13:35:52Z, state_reason: completed); (2) origin/main contains the squash 858cd72e ("fix(types): declare avatar and avatarFallback on ChatMessage, on both faces (objectui#7295) (#7732)"), main tip 83c77dc3 at 15:23Z. Landed head fdd09f70; reading 1 executed under the declared veto window (claim 5551795779) and PASSED at tier (5552020607) — the window stays open, a two-line re-declaration reverses it.

    pm:dispatched stripped in this round; finding, priority:p3, domain:spec stay (finding is triage's to drop). The reading was delayed from its 13:39Z timer by the account usage-window reset (15:20Z); no state moved in between.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanefindingpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions