Skip to content

fix(ui): v1-honest start chips, assistant panel scroll, toast lifetime, and the remaining built-in aggregate captions - #7710

Merged
hotlong merged 1 commit into
mainfrom
fix/v1-start-chips-and-ui-fragments-1984
Sep 5, 2026
Merged

hotlong merged 1 commit into
mainfrom
fix/v1-start-chips-and-ui-fragments-1984

Conversation

@hotlong

@hotlong hotlong commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Six small user-visible fixes on the maker / assistant / dataset surfaces, batched into one PR because they are all in this repo and none is bigger than a file or two. Verified on 6129b3b0c.

References cloud#1984. Closes #7476, Closes #7480, Closes #7481, Closes #7482, Closes #7534.

⚠️ cloud needs a .objectui-sha bump before any of this reaches the cloud product — nothing here ships to a tenant environment until that pin moves.


cloud#1984 — the maker's start chips promised what v1 cannot deliver

The five chips on the environment home's "用 AI 搭建" are the product's own recommendations, and two of them asked for automation ADR-0112 v1 (cloud#1956 / PR #1970) does not author: the ticket chip said 「状态流转」, the inventory chip said 「低库存预警」. The measured behaviour was not a refusal — the model degraded them into a status kanban and a low-stock view — so the chip promised an alert and delivered a page.

All five reworded in all ten packs, plus the defaultValue fallbacks in metadataAssistantSuggestions(), which are a second copy of the same strings and would otherwise have kept the old promises on the surface with the least i18n. Each stays a real business scenario and asks only for objects, fields, views, pages, dashboards and sample data — the ticket chip now asks for a status field and a board grouped by it, the inventory chip for a view that filters items below their reorder point. A REVERT note sits beside the keys in every pack and at the call site, naming ADR-0112 v2.

Also checked but NOT changed, and filed instead as objectui#7709: the maker's EDIT-mode starter console.ai.suggestions.editApp.addAutomation (「加一个自动化 —— 审批、状态流转或通知」) is the same defect class one chip family over, but it has no v1-honest rewording that does not duplicate the three chips beside it — dropping it for v1 vs. replacing it with a different v1 capability is a product call, not a translation fix.

Verified: packages/i18n/src/__tests__/makerStartChips-v1-scope-1984.test.ts (32 cases) — all five keys present and non-empty in each of the ten packs, no chip matching that pack's OWN automation vocabulary (English-only scanning would have declared the five non-Latin packs clean without reading a character of them), and a non-vacuity control that re-runs each banned list against the wording that pack actually shipped before this PR and requires it to be flagged. Plus AiChatPage.startChips-1984.test.ts (4 cases) for the call-site copy, asserting byte-equality with the en pack.

objectui#7481 — Get authoring rules was unlocalized among Chinese step labels

get_authoring_rules (cloud#1837) had no chatbot.tool.* entry, so it fell through to humanizeToolName's English title-caser. Checking the other recently-added tool names found four more in the same state: load_tools, open_record, test_flow, toggle_flow. All five are registered by cloud service-ai-studio's plugin.ts but are newer than the pinned @objectstack/spec's PLATFORM_TOOLS_BY_PACKAGE snapshot. Labels added to all ten packs.

The root cause of the drift is upstream and filed as cloud#1995: the conformance test that exists to catch exactly this hand-lists four fewer definition groups than plugin.ts registers, so it asserts a stale list against an equally stale registry and stays green.

Verified: packages/plugin-chatbot/src/__tests__/toolLabels-locale-parity-7481.test.ts (17 cases). Coverage is registry ∪ AHEAD_OF_PIN, so it becomes a real ratchet the moment the pin advances; a separate case fails if a name is left in the hand-held list after the registry lists it, which is what keeps that list shrinking to zero rather than becoming a second registry. Also pins that the en labels are byte-equal to the title-caser (the packs' own stated rule, previously unchecked) and that an unknown/third-party tool still degrades to English.

objectui#7480 — the assistant rail did not follow the thread after a send

Worth stating because the card's premise reads as a code divergence and is not one: the rail and the full-page maker are the same component (ChatDock → ChatPane → ChatbotEnhanced → Conversation). What differs is width. StickToBottom auto-follows only while the view is at the bottom; in a ~360px rail a reply is two or three times taller than in the full-page column, so by the time the user has read it the lock is escaped and the next send appends off-screen.

Every send path now re-arms the lock via the library's own contextRef escape hatch — so the vendored ai-elements elements/conversation.tsx is not edited. That includes the plan card's "Build it" and the 确认修改 approval, whose own code comment already said "the approval's visible effect lands at the BOTTOM of the thread — outside the viewport when the plan card is scrolled into view". Message APPENDS deliberately do not scroll, so a user reading back through the thread while the answer streams is never yanked to the bottom.

Verified: ChatbotEnhanced.followOnSend-7480.test.tsx (4 cases) — a contextRef is handed to the conversation at all; a typed send and a start-chip send each scroll; and two successive streaming appends do not.

objectui#7482 — the success toast covered the send button and never dismissed

One defect, two symptoms. apps/console pinned the toaster to bottom-right, an override that predates ADR-0057 P3a and the corner it gave to the ChatDock composer and its FAB. So a toast covered the send button — and sonner pauses a toast's dismiss timer whenever the pointer is inside the toaster region:

if (expanded || interacting || isDocumentHidden) pauseTimer();
else startTimer();            // sonner 2.0.8, the Toast auto-close effect

expanded is set by that region's own onMouseEnter/onMouseMove, so a pointer resting on the composer underneath held the timer at zero. The 4s default was never wrong; it never got to run. The override is gone and the console takes ConsoleToaster's own documented top-right anchor.

On the second half of the ask — "errors may persist" — deliberately not done, and the reason is written on the duration: sonner's Toaster has no per-type duration, so saying it means a duration at each of the ~100 toast.error(...) call sites. closeButton already gives every toast a manual exit. Success toasts are at 4s, inside the 3–5s band the card asked for, and that is now pinned — nothing checked it before.

Verified: ConsoleToaster.autoDismiss-7482.test.tsx (3 cases: the default anchor is top-right, the override still works, and 「客户更新成功」 is present at 2.5s and gone by 5.5s) and consoleToasterAnchor.ratchet-7482.test.ts (3 cases) which reads apps/console/src/App.tsx from source and refuses a bottom-right re-anchor, with its own non-vacuity control.

objectui#7534 — built-in aggregate captions still printed the server's Count

buildDatasetFieldHelpers().headerLabel now takes an optional builtinAggregateLabels and resolves through resolveMeasureLabel first — the seam objectui#7535/#7258 already landed for buildChartSeries() — so there is ONE resolution order rather than two, and the five call sites pass it: the dashboard DatasetWidget (KPI caption + table/pivot headers), the three sites in DatasetReportRenderer (summary header, metric caption / chart measureLabel, matrix header), and app-shell's DatasetPreview. Omitting the argument reproduces the previous output byte for byte.

Verified: dataset-format.builtinAggregate-7534.test.ts (10 cases — the full resolution order, every member of the closed vocabulary, an author-declared measure kept verbatim, a field literally named count with no discriminator kept verbatim, an unrecognised discriminator, an unresolved and an empty label, and that omitting the argument changes nothing) plus DatasetWidget.builtinAggregateHeader-7534.test.tsx (5 cases) which renders the widget under a real I18nProvider and reads the KPI caption and the table header — the wiring half, which no pure test can see.

objectui#7476 — sys_activity 404 on every open

Premise partly retracted, and the retraction is why the change is small. "Handle the absence quietly" is already implemented, at four layers: the adapter memoizes the missing collection so no second request goes out, its createQuietHttpLogger demotes the 404 to console.debug, sharedUserFeeds retires the feed as an ANSWER (ready, not error, with a bounded 3-probe budget), and the panel renders its earned 「暂无最近动态」. None of that changes here. What was left is the one doomed request per page load, and data-objectstack states the rule for exactly that case in its own comment: "The cure for doomed requests is not issuing them, never hiding them once issued."

So new useObjectPresence reads the object registry the shell already loads for the nav (sys_* objects are in it where they exist — AppHeader filters them out of the app-object picker by name, and the console resolves /apps/{any app}/sys_activity as an ordinary object route). Absence has to be earned, because the two mistakes are not symmetric: a missed skip costs one request that already degrades correctly, while a wrong skip costs a real deployment its feed with no error anywhere. Only a registry that has ANSWERED and lists other objects without this one skips the read.

The clause that would otherwise have shipped this broken: useMetadata() outside a <MetadataProvider> returns a frozen no-op whose getTypeStatus says 'ready' and whose getItemsByType says [] — a shape that reads exactly like "answered, and your object is not there". An empty registry therefore reads as unknown.

Verified: sharedUserFeeds.activityGate-7476.test.tsx (14 cases). Four of the six behavioural cases are "still reads" on purpose — no provider, empty registry, errored registry, present — one is "asks nothing yet" (still loading), and only the tenant-registry case is "no request at all". The pure predicate is pinned across all six status × registry-contents combinations.


Verification

Host is macOS on Node v26.7.0; CI runs Node 22.x. The shared heavy-verify lock is Linux-only (flock is util-linux), so it reports DECLARED UNLOCKED MODE here and provides no exclusion — noted rather than claimed.

what command result
build (dep closures) pnpm --filter '@object-ui/app-shell...' --filter '@object-ui/plugin-report...' --filter '@object-ui/plugin-dashboard...' --filter '@object-ui/plugin-chatbot...' --filter '@object-ui/console...' run build exit 0
type-check pnpm --filter core --filter i18n --filter plugin-chatbot --filter plugin-dashboard --filter plugin-report --filter app-shell --filter console run type-check exit 0, every package Done
tests (core / i18n / chatbot / dashboard / report) pnpm exec vitest run packages/i18n/ packages/core/ packages/plugin-chatbot/ packages/plugin-dashboard/ packages/plugin-report/ exit 0 — Test Files 322 passed (322), Tests 4937 passed (4937)
tests (app-shell + console) pnpm exec vitest run packages/app-shell/ apps/console/ exit 1 — Test Files 1 failed | 708 passed (709), Tests 7 failed | 6920 passed | 1 skipped — see below
the nine new files, on 6129b3b0c pnpm exec vitest run <the nine> exit 0 — Test Files 9 passed (9), Tests 91 passed (91)
lint (whole farm) pnpm lint exit 0 — Tasks: 47 successful, 47 total, 0 errors (2883 pre-existing warnings)
lint (this diff) eslint --format json <the 30 changed .ts/.tsx> exit 0 — 30 files, 0 errors, 63 warnings, all pre-existing no-explicit-any / fast-refresh in files this PR touches by a few lines
i18n gates check:i18n-keys, check:i18n-drift, check:i18n-dead-keys exit 0, 0, 0 — drift: "5 en value(s) changed … Every changed en value was followed by all nine translation packs."
changeset gates check-changeset-presence.mjs, check-changeset-no-major.mjs exit 0, 0
control bytes pnpm check:control-bytes exit 0 — "scanned 6271 tracked text file(s)"

The one red suite is pre-existing, proven

packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx fails 7/7 with expected 0 to be greater than 0 on its own counter-probes — the instrument records zero sessionStorage writes, so nothing downstream of it can be measured.

Checked against the merge-base rather than asserted: with the branch committed, git checkout 8ad218d5 -- packages apps reverted the working tree to the base content (git diff --stat HEAD showed the 20 files reverting, 470 deletions), the file was re-run there and failed identically, 7/7. The tree was then restored with git checkout HEAD -- packages apps and the restore proven by git diff HEAD being empty — not by the command's exit code.

It also fails standalone, so it is not load-flakiness. The likeliest cause is the host's Node 26 (localStorage is not available because --localstorage-file was not provided is emitted throughout) shadowing jsdom's storage; CI runs Node 22.x and will say. Reported rather than filed, since it reproduces only on this host so far.

Ablation

The two i18n suites carry their non-vacuity controls in the file rather than as a one-off run: each locale's banned-vocabulary list is re-run against the exact wording that pack shipped before this PR and must flag it, and the toaster ratchet's matcher is re-run against the position="bottom-right" line it replaced. A list that silently stops matching therefore fails the suite instead of passing it — which is the failure mode a manual ablation only rules out once.

🤖 Generated with Claude Code

…e, and the remaining built-in aggregate captions

- cloud#1984: reword the five maker start chips in all ten packs (and the
  call-site defaultValue fallbacks) so they ask only for what ADR-0112 v1
  builds; note beside the keys to revert when v2 re-adds flows.
- objectui#7481: add step labels for get_authoring_rules, load_tools,
  open_record, test_flow and toggle_flow, which are newer than the pinned
  spec's platform-tool registry.
- objectui#7480: re-arm StickToBottom on every send path so the assistant rail
  follows the thread; message appends deliberately do not.
- objectui#7482: drop the console's bottom-right toaster override — the corner
  belongs to the ChatDock composer, where a resting pointer also pauses
  sonner's dismiss timer indefinitely.
- objectui#7534: resolve builtinAggregate through buildDatasetFieldHelpers at
  the five headerLabel call sites, so a KPI/table/pivot/preview caption reads
  what the chart legend beside it reads.
- objectui#7476: gate the sys_activity read on the object registry the shell
  already loads; every uncertainty still reads.
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3186.2 KB 3191.4 KB
Main entry chunk (gzip) 143.2 KB 350 KB
Entry file index-DUZAYv1p.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) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 510.70KB 116.21KB
core (index.js) 6.96KB 2.79KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 180.00KB 50.20KB
fields (index.js) 242.27KB 61.22KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
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) 5.12KB 1.74KB
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) 15.75KB 3.80KB
plugin-calendar (index.js) 47.87KB 13.31KB
plugin-charts (index.js) 70.92KB 19.75KB
plugin-chatbot (index.js) 196.37KB 46.50KB
plugin-dashboard (index.js) 132.87KB 34.68KB
plugin-designer (index.js) 212.86KB 43.19KB
plugin-detail (index.js) 250.55KB 64.06KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.87KB 32.66KB
plugin-gantt (index.js) 167.26KB 41.00KB
plugin-grid (index.js) 209.29KB 56.78KB
plugin-kanban (index.js) 52.71KB 14.55KB
plugin-list (index.js) 113.28KB 27.59KB
plugin-map (index.js) 20.44KB 6.78KB
plugin-markdown (index.js) 13.93KB 4.81KB
plugin-report (index.js) 43.59KB 11.97KB
plugin-timeline (index.js) 30.84KB 8.85KB
plugin-tree (index.js) 9.20KB 3.19KB
plugin-view (index.js) 85.24KB 20.94KB
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) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
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 (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (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

@hotlong
hotlong marked this pull request as ready for review September 5, 2026 08:59
@hotlong
hotlong added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit 64dae8e Sep 5, 2026
34 checks passed
@hotlong
hotlong deleted the fix/v1-start-chips-and-ui-fragments-1984 branch September 5, 2026 09:14
os-bill pushed a commit that referenced this pull request Sep 9, 2026
Neither has an open objectui card. Both are tests that predicted this exact
event in their own prose and said what to do when it fired, so the repair is the
one each prescribes — not a new decision.

`MetadataService.specKeyReference.test.ts` — the VERSION SKEW row. Until 17.3.0
the spec's `reference` emptiness test was an equality against `''`, so `'   '`
parsed green while this writer refused it: a real divergence, declared rather
than hidden, with the spec half written as a tripwire for the day
objectstack#16920 reached the pin ("this line reddens when the pin reaches it —
at which point the reading becomes `false` and nothing else here moves").
17.4.0 carries the fix. Measured: `'   '`, `''` and an absent target are now all
refused under the same `custom` issue at the same `reference` path the writer
names, while `'account'` and `' account '` still parse — the trim is for the
TEST only, which is what the writer's own pass-it-through row depends on. The
verdict flips and is asserted as an envelope (code plus path), because "refused"
and "refused AS a blank target" are separable claims. `MetadataService.ts` is
untouched, exactly as the note predicted. Closes the divergence objectui#8621
recorded; objectstack#16920 is the other repo's card and is not touched.

`toolLabels-locale-parity-7481.test.ts` — `AHEAD_OF_PIN`, the hand-held list of
tools the cloud runtime registers ahead of the spec snapshot. All five names it
carried are in `PLATFORM_TOOLS_BY_PACKAGE` at 17.4.0, and the file's own rule is
that a caught-up name must be REMOVED so the list never becomes a second
registry. Emptied, ⛔ not deleted: the next tool registered ahead of a snapshot
needs the same hatch.

⚠️ Emptying it turns three guards into tautologies — `[].filter(…)` is `[]`,
`arrayContaining([])` passes on anything — including the one whose comment says
it exists to stop the suite going vacuously green if the spec export disappears.
So they are re-anchored: a `GRADUATED` constant records the five names and
asserts they ARE in the registry, and the coverage floor is read off the
registry rather than off the emptied list. If the registry ever stops listing
one of them, that is now what reddens, and the repair is to put the name back on
`AHEAD_OF_PIN`.

⛔ objectui#7481 is CLOSED (completed via PR #7710) and is not reopened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wtfW1ZxGnP1XKGc9uZVms
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 17, 2026
* chore(deps): take the 17.4.0 @objectstack/* line in the lockfile

Float all seven `@objectstack/*` entries from 17.3.0 to 17.4.0 as one line:
client, core, formula, lint, sdui-parser, spec, types.

`package.json` is untouched — every declared range is a caret inside major
17 (`^17.0.0`, `^17.1.0`, `^17.2.0`, `^17.3.0`), all of which already admit
17.4.0. This is a lockfile refresh only.

The re-resolve is pinned so the diff carries nothing but the seven
identities. A plain `pnpm update -r "@objectstack/*"` also floats
`zod ^4.4.3 -> 4.5.4`, which forks `ai@7.0.65`'s peer context and yields two
physical copies each of spec, formula and lint — the objectui#8326 / objectstack-ai#8333
defect class that `scripts/check-lockfile-integrity.mjs` exists to catch.
That float is not caused by this bump: `@objectstack/spec` and
`@objectstack/core` declare the identical `zod: ^4.4.3` at 17.3.0 and
17.4.0. It is `pnpm update` re-resolving an already-satisfied range.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wtfW1ZxGnP1XKGc9uZVms

* chore(deps): move the two pins that must track the lockfile to 17.4.0

Both are mechanically coupled to the family move in the previous commit, and
each carries its own prescription for exactly this event.

`e2e/live/ci/backend.env` — `OBJECTSTACK_VERSION` 17.3.0 -> 17.4.0. The file's
own header states the rule ("bump this in the same PR that bumps the console's
@objectstack pins"), `content/docs/guide/ci-cd-pipeline.md` repeats it, and
`scripts/__tests__/ci-cd-pipeline-doc.test.ts` enforces equality with the
version `pnpm-lock.yaml` resolves. A mismatched pair makes the live-e2e lane
carry no information either way (objectui#7689).

`scripts/__tests__/check-lockfile-integrity.test.ts` — its two samples are
transformations OF the real lockfile, anchored on the current pin, and the
assertion that broke says so itself: "the spec snapshot key moved — rebuild
this sample before trusting it". Only the anchor moves; the simulated
downgrade target (17.2.0) and every assertion are unchanged, so both samples
still reproduce objectui#8326 and objectui#8333 as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wtfW1ZxGnP1XKGc9uZVms

* fix(scripts): keep the RegExp dot escaped when rebuilding the lockfile sample

The previous commit rewrote the sample anchor from 17.3.0 to 17.4.0 and lost
one backslash level in the process. The pattern is built through
`new RegExp(...)` from a template literal, so `17\.4\.0` reaches the
constructor as `17.4.0` and both dots become "any character" — matching, but
by accident. `no-useless-escape` caught it. Restored to `17\\.4\\.0`, which is
byte-identical to the base line apart from the version digit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wtfW1ZxGnP1XKGc9uZVms

* refactor(gantt): derive the ten extension keys from GanttConfigSchema.shape (objectui#7845)

objectstack#15469 closed `GanttConfigSchema` and declared the ten keys
plugin-gantt used to read through its `.passthrough()` window. objectui carried
its own copy of those ten in two places; with the spec declaring them, the copies
became duplicates and `ObjectGantt.blockPrecedence.test.tsx`'s no-duplicates pin
measured it (40 keys, 30 unique).

Retired both copies:

- `GANTT_CONFIG_EXTENSION_KEYS` (`plugin-gantt/src/ObjectGantt.tsx`) — the flat
  key list is now `GanttConfigSchema.shape` plus the `dependencyField` alias.
- `GanttConfigExtensionFields` (`types/src/zod/objectql.zod.ts`) — the flat face
  takes all ten from `SpecGanttConfigSchema.shape` by reference like its other
  members, and the nested `gantt` block is the spec schema itself rather than
  the spec schema extended with the local map.

Compared member for member before deleting, as the card required: the spec's
declarations cover the same ten keys, the same `interactions`
(move/resize/progress/link) and `timeSegments` (dayStart/bands/showMidnight)
sub-shapes and the same band members, with strictly fuller describes. The local
copy carried no describe, alias or member the spec's lacks, so there is no
difference to file.

Also corrected the prose that still called the spec loose: `types/objectql.ts`
said `timeSegments` was "the one key the spec genuinely does not model" and that
`GanttConfigSchema` is `$loose` upstream — both false, and the first read as an
invitation to write undeclared sub-keys that the `strictObject` now refuses by
name. `zod/objectql.zod.ts` justified its own local `.passthrough()` by pointing
at the spec's gantt/tree windows, which no longer exist; the local reason (and
the measurement that `swimlaneField` / `endField` are still spec-undeclared)
replaces it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wtfW1ZxGnP1XKGc9uZVms

* feat(dashboard)!: refreshInterval -> refreshIntervalSeconds, following the spec rename (objectui#7783)

@objectstack/spec 17.4.0 renamed the dashboard's auto-refresh key
(objectstack#15680, ruling B on objectstack#14478: a duration-shaped number
carries its unit in the key name, not only in the describe prose) and left a
`retiredKey` tombstone behind the old spelling.

objectui's zod `DashboardComponentSchema` takes its spec-owned half from
`SpecDashboardFields` by reference, so that face moved on its own the moment the
lockfile did. What did NOT move was the TypeScript twin, and the pair's drift
ledger caught it: `zod-mirror-parity.test.ts` refused to compile with
`complex.zod.ts#DashboardComponentSchema` gaining `refreshInterval`.

Moved here: the TS declaration; the four reader sites, keeping the `* 1000`
arithmetic identical (the value is still seconds — the multiply is exactly the
tell the rename exists to remove, and it now sits under a key that says so); the
published registration input; the metadata-admin zh overlay key; the config
panel's field key and the `DashboardConfig` model behind it; and the fixtures
and pins that spell the key.

⛔ Deliberately NOT renamed, measured rather than swept:

- `plugin-report` / `types` `reports.ts` / `reports.zod.ts` — the report
  component's own key, excluded by the card.
- `DataBinding.refreshInterval` (`types/src/data.ts`) and
  `AIInsightsSchema.refreshInterval` (`types/src/ai.ts`). The card lists both
  among "the TS types" for this rename; they are not. Neither has a zod mirror
  or a `@objectstack/spec` counterpart (spec/ui exports nothing matching either
  name), so there is no producer behind a rename — and `DataBinding`'s is
  documented in MILLISECONDS, which makes `refreshIntervalSeconds` the wrong
  name for it outright.

`content/docs/api/schema-reference.md` documented this key as milliseconds with
a `30000` sample. It was never milliseconds; corrected with the rename.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wtfW1ZxGnP1XKGc9uZVms

* fix(components): element:record_picker.filter declares the ViewFilterRule array arm (objectui#7663)

objectstack#14406 converged `ComponentPropsMap['element:record_picker'].filter`
from the MongoDB-style `FilterConditionSchema` record onto
`z.array(ViewFilterRuleSchema)` — the last record-form `filter` in the map,
closing the maintainer's 2026-08-25 ruling that the platform carries one filter
orthography (objectui#6206-B). @objectstack/spec 17.4.0 ships it, so the
declaration here now teaches a shape the save gate refuses and a JSX page
writing the legal array form draws a false `type-mismatch`.

Measured at the installed spec before flipping: the record form is refused by
KIND at `filter` (`invalid_type`, expected array), a bare tuple is refused at
`filter.0` (expected object), and `[{ field, operator, value }]` is accepted —
with `operator: 'eq'` normalised to `'equals'` on parse, which is why the
fixture spells the canonical form.

- `inputs.filter` flips `type: 'object'` to `'array'` and its description is
  rewritten around `[{ field, operator, value }, …]`. The PRECEDENCE paragraph
  is kept verbatim in substance: `dataSource.filter ?? filter` did not change.
- The two parity pins swap sides — the record form is now the refused shape,
  the rule array the accepted one — and the refusals are asserted as an
  envelope (issue code plus path), so a parse that failed for an unrelated
  reason cannot stand in for the contract refusing the record form.
- The coarse-type agreement test moves with them, and now states WHERE the
  agreement ends: `'array'` has no member arm, so a tuple list passes the
  manifest check and is refused only by the spec. That gap is named rather than
  papered over — the description carries what the coarse type cannot say.

No renderer change: `filter?: unknown` passes through to `$filter` and `find()`
already lowers the array form. The console parity test's `element:record_picker`
arm goes green by derivation once the declaration moves; only its MEMBER_PINS
prose is edited, to describe the arm that is actually pinned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wtfW1ZxGnP1XKGc9uZVms

* feat(plugin-kanban): publish object-kanban `limit`, now that the spec declares it (objectui#8172)

objectui#8172 was filed because four faces taught `limit` on an `object-kanban`
— the renderer sending `$top: schema.limit ?? DEFAULT_KANBAN_LIMIT`, both
`@object-ui/types` faces, and a documented section — while the strict
`ComponentPropsMap` refused it BY NAME, so an author following this repo's own
docs wrote a node the save gate rejected. The maintainer ruled option A (the
contract catches up, 2026-09-07 「同意」), objectstack#16503 landed as
objectstack#16562, and @objectstack/spec 17.4.0 ships
`limit: z.number().int().positive().optional()`.

⭐ All three of the card's disagreeing faces were RE-MEASURED at this pin rather
than assumed, as the card asked:

- renderer — `ObjectKanban.tsx` still sends `$top: schema.limit ??
  DEFAULT_KANBAN_LIMIT`, with `schema.limit` in the effect deps. Unchanged.
- `@object-ui/types` — BOTH faces declare `limit` now (`objectql.ts` and the
  zod mirror `objectql.zod.ts`). ⚠️ This falsifies the reading recorded on the
  card by the spec seat, which measured only the plugin's own `KanbanSchema`
  declaring it at pin `a472b07`; objectui's shared faces have since caught up,
  so there was no missing declaration to add.
- docs — `plugin-kanban.mdx` teaches it correctly. Its one loose sentence is
  tightened here, because the key is now published with a precedence-bearing
  description and the two must agree: the binding's own `limit` wins outright,
  a named view's `pagination.pageSize` fills the cap only when the board leaves
  `limit` unset. Verified against `ElementDataSourceGate`'s `readLimit(base) ===
  undefined` branch, not restated from the spec's describe.

So the only face still out of step was this repo's registration, and that is
what moves. ⛔ The declaration carries NO default: a materialised `limit` would
defeat that same `readLimit(base) === undefined` branch and a bound view's
`pageSize` would then never fill it — the spec seat's explicit warning.

The `object-kanban::limit@spec` ledger row in `block-config-schema-parity-8216`
is deleted, which is the ratchet working: a row that no longer describes a
violation is as red there as a violation with no row. Its self-deleting probe
went with it, so the spec-face ledger is now empty — and an empty probe list is
indistinguishable from a broken oracle lookup, so a live control replaces it:
the former violation must now parse clean AND an undeclared sibling must still
be refused by name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wtfW1ZxGnP1XKGc9uZVms

* fix(console): the three unannounced @objectstack/spec 17.4.0 changes this repo asserted against (objectui#8785)

Each was measured against BOTH spec versions before being repaired, so
"changed in 17.4.0" is a reading rather than an inference.

1. `job.timeout` retired in favour of `timeoutMs`. The preview sample carried
   `timeout: 600000` and a comment arguing for that spelling ("the unit is
   already milliseconds"). The spec now refuses the key with a tombstone making
   the opposite argument — the unit belongs in the name, since the sibling
   `retryPolicy.backoffMs` already spelled its own and one surface was teaching
   two conventions. Same ruling family as objectui#7783. The SAMPLE moves; the
   value is unchanged. Measured: `timeoutMs: 600000` parses clean through
   `ObjectStackSchema`, `timeout: 600000` is refused at `jobs.0.timeout`.

2. The flow end-node `outcome` enum narrowed to `completed | refused`. The
   sample's `outcome: 'success'` is no longer a member. It becomes `'completed'`
   — ⛔ NOT `'refused'`, which the spec additionally requires a `message` beside
   (measured: `refused` with no message fails at `config.message`), and which
   would in any case describe the wrong ending for this sample.

3. The dashboard `gap` help text. The pin asserted that the ENGLISH producer
   copy still says "Tailwind"; 17.4.0 rewrote it to "Space between widgets, in
   steps of 0.25rem (4 = 1rem)".

   That leg is a non-vacuity control, not the subject: without it, "the zh copy
   does not say Tailwind" would pass just as well on copy that never said it.
   Re-pointing it at the new sentence buys exactly one release — this repo does
   not own that text. So the control now asserts the PROPERTY the overlay exists
   for: the producer's copy reaches for developer vocabulary (a framework name,
   or digits welded to a CSS unit) and the zh overlay carries none of it. Still
   non-vacuous, no wording pinned, and if the spec ever writes this hint in
   plain author language the control reddens and someone re-reads the overlay —
   which is correct, since the overlay would then have nothing left to do here.

   ⚠️ `\brem\b` does not work for this: the boundary before `rem` never matches
   in `0.25rem`, because `5` is a word character. Measured both ways against
   both spec versions' copy before landing.

⛔ None of the three is patched green by relaxing an assertion: in all three the
spec is the producer and it moved deliberately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wtfW1ZxGnP1XKGc9uZVms

* test: discharge the two self-declared tripwires the 17.4.0 pin reached

Neither has an open objectui card. Both are tests that predicted this exact
event in their own prose and said what to do when it fired, so the repair is the
one each prescribes — not a new decision.

`MetadataService.specKeyReference.test.ts` — the VERSION SKEW row. Until 17.3.0
the spec's `reference` emptiness test was an equality against `''`, so `'   '`
parsed green while this writer refused it: a real divergence, declared rather
than hidden, with the spec half written as a tripwire for the day
objectstack#16920 reached the pin ("this line reddens when the pin reaches it —
at which point the reading becomes `false` and nothing else here moves").
17.4.0 carries the fix. Measured: `'   '`, `''` and an absent target are now all
refused under the same `custom` issue at the same `reference` path the writer
names, while `'account'` and `' account '` still parse — the trim is for the
TEST only, which is what the writer's own pass-it-through row depends on. The
verdict flips and is asserted as an envelope (code plus path), because "refused"
and "refused AS a blank target" are separable claims. `MetadataService.ts` is
untouched, exactly as the note predicted. Closes the divergence objectui#8621
recorded; objectstack#16920 is the other repo's card and is not touched.

`toolLabels-locale-parity-7481.test.ts` — `AHEAD_OF_PIN`, the hand-held list of
tools the cloud runtime registers ahead of the spec snapshot. All five names it
carried are in `PLATFORM_TOOLS_BY_PACKAGE` at 17.4.0, and the file's own rule is
that a caught-up name must be REMOVED so the list never becomes a second
registry. Emptied, ⛔ not deleted: the next tool registered ahead of a snapshot
needs the same hatch.

⚠️ Emptying it turns three guards into tautologies — `[].filter(…)` is `[]`,
`arrayContaining([])` passes on anything — including the one whose comment says
it exists to stop the suite going vacuously green if the spec export disappears.
So they are re-anchored: a `GRADUATED` constant records the five names and
asserts they ARE in the registry, and the coverage floor is read off the
registry rather than off the emptied list. If the registry ever stops listing
one of them, that is now what reddens, and the repair is to put the name back on
`AHEAD_OF_PIN`.

⛔ objectui#7481 is CLOSED (completed via PR objectstack-ai#7710) and is not reopened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wtfW1ZxGnP1XKGc9uZVms

* fix(docs): re-anchor the two doc ratchets this branch's own edits moved

Both surfaced in the full suite, both caused by edits in this branch and neither
by a spec change:

- `check-doc-example-types.mjs`'s `UNGATED_EXAMPLES` is keyed by `path:line
  symbol`, and the gantt prose corrections in `packages/types/src/objectql.ts`
  added six lines above `ObjectFormSchema`'s `@example`. Re-anchored 1607 to
  1613; verified by reading the `@example` line at both revisions.

- `doc-version-claims` refuses a version literal on a doc surface that nothing
  in this repo can re-check. The `refreshIntervalSeconds` row added to
  `schema-reference.md` carried "@objectstack/spec 17.4.0". Taken out rather
  than added to `KNOWN_CLAIMS`, which is the remedy the gate itself prefers: the
  sentence needs the RENAME, not the release it shipped in, and the card number
  already points at the full account.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wtfW1ZxGnP1XKGc9uZVms

* chore(changeset): declare the 17.4.0 line move and the contracts it follows

`node scripts/check-changeset-presence.mjs` refused without one: 26 of the
changed files are published source of a package the release covers. Scored
`minor` across the eight packages whose `src/` moved — never `major`, because
this repo's fixed group tracks the @objectstack major (AGENTS.md §版本号策略,
enforced by scripts/check-changeset-no-major.mjs).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wtfW1ZxGnP1XKGc9uZVms

---------

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