Repository navigation
test(ui): relax type-level restatements of spec literal unions; keep the two exhaustive pins on their consumers - #26328
Merged
Conversation
…(WIP) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ar9SeLTMJHQukiSgeSM45R
…ter the #26238 sweep (WIP) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ar9SeLTMJHQukiSgeSM45R
Contributor
Author
席位核对 · ACCEPT · seat
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #26275
Clause-②: no
Metadata-protocol: none
What
Type Check UI went red on every ui PR whenever
@objectstack/specadded a member to a union that a ui test pinned exactly withEqual<…>, 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 carriesskip-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 main96635f57with a TypeScript AST scan. It counts everyEqual-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 (PendingActionStatusin plugin-chatbotspec-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.tsand.d.mts, which ui test configs resolve throughpaths: {}) in three ways:z.ZodEnum, literalZodUnionand quoted literal union;z.ZodObjectand interface;I then ran
tsc --noEmitandtsc -p tsconfig.test.jsonon 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
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 withEqual<string extends U ? true : false, false>(or the file's existing "is not string" pin), so the union cannot quietly widen tostring. No assertion about a member ui reads was weakened.The two exhaustive pins stay exact:
_ReasonIsTheEnum(DroppedFieldsEvent['reason'], data-objectstackspec-symbol-batch6.test.ts). Its consumer isSTRIPPED_LINEin app-shellwriteWarningToast.ts, aRecord<DroppedFieldsEvent['reason'], StrippedLine>._VisualizationUnionIsTheNineDrawnKinds(corenormalize-list-view.undrawableBothLegs-9880.test.ts). Its consumer isLIST_VIEW_KINDSinnormalize-list-view.ts, a totalRecord, plusListView'scaselist.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/.app-shell/src/__tests__/spec-symbol-parity.test.ts_TypeIsClosedDecisionOutputDef['type']string+ the five kinds presentapp-shell/src/__tests__/spec-symbol-parity.test.ts_TwoMembersWidekeyof TrackedObjectcomponents/src/__tests__/filter-builder-name-authority-9306.test.tsxExclude<FilterBuilderOperator, AuthorityCondition['operator']>components/src/__tests__/share-filter-sort-spec-parity.test.ts_RowKeyskeyof FilterBuilderRowcore/src/registry/__tests__/component-config-single-declaration.test.tsregistryOnlyKeysExclude<keyof RegistryComponentConfig, keyof ComponentConfig>core/src/registry/__tests__/component-meta-derives-from-canonical.test.tsregistryOnlyKeysOnlyOnRegistrycore/src/utils/__tests__/normalize-list-view.undrawableBothLegs-9880.test.ts_DrawableUnderPublishedDrawable<PublishedVocabulary, SimulatedTable>core/src/utils/__tests__/normalize-list-view.undrawableBothLegs-9880.test.ts_DrawableUnderMainDrawable<MainVocabulary, SimulatedTable>core/src/utils/__tests__/normalize-list-view.undrawableBothLegs-9880.test.ts_VisualizationUnionIsTheNineDrawnKindsListViewVisualizationLIST_VIEW_KINDS(normalize-list-view.ts) is a totalRecordand reds first; the pin now names it andListView'scaselistdata-objectstack/src/metadata-client.draftHeaderMember-12413.types.test.ts_Keyskeyof MetadataDraftHeaderdata-objectstack/src/queryDataset.test.ts_DateGranularityEnumSelectionParam['dateGranularity']stringdata-objectstack/src/spec-symbol-batch6.test.ts_ReasonIsTheEnumDroppedFieldsEvent['reason']STRIPPED_LINE(app-shellwriteWarningToast.ts) is aRecordover the union; the pin now names it as the place a new arm is handleddata-objectstack/src/spec-symbol-batch6.test.ts_SharedExtract<keyof MetadataCacheStats, keyof SpecCacheStats>data-objectstack/src/spec-symbol-batch6.test.ts_SpecOnlyExclude<keyof SpecCacheStats, keyof MetadataCacheStats>keyCountandmemoryUsageare spec-onlydata-objectstack/src/spec-symbol-batch6.test.ts_LocalOnlyExclude<keyof MetadataCacheStats, keyof SpecCacheStats>data-objectstack/src/spec-symbol-batch6.test.ts_HasDraftModeMetadataClientSaveOptions['mode']plugin-chatbot/src/__tests__/chat-message-contract.test.ts_RoleSurvivesBarrelChatMessage['role']plugin-chatbot/src/__tests__/chat-message-contract.test.ts_AuthoringHasToolRoleAuthoredChatMessage['role']plugin-chatbot/src/__tests__/chat-message-contract.test.ts_LegacyToolStatesAreAuthorableExtract< NonNullable<NonNullable<AuthoredChatMessage['toolInvocations']>[number]['state']>…plugin-chatbot/src/__tests__/chat-message-contract.test.ts_KeepsToolRoleObjectChatMessage['role']plugin-chatbot/src/__tests__/chat-message-contract.test.ts_KeepsLegacyToolStatesExtract< NonNullable<NonNullable<ObjectChatMessage['toolInvocations']>[number]['state']>, …plugin-chatbot/src/__tests__/chat-message-contract.test.ts_IsTheApprovalTripleChatbotRuntimeOnlyplugin-chatbot/src/__tests__/spec-symbol-batch6.test.ts_VocabularyPendingActionStatus_StatusIsNotStringkeeps it closedplugin-chatbot/src/__tests__/spec-symbol-batch6.test.ts_ApproveVocabularyApproveOutcome['status']executed/failedpresent (andrejectedon the reject side)plugin-detail/src/__tests__/ConcurrentUpdateDialog.narrowedCode-6421.test.tsx_CodeReadsAsMaybeUndefinedtypeof codeplugin-detail/src/renderers/__tests__/record-related-list.propsRefusal-9963.test.tsx_EqualRefusesAnIndexKeySetstring | numberplugin-detail/src/renderers/__tests__/record-related-list.propsRefusal-9963.test.tsx_HostPropsAreTheOnesReadkeyof RecordRelatedListRendererPropsplugin-form/src/__tests__/masterDetailDetailsMembers-8071.test.tsxmemberListkeyof MasterDetailDetailConfigderivedFromSpecalready makes the config the spec entry. Now: the eleven members presentplugin-form/src/__tests__/objectFormLayout-11168.test.tsx_FormLayoutIsTwoValuesFormSchema['layout']plugin-form/src/__tests__/parameterTypesNameable-7324.test.tsPINkeyof ChildObjectSchemaLikeplugin-form/src/__tests__/parameterTypesNameable-7324.test.tsPINkeyof DefaultsFieldValueplugin-grid/src/__tests__/dataTableSchemaSlot-6459.test.ts_EveryHoldPinnedkeyof ObjectGridDataTableSchemaHoldsplugin-grid/src/__tests__/onNavigateProp-9547.test.tsx_TypeIsTheTwoMembersRecordNavigateActionplugin-grid/src/hooks/useRecordCrudVerdicts.explainRequest.test.ts_StillExactlyTwoVerbsRecordCrudOperationreact/src/hooks/__tests__/navigationOverlayConsumers.onRowClickArity-9357.test.ts_WideAritySpansBothParameters<WideConsumer>['length']react/src/hooks/__tests__/offline-nav-performance-spec-parity.test.ts_StorageMembersNonNullable<OfflineCacheConfig['persistStorage']>react/src/hooks/__tests__/offline-nav-performance-spec-parity.test.ts_OfflineKeyskeyof OfflineConfigreact/src/hooks/__tests__/offline-nav-performance-spec-parity.test.ts_SyncKeyskeyof OfflineSyncConfigreact/src/hooks/__tests__/useNavigationOverlay.onRowClickArity-9357.test.tsx_AritySpansBothOnRowClickParams['length']types/src/__tests__/action-bar-chart-flag-11355.test.ts_ActionBarMemberskeyof ActionBarSchematypes/src/__tests__/ai-zero-read-members-retired-8178.test.tsassertionLiveMembersKeepTheirTypesAIRecommendationsSchema['layout']types/src/__tests__/ai-zod-arms-10859.test.tsassertionMemberTypesAgreeMismatchedKeys<typeof AIRecommendationsSchema, Ts_AIRecommendationsSchema>types/src/__tests__/alert-dialog-read-dialect-7104.test.ts_ActionVariantAlertDialogSchema['actionVariant']types/src/__tests__/authoring-nodes-bag-carriers-11468.test.ts_ControlShapeOnlyLeavesTheOwnMemberBagsOpenOpenBagTypes<ShapeOnlyArmInput<(typeof ObjectQLPublicBlockComponentSchema)['options'][numb…types/src/__tests__/chart-series-keys-7546.test.tsvariantChartDataSeries['variant']types/src/__tests__/chart-series-keys-7546.test.tsyAxisChartDataSeries['yAxis']types/src/__tests__/chatbot-registration-authoring-faces-7655.test.tsassertionSharedKeyAliasIsTheCensusChatbotSharedKeytypes/src/__tests__/chatbot-registration-authoring-faces-7655.test.tsassertionSurfaceVocabularyChatbotEnhancedSchema['surface']types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.tstrendDashboardWidgetSlotComponentSchema['trend']types/src/__tests__/designer-ondelete-respelled-11434.test.tsassertionRespelledDataModelRelationship['deleteBehavior']types/src/__tests__/designer-ondelete-respelled-11434.test.tsassertionRespelledInputOf<ShapeOf<typeof DataModelRelationshipSchema>['deleteBehavior']>types/src/__tests__/drill-down-config-declared-keys.test.ts_TargetUnionNonNullable<DrillDownConfig['target']>types/src/__tests__/export-options-spec-parity.test.ts_ArrayArmIsCsvXlsxJsonExtract< ListViewExportFace, unknown[] >[number]pdfabsent, notstringtypes/src/__tests__/filter-builder-mirror-6939.test.tsTsFilterField['type']types/src/__tests__/filter-builder-mirror-6939.test.tsTsFilterGroup['logic']types/src/__tests__/form-barrel-mirror-9406.test.tstypeShorthandFromRoot['type']types/src/__tests__/form-field-channel-keys-12130.test.tsassertionLocalMembersFormField['picker']types/src/__tests__/list-view-options-bag-6152.test.tskindskeyof Bagclosedkeeps the no-index-signature halftypes/src/__tests__/list-view-properties-bag-12670.test.tsassertionInstrumentFiresSpecListView['type']types/src/__tests__/menu-item-union.test.ts_DividerKeySetkeyof MenuDividerItemtypes/src/__tests__/node-recursion-point-8344.test.tsNodeRecursionPointDeclarationDriftLiteralArmType< ArmsNotAssignableToSchemaNode >SchemaNode; red names a real TS/zod gaptypes/src/__tests__/object-chart-properties-bag-11276.test.tsassertionInstrumentFiresExclude<keyof Mirror, NodeLevel>types/src/__tests__/object-data-table-row-click-arity-9799.test.tsassertionAritySpansOneOrTwoParameters<Slot>['length']types/src/__tests__/object-form-properties-bag-10859-b4.test.tsassertionTwinStaysPublishedTsObjectFormSchema['mode']types/src/__tests__/objectql.exportOptions.test.ts_KeysAreExactlyTheSpecFivekeyof ListViewExportOptionstypes/src/__tests__/objectql.exportOptions.test.ts_FormatsAreCsvXlsxJsonListViewExportFormattypes/src/__tests__/page-kind-jsx-retired-12464.test.ts_KindIsTheSpecVocabularyNonNullable<PageNodeSchema['kind']>types/src/__tests__/query-params-closed-keys-7497.test.ts_Closedkeyof QueryParamstypes/src/__tests__/record-highlights-fields-icon-9280.test.ts_EntryKeysMatchContractkeyof DeclaredEntrytypes/src/__tests__/report-chart-query-spec-parity.test.ts_blockTypeIsUnchangedBlockMember<PublishedJoinedReportBlock, 'type'>resolveReportPresentationfalls back, nothing is total. Now: the three block types present, optional, notstringtypes/src/__tests__/report-schema-authoring-face.test.ts_SeriesTypeStaysThreeFamiliesSeriesTypetypes/src/__tests__/select-option-form-view-boundary-12736.test.tsassertionObjectFieldOnlyKeysAreDefaultAndKindObjectFieldOnlyKeytypes/src/__tests__/select-option-form-view-boundary-12736.test.tsassertionBaseKeepsKindAndDefaultExtract< keyof SelectOptionBase, 'kind' | 'default' >types/src/__tests__/select-option-form-view-boundary-12736.test.tsassertionMetadataFaceKeepsKindAndDefaultExtract< keyof SelectOptionMetadata, 'kind' | 'default' >types/src/__tests__/sidebar-declared-surface-11465.test.tsassertionVariantIsTheRenderersEnumTs_SidebarSchema['variant']types/src/__tests__/sidebar-declared-surface-11465.test.tsassertionVariantIsTheRenderersEnumInputOf<'variant'>types/src/__tests__/spec-symbol-parity.test.ts_elementIsAuthoringOnlyMirrorFilters['element']types/src/__tests__/strict-face-read-keys-11070.test.tsassertionGridFieldKeysOnTheFormFieldFaceExtract<GridFieldOwnKeys, 'columns' | 'sortField' | 'sort_field' | 'name'>types/src/__tests__/strict-face-read-keys-11070.test.tsassertionSidebarSideIsTheTwoEdgesTs_SidebarSchema['side']types/src/__tests__/strict-face-read-keys-11070.test.tsassertionSidebarSideIsTheTwoEdgesSidebarSideInputtypes/src/__tests__/undeclared-but-consumed-keys-6150.test.ts_OrientationIsAxisNonNullable< TsCarouselSchema['orientation'] >types/src/__tests__/view-navigation-config-spec-parity.test.ts_KeysAreExactlyTheSpecSixkeyof ViewNavigationConfig_IsExactlyTheSpecInputalready refuses a local key. Now_KeysIncludeTheSpecSix+_ViewIsStillDeclared(the tombstone by name)types/src/__tests__/view-navigation-config-spec-parity.test.ts_ModeMembershipIsTheSevenSpecModesNonNullable< ViewNavigationConfig['mode'] >_ModeIsClosedtypes/src/__tests__/zod-mirror-parity.test.tsassertionOpenRecordClauseIsAdditiveWiderThanDeclaredKeys< SyntheticBothClassesMirror, SyntheticBothClassesDeclaration >types/src/__tests__/zod-mirror-parity.test.tsassertionAllowListReportsUnfiledAndCallbackRows{ [K in keyof SyntheticAllowList]: |${K}::${Exclude< SyntheticAllowKeys< K >, K extends …`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:
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 topositionis not redspec-symbol-parity.test.ts_NoInventedKeys(ObjectFieldGroup)_HasSpecKeyskeeps the keys ui readsspec-symbol-parity.test.ts_TypeIsClosed(DecisionOutputDef['type'])stringplus_TypeKeepsTheFiveKinds(counted in the table)spec-symbol-batch6.test.ts_SpecShape(SpecValidationError)field,messageandcodepresentspec-symbol-batch6.test.ts_RejectVocabularyrejectedpresent (a single literal, relaxed with its two siblings)select-option-tier1-convergence-7014.test.tsPickequals the old shape); gained keys must be spec keys (assertion…GainedOnlySpecKeys), and the gains ui relies on (description;defaultandkind) are pinned by namewidget-schema-anchors-7946.test.tsgroupBypinfieldand optionalalias; the five date granularities presentfield-metadata-depends-on-declared-6153.test.ts_SpecShapeIsArrayOfNameOrEntryfieldand optionalparamReds that stay, and why
The
keysprobe turns many other ui type checks red. They fall into three groups, and none are restatements:_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-parityanddesigner-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 app-shell: the ObjectSchema corpus guards pin an exact top-level key count, so every additive spec change turns Test UI red on main; assert only the membership claims they guard #26238's ruling on derived pins.Record<SpecUnion, …>is a value, not a type pin:AppContent.recordDialogInterimBound-12521,identityImportWriteModes-12697andAuditPanel.lockState. They are listed as an acceptance note below.MetadataServiceand 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.Baseline. All 10 packages, both configs: 0 errors.
A single member on a restated union stays green. I added
'__one26275'toPendingActionStatusonly (contracts/index.d.tsand.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_Vocabularyred.A single member on an exhaustive union turns red at the consumer. I added
__one26275toDroppedFieldsEventSchema.reasononly. Result: TS2741 atSTRIPPED_LINEin app-shellwriteWarningToast.ts, under both app-shell configs, and TS2344 at_ReasonIsTheEnum.These 3 are the only errors in all 19 runs.Full probe, head vs main. With all three probes applied, the only reds left in the 14 changed files are:
STRIPPED_LINEandLIST_VIEW_KINDS;_FaceIsTheSpecInputand_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.
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.tsfiles 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
Record<SpecUnion, …>:AppContent.recordDialogInterimBound-12521,identityImportWriteModes-12697andAuditPanel.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 theExcludeform; I left them because they are values that tests iterate, not type pins.Gates
node scripts/pm/dispatch-gates.mjs --commandsderived 24 families. I ran all 24, and every one exits 0.--ranreconciles them as 24 derived, 24 run, 0 NOT-MEASURED.check:type-check-debtran under the verify lock: 26 raw errors, none above its recorded number, no surplus.vitest runon the 14 changed files: 14 files and 350 tests pass.tsc --noEmitandtsc -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