Repository navigation
Implement visual designer for Object UI schemas - #1
Conversation
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR implements a comprehensive visual designer package for creating and modifying Object UI schemas through a graphical interface. The designer provides real-time preview, component palette, property editing, and JSON import/export capabilities.
Changes:
- Added
@object-ui/designerpackage with 6 core React components - Implemented context-based state management for schema operations
- Created demo application with Vite + React 19
- Added comprehensive documentation (English README, Chinese implementation guide, visual guide)
Reviewed changes
Copilot reviewed 21 out of 22 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/designer/tsconfig.json | Configured TypeScript with ES2020 target, module paths, and build settings |
| packages/designer/src/index.ts | Main entry point exporting Designer components, context, hooks, and types |
| packages/designer/src/context/DesignerContext.tsx | State management context with schema CRUD operations and helper functions |
| packages/designer/src/components/Toolbar.tsx | Top toolbar with JSON import/export and modal editor |
| packages/designer/src/components/PropertyPanel.tsx | Dynamic property editor with type-specific inputs |
| packages/designer/src/components/Designer.tsx | Main component integrating three-column layout |
| packages/designer/src/components/ComponentPalette.tsx | Categorized component browser with click-to-add functionality |
| packages/designer/src/components/Canvas.tsx | Live preview canvas with selection and hover highlighting |
| packages/designer/package.json | Package configuration with dependencies and scripts |
| packages/designer/VISUAL_GUIDE.md | Visual documentation of interface layout and features |
| packages/designer/README.md | Comprehensive usage guide with API reference |
| packages/designer/IMPLEMENTATION.zh-CN.md | Chinese implementation guide |
| examples/designer-demo/* | Complete demo application setup |
| DESIGNER_SUMMARY.md | Bilingual implementation summary |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }, | ||
| "include": ["src"] | ||
| "include": ["src"], | ||
| "exclude": ["node_modules", "dist", "../renderer", "../ui"] |
There was a problem hiding this comment.
The exclude pattern '../renderer' and '../ui' use relative paths that may not work as intended from the tsconfig location. Consider using absolute patterns or verifying these paths exclude the intended directories.
| "exclude": ["node_modules", "dist", "../renderer", "../ui"] | |
| "exclude": ["node_modules", "dist"] |
|
|
||
| // Helper function to ensure all nodes have unique IDs | ||
| const ensureNodeIds = (node: SchemaNode, idPrefix = ''): SchemaNode => { | ||
| const nodeId = node.id || `${idPrefix}${node.type}-${Math.random().toString(36).substr(2, 9)}`; |
There was a problem hiding this comment.
Using Math.random() for ID generation can result in collisions. Consider using a more robust ID generation method like crypto.randomUUID() or a library like uuid for guaranteed uniqueness.
|
|
||
| const handleCopyJson = () => { | ||
| navigator.clipboard.writeText(JSON.stringify(schema, null, 2)); | ||
| alert('Schema copied to clipboard!'); |
There was a problem hiding this comment.
Using alert() is not user-friendly and blocks the UI. Consider using a toast notification or non-blocking feedback mechanism instead.
| }; | ||
|
|
||
| const handleDelete = () => { | ||
| if (confirm('Are you sure you want to delete this component?')) { |
There was a problem hiding this comment.
Using confirm() is not consistent with modern UI patterns. Consider using a custom dialog component or alert-dialog from @object-ui/ui for better user experience.
| content: [ | ||
| "./index.html", | ||
| "./src/**/*.{js,ts,jsx,tsx}", | ||
| "../../packages/ui/src/**/*.{js,ts,jsx,tsx}", | ||
| "../../packages/renderer/src/**/*.{js,ts,jsx,tsx}", | ||
| "../../packages/designer/src/**/*.{js,ts,jsx,tsx}", | ||
| ], |
There was a problem hiding this comment.
The 'content' field is for Tailwind CSS configuration, not PostCSS. This configuration should be in tailwind.config.js. PostCSS config should only contain plugins like 'tailwindcss' and 'autoprefixer'.
| content: [ | |
| "./index.html", | |
| "./src/**/*.{js,ts,jsx,tsx}", | |
| "../../packages/ui/src/**/*.{js,ts,jsx,tsx}", | |
| "../../packages/renderer/src/**/*.{js,ts,jsx,tsx}", | |
| "../../packages/designer/src/**/*.{js,ts,jsx,tsx}", | |
| ], | |
| plugins: { | |
| tailwindcss: {}, | |
| autoprefixer: {}, | |
| }, |
|
✅ All checks passed!
|
- Fix handleExportCSV to guard on gridRef.current?.api (issue #1) - Add dedicated onContextMenuAction callback instead of overloading onCellClicked (issue #2) - Remove icon property from customItems to prevent HTML injection (issue #3) - Remove validation claim from README - only basic AG Grid editing (issue #4) - Add test assertions for all new inputs (editable, exportConfig, etc.) (issue #5) - Fix onExport type to only support 'csv' format (issue #6) - Remove unused ColumnConfig properties (autoSize, groupable) (issue #9) - Type schema props with proper interfaces instead of 'any' (issue #10) - Update export description to only mention CSV (issue #11) - Add AG Grid Community vs Enterprise section to docs (issue #8) - Update README and docs with new callback and clarifications All tests pass (8/8), lint clean (0 errors) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…warning, i18n fallback - Issue #1: Normalize `in`/`not in` operators to backend-compatible `or`/`and` of `=`/`!=` - Issue #2: Filter merging now validates and filters empty conditions - Issue #3: CSV export safely serializes arrays (semicolon-separated) and objects (JSON) - Issue #5: Request counter prevents stale data from overwriting latest results - Issue #6: PullToRefresh resets pull distance immediately to prevent UI lock - Issue #7: $top configurable via schema.pagination, data limit warning shown - Issue #8: Extended i18n fallback translations for all ListView labels - Issue #9: Defensive null checks in effectiveFields for mismatched objectDef - Issue #10: Added FilterNormalization, Export, and DataFetch test suites Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…rom the package entry (objectui#10913) (objectstack-ai#10954) Fixes objectstack-ai#10913 Clause-②: no Supersedes PR objectui#10949, which was re-cut to drop a model-named commit trailer: this head `d28ba66c9` (base `5c94589f0`) carries the same diff (stable patch-id `2e6c60c1` for both commits), so the measurements below, taken at `1df262efb`, carry over, and the changeset gates and `check-control-bytes` were re-run on this head with exit 0. ## What this does The pending changeset `.changeset/9954-read-rate-banner.md` says that `useReadRateReading` and `ReadRateBanner` are exported from `@object-ui/app-shell`. The package entry exports neither of them. A pending changeset is published verbatim into the CHANGELOG at the next release, so this sentence would ship false. An author who read it and imported either name would get a module-not-exported error. The fix edits that one sentence, as triage `5865850392` execution note 1 directs: - **Before:** "Both are exported from `@object-ui/app-shell`." - **After:** "Neither is exported from the package entry; a host reaches the banner only through `ConsoleShell`, which mounts it." The sentence before it already says the banner is "mounted in `ConsoleShell`", so the paragraph now says both things triage asked for. The wording follows the sibling entry `.changeset/10439-storage-usage-banner.md` from PR objectui#10910, which says "Neither banner is exported from the package entry." The frontmatter is byte-identical: the md5 of the frontmatter block is `968447af` at base `3b469c8ea` and at head. `git diff --numstat` gives 2 insertions and 1 deletion in this one file. The only other change is the line wrap of the replaced sentence. ## Premise, re-measured at base `3b469c8ea` (still true at `origin/main` `5c94589f0`, which changed none of these files) - `packages/app-shell/src/index.ts`: `grep -c ReadRate` gives **0**. As a control, `grep -c ConsoleShell` on the same file gives 2, so the instrument does read the entry. The entry imports named members from `./hooks/index.js` and `./layout/index.js`. It has no `export *` from either, and neither named list includes the read-rate hook, its types, `classifyReadRate`, or the banner. The two sub-barrels do list the pair, but nothing outside the package can address them. - `packages/app-shell/package.json` `exports` maps only `.` and `./styles.css`. - `ConsoleShell` renders the private `ConsoleShellProviders`, and that component mounts `ReadRateBanner` beside `ImpersonationBanner` and `StorageUsageBanner`. Outside tests, `ConsoleShell.tsx` is the only module that imports `ReadRateBanner`. `ConsoleShell` itself is exported from the entry. - The changeset is still pending. It is on `main`, and release PR objectui#5400 is still open. ## No new changeset, and why the edit is in place - `check-changeset-presence` says none is owed: the diff touches `.changeset/` only. - `check-changeset-overwrite` is report-only. It lists this file as a pre-existing changeset that was modified, with the declared packages identical at base and head (`@object-ui/app-shell: minor`, `@object-ui/i18n: minor`). That is its case 2, a deliberate prose correction. The precedents PR objectui#10828 and PR objectui#10891 got the same report-only reading and landed with no new changeset. - Those two precedents appended dated supersession notes and deleted nothing. Their sentences were true when written and a later PR made them false. This sentence was false when it was written: the entry has never exported the pair. Triage's binding note says to edit the sentence itself, and `changeset-polarity-census` says the same in its own output: "ROTTED and BORN FALSE take opposite repairs". ## Gates (at head `1df262efb`, hand-derived from `package.json` and `.github/workflows/`, each exit captured by redirect-then-status) | Command | Verdict line | Exit | | --- | --- | --- | | `node scripts/check-changeset-presence.mjs` | ✅ No source or published contract of a released package changed in this range, so no changeset is owed. | 0 | | `node scripts/check-changeset-claims.mjs` | ✅ No pending changeset names a file this change touches. | 0 | | `node scripts/check-changeset-overwrite.mjs` | report-only: 1 modified, declared packages identical at base and head | 0 | | `node scripts/check-changeset-no-major.mjs` | ✅ No changeset declares a `major` bump. | 0 | | `node scripts/check-changeset-fixed.mjs` | ✅ All workspace packages are in the changeset fixed group. | 0 | | `node scripts/check-pending-changeset-literals.mjs` | ✅ No test source names a pending changeset. | 0 | | `node scripts/check-control-bytes.mjs` | ✅ check-control-bytes: OK | 0 | | `node scripts/check-new-cross-file-line-citations.mjs` | VERDICT new-cross-file-line-citations: 0 new citation(s) | 0 | | `node scripts/check-spec-symbol-derivation.mjs` | ✅ spec member citations: nothing cites a key its spec symbol does not declare. | 0 | | `node scripts/check-installed-spec-pin-claims.mjs` | OK (it excludes `.changeset/`, so it was run for completeness only) | 0 | | `node scripts/check-governed-queue-guard.mjs --test` (the one path) | ✅ NOT GOVERNED | 0 | **Tests.** These ran from the repo root, under the shared verify lock, at head `1df262efb`. The set is every `scripts/__tests__` test file that names `.changeset`, derived with `git grep -l` (24 files), plus the suites of the other scripts that read `.changeset/` (9 files). Result: **Test Files 33 passed (33), Tests 1451 passed (1451)**, lock `VERDICT command-exit 0`. No package test or type-check was run, and none is owed: the diff touches no package, and no gate above reads `dist/`. The `Spec Main Shape Gate` and the rest of CI run on the PR. Session of this run: `https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk` (the `domain:ui` seat objectstack-ai#1 dispatch, dev subagent). --- _Generated by [Claude Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_ Co-authored-by: Claude <noreply@anthropic.com>
… the 2026-09-28 cloud E2E read the session's language (objectui#10900) (objectstack-ai#10953) Fixes objectstack-ai#10900 Clause-②: yes The four surfaces the 2026-09-28 cloud E2E read in English under zh-CN now read the session's language. English is unchanged and stays the default.⚠️ **Clause-② differs from the claim.** The claim (comment 5866735286) declared `Clause-②: no` on the premise "display strings and locale entries only; no published type". The implementation widens the public surface by four optional members, so this body declares `yes`: `ActionRunner.setTranslator` (`@object-ui/core`), `SocialSignInButtonsProps.buttonText`, `LoginFormLabels.socialButton` and `RegisterFormLabels.socialButton` (`@object-ui/auth`). Nothing is removed, renamed or narrowed, and no accept set changes. The changeset declares `minor` for those two packages (the repository's precedent for a widening) and `patch` for `@object-ui/react`, `@object-ui/i18n`, `@object-ui/app-shell` and `@object-ui/console`. The seat may amend either. ## Strings | Surface | Key | en (unchanged) | zh before → after | |---|---|---|---| | Generic action success toast | `actions.completedSuccessfully` | Action completed successfully | English literal → 操作已成功完成 | | Login provider button | `auth.login.socialButton` | Continue with {provider} | English literal → 使用 {provider} 继续 | | Login divider (CSS uppercases it) | `auth.login.orText` | or continue with email | English literal → 或使用邮箱继续 | | Sign-up provider button | `auth.register.socialButton` | Sign up with {provider} | English literal → 使用 {provider} 注册 | | Sign-up divider | `auth.register.orText` | or continue with email | English literal → 或使用邮箱继续 | | Build Doctor button name + drawer title | `console.ai.buildDoctor` | Build Doctor | English literal → 构建诊断 | | Build Doctor tooltip | `console.ai.buildDoctorTitle` | Build Doctor — what actually landed? | English literal → 构建诊断 — 实际生效了哪些变更? | | Build Doctor tooltip before the first message | `console.ai.buildDoctorDisabledTitle` | Send a message first | English literal → 请先发送一条消息 | | Setup → marketplace breadcrumb | `console.breadcrumb.marketplace` | Marketplace | humanized URL slug → 应用市场 | | Marketplace search placeholder | `marketplace.searchPlaceholder` | Search apps by name or manifest ID… | 按名称或 manifest ID 搜索应用… → 按名称或标识搜索应用… | `{provider}` is the component's own single-brace hole (the `{seconds}` convention in `auth.login`), filled with the provider's display name: the component's own label for the branded providers it knows, otherwise the name the server reports. The nine new keys are in all ten packs; the eight other packs carry their own translations (see the diff). ## What was measured before each choice 1. **The toast.** `@object-ui/core` cannot reach a translator (`@object-ui/i18n` depends on core). A toast handler receives only the final string, so it cannot tell the runner's fallback from an author's identical text. The only two sites in this repository that install a toast handler on a runner are `ActionProvider` and `useActionRunner` in `@object-ui/react`, which already depends on `@object-ui/i18n`. Three other constructors (the provider-less `useAction()` fallback, `useActionEngine`'s standalone engine, and core's one-off `executeAction()`) never receive a toast handler from this repository's code. So the runner takes an injected translator (the same seam `recordDelete` takes its `t` through) and those two owners install the session's `t`, read at call time so a language switch reaches an existing runner. It is asked only for the fallback: an author's `successMessage` and a server message stay verbatim. No new dependency edge. The provider-less `useAction()` fallback runner shows no toast unless a consumer installs a handler by hand, and then it is English. 2. **Login.** `@object-ui/auth` takes text only through `labels` with English defaults. `LoginForm` / `RegisterForm` passed no text to `SocialSignInButtons`, and their `orText` label was documented as the divider but rendered nowhere. `orText` now feeds the divider and a new `socialButton` template feeds the button. All four callers pass the keys: the console's `LoginPage` / `RegisterPage` and `@object-ui/app-shell`'s `DefaultLoginPage` / `DefaultRegisterPage`. The capitals in the E2E are the divider's `uppercase` class. 3. **Build Doctor.** The Share button beside it already read `console.ai.*` through `ChatPane`'s `t`; the Build Doctor button and `BuildDebugDrawer`'s title now use the same translator. 4. **Marketplace.** The breadcrumb is not under `console/marketplace/**` as the claim assumed. `AppHeader`'s `system` branch drew the segment after System from `humanizeSlug` of the URL. Only the `system/marketplace` routes are mounted by this package, so that segment now reads the pack; host-mounted `system/*` pages keep their slug. The search matches the localized display name, `manifest_id` and the localized description. The zh placeholder names the name and 标识, the identifier each card prints under its name, which is the zh pack's existing word for identifiers. It promises nothing the filter does not match, and a pin measures both halves through the page's own filter. The en value is unchanged. 5. **Parity.** `all-locales-key-parity.test.ts` requires every `en` key in all nine other packs and nothing extra, so each key is in all ten packs. `check:i18n-drift` reads no `en` value change. 6. **Serial constraints.** PRs 10910, 10911 and 10914 had landed before the first edit (ancestry exit 0); the branch fast-forwarded onto them. PR 10924 landed during the work and is merged in (a merge commit). Its locale edits add `commentFailed` and the `widgetForbidden*` keys in blocks this change does not touch. Its `AppHeader.tsx` hunks (the imports, the admin-status block and the `studioDesignPath` block) are more than thirty lines from this change's `system` branch edit. `MarketplacePackagePage.tsx` is untouched here. ## Tests - **New pins**, each rendering the real component or running the real runner under a real `I18nProvider` in zh and en: - `ActionRunner.defaultSuccessToast-10900` (`@object-ui/core`); - `ActionProvider.defaultSuccessToast-10900`, for `ActionProvider` and `useActionRunner`, including a language switch on an existing runner (`@object-ui/react`); - `socialButtonLabels-10900` (`@object-ui/auth`); - `socialButtonsLocale-10900` (the console's login and sign-up pages); - `authPages.socialLabels-10900` (`DefaultLoginPage` / `DefaultRegisterPage`); - `buildDoctor.locale-10900` (button and drawer); - `AppHeader.marketplaceBreadcrumb-10900`; - `MarketplacePage.searchPlaceholder-10900` (placeholder plus the filter it describes). - **Red on base, green on head.** With the fix committed, every non-test source file was put back to base `1dae95a41`. The fix's anchors were counted at 0 on disk, then the eight suites ran: `Test Files 8 failed (8)`, `Tests 19 failed | 17 passed (36)`. The passing 17 are the English controls and the checks that already held. The restore from HEAD was proved by blob hashes and an empty `git diff HEAD`. On head: `Test Files 8 passed (8)`. - **Reverse type check.** A label typo in `DefaultLoginPage` gives exactly `TS2353 … 'orTextTypo' does not exist in type 'LoginFormLabels'` against the rebuilt `@object-ui/auth` declarations, so the type-check reads the new `.d.ts`, not a stale one. It was restored by blob check. - **At head `f6582636f`** (after merging `main` at `5c94589f0`): - the dependency closure of `@object-ui/console` was rebuilt (34 of 34 tasks); - `type-check` exits 0 for `@object-ui/core`, `i18n`, `react`, `auth`, `app-shell` and `console`; - `vitest` over `packages/core/`, `packages/react/`, `packages/auth/` and `packages/i18n/`, plus the four touched `app-shell` directories (`layout`, `console/ai`, `console/auth`, `console/marketplace`) and the console's `pages/auth`: `Test Files 479 passed (479)`, `Tests 7063 passed | 13 skipped`; - `check:control-bytes`, `check:i18n-keys`, `check:i18n-drift`, `check-changeset-presence` and `check:new-line-citations` pass. - **At `9ab3b7207`**, before the merge: - the full `packages/app-shell/` and `apps/console/` suites: `Test Files 978 passed | 1 skipped (979)`, `Tests 10202 passed | 9 skipped`; - every `scripts/` suite: `Test Files 177 passed | 2 skipped (179)`; - ESLint over the changed `.ts`/`.tsx` files: 0 errors, and each changed source file's warning count is equal on base and head. - **Declared narrowing.** After the merge, the full `app-shell`, `console` and `scripts/` suites were not re-run locally; CI runs the full farm on `f6582636f`. - **NOT MEASURED: Bundle Analysis** (the eager-closure budget, including the `i18n-locale-en` chunk). Reason: it needs a console `vite build`, which CI runs. ## Acceptance notes (observed, not changed here, not filed) - The other `system/*` breadcrumb segments the console host mounts (settings, apps, profile, approvals, ai-approvals, audit-log) still show their humanized English slug under zh. - The Build Doctor drawer's body (description, verdict lines, section titles) is still English; only its title was in scope. - The runner's other English fallbacks remain: its error-toast fallback for a non-string error, the parallel-chain failure text, and the Undo label on an undoable success toast. `setTranslator` is the seam any of them would use. - The eight non-zh packs translate "manifest ID" literally in the marketplace placeholder, and the English source says "manifest ID" too. - `LoginForm` keeps a `hasSocialProviders` state that nothing reads (an existing lint warning). --- _Generated by [Claude Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_ _Body wording amended by the `domain:ui` seat objectstack-ai#1 after contract review `5868740252` (its D1 to D3); no code claim moved._ --------- Co-authored-by: Claude <noreply@anthropic.com>
…y on that day in every zone, and the formula date functions do a calendar day's arithmetic on the UTC calendar (objectui#10866, slice 3) (objectstack-ai#10957) Part of objectstack-ai#10866 Clause-②: no Slice 3 of the date-only zone family's carrier card: the timeline renderer's gantt variant and the D1 day arithmetic of core `FormulaFunctions`, per release note `5866905836`, ruling `5864664470` (objectui#10903, its last execution parameter) and claim `5867569822`. The card stays open for slice 1's owed items, listed under Acceptance notes. Why `Clause-②: no`: stored date-only values read and compute as the stored day. No schema, published type or accepted set moves, and ruling `5864664470` states the arithmetic needs no statement. ## What changed ### Timeline, gantt variant (`plugin-timeline` `renderer.tsx`) One read, `readGanttDate`: a date string goes through core `toDisplayDate` (the objectui#10183 convention, which the bar tooltip's `formatDate` has taken since slice 2), so a date-only `2026-10-05` is local midnight of the day it names and a value with a time part keeps its instant. A number or a `Date` is an instant and is read by `new Date(value)` as before; for a `Date` that is a fresh copy, so no method the authored object carries is called afterwards (objectui#7027). Every stop on the gantt branch now reads through it: - `generateTimeScaleHeaders` (exported): both ends. The walk moves and prints with local setters and getters, so a date-only end starts it on that day in every zone. - `calculateDateRange`: reduces with the same read and prints each end as the viewer's calendar day (`toDateInputValue` of a `Date`, local getters). `toISOString()`'s date part named the day before east of UTC once the read is local. - `calculateBarDimensions`: all four ends. - `findUnusableGanttDate`: its "does it parse" half asks the same read, so `2026-02-30` is refused with the existing `timeline.gantt.unusableRange.malformedDate` message (no new key), where the engine rolled it into March and the bar was drawn there. Kept on the engine parse, the gate would pass a value the reduce below it reads as an Invalid Date. - The min-over-max guard: both ends, as the headers read them. - `emptyGanttDateRange`: the viewer's today (`toDateInputValue(new Date())`), where it was the UTC day. No import line of the renderer changed: `toDisplayDate` and `toDateInputValue` were already imported from `@object-ui/core`. ### Core formula date functions (`core` `evaluator/FormulaFunctions.ts`) - `readFormulaDate`: the engine parse (a date-only value is UTC midnight of its day, as before), the same named `Invalid date` error, and a `day` flag from `isRealCalendarDate`, the shared judgement, imported inside core from `utils/date-display` (already on core's barrel). - `DATEADD`: a calendar day goes through `addToDay`, which uses UTC setters. Its months go through `addMonthsUtc`: step to the 1st, move the month, clamp the day to the target month's last day. That is the rule of objectstack `@objectstack/formula`'s stdlib `addMonthsUtc` behind CEL `addMonths(d, n)`. A year is twelve months on the same rule. A day moved by days, months or years comes back as `YYYY-MM-DD`; hours and minutes give an instant (UTC midnight of the day, moved). Anything else (an instant, or a date-only string naming a day its month does not have) keeps the base code path verbatim: local setters, `setMonth` with no clamp, an instant out. - `DATEDIFF`: the month and year units read a day's fields with UTC getters and an instant's with local getters (`calendarFields`), so two instants count exactly as on the base. Day, hour and minute units are millisecond differences, unchanged. - `DATEFORMAT`: a day's fields with UTC getters, an instant's with local getters (`calendarFields`), as on the base. - `TODAY()` and `NOW()` are untouched. The objectui#10915 docblock on `TODAY()` stands, and a new docblock above `DATEADD` states the three functions' rule. ### Pins, docs, changeset - Two pins registered in `PINS` of `scripts/__tests__/date-only-zone-pins-10183.test.ts`, plus one header sentence: `TimelineGantt.dateOnlyZone-10866` (plugin-timeline) and `FormulaFunctions.dateOnlyZone-10866` (core). Each runs under `America/Los_Angeles` and `Asia/Shanghai`, each zone opens with a rig case (the gantt pin adds a fixture-validity case per zone), and each carries instant controls. The gantt pin reads the named producer, `examples/schema-catalog/src/schemas/plugin-timeline/gantt-style-timeline.json`, from disk. The formula pin's every-zone cases also run in the suite zone. - Docs: one paragraph in `content/docs/plugins/plugin-timeline.mdx`, "Time Scales (Gantt)". - Changeset `.changeset/10866-gantt-axis-formula-day-arithmetic.md`: `patch` for `@object-ui/plugin-timeline` and `@object-ui/core`. ## Readings, base `3b469c8ea` against head, in both zones Base readings: the base sources with the two new pins, in a separate comparison worktree, forks pool with the driver's flag (`Tests 27 failed | 24 passed (51)`, every red a row below), plus a direct probe of core `FormulaFunctions` under `TZ`. Head readings: the pins, green. Gantt, `en-US`: | reading | LA base | LA head | Shanghai base | Shanghai head | |:--|:--|:--|:--|:--| | producer `gantt-style-timeline.json`, month axis | Dec 2023, Jan 2024, Mar 2024, Apr 2024 | Jan 2024, Feb 2024, Mar 2024, Apr 2024 | Jan 2024 … Apr 2024 | same | | computed day axis over bars `2026-10-05`…`2026-10-07` | Oct 4, Oct 5, Oct 6 | Oct 5, Oct 6, Oct 7 | Oct 5, Oct 6, Oct 7 | same | | pinned `2026-10-05`…`2026-10-07`, instant bar from `2026-10-06T04:00Z`, its `left` | axis Oct 4, Oct 5, Oct 6 (red before the bars) | 43.75% (21:00 on the 5th) | 58.333…% (the UTC hour) | 75% (12:00 on the 6th) | | pinned start `2026-10-06`, pinned end `2026-10-06T03:00Z` | draws | refused as inverted (the end is 20:00 on the 5th here) | draws | draws, axis Oct 6 | | empty plan at `2026-10-05T20:00Z`, day scale | Oct 4 | Oct 5 | Oct 5 | Oct 6 | | bar starting `2026-02-30` (suite zone, UTC) | no refusal: the gate let it through | refused, naming `items[0].items[0].startDate` | | | Formula functions: | call | base | head, every zone (UTC, LA, Shanghai) | |:--|:--|:--| | `DATEADD('2026-01-31', 1, 'month')` | `2026-03-03T00:00:00.000Z` in every zone | `2026-02-28` | | `DATEADD('2026-03-31', -1, 'months')` | `2026-03-03T00:00:00.000Z` (UTC, Shanghai), `2026-03-03T01:00:00.000Z` (LA) | `2026-02-28` | | `DATEADD('2024-02-29', 1, 'year')` | `2025-03-01T00:00:00.000Z` in every zone | `2025-02-28` | | `DATEADD('2026-09-01', 1, 'day')` | `2026-09-02T00:00:00.000Z` | `2026-09-02` | | `DATEADD('2026-11-01', 1, 'day')` | LA `2026-11-02T01:00:00.000Z`; UTC and Shanghai `2026-11-02T00:00:00.000Z` | `2026-11-02` | | `DATEADD('2026-03-08', 1, 'day')` | LA `2026-03-08T23:00:00.000Z`; UTC and Shanghai `2026-03-09T00:00:00.000Z` | `2026-03-09` | | `DATEADD('2026-09-01', 1, 'month')` | LA `2026-10-02T00:00:00.000Z`; UTC and Shanghai `2026-10-01T00:00:00.000Z` | `2026-10-01` | | `DATEADD('2026-01-31T12:00:00.000Z', 1, 'month')` (instant) | `2026-03-03T12:00:00.000Z` in every zone | same | | `DATEADD('2026-10-31T12:00:00.000Z', 1, 'day')` (instant) | LA `2026-11-01T13:00:00.000Z`; UTC and Shanghai `2026-11-01T12:00:00.000Z` | same | | `DATEADD('2026-09-01', 3, 'hours')` | `2026-09-01T03:00:00.000Z` | same | | `DATEFORMAT('2026-09-01', 'YYYY-MM-DD')` | LA `2026-08-31` | `2026-09-01` | | `DATEFORMAT('2026-09-01', 'DD/MM/YY HH:mm:ss')` | LA `31/08/26 17:00:00`, Shanghai `01/09/26 08:00:00` | `01/09/26 00:00:00` | | `DATEFORMAT('2026-09-01T03:00:00.000Z', 'YYYY-MM-DD HH:mm')` (instant) | LA `2026-08-31 20:00`, Shanghai `2026-09-01 11:00`, UTC `2026-09-01 03:00` | same | | `DATEDIFF('2025-12-31', '2026-01-01', 'year')` | LA 0 | 1 | | `DATEDIFF('2026-08-31', '2026-09-01', 'month')` | LA 0 | 1 | | `TODAY()` and `DATEFORMAT(NOW(), 'YYYY-MM-DD')` at `2026-09-28T05:30Z` | LA `2026-09-28` and `2026-09-27` | same | Server parity, `@objectstack/formula` 17.4.0 as installed here (CEL `celEngine.evaluate`), the same in LA and Shanghai: `addMonths('2026-01-31', 1)` is `2026-02-28T00:00Z`, `addMonths('2024-02-29', 12)` is `2025-02-28T00:00Z`, `addDays('2026-11-01', 1)` is `2026-11-02T00:00Z`, `addDays('2026-10-31T12:00:00.000Z', 1)` is `2026-11-01T12:00Z`, `addMonths('2026-01-31T12:00:00.000Z', 1)` is `2026-02-28T12:00Z`, and `addDays('2026-02-30', 1)` is `2026-03-03T00:00Z`, which head `DATEADD` answers as the same instant string. ## Tests and gates Every heavy run went through objectstack's `os-verify-lock.sh`; each VERDICT line is quoted. The final head is `5b48d0292`, the contract-review repair; its readings are in the first bullet below. - **Repair round, at `5b48d0292`** (contract review `5869002399`, item 1): instants are back on the base path. A probe of base `3b469c8ea` against head under UTC, Los Angeles and Shanghai reads every instant row the same on both: `DATEFORMAT('2026-09-01T03:00:00.000Z', 'HH:mm')` is `03:00` / `20:00` / `11:00`; `DATEADD('2026-10-31T12:00:00.000Z', 1, 'day')` is `2026-11-01T12:00:00.000Z` / `2026-11-01T13:00:00.000Z` / `2026-11-01T12:00:00.000Z`; `DATEADD('2026-01-31T12:00:00.000Z', 1, 'month')` is `2026-03-03T12:00:00.000Z` in all three. Ten more rows over instants, epoch milliseconds, a zone-less date-time and an impossible day are also all equal, and the date-only rows still move. Under os-verify-lock: closure build exit 0; `type-check` for core and plugin-timeline echoes `tsc --noEmit && tsc -p tsconfig.test.json` and `Done`; `pnpm exec vitest run packages/core/ packages/plugin-timeline/ scripts/__tests__/date-only-zone-pins-10183.test.ts --maxWorkers=2` gives `Test Files 220 passed (220)`, `Tests 4037 passed | 58 skipped (4095)`, VERDICT command-exit 0. Red on base, with the base `FormulaFunctions.ts` blob `b5e63812fe3f` proven on disk: the formula pin in the forks child gives `Tests 15 failed | 24 passed (39)`. The date-only rows are red (the month and year clamp and the output shape in all three zones, the stored day's `DATEFORMAT` in Los Angeles and Shanghai, `DATEDIFF` in Los Angeles), and all six instant controls are green. The driver gives `Tests 2 failed | 18 passed (20)`. Restored: blob `edb61cdf519c` equals HEAD, `git diff HEAD` empty. Ablation (an instant's hour read with `getUTCHours`): exactly the Los Angeles and Shanghai instant-format controls go red, `Tests 2 failed | 37 passed (39)`; restored. Gates exit 0: `check-changeset-presence`, `changeset:check`, `check-changeset-overwrite`, `check:pending-changeset-literals`, `check:changeset-claims`, `check:control-bytes`, `check:new-line-citations` (0 new citations); NOT GOVERNED. ESLint on the two changed code files: 0 errors, `FormulaFunctions.ts` 26 warnings, as on base. The 11 changeset-reading `scripts/__tests__` suites were not re-run locally (two lock queue-timeouts); CI's eight test shards, which include `scripts/**`, are green. CI on `5b48d0292`: 43 check-runs, 40 success, 3 skipped, 0 failed. - **Red first, base `3b469c8ea` plus the two pins** (comparison worktree, `OBJECTUI_DATE_ZONE_CHILD=1 pnpm exec vitest run` over the two pins `--pool=forks`): `Tests 27 failed | 24 passed (51)`, VERDICT command-exit 1. Every red asserts a head value of the tables above (19 in the formula pin, over its three zones, and 8 in the gantt pin); every rig, fixture-validity and control case green. - **Reverse validation after commit, at `2006ddb03`** (the driver itself): the base blobs of `renderer.tsx` (`445a11147770`) and `FormulaFunctions.ts` (`b5e63812fe3f`) checked out and proven on disk (each equal to its base blob, not its head blob), then `pnpm exec vitest run scripts/__tests__/date-only-zone-pins-10183.test.ts`: `Tests 3 failed | 17 passed (20)`, the two new pin rows and the child-exit row; the 17 other pins green. Restored by a trap with `git checkout HEAD --` on absolute paths: each blob equal to HEAD (`e0bc666ef587`, `717f3bd2cdc7`), `git diff HEAD` empty. - **Head suites, at `2006ddb03`:** `pnpm exec vitest run packages/plugin-timeline/ packages/core/ scripts/__tests__/date-only-zone-pins-10183.test.ts --maxWorkers=2`: `Test Files 220 passed (220)`, `Tests 4035 passed | 54 skipped (4089)`, VERDICT command-exit 0. The driver row is green, so every zone case of every registered pin ran and passed in its forks child. - **At `8dd4543cc`** (one commit later, which types `readFormulaDate`'s argument instead of casting it): closure build `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-timeline^...' --filter '@object-ui/core^...' run build` exit 0; `pnpm --workspace-concurrency=2 --filter @object-ui/plugin-timeline --filter @object-ui/core run type-check` echoes `tsc --noEmit && tsc -p tsconfig.test.json` and `Done` for each; `pnpm type-check:scripts` exit 0; `pnpm exec vitest run packages/core/src/evaluator/ scripts/__tests__/date-only-zone-pins-10183.test.ts --maxWorkers=2`: `Test Files 18 passed (18)`, `Tests 455 passed | 23 skipped (478)` (the 23 are the formula pin's zone cases, run inside the driver's child). VERDICT command-exit 0. `--listFiles` on each package's test program lists its new pin once. - **Two ablations, at `8dd4543cc`** (objectstack `scripts/ablation-replace.mjs`, wrap mode, each run through the pin on the forks pool with the driver's flag; no `dist` involved, the pins import `../renderer` and `../FormulaFunctions` relatively): - the extent's min end printed with `toISOString()` again, the parse left on the shared step (anchor `minDate: toDateInputValue(new Date(minTimestamp)),`, 1 to 0, blob `e0bc666ef587` to `3265f47d94fa`). Predicted and measured: exactly the two Shanghai rows that compute the extent red (the producer and the computed day axis), `Tests 2 failed | 16 passed (18)`; every LA row green. So the east rows are the ones that catch a parse-only repair. - months moved with UTC setters but without the clamp (anchor `addMonthsUtc(date, amount);`, 1 to 0, blob `1695444571e2` to `a50648d3900b`). Predicted and measured: exactly the month-clamp and instant-clamp rows red in each of the three zones, `Tests 6 failed | 27 passed (33)`. - Both restored by the tool: blob equal to HEAD, `git diff HEAD` empty. - **Schema catalog render, suite zone:** `pnpm exec vitest run examples/schema-catalog/test/catalog-gallery-render.test.tsx examples/schema-catalog/test/smoke.test.tsx`: `Test Files 2 passed (2)`, `Tests 1020 passed (1020)`. - **Every `scripts/__tests__` gate, at `8dd4543cc`:** `pnpm exec vitest run scripts/__tests__/ --maxWorkers=2`: `Test Files 177 passed | 2 skipped (179)`, `Tests 5334 passed | 2 skipped (5336)`, VERDICT command-exit 0. - **Gates at `8dd4543cc`, each exit 0:** `check-changeset-presence` ("4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"), `changeset:check` (fixed group, no major), `check-changeset-overwrite`, `check:pending-changeset-literals`, `check:changeset-claims` ("No pending changeset names a file this change touches"), `check:control-bytes`, `check:new-line-citations` (`VERDICT new-cross-file-line-citations: 0 new citation(s)`), `check:test-path-roots`. A control-byte self-scan over the 7 changed files: no hits. - **ESLint, narrowed** to the 5 changed code files (root `eslint.config.js`, `--no-inline-config --format json`, 5 results): 0 errors. `FormulaFunctions.ts` 26 warnings and `renderer.tsx` 14, each equal to its base blob via `--stdin`; the two pins and the driver 0. The config sets no `parserOptions.project` or `projectService`, so the rules are not type-aware and this diff cannot move an untouched file's verdict. The repo-wide lint is CI's. - **Server parity** was read with `@objectstack/formula` 17.4.0 as installed here, above; the rule was read in objectstack `packages/formula/src/stdlib.ts` (`addMonthsUtc`, `addDaysUtc`) at objectstack `origin/main` `50e273fd`. ## Acceptance notes - **Decisions taken here, for the contract reviewer.** - The gantt gate now refuses a day its month does not have (`2026-02-30`), because the gate must read what the extent and the headers read: the shared step refuses it (objectui#10026), and a gate that let it through would hand the reduce an Invalid Date. The message is the existing "not a valid date" key. - An instant's gantt extent is the VIEWER's day of that instant, where it was its UTC day, so its bar sits at its local hour on an axis of the viewer's days, the day its tooltip names. - An instant in `DATEADD` / `DATEDIFF` / `DATEFORMAT` is unchanged from the base (contract review `5869002399`, item 1). Only an argument that is a calendar day, a date-only value naming a day its month has, takes the UTC calendar. An instant is still moved and read with local setters and getters in the zone the formula runs in, so `DATEFORMAT` prints its clock in the viewer's zone: `DATEFORMAT('2026-09-01T03:00:00.000Z', 'HH:mm')` is `20:00` in Los Angeles, `11:00` in Shanghai and `03:00` in UTC, as on the base. `DATEADD` of an instant by a day follows the viewer's calendar across a DST change, and a month from January 31st on an instant still overflows into March. Triage `5861646819` says a `datetime` keeps its instant, and ruling `5864664470`'s parameter names only the date-only month overflow; putting instants on the UTC calendar, or clamping their months, would be a separate decision. So `DATEFORMAT(NOW(), 'YYYY-MM-DD')` still names the viewer's day, which can differ from `TODAY()`'s UTC day for part of every day, as on the base. - A date-only string naming a day its month does not have is not refused by the formula functions: the engine rolls it forward as the server's parse does, and `DATEADD` hands it back as an instant, as before. A date-time written without an offset is still read in the viewer's zone by the engine's parse. - **DST.** Two local midnights across a DST change are 23 or 25 hours apart, so on the gantt a bar's edge can sit up to one hour's share of the axis off its column's edge. The pins stay clear of DST dates on the gantt. - **Not changed, pre-existing and zone-independent (noted, not filed):** the gantt's percentage scale spans min to max midnight while the header row draws one equal-width column per bucket from min to max inclusive, so on a day axis a bar is placed on a scale one column narrower than the header row (the computed-axis pin's second bar starts at 50% under three columns). Measured: the style percentages; the column widths are inferred from `flex-1`, not measured. - **Slice 1's owed items stay for a slice 4** (measured, not small): - The `timeZone` "persisted data stays real instants" prose sits in `@object-ui/types` (`GanttConfig.timeZone`'s JSDoc), in `plugin-gantt` (`GanttViewProps.timeZone`'s JSDoc, "Writes still persist real instants", and `makeTzShift`'s JSDoc), and in the spec's `view.zod.ts` describe, which the `@object-ui/types` zod mirror reuses and which only objectstack can edit. That is two more packages here and a cross-repo half. - The calendar's DST-day move needs a drag measured across the November change before any edit. - The calendar no-schema fallback, the `endDateField` write and the gantt no-schema path off UTC each need a new zone pin. - **Bundle.** No new cross-package import: the renderer's import line is unchanged, and `FormulaFunctions` imports `utils/date-display` inside core, a module core's barrel already re-exports. `Bundle Analysis` is left to CI. - **Governed surface.** `check-governed-queue-guard --test` over the 7 paths: NOT GOVERNED (control: `AGENTS.md` exit 3). ## Serial - Branch base `3b469c8ea`. `git merge-tree --write-tree` of head `8dd4543cc` against `main` `06a96e948`: clean (exit 0), and nothing `main` gained since the base touches these 7 paths. Implemented by the dev agent for `domain:ui` seat objectstack-ai#1, session `https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk`. --- _Generated by [Claude Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_ _Body amended by the `domain:ui` seat objectstack-ai#1 after contract review `5869002399` and the repair commit `5b48d0292`; the repair text is the dev's._ --------- Co-authored-by: Claude <noreply@anthropic.com>
… bare apiMethods-card number (objectui#10803, batch 7) (objectstack-ai#10962) Part of objectstack-ai#10803 Clause-②: no Dispatched implementation of the `domain:ui` seat objectstack-ai#1 claim (comment `5867587761`) on objectui#10803, batch 7, session `https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk`. Citations only: no sentence's claim moves, and every edited pending changeset's frontmatter is byte-identical. The only runtime text that moves is two console warnings, one in `@object-ui/app-shell` and one in `@object-ui/plugin-detail`, which lose their dead pointer and nothing else (amendment `5860244997`, Q1 = A; `patch` changeset). No test pins any changed text: the literal-anchor sweep below finds no specific anchor, so no test file is edited. This batch carries release `5866922219`'s two lists: - the **30 `objectstack#N` citations that answer 404**, in the card's two classes (pending changeset prose and non-test `packages/*/src`); - the **28 bare `objectstack-ai#3391` lines in 10 files** that mean objectstack's apiMethods whitelist card, which batch 6 fixed at 2 other sites. ## Why `Part of`, not a closing line Both lists read **0** after this batch (**Census**). The brief's rule was a closing line if the card's lists all read 0. They do, but reading the sentences found **10 more lines in the same two classes that cite a dead objectstack number written bare**, three numbers in all (**Acceptance notes** 1). Triage item 3 puts a dead number found later in these classes on this card, so the card is not finished. Whether it carries them as a batch 8 or closes is the seat's call. ## Premise, re-measured on `origin/main` `3b469c8ea` (the branch point) - **Every distinct `objectstack#N` in the two in-scope classes.** 372 numbers (the one objectstack issue URL in these classes names 6227, which is among them). Each was read once with REST `GET /repos/objectstack-ai/objectstack/issues/N`: - 341 answer 200; - 1 answers 301: objectstack#14026, transferred to objectui#10102, which batch 6 re-qualified; - **30 answer 404**, exactly the 30 batch 6 listed. - **The 30, read again.** A second `issues/N` read of each answers 404 (30 of 30), and `pulls/N` answers 404 for all 30. Lit controls in the same run: objectstack#3391, objectstack-ai#3720 and objectstack-ai#3546 answer 200 as issues, and `pulls/13267` answers 200. - **objectstack-ai#3391 and objectstack-ai#3546, both repositories.** - objectui#3391 is the record-header api-action placeholder card, unrelated. - objectstack#3391 is the apiMethods whitelist contract card: "UI 操作按钮与 apiMethods 白名单一致性契约落地". Its body names the effective operation set, `/me/permissions`, the 405 import refusal and export derived from list, which is what each of the 28 sentences says. - objectui#3546 is the missing-i18n-keys card, unrelated. - objectstack#3546 is "detail/form 面的 edit/delete 按钮接入服务端 effective 操作集", the inline-edit gate the two paired lines describe. - **objectstack history.** Read from a full, not shallow, treeless clone of objectstack `main` (`git rev-parse --is-shallow-repository`: false). - Every objectstack sha this PR cites is an ancestor of objectstack `main` (`git merge-base --is-ancestor`, exit 0): the 24 this PR adds to the tree, the 3 its edited sentences already cited (`c459da6bc`, `89448a52b`, `9bd4344e4`), and the 7 this body names besides. - `git rev-parse --short=9` returns the same 9 characters for each. - Control legs in the same clone: the head of the open PR objectstack#20421 (`a22b90fc0`) answers exit 1; the known ancestor `51789064` answers exit 0. - **The one objectui sha.** `7a197e7c5` is an ancestor of the branch point, exit 0. Control legs: the head of PR objectui#10945 answers exit 1, and `5f789538d` answers exit 0. This checkout is not shallow. - **A cross-check, not the method.** objectstack's own sweep of dead tracker citations in its tree (objectstack#19123's landing `66e266c93`, its stages `21ab41041`, `5cf58eb16` and `0d7ed5a37`, and `f415bcf18`) anchored eight of these numbers in its own files. For each of the eight (objectstack-ai#5970, objectstack-ai#6483, objectstack-ai#9934, objectstack-ai#10485, objectstack-ai#11330, objectstack-ai#11846, #12868 and #17147) it chose the same commit this PR cites. - Every edited changeset is pending: it is present in `.changeset/` on `main`. ## Census (the enumeration pin for this batch) The 30 numbers (REF = a commit or tree): ``` git grep -nE 'objectstack#(5970|5976|6038|6124|6281|6331|6450|6483|6515|9933|9934|10354|10485|10695|11330|11507|11513|11658|11703|11753|11846|12009|12868|13117|13670|16126|17147|17762|17987|18012)([^0-9]|$)' REF -- '.changeset/*.md' 'packages/*/src/**' ':!**/__tests__/**' ':!**/*.test.*' ':!**/*.spec.*' ':!**/test/**' ':!**/tests/**' | wc -l ``` The bare `objectstack-ai#3391`, with the same pathspec: ``` git grep -nE '(^|[^0-9A-Za-z_#/])objectstack-ai#3391([^0-9]|$)' REF -- '.changeset/*.md' 'packages/*/src/**' ':!**/__tests__/**' ':!**/*.test.*' ':!**/*.spec.*' ':!**/test/**' ':!**/tests/**' | wc -l ``` | REF | the 30 | bare `objectstack-ai#3391` | |:--|:--|:--| | `3b469c8ea` (branch point) | **82**: 26 changeset lines in 24 files, 56 src lines in 39 files | **28** in 10 files | | `c292a6400` (this head) | **0** | **0** | | this head merged with `main` `5c94589f0` (`git merge-tree --write-tree`, clean, tree `bba27eece`) | **0** | **0** | | `5c94589f0` (`main` alone) | 82 | 28 | - Lit controls on the same instruments at this head: live objectstack#10856 reads 4 lines (4 at the branch point); `objectstack#3391` reads 32 lines, against 3 at the branch point (28 re-qualified here, and this PR's sweep changeset names it once). - **Out of scope, as it stands** (whole tree at this head, unfiltered): - the 30 numbers: 84 test lines in 47 files, 1 scripts line, 2 lines in 2 `apps/console` files, 2 package READMEs (`auth`, `react`), 11 lines of published `CHANGELOG.md` history in 8 files; 0 `.github`, 0 governed, 0 `content/docs`; - bare `objectstack-ai#3391`: 12 test lines in 9 files and 23 `CHANGELOG.md` lines in 8 files. ## Citation form - **An objectstack commit** is written the way batch 6 wrote its stand-ins: objectstack and the 9-character backticked sha of the commit on objectstack `main` that landed the change the sentence rests on. - **A dead card beside its own dead pull request** collapses to that pull request's squash commit (objectstack-ai#5970 with PR objectstack-ai#6450, objectstack-ai#10485 with PR objectstack-ai#10695). - **A dead number beside the live landing it already names** is dropped: objectstack-ai#11846 beside PR objectstack#12718, #16126 beside PR objectstack#16920, #12868 beside objectstack `c459da6bc`, and objectstack-ai#12009 beside objectstack `89448a52b` (**Special cases** 3). - **A ruling the dead card carried** is cited by its date, with the commit that executed or recorded it, as batches 3 to 6 did for objectui rulings. - **One sentence cites this repository's commit**, `7a197e7c5`, because the change it names landed here (objectstack-ai#6331). - **Runtime text carries no sha.** In the two console warnings the dead pointer is dropped (**Special cases** 8). - **The bare `objectstack-ai#3391`** becomes `objectstack#3391`, and on the two lines that write `objectstack-ai#3391/objectstack-ai#3546`, `objectstack#3391/objectstack#3546`. ## Mapping, the 30 numbers Lines / files are the branch-point census for that number (a line naming two of them counts under both). | dead number | resolution | what that commit carries | lines / files | |:--|:--|:--|:--| | objectstack-ai#5970 | objectstack `97e7e3caa` | "unify ActionSchema.visible/disabled on one condition shape (objectstack-ai#6450)", body "(objectstack-ai#5970)": `visible` gains the boolean arm | 2 / 1 | | objectstack-ai#6450 (PR) | the same `97e7e3caa`, its squash | as above; the card / PR pair collapses | 2 / 1 | | objectstack-ai#5976 (PR) | objectstack `795b6e1aa`, its squash | "5 值子集改名 `HttpMethodSubset`" | 1 / 1 | | objectstack-ai#6038 | objectstack `7618ee814` | "key a container's default `list` `_views` name by the runtime identity": leg 2 of 3 of the objectstack#5164 ruling, the `packages/lint` half | 1 / 1 | | objectstack-ai#6124 (PR) | objectstack `b3c1f3cd5`, its squash | "key `_views` translations by the runtime view identity"; "The extractor now ASKS the composer for the key" | 1 / 1 | | objectstack-ai#6281 (PR) | objectstack `85ec26d28`, its squash, 2026-08-07 | "SDUI props — enforce or remove (objectstack-ai#5775) (objectstack-ai#6281)": the shared `PageContainerProps`, whose single key is `children`, for `page:section` / `page:footer` / `page:sidebar`, which were `EmptyProps` | 1 / 1 | | objectstack-ai#6331 | objectui `7a197e7c5` | this repository's "SchemaForm reads the canonical `visibleWhen`, reviving every metadata-form predicate (objectstack#6331)" | 1 / 1 | | objectstack-ai#6483 | objectstack `ee58392e1` | "ADR-0005 白名单强制 … (objectstack-ai#6483)"; its diff carries the sentence the comment quotes, "Runtime-created sets … ride `allowRuntimeCreate` (still `true`) and keep working" | 1 / 1 | | objectstack-ai#6515 (PR) | objectstack `2fdb36eb9`, its squash | "SpecifierSchema gains a closed `valueDomain` enum": "`bcp47_locale` is deliberately not in the vocabulary", because `localization.locale`'s options ARE the shipped catalogs | 1 / 1 | | objectstack-ai#9933 | objectstack `d5552ca13` | "admit columnState as an explicitly runtime-only view-overlay key" (subject ending "(objectstack-ai#9996)"; "(objectstack-ai#9933)" is on the message's first body line), on the overlay faces including `viewItemWireFields` | 3 / 3 | | objectstack-ai#9934 | objectstack `79c46da90` | "producer-side user-facing marking for hook refusal messages — userMessage channel (objectstack-ai#9934)": `ApiErrorSchema.userMessage`, the contract half of the objectui#5210 split | 10 / 9 | | objectstack-ai#10354 (PR) | objectstack `9e04c3e35`, its squash | "let the publish door state the package it is promoting"; its changeset and code comment carry the key-presence / `no_draft` warning `ResourceEditPage.tsx` points at | 4 / 3 | | objectstack-ai#10485 | objectstack `35ad101bc` | "retire the `themes` carrier key and ThemeSchema (objectstack-ai#10485, ADR-0049) (objectstack-ai#10695)": "Ruled B (退役授权面, 2026-08-21)", "delete ui/theme.zod.ts whole" | 15 / 13 | | objectstack-ai#10695 (PR) | the same `35ad101bc`, its squash | as above; the card / PR pair collapses | 6 / 6 | | objectstack-ai#11330 | objectstack `a9ee98992` | "manifest.runtime trust-tier text states publish-gate-only enforcement truthfully", the trust-tier half (**Special cases** 2) | 1 / 1 | | objectstack-ai#11507 | objectstack `88b9d749a` | "declare sys_activity.type as an open, author-extensible vocabulary": "Maintainer ruling 2026-08-24, direction 4" | 13 / 9 | | objectstack-ai#11513 | objectstack `e170b0ae5` | "lock package-declared permission sets at the save door; clone to customize", quoting the 2026-08-24 ruling 「同意 第一步(创业阶段,Salesforce 式)」 | 3 / 3 | | objectstack-ai#11658 | objectstack `1a6a19c31` | "open RecordActivityProps.types to author-contributed activity kinds"; its message names objectstack-ai#11658 as the card it settles, and it executes the 2026-08-24 ruling | 1 / 1 | | objectstack-ai#11703 | objectstack `5cb62d88b` | "make clone_permission_set carry all five copied facets"; its message names objectstack-ai#11703 as the card it settles: the silent-grant-loss shape | 1 / 1 | | objectstack-ai#11753 | "the 2026-08-25 ruling whose spec half is objectstack `0e4e51b0a`" | `ActionParamSchema.carryOver`, whose changeset reads "(objectstack-ai#11753 ruling, spec half; objectstack-ai#11992)" and "The maintainer's 2026-08-25 ruling on objectstack-ai#11753" | 2 / 2 | | objectstack-ai#11846 | objectstack `0c2334f6c`; dropped beside PR objectstack#12718 | "retire preview mode — the RuntimeMode 'preview' value and the whole PreviewModeConfig block (#12718)" | 3 / 3 | | objectstack-ai#12009 | dropped beside objectstack `89448a52b` | the card of the `AUTH_SSO_PROVIDER_SCHEMA` removal, whose landing the line already cites (**Special cases** 3) | 1 / 1 | | #12868 | dropped beside objectstack `c459da6bc` | the line already cites the commit that executed the ruling; objectstack's own `f415bcf18` anchors #12868 to the same `c459da6bc` | 1 / 1 | | #13117 (PR) | objectstack `225e7690f`, its squash | "Readiness read for the Phase-2 members … global:search and global:notifications both have shipped platform data sources, so per the ruling both STAY declared" | 1 / 1 | | #13670 | "maintainer ruling 2026-08-31, option 2, recorded in objectstack `8c6a7fc0b`" | "The #13670 ruling settled the question: text's intended evaluation channel is `content` alone" | 1 / 1 | | #16126 | dropped beside PR objectstack#16920 | PR objectstack#16920 (200) names #16126 in its body as the card it settles; merged 2026-09-08 as `859ded3ec` | 2 / 2 | | #17147 | objectstack `aaacf1d5c` | "the install-time granted permission set is REGISTERED at load and refuses nothing — say so, and pin the measurement (#17147)", the measurement on `9bd4344e4` | 2 / 2 | | #17762 | objectstack `4342c9923` | "guard three data lookups against Object.prototype fall-through"; its message names #17762 as a card it settles, `classifyFilterToken` among the three lookups | 1 / 1 | | #17987 | objectstack `e233db9db` | "declare element-level `navigation` on object-kanban / object-calendar …"; its message names #17987 as the card it settles, and its Downstream note: objectui#8652 waits on it, unlock criterion a released, installable `@objectstack/spec` (**Special cases** 1) | 2 / 2 | | #18012 | objectstack `176b03582` | "`$between` requires two non-blank endpoints (#18012)": "Ruling executed: decision batch objectstack-ai#146 item 5, **letter A**" | 3 / 3 | The 28 `objectstack-ai#3391` lines, all now `objectstack#3391`: `ObjectDataPage.tsx` (3) and `ObjectView.tsx` (2) in app-shell; `managedBy.ts` (5); `MePermissionsProvider.tsx` (2), `PermissionContext.ts`, `PermissionProvider.tsx`; `fieldWriteGate.ts`; `ImportWizard.tsx` (6), `ObjectGrid.tsx` (4); `ListView.tsx` (3). Each was read: every one names the server's effective API operation set, `/me/permissions` `apiOperations`, or the 405 import refusal. ## Special cases (the judgement calls) 1. **#17987, two sentences.** - `ObjectTree.tsx`: "blocked on objectstack#17987, whose unlock criterion is a released `@objectstack/spec` carrying the declaration being installable here" becomes "blocked on objectstack `e233db9db`, whose unlock criterion …". That commit's Downstream note states the same criterion. - `ObjectCalendar.tsx`: "that card is `pm:blocked` on objectstack#17987" becomes "that card waits on objectstack `e233db9db`". The label word is not kept, because objectui#8652's label reads `pm:on-hold` today (measured); "waits on" is the phrase `e233db9db`'s own note uses for that card. 2. **objectstack-ai#11330.** "it is objectstack#11330's half of the same panel" becomes "it is the trust-tier half of the same panel, which objectstack `a9ee98992` settled separately". `aaacf1d5c`'s message calls objectstack-ai#11330 "the sibling half of this very sentence", ruled the same way on 2026-08-30, and `a9ee98992` (2026-08-30) is that half's landing. 3. **objectstack-ai#12009 collapses into the sha beside it.** objectui#6910's body and ruling comment `5534414562` name "objectstack#12009 / PR #13413" together as the one `AUTH_SSO_PROVIDER_SCHEMA` precedent, a card and its pull request. Batch 6 replaced PR #13413 with its squash `89448a52b`, so the card goes the way of batch 3's objectstack-ai#5401 / objectstack-ai#5505 pair. 4. **objectstack-ai#11753, two sites.** The card carried the ruling, and `0e4e51b0a` is its spec half. Both sites keep "ruling" as the antecedent that `ActionParamDialog.tsx`'s next paragraph ("The ruling's point …") reads. 5. **objectstack-ai#10354 in `ResourceEditPage.tsx`.** "since objectstack#10354 `doPublish` states" gains a comma, "since objectstack `9e04c3e35`, `doPublish` states", so two adjacent code spans do not read as one. 6. **objectstack-ai#11507 in the 8137 changeset.** "objectstack#11658 executing the maintainer's 2026-08-24 ruling on objectstack#11507" becomes "objectstack `1a6a19c31` executing the maintainer's 2026-08-24 ruling": the executing commit is named, and the ruling is cited by its date. 7. **Line breaks moved** where the stand-in is longer or shorter: `ActionRunner.ts` (two sites), `ActionParamDialog.tsx`, `theme.ts`, `theme.zod.ts` (two sites), `index.zod.ts` and the metadata-admin `i18n.ts` comment, where "ruling on" became "ruling of 2026-08-24,". 8. **The runtime strings.** Only the listed text moves. | file | member | before | after | |:--|:--|:--|:--| | `app-shell/src/layout/activityItemType.ts` | the `console.warn` in `warnUnmappedActivityType` | "… `sys_activity.type` is author-extensible (objectstack#11507, ruled 2026-08-24) and is not validated on write …" | "… `sys_activity.type` is author-extensible (ruled 2026-08-24) and is not validated on write …" | | `plugin-detail/src/renderers/recordActivityFeed.ts` | the `warnOnce` message in `warnUnknownActivityType` | "… `sys_activity.type` is author-extensible (objectstack#11507, ruled 2026-08-24) and is not validated on write …" | "… `sys_activity.type` is author-extensible (ruled 2026-08-24) and is not validated on write …" | No test, doc or changeset quotes either message with the pointer: the census reads 0 in `.changeset/`, and the anchor sweep finds no test literal that drops. 9. **`objectstack-ai#3391/objectstack-ai#3546`.** On the two lines that pair them (`managedBy.ts`, `ObjectGrid.tsx`), both halves are qualified, as batch 6 qualified both halves of "#13337/#13086". The other bare `objectstack-ai#3546` lines are not in this batch's lists and are left (**Acceptance notes** 2). ## The literal-anchor sweep (both test-pin classes, ruling `5861900779`) - **Instrument.** Every string, template and regex literal in all 4073 tracked test and script files (106544 distinct literals), read with the TypeScript scanner. - **Candidate filter.** A literal is a candidate if it matches the diff's removed lines with two lines of context, raw or comment-flattened: 1983. - **Test.** Does its occurrence count DROP between `3b469c8ea` and `c292a6400` in any of the 74 changed files, raw or comment-flattened? 136 do. - **Every one is generic:** digits, punctuation, single words ("object", "blocked", "locked"), character classes, and two regexes that read no changed file: `/objectui#\d+|objectstack#\d+/` in `registry-inputs-spec-parity`, which asserts over its own ledger's reasons, and the older spelling of the three submitRedirect tests' ruling matcher, quoted in their own doc comments (the live `CITES_ITS_RULING` asserts over their own refusal text). None is a changed phrase, a dead number or a changed warning. ## Held **By the serial rule: nothing.** Open PRs were mapped at branch time (9 open) and again after the push, before this PR opened (11 open). The second mapping came after the push, not before it; the same three files were shared both times. Three open PRs share a file with this PR: - _Both PRs below have merged since this PR opened (objectui#10945 as `06a96e948`, objectui#10908 as `b45d463a9`). The trial merge with today's `main` is clean, and both censuses read 0 on it (contract review `5870922323`), so nothing is owed. The two rows are kept as the record at the time._ - **objectui#10945, `RecordDetailView.tsx`.** The blob at its merge-base equals the branch point's. Its hunks are the imports and one block far below; this PR's one changed line in that file is far from both. - **objectui#10908, `types/src/zod/index.zod.ts`.** Its one insertion is in the export list, far below this PR's two changed comment lines. - **objectui#10278, `plugin-grid/src/ObjectGrid.tsx`.** The file drifted between its merge-base and the branch point, so this PR's four changed lines were mapped onto its merge-base by a line alignment: the nearest of its hunks is more than 150 lines from any of them. Trial merges with this head (`git merge-tree --write-tree`): - clean for objectui#10952, objectstack-ai#10950, objectstack-ai#10949, objectstack-ai#10947, objectstack-ai#10945, objectstack-ai#10944, objectstack-ai#10930, objectstack-ai#10908 and objectstack-ai#10777; - objectui#10278 conflicts in `ObjectGrid.tsx`, `plugin-grid/README.md` and `content/docs/plugins/plugin-grid.mdx`, and conflicts in the same three files against `main` alone; - objectui#5400 (Version Packages) regenerates and is not a hold. `.changeset/9954-read-rate-banner.md` is held by this seat's objectui#10913 dispatch (PR objectui#10949) and is untouched here. It carries none of this batch's numbers. ## Changesets - `.changeset/10803-dead-citation-sweep-seventh-batch.md`, EMPTY frontmatter. It covers the comment-only edits in 17 released packages; no published behaviour changes through them. It points at the second file for the runtime text. - `.changeset/10803-seventh-batch-runtime-strings.md`, `'@object-ui/app-shell': patch` and `'@object-ui/plugin-detail': patch`: the two warnings lose their pointer. What renders, and when and how often each warning fires, are unchanged. ## Proof of prose-only (C4), against `3b469c8ea` - **Source.** Each of the 48 touched `.ts` / `.tsx` files was parsed at `3b469c8ea` and at this head with TypeScript 6.0.3's `createSourceFile`, and re-printed by `createPrinter({ removeComments: true })`. - 46 of 48 prints are identical. - `activityItemType.ts` and `recordActivityFeed.ts` are equal once the one listed substitution each (**Special cases** 8) is applied to the base print, each matched once. - 0 parse diagnostics. - Lit controls on the same instrument: editing a string literal moves the print; re-spacing a comment does not. - **Changesets.** The frontmatter block of every one of the 24 edited changesets is byte-identical at `3b469c8ea` and this head (24 of 24, md5). The overwrite gate below agrees. - **Scope of the diff:** 74 files, +157 / −115: 24 edited and 2 new changesets, and 48 non-test source files in 17 released packages. No test file. ## Gates, on this head `c292a6400` Each line is the gate's own verdict and exit code, captured by redirect-then-`$?`. - `node scripts/check-changeset-presence.mjs`, exit 0: "48 source file(s) of 17 released package(s) changed, and this change declares 2 changeset(s): .changeset/10803-dead-citation-sweep-seventh-batch.md, .changeset/10803-seventh-batch-runtime-strings.md." - `pnpm changeset:check`, exit 0: "All workspace packages are in the changeset fixed group." / "No changeset declares a `major` bump." - `node scripts/check-changeset-overwrite.mjs` (report-only), exit 0: "2 changeset(s) added, 24 modified, 0 deleted". `declared at base` equals `declares now` for each of the 24. - `pnpm check:changeset-claims` (report-only), exit 0: - "Every one of those 1 address(es) either names the tree it was read from, or points at a line this change does not move"; - "Every package declared across those 22 body(ies) is either not negated …"; - the standing notice "87 pending changeset(s) describe a file this change touches". Read against the diff: a pending changeset quoting a replaced pointer would itself carry a dead number and sit in the census, which reads 0. - `pnpm check:control-bytes`, exit 0: "check-control-bytes: OK (scanned 9229 tracked text file(s); skipped 85 binary)." A `grep -P` control-byte self-scan of the 74 files finds none. - `pnpm check:new-line-citations`, exit 0: "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0". - `pnpm check:pending-changeset-literals`, exit 0: "No test source names a pending changeset." - Also run over the touched comments: - `pnpm check:spec-symbols`, exit 0: "spec member citations: 1421 sources + 184 documentation pages; nothing cites a key its spec symbol does not declare."; - `pnpm check:installed-pin-claims`, exit 0 ("OK"); - `pnpm check:comment-mask-corpus`, exit 0 (1 disagreeing file, within the ceiling objectui#7882 holds open); - `node scripts/check-hand-rolled-comment-mask.mjs`, exit 0 ("OK every carrier is a DEBT entry, and every DEBT entry still carries one."); - `pnpm check:handler-key-reads`, exit 0 ("every judged read is a declared member of it"). - The governed-surface predicate over the 74 paths, exit 0: "NOT GOVERNED — 74 path(s) checked against 5 governed surface(s); none matched." Lit control `AGENTS.md`: exit 3. **Tests and type-check**, through the shared verify lock, on `c292a6400`. Each is `VERDICT command-exit 0`. - `scripts/__tests__/`, the whole directory, whose whole-tree scanners read the touched files and changesets: `Test Files 177 passed | 2 skipped (179)`, `Tests 5332 passed | 2 skipped (5334)`. The two skipped files are the network-escape fixtures that run only as a child. - `packages/types/`, `core/`, `react/`, `i18n/`, `providers/`, `permissions/` and `data-objectstack/`, whole packages, in one run: `Test Files 704 passed (704)`, `Tests 13170 passed | 13 skipped (13183)`. - The eight touched plugin packages (calendar, designer, detail, form, grid, kanban, list, tree): `Test Files 787 passed | 1 skipped (788)`, `Tests 7521 passed | 27 skipped (7548)`. - `packages/components/`: `Test Files 324 passed | 1 skipped (325)`, `Tests 3148 passed | 24 skipped (3172)`. - `packages/app-shell/`: `Test Files 854 passed | 1 skipped (855)`, `Tests 8789 passed | 9 skipped (8798)`. - Type-check: `turbo run build` of the 28-package dependency closure (`Tasks: 28 successful, 28 total`), then `pnpm --workspace-concurrency=2` with the 17 package filters `run type-check`: 17 script echoes, 17 `Done`. A first attempt before the build exited 2 on an unbuilt dependency (`Cannot find module '@object-ui/types'`) and measured nothing. - No red leg: the sweep found no anchor to move, so there is no pin whose old copy should fail. CI on `c292a6400`: 43 check-runs, 40 success, 3 skipped, 0 failed; `Spec Main Shape Gate` success. ## Acceptance notes 1. **Dead objectstack numbers written bare: 10 more lines in the same two classes.** A bare number resolves to this repository, where each of these is a live, unrelated card, so no `objectstack#` census sees them. - **Measurement.** The bare-number instrument of PRs objectui#10875 / objectstack-ai#10892 / objectstack-ai#10914 reads 965 distinct numbers at this head. The 916 between 100 and 25000 were each read once as objectstack issues: 877 answer 200 and 39 answer 404. Reading the sentences of those 39, three mean an objectstack card or pull request (below); the other 36 cite objectui cards or objectui pull requests. - **objectstack-ai#9934**, 7 lines in 7 files: `.changeset/7980-agent-key-envelope-read.md`, and in app-shell `index.ts`, `apiErrorEnvelope.ts` (2), `PackageFormDialog.tsx`, `StudioDesignSurface.tsx` and `packages-io.ts`. All mean the `userMessage` channel, objectstack `79c46da90`. - **"PR objectstack-ai#6281"**, 2 lines in `containers.tsx`, beside objectstack#5775. The landing is objectstack `85ec26d28`. - **"objectstack PR objectstack-ai#8452"**, 1 line in `useRecordCrudVerdicts.ts`. The landing is objectstack `27358d517` ("add batch recordIds to security/explain (objectstack-ai#8326) (objectstack-ai#8452)"). - None of them sits in a sentence this PR edits, so they are outside this batch's lists and left. Carrier: this card, triage item 3. 2. **The rest of the bare `objectstack-ai#3546` population.** Five more comment lines write objectstack#3546 as a bare `objectstack-ai#3546` (`RecordDetailView.tsx` 2, `RelatedRecordActionsBridge.tsx`, `record-details.tsx`, `fieldWriteGate.ts`), and one writes it as `objectui#3546` (`plugin-detail`'s `index.tsx`). Each names the server's effective API operation set on a detail or form surface. It is live-but-wrong, not a 404, like the `objectstack-ai#3391` class this batch closed. Carrier: none. 3. **A stale label in a comment.** The `ObjectCalendar.tsx` sentence said objectui#8652 is `pm:blocked`; that card reads `pm:on-hold` today. **Special cases** 1 says how the repaired sentence avoids the label. 4. **Filenames are not citations.** `.changeset/17147-plugin-disclosure-not-enforced.md` carries one of the 30 numbers in its name; it stays, as in PRs objectui#10707, objectstack-ai#10797, objectstack-ai#10854, objectstack-ai#10869, objectstack-ai#10875, objectstack-ai#10892 and objectstack-ai#10914. --- _Generated by [Claude Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_ _Tests block completed by the `domain:ui` seat objectstack-ai#1 from the dev report `5870507620` (the suites that finished after this PR opened), and three figures corrected after contract review `5870922323` (the objectstack-ai#9933 and objectstack-ai#6515 quotes, and the Held rows); no code claim moved._ Co-authored-by: Claude <noreply@anthropic.com>
…itText and cancelText (objectui#10935) (objectstack-ai#10986) Fixes objectstack-ai#10935 Clause-②: yes `MasterDetailForm` resolves its three `I18nLabel` members, `title`, `submitText` and `cancelText`, with `pickLocalized` against the active UI language, the way `ObjectMetricWidget` does (triage comment 5867953691). `MasterDetailFormSchema` types the three as `I18nLabel`, as `@objectstack/spec`'s `ComponentPropsMap['object-master-detail-form']` does; the zod arm is not narrowed. Draft, head `c357b233a` (the repair round; the first round was `dbb004b16`): a dispatched dev's PR (claim comment 5869374008), so ready and landing belong to the seat. ## What changed - **`packages/plugin-form/src/MasterDetailForm.tsx`** - `MasterDetailFormSchema.title`, `.submitText` and `.cancelText` go from `string` to `I18nLabel` (imported as a type from `@object-ui/types`). - `MasterDetailForm` reads `useObjectTranslation().language` once, the source `ObjectMetricWidget` (and the `MetricWidget` it forwards to) resolves its `I18nLabel` members against. `title` resolves once and feeds the built-in edit-save toast and the parent `ObjectForm`'s `title` (the `string` that `ObjectFormSchema.title` declares). `submitText` and `cancelText` resolve where the two Buttons read them. - Defaults: with nothing authored the text is unchanged ('Create' or 'Save', 'Cancel', 'Created' or 'Saved'). The button fallback is now `||` where it was `??`, so an authored empty string, or a map with no string entry, shows the English default. An authored empty string used to render an empty button; a map with no string entry used to throw as a React child. - **`packages/plugin-form/src/MasterDetailForm.i18nLabels.test.tsx`** (new): the pins, below. - **`packages/plugin-form/src/MasterDetailForm.test.tsx`**: the existing host-string `cancelText` test was named "i18n is the host's job" and commented "the plugin is locale-agnostic". This change makes both false, so the name and comment are reworded. The assertion is unchanged. - **`packages/types/src/zod/objectql.zod.ts`**: comment only. The `ObjectMasterDetailFormBlockSchema` docblock said `MasterDetailFormSchema` types the three labels "as a plain string", which this change makes false. It now says `I18nLabel` and names the type pin. - **`packages/plugin-form/src/index.tsx`**: the `object-master-detail-form` inputs `title`, `submitText` and `cancelText` declare `type: ['string', 'object']`, with descriptions that teach the per-locale map (repair round, `c357b233a`). - **`apps/console/src/__tests__/`**: three `MEMBER_PINS` rows in `registry-inputs-spec-parity.test.ts`, and the new public-door pin `masterDetailFormI18nLabelManifest.test.ts` (repair round, `c357b233a`). - **`.changeset/10935-master-detail-i18n-labels.md`**: `'@object-ui/plugin-form': minor`, with the `Clause-②: yes` line. ## Premise checks, on `origin/main` `328abeb55` - **The three raw reads** are where the card puts them. They are `schema.cancelText ?? 'Cancel'` and `submitText` (`schema.submitText ?? …`) as Button children, and the `${schema.title} saved` toast in `handleSaved`. `title` has one more read on this node: the `parentSchema` memo forwards it to the parent `ObjectForm`. That forward now gets the resolved string, which is the same one-line repair. - **Locale source.** `ObjectMetricWidget` resolves `label` / `title` with `pickLocalized(…, language)`, where `language` comes from `useObjectTranslation()`, the UI language. This file already imported `useDisplayLocale()`, but that is the NUMBER locale: it puts the tenant's localization before the UI language. So it is not the source for label text, and it is not used for it. - **Spec-row census.** The row has exactly three members that are `I18nLabel`: `title`, `submitText` and `cancelText`. `details`, `sections` and `fields` are `z.array(z.unknown())`, so no nested label is typed there. The census came from enumerating the installed spec row (17.4.0) at runtime, not from reading the source. Nothing else rides this PR. - **Published or not.** `src/index.tsx` exports `MasterDetailFormSchema` from the package entry (`exports['.']` is `dist/index.d.ts`). After the build, `dist/MasterDetailForm.d.ts` reads `I18nLabel` for all three. So the type widening is published: `minor`, and `Clause-②: yes` stands. - **Consumers of the widened type.** `git grep MasterDetailFormSchema` in `*.ts` / `*.tsx` outside `plugin-form` finds only two comment or string mentions in `@object-ui/types`, and no code that reads the type (control: the same grep hits `plugin-form/src/index.tsx`). `ObjectForm` writes the three members from its own `string` fields, which a widening keeps compiling. ## Evidence All at `dbb004b16` unless a line says otherwise. The repair round's readings at `c357b233a` are in the next bullet. The runs went through the shared verify lock, on a shared box. - **Repair round, at `c357b233a`** (contract review `5871765302`): the public-door pin with `index.tsx` at `dbb004b16` gave `6 failed | 7 passed (13)` (the three arm rows and three map rows); at the head, 13 passed. Registry-reading suites: `Test Files 10 passed (10) / Tests 309 passed (309)`. The cli ratchet and the schema-catalog gallery: `2 passed / 601 passed`. `plugin-form` and `apps/console` type-check exit 0. Gates exit 0, including `check-changeset-presence` and `check:new-line-citations`. CI: 43 check-runs, 40 success, 3 skipped, 0 failed, `Spec Main Shape Gate` included. **Pins, through the real `SchemaRenderer` and the registry.** Each node is authored in the `{ type, properties }` form under an `I18nProvider` set to `zh`. Every map lists `en` first, so falling back to `en` or to the first entry fails the row. - The locale-map documents the rows mount pass `safeValidateSchema`. - A map `submitText` renders `保存订单` on the Save button. - A map `cancelText` renders `返回` on the Cancel button. `onCancel` sits beside the document as a host key: the arm refuses it as a runtime slot, and the button renders only when it is present. - A map `title` toasts `采购单 saved` on an edit save. - Control: plain strings render as authored (`Save PO`, `Discard`, `PO saved`). - Control: with nothing authored, the defaults are unchanged (`Create`, `Cancel`, `Save`, `Saved`). - Type pin: `assertionLabelMembersAreI18nLabel`, an `Expect`/`Equal` tuple that holds all three members equal to `I18nLabel | undefined`. **Red on base, then green (ablation).** `MasterDetailForm.tsx` was committed first, then replaced on disk by its `328abeb55` blob, under a trap-restore. The hash matched the base blob and the `pickLocalized` count went from 5 to 0. - `vitest run packages/plugin-form/src/MasterDetailForm.i18nLabels.test.tsx` gave `Tests 3 failed | 3 passed (6)`. The `submitText` and `cancelText` rows failed on "Objects are not valid as a React child (found: object with keys {en, zh-CN})". The `title` row failed on ``expected [ '[object Object] saved' ] to deeply equal [ '采购单 saved' ]``. The validator row and the two controls passed. - `tsc -p tsconfig.test.json` failed on the three `Expect` lines (`TS2344: Type 'false' does not satisfy the constraint 'true'`). - Restored: the hash equals the HEAD blob, and `git diff HEAD` is empty. At HEAD the same file gives `Tests 6 passed (6)`, and with `MasterDetailForm.test.tsx` `Tests 16 passed (16)`. **Consumer probe of the built `.d.ts`.** A one-shot file, outside the repo, imports `MasterDetailFormSchema` through a `paths` entry pointing at `packages/plugin-form/dist/index.d.ts`. Assigning locale maps to `title` and `submitText` compiles. With the `@ts-expect-error` dropped from a `submitText: 42` line, it fails with `TS2322 … not assignable to type 'string | (Record … ) | undefined'`, which proves the rebuilt declaration is read and is not `any`. **Suites, type-check and lint.** - `vitest run packages/plugin-form/`, run together with a one-off manifest probe file (since deleted), gave `Test Files 138 passed (138)`, `Tests 1600 passed | 1 skipped`, at `c4d7c74f6`. That is 137 plugin-form files plus the probe. The two later commits change test files, a type-only pin and one comment in `@object-ui/types`. The two changed plugin-form test files were re-run at `dbb004b16`, as above. - `vitest run packages/types/` gave `Test Files 268 passed (268)`, `Tests 5955 passed`. - `vitest run scripts/` gave `Test Files 177 passed | 2 skipped (179)`, `Tests 5333 passed | 2 skipped`. - `pnpm --filter @object-ui/plugin-form type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`) exits 0. The test project lists the new pin file (`--listFilesOnly`). - `pnpm --filter @object-ui/types type-check` exits 0. - Before these, the plugin-form dependency closure was built (`turbo run build --filter='@object-ui/plugin-form^...'`, 11 of 11 tasks, restored from the turbo cache), and plugin-form itself was built. - `eslint` over the four touched source files: 0 errors. The new test file has no warnings. **Gates** (each exit 0): `check:new-line-citations` (0 new citations), `check:control-bytes`, `check-changeset-presence`, `changeset:check`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, `check:test-path-roots`, `check:phantom-deps`, `check:handler-key-reads`, `check:i18n-keys`, `check:unreferenced-sources`. `check-governed-queue-guard --test` over the five paths answers NOT GOVERNED. **Bundle.** No new module enters the graph: `@object-ui/i18n` was already imported by this file, and only `pickLocalized` and `useObjectTranslation` join the named imports. A single-file esbuild minify of `MasterDetailForm.tsx`, base against head, gives +95 bytes minified and +50 bytes gzip. That is a proxy for the plugin-form chunk, not the console's eager-closure gate: see NOT MEASURED. **Bundle, repair round (from delta review `5872990965`).** `apps/console`'s `register-plugins.ts` imports `@object-ui/plugin-form` eagerly, so the three new input descriptions ship in the eager closure. The budget comment on `c357b233a` reads **3104.4 KB** against the 3104.5 KB ceiling (PASS), against 3103.9 KB on `dbb004b16` and on every other PR built after objectui#10992. So this round costs about +0.5 KB, and landing it leaves about 0.1 KB of eager headroom on `main`. The exhausted headroom is carried by objectui#10996. ## Acceptance notes - **NOT MEASURED: `check:eager-closure` / Bundle Analysis.** Reason: it needs a full console build. The proxy delta is above. - **NOT MEASURED: `check:readme-exports`.** Reason: it refuses to judge while 24 packages are unbuilt ("population collapsed"). This PR touches no README and no export name. - **The registry `inputs` for these three keys now declare `type: ['string', 'object']`** (`object-master-detail-form` in `plugin-form/src/index.tsx`), with descriptions that teach the `{ en, 'zh-CN' }` map. This repairs defect 1 of contract review `5871765302`. Through the production public door (`ComponentRegistry.getPublicConfigs()` → `manifestFromConfigs` → `validateTree`), the `'string'`-only declaration reported `type-mismatch` on a legal locale map, with the message `prop "submitText" expected a string`, and the same for `title` and `cancelText`. My first probe missed it because it read `getAllConfigs()`, which keys the block by its namespace, so the bare type answered `unknown-component`. `apps/console/src/__tests__/masterDetailFormI18nLabelManifest.test.ts` pins the door. With `index.tsx` at `dbb004b16` it gave `6 failed | 7 passed (13)`; at `c357b233a` all 13 pass. A string, and a number that is still reported, are the controls. The console parity gate's three `MEMBER_PINS` rows point at `MasterDetailForm.i18nLabels.test.tsx`. - **The English defaults stay English**, as before: 'Create', 'Save', 'Cancel', 'Created', 'Saved', 'Saving…', and the " saved" suffix after an authored `title`. This PR only resolves authored labels. - **The same shape on the sibling `object-form` node** is in the report as a finding for the seat to judge, and is not touched here. The spec row takes a locale map for `submitText`, and a probe through the real `SchemaRenderer` renders `Component "form" failed to render` with "Objects are not valid as a React child". `ObjectFormSchema` types it `string`. --- _Generated by [Claude Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_ _Body amended by the `domain:ui` seat objectstack-ai#1 with the dev's repair-round text, after contract review `5871765302`; no code claim moved beyond that round's commit._ --------- Co-authored-by: Claude <noreply@anthropic.com>
…re-qualify objectstack#3546 (objectui#10803, batch 8) (objectstack-ai#11012) Fixes objectstack-ai#10803 Clause-②: no Dispatched implementation of the `domain:ui` seat objectstack-ai#1 claim (comment `5873413663`) on objectui#10803, batch 8, session `https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk`. Citations only: no sentence's claim moves, the one edited pending changeset keeps a byte-identical frontmatter, and no runtime string moves, so the only new changeset is an empty-frontmatter sweep. No test pins any changed text (**The literal-anchor sweep**), so no test file is edited. This batch carries release `5872268200`'s two lists (the seat's ruling A in ACCEPT `5870953708`, which review `5870922323` read the same way): - the **10 in-class lines that cite a dead objectstack number written bare**: `objectstack-ai#9934` seven times, "PR `objectstack-ai#6281`" twice, "objectstack PR `objectstack-ai#8452`" once; - the **6 live-but-wrong `objectstack-ai#3546` lines** that mean objectstack#3546: five written bare, one written with this repository's name. Numbers are written in code spans in this body when they are the dead or wrong spelling, so GitHub does not link them to the unrelated objectui items they would resolve to. ## Why `Fixes` Both lists read **0** after this batch (**Census**), and the closure census over the card's two classes (pending `.changeset/*.md` prose and non-test `packages/*/src`) finds nothing else dead: - **Bare numbers**, batch 7's instrument (printed under **Census**): 965 distinct at the branch point, 961 at this head. The 916 between 100 and 25000 at the branch point were each read once as objectstack issues: 877 answer 200 and 39 answer 404. The 39 were read again as objectstack issues and as pulls (404 each) and as objectui issues (200 each). Three of them are this batch's (`objectstack-ai#9934`, `objectstack-ai#6281`, `objectstack-ai#8452`), gone at this head. The other 36 remain, and each sentence was read: every one cites an objectui card or pull request, and each answers 200 in objectui with the subject its sentence names. - **The instrument's blind spot** (a number written after `/`, `-` or `.`, as in `objectui#9469/objectstack-ai#8649`): 46 numbers between 100 and 25000 appear only in that form at this head. Read as objectstack issues: 45 answer 200; one answers 404, `objectstack-ai#8649`, which sits in two lines as `objectui#9469/objectstack-ai#8649` and means objectui#8649 (200). - **Qualified citations** at this head: every distinct `objectui#N` and objectui issue or pull URL (2723 numbers) answers 200; every distinct `objectstack#N` and objectstack issue or pull URL (345 numbers) answers 200, except objectstack#14026, which answers 301 (transferred to objectui#10102, re-qualified by batch 6). So nothing dead is left in the card's classes, and nothing in them resolves to the wrong repository as far as these instruments see. **Acceptance notes** 1 and 2 record what is outside the classes, and what could not be measured. ## Premise, re-measured on `origin/main` `665025908` (the branch point) - **The three dead numbers.** objectstack `issues/N` and `pulls/N` answer 404 for 9934, 6281 and 8452 (the in-range census read each as an issue a second time). Lit controls in the same run: objectstack issues 3546, 3391, 5775 and 8326 answer 200; objectstack `pulls/13267` and `pulls/16920` answer 200. - **What they resolve to here.** objectui `objectstack-ai#9934` is a CI finding about the `needs:contract-review` label; objectui PR `objectstack-ai#6281` is an app-shell Studio grid change; objectui `objectstack-ai#8452` is a `not_contains` operator bug. None is what its sentence meant. - **`objectstack-ai#3546`, both repositories.** objectstack#3546 (200) is "detail/form 面的 edit/delete 按钮接入服务端 effective 操作集", the effective operation set on detail and form surfaces, which each of the six sentences describes. objectui `objectstack-ai#3546` (200) is the missing-i18n-keys card, unrelated. The 96 `objectui#3546` lines in the locale packs and the "slice seven" line in `.changeset/3880-es-done-listo.md` are that i18n card, and are untouched. - **The landing commits**, each read over REST (`commits/SHA`) and checked with `compare/SHA...main`, which answers `behind_by 0` for each, i.e. each is an ancestor of objectstack `main`: - `79c46da90`, "feat(contract): producer-side user-facing marking for hook refusal messages — userMessage channel (objectstack-ai#9934) (objectstack-ai#9992)", 2026-08-19: `ApiErrorSchema.userMessage`, and the dispatcher door (`packages/runtime/src/http-dispatcher.ts`) emits it as a declared sibling of `code` and `message`; - `85ec26d28`, "spec: SDUI props — enforce or remove (objectstack-ai#5775) (objectstack-ai#6281)", 2026-08-07: retires `page:card.body` in favour of `children`, with an ADR-0087 D2 conversion; - `27358d517`, "feat(spec,rest): add batch recordIds to security/explain (objectstack-ai#8326) (objectstack-ai#8452)", 2026-08-13. - These are the landings release `5872268200` names; review `5870922323` read all three against their messages. Batch 7 already cites `79c46da90` and `85ec26d28` at other sites in these classes. - The one edited changeset, `.changeset/7980-agent-key-envelope-read.md`, is pending: it is present in `.changeset/` on `main`. This checkout is not shallow. ## Census (the enumeration pin for this batch) The two lists (REF = a commit or tree): ``` git grep -nP '(?:^|(?<=[^\w#&/.\-]))#(9934|6281|8452|3546)(?![0-9A-Za-z_])' REF -- '.changeset/*.md' 'packages/*/src/**' ':!**/__tests__/**' ':!**/*.test.*' ':!**/*.spec.*' ':!**/test/**' ':!**/tests/**' | wc -l git grep -n 'objectui#3546' REF -- '.changeset/*.md' 'packages/*/src/**' ':!**/__tests__/**' ':!**/*.test.*' ':!**/*.spec.*' ':!**/test/**' ':!**/tests/**' ':!packages/i18n/src/locales/**' | wc -l ``` | REF | bare four | `objectui#3546` outside the locale packs | |:--|:--|:--| | `665025908` (branch point) | **15**: `objectstack-ai#9934` 7, `objectstack-ai#6281` 2, `objectstack-ai#8452` 1, `objectstack-ai#3546` 5 | **2**: the `plugin-detail` line and the i18n card's own line in `3880-es-done-listo.md` | | `8fe632d96` (this head) | **0** | **1**: the `3880-es-done-listo.md` line, which is the i18n card and stays | - Lit controls at this head: `objectstack#3546` reads 10 lines against 3 at the branch point (the 6 re-qualified here, and one in this PR's sweep changeset); the 96 `objectui#3546` lines in the locale packs are unchanged. - The bare-number instrument, batch 7's (the census of **Why `Fixes`**): ``` git grep -hoP '(?:^|(?<=[^\w#&/.\-]))#\d+(?![0-9A-Za-z_])' REF -- '.changeset/*.md' 'packages/*/src/**' ':!**/__tests__/**' ':!**/*.test.*' ':!**/*.spec.*' ':!**/test/**' ':!**/tests/**' | sort -u | wc -l ``` 965 at the branch point, 961 at this head: exactly `objectstack-ai#3546`, `objectstack-ai#6281`, `objectstack-ai#8452` and `objectstack-ai#9934` are gone, and nothing is new. The blind-spot reading replaces the lookbehind class with `[^\w#&]`. ## Citation form - **An objectstack commit** is written as batches 6 and 7 wrote their stand-ins: objectstack and the 9-character backticked sha of the commit on objectstack `main` that landed the change the sentence rests on. - **A bare `objectstack-ai#3546`** becomes `objectstack#3546`, including inside the `[objectstack-ai#3546]` tags that open four comments; the one written with this repository's name is re-qualified the same way. ## Mapping | site | before | after | |:--|:--|:--| | `.changeset/7980-agent-key-envelope-read.md` | "(the `objectstack-ai#9934` channel, whose presence *is* the marking)" | "(the objectstack `79c46da90` channel, …)" | | app-shell `index.ts`, the note above the `readEnvelopeFailureText` export | "(the `objectstack-ai#9934` channel, which rides through the 5xx prose withhold untouched)" | "(the objectstack `79c46da90` channel, …)" | | app-shell `utils/apiErrorEnvelope.ts`, the `userMessage` bullet | "the END USER (`objectstack-ai#9934`)" | "the END USER (objectstack `79c46da90`)" | | app-shell `utils/apiErrorEnvelope.ts`, the `errorFromThrown` bullet | "… has emitted the channel since `objectstack-ai#9934`" (a quotation) | "… has emitted the channel since [objectstack `79c46da90`]" (**Special cases** 1) | | app-shell `PackageFormDialog.tsx`, `readEnvelopeUserMessage`'s docblock | "(`error.userMessage`, `objectstack-ai#9934`)" | "(`error.userMessage`, objectstack `79c46da90`)" | | app-shell `StudioDesignSurface.tsx`, the package-switcher failure read | "the producer's `objectstack-ai#9934` channel" | "the producer's channel (objectstack `79c46da90`)" | | app-shell `packages-io.ts`, `duplicatePackage`'s failure arm | "has emitted since `objectstack-ai#9934`" | "has emitted since objectstack `79c46da90`" | | components `containers.tsx`, the `PageCardRenderer` `body` read | "objectstack#5775 (PR `objectstack-ai#6281`, ADR-0087 D2)" | "objectstack#5775 (objectstack `85ec26d28`, ADR-0087 D2)" | | components `containers.tsx`, the `card` registration's `children` input | "objectstack#5775 (PR `objectstack-ai#6281`)" | "objectstack#5775 (objectstack `85ec26d28`)" | | plugin-grid `hooks/useRecordCrudVerdicts.ts` | "shipped in objectstack PR `objectstack-ai#8452`" | "shipped in objectstack `27358d517`" | | app-shell `RecordDetailView.tsx`, two comments | "[`objectstack-ai#3546`] …" | "[objectstack#3546] …" | | app-shell `RelatedRecordActionsBridge.tsx` | "[`objectstack-ai#3546`] Intersect the child object's …" | "[objectstack#3546] …" | | plugin-detail `renderers/record-details.tsx` | "[`objectstack-ai#3546`] Also AND inline-editability …" | "[objectstack#3546] …" | | plugin-form `fieldWriteGate.ts` | "`objectstack-ai#3546` intersects that with the server's effective API operation set" | "objectstack#3546 intersects …" | | plugin-detail `index.tsx`, the `inlineEdit` input comment | "(`objectui#3546`)" | "(objectstack#3546)" | Each `objectstack-ai#9934` sentence is about the `userMessage` channel that `79c46da90` added; each `objectstack-ai#6281` sentence is about the `page:card` `body` retirement that `85ec26d28` carries; the `objectstack-ai#8452` sentence is about the batch `recordIds` form that `27358d517` shipped. ## Special cases (the judgement calls) 1. **A quotation.** The `apiErrorEnvelope.ts` bullet quotes the note beside `sendThrownError` in objectstack's `packages/rest/src/package-routes.ts`, verbatim. That note still reads "since objectstack-ai#9934" on objectstack `main` (`e956924` at the time of this PR); there the number means objectstack's card, which answers 404, and here it resolves to the unrelated objectui item. The quote keeps its words and takes the commit in square brackets, the editorial mark for a substituted word. The note's claim is the one `79c46da90` carries: its `http-dispatcher.ts` hunk adds the `userMessage` spread to the dispatcher door's `extra`. 2. **Line breaks moved** only where the stand-in made a line markedly longer: app-shell `index.ts`, `StudioDesignSurface.tsx` and `packages-io.ts`. Elsewhere the one line just grows, as batch 7's `objectstack#3391/objectstack#3546` lines did. 3. **Wording kept.** "the `objectstack-ai#9934` channel" becomes "the objectstack `79c46da90` channel" in two places, and "the producer's channel (objectstack `79c46da90`)" in `StudioDesignSurface.tsx`, where "the producer's objectstack … channel" would read as if the producer were objectstack. No other word moves. ## The literal-anchor sweep (both test-pin classes, ruling `5861900779`) - **Instrument.** Every string, template and regex literal in the 4108 tracked test and script files (104609 distinct strings, 2339 regexes), read with the TypeScript scanner. - **Test.** Does its occurrence count DROP between `665025908` and this head in any of the 14 changed files, raw or comment-flattened? - **Result.** 30 distinct literals drop, every one generic: digits, punctuation, whitespace and character classes (`#\d+`, `\d{3}`, `[:#]`, `\s+`), the bare words `objectui` and `objectui#`, `/objectui#\d+/`, and comma-and-line-break fragments. None is a changed phrase, a dead number or a `objectstack-ai#3546` tag. A search of tests for the changed spellings finds them only in test doc comments (listed in **Acceptance notes** 1), which no assertion reads. ## Held **By the serial rule: nothing.** Open PRs were mapped at the branch point before the first edit (7 open) and again before the push (7 open; objectui#11003 and objectstack-ai#10997 have since merged, and objectui#11010 and objectstack-ai#11006 have opened). - **objectui#10990** and **objectui#11010** share `packages/app-shell/src/index.ts`. objectui#10990's hunk is in the side-effect import list, near the Cloud widgets; objectui#11010's is one line in the `views` type export list. This PR's two changed lines are in the note above the `readEnvelopeFailureText` export, more than ninety lines above both hunks. - **objectui#5400** (Version Packages) carries `.changeset/7980-agent-key-envelope-read.md`, which it consumes at release; it regenerates and is not a hold. If it lands first, that file's one changed line has already published, and this PR's hunk there conflicts with the deletion. - Trial merges with this head (`git merge-tree --write-tree`): clean for objectui#11010, objectstack-ai#11006, objectstack-ai#10990, objectstack-ai#10930 and objectstack-ai#10777. objectui#10278 conflicts in `ObjectGrid.tsx`, `plugin-grid/README.md` and `content/docs/plugins/plugin-grid.mdx`, and conflicts in the same three files against the branch point alone; none is in this diff. ## Changesets - `.changeset/10803-dead-citation-sweep-eighth-batch.md`, EMPTY frontmatter. It covers the comment-only edits in 5 released packages (`app-shell`, `components`, `plugin-detail`, `plugin-form`, `plugin-grid`) and the one edited pending changeset; no published behaviour changes through them. It writes no bare number itself. - No runtime string moves, so there is no `patch` changeset (amendment `5860244997` Q1 = A applies to none of these lines: the `plugin-detail` `index.tsx` line is a comment beside the `inlineEdit` input, not its `description` string). ## Proof of prose-only (C4), against `665025908` - **Source.** Each of the 12 touched `.ts` / `.tsx` files was parsed at `665025908` and at this head with TypeScript 6.0.3's `createSourceFile`, and re-printed by `createPrinter({ removeComments: true })`: 12 of 12 prints are identical, with 0 parse diagnostics. Lit controls on the same instrument: editing a string literal moves the print; re-spacing a comment does not. - **Changesets.** The frontmatter block of `.changeset/7980-agent-key-envelope-read.md` is byte-identical at `665025908` and this head (md5); the overwrite gate below agrees. - **Scope of the diff:** 14 files, +39 / −20: 1 edited and 1 new changeset, and 12 non-test source files in 5 released packages. No test file. ## Gates, on this head `8fe632d96` Each line is the gate's own verdict and exit code, captured by redirect-then-`$?`. - `node scripts/check-changeset-presence.mjs`, exit 0: "12 source file(s) of 5 released package(s) changed, and this change declares 1 changeset(s): .changeset/10803-dead-citation-sweep-eighth-batch.md." - `pnpm changeset:check`, exit 0: "All workspace packages are in the changeset fixed group." / "No changeset declares a `major` bump." - `node scripts/check-changeset-overwrite.mjs` (report-only), exit 0: "1 changeset(s) added, 1 modified, 0 deleted"; for the modified one, `declared at base` equals `declares now` ("@object-ui/app-shell: minor, @object-ui/console: patch"). - `pnpm check:changeset-claims` (report-only), exit 0: "Every package declared across those 1 body(ies) is either not negated …", and the standing notice "15 pending changeset(s) describe a file this change touches". Read against the diff: a pending changeset quoting a replaced spelling would carry a bare number the census sees, and the census reads 0 in `.changeset/`. - `pnpm check:control-bytes`, exit 0: "check-control-bytes: OK (scanned 9291 tracked text file(s); skipped 85 binary)." A `grep -P` control-byte self-scan of the 14 files finds none. - `pnpm check:new-line-citations`, exit 0: "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0". - `pnpm check:pending-changeset-literals`, exit 0: "No test source names a pending changeset." - Also run over the touched comments, on the tree this head carries: `pnpm check:spec-symbols` exit 0 ("spec member citations: 1424 sources + 184 documentation pages; nothing cites a key its spec symbol does not declare."); `pnpm check:installed-pin-claims` exit 0 ("OK"); `pnpm check:comment-mask-corpus` exit 0 (1 disagreeing file, within the ceiling objectui#7882 holds open); `node scripts/check-hand-rolled-comment-mask.mjs` exit 0 ("OK every carrier is a DEBT entry, and every DEBT entry still carries one."); `pnpm check:handler-key-reads` exit 0 ("every judged read is a declared member of its arm"). - The governed-surface predicate over the 14 paths, exit 0: "NOT GOVERNED — 14 path(s) checked against 5 governed surface(s); none matched." Lit control `AGENTS.md`: exit 3. **Tests and type-check**, through the shared verify lock, on `8fe632d96`. - `scripts/__tests__/`, the whole directory, whose whole-tree scanners read the touched files and changesets: `Test Files 177 passed | 2 skipped (179)`, `Tests 5350 passed | 2 skipped (5352)`, `VERDICT command-exit 0`. - `packages/components/`, `plugin-detail/`, `plugin-form/` and `plugin-grid/`, whole packages, in one run: `Test Files 847 passed | 2 skipped (849)`, `Tests 8441 passed | 33 skipped (8474)`, `VERDICT command-exit 0`; the source files were byte-identical to this head's. - `packages/app-shell/`, whole package: `Test Files 1 failed | 863 passed | 1 skipped (865)`, `Tests 1 failed | 8835 passed | 9 skipped (8845)`. The one failure is `AppContent.inaccessibleAppStrand.test.tsx`, 'bounces to the DECLARED landing where the deployment declares one', a network-escape guard trip (a real socket at `localhost:3000/api/v1/meta`) under the full suite on a shared box. That file is not in this diff, and C4 shows no code token moves; re-run alone three times on this head it reads `Tests 6 passed (6)` each, and the CI test shards are green on this head. - Type-check: `turbo run build` of the 28-package dependency closure (`Tasks: 28 successful, 28 total`), then `pnpm --workspace-concurrency=2` with the 5 package filters `run type-check`: 5 script echoes, 5 `Done`, `VERDICT command-exit 0`. - No red leg: the sweep found no anchor to move, so there is no pin whose old copy should fail. CI on `8fe632d96`: 43 check-runs, 39 success, 3 skipped, 1 failure; `Spec Main Shape Gate` success. The failure is `Bundle Analysis`, step "Check console performance budget", which fails the same way on `main` at this PR's branch point `665025908` and at `d6d8fb9d7` and `2049b03df` after it; this diff moves no code token. ## Acceptance notes 1. **Outside the card's two classes, as it stands** (whole tree at this head, bare spelling): `objectstack-ai#9934` in 3 test doc-comment lines in 2 files and in 1 `apps/console` source line (`AgentConnectSection.tsx`, "the objectstack-ai#9934 channel"); `objectstack-ai#6281` in 1 test doc-comment line; `objectstack-ai#8452` in 1 test doc-comment line; bare `objectstack-ai#3546` in 23 test lines in 14 files and 19 published `CHANGELOG.md` lines in 8 files. `apps/console`, tests and `CHANGELOG` history are outside the classes this card carries (triage `5858031244`; release `5861553562`: recorded, not carried). 2. **Not measured: `framework#N` and `cloud#N`.** The two classes also cite 82 distinct `framework#N` and 43 distinct `cloud#N` numbers. Neither repository is on this card's lists, and both answer 403 through this container's proxy, so no reading of them is offered. 3. **Left as the brief allows.** The two test comments that call objectui#8652 `pm:blocked` (it reads `pm:on-hold`) and batch 7's sweep-changeset phrase "declared separately below" are in files this PR does not touch, and stay. --- _Generated by [Claude Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_ Co-authored-by: Claude <noreply@anthropic.com>
…jectui#10998) (objectstack-ai#11029) Fixes objectstack-ai#10998 Clause-②: no A console page's localization of two server codes it already receives; no published type, schema, spec row or documented contract moves. ## What changed - **`apps/console` `RegisterPage`**: the error-code map it passes to `RegisterForm` gains `SELF_REGISTRATION_CLOSED` → `auth.register.errors.selfRegistrationClosed` and `EMAIL_DOMAIN_NOT_ALLOWED` → `auth.register.errors.emailDomainNotAllowed`. A code the page does not map still shows the server's own message. - **`packages/i18n`**: all ten locale packs carry both keys beside `auth.register.errors.userExists`. No existing value changes. - **Pin**: `registerRefusalCodes-10998.test.tsx` in the console's auth tests. - **Changeset**: `patch` for `@object-ui/console` and `@object-ui/i18n`. No server change, no new error code, no auth-flow change. ## Premise, checked against `origin/main` before editing - **Fall-through.** `RegisterForm`'s submit handler sets `(code && errorMessages?.[code]) || authError.message`, so an unmapped code renders the server's `message`. `createAuthClient().signUp` keeps the server's `code` on the thrown error (`toAuthError`). - **The codes and the door.** objectstack `origin/main` (`fb386074`): `packages/plugins/plugin-auth/src/audience-posture.ts` exports `SELF_REGISTRATION_CLOSED` and `EMAIL_DOMAIN_NOT_ALLOWED`, and `decideAudienceAdmission` returns them for the `invite_only` and `email_domain` postures. The `/sign-up/email` before-hook in `AuthManager` (`auth-manager.ts`) throws `APIError('FORBIDDEN', { code, message })` from that verdict, so `POST /api/v1/auth/sign-up/email` answers 403 with either code. Both are rows of the spec's error-code ledger (`error-code-ledger.zod.ts`). - **Where the strings live.** `auth.register.errors.userExists` is defined in all ten packs (en, zh, ja, ko, de, fr, es, pt, ru, ar), and `all-locales-key-parity` requires every pack to define every `en` key. So both new keys are in all ten. ## Wording - zh: 「本环境未开放自助注册,请联系管理员获取邀请。」 and 「该邮箱的域名不允许在此注册,请使用组织邮箱,或联系管理员获取邀请。」. The punctuation is full-width, as in the rest of the zh pack. - en: "Self-registration is not open on this environment. Ask an administrator for an invitation." and "This email's domain is not allowed to register here. Use your organization email, or ask an administrator for an invitation." - The other eight packs are translated, not copied from English, because their neighbouring `userExists` is translated in every pack. "Ask an administrator" follows each pack's existing wording for that phrase. - No internal terms: no posture name and no code name reaches the user. ## Verification (at `7a33b2363`, the head of this branch) - **Pin.** `pnpm exec vitest run apps/console/src/pages/auth/__tests__/registerRefusalCodes-10998.test.tsx`: `Tests 5 passed (5)`. It renders the shipped page, the real `RegisterForm`, a real `AuthProvider` over a real `createAuthClient`, and a real `I18nProvider`. Only `fetch` is stubbed, and it answers `/sign-up/email` with `403 { code, message }`. Under zh, each code shows the zh pack's text and not the server's message. An unmapped code still shows the raw server message. Under en, the en pack text shows. The expected text is read from the packs, so the pin holds the mapping and not the wording. - **Red leg**, against `main`'s map, with the fix committed at `8e62629df`. `RegisterPage.tsx` was replaced by its merge-base blob: `616839c0` on disk, equal to the base blob, with 0 occurrences of `SELF_REGISTRATION_CLOSED`. Result: `Tests 3 failed | 2 passed (5)`. The zh case received `"Self-registration is closed on this environment (audience posture invite_only). Ask an administrator for an invitation."`, which is the card's symptom. It was restored with `git checkout HEAD --`. Blob `91230581` equals the HEAD blob, and `git diff HEAD` is empty. - **Suites.** `pnpm exec vitest run apps/console/ packages/i18n/`: `Test Files 202 passed (202)`, `Tests 2644 passed | 13 skipped (2657)`. That is all 129 console test files and all 73 i18n test files, including `all-locales-key-parity` and `untranslated-identity-4376`. - **Type-check.** `pnpm --filter @object-ui/i18n run type-check` exited 0. `pnpm --filter @object-ui/console run type-check` exited 0, after building the console's dependency closure with turbo `--filter=@object-ui/console^...`. Both touched console files are in its `tsc` program (`--listFiles`). - **ESLint** on the 12 touched source files (`--format json`, `--no-inline-config`): 12 linted, 0 errors, 2 warnings. Both warnings are `react-hooks/set-state-in-effect` on the page's two pre-existing effects, on lines this diff does not touch. The root `eslint.config.js` is the only config and is not type-aware, so this diff cannot move a verdict on an untouched file. The repo-wide `pnpm lint` is CI's. - **Gates**, all exit 0: `check:i18n-keys`, `check:i18n-drift` ("2 key(s) added", 0 en values changed), `check:i18n-dead-keys`, `check:i18n-designer-parity`, `check:control-bytes`, `check:new-line-citations` ("0 new citation(s)"), `check-changeset-presence`, `changeset:check`, `check-changeset-overwrite`, `check:changeset-claims`, `check:pending-changeset-literals`, `check-lint-coverage`, `check-test-path-roots`, `check-vi-mock-inherit`, `check-vi-mock-override-shape`, `check-vi-mock-specifiers`, `check-type-check-coverage`, `check:phantom-deps`, `check:self-import`, `check:unreferenced-sources`, `check:esm-specifiers`, `check:handler-key-reads`. - **Eager closure.** No import is added to the console. `en` is the eager catalogue and grows by two strings. After `pnpm --filter @object-ui/console run build`, `check:eager-closure` passes with `i18n-locale-en 40.9 KB / 48.8 KB ceiling` and aggregate headroom 44.2 KB. `check:eager-locale-catalogues` passes: exactly one catalogue is eager. - **`scripts/__tests__`.** The 65 files whose source names a touched path (by `git grep`) passed: `Tests 2782 passed`. The other 114 tracked test files there were also run: `112 passed | 2 skipped` files. - **`check:changeset-claims`** reported one pending changeset naming `en.ts`. That is the `console.nav` paragraph of `6661-app-launcher-nav-menu-renderers.md`. This diff does not touch `console.nav`, and the paragraph stays true. ## Acceptance notes - `@object-ui/app-shell`'s `DefaultRegisterPage`, which `examples/console-starter` mounts at `/register`, passes no `errorMessages` at all. On that page every sign-up refusal, the user-exists codes included, shows the server's message. It is outside this card's claimed file surface and is not touched here. Carrier: none. - The same `/sign-up/email` door also answers `AUTH_CONFIG_ERROR` when the audience configuration itself is unusable. That message is for the operator and names the setting. It is left unmapped on purpose (the card names two codes) and shows raw, as any unmapped code does. Session: `session_01DuWo5bdP9SdVebamn99GGk` (dispatched `os-dev`, `domain:ui` seat objectstack-ai#1). --- _Generated by [Claude Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…through the i18n catalogue (objectui#11039) (objectstack-ai#11066) Fixes objectstack-ai#11039 Clause-②: no ## What The form family's own feedback chrome now resolves through the i18n catalogue instead of English literals: the default success toast, the note it carries when a declared `navigateOnSuccess` was refused, the thank-you heading, the loading line, the load-failure heading, and the default submit and cancel labels. plugin-form goes through `createSafeTranslation`: a new shared hook in `formChrome.ts` (read by `ObjectForm`'s default arm, `TabbedForm`, `SplitForm` and `MasterDetailForm`) and the tables `WizardForm`, `DrawerForm` and `ModalForm` already had. `pnpm check:i18n-keys` holds every table to the `en` pack, so a provider-less host still renders English. The console's `FormPage` uses the console's own hook, `useObjectTranslation`, with the same pack keys; `main.tsx` mounts the `I18nProvider` above every console route. An authored value still wins, and it is untouched: each default sits behind the same `||` / `??` as before, after objectui#11050's resolution of the seven `I18nLabel` members (a locale map, or a plain string). No schema key is added, renamed or retyped. ## The enumeration (on `origin/main` at `6466a09df`, after PR 11050) | English default, as it was | Where | Author key that overrides it | Key now | |---|---|---|---| | `Created` / `Saved` success toast | default arm (three toasts, plus the confirmation after a refused `redirect`), `WizardForm` (the same four) | `successMessage`; a `thank-you` behaviour's `message` | `form.created` / `form.saved` (new) | | `Created`, `Saved`, and "TITLE saved" | `MasterDetailForm`'s built-in save toast (shown only without a host `onSuccess`) | `title` names the record; no message key | `form.created`, `form.saved`, `form.savedNamed` (new, `{{title}}`) | | the refused-navigation note | default arm, `WizardForm` | none: chrome | `form.navigateRefused` (new) | | `Thanks!` | default arm, `WizardForm` | a `thank-you` behaviour's `title` | `publicForm.thankYouTitle` (existing) | | `Loading form...` | default arm, `TabbedForm`, `SplitForm`, `WizardForm`, `DrawerForm` | none | `publicForm.loading` (existing) | | `Error loading form` | those five and `ModalForm` | none | `form.errorLoading` (new) | | `Create` / `Update` submit label | default arm (both render branches), `TabbedForm`, `SplitForm`, `DrawerForm`, `ModalForm` | `submitText` | `form.create` / `form.update` (existing) | | `Cancel` | `DrawerForm`, `ModalForm` | `cancelText` | `common.cancel` (existing) | | `Save` / `Create` / `Cancel` | `MasterDetailForm` | `submitText` / `cancelText` | `common.save` / `form.create` / `common.cancel` (existing) | | `Loading…` | `FormPage` | none | `common.loading` (existing) | | `Submitted` toast | `FormPage` | none | `form.submitted` (new) | | `Thanks!` (two panels) | `FormPage` | the form's `thank-you` `title` | `publicForm.thankYouTitle` | | `Your submission has been received.` (two panels) | `FormPage` | the form's `thank-you` `message` | `publicForm.thankYouMessage` (existing) | The card named the toast, `Thanks!`, the loading line, the load-failure heading and the note. The submit and cancel defaults are the same class in the same files (the brief's example); `FormPage`'s loading line, its `Submitted` toast and the message under its `Thanks!` are the card's classes in the card's file. Beyond this line, see Acceptance notes. ### Keys: reused where one already meant the same thing - `publicForm.loading` (`Loading form…`), `publicForm.thankYouTitle` and `publicForm.thankYouMessage` were in all ten packs with nothing reading them: `pnpm check:i18n-dead-keys` listed all three as confirmed dead at the merge base. They say exactly this, so they are reused, and they are off that list now (105 confirmed dead before, 102 after). - `form.saveSuccess` (`Saved successfully`, also confirmed dead) is not reused for the toast: it has no no-argument create twin (`form.createSuccess` takes `{{object}}`), so reusing it would either pair `Created` with `Saved successfully` or rewrite both toasts' English on every form. `form.created` / `form.saved` keep the toast byte-identical in English. - `MasterDetailForm`'s edit Save is `common.save`, not `form.saveRecord`: seven packs word `form.saveRecord` as "save record" (de `Datensatz speichern`), while this button's English is the plain verb, which `common.save` is in every pack. - No existing key says "Error loading form"; `form.errorLoading` follows `grid.errorLoading`. ### `NAVIGATE_ON_SUCCESS_REFUSED_NOTE`: an end user sees it, so it joins It is passed as the `description` of `toast.success(...)` (sonner, through `@object-ui/components`), which renders under the toast's title, so the submitter reads it. Its own doc block says so ("What the submitter is told"), quoting the maintainer ruling of 2026-08-17: "the success toast carries a note that the declared navigation was not performed". The zh pin reads it on the toast (red on the base). The constant still exists: it is now read from the `formChrome.ts` table, so it has one spelling, and `WizardForm` re-exports it, because the tests import it from there. `navigateOnSuccess` stays as written in every pack, since it is the key's name. ### Wording zh: 已创建, 已保存, "TITLE已保存", 已提交, 表单加载失败, and 此表单声明的 `navigateOnSuccess` 跳转目标被拒绝,因此未执行跳转。 The reused keys read 正在加载表单…, 感谢您的提交! and 我们已成功收到您的信息。 The zh `publicForm.thankYouTitle` changes its ASCII `!` to the full-width `!`, the zh pack's punctuation, now that a form shows it. The other eight packs follow each pack's own neighbours (`form.createSuccess`, `grid.errorLoading`, `publicForm.loading`). No new value carries an ellipsis; the reused `publicForm.loading` has U+2026 in all ten packs. **Three English strings change their rendered text**, each because it is now a key whose English differs from the old literal. The changeset says so: - `Loading form...` becomes `Loading form…` (U+2026); - `Thanks!` becomes `Thank you!` (default arm, `WizardForm`, `FormPage`); - `FormPage`'s `Your submission has been received.` becomes `Your submission has been received successfully.` Every other English default renders the same text as before. ## Pins - `packages/plugin-form/src/formChrome.i18n-11039.test.tsx`: 29 cases through `ObjectForm` under a real `I18nProvider`. Each zh case first waits for a probe outside the form to read the zh pack. The zh loading line in the simple, tabbed, split, wizard and drawer forms; the zh load-failure heading in all six; 创建 / 更新 in simple, tabbed, split, drawer and modal; 取消 in drawer and modal; 已创建 (simple, wizard) and 已保存 (simple edit) toasts; the zh thank-you heading (simple, wizard); and the zh note on a refused `navigateOnSuccess` (simple, wizard). Three `CONTROL` cases: an `I18nLabel` map `successMessage` with a plain `submitText`; a map `submitText` with a plain `successMessage` on a wizard, whose toast keeps the authored message and carries the zh note; and an authored thank-you `title` with a map `cancelText`. Plus an `en` case: `Loading form…`, `Error loading form`, `Create`, `Created` with the English note, and `Thank you!`. - `apps/console/src/components/FormPage.chromeI18n-11039.test.tsx`: 5 cases on the real `/f/:slug` route, with nothing wrapped around it: the zh loading line 加载中…, the zh toast 已提交, the zh thank-you heading and message, a `CONTROL` with a declared thank-you `title` and `message`, and `en`. **Red leg**, at `80db9c47a`. The eight edited containers (`ObjectForm`, `WizardForm`, `TabbedForm`, `SplitForm`, `DrawerForm`, `ModalForm`, `MasterDetailForm`, `FormPage`) were replaced by their merge-base blobs, each hash-checked on disk (`t('form.errorLoading')` in `ObjectForm.tsx` went from 1 to 0, and the `Loading form...` literal came back). Everything else stayed at HEAD. Over the two pins and the eight moved fixture files: **43 failed, 52 passed**. The formChrome pin: 27 of 29 fail. Two `CONTROL` cases pass, because authored values always won, so they are the must-not-change guards. The wizard `CONTROL` fails only on the note, which is chrome. The FormPage pin: 4 of 5 fail, and its `CONTROL` passes. The script restored with `git checkout HEAD --` under a `trap`, verified every file against its HEAD blob hash, and read an empty `git diff HEAD`. Green at the same head: 29 and 5 passed. ## Fixtures that moved with the change (pin updates, not regressions) - `ObjectForm.i18nLabels.test.tsx` (zh): the create button is 创建, and the nothing-authored control's toast is 已创建. - `MasterDetailForm.i18nLabels.test.tsx` (zh): the toasts read 采购单已保存 and PO已保存, and the nothing-authored control reads 创建, 取消, 保存 and 已保存. - `ObjectForm.submitRedirect.test.tsx`, `WizardForm.submitRedirect.test.tsx`: `Thank you!`. - `FormPage.submit`, `.outcomeToast`, `.recordId`, `.redirect`: each now installs the en catalogue the way the app does (`createI18n`, whose global the harness resets after every test), and asserts the new message. The negative assertions moved to the new string too; left alone they would have passed vacuously. - `examples/schema-catalog/test/catalog-gallery-render.test.tsx`: `FORM_LOADING` is `Loading form…`. Its wait is negative (`not.toContain`), so the old literal would not have turned red: it would have silently stopped waiting. The comment's stale cross-file line citation is now a content citation. Also: the pending objectui#10935 changeset said the master-detail defaults "are still English". Its prose now says this change moves them; its frontmatter is untouched (`check-changeset-overwrite` reports it under its case 2, a deliberate correction; report-only). The `object-master-detail-form` registry descriptions of `submitText` / `cancelText` now say the default is in the active UI language. ## Verification (all at `80db9c47a`) - `pnpm exec vitest run packages/i18n/ packages/plugin-form/`: Test Files 216 passed (216), Tests 2910 passed, 14 skipped. This includes `all-locales-key-parity`, `en-zh-key-parity`, `ellipsis-glyph-3878`, `untranslated-identity-4376`, `de-quote-pairing-3876` and `raw-key-call-sites-3546`. - `pnpm exec vitest run apps/console/`: Test Files 131 passed (131), Tests 1475 passed. - `packages/plugin-view/`, plus app-shell's `ObjectFormDesigner.test.tsx` and `RecordFormPage.i18n.test.tsx`, plus the gallery test: 63 files, 1207 passed. - `scripts/__tests__`, derived with `git grep -l -E` over the touched names and paths (the form files, `FormPage`, `formChrome`, `i18n/src/locales`, `plugin-form/src`, `.changeset`, `schema-catalog`, `apps/console/src/components`): 46 files, Tests 2171 passed. - Type-check: `@object-ui/i18n` and `@object-ui/plugin-form` Done, and `@object-ui/console` exit 0, each after building its dependency closure. `tsc -p tsconfig.test.json --listFilesOnly` lists the plugin-form pin, and the console's `--listFilesOnly` lists its pin. - `pnpm check:i18n-keys` exit 0 (37 factory tables, 912 rows, 912 matching). `pnpm check:i18n-drift` exit 0 (0 en values changed, 6 keys added). `pnpm check:control-bytes` OK. `pnpm check:new-line-citations`: 0 new. `node scripts/check-changeset-presence.mjs`, `pnpm changeset:check` and `pnpm check:pending-changeset-literals` pass. `pnpm check:changeset-claims` (report-only) names seven pending changesets that mention a touched file (`6237-wizard-step-config-split`, `6661-app-launcher-nav-menu-renderers`, `8738-object-form-fields-description`, and four `console-form…` ones). All re-read, all still true: each is about another aspect of those files. - eslint on the 31 touched ts/tsx files (`--format json` reports 31): with inline config honoured, as each package's `lint` runs, 0 errors and 210 warnings; with `--no-inline-config`, 1 error and 217 warnings, and those per-file counts equal the merge-base blobs linted the same way through `--stdin --stdin-filename`. The one error is an existing, inline-suppressed `react-hooks/static-components` in `FormPage`'s widget row. The narrowing excludes nothing: the population is the changed files from the diff against the merge base, `eslint.config.js` sets no `parserOptions.project` or `projectService` (no type-aware linting), and no rule under `eslint-rules/` reads a file, so this diff cannot move a verdict on an untouched file. ## Bundle Console built at the merge-base sources and at HEAD: the sources were swapped, rebuilt, restored and rebuilt, and a marker was checked in dist both ways. The eager closure is 330 of 2446 chunks both times, so no chunk entered it, and 3,181,419 became 3,181,741 bytes gzipped (+322; raw +1,788). By chunk: `i18n-locale-en` +241 raw / +82 gzipped (the en pack), `plugin-form` +1,471 / +252, `index` (`FormPage`) +76 / −7. No new external module entered either chunk: `FormPage` already imported `@object-ui/i18n`, and the new `formChrome.ts` imports only `createSafeTranslation`, which plugin-form already did. `check-eager-closure-budget`: 3107.2 KB of 3149.4 KB (headroom 42.2 KB, 42.6 KB at the base). `check-eager-locale-catalogues`: exactly one eager catalogue, `en`. ## Acceptance notes Left English on purpose. Each is chrome, not a default behind an author key the card named, and each is noted rather than widened into here: - `FormPage`: the submit button's `Submit` / `Submitting…` / `Uploading…`, `Redirecting…`, and the `Required: …` refusal. The zh pin submits through that English `Submit` under a zh catalogue. - `MasterDetailForm`'s line-item chrome: `Loading columns…`, `Subtotal`, `Tax (…%)`, `Total`, `Line item — row N`, `Apply`, `Add`, and `Saving…` on its Save button. - `ModalForm`'s master-detail dialog: its `sr-only` description, `Enter the record and its line items, then save.` - `ObjectForm`'s field-security hint, `You do not have edit access to this field.` - `EmbeddableForm` takes its chrome from host-supplied `texts` (the `publicForm.*` vocabulary in `public-forms.md`), which is a different mechanism. No README or `content/docs` page describes these defaults (grepped for the literals; the one `Thanks!` in `public-forms.md` is an authored `title` in an example), so no docs change is due. Dispatched dev run for the `domain:ui` seat objectstack-ai#1, session `session_01DuWo5bdP9SdVebamn99GGk`, claim comment 5885696581. --- _Generated by [Claude Code](https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…and row queries, so the groups are the searched ones (objectui#11021) (objectstack-ai#11178) Part of objectstack-ai#11021 Clause-②: no Dispatched by the `domain:ui` seat 2 PM loop, session `https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec`, claim comment 5904959620 on objectui#11021. ## What this does A grid that groups on the server (objectui#7189) asks two queries: the group header query (the set of groups and every count) and one row query per open group. Each group's row query stripped `$search` / `$searchFields`, and the header query was asked with `where` only. So a grid carrying a search term drew the UNSEARCHED groups, and said nothing about it. `@objectstack/spec` 17.5.0, installed here since objectui#11086, declares ADR-0061 `search` / `searchFields` on `EngineAggregateOptions`, and the platform's grouped branch honours them (objectstack#20487). This PR sends the term on BOTH queries, as one pair: - `packages/plugin-grid/src/useServerGrouping.ts` - `useServerGroupHeaders` takes `search` / `searchFields` and sets them on every depth's compiled `EngineAggregateOptions`, keyed on content. - Each group's row query no longer strips `$search` / `$searchFields`. The stripping and its comment are retired. `$filter` is still replaced, because the view's filter is already inside the compiled group `where`. - New export `groupSearchOf` reads the header's pair off the very row query each group's page is asked with, so the two cannot disagree. - `packages/plugin-grid/src/ObjectGrid.tsx`: only the one `useServerGroupHeaders` call changes. It spreads `groupSearchOf(groupRowQuery)`. The import line changes with it. - `@object-ui/data-objectstack` is unchanged, and this was measured rather than assumed: - `queryGroupHeaders` hands the compiled query to `client.data.query`, which posts it verbatim to `POST /data/:object/query` (read in the installed `@objectstack/client` 17.5.0). - The platform's grouped branch passes `search` / `searchFields` to `engine.aggregate`. That is objectstack `1c1b8c80`, and `git merge-base --is-ancestor` against the `@objectstack/spec@17.5.0` tag answers exit 0. - The row pages now take the adapter's raw-GET route, which a search already selects. That route lowers the object-form `$filter` through the same `convertFiltersToAST` as the non-search route, and grouped rows with `$expand` already used it. - Docs: the plugin-grid README and `content/docs/plugins/plugin-grid.mdx` said "A toolbar search has no counterpart on the group header query". This PR makes that false, so the paragraph now says the grid sends the pair on both queries. The `ListView` sentence stays, because it is still true. - Changeset: `'@object-ui/plugin-grid': patch`. ## Why `Part of`, and the half this PR leaves Measured on `origin/main` `0ffc423b1` with a throwaway render probe, which is not committed: 1. **A server-grouped `ObjectGrid` renders no search box.** The grid's term has one writer, the flat table's box. When grouped, the grid draws group sub-tables with `searchable: false`, and a toolbar holding only row height and export. Probe: the flat grid shows the box; the grouped grid shows none, and the DOM holds 0 inputs. 2. **`ListView` withholds the grouped fetch from the grid while its toolbar search is active.** The console's list views are ListView. Its `gridOwnsGroupedFetch` ends in `!searchTerm`, with the comment "`$search` has no counterpart on the header query the platform answers". It fetches one window with `$search` and hands that window to the grid, which groups it in the browser. Once the matches exceed the page size, the counts are the window's. So today the fixed code has one door: a term typed while the grid is flat, then grouping switched on at runtime. At that door, before this PR, with the term `Plant`: - the header query carried no `search`; - 0 of 5 group row finds carried `$search`; - the screen drew all five unsearched groups (86/61/31/1/7). The card's first acceptance line ("in a grouped list view, a toolbar search shows only the groups that contain matching rows") needs the other half through `ListView`: it must lift its `!searchTerm` carve-out and hand its term to the server-grouped grid. That is outside this dispatch's file surface and needs a channel decision, so it goes to the seat and is not done here. This PR is that half's prerequisite. Without it, a term handed to the grid would still be stripped from the rows and never reach the headers. ## Pins: `packages/plugin-grid/src/__tests__/serverGroupedSearch-11021.test.tsx` The pins run through the real grid, against a double that answers both queries with ONE matcher. The term `objectstack-ai#1` matches a SUBSET of each unit (numbers 1 and 10 to 19), so a search sent on only one of the two queries makes the counts and the rows disagree. - **SEARCHED** - The headers read `Northgate Operations 11 · Northgate Plant 11 · Northgate Quality 11 · Riverside Plant 1`. The Depot's one row does not match, so it has no group. - Each group draws exactly the rows its header counted. - The header query equals the spec's compiled query plus `search` / `searchFields`. - Every group row page carries the same `$search` / `$searchFields`. - **UNSEARCHED**: neither query carries a search key, and the five units read 86/61/31/7/1. - **Clearing** - Back in flat mode, the box still holds the term. - Clearing it and grouping again restores 86/61/31/7/1. - The settled header query, and each unit's latest row page, carry no search. **Ablation.** One-shot, from the committed `aa46977f9`, through `scripts/ablation-replace.mjs` (objectstack). For each leg: - the anchor hit once; - the on-disk anchor count and the blob moved; - the restore was proven by blob == HEAD and an empty `git diff HEAD`, and the tree diff afterwards was 0 bytes. The pin imports `../ObjectGrid` by relative path, so no `dist/` sits in between. - **Leg A**, the header query without the term (the `...groupSearchOf(groupRowQuery),` line deleted): SEARCHED and Clearing go red, `Tests 2 failed | 1 passed (3)`. The failure: `expected { 'Northgate Operations': 86, …(4) } to deeply equal { 'Northgate Operations': 11, …(3) }`. - **Leg B**, the rows strip the term again: SEARCHED goes red, `Tests 1 failed | 2 passed (3)`. The failure: `expected [ 'Northgate Operations #0', …(85) ] to have a length of 11 but got 86`. ## Gates, at HEAD `aa46977f9` Each gate ran from the worktree root, with its exit code captured before any pipe. objectui has no `dispatch-gates` derivation, so this list is hand-derived from the root `package.json` and `.github/workflows/` against the actual diff. - `pnpm exec vitest run packages/plugin-grid/` (under the verify lock): exit 0, `Test Files 171 passed (171)`, `Tests 1582 passed (1582)`. - The new pins together with objectui#7189's suite: exit 0, `Tests 14 passed (14)`, which is 3 new and 11 existing. - The five `plugin-list` suites that render a grouped grid over `queryGroupHeaders` (`ListView.groupedGridOwnsFetch-7189`, `speculativeFls-7216`, `groupingProjection-7179`, `expandFls-7215`, `groupingNeedsHeaderQuery-10881`): exit 0, `Tests 44 passed (44)`. These are not owed, because the package's published surface is unchanged: `useServerGrouping` is not re-exported from the entry. - `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-grid^...' build`, the 14-package closure the type-check resolves through `dist/`: exit 0. - `pnpm --filter @object-ui/plugin-grid type-check`, which echoes `tsc --noEmit && tsc -p tsconfig.test.json`: exit 0. `--listFiles` counts the new pin once in the test program. - `pnpm exec eslint` on the three changed source and test files, with the package's own spelling (its `lint` is `eslint .`): exit 0, with 0 errors. - For both source files, the per-rule warning set is identical to BASE `0ffc423b1`, linted from stdin under the same path. The warnings are pre-existing. - The new pin carries 7 `no-explicit-any` warnings, the same style as the objectui#7189 suite. - Linting is not type-aware (`eslint.config.js` sets no `projectService` or `parserOptions.project`), so this diff cannot move the verdict on any untouched file. - An extra run with `--no-inline-config` answers 1 error. It sits on a base line whose `eslint-disable-next-line react-hooks/static-components` that flag ignores, and it is not this diff. - `node scripts/check-changeset-presence.mjs`: exit 0, "3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)". - `pnpm check:control-bytes`: exit 0. - `pnpm check:new-line-citations`: exit 0, `VERDICT new-cross-file-line-citations: 0 new citation(s)`. - `check-changeset-no-major`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:installed-pin-claims`, `check:test-path-roots`, `check-vi-mock-specifiers`, `check:unreferenced-sources`, `check:spec-symbols`, `check:doc-fences`, `check-doc-links`, `check-doc-expression-carriage`, `check:doc-types`, `check-doc-example-ids`, `check-type-check-coverage` and `check:lint-coverage`: all exit 0. - `check-governed-queue-guard --test` over the six paths: NOT GOVERNED. - NOT MEASURED: `check:readme-exports`. Its reading is a prerequisite refusal ("type entry `./dist/index.d.ts` is not on disk -- run `pnpm build` first", across every README), not a verdict. This diff changes no fenced block in either doc: `git diff` shows 0 changed fence lines. CI builds before it runs the gate. ## Acceptance notes - **Re-entry staleness.** It predates this PR (it has existed since objectui#7189), and this PR does not fix it. - While the grid is flat, `groupRowQuery` keeps the row query of the last grouped phase. On re-entering grouped mode, the header query is asked first with that stale query and then with the fresh one. - Measured after clearing the term: the header queries went out as `search: "objectstack-ai#1"`, then `search: null`. The painted header sets were the stale searched set, then the unsearched one. - The same staleness applies to `$filter`, sort and projection. The clearing pin therefore reads the settled answer. - The fix is to reset `groupRowQuery` when the grid leaves server grouping. That is a second hunk in `ObjectGrid.tsx`, held back to keep this PR to the one hook call: parked draft objectui#10278 conflicts in that file. - In `ListView`, re-entry happens every time a toolbar search is cleared. - **A stale comment.** The comment above `manualSearchOn` in `ObjectGrid.tsx` says that grouped grids "hold every row they display, so their box keeps filtering client-side". A server-grouped grid holds one page per group and has no box. It is left as is, under the same one-hook-call limit. - **The spec compiler takes no search.** `compileListViewGroupQuery` has no `search` option, so the pair is set on the options it compiles, under the spec's own key names. A spec-side option would let the compiler own the pair. It is not needed for correctness. ## Handed to the seat, not done here - **`ListView`'s `!searchTerm` carve-out on `gridOwnsGroupedFetch`** (`packages/plugin-list/src/ListView.tsx`). - Its stated reason is false against the installed 17.5.0 plus this PR. - Under a toolbar search, the console's grouped list view groups one window, so its counts are page slices once the matches exceed the page size. - It is the remaining half of objectui#11021's own acceptance, so the report names it as that card's sub-issue. - The same stale sentence stands in its comment. --- _Generated by [Claude Code](https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…rch groups on the server, with the searched counts (objectui#11021) (objectstack-ai#11243) Fixes objectstack-ai#11021 Clause-②: no The ListView half of objectui#11021. The grid half (PR objectui#11178, merged as `7e4fa1bb2`) made a server-grouped grid send its search term on the group header query and on every group's row query. This PR makes the console's grouped **list view** use it. With this, all four of the card's acceptance lines hold through the list view, so the PR closes the card. ## What changes - **`packages/plugin-list/src/ListView.tsx`**: `gridOwnsGroupedFetch` loses its `!searchTerm` carve-out and its comment. That comment gave "`$search` has no counterpart on the header query" as the reason, which is false against the installed `@objectstack/spec` plus PR objectui#11178. Under a toolbar search, a grouped grid over a data source that answers `queryGroupHeaders` is still handed **no rows**. It gets the list's term as its `search` host prop (an empty one too: the list owns the term), and the view's `searchableFields` on its node, beside the effective `filter` it already carried. `ListView`'s own fetch sends that same pair as `$search` / `$searchFields`. - **`packages/plugin-grid/src/ObjectGrid.tsx`**, only in the three regions the claim names: - **Host search.** `hostOwnsSearch` / `querySearchTerm`: a host that passes `search` owns the term, under host-driven paging (unchanged) and also when the grid fetches for itself. The load effect's `$search` reads that term, so `groupSearchOf(groupRowQuery)` carries it to both group queries. With no `search` prop, the term is the grid's own box, as before. - **Re-entry reset.** `groupRowQuery` goes back to `null` when the grid leaves server grouping. Before, re-entering grouping asked the first header query with the query held from the last grouped phase, and painted that answer before the fresh one (see the measurements). - **Comments.** The false `manualSearchOn` comment is rewritten to say what the code does, and so is the `manualSearch` comment. `ObjectGridExternalPaginationProps` gains one paragraph: `search` is read outside host-driven paging too. - These regions are disjoint from objectui#9547's hunks (merged into this branch from `main`, and the merge was clean). - **`packages/plugin-grid/package.json`**: the `@objectstack/spec` floor goes `^17.4.0` to `^17.5.0` on both lines. `pnpm install` rewrote one lockfile specifier line, and the resolved version did not move (it was already `17.5.0`). - **Prose this change made false** (AGENTS.md objectstack-ai#2): three docs and one comment said a `ListView` keeps grouping its own window under a search. Those are `packages/plugin-list/README.md`, `packages/plugin-grid/README.md`, `content/docs/plugins/plugin-grid.mdx`, and a `useGroupedData.ts` comment. The grid half's still-pending changeset `.changeset/11021-grouped-grid-search-both-queries.md` ended on the same sentence, and that paragraph is removed; this is a prose correction to a not-yet-released declaration, and its package names are untouched. Three plugin-list test comments carried the same claim and are corrected too. - **Changeset**: `.changeset/11021-listview-grouped-search.md`, with `@object-ui/plugin-list` and `@object-ui/plugin-grid` at patch. ## Measurements of the dispatch's mechanism assumptions (all at `origin/main` `3c13675e5f`) 1. **Carve-out: confirmed, and reproduced red first** through the real `ListView` + real `ObjectGrid`, with no source edits. The new pin `packages/app-shell/src/__tests__/listViewGroupedSearch-11021.test.tsx` uses 186 rows in five units (86/61/31/7/1) and the term `objectstack-ai#1`. The term matches 34 rows, and the view's page is 20. On base, the searched grouped list drew `{ Northgate Operations: 11, Northgate Plant: 9 }`: the page of 20 grouped in the browser. The query's answer is `{ 11, 11, 11, 1 }`. The result was `Tests 3 failed | 2 passed (5)`. 2. **Channel: confirmed, one channel.** `ListView` fed the grid no search at all before, since its flat grid is handed a window that is already searched. The grid's existing host prop `search` now carries the term. The measured widening beyond "server-grouped mode" is deliberate. Under `gridOwnsGroupedFetch`, the grid can still fetch a flat or bucketed window of its own, when the grouping key is unreadable or known-masked. Reading `search` only when server-grouped would drop the term there, which is a regression, so a passed `search` is read wherever the grid fetches for itself. Searchable fields ride the node's declared `searchableFields`, the key the grid's own `$searchFields` already reads. No new prop was added. The box stays hidden: a host that owns the term but passes no `onSearchChange` gets none. 3. **Re-entry staleness: confirmed and fixed.** On base, the tightened grid pin (search, flat, clear, regroup) saw a header query after re-entry that carried `search: 'objectstack-ai#1'` and `searchFields: ['subject']`. A second ablation leg (below) switched the query-level check off and left only the paint check. Without the reset, **both** pins then went red on a painted searched header set, `{ 11, 11, 11, 1 }`, before the unsearched one. With the reset, no re-entry header query carries the term and no searched set paints, while at least one set was sampled. 4. **`manualSearchOn` comment: rewritten.** `manualSearchOn` itself does not change. A grouped grid renders no flat table, and each group's table is `searchable: false`, so this flag governs only the flat table. 5. **Floor: raised.** The specifier moved; the resolved version stayed `17.5.0`. `pnpm install --frozen-lockfile` answered "Lockfile is up to date". ## Tests - New `packages/app-shell/src/__tests__/listViewGroupedSearch-11021.test.tsx` mounts the real `ListView` and the real registered `object-grid`. It has five cases: - the fixture precondition: the matches outnumber the page; - SEARCHED: only the groups holding matches, each count equal to its matches, each group drawing exactly those rows, and both queries carrying `search` / `searchFields`; - UNSEARCHED: the five whole units, with no search key on either query; - clearing through the toolbar box, which restores the five units, and whose last header query and each unit's latest row page carry no term; - re-entry: grouping off, the term cleared, grouping on. No header query after re-entry carries the term, and no searched group set is painted. - `plugin-grid` `serverGroupedSearch-11021.test.tsx`: the clearing case now asserts no stale ask and no stale paint on re-entry. It used to tolerate both, in a comment. A new case covers a host's `search` prop driving both queries and clearing them. - `plugin-list`: `ListView.groupedGridOwnsFetch-7189.test.tsx` gains the searched handoff (no rows; `search` and `searchableFields` equal to the list's own `$search` / `$searchFields`; the filter unchanged) and an ungrouped control (a searched window, no term handed). In `ListView.groupingNeedsHeaderQuery-10881.test.tsx`, the case that pinned "a grouped grid handed a searched window" pinned the very branch this PR deletes. It is replaced by the same question asked of the new behaviour: no window and no host paging. **Ablation**, one-shot, from committed `620c07cd76`, through objectstack's `scripts/ablation-replace.mjs`. Every mutation was proven on disk (anchor x1 to x0, replacement x0 to x1, blob moved). Every restore was proven by the blob equal to HEAD and an empty `git diff HEAD`, and no permanent test file was left. The pins resolve `@object-ui/plugin-grid` and `@object-ui/plugin-list` through the vitest alias to `src`, so no `dist/` is in the path. - Leg A, the reset line removed: `Tests 2 failed | 7 passed (9)`. The grid clearing case and the list re-entry case went red on a re-entry header query carrying `search: "objectstack-ai#1"`. - Leg A-paint, the reset removed and both query loops skipped: `Tests 2 failed | 7 skipped (9)`, both on `expect(counts).not.toEqual(SEARCHED)`. - Leg B, `&& !searchTerm` restored: `Tests 5 failed | 10 passed (15)`. SEARCHED, clearing and re-entry through the list failed, and so did the 7189 searched handoff and the 10881 replacement. - Leg C, host `search` read only under host paging: `Tests 4 failed | 5 passed (9)`. The grid host-search case failed, and so did the three searched list cases. ## Gates Branch head `cee5c02158`: `620c07cd76` merged with `main` at `dded788ada`, which brought in objectui#9547. Every exit was captured before any pipe. Heavy runs went through the shared verify lock. **At `cee5c02158`:** - `pnpm install --frozen-lockfile`: exit 0, "Lockfile is up to date". - `pnpm turbo run build --filter='@object-ui/app-shell^...' --concurrency=2`: exit 0, 28/28 tasks. This closure contains both `@object-ui/plugin-list^...` and `@object-ui/plugin-grid^...`. - Type-check, `pnpm --filter @object-ui/plugin-grid type-check && … plugin-list … && … app-shell …`: exit 0. Each run echoed `tsc --noEmit && tsc -p tsconfig.test.json`. - `pnpm exec vitest run packages/plugin-grid/`: exit 0, `Test Files 174 passed (174)`, `Tests 1596 passed (1596)`. - The pins plus the importer set, 14 files: exit 0, `Tests 148 passed (148)`. That is the new app-shell pin, the six changed plugin-list files, `displayPageSizeFromSpec-9853`, four plugin-view `ObjectView` suites, two app-shell `ObjectView` suites and `data-objectstack` `queryGroupHeaders-7189`. - The 18 suites that `scripts/markdown-test-inputs.mjs --list` names as readers of the five changed markdown files (`doc-version-claims.test.ts` among them): exit 0, `Tests 841 passed (841)`. - Exit 0: - `check-changeset-presence` ("10 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)"); - `check-changeset-no-major`, `check-changeset-fixed`; - `check-changeset-claims` and `check-changeset-overwrite` (both report-only); - `check:pending-changeset-literals`, `check:installed-pin-claims`, `check:control-bytes`; - `check:new-line-citations` ("0 new citation(s)"); - `check:doc-fences`, `check-doc-links` ("Links are valid across 17 scan roots"), `check-doc-expression-carriage`, `check:doc-types`, `check-doc-example-ids`; - `check-lockfile-integrity` ("VERDICT clean"), `check-lockfile-dedupe` ("VERDICT deduped"), `check:phantom-deps`; - `check:test-path-roots`, `check-vi-mock-specifiers`, `check:unreferenced-sources`, `check-type-check-coverage`, `check-lint-coverage`, `check:spec-symbols`; - `markdown-test-inputs --audit`; - `check-governed-queue-guard --test` over the changed paths ("An ordinary pull request"). - **NOT MEASURED:** - `check:spec-floors` exited 1 on 5 `no-artifact` findings, for `app-shell`, `cli`, `plugin-gantt`, `plugin-timeline` and `plugin-tree`: packages outside this diff that this worktree did not build. That is a prerequisite refusal, not a verdict. `plugin-grid`, whose floor moved, was built and judged, with no finding. - `check:readme-exports` exited 1 on 88 "`./dist/index.d.ts` is not on disk" refusals, none of them for the two READMEs changed here. This diff changes 0 fence lines in any doc. **At `620c07cd76`** (the same diff before the merge): - `pnpm exec vitest run packages/plugin-list/`, in two shards: exit 0 both times, 55 + 54 files, 387 + 814 tests. The merge changed no `plugin-list` file. - `pnpm exec vitest run scripts/__tests__/`, in two shards: exit 0 both times, 177 files passed and 2 skipped of 179, 5371 tests passed and 2 skipped. - `tsc -p tsconfig.test.json --listFiles` counts: the new app-shell pin appears once, the grid pin once, and the five changed plugin-list suites once each. - eslint on the 10 changed TypeScript files: 0 errors. Compared per rule with base, the only delta is +1 `react-hooks/set-state-in-effect` in `ObjectGrid.tsx`, from the reset effect. The config is not type-aware, so no untouched file's verdict can move. CI is not waited on; its verdict is the merge queue's. ## Deviations from the declared file surface (each forced by what the change made false, or by the pin the dispatch asks for) - The real-`ListView`-plus-real-`ObjectGrid` pin lives in **`packages/app-shell/src/__tests__/`**, not `packages/plugin-list/src/__tests__/`. `plugin-list` declares no `plugin-grid` edge, so a pin there would need either a `vitest.config.mts` `heavyDomTests` entry (a root config that dozens of `scripts/__tests__` suites read) or an undeclared test import (`check:phantom-deps`). `app-shell` already declares both packages, the route `displayPageSizeFromSpec-9853.test.tsx` takes. - The docs, the `useGroupedData.ts` comment, the pending grid-half changeset and the three test comments listed above. ## Acceptance notes - `ObjectGrid.tsx` gains one `react-hooks/set-state-in-effect` warning, from the reset effect. The file carries that pattern already (its `setSearchTerm('')` reset on an object change), and eslint reports 0 errors. Every other changed file's per-rule warning set is identical to base. - Commits `7470afd89b` and `620c07cd76` end with a `Co-Authored-By` trailer that carries a model name, copied from the harness's attribution reminder. The model-free pair is what AGENTS.md asks for, and the merge commit carries it. History is not rewritten: objectui forbids force-push. Session: `https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ` --- _Generated by [Claude Code](https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… a rule-list filter draws its number (objectui#11526) (objectstack-ai#11535) Fixes objectstack-ai#11526 Clause-②: no ## What this changes An authored `object-metric` whose `aggregate.groupBy` is the structured node (`{ field, dateGranularity }`) and whose `filter` is the bag's rule list (`ViewFilterRule[]`, as `ObjectMetricPropsSchema` in `@objectstack/spec/ui` declares it) passes both authoring faces (`safeValidateSchema`, which `objectui validate` runs, and `StrictAnyComponentSchema`; the pin asserts both, beside the block's own arm `ObjectMetricBlockSchema`), and on `ObjectStackAdapter` it drew `UnloweredAggregateWhereError` instead of its number. `ObjectMetricWidget` now lowers the rule list for the spec-shape aggregate query only, in a new module function `specShapeWhere` in `packages/plugin-dashboard/src/ObjectMetricWidget.tsx`. Files: - `packages/plugin-dashboard/src/ObjectMetricWidget.tsx`: `specShapeWhere`, called at the one spec-shape call site in `computeOne`. - `packages/plugin-dashboard/src/__tests__/ObjectMetricWidget.ruleFilterSpecShape-11526.test.tsx`: the pins. - `.changeset/11526-object-metric-rule-filter-spec-shape.md`: `@object-ui/plugin-dashboard` patch. No `@object-ui/core` edit, no adapter edit, no exported type moves (so `Clause-②: no` holds). ## The lowering, named - **What the legacy aggregate wire applies** (string or absent `groupBy`, the control that draws the number): `lowerAnalyticsFilterForWire` in `packages/data-objectstack/src/index.ts`. It runs `translateFilterArray` (rule list to filter AST, same file) and then `parseFilterAST` from `@objectstack/spec/data` (filter AST to `FilterCondition`). Both adapter functions are module-private. - **Why it is not imported or re-exported.** `@object-ui/data-objectstack` depends on `@object-ui/core`, so a re-export from core would be a dependency cycle. `plugin-dashboard` does not depend on the adapter package and reads any `DataSource` (AGENTS.md objectstack-ai#1). Moving the translator into core would be a logic move in a package this claim does not cover. The brief's re-export fallback is therefore not available; see Acceptance notes. - **What the widget uses instead:** the same two stages, from where they are public. Stage 1 is `toFilterNode` (`packages/core/src/utils/filter-converter.ts`, exported from `@object-ui/core`). It is core's sink for a `ViewFilterRule[]`, and `object-grid` lowers its own rule-list `filter` through it (via `toFilterNodeSafely`). Stage 2 is `parseFilterAST`, the call the adapter makes. No conversion is written in the widget. The `isFilterAST` gate between them hands an array that is not a filter on unparsed, because `parseFilterAST` answers `undefined` for it, which would post an unfiltered aggregate. Handed on, the adapter's refusal answers it. - **One-time measurement (not a standing pin), real adapter, fetch stubbed, `@objectstack/spec` 17.5.0.** I compared `parseFilterAST(toFilterNode(rules))` with the `where` the legacy wire posts for the same rules. They are byte-equal for each operator in `VIEW_FILTER_OPERATORS` (20 at this spec version), for a two-rule list (`$and`), and for the legacy alias `eq`. Where a rule is refused, both paths refuse. An array comparand on `equals` and an empty `icontains` comparand are refused by both, with different error classes: the adapter's `UnlowerableAnalyticsFilterError` / `MalformedFilterError`, and core's `FilterOperatorError`. A scalar on `in` is refused by `parseFilterAST` on both paths. Nothing in this repo re-derives this equality, and the docblock says so. ## Reverse verification and ablation One-time proofs, run from the committed fix (`b60869195`). The probe that drives a real `ObjectStackAdapter` lived in the worktree only during these runs and is not in this PR. **Prediction, written before the runs:** BASE and the ablation are both red on the pin's four lowering rows (structured groupBy plus rules; multi-rule count; every operator; empty list) and green on the boundary row and the two controls. With the real adapter, the structured groupBy plus rules document draws the refusal, and both controls draw 42. | leg | new pin | real adapter: structured groupBy, rule list | real adapter: string groupBy, rule list | real adapter: structured groupBy, record filter | | --- | --- | --- | --- | --- | | BASE (`ObjectMetricWidget.tsx` at `58d1f5f4b`) | 4 failed, 3 passed | draws "aggregate('opportunity'): the spec-shape branch received a `where` array that is not a filter ..."; nothing posted | 42; analytics `where` `{"stage":"won"}` | 42; spec-shape `where` `{"stage":"won"}` | | ablation (`specShapeWhere(filterForRun)` replaced by `filterForRun`) | 4 failed, 3 passed | draws the same refusal; nothing posted | 42 | 42 | | fix (`b60869195`) | 7 passed | 42; spec-shape `where` `{"stage":"won"}`, the same bytes the legacy wire posts | 42 | 42 | Observed matches predicted. The empty-list row's BASE red is a change in the posted body (`where: []` becomes no `where`), not the card's defect. The platform engine reads `where: []` as no filter (objectstack's `engine-filter-array-lowering.test.ts`, the row 'an empty array is "no filter", exactly as before'). Landing and restore evidence: - **Ablation** (objectstack's `scripts/ablation-replace.mjs`): anchor count went from 1 to 0, and the blob went from `041e0f5d` to `42c7914d`. Restore gave blob equal to HEAD `041e0f5d` with `git diff HEAD` empty. - **BASE leg:** restored with `git checkout HEAD --` on the absolute path. Blob equality with HEAD and an empty `git diff HEAD` were checked by the leg script, which carries a `trap` restore. - **No build/dist leg:** the pin imports the widget by relative path, and `@object-ui/*` resolve to `src` through the vitest alias. ## Tests and gates The final head is `fea8ccb0e`. From `b60869195` to `fea8ccb0e`, only comment lines changed (a docblock correction and a test comment). | at | command | verdict | | --- | --- | --- | | `fea8ccb0e` | `pnpm exec vitest run` on the new pin, `objectMetricStructuredGroupBy-8613`, `ObjectMetricWidget.drillRoutedToSharedDrawer-8970`, `ObjectMetricWidget.compareTo`, `objectMetricQueryMembers-8071`, and `data-objectstack`'s `aggregate-spec-shape-where` | Test Files 6 passed (6), Tests 59 passed (59) | | `fea8ccb0e` | `pnpm --filter @object-ui/plugin-dashboard type-check` (`tsc --noEmit` and `tsc -p tsconfig.test.json`; the new pin is in the test program per `--listFiles`) | exit 0 | | `fea8ccb0e` | `pnpm check:control-bytes` / `check:new-line-citations` / `node scripts/check-changeset-presence.mjs` / `changeset:check` / `check:changeset-claims` / `check:pending-changeset-literals` | OK / 0 new citation(s) / 1 changeset for 1 released package / OK / OK / OK | | `fea8ccb0e` | eslint, narrowed (below) | 2 files, 0 errors | | `b60869195` | `pnpm exec vitest run packages/plugin-dashboard/` | Test Files 163 passed (163), Tests 1554 passed, 6 skipped (1560) | | `b60869195` | the 33 tests outside `plugin-dashboard` that name `ObjectMetricWidget`, `object-metric` or `objectAggregateSpecQuery` (`git grep -l`, `apps/console` and `packages/core` included), plus `aggregate-spec-shape-where` | Test Files 34 passed (34), Tests 2193 passed (2193) | | `b60869195` | `pnpm turbo run build --filter='./packages/*'`, then `pnpm --filter @object-ui/console build` (the Bundle Analysis type program) | 39 of 39 tasks successful; exit 0 | | `b60869195` | `check:phantom-deps`, `check:spec-symbols`, `check:test-path-roots`, `check:vi-mock-specifiers` / `-inherit` / `-override-shape`, `check:unused-deps`, `check:component-surface-parity`, `check:handler-key-reads`, `check:unreferenced-sources` | exit 0 each | | | `node scripts/check-governed-queue-guard.mjs --test` (the 3 paths) | NOT GOVERNED | ESLint narrowing, as a measurement: 1. **Population:** `eslint.config.js` lints `**/*.{ts,tsx}` with `tseslint.configs.recommended` and sets no parser `project`, so linting is not type-aware. 2. **Count:** `--format json` lists 2 files. `ObjectMetricWidget.tsx` has 0 errors and 11 warnings, and its BASE blob linted the same way also has 0 errors and 11 warnings, from the same two rules. The new pin has 0 errors and 0 warnings. 3. **Invariance:** with no type-aware rules, and no `fs` read in any `eslint-rules/*.js` (grep returns 0 matches; the same pattern hits `scripts/check-control-bytes.mjs`, the positive control), this diff cannot move a verdict on an untouched file. Repo-wide lint is CI's. - Patch round 1 (head `e37c484be`, at the seat's request): both authoring faces measured and pinned. The card's document, the showcase-shaped document and all 20 per-operator documents pass `ObjectMetricBlockSchema`, `safeValidateSchema` (the tolerant face `objectui validate` runs) and `StrictAnyComponentSchema`. The probe's controls show each face can refuse: an unknown `properties` key is refused by all three, and an unknown node-level key is refused by the strict face only. The changeset and the pin comment now say "passes both authoring faces" instead of "the strict face". The pin passes 7 of 7, `plugin-dashboard` type-check is green, and there is no source change. ## Acceptance notes - **The legacy wire is untouched.** The control pins the raw rule list on the legacy bag; the adapter lowers that wire. - **The drill drawer is unchanged.** It still receives `resolvedFilter`, and the 8970 pin is green. objectui#11506's drill path is not touched. - **`UnloweredAggregateWhereError` and its check are untouched.** The refusal of a caller that skips the lowering is pinned by `data-objectstack`'s `aggregate-spec-shape-where.test.ts` ("throws UnloweredAggregateWhereError instead of posting a rule array"). The ablation above shows the refusal coming back end to end once the lowering call is removed. That file also pins the other half of the seam: "a FilterCondition OBJECT is not an array, and this gate leaves it alone". - **The committed pin uses a recording adapter, not the real one.** `plugin-dashboard` does not declare `@object-ui/data-objectstack`, and declaring it (a `package.json` and lockfile edit) is outside this claim's file surface. The pin therefore asserts the posted `where` is a `FilterCondition` (`FilterConditionSchema` from `@objectstack/spec/data`). The adapter's acceptance of that shape is pinned on the adapter's side, as above. - **The brief's mechanism assumption 2 was falsified.** The legacy wire's lowering lives in the adapter package, and a re-export from core is a cycle. The route changed to the two public stages, per the ruling's intent: lower at the producer, no hand-written conversion, the refusal stays, and the bag is not narrowed. - **Observation, not filed:** this repo has two rule-list translators, `translateFilterArray` (adapter-private) and `toFilterNode` (core). They agreed after `parseFilterAST` across the declared vocabulary in the measurement above, and on refused rules they differ only in error class. No defect was measured. Carrier: none. - **Not measured:** `object-chart` makes the same spec-shape call. Its declared `filter` is a `FilterArray` or a record (`ObjectChartSchema.filter`), not a rule list, so this card's dialect does not reach it. --- _Generated by [Claude Code](https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Adds
@object-ui/designerpackage - a visual editor for creating and modifying Object UI schemas through a GUI.Components
Usage
Implementation Details
Documentation
Node operations handle schema mutation immutably. Selection state syncs across all panels via Context. Component metadata drives property form generation dynamically.
Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.