Repository navigation
fix(spec): state the RLS using / check texts as the write gate enforces them - #20268
Conversation
… them The check describe, TSDoc and the texts that quote them now say every row an insert or an update writes is judged, a check-only update policy is legal, an insert policy's using is its check when none is declared, and OR-combination without qualification holds on reads only. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 139 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 8b5999e053334a30d46c0f3223a1672f003b5727 && git checkout 8b5999e053334a30d46c0f3223a1672f003b5727
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 17bd31877109b7cc692e7e54c4fe39f82a5c32d5 7bfc993cb58342b79f4bf4990e4f9e77189bad2c && git checkout -B drift-repro 17bd31877109b7cc692e7e54c4fe39f82a5c32d5 && git merge --no-ff 7bfc993cb58342b79f4bf4990e4f9e77189bad2c
node scripts/docs-audit/affected-docs.mjs --json 17bd31877109b7cc692e7e54c4fe39f82a5c32d5
|
Contract reviewServed-tier: 84/84 ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…a deleted card The `check` TSDoc cited #16608, which no longer resolves on the board. Anchor the seam to commit a016f08, which introduced it, and keep the live numbers that extended it. Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: 78/78 ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…nd the 25 missing entries (objectstack-ai#20201) (objectstack-ai#20255) Fixes objectstack-ai#20201 Clause-②: no ## What Ruling B on objectstack-ai#17152 (director `5615360777`, restated `5634031140`, on the maintainer's objectstack-ai#15954 authority `5559778263`): every retirement family carries ONE ADR-0087 D3 (`semantic`) entry, even when a lossless D2 conversion repairs its data; D2 carries the mechanical repair only. This PR takes the major-18 family census the card asks for, adds the 25 D3 entries it found missing, corrects the prose that justified their absence, and pins the census in the registry's own test. - **25 new D3 entries** under `packages/spec/src/migrations/entries/semantic/18.*.ts`, one per D2-backed family that had none. Each names its family and its D2 conversion id, says what D2 already repairs, and says what judgment the consumer still owes (the `reason`), with an `acceptanceCriteria` the consumer can check. None is a placeholder: every one states a residue specific to its family (a unit only the author knows, a belief the platform never honoured, a shape the conversion deliberately leaves alone, code the chain cannot reach). - **Prose corrected** at the sites of `5854110917` and `5854456343`, plus step 18's rationale and step 17's docblock fact (list below). - **Census pin** in the existing `packages/spec/src/migrations/migrations.test.ts` (registry integrity). No new check script. - `MIGRATIONS_BY_MAJOR[18].semantic` 186 → 211 entries at the census base; after merging `main` (four sibling D3 entries landed meanwhile, none with a D2 conversion) the generated region holds 215. ## The census (the card's main deliverable) **Tree.** `objectstack-ai/objectstack` at `3cb84d084` (this branch's fork point; it already contains objectstack-ai#20227's view-item retirement). Major-18 population there: **185** `retired-keys`, **146** `retired-defs`, **186** `semantic` entry files, and **36** D2 conversions graduated into step 18. (The card measured 181 / 130 / 175 at `d7c024133e`.) **Grouping rule (ruling B's unit, held constant).** Where a D2 conversion exists, the conversion is the family: every retired key or def it repairs belongs to it, and a D3 entry may cover more than one conversion only where one judgment covers them (the pre-existing `element-filter-and-form-node-refused` covers `element-filter-removed` and `element-form-removed`). Every new entry here covers exactly one conversion. Where no conversion exists, the records are grouped by the D3 entry that names them. **Method.** 1. Mechanical pass (scratch scripts, not committed): for each of the 36 conversions, the major-18 `semantic/` entry files that name its id as a whole id (comment or field); for each retired key, the conversion its own comment names, else the major-18 entries naming it as `cat/Def:key`, `Def.path` / `Def:path`, or the def name plus the leaf key; for each retired def, the entries naming the def. 2. Reading pass, where a string match cannot decide: (a) ownership: an entry that names a conversion only in passing is not that family's entry (this is how `metric-filters-removed` was classed missing although `analytics-authorable-unknown-keys-refused` names it); (b) 14 records matched by more than one entry, each placed by its own comment (for example `kernel/PluginStartupResult:plugin`, which goes to `startup-orchestrator-retired`); (c) 9 records whose comment names no conversion but which belong to a D2 family (`integration/DeclarativeConnectorEntry:connectionTimeoutMs` and `:errorMapping`, the three error-mapping defs, the four responsive-shape defs), plus `integration/Connector:connectionTimeoutMs`, whose comment names two conversion ids and belongs to `connector-connection-timeout-ms-removed` (it names the permission conversion only as a comparison; round 1's Table 1 placed it wrongly, corrected in patch round 1); (d) 5 theme sub-block defs, named by the theme family's entry as its sub-blocks. **Control.** The pairing sees a family that has its entry: 11 of the 36 conversions pair with a pre-existing entry, among them `cube-join-sql-and-relationship-removed` with `cube-join-sql-and-relationship-retired` (which the pin's own control test also asserts), and the whole-id matcher refuses a prefix (`record-chatter-position-vocabulary` is a prefix of its entry's own id and matches only the entry's real citation). Evaluated at the base with the pin's logic: **24** conversions named by no major-18 entry; the reading pass adds `metric-filters-removed` for **25**. Evaluated at this head: **0**. **Result.** 331 records (185 keys + 146 defs) plus 36 conversions: - **36 D2-backed families** covering 58 records: 11 had their D3 entry, **25 had none**. Table 1. - **273 D2-less records** in 68 groups: every one is named by an existing D3 entry. Table 2. None missing, as expected: before ruling B, a retirement with no conversion needed a D3 entry anyway. The card's grep for 「lossless」 found the `tenancy.organizationField` site and step 17. The census finds 25 major-18 families, most of them with no 「lossless」 wording at all. ### Table 1 — D2-backed families (step 18 `conversionIds`, in order) | # | D2 conversion (the family) | registered records | D3 entry at base | D3 entry after | |---|---|---|---|---| | 1 | `field-malformed-scale-precision-removed` | none (value or strict-key retirement, not in the two tables) | `field-scale-precision-integer-refused` | unchanged | | 2 | `record-chatter-position-vocabulary` | none (value or strict-key retirement, not in the two tables) | `record-chatter-position-vocabulary-converged` | unchanged | | 3 | `element-input-target-variable-removed` | `ui/ElementRecordPickerProps:targetVariable`, `ui/ElementTextInputProps:targetVariable` | MISSING | `element-input-target-variable-retired` (new) | | 4 | `element-filter-removed` | `ui/ElementFilterProps:aria`, `ui/ElementFilterProps:fields`, `ui/ElementFilterProps:layout`, `ui/ElementFilterProps:object`, `ui/ElementFilterProps:showSearch`, `ui/ElementFilterProps:targetVariable` | `element-filter-and-form-node-refused` | unchanged | | 5 | `element-form-removed` | `ui/ElementFormProps:aria`, `ui/ElementFormProps:fields`, `ui/ElementFormProps:mode`, `ui/ElementFormProps:object`, `ui/ElementFormProps:onSubmit`, `ui/ElementFormProps:submitLabel` | `element-filter-and-form-node-refused` | unchanged | | 6 | `field-column-lists-canonicalized` | none (value or strict-key retirement, not in the two tables) | MISSING | `field-inline-and-related-list-columns-closed` (new) | | 7 | `metric-filters-removed` | `data/Metric:filters` | MISSING (named only in passing by `analytics-authorable-unknown-keys-refused`) | `cube-metric-filters-retired` (new) | | 8 | `cube-sub-day-granularities-removed` | none (value or strict-key retirement, not in the two tables) | `time-update-interval-sub-day-retired` | unchanged | | 9 | `cube-join-sql-and-relationship-removed` | `data/CubeJoin:relationship`, `data/CubeJoin:sql` | `cube-join-sql-and-relationship-retired` | unchanged | | 10 | `record-highlights-field-icon-removed` | `ui/RecordHighlightsField:icon` | MISSING | `record-highlights-field-icon-retired` (new) | | 11 | `mapping-lookup-params-removed` | none (value or strict-key retirement, not in the two tables) | MISSING | `mapping-lookup-params-retired` (new) | | 12 | `translation-component-submit-label-removed` | none (value or strict-key retirement, not in the two tables) | MISSING | `translation-component-submit-label-retired` (new) | | 13 | `page-component-responsive-removed` | `ui/PageComponent:responsive`, `ui/BreakpointColumnMap`, `ui/BreakpointName`, `ui/BreakpointOrderMap`, `ui/ResponsiveConfig` | MISSING | `page-component-responsive-retired` (new) | | 14 | `object-grid-default-sort-removed` | `ui/ObjectGridProps:defaultSort` | MISSING | `object-grid-default-sort-retired` (new) | | 15 | `object-kanban-quick-add-removed` | `ui/ObjectKanbanProps:quickAdd` | MISSING | `object-kanban-quick-add-retired` (new) | | 16 | `permission-allow-restore-purge-removed` | `security/EffectiveObjectPermission:allowPurge`, `security/EffectiveObjectPermission:allowRestore`, `security/ObjectPermission:allowPurge`, `security/ObjectPermission:allowRestore` | MISSING | `permission-restore-purge-bits-retired` (new) | | 17 | `form-view-option-default-removed` | none (value or strict-key retirement, not in the two tables) | MISSING | `form-view-option-default-retired` (new) | | 18 | `field-reference-to-alias` | none (value or strict-key retirement, not in the two tables) | MISSING | `field-reference-to-spelling-retired` (new) | | 19 | `connector-error-mapping-removed` | `integration/Connector:errorMapping`, `integration/DeclarativeConnectorEntry:errorMapping`, `integration/ConnectorErrorCategory`, `integration/ErrorMappingConfig`, `integration/ErrorMappingRule` | MISSING | `connector-error-mapping-retired` (new) | | 20 | `connector-connection-timeout-ms-removed` | `integration/Connector:connectionTimeoutMs`, `integration/DeclarativeConnectorEntry:connectionTimeoutMs` | `connector-provider-context-connection-timeout-ms-retired` | unchanged | | 21 | `hook-timeout-to-timeout-ms` | none (value or strict-key retirement, not in the two tables) | MISSING | `hook-timeout-unit-in-key` (new) | | 22 | `job-timeout-to-timeout-ms` | `system/Job:timeout` | MISSING | `job-timeout-unit-in-key` (new) | | 23 | `api-endpoint-cache-ttl-to-cache-ttl-seconds` | `api/ApiEndpoint:cacheTtl` | MISSING | `api-endpoint-cache-ttl-unit-in-key` (new) | | 24 | `dashboard-refresh-interval-to-refresh-interval-seconds` | `ui/Dashboard:refreshInterval` | MISSING | `dashboard-refresh-interval-unit-in-key` (new) | | 25 | `connector-health-and-trigger-durations-unit-in-key` | `integration/CircuitBreakerConfig:monitoringWindow`, `integration/ConnectorTrigger:interval` | MISSING | `connector-resilience-durations-unit-in-key` (new) | | 26 | `memory-persistence-auto-save-interval-to-ms` | `data/AutoPersistenceConfig:autoSaveInterval`, `data/FilePersistenceConfig:autoSaveInterval` | MISSING | `memory-persistence-auto-save-interval-unit-in-key` (new) | | 27 | `turso-config-timeout-to-timeout-ms` | `data/TursoConfig:timeout` | MISSING | `turso-config-timeout-unit-in-key` (new) | | 28 | `view-page-mount-removed` | `ui/ListView:pageName`, `ui/ObjectListView:pageName` | MISSING | `list-view-page-mount-retired` (new) | | 29 | `list-view-sort-string-clause-to-array` | none (value or strict-key retirement, not in the two tables) | MISSING | `list-view-sort-string-clause-retired` (new) | | 30 | `page-assigned-profiles-removed` | `ui/Page:assignedProfiles` | `page-assigned-profiles-audience-to-permission-set` | unchanged | | 31 | `chart-config-aria-removed` | `ui/ChartConfig:aria`, `ui/ReportChart:aria` | MISSING | `chart-config-aria-retired` (new) | | 32 | `dashboard-widget-chart-config-structure-removed` | `ui/DashboardWidgetChartConfig:series`, `ui/DashboardWidgetChartConfig:type`, `ui/DashboardWidgetChartConfig:xAxis`, `ui/DashboardWidgetChartConfig:yAxis` | `dashboard-widget-chart-config-structure-refused` | unchanged | | 33 | `translation-per-app-settings-removed` | none (value or strict-key retirement, not in the two tables) | `translation-per-app-settings-platform-only` | unchanged | | 34 | `object-tenancy-organization-field-removed` | `data/TenancyConfig:organizationField` | MISSING | `object-tenancy-organization-field-retired` (new) | | 35 | `page-component-filter-record-to-rule-array` | none (value or strict-key retirement, not in the two tables) | `element-data-source-and-object-block-filter-rule-array`, `object-grid-default-filters-rule-array` | unchanged | | 36 | `view-item-owner-hidden-removed` | `ui/ViewItemWire:hidden`, `ui/ViewItemWire:owner`, `ui/ViewItem:hidden`, `ui/ViewItem:owner` | MISSING | `view-item-owner-hidden-retired` (new) | ### Table 2 — D2-less records, grouped by the existing D3 entry that names them | D3 entry (existing) | records it names | |---|---| | `advanced-plugin-lifecycle-config-retired` | `kernel/AdvancedPluginLifecycleConfig`, `kernel/GracefulDegradation`, `kernel/PluginUpdateStrategy` | | `ai-conversation-analytics-duration-unit-in-key` | `ai/ConversationAnalytics:duration` | | `api-error-retry-after-unit-in-key` | `api/EnhancedApiError:retryAfter` | | `api-runtime-config-durations-unit-in-key` | `api/DataLoaderConfig:cacheTtl`, `api/RouteDefinition:timeout` | | `automation-flow-list-route-retired` | `api/FlowSummary`, `api/ListFlowsRequest`, `api/ListFlowsResponse` | | `automation-runs-cursor-retired` | `api/ListRunsRequest:cursor` | | `branded-identifier-schemas-retired` | `shared/AppName`, `shared/FieldName`, `shared/FlowName`, `shared/ObjectName`, `shared/RoleName`, `shared/ViewName` | | `change-management-duration-keys-retired` | `system/ChangeImpact:downtime.durationMinutes`, `system/ChangeRequest:implementation.steps.estimatedMinutes`, `system/RollbackPlan:steps.estimatedMinutes` | | `change-management-family-retired` | `system/ChangeImpact`, `system/ChangePriority`, `system/ChangeRequest`, `system/ChangeStatus`, `system/ChangeType`, `system/RollbackPlan` | | `cli-command-contribution-retired` | `kernel/CLICommandContribution` | | `cloud-subpath-retired` | 62 records, all `cloud/` defs | | `data-file-value-duration-unit-in-key` | `data/FileValue:duration` | | `data-nosql-query-options-timeout-unit-in-key` | `data/NoSQLQueryOptions:timeout` | | `device-request-response-interval-unit-in-key` | `api/DeviceRequestResponse:interval` | | `driver-options-timeout-to-timeout-ms` | `data/DriverOptions:timeout` | | `epoch-instant-keys-renamed` | `api/SimplePresenceState:lastSeen`, `api/WebSocketEvent:timestamp`, `kernel/HealthStatus:timestamp`, `kernel/KernelContext:startTime`, `kernel/TenantRuntimeContext:startTime` | | `esignature-config-deadline-keys-retired` | `data/ESignatureConfig:expirationDays`, `data/ESignatureConfig:reminderDays` | | `event-name-schema-retired` | `shared/EventName` | | `export-job-family-retired` | 13 records, all `api/`, `automation/` defs | | `hot-reload-inert-state-strategies-retired` | `kernel/DistributedStateConfig` | | `hot-reload-watch-placeholder-retired` | `kernel/HotReloadConfig:watchPatterns` | | `identity-api-key-schema-retired` | `identity/ApiKey` | | `incident-response-deadline-keys-retired` | `system/IncidentNotificationMatrix:escalationTimeoutMinutes`, `system/IncidentNotificationRule:regulatorDeadlineHours`, `system/IncidentNotificationRule:withinMinutes`, `system/IncidentResponsePhase:targetHours`, `system/IncidentResponsePolicy:retentionDays`, `system/IncidentResponsePolicy:triageDeadlineHours` | | `incident-response-family-retired` | `system/Incident`, `system/IncidentCategory`, `system/IncidentNotificationMatrix`, `system/IncidentNotificationRule`, `system/IncidentResponsePhase`, `system/IncidentResponsePolicy`, `system/IncidentSeverity`, `system/IncidentStatus` | | `kernel-compatibility-matrix-estimated-migration-time-unit-in-key` | `kernel/CompatibilityMatrixEntry:estimatedMigrationTime` | | `kernel-context-preview-mode-retired` | `kernel/KernelContext:previewMode`, `kernel/PreviewModeConfig`, `kernel/TenantRuntimeContext:previewMode` | | `kernel-event-bus-retention-unit-in-key` | `kernel/EventPersistence:retention`, `kernel/EventSourcingConfig:retention` | | `kernel-health-check-and-hot-reload-durations-unit-in-key` | `kernel/HotReloadConfig:debounceDelay`, `kernel/PluginHealthCheck:interval`, `kernel/PluginHealthCheck:timeout` | | `kernel-package-lifecycle-durations-unit-in-key` | `kernel/MultiVersionSupport:rollout.duration`, `kernel/PackageDependencyResolutionResult:resolvedIn`, `kernel/UpgradePlan:estimatedDuration` | | `kernel-plugin-health-report-durations-unit-in-key` | `kernel/PluginHealthReport:metrics.responseTime`, `kernel/PluginHealthReport:metrics.uptime` | | `kernel-plugin-security-durations-unit-in-key` | `kernel/KernelSecurityPolicy:auditLog.retention`, `kernel/KernelSecurityPolicy:authentication.tokenExpiration`, `kernel/PluginSecurityManifest:vulnerabilityDisclosure.responseTime` | | `kernel-runtime-config-timeout-unit-in-key` | `kernel/RuntimeConfig:resourceLimits.timeout`, `kernel/SandboxConfig:process.timeout` | | `kernel-startup-orchestrator-durations-unit-in-key` | `kernel/PluginStartupResult:duration`, `kernel/StartupOptions:timeout`, `kernel/StartupOrchestrationResult:totalDuration` | | `list-view-navigation-view-retired` | `ui/NavigationConfig:view` | | `logging-durations-unit-in-key` | `system/HttpDestinationConfig:batch.flushInterval`, `system/HttpDestinationConfig:retry.initialDelay`, `system/HttpDestinationConfig:timeout`, `system/LoggingConfig:buffer.flushInterval` | | `metadata-changed-event-payload-retired` | `kernel/MetadataChangeOperation`, `kernel/MetadataChangedEventPayload` | | `metadata-customization-protocol-retired` | 13 records, all `api/`, `kernel/` defs | | `metadata-manager-config-cache-ttl-unit-in-key` | `kernel/MetadataManagerConfig:cache.ttl` | | `metadata-manager-config-inert-cache-keys-retired` | `kernel/MetadataManagerConfig:cache.enabled`, `kernel/MetadataManagerConfig:cache.maxSize`, `kernel/MetadataManagerConfig:cache.ttlSeconds` | | `metadata-plugin-additional-types-retired` | `kernel/MetadataPluginConfig:additionalTypes` | | `package-rollback-response-retired` | `api/PackageRollbackResponse` | | `packages-list-pagination-retired` | `api/ListInstalledPackagesRequest:cursor`, `api/ListInstalledPackagesRequest:limit` | | `plugin-auto-restart-never-reinitialised` | `kernel/PluginHealthCheck:autoRestart`, `kernel/PluginHealthCheck:maxRestartAttempts`, `kernel/PluginHealthCheck:restartBackoff` | | `plugin-manifest-contributes-dead-members-retired` | `kernel/Manifest:contributes.actions`, `kernel/Manifest:contributes.commands`, `kernel/Manifest:contributes.drivers`, `kernel/Manifest:contributes.events`, `kernel/Manifest:contributes.fieldTypes`, `kernel/Manifest:contributes.functions`, `kernel/Manifest:contributes.menus`, `kernel/Manifest:contributes.themes`, `kernel/Manifest:contributes.translations` | | `plugin-manifest-contributes-routes-retired` | `kernel/Manifest:contributes.routes` | | `plugin-manifest-dead-containers-retired` | `kernel/Manifest:capabilities`, `kernel/Manifest:configuration`, `kernel/Manifest:extensions` | | `plugin-manifest-kind-globs-retired` | `kernel/Manifest:contributes.kinds.globs` | | `plugin-security-scan-result-surface-retired` | `kernel/KernelSecurityScanResult`, `kernel/KernelSecurityVulnerability`, `kernel/PluginQualityMetrics:securityScan`, `kernel/PluginSecurityManifest:scanResults`, `kernel/PluginSecurityManifest:vulnerabilities` | | `rest-api-endpoint-handler-status-retired` | `api/HandlerStatus`, `api/RestApiEndpoint:handlerStatus`, `api/RouteCoverageEntry`, `api/RouteCoverageReport` | | `rest-api-plugin-durations-unit-in-key` | `api/RestApiEndpoint:cacheTtl`, `api/RestApiEndpoint:timeout`, `api/RestApiPluginConfig:performance.defaultCacheTtl` | | `rest-server-config-dead-keys-retired` | 11 records, all `api/` defs | | `session-user-language-retired` | `api/SessionUser:language` | | `stack-themes-carrier-retired` | `ui/BorderRadius`, `ui/ColorPalette`, `ui/Shadow`, `ui/Theme`, `ui/ThemeMode`, `ui/Typography` | | `startup-orchestrator-retired` | `kernel/HealthStatus`, `kernel/PluginStartupResult:health`, `kernel/PluginStartupResult:plugin`, `kernel/PluginStartupResult:startTime`, `kernel/StartupOptions`, `kernel/StartupOrchestrationResult` | | `system-cache-durations-unit-in-key` | `system/CacheAvalanchePrevention:circuitBreaker.resetTimeout`, `system/CacheTier:ttl` | | `system-collaboration-durations-unit-in-key` | `system/CollaborationSessionConfig:idleTimeout`, `system/CollaborationSessionConfig:snapshot.interval` | | `system-failover-health-check-interval-unit-in-key` | `system/FailoverConfig:healthCheckInterval` | | `system-metrics-jsdoc-durations-unit-in-key` | `system/MetricDefinition:summary.maxAge`, `system/MetricExportConfig:interval`, `system/MetricsConfig:collectionInterval`, `system/MetricsConfig:retention.period`, `system/ServiceLevelObjective:errorBudget.burnRateWindows.window` | | `system-metrics-window-durations-unit-in-key` | `system/MetricAggregationConfig:window.size`, `system/ServiceLevelIndicator:window.size`, `system/ServiceLevelObjective:period.duration` | | `system-object-storage-durations-unit-in-key` | `system/AccessControlConfig:maxAge`, `system/StorageConnection:timeout` | | `system-registry-config-durations-unit-in-key` | `system/RegistryConfig:cache.ttl`, `system/RegistryUpstream:syncInterval`, `system/RegistryUpstream:timeout` | | `system-tracing-otel-exporter-durations-unit-in-key` | `system/OpenTelemetryCompatibility:exporter.batch.exportTimeout`, `system/OpenTelemetryCompatibility:exporter.batch.scheduledDelay`, `system/OpenTelemetryCompatibility:exporter.timeout`, `system/TracingConfig:performance.exportInterval` | | `system-tracing-span-duration-unit-in-key` | `system/Span:duration` | | `system-worker-queue-rate-limit-duration-unit-in-key` | `system/QueueConfig:rateLimit.duration` | | `tenant-schema-cache-ttl-unit-in-key` | `system/SchemaLevelIsolationStrategy:performance.schemaCacheTTL` | | `training-deadline-keys-retired` | `system/TrainingCourse:durationMinutes`, `system/TrainingCourse:validityDays`, `system/TrainingPlan:gracePeriodDays`, `system/TrainingPlan:recertificationIntervalDays`, `system/TrainingPlan:reminderDaysBefore` | | `training-family-retired` | `system/TrainingCategory`, `system/TrainingCompletionStatus`, `system/TrainingCourse`, `system/TrainingPlan`, `system/TrainingRecord` | | `websocket-durations-unit-in-key` | `api/WebSocketConfig:pingInterval`, `api/WebSocketConfig:reconnectInterval`, `api/WebSocketConfig:timeout`, `api/WebSocketServerConfig:heartbeatInterval` | ## Prose corrected (the single-entry sites of `5854110917` / `5854456343`, and the rationale sentences) | site (at this head) | was | now | |---|---|---| | `packages/spec/src/migrations/registry.ts:78–86` (step 17 docblock, fact correction only) | 「Mechanical, and mechanical only … there is no semantic residue and the `semantic` list is deliberately empty」 | the three renames replay losslessly as D2; they carry no D3 entry because step 17 shipped before the rule and was not back-filled; the `semantic` list is NOT empty. ⛔ No step-17 entry added. | | `packages/spec/src/migrations/registry.ts:5256` (step 18 rationale, `tenancy.organizationField`) | 「The conversion is a lossless delete and there is no semantic residue」 | a lossless delete still leaves the author a judgment, carried by `object-tenancy-organization-field-retired` | | `packages/spec/src/conversions/registry.ts:3460` (`datasource-driver-mongo-to-mongodb`, protocol 17) | 「Why D2 and not D3」 | 「Why the data repair is D2」, plus: losslessness does not decide whether a family owes D3; this one is protocol 17 and has none | | `packages/spec/src/conversions/registry.ts:9312` (`api-endpoint-cache-ttl-to-cache-ttl-seconds`) | 「gets a conversion rather than a semantic entry」 | 「also gets a conversion」, and names its D3 entry | | `packages/spec/src/conversions/registry.ts:9804` (`list-view-sort-string-clause-to-array`) | 「which is why this is a D2 conversion rather than a semantic TODO」 | the data repair is D2; the family's D3 entry carries the clauses the rewrite leaves alone | | `packages/spec/src/migrations/entries/retired-keys/18.api__ApiEndpoint__cacheTtl.ts:11–19` | 「a D2 CONVERSION rather than a semantic entry」 | also a D2 conversion, and names the D3 entry | | `packages/spec/src/migrations/entries/semantic/18.metadata-endpoints-switch-radius-repartitioned.ts:11–13` | 「exactly the residue D2 cannot express, which is why this is a semantic entry」 | that residue is why there is no D2 at all; the D3 entry is owed either way | | `packages/spec/scripts/build-migration-registry.ts:276` | 「a major whose semantic residue is genuinely nil (protocol 14)」 | an empty region is a real state (a freshly opened step, or protocol 14's, which predated the rule) | ⛔ Not touched: the governed texts (ADR-0087, `.claude/skills/spec-property-retirement/SKILL.md` §3), which are objectstack-ai#20188's. ## The census pin **Where:** `packages/spec/src/migrations/migrations.test.ts` › `registry integrity`: `from protocol 18 on, every graduated D2 conversion is named by a D3 entry of its own step (ruling B)`, plus a control test. It reads the major's `entries/semantic/` files (comment and literal) inside its own package; `check:migration-registry` already proves those files and the generated region are one set. **What it asserts:** for every step whose `toMajor` is 18 or later, every id in `conversionIds` appears as a whole id in at least one `semantic/` entry of that major. A new major-18 (or later) retirement that lands a D2 conversion with no D3 entry naming it goes red, naming the conversion. **What it cannot see**, stated so a green run is not over-read: (1) whether the naming entry is that family's OWN (a passing mention satisfies it; the census judged ownership by reading); (2) a family retired with no conversion at all (no machine-readable link joins a retired key or def to its D3 entry; the census paired those by reading, and found none missing). Protocol 17 is outside the pin by design: measured with the same logic, 53 of its 57 graduated conversions are named by no step-17 entry, and step-17 backfill is out of scope (triage `5854164872`). **Reverse verification (one-shot, no permanent test file).** At `c8656ad35`, with the entries committed: deleted `18.object-tenancy-organization-field-retired.ts` (absence confirmed on disk before the run), ran the pin: `× from protocol 18 on …` with `+ "protocol 18: object-tenancy-organization-field-removed"`, `Tests 1 failed | 140 skipped`. Restored with `git checkout HEAD -- PATH` (that path) inside a `trap … EXIT INT TERM`: blob `2cf3007d9fff` equals HEAD's, `git diff HEAD` empty. Direction observed: red, the expected one. No build involved: the test imports `src/` and reads the entry files directly. ## Patch round 1 (contract review `5857834457`: FAIL at `3197fce29`) **Blocking: fixed.** `Lint & Repo Gates` step 189 (`check-issue-citations.mjs`, judging pass) was red. Four bare citations this PR added answer 404 on the board: objectstack-ai#10329, objectstack-ai#10926, objectstack-ai#12868 and objectstack-ai#14676. Each was in an entry's leading comment, and again in the regenerated region. `--probe-cause` classes all four as **deleted** (the web endpoint also answers 404, so none was transferred), so none of them is a reference to another repository to qualify. Each comment now anchors to the commit in this repository's history that retired the family, and says in words what that commit decided. That is the precedent of commit `66e266c93` (ruling C+D on objectstack-ai#19123). Every sha is an ancestor of `origin/main`: | entry | was | now anchored to | |---|---|---| | `mapping-lookup-params-retired` | objectstack-ai#10329 | commit `15d58dbf1` (the import path never read the four lookup steering params) | | `translation-component-submit-label-retired` | objectstack-ai#10926 | commit `d173125fb` (the copy key left with its only declarer, `element:form`) | | `form-view-option-default-retired` | objectstack-ai#12868 | commit `c459da6bc` (the ruled narrowing: the form-view face drops per-option `default`, the object-field face keeps it enforced) | | `connector-error-mapping-retired` | objectstack-ai#14676 | commit `13c48c2a5` (eleven inert keys, one spelled like the live `userMessage` channel) | Only the comments changed; no string an author is shown moves. The region was regenerated with `gen:migration-registry`. The round-1 report's `pnpm check:issue-citations :: exit 0` was the package script, which runs only the `--self-test`. The judging pass CI runs was exit 2 at `3197fce29` (8 findings = 4 numbers × 2 sites) and is exit 0 now (below). **Pin message.** The census pin's assertion now names the unnamed `protocol N: conversion-id` pairs and the remedy: add a D3 `semantic` entry of that step whose text names the conversion id as a whole word. Its logic and scope are unchanged. Shown firing at `21418c4d2` with one entry removed (trap-guarded restore, blob equal to HEAD's, `git diff HEAD` empty): `AssertionError: graduated D2 conversion(s) named by no D3 entry of their own step: protocol 18: object-tenancy-organization-field-removed. Remedy: add a D3 semantic entry of that step …`, `Tests 1 failed | 140 passed`. **Body.** Table 1: `integration/Connector:connectionTimeoutMs` moved from row 16 to row 20. Its own comment names `connector-connection-timeout-ms-removed`; the permission id appears there only as a comparison. The code was already right. ## Sibling PRs - **PR objectstack-ai#20238** (objectstack-ai#20161) has since LANDED as `6a6a17b62`, with the D2 conversion `report-joined-chart-removed` and `18.ui-report-joined-chart-retired.ts`, which names that id. The union of this head with `main` at `6a6a17b62` is clean and passes the pin (37 pairs, 0 unnamed; delta review `5859315908`). - `main` was merged three times with `os-regen-merge.sh`, and never by hand in a generated region: at `a70cd62e5` (objectstack-ai#20223 and objectstack-ai#20245), at `21418c4d2` (`cel-predicate-one-value-comparand-refused` and `filter-query-face-comparands-refused-at-save`) and at `a930cacea` (step-17 rationale prose from objectstack-ai#20268). Each is D3-only or prose, with no new step-18 conversion. Regeneration produced a commit only after the first merge (`3197fce29`) and changed nothing after the other two. Every sibling entry id was verified present. ## Verification (head `a930cacea`) - `pnpm check:issue-citations && node scripts/check-issue-citations.mjs`, exactly as CI runs it (base `origin/main`): **exit 0**, 112 citations across 29 files: 106 resolve, 6 cross-repo unjudged, 0 findings. At `3197fce29` the same command exited 2. - `pnpm --filter @objectstack/spec build` under the verify lock: ok. `check:generated`: all 15 generated artifacts up to date. `check:migration-registry`: current (292 semantic, 214 retired-key, 199 retired-def). `spec-changes.json` and `docs/protocol-upgrade-guide.md` do not move: they project up to the current protocol major, and step 18 is beyond it. - `pnpm --filter @objectstack/spec exec vitest run --project local`: **552 files, 16257 passed, 1 todo**. The `src/migrations/` directory alone: 3 files, 151 passed. - `pnpm --filter @objectstack/spec typecheck` (tsc, scripts, test layer) at `21418c4d2`, the head before the last merge, which brought only another PR's prose into this diff's files: exit 0, test-typecheck debt unchanged (53 files / 255 errors / 142 signatures). - `node scripts/pm/dispatch-gates.mjs --commands` (no paths) at `a930cacea`, every command run and its exit recorded, reconciled with `--ran`: **89 derived, 87 run (all exit 0), 2 NOT MEASURED**. `check:dual-build-cjs-loads` and `check:type-check-debt` exited 3 (PREREQUISITE NOT MET: the full 86-package workspace build does not fit the foreground cap on this shared box). CI runs both. `check:pm-dispatch-gates` finished this time: exit 0, in 907.6 s. - ESLint, narrowed and proven: all 31 changed `.ts` files, `--no-inline-config --format json`: 0 errors, 0 warnings, none reported ignored. `eslint.config.mjs` enables no type-aware linting (its own statement at `eslint.config.mjs:326–328`), so this diff cannot move any untouched file's verdict. - Changeset: `@objectstack/spec` `patch`. The published registry text changes; no accept set moves. ## Acceptance notes (observed, not filed) - `registry.ts:108` (released step-17 text) still says the sharing-rule `full` conversion 「leaves no semantic residue」: triage scoped step-17 backfill out, so it is left as is. - New entries keep tracker numbers in their `//` comments only, never in the strings an author is shown (AGENTS.md runtime-strings rule). Several older entries do cite numbers in `reason`; not touched. - `dashboard-refresh-interval-unit-in-key` states the console renderer's release lag as a verification step, not as a present fact: this container has no objectui checkout at the pin to measure it. - `main` moved after the last merge (`a930cacea`). objectstack-ai#20285 (`2aa25efb4`, prose in five semantic entries) and objectstack-ai#20238 (`6a6a17b62`, a new step-18 conversion with its entry) landed under `migrations/` and `conversions/`. The delta review merged this head onto `6a6a17b62`: clean, with all six regions still mirrored. The queue verifies the merged generation. (Corrected by the seat at 2026-09-27T19:57Z; the earlier wording said nothing under those paths had moved.) --- _Generated by [Claude Code](https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ocally, off the checker's own local-env declaration (objectstack-ai#20314) Fixes objectstack-ai#20278 Clause-②: no ## What this changes `dispatch-gates --commands` now lists the diff-scoped issue-citation verdict, bare `node scripts/check-issue-citations.mjs`. That is the run `Lint & Repo Gates` blocks on, and before this change the tool printed it as `⊘ NOT MEASURED`, so a dev's local sweep could reconcile clean while CI went red (measured on objectstack-ai#20268's CI run). The census and every other invocation of the same script keep exactly the classification they had. This implements option A of the seat ruling on the card (ruling comment 5858896081): two files, one declaration and one reader. ## Why the family was NOT MEASURED The `&&` in `pnpm check:issue-citations && node scripts/check-issue-citations.mjs` was already split into two families. The cause is the **env carrier**: the lint.yml step passes `GITHUB_TOKEN: ${{ github.token }}` and `OS_GATE_MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}` through `env:`. `workflowEnvValues` treated both as values with none outside CI, so `discoverFamilies` set `notRunnable` and `commandsFor` dropped the family. The script needs neither on a pull-request run: - The merge-group base renders empty on `pull_request`, and the checker then uses `merge-base origin/main HEAD`. - The token is used when present, and the diff mode reads the public board with one probe per added number. A rule about the variables alone cannot fix this. Dropping the job token and the merge-group base generically was measured to also admit the same script's `--census` (a report-only enumeration of the whole board) and `check-required-contexts.mjs --verify-required-set` (exit 2 here). The fact belongs to the program and to the mode, so the program declares it. ## The change (2 files) 1. **`scripts/check-issue-citations.mjs`**: one whole-line comment in the existing `dispatch-gates:` marker idiom: `// dispatch-gates: local-env GITHUB_TOKEN OS_GATE_MERGE_GROUP_BASE_SHA -- REASON`. The reason states the file's own facts. It covers the bare invocation only; the census needs the token and is not declared. 2. **`scripts/pm/dispatch-gates.mjs`**: - `local-env` joins `PATH_LIST_MARKER_KEYS`, the LIST-then-reason grammar. It gets the wholeness reading, the lookalike probe and the census by construction. There is no new grammar copy, no table and no gate. - `declaredLocalEnv` reads the declaration. It refuses a cut reason and any token that is not an env name. - `localEnvRefusal` grades the declaration against the tree's workflows. It throws, so the CLI answers `derivation failed` (exit 2), when either: - a declared name is not passed by every step running the bare invocation as an `env:` workflow value; - no workflow runs the bare invocation. - `localEnvAdmitted` spends the declaration on the bare key alone. `--census`, `--self-test`, `check:*` keys and every other argv admit nothing. - `workflowEnvValues` gains one per-NAME limb: only the declared names leave the list, so an undeclared name still keeps the family out. - The matched row carries `localEnv`, in `--json` and in a same-line note in the human block, so no rendering lists the command without saying why. - The docblock states what the reader does NOT do: guess; admit a name the workflow does not pass; admit an argv no workflow runs; touch any other invocation of the same script; repair an argv value. - Self-test: - Direction pins on the lint.yml step text: the bare command is in the runnable union and not in the not-runnable set. - The census invocation stays not-runnable on `GITHUB_TOKEN`. - Controls: an undeclared script keeps the old reading, and a partial declaration keeps the undeclared name. - Refusals for a stale name, a name only some steps pass, and a missing bare run. - Live in-process pins on this tree for the `rls.zod.ts` specimen. - The key roster and marker census moved from six to seven keys and from seven to eight declarations, each row named. - No absolute counts. `package.json` is untouched. Renaming `check:issue-citations` was optional in the triage notes and would not make the diff-scoped run runnable: the derivation keys on what lint.yml runs, not on the alias name. ## Evidence **`--commands` for `packages/spec/src/security/rls.zod.ts`.** Before is base `3f86dc52`, after is head `15ab041f`. Stdout gains exactly one line; nothing else on stdout moves. ```text before dispatch-gates --commands: 71 command(s) — 42 pnpm, 29 direct node (65 matched by path, ...) before + 7 famil(ies) matched by path take a VALUE FROM THE WORKFLOW and are NOT above ... before ⊘ NOT MEASURED — scripts/check-issue-citations.mjs --census before ⊘ NOT MEASURED — scripts/check-issue-citations.mjs after stdout line 12: node scripts/check-issue-citations.mjs after dispatch-gates --commands: 72 command(s) — 42 pnpm, 30 direct node (66 matched by path, ...) after + 6 famil(ies) matched by path take a VALUE FROM THE WORKFLOW and are NOT above ... after ⊘ NOT MEASURED — scripts/check-issue-citations.mjs --census ``` The human block now renders the row as `- node scripts/check-issue-citations.mjs [lint.yml] matched via packages/spec/src/security/rls.zod.ts ⇢ gate source 'packages/**' · runs here without GITHUB_TOKEN, OS_GATE_MERGE_GROUP_BASE_SHA: its script declares local-env for this bare invocation, and CI passes them through the step's env:`. **The command it now lists answers the question here.** Measured in a detached probe worktree at `3f86dc52` with an uncommitted edit, since restored and removed: - `objectstack-ai#16608` re-added on `rls.zod.ts:426`: exit 2, `[allocated-but-absent] packages/spec/src/security/rls.zod.ts:426 objectstack-ai#16608`, the finding from objectstack-ai#20268's CI run. - Control with a resolving number: exit 0. - Control with `GITHUB_TOKEN`/`GH_TOKEN` unset: exit 0. **Self-test, through the verify lock.** - `pnpm check:pm-dispatch-gates` at `15ab041f`: `os-verify-lock: VERDICT command-exit 0 · held the lock 975s (16m15s) · waited 0s · ⚠ SHARED-BOX SECONDS`. Its log reads `✓ dispatch-gates self-test: 1976 cases pass.` and `✓ check:pm-dispatch-gates --self-test: the exit contract holds in all three directions.` - An earlier `node scripts/pm/dispatch-gates.mjs --self-test` at `23973549` also answered `VERDICT command-exit 0`, 1976 cases. **Ablations.** Both legs used `scripts/ablation-replace.mjs`, which checks the anchor hit and that the blob changed, then restores with blob == HEAD and an empty `git diff HEAD`. - Leg 1, declaration disabled (`// dispatch-gates: local-env GITHUB_TOKEN` → `// ablated-marker: local-env GITHUB_TOKEN`, blob `49bf630d` → `64ab1b63`): `--commands` for `rls.zod.ts` loses `node scripts/check-issue-citations.mjs` from stdout (0 hits), and stderr prints `⊘ NOT MEASURED — scripts/check-issue-citations.mjs` again. Restored: blob == HEAD `49bf630d`. - Leg 2, stale name planted (`... OS_GATE_MERGE_GROUP_BASE_SHA PR_BODY --`): the CLI exits 2 with `derivation failed — dispatch-gates: scripts/check-issue-citations.mjs:204 declares local-env PR_BODY, which the step(s) running node scripts/check-issue-citations.mjs do not all pass as a workflow value through env: ...`. Restored: blob == HEAD. **Derived gate sweep for this diff.** Ran `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` with no paths, 2 paths against merge base `3f86dc52`. It derived 33 families, and each was run with its exit code captured before any pipe. Reconciled at head `15ab041f`: ```text Run reconciliation — 33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN. ✓ dispatch-gates --ran: 33 derived famil(ies) accounted for — 33 run, 0 NOT-MEASURED (a DERIVED zero — all 33 recorded an exit code and none of them is 3). ``` All 33 exit 0 at `15ab041f`, among them `node scripts/check-issue-citations.mjs`, `pnpm check:issue-citations`, `pnpm check:pm-dispatch-gates`, `pnpm check:watch-hint-literal`, `pnpm check:nul-bytes` and `pnpm check:declared-population-live`. The first sweep, at `23973549`, was red on one family: `pnpm check:watch-hint-literal` exit 1, `every live declaration is a literal -- scripts/pm/dispatch-gates.mjs:ROOT_DIR_WATCH_HINTS`. A self-test fixture string used that rostered name. `15ab041f` renames the fixture constant, and the whole sweep was then re-run on that head. `pnpm check:pm-skill-id-lint` (named by the dispatch) also ran and exited 0: `34 file(s) clean`. **Lint, a narrowed run.** `eslint --no-inline-config --format json` on the two changed files at `15ab041f` exited 0. The JSON reports 2 files, 0 errors and 0 warnings. The config `--print-config` reports for `scripts/pm/dispatch-gates.mjs` carries no `parserOptions.project` and no `projectService`, so linting is not type-aware and this diff cannot move the verdict on any untouched file. Repo-wide `pnpm lint` is CI's. **Changeset: `skip-changeset`.** - Both files are in the repo-root package, which is `"private": true` with no `files[]`. - `git grep` for `declaredLocalEnv`, `localEnvRefusal` and the marker over `packages/**` and `apps/**`: 0 hits. - Positive control: `defineStack` hits `packages/spec/src/stack.zod.ts`, which `@objectstack/spec`'s `files[]` ships (`src/**/*.zod.ts`). **Line budget.** - `scripts/pm/dispatch-gates.mjs`: 29507 → 29866 lines (1,746,809 → 1,768,400 bytes). - `scripts/check-issue-citations.mjs`: 1213 → 1215 lines (65,982 → 66,904 bytes). No gate added, no ceiling moved, no table added. ## Acceptance notes - The census row, `node scripts/check-issue-citations.mjs --census`, is still printed NOT MEASURED on every card in the checker's `packages/**` population. Its only workflow, `half-state-patrol.yml`, is path-filtered to the checker and patrol files, and the step is report-only. The ruling records this as a note, and it is unchanged here. - `check-required-contexts.mjs --verify-required-set` refuses here for a proxy-route reason (exit 2, HTTP 401), while dispatch-gates names `env GITHUB_TOKEN`. The ruling records this as a note, and it is unchanged here. - The census row's existing sibling note ("a DIFFERENT invocation of the same script …") now names the bare diff run as that sibling. The wording holds as written: a different argv asks another question. --- _Generated by [Claude Code](https://claude.ai/code/session_01MjvgiFAmjHqsxy1XLiVYfH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ADR-0066 and the data skill (objectstack-ai#20298) Fixes objectstack-ai#20275 Clause-②: no Two governed texts said the RLS write check wrong. ADR-0066's combination rule (item 3) called row policies "OR-combined (any matching policy admits the row)" for every operation; the published data skill's RLS section called `using` the read filter and `check` the write filter, with no stand-in rule, no per-row statement and no refusal rule. Both now say what `main` enforces, in the wording the schema already carries. objectstack-ai#20268 landed the non-governed half (the `rls.zod.ts` overview and describe texts, the docs pages); this PR is the governed half and waits on the maintainer's hand (Tier H: `docs/adr/**` and `skills/**`). No code change. Provenance of the three facts the skill now states: every-row judging of inserts and updates landed in objectstack-ai#19988 and objectstack-ai#20012; the refusal of a non-blank `check` on a `select` / `delete` policy in objectstack-ai#20167; the per-operation default and the default-posture wording in objectstack-ai#20268. ## What changed ### `docs/adr/0066-unified-authorization-model.md` — item 3 of "Precedence / combination semantics" One sentence. It now reads: on a read, the applicable policies' `using` predicates OR-combine (any matching policy admits the row); on an insert or update, the check is chosen once per operation across the applicable policies — the declared `check` predicates when any declares one, else each applicable policy's `using` standing in — and the chosen predicates OR-combine over every row written. The tenant-isolation clause and the superuser-bypass sentence are unchanged; Status, headings and the other items are untouched. The enforcing code is cited as symbol anchors, so `check:adr-symbol-anchors` holds them: - `packages/plugins/plugin-security/src/rls-compiler.ts#RLSCompiler.compileFilter` — read side: OR-combines the applicable policies' `using` (its docblock: "Multiple policies for the same object/operation are OR-combined"). - `packages/plugins/plugin-security/src/security-plugin.ts#writeCheckPolicies` — write side: takes the applicable policies that declare `check`; when none does, the ones that declare `using` (the platform ownership floor kept exactly when the pre-image gate kept it, `keepOwnershipFloor`). `computeWriteCheckFilter` then hands that set to `compileFilter` with the `check` clause, which OR-combines. ### `skills/objectstack-data/rules/security.md` — the "Row-Level Security (RLS)" paragraph and its example comments The paragraph now states, in this order: 1. `using` admits rows — what a `select` policy lets the caller read, and the existing rows an `update`/`delete` policy lets it change or remove; on a read the applicable `using` OR-combine, then AND into the query. 2. `check` is judged on every row an `insert`/`update` writes (array inserts and `multi: true` included; one failing row refuses the write), chosen per operation: when any applicable policy declares `check`, only those decide (OR-combined); else each applicable `using` stands in. 3. A non-blank `check` on a `select`/`delete` policy is refused. 4. The default posture, in the schema overview's words ("Default deny, among the policies that apply … when none applies the policies restrict nothing (the tenant wall still applies)"), plus the one clause the review of the non-governed half named: an `update`/`delete` target with no write-class `using` is bounded by the caller's `select` policies (the by-id pre-image gate derives its scope from the caller's SELECT narrowing when no write-class `using` applies; not for `insert`, not under the read-side superuser bypass). The example's two comments (`// read scope` / `// write scope`) now read `// rows readable / targetable` and `// every row written`. Wording follows the `RowLevelSecurityPolicySchema.using` / `.check` describe texts and the `rls.zod.ts` overview as landed in `3f86dc52`; no second phrasing of the same fact was introduced. No issue or PR number appears in the skill text (`check:doc-authoring` refuses one under `skills/`). ### Paying the token ceiling `check:skills-token-ratchet` holds `security.md` at 2543 tokens with headroom 0. The RLS paragraph grew by 687 bytes and the two comments by 22; the difference is paid in the same file by removing sentences the file already states elsewhere — nothing moved to another file, the ceiling is untouched: - the `permissions`-vs-`permissionSets` bullet no longer repeats the code comment two lines above it (the refusal text, the `ObjectStackDefinitionSchema` source and "never a silent drop" stay); - the `permission_set_id` warning no longer says "record id" twice; - the RLS source line cites `rls.zod.ts` once (policy shape, grammar, `check` composition) instead of `permission.zod.ts` again (already cited under RBAC); - the owner-scoping bullet, the `requiredPermissions` paragraph (its enforcer was already named under `maskingRule`), the platform-global paragraph and its blockquote lose filler words, no facts. ## Readings Line/token budget (tokens = `ceil(utf8 bytes / 4)`, the ratchet's own unit; `3f86dc52` → `4b330f38`): | surface | lines before → after | tokens before → after | |---|---|---| | `skills/objectstack-data/rules/security.md` | 214 → 216 | 2543 → 2541 (ceiling 2543, headroom 2) | | `skills/objectstack-data/**` (17 files) | 3807 → 3809 | 40934 → 40932 | | `skills/**/SKILL.md` (10 files) | 4402 → 4402 | 51903 → 51903 | | whole `skills/` tree (65 files) | 13427 → 13429 | 155990 → 155988 | | ratchet "bundle total (whole shipped tree)" | — | 153970 → 153968 | | `docs/adr/0066-unified-authorization-model.md` | 114 → 114 | 5096 → 5224 (no token ratchet on `docs/adr/**`) | The +2 lines in the skill file are the natural wrapping of a longer paragraph; the gate that prices `skills/**` is the token ratchet, and it went down. Gates — derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` off the merge base at `4b330f38`: 29 commands; every exit code captured before any pipe; `--ran` reconciliation: "29 derived, 29 run, 0 NOT-MEASURED, 0 UNRUN … all 29 recorded an exit code and none of them is 3". - `node scripts/check-skills-token-ratchet.mjs` → 0 — "`skills/objectstack-data/rules/security.md` is 2541 tokens (ceiling 2543; headroom 2)"; `--self-test` → 0 (65 cases) - `node scripts/check-adr-symbol-anchors.mjs` → 0 — "2125 anchors across 140 records resolve"; `--self-test` → 0 - `node scripts/check-adr-links.mjs` → 0; `--self-test` → 0 - `node scripts/check-ci-filter-parity.mjs` → 0 - `node scripts/check-closing-keyword-parity.mjs` → 0; `--self-test` → 0 - `node scripts/check-comment-mask-corpus.mjs` → 0 - `node scripts/check-doc-route-spelling.mjs --advisory` → 0; `--self-test` → 0 - `pnpm --filter @objectstack/lint run check:doc-formula-expressions` → first run exit 3 (PREREQUISITE NOT MET: `@objectstack/formula` and `@objectstack/lint` not built — a refusal, not a measurement); after `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` under `os-verify-lock.sh` (VERDICT command-exit 0, held 240s) → 0 - `pnpm check:adr-anchors` → 0 · `check:agent-test-spelling` → 0 · `check:corpus-claim-drift` → 0 · `check:cross-package-test-inputs` → 0 · `check:doc-authoring` → 0 · `check:driver-memory-census` → 0 · `check:gitlink-declared` → 0 · `check:nul-bytes` → 0 · `check:pm-governed-merges` → 0 · `check:pm-prior-rulings` → 0 · `check:refd-timer-probe` → 0 · `check:role-word` → 0 · `check:skill-compatibility` → 0 · `check:skill-frame-sync` → 0 · `check:skill-identifier-liveness` → 0 · `check:watch-hint-literal` → 0 Reverse verification of the ADR anchors (the gate lists only findings, so resolution was proven by failure): with `writeCheckPolicies` mutated to `writeCheckPoliciesNOPE` in the committed ADR, `check-adr-symbol-anchors` exits 1 with `[unresolved-symbol] docs/adr/0066-unified-authorization-model.md:93`; restored with `git checkout HEAD -- PATH`, `git hash-object` of the path equals the HEAD blob (`01878adb…`) and `git diff HEAD` is empty. Package tests / typecheck: none owed — the diff touches no `packages/**` file, so there is no ① dependency closure and no ② package suite; the whole-repo `pnpm lint` sweep is CI's. Changeset: `skip-changeset` applies. Both paths are outside every published package: 0 of the 69 non-private `package.json` manifests list `skills/`, `docs/adr` or a parent path in `files[]`; positive control — `RowLevelSecurityPolicySchema` is found in `packages/spec/dist/security/index.d.ts` (grep exit 0) and the schema's describe phrase "decided per operation across the applicable policies" in 9 spec dist files (exit 0); negative — the new skill sentence "chosen per operation across the applicable" is in no built output under `packages/` (exit 1). `docs/adr/**` is on the fast track (never published). ## Acceptance notes - The dispatch order's suggested ADR phrasing named "the `using` of the applicable insert-class policies" as the stand-in. The code is wider: `writeCheckPolicies` runs for `insert` and `update` alike (an `all` policy included), and when no applicable policy declares `check`, every applicable policy's `using` stands in for that operation. The landed text says "each applicable policy's `using`", matching `RowLevelSecurityPolicySchema.using` ("on an insert or an update, when no applicable policy for that operation declares `check`, each applicable policy's `using` also stands in"). The triage note's "an insert policy's `using` stands in when no `check` is declared" is one instance of that rule, not the whole rule. - The example policy `org_isolation` (a hand-written `organization_id == current_user.organization_id` select policy) sits beside the file's own Multi-tenancy section ("⛔ never `single` + your own RLS") and duplicates the Layer 0 wall. Left as is: not this card, and the token ceiling was paid without touching it. Observation only, nothing to file. - `check-doc-formula-expressions` refuses (exit 3) on a fresh worktree until `@objectstack/formula` and `@objectstack/lint` are built; its refusal text names the fix. Not a finding. ## 维护者速读(草稿) **改了什么**:两处受管文本各改一段。ADR-0066「优先级/组合语义」第 3 项那一句,从「同一对象/操作的多条行策略 OR 合并(任一匹配即放行)」改为:读侧按 `using` OR 合并;写侧(insert/update)先按操作在适用策略中选出检查——有声明 `check` 的只用它们,否则每条适用策略的 `using` 顶上——再 OR 合并,逐行判定写出的每一行;并以符号锚引用 `compileFilter` 与 `writeCheckPolicies`。发布技能包 `objectstack-data` 的 RLS 段改写为四句:`using` 放行哪些行;`check` 逐行判定 insert/update 写出的每一行(数组插入与 `multi: true` 包含)、按操作选定、无 `check` 时 `using` 顶上;`select`/`delete` 策略上的非空 `check` 被拒;默认姿态(只在有策略适用时默认拒绝;无策略适用则不限制,租户墙照旧;update/delete 目标行在没有写侧 `using` 时受调用者的 `select` 策略约束)。示例块两行注释同步。 **为什么改**:这两段是 AI 写 RLS 策略时读的唯一说明,原文把 `check` 当成读过滤的对偶、且不写顶替规则,按它写出的策略与运行时真实执行不一致(NORTH-STAR 优先级规则 4:写给 AI 的文档与 skills 说错一句等于产品缺陷)。执行代码本身是对的,非受管文本已由 objectstack-ai#20268 修正;本 PR 只改受管的两处,不动代码。 **风险与代价(含回滚)**:纯文本;不改 schema、不改运行时。`security.md` 的 token 上限为 2543、余量 0,新增内容以删除同文件重复句付账(2543 → 2541),上限未动、未挪内容到别的文件。29 个派生门禁全绿,ADR 符号锚经反向验证(改坏符号名即变红)。回滚即 revert 本 PR 的单个 commit,无迁移、无数据影响。 **席位意见**:(留空) **你要做的**:审阅两处措辞后在本 PR 上 Approve(Tier H);落地由席位执行。 --- _Generated by [Claude Code](https://claude.ai/code/session_01MjvgiFAmjHqsxy1XLiVYfH)_ Co-authored-by: objectstack-fleet[bot] <noreply@anthropic.com>
Fixes #19967
Clause-②: no
Text only. No schema shape, accepted value or runtime behaviour moves. Every sentence below was re-measured against the code on
origin/mainat0d3ec47137, where PR #19962 (b5853da1ca), PR #19988 (009da14713), PR #20012 (44639665ee, #19989) and PR #20167 (b276d4463f) are all merged. Eachgit merge-base --is-ancestorprobe returned exit 0.What
mainenforces (the measurement the texts now state)Write
checkselection, per operation.writeCheckPolicies(packages/plugins/plugin-security/src/security-plugin.ts:890-899) returns the policies that declarecheckwhen any applicable policy does. Otherwise it returns every applicable policy with ausing, and keeps the platform ownership floor only where the write's row gate kept it.compileFilter(rls-compiler.ts:600-602) readscheckfor a policy that declares one andusingfor the rest, then OR-combines (:676).A USING-only
insertpolicy'susingis the insert check when no applicable policy declarescheck. Pinned byrls-check-defaults-to-using.test.ts, case "an insert-class USING-only policy gates INSERT the same way", which is green on this head.A check-only
updatepolicy is legal and enforced. The schema accepts it (rls.zod.ts, the at-least-one rule). The row gate derives the write scope fromselectwhen no write-classusingapplies (security-plugin.ts:7018), and the post-image check judges the written rows. Pinned bycheck-only-write-scope.test.ts, which is green. The showcase'sinvoice_owner_immutableis one such policy.A non-blank
checkonselect/deleteis refused (rls.zod.ts, thesuperRefineat thecheckpath, from PR feat(spec)!: refuse acheckon aselect/deleterow-level security policy #20167).OR-combination. On reads, and on the rows an update or delete may target, the applicable policies'
usingclauses OR-combine (compileFilter). On the written rows, the check is chosen first and then OR-combined. So "most permissive wins" is true on reads and false as a statement about writes.Post-image scope: every insert and update shape. The seam is installed for every insert and for every update (
security-plugin.ts:3191,:3261,:3508). The engine runs it:beforeInsert(packages/objectql/src/engine.ts:12470);beforeUpdate(:14085, PR fix(plugin-security, objectql)!: a by-id update's row-level check holds for the row it stores #20012);multi: trueupdate (:14345, PR fix(plugin-security, objectql): a row-level check holds for every row of an array insert and a predicate update #19988).The by-id update is also judged in the middleware on the change set as sent (
security-plugin.ts:3116). Pinned byrls-check-multi-row-writes.test.ts,rls-check-by-id-update-post-hook.test.tsandinsert-check-post-image.test.ts, all green on this head.Sites, old text, new text, and the code that makes the new text true
rls.zod.tscheckdescribemulti: trueupdate are not post-image checked."beforeInserthooks leave it, and each row an update changes, by id ormulti: true, as the prior row merged with the final payload after itsbeforeUpdatehooks. One failing row refuses the whole write. A by-id update is also judged, before its hooks, on the prior row merged with the change set as sent."engine.ts:12470,:14085,:14345;security-plugin.ts:3116rls.zod.tscheckTSDocmulti: trueupdate are not post-image checked."secretfield's stored reference, an absent tenant column).engine.ts, the comment above:12470)rls.zod.tscheckTSDoc, floorcomputeWriteCheckFilter: amulti: trueupdate has no by-id gate and keeps the floor unless the OWD yields (platformFloorYieldsToObjectWriteModel)rls.zod.tsusingdescribeselectpolicy lets a caller read, and the existing rows anupdateordeletepolicy lets a caller change or remove. On an insert or an update, when no applicable policy for that operation declarescheck, each applicable policy'susingalso stands in as its check on every row written ...; on aninsertpolicy that is its only effect. ... Needed on aselectordeletepolicy (acheckthere is refused); optional on aninsert,updateorallpolicy that declarescheck."writeCheckPolicies:896;getApplicablePolicies:846; thecheckrefusalrls.zod.tsusingTSDocupdatedoes not requireusing: a check-onlyupdatepolicy is accepted and enforced, and its target rows come from the otherupdate/allusingor fromselect. Aninsertpolicy'susingfilters nothing, but it is the insert check when none is declared.security-plugin.ts:7018;check-only-write-scope.test.tsrls.zod.tssuperRefinemessageselect/deletetake "using";inserttakes "check", or a "using" alone as that check when no applicable insert policy declares one;update/alltake either or both.rls.zod.tsschema TSDoccheck)compileFilter;writeCheckPoliciesrls.zod.tspriorityTSDocRLSCompiler.compileFilterand this schema's own former describe both say most-permissive-wins)"rls.zod.tsoverview, "Default Deny"compileFilter:656(applicable === 0returnsnull, no filter);content/docs/permissions/rls.mdxcalloutmigrations/registry.ts--from 16prose (and regenerateddocs/protocol-upgrade-guide.md)liveness/permission.jsoncheck/usingevidencecompileFilter"(policy as { check?: string }).check ?? policy.using" (the expression is gone)security-plugin.ts#writeCheckPoliciesplusrls-compiler.ts#compileFilter, with the live expressioncheck:livenessresolves both anchors (green)security-plugin.tswriteCheckPoliciesdocblockRowLevelSecurityPolicySchema.check: "defaults to USING clause if not specified"."RowLevelSecurityPolicySchema.checkwithout quoting it, so it cannot go stale. PostgreSQL's per-policy rule is named as the starting point.rls-check-defaults-to-using.test.tsheadercheckholds the write post-image to itsusing, which is whatRowLevelSecurityPolicySchema.checkpublishes: "defaults to USING clause if not specified"."check, eachusingstands in. The choice is per operation, not per policy.writeCheckPoliciescontent/docs/permissions/authorization.mdx:108-109usingon reads and on the rows a write may target. The written-rowcheckis chosen per operation first. Links the fail-closed contract.In-place fixes beyond the claim's file surface (same defect class: the same stale sentences)
The dispatch asked for a grep for other copies of the old sentences. These hits are not governed and not pending changesets, so they are fixed here. File-surface supplement for the claim:
content/docs/permissions/rls.mdx,content/docs/protocol/objectql/security.mdxandpackages/spec/src/conversions/registry.ts(TSDoc only). The two changesets below are also outside it.content/docs/permissions/rls.mdx:63: "after a single-record insert or a by-id update; an array insert and amulti: trueupdate are not checked" now says every row an insert or update writes.content/docs/protocol/objectql/security.mdx:144: the same parenthetical.usingis no longer "for SELECT/UPDATE/DELETE" only.packages/spec/src/conversions/registry.ts: thepermission-rls-priority-removedTSDoc said "most permissive wins". Itssummary(which feedsspec-changes.json) is unchanged and not false.packages/spec/src/security/rls.test.ts: theprioritytest comment said "(most permissive wins)".Pin sweep for the
superRefinemessage① The repo-wide grep for
At least one ofandprovide "using"finds pins only inpackages/spec/src/security/rls.test.ts:toContain('At least one of')and its negation. Both still hold, because the head is unchanged. No other package, doc or translation carries the message.② New load-bearing pins, in the
RowLevelSecurityPolicySchema — the "at least one" refusalblock ofrls.test.ts. For each of the five operations they assert the refusal'scode(custom),path([]) and head, that every operation is named, and that the false sentence is absent. They then prove each prescription against the schema itself:select/delete+using,insert+check,insert+usingalone, andupdate/allwith check only, using only, and both. All parse.Pending release notes corrected (DELIBERATE CORRECTION:
check-empty-changesetis red by design).changeset/19953-rls-check-default-composition-text.md(from PR fix(spec, lint): state the RLS check default per operation across the applicable policies, as the runtime applies it #19962). It said: "The check runs on the new row of a single-record insert and of a by-id update. An array insert and amulti: trueupdate are not post-image checked; those are tracked in [finding] security: a row-levelcheck(declared, or defaulted fromusing) is never evaluated on an ARRAY insert —engine.insert(object, [rows]), whichcreateManyDatacalls, stores rows a single-record insert refuses #19964 and security: acheck-only row-level policy does not gate a bulk update (update(…, { where, multi: true })): the post-image check is skipped as "governed by the using-scoped where", and nousingexists to scope it #19950, and the texts now say so". That would ship false. fix(plugin-security, objectql): a row-level check holds for every row of an array insert and a predicate update #19988 and fix(plugin-security, objectql)!: a by-id update's row-level check holds for the row it stores #20012 land in the same release, and this PR changes the texts it says "now say so". The rewrite dates the old scope to when that change was written, and states the release's scope..changeset/rls-check-defaults-to-using.md(from PR fix(plugin-security): a USING-only RLS policy holds INSERT and UPDATE rows to its using #19952). It said: "RowLevelSecurityPolicySchema.checkreads "defaults to USING clause if not specified"". The describe no longer reads that in the release this note ships in (PR fix(spec, lint): state the RLS check default per operation across the applicable policies, as the runtime applies it #19962). Changed to "read ... (it now states the default per operation across the applicable policies, spec(security): RLScheckdefaults tousingper policy in the contract, but the write check ignores a USING-only policy when a sibling declarescheck— state the composition, or rule OR semantics #19953)". The triage on [finding] After PR #19962 rewrites the RLScheckdescribe, three texts still quote the old "defaults to USING clause if not specified" contract, and theusingtext never says it is the insert check when nocheckis declared #19967 raised this note for the dev to judge.The gate's remedy is to say so here and get it confirmed. Restoring either note from base would put the false sentence back.
Reported only, not edited
docs/adr/0066-unified-authorization-model.md:93: "Multiple row policies for the same object/operation are OR-combined". This is the ADR sentence thatauthorization.mdxparaphrases, and it has the same false-for-writes reading.skills/objectstack-data/rules/security.md:72-75: callsusingthe "read filter" andcheckthe "write filter", and does not state the stand-in check.docs/adr/0095-authz-kernel-tenant-layer-and-posture-ladder.md:79-81: about the read filter, and not false.packages/spec/CHANGELOG.md:46934,:73373andpackages/plugins/plugin-security/CHANGELOG.md:6610,:10225. These say "(the schema's own describe says most-permissive-wins)". That was true of the describe when they shipped.packages/spec/spec-changes.json:79,:971: the generatedsummary"policies OR-combine". It is shorthand and not false. It is regenerated from the conversion registry, which this PR leaves as is.Published surface
@objectstack/spec:patch. It shipssrc/**/*.zod.ts,dist,livenessand the--from 16prose.@objectstack/plugin-security: no changeset. The two edits are a comment in a non-exported function and a test header. Measured: tsup strips in-body comments. A positive control inpackages/objectql/dist/index.jsshows the code linepostHookWriteImageCheck.honoured = truepresent (1 hit) and the comment above it, "INSERT POST-IMAGE seam", absent (0 hits).writeCheckPoliciesis not exported, so no.d.tscarries its docblock.Verification
Every reading below was taken on head
e2bd8f3680, the final commit, with a clean tree.Gate union.
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived the list.--ranreconciles it: "116 derived famil(ies) accounted for — 112 run, 4 NOT-MEASURED". The 116 includes 2 families the derivation added after the regeneration commit (pnpm --filter @objectstack/spec run check:generatedandpnpm check:quick-reference-counts). Both were run: exit 0.node scripts/check-empty-changeset.mjs --base origin/mainexits 1 with the DELIBERATE CORRECTION class, for the two notes named above.check:skill-examplesneeds a built@objectstack/client-react(a 36-package closure);check:dual-build-cjs-loadsneeds a fullpnpm build;check:i18nneeds the built CLI closure;check:type-check-debtneeds a whole-workspace build.@objectstack/specbuild: exit 0, DTS included.check:generated: 2 of 15 stale (docs/protocol-upgrade-guide.md,content/docs/references/**). Regenerated with exactlygen:upgrade-guide && gen:docs, then "All 15 generated artifacts are up to date".@objectstack/spectests:vitest run --project local --maxWorkers=2: "Test Files 547 passed (547) · Tests 16086 passed | 2 todo".@objectstack/plugin-securitytests:vitest run --maxWorkers=2: "Test Files 139 passed (139) · Tests 2839 passed (2839)". This includes the semantics pins cited above:rls-check-defaults-to-using(22 cases),check-only-write-scope(21),rls-check-multi-row-writes(32),rls-check-by-id-update-post-hook(24) andinsert-check-post-image(25), all green.@objectstack/plugin-securitytypecheck: exit 0, test layer included ("check:test-typecheck: OK").@objectstack/spectypecheck: declared narrowing. The fullpnpm --filter @objectstack/spec typechecknever got a turn on the shared verify lock in about 40 minutes of queueing: 8 attempts, each exit 99. Its three legs are covered as follows:rls.zod.ts,migrations/registry.tsandconversions/registry.ts;check:test-typecheckis green insidecheck:generatedon this head, andtsconfig.test.jsonincludessrc/**/*.test.ts;check:scripts-typecheck: no spec script is touched.CI's
TypeScript Type Checkruns the full command.eslint, a proven narrowing:
eslint.config.mjslints**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}minus build output, which contains all 6 changed.tsfiles.pnpm exec eslint --no-inline-config --format jsonover those 6 files reports 6 files, 0 errors, 0 warnings, exit 0.parserOptions.project, so no rule is type-aware, and this diff cannot move a verdict on an untouched file.Control bytes:
check:nul-bytesis green. The self-scangrep -naPfor C0 control characters and DEL over the changed files exits 1 with no match.Acceptance notes
check: the schema and the runtime disagree (observation, not filed; no public-door measurement, no named producer). The at-least-one rule tests!data.check, socheck: ' 'satisfies it. The runtime and theselect/deleterefusal read a blank clause as absent (policyDeclaresClause). So a policy with only a whitespacecheckparses and is inert. Theusingdescribe's "needed on aselectordeletepolicy" is worded as the runtime reads it.@objectstack/lintrls-predicate-*messages understate the scope (packages/lint/src/validate-rls-predicate-enforceability.ts:272,:318,:943,:969). They say "the single-record INSERT check" and "every single-record insert and by-id update ... fails". Since PR fix(plugin-security, objectql): a row-level check holds for every row of an array insert and a predicate update #19988 and PR fix(plugin-security, objectql)!: a by-id update's row-level check holds for the row it stores #20012 this is true but narrow: array inserts andmulti: trueupdates are refused too. These are not false, and they are outside this card's file surface. Carrier: none.Generated by Claude Code