Skip to content

fix(spec): one D3 entry per major-18 retirement family — the census and the 25 missing entries (#20201) - #20255

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-20201-d3-entry-per-family
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-20201-d3-entry-per-family

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20201
Clause-②: no

What

Ruling B on #17152 (director 5615360777, restated 5634031140, on the maintainer's #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 #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 #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: #10329, #10926, #12868 and #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 #19123). Every sha is an ancestor of origin/main:

entry was now anchored to
mapping-lookup-params-retired #10329 commit 15d58dbf1 (the import path never read the four lookup steering params)
translation-component-submit-label-retired #10926 commit d173125fb (the copy key left with its only declarer, element:form)
form-view-option-default-retired #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 #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

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)


Generated by Claude Code

…slessness (#20201)

Ruling B (ADR-0087 D3): every retirement family carries one D3 entry, even
when a lossless D2 conversion repairs its data. Corrects the step-17
docblock's stale 'semantic list is deliberately empty' fact, step 18's
tenancy.organizationField rationale sentence, and the single-entry sites in
conversions/registry.ts, the cacheTtl retired key, the metadata-endpoints
entry comment and the migration-registry generator docblock.

Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
Co-authored-by: Claude <noreply@anthropic.com>
…issing one

WIP toward #20201's census: one D3 entry per D2-backed retirement family
that had none (ruling B, ADR-0087 D3).

Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
Co-authored-by: Claude <noreply@anthropic.com>
Completes one D3 entry per D2-backed major-18 retirement family (25 new
entries), and pins in the registry's own test that from protocol 18 on
every graduated conversion is named by a D3 entry of its step.

Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Sep 27, 2026
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 2 changed file(s) yielded no anchor (packages/spec/src/migrations/entries/retired-keys/18.api__ApiEndpoint__cacheTtl.ts, packages/spec/src/migrations/entries/semantic/18.metadata-endpoints-switch-radius-repartitioned.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/src/migrations/entries/retired-keys/18.api__ApiEndpoint__cacheTtl.ts, packages/spec/src/migrations/entries/semantic/18.metadata-endpoints-switch-radius-repartitioned.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 34 pages)
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3f86dc52f22668dd92de00f604148e04d3d048da → packageMentionDocs.

Which tree this was computed on

This run read content/docs from bdaa8e7d90060567fc2e2bae285462fcaf5acde0 — the merge of head a930cacea56720b2ef6a2b88729be3aca662e85a into base 3f86dc52f22668dd92de00f604148e04d3d048da, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin bdaa8e7d90060567fc2e2bae285462fcaf5acde0 && git checkout bdaa8e7d90060567fc2e2bae285462fcaf5acde0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3f86dc52f22668dd92de00f604148e04d3d048da a930cacea56720b2ef6a2b88729be3aca662e85a && git checkout -B drift-repro 3f86dc52f22668dd92de00f604148e04d3d048da && git merge --no-ff a930cacea56720b2ef6a2b88729be3aca662e85a

node scripts/docs-audit/affected-docs.mjs --json 3f86dc52f22668dd92de00f604148e04d3d048da

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3197fce2947c5aa3ea39b4675202ae366d9b0700

Read: card #20201 and comments 5854110917 / 5854456343 / 5854164872 / 5855991609 / 5857585649, ruling B 5615360777 / 5634031140 / 5559778263, PR #20255 object, body, 32 files, 6 commits, full diff vs merge base 08c8484a1, the 25 new entries, the two edited entries, both registries, build-migration-registry.ts, migrations.test.ts, walk.ts, types.ts, the changeset gate headers, check-issue-citations.mjs, check-required-contexts.mjs, lint.yml, PR #20238 files. Ran (scratchpad only, no builds, no gate families): a whole-id census over git archive snapshots at merge base and head; a Table 2 pairing check against the 331-record population; the pin's two helper functions lifted verbatim and run under Node with one entry ablated in a copy; a generated-region mirror check; a step-17 census; REST resolution of every bare issue number the diff adds; check-run polling to convergence. NOT MEASURED: the failed CI job's log text (its blob host is refused by the proxy; the cause is reconstructed from the gate's exit-code table and my own REST resolution); PR #20230 (Not Found); vitest itself; check:generated in CI by name.

① Derived judgments

(a) Census: re-derived at merge base 08c8484a1 (188 semantic/18.* files, 185 retired-keys, 146 retired-defs, 36 step-18 conversionIds). The whole-id matcher leaves 24 conversions unnamed; metric-filters-removed is named only in passing by 18.analytics-authorable-unknown-keys-refused.ts (a bookkeeping note at lines 7-11 and 30, not that family's entry), so 25 missing, matching Table 1 row for row. The 11 pre-existing pairs are each the family's own entry (each names the conversion as its paired mechanical half). At head: 0 of 36 unnamed. No family missed, none double-counted. D2-less records: population 331 = 58 Table 1 records + 174 explicit Table 2 records + the four aggregate rows (62 cloud/, 13 export-job, 13 metadata-customization, 11 rest-server-config), disjoint, exactly covering the population; all 68 Table 2 rows checked mechanically (the entry text names each record), the six the matcher could not name (contributes.routes, the five theme sub-blocks) confirmed by reading. One table defect: Table 1 row 16 lists integration/Connector:connectionTimeoutMs under permission-allow-restore-purge-removed; that record's own comment names connector-connection-timeout-ms-removed as its conversion (the permission id appears only as a comparison) and the existing connector-provider-context-connection-timeout-ms-retired entry names it, so it belongs to row 20. The new permission entry's comment says "the four registered keys", so the code is right and only the body table is wrong.

(b) Entries: all 25 read. Each names its family and D2 id, states what D2 repairs, states a judgment the consumer still owes, carries the five SemanticMigration fields (id, surface, replacement, reason, acceptanceCriteria) with the generator's id: '…' line, and appears verbatim in the generated region. Read against apply for 16: the seven duration renames (renameKey leaves a canonical timeoutMs alone and returns null on a disagreeing pair, walk.ts:654-668; turso gated on resolveDriverId; both memory persistence arms; one notice per connector key), field-reference-to-alias (both spellings with different targets left alone, fixture parent_id), field-column-lists-canonicalized (field+name left alone, no-identity object left alone, decoration dropped on fold), object-grid-default-sort-removed (renderer precedence), list-view-sort-string-clause-to-array (leading-minus left alone; objects[].listViews walked by no conversion), view-page-mount-removed (three spellings = config/list/listViews), permission-allow-restore-purge-removed (true stripped; the 17.x false residue is documented at permission.zod.ts:527-599), view-item-owner-hidden-removed (views plus ASSEMBLED_VIEW_ITEMS_KEY; the flattened overlay untouched), tenancy, metric-filters. Cited schema facts hold: dashboard rename hints (dashboard.zod.ts:1426), rateLimit.windowMs (contract.zod.ts:561), memory min(100).default(2000) (memory.zod.ts:121), ledger dead rows (liveness/connector.json:136, 398). No placeholder and no false claim about the conversions. Defect: four entries cite issue numbers that return 404 on the board: #10329 (mapping-lookup-params-retired), #10926 (translation-component-submit-label-retired), #12868 (form-view-option-default-retired), #14676 (connector-error-mapping-retired). They are carried over from the conversions' pre-existing docblocks (base conversions/registry.ts:8038, :7275, :8723, :9030), but here they are newly added lines, in the entry files and again in migrations/registry.ts through regeneration. Comment-only; the D3 text an author is shown is unaffected.

(c) Prose: the step-17 docblock is true (the semantic:17 region holds 77 entries; action-execute-to-target, field-conditionalRequired-to-requiredWhen and agent-knowledge-removed are named by no 17.* entry). The step-18 rationale names object-tenancy-organization-field-retired, which exists. conversions/registry.ts: the mongo conversion is toMajor: 17 and unnamed by any 17.* entry, so "it has none" is true; :9312 names api-endpoint-cache-ttl-unit-in-key and :9804 names list-view-sort-string-clause-retired, both present. The cacheTtl retired key names its entry and the regenerated retired-key:18 region mirrors it. The metadata-endpoints comment is true: no conversion for that family exists. build-migration-registry.ts:276 is true as history: at the last commit carrying step 14 (f20fe298a^) it had semantic: [] and an empty semantic:14 region. Step 17 gained no entry (77 files before and after; no 17.* file in the diff). registry.ts:108 is untouched and still says the sharing-rule full conversion "leaves no semantic residue"; sharing-rule-access-level-full-to-edit is named only in passing by 17.sharing-rule-recipient-reconcile.ts. It is released text in a major triage scoped out of backfill and outside the claimed surface: non-blocking.

(d) Pin: it lives in the existing migrations.test.ts › registry integrity, no new script. It is real: the test's semanticEntrySources and entriesNaming lifted verbatim and run under Node over the head snapshot give 36 pairs and an empty unnamed; with 18.object-tenancy-organization-field-retired.ts removed from a copy the result is exactly ["protocol 18: object-tenancy-organization-field-removed"], and removing the analytics entry as well changes nothing because the family now has its own entry. The control test asserts a known pair and the prefix refusal. Scope (MIGRATION_MAJORS at or above 18) is the intended enforcement of ruling B's D2-backed half, with the two blind spots (ownership, D2-less families) stated in the test. PR #20238 at its current head 4aecf01d2f adds 18.ui-report-joined-chart-retired.ts naming report-joined-chart-removed as a whole id, so neither landing order goes red any more; #20230 does not exist. The failure names the protocol and the conversion id; what to add is carried by the test title and its comment, not by an assertion message: actionable, could be tighter.

(e) Generated regions: every non-header line of the 25 entries and of the edited cacheTtl comment appears verbatim (four spaces deeper) in its region; the semantic:18 region holds 213 ids for 213 files; the only hunks outside markers are the docblock (:75-86) and the step-18 rationale (:5253). CI step "Migration registry matches its entry files" is green at this head. check:generated is not a named CI step in this job; the dev reports it green locally (NOT MEASURED here).

② Semver level

Clause-②: no holds: ledger entries and prose, no accept set moves, no conversion changes. The changeset is @objectstack/spec: patch. AGENTS.md § Post-Task Checklist 3 says a bug fix in a released package takes patch; check-changeset-no-major's level axis binds only when Clause ② is declared yes, and check-adr-0087-registration only on a breaking changeset. The D3 text that os migrate meta prints (packages/cli/src/commands/migrate/meta.ts:525) is additive. patch is what the gates' stated rules want.

③ Boundary flags

Blocking: the required context Lint & Repo Gates is red at this head, at step 189 "Issue citations this change adds resolve on the board" (node scripts/check-issue-citations.mjs, exit code 2 = its findings path, line 870). Four bare citations this PR adds do not resolve on the board (HTTP 404): #10329, #10926, #12868, #14676, in the four entries named in ①(b) and again in migrations/registry.ts through regeneration; the same step is green on main at the PR's base e46218674. The dev report's pnpm check:issue-citations :: exit 0 is the package script, which is --self-test only; CI also runs the judging pass. Remedy: remove or requalify the four numbers in the four entry files, regenerate the region, push.

Non-blocking: Table 1 row 16 misplaces integration/Connector:connectionTimeoutMs (belongs to row 20; body-only). registry.ts:108 keeps the pre-ruling sentence for the sharing-rule full family (released step, backfill scoped out). Step 17: 53 of 57 graduated conversions unnamed, reproduced; the pin starts at 18 by design. The pin's failure carries no assertion message naming what to add. The #20238 interaction is already resolved at that PR's current head.

CI at this head: 34 check-runs after convergence: 31 success, 3 skipped, 1 failure (Lint & Repo Gates, a pinned required context; steps 11 "Migration registry matches its entry files" and 12 "ESLint" green, step 189 failed, exit code 2). Closing keywords: the body carries exactly Fixes #20201.

Implemented-by: claude/issue-20201-d3-entry-per-family
Reviewed-by: session_01CiCTczDo7tGhafXjf61dUJ

VERDICT: FAIL

…mmits; the census pin says what to add

Four new D3 entries carried issue numbers that no longer resolve on the
board (deleted, not transferred). Each now names the commit in this
repository's history that retired its family, and says what that commit
decided. The pin's assertion message names the unnamed conversions and
the remedy; its logic and scope are unchanged.

Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a930cacea56720b2ef6a2b88729be3aca662e85a

Delta from 3197fce29 (FAIL 5857834457): three commits, 5c60be7ee (four comments re-anchored, pin message), 21418c4d2 and a930cacea (merges of main), plus a rewritten body; merge base moved 08c8484a1 to 3f86dc52f. Read: the PR object twice (head unchanged), the 9-commit list, the 32-file list, the full body, the fix commit's diff, the four cited commits (subjects, changesets, conversion diffs), 66e266c93, AGENTS.md's citation rule, check-issue-citations.mjs and its sibling scripts, check-required-contexts.mjs, the test file at the head, build-migration-registry.ts, the view-item entry, flattenedViewOverlayFields on main, the sibling PRs' migrations and conversions diffs. Ran (scratchpad only): a normalised interdiff of the two PR patches; merge-tree --write-tree for both merges and for the head against pinned main 6a6a17b62, #20244, #20286 (at both its heads) and #20238; the pin's two helpers lifted verbatim under Node over the head snapshot and each union tree, with one entry ablated; a generator re-implementation mirroring all six regions at the head and the unions; the citation self-test and judging pass in a detached worktree at the head; REST check-run polling (converged); REST resolution of #17152. NOT MEASURED: vitest itself (CI Test Core green); the three-way tree main plus head plus #20286 (argued by composition of disjoint additions); the exact origin/main sha CI step 189 read at 18:48 (my run used the same merge base 3f86dc52f).

① Derived judgments

(a) The four re-anchored citations. Every named sha is an ancestor of origin/main at both 733822cf8 and the pinned 6a6a17b62, and each commit's conversions diff adds exactly one id: line, the family's conversion. 15d58dbf1 (2026-08-23, "retire the import mapping lookup transform's steering params (#10329, ADR-0049) (#11280)") introduces mapping-lookup-params-removed; its changeset says the four params keys existed only to steer lookup and "the import path read none of them", and the conversion strips them: the sentence is true. d173125fb (2026-08-23, "retire the component-translation submitLabel copy key (#10926, ADR-0049) (#11438)") introduces translation-component-submit-label-removed; its changeset says submitLabel's only declarer was element:form, retired whole by #9249, and "the maintainer ruled retire over re-anchor" because object-form speaks submitText: true. c459da6bc (2026-08-28, "narrow the per-option default key out of the form-view options vocabulary — the object-field face keeps it enforced (#13033)") introduces form-view-option-default-removed; its changeset records the maintainer-ruled disposition 甲 narrowing the key out of the form-view face only with object-field options "ENFORCED and UNTOUCHED", and its diff cites the objectui#6263 analysis seven times: true. 13c48c2a5 (2026-09-04, "retire connector.errorMapping — eleven inert authorable keys, one spelled like the live userMessage channel (#14676, ADR-0049) (#15299)") introduces connector-error-mapping-removed; the sentence is the subject: true. Form: each comment reads "landed in commit sha: what it decided", the 66e266c93 form; AGENTS.md line 13-16 wants the ruling record or else "the commit sha in this repository's history", a PR number only as a convenience link. No PR number is used as a citation, #17152 (the ruling record) stays and answers 200, objectui#6263 is a declared cross-repo reference the gate never judges, and no dead number remains. Judging pass, in a detached worktree at a930cacea: node scripts/check-issue-citations.mjs --self-test (what pnpm check:issue-citations runs) exit 0, 73 cases in 7 batteries; node scripts/check-issue-citations.mjs (base: merge-base with origin/main = 3f86dc52f) exit 0: 112 citations across 29 files, 106 resolve, 6 cross-repo-unjudged, 0 findings, matching the body's numbers. CI step 189 "Issue citations this change adds resolve on the board" is success at the head (job 108683864072).

(b) The pin's assertion message. git diff 3197fce29 a930cacea -- migrations.test.ts is one hunk: expect(unnamed).toEqual([]) becomes expect(unnamed, message).toEqual([]). The message carries the joined protocol N: conversion-id list and the remedy (a D3 semantic entry of that step, a file under entries/semantic/ prefixed with its major, then gen:migration-registry, naming the id as a whole word and saying the judgment owed). D3_PER_FAMILY_FROM_MAJOR, the MIGRATION_MAJORS filter, the anti-vacuity check, semanticEntrySources, entriesNaming's whole-id regex, the pairs counter and the control test are byte-identical: logic and scope unchanged. Fired: the two helpers lifted verbatim under Node over the head snapshot give 36 pairs, unnamed empty; with 18.object-tenancy-organization-field-retired.ts removed from a copy, unnamed is exactly ["protocol 18: object-tenancy-organization-field-removed"] and the message renders "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 …" (exit 1).

(c) Merges and regions. 21418c4d2 has parents 5c60be7ee and 1207baf01 (an ancestor of main); git merge-tree --write-tree of the parents is 13f3440f6…, equal to the merge commit's tree; under migrations/ it brought 18.cel-predicate-one-value-comparand-refused.ts, 18.filter-query-face-comparands-refused-at-save.ts and registry.ts, as the body says. a930cacea has parents 21418c4d2 and 3f86dc52f (#20268's landing commit, the PR's merge base); the clean merge-tree is 3a1697de9…, equal to the merge commit's tree; it brought only conversions/registry.ts and a step-17 hunk in migrations/registry.ts. Neither merge brought anything but main's content. Regions at the head: re-rendered from the entry files with the generator's parseEntry and renderRegion logic, all six regions (semantic:17 77, semantic:18 215, retired-key:17 29, retired-key:18 185, retired-def:17 53, retired-def:18 146) are byte-identical to registry.ts; totals 292/214/199 as the body states; CI step 11 "Migration registry matches its entry files" success. Census at the head: MIGRATIONS_BY_MAJOR holds 17 and 18; step 18 lists 36 conversionIds; every one is named as a whole id by at least one 18.* entry (36 pairs, 0 unnamed); the pre-existing 11 pairs and the 25 new entries pair as in the prior review.

(d) Nothing else moved. A normalised interdiff (index lines dropped, hunk offsets normalised) of git diff 08c8484a1 3197fce29 against git diff 3f86dc52f a930cacea has exactly nine hunks: the four rewritten entry comments, their four generated mirrors in migrations/registry.ts, and the pin message. The file set is the same 32 files; the totals move from +1733/-36 to +1756/-36.

② Semver level

.changeset/20201-d3-entry-per-family-major-18.md is unchanged: @objectstack/spec patch, Clause-②: no. The delta is // comments and a test assertion message; no accept set, conversion, or runtime string moves. The grade is unchanged and still right.

③ Boundary flags

Blocking: none.

Non-blocking: (1) The body's sentence "main moved after the last merge (a930cacea), with nothing under migrations/ or conversions/" is now false: #20285 (2aa25efb4, edits five semantic entries' prose) and #20238 (6a6a17b62, a new step-18 conversion report-joined-chart-removed with its entry and retired key) landed after the merge base; the union with main is green (below), so it is body-only. The body's "PR #20238 (#20161, draft)" is stale for the same reason. (2) 18.view-item-owner-hidden-retired.ts says a flattened view overlay keeps its own owner and hidden; on pinned main 6a6a17b62, flattenedViewOverlayFields (view.zod.ts:5284-5285) still declares both, so the sentence is true in this PR's landing state. #20286 at its current head e38a8027b does not touch this file, so if #20286 lands second its own head must carry the correction, per the seat's arrangement. (3) registry.ts:108 keeps its released step-17 sentence, noted in the body's Acceptance notes.

Merge risk now. Landed since the last review: #20279 (733822cf8, 19:35Z), #20238 (6a6a17b62, 19:49Z), #20266, #20285, #20284, #20276, #20253, #20229. origin/main moved during this review, so it was pinned at 6a6a17b62; the merge base stays 3f86dc52f. Head × main 6a6a17b62: clean, tree 258c88d5c; union census 37 pairs, 0 unnamed; all six regions still mirror (293/215/199). Head × #20244 bafa19790: clean, tree 54d2ac968; #20244 adds no step-18 conversion and edits two pre-existing 18.* entries outside this PR's file set; union 36/36; mirror ok. Head × #20286 e38a8027b (moved from 2a40c104c during the review; both probed): clean, tree 3f86782fe; #20286 adds D2 view-overlay-owner-hidden-removed and D3 18.view-overlay-owner-hidden-retired.ts naming it; union 37/37; mirror ok. Head × #20238 4aecf01d2 before it landed: clean, tree c05a07c9e, union 37/37. Each sibling's additions are disjoint files and disjoint conversionIds, each named by its own entry, so the pin stays green in every landing order.

The PR body. Table 1 row 16 lists only the four permission records and integration/Connector:connectionTimeoutMs sits in row 20: fixed. The Acceptance note for registry.ts:108 is present. The body's citation counts, region count (215), totals (292/214/199), the three merges and what they brought, and the pin firing are all reproduced; the one false sentence is item (1) above.

CI at this head: 42 check-runs, all completed: 37 success, 5 skipped, 0 failure. All seven pinned required contexts (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard) are success. Lint & Repo Gates (job 108683864072, run 36342028740): 197 steps, 193 success, 4 skipped; steps 11, 12 and 189 success. Closing keywords: the body carries exactly Fixes #20201 and Clause-②: no.

Implemented-by: claude/issue-20201-d3-entry-per-family
Reviewed-by: session_01CiCTczDo7tGhafXjf61dUJ

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit f415bcf Sep 27, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20201-d3-entry-per-family branch September 27, 2026 20:21
os-sales pushed a commit that referenced this pull request Sep 27, 2026
…amily

After #20255 landed, its view-item D3 entry still said a flattened overlay
keeps its own owner/hidden. The overlay pair is a separate family with its
own D2 view-overlay-owner-hidden-removed and D3 view-overlay-owner-hidden-retired.
The overlay pin now reads another entry naming the conversion as a
cross-reference that must point at this family's record.

Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…s (ADR-0049) (objectstack-ai#20286)

Fixes objectstack-ai#20230
Clause-②: no (narrowing)

## What this does

Retires the flattened view overlay's `owner` and `hidden` keys under
ADR-0049 enforce-or-remove. Triage direction on the card (comment
5856621469), verbatim: 「follow objectstack-ai#20085's disposition for the same key
pair」. PR objectstack-ai#20227 retired the same pair on the view item record; this PR
retires it on the other door, with the same prescription texts.

The overlay door is the lean `PUT /api/v1/meta/view/:name` body with no
`config`: members 3 and 4 of the `view` union
(`VIEW_METADATA_MEMBERS.listOverlay` / `.formOverlay`), built from
`flattenedViewOverlayFields()` in `packages/spec/src/ui/view.zod.ts`. It
declared both keys, the write door accepted them, and `saveMetaItem`
stored them verbatim. Nothing read either one. After this PR, every door
that parses an overlay refuses both keys with the prescription. A stored
overlay row that holds either one is stripped on read:
- a row with other view keys is valid again and re-saves;
- a hide-only row (`{ object, viewKind, hidden: true }`) is left
identity-only, which the door refuses. It is badged invalid, refused on
a whole-row re-save, and reported `failed` by `os migrate meta --stored
--apply` until it is deleted or given the setting its author meant.

The D2 docblock, the D3 entry and the changeset all state this, and it
is pinned.

## Stop valve: the writer census, taken first

Taken before any edit, each reading with a lit control on the same ref.
No real writer was found, so the retirement proceeds.

| where | ref | writers of overlay `owner` / `hidden` | lit control |
|---|---|---|---|
| objectui at the `.objectui-sha` pin | `f8a9d0fb` | 0. All 12 view
write call sites were read one by one (the `persistViewPatch` toolbar
path, `updateView` / `updateViewConfig` / `createView` in the data
adapter, `viewEnvelope` saves, and the two `PublicFormsPage` saves).
None writes either key. The toolbar's overlay keys are
`VIEW_OVERLAY_OWNED_KEYS` = `rowHeight`, `sort`, `hiddenFields`,
`columnState`, `inlineEdit`. The switcher writes `label` and `isPinned`.
| 8 `persistViewPatch` call sites writing the owned keys; 2 `updateView`
row-key writes |
| objectui `main` | `6cf5999` | 0 (same 12 call sites, same reading) | 7
`persistViewPatch` call sites; 2 row-key writes |
| objectstack `packages/**`, `examples/**` | `e46218674` | 0. Examples
author no `viewKind` at all, and no view-level `hidden` / `owner` in 10
view files. No framework source writes an overlay body with either key.
| `label:` 88 times in the same 10 example view files |
| HotCRM | `2f7b2326` (= its remote `main`) | 0. No view write call, and
no view-level `hidden` / `owner` in 14 view files. | `label:` 159 times
in those files |
| cloud | not reachable | NOT MEASURED. REST read answered 403 and
`add_repo` was refused for this session. objectstack-ai#20227's census at cloud
`48d70663` recorded no code writer, and one test double that pins a lean
`{hidden:true}` PUT as accepted. That is a test fixture, not a writer.
It goes red on cloud's next spec bump only if it parses through the spec
schema. | — |

Readers, re-checked: `.hidden` / `.owner` reads on a view in
`rest-server.ts` = 0/0 and in `metadata-manager.ts` = 0/0. The 5
`.hidden` reads in `metadata-protocol/src/protocol.ts` are all
field-level. Control: `.order` is read 2 / 1 / 2 times in the same three
files.

## Dispatch assumptions, measured

1. The two keys were at `view.zod.ts:5284-5285` on `e46218674`, and
`flattenedViewOverlayFields(kind)` takes a `kind` argument. **Held.**
Only those two keys move.
2. The `retiredKey()` tombstone applies. **Held.** Both overlay members
`.strip()`, and a `z.never()` member refuses loudly instead of
stripping: the pins below assert issue code `invalid_type` at path
`[key]`, carrying the prescription.
3. A D2 conversion is owed. **Held, and the view-item entry does not
cover it.** `view-item-owner-hidden-removed` skips any body without a
`config` dict, and its own fixture pinned an overlay's `hidden: true` as
kept. This PR adds a separate entry, disjoint by `config`.
4. Other `view.zod.ts` regions were not touched: no edit in
`FormViewSchema.layout`, `ViewMetadataParsed` or `diagnoseViewMetadata`.

## The route

- **Tombstones.** `owner: retiredKey(VIEW_ITEM_OWNER_RETIRED)` and
`hidden: retiredKey(VIEW_ITEM_HIDDEN_RETIRED)` in
`flattenedViewOverlayFields()`. These are the view item's own constants,
so both doors answer with the same text, as the order asked. A pin
asserts the overlay's issue message is byte-equal to the view item's.
- **D2 `view-overlay-owner-hidden-removed`** (`toMajor: 18`,
`retiredFromLoadPath: true`, lossless `stripKeys`). Scope: the flattened
spelling, meaning a body with no `config` and no container slot. It
walks `views` (stack sources, and every stored row, which
`convertStoredItem` replays before serving or badging) and `viewItems`
(the assembled channel). It does NOT require `viewKind`: a flat row
stored before the objectstack-ai#7741 binding has none until the write path heals it
in, and then the save would refuse the key it still held. It is wired
into `MIGRATIONS_BY_MAJOR[18]`, and the step rationale is extended.
- **Why the D2 matters at runtime, and what it cannot do.** objectui's
`updateView` is a read-merge-write, and `buildPersistedViewBody`
re-sends a saved view whole. A stored row served WITH `hidden` would
make the next toolbar toggle a 422, so the read path strips first.
  - For a content-bearing row, that is the whole story.
- For a hide-only row, the strip leaves identity only, and the door
refuses that (the identity precondition: only identity fields). The
badge turns invalid, a whole-row re-save or a rename (`label` is
identity) answers 422, and `--apply` reports `failed` and leaves the row
as stored. A toggle that adds a real key saves.
  - Remedy: delete the row, or add the setting its author meant.
- **D3 `view-overlay-owner-hidden-retired`** (ruling B on objectstack-ai#17152). It
names its conversion by id in `reason`, which is the shape objectstack-ai#20255's
census pin reads. That pin is now live on `main` and green here. Its
`acceptanceCriteria` state both classes, the hide-only row included. The
view item's pair is a separate family with its own D2 and its own D3
(`18.view-item-owner-hidden-retired.ts`, from objectstack-ai#20255). This PR corrects
that entry's one stale sentence (amendment `5859181450`).
- **`RETIRED_KEYS_BY_MAJOR[18]`**: `ui/ViewMetadata:owner` and
`ui/ViewMetadata:hidden`. The overlay members are not exported.
`ui/ViewMetadata` is the exported door they are reached through, and it
is listed in `unemitted-schemas.baseline.json`, so these rows are
declared, not judged. The retirement test pins them.
- **No liveness row.** The `view` ledger walks the container keys only
(`name`, `label`, `object`, `list`, `form`, `listViews`, `formViews`),
so a row would be an ORPHAN. `check:liveness` is green without one.
- **Generated artefacts.** `check:generated`: all 15 were current, and
there was nothing to regenerate. The four surface ratchets are
byte-identical, which is expected on this route: the def is unemitted.
`spec-changes.json` and the upgrade guide project up to protocol 17, so
no major-18 entry shows there either (the same reading as PR objectstack-ai#20227).
- **Forms / examples / skills / docs.** No form offers either key. There
are zero authorings in `examples/`, `skills/` and `content/docs/`. The
tree-scoped pin below holds that.
- **Changeset.** `@objectstack/spec: minor`, `**BREAKING**`, FROM → TO,
the one-line fix, `Clause-②: no (narrowing)`, ADR-0087 disposition
`registered view-overlay-owner-hidden-removed,
view-overlay-owner-hidden-retired`.

## Pins

The new file is
`packages/spec/src/ui/view-overlay-owner-hidden-retirement.test.ts`
(in-package, local project):

- Both overlay members refuse each key at its path: `invalid_type`, the
path, and the prescription. The `view` door
(`getMetadataTypeSchema('view')`) refuses with `invalid_union`, the
prescription surfaces as the union's message, and the claimed member
locates the key. The assembled channel refuses too.
- CONTROL: the same overlays without the keys pass every door, with
`isDefault` / `order` / `scope` intact and no key grown. The view item
door refuses the pair as well, so the family is closed on both doors.
`defineView` is the container door, not an overlay door.
- D2: a stored row rehydrates clean and then parses at the door, while
the unconverted row is refused. A `viewKind`-less flat row is stripped.
The `viewItems` channel is reached, with each door's key stripped by its
own entry. Containers are left alone. Idempotence: the second replay has
0 notices and returns the same reference. Load path: a live author is
refused, not rewritten.
- Registration: the two keys, the chain id, and one D3 record for the
conversion. Any other entry naming the conversion must also name
`view-overlay-owner-hidden-retired`, so it is a pointer, never a second
record.
- **Hide-only residue** (patch round 1):
- A stored `hidden`, `owner` or both row strips to identity only. The
door refuses it with the identity precondition's own text, as one custom
issue at the root rather than the prescription.
  - A rename (`label`) is refused.
  - Controls `isDefault` / `order` / `columnState` save.

The ADR-0112 envelope is pinned at the door that produces it.
`packages/metadata-protocol/src/protocol.save-union-issues.test.ts` adds
a describe block over the existing stub-engine harness (no new double).
For each key and each family, `saveMetaItem` rejects with `code`
`INVALID_METADATA` and `status` `422`, persists 0 rows, and carries an
issue located at the key with the prescription. CONTROL: the same bound
overlays without the keys save, 1 row each.

Patch round 1 adds two pins here:
- **Save door:** a whole-row PUT of the stripped hide-only row answers
`INVALID_METADATA` / 422, with 0 rows and "only identity fields". The
same row plus `isDefault` saves.
- **Read path:** `getMetaItem` over a seeded row serves a stored overlay
without `owner` / `hidden`. `_diagnostics` is valid when the row carries
content and invalid when it is hide-only. The existing harness gains an
optional seed; its default is unchanged.

## Flipped pins: repo-wide sweep, each one load-bearing

The sweep grepped every test file that spells `viewKind` beside `hidden`
/ `owner`, in all packages.

| pin | before | after |
|---|---|---|
| `spec/ui/view-item-owner-hidden-retirement.test.ts` BOUNDARY | an
overlay with the keys parses | refused, with the SAME prescription |
| same file, conversion test | overlays left alone | the overlay key is
stripped by `view-overlay-owner-hidden-removed`, the record key by the
view-item entry (asserted as pairs) |
| same file, tree-scoped matcher | record spelling only | both spellings
(`viewKind` + a retired key); anti-vacuity cases for an overlay (TS,
YAML) and a container |
| `spec/conversions/registry.ts` view-item fixture | overlay neighbour
kept `hidden: true` | the neighbour carries neither key, which keeps the
fixtures disjoint once the overlay entry replays |
| `spec/ui/view-metadata-schema.test.ts` | `a hide PUT` accepted;
`identity + hidden` accepted | the hide PUT is refused at the member
with the prescription (not by the precondition); identity + a live key
is accepted, identity + `hidden` refused |
| `spec/ui/view-union-diagnostics.test.ts` | `overlay.list.aux` (with
`hidden`) and `put.hidden` ACCEPTED | moved to REFUSED, plus
`put.owner`; a new test asserts those rows are refused BY the tombstone
(the prescription, `invalid_type` at the key) |
| `spec/conversions/view-spelling-walk.test.ts` | the overlay's `owner`
survives conversion | `owner` stripped, with the notice under the
overlay entry; every binding key still survives |
| `metadata-protocol/src/metadata-diagnostics.union-issues.test.ts` |
`{hidden, object, viewKind}` badged `valid: true` | badged invalid, with
the prescription at `hidden`; a live key is badged valid |

## Verification

**Patch round 1, final head `a05b32f8b`**, merged with `origin/main` at
`4e0f72e8d`, which carries objectstack-ai#20238, objectstack-ai#20255 and objectstack-ai#20244 (dev report
`5860055944`):
- spec `--project local`, full: 553 files / 16341 tests.
- The touched pins plus `migrations.test.ts`, with the census pin shown
verbosely: 6 / 530.
- The repo view-item pin: 18/18.
- `turbo build rest^...`: 24/24.
- metadata-protocol save-door + diagnostics: 2 / 40.
- Typecheck spec + metadata-protocol: exit 0.
- `check:generated`: 15/15 current.
- Gates: 88 derived, 86 run and exit 0, 2 NOT-MEASURED
(`check:dual-build-cjs-loads`, `check:type-check-debt`: exit 3,
PREREQUISITE NOT MET), 0 UNRUN.
- Ablation (round 1, at `e38a8027b`): the overlay strip replaced by
`return view` → 6 red (the residue, stored-row, `viewKind`-less and
`viewItems` pins) / 18 green. The restore was proven by blob == HEAD and
an empty `git diff HEAD`.

The round-0 readings below are at `2a40c104c`.

Round 0: final head **`2a40c104c`**. That is after merging `origin/main`
at `17bd3187`, which carried objectstack-ai#19920's `view.zod.ts` /
`assembled-views.zod.ts` type change. Heavy runs went through
`scripts/pm/os-verify-lock.sh`, and every exit code was written to disk
before its log was read. The box was shared, with lock waits of 3–9 min,
so wall-clock readings are contended.

| run | head | reading |
|---|---|---|
| `turbo run build --filter='@objectstack/rest^...'` (spec + the
consumer closure) | `2a40c104c` | exit 0, 24/24 tasks |
| `pnpm --filter @objectstack/spec check:generated` | `2a40c104c` | exit
0, all 15 artifacts current; nothing regenerated |
| spec `--project local`, full | `2a40c104c` | 553 files / 16275 tests
passed |
| spec `--project repo`, `view-item-owner-hidden-retirement.test.ts`
(tree-scoped pin) | `2a40c104c` | 18/18 passed |
| metadata-protocol, the edited files +
`view-write-path-identity.test.ts` | `2a40c104c` | 3 files / 41 tests
passed |
| typecheck: spec (`tsc` + scripts + `check:test-typecheck`), lint,
metadata-protocol | `2a40c104c` | exit 0 ×3 |
| consumers, full: metadata-protocol / lint / metadata; objectql and
rest (their 24 / 13 view files) | `cbc81c574` | 189 files / 2720 tests
(3 skipped) · 110 / 4262 · 54 / 821 · 24 / 380 · 13 / 191, all exit 0 |

**Reverse verification** (a one-shot probe removed by an EXIT trap,
verified absent afterwards):
`packages/lint/src/zz-issue20230-dts-probe.ts` typed `{ object,
viewKind: 'list', hidden: true }` as `ViewMetadata`, against the REBUILT
spec `.d.ts`. `@objectstack/lint` `tsc --noEmit` exited 2:
`src/zz-issue20230-dts-probe.ts(2,14): error TS2322: Type '{ object:
string; viewKind: "list"; hidden: boolean; }' is not assignable to type
'ViewMetadata'.` With the probe removed, `git status` showed 0 lines and
`lint typecheck` exited 0. Predicted direction: red. Observed: red.

**Ablation** (`scripts/ablation-replace.mjs`, on committed state, wrap
mode). The mutation swapped the overlay's `hidden:
retiredKey(VIEW_ITEM_HIDDEN_RETIRED),` for `hidden:
z.boolean().optional(),`: anchor 1 → 0, blob `1f93b520` → `e9ad3dec`.
Three spec files then read 10 failed / 139 passed, and the 10 are
exactly the overlay `hidden` pins: both members, the same-text pin, the
door, the assembled channel, the hide-PUT refusal, the identity pin, and
the three union-diagnostics rows. The `owner` pins stayed green, as they
should. The restore brought the blob back to HEAD `1f93b520`, with `git
diff HEAD` at 0 bytes and `git status --porcelain` at 0 lines. Predicted
direction: red. Observed: red. (The metadata-protocol save-door pins
resolve spec through `dist/`, so they were not part of this ablation.)

**Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `2a40c104c` derived 88 commands. All 88
ran with the exit code captured before any pipe, and were reconciled
with `--ran`: **88 derived, 86 run, 2 NOT-MEASURED, 0 UNRUN**. All 86
measured commands exited 0. That includes `check-adr-0087-registration`
(`registered view-overlay-owner-hidden-removed,
view-overlay-owner-hidden-retired (new here …)`,
`[BREAKING+bang+clause-②-narrowing]`), `check-changeset-no-major`,
`check-empty-changeset`, `check:nul-bytes`,
`check:cross-package-test-inputs`, `check:doc-authoring`, and the spec
`check:*` family (`check:authorable-surface`, `check:liveness`,
`check:migration-registry`, `check:spec-changes`, `check:upgrade-guide`,
`check:api-surface`, `check:docs`).

NOT MEASURED (exit 3, `PREREQUISITE NOT MET`; each reads built output of
the whole workspace, which was not built locally): `pnpm
check:dual-build-cjs-loads`, `pnpm check:type-check-debt`. This diff
touches no package entry point, export or tsconfig. CI's build lanes
measure both. Also owned by CI: `pnpm lint`, the remaining objectql /
rest suites, and the lanes `dispatch-gates` lists outside the derived
total. The CLI `integration` tier does not apply (no `packages/cli`
change).

## Acceptance notes (observed, not fixed here)

_The seat updated this body at 2026-09-27T21:40Z after patch round 1,
per dev report `5860055944`. Reviews: `5859174998` (FAIL at
`2a40c104c`)._


1. **One family or two for D3, and the overlap with PR objectstack-ai#20255.** PR
objectstack-ai#20255 (objectstack-ai#20201, not merged when this opened) adds
`18.view-item-owner-hidden-retired.ts` as the view item family's D3
entry, and a census pin requiring every major-18 conversion to be named
by a D3 entry of its step. This PR's conversion is separate, disjoint by
`config`, so it carries its own D3 entry naming it. That keeps one
record per conversion and no second file under objectstack-ai#20255's filename, which
would be an add/add collision. objectstack-ai#20255 has since landed (`f415bcf18`),
and the census pin is green here at `a05b32f8b`. Its sentence 「A
flattened view overlay keeps its own `owner` and `hidden` …」 is replaced
in this PR (amendment `5859181450`): the overlay pair is a separate
family, with its own D2 `view-overlay-owner-hidden-removed` and D3
`view-overlay-owner-hidden-retired`.
2. **This PR supersedes one sentence of objectstack-ai#20227's pending changeset.**
`.changeset/view-item-owner-hidden-retired.md` says an overlay "still
parses". It is left as landed, because `check-empty-changeset` refuses
an edit to another PR's release note. This PR's changeset states the
supersession instead. The release compiler should read the two together.
3. **Cloud is NOT MEASURED** (above). If its mock-protocol double parses
`{hidden:true}` through the spec, it goes red at cloud's spec bump. That
is a fixture edit there. Carrier: cloud, at its next `@objectstack/spec`
bump.
4. **The assembled channel's refusal loses the branch diagnostics**
(objectstack-ai#20227's acceptance note 4, pre-existing):
`AssembledViewArtifactSchema` is a plain `z.union`. Carrier: none.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants