Skip to content

docs(approvals): reassign TSDoc holds slot addresses; ADR-0042 §2 superseded in part by ADR-0118 D1; SLA checklist expects no actor - #21556

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-21517-approval-actor-texts
Oct 3, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-21517-approval-actor-texts

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 (comment 5964851617): one card, one PR, three text positions. Text only: no schema, export, type or runtime change.

What changes

  1. ApprovalActionRow reassign TSDoc (packages/spec/src/contracts/approval-service.ts).

    • Before: reassign_from / reassign_to were "the user whose pending-approver slot was moved, and the user who received it", and each *_name was "sys_user.name, when resolvable".
    • After: both hold a slot address in its stored spelling: a user id, an email, or a position address (position:NAME; any other type:value literal a slate kept is stored the same way). Like acted_as, neither is a sys_user reference, and neither makes a claim about who made the move: that person is actor_id. A *_name resolves 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".
    • The type (string) does not change. The rewritten block cites no tracker number; the old block's tracker citation went with the rewrite.
  2. 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:

    · Superseded in part (2026-08-02, ADR-0118 D1) — §2's reserved actor system:sla, wherever this record names it: machine actions record actor_id null, and the escalate row is the attribution.

    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.

  3. Platform checklist approvals.sla-escalation (docs/qa/platform-checklist/areas/approvals.json), revision 1 to 2 with a history entry.

    • Before: step 5 and the single-shot clause's verify text expected the escalate row's actor to be SLA_ACTOR_ID.
    • After: both expect no actor. actor_id is absent on the GET /:id/actions read and stored null, because a machine action records no actor and the escalate row is the attribution (ADR-0118 D1). A QA run that followed the old text would report a false failure.
  4. Changeset: @objectstack/spec patch, because the TSDoc ships in the published .d.ts.

The texts were checked against the runtime (read at 24dc7c1134)

  • Reassign write. reassign() in packages/plugins/plugin-approvals/src/approval-service.ts records reassign_from: from, reassign_to: to, the two slot addresses as the slate spells them. sys-approval-action.object.ts declares both as Field.text, described as "in the slot's stored spelling: a user id, an email, or a position address".
  • Names. listActions() sends only addresses without a : to resolveUserNames. That function looks each one up by sys_user.id, then looks up the ones still unresolved that contain an @ by sys_user.email.
  • Escalate row. escalateRequest() inserts actor_id: null. rowFromAction maps null to undefined, and the REST route answers res.json({ data: rows }), so the key is absent on the read.
  • New TSDoc in the build. After pnpm --filter @objectstack/spec build, the new sentence is in packages/spec/dist/contracts/index.d.ts and index.d.mts, and the old "pending-approver slot was moved" sentence is in no dist file.

Gates

The union was re-run on HEAD bfc5e472c2, the final commit. That commit is a merge of origin/main fd5a1cd597, which touched only driver-turso and 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 date
  • pnpm --filter @objectstack/spec typecheck: exit 0. check:test-typecheck: OK
  • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: exit 0. Test Files 604 passed (604), Tests 17861 passed | 1 todo
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 86 commands. All 86 were run on bfc5e472c2 with 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 references
    • pnpm 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 --event payload (see the report on the card).
    • NOT MEASURED: pnpm check:dual-build-cjs-loads. Reason: PREREQUISITE NOT MET. The gate reads the built output of all 83 workspace packages, which takes a full pnpm build. This PR edits one comment block in packages/spec, and CI's Build Core runs the gate.
  • Roster gates whose roster sits in a directory this diff touches were run too, all exit 0: check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity. check:meta-url-spelling and check:spec-changes ran inside check:generated.
  • node scripts/pm/check-governed-merges.mjs --branch claude/issue-21517-approval-actor-texts: ⛔ GOVERNED, Tier H, by docs/adr/0042-approval-sla-escalation.md. Size is 45 changed lines.

Acceptance notes

  • Governed, Tier H. The ADR line makes this PR governed, and triage accepted that. It stays a draft. It lands only on an authorized approval or a human merge.
  • Only the status line, no section note. ADR-0046 also puts a note at the superseded section. The ruling names the status line, so only that line is added. A reader who lands on §2 directly sees no marker there. Adding one is a follow-up for the ADR's owner if wanted.
  • Reassign edge, read in code and not measured. In reassign(), from is input.from ?? slot ?? actorId. On the admin-rescue branch (the caller holds no slot, and from is not on the slate), the whole slate is replaced with the one address to. The audit row then records reassign_from as the caller-named from or 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.
  • Not here: the objectui timeline's "System (SLA)" rendering. Triage ruled it stays PR fix(approvals): every sys_user lookup approvals writes holds an id or null — machine actors record none, notify forwards only a person, reassign parties are slot addresses (ADR-0118 D1) #21514's acceptance note.
  • Release: a patch changeset (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

claude added 2 commits October 3, 2026 05:44
…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
@github-actions github-actions Bot added the size/s label Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 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): node scripts/docs-audit/affected-docs.mjs --json 6cf1154a65ffcae3fdfe607a5d221572157ee353 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 22deb8f42377a842a3b6ca4b5f56248741178458 — the merge of head bfc5e472c2717552b7d1226dd7fb78af6cebc348 into base 6cf1154a65ffcae3fdfe607a5d221572157ee353, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Oct 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bfc5e472c2717552b7d1226dd7fb78af6cebc348
Local-runs: none

Inputs read, and nothing else: card #21517 (body; comments 5964851617 triage grade, the ruling of record; 5966025001 claim; 5966458246 dev report), PR #21556 (body, the 4-file list, the net diff against main at the head, and its one comment, the docs-drift bot), the head's check-runs, and the sources the three texts must agree with, at refs: docs/adr/0118-non-user-actor-contract.md at origin/main 6cf1154a65, PR #21514 (landed 88fb5e85a0, card #21455), and the runtime that writes the reassign and escalate rows at origin/main (packages/plugins/plugin-approvals/src/approval-service.ts, sys-approval-action.object.ts, packages/rest/src/rest-server.ts). Nothing was built, run or re-run.

① Derived judgments

The 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 @objectstack/spec's declaration files. Each position, judged against the runtime at origin/main:

  1. ApprovalActionRow reassign TSDoc — right. reassign() writes reassign_from: from, reassign_to: to, where from = input.from ?? slot ?? actorId and to is the caller's address, both as the slate spells them; the person is actor_id: recordedActor(context). sys-approval-action.object.ts declares both columns as Field.text({ maxLength: 255 }), "in the slot's stored spelling: a user id, an email, or a position address". listActions() resolves a *_name only for an address without a colon, through resolveUserNames (by sys_user.id, then by sys_user.email for an unresolved address carrying an at-sign); every type:value literal is skipped, so a position never resolves. A slate keeps position: and the deprecated role: spelling (POSITION_ADDRESS_PREFIXES), so "any other type:value literal a slate kept is stored the same way" is the runtime's own comment at the insert, restated. "Like acted_as, neither is a sys_user reference" matches the object. The optional string types are unchanged, and the tracker citation the old block carried is gone, as the house rule asks. One reading recorded, not a fault: resolveUserNames falls back to the account's email when it has no name, so "display name (sys_user.name)" is the same approximation the old text made.

  2. ADR-0042 hunk (governed, Tier H) — right on all four counts.

    • Status line only. The hunk is one added line, line 4, between **Status** and **Deciders**. The TL;DR, §1 to §3, Mechanics, Consequences and References are byte-identical to main; no decision text is rewritten.
    • House form. ADR-0046 line 4 reads · **Superseded in part (2026-06-13, ADR-0048)** — the bare-name overwrite premise; note at §3.2. The new line has the same shape, · **Superseded in part (2026-08-02, ADR-0118 D1)** — what is superseded, with the ADR id as a relative link, the form ADR-0005, ADR-0017 and ADR-0055 use in their **Amended** lines. It carries no "note at §2" pointer because no section note is added, which is honest.
    • True to ADR-0118's text. ADR-0118's status is Accepted on 2026-08-02, so the date is right. D1 says every actor column pointing at sys_user stores null for a system-initiated write, forbids every sentinel string, and makes "System" a render rule rather than data; "machine actions record actor_id null" states that decision correctly for system:sla. "The escalate row is the attribution" is the wording the ruling of record prescribes (triage 5964851617, the card's proposed line); it rests on ADR-0042 §1's own marker and on ADR-0118 D5 (which automation acted is answered by an association, never by the actor column), and nothing in it contradicts ADR-0118.
    • Runtime agrees. escalateRequest() inserts the escalate row with actor_id: null; the auto-decision calls decide() under SYSTEM_CTX naming SLA_ACTOR_ID as its acting identity, and recordedActor(SYSTEM_CTX) is null, so nothing records the sentinel (SLA_ACTOR_ID's docblock: never recorded). "Wherever this record names it" is needed and true: the TL;DR, §2, Mechanics and Consequences still say system:sla, and the line covers them without rewriting them.
    • The PR is a draft (draft: true, auto_merge: null); landing needs an authorized approval, which this record does not give.
  3. Checklist approvals.sla-escalation — right. Step 5 and the single-shot clause now expect no actor on the escalate row, absent on the read and stored null. Stored: actor_id: null, above. Read: rowFromAction maps row.actor_id ?? undefined, and the REST handler for GET /approvals/requests/:id/actions answers res.json({ data: rows }), so the key is absent in the JSON. Revision 1 to 2 with a history entry in the file's own ref convention; the only SLA_ACTOR_ID left in the file is that entry's description of the old text. A run following the old text would have reported a false failure, as the card said.

  4. Changeset — judged in ②.

Check-runs on the head, read at 2026-10-03T06:52Z: 24 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), 6 in progress (Lint & Repo Gates, Test Core 1, 2, 4 and 5 of 6, Type Check · workspace), 0 failed. Among the successes: Governed Surface Queue Guard, Check Changeset, Check Documentation Links, Spec property liveness, Build Core (which hosts check:dual-build-cjs-loads), Type Check · source gates, and the three card-and-branch claim gates. An in-progress gate is read as in progress, not as a pass; the landing waits on them whatever this record says.

② Semver level

@objectstack/spec patch — right. The diff publishes exactly one thing, the rewritten TSDoc, which ships in the package's declaration files, so skip-changeset (for a diff that publishes nothing from any released package) would be wrong, and patch is the floor. Nothing is added to or removed from the exported shape: reassign_from, reassign_to and both *_name members stay optional string, so no minor is owed and no clause-② arm applies. The PR body's Clause-②: no line is right for the same reason, and the changeset's own Clause-②: no agrees with it. The changeset body says what the text said before and what it says now, names no tracker, and Check Changeset is green.

③ Boundary flags

Dev flags, from report 5966458246 and the PR's Acceptance notes, each answered:

  • open_questions: [] — nothing to answer.
  • Labels (deviation 1): dispatch named none; documentation, size/s and tooling are the auto-labeler's. No action.
  • Commit trailer (deviation 2): a1c75e6813 ends with the model-free pair AGENTS.md names; the merge commit carries none. Compliant.
  • Checklist ref: "#21517" (deviation 3): the item's history entries already cite cards (#7331, #7530), and check:doc-authoring covers spec runtime strings, not docs/qa/**. Compliant.
  • Status line only, no §2 note: the ruling of record names the status line, and only that line is added; a marker under §2 is the maintainer's option at approval, which the PR's 维护者速读 already asks about. Within the ruling.
  • Reassign admin-rescue branch (out-of-scope finding, carrier: acceptance note): verified in code. When the caller holds no slot and from is not on the slate, next = [to] and the row records reassign_from as the caller-named from, or the admin's own id when none was given. The stored value is still a slot address in its stored spelling (a user id), so the TSDoc's shape claim holds; its "slot that was handed over" is loose on that one branch. It was read in code and not measured through a public door, so the acceptance-note carrier is the same disposition the card gave the objectui rendering. Answered; a measured finding would be a new card on the approvals runtime, not this PR's.
  • objectui "System (SLA)" rendering: triage ruled it stays PR fix(approvals): every sys_user lookup approvals writes holds an id or null — machine actors record none, notify forwards only a person, reassign parties are slot addresses (ADR-0118 D1) #21514's acceptance note. Not this card's.
  • NOT MEASURED check:dual-build-cjs-loads: it needs the full workspace build; Build Core runs it on the head and is green. Accepted.
  • Tier H: the PR touches docs/adr/**; it is a draft, not readied, queued or armed (Governed Surface Queue Guard green). It lands only on an authorized approval or a human merge. This record approves nothing and merges nothing.

Implemented-by: claude/issue-21517-approval-actor-texts
Reviewed-by: session_01YDt3PzwfrkuFzUBF89WPmM

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT — PR #21556 @ bfc5e472 (GOVERNED, Tier H: awaiting an authorized approval)

domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim 5966025001 · 2026-10-03T07:10Z

  • Shape (read on GitHub): a draft against main. The first line is Fixes #21517, and Clause-②: no sits at a line start. 4 files, +34 / −11: three text positions, as triage's ruling of record directed (5964851617, one card, one PR), and one changeset.
  • Contract review: at-tier PASS 5966526843 on this exact head. It judged each position against the runtime at origin/main:
    • ApprovalActionRow's reassign TSDoc matches reassign() and the object: both fields hold a slot address in its stored spelling (a user id, an email, or a position address). A *_name resolves only for an address without a colon, by account id, then by email. The type is unchanged, and the tracker citation is gone.
    • The ADR-0042 hunk (the governed one): one added status line, in ADR-0046's "Superseded in part" house form. The decision text is byte-identical, the date is ADR-0118's acceptance date, and "machine actions record actor_id null" is true to ADR-0118 D1. The runtime agrees: escalateRequest() inserts actor_id: null.
    • The approvals.sla-escalation checklist item expects no actor on the escalate row: stored null, and absent on the REST read.
    • Release: @objectstack/spec patch, because the TSDoc ships in the published .d.ts. Clause-②: no.
  • CI on bfc5e472: 32 success and 3 skipped by design (Build Docs, Console Pin Gate, packed-tarball smoke).
  • Governed surface: check-governed-merges reads GOVERNED, Tier H, from docs/adr/0042-approval-sla-escalation.md. 45 changed lines.
  • Serial: at this read, no other open PR touches the three files.

Carried, decided here:

  • The admin-rescue reassign branch (the dev's acceptance note, verified in code by the review). When the caller holds no slot and from is not on the slate, the row records the caller-named from, or the admin's own user id, rather than a slot that was handed over. The stored value is still a slot address, so the TSDoc's shape claim holds. Its "slot that was handed over" is loose on that one branch. → noted, not filed: read in code and not measured at a door, so it is not a card under the filing gate.

The landing, as the governed rule sets it:

  1. needs-user-decision goes on PR docs(approvals): reassign TSDoc holds slot addresses; ADR-0042 §2 superseded in part by ADR-0118 D1; SLA checklist expects no actor #21556 with this ACCEPT, and the final 维护者速读 is posted on the PR.
  2. A review is requested from the authorized approvers.
  3. ⛔ This seat does not flip it ready, enqueue it or arm auto-merge until an authorized approval is on it. Then it clears the label with a paired comment, runs the pre-landing checks and lands it.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)— PR #21556

domain:spec 坐席 2(session_01YDt3PzwfrkuFzUBF89WPmM)· 2026-10-03T07:11Z · 对照达档复核 5966526843 校正

改了什么:三处与审批「谁做的」有关的过时文字,一张卡、一个 PR(分诊裁定,5964851617):

  • ApprovalActionRow 的转办字段注释:reassign_from / reassign_to 存的是「槽位地址」原样(用户 id、邮箱或 position:名称),不是 sys_user 引用;显示名只在地址对应到账号时才解析得出。字段类型不变。
  • ADR-0042 只加一行状态说明:「部分被 ADR-0118 D1 取代」——机器动作记录 actor_id 为空,escalate 行本身就是归属。ADR 原有决策正文一字未改。
  • QA 清单 approvals.sla-escalation:期望 escalate 行不带操作人(库里为 null,REST 读取时没有这个键)。

为什么改:PR #21514(#21455)按 ADR-0118 D1 把审批里的「系统操作人」改成了空值、把转办字段改成了槽位地址;这三处文字还停在旧说法上。

风险与代价(含回滚):

  • 只改文字,零运行时、零契约变化。@objectstack/spec patch(注释会进发布的 .d.ts)。
  • 碰到 docs/adr/**,所以是 Tier H(人合)——这是分诊明确接受的,未拆分。
  • 回滚:发布前 revert 即可。
  • 达档复核 PASS;CI 全绿后才可落地。

席位意见:建议批准。改动最小,三处都已对照运行时代码核实。ADR 只加状态行、不改历史决策,符合 ADR 的记录方式。若你希望在 §2 正文旁再加一条注记,可以在批准时说明。

你要做的:在 PR #21556 上点 APPROVE(批准后由本坐席清标签并落地)。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review October 3, 2026 07:15
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Approval received on PR #21556: this seat lands it

domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim 5966025001 · 2026-10-03T07:16Z


Generated by Claude Code

Merged via the queue into main with commit f83d066 Oct 3, 2026
44 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-21517-approval-actor-texts branch October 3, 2026 07:45
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tooling

Projects

None yet

3 participants