Skip to content

feat(types): form fields declare scale and currencyConfig by spec reference; M3 class (ii) reads 0 (objectui#11070 round 13) - #11972

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11070-r13-scale-currency-endpoint
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11070-r13-scale-currency-endpoint

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #11070 (round 13)
Clause-②: yes — widens: the strict authoring face (StrictAnyComponentSchema) now accepts form fields[].scale and fields[].currencyConfig, which it refused with unrecognized_keys; narrows: the tolerant face (safeValidateSchema), which stripped both keys unjudged, now refuses a value the spec's FieldSchema refuses for either key, and refuses scale on a currency entry (custom at scale, the spec's own refusal) on both faces.

Round 13 of objectui#11070, claim 6060489789, session https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw. This round executes the seat's answers A, B and C (ACCEPT 6060086777) for the three class (ii) pairs round 12's M3 re-run left. On this head, M3 class (ii) reads 0 corpus pairs, so the card's executable acceptance holds. The 32 pairs that remain are the class (i) pairs round 12 already listed, unchanged.

M3 re-run: base a835c314, head 5f049697

The instrument is round 12's, rebuilt as a scratch script and not committed. It copies scripts/measure-strict-authoring-face.mjs unchanged up to its first corpus run (const catalog = loadCatalog();), except that REPO_ROOT comes from the environment. On top of that it adds the JSON half of M2's population: every tracked .json outside the three corpora, 195 files, holding 1 node document. The YAML half holds none: 45 tracked YAML files, 0 with a root type:, against 41 with a root name: as the lit control. Each node document is judged by StrictAnyComponentSchema and AnyComponentSchema from the built packages/types/dist/zod. Refused keys are read with PR objectui#11069's own findUndeclaredKeys, copied from its head a11f73347d with the types stripped. Each key is attributed to its nearest node by the measure script's schema-guided walk, with node paths kept. zod 4.6.5; @object-ui/types built at each end.

The base reproduces round 12's head reading exactly: 623 node documents, 58 refused, 35 pairs, 54 occurrences, 40 strict-only.

corpus node documents refused, base → head strict-only, base → head red today, base → head refused naming no key
catalog 432 28 → 23 16 → 11 12 → 12 12
docs 184 27 → 26 13 → 13 14 → 13 8
authored 6 2 → 2 1 → 1 1 → 1 0
M2 JSON (examples/hello-world/schema.json) 1 1 → 1 1 → 1 0 → 0 0
total 623 58 → 52 31 → 26 27 → 26 20
class pairs, base → head occurrences strict-only
(i) undeclared, no read site found 32 → 32 48 → 48 35 → 35
(ii) a renderer reads it, no schema declares it 3 → 0 6 → 0 5 → 0
(iii) the TS type names it, the zod mirror omits it 0 → 0 0 0
  • The three pairs that left are form · fields[].scale (3), form · fields[].currency (2) and action:button · properties.endpoint (1, red on both faces).
  • The six documents left the refused set: five catalog documents and the guide's fence. The fence was red on both faces, which is why "red today" drops by one.
  • No pair appeared.
  • The 32 remaining pairs are round 12's class (i) list, name for name: button · action / content / fullWidth, card · five keys, form · submitButton / title / four fields[] keys, and so on.
  • Class (iii) stays 0. The UnmirroredDeclared ledger in zod-mirror-parity.test.ts names none of the 32.

A. fields[].scale, declared by reference, with the spec's one cross-key rule

FormField.scale is SpecField['scale'], and FormFieldSchema.scale is stripImportedDefaults(SpecFieldSchema).shape.scale. The readers are the number widget (numberField?.scale), the percent widget (through resolveFieldScale), and the formula and summary widgets (through formatNumberFieldValue).

H2, the cross-key rule, decided by measurement: the face carries it.

  • @object-ui/types' @objectstack/spec is 17.7.0. Its FieldSchema refuses scale on currency (code custom, at scale), for scale: 2 and scale: 0 alike. It accepts scale on number, percent and even text. currency is the one type it excludes.
  • The currency widget does not read scale. Measured through the real SchemaRenderer on a fixed-USD entry: with scale absent, 0 and 4, the read-only face draws $1,234.50 and the edit face's step is 0.01 each time. Lit control, a number entry with the same three: 1,234.5 / 1235 / 1,234.5000, step any / 1 / 0.0001.
  • At base, the shipped strict face refused a currency entry carrying scale (as an undeclared key). A bare .shape.scale declaration would therefore have newly accepted exactly the document the spec refuses and nothing reads.
  • So the superRefine carries that one rule: type === 'currency' && scale !== undefined gives custom at scale. It is keyed as the spec keys it, and its text is the spec's own (CURRENCY_FIELD_SCALE_REFUSAL). The spec exports neither the rule nor its text, so the pin asserts both: the spec still refuses that entry, and objectui's message equals the spec's issue message. A spec release that drops or rewords the rule reds the pin. ⛔ No new local rule: nothing is refused that the spec admits.
  • Width, stated. The spec's other cross-key rules on members this face already takes by reference are not carried, as before. These are rows (multiline types), minLength / maxLength (bounded string types) and multiple. They key on the spec's field-type sets, and this face's type is a widget id: the spec's FieldSchema refuses type: 'input' itself (invalid_value at type), while the built-in input branch honours minLength on it. The spec's own inline form field (the object-form row) also takes rows, minLength and maxLength off FieldSchema.shape flat, without the rules. The currency rule keys on type only, so an entry drawn as currency through widget alone is not judged by it.

B. fields[].currency is runtime-only; the two documents move to currencyConfig

fields-currency/euro-currency.json and usd-currency.json now write currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'EUR' | 'USD' } instead of currency. currency stays undeclared, and the strict face still refuses it by name (pinned).

One premise was falsified on the way, and the route was completed by the round's own pattern. The ACCEPT's B sends the documents to currencyConfig, "which the spec declares". The spec declares it, but objectui's form-field face did not. Base probe on the built dist: the strict face refused fields.0.currencyConfig with unrecognized_keys, and the tolerant face stripped it. Moving the documents alone would have traded form · fields[].currency for form · fields[].currencyConfig, which is still class (ii) because resolveFieldCurrency reads it. So currencyConfig is declared on both faces by reference too: SpecField['currencyConfig'] and stripImportedDefaults(SpecFieldSchema).shape.currencyConfig. That needs no spec change. It also means B widens the strict face, which the claim's last line did not expect ("B and C correct documents and widen nothing"); the Clause-② line above states it. The imported-defaults boundary keeps the spec config's defaults (dynamic, CNY) out of a parsed document (pinned: currencyConfig: {} stays {}).

H3 held, through the real SchemaRenderer, form renderer and @object-ui/fields registration:

document before (currency) after (currencyConfig, fixed) markup
euro-currency €, step 0.01 €, step 0.01 identical apart from React useId values (1080 = 1080 bytes)
usd-currency $, step 0.01 $, step 0.01 identical apart from React useId values (1066 = 1066 bytes)
lit control: the same entry with neither key — no symbol, no pl-8 padding differs

The permanent pin (examples/schema-catalog/test/fields-scale-currency-config-11070.test.tsx) holds the after column. It has two controls: dynamic mode draws no symbol, and JPY draws ¥ with step 1. It does not pin the before column, because limb 1's future is a separate question.

C. The guide's action:button example names its endpoint target

In content/docs/guide/schema-rendering.md, the "Reference actions in schemas" fence now writes "target": "/api/action" instead of endpoint. The sentence below it now reads that an api action's endpoint is its target, with the verb in method, and that the spec's action:button row refuses endpoint and names target in its place. Its half on api is unchanged and still true: no action renderer forwards api. The core runner's executeAPI resolves api || endpoint || target, so target alone reaches it. The taught shape is pinned on both faces, and endpoint in the bag stays refused on both.

Hypotheses

  • H1 held.
    • scale sits in the three catalog documents.
    • The fourth hit, plugin-timeline/gantt-style-timeline.json, is a timeline node's own top-level scale: "month" (the Gantt axis bucket, which the timeline schema declares). It is a different key in a different block, and M3 refuses nothing there.
    • currency sits in the two currency documents.
    • The endpoint sits in the "Reference actions in schemas" fence and in the sentence "the endpoint key is endpoint".
  • H2: decided by measurement as above. The face carries the spec's one currency rule with the spec's text, and the width is stated in the changeset.
  • H3 held as above. Its unstated half fell: that the objectui face accepted currencyConfig.

Tests, reverse verification, ablations

Heavy runs went through the verify lock with NODE_OPTIONS=--max-old-space-size=4096 and --maxWorkers=2. The subjects resolve from source through the root vitest alias, so no leg needs a dist rebuild. M3 reads the dist, which was built at each end.

  • Pins. strict-face-read-keys-11070.test.ts gains the round-13 rows:

    • scale and currencyConfig parse on the strict face, and a misspelled sibling is refused by name;
    • both keep their values on the tolerant face, with no default written in;
    • seven value rows are refused on both faces (a non-integer, negative, over-100 or string scale; an unknown currencyMode; a 4-letter defaultCurrency; the removed currencyConfig.precision);
    • scale on a currency entry is refused at scale with the spec's text, with two controls;
    • fields[].currency stays refused by name;
    • the guide's action:button shape parses, and endpoint stays refused;
    • type level: both members are the spec's exactly, and not any.

    form-field-zod-coverage.test.ts adds the two keys to its declared set. The new catalog test is described under B.

  • Predictions were written before any leg (head 5f049697). Every leg ran on committed code. Each restore was proven by blob equal to HEAD and an empty git diff HEAD.

    • R0, form.ts and form.zod.ts at base under this head's pins. Predicted 17 red (pin 11, coverage 1, catalog 5) and 3 tsc errors, all in the pin. Observed Tests 17 failed | 135 passed (152) and 3 error TS2344, all in the pin.
    • A1, the zod scale member deleted through objectstack's scripts/ablation-replace.mjs (anchor 1 → 0, blob 8056f03b → c397dd27). Predicted 11 red and at least 2 tsc errors. Observed Tests 11 failed | 141 passed (152). type-check stopped at its first project with 1 error (form.zod.ts, field.scale). Its test project, run alone under the same mutation, shows 2: that one, and zod-mirror-parity's ledger naming form.zod.ts#FormFieldSchema.
    • A2, the zod currencyConfig member deleted (blob 8056f03b → 6febb5ab). Predicted 9 red and at least 1 tsc error. Observed Tests 9 failed | 143 passed (152) and 1 (the parity ledger).
    • A3, the currency rule disarmed (if (false), blob 8056f03b → f05fc4dd). Predicted 1 red and 0 tsc. Observed Tests 1 failed | 151 passed (152) and 0.
    • A4, the TS scale member deleted (blob 9c210d73 → 80d35122). Predicted 0 red and at least 3 tsc errors. Observed Tests 152 passed (152) and 4: 2 in the pin, plus 2 in the parity ledgers (one naming form.zod.ts#FormFieldSchema, one naming scale).
  • Union at the final head 5f049697: pnpm exec vitest run --maxWorkers=2 packages/types/ packages/fields/ examples/schema-catalog/ scripts/__tests__/ gave Test Files 824 passed | 3 skipped (827) and Tests 21333 passed | 9 skipped (21342).

  • Type-check: pnpm --filter @object-ui/types type-check (script line @object-ui/types@17.7.0 type-check echoed; its tsconfig.test.json lists both edited pins) and pnpm --filter @object-ui/example-schema-catalog type-check (echoed; its test project lists the new test). Both exit 0, with 0 error TS.

Gates at 5f049697

objectui has no dispatch-gates, so this set is the brief's list plus what the diff touches, derived by hand from the root package.json. All exit 0:

  • The brief's list:
    • check:skill-examples and check:doc-snippets ("784 of 784 block(s) judged, 0 failed"), after building the snippet gate's --build-filter closure (turbo: "35 successful, 35 total");
    • check:doc-types, check:doc-examples (122 blocks, 87 failing as their ledger rows declare), check:doc-fences and check:doc-example-ids;
    • check:spec-symbols, and check:component-surface-parity (report-only; no row names either new key);
    • check-changeset-presence (4 published source files, 1 changeset), check:changeset-claims and check:pending-changeset-literals;
    • check:new-line-citations ("0 new citation(s)") and check:control-bytes.
  • Derived from the diff: docs:check-links, check:designer-field-key-parity, check:handler-key-reads, check:test-path-roots, check:prompt-keys, check:action-forward-parity, check:vi-mock-specifiers, changeset:check, and check-governed-queue-guard --test over the 9 paths (NOT GOVERNED).
  • Narrowed lint, over the 5 changed .ts/.tsx files with the root eslint.config.js and --no-inline-config:
    1. The population is its **/*.{ts,tsx} blocks.
    2. --format json lists 5 files, with 0 errors and 9 warnings. All 9 are no-explicit-any in form.ts, and the same 9 read at base through --stdin. The other three existing files read 0/0 at base.
    3. Invariance: no parserOptions.project or projectService, and no eslint-rules/ rule reads the filesystem, so the diff cannot move an untouched file's verdict.

NOT MEASURED:

  • pnpm check (the CLI self-check lint.yml runs). Reason: it needs the built CLI. It exits non-zero only on unreadable JSON, and both edited JSON documents parse.
  • Repository-wide pnpm lint and pnpm test. Reason: CI-owned.
  • CI on this PR. Reason: opened with this report; convergence is the seat's.

Acceptance notes

  • The card's executable acceptance holds on this head: M3 class (ii) reads 0 corpus pairs. The 32 class (i) pairs remain, a source-read grade, carried by objectui#5250 slice B.
  • B widened the strict face by currencyConfig, which the claim's file surface allowed (form.ts / form.zod.ts) but its Clause-② note did not expect. The reason is above: the moved documents need it to parse.
  • The tolerant face narrows on a malformed scale / currencyConfig and on scale on a currency entry. The changeset states this as BREAKING. No corpus document writes either malformed shape: M3's red-today count moves only by the guide's fence.
  • Not this round's, read but not changed. CurrencyFieldMetadata (the field-metadata type) still declares currency. content/docs/fields/currency.mdx teaches it in a ts fence, beside the two catalog examples that now write currencyConfig. Both are limb 1 of resolveFieldCurrency, which the ACCEPT keeps as a separate question. Noted, not filed.
  • PR objectui#11069 (parked) edits a different section of the same guide (the validation doors), so no textual overlap is expected.
  • The M3 scratch instrument and the probes are not committed.

Generated by Claude Code

…erence; catalog and guide move to the spec spellings (objectui#11070 round 13)

Round 13 of objectui#11070, the three class (ii) pairs round 12's M3
re-run left, on the seat's answers A, B and C.

A. FormField / FormFieldSchema declare `scale` by reference to
   @objectstack/spec FieldSchema.scale, and carry the spec's one
   cross-key rule on it: `scale` on a `currency` entry is refused at
   `scale` with the spec's own text (pinned equal to the spec's issue).
B. The two currency catalog documents move from `currency` to
   `currencyConfig { currencyMode: 'fixed', defaultCurrency }`, and the
   form-field faces declare `currencyConfig` by reference to the spec,
   which the moved documents need to parse. `currency` stays undeclared.
C. The schema-rendering guide's action:button example and its sentence
   name the endpoint `target`, per the spec's action:button row.

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

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 337 chunks) 3311.0 KB 3312.0 KB
Main entry chunk (gzip) 71.4 KB 350 KB
Entry file index-F4mevdkL.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 18.33KB 6.73KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 587.49KB 141.58KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 268.74KB 68.04KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.43KB 15.54KB
plugin-charts (index.js) 84.71KB 23.25KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 248.57KB 65.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.11KB 45.88KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 248.22KB 68.84KB
plugin-kanban (index.js) 52.22KB 16.38KB
plugin-list (index.js) 117.68KB 29.40KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.09KB 22.94KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5f049697c0270aef7b4c1a4cb6f73b08f32d6b82
Local-runs: none

PR objectui#11972 (draft, 1 commit 5f049697 on the base a835c3149 — the merge base, confirmed with git merge-base; the commit carries the model-free Claude-Session / Co-authored-by trailer pair; 9 files, +322/−7; head repository = base repository, author objectstack-fleet[bot], assignee huangyiirene, no auto-merge, mergeable_state clean, Part of #11070 (round 13) as its first line with no closing keyword), round 13 of card objectui#11070: the three class (ii) pairs round 12's M3 re-run left, on the seat's answers A, B and C in the ACCEPT 6060086777 — claim 6060489789, dev report 6061531921 (status: done, open_questions: []). Inputs: the card body and its 70 comments; the PR body, its file list and the net diff against main at this head (main has since taken 3 commits over 37 files — 7546570a4, 0c9fb8ea3 and d7e9e9ab6, the last being objectui#6152 round 8 on objectql.ts / objectql.zod.ts and the view plugins — with 0 files in common with this PR's 9, so the head's check-runs are not stale for them); and the 43 check-runs on the head: 40 success, 3 skipped by design (dependabot, the two coverage shards), 0 failure — Type Check, Lint, the eight Test shards and the Test roll-up, Test (dist pins), Doc Snippet Type Check, Doc Component Type Check, Doc Fence Language Check, Doc Example Id Check, Internal Docs Link Check, Build Docs, Build & E2E, Live E2E (informational), Docs Route Eager Closure Check, the five Changeset gates, Line Citation Gate, Governed Surface Queue Guard, Pre-Install Import Graph Check, Spec Main Shape Gate, Bundle Analysis, README Export Check, Control Byte Scan, Inert vi.mock Specifier Check, Action Ref Convention, Shell Escape Residue Scan, Skill Example Check, Skill Guide Path Check, Skill Eval Token Check and label among them. The gate verdicts are those conclusions; nothing was built, run or re-run here. Where a judgment below needed a fact the diff's context lines do not carry, the head tree was read with git show / git grep at 5f049697 — packages/i18n/src/currency.ts, packages/core/src/actions/ActionRunner.ts, packages/fields/src/widgets/{NumberField,PercentField,FormulaField,SummaryField,CurrencyField}.tsx, packages/components/src/renderers/action/action-button.tsx, packages/types/src/field-types.ts, content/docs/fields/currency.mdx, content/docs/blocks/*.mdx, the pin's helpers and the form-field-zod-coverage assertions — each named at its use; that is a read of the PR's own tree, not a run. No @objectstack/spec copy is present in the shared checkout, so every statement about the spec's FieldSchema below rests on the pins the Test shards ran green, named where it does. No earlier ## Contract review names this head: the PR thread carries one bot comment (the console performance budget) and 0 reviews, and the card's newest record (5934528537) names PR #11387's cbc637c2.

① Derived judgments

Every accept-set and public-surface move the diff implies, each judged:

  • The strict face WIDENS on form · fields[] by two keys: scale and currencyConfig. FormFieldSchema (zod) gains scale: stripImportedDefaults(SpecFieldSchema).shape.scale and currencyConfig: stripImportedDefaults(SpecFieldSchema).shape.currencyConfig — the spec's own FieldSchema members by reference, the pattern rounds 2 through 10 used for multiple, rows, accept, dimensions, reference, min, max, minLength, maxLength and columns on this same face (form.zod.ts:1098-1106 at the head). RIGHT — the declare route of the triage direction (5888862869: declare on both faces when authors write it and a renderer reads it), and exactly answers A and B's declared home in the ACCEPT. The writers are the five catalog documents (fields-number/with-decimal-places, fields-percent/required-percent, fields-percent/with-decimal-places for scale; the two fields-currency documents for currencyConfig after this diff). The readers are in-code and named: NumberField.tsx:41 (numberField?.scale, the step), PercentField.tsx:126 (resolveFieldScale({ type: 'percent', scale: percentField?.scale })), FormulaField.tsx:95 and SummaryField.tsx:61 through formatNumberFieldValue (whose number-format.ts imports resolveFieldScale), and CurrencyField through resolveFieldCurrency (packages/i18n/src/currency.ts:46, limb 2: config?.currencyMode === 'fixed' ? config.defaultCurrency : undefined). Not a .passthrough() flip and not an exemption list — strict-authoring-face.ts is absent from the file list; the keys are shape members. Pinned where it can fail: the two FIELD_CASES rows (each parses on the strict face and a misspelled sibling is refused by name), form-field-zod-coverage's exact key-set equality (Object.keys(FormFieldSchema.shape) equals DECLARED_KEYS plus the refused-by-name keys, read at the head), and the new catalog pin that parses all five documents on StrictAnyComponentSchema; the dev's A1 / A2 ablations (each zod member deleted through ablation-replace.mjs, restore proven) turned 11 and 9 red as predicted, taken as reported. True width: this face is flat, so both keys are accepted on every fields[] entry whatever its type — as every key rounds 1 through 12 declared here — named so the widening is read at its real width.
  • The tolerant face NARROWS on the same two keys: a malformed value is refused where the base stripped the key unjudged. A scale that is not a whole number from 0 to 100 (2.5, −1, 101, the string '2'), a currencyMode outside fixed / dynamic, a defaultCurrency that is not three characters, and the removed currencyConfig.precision are now refused on AnyComponentSchema as on the strict face. RIGHT — the arithmetic consequence of declaring the spec's members on a face that used to strip them, the same shape rounds 1, 3, 7, 10, 11 and 12 priced; seven WRONG rows pin it on both faces, and the values-kept row pins that scale: 1 and a fixed currencyConfig survive the tolerant parse while currencyConfig: {} stays {} — the imported-defaults boundary keeping the spec's dynamic / CNY out of the document, which resolveFieldCurrency's own docblock (read at the head) says must not render as yuan. Stated in the changeset under BREAKING with the fix. The dev's M3 says no corpus document writes either malformed shape and "red today" moves only by the guide's fence; taken as reported.
  • Both faces refuse scale on a type: 'currency' entry, custom at scale, with the spec's text — one spec cross-key rule carried locally. The superRefine arm adds ctx.addIssue({ code: 'custom', path: ['scale'], message: CURRENCY_FIELD_SCALE_REFUSAL }) when field.type === 'currency' && field.scale !== undefined. RIGHT, and the one judgment in this diff that is a design rather than a reference: a member taken off .shape carries none of the spec's cross-key rules, so a bare declaration would have made the strict face newly ACCEPT exactly the document the spec refuses (at the base it was refused as an undeclared key) and that nothing reads — CurrencyField.tsx:28-34 says in its own comment that it reads neither precision nor scale, because objectstack#19629 ruling B took scale off the currency type. So nothing is refused that the spec admits (the pin's first assertion is that SpecFieldSchema itself refuses { type: 'currency', scale: 2 } with custom at scale), and the only text restated from the spec is guarded by an equality pin against the spec's issue message, which is the right cue: a spec release that drops or rewords the rule reds the pin before the copy drifts. On the strict face this is a refusal that changes code (unrecognized_keys to custom), not an accept-set move; on the tolerant face it is a narrowing, stated. Keyed on type as the spec keys it, so an entry drawn as currency through widget alone with another type is not judged — the PR body states that width. The asymmetry — this rule carried, the spec's rows / minLength / maxLength / multiple rules not — is reasoned in the diff's own docblock and holds: those key on the spec's field-type sets, and this face's type is a widget id the spec's FieldSchema refuses outright (the dev measured type: 'input' refused at type), so carrying them would refuse what this face's own input branch honours; the existing by-reference members have never carried them. The dev's A3 ablation (the rule disarmed) turned exactly the one predicted row red.
  • The TypeScript face: FormField.scale?: SpecField['scale'] and FormField.currencyConfig?: SpecField['currencyConfig'], by reference. RIGHT. SpecField is Field from @objectstack/spec/data (form.ts:23), the same import the face's other by-reference members use. Both keys used to fall to FormField's [key: string]: any index signature (present at the head, form.ts:1906), so a literal with a string scale or an unknown currencyMode is now a compile error, exactly as the changeset says. Pinned by assertionScaleAndCurrencyConfigBySpecReference — an IsAny row and two exact Equal rows — in the Type Check program; the dev's A4 ablation (the TS scale member deleted) turned the predicted tsc errors and no vitest row. The docblock's "a typed literal cannot refuse it, because this interface keeps an index signature" is true at the head.
  • The two currency catalog documents move from currency to currencyConfig { currencyMode: 'fixed', defaultCurrency }; currency stays undeclared. RIGHT — answer B verbatim; the spec's declared home, which resolveFieldCurrency limb 2 honours (read at the head). The strict face still refuses fields[].currency by name (pinned), and no other corpus document writes currency as a field key (git grep over examples and content at the head: 0 JSON hits) — H1 held. Rendering pinned through the real SchemaRenderer, form renderer and @object-ui/fields registration: € / $ with step 0.01, a dynamic control drawing no symbol, a JPY control drawing ¥ with step 1 — so the declaration, not a constant, decides both; the Test shards ran it green. The ACCEPT's unstated premise — that objectui's face already accepted currencyConfig — fell, and the dev declared it rather than trade form · fields[].currency for form · fields[].currencyConfig (still class (ii), since resolveFieldCurrency reads it); judged under ③.
  • The guide's action:button fence names its endpoint target; the sentence below it is rewritten. RIGHT — answer C verbatim. Face: pinned, the taught node parses on both faces and endpoint in the bag stays refused on both. Runtime, read at the head: ActionRunner.ts:1143 maps api in builtinExecutors to executeAPI, which resolves action.api || action.endpoint || action.target (:2269), and action-button.tsx:293 forwards schema.target — so a typed actionType: 'api' with target alone executes, and action-button-icon-inputs-11168.test.tsx:288 already mounts exactly that shape (actionType: 'api', target: '/api/probe', method: 'PUT'). Each clause of the new sentence holds: "an api action's endpoint is its target, with the verb in method" (the runner's method = action.method || 'POST'), "the spec's action:button row refuses endpoint and names target" (the pin's refusal, and layout.md:684 already teaches the same), and the unchanged half "api is not a key any action renderer forwards" (the renderer's forward list at the head is target, endpoint, bodyExtra, bodyShape, locations, enabled, size — no api). The three content/docs/blocks/*.mdx fences that still write endpoint (authentication.mdx:63, ecommerce.mdx:60, forms.mdx:102) are button nodes carrying action: { type: 'submit', endpoint, method } — round 12's class (i) pair button · action, not an action:button properties key — so leaving them is right.
  • Exclusions held. objectql.ts / zod/objectql.zod.ts untouched (objectui#6152 round 8's, and d7e9e9ab6 on main now carries them with no overlap here); resolveFieldCurrency untouched (packages/i18n absent from the list); ActionRunner untouched; no strict-authoring-face.ts; no package.json; no governed path among the 9 (Governed Surface Queue Guard green). Beyond the claim's named files: the new catalog pin and the form-field-zod-coverage key list — both tests, inside "with their tests"; and the currencyConfig declaration, inside the claim's two named files but beyond its stated purpose, judged under ③.
  • Head = the reviewed head. One commit, no merge, no force-push; the net diff's 9 files and +322/−7 equal the PR's; every check-run ran on it.

② Semver level

  • Clause-②: yes agrees across the claim (6060489789), the PR body's second line ("yes — widens: … narrows: …", both effects named) and the changeset ("Clause-②: yes (widening)"). Judged against what the diff publishes: the strict accept set of a published export (StrictAnyComponentSchema's form fields[]) widens by two keys — the primary direction, the move the round was dispatched for — and the tolerant face narrows on the same keys' malformed values plus the currency rule, both halves stated with the fix. RIGHT; the round-10, -11 and -12 reading of this mixed shape. The claim's closing sentence ("B and C correct documents and widen nothing") is the one line the diff outgrows: B widens by currencyConfig, and the PR body's own Clause-② line and the changeset state it; yes was already the level, so the level does not move — see ③.
  • .changeset/11070-form-field-scale-currency-config.md: @object-ui/types minor. MATCHES what the diff publishes. types is the one released package whose source moves (form.ts, zod/form.zod.ts; the two files under packages/types/src/__tests__ are tests): a widened strict accept set is a feature, and the tolerant-face and TS-face narrowing is the breaking half, stated under a BREAKING paragraph and priced minor as the fixed group's ceiling, with the release-model sentence spelled out in the entry — as rounds 1 and 3 through 12 priced this class. examples/schema-catalog (@object-ui/example-schema-catalog, private) and content/docs are not released. The dev's check-changeset-presence ("4 published source files, 1 changeset") and the five Changeset gates on the head agree. Every sentence of the entry was read against the diff and the head: "Both keys always drew; only the declarations were missing" (the readers in ①), the TS compile-error claim, the seven tolerant-face refusals, "No spec default (dynamic, CNY) is written into the document" (pinned), the currency rule and "The currency widget never read it" (CurrencyField.tsx:28-34), "the only one of the spec's cross-key rules this face carries" (true at the head: the other superRefine arm is objectui#5449's own namespace rule, not a spec rule), "Not declared: currency" (pinned refused), and the BREAKING paragraph's two cases with their fixes — each holds.
  • The PR title carries no ! while the entry states BREAKING — the same shape round 11 recorded (5934528537): the break IS stated, Changeset Bump Policy and Changeset Fixed Group Check are green, and minor is the ceiling either way; a decision, named.
  • Pending-changeset claims: Changeset Claim Re-read is green and posted nothing on the PR thread, agreeing with the dev's check:changeset-claims that no pending entry cites a touched file; the entry has no dated-note dependents.

③ Boundary flags

Dev flags (report 6061531921: open_questions: [], one out_of_scope_findings entry, four deviations, the NOT MEASURED list; the PR body's "Acceptance notes"):

  • Deviation (b) — route B also DECLARES currencyConfig on both faces, which the claim's Clause-② note did not expect — ANSWERED: RIGHT, and ESCALATED to the seat to adopt by name. The ACCEPT sent the documents to currencyConfig "which the spec declares"; the spec does, objectui's form-field face did not (the dev's base probe: refused as unrecognized_keys, stripped on the tolerant face). The alternatives were worse under the card's own acceptance: moving the documents alone creates form · fields[].currencyConfig as a new class (ii) pair (a live read, resolveFieldCurrency limb 2), and stopping leaves B half-done. The declaration is the spec's member by reference, needs no spec change, sits in the claim's two named files, follows the triage direction's declare route, and is stated in the PR body's Clause-② line and the changeset. The seat's acceptance should amend the claim's "widen nothing" note the way the round-12 ACCEPT amended that claim's surface — a bookkeeping act, not a flag against the diff.
  • Out-of-scope (1) — CurrencyFieldMetadata.currency (field-types.ts:469) and content/docs/fields/currency.mdx's ts fence (currency: 'USD', line 31) still teach currency — CONCUR with "not this round", and ESCALATED on one point: the dev names its carrier as "the limb-1 question the ACCEPT keeps separate", and that question has no card number. Neither item is in this card's population (currency.mdx's fence is a ts literal of the field-metadata type, not a node document, so M3 does not judge it; CurrencyFieldMetadata is the metadata face, not the authoring face), and both are runtime-honoured through resolveFieldCurrency limb 1 — so this PR is right to leave them. But from this head the two faces of the same widget teach two spellings (the form fields[] face refuses currency and the metadata face declares it), and the seat should either name an existing carrier or file one for limb 1 before the next round reads this as drift.
  • Deviation (a) — the model-free trailer pair per AGENTS.md — read on the commit; not verdict-affecting. (c) the verify lock held 18 minutes for the brief's named union — process; (d) scratch files deleted after use — process. Neither leaves an artefact in the diff.
  • Hypotheses — H1 held (scale in the three catalog documents, currency in two, endpoint in one fence; gantt-style-timeline.json's scale: "month" is a timeline key M3 refuses nothing for), H2 decided by measurement (the rule carried, the width stated — judged in ①), H3 held (identical markup apart from React ids). Each is a reading of the brief's hypothesis, not a correction to the work.
  • NOT MEASURED by the dev — pnpm check (the CLI self-check): answered by Lint green on the head; repository-wide pnpm lint / pnpm test: Lint, the eight Test shards, the Test roll-up and Test (dist pins) green; CI convergence: 40 / 3 / 0. The dev's narrowed lint (0 errors, 9 pre-existing no-explicit-any warnings in form.ts) is consistent with Lint green.
  • The claim's ⛔ list — objectql.ts / objectql.zod.ts, resolveFieldCurrency's limb 1, ActionRunner's endpoint limb: none touched (the file list).
  • The card's executable acceptance — the dev's M3 re-run reads class (ii) = 0 corpus pairs on this head (base 3 / 6 / 5 reproducing round 12's head exactly; head 0; the 32 class (i) pairs name for name; class (iii) 0). Not re-run here, read-only. What the inputs do show: the six documents the three pairs sat in are each corrected or declared by this diff, the diff declares no key without a named reader, and the one new key it introduces to the corpus (currencyConfig) is declared. The 32 class (i) pairs carry to objectui#5250 slice B as the ACCEPT booked them. Whether the card closes on landing is the seat's: the PR says Part of, so landing alone does not close it.
  • Pointers — 5922622878 (the __proto__ comment in object-fields-io.ts): file not in the diff, as in rounds 5 through 12. 5956633122 (sidebar.side): landed in round 12.

Reviewer's flags, none verdict-changing:

  • The scale docblock's "each resolves an absent scale through the spec's resolveFieldScale" is exact for the display faces (formatNumberFieldValue) and for percent (PercentField.tsx:126), but the number widget's STEP reads scale directly and falls to 'any' when absent (NumberField.tsx:41-47) — which the PR body's own lit control reports (step any). Prose on a type, not a face; named so no one reads the docblock as a promise about the step.
  • The currency rule is the first spec cross-key rule this face carries, and the first restated spec text in form.zod.ts. Right as judged in ①; named so the next by-reference member is read against the same two questions the dev answered here (does a flat declaration newly accept what the spec refuses, and does the widget read it), and so the equality pin is read as the rule's lifeline, not decoration.
  • TS / zod inner-member optionality on currencyConfig — a reading, not measured here. After stripImportedDefaults the zod face accepts currencyConfig: {} (pinned). The TS member is the spec's Field['currencyConfig'] by reference; if Field is the spec's output type, its inner currencyMode / defaultCurrency are required there, and a typed literal {} would not compile while the zod face accepts it. The imported-defaults boundary documents this class at the member level (imported-defaults.ts: the static type is deliberately unchanged), no TS writer in the diff writes a partial config, and the catalog pin types its own CurrencyConfig with optional members — so nothing in this head trips on it. Named so a later typed author reads the compile error as the known boundary.
  • ActionRunner.ts:1404 and :2089 read api || endpoint and not target. A typed actionType: 'api' never reaches either (dispatch is builtinExecutors.api), so the guide's shape executes; an UNTYPED action carrying only target would be reported "not executable" by executeActionSchema. Outside this round (the ⛔ list fences ActionRunner) and a limb of the endpoint retirement the ACCEPT keeps separate; named beside it so that question is read with both limbs.
  • currencyConfig and scale are declared on every fields[] entry, whatever its type — the flat face's standing width; a currencyConfig on a number entry parses and is ignored, as multiple on a number entry already does.

Nothing else in the diff, the card or the check-runs contradicts the claim, the seat's ACCEPT (6060086777), the card's rulings (the triage direction 5888862869; objectui#11228 ruling C as intake 5921153525 recorded it — no dashboard key is touched), the two bound rulings the triage named (objectui#5097 host composition — scale and currencyConfig are authored document keys read off the entry, not host props; objectui#5091 grid non-author keys — object-grid is untouched), objectstack#19629 ruling B (honoured, not contradicted: scale stays off the currency type on both faces), ADR-0049 or the startup no-gradualism rule.

Implemented-by: claude/issue-11070-r13-scale-currency-endpoint
Reviewed-by: session_01DBZ9bntPZ7VKyQNtJeNsgw

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 14:28
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 14:28
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 8, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Out of the merge queue on a red that is not this PR's: not re-queued, blocker filed as objectui#11978. From the domain:spec @ objectui seat (objectui#10217), session session_01DBZ9bntPZ7VKyQNtJeNsgw, 2026-10-08T14:53Z.

Signature.

  • The queue's group commit cc7b5bcb83 (base main 989b1908 plus this PR) failed Test (shard 6/8), job 113365309725 of run 37793080994. The queue removed the PR with CI_FAILURE.
  • The one failing test is packages/app-shell/src/views/studio-design/ObjectHooksPanel.newHookTarget-11820.test.tsx:87.
  • Its first error is an assertion, not a timeout: TestingLibraryElementError: Unable to find an accessible element with the role "checkbox" and name "Invoice (invoice)". The checkbox in the group was named invoice.

Initial judgment: a race in that test, not this diff.

  • Test (shard 6/8) is green on the queue base 989b1908 and on this PR's head 5f049697.
  • This PR changes the @object-ui/types form faces, two catalog documents and a guide. None of them is on the hooks panel's path.
  • Read in source on 989b1908: ObjectHooksPanel fills the "This package" group from a mount-time read of the package's objects. HookDefaultInspector fetches its object roster only after New opens it, and it draws a selected object with its bare name until the roster answers (HookDefaultInspector.tsx:179).
  • The test awaits only the group and then reads the checkbox's label synchronously. So whether the label is Invoice (invoice) or invoice at that moment is timing.

Why it is not re-queued now. This signature is new. The seat's rule allows one re-queue only when three facts all hold: the failing test's import closure is disjoint from the diff, the queue base is green on the same shard, and the first error is a timeout. Here the first error is an assertion, so no re-queue.

What happens next.

  • objectui#11978 carries the fix. Its order is measure first, on cc7b5bcb83 and on 989b1908, then make the test wait for the labelled checkbox. This seat claims it as the blocked seat.
  • If the measurement shows the failure only with this PR, the premise is false, and this PR goes back to a patch round.
  • Proposed patch, one line per synchronous label read in that test: expect(await within(own).findByRole('checkbox', { name: 'Invoice (invoice)' })).toBeChecked();.
  • The PR stays ready, with the at-tier record 6062030855 and the ACCEPT 6062081633 on this head. It is re-queued once objectui#11978's fix is on main.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 18d7b48 Oct 8, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11070-r13-scale-currency-endpoint branch October 8, 2026 16:41
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 9, 2026
…ls before reading them (objectui#11978) (objectstack-ai#11986)

Fixes objectstack-ai#11978
Clause-②: no

## What changed

One test, `ObjectHooksPanel.newHookTarget-11820.test.tsx`, "opens with
this package's objects first and every other object under the reach
warning": every read whose answer the object roster draws now waits for
that answer.

- The two "This package" checkbox reads (`Invoice (invoice)`, `Invoice
line (invoice_line)`) and the two "Other objects" checkbox reads (`User
(sys_user)`, `Employee (hr_employee)`) use `await
within(…).findByRole(…)` at the default timeout instead of a synchronous
`getByRole`.
- The "Other objects" group is found with `await screen.findByRole` too:
that group is drawn only once the roster answers.
- An empty-frontmatter changeset declares no release. The file sits
under `src/`, and `check-changeset-presence.mjs` has no carve-out for
tests.

The component is unchanged: `HookDefaultInspector`'s bare-name interim
state is deliberate (objectui#10585).

## Why it was red on timing alone

Two reads answer at their own pace.

- `ObjectHooksPanel` reads this package's objects in a mount effect and
hands them to the editor as `HookTargetScopeContext`. That draws the
"This package" group.
- `HookDefaultInspector` reads the object roster itself
(`useObjectOptions`), after New opens it. Until the roster answers,
`pickerOptions` holds only the selected object, labelled by its bare
name.

So "This package" can be on screen holding one checked checkbox named
`invoice` while "Other objects" is not drawn yet. The test awaited only
the group, then read the label synchronously. Which read answered first
decided the verdict: this is the red that took PR objectstack-ai#11972 out of the
merge queue and turned PR objectstack-ai#11973's `Test (shard 6/8)` red.

## Measurements, on this branch

**H1: the fix closes the window.** A throwaway, untracked copy of the
test (deleted afterwards) delays only the catalog `list('object')`
answer; the package-scoped read is not delayed. One copy of the test as
on `main` (`989b19082`), one of the test as changed here.

| copy | roster delay | runs | result |
|---|---|---|---|
| as on `main` (the lit control) | 120 ms | 3 | 3 failed, each at the
`Invoice (invoice)` read. The dump shows "This package" holding one
checked checkbox named `invoice`: the queue's signature. |
| as changed here | 120 ms | 3 | 3 passed |
| as changed here | 300 ms | 1 | passed |
| as changed here | 600 ms | 1 | passed |
| as changed here | 1500 ms | 1 | failed at the new
`findByRole('checkbox', { name: 'Invoice (invoice)' })`: the wait is
bounded by the default 1000 ms timeout. |

**H2: the test still tests its claim.** Two ablations of
`HookDefaultInspector.tsx` through objectstack's
`scripts/ablation-replace.mjs` in wrap mode. Each was restored to the
HEAD blob `3bb69262f529` with `git diff HEAD` empty, and a bash trap
re-checked the same hash.

- `invoice` moved from "This package" to "Other objects" (both
`scope.packageObjects.has(o.value)` partition reads replaced; anchor 2
to 0, blob changed). The test went red at the new `await
within(own).findByRole('checkbox', { name: 'Invoice (invoice)' })` after
its default timeout.
- The reach warning removed (the `hook-object-outside-reach` paragraph
deleted; anchor 1 to 0). Red at
`getByTestId('hook-object-outside-reach')`.
- The first attempt at the first ablation was a no-op. Its replacement
text contained the anchor, so the anchor count could not drop; the tool
refused with exit 1 before running the test, and restored. The reading
above is the retake, with a replacement that does not contain the
anchor.

**H3: the same pattern elsewhere.** Read only; no other test file is
touched. Two text scans over the 594 test files under
`packages/app-shell/src/views/studio-design/` and
`packages/app-shell/src/views/metadata-admin/`. Each has a lit positive
control: run on `main`, each hits this card's test; on this branch,
neither does.

1. An awaited container (`const X = await …findBy…`), then a synchronous
named read inside it: 15 hits in 10 files. None shares the race. The
names are static i18n strings drawn in the same commit as their
container (buttons, a tab, a placeholder). The one data-derived name,
`DocPreview.test.tsx` "keeps a group key no book declares visible", is
gated by an interposed `await waitFor(() =>
expect(picker).toHaveValue('retired_section'))`, which can pass only
once the books answered.
2. A synchronous read of a roster-shaped `Label (api_name)` name: 25
hits in 11 files. None shares the race. Most are synchronous renders
with no async read at all (static strings such as `Values (measures)`,
`role (deprecated)`). `HookDefaultInspector.reach-11820.test.tsx` awaits
`findByRole('checkbox', { name: 'Account (crm_account)' })`, the
roster's answer, before its synchronous reads.
`AccessExplainPanel.test.tsx` waits for the Object field to become a
select, which is drawn once the roster answered.
`ObjectFieldInspector.usePicklist-10202.test.tsx` awaits one served
option before reading its sibling from the same answer.

The scans' blind spot: a data-derived name in another form, read through
`screen.getBy…` after an unrelated await, is outside both.

## Gates, at `04a3cad2a`

- `pnpm exec vitest run --maxWorkers=2
packages/app-shell/src/views/studio-design/
packages/app-shell/src/views/metadata-admin/inspectors/` (verify lock):
`Test Files 296 passed (296)`, `Tests 2935 passed | 1 skipped (2936)`.
- `pnpm exec vitest run --maxWorkers=2 scripts/__tests__/` (verify
lock): `Test Files 179 passed | 2 skipped (181)`, `Tests 5452 passed | 2
skipped (5454)`.
- `pnpm --filter @object-ui/app-shell type-check` (verify lock; echoes
`@object-ui/app-shell@17.7.0 type-check`, which runs `tsc --noEmit &&
tsc -p tsconfig.test.json`): exit 0, after `turbo run build
--filter='@object-ui/app-shell^...'` built the dependency closure (28 of
28 tasks). `tsc -p tsconfig.test.json --listFilesOnly` lists this test
file. A first run before the closure was built could not resolve the
`@object-ui/*` declarations: NOT MEASURED, not a red.
- `node scripts/check-changeset-presence.mjs`: exit 0 (an empty
frontmatter, the explicit exemption). `check-changeset-no-major.mjs`,
`check-changeset-overwrite.mjs`, `pnpm check:changeset-claims`, `pnpm
check:pending-changeset-literals`: exit 0.
- `pnpm check:new-line-citations`: `0 new citation(s)`, exit 0. `pnpm
check:control-bytes`: exit 0.
- `pnpm check:vi-mock-specifiers`, `check:vi-mock-inherit`,
`check:vi-mock-override-shape`, `check:test-path-roots`: exit 0.
- ESLint, narrowed to the changed file, with an unchanged sibling test
as control: 2 files in the JSON result, 0 errors and 0 warnings each.
`--print-config` shows 119 rules active on the changed file, so it is in
ESLint's population. The config enables no type-aware linting, so this
diff cannot move another file's verdict. The repo-wide `pnpm lint` and
the full sharded `pnpm test` are CI's.

## Acceptance notes

- The sibling test of the same picker,
`HookDefaultInspector.reach-11820.test.tsx`, already waits the way this
change does: its mount helper awaits the roster-labelled checkbox before
any synchronous read.
- objectui#11913 waits on this card (`Blocked-by: objectstack-ai#11978`). PR objectstack-ai#11972
re-enters the queue once this lands.

Session: `https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw`

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation examples package: types tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants