feat(spec)!: retire rowLevelSecurity[].tags — no mainstream platform tags a row-level policy (ADR-0049) - #20353
Conversation
A retiredKey() tombstone on RowLevelSecurityPolicySchema with its prescription, the D2 conversion permission-rls-tags-removed wired into the protocol-18 chain step, the RETIRED_KEYS_BY_MAJOR[18] entry, the family D3 entry permission-rls-tags-retired, the liveness row kept dead under its tombstone, and the pin tests. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…d the changeset authorable-surface/security.json marks security/RowLevelSecurityPolicy:tags [RETIRED]; the two reference pages print the prescription in place of the old describe. The changeset carries the BREAKING banner, the FROM -> TO mapping and the ADR-0087 disposition. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
The ledgers key one row per schema property, so the RLS block of permission.json names operation, using, check and the retired tags row side by side: a classification of the shape, not an authoring. Measured as the walk's one hit before the exclusion. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 006b56d5a764e21ae26c5fd4306ea294842849c8 && git checkout 006b56d5a764e21ae26c5fd4306ea294842849c8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eee0974236c87a7232f9805a19b6e53e3e36da59 8acd22854ad0bb24f4645ba10cfdc933fa2ddedc && git checkout -B drift-repro eee0974236c87a7232f9805a19b6e53e3e36da59 && git merge --no-ff 8acd22854ad0bb24f4645ba10cfdc933fa2ddedc
node scripts/docs-audit/affected-docs.mjs --json eee0974236c87a7232f9805a19b6e53e3e36da59
|
…ement The seat's measured answer: the tombstone narrows the accept set, the D2 conversion only heals rows the load path already accepted, and no export is added, so nothing widens. No other sentence in the changeset asserted a widening; minor, the BREAKING banner and the ADR-0087 marker are unchanged. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: PR #20353 (card #20321), reviewed at the head the brief named; it did not move during the review (read from the PR three times, last after every check run completed). The head is the merge ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…s-tags-retire # Conflicts: # packages/spec/src/conversions/registry.ts # packages/spec/src/migrations/registry.ts
…tree The two pages are os-regen artifacts both sides moved (main's description rewrites, this branch's tags tombstone row); regenerated with gen:schema and gen:docs after the merge rather than text-merged. Against origin/main each page differs only in the tags row. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
Seat verification: base-merge round · head
|
…s-tags-retire # Conflicts: # packages/spec/src/conversions/registry.ts # packages/spec/src/migrations/registry.ts
…om the merged tree The merge took main's generated regions; gen:migration-registry puts this branch's two entries back (retired-key security/RowLevelSecurityPolicy:tags and the D3 semantic entry permission-rls-tags-retired) beside #20251's. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
Seat verification: base-merge round 2 · head
|
… capability (objectstack-ai#20364) Fixes objectstack-ai#20354 Clause-②: no ## What changed One rule text in `.claude/skills/spec-property-retirement/SKILL.md` §0. No new gate, no code, net 0 lines. 1. **The exemption bullet becomes the maintainer's criterion.** The old bullet 「它是文档形状的吗?」 ended with 「良性展示元数据(`description`、`tags`、`icon`)永远谈不上「误导」;不要标 `authorWarn`,也不要退役它。」 It is replaced by one bullet that states the per-family criterion in the maintainer's own words and derives the documentation-shaped case from it: - header 「主流平台有没有这个能力?」 and body 「有 ⇒ 补消费端(一次做对);没有 ⇒ 退役,而不是看仓里有没有人读」: both halves of the ruling sentence, quoted verbatim, with the ruling record cited as comment `5727134555`; - documentation-shaped keys (`hook.label`, `flow.description`) record intent for the next reader (ADR-0033), and the text now calls them an instance of the 「有」 branch: render, do not retire. The verdict still goes into the ledger `note` so the next audit does not re-open it. It is one criterion's outcome, not a second rule; - `description`, `tags` and `icon` are no longer exempt by kind. The row-level-policy `tags` key is named as the 「没有」 example (mainstream platforms have no such attribute, so it retires); - whether to mark `authorWarn` now defers to the liveness README's author-warning rule, which is unchanged. The playbook no longer carries a second copy of it. 2. **One more line in §0 carried the rejected criterion.** The bullet 「零编写实例」≠「没有代码读它」 ended 「后者才是退役的证据」, which says "nobody reads it" is the evidence for retirement. That is the in-repo-reader test the ruling rejects, and it would have sat eight lines under the new bullet and contradicted it. It now ends 「后者才证 `dead`」. An unread key is `dead`, and `dead` is what the criterion is asked about. It does not answer the criterion. Same defect class, same section, one line, 8 bytes shorter. It sits inside the claimed file surface (§0) and is named here so review can take or drop it separately. Before (lines 43–47 at `8af914a3`): ```text - [ ] **它是文档形状的吗?** `hook.label`、`hook.description`、`flow.description` 没有运行时消费者,但被**有意保留** —— 它们为下一个读者(按 ADR-0033,常是模 型)记录意图。把豁免写进台账 `note`,下次审计不再重审。良性展示元数据 (`description`、`tags`、`icon`)永远谈不上「误导」;不要标 `authorWarn`,也 不要退役它。 ``` After: ```text - [ ] **主流平台有没有这个能力?** 声明而未执行的键每族问这句,按维护者原话(裁决评论 `5727134555`):「有 ⇒ 补消费端(一次做对);没有 ⇒ 退役,而不是看仓里有没有人读」。 文档形状的键(`hook.label`、`flow.description`)为下一个读者记录意图(ADR-0033),是「有」的 一例:补渲染、不退役,判定写进台账 `note`,下次审计不再重审。`description`、`tags`、`icon` 不自动豁免:RLS 策略的 `tags` 主流没有 ⇒ 退役。`authorWarn` 另按 README 判。 ``` ## Why The maintainer's ruling on objectstack-ai#18900 (comment `5727134555`, item ④, A′), verbatim: 「18900 同意 每族该问的是:主流平台有没有这个能力 —— 有 ⇒ 补消费端(一次做对);没有 ⇒ 退役,而不是看仓里有没有人读。」 The ruling record's own gloss is "a family is judged by whether mainstream platforms in the domain have the capability, ⛔ not by whether anything in this repo reads the key". Triage applies it to every family card. The playbook said the opposite for `description` / `tags` / `icon`, and a dev on the RLS family already hit the fork: objectstack-ai#20353 retires a `tags` key the old text forbade. Triage direction (5862082626): "the ruling wins, and the text follows it". The documentation-shaped exemption is kept as the case where the mainstream carries the capability, which is how objectstack-ai#20299 was graded (render, do not retire). ## Inventory: other texts that restate the old exemption (triage note 2) Searched at `8af914a3` with `git grep` over `.claude/**`, `skills/**`, `AGENTS.md`, `CLAUDE.md`, `docs/**` (ADRs included), `content/docs/**`, `packages/spec/liveness/**`, `packages/spec/README.md` and `packages/lint/src/lint-liveness-properties.ts`. Patterns: 良性 / 展示元数据 / benign display / display metadata / 永远谈不上 / never retire / 不要退役 / 文档形状 / docs-shaped / 有意保留 / `authorWarn` / 主流平台 / mainstream / 零消费者. | Surface | Restates the exemption? | Action | |---|---|---| | `.claude/skills/spec-property-retirement/SKILL.md` §0 | yes, the two sentences above, plus the line 61 corollary | edited here | | other `.claude/skills/**`, `.claude/agents/**` | no, 0 hits (`authorWarn` has exactly 1 hit in `.claude/**`, the §0 line) | none | | `skills/**`, `AGENTS.md`, `CLAUDE.md` | no, 0 hits (`skills/objectstack-ai/SKILL.md:225` "Display metadata" is a table cell describing `label`/`description`, not an exemption) | none | | `docs/adr/**` (ADR-0033, ADR-0049 read) | no, neither carries a docs-shaped or benign-display exemption | none | | `packages/spec/liveness/README.md` "Author warnings", rule 1 (`:562–565`) | only the `authorWarn` half: benign display metadata is not **warned**. It never says "never retire" | none, and outside this file surface anyway (`packages/**`). The new §0 points at it rather than restating it | | `packages/lint/src/lint-liveness-properties.ts` header (`:21`) | same `authorWarn` half only | none | The ruling does not address warnings, and the README's warning rule is consistent with it. A key the mainstream carries is a missing consumer, and warning its author would call it misleading. A key the mainstream lacks retires. So the family to fix is this one file. ## PM mechanism hypotheses, as measured 1. **Site**: confirmed. The branch starts at `8af914a3` (`67047171` plus one unrelated docs commit). The bullet spans `:43–47` and the two target sentences `:45–47`. 2. **Inventory**: confirmed, only §0 in the governed texts (table above). One addition: the §0 line 61 corollary, fixed in place (item 2 above). 3. **Line ratchet**: holds at headroom 0. Details under Line budget. 4. **`check:pm-skill-id-lint`**: it does **not** scan this file. Its scan set is the pm-dispatch tree, `os-dev.md` and `AGENTS.md` (34 files, green), and this playbook carries 20 existing tracker-number citations. The bare comment id does not match its `#` pattern either. So the ruling record id stays in the rule text, as the triage direction asked, and no tracker number is added to the file. 5. **Documentation-shaped keys as the criterion's outcome**: the wording is 「是「有」的一例」, the same criterion's result and not a second rule. ## Line budget | | before | after | |---|---|---| | `SKILL.md` lines (ceiling 337) | 337 | 337 | | widest table row, bytes (pin 326) | 326 | 326 | | bytes | 25,497 | 25,639 (+142) | | tokens, `ceil(bytes/4)` (the token ratchet's convention) | 6,375 | 6,410 (+35) | | whole skill package (`.claude/skills/spec-property-retirement/`, 1 file) | 337 lines / 6,375 tokens | 337 lines / 6,410 tokens | Paid by deletion in the same bullet. Removed: 「没有运行时消费者,但被有意保留 —— 它们」, the `hook.description` example, 「按 … 常是模型」, and the whole 「良性展示元数据 … 也不要退役它」 sentence. No re-wrap of untouched text. New lines are 116 / 115 / 115 / 119 / 102 bytes, and line 61 goes from 120 to 112. Every line break falls between two wide characters or at a real space, so none renders as a stray space. No ceiling was raised and nothing moved to another file. ## Verification (HEAD `085a5026`) The derived list comes from `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (18 commands, re-derived after `git fetch origin main`, same list). Every command ran. Exit codes were captured before any pipe. | Command | Exit | |---|---| | `node scripts/check-ci-filter-parity.mjs` | 0 | | `node scripts/check-closing-keyword-parity.mjs` (and `--self-test`) | 0 / 0 | | `node scripts/check-comment-mask-corpus.mjs` | 0 | | `node scripts/pm/check-harness-current.mjs --self-test` | 0 | | `pnpm --filter @objectstack/lint run check:doc-formula-expressions` | 3 (prerequisite not met: formula and lint not built, so NOT MEASURED); built both under the verify lock, then 0 | | `pnpm check:agent-test-spelling` | 0 | | `pnpm check:cross-package-test-inputs` | 0 | | `pnpm check:doc-authoring` | 0 | | `pnpm check:driver-memory-census` | 0 | | `pnpm check:gitlink-declared` | 0 | | `pnpm check:nul-bytes` | 0 | | `pnpm check:pm-governed-merges` | 0 | | `pnpm check:pm-skill-ratchet` | 0, "SKILL.md is 337 lines (ceiling 337; headroom 0)" and "widest table row is 326 bytes (pin 326; headroom 0)" | | `pnpm check:refd-timer-probe` | 0 | | `pnpm check:required-contexts` | 0 | | `pnpm check:skill-frame-sync` | 0 | | `pnpm check:watch-hint-literal` | 0 | Reconciliation: `dispatch-gates --ran` with coded exits reports "18 derived famil(ies) accounted for — 18 run, 0 NOT-MEASURED (a DERIVED zero)". Also run: - **Test Core job, scheduled by this path.** `packages/spec/src/shared/retired-key-migrate-sentence.test.ts` reads this playbook. `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/shared/retired-key-migrate-sentence.test.ts` passed: 1 file, 14 tests, exit 0. The rest of the shard is left to CI. - **Roster gates under `.claude`/`skills`, which the derivation marks as not placeable.** Each exited 0: `check:pm-settings-deny-roster`, `check-skills-token-ratchet.mjs`, `check:pm-skill-id-lint`, `check:pm-governed-prose`, `check:skill-top-level-keys` and `check-published-list-mirrors.mjs`. - **Type-check lanes: NOT MEASURED.** Reason: the diff touches no TypeScript. - **Ablation: none owed.** This PR adds no gate or assertion. ## Changeset `skip-changeset`. The only path is under `.claude/**` (fast lane). As a measurement, the root package that holds `.claude/` is `private: true`, no workspace `package.json` names `.claude`, and the new text 「主流平台有没有这个能力」 has exactly one hit in the tree, this file. Positive control: the liveness README sentence "Only mark genuinely" is found under `packages/spec/liveness/`, which `@objectstack/spec` ships through `files[]` (`liveness`). ## Acceptance notes - **Byte cost.** The rule text grows by 142 bytes at net 0 lines. That is the verbatim criterion replacing a 24-byte tail line. The line ratchet meters lines under a 120-byte cap, and every new line is within it. - **Examples trimmed.** `hook.description` left the example list, and 「按 ADR-0033,常是模型」 is shortened to 「(ADR-0033)」, to pay for the criterion inside the same five lines. - **Ledger notes are data.** Several ledger row notes still say "exempt from enforce-or-remove (ADR-0033)" or "benign display metadata — not authorWarn'd", for example the `job` / `datasource` label rows and the RLS policy label row. They are left as they are. Their keep / do-not-warn outcomes agree with the criterion for keys the mainstream carries. objectstack-ai#20299 carries the render for seven such keys, and family grades re-grade rows as they land (objectstack-ai#20353 retires a tags key on the RLS family). - **Same-day history.** objectstack-ai#20254 landed on this file today, before this branch point. No open PR touches the file (the claim's read). - **Governance.** Tier S, governed surface `.claude/**`. Draft. It lands through the skills seat's contract review. This PR does not flip it to ready, arm auto-merge or merge it. ## 维护者速读(草稿) - **改了什么**:退役手册(`spec-property-retirement`)§0 那句「良性展示元数据(description、tags、icon)永远不退役」,换成您在 objectstack-ai#18900 定的判据原话:每族问主流平台有没有这个能力,有就补消费端,没有就退役,不看仓里有没有人读。hook.label、flow.description 这类文档型键仍然保留,但理由改成「主流平台也有」,是同一条判据的结果,不再是另一条豁免规则。同节另一句「没有代码读它才是退役的证据」一并改成「才证 dead」。 - **为什么改**:手册和您的裁决相互矛盾。已经有开发按裁决退役了 RLS 策略上的 tags 键,违背了手册原文。今后每张含 description / tags / icon 成员的族卡,都会让先读手册的 agent 卡在这个分叉上。 - **风险与代价(含回滚)**:只改给 agent 读的规则文本,不动代码、schema 和门禁;行数净 0,字节 +142。回滚就是 revert 这一个 commit。可能的误读是把「补消费端」当成所有文档型键都要马上补渲染,实际仍按每族定级排期。 - **席位意见**: - **你要做的**:无需操作。本单属 Tier S,由技能席契约复核后落地;若对判据的写法有意见,在本 PR 留言即可。 --- _Generated by [Claude Code](https://claude.ai/code/session_01MjvgiFAmjHqsxy1XLiVYfH)_ Co-authored-by: Claude <noreply@anthropic.com>
…(ADR-0049) (objectstack-ai#20398) Fixes objectstack-ai#20323 Clause-②: yes ## What this does Retires `action.aria` under ADR-0049 enforce-or-remove, following the triage direction on the card (RETIRE, on the chart-config precedent `2bf6ef18d`). No maintainer word reversed it to ENFORCE. - **Schema.** `ActionSchema.aria` becomes a `retiredKey()` tombstone. It is a `tsc` error (the input type is `never`) and a parse error that carries the prescription. The accessible name that IS applied is the action's required `label`, and the placing node's `aria` block (`page.components[].aria` or the list view `aria`) names the region. `AriaPropsSchema` is untouched: this is a key retirement, not a def retirement. - **ADR-0087.** The D2 conversion `action-aria-removed` (protocol 18, `retiredFromLoadPath`) strips the key from `actions[]` and `objects[].actions[]` as a lossless delete. The D3 semantic entry `action-aria-retired` is its own family, per the one-entry-per-family rule. The retired key `ui/Action:aria` is registered under 18, the major of the chart-config sibling. - **Ledger.** `liveness/action.json` regrades `aria` from `live` to `dead`. Its `REMOVED` note records that the uncited 「PARTIAL — honored by a few objectui renderers」 claim had no reader behind it. The undrilled-container row `action/aria` goes, and `state-counts.md` is regenerated. - **Docs.** Two hand-written pages taught `aria` on an action and are corrected: `protocol/objectui/actions.mdx` and `widget-contract.mdx`. The three reference pages that render `ActionSchema` are regenerated. - **Changeset.** `minor`, BREAKING, with `Clause-②: yes`, the FROM → TO table and the ADR-0087 `registered` marker. ## Premise, re-measured on this checkout - **objectui at the `.objectui-sha` pin `f8a9d0fb05`.** A reader grep for an action's `aria` (`action`, `actionDef`, `def`, `spec`, `btn`, `a`, `act`, `item` followed by `.aria`) over `packages/**` and `apps/**` non-test sources hits **0** lines. The control, the same grep for `.variant`, hits **18** files. Every `schema.aria` reader there is a placing node: the `record:*` components, `ListView`, `ObjectView` and `element:button`'s props. - **Icon-only reversal condition.** Triage named "icon-only actions have no accessible name" as the condition that would reverse this to ENFORCE. It does not hold. `action-icon.tsx:243` renders `aria-label={schema.label || schema.name}`, `action-menu.tsx:341` renders `aria-label={schema.label || moreActionsLabel}`, and `action-button.tsx:346` renders `{schema.label}` as the visible text. - **Framework.** A grep for `.aria` over `packages/**` non-test TS outside `packages/spec` hits **0** lines. `action.form.ts` has 0 `aria` rows; its one hit is the `variant` substring. - **Authors in this repo.** Across `examples/**`, the published `skills/**` and `packages/**` fixtures, **0** actions author `aria`. The control is **15** `variant:` lines in `examples/**`. - **Authors in HotCRM `2f7b2326`.** **0** actions author `aria`. Its 6 `aria:` blocks are all page-level `page.aria`, which stays live. The control: 7 files under `src/**/actions/` declare `locations:`, 17 times. - **Served schema.** `metadata-protocol` drops a tombstone's `{ not: {} }` node from the served JSON Schema, via `stripUnauthorableProperties`. So the Studio "More fields" form stops offering `aria` once this ships. - **Pinned sibling.** objectui's `ActionRunner.ts:410` mirrors the key as `aria?: SpecActionInput['aria']`. A type probe against this branch's built `dist` compiles that mirror at exit 0, because it evaluates to `undefined`. An authored block on it is refused with TS2322, and the control leg without `@ts-expect-error` exits 2. So the Console Pin Gate's objectui build is not broken by this change. ## Hand-over review (the stopped run's six commits, read hunk by hunk) | commit | verdict | | --- | --- | | `0c5dbbff` sources, ledger, tests, changeset | **kept, with corrections.** The prescription said "removed in @objectstack/spec 17"; it now says `17.5.0`, the spelling every sibling retirement on this line uses. The refusal pin in `action.test.ts` now also asserts the tombstone's own issue kind (`invalid_type` at `aria`, and no root `unrecognized_keys`, which is what a bare deletion would answer instead). The changeset's BREAKING sentence now names the replacement. The HotCRM control is re-measured: 7 files and 17 lines, not "5 action files". | | `6334c137` regenerated artifacts | kept, and re-derived after both merges | | `bfc86375` the stored-row pin uses a parseable script action | kept | | `0f20c62f` the `action/aria` undrilled-container row goes | kept; `check:liveness` is green | | `8cbcfa46` merge of `main` | kept | | `90b8fcdc` regenerated state counts | kept; superseded by the post-merge regeneration | Added in this round: - `33407119` and `c38c18ae` merge `origin/main` through `scripts/pm/os-regen-merge.sh`. Both merges stopped on the two registries. Each was settled by stacking both sides: main's conversions and rationale paragraphs first, then this branch's. The merges brought in objectstack-ai#20262, objectstack-ai#20352, objectstack-ai#20251 and objectstack-ai#20353. - `2b15085d` and `7faf0e9f` are the deferred regenerations. - `68b038ef` carries the corrections above, plus two more: - a new pin that the stored-row seam reaches an `object` row's nested action (the changeset's second at-rest coordinate); - one sentence on the `action` row of `liveness/README.md`. That row said "makes the dead set three", which this change makes false in a published file (`liveness` is in `@objectstack/spec`'s `files`). - `8dd3a2ce` regenerates the reference pages for the 17.5.0 prescription. ## Verification Head `7faf0e9f`, on `origin/main` `15bf186f`: - `pnpm --filter @objectstack/spec test`: `Test Files 558 passed (558)` · `Tests 16509 passed | 1 todo (16510)`. - `pnpm --filter @objectstack/spec test:repo`: `Test Files 35 passed (35)` · `Tests 634 passed (634)`. - `pnpm --filter @objectstack/spec check:generated`: all 15 generated artifacts up to date, measured over a spec build made on this head. - Gates: `node scripts/pm/dispatch-gates.mjs --commands` derives 114 commands. All 114 exit 0, and `--ran` reconciles them as `114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN`. The run covers every package's build closure, rebuilt on this head, so the gates that read `dist` measured it. Head `8dd3a2ce`, before the second `main` merge, on `origin/main` `862b6ce8`. The second merge touched none of these packages' interaction with this diff; the incoming commits retire other keys and touch no action surface. | package | command | Test Files | Tests | | --- | --- | --- | --- | | `@objectstack/lint` | `vitest run` | 112 passed | 4640 passed | | `@objectstack/cli` | `vitest run --project unit` | 231 passed | 3309 passed | | `@objectstack/runtime` | `vitest run --project local` | 282 passed | 4059 passed, 1 skipped | | `@objectstack/metadata-protocol` | `vitest run` | 189 passed, 3 skipped | 2736 passed, 19 skipped | | `@objectstack/metadata-core` | `vitest run` | 16 passed | 285 passed | | `@objectstack/objectql` | `vitest run --project local` | 322 passed | 5857 passed | `pnpm --filter @objectstack/spec typecheck` was green at the same head. The cli `integration` layer is declared to CI: this diff touches no spawn entry and no integration file. **Ablation**, from the committed state at `8dd3a2ce`. The blobs of `action.zod.ts` and of the three pin files are byte-identical at `7faf0e9f`. - The mutation goes through `scripts/ablation-replace.mjs`: the anchor ` aria: retiredKey(` becomes ` aria: z.any().optional().describe(`, so the key is accepted again. The tool reports the anchor going from 1 to 0, the replacement from 0 to 1, and the blob from `2e0a17b14b06` to `45e6bd9b1156`. The wrapper also arms a `trap` that restores the file. - The control leg runs the three pin files on the committed state: `Tests 166 passed (166)`. - The mutant leg: `Tests 4 failed | 162 passed (166)`. The four red tests: - `action.test.ts` · refuses an action carrying `aria`; - `aria-carrier-tombstones.test.ts` · the action tombstone fires and prescribes; - `aria-carrier-tombstones.test.ts` · the object-nested coordinate; - `action-aria-removed.test.ts` · the stored-row seam. - The restore is proven by content, not by an exit code. The file's blob equals HEAD's blob `2e0a17b14b06`, `git diff HEAD` is empty, and the porcelain status has 0 lines. - The pins import `./action.zod` and `../ui/action.zod.js` relatively. They read `src`, not `dist`, so this ablation has no dist leg. ## Acceptance notes - **objectui, owned by seat 4 after landing; not in this PR.** `ActionDefaultInspector` should list `aria` in its `RETIRED_FIELDS`, per triage note 3. `ActionRunner.ts:410` should drop its `aria?: SpecActionInput['aria']` mirror, which evaluates to `undefined` once this ships and still compiles (the probe above). - **objectstack-ai#19332, owned by seat 4 after landing.** The disposition of `action.aria` goes to objectstack-ai#19332's item that waits on this card. - **Advisory lint.** No `lint-liveness-properties` non-warn pin is added. The `aria` row never carried `authorWarn`, so the advisory lint's behaviour is unchanged: it was silent before and is silent now. - **Aliases.** `ActionSchema` never aliased `accessibility` or `ariaProps` onto `aria`, unlike the chart config. A probe shows both already refused as `unrecognized_keys`, so no alias refusal pin is owed. --- _Generated by [Claude Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20321
Clause-②: no (narrowing)
What this does
Retires
rowLevelSecurity[].tags(RowLevelSecurityPolicySchema.tags) by thespec-property-retirementroute. It is ADR-0049 enforce-or-remove, graded RETIRE by triage (5860425529) under the maintainer's criterion on #18900 (5727134555). Triage's verdict, verbatim: 「Row-level policies carry no tag attribute in the mainstream: Salesforce sharing rules, Dataverse security roles and PostgreSQL RLS policies. Compliance reporting keys on the rule itself. ⇒ RETIRE.」 Retirement is immediate, with no staged window.The Clause-② line is the seat's measured answer,
no (narrowing), which supersedes the claim'syes(5860732909, triage's execution note).scripts/pm/clause2-line.mjsdefines the value as the answer to 「本卡放宽接受集或扩大公开面吗」. Nothing widens: the tombstone narrows the accept set, the D2 conversion only heals stored rows the load path already accepted, and no export is added. A policy that carriedtagsparsed before this change and is refused after it, which is the(narrowing)arm. The changeset carries the same line.Accept/refuse changes (all pinned)
RowLevelSecurityPolicySchemawithtags(any value,[]included)invalid_typeat['tags'], with the prescriptionrls-tags-retirement.test.ts,rls.test.tspermissionwrite door (getMetadataTypeSchema('permission')=PermissionSetSchema) with a policy carryingtagsinvalid_typeat['rowLevelSecurity', 0, 'tags'], with the prescriptionrls-tags-retirement.test.tsdefineStackwith such a permission setSTACK_SCHEMA_INVALID/422, issue at['permissions', 0, 'rowLevelSecurity', 0, 'tags'], with the prescriptionrls-tags-retirement.test.tstagstagsmaterializedtagtagsunrecognized_keys); the tombstone is never offered (acceptsNothing)rls-tags-retirement.test.tstagspermission-rls-tags-removed, then accepted by the write doorrls-tags-retirement.test.tsThe new refusal text, verbatim (
RLS_POLICY_TAGS_RETIREDinrls.zod.ts)The generated
.describe()is that text prefixed with[REMOVED](theretiredKey()helper). It replacesPolicy categorization tagsincontent/docs/references/security/rls.mdxandpermission.mdx. A repo-wide grep for the old describe string finds no pin anywhere.Measured before changing anything (Zone 2 of the dispatch)
Each reading has a lit control on the same ref.
tagstagsplugin-security/lint/rest/objectql/runtime/metadata*/servicessrca78f731atagshits are record-field CEL (size(record.tags)), OpenAPI routetags, and the docs-audienced.tagsinmeta-item-read-gate.ts, and none of them is a policy.examples/**and in the plugin-security producers (default-permission-sets.ts,bootstrap-platform-admin.ts,platform-*-policies.ts,permission-set-projection.ts,security-plugin.ts).positionsread 35 times in plugin-security src;rowLevelSecuritypresent in all 7 producer files.objectui-shapinf8a9d0fbPermissionAdvancedFacets.tsxhas 0tags(its seed is{name,object,operation,using,check,enabled}, andRETIRED_RLS_KEYSis['priority']).PermissionPreview.tsxrenders${rls.length} RLS rules.permission-slice.ts/clientValidation.tshave 0..using3,.enabled2,prioritylitmain972c1685priority3,.using3main96eb092fapps/ee-group-showcasesecurity sources)rowLevelSecurity1 in each fileSo the dispatch's mechanism assumptions 1 and 2 hold. On assumption 3:
RowLevelSecurityPolicySchemais astrictObject, so this is not the ADR-0104 silent-strip case. The def is reachable from thepermissionroot, and the precedent one key over (priority) is aretiredKey()tombstone on this same closed shape. That tombstone route keeps the liveness row, which staysdead.What changed
packages/spec/src/security/rls.zod.ts:tagsbecomesretiredKey(RLS_POLICY_TAGS_RETIRED). The const is declared above the lazy schema, per theOS_EAGER_SCHEMASTDZ rule. The docblock records the census and the mainstream reading. The suggestion-pool comment now names both tombstones.permission-rls-tags-removedinconversions/registry.ts(toMajor: 18,retiredFromLoadPath: true). It is astripKeysdelete overpermissions[].rowLevelSecurity[], copy-on-write, with a fixture of 1 notice that is disjoint from every other entry.MIGRATIONS_BY_MAJOR[18].conversionIds, and the step rationale is extended.migrations/entries/retired-keys/18.security__RowLevelSecurityPolicy__tags.tsholds the exact keysecurity/RowLevelSecurityPolicy:tags.migrations/entries/semantic/18.permission-rls-tags-retired.ts, per ruling B on [Decision] 一次退役,要写一条记录还是两条?—— 迁移条目的 D2/D3 约定,两处成文相互矛盾 #17152: one D3 entry per retirement family, even when D2 is lossless.registry.tsgenerated regions were regenerated withgen:migration-registry.spec-changes.jsonand the upgrade guide are byte-identical, because major-18 entries do not project yet (check:spec-changes/check:upgrade-guidegreen).liveness/permission.json'srowLevelSecurity.tagsrow staysdeadunder its tombstone. It getsverifiedAt: 2026-09-27, evidence pointing at the tombstone, and a REMOVED note in thepriorityrow's house style. Thepermissionrow ofliveness/README.mdgains one sentence. Counts are unchanged, because the row goes dead to dead.authorable-surface/security.jsonnow readssecurity/RowLevelSecurityPolicy:tags [RETIRED]. Two reference pages were regenerated (check:generatednamed onlycheck:docsstale).rls.test.tsfixture triage:should validate tagswas replaced by a refusal pin, because it pinned exactly the retired branch.descriptionand in the predicate, and that notagscomes back.tagsis materialized.rls-tags-retirement.test.ts(14 cases) covers every door above, the tsc channel (@ts-expect-error, compiled bytsconfig.test.json), the D2 (stored-row rehydration, per-policy scope, measured idempotence, load-path retirement) and the registration. It also has a tree-scoped absence leg with a structural matcher: an object or YAML mapping whose own keys includetags,operationandusing/check. The leg has an anti-vacuity battery over 14 specimens and walks the 5 roots already declared for@objectstack/spec#test. It is listed invitest.repo-tests.json..changeset/20321-rls-policy-tags-retired.md:@objectstack/specminor, with a BREAKING banner, FROM → TO, and the ADR-0087 dispositionregistered permission-rls-tags-removed, permission-rls-tags-retired.No form or i18n edit:
permission.form.tseditsrowLevelSecurityas onejsonwidget, with no per-key input. No skill teaches the key. There is no hand-written doc mention:content/docs/permissions/rls.mdx:231's "tags each withkernelTier" is prose about the explain engine. No objectui change is needed: the pin reads nothing of the key and does not importRowLevelSecurityPolicy['tags'].Verification, at
62fd232a73Heavy runs went through
scripts/pm/os-verify-lock.sh. The specdistwas rebuilt at this head before any dist-reading gate.pnpm --filter @objectstack/spec build: VERDICT command-exit 0, tree clean afterwards (no artifact drift).pnpm --filter @objectstack/spec run typecheck→ exit 0.check:test-typecheck: 53 files, 255 errors, 142 pinned signatures held. So the@ts-expect-errorcompiles in a real program.vitest run --project local: exit 0, 554 files / 16357 tests passed, 1 todo.vitest run --project repo: exit 0, 34 files / 618 tests passed. This includes the new retirement file (14/14).@objectstack/lintvitest run: exit 0, 111 files / 4304 tests.@objectstack/plugin-securityvitest run: exit 0, 141 files / 2990 tests.git statusis clean afterwards): a probe inplugin-security/srcresolves@objectstack/spec/securitythrough its exports to the builtdist/security/index.d.mts.{ …, tags: ['gdpr'] }asRowLevelSecurityPolicy→ exit 2,error TS2322: Type 'string[]' is not assignable to type 'undefined'at thetagscolumn.tags→ exit 0.--ignoreConfigwas passed.dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat62fd232a73, 113 lines). All were run with exit codes recorded, then reconciled:--ran→ 113 derived, 112 run, 1 NOT-MEASURED, 0 UNRUN. All 112 run exit 0. That includescheck:liveness(86 tombstones reached, all graded with an allowed status),check:generated(15/15 current),check:migration-registry,check:spec-changes,check:upgrade-guide,check:adr-0087-registration --base origin/main,check:changeset-no-major,check:authorable-surface,check:api-surface,check:doc-authoring,check:cross-package-test-inputs,check:nul-bytesandcheck:type-check-debt(re-measure, 4 entries, none above record).check-changeset-fixed,check:meta-url-spelling,check:authz-resolver,check:error-code-casingandcheck:filter-alias-parity. All exit 0.NOT MEASURED:
check:dual-build-cjs-loads. Reason: PREREQUISITE NOT MET, exit 3. It loads every package's built entry, and 38 workspace packages are unbuilt in this worktree (apps, connectors, most services). That is a whole-workspace build, which is CI'sBuild Corejob.NOT MEASURED:
packages/cliintegrationtier (migrate-meta.e2e.test.tsreplays the chain). This diff touches nobin/or spawn entry, so it is declared to CI.NOT MEASURED:
packages/qa/dogfoodexpression conformance.tagscarries no expression surface, and the ledger's RLScoversrows name only.using/.check.maingained one commit (d3958bac, driver-sql only) after the basea78f731a. It is disjoint from this diff and was not merged in.Acceptance notes (observations, not filed)
RETIRED_RLS_KEYS = ['priority']on load. A stored permission row carryingtagsis healed before it reaches the editor, because the D2 replays at rehydration, so no path to a refused save is measured. This is an inference only. Carrier: none..claude/skills/spec-property-retirement§0 still says benign display metadata (description,tags,icon) should never be retired. This card was graded RETIRE under the maintainer's later [Decision] Route declared≠enforced work by the SEAM, not the layer — aSeam:line on filing, vertical dispatch by default in the spec lane, automatic parent + sub-issues for spec↔objectui seams, Journey as a filter, bulk retirement per spec family, Console Pin Gate back to required (the maintainer's 「同意」 on the five-line batch, 2026-09-18) #18900 criterion, which asks about the mainstream capability rather than readers. That skill line now contradicts ruled practice for this family. It is a governed surface and the PM's call. Carrier: none.authorable-surface.base.jsonstill lists the key without[RETIRED]. That is by design: onlygen:authorable-surface-basewrites that file, andcheck:authorable-surfaceis green.Generated by Claude Code