Skip to content

feat(app-shell): register the ref:dataset spec-form widget, fed by the report inspector's dataset catalog (objectui#11601) - #11611

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11601-ref-dataset-widget
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11601-ref-dataset-widget

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11601
Clause-②: yes (widening). SchemaForm is exported from packages/app-shell/src/index.ts, and its widgetContext prop type WidgetContext gains one optional member, datasets. Measured on the built dist: index.d.ts exports SchemaForm, SchemaForm.d.ts declares widgetContext?: WidgetContext, and widgets.d.ts now declares datasets?: LoadState of DatasetCatalogEntry[] beside objectNames (the positive control). The member is additive and optional, so no code that type-checked before stops type-checking. The claim's no counted the index's metadata-admin exports as the page components and did not count the SchemaForm re-export. The changeset is minor for the same reason.

What this does

ref:dataset is a widget hint the spec already declares (the report form's top-level dataset), but WIDGETS had no renderer for it. resolveFieldFace therefore sent a field declaring it to the announced raw-JSON face: a JSON textarea under "widget ref:dataset — falling back to JSON until a custom renderer is registered." This PR registers that renderer.

  • RefDatasetWidget in widgets.tsx, registered in WIDGETS as ref:dataset and declared 'control' in WIDGET_LABELLING. It reads the catalog from a new optional WidgetContext.datasets (a LoadState of the catalog entries), the same way ref:object and view-ref take theirs. It handles the four catalog states as RefObjectWidget does:
    • failed: the shared PickerLoadFailure notice (ref-dataset-load-failed) beside a text box that stays editable;
    • loading: the stored name in a disabled box;
    • idle (no host feeds a catalog), or a completed load with no datasets: a labelled text input that writes the typed name. Never the raw-JSON face;
    • loaded: a Select over datasetSelectOptions, which shows LABEL (name) and then — description (the objectui#11161 author text). It writes the dataset name. A stored name that the catalog does not offer stays as its own option, flagged through engine.form.flaggedValue and engine.form.notFound, so it is not blanked and does not look like an offered dataset.
  • inspectors/dataset-picker-options.ts (new, pure): labelBesideName and datasetPickerOptions moved here unchanged from ReportDefaultInspector.tsx, plus datasetSelectOptions, the select-item text form the inspector already built inline. Importing them from the inspector would have created a cycle: the inspector renders SchemaForm, which imports the widget registry. The inspector re-exports datasetPickerOptions for DashboardWidgetInspector, which imports it from there. There is one definition.
  • ReportDefaultInspector passes its existing useDatasetCatalog result to the spec form as widgetContext.datasets, built with mapLoaded(rosterFrom(...)) so a failure is read before loading, as for its own pickers. conditionScope is required on every WidgetContext. This PR states 'none', which is the report row of CONDITION_SCOPE_BY_METADATA_TYPE. The bundled ReportSchema has no key with a predicate-style name (counted over its derived JSON Schema: 73 property paths, none matched), so the report form's existing fields render unchanged.
  • No new i18n strings. Every copy is an existing engine.form.* key.

Not an inspector-local override, and this PR alone does not end the joined-block symptom

The "Joined blocks" row spec belongs to objectstack (reportForm), and it is the only source of truth for which control a block's dataset gets. This PR does not patch that row in the inspector. The bundled @objectstack/spec row is still { field: 'dataset', label: 'Dataset' }, so a joined report's block dataset still renders as free text until objectstack-ai/objectstack#21714 declares widget: 'ref:dataset' and required: true on the row and that declaration reaches this inspector: through objectstack's .objectui-sha pin moving past this PR, and, for this repository's own console, through an @objectstack/spec release that carries objectstack-ai/objectstack#21714. This PR is the widget that row will resolve to. Read alone, it does not end the joined-block symptom on the card. That half belongs to objectstack-ai/objectstack#21714.

Every other SchemaForm host leaves datasets absent. A field declaring ref:dataset there now renders the labelled text input where it rendered the raw-JSON face before. This is pinned below.

SchemaForm.tsx is not edited. Both repeater layouts already route a row field's widget through resolveFieldWidget: the grid cell passes fieldSpec: s, and the card row goes through FieldRow. The pins below exercise both layouts.

Tests

Measured at 9ebe77b, the final commit, unless noted.

  • Reproduced first (red on b92329c, before any edit): pnpm exec vitest run packages/app-shell/src/views/metadata-admin/SchemaForm.refDatasetWidget-11601.test.tsx gave Tests 9 failed (9). Every case found the announced fallback notice and a textarea, which is the raw-JSON face.
  • New pins:
    • SchemaForm.refDatasetWidget-11601.test.tsx (9 cases): with a catalog, the picker in a section and in a repeater row (grid and card) writes the dataset name, the options are the author text, and an out-of-catalog stored name stays visible and flagged. With no catalog (no WidgetContext, or one without datasets), the field degrades to a text input named by its label, in a section and in both repeater layouts, writes the typed name, and shows no raw-JSON face.
    • inspectors/ReportDefaultInspector.blockDatasetPicker-11601.test.tsx (2 cases): the bundled reportForm is mocked with exactly the spec(ui): reportForm's "Joined blocks" row offers a block's dataset as free text with no required, so Studio saves the block #21702 now refuses objectstack#21714 declaration added to the joined-blocks dataset row. The fixture throws unless it declares exactly one row. The inspector's catalog then reaches the block row's picker, and picking writes { ...draft, blocks: [{ name: 'won_deals', dataset: 'support_load' }] } with nothing else changed.
  • Registry pins updated (each derives its population from WIDGET_LABELLING, so adding the widget requires a row in each): SchemaForm.widgetLabelling.test.tsx (a loaded case and a no-catalog case), SchemaForm.controlWidgetGridNaming-9889.test.tsx (the grid cell is named by the column header), and __tests__/SchemaForm.controlWidgetFailureArmNaming-9931.test.tsx (the failure arm keeps a labelable control in both layouts, the banner ledger reads ref-dataset-load-failed, and the loaded leg shows no banner).
  • Scoped suite at b58a18b (the source commit; 9ebe77b only edits the changeset): pnpm exec vitest run --maxWorkers=2 over packages/app-shell/src/views/metadata-admin/ plus every test that imports the three edited source files from outside it, or reads them as source text: spec-symbol-parity, the four ReportConfigPanel / ReportView host tests, view-filter-fold.ratchet (reads widgets.tsx), packages/core column-identity.ratchet (scans widgets.tsx / SchemaForm.tsx), and packages/i18n report-editor-retired-4145. Result: Test Files 389 passed (389), Tests 4550 passed | 1 skipped (4551). Re-run at 9ebe77b for the two ratchets and the two new pin files: Test Files 4 passed (4), Tests 27 passed (27). The narrowing selected tests by import and by source-text read. Tests that reach these files only transitively (for example the StudioDesignSurface suites) were not run locally. CI's pnpm test runs them.
  • pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build passed, then pnpm --filter @object-ui/app-shell type-check passed (tsc --noEmit && tsc -p tsconfig.test.json). --listFilesOnly on the test project lists all five touched test files and the new module.

Ablations (each one ran on committed b58a18b through ablation-replace.mjs, which mutates through an anchor that must hit and restores by blob hash)

  1. Delete the WIDGETS entry (anchor 1 to 0, blob fbfd4a2d90af to 83ddbf6c965c): 17 failed and 138 passed across the two new pin files and the three registry pins. Every picker pin and every degrade pin failed, because the field fell back to the raw-JSON face. The registry gates failed ("declares exactly the registered keys", "probes every registered key"), and so did the 9889 and 9931 rows for ref:dataset. Restored: blob fbfd4a2d90af equals HEAD, and git diff HEAD is empty.
  2. Remove the catalog from the inspector's context (anchor 1 to 0, blob 8a6b57240b1d to 76637674f521): both inspector picker pins failed (Unable to find … role "combobox" and name "Dataset"), and the block row rendered a textbox named "Dataset", which is the degrade face. All 9 SchemaForm pins, including the no-catalog degrade pins, stayed green. Restored: blob 8a6b57240b1d equals HEAD, and git diff HEAD is empty.

Gates

All exited 0 at 9ebe77b unless noted: pnpm check:control-bytes, check:new-line-citations (0 new citations), check:changeset-claims, check:pending-changeset-literals, node scripts/check-changeset-presence.mjs, pnpm changeset:check. These also exited 0 at b58a18b: check:icon-record-names (it reads widgets.tsx), check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape (the new test adds a vi.mock), check:test-path-roots, check:unreferenced-sources (the new module), check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, check:i18n-designer-parity, check:designer-field-key-parity, check:handler-key-reads, check:metadata-write-doors, check:spec-symbols, check:self-import, check:esm-specifiers, check:phantom-deps, check:registry-bare-names, check:lint-rule-coverage.

ESLint (--no-inline-config) over the eight touched .ts/.tsx files: 8 files linted, 0 errors. Warning counts are unchanged from the base for the two edited source files (3 and 3, 37 and 37). The new files have none. Type-aware linting is not enabled (tseslint.configs.recommended, with no project or projectService), so this diff cannot change the lint verdict of any file it does not touch. CI's pnpm lint runs the repo-wide scan.

NOT MEASURED: check:eager-locale-catalogues and check:sdui-registration-pins. Reason: both read a built apps/console bundle (PREREQUISITE NOT MET), and a console build was not run locally. The same applies to check:eager-closure. CI runs them.

Acceptance notes

  • RowCell, the cell renderer inside master-detail rows in widgets.tsx, special-cases only ref:object and ref:component. A ref:dataset column inside a master-detail widget would therefore not render the picker. No producer declares that shape (the joined-blocks row is a repeater, which this PR covers), so this is noted here and not filed.
  • Docs: no content/docs page or package README lists the metadata-admin widget hints, so there is no doc to update.

Generated by Claude Code

claude added 2 commits October 4, 2026 10:41
…eport inspector's dataset catalog (objectui#11601)

A field declaring `widget: 'ref:dataset'` resolved to the raw-JSON face
because WIDGETS had no entry for it. RefDatasetWidget renders the dataset
picker over a new optional WidgetContext `datasets` catalog, writes the
dataset name, and degrades to a labelled text input when no host feeds a
catalog. The report inspector passes its useDatasetCatalog result to the
spec form; datasetPickerOptions moves to a pure shared module so the
widget registry can import it without a cycle.

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
… widening of SchemaForm's widgetContext (objectui#11601)

SchemaForm is exported from the package index and its widgetContext prop
type gains an optional `datasets` member, measured on the built dist. An
additive published widening reads as minor.

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3312.8 KB 3330.4 KB
Main entry chunk (gzip) 151.4 KB 350 KB
Entry file index-D6gmf3ut.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) 17.22KB 6.37KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
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.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
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.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
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.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 572.97KB 137.34KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 232.57KB 64.51KB
fields (index.js) 261.62KB 66.20KB
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.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 39.47KB 11.25KB
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.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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.04KB 3.92KB
plugin-calendar (index.js) 53.17KB 15.46KB
plugin-charts (index.js) 83.60KB 22.89KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 142.89KB 38.62KB
plugin-designer (index.js) 231.41KB 48.84KB
plugin-detail (index.js) 245.74KB 64.59KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 175.74KB 45.38KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 235.63KB 64.77KB
plugin-kanban (index.js) 49.59KB 15.58KB
plugin-list (index.js) 116.46KB 28.93KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.80KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
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.81KB 3.58KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 5.07KB 2.39KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
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: 9ebe77bf9b05ec7657bcbd22de8e53a6ac68ac79
Local-runs: none

Written 2026-10-04T11:35Z by the seat's at-tier review subagent, against GitHub and objectui's tree by ref (git show 9ebe77bf9...:PATH, no checkout): card objectui#11601 (body; triage 5978719353; claim 5979035511; dev report 5979374160; seat review 5979387934), PR objectui#11611 (body, 9-file list, its one bot comment, the net diff from merge-base b92329c8 to the head), and the head's check-runs.

Check-runs on the head. First read 2026-10-04T11:20Z: 42 runs, 29 success, 3 skipped, 10 in progress (Lint, Spec Main Shape Gate, Test (shard 1/8) to Test (shard 8/8)), 0 failed. Second read 2026-10-04T11:29Z: 43 runs, 40 success, 3 skipped, 0 in progress, 0 failed. The three skips are the expected ones: Test (coverage), its shard matrix, dependabot. Every changeset and declaration run is success: Changeset Declaration, Changeset Claim Re-read, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Overwrite Report, Governed Surface Queue Guard; so is Type Check, Build & E2E, Test (dist pins), Bundle Analysis (eager closure 3312.8 KB of 3330.4 KB, PASS). The PR's only bot comment is the performance-budget and bundle-size report; it re-reads no pending changeset, so no paragraph is named to judge. The claim re-read gate posted nothing, which is its "nothing to re-read" shape. Final read before this record was written: see the line below the verdict.

① Derived judgments

  1. WidgetContext.datasets, an optional LoadState of DatasetCatalogEntry[] — RIGHT, and it IS a published-surface change. packages/app-shell/src/index.ts has export { SchemaForm } from './views/metadata-admin/SchemaForm.js';, and SchemaFormProps declares widgetContext?: WidgetContext, so every member of WidgetContext is on the published type surface through that prop. The member sits beside objectNames?, objectFields?, objectViews?, objectActions?, which have the same shape (an optional LoadState catalog). DatasetCatalogEntry is declared in previews/useDatasetCatalog.ts and is NOT name-exported from the index; neither are WidgetContext, LoadState, ObjectFieldOption or ObjectViewOption, so the new member follows the existing precedent exactly: reachable structurally (the prop type of the exported component), not by name. Declaration emit: the package builds with a bare tsc ("build": "tsc && node ../../scripts/check-dist-completeness.mjs", declaration: true, module: nodenext), so widgets.d.ts's import type { DatasetCatalogEntry } from './previews/useDatasetCatalog.js' resolves to a .d.ts emitted in the same tree; Type Check, Build & E2E and Test (dist pins) are success on the head.

  2. The ref:dataset face moves from the raw-JSON fallback to a picker or a labelled text input for every SchemaForm host — RIGHT; no host and no stored document is hurt. resolveFieldFace sends a hint that is in WIDGETS to registered; before this head ref:dataset was in neither WIDGETS nor KNOWN_PASSTHROUGH_WIDGETS, so it fell to raw-json with the engine.form.fallbackJson notice (the new pin file reproduced that red at b92329c8, 9 of 9). Which hosts render such a field today: the bundled @objectstack/spec 17.6.0 (pnpm-lock.yaml) reportForm declares widget: 'ref:dataset' on the top-level dataset only. ReportDefaultInspector prunes dataset (FORM_FIELDS_OWNED_ELSEWHERE in report-schema.ts, and again in its hiddenFields), so nothing there renders the widget until objectstack#21714's row declaration arrives. ResourceEditPage, the generic editor, renders entry.form (the server-served RichMetadataTypeEntry.form) with a context that has no datasets, so the report's top-level dataset moves there from the JSON textarea to the labelled text input. Stored shape: RawJsonEditor wrote JSON.parse(text) (any JSON value; undefined on a blank box; an unquoted bare name was a parse error that wrote nothing), the text input writes e.target.value || undefined, the picker writes next || undefined: a string or undefined in both new arms. ReportSchema.dataset and JoinedReportBlockSchema.dataset are both SnakeCaseIdentifierSchema.optional(), a string, so the new faces write only what the schema accepts, where the JSON face could also write a number or an object the schema refuses. A stored string displays identically in every face (String(value)). No doc lists the metadata-admin hints (content/docs and the package READMEs carry none of ref:object, ref:component, view-ref, field-ref), so the body's "no doc to update" holds.

  3. The four arms — RIGHT, and in RefObjectWidget's order. RefDatasetWidget reads context?.datasets ?? NOT_ASKED; status === 'error' first: PickerLoadFailure with testId="ref-dataset-load-failed" beside an enabled Input that still writes; then isLoading: a disabled Input holding the stored name with the engine.form.loadingOptions placeholder; then offeredOptions(datasetsState, NO_DATASETS) through datasetSelectOptions, and options.length === 0 (the idle arm, i.e. no host fed a catalog, or a completed load with no datasets) returns the same labelled Input; otherwise a Select whose trigger carries controlNaming({ id, ariaLabelledBy, required }), with a stored value the catalog does not offer kept as its own SelectItem worded through engine.form.flaggedValue and engine.form.notFound (en: (not found), zh: (未找到)) so it neither blanks the trigger nor reads as offered. Compared with RefObjectWidget: same arm order, same single labelable control per arm, same offeredOptions discipline; the two differences are that ref:object's empty arm carries a noObjects placeholder and has no not-found row, while ref:dataset's empty arm is a bare labelled input and its loaded arm keeps the stored value flagged, which is the inspector's own InspectorSelectField convention. Every string is an existing engine.form.* key in metadata-admin/i18n.ts, in both locales; the body's "no new i18n strings" holds. The 9931 pin's banner: 'ref-dataset-load-failed' row and the 9889 grid-naming row read these arms off the DOM.

  4. ReportDefaultInspector now passes a widgetContext with conditionScope: 'none' where it passed none — RIGHT; no other field of the report form changes. conditionScope is a required member of WidgetContext, so a value had to be stated, and report: 'none' is the ruled row of CONDITION_SCOPE_BY_METADATA_TYPE. The only consumer that behaves differently between "no context" and 'none' is ConditionWidget: with context?.conditionScope undefined it renders ConditionBuilder with no scope, with 'none' it renders the plain Input. A field reaches ConditionWidget only through detectConditionWidget (name in visible, hidden, disabled, visibleOn, condition, predicate, or ending in When, string-admitting, no enum) or an explicit widget: 'condition'. The form this inspector renders after getReportForm()'s prune has the fields label, description, type, order (by, direction), drilldown, blocks (name, label, description, type, dataset, rows, columns, values, runtimeFilter, order), runtimeFilter (widget: 'json') and the hidden chart; ReportSchema's own keys add protection and the _lockReason, _packageId, _packageVersion, _lockDocsUrl protection fields. None matches the detector, and the form's visibleWhen strings are section and field predicates SchemaForm evaluates itself, not fields. The other context readers are unchanged between the two states: detectFieldRefWidget returns undefined whenever widgetContext?.objectFields is absent, and ref:object, object-selector, field-ref, view-ref, action-multi, dynamic-config each read their own absent member as NOT_ASKED or undefined exactly as with no context. My reading is by key names of report.form.ts and report.zod.ts; I did not re-run the dev's 73-path count.

  5. The moved datasetPickerOptions and labelBesideName, and the new datasetSelectOptions — RIGHT; one definition each, and the inspector's select text is byte-identical. The diff removes both function bodies from ReportDefaultInspector.tsx and adds them to inspectors/dataset-picker-options.ts with identical bodies; labelBesideName was module-private and is now exported from that internal module (not on the index). ReportDefaultInspector keeps export { datasetPickerOptions };, and DashboardWidgetInspector still imports datasetPickerOptions from ./ReportDefaultInspector.js, untouched. datasetSelectOptions maps datasetPickerOptions(datasets) to { value: o.value, label: o.hint ? LABEL-DASH-HINT : o.label }, where LABEL-DASH-HINT is the template o.label, a spaced em dash, o.hint; that is the removed inline body of the inspector's datasetOptions memo, template and ternary unchanged, so the select items read the same bytes as before. The cycle the move avoids is real: ReportDefaultInspector imports ../SchemaForm.js, SchemaForm imports ./widgets.js, so widgets.tsx importing the inspector would close it. The catalog hand-off, mapLoaded over rosterFrom({ loading, error }) with a selector returning catalog.datasets, reads failure before loading and projects useDatasetCatalog's { datasets, loading, error } onto the LoadState the widget expects; with datasetCatalogOverride the hook reports loaded at once, which is what the new inspector pin relies on.

② Semver level

  • Clause-②: yes (widening) is the right declaration, for the reason in ①.1: an optional member added to a type reachable from a re-exported component's prop widens the published type surface. The brief's own rule applies: yes takes at least minor, and the changeset is '@object-ui/app-shell': minor. The two agree. The claim's no was wrong on a false premise (it counted the index's metadata-admin exports as the page components only); the PR body states the correction in its own words on line 2.
  • Nothing is narrowed. No export is removed or renamed; datasetPickerOptions stays importable from the inspector module; the eight publish-contract fields of package.json are untouched (exports is . and ./styles.css, as before). Replacing the raw-JSON face by a string input for ref:dataset narrows what the control can WRITE to the schema's own accept set (a string); the accept set itself is unchanged, so it is not a narrowing of a published accept set.
  • The PR body line 2 begins Clause-②: yes (widening). at line start, with the measurement following on the same line, and line 1 is Fixes #11601 with no other closing keyword. No changeset or declaration check-run on the head failed; all six named above are success. None of objectui's changeset gates parses the line's form (check-changeset-no-major.mjs reads changeset front matter; the fleet's one reader of the line, clause2-line.mjs, lives in objectstack), so the form is judged here by reading: one of the two legal values, one arm from the closed pair (widening)/(narrowing), at line start, prose after it on the same line, which is the shape the brief names as the one to accept.
  • The changeset text is true against the diff, paragraph by paragraph: the picker replaces the JSON fallback (①.2); label beside name and description after it (datasetSelectOptions, ①.5); writes the name (the onChange(next || undefined) arm); section and repeater row, grid and card (the 9-case pin file covers all four); out-of-catalog value kept and marked (not found) (①.3); loading shows the stored name (the disabled Input holds current); failure notice beside an editable box (①.3); no catalog gives a plain text input (①.3); the inspector supplies its catalog and the block row picks up the picker only once the spec's row declares ref:dataset (the inspector pin mocks exactly that declaration; the bundled 17.6.0 row is { field: 'dataset', label: 'Dataset' }); the existing dataset option list is unchanged (①.5); widgetContext gains one optional member and a host passing no datasets renders as before except for the ref:dataset face (①.1, ①.2). It names no model identifier. Practical effect on the fixed group is nil: @object-ui/types and @object-ui/plugin-form minors are already pending at this head (.changeset/11564-flex-bag-children-list.md, .changeset/11569-line-items-child-object-optional.md).

③ Boundary flags

  • The claim's wrong no — answered; nothing is owed on this head. The seat's review 5979387934 owns the error, states the false premise and the corrected reading ("the claim's export check has to follow prop types reachable from re-exported components, not only the names re-exported"), and the surfaces the gates read, PR body line 2 and the changeset, carry the right declaration. The claim comment itself still reads Clause-②: no; it is superseded on the same thread by the newer review and the dev report, and no gate reads it. Escalated to the seat as a process note, not a gate item: this is the second wrong Clause-② call this round by the seat's own account (after objectui#11569), so the corrected reading may deserve a home in the dispatch procedure; that is the seat's and the maintainer's call, outside this record.
  • The new module outside the claim's file surface — RIGHT, and declared. inspectors/dataset-picker-options.ts sits inside the claimed views/metadata-admin/ family, exists because the import would otherwise cycle (①.5), carries no React and no new behaviour (two moved functions plus the lifted inline map), and is named in the dev report's deviations and in the PR body. The claim had already left the catalog path open ("the dev measures which"). Not a scope expansion.
  • The three registry pins gaining a ref:dataset row each, and the 9931 header prose — RIGHT, and owed. SchemaForm.widgetLabelling.test.tsx, SchemaForm.controlWidgetGridNaming-9889.test.tsx and __tests__/SchemaForm.controlWidgetFailureArmNaming-9931.test.tsx each derive their population from WIDGET_LABELLING and assert that their case table equals the registered 'control' keys, so registering a 'control' widget without a row would red them; the rows are required, not padding, and the dev's ablation of the WIDGETS line turning the registry gates red is the expected shape. The 9931 header's count sentence was rewritten from a live count ("Six of the declared 'control' widgets have a catalog-failure arm") to a dated one ("When this card landed, six … (the banner column below is the live reading)"); that is truthful (with ref:dataset there are now seven such widgets: six non-null banner rows plus filter-builder's null-bannered popover arm, which the same header explains) and it points at the instrument rather than restating a number, which is objectui AGENTS.md commandment 完善设计器的每一个细节 #9's shape. The 9889 header's "The eleven" was likewise reduced to "The labelling: 'control' widgets", for the same reason.
  • The RowCell master-detail note — RIGHT as an acceptance note. RowCell special-cases schema.widget === 'ref:object' and 'ref:component' only, then falls to enum, boolean, number and a default text Input, so a ref:dataset column inside a master-detail widget would render the plain text box (never the raw-JSON face, since RowCell does not consult the registry). No producer declares a master-detail with such a column; the joined-blocks row is a repeater, which this PR covers through RepeaterField's grid cell and card FieldRow. Prime Directive 10 files a reproducible defect, a contract violation or an authoring trap and notes everything else in the acceptance notes; with zero pull this is the latter, and the dev report records it with carrier: none.
  • The joined-block symptom stays free text until objectstack#21714 lands and reaches objectui — stated truthfully, with no claim that this PR fixes it. The PR body: "Read alone, it does not end the joined-block symptom on the card. That half belongs to spec(ui): reportForm's "Joined blocks" row offers a block's dataset as free text with no required, so Studio saves the block #21702 now refuses objectstack#21714", and it names both routes the declaration takes to a user (objectstack's .objectui-sha moving past this PR; for objectui's own console, a spec release carrying #21714). The changeset: "Until objectui is built against a spec that carries that declaration, a block's dataset is still entered as free text." The inspector pin is honest about this too: it mocks getReportForm to add exactly the #21714 declaration and throws unless exactly one row was declared. One thing for the seat, not a gate item: line 1 Fixes #11601 closes the card on merge, which is consistent with the triage grade's scoping ("This card (objectui): register ref:dataset in WIDGETS … The objectstack half is spec(ui): reportForm's "Joined blocks" row offers a block's dataset as free text with no required, so Studio saves the block #21702 now refuses objectstack#21714"), while the card's title still names the user-visible symptom; the seat may want the card's closing word to point at #21714 as where that symptom closes.

Implemented-by: claude/issue-11601-ref-dataset-widget
Reviewed-by: session_01FjqrwXPfSMkSfkKYDSRkN2

VERDICT: PASS

Final check-run read before this record was written, 2026-10-04T11:33Z: 43 runs, 40 success, 3 skipped (Test (coverage), its shard matrix, dependabot), 0 in progress, 0 failed; the PR head was still 9ebe77bf9b05ec7657bcbd22de8e53a6ac68ac79, draft, mergeable_state: clean, labels tests, package: app-shell, needs:contract-review. The run that appeared between the first and second reads is the aggregate Test job, success.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

needs:contract-review removed: an at-tier PASS is on record at this head · domain:ui seat 1 · session_01FjqrwXPfSMkSfkKYDSRkN2 · 2026-10-04T11:35Z.

  • Record: 5979512912 on this PR: ## Contract review, Served-tier: CONTRACT_REVIEW_TIER, Head-sha: 9ebe77bf9b05ec7657bcbd22de8e53a6ac68ac79, Local-runs: none, Implemented-by: claude/issue-11601-ref-dataset-widget / Reviewed-by: session_01FjqrwXPfSMkSfkKYDSRkN2, PASS. It is the first render on this PR, and no FAIL item was found.
  • What it confirmed:
  • The reviewer's three notes, answered:
    1. The claim 5979035511 still reads Clause-②: no. It is superseded on the card by the seat's review 5979387934, which owns the error, and no gate reads it. The process lesson goes to the maintainer in this seat's round report.
    2. Fixes #11601 closes the card on merge, as triage scoped it, to the objectui half. The landing note on the card will name spec(ui): reportForm's "Joined blocks" row offers a block's dataset as free text with no required, so Studio saves the block #21702 now refuses objectstack#21714 as where the user-visible joined-block symptom closes.
    3. No objectui gate parses the Clause-② line's form. This is informational, and the form was judged by reading.
  • The reviewer's writes: none. It ran read-only, and the seat posted the record from its file.
  • Pre-landing checks on 9ebe77b:
    • 43 check runs: 40 success and 3 expected skips (dependabot, Test (coverage) and its shard matrix);
    • mergeable_state: clean;
    • check-governed-merges --pr objectstack-ai/objectui#11611: NOT governed, 578 changed lines, under the human-merge threshold.
  • Carried as Acceptance notes: the RowCell master-detail case (zero pull).
  • Landing in this act: ready, then auto-merge, so the PR enters the merge queue.

Generated by Claude Code

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

Projects

None yet

2 participants