Repository navigation
docs(approvals): reassign TSDoc holds slot addresses; ADR-0042 §2 superseded in part by ADR-0118 D1; SLA checklist expects no actor - #21556
Conversation
…erseded line; SLA checklist expects no actor - spec ApprovalActionRow: reassign_from / reassign_to hold the slot address in its stored spelling (a user id, an email, or a position address); the *_name companions resolve only for a user id or an email an account carries. - ADR-0042: status line records §2's reserved actor `system:sla` as superseded in part by ADR-0118 D1 (machine actions record actor_id null; the escalate row is the attribution). - platform checklist approvals.sla-escalation (revision 2): the escalate row carries no actor. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM
…proval-actor-texts
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 138 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 22deb8f42377a842a3b6ca4b5f56248741178458 && git checkout 22deb8f42377a842a3b6ca4b5f56248741178458
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6cf1154a65ffcae3fdfe607a5d221572157ee353 bfc5e472c2717552b7d1226dd7fb78af6cebc348 && git checkout -B drift-repro 6cf1154a65ffcae3fdfe607a5d221572157ee353 && git merge --no-ff bfc5e472c2717552b7d1226dd7fb78af6cebc348
node scripts/docs-audit/affected-docs.mjs --json 6cf1154a65ffcae3fdfe607a5d221572157ee353 |
Contract reviewServed-tier: Inputs read, and nothing else: card #21517 (body; comments ① Derived judgmentsThe diff is 4 files, +34/−11: one new changeset, one ADR status line, one checklist item revised, one TSDoc block. No schema, export, type, runtime or test line changes, so the diff implies no accept-set change and one public-surface text change: the TSDoc that ships in
Check-runs on the head, read at 2026-10-03T06:52Z: 24 success, 3 skipped ( ② Semver level
③ Boundary flagsDev flags, from report
Implemented-by: VERDICT: PASS Generated by Claude Code |
ACCEPT — PR #21556 @
|
维护者速读(终稿)— PR #21556
改了什么:三处与审批「谁做的」有关的过时文字,一张卡、一个 PR(分诊裁定,
为什么改:PR #21514(#21455)按 ADR-0118 D1 把审批里的「系统操作人」改成了空值、把转办字段改成了槽位地址;这三处文字还停在旧说法上。 风险与代价(含回滚):
席位意见:建议批准。改动最小,三处都已对照运行时代码核实。ADR 只加状态行、不改历史决策,符合 ADR 的记录方式。若你希望在 §2 正文旁再加一条注记,可以在批准时说明。 你要做的:在 PR #21556 上点 APPROVE(批准后由本坐席清标签并落地)。 Generated by Claude Code |
Approval received on PR #21556: this seat lands it
Generated by Claude Code |
…ion in words instead of a tracker number (stage 5) (objectstack-ai#21568) Part of objectstack-ai#20749 Clause-②: no Stage 5 of the `domain:spec` lane's share of the runtime-string burn-down (ruling `5902360492`, form D): the rest of class (b), the protocol 17 → 18 conversion summaries in `packages/spec/src/conversions/registry.ts`. Every rewritten summary now states in words what the cited decision was, or drops a citation its sentence already explained. Text only. ## What changed - **35 ADR-0087 conversion summaries** (40 tracker ids, 34 distinct cards): every toMajor-18 summary that carried an id, from `field-malformed-scale-precision-removed` to `flow-decision-mode-inclusive-explicit`. A summary is what `os migrate meta --json` reports under `specChanges` (its chain already runs to protocol 18, `CHAIN_TERMINUS_MAJOR`), and it becomes the upgrade guide's "Change" column and the `to` text of `spec-changes.json`'s `converted[]` once protocol 18 ships, so an author upgrading metadata reads it. - One `@objectstack/spec` **patch** changeset, `Clause-②: no` (message text only). - **No generated file changes** (A2 below): no generator projects a toMajor-18 summary today. ## Census at the base (A1) Stage 4's instrument (`convtable.cjs`, byte-identical copy, md5 `9b7539067ffc38598172c692de637db5`) at base `e901c27449` (the worktree before any edit): 127 conversions, **35 summaries with ids, 40 id occurrences, 34 distinct cards** — stage 4's split unchanged. The toMajor-18 conversion that PR objectstack-ai#21547 added carries no id. Under the ~60-card bar, so one stage. The stage-3 census re-run at the same base agrees (class b: 35 messages / 40 ids; nothing else in class b). | file:line (base) | id | conversion | |---|---|---| | `registry.ts:6551` | objectstack-ai#8321 | `field-malformed-scale-precision-removed` | | `registry.ts:6659` | objectstack-ai#8762 | `record-chatter-position-vocabulary` | | `registry.ts:6777` | objectstack-ai#9198 | `element-input-target-variable-removed` | | `registry.ts:7024` | objectstack-ai#9220 | `element-filter-removed` | | `registry.ts:7177` | objectstack-ai#9249 | `element-form-removed` | | `registry.ts:7356` | objectstack-ai#15178 | `translation-per-app-settings-removed` | | `registry.ts:7356` | objectstack-ai#19620 | `translation-per-app-settings-removed` | | `registry.ts:7610` | objectstack-ai#9249 | `translation-component-submit-label-removed` | | `registry.ts:7782` | objectstack-ai#3951 | `field-column-lists-canonicalized` | | `registry.ts:7783` | objectstack-ai#9227 | `field-column-lists-canonicalized` | | `registry.ts:7909` | objectstack-ai#10414 | `metric-filters-removed` | | `registry.ts:8104` | objectstack-ai#17296 | `cube-sub-day-granularities-removed` | | `registry.ts:8237` | objectstack-ai#18612 | `cube-join-sql-and-relationship-removed` | | `registry.ts:8502` | objectstack-ai#10054 | `record-highlights-field-icon-removed` | | `registry.ts:8759` | objectstack-ai#11027 | `page-component-responsive-removed` | | `registry.ts:8860` | objectstack-ai#11805 | `object-grid-default-sort-removed` | | `registry.ts:9047` | objectstack-ai#21445 | `object-grid-resizable-columns-removed` | | `registry.ts:9250` | objectstack-ai#17260 | `object-kanban-quick-add-removed` | | `registry.ts:9634` | objectstack-ai#12497 | `permission-allow-restore-purge-removed` | | `registry.ts:9637` | objectstack-ai#1883 | `permission-allow-restore-purge-removed` | | `registry.ts:9954` | objectstack-ai#6837 | `field-reference-to-alias` | | `registry.ts:10372` | objectstack-ai#14478 | `hook-timeout-to-timeout-ms` | | `registry.ts:10413` | objectstack-ai#14478 | `job-timeout-to-timeout-ms` | | `registry.ts:11017` | objectstack-ai#14478 | `api-endpoint-cache-ttl-to-cache-ttl-seconds` | | `registry.ts:11086` | objectstack-ai#14478 | `dashboard-refresh-interval-to-refresh-interval-seconds` | | `registry.ts:11447` | objectstack-ai#14478 | `memory-persistence-auto-save-interval-to-ms` | | `registry.ts:11671` | objectstack-ai#14478 | `turso-config-timeout-to-timeout-ms` | | `registry.ts:11761` | objectstack-ai#17063 | `view-page-mount-removed` | | `registry.ts:11866` | objectstack-ai#17053 | `list-view-sort-string-clause-to-array` | | `registry.ts:11868` | objectstack-ai#8221 | `list-view-sort-string-clause-to-array` | | `registry.ts:12366` | objectstack-ai#19054 | `object-tenancy-organization-field-removed` | | `registry.ts:12476` | objectstack-ai#20085 | `view-item-owner-hidden-removed` | | `registry.ts:12620` | objectstack-ai#20230 | `view-overlay-owner-hidden-removed` | | `registry.ts:13214` | objectstack-ai#17321 | `page-component-filter-record-to-rule-array` | | `registry.ts:13214` | objectstack-ai#6206 | `page-component-filter-record-to-rule-array` | | `registry.ts:13463` | objectstack-ai#20161 | `report-joined-chart-removed` | | `registry.ts:13728` | objectstack-ai#20221 | `form-layout-inline-grid-to-vertical` | | `registry.ts:13884` | objectstack-ai#19992 | `currency-config-precision-removed` | | `registry.ts:13988` | objectstack-ai#20321 | `permission-rls-tags-removed` | | `registry.ts:14128` | objectstack-ai#15429 | `flow-decision-mode-inclusive-explicit` | ## Projections (A2) Neither generator projects a toMajor-18 summary at this base. `build-spec-changes.ts` and `build-upgrade-guide.ts` both loop `major` from `MIGRATION_SUPPORT_FLOOR + 1` to `PROTOCOL_MAJOR`, which are 16 and 17 here (`PROTOCOL_VERSION = '17.0.0'`), so `spec-changes.json` carries one `perMajor` record (16 → 17) and the guide one "Protocol 16 → 17" table. `check:generated` reads all 15 artifacts up to date on this head with no regeneration, so no generated file is in the diff. Both projections will pick these summaries up when protocol 18 ships. ## Delivered: each site, the decision read, the new words (A3) Every cited card was read through REST with all its comments (30 objectstack cards, objectui#3951, objectstack-ai#6206, objectstack-ai#6837, objectstack-ai#8221). The record column names the comment the decision was read from. Where a summary already said why, the citation is dropped and the sentence kept; where an `(#N, ADR-0049 — …)` opener cited both, the card number goes and the ADR stays, as stage 4 did. In the `cube-join-sql-and-relationship-removed` row, `ALIAS` stands for the angle-bracket placeholder in the source. | conversion (head line) | cited | decision as read (record) | summary now reads | |---|---|---|---| | `field-malformed-scale-precision-removed` (:6550) | objectstack-ai#8321 | refuse a malformed scale/precision at the producer (z.number().int().min(0)); a stored malformed value takes the D2 strip so the row stays loadable; citation dropped, the sentence already said it (body + ACCEPT 5296942541) | malformed field 'scale'/'precision' declarations (non-integer or negative) are removed — they were silently unenforced; the schema now refuses them at authoring | | `record-chatter-position-vocabulary` (:6658) | objectstack-ai#8762 | the row's vocabulary converges on the renderer's bottom/right/left: one vocabulary, no mapping layer; the three old spellings take a conversion (ruling 5299771841) | record:chatter / record:discussion 'position' respelled to the renderer's vocabulary — 'sidebar' → 'right', 'inline' → 'bottom', 'drawer' → 'right' (one vocabulary, the renderer's, rather than a mapping layer between two: the renderer compares only bottom/right/left, and the old set fell through every branch) | | `element-input-target-variable-removed` (:6778) | objectstack-ai#9198 | ADR-0049 enforce-or-remove: verdict dead (a declarative hint with zero readers), retired with tombstones and a D2 conversion (ACCEPT 5311252358 (PR body verdict)) | text-input/record-picker component prop 'targetVariable' removed (retired under ADR-0049 enforce-or-remove as a declarative hint nothing read; the live binding resolves from the page variable whose `source` names the component id) | | `element-filter-removed` (:7025) | objectstack-ai#9220 | dead at ELEMENT grain (no renderer anywhere; Studio excludes it from the palette), so the whole element retires under ADR-0049, not key by key (verdict 5312176877 + ACCEPT 5312709553) | the whole 'element:filter' element retired (ADR-0049 enforce-or-remove at element grain, not key by key — no renderer for it ever shipped in any repo, so every key was a capability claim nothing kept; list surfaces own their filtering via a view's userFilters / the list filter builder). All six props are stripped; the bare node the conversion leaves is refused by name at the parse, with the prescription to delete the component | | `element-form-removed` (:7179) | objectstack-ai#9249 | dead at element grain, the objectstack-ai#9220 precedent: the whole element retires; the palette already names object-form as the replacement (dev report 5384430470 (verdict re-taken in the PR body)) | the whole 'element:form' element retired (ADR-0049 enforce-or-remove at element grain, not key by key — no renderer for it ever shipped in any repo, so every key was a capability claim nothing kept; use the object-bound 'object-form' block instead — rendered and designer-publishable). All six props are stripped; the bare node the conversion leaves is refused by name at the parse, with the prescription to delete the component | | `translation-per-app-settings-removed` (:7358) | objectstack-ai#15178, objectstack-ai#19620 | objectstack-ai#15178: the bundle type splits, the platform bundle keeps `settings`, a per-app bundle refuses it (settings is a platform key). objectstack-ai#19620 ruling B: `settings` leaves the translation item too, because the file door and the item door are two authoring surfaces of one app metadata type and accept one shape (ruling 5653315643 (objectstack-ai#15178); ruling 5770445203 (objectstack-ai#19620)) | translation group 'settings' removed from both application-authored faces, the per-app bundle entry and the registered translation item: settings copy belongs to the platform, and the two authoring doors of one application translation type accept one shape. It is keyed by SettingsManifest.namespace and only platform code declares a manifest. A per-app bundle entry could only fill gaps the platform's own bundle left in the one merged served tree, and was overwritten wherever both defined the key; a stored item OVERRODE the platform copy, because the runtime-authored layer is read over the shipped bundles. Overrides now give way to the platform copy, gaps fall back to the manifest literal, and the group stays on the PLATFORM bundle, PlatformTranslationData | | `translation-component-submit-label-removed` (:7613) | objectstack-ai#9249 | `element:form` retired whole because no renderer for it ever shipped (dead at element grain), which left `submitLabel` with no carrier (dev report 5384430470) | translation component-copy key 'submitLabel' removed (retired rather than re-anchored — its only declared carrier, 'element:form', retired whole because no renderer for it ever shipped, so the resolver no longer overlays it and a stored string was read by nothing; the live form surface's submit copy is 'object-form''s 'submitText', localized at its own authoring site, and re-anchoring the key there would only have added a second place to translate one word) | | `field-column-lists-canonicalized` (:7787) | objectui#3951, objectstack-ai#9227 | objectui#3951: the published spec spelling `name` wins and the grid reader is fixed to read it. objectstack-ai#9227: `inlineColumns` gets a strict name-keyed element schema (an unknown key is a named rejection at publish, not a blank cell); `relatedListColumns`, checked in the same pass, takes field-name strings (ruling 5236150020 (objectui#3951); ruling 5315735776 + ACCEPT 5317488979 (objectstack-ai#9227)) | inline-grid column entries respelled 'field' → 'name' (the declared spelling wins, and the grid renderer now reads 'name' too) and related-list column objects folded to their child field-name string (both lists were z.any(), so a mis-keyed column published clean and rendered blank cells; inline columns now take a strict name-keyed shape and related-list columns plain field names, so a mis-keyed column is refused at publish) | | `metric-filters-removed` (:7916) | objectstack-ai#10414 | the remove leg of enforce-or-remove: zero consumers (measured with a positive control) and a raw-SQL carrier; retire per the playbook; citation dropped, the sentence already said it (triage grading 5363699539) | cube metric key 'filters' removed (ADR-0049 — no strategy ever read it: the authored raw-SQL condition was parsed and dropped, and the query returned the unfiltered aggregate. Filter at query time with `where`, or use an ADR-0021 dataset measure's structured `filter`; a metric's own `sql` is a column reference) | | `cube-sub-day-granularities-removed` (:8111) | objectstack-ai#17296 | each of second/minute/hour is residue and removed: no layer outside the enum names them and `queryDateGranularity` cannot advertise them; ADR-0049 prefers removal with no committed roadmap; citation dropped (dev report 5648182389 + landing 5648923185) | cube dimension granularities 'second' / 'minute' / 'hour' removed (ADR-0049 — no backend bucketed them and none could advertise them: `supports.queryDateGranularity` is a record over `DateGranularity`, which declares day, week, month, quarter, year. Offer the coarsest interval that still answers the question) | | `cube-join-sql-and-relationship-removed` (:8244) | objectstack-ai#18612 | retire `sql` and `relationship` from CubeJoin: the join is derived from the FK relationship and no author-supplied ON clause executes; the addendum adds the D2 strip for persisted artifacts; citation dropped, the sentence already said it (ruling 5725370783 + addendum 5727426171) | cube join keys 'sql' and 'relationship' removed (ADR-0049 — neither was ever read: both strategies synthesise the ON clause as a foreign-key equality, so an authored join condition was REPLACED under a 200 and a declared cardinality changed no SQL. Keep `joins.ALIAS.name` alone; the record KEY is the foreign-key field on the base object) | | `record-highlights-field-icon-removed` (:8509) | objectstack-ai#10054 | option A: measured dead (zero read points, not designer-publishable), so it retires under the ADR-0087 flow; citation dropped (ruling 5364978909) | record:highlights highlight-field key 'icon' removed (ADR-0049 — no render path: the highlight chip has no icon slot, the register hook carries field names only, and the Studio designer publishes the field list as plain strings, so an authored icon was accepted and drawn by nothing) | | `page-component-responsive-removed` (:8766) | objectstack-ai#11027 | ruling B: retire `page.components[].responsive` (ADR-0049, wired into no renderer) and repair the texts that redirected authors to it (ruling 5380752244) | page component key 'responsive' removed (ADR-0049 enforce-or-remove — no renderer ever applied per-component breakpoint layout overrides, and the shared ResponsiveConfig shape leaves with its last carrier; use responsiveStyles (ADR-0065) for breakpoint behaviour that IS applied) | | `object-grid-default-sort-removed` (:8868) | objectstack-ai#11805 | retire object-grid `defaultSort` (the strict route per the playbook), completing the objectui-side direction ruling at the producer (ruling 5404972152) | object-grid component prop 'defaultSort' removed (retired under ADR-0049 enforce-or-remove as the legacy single-sort second spelling of 'sort', read only when 'sort' was absent; the pair moves to sort: [{ field, order }], the array shape every read path honours) | | `object-grid-resizable-columns-removed` (:9055) | objectstack-ai#21445 | `resizable` is canonical and `resizableColumns` retires now as a tombstone naming it: zero writers, so no window (immediate retirement) (triage direction 5958164933) | object-grid component prop 'resizableColumns' removed (the legacy second spelling of 'resizable', read only when 'resizable' was absent, retires at once so 'resizable' is the one spelling; the value moves to 'resizable' when that is absent, and is deleted when it is present) | | `object-kanban-quick-add-removed` (:9258) | objectstack-ai#17260 | option B: `quickAdd` leaves `object-kanban` (accepted and dropped there); this repo carries the tombstone half (card body (the objectui ruling it executes, option B) + triage 5620331176) | object-kanban component prop 'quickAdd' removed (retired from the board under ADR-0049 enforce-or-remove — the affordance is gated on a host-supplied 'onQuickAdd' function no producer puts on an object-kanban node, so the key was accepted and dropped; delete the key — object-kanban offers no quick-add control) | | `permission-allow-restore-purge-removed` (:9643) | objectstack-ai#12497, objectstack-ai#1883 | option B: retire `allowRestore` / `allowPurge`, which gate operations that do not exist; objectstack-ai#1883 stays open as the M2 anchor, where undelete/purge ship as feature + RBAC in one batch and the keys return with it (card body (objectstack-ai#12497); ruling 5421209848 (objectstack-ai#1883)) | object-permission keys 'allowRestore' and 'allowPurge' removed (ADR-0049 — the `restore`/`purge` operations they claimed to gate have never existed, so granting the bits delivered nothing; dispatched destructive lifecycle verbs stay denied fail-closed. The keys return with the M2 lifecycle initiative, which builds undelete and purge together with the permission bits that gate them) | | `field-reference-to-alias` (:9962) | objectui#6837 | ruling C: protocol normalisation belongs to the server and the frontend only executes the protocol; half 1 (this repo) guarantees the serve path carries only `reference`, half 2 deletes objectui's legacy fallback arms (ruling 5475017957 + half-1 pointer 5475055291) | field key 'reference_to' → 'reference' (the legacy objectql runtime dialect for a lookup/master_detail target; normalising to the protocol is the server's job and the renderer only executes the protocol, so stored rows must serve the canonical spelling before objectui deletes its `reference ?? reference_to` fallback arms) | | `hook-timeout-to-timeout-ms` (:10383) | objectstack-ai#14478 | ruling B: a duration-shaped number key carries its unit in its name (or a unit-carrying value), every existing offender renamed under an ADR-0087 conversion, no grandfathered baseline (ruling 5518649320 + population ruling 5548763981) | hook key 'timeout' → 'timeoutMs' (a duration key carries its unit in its name, and this one's unit lived only in the description; the value, milliseconds, is unchanged) | | `job-timeout-to-timeout-ms` (:10424) | objectstack-ai#14478 | as above (as above) | job key 'timeout' → 'timeoutMs' (a duration key carries its unit in its name, and this one's unit lived only in the description; the value, milliseconds, is unchanged) | | `api-endpoint-cache-ttl-to-cache-ttl-seconds` (:11028) | objectstack-ai#14478 | as above (as above) | api endpoint key 'cacheTtl' → 'cacheTtlSeconds' (a duration key carries its unit in its name, and this one's unit lived only in the description; the value, seconds, is unchanged, and the key stays GET-only) | | `dashboard-refresh-interval-to-refresh-interval-seconds` (:11097) | objectstack-ai#14478 | as above (as above) | dashboard key 'refreshInterval' → 'refreshIntervalSeconds' (a duration key carries its unit in its name, and this one's unit lived only in the description; the value, seconds, is unchanged) | | `memory-persistence-auto-save-interval-to-ms` (:11458) | objectstack-ai#14478 | as above (as above) | memory datasource key 'config.persistence.autoSaveInterval' → 'autoSaveIntervalMs', on both the file and auto arms (a duration key carries its unit in its name, and this one's unit lived only in the description; the value, milliseconds, is unchanged) | | `turso-config-timeout-to-timeout-ms` (:11682) | objectstack-ai#14478 | as above (as above) | turso datasource key 'config.timeout' → 'config.timeoutMs' (a duration key carries its unit in its name, and this one's unit lived only in the description and a .meta() title no parse reads; the value, milliseconds, is unchanged) | | `view-page-mount-removed` (:11772) | objectstack-ai#17063 | the maintainer chose to retire (「撤」) over finishing the objectui render half or parking it: the `page` member and its mount leave the spec under enforce-or-remove (card body (the maintainer ruling it records)) | list-view type 'page' and its `pageName` binding removed (retired rather than finished: the delegating render half was never built, so a page view fell through to the grid branch and drew an empty table; ADR-0049 enforce-or-remove) | | `list-view-sort-string-clause-to-array` (:11877) | objectstack-ai#17053, objectui#8221 | objectui#8221 option B: the legacy string `sort` is retired, one spelling platform-wide, the array. objectstack-ai#17053: the spec slot that produces those documents stops accepting the string objectui now refuses (triage 5620223775 (objectstack-ai#17053); ruling 5567944420 (objectui#8221)) | the bare string list-view `sort` clause becomes the `{ field, order }[]` array (one sort orthography platform-wide, the array: objectui already refuses the string, so the schema stops minting documents its own consumer refuses) | | `object-tenancy-organization-field-removed` (:12377) | objectstack-ai#19054 | take `organizationField` off the authorable surface; its one real use stays a platform-internal fact; citation dropped, the sentence already said it (card body (the maintainer ruling it records)) | object `tenancy.organizationField` removed (ADR-0049 — the stamp-only column declaration was authorable by every application and declared exactly once in the whole protocol, on the platform's own credential table; the divergence moves to a platform-internal table in @objectstack/metadata-core and stops being a knob) | | `view-item-owner-hidden-removed` (:12487) | objectstack-ai#20085 | retire both keys (ADR-0049 enforce-or-remove, zero pull) via the retirement playbook; citation dropped (triage direction 5826969296) | view item keys 'owner'/'hidden' removed (ADR-0049 — declared on the view item record and stored verbatim, read by nothing: no view switcher ever filtered on `hidden`, and no per-user scope ever read `owner`, so a view marked as one user's was listed for everyone) | | `view-overlay-owner-hidden-removed` (:12631) | objectstack-ai#20230 | follow objectstack-ai#20085's disposition for the same key pair on the overlay door: the same retirement (triage direction 5856621469) | flattened view overlay keys 'owner'/'hidden' removed (ADR-0049 — the view item's pair on the overlay door, retired the same way: declared, accepted by the write door and stored verbatim, read by nothing, so a `hidden: true` overlay hid no view and an `owner` scoped none) | | `page-component-filter-record-to-rule-array` (:13220) | objectui#6206, objectstack-ai#17321 | objectui#6206 option B: one filter orthography platform-wide, the rule array. objectstack-ai#17321 ruling B: a partial D2 conversion of what maps losslessly; combinator-carrying rows pass through untouched and are named as a TODO (flattening would silently change what a page selects) (ruling 5406409590 (objectui#6206); ruling 5644018752 (objectstack-ai#17321)) | a record-form or single-level AST filter at a converged rule-array door becomes the `[{ field, operator, value }]` rule array wherever the mapping is lossless (flat keys → `equals` rules, `{ $op: v }` → the mapped operator, AST comparisons → one rule each); a filter carrying `$and` / `$or` / `$not` or any part with no lossless rule spelling is left exactly as stored — reported as a TODO, which `os migrate meta --stored` lists — and is not the form its door declares (one filter orthography platform-wide, the rule array; the migration converts only what maps losslessly and names the rest, because flattening a combinator would silently change what a page selects) | | `report-joined-chart-removed` (:13477) | objectstack-ai#20161 | retire, not build block charts: the joined arm refuses a container chart, the block key goes, a non-joined report keeps its live chart; citation dropped (triage direction 5852548444) | a joined report's 'chart' removed from its blocks and refused on the container (ADR-0049 enforce-or-remove: the joined renderer draws each block as a table and never read either, so the chart parsed and nothing was plotted; a non-joined report keeps its live 'chart') | | `form-layout-inline-grid-to-vertical` (:13742) | objectstack-ai#20221 | retire the `inline` / `grid` arms: multi-column already exists as `columns` and `inline` is not a record-form layout; citation dropped, the sentence already said it (triage direction 5855767378) | form 'layout' arms 'inline' and 'grid' rewritten to 'vertical' (ADR-0049 — no renderer ever gave either a behaviour of its own: every form presentation folded both to 'vertical'. Multi-column is 'columns', honoured under either layout, and is left untouched) | | `currency-config-precision-removed` (:13898) | objectstack-ai#19992 | remove `currencyConfig.precision`: a currency's decimal places are the currency's, not a setting; citation dropped, the sentence already said it (triage 5817146460 (ruling 乙 on objectstack-ai#19910 it executes)) | currency field key 'currencyConfig.precision' removed (ADR-0049 — no renderer or runtime ever read it: an amount's decimal places are its currency's ISO 4217 minor unit, derived from the currency itself. Its ISO 4217 contradiction check and the default `2` baked into parse output went with it; the field-level `precision` is a total digit count and is untouched) | | `permission-rls-tags-removed` (:14002) | objectstack-ai#20321 | RETIRE by the maintainer's criterion (no mainstream platform has the capability); citation dropped, the sentence already said it (triage verdict 5860425529) | RLS-policy key 'tags' removed (ADR-0049 — nothing ever read a policy's tags and no mainstream platform tags a row-level policy; dropping it changes no access decision) | | `flow-decision-mode-inclusive-explicit` (:14141) | objectstack-ai#15429 | align with mainstream engines: an edge-branched decision is exclusive (first match), and taking every true edge must be declared (`mode: 'inclusive'`); the migration writes it explicitly for existing nodes so authored behaviour is unchanged (ruling C narrows that promise to sources and artifacts) (ruling 5793803317; ruling C 5863827385) | edge-branched decision with two or more conditioned out-edges and no `mode`: `mode: 'inclusive'` written explicitly (the traversal became exclusive, first match in declaration order, as mainstream engines treat a decision, and taking every true edge must now be declared; the key keeps the every-true-edge behaviour those nodes had, and the author deletes it where the branches partition) | No site was left in place as unclear; `open_questions` is empty. ## Text only (A4) Stage 3's AST-skeleton plus string-text tool (`skeleton.cjs`, TypeScript 6.0.3; stage 4's copy with only its TypeScript load path changed to this worktree, md5 `3b10ec8a7e6284f19def54d35f001698` → `32b4630d7d0a532ee26239319269fe91`). Leg 1 compares an AST skeleton with every string's text masked (a `+` chain of string operands reads as one string, so re-wrapping is invisible); leg 2 compares the text of every string group, requiring each changed group to carry a tracker id before and none after, and every other group byte-identical. - `registry.ts`, base `e901c27449` vs the committed copy at `9c1d040145`: **1 of 1 SAME**, exit 0 — 62656 tokens both sides, 4955 string groups, 35 changed, every changed group carried an id before and carries none after; parse diagnostics 0/0. - Controls on scratch copies of the head file, each mutation counted on disk first (anchor hits 1, replacement present 1, anchor left 0): `renameFlowConfigAliases` renamed → DIFF exit 1; `!==` flipped to `===` → DIFF exit 1; one summary re-split into two `+` operands → SAME, 0 groups changed, exit 0; the `hook.timeout` surface string (never carried an id) changed → skeleton SAME, text leg VIOLATION, exit 1. No repo file was mutated for the controls. - The edits were applied by a script whose every anchor was asserted to hit exactly once, inside its own conversion's summary line span, and verified on disk after the write (36 anchors over 35 conversions). The conversion-table extractor reads 35 summaries changed, `toMajor` / `surface` / declaration unchanged on all 127, and 0 ids left in any summary. ## Pins and quotes (A6) No test asserts a summary, so no pin moves: nothing under `*.test.*` reads `.summary` off a conversion (the only summary reads are `spec-changes.ts` and `build-upgrade-guide.ts`), and every removed id-bearing fragment was searched across the repo. The hits are other files' own prose with their own citations (CHANGELOGs, `migrations/registry.ts` rationale, docblocks, `liveness/*.json` notes, test titles such as `permission.test.ts:278`, docs prose in `content/docs/permissions/*.mdx`), not quotes of a summary. `content/docs/**`: no quote of a changed summary. `skills/**`: none. ## Verification All builds and tests through `scripts/pm/os-verify-lock.sh` (slot `issue-20749-s5`), each `VERDICT command-exit 0`: - `pnpm --filter @objectstack/spec build`, then `check:generated` on the merged head `9a35e049e5`: "✓ All 15 generated artifacts are up to date". - The package `test` script (`vitest run --project local --maxWorkers=2`) on `9a35e049e5`: "Test Files 605 passed (605) / Tests 17904 passed | 1 todo (17905)". - `test:repo`: the 15 repo-project files that read the conversion registry, `spec-changes` or the guide, "Test Files 15 passed (15) / Tests 221 passed (221)". NOT MEASURED: `scripts/build-schemas-check-mode.test.ts` (88 cases at about 7 s each, over the foreground cap; it reads only conversion surfaces, which the proof shows unchanged) and the remaining repo-project files; reason: wall clock on a shared box. CI runs them. - `pnpm --filter @objectstack/spec run typecheck` on `9a35e049e5`: exit 0; "check:test-typecheck: OK — 52 file(s) / 246 error(s) / 135 pinned signature(s) held". - `pnpm turbo run build --concurrency=2 --filter=./packages/* --filter=./packages/*/*`: "Tasks: 71 successful, 71 total", for the gates that read built packages. - Gates: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths) at `9a35e049e5` derives 79 commands; each ran with its exit code written to disk before any pipe. Three first exited 3 (PREREQUISITE NOT MET: `check:doc-formula-expressions`, `check:dual-build-cjs-loads`, `check:lean-entry-closure` need built packages) and exit 0 after the build; the dist-reading gates were re-run after it too. `--ran`: "✓ dispatch-gates --ran: 79 derived famil(ies) accounted for — 79 run, 0 NOT-MEASURED". - `pnpm check:doc-authoring` (self-test and run): exit 0, "17323 customer-facing string(s) across 1250 spec sources clean"; the sibling-package ledger holds its baseline (`packages/spec` sits outside it, so no ledger change). `pnpm check:nul-bytes`: exit 0, "no raw ASCII control bytes". - Changeset gates with this body as the `--event` payload: `check-changeset-no-major.mjs --base origin/main --event` exit 0 ("✓ LEVEL AXIS: this PR declares clause-② `no`", declaration line `Clause-②: no`); `check-partof-closing-keyword.mjs` with `PR_BODY` exit 0 ("no Part-of/closing-keyword contradiction"); `check-adr-0087-registration.mjs --base origin/main` exit 0 ("adds no declared-breaking changeset (1 non-breaking changeset(s) seen)"); `check-empty-changeset.mjs --base origin/main` exit 0; `check-changeset-fixed.mjs` exit 0. - ESLint, a proven narrowing: `eslint --no-inline-config --format json` over the one changed TS file reads 1 file, 0 errors, 0 warnings; the population is read from ESLint's own config (`calculateConfigForFile` resolves it, `isPathIgnored` false); invariance: `eslint.config.mjs` enables no type-aware linting (`parserOptions.project` / `projectService` null for this file, its header at :327-328 says so), so a string-text edit cannot move an untouched file's verdict. Repo-wide `pnpm lint` is CI's. ## Acceptance notes - `origin/main` was merged once (`9a35e049e5`, five commits: objectstack-ai#21539, objectstack-ai#21473, objectstack-ai#21554, objectstack-ai#21556, objectstack-ai#21557; spec moved only in `contracts/approval-service.ts` TSDoc); spec rebuilt, `check:generated`, the spec test project, typecheck and the gate union re-ran on the merged head. No os-regen deferral was recorded. - Hot file: no open PR touches `conversions/registry.ts`, `spec-changes.json` or the upgrade guide (all nine open PRs' file lists read just before opening this one). - Census after this PR (stage 3's instrument at `9a35e049e5`): non-test 160 → 125 messages, 358 → 318 ids; class (b) is empty. Left for the later stages: class (c) conformance-case notes 58 messages / 68 ids (the `objectstack-ai#5322` selector and the `objectstack-ai#8934` name pin move with their tests), class (f) internal registry rationale 17 / 33, the test strings 1804 / 1920 in 425 files; `migrations/registry.ts` 50 / 217 stays with objectstack-ai#20234's stage 11. Word-form hits (an id spelled after "PR", "issue" and the like) stay 6, all outside this diff. - Excluded, untouched: `migrations/registry.ts`, comments anywhere, classes (c) and (f), test strings, the `.mjs` gate scripts. No gate is added or loosened. --- _Generated by [Claude Code](https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21517
Clause-②: no
Three texts still described the approval actor as a sentinel or as a user after the actor-column change landed (
88fb5e85a0). This PR corrects all three, as triage ruled on the card (comment5964851617): one card, one PR, three text positions. Text only: no schema, export, type or runtime change.What changes
ApprovalActionRowreassign TSDoc (packages/spec/src/contracts/approval-service.ts).reassign_from/reassign_towere "the user whose pending-approver slot was moved, and the user who received it", and each*_namewas "sys_user.name, when resolvable".position:NAME; any othertype:valueliteral a slate kept is stored the same way). Likeacted_as, neither is asys_userreference, and neither makes a claim about who made the move: that person isactor_id. A*_nameresolves only when the address names an account: a user id, or an email an account carries. A position address never resolves, so an absent name means "render the address".string) does not change. The rewritten block cites no tracker number; the old block's tracker citation went with the rewrite.ADR-0042 status line (
docs/adr/0042-approval-sla-escalation.md). One continuation line under Status, in the form ADR-0046 uses for a decision superseded in part:ADR-0118 is linked in the file. The decision text is not rewritten: the record keeps what was decided on 2026-06-12, and the status line points to what supersedes it (Prime Directive 13). "Wherever this record names it" covers the TL;DR, Mechanics and Consequences mentions as well as §2 itself.
Platform checklist
approvals.sla-escalation(docs/qa/platform-checklist/areas/approvals.json), revision 1 to 2 with a history entry.escalaterow's actor to beSLA_ACTOR_ID.actor_idis absent on theGET /:id/actionsread and stored null, because a machine action records no actor and theescalaterow is the attribution (ADR-0118 D1). A QA run that followed the old text would report a false failure.Changeset:
@objectstack/specpatch, because the TSDoc ships in the published.d.ts.The texts were checked against the runtime (read at
24dc7c1134)reassign()inpackages/plugins/plugin-approvals/src/approval-service.tsrecordsreassign_from: from, reassign_to: to, the two slot addresses as the slate spells them.sys-approval-action.object.tsdeclares both asField.text, described as "in the slot's stored spelling: a user id, an email, or a position address".listActions()sends only addresses without a:toresolveUserNames. That function looks each one up bysys_user.id, then looks up the ones still unresolved that contain an@bysys_user.email.escalateRequest()insertsactor_id: null.rowFromActionmaps null to undefined, and the REST route answersres.json({ data: rows }), so the key is absent on the read.pnpm --filter @objectstack/spec build, the new sentence is inpackages/spec/dist/contracts/index.d.tsandindex.d.mts, and the old "pending-approver slot was moved" sentence is in nodistfile.Gates
The union was re-run on HEAD
bfc5e472c2, the final commit. That commit is a merge oforigin/mainfd5a1cd597, which touched onlydriver-tursoand a changeset, nothing on this PR's paths.pnpm --filter @objectstack/spec build: exit 0.check-dts-emitted: @objectstack/spec - 38/38 declared declaration file(s) present.pnpm --filter @objectstack/spec check:generated: exit 0.✓ All 15 generated artifacts are up to datepnpm --filter @objectstack/spec typecheck: exit 0.check:test-typecheck: OKpnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: exit 0.Test Files 604 passed (604),Tests 17861 passed | 1 todonode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 86 commands. All 86 were run onbfc5e472c2with their exit codes recorded, then reconciled:✓ dispatch-gates --ran: 86 derived famil(ies) accounted for — 85 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).Among them:pnpm check:doc-authoring:✓ doc authoring guard: 17314 customer-facing string(s) across 1249 spec sources clean — no internal issue-id referencespnpm check:platform-checklist:check-platform-checklist: OK — 15 areas, 269 items (265 active, 2 planned)pnpm check:nul-bytes:check-nul-bytes: OK (scanned 9898 text file(s) ... no raw ASCII control bytes).check-empty-changeset:✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).check-adr-0087-registration:✓ ... this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).check-changeset-no-major:✓ This diff introduces no `major` bump.The level axis was also run offline with this body as the--eventpayload (see the report on the card).pnpm check:dual-build-cjs-loads. Reason:PREREQUISITE NOT MET. The gate reads the built output of all 83 workspace packages, which takes a fullpnpm build. This PR edits one comment block inpackages/spec, and CI's Build Core runs the gate.check-changeset-fixed,check:authz-resolver,check:error-code-casing,check:filter-alias-parity.check:meta-url-spellingandcheck:spec-changesran insidecheck:generated.node scripts/pm/check-governed-merges.mjs --branch claude/issue-21517-approval-actor-texts:⛔ GOVERNED, Tier H, bydocs/adr/0042-approval-sla-escalation.md. Size is 45 changed lines.Acceptance notes
reassign(),fromisinput.from ?? slot ?? actorId. On the admin-rescue branch (the caller holds no slot, andfromis not on the slate), the whole slate is replaced with the one addressto. The audit row then recordsreassign_fromas the caller-namedfromor the admin's own user id, not a slot that was handed over. The new TSDoc states the contract ("the pending-approver slot that was handed over"), which matches the object's field description. This branch was not driven through a public door, so it is noted here and not filed.patchchangeset (Clause-②: no). The TSDoc ships in the published.d.ts, and every changeset gate run passed.维护者速读(草稿)
改了什么:三处文字,全部是说明性文本,不改任何类型、接口或运行时行为。① 审批动作记录的 TSDoc 现在写明:「转办来源 / 转办目标」两个字段存的是审批槽位的地址(用户 id、邮箱或岗位地址),不一定是某个人,显示名只在地址对应到一个账号时才解析得出。② ADR-0042 在状态行加一句:第 2 节「SLA 机器决策记为保留身份
system:sla」已被 ADR-0118 D1 部分取代,机器动作的行为人记为空,由escalate那一行本身说明是 SLA 触发的。③ QA 测试清单里的 SLA 升级用例改为期望escalate行没有行为人。为什么改:运行时早已按 ADR-0118 实现(转办字段存槽位地址,SLA 扫描记空行为人),但这三处文字还是旧说法。AI 或开发者按旧文字写代码、跑 QA,会把岗位地址当成用户去关联,或把正确行为误报成失败。ADR 被推翻却没有状态行,也违反「已接受的 ADR 在被新 ADR 取代前一直有效」的原则。
风险与代价(含回滚):风险很低:只改文字,发布的
.d.ts只有注释变化,附 patch 级 changeset。因为碰了docs/adr/**,本 PR 属于受管面(Tier H),需要您批准才能合入。回滚就是 revert 这个提交,没有数据或迁移影响。席位意见:
你要做的:看一眼 ADR-0042 新加的那一行状态说明,措辞没问题就批准;如果还希望在第 2 节标题下也加一条指向说明,请在 PR 上说一声。
Generated by Claude Code