Repository navigation
feat(types,core,data-objectstack)!: the three surviving record ids are strings (objectui#10078) - #10733
Conversation
…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
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
Red check that is not this PR's: The same check is red on Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ 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: (b) Reader-side (c) (d) Out-of-scope census of remaining Self-raised: (e) Blocking item: the changeset's first sentence "the last three record ids that still admitted a number are strings" is false on the published Implemented-by: VERDICT: FAIL Adopted by the 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
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ 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. Implemented-by: VERDICT: PASS Generated by Claude Code |
…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>
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/typesstaystring | number; see the changeset), under the PM seat's inheritance decision in claim5851634158(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
packages/types/src/views.tsCommentSearchResult.recordIdandRecordSubscription.recordId:string | numbertostringpackages/types/src/data.tsDataSourceMutationEvent.id:string | numbertostring, docblock names the rule and where the conversion livespackages/core/src/adapters/ValueDataSource.tsupdate/deleteemitid: String(id)packages/data-objectstack/src/index.tsupdate/deleteemitid: String(id), the same string already sent on the wirecontent/docs/plugins/plugin-detail.mdxRecordSubscriptionshape readsrecordId: stringcontent/docs/guide/data-source.mdfindOne/update/deleteid readsstring, matchingdata.ts(patch round 1)packages/types/src/__tests__/record-id-string-survivors-10078.test.ts.changeset/10078-record-id-string-survivors.mdminorfor types / core / data-objectstack, BREAKING banner, ADR-0087 disposition lineFile-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.mdxwas added in this stroke: it prints theRecordSubscriptionshape 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.mdwas added in patch round 1, granted by the PM addendum5852035357: its canonical-interface block printedid: string | number, which is false againstdata.tsonmain.Premise P — measured on
5c61e5241before any editP holds for all three keys, so all three ship together.
CommentSearchResult.recordId. Same sense asCommentEntry.recordId: its one producer,useCommentSearchin@object-ui/collaboration, copies it fromCommentEntry.recordId(alreadystringsince objectui#9511) with an empty-string default, so it has only ever produced a string. Readers: none in the tree beyond the type (useCommentSearchis exported, and nothing in the tree readsresults[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,RecordChatterPanelandRecordActivityTimelinetake the object and readsubscribedonly. No reader touchesrecordId.DataSourceMutationEvent.id. The id theupdate/deletedoors already take asstring. Producers:ValueDataSourceandObjectStackAdapter, whose ownupdate/deletestill declare the widerstring | numberparameter (allowed, because TypeScript compares method parameters bivariantly, as theDataSourcedocblock says) and forwarded it unchanged. No flow in the tree hands them a number: every caller that goes through theDataSourceinterface passes astring, and every test fixture uses string ids. The paths that COULD carry one areValueDataSource.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'sonMutationhandler anduseMutationInvalidationBridgereadevent.idand already pass it throughString(...);ObjectCalendar,ObjectKanban,ListViewanduseDataRefreshreadresourceonly. Neither of the two readers does arithmetic, a numeric comparison, atypeof === 'number'branch, or a numeric-keyed lookup.Mechanism hypotheses
string | numberat the cited places on5c61e5241. The card's older address forDataSourceMutationEventhad drifted, as H1 said.packages/types/src/zodnames none of them, noz.unionof string and number there sits at an id, andzod-mirror-parity.test.tsholds 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 0KnownDriftor mirror-parity ledger rows to shrink.onMutationthrough theDataSourcetype:@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 onDataSourceMutationEvent).plugin-list,plugin-kanbanandplugin-calendartype the handler's event asany, so the type does not reach them.ObjectStackAdapteremits eitherop.id(alreadystring) or the server echo's id (typedany); left as is, see Acceptance notes.Pin —
record-id-string-survivors-10078.test.tstsc -p packages/types/tsconfig.test.json(chained fromtype-check). There is an Equal row per member (the optionalidgoes through NonNullable first), a separate is-not-anyrow per member, a@ts-expect-errornumeric literal at each of the three, and the three string spellings beside them as controls.CommentEntry.recordId(already narrowed) asstring, which is the lit control.Patch round 1 — HEAD
cbe0bcee1This round answers contract review
5852032361(FAIL at2a74f2c26) and the PM note5852035357:content/docs/guide/data-source.md, the canonical-interface block's three id widths (findOne/update/delete) are aligned todata.ts.origin/mainc3a26ccdawas merged in by a merge commit.Readings at
cbe0bcee1:pnpm --filter @object-ui/types type-checkplusvitest run packages/types/: exit 0, 5290 tests.check:new-line-citations,check:control-bytes,check:spec-symbols, thecheck:doc-*gates,check:doc-snippets(676 of 676 blocks) andcheck-governed-queue-guard --test(NOT GOVERNED): exit 0 each.DataSourceMutationEvent.idreddens the pin (1 failed | 6 passed). It was restored, andgit diff HEADis empty.Verification — HEAD
2a74f2c26(round 0)Exit codes are captured before any pipe.
pnpm --filter @object-ui/types type-checkpnpm --filter @object-ui/core type-checkpnpm --filter @object-ui/data-objectstack type-checkpnpm exec vitest run packages/types/ packages/core/ packages/data-objectstack/ packages/collaboration/ packages/react/pnpm --filter @object-ui/collaboration type-checka1da85b60)pnpm --filter @object-ui/plugin-detail type-checka1da85b60)pnpm --filter @object-ui/react type-checka1da85b60)pnpm --filter @object-ui/plugin-view type-checka1da85b60)pnpm exec vitest run packages/plugin-detail/a1da85b60)pnpm exec vitest run packages/plugin-view/a1da85b60)node scripts/check-changeset-presence.mjsnode scripts/check-changeset-no-major.mjs/check-changeset-fixed.mjs/check-changeset-overwrite.mjsnode scripts/check-changeset-claims.mjsviews.ts/data.ts, all read, none falsifiedpnpm check:pending-changeset-literalspnpm check:new-line-citations0 new citation(s)pnpm check:control-bytespnpm check:spec-symbolspnpm check:test-path-rootspnpm type-check:coveragepnpm check:doc-types/check:doc-fences/check:doc-example-readersnode scripts/check-governed-queue-guard.mjs --test(the 7 paths)--no-inline-configon the 5 touched.tsfilesno-consolefar from the edit). The new pin has 0 errors and 3no-explicit-anywarnings, the same pattern as the objectui#9511 pin.The only commit after
a1da85b60adds the changeset.md, which no compiler or suite in the rows markeda1da85b60reads.Consumer readings, with direction: the dependency closure was built first (
pnpm --filter '@object-ui/plugin-detail^...' build, thenmobile,plugin-form,plugin-gridandplugin-detailfor plugin-view). The dist was confirmed to carryrecordId: stringandid?: string. Every downstream consumer of@object-ui/typesnamed 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 emptygit diff HEAD.RecordSubscription.recordIdback.tsc -p tsconfig.test.jsonexit 2, with TS2344 on the Equal row and TS2578 on the numeric@ts-expect-errorrow. The pin under vitest showed 1 failed and 6 passed, the failing row namingRecordSubscription.recordId. Direction: red, as predicted.String(...)fromValueDataSource.delete's emit. Coretsc --noEmitexit 2, TS2322 (string | numbernot assignable tostring | undefined).ObjectStackAdapter.delete. data-objectstacktsc --noEmitexit 2, TS2322.Acceptance notes
String(event.id)is now an identity for typed producers. Both were left in place:useMutationInvalidationBridgetakesdataSource: unknownand declares its own structural event type, so a plain-JS adapter is not checked by the compiler. Its test feedsid: 42and 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.update/deleteparameters staystring | number. TheDataSourcedocblock 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, typedany. The ObjectStack server's record id isz.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 theRecordSubscriptiondocblock already says. This is stale prose, noted and not changed.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 fencedplaintext, which is outside that gate'sts/tsxpopulation.@objectstack/spec.pnpm lint. CI owns it. The narrowing is stated in the ESLint row above: the population is the rooteslint.config.js, which has no type-aware parser options, and no custom rule ineslint-rules/reads another file, so this diff cannot move a verdict on an untouched file.Session:
https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYWGenerated by Claude Code