Skip to content

feat(types,core,data-objectstack)!: the three surviving record ids are strings (objectui#10078) - #10733

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-10078-record-id-string-survivors
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-10078-record-id-string-survivors

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10078

Clause-②: yes

Executes director batch #195 item 1, letter A (objectui#9511, comment 5749154869, maintainer 「同意」 2026-09-20) on three more record-id declarations it did not enumerate (other record ids in @object-ui/types stay string | number; see the changeset), under the PM seat's inheritance decision in claim 5851634158 (SKILL.md 升级与决策 meta-criterion ①: a same-family branch reuses the parent ruling). The ruling's operative clause: 「A record id is a string wherever metadata names one」, with the conversion living in one typed place at the host's own adapter boundary. PR objectui#10141 (87af769) is the precedent for the changeset, the pin and the ledger handling.

维护者速读

改了什么。 三个还允许数字的记录 id 收窄为字符串:CommentSearchResult.recordId、RecordSubscription.recordId、DataSourceMutationEvent.id。它们与 objectui#9511 已收窄的 CommentEntry.recordId 是同一个 id,却因为没被裁决点名而留着旧写法。

为什么。 裁决原话是「元数据里出现的记录 id 一律是字符串」,并以「同一原则」点名了两个评论形状;这三个是运行时形状,由席位的继承决定纳入(认领 5851634158)。其余仍为 string | number 的记录 id 不在本 PR 内。

运行时影响。 两个适配器(ValueDataSource、ObjectStackAdapter)发出的 onMutation 事件,id 现在一律是字符串 —— 转换发生在适配器自己的边界,也就是裁决要求的「一个有类型的地方」。仓内两处读 event.id 的订阅者本来就先 String(...),行为不变。

风险与回滚。 三个都是纯 TypeScript 的运行时/API 形状,没有 zod 镜像、不是作者手写的元数据,所以没有「解析时拒绝」这扇门,编译器报错就是送达渠道。外部代码若用数字构造这些值,升级后编译报错,改成字符串即可。回滚 = revert 本 PR,无数据迁移、无生成物。

你要做的。 无额外动作;Clause-② 已申报 yes,draft 由派发席接手。

What changed

file change
packages/types/src/views.ts CommentSearchResult.recordId and RecordSubscription.recordId: string | number to string
packages/types/src/data.ts DataSourceMutationEvent.id: string | number to string, docblock names the rule and where the conversion lives
packages/core/src/adapters/ValueDataSource.ts update / delete emit id: String(id)
packages/data-objectstack/src/index.ts update / delete emit id: String(id), the same string already sent on the wire
content/docs/plugins/plugin-detail.mdx the documented RecordSubscription shape reads recordId: string
content/docs/guide/data-source.md the canonical-interface block's findOne / update / delete id reads string, matching data.ts (patch round 1)
packages/types/src/__tests__/record-id-string-survivors-10078.test.ts new pin, both halves (see below)
.changeset/10078-record-id-string-survivors.md minor for types / core / data-objectstack, BREAKING banner, ADR-0087 disposition line

File-surface addition, declared. The claim named the two type files, their mirrors, the compile-failing consumers, one pin and one changeset. content/docs/plugins/plugin-detail.mdx was added in this stroke: it prints the RecordSubscription shape verbatim, so the narrowing made it false (AGENTS.md Commandment #2). The two emitter files are the "consumer sites the narrowing makes fail to compile" the claim anticipated; they are the producers, which is where H4 put the conversion. content/docs/guide/data-source.md was added in patch round 1, granted by the PM addendum 5852035357: its canonical-interface block printed id: string | number, which is false against data.ts on main.

Premise P — measured on 5c61e5241 before any edit

P holds for all three keys, so all three ship together.

  • CommentSearchResult.recordId. Same sense as CommentEntry.recordId: its one producer, useCommentSearch in @object-ui/collaboration, copies it from CommentEntry.recordId (already string since objectui#9511) with an empty-string default, so it has only ever produced a string. Readers: none in the tree beyond the type (useCommentSearch is exported, and nothing in the tree reads results[i].recordId).
  • RecordSubscription.recordId. The record the bell subscribes to. No producer in the tree; the one value built is a test fixture spelled 'r-1'. Readers: SubscriptionToggle, RecordChatterPanel and RecordActivityTimeline take the object and read subscribed only. No reader touches recordId.
  • DataSourceMutationEvent.id. The id the update / delete doors already take as string. Producers: ValueDataSource and ObjectStackAdapter, whose own update / delete still declare the wider string | number parameter (allowed, because TypeScript compares method parameters bivariantly, as the DataSource docblock says) and forwarded it unchanged. No flow in the tree hands them a number: every caller that goes through the DataSource interface passes a string, and every test fixture uses string ids. The paths that COULD carry one are ValueDataSource.bulk, which reads an inline item's own key, and direct class-typed calls. That is H4's case, and the conversion now sits in the emitter. Readers: ObjectView's onMutation handler and useMutationInvalidationBridge read event.id and already pass it through String(...); ObjectCalendar, ObjectKanban, ListView and useDataRefresh read resource only. Neither of the two readers does arithmetic, a numeric comparison, a typeof === 'number' branch, or a numeric-keyed lookup.

Mechanism hypotheses

  • H1, confirmed. All three read string | number at the cited places on 5c61e5241. The card's older address for DataSourceMutationEvent had drifted, as H1 said.
  • H2, confirmed. No zod mirror exists for any of the three. packages/types/src/zod names none of them, no z.union of string and number there sits at an id, and zod-mirror-parity.test.ts holds no row for them. ⇒ The change is TypeScript-only, and ruling A's 「refused at parse」 has no door here; the compiler is the door. There are 0 KnownDrift or mirror-parity ledger rows to shrink.
  • H3, re-run by type reachability. Every package that types a value as one of these interfaces, or implements or subscribes to onMutation through the DataSource type: @object-ui/collaboration, @object-ui/plugin-detail, @object-ui/core, @object-ui/data-objectstack, plus @object-ui/react (useDataRefresh, and the bridge's own structural type) and @object-ui/plugin-view (two test doubles typed on DataSourceMutationEvent). plugin-list, plugin-kanban and plugin-calendar type the handler's event as any, so the type does not reach them.
  • H4, confirmed. Both emitters had to convert, or they would not compile (see the reverse verification). The batch path in ObjectStackAdapter emits either op.id (already string) or the server echo's id (typed any); left as is, see Acceptance notes.
  • H5. Census only; it is carried in the dev report as one family entry, not changed here.

Pin — record-id-string-survivors-10078.test.ts

  • Compile half, run by tsc -p packages/types/tsconfig.test.json (chained from type-check). There is an Equal row per member (the optional id goes through NonNullable first), a separate is-not-any row per member, a @ts-expect-error numeric literal at each of the three, and the three string spellings beside them as controls.
  • Runtime half. A source-text census of the three declarations. Controls: the matcher reads a synthetic wide spelling back, it does not answer for a member of a later interface, it returns null for an absent interface, and it reads CommentEntry.recordId (already narrowed) as string, which is the lit control.

Patch round 1 — HEAD cbe0bcee1

This round answers contract review 5852032361 (FAIL at 2a74f2c26) and the PM note 5852035357:

  • The changeset headline now says these are three more record ids that the objectui#9511 enumeration left out, and it names some of the ones still wide (「among them」).
  • The attribution is corrected in the changeset, the pin header and the pin's failure message.
  • In content/docs/guide/data-source.md, the canonical-interface block's three id widths (findOne / update / delete) are aligned to data.ts.

origin/main c3a26ccda was merged in by a merge commit.

Readings at cbe0bcee1:

  • pnpm --filter @object-ui/types type-check plus vitest run packages/types/: exit 0, 5290 tests.
  • Changeset presence / no-major / fixed / overwrite / claims, check:new-line-citations, check:control-bytes, check:spec-symbols, the check:doc-* gates, check:doc-snippets (676 of 676 blocks) and check-governed-queue-guard --test (NOT GOVERNED): exit 0 each.
  • A widened DataSourceMutationEvent.id reddens the pin (1 failed | 6 passed). It was restored, and git diff HEAD is empty.

Verification — HEAD 2a74f2c26 (round 0)

Exit codes are captured before any pipe.

command reading
pnpm --filter @object-ui/types type-check exit 0 (at HEAD)
pnpm --filter @object-ui/core type-check exit 0 (at HEAD)
pnpm --filter @object-ui/data-objectstack type-check exit 0 (at HEAD)
pnpm exec vitest run packages/types/ packages/core/ packages/data-objectstack/ packages/collaboration/ packages/react/ exit 0, 588 files, 11139 tests passed (at HEAD)
pnpm --filter @object-ui/collaboration type-check exit 0 (at a1da85b60)
pnpm --filter @object-ui/plugin-detail type-check exit 0 (at a1da85b60)
pnpm --filter @object-ui/react type-check exit 0 (at a1da85b60)
pnpm --filter @object-ui/plugin-view type-check exit 0 (at a1da85b60)
pnpm exec vitest run packages/plugin-detail/ exit 0, 214 passed and 1 skipped files, 2102 passed and 8 skipped tests (at a1da85b60)
pnpm exec vitest run packages/plugin-view/ exit 0, 51 files, 485 tests passed (at a1da85b60)
node scripts/check-changeset-presence.mjs exit 0
node scripts/check-changeset-no-major.mjs / check-changeset-fixed.mjs / check-changeset-overwrite.mjs exit 0 / 0 / 0
node scripts/check-changeset-claims.mjs exit 0, report-only; 5 pending changesets name views.ts / data.ts, all read, none falsified
pnpm check:pending-changeset-literals exit 0
pnpm check:new-line-citations exit 0, 0 new citation(s)
pnpm check:control-bytes exit 0
pnpm check:spec-symbols exit 0
pnpm check:test-path-roots exit 0
pnpm type-check:coverage exit 0
pnpm check:doc-types / check:doc-fences / check:doc-example-readers exit 0 / 0 / 0
node scripts/check-governed-queue-guard.mjs --test (the 7 paths) exit 0, NOT GOVERNED
ESLint --no-inline-config on the 5 touched .ts files exit 1, and every finding predates this change: errors and warnings per file are identical to the base blobs (data-objectstack 4 errors and 126 warnings on both sides, all 4 no-console far from the edit). The new pin has 0 errors and 3 no-explicit-any warnings, the same pattern as the objectui#9511 pin.

The only commit after a1da85b60 adds the changeset .md, which no compiler or suite in the rows marked a1da85b60 reads.

Consumer readings, with direction: the dependency closure was built first (pnpm --filter '@object-ui/plugin-detail^...' build, then mobile, plugin-form, plugin-grid and plugin-detail for plugin-view). The dist was confirmed to carry recordId: string and id?: string. Every downstream consumer of @object-ui/types named in H3 is green on type-check and vitest; none needed a change.

Reverse verification — run once, not kept

Each leg ran through ablation-replace.mjs, with its anchor hitting exactly once, the blob change proven, and the restore proven by blob equal to HEAD and an empty git diff HEAD.

  • A. Widen RecordSubscription.recordId back. tsc -p tsconfig.test.json exit 2, with TS2344 on the Equal row and TS2578 on the numeric @ts-expect-error row. The pin under vitest showed 1 failed and 6 passed, the failing row naming RecordSubscription.recordId. Direction: red, as predicted.
  • B. Drop String(...) from ValueDataSource.delete's emit. Core tsc --noEmit exit 2, TS2322 (string | number not assignable to string | undefined).
  • C. The same in ObjectStackAdapter.delete. data-objectstack tsc --noEmit exit 2, TS2322.

Acceptance notes

  • The two readers' String(event.id) is now an identity for typed producers. Both were left in place: useMutationInvalidationBridge takes dataSource: unknown and declares its own structural event type, so a plain-JS adapter is not checked by the compiler. Its test feeds id: 42 and asserts '42', which is tolerance, not dependence. Removing either conversion is a separate reader-side decision, and it moves a package this card does not own.
  • The adapters' own update / delete parameters stay string | number. The DataSource docblock records that the rule binds callers and asks implementors to honour it. Narrowing the two public adapter classes would be a wider BREAKING surface than this card.
  • ObjectStackAdapter's batch emit takes the server echo's id, typed any. The ObjectStack server's record id is z.string() by spec, so no conversion was added there; a reader-side coercion of our own server's output is the option ruling A refused.
  • SubscriptionToggle's docblock still says 「Aligned with @objectstack/spec RecordSubscriptionSchema」. That schema left with the 16.0.0 feed removal, as the RecordSubscription docblock already says. This is stale prose, noted and not changed.
  • Round 0: NOT MEASURED (both were measured in patch round 1: exit 0, 676 of 676 blocks judged) : pnpm check:doc-snippets / check:doc-examples. Both reported PRECONDITION NOT MET (exit 2): the scoped build of the packages the documents import was not run. The block this change touches is fenced plaintext, which is outside that gate's ts / tsx population.
  • NOT RUN: the Spec Main Shape Gate. None of the three touched types is derived from @objectstack/spec.
  • NOT RUN: repo-wide pnpm lint. CI owns it. The narrowing is stated in the ESLint row above: the population is the root eslint.config.js, which has no type-aware parser options, and no custom rule in eslint-rules/ reads another file, so this diff cannot move a verdict on an untouched file.

Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW


Generated by Claude Code

…e strings (objectui#10078)

CommentSearchResult.recordId, RecordSubscription.recordId and
DataSourceMutationEvent.id narrow from `string | number` to `string`,
under the one record-id rule of objectui#9511 (director batch #195
item 1, letter A). None has a zod mirror, so the change is TS-only.

The two mutation emitters (ValueDataSource, ObjectStackAdapter) convert
the id at their own boundary before emitting, as the rule prescribes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
…ner (objectui#10078)

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

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 5 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6594-headercolor-mirror-enum.md

  • names packages/types/src/views.ts → packages/types/src/views.ts — edited by this change

    The nearest precedent is objectui#5853 (.changeset/5853-tablecolumn-type-canonical-union.md), which narrowed TableColumn.type on the same three-ends pattern and exported a TABLE_COLUMN_TYPES tuple for the zod mirror to build its enum from. That shape is not available here and the difference is structural, not a preference: packages/types/src/views.ts is a type-only module, so a tuple there would add a runtime export to the package barrel (a value export cannot ride the barrel's export type block) and a runtime import edge from the zod entry into views.js. finding(types): TableColumn.type disagrees three ways — interface declares 8 literals, zod mirror accepts any string, renderer's live read set handles values outside the union via an as any cast #5853 had a second reason to export — producers needed its normalizeTableColumnType() at their emit seam — and headerColor has no producer that needs a runtime value. The literals are therefore written on each half and the anti-drift guarantee is carried by the pin above, which also covers the third end a shared tuple could not reach: the renderer, in a package @object-ui/types must not depend on.

.changeset/7287-resolve-title-field-shared-ladder.md

  • names views.ts → packages/types/src/views.ts — edited by this change

    def.primaryField — a DetailViewSchema key (@object-ui/types views.ts), read off an OBJECT def and ranked ABOVE the canonical nameField ADR-0079 Phase 2 made the pointer (AGENTS.md Commandment #0.1). No producer can put it there: @objectstack/spec's object schema is a strictObject that answers unrecognized_keys: ['primaryField'], and ObjectSchema.create() throws — which is why objectstack#6326 deleted the identical read from two lint rules. A census across both repos found zero object payloads carrying it (the only writers are three test fixtures), and primaryField appears in zero files of the shipped @objectstack/spec@17.2.0 dist against 68 for nameField. Same shape as the undeclared objectDef.titleField read objectui#6531 measured and Five more consumer-side reads of the undeclared object-level titleField survive #6531 — in ObjectView view-config fallbacks and a useRecordSearch memo key #6557 removed. DetailViewSchema.primaryField is untouched and still honoured by DetailView's own header — it is a view key, and on a view it is legitimate.

.changeset/7586-primary-field-consumer-reads.md

  • names views.ts → packages/types/src/views.ts — edited by this change

    primaryField is a DetailViewSchema key (@object-ui/types views.ts) — a view key, which DetailView.resolveDisplayTitle reads off schema and is welcome to. Read off an object def it is undeclared: @objectstack/spec's object schema is a strictObject answering unrecognized_keys: ['primaryField'], and ObjectSchema.create() throws. primaryField appears in zero files of the shipped @objectstack/spec@17.2.0 dist, against 68 for the canonical nameField. objectstack#6326 removed the identical read from two lint rules; objectui#7287 / PR fix(plugin-detail): resolveTitleField delegates to the shared ADR-0079 ladder #7585 removed it from resolveTitleField. These two survived it — and three of this repo's own changelogs already called the probe "not a spec property — always undefined" while the code kept honouring it.

.changeset/9309-object-gallery-filter-destination-typed.md

  • names data.ts → packages/types/src/data.ts — edited by this change

    It was filter?: unknown under the docblock "Query filter, forwarded verbatim as $filter", and $filter is RecordANGLE-BRACKETS(string, any) | FilterArray (QueryParams, data.ts). The declaration therefore named a destination it did not type: an author told to forward the value verbatim got a type error on the key the sentence had just told them to forward, and the way through was as — which un-checks the destination's real type at that call site too. The declaration is now an INDEXED ACCESS on QueryParams, not a copy of its arms, so the two cannot drift.

.changeset/9511-record-id-is-a-string.md

  • names views.ts → packages/types/src/views.ts — edited by this change

    The three authorable keys, each on BOTH faces. ObjectFormSchema.recordId (objectql.ts + zod/objectql.zod.ts), DetailViewSchema.resourceId (views.ts + zod/views.zod.ts) and DetailSchema.resourceId (crud.ts + zod/crud.zod.ts). ⚠️ The crud pair is DetailSchema, not DetailViewSchema, and it reaches the same renderer — not by symbol but by data flow: plugin-detail registers the 'detail' node type onto DetailView. A read that follows TypeScript symbols alone finds two keys and is incomplete.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with c3a26ccda (merge-base with origin/main): 7 file(s) changed outside .changeset/, read against 1562 pending declaration(s) that publish a body (2149 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3062.4 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-peMbt7Mj.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.57KB 6.15KB
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) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
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.39KB 1.21KB
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.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
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) 27.13KB 7.95KB
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) 547.82KB 131.15KB
core (index.js) 9.52KB 3.79KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.93KB 62.31KB
fields (index.js) 259.29KB 65.64KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
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.33KB 3.07KB
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) 16.01KB 3.93KB
plugin-calendar (index.js) 51.65KB 14.69KB
plugin-charts (index.js) 81.82KB 22.51KB
plugin-chatbot (index.js) 198.40KB 47.22KB
plugin-dashboard (index.js) 134.05KB 35.61KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.23KB 61.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 155.41KB 39.70KB
plugin-gantt (index.js) 169.75KB 41.96KB
plugin-grid (index.js) 218.12KB 59.72KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.70KB 28.42KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 87.83KB 22.01KB
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) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
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) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
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) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
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) 17.15KB 6.32KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Red check that is not this PR's: Inert vi.mock Specifier Check. Read at 2026-09-27T02:29Z on head 2a74f2c26.

The same check is red on main 5c61e5241. Its one offender is apps/console/src/__tests__/filterContextTokensSweep-10666.test.tsx:51, which PR objectui#10729 added and this diff does not touch. No fix exists yet; the anchor card is objectui#10731. The check is not in the main ruleset's required set (Lint, Type Check, Build & E2E, Build Docs, Changeset Declaration, Test). It reproduces deterministically on the base, so it is not re-run here.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2a74f2c2637612ce924942b656b05236c4db3d40

① Derived judgments

  • CommentSearchResult.recordId narrows string | number to string (views.ts, published via packages/types/src/index.ts:938). Evidence: grep -n recordId packages/collaboration/src/useCommentSearch.ts shows the only producer builds it as recordId: c.recordId ?? '' from CommentEntry.recordId, which is already string (views.ts:515, narrowed by objectui#9511); grep -rn CommentSearchResult packages apps finds no reader of the member beyond the type. Same kind of shape as the two the ruling named (TS-only runtime shape, no zod mirror: grep -rn "CommentSearchResult|RecordSubscription|DataSourceMutationEvent" packages/types/src/zod returns 0 hits). Verdict: the parent ruling's "same principle" reaches it with no semantic difference; correct accept-set judgment.
  • RecordSubscription.recordId narrows to string (views.ts:729-736, published via index.ts:946). Evidence: grep -rn "subscribed:" packages apps (non-test) finds no in-tree producer; the one value built is a test fixture { recordId: 'r-1', subscribed: false }; readers SubscriptionToggle, RecordChatterPanel, RecordActivityTimeline type the object and read subscribed only. A host-built value, so narrowing binds the host to convert once, consistent with the ruling's "host converts at its own adapter boundary". Verdict: reaches; correct.
  • DataSourceMutationEvent.id? narrows to string (data.ts:1167-1181, published via index.ts:401). Real semantic difference, and the PR names it rather than glossing it: the doors are inbound method parameters that bivariance leaves unenforced on implementors (data.ts:385-387 "The rule binds CALLERS; adapters are asked to honour it"), while the event is an object literal the emitter assigns, so the compiler DOES force producers here (the PR's H4, ablations B/C TS2322). The id is the same value update/delete take as string (data.ts:462-482). Verdict: reaches; correct, and the enforcement asymmetry is stated.
  • Producers now emit String(id) in ValueDataSource.update/delete (core:1185/1195) and ObjectStackAdapter.update/delete (data-objectstack:3913/3952). These are the only two onMutation implementations in the tree (grep -rn "^\s*onMutation\s*(" packages apps non-test: 2 hits). ObjectStackAdapter already sends String(id) on the wire (index.ts:3907, 3937), so the event now matches the wire. ValueDataSource.bulk (core:1213/1220) forwards an inline item's own key, the one in-tree path that could carry a number; it now converts. Readers: grep -rn "\.onMutation(" packages apps gives 6 subscribers; ObjectView.tsx:956 and react/data-invalidation.ts:136 read event.id and already String(...) it into DataChange.recordId?: string, compared by dataChangeMatches as change.recordId === recordId (string to string, data-invalidation.ts:82-94); useDataRefresh, ObjectKanban, ObjectCalendar, ListView read resource only. No arithmetic, typeof === 'number', or numeric-keyed lookup anywhere. Verdict: producer-side conversion is the single typed place per adapter; nothing silently breaks. Residual (non-blocking): emitBatchMutations (data-objectstack:4201) emits op.id ?? echo?.id ?? echo?._id where echo is any, so a non-string server echo would still pass at runtime; BatchTransactionOperation.id?: string and the spec's z.string() cover it, and the dev declared it.
  • Docs change content/docs/plugins/plugin-detail.mdx:401. The RecordSubscription block now reads recordId: string, subscribed: boolean, channels?: Array<'in_app' | 'email' | 'push'>, which matches views.ts:729-736 member for member. Verdict: the new text is true. The id: string | number at mdx:379 is FeedItem.id (views.ts:619, untouched) and stays true.
  • Published entry points. @object-ui/types exports all three (index.ts:401, 938, 946); @object-ui/core and @object-ui/data-objectstack change runtime emit behaviour on their exported classes; neither re-exports the event type. Transitively, @object-ui/collaboration (useCommentSearch returns CommentSearchResult[]) and @object-ui/plugin-detail (SubscriptionToggleProps.subscription: RecordSubscription) publish narrower .d.ts without a source change; the PR's H3 names them, the changeset frontmatter does not, exactly as the precedent 9511 changeset omitted plugin-detail and collaboration. Verdict: not wider or narrower than the PR says; non-blocking nit that the changeset prose could name the two transitively narrowed packages.
  • Pin record-id-string-survivors-10078.test.ts can fail. Compile half is reached: packages/types/package.json type-check chains tsc -p tsconfig.test.json, whose include is src/**/*.test.ts (CI Type Check green). The three @ts-expect-error rows each sit beside an undirected string-spelling control that compiles, so each directive is used by exactly the numeric member. Runtime half: 4 controls + 3 site rows = 7 tests, matching the dev's "7 passed". Ablation A (widen RecordSubscription.recordId) predicts TS2344 on _RecordSubscriptionRecordIdIsAString, TS2578 on the _numericSubscription directive, and vitest 1 failed / 6 passed: coherent with the file's content. Ablations B/C are compiler ablations of the two emitters (TS2322 string | number to string | undefined): coherent with id?: string. memberType cuts the interface body at the first \n} and reads the member line; all three interfaces are flat, so the census reads the right line. Verdict: the pin can fail; the three ablations are coherent.
  • CI. Inert vi.mock Specifier Check is the only red at head (43 runs: 39 success, 3 skipped, 1 failure). Job log 108533443895 and main's job 108526869853 on 5c61e5241 both report exactly "1 mock resolves to no file on disk" at apps/console/src/__tests__/filterContextTokensSweep-10666.test.tsx:51; git log origin/main -- that file shows it landed in f9c06ef6aa (fix: resolve context tokens in every directly authored data node's own filter (objectui#10666) #10729); git diff --stat origin/main...2a74f2c26 -- apps/console is empty. Same signature, inherited from main, not this PR's.

② Semver level

  • Level: minor for @object-ui/types, @object-ui/core, @object-ui/data-objectstack. Policy line: AGENTS.md line 271 "changeset 里不要声明 major ... objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可)", enforced by scripts/check-changeset-no-major.mjs (CI Changeset Bump Policy green). The three packages are the three whose src/ changed (Changeset Declaration green). Consistent with the precedent .changeset/9511-record-id-is-a-string.md (minor, BREAKING banner, disposition line).
  • Changeset sentences checked:
    • "the last three record ids that still admitted a number are strings": FALSE. grep -n "string | number" packages/types/src/views.ts packages/types/src/objectql.ts shows published record ids still wide: views.ts:968 recordNavigation.recordIds: Array<string | number>, views.ts:972 onNavigate: (recordId: string | number) => void, objectql.ts:1117 / 2140 / 2729 onNavigate?: (recordId: string | number, ...) on ObjectGridSchema, ObjectViewSchema, ListViewRuntimeProps, views.ts:655 FeedItem.sourceId?: string | number (marked RECORD-ID in the dev's own H5 census). The headline contradicts the dev report. Fix is one phrase: "three more record ids the objectui#9511 ruling did not enumerate".
    • "objectui#9511 ruled that a record id is a string wherever this package names one": over-broad attribution; the ruling's words are "wherever metadata names one" plus "the same principle" for two comment shapes; the reach to these runtime shapes is the PM inheritance decision (claim 5851634158). Non-blocking wording nit; the pin's failure message repeats it.
    • "CommentSearchResult.recordId is copied straight from CommentEntry.recordId": true (useCommentSearch.ts recordId: c.recordId ?? '').
    • "the id the update / delete doors already take as a string": true (data.ts:462-482).
    • "Both methods still accept a numeric id through their own wider parameter, and each converts it at its own boundary before emitting": true (core:1170-1195, data-objectstack:3897-3952).
    • "The two in-repo subscribers that read event.id already pass it through String(...)": true, and "two" is complete (ObjectView.tsx:956, data-invalidation.ts:136).
    • "None of the three has a zod mirror, none is authored into JSON, and no schema references them": true (0 hits in packages/types/src/zod, 0 rows in zod-mirror-parity.test.ts, 0 KnownDrift rows).
    • "Disposition: ADR-0087 D7 ...": D7 exists (objectstack docs/adr/0087-metadata-protocol-upgrade-contract.md line 646, heading quoted accurately) and its four conditions hold at HEAD (exported interfaces; not *.zod.ts/contracts/object definitions; not z.input projections; no metadata surface references them). Non-blocking flag: D7 "inherits the no-migration-prescription refusal", and this body carries a bold FROM / TO line and a consumer prescription that objectstack's gate reads as a prescription (its fixture R2); objectui runs no ADR-0087 registration gate and the precedent carries the same FROM/TO line under D2, so this is a label question for the PM, not a falsity.
    • "Pre-GA a breaking change ships minor with this banner (ADR-0087, amended 2026-09-13)": the amendment exists (line 388, #18003) but its sentence is "a metadata-facing retirement or break ships minor"; this change is not metadata-facing. The minor level is still right by AGENTS.md line 271, so the generalisation is a nit.
    • "major is refused outright because every publishable package sits in one fixed group ... (scripts/check-changeset-no-major.mjs)": true (script header, .changeset/config.json fixed group).
    • "Pin ... asserts all three members read string (not any), refuses a numeric literal at each, and keeps a source-text census ... with controls": true.

③ Boundary flags

(a) Docs-file surface extension: acceptable, in fact required. AGENTS.md Commandment #2 (line 94, "Docs-driven ... Not done until docs reflect the code"); the narrowing made the block false and the new block is true. Self-raised, pre-existing and not this PR's: content/docs/guide/data-source.md:18-28 labels a block "The canonical interface lives in @object-ui/types" that still prints findOne / update / delete with id: string | number, false on main since objectui#9333 / #9712 / #9511; a card is owed, not a change to this PR.

(b) Reader-side String(event.id) left in ObjectView and useMutationInvalidationBridge: acceptable. Both readers take untyped input (event: any at ObjectView.tsx:951; the bridge declares its own structural { resource: string; id?: string | number } over dataSource: unknown at data-invalidation.ts:126-131, and its test feeds id: 42 through a handler typed any). The ruling refused conversion INSTEAD of narrowing (option C); tolerance for plain-JS producers is not that. Nit: the bridge's structural type and ObjectView's any could now state string; both live outside this card's packages.

(c) ValueDataSource / ObjectStackAdapter still declare update / delete id as string | number: acceptable. data.ts:385-387 on main already says the rule binds callers and adapters are asked, not forced; the two classes are public and narrowing them is a separate BREAKING surface. The conversion at the emit site is the adapter converting "at its own boundary". Recommend a follow-up card; not a defect of this PR.

(d) Out-of-scope census of remaining string | number id sites not changed: acceptable as scope, but it must be escalated, because it is the direct reason the changeset headline is false. PM decides whether views.ts:655 FeedItem.sourceId, :918/:921 history entry ids, :968 recordIds, :972 and objectql.ts:1117/2140/2729 onNavigate(recordId) get a close-out card; this PR must stop claiming the family is closed.

Self-raised: (e) emitBatchMutations emits echo?.id typed any (data-objectstack:4201), the one residual runtime path where a non-string id could still reach subscribers; declared by the dev, non-blocking. (f) A file standdown.md (629 bytes) was present in my scratch directory scratchpad/pr-10733/ that I did not write this run; per the rules it was not read and its content played no part in this review. (g) origin/main in /home/user/objectui advanced from 5c61e5241 to c3a26ccda during this run (another process fetched); the diff and merge base were taken while origin/main was 5c61e5241, the PR's base, and match the PR files listing.

Blocking item: the changeset's first sentence "the last three record ids that still admitted a number are strings" is false on the published @object-ui/types surface (views.ts:968, :972, :655; objectql.ts:1117, :2140, :2729 still admit a number) and is contradicted by the dev's own H5 census. One-phrase fix in .changeset/10078-record-id-string-survivors.md; nothing else in the PR needs to move.

Implemented-by: claude/issue-10078-record-id-string-survivors
Reviewed-by: session_014fWVhLzhxR8qrFsJ5o8TYW

VERDICT: FAIL

Adopted by the domain:spec#1 @ objectui seat. The Served-tier value is written as the constant's name; the reviewer's output carried the served model's id there.


Generated by Claude Code

…survivors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
…e ids read string (objectui#10078)

Contract review of 2a74f2c (record 5852032361) found the changeset
headline false: other record ids in @object-ui/types still admit a
number. The changeset and the pin now say these are three MORE ids the
objectui#9511 enumeration left out, name the ones still wide, and
attribute the reach to the seat's inheritance decision rather than to
the ruling, whose words are "wherever metadata names one".

content/docs/guide/data-source.md: the block labelled as the canonical
@object-ui/types interface printed findOne / update / delete with a
string-or-number id; data.ts declares `id: string` on all three.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3062.4 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-yOvSPUWY.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.57KB 6.15KB
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) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
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.39KB 1.21KB
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.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
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) 27.13KB 7.95KB
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) 547.82KB 131.15KB
core (index.js) 9.52KB 3.79KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.93KB 62.31KB
fields (index.js) 259.29KB 65.64KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
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.33KB 3.07KB
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) 16.01KB 3.93KB
plugin-calendar (index.js) 51.65KB 14.69KB
plugin-charts (index.js) 81.82KB 22.51KB
plugin-chatbot (index.js) 198.40KB 47.22KB
plugin-dashboard (index.js) 134.05KB 35.61KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.23KB 61.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 155.41KB 39.70KB
plugin-gantt (index.js) 169.75KB 41.96KB
plugin-grid (index.js) 218.12KB 59.72KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.70KB 28.42KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 87.83KB 22.01KB
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) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
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) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
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) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
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) 17.15KB 6.32KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: cbe0bcee17bba90fac369e95f23d4069d881db32

① Derived judgments

  • Previous blocking item: FIXED. The changeset now opens "three more record ids that the objectui#9511 ruling's enumeration left out are strings now", followed by "Other record ids in @object-ui/types still admit a number and are not touched here, among them ...". Every site it names is still wide on the head tree: DetailViewSchema.recordNavigation.recordIds (views.ts:968, an Array of string | number) and .onNavigate (:972, recordId: string | number), both inside the recordNavigation? block at :966 of DetailViewSchema (declared :742); FeedItem.sourceId? (:655, inside FeedItem :617); onNavigate?(recordId: string | number, ...) on ObjectGridSchema objectql.ts:1117, ObjectViewSchema :2140, ListViewRuntimeProps :2729. "among them" keeps the list non-exhaustive, which is correct: DetailViewSchema.history.entries[].id (:918) and .user_id (:921) are also wide and not named.
  • Attribution corrected and true. Changeset: "ruled that a record id is a string wherever metadata names one, and extended that, as 「the same principle」, to CommentEntry.recordId and MentionNotification.recordId" matches ruling 5749154869 verbatim ("A record id is a string wherever metadata names one" / "(the same principle; a knowing decision...)"). "Carrying the rule to them is the domain:spec seat's inheritance decision, recorded in the claim on objectui#10078" matches claim 5851634158 (SKILL.md meta-criterion ①). The pin header (lines 15-21) and the pin failure message ("carries the objectui#9511 record-id rule (a record id is a string wherever metadata names one) to this runtime shape") carry the same corrected wording; the pin header adds "This file pins these three ONLY. Other record ids in this package still admit a number". The seat-edited PR body says "three more record-id declarations it did not enumerate (other record ids in @object-ui/types stay string | number; see the changeset)" and the 为什么 paragraph quotes the ruling correctly and says the rest is out of this PR.
  • No text still claims the family is closed. Three non-blocking wording nits: (i) the PR title and the pin's describe name keep the card's phrase "the three surviving record ids"; the definite article can be read as "the only three", but the pin header's scope note and the PR body's first paragraph refute that in place. (ii) PR body, Patch round 1 section: "it lists the ones still wide" overstates the changeset, which says "among them"; "some of the ones" would match. (iii) PR body: "content/docs/guide/data-source.md is aligned to data.ts" is a whole-file claim; only the canonical-interface block's three id widths were aligned, as the What-changed table row says precisely.
  • Other newly written sentences are true. "Packages that narrow without a source change": collaboration's barrel exports useCommentSearch (packages/collaboration/src/index.ts:69) whose results: CommentSearchResult[] (useCommentSearch.ts:23,57); plugin-detail's barrel exports SubscriptionToggle and SubscriptionToggleProps (packages/plugin-detail/src/index.tsx:142,163) with subscription: RecordSubscription (SubscriptionToggle.tsx:17). A non-test grep for the three type names hits only types, plugin-detail, collaboration, data-objectstack and core, so the pair is complete. "It did not name these three": the ruling text names none of them.
  • Docs edit content/docs/guide/data-source.md: true, and nothing else changed. The diff is exactly three line pairs (findOne, update, delete id from string | number to string, +3 −3). On head, packages/types/src/data.ts declares findOne(resource: string, id: string, params?: QueryParams): Promise of T | null (:433), update(resource, id: string, data: Partial of T, opts?: { ifMatch?: string }) (:462-467) and delete(resource, id: string, opts?: { ifMatch?: string }) (:478-482), member for member with the block. Declared residue, outside the PM addendum's scope and noted by the dev as carrier none: the custom-adapter example at :139/:147/:151 still declares string | number on an implementing class (compiles by bivariance); the block's T defaults to unknown where data.ts has any; getObjectSchema returns a Promise of unknown where data.ts has any.
  • Merge of main broke nothing. 5870203 is a trivial merge: git diff c3a26ccda 5870203092 is byte-identical to git diff 5c61e5241 2a74f2c26 (cmp exit 0), and git show -c on it lists only main's own files. main's c3a26cc (ColumnWidthConfig retirement: complex.ts, index.ts −1 line, zod/complex.zod.ts, zod-mirror-parity.test.ts, plugin-kanban, changeset 10582 and its pin) touches neither views.ts nor data.ts; the three exports moved by one line (index.ts:400, :937, :945). The dev's own round-1 commit cbe0bce touches exactly the changeset, data-source.md and the pin (+41 −25), all inside the PM addendum's surface.
  • Accept-set re-confirmations on the head tree, one line each:
    • CommentSearchResult.recordId to string (views.ts:555): the only producer is recordId: c.recordId ?? '' (useCommentSearch.ts:70) from CommentEntry.recordId?: string (views.ts:515); no non-test reader of the member. RIGHT.
    • RecordSubscription.recordId to string (views.ts:731): no non-test producer (every subscribed: hit is the type or a callback parameter name); readers SubscriptionToggle:17, RecordChatterPanel:35, RecordActivityTimeline:80 type the object and none reads .recordId. RIGHT.
    • DataSourceMutationEvent.id? to string (data.ts:1180): the same id update/delete take as string (:464, :480); an object literal, so the compiler forces emitters, as the PR says. RIGHT.
    • Emitters: ValueDataSource.ts:1185 and :1195 emit String(id); the bulk path (:1212, :1219) forwards getRecordId(item, this.idField) through those two methods, so an inline numeric key converts; ObjectStackAdapter index.ts:3913 and :3952 emit String(id), matching the wire String(id) at :3907 and :3937. The only two onMutation implementations in tree are core:1301 and data-objectstack:3554. RIGHT.
    • Readers: ObjectView.tsx:956 and react data-invalidation.ts:136 wrap e.id in String(...); the other four subscribers (ObjectKanban:537, ObjectCalendar:406, useDataRefresh:50, ListView:1382) compare event.resource only. RIGHT.
    • plugin-detail.mdx:401 block reads recordId: string, subscribed: boolean, channels?: Array of 'in_app' | 'email' | 'push', equal to views.ts:731-735. RIGHT.
    • Pin can fail: packages/types type-check chains tsc -p tsconfig.test.json (include src/**/*.test.ts), so the compile half runs under CI Type Check (green); runtime half is 4 controls + 3 SITES rows = 7, and the dev's round-1 ablation (widen data.ts id) reporting 1 failed / 6 passed is coherent with the file. RIGHT.
    • Residual emitBatchMutations (data-objectstack:4190) echo id typed any: unchanged and declared. Non-blocking.
  • CI at head (check-runs re-read 02:58:41Z): 39 success, 3 skipped, 1 failure. Required contexts from rules/branches/main (strict policy) are Lint, Type Check, Build & E2E, Build Docs, Changeset Declaration, Test, Spec Main Shape Gate; all seven are success on cbe0bcee1 (Test rollup completed 02:56:12Z, Type Check 02:52:25Z). The one red, Inert vi.mock Specifier Check job 108536343124, logs "1 mock resolves to no file on disk" with the single offender apps/console/src/__tests__/filterContextTokensSweep-10666.test.tsx:51 -- vi.mock('../../../../packages/plugin-map/node_modules/react-map-gl/dist/maplibre.js'); main c3a26cc's job 108534558381 prints the identical line; git diff --stat origin/main...head -- apps/console is empty. Same signature, inherited from main, not a required context.

② Semver level

  • minor for @object-ui/types, @object-ui/core, @object-ui/data-objectstack: exactly the three packages whose src/ changed (diff stat). Consistent; Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check, Changeset Overwrite Report, Changeset Claim Re-read and Line Citation Gate all green at head.
  • New grounding is true: AGENTS.md §版本号策略(version alignment), line 271, "objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可)"; line 272 names scripts/check-changeset-no-major.mjs as the enforcer, and that script's header restates the rule; .changeset/config.json holds one fixed group (40 entries) containing all three packages; "pinned to the @objectstack major" is line 268. The changeset cites the section by its own parenthetical name, "(AGENTS.md, version alignment)".
  • Every other changeset sentence re-checked true on head: copied straight from CommentEntry.recordId (useCommentSearch.ts:70); the update/delete doors take a string (data.ts:464, :480); both adapter methods keep a wider parameter and convert at their own boundary before emitting (core:1170-1195, data-objectstack:3897-3952); the two in-repo subscribers already String(...) it; no zod mirror, nothing authored into JSON, no ledger rows (0 hits for the three names in packages/types/src/zod and zod-mirror-parity.test.ts); the pin description matches the file.
  • Non-blocking, still open for the seat (the dev left it as the previous review asked): "Disposition: ADR-0087 D7". The D7 heading (objectstack docs/adr/0087, line 646) is paraphrased accurately and its four conditions hold at head, but D7 "also inherits the no-migration-prescription refusal" (line 683), whose refusal row reads "refused — the body carries a FROM/TO block" (line 740), and this body carries "FROM string | number TO string" plus a host prescription. objectui runs no ADR-0087 registration gate (the only scripts mentioning ADR-0087 are check-component-surface-parity.mjs and doc-version-claims.test.ts, neither reads dispositions) and the 9511 precedent carries the same FROM/TO line under D2, so this is a label choice, not a false statement.

③ Boundary flags

(a) Docs surface: both docs files are inside the PM addendum 5852035357; plugin-detail.mdx was required by AGENTS.md Commandment #2 (line 94). Acceptable.
(b) Reader-side String(event.id) left in ObjectView and the bridge: acceptable, unchanged from the previous review; both readers take untyped input.
(c) The adapter classes' own update/delete parameters stay string | number: acceptable; data.ts:383-387 says the rule binds callers and asks implementors; a separate breaking surface.
(d) Family census not closed by this PR: now stated in the changeset and the pin header instead of contradicted. A close-out card for views.ts:918/:921/:968/:972/:655 and objectql.ts:1117/:2140/:2729 remains the seat's call at ACCEPT.
(e) PR body internal staleness, non-blocking: the Acceptance notes still say "NOT MEASURED: pnpm check:doc-snippets / check:doc-examples" while the Patch round 1 section reports them measured (676 of 676 blocks) and CI Doc Snippet Type Check is green; per the dev contract the seat owns PR-body edits, together with nits (ii) and (iii) above.
(f) Self-raised: the PR is still a draft with no assignee (the label-write was refused in round 0 and, as ordered, not retried); a seat write.
(g) Self-raised: the scratch directory pr-10733-r1 was empty before this run, nothing was read there that this run did not write, the worktree was removed, /home/user/objectui and /home/user/objectstack are untouched, and zero GitHub writes were made.

Implemented-by: claude/issue-10078-record-id-string-survivors
Reviewed-by: session_014fWVhLzhxR8qrFsJ5o8TYW

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 05:07
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 7b395d8 Sep 27, 2026
44 of 45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10078-record-id-string-survivors branch September 27, 2026 05:20
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…e html tier's intrinsic elements, marked tier html — div stays out (objectui#10735) (objectstack-ai#10753)

Fixes objectstack-ai#10735
Clause-②: yes — the published manifest's declared vocabulary grows.

Executes the maintainer's ruling A on objectstack#20112 (batch objectstack-ai#225 item
1, 「其他同意」): the published `sdui.manifest.json` declares the html tier's
registered intrinsic set exactly as the registry declares it, with each
tag's `inputs` and child slot; `div` stays undeclared. Base
`origin/main` 5ad3b88; head 6417e8d (round 2: wording only, one commit
on top of 5bd6270).

## What changed

- `packages/core/src/registry/html-tier-intrinsics.ts` (new):
`HTML_TIER_INTRINSICS` — the 37 tags of `html-elements.tsx`'s `TAGS`,
plus `span`, `table`, `label` and the seven sectioning tags of
`semantic.tsx` (47). Exported from `@object-ui/core` beside
`PUBLIC_BLOCKS`.
- `packages/core/src/registry/Registry.ts`: `getPublicConfigs()` appends
the roster's registered tags LAST, each stamped `tier: 'html'` in the
projection (never written onto the registration);
`PublicComponentConfig.tier` admits `'html'`. `PUBLIC_BLOCKS` is
untouched.
- `packages/sdui-parser`: `ManifestComponent.tier?: 'html'`;
`manifestFromConfigs` writes exactly `'html'` or omits the key (every
curated entry serialises byte-identically); `generateBlockList` sections
the html tier under its own count; `RegistryConfigLike.tier` admits
`'html'`. `gen-manifest.ts` header updated, behaviour unchanged.
- `packages/components/src/renderers/layout/react-page.tsx`: the
`kind:'react'` scope builder skips `tier === 'html'` entries, so no `P`
/ `A` / `Img` wrapper is injected (file-surface amendment, forced by the
card's ⛔ on silent widening — see H2).
- Tests: new pins in core, sdui-parser, console;
`react-page-scope.test.tsx` and `container-declaration-census.test.tsx`
updated; `public-contract.test.ts` prose amended (two ledger passages
that said curating a sectioning tag would newly widen the manifest).
- Docs: `content/docs/guide/react-pages.md` (the html tier is not
injected on the react tier), `content/docs/guide/plugin-development.md`
(the component-level `tier: 'html'` marker).
- `.changeset/10735-html-tier-manifest.md`: `minor` for
`@object-ui/core`, `@object-ui/sdui-parser`, `@object-ui/components`.
- Round 2 (contract review, wording): the react-page skip line is quoted
as it now reads — `if (!tag || cfg.isContainer || cfg.tier === 'html')
continue;` — in `content/docs/guide/react-pages.md` ("skips every
container and every html-tier entry"; frontmatter lines 1–4 md5
unchanged, 9c5f94a7…), in both `public-contract.test.ts` passages, in
the `container-declaration-census` pin, and in
`layout-containers-declare-containment.test.tsx` (comment-only,
previously untouched). `git grep -F` of the old line at head: 2 hits,
both inside `.changeset/6740-flex-is-container.md` — its original
paragraph and the dated note that marks it. The 10735 changeset now says
"200 ledgered findings" (10 ledger rows whose counts sum to 200).
- `.changeset/6740-flex-is-container.md` (pre-existing,
`@object-ui/components: patch`): reconciled in the objectui#10533
dated-note form, scoped "at this change" — `⚠️ **Dated note, 2026-09-27
— the skip line quoted above has since gained a third arm —
objectui#10735.**` — frontmatter byte-identical (md5 ce888ebd… before
and after). `check-changeset-overwrite` REPORTS the modification
(report-only, exit 0) and did not refuse it.
- ⛔ Not touched: `html-elements.tsx`, `div.tsx`, `PUBLIC_BLOCKS`, the
registration-uniqueness test.

## H1 — the census (measured from `ComponentRegistry.getKnownTypes()`
after `@object-ui/components` + `apps/console/src/register-plugins.ts`
load, in the console vitest project)

Instrument for "is an HTML tag": the keys of `JSX.IntrinsicElements` in
`@types/react` (178 names, HTML and SVG; lit control: `div`, `p`, `svg`
in, `object-grid` out). Registry: 485 known keys, 208 bare.
Intersection: **64** bare registrations carry an element name.

- **(a) `html-elements.tsx` `TAGS` — 37, all registered:** h1 h2 h3 h4
h5 h6 p a blockquote pre strong em b i u small mark sub sup del ins abbr
ul ol li dl dt dd figure figcaption img hr br time address cite q.
- **(b) registered elsewhere, declared — 10:** `span` (basic/span.tsx;
inputs className, children), `table` (complex/table.tsx; caption,
footer, columns, data, className — no child slot), `label`
(form/label.tsx; text required, className — no child slot), and `aside
main header nav footer section article` (layout/semantic.tsx; className,
children; `isContainer`).
- **(c) excluded — 17:**
- `div` — deprecated on the JSON surface in favour of `box`
(objectui#3965, PR objectui#6878); kept out by the ruling.
- `code`, `summary`, `object` — bare namespace fallbacks of `field:code`
/ `field:summary` / `field:object` (inputs `[]`, no child slot); `view`
— `plugin-view`'s bare key (SVG name coincidence). ⚠️ The ruling's list
names `code` as "registered elsewhere"; measured, the only `code`
registration is the field widget (a code editor / `pre` block reading
`value`). Declaring it would teach a tag whose children the runtime
drops, so it is NOT declared — see Acceptance notes and the report's
open question.
- `kbd` — a `ui` component reading `keys` / `label`, not named by the
ruling; not declared (additive later on a named need; retracting is a
narrowing).
- `form input textarea select switch` — JSON-surface form components;
the tier excludes form controls by design (the exclusion list in
`html-elements.tsx`). `progress`, `dialog` — `ui` components under tag
names, not passthroughs, not named.
- `button text image html` — already curated in `PUBLIC_BLOCKS`; they
reach the manifest from there, unstamped.

The (c) list is not prose:
`apps/console/src/__tests__/html-tier-manifest.test.ts` holds every
HTML-named bare registration to one of `PUBLIC_BLOCKS`,
`HTML_TIER_INTRINSICS` or its exclusion ledger `EXCLUDED_HTML_NAMED` (13
entries, each with a reason, each pinned live), so a registration that
newly squats on a tag name lands there by absence.

## H2 — the fork, the consumer map, and the marker decision

**Premise measured true:** `getPublicConfigs()` through
`manifestFromConfigs` is the sole source of the published manifest —
objectui's `apps/console/dev/manifest-dump.tsx` /
`scripts/dump-public-manifest.mjs`, and objectstack's
`scripts/gen-sdui-manifest-node.mjs` (which imports the 16 registry
modules from objectui's built tree at `.objectui-sha` and calls exactly
`ComponentRegistry.getPublicConfigs()` then its lockstep copy's
`manifestFromConfigs`). The manifest is ONE flat `components` map with
no tier marker; `compile()` derives `allowedTags =
Object.keys(manifest.components)`. So listing the html tags in
`PUBLIC_BLOCKS` would have widened the curated JSON-surface vocabulary
silently — the fork the card names.

**Marker chosen: a per-entry `tier: 'html'` on `ManifestComponent`,
carried from a projection-only stamp in `getPublicConfigs()`.** Why not
a second registry read or a second manifest section: the objectstack
generator calls `getPublicConfigs()` and nothing else, and `compile()`
reads only `components` — either alternative needs an objectstack-side
change before the gate accepts a single html tag, and the second-section
shape would leave the gate refusing the tags until its lockstep copy was
ported. With the per-entry marker the objectstack gate accepts the tags
with NO reader change: it ignores the extra key.

Consumers, and what each does with the marker:

| reader | reads | change |
|---|---|---|
| objectstack `packages/lint` `validateJsxPages` → `compile()` | keys as
whitelist; `validateTree` reads `inputs` / `namespace` | none — accepts
the tags; ignores `tier` |
| objectstack `scripts/check-sdui-manifest.mjs` | `components[k].type
=== k`, count, sha | none |
| objectstack `scripts/gen-sdui-manifest-node.mjs` |
`getPublicConfigs()` + lockstep `manifestFromConfigs` | none to run; ⚠️
its lockstep copy of `packages/sdui-parser/src/{index,types,codegen}.ts`
needs this port for the marker to reach the tracked file — until then
the tags are declared there WITHOUT `tier`, which the gate treats
identically |
| objectui `react-page.tsx` `buildComponentScope` | `getPublicConfigs()`
| skips `tier === 'html'` (else `P` / `A` / `Img` wrappers would be
injected) |
| objectui `generateBlockList` | manifest | sections the html tier;
curated count unchanged (59) |
| objectui `generateDts` | manifest | emits the tags into
`JSX.IntrinsicElements` (the html-tier authoring surface) |
| objectui `page.tsx` html-tier compile | `getKnownTypes()`, not the
manifest | none (already accepted these tags) |
| console census tests | `getPublicConfigs()` |
`container-declaration-census` reads the curated contract as the
unstamped entries; `public-contract` coverage lists filter by
`PUBLIC_BLOCKS` and are unchanged |

Reverse verification that consumers read the REBUILT `.d.ts`: a scratch
console file assigning `tier: 'json'` to a `ManifestComponent` and to a
`PublicComponentConfig` — `tsc --noEmit` exit 2, `TS2322: Type '"json"'
is not assignable to type '"html"'` and `… to type '"html" | "public" |
"internal" | undefined'`; the `'html'` assignments compiled. Probe
removed; `git status` clean.

## H3 — the regenerated artefacts

Generated with the repo's own generator (`buildArtifacts` in
`packages/sdui-parser/scripts/gen-manifest.ts`) over the console
registration graph with the 16 plugin modules eager-imported in
`manifest-dump.tsx` order, once on the base tree and once on head:

| artefact | base | head |
|---|---|---|
| `sdui.manifest.json` components | 59 | **106** (+47 added, 0 removed,
0 curated entries changed) |
| `div` / `code` / `kbd` present | no | no |
| entries with `tier: 'html'` / without | 0 / 59 | 47 / 59 |
| `sdui-intrinsics.d.ts` intrinsic entries | 59 | 106 (+253 lines, −0) |
| `sdui-blocks.md` | 65 lines, one table | 119 lines: `# SDUI public
blocks (59)` unchanged + `## html tier intrinsic elements (47)` |

Sample head entries: `a` → inputs className, href, target, rel, title,
children (slot); `br` → className only (void, no slot); `label` → text
(required), className (no slot). The base run's manifest differs from
objectstack's tracked `sdui.manifest.json` only because the pin f8a9d0f
predates this base (`cmp` differs at byte 1183) — the regeneration at
the next pin bump is the `domain:cli` lane's step.

## Pins — red on base, green on head

Two mutation legs (implementation files checked out at base, tests at
head; trap-guarded restores to `HEAD`, proven by `git diff HEAD` empty
and blob-hash equality — "RESTORE PROVEN" printed twice):

- Leg 1 — `Registry.ts`, sdui-parser `index.ts` / `types.ts` /
`codegen.ts`, `react-page.tsx` at base: `Test Files 5 failed (5)`,
`Tests 17 failed | 61 passed (78)` — every behavioural pin red (the
roster-only pins stay green by design).
- Leg 2 — `react-page.tsx` alone at base: `Tests 1 failed | 8 passed
(9)` — `a PascalCased html-tier tag is an unknown identifier on this
tier` red (an `Img` wrapper was injected).
- Green on head 5bd6270 (round 1) after the restores: `Test Files 9
passed (9)`, `Tests 114 passed (114)` over the five pin files plus
`registration-uniqueness.test.tsx` (unchanged, green),
`public-tier.test.ts`, `tier.test.ts`, `public-contract.test.ts`.

Round 2 (head 6417e8d): the three re-quoted test files plus
`react-page-scope` and `html-tier-manifest` re-run — `public-contract`,
`container-declaration-census`, `layout-containers-declare-containment`,
`react-page-scope`, `html-tier-manifest` — `Test Files 5 passed (5)`,
`Tests 89 passed (89)`.

Broader affected suites at head 5bd6270: console `html-tier-manifest`,
`public-contract`, `public-block-binding-reach`,
`record-block-record-reach`; components `react-page-scope`,
`container-declaration-census`, `container-declaration-ratchet`,
`layout-containers-declare-containment`, `registration-uniqueness`;
plugin-tree `registration.publicTier-10064` — `Test Files 10 passed`,
`Tests 137 passed` (plus the scratch generator run).

## Gates (exit captured before any pipe; verdict lines quoted)

- Build: `turbo run build --filter="@object-ui/console^..."
--concurrency=2` under the verify lock — `Tasks: 34 successful, 34
total`, `VERDICT command-exit 0`.
- Type-check (`pnpm run type-check`, script echoed): `@object-ui/core`
exit 0, `@object-ui/sdui-parser` exit 0, `@object-ui/components` exit 0,
`@object-ui/console` exit 0. `--listFiles` counts the new / edited test
files inside their programs (1, 1, 2, 1).
- Lint, CI-parity: `turbo run lint` over the four packages — `Tasks: 5
successful, 5 total`, 0 errors; eslint `--format json` over the 14
touched files: 0 errors, 22 warnings, all pre-existing `no-explicit-any`
lines not introduced here.
- `check:control-bytes` — `✅ check-control-bytes: OK (scanned 8928
tracked text file(s))`.
- `check:new-line-citations` — `VERDICT new-cross-file-line-citations: 0
new citation(s)`.
- `check-changeset-presence` — round 2: `✅ 15 source file(s) of 4
released package(s) changed, and this change declares 1 changeset(s)`;
`changeset:check` — `✅ No changeset declares a major bump` and `✅ All
workspace packages are in the changeset fixed group`.
- Round 2 text gates on head 6417e8d: `check:control-bytes` `✅ OK
(scanned 8928 tracked text file(s))`; `check:new-line-citations`
`VERDICT new-cross-file-line-citations: 0 new citation(s)`;
`check-changeset-claims` exit 0 (2 bodies judged, no negation; the
re-read list is unchanged); `check-changeset-overwrite` exit 0
report-only — `1 changeset(s) added, 1 modified, 0 deleted`, the
modified one being `.changeset/6740-flex-is-container.md` (the dated
note above); `check-doc-links` — `Links are valid across 17 scan roots.`
- `check:unreferenced-sources` OK; `check:pending-changeset-literals` `✅
No test source names a pending changeset`; `check:test-path-roots` OK;
`check:doc-types` `✅ Every documented component type is registered`;
`check:doc-fences` OK; `check:registry-bare-names` `✓ no new bare-name
collision`; `check:esm-specifiers` (specifier leg) OK;
`check:component-surface-parity` report-only exit 0.
- `check:changeset-claims` (report-only): 13 pending changesets name a
file this change touches; each re-read — the "forwards exactly seven
keys per input" claims stay true (this adds a component-level key, no
input key), and "none of the 8 is in the curated public contract" stays
true (the seven now enter the manifest as html-tier entries, not
curated).
- NOT MEASURED locally, declared to CI: `check:readme-exports` (exit 1 =
8 self-imports unjudgeable because `packages/cli` and
`packages/plugin-ai` are outside the built closure; 538 judged, 0
fabricated), `check:doc-snippets` (exit 2 PRECONDITION NOT MET, same
unbuilt packages; the two edited guides gained prose only),
`check:eager-closure` and `check:sdui-registration-pins` (need a console
build), repo-wide `pnpm lint` (per-package eslint via turbo covered the
four touched packages exactly as CI runs them; `eslint.config.js`
declares no type-aware `project`, so this diff moves no untouched file's
verdict).

## Serial

Claim read: no open PR touches `gen-manifest.ts`, `public-blocks.ts` or
`html-elements.tsx`. Round 1: `origin/main` fetched into the private ref
`refs/issue-10735/main` (01700dc, four commits past base), `git
merge-tree --write-tree HEAD refs/issue-10735/main` exit 0 (tree
368f01d). Round 2, before the push of 6417e8d: main re-fetched at
704e05b (nine commits past base: objectstack-ai#10741, objectstack-ai#10574, objectstack-ai#10736, objectstack-ai#10745,
objectstack-ai#10749, objectstack-ai#10734, objectstack-ai#10733, objectstack-ai#10744, objectstack-ai#10752), `merge-tree --write-tree` exit
0 (tree 0d9bcb9); the set of files main moved since base is disjoint
from this branch's files (`comm -12` empty), so no merge commit was
needed.

## Acceptance notes

- **`div` on the html tier is now split across two faces.** The
published contract refuses it (`forbidden-tag`), while the renderer's
declaration still reads `deprecated: { surfaces: ['json'] }`
(objectui#4000) and the console's html-tier compile (`page.tsx`,
`getKnownTypes()`) still accepts and renders it. The ruling's item 3
parks the renderer-side retirement in these notes: the reconciliation is
a separate card (declare the `html` surface on `div`'s deprecation with
`box` as the replacement, and decide whether the console compile should
read the same roster).
- **"Refused with `box` named as the replacement" has no mechanism
today.** The gate's refusal is the parser's generic `forbidden-tag` text
(the tag is not an allowed component); nothing in the manifest or the
parser names `box` for an absent `div`. `Registry.deprecationFor('div',
'json').replacement` names it, but the gate never reaches the registry.
Not implemented here (a new contract shape); raised in the report's open
questions.
- **`code`:** the `html-elements.tsx` header and ADR-0081 §2 both say
`code` is "registered elsewhere"; the only registration is
`field:code`'s bare fallback. On an html page today `code` resolves to
that widget and drops its text — a reproducible runtime defect
independent of this card; reported as an out-of-scope finding.
- **`label` and `table` are declared as registered**, so the HTML-shaped
`label` with text children draws `missing-required-prop` (`text`) and
`not-a-container`, and a `table` authored with element children draws
`not-a-container`; the registered spellings (`text="…"`, `columns` /
`data`) are clean. Pinned.
- **objectstack side, for the `domain:cli` lane:** regenerate
`sdui.manifest.json` at the pin; port
`packages/sdui-parser/src/{index,types,codegen}.ts` to the lockstep copy
so the marker reaches the tracked file; the ledger's 200 `a` / `div`
rows go stale (the `a` rows) or stay refused (the `div` rows, to be
rewritten to `box` as the ruling says).
- `semantic.tsx`'s comment ("none of these seven tags is in the curated
`PUBLIC_BLOCKS` contract, so there is no injected identifier") remains
literally true and its conclusion holds on two grounds now; not edited
(no registration change).

## Pending changesets re-read (round 2, head 6417e8d)

The `changeset-claim-re-read` bot lists 12 pending changesets naming
files this diff touches (13 in round 1, `6740` now being one this change
modifies). Each re-read paragraph by paragraph against head:

- `5905-componentinput-inputtype-tombstone.md` — still true:
`packages/sdui-parser/src/index.ts` still forwards exactly seven keys
per input (`name`, `type`, `of`, `required`, `enum`, `binding`,
`description`); this change's `tier` write sits at component level,
beside `type` / `namespace` / `isContainer`, and adds no per-input key.
- `5905-componentinput-retire-constraint-keys.md` — still true, same
seven-keys-per-input claim, same reason.
- `7493-componentinput-retire-label-defaultvalue-advanced.md` — still
true, same seven-keys-per-input claim; `min` / `max` / `step` /
`placeholder` / `label` / `defaultValue` / `advanced` still reach no
manifest.
- `6067-component-meta-derive-from-canonical.md` — still true: it
describes the pre-6067 shape of `Registry.ts` in the past tense; at head
`ComponentMeta` is still `CanonicalComponentMeta &
RegistryComponentMetaExtras`, and `RegistryComponentMetaExtras.tier` is
unchanged (`'public' | 'internal'`). The `'html'` value lives only on
the `PublicComponentConfig` projection.
- `componentinput-reexport-4972.md` — still true: `Registry.ts` still
re-exports `ComponentInput` from `@object-ui/types` (`export type {
ComponentInput } from '@object-ui/types'`).
- `default-children-retired-5051.md` — still true: none of the three
`ComponentMeta` declarations offers `defaultChildren`; this change adds
no `ComponentMeta` key.
- `6764-container-declaration-census.md` — still true as written:
`react-page.tsx` drops containers from the react scope and reads
`getPublicConfigs()`, and none of the 8 is in the CURATED public
contract. The seven sectioning tags now ride that read as html-tier
entries, stamped `tier: 'html'` and skipped, which leaves the claim's
operative words ("curated public contract") intact; not corrected.
- `9280-record-highlights-entry-icon-retired.md` — still true:
`gen-manifest.ts` still serialises registry `inputs` into
`sdui.manifest.json` and `sdui-intrinsics.d.ts` (the header now also
names the html tier's stamped entries; behaviour unchanged).
- `components-react-page-published-dts-text-5666.md` — still true:
`react-pages.md`'s injected-scope note and §Styling still say plain HTML
with inline `style`, not Tailwind; the guide's edited sentence quotes
the skip line and adds "and every html-tier entry", nothing about
styling.
- `page-source-tailwind-framing-5461.md` — still true: `react-page.tsx`
keeps both styling comments (the header note and the
`buildComponentScope` note); this change adds a comment inside the loop
and removes none.
- `page-source-tailwind-prose-retraction-5469.md` — still true:
`react-pages.md` §Styling wording is unchanged.
- `palette-canonical-discussion-5495.md` — still true:
`public-blocks.ts` still records `record:chatter` as the same block
"under a Salesforce-familiar name"; this change adds a header paragraph
and touches no roster entry.
- `6740-flex-is-container.md` — CORRECTED (prose-only): its paragraph
quotes the react-page skip line as `if (!tag || cfg.isContainer)
continue;`, which this PR changed; a dated note in the objectui#10533
form (`⚠️ **Dated note, 2026-09-27 — the skip line quoted above has
since gained a third arm — objectui#10735.**`) records the current line
and keeps the entry as the reading of that change. Frontmatter
byte-identical, md5 ce888ebde4729564f67bca69a7928743 before and after.
`check-changeset-overwrite` reports the modification (report-only, exit
0).

---
_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

1 participant