Skip to content

Commit da99a4b

Browse files
test(ui): relax type-level restatements of spec literal unions; keep the two exhaustive pins on their consumers (#26328)
Fixes #26275 Clause-②: no Metadata-protocol: none ## What Type Check UI went red on every ui PR whenever `@objectstack/spec` added a member to a union that a ui test pinned exactly with `Equal<…>`, even when no ui behaviour changed. This PR measures every such pin, decides each one, and relaxes the restatements. The pins over a union that ui is genuinely total over stay exact, and their red now names the ui site that needs the new case. The surface is test files only: 14 files under `ui/packages/**/__tests__` or `*.test.ts(x)`. No source file, sdui record or published file changes, so it carries `skip-changeset`. ## Population: 85, not 51 The card's figure (51 pins in 31 files at `df8aac41f`) came from a scanner whose list was not published. I re-measured at main `96635f57` with a TypeScript AST scan. It counts every `Equal`-family assertion (`Equal`, `IsEqual`, `Equals`, `TypeEqual`, `Eq`, `Same`, `Exact`, `SameType`, `SameKeys`, `DeclaresExactly`, including file-local aliases) where one argument is a literal union. That gives **85 candidates in 56 files**, including the card's example (`PendingActionStatus` in plugin-chatbot `spec-symbol-batch6.test.ts`). The scan was rerun after #26272 merged and found the same set. A syntactic scan cannot tell which unions the spec owns, so each verdict comes from an experiment, not from reading. I mutated the built spec declarations (`packages/spec/dist/**/*.d.ts` and `.d.mts`, which ui test configs resolve through `paths: {}`) in three ways: 1. **members:** a new member on every `z.ZodEnum`, literal `ZodUnion` and quoted literal union; 2. **keys:** a new key on every `z.ZodObject` and interface; 3. **tuples:** a new member on every literal tuple. I then ran `tsc --noEmit` and `tsc -p tsconfig.test.json` on all 10 packages that host a candidate. A pin that stays green under all three probes is one that a spec addition cannot move, so it was not part of the problem. ## Verdicts | Verdict | Count | |---|---| | restatement → relaxed | 12 | | exhaustive consumer → kept exact, red points at the consumer | 2 | | kept: a measured drift ledger, not a restatement | 1 | | kept: probe artifact (the probe desynchronises a tuple from its enum) | 1 | | kept: green under every probe | 69 | **Relaxation form.** A relaxed pin becomes `Equal<Exclude<'a' | 'b' | …, U>, never>`, so every member ui reads is still required. Where the old pin also guarded against widening, it is paired with `Equal<string extends U ? true : false, false>` (or the file's existing "is not string" pin), so the union cannot quietly widen to `string`. No assertion about a member ui reads was weakened. **The two exhaustive pins** stay exact: - `_ReasonIsTheEnum` (`DroppedFieldsEvent['reason']`, data-objectstack `spec-symbol-batch6.test.ts`). Its consumer is `STRIPPED_LINE` in app-shell `writeWarningToast.ts`, a `Record<DroppedFieldsEvent['reason'], StrippedLine>`. - `_VisualizationUnionIsTheNineDrawnKinds` (core `normalize-list-view.undrawableBothLegs-9880.test.ts`). Its consumer is `LIST_VIEW_KINDS` in `normalize-list-view.ts`, a total `Record`, plus `ListView`'s `case` list. Both consumers are typed totally, so they go red on their own, and each pin's comment now names its consumer as the place to add the new case. ### Full table The Pin column is the alias name, or "(unnamed)" for an inline assertion. Files are under `ui/packages/`. | # | File (under ui/packages) | Pin | Pinned type | Red on a spec addition? | Verdict | Reason | |---|---|---|---|---|---|---| | 1 | `app-shell/src/__tests__/spec-symbol-parity.test.ts` | `_TypeIsClosed` | `DecisionOutputDef['type']` | **yes** | restatement → relaxed | re-export of the spec enum; nothing in ui is total over it. Now: not `string` + the five kinds present | | 2 | `app-shell/src/__tests__/spec-symbol-parity.test.ts` | `_TwoMembersWide` | `keyof TrackedObject` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 3 | `components/src/__tests__/filter-builder-name-authority-9306.test.tsx` | (unnamed) | `Exclude<FilterBuilderOperator, AuthorityCondition['operator']>` | **yes** | kept (probe artifact) | reds only because the probe desynchronises the spec's operator tuple from its enum; a real spec addition moves both together | | 4 | `components/src/__tests__/share-filter-sort-spec-parity.test.ts` | `_RowKeys` | `keyof FilterBuilderRow` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 5 | `core/src/registry/__tests__/component-config-single-declaration.test.ts` | `registryOnlyKeys` | `Exclude<keyof RegistryComponentConfig, keyof ComponentConfig>` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 6 | `core/src/registry/__tests__/component-meta-derives-from-canonical.test.ts` | `registryOnlyKeys` | `OnlyOnRegistry` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 7 | `core/src/utils/__tests__/normalize-list-view.undrawableBothLegs-9880.test.ts` | `_DrawableUnderPublished` | `Drawable<PublishedVocabulary, SimulatedTable>` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 8 | `core/src/utils/__tests__/normalize-list-view.undrawableBothLegs-9880.test.ts` | `_DrawableUnderMain` | `Drawable<MainVocabulary, SimulatedTable>` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 9 | `core/src/utils/__tests__/normalize-list-view.undrawableBothLegs-9880.test.ts` | `_VisualizationUnionIsTheNineDrawnKinds` | `ListViewVisualization` | **yes** | exhaustive → kept exact | `LIST_VIEW_KINDS` (`normalize-list-view.ts`) is a total `Record` and reds first; the pin now names it and `ListView`'s `case` list | | 10 | `data-objectstack/src/metadata-client.draftHeaderMember-12413.types.test.ts` | `_Keys` | `keyof MetadataDraftHeader` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 11 | `data-objectstack/src/queryDataset.test.ts` | `_DateGranularityEnum` | `SelectionParam['dateGranularity']` | **yes** | restatement → relaxed | no ui structure total over the buckets. Now: the five buckets present + not `string` | | 12 | `data-objectstack/src/spec-symbol-batch6.test.ts` | `_ReasonIsTheEnum` | `DroppedFieldsEvent['reason']` | **yes** | exhaustive → kept exact | `STRIPPED_LINE` (app-shell `writeWarningToast.ts`) is a `Record` over the union; the pin now names it as the place a new arm is handled | | 13 | `data-objectstack/src/spec-symbol-batch6.test.ts` | `_Shared` | `Extract<keyof MetadataCacheStats, keyof SpecCacheStats>` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 14 | `data-objectstack/src/spec-symbol-batch6.test.ts` | `_SpecOnly` | `Exclude<keyof SpecCacheStats, keyof MetadataCacheStats>` | **yes** | restatement → relaxed | closed the spec's own key set. Now: `keyCount` and `memoryUsage` are spec-only | | 15 | `data-objectstack/src/spec-symbol-batch6.test.ts` | `_LocalOnly` | `Exclude<keyof MetadataCacheStats, keyof SpecCacheStats>` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 16 | `data-objectstack/src/spec-symbol-batch6.test.ts` | `_HasDraftMode` | `MetadataClientSaveOptions['mode']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 17 | `plugin-chatbot/src/__tests__/chat-message-contract.test.ts` | `_RoleSurvives` | `BarrelChatMessage['role']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 18 | `plugin-chatbot/src/__tests__/chat-message-contract.test.ts` | `_AuthoringHasToolRole` | `AuthoredChatMessage['role']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 19 | `plugin-chatbot/src/__tests__/chat-message-contract.test.ts` | `_LegacyToolStatesAreAuthorable` | `Extract< NonNullable<NonNullable<AuthoredChatMessage['toolInvocations']>[number]['state']>…` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 20 | `plugin-chatbot/src/__tests__/chat-message-contract.test.ts` | `_KeepsToolRole` | `ObjectChatMessage['role']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 21 | `plugin-chatbot/src/__tests__/chat-message-contract.test.ts` | `_KeepsLegacyToolStates` | `Extract< NonNullable<NonNullable<ObjectChatMessage['toolInvocations']>[number]['state']>, …` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 22 | `plugin-chatbot/src/__tests__/chat-message-contract.test.ts` | `_IsTheApprovalTriple` | `ChatbotRuntimeOnly` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 23 | `plugin-chatbot/src/__tests__/spec-symbol-batch6.test.ts` | `_Vocabulary` | `PendingActionStatus` | **yes** | restatement → relaxed | no inbox structure is total over the statuses. Now: the five statuses present; `_StatusIsNotString` keeps it closed | | 24 | `plugin-chatbot/src/__tests__/spec-symbol-batch6.test.ts` | `_ApproveVocabulary` | `ApproveOutcome['status']` | **yes** | restatement → relaxed | the inbox compares against named statuses only. Now: `executed`/`failed` present (and `rejected` on the reject side) | | 25 | `plugin-detail/src/__tests__/ConcurrentUpdateDialog.narrowedCode-6421.test.tsx` | `_CodeReadsAsMaybeUndefined` | `typeof code` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 26 | `plugin-detail/src/renderers/__tests__/record-related-list.propsRefusal-9963.test.tsx` | `_EqualRefusesAnIndexKeySet` | `string \| number` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 27 | `plugin-detail/src/renderers/__tests__/record-related-list.propsRefusal-9963.test.tsx` | `_HostPropsAreTheOnesRead` | `keyof RecordRelatedListRendererProps` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 28 | `plugin-form/src/__tests__/masterDetailDetailsMembers-8071.test.tsx` | `memberList` | `keyof MasterDetailDetailConfig` | **yes** | restatement → relaxed | `derivedFromSpec` already makes the config the spec entry. Now: the eleven members present | | 29 | `plugin-form/src/__tests__/objectFormLayout-11168.test.tsx` | `_FormLayoutIsTwoValues` | `FormSchema['layout']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 30 | `plugin-form/src/__tests__/parameterTypesNameable-7324.test.ts` | `PIN` | `keyof ChildObjectSchemaLike` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 31 | `plugin-form/src/__tests__/parameterTypesNameable-7324.test.ts` | `PIN` | `keyof DefaultsFieldValue` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 32 | `plugin-grid/src/__tests__/dataTableSchemaSlot-6459.test.ts` | `_EveryHoldPinned` | `keyof ObjectGridDataTableSchemaHolds` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 33 | `plugin-grid/src/__tests__/onNavigateProp-9547.test.tsx` | `_TypeIsTheTwoMembers` | `RecordNavigateAction` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 34 | `plugin-grid/src/hooks/useRecordCrudVerdicts.explainRequest.test.ts` | `_StillExactlyTwoVerbs` | `RecordCrudOperation` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 35 | `react/src/hooks/__tests__/navigationOverlayConsumers.onRowClickArity-9357.test.ts` | `_WideAritySpansBoth` | `Parameters<WideConsumer>['length']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 36 | `react/src/hooks/__tests__/offline-nav-performance-spec-parity.test.ts` | `_StorageMembers` | `NonNullable<OfflineCacheConfig['persistStorage']>` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 37 | `react/src/hooks/__tests__/offline-nav-performance-spec-parity.test.ts` | `_OfflineKeys` | `keyof OfflineConfig` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 38 | `react/src/hooks/__tests__/offline-nav-performance-spec-parity.test.ts` | `_SyncKeys` | `keyof OfflineSyncConfig` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 39 | `react/src/hooks/__tests__/useNavigationOverlay.onRowClickArity-9357.test.tsx` | `_AritySpansBoth` | `OnRowClickParams['length']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 40 | `types/src/__tests__/action-bar-chart-flag-11355.test.ts` | `_ActionBarMembers` | `keyof ActionBarSchema` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 41 | `types/src/__tests__/ai-zero-read-members-retired-8178.test.ts` | `assertionLiveMembersKeepTheirTypes` | `AIRecommendationsSchema['layout']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 42 | `types/src/__tests__/ai-zod-arms-10859.test.ts` | `assertionMemberTypesAgree` | `MismatchedKeys<typeof AIRecommendationsSchema, Ts_AIRecommendationsSchema>` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 43 | `types/src/__tests__/alert-dialog-read-dialect-7104.test.ts` | `_ActionVariant` | `AlertDialogSchema['actionVariant']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 44 | `types/src/__tests__/authoring-nodes-bag-carriers-11468.test.ts` | `_ControlShapeOnlyLeavesTheOwnMemberBagsOpen` | `OpenBagTypes<ShapeOnlyArmInput<(typeof ObjectQLPublicBlockComponentSchema)['options'][numb…` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 45 | `types/src/__tests__/chart-series-keys-7546.test.ts` | `variant` | `ChartDataSeries['variant']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 46 | `types/src/__tests__/chart-series-keys-7546.test.ts` | `yAxis` | `ChartDataSeries['yAxis']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 47 | `types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts` | `assertionSharedKeyAliasIsTheCensus` | `ChatbotSharedKey` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 48 | `types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts` | `assertionSurfaceVocabulary` | `ChatbotEnhancedSchema['surface']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 49 | `types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.ts` | `trend` | `DashboardWidgetSlotComponentSchema['trend']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 50 | `types/src/__tests__/designer-ondelete-respelled-11434.test.ts` | `assertionRespelled` | `DataModelRelationship['deleteBehavior']` | **yes** | restatement → relaxed | the row above already equates the face with the spec member. Now: the three actions present | | 51 | `types/src/__tests__/designer-ondelete-respelled-11434.test.ts` | `assertionRespelled` | `InputOf<ShapeOf<typeof DataModelRelationshipSchema>['deleteBehavior']>` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 52 | `types/src/__tests__/drill-down-config-declared-keys.test.ts` | `_TargetUnion` | `NonNullable<DrillDownConfig['target']>` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 53 | `types/src/__tests__/export-options-spec-parity.test.ts` | `_ArrayArmIsCsvXlsxJson` | `Extract< ListViewExportFace, unknown[] >[number]` | **yes** | restatement → relaxed | nothing in ui is total over the formats. Now: csv/xlsx/json present, `pdf` absent, not `string` | | 54 | `types/src/__tests__/filter-builder-mirror-6939.test.ts` | (unnamed) | `TsFilterField['type']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 55 | `types/src/__tests__/filter-builder-mirror-6939.test.ts` | (unnamed) | `TsFilterGroup['logic']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 56 | `types/src/__tests__/form-barrel-mirror-9406.test.ts` | `type` | `ShorthandFromRoot['type']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 57 | `types/src/__tests__/form-field-channel-keys-12130.test.ts` | `assertionLocalMembers` | `FormField['picker']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 58 | `types/src/__tests__/list-view-options-bag-6152.test.ts` | `kinds` | `keyof Bag` | **yes** | restatement → relaxed | the bag keys are the spec's. Now: the eight kinds present; `closed` keeps the no-index-signature half | | 59 | `types/src/__tests__/list-view-properties-bag-12670.test.ts` | `assertionInstrumentFires` | `SpecListView['type']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 60 | `types/src/__tests__/menu-item-union.test.ts` | `_DividerKeySet` | `keyof MenuDividerItem` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 61 | `types/src/__tests__/node-recursion-point-8344.test.ts` | `NodeRecursionPointDeclarationDrift` | `LiteralArmType< ArmsNotAssignableToSchemaNode >` | **yes** | kept (not a restatement) | a measured drift ledger of arms whose zod output is not a `SchemaNode`; red names a real TS/zod gap | | 62 | `types/src/__tests__/object-chart-properties-bag-11276.test.ts` | `assertionInstrumentFires` | `Exclude<keyof Mirror, NodeLevel>` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 63 | `types/src/__tests__/object-data-table-row-click-arity-9799.test.ts` | `assertionAritySpansOneOrTwo` | `Parameters<Slot>['length']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 64 | `types/src/__tests__/object-form-properties-bag-10859-b4.test.ts` | `assertionTwinStaysPublished` | `TsObjectFormSchema['mode']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 65 | `types/src/__tests__/objectql.exportOptions.test.ts` | `_KeysAreExactlyTheSpecFive` | `keyof ListViewExportOptions` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 66 | `types/src/__tests__/objectql.exportOptions.test.ts` | `_FormatsAreCsvXlsxJson` | `ListViewExportFormat` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 67 | `types/src/__tests__/page-kind-jsx-retired-12464.test.ts` | `_KindIsTheSpecVocabulary` | `NonNullable<PageNodeSchema['kind']>` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 68 | `types/src/__tests__/query-params-closed-keys-7497.test.ts` | `_Closed` | `keyof QueryParams` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 69 | `types/src/__tests__/record-highlights-fields-icon-9280.test.ts` | `_EntryKeysMatchContract` | `keyof DeclaredEntry` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 70 | `types/src/__tests__/report-chart-query-spec-parity.test.ts` | `_blockTypeIsUnchanged` | `BlockMember<PublishedJoinedReportBlock, 'type'>` | **yes** | restatement → relaxed | `resolveReportPresentation` falls back, nothing is total. Now: the three block types present, optional, not `string` | | 71 | `types/src/__tests__/report-schema-authoring-face.test.ts` | `_SeriesTypeStaysThreeFamilies` | `SeriesType` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 72 | `types/src/__tests__/select-option-form-view-boundary-12736.test.ts` | `assertionObjectFieldOnlyKeysAreDefaultAndKind` | `ObjectFieldOnlyKey` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 73 | `types/src/__tests__/select-option-form-view-boundary-12736.test.ts` | `assertionBaseKeepsKindAndDefault` | `Extract< keyof SelectOptionBase, 'kind' \| 'default' >` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 74 | `types/src/__tests__/select-option-form-view-boundary-12736.test.ts` | `assertionMetadataFaceKeepsKindAndDefault` | `Extract< keyof SelectOptionMetadata, 'kind' \| 'default' >` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 75 | `types/src/__tests__/sidebar-declared-surface-11465.test.ts` | `assertionVariantIsTheRenderersEnum` | `Ts_SidebarSchema['variant']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 76 | `types/src/__tests__/sidebar-declared-surface-11465.test.ts` | `assertionVariantIsTheRenderersEnum` | `InputOf<'variant'>` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 77 | `types/src/__tests__/spec-symbol-parity.test.ts` | `_elementIsAuthoringOnly` | `MirrorFilters['element']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 78 | `types/src/__tests__/strict-face-read-keys-11070.test.ts` | `assertionGridFieldKeysOnTheFormFieldFace` | `Extract<GridFieldOwnKeys, 'columns' \| 'sortField' \| 'sort_field' \| 'name'>` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 79 | `types/src/__tests__/strict-face-read-keys-11070.test.ts` | `assertionSidebarSideIsTheTwoEdges` | `Ts_SidebarSchema['side']` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 80 | `types/src/__tests__/strict-face-read-keys-11070.test.ts` | `assertionSidebarSideIsTheTwoEdges` | `SidebarSideInput` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 81 | `types/src/__tests__/undeclared-but-consumed-keys-6150.test.ts` | `_OrientationIsAxis` | `NonNullable< TsCarouselSchema['orientation'] >` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 82 | `types/src/__tests__/view-navigation-config-spec-parity.test.ts` | `_KeysAreExactlyTheSpecSix` | `keyof ViewNavigationConfig` | **yes** | restatement → relaxed | `_IsExactlyTheSpecInput` already refuses a local key. Now `_KeysIncludeTheSpecSix` + `_ViewIsStillDeclared` (the tombstone by name) | | 83 | `types/src/__tests__/view-navigation-config-spec-parity.test.ts` | `_ModeMembershipIsTheSevenSpecModes` | `NonNullable< ViewNavigationConfig['mode'] >` | **yes** | restatement → relaxed | no ui consumer is total over the modes. Now: the seven present + `_ModeIsClosed` | | 84 | `types/src/__tests__/zod-mirror-parity.test.ts` | `assertionOpenRecordClauseIsAdditive` | `WiderThanDeclaredKeys< SyntheticBothClassesMirror, SyntheticBothClassesDeclaration >` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | | 85 | `types/src/__tests__/zod-mirror-parity.test.ts` | `assertionAllowListReportsUnfiledAndCallbackRows` | `{ [K in keyof SyntheticAllowList]: \| `${K}::${Exclude< SyntheticAllowKeys< K >, K extends …` | no | kept (probe green) | green under all three probes, so a spec addition does not move it (ui-owned union, a ui narrowing, or both sides derived from the spec) | ### Same-family pins outside the literal-union scan The probes also turned these red. They are exact pins on spec-derived key sets or entry shapes, not on literal unions, so they are the same class, and I relaxed them the same way: | File | Pin | Change | |---|---|---| | app-shell `spec-symbol-parity.test.ts` | `_BothRequired` (`FlowNodePosition`) | `Pick<…, 'x' \| 'y'>` equals `{ x: number; y: number }`: both coordinates the canvas reads stay required, and a key the spec adds to `position` is not red | | app-shell `spec-symbol-parity.test.ts` | `_NoInventedKeys` (`ObjectFieldGroup`) | deleted: it closed the spec's own key list; `_HasSpecKeys` keeps the keys ui reads | | app-shell `spec-symbol-parity.test.ts` | `_TypeIsClosed` (`DecisionOutputDef['type']`) | split into not-`string` plus `_TypeKeepsTheFiveKinds` (counted in the table) | | data-objectstack `spec-symbol-batch6.test.ts` | `_SpecShape` (`SpecValidationError`) | `field`, `message` and `code` present | | plugin-chatbot `spec-symbol-batch6.test.ts` | `_RejectVocabulary` | `rejected` present (a single literal, relaxed with its two siblings) | | types `select-option-tier1-convergence-7014.test.ts` | the form-face and metadata-face "kept every member / gained only …" pins | every pre-convergence member kept, exactly (`Pick` equals the old shape); gained keys must be spec keys (`assertion…GainedOnlySpecKeys`), and the gains ui relies on (`description`; `default` and `kind`) are pinned by name | | types `widget-schema-anchors-7946.test.ts` | the aggregate `groupBy` pin | split into: admits a field name; the structured node keeps `field` and optional `alias`; the five date granularities present | | types `field-metadata-depends-on-declared-6153.test.ts` | `_SpecShapeIsArrayOfNameOrEntry` | split into: an array; admits a bare name; the entry keeps `field` and optional `param` | ### Reds that stay, and why The `keys` probe turns many other ui type checks red. They fall into three groups, and none are restatements: - **Derived parity**, where both sides are computed from the spec: `_FaceIsTheSpecInput`, `_InputEqualsParsed`, the zod-mirror ledgers, `spec-derived-unions`, the record-details, record-highlights, gantt, conditional-formatting and object-chart pins, `filter-operator-protocol-set`, `buildVerify-11988`, `share-filter-sort-spec-parity` and `designer-zod-arms`. These go red only because the probe mutates one side of a pair that a real spec build keeps in step. This follows #26238's ruling on derived pins. - **Value-level totals** in tests, where a test-local `Record<SpecUnion, …>` is a value, not a type pin: `AppContent.recordDialogInterimBound-12521`, `identityImportWriteModes-12697` and `AuditPanel.lockState`. They are listed as an acceptance note below. - **ui types narrower than the spec**: `MetadataService` and friends, mostly TS2345 on payload objects. These are real ui gaps the probe surfaced, not test pins. They are out of this card's scope. ## Proof All runs happened on this head after merging main (#26272 included) and rebuilding the ui closure (75/75 turbo tasks), under `scripts/pm/os-verify-lock.sh`. Spec dist edits were backed up and restored with a hash check. The tree and dist were verified clean after each run. 1. **Baseline.** All 10 packages, both configs: 0 errors. 2. **A single member on a restated union stays green.** I added `'__one26275'` to `PendingActionStatus` only (`contracts/index.d.ts` and `.d.mts`). Result: 0 errors in all 19 type-check runs (data-objectstack has no test config). On main's version of the file, the full probe makes `_Vocabulary` red. 3. **A single member on an exhaustive union turns red at the consumer.** I added `__one26275` to `DroppedFieldsEventSchema.reason` only. Result: TS2741 at `STRIPPED_LINE` in app-shell `writeWarningToast.ts`, under both app-shell configs, and TS2344 at `_ReasonIsTheEnum`.These 3 are the only errors in all 19 runs. 4. **Full probe, head vs main.** With all three probes applied, the only reds left in the 14 changed files are: - the two exhaustive pins, and their consumers `STRIPPED_LINE` and `LIST_VIEW_KINDS`; - `_FaceIsTheSpecInput` and `_InputEqualsParsed` (derived parity). Putting main's versions of the 14 files back under the same probe adds 22 reds, all on the pins this PR relaxes. Every red outside those files is the same in both runs. 5. **The relaxed pins are not vacuous.** I injected a member that does not exist (`'__absent26275'`, or an extra required key) into each of the 21 relaxed assertions. Result: exactly 21 errors, one per edit. The probe mutates spec dist only. ui packages' own dist `.d.ts` files reference the spec rather than inlining it, so the mutation reaches them. Only the 10 packages that host a candidate were type-checked under the probes. ## Acceptance notes - **Value-level totals.** Three tests hold a test-local `Record<SpecUnion, …>`: `AppContent.recordDialogInterimBound-12521`, `identityImportWriteModes-12697` and `AuditPanel.lockState`. They are the runtime-shaped half of this class and will go red on a spec addition by design. Each is a single-line fix if a later card wants them on the `Exclude` form; I left them because they are values that tests iterate, not type pins. - **Commit titles.** The branch commits are titled WIP because they predate the merge of #26272. Squash-merge yields one commit. ## Gates - `node scripts/pm/dispatch-gates.mjs --commands` derived 24 families. I ran all 24, and every one exits 0. `--ran` reconciles them as 24 derived, 24 run, 0 NOT-MEASURED. `check:type-check-debt` ran under the verify lock: 26 raw errors, none above its recorded number, no surplus. - `vitest run` on the 14 changed files: 14 files and 350 tests pass. - Type Check UI shape (`tsc --noEmit` and `tsc -p tsconfig.test.json`) on all 10 host packages: 0 errors. Console build (`turbo run build --filter=@object-ui/console`): 71 of 71 tasks pass on the final merged head. --- _Generated by [Claude Code](https://claude.ai/code/session_01Ar9SeLTMJHQukiSgeSM45R)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 495fa07 commit da99a4b

14 files changed

Lines changed: 190 additions & 73 deletions

‎ui/packages/app-shell/src/__tests__/spec-symbol-parity.test.ts‎

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -413,9 +413,12 @@ describe('DecisionOutputDef is the spec type, with no local divergence left', ()
413413
type _IsExactlyTheSpecType = Assert<Equal<DecisionOutputDef, SpecDecisionOutputDef>>;
414414

415415
// Deriving NARROWED `type` from the bare `string` this file used to declare
416-
// to the spec's closed enum — that narrowing is the point, so pin it.
417-
type _TypeIsClosed = Assert<
418-
Equal<DecisionOutputDef['type'], 'user' | 'department' | 'position' | 'team' | 'text' | undefined>
416+
// to the spec's closed enum — that narrowing is the point, so pin it: not
417+
// `string`, and the five kinds still in it. Nothing in ui switches over
418+
// every kind, so a kind the spec adds is the spec's to add, not a red here.
419+
type _TypeIsClosed = Assert<Equal<string extends DecisionOutputDef['type'] ? true : false, false>>;
420+
type _TypeKeepsTheFiveKinds = Assert<
421+
Equal<Exclude<'user' | 'department' | 'position' | 'team' | 'text', DecisionOutputDef['type']>, never>
419422
>;
420423

421424
expect(true).toBe(true);
@@ -447,8 +450,10 @@ describe('flow node geometry IS the spec `FlowNode.position` (#3172)', () => {
447450
type _InputEqualsParsed = Assert<
448451
Equal<NonNullable<SpecFlowNode['position']>, NonNullable<SpecFlowNodeParsed['position']>>
449452
>;
450-
// Both coordinates required: a half-position is not representable.
451-
type _BothRequired = Assert<Equal<FlowNodePosition, { x: number; y: number }>>;
453+
// Both coordinates required: a half-position is not representable. Pinned
454+
// on the two coordinates the canvas reads, so a key the spec adds to
455+
// `position` is not a red here.
456+
type _BothRequired = Assert<Equal<Pick<FlowNodePosition, 'x' | 'y'>, { x: number; y: number }>>;
452457
type _HalfIsNotAPosition = Assert<Equal<Extends<{ x: number }, FlowNodePosition>, false>>;
453458

454459
// …and the designer's node carries exactly that key, optional exactly as the
@@ -488,22 +493,16 @@ describe('ObjectFieldGroup derives from the spec schema INPUT side', () => {
488493
// unrepresentable. If this flips, someone swapped z.input for z.infer.
489494
type _CollapseOptional = Assert<Extends<{ key: string; label: string }, ObjectFieldGroup>>;
490495

491-
// Still the real spec vocabulary, not a hand copy that merely agrees.
496+
// Still the real spec vocabulary, not a hand copy that merely agrees. The
497+
// type is `z.input` of the spec schema itself, so the keys it carries are
498+
// the spec's by construction: this names the ones the designer reads, and a
499+
// key the spec adds is not a red here.
492500
type _HasSpecKeys = Assert<
493501
Extends<
494502
'key' | 'label' | 'icon' | 'description' | 'visibleWhen' | 'collapse' | 'collapsible' | 'collapsed' | 'defaultExpanded',
495503
keyof ObjectFieldGroup
496504
>
497505
>;
498-
type _NoInventedKeys = Assert<
499-
Equal<
500-
Exclude<
501-
keyof ObjectFieldGroup,
502-
'key' | 'label' | 'icon' | 'description' | 'visibleWhen' | 'collapse' | 'collapsible' | 'collapsed' | 'defaultExpanded'
503-
>,
504-
never
505-
>
506-
>;
507506

508507
expect(true).toBe(true);
509508
});

‎ui/packages/core/src/utils/__tests__/normalize-list-view.undrawableBothLegs-9880.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,12 @@ export const drawableUnderGrownOnceExplained: Record<
148148
* for — under the resolved spec AND under `main`'s. This is what makes the
149149
* change a re-spelling rather than a re-definition: the published type does not
150150
* move, only the way it is derived.
151+
*
152+
* Exact on purpose: ui draws this union exhaustively. A view kind the spec adds
153+
* lands in `ListViewVisualization` unless `UndrawableViewKind` names it, and
154+
* then `LIST_VIEW_KINDS` in `normalize-list-view.ts` (a total `Record` over the
155+
* union) fails the build there first. That record and `ListView`'s `case` list
156+
* are where the new kind is drawn; this list follows them.
151157
*/
152158
export type _VisualizationUnionIsTheNineDrawnKinds = Expect<
153159
Equal<

‎ui/packages/data-objectstack/src/queryDataset.test.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,12 +449,18 @@ describe('queryDataset(selection) is @objectstack/spec DatasetSelection itself',
449449
// month at all. A restatement does not only drift on the fields it has — it
450450
// also stops at whatever the contract looked like the day it was written.
451451
type _HasDateGranularity = Assert<HasKey<SelectionParam, 'dateGranularity'>>;
452+
// The same lesson holds for this pin, so it names the five buckets rather
453+
// than restating the whole enum: a bucket the spec adds is not a red here,
454+
// and a widening to `string` still is.
452455
type _DateGranularityEnum = Assert<
453456
Equal<
454-
SelectionParam['dateGranularity'],
455-
'day' | 'week' | 'month' | 'quarter' | 'year' | undefined
457+
Exclude<'day' | 'week' | 'month' | 'quarter' | 'year', SelectionParam['dateGranularity']>,
458+
never
456459
>
457460
>;
461+
type _DateGranularityIsClosed = Assert<
462+
Equal<string extends SelectionParam['dateGranularity'] ? true : false, false>
463+
>;
458464

459465
expect(true).toBe(true);
460466
});

‎ui/packages/data-objectstack/src/spec-symbol-batch6.test.ts‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,12 @@ describe('DroppedFieldsEvent IS the spec type, not a mirror of it', () => {
260260
// and `main` agree again and this is the exact five-member union, with no
261261
// `Exclude`: any new arm is a remainder `Equal` refuses, and
262262
// `_ReasonIsNotString` below still refuses a widening to `string`.
263+
//
264+
// This pin stays exact on purpose: ui renders the union exhaustively.
265+
// `STRIPPED_LINE` in `@object-ui/app-shell`'s `writeWarningToast.ts` is a
266+
// `Record` over `DroppedFieldsEvent['reason']`, so a new arm also fails the
267+
// app-shell build there. That map is where the new arm needs its wording
268+
// (and both locale tables their row); this list follows it.
263269
type _ReasonIsTheEnum = Assert<
264270
Equal<
265271
DroppedFieldsEvent['reason'],
@@ -281,7 +287,11 @@ describe('MetadataCacheStats is not the spec ICacheService stats', () => {
281287
// spec's is a server KV store sized in keys and bytes; this one is a bounded
282288
// browser LRU that evicts, coalesces in-flight fetches and reports a rate.
283289
type _Shared = Assert<Equal<Extract<keyof MetadataCacheStats, keyof SpecCacheStats>, 'hits' | 'misses'>>;
284-
type _SpecOnly = Assert<Equal<Exclude<keyof SpecCacheStats, keyof MetadataCacheStats>, 'keyCount' | 'memoryUsage'>>;
290+
// The spec side is named by the two keys that make it a KV store, not
291+
// closed: a key the spec adds to its own stats is not a red here.
292+
type _SpecOnly = Assert<
293+
Equal<Exclude<'keyCount' | 'memoryUsage', Exclude<keyof SpecCacheStats, keyof MetadataCacheStats>>, never>
294+
>;
285295
type _LocalOnly = Assert<
286296
Equal<
287297
Exclude<keyof MetadataCacheStats, keyof SpecCacheStats>,
@@ -319,7 +329,9 @@ describe('DataApiValidationError is an Error, the spec ValidationError is a reco
319329

320330
// The spec's is one entry in a validation report. `@object-ui/types`
321331
// re-exports it under that name, so the two were importable side by side.
322-
type _SpecShape = Assert<Equal<Exclude<keyof SpecValidationError, 'field' | 'message' | 'code'>, never>>;
332+
// Named by the three members a report entry carries; a key the spec adds
333+
// to it is not a red here.
334+
type _SpecShape = Assert<Equal<Exclude<'field' | 'message' | 'code', keyof SpecValidationError>, never>>;
323335

324336
// This one is a thrown runtime object: it has a `stack`, which no data
325337
// record does, and it carries the whole list the spec type is one item of.

‎ui/packages/plugin-chatbot/src/__tests__/spec-symbol-batch6.test.ts‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,12 @@ describe('the pending-action row and status ARE the spec contract', () => {
246246
type _NoCreatedAt = Assert<Equal<'created_at' extends keyof PendingActionRow ? true : false, false>>;
247247
type _NoUpdatedAt = Assert<Equal<'updated_at' extends keyof PendingActionRow ? true : false, false>>;
248248

249-
// The vocabulary the inbox's tabs and badges are built on.
249+
// The vocabulary the inbox's tabs and badges are built on: each status they
250+
// name is still the spec's. No inbox structure is total over the union, so
251+
// a status the spec adds is not a red here; `_StatusIsNotString` above
252+
// still refuses a widening.
250253
type _Vocabulary = Assert<
251-
Equal<PendingActionStatus, 'pending' | 'approved' | 'executed' | 'failed' | 'rejected'>
254+
Equal<Exclude<'pending' | 'approved' | 'executed' | 'failed' | 'rejected', PendingActionStatus>, never>
252255
>;
253256

254257
expect(true).toBe(true);
@@ -291,8 +294,10 @@ describe('the decision outcomes ARE the spec wire responses', () => {
291294
type _RejectStatusNotString = Assert<
292295
Equal<string extends RejectOutcome['status'] ? true : false, false>
293296
>;
294-
type _ApproveVocabulary = Assert<Equal<ApproveOutcome['status'], 'executed' | 'failed'>>;
295-
type _RejectVocabulary = Assert<Equal<RejectOutcome['status'], 'rejected'>>;
297+
// The statuses the inbox compares against are still on each response; a
298+
// status the spec adds to either is not a red here.
299+
type _ApproveVocabulary = Assert<Equal<Exclude<'executed' | 'failed', ApproveOutcome['status']>, never>>;
300+
type _RejectVocabulary = Assert<Equal<Exclude<'rejected', RejectOutcome['status']>, never>>;
296301

297302
// Drift 3 — `[k: string]: unknown` on the approve copy. objectstack#4075:
298303
// with it, this whole describe block would have been green on the copies.

‎ui/packages/plugin-form/src/__tests__/masterDetailDetailsMembers-8071.test.tsx‎

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -520,23 +520,28 @@ const derivedFromSpec: Equal<MasterDetailDetailConfig, Omit<SpecDetailEntry, 'so
520520
const columnsAreTheGridColumn: Equal<NonNullable<MasterDetailDetailConfig['columns']>[number], GridColumn> = true;
521521

522522
/**
523-
* Every member a detail can be authored with, spelled out: the spec entry's
524-
* keys minus `sortField` (objectui#11070 round 9). A key the spec adds is a
525-
* red row here, never a silent widening of what this block is said to read.
523+
* Every member a detail is authored with today, spelled out: the spec entry's
524+
* keys minus `sortField` (objectui#11070 round 9). Each must stay on the
525+
* config. The list is not closed: `derivedFromSpec` above already makes the
526+
* config the spec's entry, so a closed list would only restate the spec, and a
527+
* key the spec adds would be a red row here with no ui change behind it.
526528
*/
527529
const memberList: Equal<
528-
keyof MasterDetailDetailConfig,
529-
| 'childObject'
530-
| 'relationshipField'
531-
| 'columns'
532-
| 'formFields'
533-
| 'inlineMode'
534-
| 'amountField'
535-
| 'totalField'
536-
| 'title'
537-
| 'minRows'
538-
| 'maxRows'
539-
| 'addLabel'
530+
Exclude<
531+
| 'childObject'
532+
| 'relationshipField'
533+
| 'columns'
534+
| 'formFields'
535+
| 'inlineMode'
536+
| 'amountField'
537+
| 'totalField'
538+
| 'title'
539+
| 'minRows'
540+
| 'maxRows'
541+
| 'addLabel',
542+
keyof MasterDetailDetailConfig
543+
>,
544+
never
540545
> = true;
541546

542547
/** The lit control for the directive below: the same literal without the retired member compiles. */

‎ui/packages/types/src/__tests__/designer-ondelete-respelled-11434.test.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,15 @@ type InputOf<T> = T extends z.ZodType ? z.input<T> : never;
5555
* `deleteBehavior` carries the spec's vocabulary on both faces, and `onDelete`
5656
* reads as `undefined` on both — a `?: never` tombstone, and an alias refusal
5757
* whose `z.input` is `undefined`.
58+
*
59+
* The TypeScript face IS the spec's member (first row), so the second row names
60+
* the three actions rather than restating the spec's enum: an action the spec
61+
* adds is not a red here. The zod face's enum is this package's own list, so
62+
* its row stays exact, and `zod-mirror-parity.test.ts` holds it to the spec's.
5863
*/
5964
export type assertionRespelled = [
6065
Expect<Equal<DataModelRelationship['deleteBehavior'], SpecField['deleteBehavior']>>,
61-
Expect<Equal<DataModelRelationship['deleteBehavior'], 'set_null' | 'cascade' | 'restrict' | undefined>>,
66+
Expect<Equal<Exclude<'set_null' | 'cascade' | 'restrict', DataModelRelationship['deleteBehavior']>, never>>,
6267
Expect<Equal<InputOf<ShapeOf<typeof DataModelRelationshipSchema>['deleteBehavior']>, 'set_null' | 'cascade' | 'restrict' | undefined>>,
6368
Expect<Equal<DataModelRelationship['onDelete'], undefined>>,
6469
Expect<Equal<InputOf<ShapeOf<typeof DataModelRelationshipSchema>['onDelete']>, undefined>>,

‎ui/packages/types/src/__tests__/export-options-spec-parity.test.ts‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,15 @@ type _FaceCarriesStreaming = Expect<
222222
Equal< Exclude< ListViewExportFace, unknown[] >['streaming'], boolean | undefined >
223223
>;
224224

225-
/** The array arm's vocabulary is the spec's three formats — `'pdf'` is not among them. */
226-
type _ArrayArmIsCsvXlsxJson = Expect<
227-
Equal< Extract< ListViewExportFace, unknown[] >[number], 'csv' | 'xlsx' | 'json' >
228-
>;
225+
/**
226+
* The array arm's vocabulary carries the spec's three formats — `'pdf'` is not
227+
* among them, and it is not `string`. Membership, not a closed list: nothing in
228+
* ui is total over the formats, so a format the spec adds is not a red here.
229+
*/
230+
type ArrayArmFormat = Extract< ListViewExportFace, unknown[] >[number];
231+
type _ArrayArmIsCsvXlsxJson = Expect< Equal< Exclude< 'csv' | 'xlsx' | 'json', ArrayArmFormat >, never > >;
232+
type _ArrayArmHasNoPdf = Expect< Equal< 'pdf' extends ArrayArmFormat ? true : false, false > >;
233+
type _ArrayArmIsClosed = Expect< Equal< string extends ArrayArmFormat ? true : false, false > >;
229234

230235
/** A minimal, valid `list-view` node — the envelope every parse below rides on. */
231236
const NODE = { type: 'list-view', objectName: 'accounts' } as const;

‎ui/packages/types/src/__tests__/field-metadata-depends-on-declared-6153.test.ts‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,18 @@ type Expect< T extends true > = T;
6161
/* ── 1. The declared member, in the spec's shape ─────────────────────────── */
6262

6363
export type _DependsOnIsDeclaredInSpecShape = Expect< Equal< NonNullable< BaseFieldMetadata['dependsOn'] >, FieldDependsOn > >;
64-
// The derived shape is exactly what the installed spec declares at field level
65-
// (`z.array(z.union([z.string(), z.strictObject({ field, param? })]))`) — if a
66-
// spec release widens or narrows it, this line moves with it and says so.
67-
export type _SpecShapeIsArrayOfNameOrEntry = Expect< Equal< FieldDependsOn, Array< string | { field: string; param?: string | undefined } > > >;
64+
// The derived shape is what the installed spec declares at field level
65+
// (`z.array(z.union([z.string(), z.strictObject({ field, param? })]))`): an
66+
// array of a bare name or an entry with `field` and an optional `param`. Pinned
67+
// on those members, so a narrowing is red here and a key the spec adds to the
68+
// entry is not.
69+
type DependsOnItem = FieldDependsOn extends Array< infer I > ? I : never;
70+
export type _SpecShapeIsArray = Expect< Equal< FieldDependsOn, Array< DependsOnItem > > >;
71+
export type _SpecShapeAdmitsAName = Expect< Equal< Extract< DependsOnItem, string >, string > >;
72+
export type _SpecShapeIsArrayOfNameOrEntry = Expect< Equal<
73+
Pick< Exclude< DependsOnItem, string >, 'field' | 'param' >,
74+
{ field: string; param?: string | undefined }
75+
> >;
6876
// Inherited, not restated: the select and lookup faces carry the SAME member.
6977
export type _SelectInherits = Expect< Equal< SelectFieldMetadata['dependsOn'], BaseFieldMetadata['dependsOn'] > >;
7078
export type _LookupInherits = Expect< Equal< LookupFieldMetadata['dependsOn'], BaseFieldMetadata['dependsOn'] > >;

‎ui/packages/types/src/__tests__/list-view-options-bag-6152.test.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,8 +305,14 @@ type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B
305305

306306
describe('objectui#6152 round 12 — the TypeScript face is closed the same way', () => {
307307
it('the bag type has the eight kinds and no index signature', () => {
308+
// `closed` is the no-index-signature half. The eight kinds are members, not
309+
// a closed list: the bag's keys are the spec's, so a kind the spec adds is
310+
// not a red here.
308311
const closed: string extends keyof Bag ? 'open' : 'closed' = 'closed';
309-
const kinds: Equal<keyof Bag, 'kanban' | 'calendar' | 'gallery' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree'> = true;
312+
const kinds: Equal<
313+
Exclude<'kanban' | 'calendar' | 'gallery' | 'timeline' | 'gantt' | 'map' | 'chart' | 'tree', keyof Bag>,
314+
never
315+
> = true;
310316
expect([closed, kinds]).toEqual(['closed', true]);
311317
});
312318

0 commit comments

Comments
 (0)