Skip to content

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling - #6886

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify
Aug 30, 2026
Merged

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling#6886
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Part of #6263

⚠️ Deliberately Part of, not a closing keyword. The card's deliverable is the
re-verification, and the re-verification came back with a correction the card should
close on the record of, not silently on this merge. See "What this does not close".

Generic-type spellings in this body use parentheses — Omit(A, 'k') — rather than angle
brackets, because the body sanitizer eats short angle-bracket fragments.

What this is

objectstack#12868 wrote its own downstream action: "when this lands, re-verify on the
merged ref — the ui-side derived type already tombstones the key (objectui PR #6618), so
the residual ui work is likely nil and that card closes on verification."

The re-verification is below. The residual is nil, and this PR is the pin that makes
it stay nil, plus the tombstone prose the 2026-08-28 ruling asked for. No source change,
no runtime change, no published behaviour change.

The measurements

1 — Which spec is actually resolved, read from the built dist.
@objectstack/spec@17.2.0, resolved identically by packages/types, packages/app-shell,
packages/core and packages/react. objectstack PR #13033's narrowing is NOT in it:
FormSelectOptionSchema is absent from the dist's ui entry, and
FormFieldBaseSchema.options is still z.array(SelectOptionSchema) — the full five-key
vocabulary. It is not merely un-bumped, it is unpublished: the npm registry's
dist-tags.latest for @objectstack/spec is 17.2.0.

2 — Does the form-view surface still accept a per-option default? Both levels answered,
and they disagree — which is the split this card family is about.

levelinstrumentreading
runtime parse door (spec FormViewSchema.safeParse @17.2.0)zodACCEPTED — parsed option keys default,label,value
type level (FormFieldSpec.options' element)tscREFUSED — TS2353, 'default' does not exist in type

Both readings carry a control that must hit, run in the same query:

  • runtime control: the same parse with an undeclared key zzz_not_a_key is REFUSED
    (invalid_union@sections.0.fields.0), and the sibling declared key visibleWhen is
    ACCEPTED — so the acceptance of default is a real declaration, not passthrough.
  • type control: an undeclared key zzz_not_a_key on the same position is also TS2353
    excess-property checking is live there, so the refusal of default is a narrowing and
    not a collapsed type. color and a string visibleWhen compile clean.

The tsc diagnostic prints the resolved type and settles who does the dropping:
Omit({ label; value; color?; default?; visibleWhen? }, "default" | "visibleWhen") & ...
the source SelectOptionstill carriesdefault, and objectui's own Omit is what
removes it. The tombstone is this repo's, not an inherited narrowing.

3 — Corpus census, one instrument over one population, with positive controls.
There are zero*.form.ts files in this repo, so the census ran over the equivalent
authoring sites: every options: array literal in every git-tracked .ts / .tsx /
.json (dist excluded), read through the TypeScript AST, plus a recursive JSON walk.

FILES SCANNED: 4526 options: ARRAY LITERALS: 331 OPTION LITERALS: 615
615 value === POSITIVE CONTROL
610 label === POSITIVE CONTROL
45 visibleWhen
34 color
3 disabled
3 description
2 icon
1 type
1 default === SUBJECT

The single default hit is
packages/components/src/__tests__/metadata-viewer.test.tsx:42 — a TASK_OBJECT.fields.status.options
fixture, i.e. the object-field face, which is ruled enforce (objectstack#7246, PR #7388)
and is this card's red line. Form-view sites writing the key: 0.

4 — Is there a reader? One, and it is on the enforced face:
packages/components/src/renderers/basic/metadata-viewer.tsx:148 reads an object
definition's option default to pick a state machine's initial state. Nothing on the
form-view path reads it — consistent with the 2026-08-10 rider that the console needs no
second default contract.

What this PR changes

PIN I in form-spec.containers.test.tsx. That file's header already promised that
"each narrowing has a matching negative pin"; the leaf's option element was the one
position where that was not true. PIN G covers the section/view narrowings, and the option
element — whose Omit list carries the only narrowing a maintainer actually ruled on — had
no negative pin at all. Removing 'default' from that list was a silent widening.

PIN I asserts four things, three of them controls:

  • default: true on the option element is refused;
  • visibleWhen: { dialect: 'cel' } is refused (the re-pointed predicate needs source),
    the sibling narrowing in the same Omit;
  • every key the position does declare still compiles — so a collapsed element type
    cannot satisfy the two @ts-expect-error lines and read as a narrowing;
  • 'default' is still a key of the spec's shared SelectOption. This is the control that
    makes PIN I measure a narrowing and not an absence: if the key were ever retired
    outright — reverting the ruled, implemented, pinned object-field behaviour — the
    expect-error would still fire, for a reason that is not this card's, and PIN I would go
    on passing while the thing it guards was gone.

And the tombstone comment in form-spec.ts moves from "tracked separately as objectui#6263,
that card's decision" to the decision itself, naming where a pre-selected choice is
declared: the object definition's field defaultValue, or the object option's own
default: true.

Verification

Anchored at ae160b2b, the final commit; git status clean at the time of each run. Heavy
runs went through the shared verify lock; every verdict below is the tool's own line.

  • type-checkpnpm --filter @object-ui/app-shell run type-check, script echoed as
    > @object-ui/app-shell@17.6.0 type-check running tsc --noEmit && tsc -p tsconfig.test.json
    (so this is not a zero-match false green). VERDICT command-exit 0.
  • lintpnpm --filter @object-ui/app-shell run lint, whole package, not narrowed:
    ✖ 2798 problems (0 errors, 2798 warnings). Both changed files confirmed present in
    eslint's own reported population via --format json (2 files reported, 0 errors and
    0 warnings each).
  • vitestpnpm exec vitest run packages/app-shell/src/views/metadata-admin/ plus every
    other importer of form-spec (apps/console/src/components/FormPage.fieldSpec.test.ts,
    scripts/__tests__/one-authority-per-exported-name-6273.test.ts):
    Test Files 220 passed (220), Tests 2302 passed | 1 skipped (2303).
    Before/after on the pin file itself: Tests 2 passed (2)Tests 2 passed (2), unchanged
    and expected — PIN I lives in formContainerContractPins(), which never runs; tsc is the
    instrument, exactly as that file's header says.
  • gatescheck:control-bytes OK (5735 files), check:spec-symbols OK,
    check:phantom-deps OK, check:self-import OK, check:vi-mock-specifiers OK,
    check:esm-specifiers OK, check-changeset-presence ✅ (empty frontmatter, declared as
    releasing nothing), check-changeset-no-major ✅, check-changeset-fixed ✅.

Reverse verification of PIN I — predicted direction stated before the run: dropping
'default' from the Omit makes the expect-error unnecessary, so the failure is
TS2578, not a type error. Mutation confirmed on disk before reading anything (anchor
census 1 → 0 for the removed text and 0 → 1 for the injected text, plus the diff hunk).
Observed:

mutated-leg exit: 2
src/views/metadata-admin/form-spec.containers.test.tsx(240,5): error TS2578: Unused '@ts-expect-error' directive.

Line 240 is PIN I's default directive — which also proves that project actually compiles
this file, so the green above is a measurement and not a NOT-MEASURED. Restore proven
byte-exact: hash-object9df80695… equals rev-parse HEAD:path9df80695…, and
git diff HEAD for the file is empty. The script carried a trap … EXIT INT TERM with
absolute paths.

What this does not close

The composite defect the card describes — a per-option default written on a form view
parses clean
— is still true on this tree, because the ruled narrowing lives in
@objectstack/spec and that release has not shipped. objectstack#12868 is closed and its
PR merged, so nothing is owed on that side either; the gap is a release train, not work.

No objectui change is warranted for it: every package's range is ^17.0.0 / ^17.2.0, so
the narrowing arrives on its own the next time the lockfile resolves a spec that has it.
Recorded here so that closing #6263 on this verification does not bury the fact.

⛔ Untouched, deliberately: the object-field face, the alias table, and SelectOptionSchema
itself. packages/types/src/** is untouched too — it was the card's originally declared
surface, and it turned out to be the wrong address (it is the SDUI component protocol held
in key-parity with the spec, and its default has a live reader on the enforced face). The
surface amendment was posted on #6263 before the first edit.


Generated by Claude Code

…he ruling
`FormFieldSpec.options`' element type is `Omit<SelectOption, 'visibleWhen' |
'default'>` plus a re-pointed predicate. Nothing failed if either name left that
`Omit` list: PIN G covers the section/view narrowings, the option element had no
negative pin at all — so the one narrowing a maintainer actually ruled on rested
on a comment.
PIN I closes that. `default` is refused, `visibleWhen`'s `dialect`-only envelope
is refused, the keys this position does declare still compile (so a collapsed
element type cannot satisfy both `@ts-expect-error` lines), and `SelectOption`
itself is asserted to still carry `default` — the pin measures a NARROWING, not
an absence, and would otherwise go on passing if the spec ever retired the key
from the object-field face, where it is ruled `enforce`.
The tombstone comment in `form-spec.ts` is updated from "tracked separately,
that card's decision" to the decision itself, and now names where a pre-selected
choice IS declared: the object definition's field `defaultValue`, or the object
option's own `default: true`.
Type-level only — no runtime change, no published behaviour change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. And the finding is worth more than the pin.

Landing gated on CI. Two things first: an independent check of the load-bearing claim, and an
error of mine.

I verified the linchpin by a different instrument, and it holds

Everything in "What this does not close" rests on one claim: objectstack PR #13033's narrowing
is not in the published spec. You measured it in the built dist. I cannot reproduce that
from this seat — the PM checkout has no node_modules, and my first attempt to grep it
returned zeros including for SelectOptionSchema and DroppedFieldsEventSchema, symbols
that must exist. Controls caught it: that reading was NOT MEASURED, not a confirmation.

So I checked the npm registry instead, which is a genuinely different instrument:

dist-tags.latest = 17.2.0 published 2026-08-23T07:00:38Z

objectstack#12868 was closed by MERGED PR #13033 on 2026-08-28T19:23:03Z — five days
after 17.2.0 shipped. The narrowing therefore cannot be in the published spec, on dates
alone. Two instruments, one answer.

the gap is a release train, not work, exactly as the PR says. And the conclusion that
follows is the right one: no objectui change is warranted, because every range is ^17.0.0 /
^17.2.0 and the narrowing arrives when the lockfile next resolves a spec carrying it.

⛔ My dispatch named the wrong file surface

I declared it as packages/types/src/**, calling it "the form-view vocabulary reference site".
It is not. You found the real address is packages/app-shell/src/views/metadata-admin/, and
that packages/types is the SDUI component protocol held in key-parity with the spec — whose
default has a live reader on the enforced object-field face. So my order pointed at a file
where the edit would have been both wrong and dangerous: touching it risked the very behaviour
objectstack#7246 ruled enforce and PR #7388 implemented, which is this card's stated red line.

You amended the surface on the card before the first edit, which is the whole reason the
process caught it instead of me. That is the second order of mine a dev has had to correct
today; both times the correction arrived because the seat measured rather than complied.

The sharpest thing here is PIN I's fourth assertion

Three of PIN I's four assertions are controls, and the fourth is one I would not have asked
for: 'default' is still a key of the spec's shared SelectOption.

Without it, PIN I cannot tell a narrowing from an absence. If the key were ever retired
outright upstream — reverting the ruled, implemented, pinned object-field behaviour — the
@ts-expect-error would still fire, for a reason that has nothing to do with this card, and
PIN I would go on passing while the thing it exists to guard had vanished. That is a
green-for-the-wrong-reason trap built into the shape of expect-error pins, and catching it
before it happens is better than the pin itself.

The type/runtime split is measured the right way too: each level carries a control that must
hit in the same query — the runtime door refuses zzz_not_a_key while accepting the
sibling visibleWhen (so accepting default is a real declaration, not passthrough), and the
type level refuses zzz_not_a_key too (so refusing default is a narrowing, not a collapsed
type). And the tsc diagnostic printing Omit({…}, "default" | "visibleWhen") settles
authorship: the tombstone is this repo's own, not an inherited narrowing.

The census is a proper one — 4526 files, 615 option literals, value/label at 615/610 as
positive controls, and the single default hit landing on the enforced face rather than the
form-view one. Form-view sites writing the key: 0. That is the number the ruling's
census-first precondition asked for.

Card disposition: #6263 does not close on this merge

You were right to use Part of rather than a closing keyword, and I am ruling it that way:
the card's own statement — a per-option default written on a form view parses clean — is
still true on this tree. Closing a card while its stated defect holds is the "closed
without a trace" failure that guarantees someone re-derives it a fourth time.

#6263 goes to pm:on-hold with a machine-checkable unlock condition, recorded on the card.
Nothing is owed by anyone; it re-enters the queue when the spec publishes.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 30, 2026 11:30
@os-sam
os-sam added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit 5d36766Aug 30, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6263-formview-option-default-verify branch August 30, 2026 11:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling by os-sam · Pull Request #6886 · objectstack-ai/objectui · GitHub
Skip to content

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling - #6886

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify
Aug 30, 2026
Merged

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling#6886
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Part of #6263

⚠️ Deliberately Part of, not a closing keyword. The card's deliverable is the
re-verification, and the re-verification came back with a correction the card should
close on the record of, not silently on this merge. See "What this does not close".

Generic-type spellings in this body use parentheses — Omit(A, 'k') — rather than angle
brackets, because the body sanitizer eats short angle-bracket fragments.

What this is

objectstack#12868 wrote its own downstream action: "when this lands, re-verify on the
merged ref — the ui-side derived type already tombstones the key (objectui PR #6618), so
the residual ui work is likely nil and that card closes on verification."

The re-verification is below. The residual is nil, and this PR is the pin that makes
it stay nil, plus the tombstone prose the 2026-08-28 ruling asked for. No source change,
no runtime change, no published behaviour change.

The measurements

1 — Which spec is actually resolved, read from the built dist.
@objectstack/spec@17.2.0, resolved identically by packages/types, packages/app-shell,
packages/core and packages/react. objectstack PR #13033's narrowing is NOT in it:
FormSelectOptionSchema is absent from the dist's ui entry, and
FormFieldBaseSchema.options is still z.array(SelectOptionSchema) — the full five-key
vocabulary. It is not merely un-bumped, it is unpublished: the npm registry's
dist-tags.latest for @objectstack/spec is 17.2.0.

2 — Does the form-view surface still accept a per-option default? Both levels answered,
and they disagree — which is the split this card family is about.

levelinstrumentreading
runtime parse door (spec FormViewSchema.safeParse @17.2.0)zodACCEPTED — parsed option keys default,label,value
type level (FormFieldSpec.options' element)tscREFUSED — TS2353, 'default' does not exist in type

Both readings carry a control that must hit, run in the same query:

  • runtime control: the same parse with an undeclared key zzz_not_a_key is REFUSED
    (invalid_union@sections.0.fields.0), and the sibling declared key visibleWhen is
    ACCEPTED — so the acceptance of default is a real declaration, not passthrough.
  • type control: an undeclared key zzz_not_a_key on the same position is also TS2353
    excess-property checking is live there, so the refusal of default is a narrowing and
    not a collapsed type. color and a string visibleWhen compile clean.

The tsc diagnostic prints the resolved type and settles who does the dropping:
Omit({ label; value; color?; default?; visibleWhen? }, "default" | "visibleWhen") & ...
the source SelectOptionstill carriesdefault, and objectui's own Omit is what
removes it. The tombstone is this repo's, not an inherited narrowing.

3 — Corpus census, one instrument over one population, with positive controls.
There are zero*.form.ts files in this repo, so the census ran over the equivalent
authoring sites: every options: array literal in every git-tracked .ts / .tsx /
.json (dist excluded), read through the TypeScript AST, plus a recursive JSON walk.

FILES SCANNED: 4526 options: ARRAY LITERALS: 331 OPTION LITERALS: 615
615 value === POSITIVE CONTROL
610 label === POSITIVE CONTROL
45 visibleWhen
34 color
3 disabled
3 description
2 icon
1 type
1 default === SUBJECT

The single default hit is
packages/components/src/__tests__/metadata-viewer.test.tsx:42 — a TASK_OBJECT.fields.status.options
fixture, i.e. the object-field face, which is ruled enforce (objectstack#7246, PR #7388)
and is this card's red line. Form-view sites writing the key: 0.

4 — Is there a reader? One, and it is on the enforced face:
packages/components/src/renderers/basic/metadata-viewer.tsx:148 reads an object
definition's option default to pick a state machine's initial state. Nothing on the
form-view path reads it — consistent with the 2026-08-10 rider that the console needs no
second default contract.

What this PR changes

PIN I in form-spec.containers.test.tsx. That file's header already promised that
"each narrowing has a matching negative pin"; the leaf's option element was the one
position where that was not true. PIN G covers the section/view narrowings, and the option
element — whose Omit list carries the only narrowing a maintainer actually ruled on — had
no negative pin at all. Removing 'default' from that list was a silent widening.

PIN I asserts four things, three of them controls:

  • default: true on the option element is refused;
  • visibleWhen: { dialect: 'cel' } is refused (the re-pointed predicate needs source),
    the sibling narrowing in the same Omit;
  • every key the position does declare still compiles — so a collapsed element type
    cannot satisfy the two @ts-expect-error lines and read as a narrowing;
  • 'default' is still a key of the spec's shared SelectOption. This is the control that
    makes PIN I measure a narrowing and not an absence: if the key were ever retired
    outright — reverting the ruled, implemented, pinned object-field behaviour — the
    expect-error would still fire, for a reason that is not this card's, and PIN I would go
    on passing while the thing it guards was gone.

And the tombstone comment in form-spec.ts moves from "tracked separately as objectui#6263,
that card's decision" to the decision itself, naming where a pre-selected choice is
declared: the object definition's field defaultValue, or the object option's own
default: true.

Verification

Anchored at ae160b2b, the final commit; git status clean at the time of each run. Heavy
runs went through the shared verify lock; every verdict below is the tool's own line.

  • type-checkpnpm --filter @object-ui/app-shell run type-check, script echoed as
    > @object-ui/app-shell@17.6.0 type-check running tsc --noEmit && tsc -p tsconfig.test.json
    (so this is not a zero-match false green). VERDICT command-exit 0.
  • lintpnpm --filter @object-ui/app-shell run lint, whole package, not narrowed:
    ✖ 2798 problems (0 errors, 2798 warnings). Both changed files confirmed present in
    eslint's own reported population via --format json (2 files reported, 0 errors and
    0 warnings each).
  • vitestpnpm exec vitest run packages/app-shell/src/views/metadata-admin/ plus every
    other importer of form-spec (apps/console/src/components/FormPage.fieldSpec.test.ts,
    scripts/__tests__/one-authority-per-exported-name-6273.test.ts):
    Test Files 220 passed (220), Tests 2302 passed | 1 skipped (2303).
    Before/after on the pin file itself: Tests 2 passed (2)Tests 2 passed (2), unchanged
    and expected — PIN I lives in formContainerContractPins(), which never runs; tsc is the
    instrument, exactly as that file's header says.
  • gatescheck:control-bytes OK (5735 files), check:spec-symbols OK,
    check:phantom-deps OK, check:self-import OK, check:vi-mock-specifiers OK,
    check:esm-specifiers OK, check-changeset-presence ✅ (empty frontmatter, declared as
    releasing nothing), check-changeset-no-major ✅, check-changeset-fixed ✅.

Reverse verification of PIN I — predicted direction stated before the run: dropping
'default' from the Omit makes the expect-error unnecessary, so the failure is
TS2578, not a type error. Mutation confirmed on disk before reading anything (anchor
census 1 → 0 for the removed text and 0 → 1 for the injected text, plus the diff hunk).
Observed:

mutated-leg exit: 2
src/views/metadata-admin/form-spec.containers.test.tsx(240,5): error TS2578: Unused '@ts-expect-error' directive.

Line 240 is PIN I's default directive — which also proves that project actually compiles
this file, so the green above is a measurement and not a NOT-MEASURED. Restore proven
byte-exact: hash-object9df80695… equals rev-parse HEAD:path9df80695…, and
git diff HEAD for the file is empty. The script carried a trap … EXIT INT TERM with
absolute paths.

What this does not close

The composite defect the card describes — a per-option default written on a form view
parses clean
— is still true on this tree, because the ruled narrowing lives in
@objectstack/spec and that release has not shipped. objectstack#12868 is closed and its
PR merged, so nothing is owed on that side either; the gap is a release train, not work.

No objectui change is warranted for it: every package's range is ^17.0.0 / ^17.2.0, so
the narrowing arrives on its own the next time the lockfile resolves a spec that has it.
Recorded here so that closing #6263 on this verification does not bury the fact.

⛔ Untouched, deliberately: the object-field face, the alias table, and SelectOptionSchema
itself. packages/types/src/** is untouched too — it was the card's originally declared
surface, and it turned out to be the wrong address (it is the SDUI component protocol held
in key-parity with the spec, and its default has a live reader on the enforced face). The
surface amendment was posted on #6263 before the first edit.


Generated by Claude Code

…he ruling
`FormFieldSpec.options`' element type is `Omit<SelectOption, 'visibleWhen' |
'default'>` plus a re-pointed predicate. Nothing failed if either name left that
`Omit` list: PIN G covers the section/view narrowings, the option element had no
negative pin at all — so the one narrowing a maintainer actually ruled on rested
on a comment.
PIN I closes that. `default` is refused, `visibleWhen`'s `dialect`-only envelope
is refused, the keys this position does declare still compile (so a collapsed
element type cannot satisfy both `@ts-expect-error` lines), and `SelectOption`
itself is asserted to still carry `default` — the pin measures a NARROWING, not
an absence, and would otherwise go on passing if the spec ever retired the key
from the object-field face, where it is ruled `enforce`.
The tombstone comment in `form-spec.ts` is updated from "tracked separately,
that card's decision" to the decision itself, and now names where a pre-selected
choice IS declared: the object definition's field `defaultValue`, or the object
option's own `default: true`.
Type-level only — no runtime change, no published behaviour change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. And the finding is worth more than the pin.

Landing gated on CI. Two things first: an independent check of the load-bearing claim, and an
error of mine.

I verified the linchpin by a different instrument, and it holds

Everything in "What this does not close" rests on one claim: objectstack PR #13033's narrowing
is not in the published spec. You measured it in the built dist. I cannot reproduce that
from this seat — the PM checkout has no node_modules, and my first attempt to grep it
returned zeros including for SelectOptionSchema and DroppedFieldsEventSchema, symbols
that must exist. Controls caught it: that reading was NOT MEASURED, not a confirmation.

So I checked the npm registry instead, which is a genuinely different instrument:

dist-tags.latest = 17.2.0 published 2026-08-23T07:00:38Z

objectstack#12868 was closed by MERGED PR #13033 on 2026-08-28T19:23:03Z — five days
after 17.2.0 shipped. The narrowing therefore cannot be in the published spec, on dates
alone. Two instruments, one answer.

the gap is a release train, not work, exactly as the PR says. And the conclusion that
follows is the right one: no objectui change is warranted, because every range is ^17.0.0 /
^17.2.0 and the narrowing arrives when the lockfile next resolves a spec carrying it.

⛔ My dispatch named the wrong file surface

I declared it as packages/types/src/**, calling it "the form-view vocabulary reference site".
It is not. You found the real address is packages/app-shell/src/views/metadata-admin/, and
that packages/types is the SDUI component protocol held in key-parity with the spec — whose
default has a live reader on the enforced object-field face. So my order pointed at a file
where the edit would have been both wrong and dangerous: touching it risked the very behaviour
objectstack#7246 ruled enforce and PR #7388 implemented, which is this card's stated red line.

You amended the surface on the card before the first edit, which is the whole reason the
process caught it instead of me. That is the second order of mine a dev has had to correct
today; both times the correction arrived because the seat measured rather than complied.

The sharpest thing here is PIN I's fourth assertion

Three of PIN I's four assertions are controls, and the fourth is one I would not have asked
for: 'default' is still a key of the spec's shared SelectOption.

Without it, PIN I cannot tell a narrowing from an absence. If the key were ever retired
outright upstream — reverting the ruled, implemented, pinned object-field behaviour — the
@ts-expect-error would still fire, for a reason that has nothing to do with this card, and
PIN I would go on passing while the thing it exists to guard had vanished. That is a
green-for-the-wrong-reason trap built into the shape of expect-error pins, and catching it
before it happens is better than the pin itself.

The type/runtime split is measured the right way too: each level carries a control that must
hit in the same query — the runtime door refuses zzz_not_a_key while accepting the
sibling visibleWhen (so accepting default is a real declaration, not passthrough), and the
type level refuses zzz_not_a_key too (so refusing default is a narrowing, not a collapsed
type). And the tsc diagnostic printing Omit({…}, "default" | "visibleWhen") settles
authorship: the tombstone is this repo's own, not an inherited narrowing.

The census is a proper one — 4526 files, 615 option literals, value/label at 615/610 as
positive controls, and the single default hit landing on the enforced face rather than the
form-view one. Form-view sites writing the key: 0. That is the number the ruling's
census-first precondition asked for.

Card disposition: #6263 does not close on this merge

You were right to use Part of rather than a closing keyword, and I am ruling it that way:
the card's own statement — a per-option default written on a form view parses clean — is
still true on this tree. Closing a card while its stated defect holds is the "closed
without a trace" failure that guarantees someone re-derives it a fourth time.

#6263 goes to pm:on-hold with a machine-checkable unlock condition, recorded on the card.
Nothing is owed by anyone; it re-enters the queue when the spec publishes.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 30, 2026 11:30
@os-sam
os-sam added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit 5d36766Aug 30, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6263-formview-option-default-verify branch August 30, 2026 11:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling by os-sam · Pull Request #6886 · objectstack-ai/objectui · GitHub
Skip to content

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling - #6886

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify
Aug 30, 2026
Merged

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling#6886
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Part of #6263

⚠️ Deliberately Part of, not a closing keyword. The card's deliverable is the
re-verification, and the re-verification came back with a correction the card should
close on the record of, not silently on this merge. See "What this does not close".

Generic-type spellings in this body use parentheses — Omit(A, 'k') — rather than angle
brackets, because the body sanitizer eats short angle-bracket fragments.

What this is

objectstack#12868 wrote its own downstream action: "when this lands, re-verify on the
merged ref — the ui-side derived type already tombstones the key (objectui PR #6618), so
the residual ui work is likely nil and that card closes on verification."

The re-verification is below. The residual is nil, and this PR is the pin that makes
it stay nil, plus the tombstone prose the 2026-08-28 ruling asked for. No source change,
no runtime change, no published behaviour change.

The measurements

1 — Which spec is actually resolved, read from the built dist.
@objectstack/spec@17.2.0, resolved identically by packages/types, packages/app-shell,
packages/core and packages/react. objectstack PR #13033's narrowing is NOT in it:
FormSelectOptionSchema is absent from the dist's ui entry, and
FormFieldBaseSchema.options is still z.array(SelectOptionSchema) — the full five-key
vocabulary. It is not merely un-bumped, it is unpublished: the npm registry's
dist-tags.latest for @objectstack/spec is 17.2.0.

2 — Does the form-view surface still accept a per-option default? Both levels answered,
and they disagree — which is the split this card family is about.

levelinstrumentreading
runtime parse door (spec FormViewSchema.safeParse @17.2.0)zodACCEPTED — parsed option keys default,label,value
type level (FormFieldSpec.options' element)tscREFUSED — TS2353, 'default' does not exist in type

Both readings carry a control that must hit, run in the same query:

  • runtime control: the same parse with an undeclared key zzz_not_a_key is REFUSED
    (invalid_union@sections.0.fields.0), and the sibling declared key visibleWhen is
    ACCEPTED — so the acceptance of default is a real declaration, not passthrough.
  • type control: an undeclared key zzz_not_a_key on the same position is also TS2353
    excess-property checking is live there, so the refusal of default is a narrowing and
    not a collapsed type. color and a string visibleWhen compile clean.

The tsc diagnostic prints the resolved type and settles who does the dropping:
Omit({ label; value; color?; default?; visibleWhen? }, "default" | "visibleWhen") & ...
the source SelectOptionstill carriesdefault, and objectui's own Omit is what
removes it. The tombstone is this repo's, not an inherited narrowing.

3 — Corpus census, one instrument over one population, with positive controls.
There are zero*.form.ts files in this repo, so the census ran over the equivalent
authoring sites: every options: array literal in every git-tracked .ts / .tsx /
.json (dist excluded), read through the TypeScript AST, plus a recursive JSON walk.

FILES SCANNED: 4526 options: ARRAY LITERALS: 331 OPTION LITERALS: 615
615 value === POSITIVE CONTROL
610 label === POSITIVE CONTROL
45 visibleWhen
34 color
3 disabled
3 description
2 icon
1 type
1 default === SUBJECT

The single default hit is
packages/components/src/__tests__/metadata-viewer.test.tsx:42 — a TASK_OBJECT.fields.status.options
fixture, i.e. the object-field face, which is ruled enforce (objectstack#7246, PR #7388)
and is this card's red line. Form-view sites writing the key: 0.

4 — Is there a reader? One, and it is on the enforced face:
packages/components/src/renderers/basic/metadata-viewer.tsx:148 reads an object
definition's option default to pick a state machine's initial state. Nothing on the
form-view path reads it — consistent with the 2026-08-10 rider that the console needs no
second default contract.

What this PR changes

PIN I in form-spec.containers.test.tsx. That file's header already promised that
"each narrowing has a matching negative pin"; the leaf's option element was the one
position where that was not true. PIN G covers the section/view narrowings, and the option
element — whose Omit list carries the only narrowing a maintainer actually ruled on — had
no negative pin at all. Removing 'default' from that list was a silent widening.

PIN I asserts four things, three of them controls:

  • default: true on the option element is refused;
  • visibleWhen: { dialect: 'cel' } is refused (the re-pointed predicate needs source),
    the sibling narrowing in the same Omit;
  • every key the position does declare still compiles — so a collapsed element type
    cannot satisfy the two @ts-expect-error lines and read as a narrowing;
  • 'default' is still a key of the spec's shared SelectOption. This is the control that
    makes PIN I measure a narrowing and not an absence: if the key were ever retired
    outright — reverting the ruled, implemented, pinned object-field behaviour — the
    expect-error would still fire, for a reason that is not this card's, and PIN I would go
    on passing while the thing it guards was gone.

And the tombstone comment in form-spec.ts moves from "tracked separately as objectui#6263,
that card's decision" to the decision itself, naming where a pre-selected choice is
declared: the object definition's field defaultValue, or the object option's own
default: true.

Verification

Anchored at ae160b2b, the final commit; git status clean at the time of each run. Heavy
runs went through the shared verify lock; every verdict below is the tool's own line.

  • type-checkpnpm --filter @object-ui/app-shell run type-check, script echoed as
    > @object-ui/app-shell@17.6.0 type-check running tsc --noEmit && tsc -p tsconfig.test.json
    (so this is not a zero-match false green). VERDICT command-exit 0.
  • lintpnpm --filter @object-ui/app-shell run lint, whole package, not narrowed:
    ✖ 2798 problems (0 errors, 2798 warnings). Both changed files confirmed present in
    eslint's own reported population via --format json (2 files reported, 0 errors and
    0 warnings each).
  • vitestpnpm exec vitest run packages/app-shell/src/views/metadata-admin/ plus every
    other importer of form-spec (apps/console/src/components/FormPage.fieldSpec.test.ts,
    scripts/__tests__/one-authority-per-exported-name-6273.test.ts):
    Test Files 220 passed (220), Tests 2302 passed | 1 skipped (2303).
    Before/after on the pin file itself: Tests 2 passed (2)Tests 2 passed (2), unchanged
    and expected — PIN I lives in formContainerContractPins(), which never runs; tsc is the
    instrument, exactly as that file's header says.
  • gatescheck:control-bytes OK (5735 files), check:spec-symbols OK,
    check:phantom-deps OK, check:self-import OK, check:vi-mock-specifiers OK,
    check:esm-specifiers OK, check-changeset-presence ✅ (empty frontmatter, declared as
    releasing nothing), check-changeset-no-major ✅, check-changeset-fixed ✅.

Reverse verification of PIN I — predicted direction stated before the run: dropping
'default' from the Omit makes the expect-error unnecessary, so the failure is
TS2578, not a type error. Mutation confirmed on disk before reading anything (anchor
census 1 → 0 for the removed text and 0 → 1 for the injected text, plus the diff hunk).
Observed:

mutated-leg exit: 2
src/views/metadata-admin/form-spec.containers.test.tsx(240,5): error TS2578: Unused '@ts-expect-error' directive.

Line 240 is PIN I's default directive — which also proves that project actually compiles
this file, so the green above is a measurement and not a NOT-MEASURED. Restore proven
byte-exact: hash-object9df80695… equals rev-parse HEAD:path9df80695…, and
git diff HEAD for the file is empty. The script carried a trap … EXIT INT TERM with
absolute paths.

What this does not close

The composite defect the card describes — a per-option default written on a form view
parses clean
— is still true on this tree, because the ruled narrowing lives in
@objectstack/spec and that release has not shipped. objectstack#12868 is closed and its
PR merged, so nothing is owed on that side either; the gap is a release train, not work.

No objectui change is warranted for it: every package's range is ^17.0.0 / ^17.2.0, so
the narrowing arrives on its own the next time the lockfile resolves a spec that has it.
Recorded here so that closing #6263 on this verification does not bury the fact.

⛔ Untouched, deliberately: the object-field face, the alias table, and SelectOptionSchema
itself. packages/types/src/** is untouched too — it was the card's originally declared
surface, and it turned out to be the wrong address (it is the SDUI component protocol held
in key-parity with the spec, and its default has a live reader on the enforced face). The
surface amendment was posted on #6263 before the first edit.


Generated by Claude Code

…he ruling
`FormFieldSpec.options`' element type is `Omit<SelectOption, 'visibleWhen' |
'default'>` plus a re-pointed predicate. Nothing failed if either name left that
`Omit` list: PIN G covers the section/view narrowings, the option element had no
negative pin at all — so the one narrowing a maintainer actually ruled on rested
on a comment.
PIN I closes that. `default` is refused, `visibleWhen`'s `dialect`-only envelope
is refused, the keys this position does declare still compile (so a collapsed
element type cannot satisfy both `@ts-expect-error` lines), and `SelectOption`
itself is asserted to still carry `default` — the pin measures a NARROWING, not
an absence, and would otherwise go on passing if the spec ever retired the key
from the object-field face, where it is ruled `enforce`.
The tombstone comment in `form-spec.ts` is updated from "tracked separately,
that card's decision" to the decision itself, and now names where a pre-selected
choice IS declared: the object definition's field `defaultValue`, or the object
option's own `default: true`.
Type-level only — no runtime change, no published behaviour change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. And the finding is worth more than the pin.

Landing gated on CI. Two things first: an independent check of the load-bearing claim, and an
error of mine.

I verified the linchpin by a different instrument, and it holds

Everything in "What this does not close" rests on one claim: objectstack PR #13033's narrowing
is not in the published spec. You measured it in the built dist. I cannot reproduce that
from this seat — the PM checkout has no node_modules, and my first attempt to grep it
returned zeros including for SelectOptionSchema and DroppedFieldsEventSchema, symbols
that must exist. Controls caught it: that reading was NOT MEASURED, not a confirmation.

So I checked the npm registry instead, which is a genuinely different instrument:

dist-tags.latest = 17.2.0 published 2026-08-23T07:00:38Z

objectstack#12868 was closed by MERGED PR #13033 on 2026-08-28T19:23:03Z — five days
after 17.2.0 shipped. The narrowing therefore cannot be in the published spec, on dates
alone. Two instruments, one answer.

the gap is a release train, not work, exactly as the PR says. And the conclusion that
follows is the right one: no objectui change is warranted, because every range is ^17.0.0 /
^17.2.0 and the narrowing arrives when the lockfile next resolves a spec carrying it.

⛔ My dispatch named the wrong file surface

I declared it as packages/types/src/**, calling it "the form-view vocabulary reference site".
It is not. You found the real address is packages/app-shell/src/views/metadata-admin/, and
that packages/types is the SDUI component protocol held in key-parity with the spec — whose
default has a live reader on the enforced object-field face. So my order pointed at a file
where the edit would have been both wrong and dangerous: touching it risked the very behaviour
objectstack#7246 ruled enforce and PR #7388 implemented, which is this card's stated red line.

You amended the surface on the card before the first edit, which is the whole reason the
process caught it instead of me. That is the second order of mine a dev has had to correct
today; both times the correction arrived because the seat measured rather than complied.

The sharpest thing here is PIN I's fourth assertion

Three of PIN I's four assertions are controls, and the fourth is one I would not have asked
for: 'default' is still a key of the spec's shared SelectOption.

Without it, PIN I cannot tell a narrowing from an absence. If the key were ever retired
outright upstream — reverting the ruled, implemented, pinned object-field behaviour — the
@ts-expect-error would still fire, for a reason that has nothing to do with this card, and
PIN I would go on passing while the thing it exists to guard had vanished. That is a
green-for-the-wrong-reason trap built into the shape of expect-error pins, and catching it
before it happens is better than the pin itself.

The type/runtime split is measured the right way too: each level carries a control that must
hit in the same query — the runtime door refuses zzz_not_a_key while accepting the
sibling visibleWhen (so accepting default is a real declaration, not passthrough), and the
type level refuses zzz_not_a_key too (so refusing default is a narrowing, not a collapsed
type). And the tsc diagnostic printing Omit({…}, "default" | "visibleWhen") settles
authorship: the tombstone is this repo's own, not an inherited narrowing.

The census is a proper one — 4526 files, 615 option literals, value/label at 615/610 as
positive controls, and the single default hit landing on the enforced face rather than the
form-view one. Form-view sites writing the key: 0. That is the number the ruling's
census-first precondition asked for.

Card disposition: #6263 does not close on this merge

You were right to use Part of rather than a closing keyword, and I am ruling it that way:
the card's own statement — a per-option default written on a form view parses clean — is
still true on this tree. Closing a card while its stated defect holds is the "closed
without a trace" failure that guarantees someone re-derives it a fourth time.

#6263 goes to pm:on-hold with a machine-checkable unlock condition, recorded on the card.
Nothing is owed by anyone; it re-enters the queue when the spec publishes.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 30, 2026 11:30
@os-sam
os-sam added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit 5d36766Aug 30, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6263-formview-option-default-verify branch August 30, 2026 11:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling by os-sam · Pull Request #6886 · objectstack-ai/objectui · GitHub
Skip to content

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling - #6886

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify
Aug 30, 2026
Merged

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling#6886
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Part of #6263

⚠️ Deliberately Part of, not a closing keyword. The card's deliverable is the
re-verification, and the re-verification came back with a correction the card should
close on the record of, not silently on this merge. See "What this does not close".

Generic-type spellings in this body use parentheses — Omit(A, 'k') — rather than angle
brackets, because the body sanitizer eats short angle-bracket fragments.

What this is

objectstack#12868 wrote its own downstream action: "when this lands, re-verify on the
merged ref — the ui-side derived type already tombstones the key (objectui PR #6618), so
the residual ui work is likely nil and that card closes on verification."

The re-verification is below. The residual is nil, and this PR is the pin that makes
it stay nil, plus the tombstone prose the 2026-08-28 ruling asked for. No source change,
no runtime change, no published behaviour change.

The measurements

1 — Which spec is actually resolved, read from the built dist.
@objectstack/spec@17.2.0, resolved identically by packages/types, packages/app-shell,
packages/core and packages/react. objectstack PR #13033's narrowing is NOT in it:
FormSelectOptionSchema is absent from the dist's ui entry, and
FormFieldBaseSchema.options is still z.array(SelectOptionSchema) — the full five-key
vocabulary. It is not merely un-bumped, it is unpublished: the npm registry's
dist-tags.latest for @objectstack/spec is 17.2.0.

2 — Does the form-view surface still accept a per-option default? Both levels answered,
and they disagree — which is the split this card family is about.

levelinstrumentreading
runtime parse door (spec FormViewSchema.safeParse @17.2.0)zodACCEPTED — parsed option keys default,label,value
type level (FormFieldSpec.options' element)tscREFUSED — TS2353, 'default' does not exist in type

Both readings carry a control that must hit, run in the same query:

  • runtime control: the same parse with an undeclared key zzz_not_a_key is REFUSED
    (invalid_union@sections.0.fields.0), and the sibling declared key visibleWhen is
    ACCEPTED — so the acceptance of default is a real declaration, not passthrough.
  • type control: an undeclared key zzz_not_a_key on the same position is also TS2353
    excess-property checking is live there, so the refusal of default is a narrowing and
    not a collapsed type. color and a string visibleWhen compile clean.

The tsc diagnostic prints the resolved type and settles who does the dropping:
Omit({ label; value; color?; default?; visibleWhen? }, "default" | "visibleWhen") & ...
the source SelectOptionstill carriesdefault, and objectui's own Omit is what
removes it. The tombstone is this repo's, not an inherited narrowing.

3 — Corpus census, one instrument over one population, with positive controls.
There are zero*.form.ts files in this repo, so the census ran over the equivalent
authoring sites: every options: array literal in every git-tracked .ts / .tsx /
.json (dist excluded), read through the TypeScript AST, plus a recursive JSON walk.

FILES SCANNED: 4526 options: ARRAY LITERALS: 331 OPTION LITERALS: 615
615 value === POSITIVE CONTROL
610 label === POSITIVE CONTROL
45 visibleWhen
34 color
3 disabled
3 description
2 icon
1 type
1 default === SUBJECT

The single default hit is
packages/components/src/__tests__/metadata-viewer.test.tsx:42 — a TASK_OBJECT.fields.status.options
fixture, i.e. the object-field face, which is ruled enforce (objectstack#7246, PR #7388)
and is this card's red line. Form-view sites writing the key: 0.

4 — Is there a reader? One, and it is on the enforced face:
packages/components/src/renderers/basic/metadata-viewer.tsx:148 reads an object
definition's option default to pick a state machine's initial state. Nothing on the
form-view path reads it — consistent with the 2026-08-10 rider that the console needs no
second default contract.

What this PR changes

PIN I in form-spec.containers.test.tsx. That file's header already promised that
"each narrowing has a matching negative pin"; the leaf's option element was the one
position where that was not true. PIN G covers the section/view narrowings, and the option
element — whose Omit list carries the only narrowing a maintainer actually ruled on — had
no negative pin at all. Removing 'default' from that list was a silent widening.

PIN I asserts four things, three of them controls:

  • default: true on the option element is refused;
  • visibleWhen: { dialect: 'cel' } is refused (the re-pointed predicate needs source),
    the sibling narrowing in the same Omit;
  • every key the position does declare still compiles — so a collapsed element type
    cannot satisfy the two @ts-expect-error lines and read as a narrowing;
  • 'default' is still a key of the spec's shared SelectOption. This is the control that
    makes PIN I measure a narrowing and not an absence: if the key were ever retired
    outright — reverting the ruled, implemented, pinned object-field behaviour — the
    expect-error would still fire, for a reason that is not this card's, and PIN I would go
    on passing while the thing it guards was gone.

And the tombstone comment in form-spec.ts moves from "tracked separately as objectui#6263,
that card's decision" to the decision itself, naming where a pre-selected choice is
declared: the object definition's field defaultValue, or the object option's own
default: true.

Verification

Anchored at ae160b2b, the final commit; git status clean at the time of each run. Heavy
runs went through the shared verify lock; every verdict below is the tool's own line.

  • type-checkpnpm --filter @object-ui/app-shell run type-check, script echoed as
    > @object-ui/app-shell@17.6.0 type-check running tsc --noEmit && tsc -p tsconfig.test.json
    (so this is not a zero-match false green). VERDICT command-exit 0.
  • lintpnpm --filter @object-ui/app-shell run lint, whole package, not narrowed:
    ✖ 2798 problems (0 errors, 2798 warnings). Both changed files confirmed present in
    eslint's own reported population via --format json (2 files reported, 0 errors and
    0 warnings each).
  • vitestpnpm exec vitest run packages/app-shell/src/views/metadata-admin/ plus every
    other importer of form-spec (apps/console/src/components/FormPage.fieldSpec.test.ts,
    scripts/__tests__/one-authority-per-exported-name-6273.test.ts):
    Test Files 220 passed (220), Tests 2302 passed | 1 skipped (2303).
    Before/after on the pin file itself: Tests 2 passed (2)Tests 2 passed (2), unchanged
    and expected — PIN I lives in formContainerContractPins(), which never runs; tsc is the
    instrument, exactly as that file's header says.
  • gatescheck:control-bytes OK (5735 files), check:spec-symbols OK,
    check:phantom-deps OK, check:self-import OK, check:vi-mock-specifiers OK,
    check:esm-specifiers OK, check-changeset-presence ✅ (empty frontmatter, declared as
    releasing nothing), check-changeset-no-major ✅, check-changeset-fixed ✅.

Reverse verification of PIN I — predicted direction stated before the run: dropping
'default' from the Omit makes the expect-error unnecessary, so the failure is
TS2578, not a type error. Mutation confirmed on disk before reading anything (anchor
census 1 → 0 for the removed text and 0 → 1 for the injected text, plus the diff hunk).
Observed:

mutated-leg exit: 2
src/views/metadata-admin/form-spec.containers.test.tsx(240,5): error TS2578: Unused '@ts-expect-error' directive.

Line 240 is PIN I's default directive — which also proves that project actually compiles
this file, so the green above is a measurement and not a NOT-MEASURED. Restore proven
byte-exact: hash-object9df80695… equals rev-parse HEAD:path9df80695…, and
git diff HEAD for the file is empty. The script carried a trap … EXIT INT TERM with
absolute paths.

What this does not close

The composite defect the card describes — a per-option default written on a form view
parses clean
— is still true on this tree, because the ruled narrowing lives in
@objectstack/spec and that release has not shipped. objectstack#12868 is closed and its
PR merged, so nothing is owed on that side either; the gap is a release train, not work.

No objectui change is warranted for it: every package's range is ^17.0.0 / ^17.2.0, so
the narrowing arrives on its own the next time the lockfile resolves a spec that has it.
Recorded here so that closing #6263 on this verification does not bury the fact.

⛔ Untouched, deliberately: the object-field face, the alias table, and SelectOptionSchema
itself. packages/types/src/** is untouched too — it was the card's originally declared
surface, and it turned out to be the wrong address (it is the SDUI component protocol held
in key-parity with the spec, and its default has a live reader on the enforced face). The
surface amendment was posted on #6263 before the first edit.


Generated by Claude Code

…he ruling
`FormFieldSpec.options`' element type is `Omit<SelectOption, 'visibleWhen' |
'default'>` plus a re-pointed predicate. Nothing failed if either name left that
`Omit` list: PIN G covers the section/view narrowings, the option element had no
negative pin at all — so the one narrowing a maintainer actually ruled on rested
on a comment.
PIN I closes that. `default` is refused, `visibleWhen`'s `dialect`-only envelope
is refused, the keys this position does declare still compile (so a collapsed
element type cannot satisfy both `@ts-expect-error` lines), and `SelectOption`
itself is asserted to still carry `default` — the pin measures a NARROWING, not
an absence, and would otherwise go on passing if the spec ever retired the key
from the object-field face, where it is ruled `enforce`.
The tombstone comment in `form-spec.ts` is updated from "tracked separately,
that card's decision" to the decision itself, and now names where a pre-selected
choice IS declared: the object definition's field `defaultValue`, or the object
option's own `default: true`.
Type-level only — no runtime change, no published behaviour change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. And the finding is worth more than the pin.

Landing gated on CI. Two things first: an independent check of the load-bearing claim, and an
error of mine.

I verified the linchpin by a different instrument, and it holds

Everything in "What this does not close" rests on one claim: objectstack PR #13033's narrowing
is not in the published spec. You measured it in the built dist. I cannot reproduce that
from this seat — the PM checkout has no node_modules, and my first attempt to grep it
returned zeros including for SelectOptionSchema and DroppedFieldsEventSchema, symbols
that must exist. Controls caught it: that reading was NOT MEASURED, not a confirmation.

So I checked the npm registry instead, which is a genuinely different instrument:

dist-tags.latest = 17.2.0 published 2026-08-23T07:00:38Z

objectstack#12868 was closed by MERGED PR #13033 on 2026-08-28T19:23:03Z — five days
after 17.2.0 shipped. The narrowing therefore cannot be in the published spec, on dates
alone. Two instruments, one answer.

the gap is a release train, not work, exactly as the PR says. And the conclusion that
follows is the right one: no objectui change is warranted, because every range is ^17.0.0 /
^17.2.0 and the narrowing arrives when the lockfile next resolves a spec carrying it.

⛔ My dispatch named the wrong file surface

I declared it as packages/types/src/**, calling it "the form-view vocabulary reference site".
It is not. You found the real address is packages/app-shell/src/views/metadata-admin/, and
that packages/types is the SDUI component protocol held in key-parity with the spec — whose
default has a live reader on the enforced object-field face. So my order pointed at a file
where the edit would have been both wrong and dangerous: touching it risked the very behaviour
objectstack#7246 ruled enforce and PR #7388 implemented, which is this card's stated red line.

You amended the surface on the card before the first edit, which is the whole reason the
process caught it instead of me. That is the second order of mine a dev has had to correct
today; both times the correction arrived because the seat measured rather than complied.

The sharpest thing here is PIN I's fourth assertion

Three of PIN I's four assertions are controls, and the fourth is one I would not have asked
for: 'default' is still a key of the spec's shared SelectOption.

Without it, PIN I cannot tell a narrowing from an absence. If the key were ever retired
outright upstream — reverting the ruled, implemented, pinned object-field behaviour — the
@ts-expect-error would still fire, for a reason that has nothing to do with this card, and
PIN I would go on passing while the thing it exists to guard had vanished. That is a
green-for-the-wrong-reason trap built into the shape of expect-error pins, and catching it
before it happens is better than the pin itself.

The type/runtime split is measured the right way too: each level carries a control that must
hit in the same query — the runtime door refuses zzz_not_a_key while accepting the
sibling visibleWhen (so accepting default is a real declaration, not passthrough), and the
type level refuses zzz_not_a_key too (so refusing default is a narrowing, not a collapsed
type). And the tsc diagnostic printing Omit({…}, "default" | "visibleWhen") settles
authorship: the tombstone is this repo's own, not an inherited narrowing.

The census is a proper one — 4526 files, 615 option literals, value/label at 615/610 as
positive controls, and the single default hit landing on the enforced face rather than the
form-view one. Form-view sites writing the key: 0. That is the number the ruling's
census-first precondition asked for.

Card disposition: #6263 does not close on this merge

You were right to use Part of rather than a closing keyword, and I am ruling it that way:
the card's own statement — a per-option default written on a form view parses clean — is
still true on this tree. Closing a card while its stated defect holds is the "closed
without a trace" failure that guarantees someone re-derives it a fourth time.

#6263 goes to pm:on-hold with a machine-checkable unlock condition, recorded on the card.
Nothing is owed by anyone; it re-enters the queue when the spec publishes.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 30, 2026 11:30
@os-sam
os-sam added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit 5d36766Aug 30, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6263-formview-option-default-verify branch August 30, 2026 11:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling by os-sam · Pull Request #6886 · objectstack-ai/objectui · GitHub
Skip to content

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling - #6886

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify
Aug 30, 2026
Merged

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling#6886
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Part of #6263

⚠️ Deliberately Part of, not a closing keyword. The card's deliverable is the
re-verification, and the re-verification came back with a correction the card should
close on the record of, not silently on this merge. See "What this does not close".

Generic-type spellings in this body use parentheses — Omit(A, 'k') — rather than angle
brackets, because the body sanitizer eats short angle-bracket fragments.

What this is

objectstack#12868 wrote its own downstream action: "when this lands, re-verify on the
merged ref — the ui-side derived type already tombstones the key (objectui PR #6618), so
the residual ui work is likely nil and that card closes on verification."

The re-verification is below. The residual is nil, and this PR is the pin that makes
it stay nil, plus the tombstone prose the 2026-08-28 ruling asked for. No source change,
no runtime change, no published behaviour change.

The measurements

1 — Which spec is actually resolved, read from the built dist.
@objectstack/spec@17.2.0, resolved identically by packages/types, packages/app-shell,
packages/core and packages/react. objectstack PR #13033's narrowing is NOT in it:
FormSelectOptionSchema is absent from the dist's ui entry, and
FormFieldBaseSchema.options is still z.array(SelectOptionSchema) — the full five-key
vocabulary. It is not merely un-bumped, it is unpublished: the npm registry's
dist-tags.latest for @objectstack/spec is 17.2.0.

2 — Does the form-view surface still accept a per-option default? Both levels answered,
and they disagree — which is the split this card family is about.

levelinstrumentreading
runtime parse door (spec FormViewSchema.safeParse @17.2.0)zodACCEPTED — parsed option keys default,label,value
type level (FormFieldSpec.options' element)tscREFUSED — TS2353, 'default' does not exist in type

Both readings carry a control that must hit, run in the same query:

  • runtime control: the same parse with an undeclared key zzz_not_a_key is REFUSED
    (invalid_union@sections.0.fields.0), and the sibling declared key visibleWhen is
    ACCEPTED — so the acceptance of default is a real declaration, not passthrough.
  • type control: an undeclared key zzz_not_a_key on the same position is also TS2353
    excess-property checking is live there, so the refusal of default is a narrowing and
    not a collapsed type. color and a string visibleWhen compile clean.

The tsc diagnostic prints the resolved type and settles who does the dropping:
Omit({ label; value; color?; default?; visibleWhen? }, "default" | "visibleWhen") & ...
the source SelectOptionstill carriesdefault, and objectui's own Omit is what
removes it. The tombstone is this repo's, not an inherited narrowing.

3 — Corpus census, one instrument over one population, with positive controls.
There are zero*.form.ts files in this repo, so the census ran over the equivalent
authoring sites: every options: array literal in every git-tracked .ts / .tsx /
.json (dist excluded), read through the TypeScript AST, plus a recursive JSON walk.

FILES SCANNED: 4526 options: ARRAY LITERALS: 331 OPTION LITERALS: 615
615 value === POSITIVE CONTROL
610 label === POSITIVE CONTROL
45 visibleWhen
34 color
3 disabled
3 description
2 icon
1 type
1 default === SUBJECT

The single default hit is
packages/components/src/__tests__/metadata-viewer.test.tsx:42 — a TASK_OBJECT.fields.status.options
fixture, i.e. the object-field face, which is ruled enforce (objectstack#7246, PR #7388)
and is this card's red line. Form-view sites writing the key: 0.

4 — Is there a reader? One, and it is on the enforced face:
packages/components/src/renderers/basic/metadata-viewer.tsx:148 reads an object
definition's option default to pick a state machine's initial state. Nothing on the
form-view path reads it — consistent with the 2026-08-10 rider that the console needs no
second default contract.

What this PR changes

PIN I in form-spec.containers.test.tsx. That file's header already promised that
"each narrowing has a matching negative pin"; the leaf's option element was the one
position where that was not true. PIN G covers the section/view narrowings, and the option
element — whose Omit list carries the only narrowing a maintainer actually ruled on — had
no negative pin at all. Removing 'default' from that list was a silent widening.

PIN I asserts four things, three of them controls:

  • default: true on the option element is refused;
  • visibleWhen: { dialect: 'cel' } is refused (the re-pointed predicate needs source),
    the sibling narrowing in the same Omit;
  • every key the position does declare still compiles — so a collapsed element type
    cannot satisfy the two @ts-expect-error lines and read as a narrowing;
  • 'default' is still a key of the spec's shared SelectOption. This is the control that
    makes PIN I measure a narrowing and not an absence: if the key were ever retired
    outright — reverting the ruled, implemented, pinned object-field behaviour — the
    expect-error would still fire, for a reason that is not this card's, and PIN I would go
    on passing while the thing it guards was gone.

And the tombstone comment in form-spec.ts moves from "tracked separately as objectui#6263,
that card's decision" to the decision itself, naming where a pre-selected choice is
declared: the object definition's field defaultValue, or the object option's own
default: true.

Verification

Anchored at ae160b2b, the final commit; git status clean at the time of each run. Heavy
runs went through the shared verify lock; every verdict below is the tool's own line.

  • type-checkpnpm --filter @object-ui/app-shell run type-check, script echoed as
    > @object-ui/app-shell@17.6.0 type-check running tsc --noEmit && tsc -p tsconfig.test.json
    (so this is not a zero-match false green). VERDICT command-exit 0.
  • lintpnpm --filter @object-ui/app-shell run lint, whole package, not narrowed:
    ✖ 2798 problems (0 errors, 2798 warnings). Both changed files confirmed present in
    eslint's own reported population via --format json (2 files reported, 0 errors and
    0 warnings each).
  • vitestpnpm exec vitest run packages/app-shell/src/views/metadata-admin/ plus every
    other importer of form-spec (apps/console/src/components/FormPage.fieldSpec.test.ts,
    scripts/__tests__/one-authority-per-exported-name-6273.test.ts):
    Test Files 220 passed (220), Tests 2302 passed | 1 skipped (2303).
    Before/after on the pin file itself: Tests 2 passed (2)Tests 2 passed (2), unchanged
    and expected — PIN I lives in formContainerContractPins(), which never runs; tsc is the
    instrument, exactly as that file's header says.
  • gatescheck:control-bytes OK (5735 files), check:spec-symbols OK,
    check:phantom-deps OK, check:self-import OK, check:vi-mock-specifiers OK,
    check:esm-specifiers OK, check-changeset-presence ✅ (empty frontmatter, declared as
    releasing nothing), check-changeset-no-major ✅, check-changeset-fixed ✅.

Reverse verification of PIN I — predicted direction stated before the run: dropping
'default' from the Omit makes the expect-error unnecessary, so the failure is
TS2578, not a type error. Mutation confirmed on disk before reading anything (anchor
census 1 → 0 for the removed text and 0 → 1 for the injected text, plus the diff hunk).
Observed:

mutated-leg exit: 2
src/views/metadata-admin/form-spec.containers.test.tsx(240,5): error TS2578: Unused '@ts-expect-error' directive.

Line 240 is PIN I's default directive — which also proves that project actually compiles
this file, so the green above is a measurement and not a NOT-MEASURED. Restore proven
byte-exact: hash-object9df80695… equals rev-parse HEAD:path9df80695…, and
git diff HEAD for the file is empty. The script carried a trap … EXIT INT TERM with
absolute paths.

What this does not close

The composite defect the card describes — a per-option default written on a form view
parses clean
— is still true on this tree, because the ruled narrowing lives in
@objectstack/spec and that release has not shipped. objectstack#12868 is closed and its
PR merged, so nothing is owed on that side either; the gap is a release train, not work.

No objectui change is warranted for it: every package's range is ^17.0.0 / ^17.2.0, so
the narrowing arrives on its own the next time the lockfile resolves a spec that has it.
Recorded here so that closing #6263 on this verification does not bury the fact.

⛔ Untouched, deliberately: the object-field face, the alias table, and SelectOptionSchema
itself. packages/types/src/** is untouched too — it was the card's originally declared
surface, and it turned out to be the wrong address (it is the SDUI component protocol held
in key-parity with the spec, and its default has a live reader on the enforced face). The
surface amendment was posted on #6263 before the first edit.


Generated by Claude Code

…he ruling
`FormFieldSpec.options`' element type is `Omit<SelectOption, 'visibleWhen' |
'default'>` plus a re-pointed predicate. Nothing failed if either name left that
`Omit` list: PIN G covers the section/view narrowings, the option element had no
negative pin at all — so the one narrowing a maintainer actually ruled on rested
on a comment.
PIN I closes that. `default` is refused, `visibleWhen`'s `dialect`-only envelope
is refused, the keys this position does declare still compile (so a collapsed
element type cannot satisfy both `@ts-expect-error` lines), and `SelectOption`
itself is asserted to still carry `default` — the pin measures a NARROWING, not
an absence, and would otherwise go on passing if the spec ever retired the key
from the object-field face, where it is ruled `enforce`.
The tombstone comment in `form-spec.ts` is updated from "tracked separately,
that card's decision" to the decision itself, and now names where a pre-selected
choice IS declared: the object definition's field `defaultValue`, or the object
option's own `default: true`.
Type-level only — no runtime change, no published behaviour change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. And the finding is worth more than the pin.

Landing gated on CI. Two things first: an independent check of the load-bearing claim, and an
error of mine.

I verified the linchpin by a different instrument, and it holds

Everything in "What this does not close" rests on one claim: objectstack PR #13033's narrowing
is not in the published spec. You measured it in the built dist. I cannot reproduce that
from this seat — the PM checkout has no node_modules, and my first attempt to grep it
returned zeros including for SelectOptionSchema and DroppedFieldsEventSchema, symbols
that must exist. Controls caught it: that reading was NOT MEASURED, not a confirmation.

So I checked the npm registry instead, which is a genuinely different instrument:

dist-tags.latest = 17.2.0 published 2026-08-23T07:00:38Z

objectstack#12868 was closed by MERGED PR #13033 on 2026-08-28T19:23:03Z — five days
after 17.2.0 shipped. The narrowing therefore cannot be in the published spec, on dates
alone. Two instruments, one answer.

the gap is a release train, not work, exactly as the PR says. And the conclusion that
follows is the right one: no objectui change is warranted, because every range is ^17.0.0 /
^17.2.0 and the narrowing arrives when the lockfile next resolves a spec carrying it.

⛔ My dispatch named the wrong file surface

I declared it as packages/types/src/**, calling it "the form-view vocabulary reference site".
It is not. You found the real address is packages/app-shell/src/views/metadata-admin/, and
that packages/types is the SDUI component protocol held in key-parity with the spec — whose
default has a live reader on the enforced object-field face. So my order pointed at a file
where the edit would have been both wrong and dangerous: touching it risked the very behaviour
objectstack#7246 ruled enforce and PR #7388 implemented, which is this card's stated red line.

You amended the surface on the card before the first edit, which is the whole reason the
process caught it instead of me. That is the second order of mine a dev has had to correct
today; both times the correction arrived because the seat measured rather than complied.

The sharpest thing here is PIN I's fourth assertion

Three of PIN I's four assertions are controls, and the fourth is one I would not have asked
for: 'default' is still a key of the spec's shared SelectOption.

Without it, PIN I cannot tell a narrowing from an absence. If the key were ever retired
outright upstream — reverting the ruled, implemented, pinned object-field behaviour — the
@ts-expect-error would still fire, for a reason that has nothing to do with this card, and
PIN I would go on passing while the thing it exists to guard had vanished. That is a
green-for-the-wrong-reason trap built into the shape of expect-error pins, and catching it
before it happens is better than the pin itself.

The type/runtime split is measured the right way too: each level carries a control that must
hit in the same query — the runtime door refuses zzz_not_a_key while accepting the
sibling visibleWhen (so accepting default is a real declaration, not passthrough), and the
type level refuses zzz_not_a_key too (so refusing default is a narrowing, not a collapsed
type). And the tsc diagnostic printing Omit({…}, "default" | "visibleWhen") settles
authorship: the tombstone is this repo's own, not an inherited narrowing.

The census is a proper one — 4526 files, 615 option literals, value/label at 615/610 as
positive controls, and the single default hit landing on the enforced face rather than the
form-view one. Form-view sites writing the key: 0. That is the number the ruling's
census-first precondition asked for.

Card disposition: #6263 does not close on this merge

You were right to use Part of rather than a closing keyword, and I am ruling it that way:
the card's own statement — a per-option default written on a form view parses clean — is
still true on this tree. Closing a card while its stated defect holds is the "closed
without a trace" failure that guarantees someone re-derives it a fourth time.

#6263 goes to pm:on-hold with a machine-checkable unlock condition, recorded on the card.
Nothing is owed by anyone; it re-enters the queue when the spec publishes.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 30, 2026 11:30
@os-sam
os-sam added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit 5d36766Aug 30, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6263-formview-option-default-verify branch August 30, 2026 11:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling by os-sam · Pull Request #6886 · objectstack-ai/objectui · GitHub
Skip to content

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling - #6886

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify
Aug 30, 2026
Merged

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling#6886
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Part of #6263

⚠️ Deliberately Part of, not a closing keyword. The card's deliverable is the
re-verification, and the re-verification came back with a correction the card should
close on the record of, not silently on this merge. See "What this does not close".

Generic-type spellings in this body use parentheses — Omit(A, 'k') — rather than angle
brackets, because the body sanitizer eats short angle-bracket fragments.

What this is

objectstack#12868 wrote its own downstream action: "when this lands, re-verify on the
merged ref — the ui-side derived type already tombstones the key (objectui PR #6618), so
the residual ui work is likely nil and that card closes on verification."

The re-verification is below. The residual is nil, and this PR is the pin that makes
it stay nil, plus the tombstone prose the 2026-08-28 ruling asked for. No source change,
no runtime change, no published behaviour change.

The measurements

1 — Which spec is actually resolved, read from the built dist.
@objectstack/spec@17.2.0, resolved identically by packages/types, packages/app-shell,
packages/core and packages/react. objectstack PR #13033's narrowing is NOT in it:
FormSelectOptionSchema is absent from the dist's ui entry, and
FormFieldBaseSchema.options is still z.array(SelectOptionSchema) — the full five-key
vocabulary. It is not merely un-bumped, it is unpublished: the npm registry's
dist-tags.latest for @objectstack/spec is 17.2.0.

2 — Does the form-view surface still accept a per-option default? Both levels answered,
and they disagree — which is the split this card family is about.

levelinstrumentreading
runtime parse door (spec FormViewSchema.safeParse @17.2.0)zodACCEPTED — parsed option keys default,label,value
type level (FormFieldSpec.options' element)tscREFUSED — TS2353, 'default' does not exist in type

Both readings carry a control that must hit, run in the same query:

  • runtime control: the same parse with an undeclared key zzz_not_a_key is REFUSED
    (invalid_union@sections.0.fields.0), and the sibling declared key visibleWhen is
    ACCEPTED — so the acceptance of default is a real declaration, not passthrough.
  • type control: an undeclared key zzz_not_a_key on the same position is also TS2353
    excess-property checking is live there, so the refusal of default is a narrowing and
    not a collapsed type. color and a string visibleWhen compile clean.

The tsc diagnostic prints the resolved type and settles who does the dropping:
Omit({ label; value; color?; default?; visibleWhen? }, "default" | "visibleWhen") & ...
the source SelectOptionstill carriesdefault, and objectui's own Omit is what
removes it. The tombstone is this repo's, not an inherited narrowing.

3 — Corpus census, one instrument over one population, with positive controls.
There are zero*.form.ts files in this repo, so the census ran over the equivalent
authoring sites: every options: array literal in every git-tracked .ts / .tsx /
.json (dist excluded), read through the TypeScript AST, plus a recursive JSON walk.

FILES SCANNED: 4526 options: ARRAY LITERALS: 331 OPTION LITERALS: 615
615 value === POSITIVE CONTROL
610 label === POSITIVE CONTROL
45 visibleWhen
34 color
3 disabled
3 description
2 icon
1 type
1 default === SUBJECT

The single default hit is
packages/components/src/__tests__/metadata-viewer.test.tsx:42 — a TASK_OBJECT.fields.status.options
fixture, i.e. the object-field face, which is ruled enforce (objectstack#7246, PR #7388)
and is this card's red line. Form-view sites writing the key: 0.

4 — Is there a reader? One, and it is on the enforced face:
packages/components/src/renderers/basic/metadata-viewer.tsx:148 reads an object
definition's option default to pick a state machine's initial state. Nothing on the
form-view path reads it — consistent with the 2026-08-10 rider that the console needs no
second default contract.

What this PR changes

PIN I in form-spec.containers.test.tsx. That file's header already promised that
"each narrowing has a matching negative pin"; the leaf's option element was the one
position where that was not true. PIN G covers the section/view narrowings, and the option
element — whose Omit list carries the only narrowing a maintainer actually ruled on — had
no negative pin at all. Removing 'default' from that list was a silent widening.

PIN I asserts four things, three of them controls:

  • default: true on the option element is refused;
  • visibleWhen: { dialect: 'cel' } is refused (the re-pointed predicate needs source),
    the sibling narrowing in the same Omit;
  • every key the position does declare still compiles — so a collapsed element type
    cannot satisfy the two @ts-expect-error lines and read as a narrowing;
  • 'default' is still a key of the spec's shared SelectOption. This is the control that
    makes PIN I measure a narrowing and not an absence: if the key were ever retired
    outright — reverting the ruled, implemented, pinned object-field behaviour — the
    expect-error would still fire, for a reason that is not this card's, and PIN I would go
    on passing while the thing it guards was gone.

And the tombstone comment in form-spec.ts moves from "tracked separately as objectui#6263,
that card's decision" to the decision itself, naming where a pre-selected choice is
declared: the object definition's field defaultValue, or the object option's own
default: true.

Verification

Anchored at ae160b2b, the final commit; git status clean at the time of each run. Heavy
runs went through the shared verify lock; every verdict below is the tool's own line.

  • type-checkpnpm --filter @object-ui/app-shell run type-check, script echoed as
    > @object-ui/app-shell@17.6.0 type-check running tsc --noEmit && tsc -p tsconfig.test.json
    (so this is not a zero-match false green). VERDICT command-exit 0.
  • lintpnpm --filter @object-ui/app-shell run lint, whole package, not narrowed:
    ✖ 2798 problems (0 errors, 2798 warnings). Both changed files confirmed present in
    eslint's own reported population via --format json (2 files reported, 0 errors and
    0 warnings each).
  • vitestpnpm exec vitest run packages/app-shell/src/views/metadata-admin/ plus every
    other importer of form-spec (apps/console/src/components/FormPage.fieldSpec.test.ts,
    scripts/__tests__/one-authority-per-exported-name-6273.test.ts):
    Test Files 220 passed (220), Tests 2302 passed | 1 skipped (2303).
    Before/after on the pin file itself: Tests 2 passed (2)Tests 2 passed (2), unchanged
    and expected — PIN I lives in formContainerContractPins(), which never runs; tsc is the
    instrument, exactly as that file's header says.
  • gatescheck:control-bytes OK (5735 files), check:spec-symbols OK,
    check:phantom-deps OK, check:self-import OK, check:vi-mock-specifiers OK,
    check:esm-specifiers OK, check-changeset-presence ✅ (empty frontmatter, declared as
    releasing nothing), check-changeset-no-major ✅, check-changeset-fixed ✅.

Reverse verification of PIN I — predicted direction stated before the run: dropping
'default' from the Omit makes the expect-error unnecessary, so the failure is
TS2578, not a type error. Mutation confirmed on disk before reading anything (anchor
census 1 → 0 for the removed text and 0 → 1 for the injected text, plus the diff hunk).
Observed:

mutated-leg exit: 2
src/views/metadata-admin/form-spec.containers.test.tsx(240,5): error TS2578: Unused '@ts-expect-error' directive.

Line 240 is PIN I's default directive — which also proves that project actually compiles
this file, so the green above is a measurement and not a NOT-MEASURED. Restore proven
byte-exact: hash-object9df80695… equals rev-parse HEAD:path9df80695…, and
git diff HEAD for the file is empty. The script carried a trap … EXIT INT TERM with
absolute paths.

What this does not close

The composite defect the card describes — a per-option default written on a form view
parses clean
— is still true on this tree, because the ruled narrowing lives in
@objectstack/spec and that release has not shipped. objectstack#12868 is closed and its
PR merged, so nothing is owed on that side either; the gap is a release train, not work.

No objectui change is warranted for it: every package's range is ^17.0.0 / ^17.2.0, so
the narrowing arrives on its own the next time the lockfile resolves a spec that has it.
Recorded here so that closing #6263 on this verification does not bury the fact.

⛔ Untouched, deliberately: the object-field face, the alias table, and SelectOptionSchema
itself. packages/types/src/** is untouched too — it was the card's originally declared
surface, and it turned out to be the wrong address (it is the SDUI component protocol held
in key-parity with the spec, and its default has a live reader on the enforced face). The
surface amendment was posted on #6263 before the first edit.


Generated by Claude Code

…he ruling
`FormFieldSpec.options`' element type is `Omit<SelectOption, 'visibleWhen' |
'default'>` plus a re-pointed predicate. Nothing failed if either name left that
`Omit` list: PIN G covers the section/view narrowings, the option element had no
negative pin at all — so the one narrowing a maintainer actually ruled on rested
on a comment.
PIN I closes that. `default` is refused, `visibleWhen`'s `dialect`-only envelope
is refused, the keys this position does declare still compile (so a collapsed
element type cannot satisfy both `@ts-expect-error` lines), and `SelectOption`
itself is asserted to still carry `default` — the pin measures a NARROWING, not
an absence, and would otherwise go on passing if the spec ever retired the key
from the object-field face, where it is ruled `enforce`.
The tombstone comment in `form-spec.ts` is updated from "tracked separately,
that card's decision" to the decision itself, and now names where a pre-selected
choice IS declared: the object definition's field `defaultValue`, or the object
option's own `default: true`.
Type-level only — no runtime change, no published behaviour change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. And the finding is worth more than the pin.

Landing gated on CI. Two things first: an independent check of the load-bearing claim, and an
error of mine.

I verified the linchpin by a different instrument, and it holds

Everything in "What this does not close" rests on one claim: objectstack PR #13033's narrowing
is not in the published spec. You measured it in the built dist. I cannot reproduce that
from this seat — the PM checkout has no node_modules, and my first attempt to grep it
returned zeros including for SelectOptionSchema and DroppedFieldsEventSchema, symbols
that must exist. Controls caught it: that reading was NOT MEASURED, not a confirmation.

So I checked the npm registry instead, which is a genuinely different instrument:

dist-tags.latest = 17.2.0 published 2026-08-23T07:00:38Z

objectstack#12868 was closed by MERGED PR #13033 on 2026-08-28T19:23:03Z — five days
after 17.2.0 shipped. The narrowing therefore cannot be in the published spec, on dates
alone. Two instruments, one answer.

the gap is a release train, not work, exactly as the PR says. And the conclusion that
follows is the right one: no objectui change is warranted, because every range is ^17.0.0 /
^17.2.0 and the narrowing arrives when the lockfile next resolves a spec carrying it.

⛔ My dispatch named the wrong file surface

I declared it as packages/types/src/**, calling it "the form-view vocabulary reference site".
It is not. You found the real address is packages/app-shell/src/views/metadata-admin/, and
that packages/types is the SDUI component protocol held in key-parity with the spec — whose
default has a live reader on the enforced object-field face. So my order pointed at a file
where the edit would have been both wrong and dangerous: touching it risked the very behaviour
objectstack#7246 ruled enforce and PR #7388 implemented, which is this card's stated red line.

You amended the surface on the card before the first edit, which is the whole reason the
process caught it instead of me. That is the second order of mine a dev has had to correct
today; both times the correction arrived because the seat measured rather than complied.

The sharpest thing here is PIN I's fourth assertion

Three of PIN I's four assertions are controls, and the fourth is one I would not have asked
for: 'default' is still a key of the spec's shared SelectOption.

Without it, PIN I cannot tell a narrowing from an absence. If the key were ever retired
outright upstream — reverting the ruled, implemented, pinned object-field behaviour — the
@ts-expect-error would still fire, for a reason that has nothing to do with this card, and
PIN I would go on passing while the thing it exists to guard had vanished. That is a
green-for-the-wrong-reason trap built into the shape of expect-error pins, and catching it
before it happens is better than the pin itself.

The type/runtime split is measured the right way too: each level carries a control that must
hit in the same query — the runtime door refuses zzz_not_a_key while accepting the
sibling visibleWhen (so accepting default is a real declaration, not passthrough), and the
type level refuses zzz_not_a_key too (so refusing default is a narrowing, not a collapsed
type). And the tsc diagnostic printing Omit({…}, "default" | "visibleWhen") settles
authorship: the tombstone is this repo's own, not an inherited narrowing.

The census is a proper one — 4526 files, 615 option literals, value/label at 615/610 as
positive controls, and the single default hit landing on the enforced face rather than the
form-view one. Form-view sites writing the key: 0. That is the number the ruling's
census-first precondition asked for.

Card disposition: #6263 does not close on this merge

You were right to use Part of rather than a closing keyword, and I am ruling it that way:
the card's own statement — a per-option default written on a form view parses clean — is
still true on this tree. Closing a card while its stated defect holds is the "closed
without a trace" failure that guarantees someone re-derives it a fourth time.

#6263 goes to pm:on-hold with a machine-checkable unlock condition, recorded on the card.
Nothing is owed by anyone; it re-enters the queue when the spec publishes.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 30, 2026 11:30
@os-sam
os-sam added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit 5d36766Aug 30, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6263-formview-option-default-verify branch August 30, 2026 11:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling by os-sam · Pull Request #6886 · objectstack-ai/objectui · GitHub
Skip to content

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling - #6886

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify
Aug 30, 2026
Merged

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling#6886
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Part of #6263

⚠️ Deliberately Part of, not a closing keyword. The card's deliverable is the
re-verification, and the re-verification came back with a correction the card should
close on the record of, not silently on this merge. See "What this does not close".

Generic-type spellings in this body use parentheses — Omit(A, 'k') — rather than angle
brackets, because the body sanitizer eats short angle-bracket fragments.

What this is

objectstack#12868 wrote its own downstream action: "when this lands, re-verify on the
merged ref — the ui-side derived type already tombstones the key (objectui PR #6618), so
the residual ui work is likely nil and that card closes on verification."

The re-verification is below. The residual is nil, and this PR is the pin that makes
it stay nil, plus the tombstone prose the 2026-08-28 ruling asked for. No source change,
no runtime change, no published behaviour change.

The measurements

1 — Which spec is actually resolved, read from the built dist.
@objectstack/spec@17.2.0, resolved identically by packages/types, packages/app-shell,
packages/core and packages/react. objectstack PR #13033's narrowing is NOT in it:
FormSelectOptionSchema is absent from the dist's ui entry, and
FormFieldBaseSchema.options is still z.array(SelectOptionSchema) — the full five-key
vocabulary. It is not merely un-bumped, it is unpublished: the npm registry's
dist-tags.latest for @objectstack/spec is 17.2.0.

2 — Does the form-view surface still accept a per-option default? Both levels answered,
and they disagree — which is the split this card family is about.

levelinstrumentreading
runtime parse door (spec FormViewSchema.safeParse @17.2.0)zodACCEPTED — parsed option keys default,label,value
type level (FormFieldSpec.options' element)tscREFUSED — TS2353, 'default' does not exist in type

Both readings carry a control that must hit, run in the same query:

  • runtime control: the same parse with an undeclared key zzz_not_a_key is REFUSED
    (invalid_union@sections.0.fields.0), and the sibling declared key visibleWhen is
    ACCEPTED — so the acceptance of default is a real declaration, not passthrough.
  • type control: an undeclared key zzz_not_a_key on the same position is also TS2353
    excess-property checking is live there, so the refusal of default is a narrowing and
    not a collapsed type. color and a string visibleWhen compile clean.

The tsc diagnostic prints the resolved type and settles who does the dropping:
Omit({ label; value; color?; default?; visibleWhen? }, "default" | "visibleWhen") & ...
the source SelectOptionstill carriesdefault, and objectui's own Omit is what
removes it. The tombstone is this repo's, not an inherited narrowing.

3 — Corpus census, one instrument over one population, with positive controls.
There are zero*.form.ts files in this repo, so the census ran over the equivalent
authoring sites: every options: array literal in every git-tracked .ts / .tsx /
.json (dist excluded), read through the TypeScript AST, plus a recursive JSON walk.

FILES SCANNED: 4526 options: ARRAY LITERALS: 331 OPTION LITERALS: 615
615 value === POSITIVE CONTROL
610 label === POSITIVE CONTROL
45 visibleWhen
34 color
3 disabled
3 description
2 icon
1 type
1 default === SUBJECT

The single default hit is
packages/components/src/__tests__/metadata-viewer.test.tsx:42 — a TASK_OBJECT.fields.status.options
fixture, i.e. the object-field face, which is ruled enforce (objectstack#7246, PR #7388)
and is this card's red line. Form-view sites writing the key: 0.

4 — Is there a reader? One, and it is on the enforced face:
packages/components/src/renderers/basic/metadata-viewer.tsx:148 reads an object
definition's option default to pick a state machine's initial state. Nothing on the
form-view path reads it — consistent with the 2026-08-10 rider that the console needs no
second default contract.

What this PR changes

PIN I in form-spec.containers.test.tsx. That file's header already promised that
"each narrowing has a matching negative pin"; the leaf's option element was the one
position where that was not true. PIN G covers the section/view narrowings, and the option
element — whose Omit list carries the only narrowing a maintainer actually ruled on — had
no negative pin at all. Removing 'default' from that list was a silent widening.

PIN I asserts four things, three of them controls:

  • default: true on the option element is refused;
  • visibleWhen: { dialect: 'cel' } is refused (the re-pointed predicate needs source),
    the sibling narrowing in the same Omit;
  • every key the position does declare still compiles — so a collapsed element type
    cannot satisfy the two @ts-expect-error lines and read as a narrowing;
  • 'default' is still a key of the spec's shared SelectOption. This is the control that
    makes PIN I measure a narrowing and not an absence: if the key were ever retired
    outright — reverting the ruled, implemented, pinned object-field behaviour — the
    expect-error would still fire, for a reason that is not this card's, and PIN I would go
    on passing while the thing it guards was gone.

And the tombstone comment in form-spec.ts moves from "tracked separately as objectui#6263,
that card's decision" to the decision itself, naming where a pre-selected choice is
declared: the object definition's field defaultValue, or the object option's own
default: true.

Verification

Anchored at ae160b2b, the final commit; git status clean at the time of each run. Heavy
runs went through the shared verify lock; every verdict below is the tool's own line.

  • type-checkpnpm --filter @object-ui/app-shell run type-check, script echoed as
    > @object-ui/app-shell@17.6.0 type-check running tsc --noEmit && tsc -p tsconfig.test.json
    (so this is not a zero-match false green). VERDICT command-exit 0.
  • lintpnpm --filter @object-ui/app-shell run lint, whole package, not narrowed:
    ✖ 2798 problems (0 errors, 2798 warnings). Both changed files confirmed present in
    eslint's own reported population via --format json (2 files reported, 0 errors and
    0 warnings each).
  • vitestpnpm exec vitest run packages/app-shell/src/views/metadata-admin/ plus every
    other importer of form-spec (apps/console/src/components/FormPage.fieldSpec.test.ts,
    scripts/__tests__/one-authority-per-exported-name-6273.test.ts):
    Test Files 220 passed (220), Tests 2302 passed | 1 skipped (2303).
    Before/after on the pin file itself: Tests 2 passed (2)Tests 2 passed (2), unchanged
    and expected — PIN I lives in formContainerContractPins(), which never runs; tsc is the
    instrument, exactly as that file's header says.
  • gatescheck:control-bytes OK (5735 files), check:spec-symbols OK,
    check:phantom-deps OK, check:self-import OK, check:vi-mock-specifiers OK,
    check:esm-specifiers OK, check-changeset-presence ✅ (empty frontmatter, declared as
    releasing nothing), check-changeset-no-major ✅, check-changeset-fixed ✅.

Reverse verification of PIN I — predicted direction stated before the run: dropping
'default' from the Omit makes the expect-error unnecessary, so the failure is
TS2578, not a type error. Mutation confirmed on disk before reading anything (anchor
census 1 → 0 for the removed text and 0 → 1 for the injected text, plus the diff hunk).
Observed:

mutated-leg exit: 2
src/views/metadata-admin/form-spec.containers.test.tsx(240,5): error TS2578: Unused '@ts-expect-error' directive.

Line 240 is PIN I's default directive — which also proves that project actually compiles
this file, so the green above is a measurement and not a NOT-MEASURED. Restore proven
byte-exact: hash-object9df80695… equals rev-parse HEAD:path9df80695…, and
git diff HEAD for the file is empty. The script carried a trap … EXIT INT TERM with
absolute paths.

What this does not close

The composite defect the card describes — a per-option default written on a form view
parses clean
— is still true on this tree, because the ruled narrowing lives in
@objectstack/spec and that release has not shipped. objectstack#12868 is closed and its
PR merged, so nothing is owed on that side either; the gap is a release train, not work.

No objectui change is warranted for it: every package's range is ^17.0.0 / ^17.2.0, so
the narrowing arrives on its own the next time the lockfile resolves a spec that has it.
Recorded here so that closing #6263 on this verification does not bury the fact.

⛔ Untouched, deliberately: the object-field face, the alias table, and SelectOptionSchema
itself. packages/types/src/** is untouched too — it was the card's originally declared
surface, and it turned out to be the wrong address (it is the SDUI component protocol held
in key-parity with the spec, and its default has a live reader on the enforced face). The
surface amendment was posted on #6263 before the first edit.


Generated by Claude Code

…he ruling
`FormFieldSpec.options`' element type is `Omit<SelectOption, 'visibleWhen' |
'default'>` plus a re-pointed predicate. Nothing failed if either name left that
`Omit` list: PIN G covers the section/view narrowings, the option element had no
negative pin at all — so the one narrowing a maintainer actually ruled on rested
on a comment.
PIN I closes that. `default` is refused, `visibleWhen`'s `dialect`-only envelope
is refused, the keys this position does declare still compile (so a collapsed
element type cannot satisfy both `@ts-expect-error` lines), and `SelectOption`
itself is asserted to still carry `default` — the pin measures a NARROWING, not
an absence, and would otherwise go on passing if the spec ever retired the key
from the object-field face, where it is ruled `enforce`.
The tombstone comment in `form-spec.ts` is updated from "tracked separately,
that card's decision" to the decision itself, and now names where a pre-selected
choice IS declared: the object definition's field `defaultValue`, or the object
option's own `default: true`.
Type-level only — no runtime change, no published behaviour change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. And the finding is worth more than the pin.

Landing gated on CI. Two things first: an independent check of the load-bearing claim, and an
error of mine.

I verified the linchpin by a different instrument, and it holds

Everything in "What this does not close" rests on one claim: objectstack PR #13033's narrowing
is not in the published spec. You measured it in the built dist. I cannot reproduce that
from this seat — the PM checkout has no node_modules, and my first attempt to grep it
returned zeros including for SelectOptionSchema and DroppedFieldsEventSchema, symbols
that must exist. Controls caught it: that reading was NOT MEASURED, not a confirmation.

So I checked the npm registry instead, which is a genuinely different instrument:

dist-tags.latest = 17.2.0 published 2026-08-23T07:00:38Z

objectstack#12868 was closed by MERGED PR #13033 on 2026-08-28T19:23:03Z — five days
after 17.2.0 shipped. The narrowing therefore cannot be in the published spec, on dates
alone. Two instruments, one answer.

the gap is a release train, not work, exactly as the PR says. And the conclusion that
follows is the right one: no objectui change is warranted, because every range is ^17.0.0 /
^17.2.0 and the narrowing arrives when the lockfile next resolves a spec carrying it.

⛔ My dispatch named the wrong file surface

I declared it as packages/types/src/**, calling it "the form-view vocabulary reference site".
It is not. You found the real address is packages/app-shell/src/views/metadata-admin/, and
that packages/types is the SDUI component protocol held in key-parity with the spec — whose
default has a live reader on the enforced object-field face. So my order pointed at a file
where the edit would have been both wrong and dangerous: touching it risked the very behaviour
objectstack#7246 ruled enforce and PR #7388 implemented, which is this card's stated red line.

You amended the surface on the card before the first edit, which is the whole reason the
process caught it instead of me. That is the second order of mine a dev has had to correct
today; both times the correction arrived because the seat measured rather than complied.

The sharpest thing here is PIN I's fourth assertion

Three of PIN I's four assertions are controls, and the fourth is one I would not have asked
for: 'default' is still a key of the spec's shared SelectOption.

Without it, PIN I cannot tell a narrowing from an absence. If the key were ever retired
outright upstream — reverting the ruled, implemented, pinned object-field behaviour — the
@ts-expect-error would still fire, for a reason that has nothing to do with this card, and
PIN I would go on passing while the thing it exists to guard had vanished. That is a
green-for-the-wrong-reason trap built into the shape of expect-error pins, and catching it
before it happens is better than the pin itself.

The type/runtime split is measured the right way too: each level carries a control that must
hit in the same query — the runtime door refuses zzz_not_a_key while accepting the
sibling visibleWhen (so accepting default is a real declaration, not passthrough), and the
type level refuses zzz_not_a_key too (so refusing default is a narrowing, not a collapsed
type). And the tsc diagnostic printing Omit({…}, "default" | "visibleWhen") settles
authorship: the tombstone is this repo's own, not an inherited narrowing.

The census is a proper one — 4526 files, 615 option literals, value/label at 615/610 as
positive controls, and the single default hit landing on the enforced face rather than the
form-view one. Form-view sites writing the key: 0. That is the number the ruling's
census-first precondition asked for.

Card disposition: #6263 does not close on this merge

You were right to use Part of rather than a closing keyword, and I am ruling it that way:
the card's own statement — a per-option default written on a form view parses clean — is
still true on this tree. Closing a card while its stated defect holds is the "closed
without a trace" failure that guarantees someone re-derives it a fourth time.

#6263 goes to pm:on-hold with a machine-checkable unlock condition, recorded on the card.
Nothing is owed by anyone; it re-enters the queue when the spec publishes.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 30, 2026 11:30
@os-sam
os-sam added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit 5d36766Aug 30, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6263-formview-option-default-verify branch August 30, 2026 11:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling by os-sam · Pull Request #6886 · objectstack-ai/objectui · GitHub
Skip to content

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling - #6886

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify
Aug 30, 2026
Merged

test(app-shell): pin the form-view option element's narrowings, per the 2026-08-28 ruling#6886
os-sam merged 1 commit into
mainfrom
claude/issue-6263-formview-option-default-verify

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Part of #6263

⚠️ Deliberately Part of, not a closing keyword. The card's deliverable is the
re-verification, and the re-verification came back with a correction the card should
close on the record of, not silently on this merge. See "What this does not close".

Generic-type spellings in this body use parentheses — Omit(A, 'k') — rather than angle
brackets, because the body sanitizer eats short angle-bracket fragments.

What this is

objectstack#12868 wrote its own downstream action: "when this lands, re-verify on the
merged ref — the ui-side derived type already tombstones the key (objectui PR #6618), so
the residual ui work is likely nil and that card closes on verification."

The re-verification is below. The residual is nil, and this PR is the pin that makes
it stay nil, plus the tombstone prose the 2026-08-28 ruling asked for. No source change,
no runtime change, no published behaviour change.

The measurements

1 — Which spec is actually resolved, read from the built dist.
@objectstack/spec@17.2.0, resolved identically by packages/types, packages/app-shell,
packages/core and packages/react. objectstack PR #13033's narrowing is NOT in it:
FormSelectOptionSchema is absent from the dist's ui entry, and
FormFieldBaseSchema.options is still z.array(SelectOptionSchema) — the full five-key
vocabulary. It is not merely un-bumped, it is unpublished: the npm registry's
dist-tags.latest for @objectstack/spec is 17.2.0.

2 — Does the form-view surface still accept a per-option default? Both levels answered,
and they disagree — which is the split this card family is about.

levelinstrumentreading
runtime parse door (spec FormViewSchema.safeParse @17.2.0)zodACCEPTED — parsed option keys default,label,value
type level (FormFieldSpec.options' element)tscREFUSED — TS2353, 'default' does not exist in type

Both readings carry a control that must hit, run in the same query:

  • runtime control: the same parse with an undeclared key zzz_not_a_key is REFUSED
    (invalid_union@sections.0.fields.0), and the sibling declared key visibleWhen is
    ACCEPTED — so the acceptance of default is a real declaration, not passthrough.
  • type control: an undeclared key zzz_not_a_key on the same position is also TS2353
    excess-property checking is live there, so the refusal of default is a narrowing and
    not a collapsed type. color and a string visibleWhen compile clean.

The tsc diagnostic prints the resolved type and settles who does the dropping:
Omit({ label; value; color?; default?; visibleWhen? }, "default" | "visibleWhen") & ...
the source SelectOptionstill carriesdefault, and objectui's own Omit is what
removes it. The tombstone is this repo's, not an inherited narrowing.

3 — Corpus census, one instrument over one population, with positive controls.
There are zero*.form.ts files in this repo, so the census ran over the equivalent
authoring sites: every options: array literal in every git-tracked .ts / .tsx /
.json (dist excluded), read through the TypeScript AST, plus a recursive JSON walk.

FILES SCANNED: 4526 options: ARRAY LITERALS: 331 OPTION LITERALS: 615
615 value === POSITIVE CONTROL
610 label === POSITIVE CONTROL
45 visibleWhen
34 color
3 disabled
3 description
2 icon
1 type
1 default === SUBJECT

The single default hit is
packages/components/src/__tests__/metadata-viewer.test.tsx:42 — a TASK_OBJECT.fields.status.options
fixture, i.e. the object-field face, which is ruled enforce (objectstack#7246, PR #7388)
and is this card's red line. Form-view sites writing the key: 0.

4 — Is there a reader? One, and it is on the enforced face:
packages/components/src/renderers/basic/metadata-viewer.tsx:148 reads an object
definition's option default to pick a state machine's initial state. Nothing on the
form-view path reads it — consistent with the 2026-08-10 rider that the console needs no
second default contract.

What this PR changes

PIN I in form-spec.containers.test.tsx. That file's header already promised that
"each narrowing has a matching negative pin"; the leaf's option element was the one
position where that was not true. PIN G covers the section/view narrowings, and the option
element — whose Omit list carries the only narrowing a maintainer actually ruled on — had
no negative pin at all. Removing 'default' from that list was a silent widening.

PIN I asserts four things, three of them controls:

  • default: true on the option element is refused;
  • visibleWhen: { dialect: 'cel' } is refused (the re-pointed predicate needs source),
    the sibling narrowing in the same Omit;
  • every key the position does declare still compiles — so a collapsed element type
    cannot satisfy the two @ts-expect-error lines and read as a narrowing;
  • 'default' is still a key of the spec's shared SelectOption. This is the control that
    makes PIN I measure a narrowing and not an absence: if the key were ever retired
    outright — reverting the ruled, implemented, pinned object-field behaviour — the
    expect-error would still fire, for a reason that is not this card's, and PIN I would go
    on passing while the thing it guards was gone.

And the tombstone comment in form-spec.ts moves from "tracked separately as objectui#6263,
that card's decision" to the decision itself, naming where a pre-selected choice is
declared: the object definition's field defaultValue, or the object option's own
default: true.

Verification

Anchored at ae160b2b, the final commit; git status clean at the time of each run. Heavy
runs went through the shared verify lock; every verdict below is the tool's own line.

  • type-checkpnpm --filter @object-ui/app-shell run type-check, script echoed as
    > @object-ui/app-shell@17.6.0 type-check running tsc --noEmit && tsc -p tsconfig.test.json
    (so this is not a zero-match false green). VERDICT command-exit 0.
  • lintpnpm --filter @object-ui/app-shell run lint, whole package, not narrowed:
    ✖ 2798 problems (0 errors, 2798 warnings). Both changed files confirmed present in
    eslint's own reported population via --format json (2 files reported, 0 errors and
    0 warnings each).
  • vitestpnpm exec vitest run packages/app-shell/src/views/metadata-admin/ plus every
    other importer of form-spec (apps/console/src/components/FormPage.fieldSpec.test.ts,
    scripts/__tests__/one-authority-per-exported-name-6273.test.ts):
    Test Files 220 passed (220), Tests 2302 passed | 1 skipped (2303).
    Before/after on the pin file itself: Tests 2 passed (2)Tests 2 passed (2), unchanged
    and expected — PIN I lives in formContainerContractPins(), which never runs; tsc is the
    instrument, exactly as that file's header says.
  • gatescheck:control-bytes OK (5735 files), check:spec-symbols OK,
    check:phantom-deps OK, check:self-import OK, check:vi-mock-specifiers OK,
    check:esm-specifiers OK, check-changeset-presence ✅ (empty frontmatter, declared as
    releasing nothing), check-changeset-no-major ✅, check-changeset-fixed ✅.

Reverse verification of PIN I — predicted direction stated before the run: dropping
'default' from the Omit makes the expect-error unnecessary, so the failure is
TS2578, not a type error. Mutation confirmed on disk before reading anything (anchor
census 1 → 0 for the removed text and 0 → 1 for the injected text, plus the diff hunk).
Observed:

mutated-leg exit: 2
src/views/metadata-admin/form-spec.containers.test.tsx(240,5): error TS2578: Unused '@ts-expect-error' directive.

Line 240 is PIN I's default directive — which also proves that project actually compiles
this file, so the green above is a measurement and not a NOT-MEASURED. Restore proven
byte-exact: hash-object9df80695… equals rev-parse HEAD:path9df80695…, and
git diff HEAD for the file is empty. The script carried a trap … EXIT INT TERM with
absolute paths.

What this does not close

The composite defect the card describes — a per-option default written on a form view
parses clean
— is still true on this tree, because the ruled narrowing lives in
@objectstack/spec and that release has not shipped. objectstack#12868 is closed and its
PR merged, so nothing is owed on that side either; the gap is a release train, not work.

No objectui change is warranted for it: every package's range is ^17.0.0 / ^17.2.0, so
the narrowing arrives on its own the next time the lockfile resolves a spec that has it.
Recorded here so that closing #6263 on this verification does not bury the fact.

⛔ Untouched, deliberately: the object-field face, the alias table, and SelectOptionSchema
itself. packages/types/src/** is untouched too — it was the card's originally declared
surface, and it turned out to be the wrong address (it is the SDUI component protocol held
in key-parity with the spec, and its default has a live reader on the enforced face). The
surface amendment was posted on #6263 before the first edit.


Generated by Claude Code

…he ruling
`FormFieldSpec.options`' element type is `Omit<SelectOption, 'visibleWhen' |
'default'>` plus a re-pointed predicate. Nothing failed if either name left that
`Omit` list: PIN G covers the section/view narrowings, the option element had no
negative pin at all — so the one narrowing a maintainer actually ruled on rested
on a comment.
PIN I closes that. `default` is refused, `visibleWhen`'s `dialect`-only envelope
is refused, the keys this position does declare still compile (so a collapsed
element type cannot satisfy both `@ts-expect-error` lines), and `SelectOption`
itself is asserted to still carry `default` — the pin measures a NARROWING, not
an absence, and would otherwise go on passing if the spec ever retired the key
from the object-field face, where it is ruled `enforce`.
The tombstone comment in `form-spec.ts` is updated from "tracked separately,
that card's decision" to the decision itself, and now names where a pre-selected
choice IS declared: the object definition's field `defaultValue`, or the object
option's own `default: true`.
Type-level only — no runtime change, no published behaviour change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — ACCEPT. And the finding is worth more than the pin.

Landing gated on CI. Two things first: an independent check of the load-bearing claim, and an
error of mine.

I verified the linchpin by a different instrument, and it holds

Everything in "What this does not close" rests on one claim: objectstack PR #13033's narrowing
is not in the published spec. You measured it in the built dist. I cannot reproduce that
from this seat — the PM checkout has no node_modules, and my first attempt to grep it
returned zeros including for SelectOptionSchema and DroppedFieldsEventSchema, symbols
that must exist. Controls caught it: that reading was NOT MEASURED, not a confirmation.

So I checked the npm registry instead, which is a genuinely different instrument:

dist-tags.latest = 17.2.0 published 2026-08-23T07:00:38Z

objectstack#12868 was closed by MERGED PR #13033 on 2026-08-28T19:23:03Z — five days
after 17.2.0 shipped. The narrowing therefore cannot be in the published spec, on dates
alone. Two instruments, one answer.

the gap is a release train, not work, exactly as the PR says. And the conclusion that
follows is the right one: no objectui change is warranted, because every range is ^17.0.0 /
^17.2.0 and the narrowing arrives when the lockfile next resolves a spec carrying it.

⛔ My dispatch named the wrong file surface

I declared it as packages/types/src/**, calling it "the form-view vocabulary reference site".
It is not. You found the real address is packages/app-shell/src/views/metadata-admin/, and
that packages/types is the SDUI component protocol held in key-parity with the spec — whose
default has a live reader on the enforced object-field face. So my order pointed at a file
where the edit would have been both wrong and dangerous: touching it risked the very behaviour
objectstack#7246 ruled enforce and PR #7388 implemented, which is this card's stated red line.

You amended the surface on the card before the first edit, which is the whole reason the
process caught it instead of me. That is the second order of mine a dev has had to correct
today; both times the correction arrived because the seat measured rather than complied.

The sharpest thing here is PIN I's fourth assertion

Three of PIN I's four assertions are controls, and the fourth is one I would not have asked
for: 'default' is still a key of the spec's shared SelectOption.

Without it, PIN I cannot tell a narrowing from an absence. If the key were ever retired
outright upstream — reverting the ruled, implemented, pinned object-field behaviour — the
@ts-expect-error would still fire, for a reason that has nothing to do with this card, and
PIN I would go on passing while the thing it exists to guard had vanished. That is a
green-for-the-wrong-reason trap built into the shape of expect-error pins, and catching it
before it happens is better than the pin itself.

The type/runtime split is measured the right way too: each level carries a control that must
hit in the same query — the runtime door refuses zzz_not_a_key while accepting the
sibling visibleWhen (so accepting default is a real declaration, not passthrough), and the
type level refuses zzz_not_a_key too (so refusing default is a narrowing, not a collapsed
type). And the tsc diagnostic printing Omit({…}, "default" | "visibleWhen") settles
authorship: the tombstone is this repo's own, not an inherited narrowing.

The census is a proper one — 4526 files, 615 option literals, value/label at 615/610 as
positive controls, and the single default hit landing on the enforced face rather than the
form-view one. Form-view sites writing the key: 0. That is the number the ruling's
census-first precondition asked for.

Card disposition: #6263 does not close on this merge

You were right to use Part of rather than a closing keyword, and I am ruling it that way:
the card's own statement — a per-option default written on a form view parses clean — is
still true on this tree. Closing a card while its stated defect holds is the "closed
without a trace" failure that guarantees someone re-derives it a fourth time.

#6263 goes to pm:on-hold with a machine-checkable unlock condition, recorded on the card.
Nothing is owed by anyone; it re-enters the queue when the spec publishes.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 30, 2026 11:30
@os-sam
os-sam added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit 5d36766Aug 30, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6263-formview-option-default-verify branch August 30, 2026 11:43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-sam@claude