Skip to content

feat(spec)!: the action blocks spell the destructive variant danger, the word ActionSchema.variant uses - #26966

Merged
objectstack-fleet[bot] merged 14 commits into
mainfrom
claude/project-thread-4h9cyt
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 14 commits into
mainfrom
claude/project-thread-4h9cyt

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #26779

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: follow-up #27243 (ui/content/docs/guide/layout.md now teaches variant: 'danger' on an action:button; check:doc-json and page-width-padding-refusal-11318 judge that fence against the rows, but no eval exercises an action block's variant)

维护者速读

  • 改了什么:四个 action 区块(action:button / action:icon / action:group / action:menu 及其成员)的 variant 把 destructive 改名为 danger,与 ActionSchema.variant 同一个拼法;旧值保存时拒收并给出改名提示,存量页面由 ADR-0087 转换自动改写。
  • 为什么改:同一个危险动作在声明里写 danger、在区块里写 destructive,两套方言让每个渲染器都要翻译一次;协议只留一个拼法(分诊裁决 6448425754)。
  • 风险与回滚:作者写的 variant: 'destructive' 区块在下次保存时被拒(收窄,带迁移处方);存量 page 行在加载时被转换改写。回滚 = revert 本 PR。
  • 先看:packages/spec/src/ui/component.zod.ts(ACTION_BLOCK_VARIANTS 与 actionBlockVariant)、packages/spec/src/conversions/registry.ts(pageActionBlockVariantDestructiveToDanger)、ui/packages/components/src/renderers/action/runner-variant.ts。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/conversions/page-action-block-variant-destructive-to-danger.test.ts,预期 35 个用例全绿(六行接受 danger、拒收 destructive 并给改名提示、存量行被改写)。
  • 需要你做的:无,席位落地。

What changes

Spec (packages/spec/src/ui/component.zod.ts). The five rows that read BUTTON_PRIMITIVE_VARIANTS (ActionButtonPropsSchema, ActionIconPropsSchema, the shared group/menu member shape, ActionGroupPropsSchema, ActionMenuPropsSchema) now read ACTION_BLOCK_VARIANTS = default | danger | outline | secondary | ghost | link, plus primary on the three rows that took it before. BUTTON_PRIMITIVE_VARIANTS had no other reader on main 8074feb6, so it is replaced, not kept beside. Each row builds its enum through actionBlockVariant(subject, extra), whose error map answers destructive with the rename onto danger (naming the key the author wrote) and leaves every other miss to the enum's own message. That is the established enum-value retirement shape (FORM_VIEW_LAYOUT_RETIRED, OBJECT_FORM_LAYOUT_RETIRED); it adds no .refine(), so no dropped-refinements row. The .describe() texts say danger and what it does.

Data at rest. ADR-0087 D2 conversion page-action-block-variant-destructive-to-danger (toMajor: 18, retiredFromLoadPath: true, retiredAfter: '18.2.0', order 77). It rewrites properties.variant on the four block nodes and variant on each action:group / action:menu member, at every position mapPageComponents visits. It also visits action:bar, renaming the bar's own properties.variant and the variant of each actions / systemActions member. The bar draws its members through the block renderers with member.variant || properties.variant, so both spellings would reach a renderer, and the bar has no props row to refuse destructive at save (round 3, after the delta review 6464021995). Every other component (element:button keeps the primitive's destructive) and every other value is left as stored. D3 entry ui-page-action-block-variant-destructive-refused. One JOIN row in retired-key-removal-version.pin.test.ts (six refusals; the pin derives @objectstack/spec 18 and --from 17 from retiredAfter).

ui renderers. runner-variant.ts now holds two closed maps and neither reads destructive: toRunnerVariant forwards danger | primary | secondary | ghost | link as they are (and nothing for default / outline), and toButtonVariant draws danger as the primitive's destructive and primary as default. The four renderers draw through toButtonVariant, and the group/menu dropdown items paint red on danger. The designer inputs of action:button and action:icon list danger in place of destructive, and sdui.manifest.json is regenerated (two enum values). UIActionSchema.variant follows the rows by reference, so its doc comment is rewritten.

The #26776 bridge is removed, not kept. It existed because stored and authored blocks said destructive while the runner reads danger. After this PR the rows refuse destructive at save, a page's next save meets the component-props gate, and every data-at-rest seam (applyConversionsToStoredItem for stored page rows, the artifact door inside its declared-floor window, os migrate meta --write for sources) replays the conversion, retired entries included. So no stored page reaches a renderer with destructive, and AGENTS.md Prime Directive 12 leaves no room for a consumer-side alias kept "just in case".

Authored values converted. No examples/**, apps/**, packages/platform-objects/**, skills/** or content/docs/** source writes variant: 'destructive' on an action block (measured with a git grep over variant + destructive at 8074feb6; every hit there is a React Button/Alert primitive or a type: 'button' component). The authored ones are in ui/: the record detail views' code-built Delete entry for the ⋯ menu (RecordDetailView.tsx, plugin-detail DetailView.tsx), the action-bar.tsx doc example, and the test fixtures below.

VS Code extension. ui/packages/vscode-extension/src/providers/SchemaValidator.ts judges the action block variants against the block rows (danger accepted, destructive answered with the rename; primary on the rows that take it). The button component's list, which is the Button primitive's and keeps destructive, is untouched. The package is private, so it needs no changeset.

Changeset .changeset/26779-action-block-variant-danger.md: @objectstack/spec, @object-ui/components, @object-ui/types, @object-ui/plugin-detail minor, with the FROM → TO prescription, both declaration lines and the registered ADR-0087 marker, plus a paragraph that supersedes the variant sentences of the pending #26665 entry.

Tests

  • New packages/spec/src/conversions/page-action-block-variant-destructive-to-danger.test.ts (35 cases): the six rows accept danger; refuse destructive with the rename at variant / actions.0.variant; CONTROL, another miss keeps the enum message; the stored-row seam rewrites all four nodes and a member alone (copy-on-write for the other); six controls left as stored; canonical page returns the same reference; idempotent; the authoring funnel does not replay it.
  • Updated fixtures to the new spelling: spec component-report-items-action-members-typed.pin.test.ts (two members), ui action-group-menu-inputs-11168.test.tsx (two members, same assertions: bg-destructive and text-destructive), action-variant-confirm-26665.test.tsx (the four blocks confirm through the destructive button on danger; new toButtonVariant rows; CONTROL rows that destructive is no longer read), and one pins: sentence in registry-inputs-spec-parity.test.ts.

Acceptance notes

  • Claim wording. The claim asks for "an aliases prescription". strictObject's aliases maps unknown KEYS to known ones; a renamed VALUE has no key to alias, so the prescription lives in the enum's error map, the shape the other enum-value retirements use.
  • File surface beyond the claim, each forced: RecordDetailView.tsx and plugin-detail DetailView.tsx (their Delete entry would lose its red row once the menus paint on danger), @object-ui/types ui-action.ts (doc comment of the by-reference type), sdui.manifest.json + scripts/sdui-manifest.record.json (the registry-change regeneration; the record's objectuiWorkspaceVersion moves 18.1.0 → 18.2.0 because main's record trailed the version label), the D3 entry and the JOIN row (both red without them, as on the locale retirement).
  • Unchanged on purpose. The page-header tests still give the host's sys_delete fixture variant: 'destructive': the header judges destructiveness by variant === 'danger' || name === 'sys_delete', and that fixture is the by-name control. ActionPreview.tsx previews an ActionSchema, not a block.
  • Unreleased action:button / icon / group / menu: UIActionSchema.variant spells danger destructive while the spec's ActionDef says danger, so a block-declared danger action confirms through a neutral button #26665 changeset. .changeset/26665-action-block-destructive-confirm.md (not yet released) describes the bridge this PR removes ("danger stays refused there"). It is left byte-identical to the merge base, because editing another card's pending note is the DELIBERATE CORRECTION class that keeps check-empty-changeset red until a person confirms it. Instead, this PR's changeset carries a paragraph that explicitly replaces that entry's variant sentences in the same release and keeps its confirm-dialog behaviour (seat 1 ruling).
  • Not measurable from here: stored deployed rows and hotcrm sources. The conversion covers stored page rows and built artifacts in their window; a hotcrm source with a destructive block meets the refusal on its next os validate.

Verification

Readings on the pre-merge head 22846fae unless marked; main was then merged (17ecbb46, 28 commits, none touching a file this PR edits) and the spec artefacts and spec tests below were re-run on the merged head.

Re-measure before any edit (main 8074feb)

A safeParse probe over the six rows: action:button, action:icon, action:group, the group member, action:menu and the menu member each REFUSE danger ("Invalid option: expected one of default|destructive|outline|secondary|ghost|link…") and ACCEPT destructive. The premise held on all six. BUTTON_PRIMITIVE_VARIANTS had five readers, all action block rows, and no other.

Spec
  • pnpm --filter @objectstack/spec build, then check:generated: 13/14 up to date, check:docs stale; --fix rewrote content/docs/references/ui/component.mdx, then 14/14 up to date. On the merged head 17ecbb46: build + check:generated, all 14 up to date, nothing to commit.
  • pnpm --filter @objectstack/spec typecheck: exit 0. pnpm --filter @objectstack/spec test: 739 files, 22139 tests passed (1 todo). Lock VERDICT command-exit 0.
  • Merged head: vitest run src/conversions src/migrations src/shared/retired-key-removal-version.pin.test.ts src/ui/component-report-items-action-members-typed.pin.test.ts: 29 files, 1054 tests passed.
ui
  • Closure build --filter '@object-ui/app-shell^...' --filter '@object-ui/plugin-detail^...' --filter @objectstack/formula --filter @objectstack/client: VERDICT command-exit 0.
  • vitest run packages/components/src/renderers/action + the three page-header-* tests + apps/console/src/__tests__/registry-inputs-spec-parity.test.ts: 40 files, 921 tests passed.
  • type-check on @object-ui/types, @object-ui/components, @object-ui/plugin-detail, @object-ui/app-shell: all four Done, VERDICT command-exit 0.
  • Reverse check of the by-reference type, against the rebuilt spec .d.ts: a scratch file assigning 'destructive' and 'danger' to UIActionSchema['variant'] reds only the first (TS2322: Type '"destructive"' is not assignable to type '"default" | "link" | "primary" | "secondary" | "danger" | "ghost" | "outline" | undefined'); the file was then deleted.
  • node scripts/gen-sdui-manifest-node.mjs: 106 components; the diff is the two destructive → danger input enum values.
Derived gates and dogfood
  • dispatch-gates.mjs --commands: 116 families. Run serially: 114 exit 0, spec check:skill-examples and check:dual-build-cjs-loads exit 3 (prerequisite not built). After the dogfood closure build both exit 0 (267 prose examples type-check; dual-build floors met). --ran reconciliation: 116 derived, 116 accounted for.
  • Dogfood (--filter '@objectstack/dogfood^...' build, then pnpm --filter @objectstack/dogfood test, one lock): VERDICT command-exit 1. Test Files 1 failed, 339 passed, 3 skipped (343); Tests 2 failed, 2752 passed, 20 skipped (2774). Both failures are in test/packaged-action-disabled-dispatch-live.dogfood.test.ts (acceptance 0 and acceptance 5/2). Run alone on the same head it passes 8/8, the load flake already recorded for that file; this diff touches no action-disable path.
  • node scripts/check-nul-bytes.mjs: exit 0. No exported symbol is renamed or removed (BUTTON_PRIMITIVE_VARIANTS was module-private), so check-platform-checklist was not owed.

Human Minimum

Each item is one rule from
CONTRIBUTING.md › Human Minimum,
which links the AGENTS.md text that states it.

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (No formatter ran. pnpm lint is CI's run.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzeYWDZN6LkXji5ziLF

The four action block rows (action:button, action:icon, action:group,
action:menu and the containers' members) took the Button primitive's
`destructive` and refused `danger`, while ActionSchema.variant, the
declaration the action runner dispatches, says `danger`. The rows now take
`danger` and refuse `destructive` with the rename in the enum's own error
map. ADR-0087 D2 conversion page-action-block-variant-destructive-to-danger
(retired from the load path, retiredAfter 18.2.0) rewrites stored rows and
sources; D3 entry ui-page-action-block-variant-destructive-refused.

Claude-Session: https://claude.ai/code/session_01WGufzeYWDZN6LkXji5ziLF
Co-authored-by: Claude <noreply@anthropic.com>
… `destructive` is gone

Claude-Session: https://claude.ai/code/session_01WGufzeYWDZN6LkXji5ziLF
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 21 documentable anchor(s).

⛔ 2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via sys_delete (literal, a string literal in fixture))
  • content/docs/releases/v17/17-5.mdx (via retiredAfter (symbol, a field of const object pageActionBlockVariantDestructiveToDanger), retiredFromLoadPath (symbol, a field of const object pageActionBlockVariantDestructiveToDanger))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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

Coarse fallback — 148 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 ae16687ad430f4f9870ea5ba2bc9b63d6f41fc31 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 701da0c562091acf7eaf60292ff3b920e277a8fc — the merge of head ea8465ed15e142c21d11849e0276173324f26056 into base ae16687ad430f4f9870ea5ba2bc9b63d6f41fc31, 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 701da0c562091acf7eaf60292ff3b920e277a8fc && git checkout 701da0c562091acf7eaf60292ff3b920e277a8fc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ae16687ad430f4f9870ea5ba2bc9b63d6f41fc31 ea8465ed15e142c21d11849e0276173324f26056 && git checkout -B drift-repro ae16687ad430f4f9870ea5ba2bc9b63d6f41fc31 && git merge --no-ff ea8465ed15e142c21d11849e0276173324f26056

node scripts/docs-audit/affected-docs.mjs --json ae16687ad430f4f9870ea5ba2bc9b63d6f41fc31

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ae16687ad430f4f9870ea5ba2bc9b63d6f41fc31 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 17ecbb46691a137a09ba42f96d011e40d36b9a1f
Local-runs: none
Scope: packages/spec/src/ui/component.zod.ts, packages/spec/src/conversions/registry.ts, packages/spec/src/conversions/page-action-block-variant-destructive-to-danger.test.ts, packages/spec/src/migrations/entries/semantic/18.ui-page-action-block-variant-destructive-refused.ts, packages/spec/src/shared/retired-key-removal-version.pin.test.ts, packages/spec/src/ui/component-report-items-action-members-typed.pin.test.ts, scripts/sdui-manifest.record.json, sdui.manifest.json, ui/apps/console/src/tests/registry-inputs-spec-parity.test.ts, ui/packages/app-shell/src/views/RecordDetailView.tsx, ui/packages/components/src/renderers/action/tests/action-group-menu-inputs-11168.test.tsx, ui/packages/components/src/renderers/action/tests/action-variant-confirm-26665.test.tsx, ui/packages/components/src/renderers/action/action-bar.tsx, ui/packages/components/src/renderers/action/action-button.tsx, ui/packages/components/src/renderers/action/action-group.tsx, ui/packages/components/src/renderers/action/action-icon.tsx, ui/packages/components/src/renderers/action/action-menu.tsx, ui/packages/components/src/renderers/action/runner-variant.ts, ui/packages/plugin-detail/src/DetailView.tsx, ui/packages/types/src/ui-action.ts + packages/spec/src/ui/action.zod.ts, packages/spec/src/shared/strict-object.ts, packages/spec/src/conversions/apply.ts, packages/spec/src/conversions/stored.ts, packages/spec/src/conversions/walk.ts, packages/metadata/src/loaders/database-loader.ts, packages/metadata-protocol/src/protocol-stored-item-conversion.ts, packages/objectql/src/plugin.ts, packages/metadata-core/src/artifact-forward-conversion.ts, packages/lint/src/authoring-rules.ts, scripts/check-adr-0087-registration.mjs, .changeset/config.json, .changeset/25922-unpublish-app-shell.md, .changeset/26665-action-block-destructive-confirm.md, ui/packages/core/src/actions/ActionRunner.ts, ui/packages/components/src/renderers/layout/containers.tsx, ui/packages/vscode-extension/src/providers/SchemaValidator.ts

Inputs read: card #26779 (body, triage 6448425754, claim 6454608654, os-dev report 6456191607), PR #26966 body and 22-file list, the net diff against main at the head, and the head's check-runs. Prose faces (content/docs/references/ui/component.mdx, .changeset/26779-action-block-variant-danger.md) are read for ②, not reviewed.

Check-runs on the head at review time (59 runs): 24 success (Check Changeset, PR-body guards, Governed Surface Queue Guard, Spec property liveness, Lint · CI, release & engine gates, Lint · ESLint, Lint · repo & tooling gates, Lint · docs & code guards, Lint · PM dispatch-gates, the three scoped lints, Type Check · source gates, Type Check · debt ledger, Build UI Deps, Build Docs, Dogfood Regression Gate, Plan Test Core shards, Auto Label, Check Documentation Links, Flag docs affected, Check PR Size, filter, Lint & Repo Gates), 5 skipped (opt-in and live-DB jobs), 30 in_progress (Test Core 1–8, Test UI 1–16, Lint UI, Type Check UI, Build & E2E UI, Build Core, Type Check · consumer gates, Type Check · workspace). The in-progress runs are the derived gates this verdict still depends on: the spec/ui test suites, the sdui.manifest.json equality check inside Build & E2E UI, and the ui type-check of the by-reference UIActionSchema['variant']. Nothing below is a local re-run.

① Derived judgments

  • The five rows move to ACTION_BLOCK_VARIANTS (default | danger | outline | secondary | ghost | link, plus primary on action:button, action:icon and the group/menu member, exactly where it was): right. Triage 6448425754 rules the protocol spelling is danger, the word ActionSchema.variant (action.zod.ts:1375) and the runner (ActionRunner.ts:1891) read. BUTTON_PRIMITIVE_VARIANTS is module-private with the five readers the diff rewrites and none left in the tree, so replacing it rather than keeping it beside is right.
  • The refusal lives in the enum's error map, not in aliases: right. strictObject's aliases is a KEY table (aliases[aliasProbe(key)], strict-object.ts:32-34, 151); a renamed VALUE has no key to alias. actionBlockVariant() keys the rename on issue.input === 'destructive' and returns undefined for every other miss, the OBJECT_FORM_LAYOUT_RETIRED / FORM_VIEW_LAYOUT_RETIRED shape (component.zod.ts:6853, view.zod.ts:4449). The claim's "aliases prescription" is met in substance: the refusal prescribes danger in so many words ("Write variant: 'danger'"), names the key the author wrote (subject), names the release ("in @objectstack/spec 18" — the pin's derivation for retiredAfter equal to the label, retired-key-removal-version.pin.test.ts header) and the chain step (os migrate meta --from 17 --write, toMajor − 1). The dev recorded the deviation in out_of_scope_findings[1]; judged right, no finding.
  • CONTROL rows: an unknown value keeps the enum's own message and lists "danger"; pinned per row in the new test. Right.
  • D2 page-action-block-variant-destructive-to-danger (toMajor: 18, retiredFromLoadPath: true, retiredAfter: '18.2.0', order 77): right. 18.2.0 is the package label and the highest stamp on main was 18.1.0, so the stamp is the newest published stable release and the pin's "every retired entry at the newest release has a JOIN row" guard is met by the six-refusal row added. Order 77 is free (76 is main's highest) and the entry is inserted alphabetically. Reach is mapPageComponents (regions, slots single or list, nested through mapComponentTree, walk.ts:403-460), the props gate's own reach. It rewrites properties.variant on the four block types and member variant under properties.actions on the two containers only; element:button and every other value are left as stored; copy-on-write and idempotent by construction; fixture pair expects 6 notices over regions, a nested page:card, and a slotted header. Right.
  • D3 ui-page-action-block-variant-destructive-refused: right. Both ids resolve where check-adr-0087-registration looks (registry.ts and the entries/semantic dir, script lines 511/527); both are added by this diff, so registered is the honest disposition. The generated migrations/registry.ts is gitignored and check:generated is reported 14/14 on the merged head.
  • ui renderers: toRunnerVariant forwards danger | primary | secondary | ghost | link and nothing for default / outline; toButtonVariant draws danger as the primitive's destructive, primary as default, fallback otherwise. Four renderers draw through it; dropdown and menu items paint red on danger. action-group.tsx:135 toButtonVariant(action.variant || variant, 'outline') is equivalent to the old chain because the group row never took primary. Registry inputs enums on button / icon list danger; sdui.manifest.json carries the two enum values; the record's objectuiWorkspaceVersion moves to the label. runner-variant.ts is not re-exported from the package index, so toButtonVariant / ButtonPrimitiveVariant widen no public surface. Right.
  • UIActionSchema.variant stays by reference to ActionButtonProps['variant'], so it narrows with the row (destructive reds, danger accepted — the PR body's reverse check; the ui type-check is in progress). Right.
  • Forced edits (RecordDetailView.tsx and DetailView.tsx Delete entry → danger; the action-bar.tsx doc example; the four fixtures): right — the menus now paint red on danger only. The page-header tests keep variant: 'destructive' on the host's sys_delete fixture as the by-name control (containers.tsx:2281 reads variant === 'danger' || name === 'sys_delete'): right.
  • Public-surface changes implied: @objectstack/spec ComponentPropsMap rows narrowed (value refused); @object-ui/types UIActionSchema['variant'] narrowed by reference; @object-ui/components toRunnerVariant input narrowed (internal). No export renamed or removed. Right.

② Semver level

  • Changeset .changeset/26779-action-block-variant-danger.md: @objectstack/spec, @object-ui/components, @object-ui/types, @object-ui/plugin-detail at minor. The kind is Metadata-protocol: narrowed (a value refused), and AGENTS.md step 3 says a kind takes at least minor on every package the diff moves, with major refused by check-changeset-no-major; minor is the level the rule yields for a narrowing. Right.
  • Clause-②: no (narrowing) line-initial in the PR body and the changeset; Metadata-protocol: narrowed on both; BREAKING banner; Before / Now / Fix; one marker adr-0087: registered page-action-block-variant-destructive-to-danger, ui-page-action-block-variant-destructive-refused; the prescription **\destructive` → `danger`.**is two backticked operands around an arrow, whichREWRITE_RE(script line 1215) reads as a FROM → TO. Check Changeset, PR-body guards and Lint · CI, release & engine gates aresuccess`. Right.
  • @object-ui/app-shell absent from the frontmatter: not owed. The package is private: true and left the Changesets fixed group at app-shell: a published library that is 66% Console product surface — retire the 67 zero-reference exports now, decide the split, and fold the second i18n table into @object-ui/i18n #25922 (.changeset/25922-unpublish-app-shell.md: "18.2.0 is the last published @object-ui/app-shell"); step 3 owes a changeset to "anything that publishes", and this package publishes nothing. Neither must-fix nor should-fix.
  • The unreleased .changeset/26665-action-block-destructive-confirm.md on main: should-fix, in this PR. It compiles into the same @object-ui/components / @object-ui/types CHANGELOG entry as this one and says the blocks translate destructive to danger and that "danger stays refused there" — both untrue once this PR lands, in the same release. AGENTS.md row 708 lets a released CHANGELOG entry be corrected only by a dedicated docs-only PR afterwards, so the cheap moment is before the release consumes the file: trim the sentence that describes the removed bridge (or note that spec: action variant is spelled in two dialects — ActionSchema.variant says danger, the action block rows say destructive, so one action changes spelling between a declaration and a block #26779 supersedes it) in this PR. Not a must-fix: the contract and this PR's own changeset carry the truth; only release prose is stale.

③ Boundary flags

  • open_questions: none declared.
  • out_of_scope_findings[0] (the action:button / icon / group / menu: UIActionSchema.variant spells danger destructive while the spec's ActionDef says danger, so a block-declared danger action confirms through a neutral button #26665 changeset): answered above as should-fix here, not left for the seat.
  • out_of_scope_findings[1] (the aliases wording): answered in ① — the deviation is right.
  • PR body "Not measurable from here: stored deployed rows and hotcrm sources": stored rows are measured by code, not by deployment (④ below). A hotcrm source with a destructive block meets validateComponentProps as a gating error on os validate, os build and os lint (authoring-rules.ts:1264-1288, AUTHORING_COMMANDS = ['validate','build','lint']) and the enum's rename at the runtime save door (surfaces: CLI_AND_RUNTIME, runtimeTypes: ['page']). Accepted.
  • PR body "Unchanged on purpose" (page-header fixtures, ActionPreview.tsx): read in the cited code; both hold.
  • Dogfood: the two failures in packaged-action-disabled-dispatch-live.dogfood.test.ts are the recorded load flake, this diff touches no action-disable path, and the head's Dogfood Regression Gate is success. Accepted.
  • Author: PR user.login is objectstack-fleet[bot]; no relay note owed.

④ Design judgment

The narrowing is judged against the published spec on main (8074feb6 as first read; aa00362f merge-base): the spec declares a destructive action block and keeps declaring it, now under the one spelling danger. The narrowing makes the spec's own refusal real at every door rather than taking a capability away at the consumer, so it is the right kind of narrowing under Prime Directive 12. The #26776 bridge existed only because the spec spoke two dialects; with one dialect the bridge is exactly the "lenient alias in a consumer" that directive forbids, so removing it is right provided no stored page reaches a renderer with destructive. I tried to break that claim path by path:

  1. Stored page rows — DatabaseLoader.rowToData (database-loader.ts:884), the protocol's convertStoredItem (getMetaItems, loadMetaFromDb, drafts; protocol.ts:7334, 9835, 10880, 10945) and ObjectQLPlugin.convertStoredRow (plugin.ts:3115) all go through applyConversionsToStoredItem, which pins includeRetired: true (stored.ts). The new test replays that seam over a stored page row for all four blocks and a member and parses the result against the row that now refuses destructive. Covered.
  2. Built artifacts and package install — applyArtifactForwardConversions (metadata plugin door, engine, load-artifact-bundle, app-plugin) opens the window per entry: floor < runtime OR floor ≤ retiredAfter OR (floor = M.0.0 AND toMajor = M). With retiredAfter: '18.2.0' equal to the running label, every artifact declaring a floor at or below 18.2.0 (including ^18 and ^18.2.0) replays; a floor above 18.2.0 can only be built by a spec that already refuses the value. Covered.
  3. Sources — os migrate meta --from 17 --write reaches the entry by id through the chain; the prescription names it. Covered.
  4. History rows are served verbatim by design; a restore goes through the save door and meets the rename loudly. Acceptable.
  5. Authored sources on the load path (defineStack / the fs loader at os dev): a page's properties is not parsed there, and the authoring funnel never replays a retired entry, so a stale source draws the fallback variant with a neutral confirm until os validate / os build / os lint refuse it. This is not a stored page and is the posture every retiredFromLoadPath page entry on main already takes (page-action-row-navigation-executor-to-url, page-template-full-width-to-default, …); the D3 entry states it. Read, not a finding.
  6. ui packages that parse pages themselves — ui/packages/types/src/zod/* carries no action-block variant list (its destructive enums are badge, alert, toast and button-group primitives); DeclaredActionsBar, record-quick-actions, MetadataTypeActions, elements.tsx already map the ActionSchema's danger onto the primitive's destructive, consistent with the new dialect. One stale duplicate remains: ui/packages/vscode-extension/src/providers/SchemaValidator.ts:234-239 validates a button node's variant against a hand-copied default | destructive | outline | secondary | ghost | link (it already lacked primary), so if that button is the action block it now warns on the canonical danger and accepts the refused destructive. Should-fix: read the vocabulary from the spec row or drop the check; a warning-severity diagnostic in a private editor extension, no render path.

No path lets a stored page carry destructive to a renderer, so the bridge removal is not a must-fix. The conversion's own docblock reads the seams correctly. Findings: two should-fix (②'s unreleased #26665 changeset, ④'s vscode validator list), no must-fix.

Implemented-by: claude/project-thread-4h9cyt
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

VERDICT: PASS

…e VS Code validator judges action block variants

Claude-Session: https://claude.ai/code/session_01WGufzeYWDZN6LkXji5ziLF
Co-authored-by: Claude <noreply@anthropic.com>
…es its variant sentences

Claude-Session: https://claude.ai/code/session_01WGufzeYWDZN6LkXji5ziLF
Co-authored-by: Claude <noreply@anthropic.com>
…-4h9cyt

# Conflicts:
#	scripts/sdui-manifest.record.json
…ter merging main

Claude-Session: https://claude.ai/code/session_01WGufzeYWDZN6LkXji5ziLF
Co-authored-by: Claude <noreply@anthropic.com>
… list successor, and spell the layout guide's Delete button danger

The major-18 merge pin wants each conversion defined directly above the
entry that follows it in MAJOR_18_CONVERSIONS. The layout guide's detail
page fence still declared `variant: 'destructive'` on an action:button,
which the row now refuses.

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

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-28T04:55Z

Head 98d05a33ce49ef3cf86c0bce6dd2db0dfc235bf9. Card #26779 (triage 6448425754: the protocol spelling is danger), claim 6454608654, dev report 6456191607.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT (armed) · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-28T07:09Z

Head 2f05526d5ba495fd8845dbffc1470b3f9e30ee20. This carries the seat's ACCEPT at 98d05a33 to this head.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 07:10
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 07:10
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 40648482070 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test UI (11/16) — 失败步骤: Run the ui tests (shard 11/16)

    × (f) CONTROL: offers it, once, to a member granted that permission 55ms
    FAIL   dom  packages/app-shell/src/views/__tests__/AppReportIndex.navGates-26892.test.tsx > ⌘K offers its navigation entries as the sidebar shows them (objectstack#26892) > (f) CONTROL: offers it, onc
      ↳ 失败原因: AssertionError: expected [ 'Alpha', 'Beta', 'Gamma' ] to deeply equal [ 'Alpha', 'Beta', 'Delta', 'Gamma' ]
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 26 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 40649482581 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test UI (10/16) — 失败步骤: Run the ui tests (shard 10/16)

    × (f) CONTROL: offers it, once, to a member granted that permission 55ms
    FAIL   dom  packages/app-shell/src/views/__tests__/AppReportIndex.navGates-26892.test.tsx > ⌘K offers its navigation entries as the sidebar shows them (objectstack#26892) > (f) CONTROL: offers it, onc
      ↳ 失败原因: AssertionError: expected [ 'Alpha', 'Beta', 'Gamma' ] to deeply equal [ 'Alpha', 'Beta', 'Delta', 'Gamma' ]
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 27 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 2f05526d5ba495fd8845dbffc1470b3f9e30ee20 · 2026-10-28T07:54Z

Triage seat, under the gap rule (#27062), condition (b). The record on file names 17ecbb46. The head has moved and has been ready for more than one round with no record and no claim on it. Triage runs the delta review at CONTRACT_REVIEW_TIER. If the earlier reviewer claims or posts a record on this head first, triage withdraws.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2f05526d5ba495fd8845dbffc1470b3f9e30ee20
Local-runs: none
Scope: packages/spec/src/conversions/registry.ts, packages/spec/src/migrations/entries/semantic/18.ui-page-action-block-variant-destructive-refused.ts, packages/spec/src/ui/component.zod.ts + packages/spec/src/ui/action.zod.ts, packages/spec/src/conversions/types.ts, packages/spec/src/conversions/walk.ts, packages/spec/src/conversions/stored.ts, packages/spec/src/conversions/page-action-block-variant-destructive-to-danger.test.ts, packages/spec/src/shared/retired-key-removal-version.pin.test.ts, packages/spec/scripts/conversions-major18-merge.test.ts, packages/metadata-core/src/artifact-forward-conversion.ts, scripts/check-adr-0087-registration.mjs, docs/adr/0087-metadata-protocol-upgrade-contract.md, .changeset/26779-action-block-variant-danger.md, .changeset/26665-action-block-destructive-confirm.md, content/docs/references/ui/component.mdx, ui/content/docs/guide/layout.md, ui/packages/components/src/renderers/action/runner-variant.ts, ui/packages/components/src/renderers/action/action-bar.tsx, ui/packages/components/src/renderers/layout/containers.tsx, ui/packages/core/src/actions/ActionRunner.ts, ui/packages/types/src/ui-action.ts, ui/packages/vscode-extension/src/providers/SchemaValidator.ts, .claude/skills/pm-dispatch/SKILL.md

Delta review on a moved head. The record on file (6456371394, Reviewed-by session_01WGufzfhobWEgUUrjz4rrbh) names 17ecbb46; this record judges the eight commits since and then re-judges the whole on 2f05526d. Inputs read: card #26779 (body, triage 6448425754, claim 6454608654, os-dev report 6456191607; no newer card comment at review time), PR #26966 body (live body re-fetched; the three declaration lines read verbatim), the 24-file list, the net diff against main at the head (git diff 50b1b7d8..prr-26966, byte-identical to the fetched net.diff apart from index lines), the eight delta commits, ruling 6431004737 on #26369 (the Skills-docs: rule), and the check-runs on the head. Prose faces (component.mdx, the two changesets, layout.md) are read for ② and ④, not reviewed.

Check-runs on 2f05526d at review time (re-fetched live): 65 runs, all completed — 58 success, 7 skipped (opt-in and live-DB jobs: Dogfood Regression Gate shards, UI Published Artifacts, Dogfood Verify CLI, Temporal Conformance, 18.x upgrade harness, Schedule gate, Packed-tarball smoke), 0 red, 0 pending. The 30 runs the earlier record left in progress are answered green here: Test Core 1–8, Test UI 1–16, Lint UI, Type Check UI, Build & E2E UI (which regenerates sdui.manifest.json and fails on a byte difference), Build Core, Type Check · consumer gates and · workspace. The two merge-queue failures on the PR (6463353150, 6463487276) are AppReportIndex.navGates-26892.test.tsx in ui/packages/app-shell, a file this PR does not touch, aggregated to #27116 as one speculative-stack hit; they are queue triage, not a check-run on this head, and do not move the verdict.

Delta since 17ecbb46

  • Four merges of main (138c4d47, 98d05a33, fe38d546, 2f05526d), recomputed. 98d05a33, fe38d546, 2f05526d: git merge-tree --write-tree P1 P2 yields a tree whose diff against the committed merge is EMPTY — clean merges, no evil-merge content. 138c4d47: the shallow clone has no reachable merge base for git merge-tree, so it was recomputed per file with git merge-file (ours 513d5b6e, base 912076f8 = 17ecbb46's main parent, theirs cb8988ee) over the 22 PR files then present. 20 are identical to the committed merge; 2 differ: scripts/sdui-manifest.record.json is a true conflict (both sides moved sha256/generatedAt) resolved by hand, and content/docs/references/ui/component.mdx was resolved to the PR side, i.e. it kept the pre-feat(spec,ui)!: a record opens one way per object — navigation.mode keeps drawer (the default), page, new_window and none; modal, split and popover are retired; the split pane becomes a reader preference #26804 navigation.mode enum text instead of main's regenerated text. Neither resolution survives to the head: 01e30183 regenerates both files, and at 2f05526d the mdx's net diff against the merge base is exactly the six variant rows and nothing else, while the record's sha256 is the one Build & E2E UI (green) regenerates. The three hits files merged clean in all four merges. No file outside the PR's own 24 differs between any merge and its main parent (name-only diffs checked for all four).
  • d34cb6e3 + 513d5b6e: the first edits .changeset/26665-action-block-destructive-confirm.md and adds the VS Code validator check; the second restores the action:button / icon / group / menu: UIActionSchema.variant spells danger destructive while the spec's ActionDef says danger, so a block-declared danger action confirms through a neutral button #26665 changeset byte-for-byte (measured: git diff 50b1b7d8..prr-26966 -- .changeset/26665-… is empty) and adds the supersession paragraph to this PR's changeset. Net effect: the earlier record's should-fix (1) is taken by supersession rather than by edit, and should-fix (2) is taken.
  • 01e30183: regeneration only (component.mdx, sdui-manifest.record.json); the objectuiWorkspaceVersion 18.1.0 → 18.2.0 hunk of the old head is gone because main now carries 18.2.0.
  • 2ff9fc87: moves the conversion's definition directly above pageActionRowNavigationExecutorToUrl — a pure move: the PR's registry.ts hunks at the old and new head are identical line for line once @@ headers are stripped, only context differs — and spells ui/content/docs/guide/layout.md's Delete action:button danger. The move is owed by packages/spec/scripts/conversions-major18-merge.test.ts rule 2 (define directly above the entry that follows in the identifier-sorted list; pageActionB… sorts before pageActionR…), and Test Core is green.
  • Main-side changes to the PR's files since the old head: feat(spec,ui)!: a record opens one way per object — navigation.mode keeps drawer (the default), page, new_window and none; modal, split and popover are retired; the split pane becomes a reader preference #26804 (navigation.mode retirement), docs(skills): the i18n skill names fields.{field}.inlineTitle and a joined report's block headings #26832, refactor(plugin-calendar,plugin-gantt)!: retire the flat fallback read; the calendar and gantt renderers read the block only #27026 touch component.zod.ts, registry.ts, the pin test and the manifest. Consequence: MAJOR_18_CONVERSIONS now carries order: 77 twice — navigationModeOverlayValuesRemoved (main) and pageActionBlockVariantDestructiveToDanger (this PR). The list's own rule allows it ("Two retirements in flight may take the same number; they then apply in id order", inApplicationOrder ties on id), the two surfaces are disjoint (navigation.mode values vs action-block variant), and orders 35, 55, 57, 61, 63, 67 are already shared on main. The earlier record's "77 is free" is stale but nothing turns on it. retiredAfter: '18.2.0' still equals the @objectstack/spec label at the head (18.2.0), so the pin still derives 18 and the refusal text's "in @objectstack/spec 18" stays right.
  • PR hunks old head vs new head over the 24 files: identical except the five deltas above (changeset paragraph, regen bytes, layout.md, SchemaValidator.ts, the pure move). The earlier record's ① and ④ readings of the hits files therefore still describe this tree; each was re-read below rather than adopted.

① Derived judgments

  • The five rows move to ACTION_BLOCK_VARIANTS (default | danger | outline | secondary | ghost | link, + 'primary' on action:button, action:icon and the group/menu member, none on the containers' own trigger): right. Triage 6448425754 rules the protocol spelling danger, the word ActionSchema.variant (action.zod.ts) and the runner (ActionRunner.ts:1958, action.variant === 'danger') read. BUTTON_PRIMITIVE_VARIANTS has no reader left anywhere in the head tree (git grep empty), so replacing it is right.
  • Refusal in the enum's error map, not aliases: right. actionBlockVariant() keys on issue.input === 'destructive' and returns undefined otherwise, so Zod 4 (zod ^4.6.6) keeps its own invalid_value message for every other miss — the CONTROL rows pin that the other message lists "danger" and never says renamed. The prescription names the key the author wrote (subject), the release ("in @objectstack/spec 18", the pin's derivation: retiredAfter ≥ newest stable ⇒ protocolMajor), the fix (Write variant: 'danger') and the chain step (os migrate meta --from 17 --write, toMajor − 1). The claim's "aliases prescription" is met in substance; strictObject aliases are a key table and a value has no key.
  • D2 page-action-block-variant-destructive-to-danger (toMajor: 18, retiredFromLoadPath: true, retiredAfter: '18.2.0', order 77): right. Reach is mapPageComponents — regions[].components[], slots.* single or list, nested properties containers and page:tabs/page:accordion items[].children[] (walk.ts:290-460). It renames properties.variant on exactly ACTION_RUNNER_BLOCK_TYPES ∪ ACTION_CONTAINER_BLOCK_TYPES and member variant under properties.actions on the two containers only; a non-dict member or non-array list is left as stored; copy-on-write (next === properties ? component : …); idempotent because a rewritten value reads danger, which the entry never matches. Fixture: 6 notices over two regions' blocks, a group trigger + one member, a nested page:card menu member and a slotted header icon; element:button's destructive, a canonical danger and an outline left alone. retiredAfter is the label at the head, as types.ts requires under a stable label. Right.
  • D3 ui-page-action-block-variant-destructive-refused: right. conversionIds names the D2 id; surface carries no backtick (the build-upgrade-guide constraint the file states); the entry file follows the 18.ID.ts convention of the 429 sibling entries; the generated migrations/registry.ts is gitignored (.gitignore:68).
  • Changeset marker adr-0087: registered with both ids: right. The gate's registered arm requires every id to resolve at HEAD and to be added by this diff (check-adr-0087-registration.mjs lines 137-145, R3/R4); both ids are new in this diff and resolve in registry.ts and entries/semantic/. Lint · CI, release & engine gates is success.
  • JOIN row in retired-key-removal-version.pin.test.ts (six refusals, one per row shape): right — the pin requires a row for every retired entry whose retiredAfter is the newest stable, and derives 18 for it.
  • ui renderers (runner-variant.ts): toRunnerVariant forwards danger | primary | secondary | ghost | link, nothing for default | outline; toButtonVariant draws danger as the primitive's destructive, primary as default, fallback otherwise; neither reads destructive (CONTROL rows pin it). The four renderers draw through it; group dropdown and menu items paint red on danger (action-group.tsx:261, action-menu.tsx:154). Every other ui reader of an action variant already maps danger onto the primitive (elements.tsx:311, record-quick-actions.tsx:61, MetadataTypeActions.tsx:71, data-table.tsx:637, RowActionMenu.tsx:489, BulkActionDialog.tsx:622, containers.tsx:2282 variant === 'danger' || name === 'sys_delete'); none reads a block's destructive. Right.
  • UIActionSchema.variant by reference to ActionButtonProps['variant']: narrows with the row; the doc comment is rewritten; Type Check UI is success on this head. Right.
  • VS Code SchemaValidator.ts (d34cb6e3): judges action:button/action:icon with primary, the two containers' trigger without it, and each member with it — the rows' shape exactly; destructive gets the rename hint; the button primitive's list keeps destructive. path and findPropertyRange are in scope (lines 201, 313). Package object-ui is private: true; no changeset owed. Right.
  • Regenerated outputs: component.mdx net hunk is exactly the six rows' enum and .describe() text; sdui.manifest.json carries the two destructive → danger input enum values; the record's sha256 is CI's. Right.
  • Forced edits: RecordDetailView.tsx, DetailView.tsx Delete entries and the action-bar.tsx doc example spelled danger; layout.md's fence spelled danger (the row refuses the old value); the page-header fixtures keep sys_delete with destructive as the by-name control. Right.
  • Public-surface changes implied: @objectstack/spec ComponentPropsMap rows narrowed (a value refused); @object-ui/types UIActionSchema['variant'] narrowed by reference; @object-ui/components renderers narrowed in what they read; no export renamed or removed; runner-variant.ts is not re-exported from the package index. Right.

② Semver level

  • Levels: .changeset/26779-action-block-variant-danger.md → @objectstack/spec, @object-ui/components, @object-ui/types, @object-ui/plugin-detail at minor. AGENTS.md step 3: a kind other than none takes at least minor on every package the diff moves; Metadata-protocol: narrowed (a value refused) is the kind, (narrowing) the arm, so minor is the level the rule yields. All four are published (no private); @object-ui/app-shell and object-ui (vscode-extension) are private: true and owe nothing. Right.
  • Declarations: Clause-②: no (narrowing) and Metadata-protocol: narrowed line-initial in the PR body and in the changeset; BREAKING banner; Before / Now / Fix; one adr-0087: registered … marker; the **\destructive` → `danger`.**FROM → TO. Check Changeset, PR-body guards and Lint · CI, release & engine gates aresuccess`. Right.
  • ADR-0087 disposition: registered, judged above — right, and the honest one for ids this diff adds.
  • The action:button / icon / group / menu: UIActionSchema.variant spells danger destructive while the spec's ActionDef says danger, so a block-declared danger action confirms through a neutral button #26665 changeset: left byte-identical to the base (measured); this PR's changeset carries a paragraph that explicitly supersedes its variant sentences in the same release and keeps its confirm-dialog behaviour. The earlier record's should-fix (1) asked for an edit; the seat's call to supersede instead is defensible (editing another card's pending note is the DELIBERATE CORRECTION class check-empty-changeset holds red), and the compiled CHANGELOG will carry the later paragraph naming the earlier one. Accepted; no finding.
  • Skills-docs: line ([Decision] pm-dispatch: which PRs owe a Skills-docs: line (the #26184 audit Part B rule draft) — protocol-kind only, protocol-kind plus narrowing, or not adopted #26369, ruling 6431004737 letter B) — MUST-FIX. The rule: a PR whose Clause-②: carries (narrowing) or whose kind is not none owes Skills-docs: updated here | follow-up owed — FILES | follow-up #N | none — REASON, "read at the seat's ACCEPT: missing or untrue refuses the landing" (SKILL.md:313, review-checklist.md:18 "在且属实 … 缺 ⇒ REWORK"). The live PR body says Skills-docs: none — no page under skills/**, content/docs/** (outside the generated references) or ui/content/** teaches an action block's variant; …. That REASON is untrue against the PR's own file list: ui/content/docs/guide/layout.md is a page under ui/content/** whose detail-page fence teaches an action:button with "variant", and this diff changes it (2ff9fc87, "destructive" → "danger", the dev's own message: "the layout guide's detail page fence still declared variant: 'destructive' on an action:button, which the row now refuses"). The line was already untrue at 17ecbb46, where that page still taught the refused value, and neither the earlier record nor the seat's two ACCEPTs re-read it after 2ff9fc87. Fix (PR body only, no head move): rewrite the line to a true value — updated here — ui/content/docs/guide/layout.md only if the package's evals exercise the new spelling (that is what updated here means, SKILL.md:313), otherwise follow-up owed — FILES or follow-up #N. Measured for the record: no OTHER teaching page carries destructive on an action block at this head (every file under skills/**, content/docs/** outside references, ui/content/**, examples/**, apps/**, packages/platform-objects/** that names an action block was grepped; the remaining destructive hits are Button/Badge/Alert primitives, --allow-destructive, and release pages; slotted-pages.md:295 already teaches variant: 'danger').

③ Boundary flags

  • open_questions: none declared.
  • out_of_scope_findings[0] (the unreleased action:button / icon / group / menu: UIActionSchema.variant spells danger destructive while the spec's ActionDef says danger, so a block-declared danger action confirms through a neutral button #26665 changeset): taken by supersession in 513d5b6e (② above). Closed.
  • out_of_scope_findings[1] (the aliases wording): the deviation is right (①). Closed.
  • The earlier record's should-fix (2) (VS Code validator): taken in d34cb6e3, judged right in ①. Closed.
  • PR body "Not measurable from here: stored deployed rows and hotcrm sources": stored rows are measured by code — applyConversionsToStoredItem pins includeRetired: true (stored.ts:113) and the new test drives that seam on all four nodes and a member; a hotcrm source meets the rows' refusal at os validate/build/lint and the save door. Accepted.
  • PR body "Unchanged on purpose" (page-header sys_delete fixtures, ActionPreview.tsx): hold in the cited code (containers.tsx:2282). Accepted.
  • Dogfood: the PR body's two packaged-action-disabled-dispatch-live failures are the recorded load flake; the head's Dogfood Regression Gate ran green (success) and this diff touches no action-disable path. Accepted.
  • Merge-queue reds (Queue-flake anchor: packages/app-shell/src/views/__tests__/AppReportIndex.navGates-26892.test.tsx #27116, AppReportIndex.navGates-26892): outside this PR's files; not a head check-run. Noted, not a flag on this PR.
  • Docs Drift Check flagged content/docs/releases/v17/17-5.mdx as release-owned and read-only; the PR does not edit it. Right.
  • Out of the card's seam, read and NOT a finding, left for the lane: record:quick_actions.variant (component.zod.ts:2339) and record:alert's action variant (:2227) keep the Button primitive's destructive. These are a container's button style on blocks that draw declared ActionSchema actions (whose danger the renderers already map), not an action's own variant, and triage 6448425754 names the four block rows only. If the lane wants one dialect on every block-level button style, that is a new card, not this PR.
  • The PR body's "Verification" and "Authored values converted" paragraphs predate 2ff9fc87 and omit ui/content/**; the tree is judged, not the body, but that stale account is the source of the untrue Skills-docs: line above.

④ Design judgment

Judged against triage 6448425754 (one spelling, danger), ADR-0087 D2/D3 (a lossless rename lands as a retiredFromLoadPath conversion beside its D3 entry; data-at-rest seams replay retired entries), Prime Directive 12 (no consumer alias), and the first consumers — ActionRunner.ts:1958 and runner-variant.ts.

packages/spec/src/ui/component.zod.ts. The narrowing is the right kind: the spec keeps declaring a destructive action block, now under the runner's own word, and makes its refusal real at every door instead of taking a capability away at the consumer. Attempts to break it: (a) the error map's undefined on other misses — Zod 4 falls to its own message, pinned by CONTROL rows; (b) issue.input comparison — only the exact string 'destructive' gets the rename, so a near-miss keeps the enum list; (c) JSON-Schema/describe projection — Spec property liveness and Build Docs are green and the regenerated mdx shows the new enum; (d) the by-reference ui type — Type Check UI is green on this head. One stale prescription remains IN THIS FILE: the group/menu member className refusal (actionContainerMemberShape, lines 4243-4244) tells the author "Style it with its variant (destructive draws it red)" — a prescription onto the very value the same row now refuses, so an author who follows it meets a second refusal (the rename). Loud, not silent, so should-fix: say danger there, and in the docblock at line 4115 ("where destructive draws the item red"). Not a FAIL.

packages/spec/src/conversions/registry.ts. Holds against walk.ts (reach), stored.ts (retired entries replayed), artifact-forward-conversion.ts (window: floor below the runtime, OR floor at or below retiredAfter, OR floor equal to M.0.0 with toMajor M; with retiredAfter = 18.2.0 = the label, every artifact declaring a floor at or below 18.2.0 replays it), types.ts (retiredAfter is the label under a stable label) and the merge pin (identifier-sorted insertion, definition directly above its list successor, shared order allowed). Paths tried: stored page rows (covered, tested), built artifacts in window (covered), sources via os migrate meta --from 17 --write (named by the prescription), history rows (served verbatim by design; a restore meets the save door), authored sources at os dev (not stored; the established retiredFromLoadPath posture). One row-less reader found: action:bar (action-bar.tsx) spreads each member onto an action:button and now draws it through toButtonVariant, so a stored or JSON-authored action:bar member written variant: 'destructive' would draw the fallback and forward nothing, with neither a refusal (no ComponentPropsMap row for action:bar) nor a rewrite (the conversion follows the four rows). Mitigants measured: action:bar is absent from sdui.manifest.json (not public tier), no in-repo authored instance writes a destructive member, and a TypeScript-authored member is typed UIActionSchema['variant'], which reds destructive at compile. Should-fix: either let the conversion also visit action:bar properties.actions[].variant (one more member of the container set, a value rename like the others) or state in the entry's docblock why action:bar is out. Not a must-fix: a non-public, row-less block with no measured instance.

packages/spec/src/migrations/entries/semantic/18.ui-page-action-block-variant-destructive-refused.ts. Holds against its first consumer (the chain step for --from 17, which reads conversionIds) and build-upgrade-guide (no backtick in surface). reason reads the seams correctly (props not parsed on the load path; save door and os validate refuse; D2 rewrites rows, artifacts and migrate meta --write); acceptanceCriteria is checkable (component-props-invalid at properties.variant / properties.actions.N.variant). No finding.

No path lets a stored page of the four rowed blocks reach a renderer with destructive, so removing the #26776 bridge remains right under Prime Directive 12. Findings on this head: one must-fix (②, the untrue Skills-docs: line — a one-line PR-body rewrite, no code change), three should-fix (④ className prescription + docblock in component.zod.ts; ④ action:bar members in the conversion or its docblock; carried from the earlier round and now closed: none). The earlier record's PASS is otherwise confirmed on this head: its two should-fixes are taken, its ① and ④ readings re-read and still hold, and the 30 gates it left pending are green.

Implemented-by: claude/project-thread-4h9cyt
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: FAIL


Generated by Claude Code

…riants too, and member guidance says danger

action:bar has no props row, but it draws each `actions` / `systemActions`
member through the block renderers, which no longer read `destructive`, so a
stored bar written that way fell back to a plain button with nothing to say
so. The conversion now renames the bar's own `variant` and its members'.
The container member `className` refusal and the member docblock named
`destructive` as the red variant; they say `danger`.

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

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT (armed) · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-28T12:05Z

Head ea8465ed15e142c21d11849e0276173324f26056. Card #26779. This supersedes the seat's ACCEPT 6462976516 at 2f05526d. The PR left the merge queue at 07:36Z while main was red, and the delta review 6464021995 then came back FAIL.

  • The must-fix was that the Skills-docs: line was untrue, which is PR-body only. The seat rewrote it to follow-up owed — skills/objectui/evals/. ui/content/docs/guide/layout.md now teaches variant: 'danger' on an action:button, but no eval exercises an action block's variant: the dev grepped skills/objectui/evals and skills/objectstack-ui/evals, and neither names destructive or danger. Closed.
  • Should-fix ④ (className prescription and docblock): 3ae001ff makes the container-member className refusal and the member docblock in component.zod.ts say danger. Closed.
  • Should-fix ④ (action:bar): 3ae001ff makes the conversion visit action:bar. It renames the bar's own properties.variant and the variant of each actions / systemActions member, as the same value rename the four rowed blocks get, copy-on-write, with each list judged independently. The docblock gives the reason: the bar has no props row, and it draws its members through the block renderers with member.variant || properties.variant. The surface, the D3 entry and the changeset's stored-rows paragraph name the bar. The fixture gains a bar (notices 6 → 8), and a stored-seam test and a systemActions-on-a-menu control are added. The seat read the hunk, and the seat patched the PR body's Data at rest paragraph to name the bar. Closed.
  • Delta: ea8465ed merges main (no rebase).
  • Gates: check-runs on this head are 60 success, 11 skipped and 0 red. Check Changeset is success, on the 11:36Z rerun after the body edit; the one cancelled run is the one that rerun superseded. mergeable_state is clean.
  • Landing: check-governed-merges --pr 26966: NOT governed, 827 changed lines. Tier S. automerge_enable in this act; the PR is already ready.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ea8465ed15e142c21d11849e0276173324f26056
Local-runs: none
Scope: packages/spec/src/conversions/registry.ts, packages/spec/src/migrations/entries/semantic/18.ui-page-action-block-variant-destructive-refused.ts, packages/spec/src/ui/component.zod.ts + packages/spec/src/ui/action.zod.ts, packages/spec/src/conversions/types.ts, packages/spec/src/conversions/walk.ts, packages/spec/src/conversions/stored.ts, packages/spec/src/conversions/page-action-block-variant-destructive-to-danger.test.ts, packages/spec/src/shared/retired-key-removal-version.pin.test.ts, packages/spec/scripts/conversions-major18-merge.test.ts, packages/metadata-core/src/artifact-forward-conversion.ts, scripts/check-adr-0087-registration.mjs, docs/adr/0087-metadata-protocol-upgrade-contract.md, .changeset/26779-action-block-variant-danger.md, .changeset/26665-action-block-destructive-confirm.md, content/docs/references/ui/component.mdx, ui/content/docs/guide/layout.md, ui/scripts/check-doc-json-fences.mjs, ui/packages/types/src/tests/page-width-padding-refusal-11318.test.ts, skills/objectui/evals/, ui/packages/components/src/renderers/action/runner-variant.ts, ui/packages/components/src/renderers/action/action-bar.tsx, ui/packages/components/src/renderers/layout/containers.tsx, ui/packages/core/src/actions/ActionRunner.ts, ui/packages/types/src/ui-action.ts, ui/packages/vscode-extension/src/providers/SchemaValidator.ts, .claude/skills/pm-dispatch/SKILL.md

Re-review after the FAIL record on 2f05526d (6464021995, this session). The head moved by two commits: 3ae001ff (the should-fix on action:bar and the member guidance) and ea8465ed (a merge of origin/main). The PR body's Skills-docs: line was rewritten. Inputs read: card #26779 (body and every comment; nothing new since the os-dev report), PR #26966 live body (the three declaration lines read verbatim at review time), the 24-file list (git diff --name-only cdec7473..ea8465ed, the same 24 paths as before), the net diff against the merge's main parent cdec7473, the two delta commits, ruling 6431004737 (#26369 B), and the check-runs on ea8465ed (fetched live). Prose faces are read for ② and ④, not reviewed.

Check-runs on ea8465ed: 72 runs, all completed, none pending — 60 success, 11 skipped, 1 cancelled. Every gate family is present and green: Check Changeset, PR-body guards, Governed Surface Queue Guard, Spec property liveness, the Lint families, Type Check (source, consumer, workspace, debt ledger, UI), Build Core / UI Deps / Docs / Build & E2E UI (which regenerates sdui.manifest.json and fails on a byte difference), Test Core 1–8, Test UI 1–16, Dogfood Regression Gate, Check Documentation Links, Check PR Size, Auto Label. The one cancelled run is Check Changeset job 122790796640 of workflow run 40680829978 (started 11:33:10Z): three workflow runs were dispatched on this head within two minutes (11:32, 11:33, 11:34) and the middle one was cancelled by concurrency; the runs before and after it both completed Check Changeset as success (jobs 122790420556 and 122791450474). A cancelled superseded run is not a red. The skipped set is the opt-in and live-DB jobs plus the duplicated Check PR Size / Auto Label jobs of the cancelled run. mergeable_state is clean.

Delta since 2f05526d

① Derived judgments

  • The five rows on ACTION_BLOCK_VARIANTS (default | danger | outline | secondary | ghost | link, + 'primary' on action:button, action:icon and the group/menu member): right — triage 6448425754, ActionSchema.variant, ActionRunner.ts:1958. BUTTON_PRIMITIVE_VARIANTS has no reader left in the tree.
  • Refusal in the enum's error map: right; destructive alone gets the rename (key named by subject, release @objectstack/spec 18 per the pin's derivation, fix, and os migrate meta --from 17 --write); every other miss keeps Zod's own message, pinned by CONTROL rows.
  • D2 page-action-block-variant-destructive-to-danger (toMajor: 18, retiredFromLoadPath: true, retiredAfter: '18.2.0', order 77), now with the action:bar reach: right. memberLists is ['actions'] for the two containers, ['actions', 'systemActions'] for action:bar, [] for action:button / action:icon, and undefined (skip) for every other type; the bar's own properties.variant is renamed like the four blocks'. That is exactly what action-bar.tsx reads: schema.actions (line 144), schema.systemActions (line 200), variant: schema.variant || 'ghost' for the overflow trigger (289) and variant: action.variant || schema.variant for each member it spreads onto action:button (323). Copy-on-write per list ([key]: nextMembers only when changed), idempotent, non-dict members and non-array lists left as stored; a systemActions list on a block that has none is a CONTROL (left as stored, no notice). Fixture gains a bar with one actions and one systemActions member → expectedNotices: 8. surface lists the three new paths. Right.
  • D3 ui-page-action-block-variant-destructive-refused: right. surface names the bar node and member (still no backtick); reason states plainly that the bar has no props row so nothing refuses destructive on it and the conversion still renames it because the bar draws through the block renderers; acceptanceCriteria gains the bar's keys; conversionIds unchanged.
  • Changeset marker adr-0087: registered with both ids: unchanged by 3ae001ff (only the "Stored rows…" paragraph moved), still the honest disposition for ids this diff adds; Check Changeset and Lint · CI, release & engine gates are success.
  • JOIN row (six refusals on the rowed shapes): unchanged, still owed and present; action:bar has no row and so no refusal to join — right that the row count did not grow.
  • ui renderers, by-reference type, VS Code validator, regenerated outputs, forced edits: as on 2f05526d, right; Type Check UI and Build & E2E UI are success on this head.
  • Public-surface changes implied: unchanged from the previous record — ComponentPropsMap rows narrowed, UIActionSchema['variant'] narrowed by reference, no export renamed or removed.

② Semver level

  • Levels: @objectstack/spec, @object-ui/components, @object-ui/types, @object-ui/plugin-detail at minor — the level AGENTS.md step 3 yields for Metadata-protocol: narrowed with a (narrowing) arm; @object-ui/app-shell and object-ui (vscode-extension) are private: true. Right.
  • Declarations: Clause-②: no (narrowing) and Metadata-protocol: narrowed line-initial in the live PR body and the changeset; BREAKING banner, Before / Now / Fix, one registered marker, the **destructive → danger.** FROM → TO. Right.
  • ADR-0087 disposition: registered — unchanged and right (①).
  • The action:button / icon / group / menu: UIActionSchema.variant spells danger destructive while the spec's ActionDef says danger, so a block-declared danger action confirms through a neutral button #26665 changeset: byte-identical to the base; superseded in prose by this PR's changeset. Accepted, as before.
  • Skills-docs: line ([Decision] pm-dispatch: which PRs owe a Skills-docs: line (the #26184 audit Part B rule draft) — protocol-kind only, protocol-kind plus narrowing, or not adopted #26369 B, ruling 6431004737) — the previous must-fix, now RESOLVED. The live body reads: Skills-docs: follow-up owed — skills/objectui/evals/ (ui/content/docs/guide/layout.md now teaches variant: 'danger' on an action:button; check:doc-json and page-width-padding-refusal-11318 judge that fence against the rows, but no eval exercises an action block's variant). Well-formed: it is the follow-up owed — FILES arm of the closed four, and FILES names where the owed work lands (the objectui skill's eval directory, which exists at the head with eleven eval files). True on this head, each clause measured: (a) layout.md's detail-page fence now writes "variant": "danger" on an action:button; (b) ui/scripts/check-doc-json-fences.mjs (check:doc-json, ui/package.json:70) parses every fence under ui/content/docs with StrictAnyComponentSchema.safeParse, and ui/packages/types/src/__tests__/page-width-padding-refusal-11318.test.ts reads content/docs/guide/layout.md under ui/ and parses its fences the same way — so the fence is judged against the rows; (c) none of the eleven skills/objectui/evals/*.json files mentions variant or any action block, and the skill's own pages (SKILL.md:86, guides/page-builder.md:124, rules/protocol.md:257) teach action:button without its variant — so updated here (which requires the package's evals to exercise it) would have been the untrue value, and follow-up owed is the right one. What remains is the seat's step the rule assigns to it, not a defect of the PR: file the owed card and rewrite owed — skills/objectui/evals/ to #N before ACCEPT (SKILL.md:313).

③ Boundary flags

  • open_questions: none declared.
  • out_of_scope_findings[0] and [1]: closed as on 2f05526d.
  • Previous record's should-fix (④, className prescription + member docblock): taken in 3ae001ff — component.zod.ts:4115 and :4244 now say danger. Closed.
  • Previous record's should-fix (④, action:bar members): taken in 3ae001ff by extending the conversion, with the D3 entry stating the gap. Closed; judged in ④.
  • Previous record's must-fix (②, Skills-docs:): resolved in the PR body; judged in ②. The owed follow-up card is the seat's to file.
  • PR body "Not measurable from here", "Unchanged on purpose", dogfood flake: as before, accepted; the head's Dogfood Regression Gate is success.
  • Merge-queue history (Queue-flake anchor: packages/app-shell/src/views/__tests__/AppReportIndex.navGates-26892.test.tsx #27116, AppReportIndex.navGates-26892): a main flake outside this PR's files; the queue will meet whatever main holds when the seat arms it. Not a flag on this PR.
  • Out of the card's seam, read and not a finding, left for the lane as before: record:quick_actions.variant and record:alert's action variant keep the Button primitive's destructive as a container button style. And now, stated by the D3 entry itself: an action:bar has no props row, so an AUTHORED source that writes destructive on a bar member is neither refused at os validate nor rewritten on the authoring load path (the entry is retired); it draws the fallback until os migrate meta --from 17 --write runs, exactly as the entry says. Giving action:bar a row would be a new surface (it is absent from sdui.manifest.json, so not public tier) and is a card of its own, not this PR's.
  • Author: PR user.login is objectstack-fleet[bot]; no relay note owed.

④ Design judgment

Judged against triage 6448425754, ADR-0087 D2/D3, Prime Directive 12, and the first consumers (ActionRunner.ts:1958, runner-variant.ts, action-bar.tsx).

packages/spec/src/ui/component.zod.ts. Holds, as on 2f05526d: the narrowing keeps declaring the destructive block under the runner's word and makes the refusal real at every door. 3ae001ff removes the one stale prescription this file carried (the member className refusal and the member docblock now name danger as the red variant), so nothing in the file points an author at the value its rows refuse. No finding.

packages/spec/src/conversions/registry.ts. Holds against walk.ts, stored.ts, artifact-forward-conversion.ts, types.ts and the merge pin, as before. The 3ae001ff extension closes the one path the previous record found: a stored or JSON-authored action:bar with a destructive trigger or member no longer reaches toButtonVariant / toRunnerVariant with a word they do not read — the stored-row seam, the artifact door and os migrate meta --write now rename it, and the test drives the stored seam over a bar with its own variant, one of two actions members and one systemActions member, asserting the three notices' paths and copy-on-write identity of the untouched member. Attempts to break it: (a) a bar member list that is not an array → continue, left as stored; (b) a systemActions key on a container → not in that type's memberLists, left as stored (CONTROL); (c) a bar with no properties.variant → only members renamed, the trigger keeps the renderer's ghost; (d) a member whose variant is already danger → untouched, idempotent; (e) mapPageComponents reach is unchanged, so a bar inside a page:card, a tab panel or a slot is visited. The conversion's docblock explains why a row-less block is visited (the bar spreads members onto the block renderers with member.variant || properties.variant), which is the measured renderer code. No finding.

packages/spec/src/migrations/entries/semantic/18.ui-page-action-block-variant-destructive-refused.ts. Holds against the chain step and build-upgrade-guide (no backtick in surface). The entry now states the action:bar gap honestly — no row, nothing refuses, the conversion still renames — and its acceptanceCriteria names the bar's keys, so an upgrading agent can check the bar as well as the rowed blocks. No finding.

No path lets a stored page of the four rowed blocks, or of an action:bar, reach a renderer with destructive; removing the #26776 bridge stays right under Prime Directive 12. Findings on this head: none must-fix, none should-fix. The previous record's must-fix and both should-fixes are taken; the check-runs that verdict depended on are all green here.

Implemented-by: claude/project-thread-4h9cyt
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

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 protocol:ui size/l tests tooling

Projects

None yet

2 participants