Skip to content

feat(types,app-shell): carry the tool approval envelope through hydration - #9229

Merged
os-tesla merged 3 commits into
mainfrom
claude/issue-8442-hydrated-approval-envelope
Sep 12, 2026
Merged

os-tesla merged 3 commits into
mainfrom
claude/issue-8442-hydrated-approval-envelope

Conversation

@os-tesla

@os-tesla os-tesla commented Sep 12, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #8442 — the ADDITIVE half of the objectui#8426 chain, under the director-seat ruling of decision batch #86 (option A, contract-first). ⛔ Deliberately not a closing keyword: the chain's sequencing is not settled here.

What this carries

  1. ChatToolInvocation in @object-ui/types gains an optional approval envelope — { id, approved?, reason?, isAutomatic?, signature? } — with its hand-written Zod mirror, and ChatbotEnhanced.ChatToolInvocation mirrors it.
  2. hydratedMessagesToChatMessages in packages/app-shell/src/console/ai/AiChatPage.tsx stops dropping approval and pendingActionId.

Three of the ten declared state values — approval-requested, approval-responded, output-denied — are states the AI SDK's own tool-part union cannot express without that envelope. The contract declared the states and not the envelope; the hydration mapper carried the states through and dropped the data that makes them actionable.

⭐ What was MEASURED, not inherited from the card body

The card cited three line addresses from an older commit. All three were re-located by symbol on this branch's base (b775500af) and both behavioural claims execute:

Card's claim Re-located Verdict
mapMessages.extractToolInvocations lifts pendingActionId (cited :687) mapMessages.ts:694 on the base ✅ true — pendingActionId: pending?.pendingActionId in the returned invocation
useHitlInChat indexes toolCallId -> pendingActionId and skips invocations without one (cited :154-166) useHitlInChat.ts:157-166 on the base ✅ true — the idMap memo guards on if (tool.pendingActionId && tool.toolCallId), and decide() answers an unindexed call with 'No pending-action id found for this tool call.'
the pinned output-denied pass-through (cited :44 / :49) same file, :45 / :50 ✅ true — still pinned, still green, untouched here

Two further readings that change how the halves had to be lifted, and that the card did not have:

  • pendingActionId is not a part key and never was. In rehydrated history it exists only INSIDE the tool result envelope. "Stop dropping it" therefore cannot be a pass-through: it has to be derived, and the only honest derivation is the one the live path already uses. Hence detectPendingApproval is exported (it was the one detector of nine that was not) rather than a second envelope reader being written next to it — AGENTS.md Commandment #0.1.
  • approval IS a part key, so it is lifted from the part — and narrowed to the declared shape rather than cast, because HydratedUIMessagePart is an open record and whatever the server wrote is reachable and unverified. An approval with no usable id cannot be replied on, so it is refused rather than half-carried.

⚠️ The honest limit of consequence 2, which the card reported at reading confidence

The card said the operator-facing outcome was NOT verified end to end, and asked for that not to become an assumption. Measured here, at the source:

toUIMessages' merge step rewrites a part's state to output-available whenever a tool result is merged onto it. So on the ModelMessage sub-path (assistant call row + separate tool-result row) the state never reaches this mapper as approval-requested at all. useHitlInChat keys purely on pendingActionId, so this change does restore the index entry and the envelope on every sub-path; but the awaiting-approval CARD is gated on state === 'approval-requested', so on that one sub-path it still does not render. That reading is pinned as a reading in the new test, so the card that changes it turns the line red rather than finding a stale sentence. ⛔ It is out of this PR's scope — it lives in the hydration pipeline, not in this mapper — and is reported to the PM rather than repaired here.

⛔ What was deliberately NOT pulled in

Every narrowing in the ruling belongs to objectui#8426 and none of it is here: the authoring state union shedding the three runtime-only approval states; the UseObjectChatOptions.initialMessages narrowing that ships minor + **BREAKING**; the as any deletion at the useChat call; the parts-builder discriminated arms; the dead toolName excess property.

The PM's scope split HELD. The additive half compiles, tests, type-checks and becomes observable with no narrowing pulled in — build 29/29 successful, type-check 32/32 successful. A pin states that the envelope is optional, precisely so a later tidy-up cannot ship objectui#8426's break under this change's name.

Bump call — patch, and the reason is sequencing, not diff size

The lane's test (does existing stored data render differently) answers no: the member is optional, every value that parsed before still parses, and nothing changes for data carrying no envelope. The counter-reading is real and is named in the changeset rather than hidden: two published capabilities DO land (the type member, and the detectPendingApproval export), and this repo's own recent precedent bumped minor for "a capability a consumer can newly rely on". It still loses — the ruling reserves the minor + **BREAKING** carrier for the NARROWING half, and spending it here would blur the signal the chain sequences on. ⭐ Flagged for the reviewer to overrule if that reading is wrong.

Verification

Gate Verdict line Exit
build (turbo run build --filter=@object-ui/app-shell...) Tasks: 29 successful, 29 total 0
type-check (types + plugin-chatbot + app-shell) Tasks: 32 successful, 32 total 0
vitest (6 targeted files incl. zod-mirror-parity) Test Files 6 passed (6) · Tests 123 passed (123) 0
check-changeset-presence.mjs ✅ 8 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s) 0
check-control-bytes.mjs ✅ check-control-bytes: OK (scanned 7408 tracked text file(s); skipped 85 binary) 0
check-new-cross-file-line-citations.mjs VERDICT new-cross-file-line-citations: 0 new citation(s) 0
check-governed-queue-guard.mjs --test ✅ NOT GOVERNED — 10 path(s) checked against 5 governed surface(s); none matched. 0
check-changeset-fixed / -no-major / -claims / -overwrite all ✅ 0
markdown-test-inputs.mjs --audit 47 candidate test files, all adjudicated; 41 declared entries, all present 0
eslint (narrowed — see below) 9 files, 0 errors, 91 warnings 0
vitest, full suites of the 3 affected packages types+plugin-chatbot 222 passed (222) / 3969 passed · app-shell 685 passed (685) / 6634 passed / 1 skipped 0
type-check @object-ui/console (the one consumer that calls the changed mapper) 0 errors 0

Every exit code captured by redirect-then-capture, never through a pipe. All numbers above are from the final commit 58e734cce.

⚠️ One reading corrected rather than reported: the first @object-ui/console type-check exited 2 with TS2307 / TS2882 across five plugins. That is a MISSING PREREQUISITE, not a red gate — those packages had no dist because the earlier build only covered app-shell's closure. Re-run after turbo run build --filter=@object-ui/console^... (Tasks: 34 successful), it reads 0 errors, exit 0. The first result is not recorded as a failed measurement.

Ablation — four legs, each with an on-disk mutation proof and a green control

⛔ No permanent test artefact: every leg mutates, measures, restores, and proves the restore by comparing git hash-object against the HEAD blob (not by reading an exit code), with a trap ... EXIT INT TERM on absolute paths. git diff HEAD was empty after every leg.

Leg Mutation (proven on disk) Read by Predicted Measured
A delete the approval arm from the Zod mirror (marker grep 1 -> 0, 0 14 numstat) vitest red red, exit 1 — Tests 3 failed / 35 passed; the three that fail are the retention, minimal-envelope and refusal pins
A2 the SAME mutation tsc red red, exit 2 — and it names zod-mirror-parity.test.ts: Type '"complex.zod.ts#ChatToolInvocationSchema"' is not assignable to type 'never'
B delete the two lifted keys from the hydration mapper (marker 1 -> 0, 0 2 numstat) vitest red red, exit 1 — Tests 5 failed / 8 passed; the 8 survivors are the pre-existing pins, untouched
C delete approval from ChatbotEnhanced.ChatToolInvocation (marker 1 -> 0, 0 18 numstat) tsc red red, exit 2 — TS2344: Type 'false' does not satisfy the constraint 'true' at the two-sided Equal pin
control none (restored tree) tsc green green — types exit 0, plugin-chatbot exit 0

⭐ A vs A2 is the load-bearing pair, and it corrects an assumption worth writing down. Under leg A the derived zod-mirror-parity.test.ts passed. Its pins are compile-time assertions and vitest erases them — its own header says so — so pnpm test proves nothing about mirror drift on this pair and type-check is the gate of record. Had only leg A been run, "the parity test covers it" would have been recorded as measured when it was NOT MEASURED. A2 is what makes the claim true.

Cards filed, not folded in

Two findings in this area, each measured, neither in this card's scope and neither repaired here:

  • objectui#9232 — sanitizeChatMessagesForCache (the localStorage cache WRITE side) rebuilds tool parts without either key, so once this lands the server path and the cache-fallback path disagree. A fix invents a serializer and owes a round-trip test, so it is not a bounded in-place repair.
  • objectui#9233 — toUIMessages' merge step rewrites state to output-available for every merged result, which is the other half of this card's consequence 2. Both were deduped through one targeted search_issues call (REST /search/* is refused for this session), with objectui#8442 itself returned as the positive control.

The eslint run is a narrowed one, and here is why the narrowing excluded nothing. (1) The repository-wide run is turbo run lint, per package. (2) The narrowed run linted exactly the 9 changed source files, counted from --format json output, not estimated. (3) eslint.config.js enables no type-aware linting — no projectService, no parserOptions.project, no tsconfigRootDir, no recommendedTypeChecked — so no rule in this configuration can read across files, and this diff cannot move the verdict on any file it does not touch. All 91 warnings are pre-existing classes; the 4 that land on added lines are react-refresh/only-export-components, which already fires on all 39 named-export sites in that barrel, including the eight sibling detectors exported beside the new one. Zero errors anywhere.

Review notes

  • ⛔ Not enqueued, auto-merge ⛔ not armed — the PM lands this.
  • needs:contract-review is on both carriers. Clause-② was declared yes at dispatch on the mechanical boundary test (a published authoring type gains a member); the default-tier review is the review of record per the maintainer ruling quoted on the card.
  • packages/spec is untouched. packages/components/src/ui/** is untouched.
  • Authored by Claude Code, seat session https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ. ⚠️ Written as prose on purpose: a PATCH to a pull-request body downgrades a session-URL attribution footer to the bare form and appends a second footer unconditionally (this body was measured gaining exactly 58 bytes on its first edit), so the session reference only survives in prose. ⛔ The footer below is the platform's own and is deliberately not re-posted by hand.

Generated by Claude Code

…tion

Three of `ChatToolInvocation`'s ten declared `state` values —
`approval-requested`, `approval-responded`, `output-denied` — are states the AI
SDK's own tool-part union cannot express without an
`{ id, approved?, reason?, isAutomatic?, signature? }` envelope. The contract
declared the states and not the envelope, and the Console's hydration mapper
built each invocation from six fields with neither the envelope nor
`pendingActionId` among them. A rehydrated pending approval therefore arrived
carrying a state that says "a human must decide" and nothing a decision could be
made with: `useHitlInChat` keys its index on `pendingActionId` and skips any
invocation without one.

Contract first: `ChatToolInvocation` gains the optional `approval` envelope with
its Zod mirror, and the runtime `ChatbotEnhanced.ChatToolInvocation` mirrors it.
A compile-time pin holds the two declarations to the SAME type in both
directions, because the member crosses the render adapter as an untouched spread
where a divergence would be invisible.

`hydratedMessagesToChatMessages` then lifts both halves, which arrive from
different places. The SDK envelope is persisted ON THE PART and is narrowed to
its declared shape rather than cast — an `approval` with no usable `id` is
refused, not passed through. `pendingActionId` is never a part key; in
rehydrated history it exists only inside the tool RESULT, so it is derived with
`detectPendingApproval`, the same parse the live mapper uses, now exported so
one envelope has one reader instead of two dialects.

Nothing is narrowed. The envelope stays optional, and a pin says so, so that the
`state`-union narrowing this sequences in front of cannot arrive early under
this change's name.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3112.7 KB 3134.8 KB
Main entry chunk (gzip) 144.3 KB 350 KB
Entry file index-zUR496PG.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.84KB 114.81KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 209.38KB 57.92KB
fields (index.js) 247.41KB 62.43KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 131.27KB 34.62KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 253.02KB 65.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.94KB 41.04KB
plugin-grid (index.js) 211.56KB 57.50KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.54KB 3.31KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…po's written precedent

PM ruling on the bump the seat flagged rather than assumed. The lane's runtime
test — does existing stored data render differently — answers no, but that test
is about stored data, and what lands here is published SURFACE: two capabilities
a consumer can newly rely on, `ChatToolInvocation.approval` and the
`detectPendingApproval` export. `.changeset/8214-chatbot-anypart-state-widen.md`
settles that case in this repo in those words.

The sequencing argument for `patch` — don't spend objectui#8426's `minor` +
`**BREAKING**` carrier early — does not hold, for two reasons measured here.
The level was never the signal: this repo ships a breaking change AS `minor`, so
what marks objectui#8426's half is the `**BREAKING**` carrier, untouched by this
declaration. And `.changeset/config.json` puts all 41 packages in ONE `fixed`
group, so the released level is the maximum across every pending changeset
regardless of this file. `patch` therefore bought no smaller release and no
preserved signal — only a changelog line that under-describes what shipped.

Prose and frontmatter only; no source file is touched.

Ref: objectui#8442

Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ

Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

PM ruling on both open questions — and my scope split was incomplete, which is the first thing to say

domain:ui PM seat (os-tesla), R16, 2026-09-12T02:1xZ. Head now c09d16a5cb (origin/main merged in, one changeset commit added by me).

⭐ The split held where it was tested, and the seat tested it honestly in both directions — the additive half compiles, tests and becomes observable with no narrowing pulled in, and the report says so with the ablation to back it. But the seat also found the place where the split was simply silent, and that is mine.


Q1 — the additive extractToolInvocations lift → A: it lands on objectui#8426

⛔ First, the correction. I wrote the split as "in scope: the additive half; out of scope: every narrowing in the ruling". The ruling's sequence contains one step that is neither — lifted in mapMessages.extractToolInvocations is additive and was in neither list. A split built as "additive vs narrowing" cannot misfile an additive step unless the split forgot it, and it did. That is a defect in my dispatch, not in the reading.

Ruled A, and on the seat's own ground rather than on tidiness: measured, extractToolInvocations returns a fixed object literal with no approval member at all, so the live path still drops the envelope the hydrated path now carries — and the envelope has no reader until objectui#8426 builds the discriminated UIMessagePart arms. Lifting it now would mint a declared-but-unread key on the exact surface ADR-0049 governs, which is the defect class this repo keeps carding (objectui#6625's decimals, objectui#6597's referenceTo, objectui#7166's three copies). ⇒ the lift belongs where its read site is born.

⛔ Not C, for the reason the seat gave: it would widen this card after the split was validated, which is precisely the cost the stop condition was written to avoid.

⚠️ The asymmetry this leaves is real and must not be silent: between this landing and objectui#8426's, the hydrated path carries approval and the live path does not. Recorded on objectui#8426 so it is that card's to close, not a surprise for whoever takes it.


Q2 — bump → B: minor, overriding the patch this shipped at. Applied by me in c09d16a5cb.

The seat flagged this rather than assuming it, and pointed at the right precedent. .changeset/8214-chatbot-anypart-state-widen.md, read at source, settles it in this repo in these words:

minor rather than patch because a published signature accepts input it refused before, which is a capability a consumer can newly rely on.

Two such capabilities land here: ChatToolInvocation.approval, and the detectPendingApproval export. The lane's runtime test (does existing stored data render differently) answers no — but that test is about stored data, and this is published surface. Different question, different instrument.

The sequencing argument for patch — don't spend objectui#8426's minor + **BREAKING** carrier early — ⛔ does not hold, on two things I measured:

  1. The level was never the signal. This repo ships a breaking change as minor. What distinguishes objectui#8426's half is the **BREAKING** carrier, not the number beside the package, and that carrier is untouched by this declaration. There is no signal here to blur.
  2. .changeset/config.json puts all 41 packages in ONE fixed group. So the released level is the maximum across every pending changeset regardless of what this file says. patch bought no smaller release — only a changelog line that under-describes what shipped.

⇒ patch cost accuracy and bought nothing. ⛔ Not C, and the seat's reason is exactly right and worth keeping: one fixed group means a split changeset cannot produce split levels, so C would only misreport intent.


⭐ The finding I am carrying out of this report as a lane rule

Under LEG A the derived zod-mirror-parity.test.ts PASSED. Its pins are compile-time assertions and vitest erases them, so pnpm test proves NOTHING about mirror drift on this pair — type-check is the gate of record. Had I stopped at LEG A I would have recorded "the parity test covers it" as measured when it was NOT MEASURED.

That is the sharpest thing in this delivery. A green suite is not evidence that a type-level pin is holding, and an ablation that only runs the test runner will certify a pin that was erased before it ran. ⇒ an ablation must be read through the gate that actually enforces the claim, and for a compile-time pin that is tsc, never vitest. The seat found this by running the same mutation through two readers and getting opposite answers — which is rule 2 done properly: one ablation excludes one hypothesis, and "the test covers it" was a second hypothesis that needed its own leg.

The implementation fact the split did not anticipate — accepted

pendingActionId is not a part key and never was; in rehydrated history it lives only inside the tool RESULT envelope. So "stop dropping it" could not be a pass-through and had to be derived. Exporting detectPendingApproval from @object-ui/plugin-chatbot — the only one of nine sibling detectors not already exported, beside six that app-shell already imports — is the right resolution, and hand-rolling a second envelope reader in app-shell would have minted a second dialect that AGENTS.md Commandment #0.1 refuses. ⇒ this is why Q2 answers minor: that export is the second new capability.

Contract review follows on the current head.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3112.8 KB 3134.8 KB
Main entry chunk (gzip) 144.3 KB 350 KB
Entry file index-DIRiK3mN.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.84KB 114.81KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 209.38KB 57.92KB
fields (index.js) 247.41KB 62.43KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 131.27KB 34.62KB
plugin-designer (index.js) 215.81KB 44.32KB
plugin-detail (index.js) 253.38KB 65.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.94KB 41.04KB
plugin-grid (index.js) 211.67KB 57.51KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.54KB 3.31KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

Contract review — head c09d16a5cb — PASS

Reviewed-by: domain:ui PM seat (os-tesla), R16, 2026-09-12T02:2xZ. Rulings on the two open questions are in the comment above; this is the review of record for the gate.

Clause-② yes — declared at dispatch on the mechanical boundary test. ⛔ Not revised on delivery.

Scope, against the merge base — ⛔ not against the seat's last head

The head carries origin/main merged in plus one changeset commit of mine, so the seat's 58e734cce is no longer a baseline. Measured against git merge-base origin/main c09d16a5cb.

The split held, and it was tested in both directions

I dispatched this as the additive half only, with every narrowing in objectui#8426's ruling explicitly out of scope and a stop condition on the split itself. Delivered: ChatToolInvocation.approval (optional, Zod mirror), ChatbotEnhanced.ChatToolInvocation mirroring it under a two-sided compile-time Equal pin, and hydratedMessagesToChatMessages lifting approval and pendingActionId. ⛔ No state union change, no initialMessages change, no as any deletion, no parts-builder arms, no toolName removal.

⭐ The one thing the split did not anticipate is the most interesting fact in the delivery: pendingActionId is not a part key and never was — in rehydrated history it exists only inside the tool RESULT envelope. So "stop dropping it" could not be a pass-through and had to be derived. Exporting detectPendingApproval from @object-ui/plugin-chatbot — the only one of nine sibling detectors not already exported, beside six app-shell already imports — is the right resolution; hand-rolling a second envelope reader in app-shell would have minted a second dialect that AGENTS.md Commandment #0.1 refuses. The approval key, which is a part key, is narrowed to its declared shape rather than cast, and an envelope with no usable id is refused.

⭐⭐ The ablation finding I am promoting to a lane rule

Under LEG A the derived zod-mirror-parity.test.ts PASSED. Its pins are compile-time assertions and vitest erases them, so pnpm test proves NOTHING about mirror drift on this pair — type-check is the gate of record. Had I stopped at LEG A I would have recorded "the parity test covers it" as measured when it was NOT MEASURED.

⇒ An ablation must be read through the gate that actually enforces the claim. For a compile-time pin that is tsc, never the test runner, which erases such pins before they run. The seat found this by pushing the same mutation through two readers and getting opposite answers — which is rule 2 done properly: one ablation excludes one hypothesis, and "the test covers it" was a second hypothesis owed its own leg. LEG A2 then named the file and the exact TS error.

Four legs total, each mutate → prove-on-disk (marker count 1→0 plus a non-zero numstat, never an editor exit code) → measure → restore → prove-restore by blob hash against HEAD, under trap … EXIT INT TERM on absolute paths. LEG B (the two lifted keys deleted) reddens 5 and leaves the 8 pre-existing pins green. LEG C reddens the two-sided Equal pin with TS2344. Controls green on the restored tree; git diff HEAD empty after every leg.

The correction inside the report, which is the right kind

The consumer type-check first exited 2 with a TS2307/TS2882 storm across five plugins with no dist — and the seat recorded that as PREREQUISITE NOT MET, not a red gate, rebuilt the closure, re-ran to 0 errors, and ⛔ did not record the first result as a measurement. That is rule 11 in its other direction: an unlit probe is a broken probe, not a finding.

Bump — minor, applied by me in c09d16a5cb

Reasoned in the ruling comment above and rewritten in the changeset body. Short form: the lane's runtime test is about stored data and answers no; what lands here is published surface, and .changeset/8214-chatbot-anypart-state-widen.md settles that case in this repo in those words. The patch reading rested on preserving objectui#8426's minor + **BREAKING** signal — but this repo ships breaking as minor, so the carrier is the signal, not the level; and all 41 packages sit in one fixed group, so the released level is the max across pending changesets regardless. patch bought nothing and cost accuracy.

Findings filed — both accepted, both correctly not folded in

  • objectui#9232 — sanitizeChatMessagesForCache rebuilds tool parts without approval / pendingActionId, so once this lands the server path and the cache-fallback path disagree about the same conversation. ⛔ Not folded in: the fix invents a serializer and owes its own round-trip test, so it fails the bounded-in-place test.
  • ⭐ objectui#9233 — mergeToolResultsInto rewrites state to output-available for every merged result, so a rehydrated approval-requested never reaches this mapper from the ModelMessage sub-path. Measured consequence: after this PR the invocation is indexed by useHitlInChat (which keys purely on pendingActionId), but the awaiting-approval card is gated on state === 'approval-requested' — so on that sub-path the operator gets no approval card at all, not a dead one. Today's reading is pinned in AiChatPage.hydration.test.ts so the card that changes it turns that line red instead of leaving a stale sentence. ⇒ recorded on objectui#8426, since it may change what "done" means there.
  • The card's three cited line addresses were all stale and were re-located by symbol; both behavioural claims still execute. ⇒ drift in the citations, not in the claims — and the diff cites by symbol throughout, which is why the line-citation gate reports 0 new citations.

Clearing needs:contract-review on both carriers in one stroke, then ready → SQUASH. ⛔ Card objectui#8442 is not closed by this PR (Part of, not Fixes) — the chain's remaining steps live on objectui#8426.


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 12, 2026 02:33
@os-tesla
os-tesla added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit 3a43a15 Sep 12, 2026
38 checks passed
@os-tesla
os-tesla deleted the claude/issue-8442-hydrated-approval-envelope branch September 12, 2026 02:51
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…404 (objectui#10755) (objectstack-ai#10766)

Fixes objectstack-ai#10755
Clause-②: no

Dispatched implementation of the `domain:ui` seat 2 claim (comment
`5853230223`) on objectui#10755, session
`https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN`. Citations
only, on three published docs pages: no sentence's claim moves, and the
frontmatter of all three files is byte-identical.

## What changed

- The ten objectui issue links that answer 404 now cite the landing
commit on `main`, as a 9-character backticked sha. That is the form
`content/docs/guide/ci-cd-pipeline.md` already uses for commits (five of
its six existing sha citations are 9 characters), and the form PR
objectui#10707 chose for the same class on objectui#10701.
- Two prose-only mentions of the same dead numbers, inside the same two
sentences, are re-pointed so each sentence stays coherent: "in objectstack-ai#7638's
card body" reads "in the second card's body", and "over the file objectstack-ai#8057
is entirely about" reads "over the file that third writer lives in".
Lychee does not read those; the reader does. Each sentence's claim is
unchanged.
- Commits, not PRs, at every site. Two of the landing PRs answer 404
themselves (PR 7637 for objectui#7627, PR 7805 for objectui#7753), so PR
numbers are not uniformly alive; commits on `main` are. The same ten
numbers answer 200 in objectstack, each an unrelated subject, so a
repo-qualified re-cite would send a reader to the wrong record.
- No lychee config or workflow change, and no changeset: `content/**` is
not a released package (`check-changeset-presence` below).

## H1 reproduction, at the base `704e05b09d` (origin/main at claim time)

- Lychee is not preinstalled in this container; the 0.18.1 release
binary was fetched into the scratchpad. `lychee --verbose --no-progress
--config lychee.toml` over the three files: `🔍 195 Total (in 15s) ✅ 182
OK 🚫 13 Errors`, exit 2. Ten `[404]` lines, one per objectui issue URL,
exactly the card's ten, on the same lines the card names (re-read at the
base). The other three errors are this container's egress proxy, not
link rot: `github.com/changesets/changesets` and
`github.com/lycheeverse/lychee` answer 403 ("GitHub access to this
repository is not enabled for this session") and `git-cliff.org` is a
CONNECT 403. The CI run `36294593837` had 10 errors and none of these;
they recur identically at the head.
- REST `GET /repos/objectstack-ai/objectui/issues/N`: all ten answer
404, on the API and on the HTML page. Lit controls: objectui#7013,
objectstack-ai#7015, objectstack-ai#9229 and objectstack-ai#7790 answer 200. Neighbours objectui#5792, objectstack-ai#5794,
objectstack-ai#7792 and objectstack-ai#9231 also answer 404 and are cited nowhere in the swept
trees.
- objectstack `GET /repos/objectstack-ai/objectstack/issues/N`: all ten
answer 200 and every subject is unrelated (a spec docs generator PR, a
bulkCreate card, a QA run, an email-template PR, and so on).

## H2 and H3, site by site

Each row: the quoted anchor in the sentence, the dead citation, the
replacement, and the evidence (the dead number's REST status is 404 in
every row; the landing commit's subject is quoted without its
conventional-commit prefix).

| file | sentence anchor | old | new | evidence |
|:--|:--|:--|:--|:--|
| `ci-cd-pipeline.md` | "three times so far, each found by a human and
never by a gate (…, the third in the same package as the first)" |
objectui#6943 | "and a third in the same package as the first, all three
recorded in `6aeba673c`, the commit that landed this gate" | `6aeba673c`
"require the directory form of the tooling exclude in every published
build tsconfig" (PR 7409 answers 200). Its message records
objectui#4006, objectstack-ai#4836 and objectstack-ai#6943 as the three incidents. objectstack-ai#4006 and objectstack-ai#4836
stay linked; both answer 200. |
| `ci-cd-pipeline.md` | "measured at two cards and three copied call
sites (…), both closed by pointing the prose at
`resolveRecordSourceObjectName`" | objectui#7627, objectui#7638 |
"measured at two cards and three copied call sites, both cards closed by
pointing the prose at `resolveRecordSourceObjectName` (`b041b9c0c`,
`2ce2612df`)" | `b041b9c0c` "one shared record-source object-name
reader, six plugins delegate" (PR 7637 answers 404); its message settles
objectui#7627 by name. `2ce2612df` "the record-page URL follows the
record source, not the top-level key" (PR 7648 answers 200);
`abdcd189c`'s message names PR 7648 as the change that removed the
spelling objectui#7638 was filed about, and its diff points the hook's
`@example` at `resolveRecordSourceObjectName` (7 occurrences). |
| `ci-cd-pipeline.md` | "in objectstack-ai#7638's card body and then in the dispatch
that repeated it" (prose, not a link) | objectui#7638 | "in the second
card's body and then in the dispatch that repeated it" | The two cards
are named in the preceding clause; the claim is unchanged. |
| `ci-cd-pipeline.md` | "a count ratchet would have been green too,
which is why … rejected that option on the instance itself" |
objectui#7753 | "which is why `72498f257`, the commit that landed this
gate, rejected that option on the instance itself" | `72498f257` "a
census derives every handler key a registered renderer reads and
requires its arm to declare it (objectui#7753)" (PR 7805 answers 404).
Its message records the substitution that held `RUNTIME_SLOT` at 44 and
`ALL_SITES` at 66, the count reading the sentence describes. |
| `ci-cd-pipeline.md` | "… then reproduced the identical defect on a
THIRD writer neither guard covered, in that card's own required dogfood"
| objectui#8057 | "The identical defect was then reproduced on a THIRD
writer neither guard covered, in that card's own required dogfood
(recorded in `9662aca56`, the commit that landed this gate)" |
`9662aca56` "apply the object-metadata write invariant at the write
doors, not at an enumeration of writers" (PR 9238 answers 200). Its
message: "objectui#8057 then reproduced the identical defect on a third
writer in that card's own required dogfood, and a sweep found nine
more". "That card" is objectui#7714, linked in the preceding sentence,
which answers 200. |
| `ci-cd-pipeline.md` | "returns ZERO over the file objectstack-ai#8057 is entirely
about" (prose, not a link) | objectui#8057 | "returns ZERO over the file
that third writer lives in" | The third writer is referenced two
sentences earlier; the claim is unchanged. |
| `ci-cd-pipeline.md` | "widened from `content/docs/**` alone by …" |
objectui#7878 | "widened from `content/docs/**` alone in `59a3a233d`" |
`59a3a233d` "widen the expression-carriage census to check:doc-types'
surface (objectui#7878)" (PR 8105 answers 200). |
| `ci-cd-pipeline.md` | "cwd the only variable (…). Root `AGENTS.md` had
taught" | objectui#7791 | "cwd the only variable (`223b1e4a3`). Root
`AGENTS.md` had taught" | `223b1e4a3` "root the capability spec-parity
pin at the file, not the cwd (objectui#7791)" (PR 7796 answers 200). Its
message carries the two-cwd measurement. |
| `ci-cd-pipeline.md` | "The defect it closes (…):" | objectui#5793 |
"The defect it closes, repaired in the gate's own landing commit
`111741454`:" | `111741454` "gate @objectstack/spec range floors against
published symbols" (PR 6076 answers 200). Its message describes the
`plugin-detail` `ReferenceRailEntry` defect in the sentence's own words,
and its diff bumps `packages/plugin-detail/package.json`'s
`@objectstack/spec` floor from `^17.0.0` to `^17.1.0`, so the same
commit landed the gate and repaired the defect. |
| `lookup.mdx` | "and must never reach authored object metadata (…)" |
objectui#7014 | "and must never reach authored object metadata
(`0e3b3be09`)" | `0e3b3be09` "correct three false spec-alignment claims
and pin the real boundary" (PR 7510 answers 200). `e1545cfe6` (PR 7994),
the commit that wrote this sentence, says it carries PR 7510's
attribution. The claim itself is stale; see Acceptance note 1 for why
the citation is kept resting on the landing commit rather than dropped.
|
| `select.mdx` | "on both the editable and the read-only path (…)" |
objectui#9230 | "on both the editable and the read-only path
(`20b507aff`)" | `20b507aff` "render a picklist option value when its
label is blank (objectui#9230)" (PR 9258 answers 200). |

All ten shas are unique at 9 characters (`git rev-parse --short=9`
returns 9 for each) and each is an ancestor of `origin/main` by
construction (they were taken from `git log origin/main`).

## H4 the whole tree, not only the ten

- `git grep` at `origin/main` over `content/**`, `docs/**`, `README.md`
and `skills/**` for
`github.com/objectstack-ai/objectui/(issues|pull)/N`: 191 distinct
numbers across 14 files. REST on every one: 181 answer 200, 10 answer
404, and the ten are the card's ten. Nothing outside the three files.
- Lychee, same config plus `--exclude '.*' --include` for objectui issue
and PR URLs (offline-safe: it reaches github.com only), over
`content/docs/**/*.md`, `content/docs/**/*.mdx`, `docs/**/*.md`,
`docs/**/*.mdx`, `README.md` and `skills/**/*.md` at the head
`08be3d7708`: `🔍 530 Total (in 14s) ✅ 188 OK 🚫 0 Errors 👻 342 Excluded`,
exit 0.
- Bare-number mentions of the same ten (`objectui#N` in prose, which
lychee cannot see) elsewhere in the tree at `origin/main`: 204 sites
outside the three files: 38 in `packages/*/src` non-test text (JSDoc and
comments, some of which ship in `dist` per PR objectui#10707's `dist`
reading), 20 in pending `.changeset/*.md` files (which publish verbatim
into the CHANGELOG at the next release), 81 in test files, 28 in
`scripts/`, 6 under `.github/`, 1 in governed agent text, 30 elsewhere.
Not widened into this PR; listed under Acceptance notes for a card.

## Gates, all on the head `08be3d7708`

- Lychee over the three files, repo config: `🔍 185 Total (in 17s) ✅ 182
OK 🚫 3 Errors`, exit 2 — the 3 are the container proxy artefacts named
under H1 and zero are objectui links (the base run had 13 errors, 10 of
them these links). Objectui-scoped (`--exclude '.*' --include` objectui
issue/PR URLs): `🔍 185 Total (in 13s) ✅ 173 OK 🚫 0 Errors 👻 12
Excluded`, exit 0.
- `pnpm docs:check-links` (`check-doc-links`): `Links are valid across
17 scan roots.`, exit 0.
- `pnpm check:control-bytes`: `check-control-bytes: OK (scanned 8943
tracked text file(s); skipped 85 binary).`, exit 0.
- `pnpm check:new-line-citations`: `VERDICT
new-cross-file-line-citations: 0 new citation(s), enforcement
report-only -> exit 0`.
- `node scripts/check-changeset-presence.mjs`: `3 file(s) changed, 0 of
them published source of a package the release covers … No source or
published contract of a released package changed in this range, so no
changeset is owed.`, exit 0.
- `pnpm check:doc-fences` exit 0; `pnpm check:doc-example-ids` exit 0
(`414 real reference(s) all resolve in the catalog registry`).
- The test readers `node scripts/markdown-test-inputs.mjs --list` names
for the classes `content/docs/**` and
`content/docs/guide/ci-cd-pipeline.md` (26 files, including
`ci-cd-pipeline-doc.test.ts` and `check-links-workflow.test.ts`), run as
one `pnpm exec vitest run` from the repo root: `Test Files 26 passed
(26)`, `Tests 1195 passed (1195)`.
- Frontmatter lines 1 to 4, md5 before and after, identical:
`ci-cd-pipeline.md` `6ca230b6b64dac22a2031d21ff311b13`, `lookup.mdx`
`289a5c887fd231f6b0b0824f53769fad`, `select.mdx`
`bae97ed7e436e0a06b8a154242ec15f1`.
- NOT MEASURED: the `Check Links` workflow itself, which runs on
`schedule` and `workflow_dispatch` only. Per the triage, re-run it via
`workflow_dispatch` once this lands to show the weekly sweep green.

## Serial

- At claim time (06:07Z) no open PR touched the three files. Before the
final push, `origin/main` was fetched into the private ref
`refs/issue-10755/main` = `f308a655b8`, two commits past the base
(`f308a655b8`, `baac95a261`). `git diff --stat` between the base and
that ref over the three files is empty, and `git merge-tree --write-tree
HEAD refs/issue-10755/main` exits 0, so no merge commit is owed.

## Acceptance notes

1. **The `lookup.mdx` paragraph around the re-cited sentence is a stale
contract claim (not widened; for a card).** It says the installed spec's
`SelectOptionSchema` "is strict over exactly `{label, value, color,
default, visibleWhen}` and refuses `description` by name", that
authoring it "fails the whole field with a 422", and that the key "must
never reach authored object metadata". Measured on the installed
`@objectstack/spec` 17.4.0 (`@objectstack/spec/data`):
`SelectOptionSchema.safeParse` of an option with `description` is
ACCEPTED, `FieldSchema` with that option is ACCEPTED, and the refusal
control `icon` is REFUSED with `unrecognized_keys`. Spec 17.3.0 declared
the key under the maintainer's 2026-08-25 ruling on objectui#6140 /
objectui#6153 (Option A); `544ecba84f` (PR 9588) already moved the
shipped docblocks off the refusal, and
`packages/types/src/__tests__/select-option-spec-extension-7014.test.ts`
pins the moved boundary. The repair is a paragraph rewrite, outside this
order's surface (citations only). The citation at that site is kept
resting on the landing commit `0e3b3be09`, the commit that made the
measurement the sentence still repeats, so a reader following it lands
on when, and against which spec version, the claim was true; dropping
the pointer would leave the stale sentence with nothing to date it by.
2. **Sibling class, one sweep card rather than ten:** the 204
bare-number mentions of the same ten numbers outside the three files
(H4, last bullet). The shipping subset is the 38 `packages/*/src`
non-test sites and the 20 pending changesets; objectui#7014 alone has 30
mentions, including three pending changesets named after it. This is the
class objectui#10701 was filed for, and that card itself suggested one
sweep card if more numbers in the range were gone.
3. **Neighbouring numbers that also answer 404** (objectui#5792, objectstack-ai#5794,
objectstack-ai#7792, objectstack-ai#9231) are cited nowhere in the swept trees; observation only.
4. **Instrument versions:** local lychee is 0.18.1;
`lycheeverse/lychee-action@v2` runs the current release. Same
`lychee.toml`, same globs; the CI reading is the authoritative one, and
the three residual errors above are container-specific.

---
_Generated by [Claude
Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants