Repository navigation
fix(spec): os migrate meta guidance for the engine-* migration entries states each lesson in words, not tracker numbers (stage 1) - #20285
Conversation
…on in words, not tracker numbers The five `engine-*` ADR-0087 semantic entries carried 67 tracker-id sites in the three fields `os migrate meta` prints to the author (replacement, why, verify). Each site now says what the cited ruling, measurement or fix decided, and carries no number; ADR ids stay. Entry ids, surfaces, from/to and matching logic are untouched. registry.ts, spec-changes.json and the protocol upgrade guide are regenerated from the entries. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
…; changeset Spawns `os migrate meta` over a stack authoring a lookup and a virtual formula field, locates each `engine-*` semantic block verbatim in the printed output, and holds it free of a tracker id. The family is derived from the registry by id prefix with the five rewritten ids as its floor, and the detector is exercised lit and dark before it is trusted. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 3 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 6b42f2c11ecfd3596e71551696f4413042990949 && git checkout 6b42f2c11ecfd3596e71551696f4413042990949
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3f86dc52f22668dd92de00f604148e04d3d048da 1fa8251067e914c758ab533cd8c1fbc96a6556fb && git checkout -B drift-repro 3f86dc52f22668dd92de00f604148e04d3d048da && git merge --no-ff 1fa8251067e914c758ab533cd8c1fbc96a6556fb
node scripts/docs-audit/affected-docs.mjs --json 3f86dc52f22668dd92de00f604148e04d3d048da
|
Contract reviewServed-tier: 91/91 ① 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>
Part of #20233
Clause-②: no
Stage 1 of a staged card. The card stays open for later stages; this PR carries no closing keyword. Text only: no entry id,
surface,from/to, conversion or matching logic moves, and the chain rewrites exactly what it rewrote before.What this does
os migrate metaprints every ADR-0087 semantic entry it crosses as one block:⚠ [protocol N] surface → replacement, thenwhy:(the entry'sreason) andverify:(itsacceptanceCriteria). Those three fields are text an author is shown, and AGENTS.md's runtime-string rule applies to them: 「Runtime strings — refusal prose, prescriptions, anything an author is shown — carry no tracker number (pnpm check:doc-authoring): the lesson goes into the text.」 Form D of ruling C+D on the parent card (comment5749154545, 「同意」) sets the shape: the lesson in words, and no number, dead or alive.This stage rewrites the busiest entry family, the five
engine-*entries: 67 sites → 0. Each site now says what the cited ruling, measurement or fix decided. ADR ids stay, because an ADR lives in this repository.registry.ts,spec-changes.jsonanddocs/protocol-upgrade-guide.mdare regenerated from the entries (gen:migration-registry,gen:spec-changes,gen:upgrade-guide), never hand-edited. One new pin holds the printed output tracker-free.Census — tracker ids in the three author-shown fields
Instrument. A TypeScript-AST walk over every
packages/spec/src/migrations/entries/**/*.ts. For eachentryobject literal it evaluates the string value ofreplacement,reasonandacceptanceCriteria(string literals joined with+), then counts#followed by 4 or 5 digits at a word boundary. Tree:objectstack-ai/objectstackat443b2f4fdc(this branch's base).Controls.
17.aggregation-node-distinct-retired.tsreads 7 sites (replacement 1, reason 6), the ids a reader sees in its text.//lines in entry files carry a tracker id, and none is counted. For example,18.client-envelope-convergence-analytics-automation.tshas 5 such comment lines and counts 0. Those comment sites belong to the sibling card, and ⛔ this PR does not touch them.surfaceis not in the three-field count.17.authoring-schemas-strict-unknown-keys.tscarries one id insurface, and it is absent from the table. Thesurfacefield is counted separately under Acceptance notes.retired-keys/andretired-defs/files have none of the three fields. They count 0, while carrying 687 comment-line hits.Totals at
443b2f4fdc: 1,016 sites in 266 semantic entries, 460 distinct ids, split major 17: 417 and major 18: 599. By field: replacement 61, reason 901, acceptanceCriteria 54. The line-level upper bound over all entry files is 1,524 lines in 679 files.Why this is not the relayed 2,060. That figure scanned
packages/spec/src/migrations/**, where the generatedregistry.tsrepeats every entry's prose. At the base,registry.tsalone holds 2,113 tracker-shaped occurrences. The entry files, which are the source the generator copies, hold 1,016 sites in these three fields.Dead ids. All 22 distinct ids in the chosen family answer 200, so none is dead. The other 438 ids are not re-probed at this stage.
After this PR: 1,016 → 949 (the
engine-family 67 → 0, every other family unchanged).Families are grouped by the entry-id prefix: the first word of the id, which is the name family. All three-field sites live in the one
semantic/directory, so the directory does not separate them.engine-(this PR)ui-plugin-driver-kernel-system-datasource-filter-field-action-element-data-export-hook-rest-api-metadata-view-analytics-audit-sharing-actor-http-object-dataset-hot-external-package-query-delete-stack-etl-flow-storage-apimethod-dashboard-notification-record-runtime-scim-aggregation-automation-cache-tenant-authoring-cli-client-evaluated-identity-spec-advanced-cloud-import-startup-sys-tool-address-declarative-packages-platform-session-sort-strategy-admin-ai-assembled-auth-change-device-epoch-incident-logging-memory-rls-send-standard-training-turso-websocket-autonumber-batch-cbp-schedule-structured-time-workflow-approval-audience-branded-cluster-connector-enhanced-esignature-event-job-position-ups-cel-(2),cube-(1),execution-(1),inline-(1),list-(1),manifest-(2),observability-(1),page-(1),saved-(1),screen-(1),translation-(1),wait-(1)Stage 1 = the
engine-familyIt is the busiest family: 67 sites in 5 entries, 22 distinct ids. It also fits a reviewable stage, at 112 changed lines in entry files (+64 / −48) against the ≈400 budget, with generated
registry.tsexcluded. The five entries are the data engine's query and write option refusals:17.engine-dotted-projection-refused17.engine-find-formula-filter-refused17.engine-find-formula-order-by-refused17.engine-update-upsert-retired18.engine-dotted-filter-refusedEvery citation read, and what the text now says
I read each cited issue or PR myself: the body, and the comments where a ruling or a measurement lives. The ids are in code spans so that this body does not post 22 cross-references. There are no unresolved sites: every citation's decision was established from what it says.
#3821findmust not turn an unknown column into "no rows": it retries without the projection or ORDER BY. That is the unknown-column recovery ladder. The GitHub title names the sharing-rule recipient picker, which is where the defect surfaced. The driver'ssql-driver-unknown-column-recovery.test.tsheader ties the two together.#4226sortnaming a nonexistent field answers400 INVALID_SORTinstead of being dropped.#4256sortpath is refused at the ingress, rather than silently unsorted.#5918#6674formulafield declared insearchableFieldsis refused loudly, never admitted as search coverage.#6924#6994orderByon aformulafield is refused at the ingress (400 INVALID_SORT).formulafield alike" / "at the REST ingress"#7095registered) although no stored row is rewritten.orderBy…" / "the formula-sort refusal (engine-find-formula-order-by-refused)" / "Registered on the ruling inherited from the SORT axis — its engine refusal was registered in this ledger although no stored row needs rewriting …"#7532400 INVALID_FIELDinstead of widening the response to every field. Resolving the path was explicitly not authorised.#7534where/$filter/ a filter AST is refused with400 INVALID_FIELD.#7537expandprojection that omittedidwas a silent no-op. The engine now keeps the join key in the sub-read and strips it from the output.idproduced before the engine began keeping that join key itself"#7588#7532.#7532sentence#7589get_recordchain was measured end to end.#7601fieldspath.#7617fieldspath"#7867RECORD_NOT_FOUND: the engine's not-found gate.#8057options.upsertis removed (ADR-0049). One prescription constant is quoted by the engine gate and by both schemas.#8296400 INVALID_FIELD, judged by the spec's own virtual-field predicate.400 INVALID_FIELD" / "The formula verdict deliberately skipped dotted keys" / "the one-source move the formula verdict made withisVirtualSearchField"#8369#8296.#8296sentence#8370#8371#8790INVALID_FILTER/ 400. It has its own entry.driver-sql-unresolvable-where-column-refused, that now refuses it on both"The only call-shaped token the rewrite touches is
find(): one is removed and one is added, so textual call-spelling ratchets that readregistry.tscount the same.Pin —
packages/cli/test/migrate-meta-engine-guidance.test.tsThe test spawns the real CLI (
os migrate meta --from 16 --to 18) over a stack authoring the shapes the family is about: a lookup and a virtualformulafield. It locates eachengine-*block verbatim in the printed output, then asserts that the printed block carries no#plus 4 or 5 digits. Three things keep it from passing vacuously:ADR-0112.The file follows the existing
migrate-meta-default-range.test.tspattern: queue tier by name (not.e2e, which is nightly-only),integrationproject by behaviour.Ablation — the pin can fail
The ablation ran from committed state, HEAD
1fa8251067, withscripts/ablation-replace.mjsin wrap mode andscripts/ablation-dist-preflight.mjsgating each leg.dist/, so the green pin run after it measured the pre-mutation build.registry.ts, not from the entry files, so the preflight again reported ABSENT and the pin was not run.registry.ts, the same line the generator emits for the entry: anchorquote one removal prescriptionbecomesquote the #8057 removal prescription. Anchor went 1 → 0 and marker 0 → 1, and the blob changed fromb956ae88tocef8c6e2.engine-update-upsert-retired: the printed guidance cites a tracker id: expected '#8057' to be undefined.b956ae88== HEAD, andgit diff HEADis empty.--absentfound the marker in none of 222 built files, and the tree was clean. The pin went green, 3 passed.Verification (all at HEAD
1fa8251067)pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/migrate-meta-engine-guidance.test.ts, withTests 3 passed (3). That is the restore-leg run, after the spec rebuild.pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 src/migrations, withTest Files 3 passed (3)andTests 149 passed (149).pnpm --filter @objectstack/spec typecheckexits 0.pnpm --filter @objectstack/cli typecheckexits 0. The test layer holds its recorded 28 errors in 3 files, unchanged.tsc --listFiles -p packages/cli/tsconfig.test.jsonputs the new file in the program with 0 errors in it.test/vitest-tiers-partition.test.tspasses, 22 tests. The rest of the CLIunit/integrationsuites are declared to CI: the diff touches no CLI source, and adds only this one integration-tier file.pnpm exec turbo run build --filter="@objectstack/cli^..." --concurrency=2, with 55/55 tasks.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderives 88 families from this change set.--ranover the recorded exit codes reads 88 derived, 87 run, 1 NOT MEASURED, 0 unrun.check:migration-registry,check:spec-changes,check:upgrade-guide,check:generated(15 artifacts up to date),check:doc-authoring,check:issue-citations,check:nul-bytes,check:adr-0087-registrationandcheck:changeset-no-major.check:dual-build-cjs-loads,PREREQUISITE NOT MET(exit 3). It reads every package'sdist/, and this worktree built only the CLI's dependency closure. CI builds the whole repo.eslint --no-inline-config --format jsonover the 7 changed.tsfiles reports 7 files, 0 errors and 0 warnings.eslint.config.mjs, which lints**/*.{ts,tsx,mts,cts,js,…}minusNEVER_LINTED, and all 7 files are in it.parserOptions.project, no typed rules). A text-only edit therefore cannot move the verdict on any file it does not touch.origin/main89f87f2344.git merge-tree --write-tree HEAD origin/mainexits 0, andregistry.tsauto-merges. Main's one new semantic entry is not anengine-*entry.Acceptance notes
surfaceis printed too, and it is outside this card's three fields. The block header⚠ [protocol N] surface → …shows thesurfacetext to the author. By the same instrument, 9 tracker-id sites sit in thesurfaceof 6 entries:authoring-schemas-strict-unknown-keys(1),dataset-measure-aggregate-field-type-refused(2),evaluated-expression-slots-source-required(2),flow-edge-condition-evaluated-slot-source-required(2),plugin-manifest-contributes-routes-retired(1) andui-react-list-view-binding-aliases-retired(1). None is in theengine-family, and the pin already holds the whole printed block,surfaceincluded. This is noted for the card's later stages, not filed.packages/spec/spec-changes.jsonanddocs/protocol-upgrade-guide.mdcarry the same entry text, and their--checkgates are red until regenerated. Both are generator output only.Line budget
Entry files: 112 changed lines (+64 / −48) across 5 files, against ≈400. The whole diff is 471 lines (+346 / −125) in 10 files. Of the rest,
registry.tsis 114, the two projections are 56, the pin is 169 and the changeset is 20.Generated by Claude Code