Repository navigation
fix(spec)!: a flattened view overlay is judged by the member its viewKind names (#20186) - #20245
Conversation
…-prime premise probe) The form overlay member admits viewKind form only; the flattened LIST overlay member makes columns optional, refuses a column-less body that names a type (located at columns), and re-applies the grid default with an overwrite check after the refusal reads the input side. Both members stay plain object schemas (no pipe). Work in progress: tests, changeset, ADR-0087 entry and generated artefacts are not in this commit. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
…so the union envelope keeps invalid_union Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
…ew-overlay-viewkind-arm
…Kind names; pins, ADR-0087 entry, changeset Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
…ist patch Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
…a patch on the view, not a personalization Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
…or on the existing ledgered double Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
…ew-overlay-viewkind-arm
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2d827b1db07765b85b39007edac0bf033daa22b3 && git checkout 2d827b1db07765b85b39007edac0bf033daa22b3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab820016b3e9691870e24a9bbb867336ee5e8f72 859730effd0edc19c642a77ce03f627837267110 && git checkout -B drift-repro ab820016b3e9691870e24a9bbb867336ee5e8f72 && git merge --no-ff 859730effd0edc19c642a77ce03f627837267110
node scripts/docs-audit/affected-docs.mjs --json ab820016b3e9691870e24a9bbb867336ee5e8f72
|
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…nd the 25 missing entries (objectstack-ai#20201) (objectstack-ai#20255) Fixes objectstack-ai#20201 Clause-②: no ## What Ruling B on objectstack-ai#17152 (director `5615360777`, restated `5634031140`, on the maintainer's objectstack-ai#15954 authority `5559778263`): every retirement family carries ONE ADR-0087 D3 (`semantic`) entry, even when a lossless D2 conversion repairs its data; D2 carries the mechanical repair only. This PR takes the major-18 family census the card asks for, adds the 25 D3 entries it found missing, corrects the prose that justified their absence, and pins the census in the registry's own test. - **25 new D3 entries** under `packages/spec/src/migrations/entries/semantic/18.*.ts`, one per D2-backed family that had none. Each names its family and its D2 conversion id, says what D2 already repairs, and says what judgment the consumer still owes (the `reason`), with an `acceptanceCriteria` the consumer can check. None is a placeholder: every one states a residue specific to its family (a unit only the author knows, a belief the platform never honoured, a shape the conversion deliberately leaves alone, code the chain cannot reach). - **Prose corrected** at the sites of `5854110917` and `5854456343`, plus step 18's rationale and step 17's docblock fact (list below). - **Census pin** in the existing `packages/spec/src/migrations/migrations.test.ts` (registry integrity). No new check script. - `MIGRATIONS_BY_MAJOR[18].semantic` 186 → 211 entries at the census base; after merging `main` (four sibling D3 entries landed meanwhile, none with a D2 conversion) the generated region holds 215. ## The census (the card's main deliverable) **Tree.** `objectstack-ai/objectstack` at `3cb84d084` (this branch's fork point; it already contains objectstack-ai#20227's view-item retirement). Major-18 population there: **185** `retired-keys`, **146** `retired-defs`, **186** `semantic` entry files, and **36** D2 conversions graduated into step 18. (The card measured 181 / 130 / 175 at `d7c024133e`.) **Grouping rule (ruling B's unit, held constant).** Where a D2 conversion exists, the conversion is the family: every retired key or def it repairs belongs to it, and a D3 entry may cover more than one conversion only where one judgment covers them (the pre-existing `element-filter-and-form-node-refused` covers `element-filter-removed` and `element-form-removed`). Every new entry here covers exactly one conversion. Where no conversion exists, the records are grouped by the D3 entry that names them. **Method.** 1. Mechanical pass (scratch scripts, not committed): for each of the 36 conversions, the major-18 `semantic/` entry files that name its id as a whole id (comment or field); for each retired key, the conversion its own comment names, else the major-18 entries naming it as `cat/Def:key`, `Def.path` / `Def:path`, or the def name plus the leaf key; for each retired def, the entries naming the def. 2. Reading pass, where a string match cannot decide: (a) ownership: an entry that names a conversion only in passing is not that family's entry (this is how `metric-filters-removed` was classed missing although `analytics-authorable-unknown-keys-refused` names it); (b) 14 records matched by more than one entry, each placed by its own comment (for example `kernel/PluginStartupResult:plugin`, which goes to `startup-orchestrator-retired`); (c) 9 records whose comment names no conversion but which belong to a D2 family (`integration/DeclarativeConnectorEntry:connectionTimeoutMs` and `:errorMapping`, the three error-mapping defs, the four responsive-shape defs), plus `integration/Connector:connectionTimeoutMs`, whose comment names two conversion ids and belongs to `connector-connection-timeout-ms-removed` (it names the permission conversion only as a comparison; round 1's Table 1 placed it wrongly, corrected in patch round 1); (d) 5 theme sub-block defs, named by the theme family's entry as its sub-blocks. **Control.** The pairing sees a family that has its entry: 11 of the 36 conversions pair with a pre-existing entry, among them `cube-join-sql-and-relationship-removed` with `cube-join-sql-and-relationship-retired` (which the pin's own control test also asserts), and the whole-id matcher refuses a prefix (`record-chatter-position-vocabulary` is a prefix of its entry's own id and matches only the entry's real citation). Evaluated at the base with the pin's logic: **24** conversions named by no major-18 entry; the reading pass adds `metric-filters-removed` for **25**. Evaluated at this head: **0**. **Result.** 331 records (185 keys + 146 defs) plus 36 conversions: - **36 D2-backed families** covering 58 records: 11 had their D3 entry, **25 had none**. Table 1. - **273 D2-less records** in 68 groups: every one is named by an existing D3 entry. Table 2. None missing, as expected: before ruling B, a retirement with no conversion needed a D3 entry anyway. The card's grep for 「lossless」 found the `tenancy.organizationField` site and step 17. The census finds 25 major-18 families, most of them with no 「lossless」 wording at all. ### Table 1 — D2-backed families (step 18 `conversionIds`, in order) | # | D2 conversion (the family) | registered records | D3 entry at base | D3 entry after | |---|---|---|---|---| | 1 | `field-malformed-scale-precision-removed` | none (value or strict-key retirement, not in the two tables) | `field-scale-precision-integer-refused` | unchanged | | 2 | `record-chatter-position-vocabulary` | none (value or strict-key retirement, not in the two tables) | `record-chatter-position-vocabulary-converged` | unchanged | | 3 | `element-input-target-variable-removed` | `ui/ElementRecordPickerProps:targetVariable`, `ui/ElementTextInputProps:targetVariable` | MISSING | `element-input-target-variable-retired` (new) | | 4 | `element-filter-removed` | `ui/ElementFilterProps:aria`, `ui/ElementFilterProps:fields`, `ui/ElementFilterProps:layout`, `ui/ElementFilterProps:object`, `ui/ElementFilterProps:showSearch`, `ui/ElementFilterProps:targetVariable` | `element-filter-and-form-node-refused` | unchanged | | 5 | `element-form-removed` | `ui/ElementFormProps:aria`, `ui/ElementFormProps:fields`, `ui/ElementFormProps:mode`, `ui/ElementFormProps:object`, `ui/ElementFormProps:onSubmit`, `ui/ElementFormProps:submitLabel` | `element-filter-and-form-node-refused` | unchanged | | 6 | `field-column-lists-canonicalized` | none (value or strict-key retirement, not in the two tables) | MISSING | `field-inline-and-related-list-columns-closed` (new) | | 7 | `metric-filters-removed` | `data/Metric:filters` | MISSING (named only in passing by `analytics-authorable-unknown-keys-refused`) | `cube-metric-filters-retired` (new) | | 8 | `cube-sub-day-granularities-removed` | none (value or strict-key retirement, not in the two tables) | `time-update-interval-sub-day-retired` | unchanged | | 9 | `cube-join-sql-and-relationship-removed` | `data/CubeJoin:relationship`, `data/CubeJoin:sql` | `cube-join-sql-and-relationship-retired` | unchanged | | 10 | `record-highlights-field-icon-removed` | `ui/RecordHighlightsField:icon` | MISSING | `record-highlights-field-icon-retired` (new) | | 11 | `mapping-lookup-params-removed` | none (value or strict-key retirement, not in the two tables) | MISSING | `mapping-lookup-params-retired` (new) | | 12 | `translation-component-submit-label-removed` | none (value or strict-key retirement, not in the two tables) | MISSING | `translation-component-submit-label-retired` (new) | | 13 | `page-component-responsive-removed` | `ui/PageComponent:responsive`, `ui/BreakpointColumnMap`, `ui/BreakpointName`, `ui/BreakpointOrderMap`, `ui/ResponsiveConfig` | MISSING | `page-component-responsive-retired` (new) | | 14 | `object-grid-default-sort-removed` | `ui/ObjectGridProps:defaultSort` | MISSING | `object-grid-default-sort-retired` (new) | | 15 | `object-kanban-quick-add-removed` | `ui/ObjectKanbanProps:quickAdd` | MISSING | `object-kanban-quick-add-retired` (new) | | 16 | `permission-allow-restore-purge-removed` | `security/EffectiveObjectPermission:allowPurge`, `security/EffectiveObjectPermission:allowRestore`, `security/ObjectPermission:allowPurge`, `security/ObjectPermission:allowRestore` | MISSING | `permission-restore-purge-bits-retired` (new) | | 17 | `form-view-option-default-removed` | none (value or strict-key retirement, not in the two tables) | MISSING | `form-view-option-default-retired` (new) | | 18 | `field-reference-to-alias` | none (value or strict-key retirement, not in the two tables) | MISSING | `field-reference-to-spelling-retired` (new) | | 19 | `connector-error-mapping-removed` | `integration/Connector:errorMapping`, `integration/DeclarativeConnectorEntry:errorMapping`, `integration/ConnectorErrorCategory`, `integration/ErrorMappingConfig`, `integration/ErrorMappingRule` | MISSING | `connector-error-mapping-retired` (new) | | 20 | `connector-connection-timeout-ms-removed` | `integration/Connector:connectionTimeoutMs`, `integration/DeclarativeConnectorEntry:connectionTimeoutMs` | `connector-provider-context-connection-timeout-ms-retired` | unchanged | | 21 | `hook-timeout-to-timeout-ms` | none (value or strict-key retirement, not in the two tables) | MISSING | `hook-timeout-unit-in-key` (new) | | 22 | `job-timeout-to-timeout-ms` | `system/Job:timeout` | MISSING | `job-timeout-unit-in-key` (new) | | 23 | `api-endpoint-cache-ttl-to-cache-ttl-seconds` | `api/ApiEndpoint:cacheTtl` | MISSING | `api-endpoint-cache-ttl-unit-in-key` (new) | | 24 | `dashboard-refresh-interval-to-refresh-interval-seconds` | `ui/Dashboard:refreshInterval` | MISSING | `dashboard-refresh-interval-unit-in-key` (new) | | 25 | `connector-health-and-trigger-durations-unit-in-key` | `integration/CircuitBreakerConfig:monitoringWindow`, `integration/ConnectorTrigger:interval` | MISSING | `connector-resilience-durations-unit-in-key` (new) | | 26 | `memory-persistence-auto-save-interval-to-ms` | `data/AutoPersistenceConfig:autoSaveInterval`, `data/FilePersistenceConfig:autoSaveInterval` | MISSING | `memory-persistence-auto-save-interval-unit-in-key` (new) | | 27 | `turso-config-timeout-to-timeout-ms` | `data/TursoConfig:timeout` | MISSING | `turso-config-timeout-unit-in-key` (new) | | 28 | `view-page-mount-removed` | `ui/ListView:pageName`, `ui/ObjectListView:pageName` | MISSING | `list-view-page-mount-retired` (new) | | 29 | `list-view-sort-string-clause-to-array` | none (value or strict-key retirement, not in the two tables) | MISSING | `list-view-sort-string-clause-retired` (new) | | 30 | `page-assigned-profiles-removed` | `ui/Page:assignedProfiles` | `page-assigned-profiles-audience-to-permission-set` | unchanged | | 31 | `chart-config-aria-removed` | `ui/ChartConfig:aria`, `ui/ReportChart:aria` | MISSING | `chart-config-aria-retired` (new) | | 32 | `dashboard-widget-chart-config-structure-removed` | `ui/DashboardWidgetChartConfig:series`, `ui/DashboardWidgetChartConfig:type`, `ui/DashboardWidgetChartConfig:xAxis`, `ui/DashboardWidgetChartConfig:yAxis` | `dashboard-widget-chart-config-structure-refused` | unchanged | | 33 | `translation-per-app-settings-removed` | none (value or strict-key retirement, not in the two tables) | `translation-per-app-settings-platform-only` | unchanged | | 34 | `object-tenancy-organization-field-removed` | `data/TenancyConfig:organizationField` | MISSING | `object-tenancy-organization-field-retired` (new) | | 35 | `page-component-filter-record-to-rule-array` | none (value or strict-key retirement, not in the two tables) | `element-data-source-and-object-block-filter-rule-array`, `object-grid-default-filters-rule-array` | unchanged | | 36 | `view-item-owner-hidden-removed` | `ui/ViewItemWire:hidden`, `ui/ViewItemWire:owner`, `ui/ViewItem:hidden`, `ui/ViewItem:owner` | MISSING | `view-item-owner-hidden-retired` (new) | ### Table 2 — D2-less records, grouped by the existing D3 entry that names them | D3 entry (existing) | records it names | |---|---| | `advanced-plugin-lifecycle-config-retired` | `kernel/AdvancedPluginLifecycleConfig`, `kernel/GracefulDegradation`, `kernel/PluginUpdateStrategy` | | `ai-conversation-analytics-duration-unit-in-key` | `ai/ConversationAnalytics:duration` | | `api-error-retry-after-unit-in-key` | `api/EnhancedApiError:retryAfter` | | `api-runtime-config-durations-unit-in-key` | `api/DataLoaderConfig:cacheTtl`, `api/RouteDefinition:timeout` | | `automation-flow-list-route-retired` | `api/FlowSummary`, `api/ListFlowsRequest`, `api/ListFlowsResponse` | | `automation-runs-cursor-retired` | `api/ListRunsRequest:cursor` | | `branded-identifier-schemas-retired` | `shared/AppName`, `shared/FieldName`, `shared/FlowName`, `shared/ObjectName`, `shared/RoleName`, `shared/ViewName` | | `change-management-duration-keys-retired` | `system/ChangeImpact:downtime.durationMinutes`, `system/ChangeRequest:implementation.steps.estimatedMinutes`, `system/RollbackPlan:steps.estimatedMinutes` | | `change-management-family-retired` | `system/ChangeImpact`, `system/ChangePriority`, `system/ChangeRequest`, `system/ChangeStatus`, `system/ChangeType`, `system/RollbackPlan` | | `cli-command-contribution-retired` | `kernel/CLICommandContribution` | | `cloud-subpath-retired` | 62 records, all `cloud/` defs | | `data-file-value-duration-unit-in-key` | `data/FileValue:duration` | | `data-nosql-query-options-timeout-unit-in-key` | `data/NoSQLQueryOptions:timeout` | | `device-request-response-interval-unit-in-key` | `api/DeviceRequestResponse:interval` | | `driver-options-timeout-to-timeout-ms` | `data/DriverOptions:timeout` | | `epoch-instant-keys-renamed` | `api/SimplePresenceState:lastSeen`, `api/WebSocketEvent:timestamp`, `kernel/HealthStatus:timestamp`, `kernel/KernelContext:startTime`, `kernel/TenantRuntimeContext:startTime` | | `esignature-config-deadline-keys-retired` | `data/ESignatureConfig:expirationDays`, `data/ESignatureConfig:reminderDays` | | `event-name-schema-retired` | `shared/EventName` | | `export-job-family-retired` | 13 records, all `api/`, `automation/` defs | | `hot-reload-inert-state-strategies-retired` | `kernel/DistributedStateConfig` | | `hot-reload-watch-placeholder-retired` | `kernel/HotReloadConfig:watchPatterns` | | `identity-api-key-schema-retired` | `identity/ApiKey` | | `incident-response-deadline-keys-retired` | `system/IncidentNotificationMatrix:escalationTimeoutMinutes`, `system/IncidentNotificationRule:regulatorDeadlineHours`, `system/IncidentNotificationRule:withinMinutes`, `system/IncidentResponsePhase:targetHours`, `system/IncidentResponsePolicy:retentionDays`, `system/IncidentResponsePolicy:triageDeadlineHours` | | `incident-response-family-retired` | `system/Incident`, `system/IncidentCategory`, `system/IncidentNotificationMatrix`, `system/IncidentNotificationRule`, `system/IncidentResponsePhase`, `system/IncidentResponsePolicy`, `system/IncidentSeverity`, `system/IncidentStatus` | | `kernel-compatibility-matrix-estimated-migration-time-unit-in-key` | `kernel/CompatibilityMatrixEntry:estimatedMigrationTime` | | `kernel-context-preview-mode-retired` | `kernel/KernelContext:previewMode`, `kernel/PreviewModeConfig`, `kernel/TenantRuntimeContext:previewMode` | | `kernel-event-bus-retention-unit-in-key` | `kernel/EventPersistence:retention`, `kernel/EventSourcingConfig:retention` | | `kernel-health-check-and-hot-reload-durations-unit-in-key` | `kernel/HotReloadConfig:debounceDelay`, `kernel/PluginHealthCheck:interval`, `kernel/PluginHealthCheck:timeout` | | `kernel-package-lifecycle-durations-unit-in-key` | `kernel/MultiVersionSupport:rollout.duration`, `kernel/PackageDependencyResolutionResult:resolvedIn`, `kernel/UpgradePlan:estimatedDuration` | | `kernel-plugin-health-report-durations-unit-in-key` | `kernel/PluginHealthReport:metrics.responseTime`, `kernel/PluginHealthReport:metrics.uptime` | | `kernel-plugin-security-durations-unit-in-key` | `kernel/KernelSecurityPolicy:auditLog.retention`, `kernel/KernelSecurityPolicy:authentication.tokenExpiration`, `kernel/PluginSecurityManifest:vulnerabilityDisclosure.responseTime` | | `kernel-runtime-config-timeout-unit-in-key` | `kernel/RuntimeConfig:resourceLimits.timeout`, `kernel/SandboxConfig:process.timeout` | | `kernel-startup-orchestrator-durations-unit-in-key` | `kernel/PluginStartupResult:duration`, `kernel/StartupOptions:timeout`, `kernel/StartupOrchestrationResult:totalDuration` | | `list-view-navigation-view-retired` | `ui/NavigationConfig:view` | | `logging-durations-unit-in-key` | `system/HttpDestinationConfig:batch.flushInterval`, `system/HttpDestinationConfig:retry.initialDelay`, `system/HttpDestinationConfig:timeout`, `system/LoggingConfig:buffer.flushInterval` | | `metadata-changed-event-payload-retired` | `kernel/MetadataChangeOperation`, `kernel/MetadataChangedEventPayload` | | `metadata-customization-protocol-retired` | 13 records, all `api/`, `kernel/` defs | | `metadata-manager-config-cache-ttl-unit-in-key` | `kernel/MetadataManagerConfig:cache.ttl` | | `metadata-manager-config-inert-cache-keys-retired` | `kernel/MetadataManagerConfig:cache.enabled`, `kernel/MetadataManagerConfig:cache.maxSize`, `kernel/MetadataManagerConfig:cache.ttlSeconds` | | `metadata-plugin-additional-types-retired` | `kernel/MetadataPluginConfig:additionalTypes` | | `package-rollback-response-retired` | `api/PackageRollbackResponse` | | `packages-list-pagination-retired` | `api/ListInstalledPackagesRequest:cursor`, `api/ListInstalledPackagesRequest:limit` | | `plugin-auto-restart-never-reinitialised` | `kernel/PluginHealthCheck:autoRestart`, `kernel/PluginHealthCheck:maxRestartAttempts`, `kernel/PluginHealthCheck:restartBackoff` | | `plugin-manifest-contributes-dead-members-retired` | `kernel/Manifest:contributes.actions`, `kernel/Manifest:contributes.commands`, `kernel/Manifest:contributes.drivers`, `kernel/Manifest:contributes.events`, `kernel/Manifest:contributes.fieldTypes`, `kernel/Manifest:contributes.functions`, `kernel/Manifest:contributes.menus`, `kernel/Manifest:contributes.themes`, `kernel/Manifest:contributes.translations` | | `plugin-manifest-contributes-routes-retired` | `kernel/Manifest:contributes.routes` | | `plugin-manifest-dead-containers-retired` | `kernel/Manifest:capabilities`, `kernel/Manifest:configuration`, `kernel/Manifest:extensions` | | `plugin-manifest-kind-globs-retired` | `kernel/Manifest:contributes.kinds.globs` | | `plugin-security-scan-result-surface-retired` | `kernel/KernelSecurityScanResult`, `kernel/KernelSecurityVulnerability`, `kernel/PluginQualityMetrics:securityScan`, `kernel/PluginSecurityManifest:scanResults`, `kernel/PluginSecurityManifest:vulnerabilities` | | `rest-api-endpoint-handler-status-retired` | `api/HandlerStatus`, `api/RestApiEndpoint:handlerStatus`, `api/RouteCoverageEntry`, `api/RouteCoverageReport` | | `rest-api-plugin-durations-unit-in-key` | `api/RestApiEndpoint:cacheTtl`, `api/RestApiEndpoint:timeout`, `api/RestApiPluginConfig:performance.defaultCacheTtl` | | `rest-server-config-dead-keys-retired` | 11 records, all `api/` defs | | `session-user-language-retired` | `api/SessionUser:language` | | `stack-themes-carrier-retired` | `ui/BorderRadius`, `ui/ColorPalette`, `ui/Shadow`, `ui/Theme`, `ui/ThemeMode`, `ui/Typography` | | `startup-orchestrator-retired` | `kernel/HealthStatus`, `kernel/PluginStartupResult:health`, `kernel/PluginStartupResult:plugin`, `kernel/PluginStartupResult:startTime`, `kernel/StartupOptions`, `kernel/StartupOrchestrationResult` | | `system-cache-durations-unit-in-key` | `system/CacheAvalanchePrevention:circuitBreaker.resetTimeout`, `system/CacheTier:ttl` | | `system-collaboration-durations-unit-in-key` | `system/CollaborationSessionConfig:idleTimeout`, `system/CollaborationSessionConfig:snapshot.interval` | | `system-failover-health-check-interval-unit-in-key` | `system/FailoverConfig:healthCheckInterval` | | `system-metrics-jsdoc-durations-unit-in-key` | `system/MetricDefinition:summary.maxAge`, `system/MetricExportConfig:interval`, `system/MetricsConfig:collectionInterval`, `system/MetricsConfig:retention.period`, `system/ServiceLevelObjective:errorBudget.burnRateWindows.window` | | `system-metrics-window-durations-unit-in-key` | `system/MetricAggregationConfig:window.size`, `system/ServiceLevelIndicator:window.size`, `system/ServiceLevelObjective:period.duration` | | `system-object-storage-durations-unit-in-key` | `system/AccessControlConfig:maxAge`, `system/StorageConnection:timeout` | | `system-registry-config-durations-unit-in-key` | `system/RegistryConfig:cache.ttl`, `system/RegistryUpstream:syncInterval`, `system/RegistryUpstream:timeout` | | `system-tracing-otel-exporter-durations-unit-in-key` | `system/OpenTelemetryCompatibility:exporter.batch.exportTimeout`, `system/OpenTelemetryCompatibility:exporter.batch.scheduledDelay`, `system/OpenTelemetryCompatibility:exporter.timeout`, `system/TracingConfig:performance.exportInterval` | | `system-tracing-span-duration-unit-in-key` | `system/Span:duration` | | `system-worker-queue-rate-limit-duration-unit-in-key` | `system/QueueConfig:rateLimit.duration` | | `tenant-schema-cache-ttl-unit-in-key` | `system/SchemaLevelIsolationStrategy:performance.schemaCacheTTL` | | `training-deadline-keys-retired` | `system/TrainingCourse:durationMinutes`, `system/TrainingCourse:validityDays`, `system/TrainingPlan:gracePeriodDays`, `system/TrainingPlan:recertificationIntervalDays`, `system/TrainingPlan:reminderDaysBefore` | | `training-family-retired` | `system/TrainingCategory`, `system/TrainingCompletionStatus`, `system/TrainingCourse`, `system/TrainingPlan`, `system/TrainingRecord` | | `websocket-durations-unit-in-key` | `api/WebSocketConfig:pingInterval`, `api/WebSocketConfig:reconnectInterval`, `api/WebSocketConfig:timeout`, `api/WebSocketServerConfig:heartbeatInterval` | ## Prose corrected (the single-entry sites of `5854110917` / `5854456343`, and the rationale sentences) | site (at this head) | was | now | |---|---|---| | `packages/spec/src/migrations/registry.ts:78–86` (step 17 docblock, fact correction only) | 「Mechanical, and mechanical only … there is no semantic residue and the `semantic` list is deliberately empty」 | the three renames replay losslessly as D2; they carry no D3 entry because step 17 shipped before the rule and was not back-filled; the `semantic` list is NOT empty. ⛔ No step-17 entry added. | | `packages/spec/src/migrations/registry.ts:5256` (step 18 rationale, `tenancy.organizationField`) | 「The conversion is a lossless delete and there is no semantic residue」 | a lossless delete still leaves the author a judgment, carried by `object-tenancy-organization-field-retired` | | `packages/spec/src/conversions/registry.ts:3460` (`datasource-driver-mongo-to-mongodb`, protocol 17) | 「Why D2 and not D3」 | 「Why the data repair is D2」, plus: losslessness does not decide whether a family owes D3; this one is protocol 17 and has none | | `packages/spec/src/conversions/registry.ts:9312` (`api-endpoint-cache-ttl-to-cache-ttl-seconds`) | 「gets a conversion rather than a semantic entry」 | 「also gets a conversion」, and names its D3 entry | | `packages/spec/src/conversions/registry.ts:9804` (`list-view-sort-string-clause-to-array`) | 「which is why this is a D2 conversion rather than a semantic TODO」 | the data repair is D2; the family's D3 entry carries the clauses the rewrite leaves alone | | `packages/spec/src/migrations/entries/retired-keys/18.api__ApiEndpoint__cacheTtl.ts:11–19` | 「a D2 CONVERSION rather than a semantic entry」 | also a D2 conversion, and names the D3 entry | | `packages/spec/src/migrations/entries/semantic/18.metadata-endpoints-switch-radius-repartitioned.ts:11–13` | 「exactly the residue D2 cannot express, which is why this is a semantic entry」 | that residue is why there is no D2 at all; the D3 entry is owed either way | | `packages/spec/scripts/build-migration-registry.ts:276` | 「a major whose semantic residue is genuinely nil (protocol 14)」 | an empty region is a real state (a freshly opened step, or protocol 14's, which predated the rule) | ⛔ Not touched: the governed texts (ADR-0087, `.claude/skills/spec-property-retirement/SKILL.md` §3), which are objectstack-ai#20188's. ## The census pin **Where:** `packages/spec/src/migrations/migrations.test.ts` › `registry integrity`: `from protocol 18 on, every graduated D2 conversion is named by a D3 entry of its own step (ruling B)`, plus a control test. It reads the major's `entries/semantic/` files (comment and literal) inside its own package; `check:migration-registry` already proves those files and the generated region are one set. **What it asserts:** for every step whose `toMajor` is 18 or later, every id in `conversionIds` appears as a whole id in at least one `semantic/` entry of that major. A new major-18 (or later) retirement that lands a D2 conversion with no D3 entry naming it goes red, naming the conversion. **What it cannot see**, stated so a green run is not over-read: (1) whether the naming entry is that family's OWN (a passing mention satisfies it; the census judged ownership by reading); (2) a family retired with no conversion at all (no machine-readable link joins a retired key or def to its D3 entry; the census paired those by reading, and found none missing). Protocol 17 is outside the pin by design: measured with the same logic, 53 of its 57 graduated conversions are named by no step-17 entry, and step-17 backfill is out of scope (triage `5854164872`). **Reverse verification (one-shot, no permanent test file).** At `c8656ad35`, with the entries committed: deleted `18.object-tenancy-organization-field-retired.ts` (absence confirmed on disk before the run), ran the pin: `× from protocol 18 on …` with `+ "protocol 18: object-tenancy-organization-field-removed"`, `Tests 1 failed | 140 skipped`. Restored with `git checkout HEAD -- PATH` (that path) inside a `trap … EXIT INT TERM`: blob `2cf3007d9fff` equals HEAD's, `git diff HEAD` empty. Direction observed: red, the expected one. No build involved: the test imports `src/` and reads the entry files directly. ## Patch round 1 (contract review `5857834457`: FAIL at `3197fce29`) **Blocking: fixed.** `Lint & Repo Gates` step 189 (`check-issue-citations.mjs`, judging pass) was red. Four bare citations this PR added answer 404 on the board: objectstack-ai#10329, objectstack-ai#10926, objectstack-ai#12868 and objectstack-ai#14676. Each was in an entry's leading comment, and again in the regenerated region. `--probe-cause` classes all four as **deleted** (the web endpoint also answers 404, so none was transferred), so none of them is a reference to another repository to qualify. Each comment now anchors to the commit in this repository's history that retired the family, and says in words what that commit decided. That is the precedent of commit `66e266c93` (ruling C+D on objectstack-ai#19123). Every sha is an ancestor of `origin/main`: | entry | was | now anchored to | |---|---|---| | `mapping-lookup-params-retired` | objectstack-ai#10329 | commit `15d58dbf1` (the import path never read the four lookup steering params) | | `translation-component-submit-label-retired` | objectstack-ai#10926 | commit `d173125fb` (the copy key left with its only declarer, `element:form`) | | `form-view-option-default-retired` | objectstack-ai#12868 | commit `c459da6bc` (the ruled narrowing: the form-view face drops per-option `default`, the object-field face keeps it enforced) | | `connector-error-mapping-retired` | objectstack-ai#14676 | commit `13c48c2a5` (eleven inert keys, one spelled like the live `userMessage` channel) | Only the comments changed; no string an author is shown moves. The region was regenerated with `gen:migration-registry`. The round-1 report's `pnpm check:issue-citations :: exit 0` was the package script, which runs only the `--self-test`. The judging pass CI runs was exit 2 at `3197fce29` (8 findings = 4 numbers × 2 sites) and is exit 0 now (below). **Pin message.** The census pin's assertion now names the unnamed `protocol N: conversion-id` pairs and the remedy: add a D3 `semantic` entry of that step whose text names the conversion id as a whole word. Its logic and scope are unchanged. Shown firing at `21418c4d2` with one entry removed (trap-guarded restore, blob equal to HEAD's, `git diff HEAD` empty): `AssertionError: graduated D2 conversion(s) named by no D3 entry of their own step: protocol 18: object-tenancy-organization-field-removed. Remedy: add a D3 semantic entry of that step …`, `Tests 1 failed | 140 passed`. **Body.** Table 1: `integration/Connector:connectionTimeoutMs` moved from row 16 to row 20. Its own comment names `connector-connection-timeout-ms-removed`; the permission id appears there only as a comparison. The code was already right. ## Sibling PRs - **PR objectstack-ai#20238** (objectstack-ai#20161) has since LANDED as `6a6a17b62`, with the D2 conversion `report-joined-chart-removed` and `18.ui-report-joined-chart-retired.ts`, which names that id. The union of this head with `main` at `6a6a17b62` is clean and passes the pin (37 pairs, 0 unnamed; delta review `5859315908`). - `main` was merged three times with `os-regen-merge.sh`, and never by hand in a generated region: at `a70cd62e5` (objectstack-ai#20223 and objectstack-ai#20245), at `21418c4d2` (`cel-predicate-one-value-comparand-refused` and `filter-query-face-comparands-refused-at-save`) and at `a930cacea` (step-17 rationale prose from objectstack-ai#20268). Each is D3-only or prose, with no new step-18 conversion. Regeneration produced a commit only after the first merge (`3197fce29`) and changed nothing after the other two. Every sibling entry id was verified present. ## Verification (head `a930cacea`) - `pnpm check:issue-citations && node scripts/check-issue-citations.mjs`, exactly as CI runs it (base `origin/main`): **exit 0**, 112 citations across 29 files: 106 resolve, 6 cross-repo unjudged, 0 findings. At `3197fce29` the same command exited 2. - `pnpm --filter @objectstack/spec build` under the verify lock: ok. `check:generated`: all 15 generated artifacts up to date. `check:migration-registry`: current (292 semantic, 214 retired-key, 199 retired-def). `spec-changes.json` and `docs/protocol-upgrade-guide.md` do not move: they project up to the current protocol major, and step 18 is beyond it. - `pnpm --filter @objectstack/spec exec vitest run --project local`: **552 files, 16257 passed, 1 todo**. The `src/migrations/` directory alone: 3 files, 151 passed. - `pnpm --filter @objectstack/spec typecheck` (tsc, scripts, test layer) at `21418c4d2`, the head before the last merge, which brought only another PR's prose into this diff's files: exit 0, test-typecheck debt unchanged (53 files / 255 errors / 142 signatures). - `node scripts/pm/dispatch-gates.mjs --commands` (no paths) at `a930cacea`, every command run and its exit recorded, reconciled with `--ran`: **89 derived, 87 run (all exit 0), 2 NOT MEASURED**. `check:dual-build-cjs-loads` and `check:type-check-debt` exited 3 (PREREQUISITE NOT MET: the full 86-package workspace build does not fit the foreground cap on this shared box). CI runs both. `check:pm-dispatch-gates` finished this time: exit 0, in 907.6 s. - ESLint, narrowed and proven: all 31 changed `.ts` files, `--no-inline-config --format json`: 0 errors, 0 warnings, none reported ignored. `eslint.config.mjs` enables no type-aware linting (its own statement at `eslint.config.mjs:326–328`), so this diff cannot move any untouched file's verdict. - Changeset: `@objectstack/spec` `patch`. The published registry text changes; no accept set moves. ## Acceptance notes (observed, not filed) - `registry.ts:108` (released step-17 text) still says the sharing-rule `full` conversion 「leaves no semantic residue」: triage scoped step-17 backfill out, so it is left as is. - New entries keep tracker numbers in their `//` comments only, never in the strings an author is shown (AGENTS.md runtime-strings rule). Several older entries do cite numbers in `reason`; not touched. - `dashboard-refresh-interval-unit-in-key` states the console renderer's release lag as a verification step, not as a present fact: this container has no objectui checkout at the pin to measure it. - `main` moved after the last merge (`a930cacea`). objectstack-ai#20285 (`2aa25efb4`, prose in five semantic entries) and objectstack-ai#20238 (`6a6a17b62`, a new step-18 conversion with its entry) landed under `migrations/` and `conversions/`. The delta review merged this head onto `6a6a17b62`: clean, with all six regions still mirrored. The queue verifies the merged generation. (Corrected by the seat at 2026-09-27T19:57Z; the earlier wording said nothing under those paths had moved.) --- _Generated by [Claude Code](https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20186
Clause-②: yes (narrowing)
A flattened
viewoverlay is now judged by the overlay member itsviewKindnames. A column-less list patch, which the console writes on every toolbar save, has its list keys judged, instead of being accepted by the form member with those keys stripped unread. This is route C-prime as ruled in seat answers5855433719and5855548706on #20186.What was wrong (measured)
Both flattened overlay members of
ViewMetadataSchemashared oneviewKind: 'list' | 'form'enum. The list member requiredcolumns, so it refused a column-lessviewKind: 'list'body. The union then tried the form member, which requires no list key and.strip()s every one, and accepted the body:diagnoseViewMetadataansweredformOverlayand the parse output wastype: 'simple', a form.I measured this through the real
saveMetaItemonorigin/main4df101c3, and again at the spec level once70876eafter #20183 landed; the verdicts were byte-identical. Each of these bodies answeredsuccess: true, and the row held it exactly as sent:{ name, object, viewKind: 'list', sort: 'name desc' }, a retiredsortstring;timeline: { …, metaFields: [...] };searchableFields: 'name'.The mirror also held: the list member accepted
{ viewKind: 'form', columns: ['name'] }.The column-less list body is not malformed; the console writes it. objectui's
buildPersistedViewBodyreturns{ ...patch, viewKind }for an overlay, andupdateViewConfigstampsobject,nameand_isOverride. That is the maintainer ruling on #7494 (comment5261754173): 「persistViewPatch只存 patch,不存 merged base」. So refusing it (route A) was ruled out.What changed —
packages/spec/src/ui/view.zod.ts, the flattened-overlay region onlyflattenedViewOverlayFields(kind): oneviewKindper member. The list member admits'list'only and the form member'form'only. An absentviewKindkeeps the authoring-validation-not-persisted: a flat view body is accepted, published and reported valid, then expands to nothing — the write door judges by the wire union, not the strict ViewSchema #7741 binding prescription.The flattened LIST member judges a patch.
columnsis optional on this member only; the authoringListViewSchemakeeps it required.typeis declared here without its.default('grid'), so the member's checks can see whether the body named one. Three checks follow on the same object schema, in this order:checkListOverlayTypeNeedsColumnsrefuses a column-less body that names atype, atcolumns. The issue is aborting, so the union envelope staysinvalid_union.applyListOverlayTypeDefault(an.overwrite()) putstype: 'grid'back, in the key position the default took.FormViewSchemaalready uses the same.overwrite()shape for itsgroups→sectionsfold.No pipe: both overlay members are still
def.type === 'object', and a test pins it.The FORM member's
columnsis a clone ofFormViewSchema's own count schema with an error map added. Its constraints are that schema's, and a field list gets a prescription. The member is built with.safeExtend(), because overriding a key onFormViewSchema, which carries refinements, is refused by.extend(). The two differ in that throw only.diagnoseViewMetadataandselectViewMetadataBranchneeded no change. Each body is now accepted by at most one overlay member, so the branch they name is the member that judged it. The conversions walk (mapViewPayloads) already picked an overlay's family fromviewKind; the parse now agrees with it.Refusal and diagnosis texts (new)
Served JSON Schema (
/api/v1/meta/types/view)The schema is still an
anyOfof four members in both directions. Members 0 and 1 are byte-identical. I diffed it againstce70876e, and it moves only by the contract:viewKind.enumis["list"], andcolumnsis no longer inrequired. In the output directiontypeis no longer inrequired. Thetypedefaultgridstays byte-identical in both directions, via.optional().meta({ default }).viewKind.enumis["form"].columnsis unchanged.Pins:
ViewMetadataSchemaANDsaveMetaItempackages/spec/src/ui/view-overlay-viewkind-arm.test.ts(schema +diagnoseViewMetadata).packages/metadata-protocol/src/protocol.graft-folded-form-sections.test.ts(its ledgered stub engine, the realsaveMetaItem, and the stored row read back). Every refusal assertsINVALID_METADATA,422, nothing stored, and the issue's path, code and prescription.objectbound)viewKind: 'list',sort: [{ field, order }](the headline)type: 'simple'type: 'grid', stored verbatimviewKind: 'list',sort: 'name desc'invalid_typeatsort(the 17.5.0 prescription)viewKind: 'list',timeline.metaFieldsunrecognized_keysattimelineviewKind: 'list',searchableFields: 'name'invalid_typeatsearchableFieldsviewKind: 'list',sharing: { enabled: true }(form block)unrecognized_keysatsharingviewKind: 'form',columns: ['name'](mirror)invalid_typeatcolumns, count prescriptionviewKind: 'list',type: 'kanban', nocolumns(overlay.list.identity)invalid_union, bareInvalid inputatcolumns)invalid_union),customatcolumnswith the prescriptioncolumnsPin sweep
view-union-diagnostics.test.tsput.isPinned,put.sortOrderandput.pinAndOrder. The verdict is still ACCEPT, but the pinned parse output moves from{ type: 'simple', … }to{ type: 'grid', … }. The seat named these three as the intended change.overlay.list.identity, which stays REFUSE and staysinvalid_union.put.*pins above (numbers under Verification).optionsbag at the view write door (door half of #20051) #20183 pin inview-overlay-options-bag.test.ts(a column-less body with a BADoptionsbag stays refused) holds: the list member judges the bag.Readers keyed on the old form parse of these rows
graftNormalizedOperators(thesaveMetaItemoperator graft). On805af4f2the form member stripped a column-less list patch'sfilter, so an alias operator (eq) was stored as written. Now the list parse normalises it, and the graft writes the canonical operator into the row, as it already did for a list overlay withcolumns. Measured through the real save on this branch:filter: [{ field: 'name', operator: 'eq', value: 'x' }]is stored asoperator: 'equals'.graftFoldedFormSections. On805af4f2a list patch carrying the form keygroupswas parsed as a form (parse outputsections: [...]), and the graft stored it assections. Now the list member dropsgroupsunread, and the row keepsgroupsas authored (measured through the real save). This is a W1-class body; see the Acceptance notes.main, has no reader ofdiagnoseViewMetadataor of the parse output'stypefor these rows. Its only hits are docblocks namingVIEW_METADATA_MEMBERS.formOverlay, for real form overlays.Declared widening (W1, W2) — why
Clause-②: yesBoth classes are column-less, type-less
viewKind: 'list'bodies that were refused and are now accepted. All readings below are on805af4f2and on this branch.{ name: 'crm_lead.all', object: 'crm_lead', viewKind: 'list', aria: { ariaLabel: 'Leads' } }. It was REFUSED and is now ACCEPTED on listOverlay.description, the listsharingblock and a valid legacyoptionsbag. Each of those is pinned ACCEPT at both doors, andariais pinned too.type,columns,description,sharing,aria,options(andviewKinditself).{ name: 'crm_lead.all', object: 'crm_lead', viewKind: 'list', isPinned: true, layout: 'diagonal' }. It was REFUSED, because the form member judgedlayout. It is now ACCEPTED, withlayoutdropped from the parse.layout,title,defaultTab,tabPosition,allowSkip,showStepIndicator,splitDirection,splitSize,splitResizable,drawerSide,drawerWidth,modalSize,sections,groups,subforms,defaultSort,submitBehavior,buttons,defaults.Census (literal and dynamic producers)
4df101c3: 236 literalviewKind: 'list'occurrences in 94 files underpackages/**. Of those, 56 are column-less flattened: 41 tests, 4 changelog lines, and 11 comments or docstrings. Control: 105 flattened-with-columns and 70configrecords were classified by the same scanner.examples/**: 0viewKindat all. Control: 15 files withlistViewsand 14 withdefineView.f8a9d0fb, 90 occurrences in 39 files; atmain25c7d584e, 95 in 42. The column-less ones are tests plus 4 type declarations or docstrings.main48d7066: 60 occurrences in 13 files; all 14 column-less ones are tests. Its two producers emitconfigrecords.buildPersistedViewBodyplusupdateViewConfig, called from thesort,hiddenFields,inlineEditandcolumnStatetoolbar handlers on objectuimain. It keeps saving.view-overlay-judged-by-viewkind-armrecords the write-time refusals, and the registry is regenerated.Verification
All heavy runs went through
os-verify-lock. The lock is shared, so every timing is a shared-box reading.Pre-merge, at
750d0c82(the branch merged with805af4f2), in one locked batch (VERDICT command-exit 0, held 984s):metadata-protocol,objectql,lintandrestdependency closures (the^...filter): exit 0.spec check:generated: exit 0, all 15 artefacts current,api-surface/included, so no generated artefact moves.specvitest--project local: 545 files passed, 16059 tests passed, 2 todo.metadata-protocolvitest: 189 files passed, 3 skipped; 2715 tests passed, 19 skipped.metadata-protocoltypecheck: exit 0.viewKind:objectql24 files / 380 tests,lint8 files / 349 tests,rest11 files / 153 tests. All passed.specvitest--project repo: 32 files / 586 tests, measured at87f9d156in an earlier locked batch. After that,packages/specchanged only by a type annotation (e272672a).spec typecheckexited 0 on those bytes.Post-merge, at
859730ef(merged withc02fa127):owner/hiddenretirement, a disjoint region ofview.zod.ts) and others. It merged clean, and the migration registry regenerates byte-identical.check:generated,speclocal vitest andmetadata-protocolvitest is queued. Until it runs it is NOT MEASURED here, and the dev report carries its reading.Gates. I re-derived the list on the actual paths (
dispatch-gates.mjs --commands: 88 families) and ran every line, plus the 5 artefact-roster gates whose roster sits in a directory this diff touches. Exit codes went to disk before anything was read.--ranreconciliation at859730ef: 88 derived, 81 run green, 7 NOT-MEASURED (exit 3, PREREQUISITE NOT MET), 0 unrun.speccheck:api-surface,check:browser-reachable-entries,check:dual-source-exports,check:entry-nameability,check:exported-any) read the builtdist. They were green at750d0c82, and the post-merge rebuild re-reads them.check:dual-build-cjs-loads,check:type-check-debt) need the whole-workspace build and are declared to CI.check:engine-double-contractandcheck:objectql-double-limitflagged the stub engine my first save-door test file declared. I moved those pins into the existing, ledgered double inprotocol.graft-folded-form-sections.test.ts, and both gates are green: no ledger change.Consumer notes.
examples/**carries noviewKindat all, so it has no objectui-facing overlay fixture to sweep.Acceptance notes
reach:measured at the public door,saveMetaItem(thePUT /api/v1/meta/viewpath), on this branch:{ name: 'crm_lead.all', object: 'crm_lead', viewKind: 'list', isPinned: true, layout: 'diagonal' }answerssuccess: true, and the row holdslayout: 'diagonal'as sent (a throwaway probe, never committed). The same save withcolumns: ['name']added answers identically.origin/main805af4f2:{ name: 'crm_lead.all', object: 'crm_lead', viewKind: 'list', columns: ['name'], layout: 'diagonal' }is ACCEPTED on listOverlay, with output{"name":"crm_lead.all","type":"grid","columns":["name"],"object":"crm_lead","viewKind":"list"}(layoutdropped). So this change routes more bodies to the list member's existing handling of undeclared keys; it does not create that handling.5855548706. The seat decides filing at ACCEPT.list overlay form-only key stripped·flattened list overlay layout dropped unread·W1 form key on list patch.graftFoldedFormSectionsbehaviour change above (a list body'sgroupsis no longer folded tosections) is the same residual seen from the save path.Generated by Claude Code