Repository navigation
fix(spec)!: refuse scale on a currency inline grid column; prefix promises no default symbol (#20045) - #20223
Conversation
… promises no default symbol InlineGridColumnSchema now refuses an authored `scale` on a column that declares `type: 'currency'`, carrying the currency-field refusal's first sentence and remedy (delete the key; the currency's ISO 4217 minor unit decides). `scale` on a number column, and on a column that declares no type, is unchanged. The `prefix` describe no longer promises a yen default: it replaces the resolved currency's symbol and has none. ADR-0087 D3 semantic entry inline-grid-column-currency-scale-refused registered under protocol major 18; registry regenerated. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-authored-by: Claude <noreply@anthropic.com>
…nt in the dropped-refinements ledger The new InlineGridColumnSchema refinement has no JSON Schema form, so the build's dropped-refinement ratchet names it: one new row for data/InlineGridColumn (site at the root) and one `inlineColumns.element` site on each of the twelve published schemas that embed the column, exactly as the gate printed them; header totals moved with the body. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-authored-by: Claude <noreply@anthropic.com>
…'s prefix and scale describes Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-authored-by: Claude <noreply@anthropic.com>
…line column scale refusal Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-authored-by: Claude <noreply@anthropic.com>
…line-currency-column-scale # Conflicts: # packages/spec/dropped-refinements.baseline.json
…line-currency-column-scale # Conflicts: # packages/spec/dropped-refinements.baseline.json
The os-regen merge driver kept the branch's bytes of field.mdx and dropped main's CurrencyConfig.defaultCurrency describe rows; regenerated from the merged sources (gen:schema then gen:docs), which restores main's rows and keeps this branch's inline-column prefix/scale rows. registry.ts was regenerated with gen:migration-registry from the merged entries/ directory and came out byte-identical to the merge. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-authored-by: Claude <noreply@anthropic.com>
…e column scale refusal Per the seat's ruling 5855671317 on #20045: this diff widens nothing and only refuses a shape that parsed. The narrowing arm, the level (minor) and the ADR-0087 disposition are unchanged. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. 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 aa222be880efabd8465cf039476ee80be53d8e82 && git checkout aa222be880efabd8465cf039476ee80be53d8e82
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 805af4f290565955d6e6b56ee46fed45f721d955 4ab1d0cac243ff74c4ec7951e3471c718974fb55 && git checkout -B drift-repro 805af4f290565955d6e6b56ee46fed45f721d955 && git merge --no-ff 4ab1d0cac243ff74c4ec7951e3471c718974fb55
node scripts/docs-audit/affected-docs.mjs --json 805af4f290565955d6e6b56ee46fed45f721d955 |
Contract reviewServed-tier: ① Derived judgmentsDiff read three-dot against objectstack
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Rendered at tier by the director seat (summon #30 续) on the fetched head. Not a governed path; the landing pre-checks are met on this head (fresh PASS, CI green, no conflict), so Generated by Claude Code |
…etire dropped-refinements.baseline.json resolved by the file's own rule (hand-maintained, gated by build-schemas.ts): #20223's inlineColumns.element sites and data/InlineGridColumn root kept, this branch's currencyConfig sites and data/CurrencyConfig root stay removed; header totals recounted from the merged body. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…(step 4 of the os-regen merge) Both sides edited the generated field reference: main's #20223 rewrote the inline grid column prefix/scale rows, this branch removed CurrencyConfig's precision row and narrowed the currencyConfig type signature. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…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>
…laces are its currency's (ADR-0049) (objectstack-ai#20251) Part of objectstack-ai#19992 Clause-②: no Retires `currencyConfig.precision` under ADR-0049 enforce-or-remove, in the direction triage set (`5853208433`: 「**Direction unchanged:** remove, per ADR-0049 with the ADR-0087 retirement entry, under ruling 乙's principle that a currency's decimal places are the currency's. The aliases go with the key.」) and ruling 乙 on objectstack-ai#19910 (`5805782503`: 「a currency's decimal places are the currency's, not a setting」). objectstack-ai#19992 remains open for its folded family site (`5854612946`): the FIELD-level `precision` ("Total digits") that nothing reads. That key is untouched here apart from its form help text; objectui's Studio inspector writes it, so retire-versus-enforce is still an open choice for that card. ## What changes for an author | before (17.4) | after | | --- | --- | | `currencyConfig: { precision: 2, currencyMode: 'fixed', defaultCurrency: 'USD' }` parses | refused at `currencyConfig` as `unrecognized_keys`, with the prescription below | | `currencyConfig.decimals` / `currencyConfig.scale` refused with *Did you mean → `precision`* | refused with the reason, and no rename suggested | | `CurrencyConfigSchema.parse({})` → `{"precision":2,"currencyMode":"dynamic","defaultCurrency":"CNY"}` | → `{"currencyMode":"dynamic","defaultCurrency":"CNY"}` | | authored `precision` contradicting a fixed currency's ISO 4217 digits → `custom` issue at `currencyConfig.precision` | the check is gone with the key | | field designer help text for the field-level `precision`: "Decimal places (e.g., 2 for $10.50)" | "Total digits" (the key's describe; the object designer's row already said so) | ### Refusal texts, verbatim (rendered from `src` at this head) `precision`: ```text Unrecognized key(s) on this currency configuration: `precision`. • `currencyConfig.precision` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer or runtime ever read it: a currency amount's decimal places are its currency's ISO 4217 minor unit (2 for USD, 0 for JPY, 3 for KWD), which every display face derives from the currency itself, so there is no decimal-places setting to declare. Do not move the number to the field-level `precision`: that key is the amount's TOTAL digit count (a DECIMAL(18,2) amount declares `precision: 18`), not its decimal places. Delete the key. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. Until this shape was closed these were dropped silently — the field was still created, minus whatever the key was meant to constrain, protect or compute. ``` `decimals` (and `scale`, identical but for the key name): ```text Unrecognized key(s) on this currency configuration: `decimals`. • `currencyConfig.decimals` is not a currency configuration key, and nothing replaces it: a currency amount's decimal places are its currency's ISO 4217 minor unit (2 for USD, 0 for JPY, 3 for KWD), which every display face derives from the currency itself, so there is no decimal-places setting to declare. Do not move the number to the field-level `precision`: that key is the amount's TOTAL digit count (a DECIMAL(18,2) amount declares `precision: 18`), not its decimal places. Delete the key. Until this shape was closed these were dropped silently — the field was still created, minus whatever the key was meant to constrain, protect or compute. ``` The trailing sentence is the schema's existing `history` (`FIELD_HISTORY`), appended by `strictObject`; it is not new text. ## Premises measured (Zone 2) 1. **No reader, anywhere** — held. Instrument: `git grep currencyConfig` (non-test) plus indirect spellings (a config held in a variable, then `.precision`), with a lit control. - objectstack `packages/**` + `examples/**` + `apps/**` at `4df101c3`: 0 readers of `precision`; the control `currencyConfig?.currencyMode` IS read (`service-analytics/src/plugin.ts:1101`). - objectui at the pin `f8a9d0fb` and at `main` `25c7d584e`: 0 readers. `CurrencyField.tsx:82` derives width as `currencyFractionDigits(currency)` on both; its `:74` comment says it never read the key. Control: `currencyMode` / `defaultCurrency` reads 7 (pin) / 10 (main). - cloud `main` `48d7066`: 0 `currencyConfig` mentions; the instrument fires on that ref (`defaultCurrency` in `connector-stripe`). 2. **Producers.** Authored: three `examples/app-showcase` objects (`account`, `field-zoo`, `semantic-zoo`), now edited. Studio inspector: none (objectui's own `ObjectFieldInspector.currencyScale-10221.test.tsx` pins that it writes the field-level key, never `currencyConfig.precision`). **Stored rows and built artifacts: nearly all of them** — the removed `.overwrite()` baked `precision: 2` into parse output, so every persisted currency config carries it unwritten. That population decides the route: a D2 conversion replayed at rest, not a bare refusal. - What a stored field experiences after upgrade: the stored-row and artifact seams replay `currency-config-precision-removed` (`includeRetired`), strip the key, and serve the row canonical; it then parses. Pinned end to end through `applyConversionsToStoredItem('object', …)` in `currency-precision-iso4217.test.ts` — the row as stored is refused by today's door, the converted row parses, and the field-level `precision: 10` survives. 3. **Aliases** — the schema is a `strictObject`, and its `aliases` are rejection-with-suggestion, not renames. With the target gone they would point at a refusal (and `alias-integrity` audits that). Precedent for a natural spelling with no landing key is `FieldSchema`'s `currency` guidance, so `decimals` / `scale` get `guidance` entries with the same reason and no command (no conversion strips them: the closed shape always refused them, so no stored row carries them). Pinned: code, path, key, text, and the absence of "Did you mean". 4. **`currencyPrecisionContradiction`** lost its only reader, and `currencyFractionDigits` was read only by it: both are removed (neither was exported from a public entry — no barrel row, no `api-surface/` row). `CURRENCY_FRACTION_DIGITS` stays: `shared/value-domain.zod.ts:160` reads its key set for the `iso_4217_currency` value domain. The module docblock now says so and keeps the CLDR provenance. 5. **Docs** — every published sentence that stated the rule is corrected: `data-modeling/field-types.mdx`, `validation-rules.mdx`, `fields.mdx`, `getting-started/common-patterns.mdx` (two `os:check` blocks), `protocol/objectql/types.mdx`, the published skill `skills/objectstack-data/rules/field-types.md`, and the generated references (`references/data/field.mdx`, `object.mdx`, `system/migration.mdx`, `shared/value-domain.mdx`). ## The retirement kit - Schema: `precision` deleted from `CurrencyConfigSchema`; `CURRENCY_CONFIG_DECIMAL_PLACES_GUIDANCE` carries the three prescriptions; the `.superRefine` and `.overwrite` are removed. Stale comments that named the twin (`FieldSchema.precision`, the objectstack-ai#20011 note, the `.overwrite` precedent notes) now say it is gone. - ADR-0087: D2 conversion `currency-config-precision-removed` (toMajor 18, `retiredFromLoadPath`, strips the key under every field's `currencyConfig` on `objects` and `objectExtensions`, 2-notice fixture), wired into step 18's `conversionIds` with the rationale extended; `RETIRED_KEYS_BY_MAJOR[18]` gains `data/CurrencyConfig:precision` (one entry file, regenerated). - Generated / ledgers: `authorable-surface/data.json` row removed deliberately (gate (a) tripwire; gate (c) proof 4 — guidance route — then passes); `authorable-defaults/data.json` loses `CurrencyConfig:precision = 2` (regenerated); `dropped-refinements.baseline.json` loses the 13 sites the removed `.superRefine` produced (`data/CurrencyConfig` itself plus 12 embeddings) — the refinement was removed with its key, the projection did not learn anything. - Form + i18n: `field.form.ts` help text; the `en` bundle regenerated, `zh-CN` / `ja-JP` / `es-ES` set to the object designer row's existing translations (总位数 / 総桁数 / Total de dígitos). - `spec-changes.json` / the upgrade guide do not move: neither carries any protocol-18 entry yet (the precedent `metric-filters-removed` is absent too), and `check:spec-changes` / `check:upgrade-guide` read up to date. - Changeset: `@objectstack/spec` minor + `@objectstack/platform-objects` patch, `**BREAKING**`, FROM → TO, and `adr-0087: registered currency-config-precision-removed` (`check:adr-0087-registration` green). ## Absence half — no tree-scoped text pin, on purpose `precision` does not leave the tree: it stays the field-level total-digit count on every numeric field. What is retired is a key in a position (`fields.NAME.currencyConfig.precision`), which a grep either matches everywhere or, scoped down, only where its author already knew to look. The `dashboard-chart-structure-refusal.test.ts` precedent covers this shape; the rationale block is in the test file. Standing in its place: `tsc` (the key is off `CurrencyConfig`'s input type — Leg A below) and the closed parse door (Leg B below). ## Proofs (one-off, nothing left behind) - **Leg A — reverse verification against the rebuilt `.d.ts`.** `node scripts/ablation-replace.mjs` put `precision: 2` back into `account.object.ts` (anchor 1 → 0, blob `58a9af7e` → `700705a8`), then `tsc --noEmit` on the showcase: exit 1, `account.object.ts(71,25): error TS2353: Object literal may only specify known properties, and 'precision' does not exist in type '{ currencyMode?: …`. Restored: blob == HEAD, `git diff HEAD` empty. Control (committed tree): showcase `typecheck` exit 0. - **Leg B — ablation of the refusal.** Re-declared `precision` on the schema (blob `7d36200c` → `86fdbf2d`), then the two pin files: **9 failed | 279 passed** — every retirement pin went red; the alias pins stayed green (they pin the guidance, which the ablation leaves in place) and so did the parse-output and conversion pins (independent of the key's declaration). Restored: blob == HEAD, `git diff HEAD` empty; tree clean after both legs (0 bytes of `git diff HEAD --stat` + `git status --porcelain`). Direction observed: red, as expected. ## Verification (head `36819398`; patch round on `71ea994d` below) - `pnpm --filter @objectstack/spec build` exit 0; `check:generated` exit 0 (15 of 15 up to date, after the one stale `check:docs` was regenerated with `--fix`). - `pnpm --filter @objectstack/spec typecheck` exit 0 (includes `check:test-typecheck`, so the `@ts-expect-error` in the pin file is a used directive). - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`: **546 files passed, 16062 tests passed, 2 todo**. - Consumer suites (contract-face fixture triage): `@objectstack/example-showcase` typecheck exit 0 (after its dependency closure built); `@objectstack/platform-objects` typecheck exit 0 and tests **55 files / 911 passed**; `@objectstack/service-analytics` `currency-mode-relay.test.ts` + `query-dataset.test.ts` **51 passed**; `pnpm check:i18n` exit 0 (9 packages in sync, after the gate's own prerequisite closure). - Gate union: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived **120** commands on this head; all run, exit codes on disk; `--ran` reconciles **119 run, 1 NOT-MEASURED, 0 UNRUN**. - `check:skill-examples` (covers the two edited `os:check` blocks): first refused on its prerequisite (exit 3, `client-react` unbuilt); after `pnpm --filter '@objectstack/client-react...' build` it exit 0 — "259 prose examples type-check across 3 surface(s)", 109 files. - NOT MEASURED: `check:dual-build-cjs-loads` — reason: its prerequisite is every package's `dist/` (a full `pnpm build`), and this diff changes no package entry or `exports`. - Not run locally: repo-wide `pnpm lint` (CI's run). - Console Pin Gate premise: objectui at the pin `f8a9d0fb` and at `main` imports neither removed helper nor `CurrencyConfigParsed`, and writes no `currencyConfig` literal carrying `precision` (multi-line scan over every file mentioning `currencyConfig`: 0 hits; control `currencyConfig: { … defaultCurrency … }` literals: 5 files at the pin, 13 at `main`). - The showcase `^...` closure build, the i18n prerequisite closure and every heavy run went through `scripts/pm/os-verify-lock.sh`. - Merged `origin/main` (`3cb84d08`) with `scripts/pm/os-regen-merge.sh`: conflicts stacked both sides (objectstack-ai#20217's `defaultCurrency` describe kept beside this removal; objectstack-ai#20085's `view-item-owner-hidden-removed` kept beside the new conversion, in both registries); every sibling entry measured present after the merge, same counts as `origin/main`. - **Patch round (merge conflict with objectstack-ai#20223), final head `71ea994d`.** - Merged `origin/main` twice with `scripts/pm/os-regen-merge.sh`: `0d3ec471` (merge `1e6f29b2`, regeneration `0b2e3964`), then `e4621867` (merge `71ea994d`). No rebase, no force-push. - Both conflicts were in `packages/spec/dropped-refinements.baseline.json`, which is hand-maintained by rule (no `gen:` script; `build-schemas.ts` gates it). Both sides are stacked: objectstack-ai#20223's 12 `inlineColumns.element` sites and `data/InlineGridColumn` root are kept, this PR's 12 `currencyConfig` sites and `data/CurrencyConfig` root stay removed, and objectstack-ai#20205's three automation entries are kept. The header totals were recounted to 210 schemas / 591 sites, and `gen:schema` accepts the ledger (exit 0). - Interaction with objectstack-ai#20223: no contradiction. Its field/column currency-scale refusals name neither `currencyConfig` nor `precision`, and both sides prescribe 「delete the key; decimal places are the currency's ISO 4217 minor unit」. - Readings on `71ea994d` (locked): - spec build exit 0; `check:generated` exit 0 (15/15); spec typecheck exit 0; - touched suites 6 files / 350 passed; full spec `--project local` **549 files / 16138 passed, 2 todo**; - `check-adr-0087-registration`, `check-changeset-no-major` and `check:migration-registry` (265 semantic / 215 retired-key / 199 retired-def) all exit 0. - The derived gate list is byte-identical to the one reconciled at `36819398`. - CI on `71ea994d`: 35 check runs, 33 success, 2 skipped, 0 failing. ## Scope notes for the reviewer - **Tier H.** `skills/objectstack-data/rules/field-types.md` is on this diff because it taught the retired key in its field-type table and code sample; leaving it would ship a published skill that teaches a refused key. The edit is a pure removal: file 328 → 327 lines; `objectstack-data` markdown 3717 → 3716; all `SKILL.md` files 4402 → 4402. It makes this PR Tier H. - Outside the claim's listed surface, each because this change would otherwise leave it false: the `value-domain.zod.ts` docblock (cited the removed function), one comment in `conversions/registry.ts` (named the removed key's bounds), the three showcase objects, `fields.mdx` / `common-patterns.mdx` / `types.mdx`, the platform-objects translation bundles, and the generated rows above. - `packages/spec/liveness/field.json` is **not** edited. The `currencyConfig` row covers the container, asserts nothing about `precision`, and stays `live` (`defaultCurrency` is read). The container is in `undrilled-containers.baseline.json`, so no per-key row existed to remove. objectstack-ai#20217 had just re-cited that row. ## Acceptance notes - `docs/qa/platform-checklist/areas/records-forms.json` item `records-forms.field-type-constraints` still describes `f_currency` as `scale 2 currencyConfig{precision 2}`. Both halves are stale now: the `scale` half since an earlier card retired it from currency, the `precision` half since this one. Its SCALE gap probe targets a key the currency type refuses. Internal QA prose, not published. carrier: the next checklist-author sweep; 承接者:无. - Standalone `field` metadata rows are outside every field conversion's reach (the stored-row seam has no `fields` stack collection), this one included, the same as its precedents. Observation only; no producer of such rows was measured. - Two semantic ledger entries of earlier protocol-18 migrations (`18.field-scale-precision-integer-refused`, `18.ui-form-field-precision-scale-integer-refused`) say `CurrencyConfigSchema.precision` "is a different surface". That is a scope remark about those migrations, historically accurate, and left as recorded. ## 维护者速读(草稿) **改了什么**:货币字段配置里的 `currencyConfig.precision`(「小数位」)被删除。现在写这个键会在保存/发布时被拒绝,并提示直接删掉;它的两个近义写法 `decimals`、`scale` 也给出同样的说明。已经存进数据库或打进构建产物里的旧配置,读取时会自动去掉这个键,不会报错。字段设计器里「精度」一栏的提示文字从「小数位数」改成「总位数」。 **为什么改**:这个键从来没有任何界面或运行时读取过——金额显示几位小数一直由币种本身决定(美元 2 位、日元 0 位、科威特第纳尔 3 位)。作者(包括 AI)以为设了小数位,实际什么都没发生。裁定乙已定原则「币种的小数位属于币种,不是一个设置项」,分诊定了删除方向。 **风险与代价(含回滚)**:这是破坏性收窄:仍在源码里写这个键的应用,升级后会在发布时报错,需要删掉这个键(`os migrate meta --from 17` 会列出要改的地方)。已存数据不受影响(读取时自动转换)。回滚:revert 本 PR 即可,已存数据没有被改写。本 PR 改到了对外发布的技能包 `skills/`,因此属于 Tier H,需要您批准才能合并。 **席位意见**: **你要做的**:审阅后在本 PR 上给出批准(APPROVED review),或告诉席位把 `skills/` 的改动拆成单独的 PR。 --- _Generated by [Claude Code](https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20045
Clause-②: no (narrowing)
Clause-②: no (narrowing)per the seat's ruling5855671317on #20045: this diff widens nothing and only refuses a shape that parsed; the claim'syesis corrected there.Branch
claude/issue-20045-inline-currency-column-scale, base1c8b320a89. It is merged withorigin/maintwice throughscripts/pm/os-regen-merge.sh: firstb09ce67870as4a6e8ed61a, then, in rework round 1,805af4f290as9f01270dd2. The regeneration is its own commit,df6b175e58. Head is4ab1d0cac2. Every reading below was taken at head4ab1d0cac2unless it names another commit.What changes
Triage direction: comment
5825687357, option A. Triage read the card as inherited from ruling B5791803339on #19629 (scaleretired from the currency field) and ruling 乙5805782503on #19910 (「a currency's ISO 4217 minor unit decides its display」).InlineGridColumnSchema(packages/spec/src/data/field.zod.ts) now refuses an authoredscaleon a column that declarestype: 'currency'. The refusal is one custom issue at the column'sscale, and it applies to any value,scale: 0included, computed or not. The message carries ruling B's shape:scaleis not valid on acurrencyinline grid column — delete the key." This is the field refusal's first sentence with only the subject swapped;scaleon anumbercolumn, and on a column that declares notype, is unchanged.prefixdescribe no longer says 「(default '¥')」. It now reads: the symbol replaces the resolved currency's own symbol; there is no default; it replaces the symbol only.prefixis still accepted. Thescaledescribe drops "numeric/currency" and names the currency refusal.18.inline-grid-column-currency-scale-refusedis added, andregistry.tswas regenerated withgen:migration-registry.@objectstack/specminor, with a BREAKING banner, a FROM → TO table and theregistereddisposition marker.dropped-refinements.baseline.jsongains the new refinement's rows, exactly as the build's ratchet printed them:data/InlineGridColumn(root) plus one…inlineColumns.elementsite on each of the 12 published schemas that embed the column. The header totals moved with the body: 208 schemas, 590 sites after the merge.content/docs/references/data/field.mdxwas regenerated (check:generated --fix, which proved onlycheck:docsstale).ADR-0087 disposition: why D3 only
5615360777, restated5634031140) says 「D3 每家族一条,D2 可无损表达者亦然;D2 只承载数据修复」.FieldSchema.scaleon acurrencyfield is offered to authors by the field designer, ignored by every display face, and still enforced on writes — half-live in the direction that surprises #19629, which says no alias and no grace window. Its own entry,field-currency-scale-refused, is "NOT mechanically converted, deliberately". A load-path conversion that dropped the key would accept it on every load, which is the grace window the ruling refused. This entry follows that precedent.retired-keys/row. The key is not removed. It stays in the walked shape and stays live onnumbercolumns, and aRETIRED_KEYS_BY_MAJORrow naming a live key reds gate (b2).Premise, measured
1c8b320a89,InlineGridColumnSchemawas a barestrictObjectwith no refinement, andscalewas declared for a "computed numeric/currency result".scaleshape parses again.origin/main25c7d58,GridField.tsxhasfunction currencyAdornment×1 andfunction currencyWidth×1, andc.prefix || '¥'×0;.objectui-shapinf8a9d0fb0596, it hasc.prefix || '¥'×2 and thecurrency ? 2default ×1 (see Acceptance notes).Mechanism hypothesis H1, measured
InlineGridColumnSchemawas at:895, astrictObject, withprefixat:924andscaleat:938. Confirmed.FieldSchema'ssuperRefine(:2295–2307on base). That block is outside this claim's file surface. So the column carries the sentence and remedy in its own constant,INLINE_GRID_CURRENCY_SCALE_REFUSAL.currencyConfigorprecision.FieldSchema's currency-precision anchor was not touched. It changed onmainduring this run (finding(spec): the #7918 anchor reads a currency field'sprecisionas its display width, refusingprecision: 18on a fixed-USD field though the key is "Total digits" and ruling 乙 gives decimals to the currency #20011, PR fix(spec): a currency field's precision is total digits — stop refusing precision: 18 on a fixed-USD field (#20011) #20209). It merged cleanly and is disjoint from this diff.Authored instances (H2), census on
1c8b320a89git grep inlineColumnsover the whole tree gives 30 files. Exactly one authored block:examples/app-showcase/src/data/objects/invoice.object.ts, with 7 identity-only{ name }columns, 0 declaringtypeand 0 declaringscale. This is the control: the same instrument counts 7 column entries in that block.skills/**,content/docs/**prose and examples, and JSON/YAML fixtures: 0 inline grid columns.inlineColumnsfixtures. Its only hits are the two renderer files.scale: 2on a currency column (inline-related-columns.test.ts, the "every renderer-read key" case). It was re-judged:scalewas deleted there, and the key stays covered by the identity-only computedamountcolumn in the same fixture.Tests, at head
4ab1d0cac2All heavy runs went through
scripts/pm/os-verify-lock.shunder one stable slot, and each verdict below is read from its VERDICT line.@objectstack/specbuild: exit 0.check:generated: "All 15 generated artifacts are up to date", including✓ check:migration-registry.registry.tswas regenerated withgen:migration-registryfrom the mergedentries/directory. The output is byte-identical to the merge. Quoted-exactgit grep -c -Fon4ab1d0cac2, 1 hit each:'inline-grid-column-currency-scale-refused'(this PR);'dataset-filter-nested-relation-equality-array-refused-at-save'(fix(spec)!: a dataset or measure filter with a list inside a nested relation is refused on save (#20080) #20207), plus two of fix(spec)!: a dataset or measure filter with a list inside a nested relation is refused on save (#20080) #20207's rewritten sibling-entry lines;'view-overlay-options-bag-judged'(fix(spec)!: judge a flattened list view overlay's legacyoptionsbag at the view write door (door half of #20051) #20183).check-adr-0087-registration --base origin/main: exit 0, "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [BREAKING+bang+clause-②-narrowing] registered inline-grid-column-currency-scale-refused".check-changeset-no-major --base origin/main --eventwith THIS body: exit 0, "✓ LEVEL AXIS: this PR declares clause-②no (narrowing)".inline-related-columns,field-currency-scale-refused,validation-boundary-description,authoring-key-lint): 5 files / 71 passed.vitest --project local: 545 files passed; 16045 passed, 2 todo.vitest --project repo: 32 files / 586 passed.typecheck: exit 0 (check:test-typecheck: OK — 53 file(s) / 255 error(s) / 142 pinned signature(s) held).inline-grid-column-currency-scale-refused.test.tscovers:FieldSchema,ObjectSchemaand the standalone column, located at…inlineColumns[i].scale, withcode: 'custom';numbercolumn, an untyped column,prefixaccepted, and the remedy parsing and re-parsing unchanged;Ablation, from committed state
aa4b6ca14f, run withscripts/ablation-replace.mjs:if (column.type === 'currency' && column.scale !== undefined) {was hit ×1 and went ×1 → ×0. The replacementif (false && …went ×0 → ×1. The blob changed from147220988d04toddf1ebbf0437.147220988d04== HEAD, andgit diff HEADis empty.src(./field.zod), so no dist rebuild was involved.Gates.
dispatch-gates.mjs --commandswas re-derived on4ab1d0cac2and gave 111 families, the same list as on4a6e8ed61a. All 111 were re-run on4ab1d0cac2.--ranreconciliation: "111 derived famil(ies) accounted for — 109 run, 2 NOT-MEASURED".NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET. Packages have nodist/, and only a full-repo build satisfies that. This diff changes no entry point, export map or build config.NOT MEASURED: check:type-check-debt, reason: PREREQUISITE NOT MET (@objectstack/driver-tursohas no built type entry point in this worktree). Its--re-measureotherwise starts a workspace-closure build outside the verify lock. This diff changes no exported type (check:api-surfaceis green) and touches none of the four DEBT-ledgered packages.Rework round 1, diff
4a6e8ed61a→4ab1d0cac2(107 files, +6088/−682):b09ce67870→805af4f290):805af4f290;field.zod.ts2 lines,registry.ts224,field.mdx4,dropped-refinements.baseline.jsonmain's rows).registry.ts, bygen:migration-registry, byte-identical to the merge;field.mdx, bygen:schema && gen:docs(df6b175e58). The os-regen driver had kept the branch side and dropped main's twodefaultCurrencyrows, and the regeneration restores them;Clause-②: yes (narrowing)→Clause-②: no (narrowing). The PR body'sClause-②line moved in this same edit.Deviations
packages/spec/dropped-refinements.baseline.jsonis outside the claim's listed surface. The build refuses a new refinement without it. The file is hand-edited on purpose and has no generator, and its rows were applied exactly as the gate printed them, including the merge-time header recount. The claim's surface names "its own refinement", so this is reported as that refinement's mechanical consequence, ⛔ not as scope growth.Acceptance notes
typeis judged. A column that declares notypegets its type from the child field when objectui hydrates it (deriveMasterDetail.tshydrateColumns, which spreads the authored column). So{ name: 'amount', scale: 2 }over a currency child field still parses. The column schema cannot see the child object. Once the objectui follow-up stops reading an authoredscaleon currency columns, that key is ignored on such a column. That lands on the objectui follow-up card (triage note 4), not here.prefixdescribe ("No default") is true of objectuimain(25c7d58). The console bundled at.objectui-shaf8a9d0fb0596still falls back to¥and rounds a computed currency with noscaleto 2. The describe becomes true of the bundled console when the pin moves past objectui PRobjectstack initscaffolds a project whosepnpm installexits 1 on pnpm 11 — its rendered pnpm-workspace.yaml omitsallowBuilds#10405. Deletingscaleloses no decimals under either console: the pin falls back to 2, andmainto the ISO minor unit. So no zero-decimal window opens.packages/spec/liveness/field.json→inlineColumns.children.prefix/.scalenotes still citec.prefix || '¥'andc.scale ?? currency default 2;packages/spec/src/data/field-scale.tsdocblock sentence "The one2that looks like a currency default belongs to an inline grid COLUMN's rounding" describes the pre-objectui#10355 grid.GridFieldfollow-up (drop the authored-scaleread on currency columns) is objectui's own card, per triage note 4. Nothing in objectui changes here.Generated by Claude Code