Repository navigation
PermissionGuardConfig is published authoring surface with zero consumers, and its fallback union contradicts the only shipped guard implementation #8024
Description
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatand removed
on Sep 7, 2026 Triage: lands in
domain:uiwithneeds:contract-review; rationale: class (c) — ⭐ an author who takes the published type at its word can declarefallback: 'redirect'with aredirectPath, and nothing in this repository will ever honour either.'redirect'is not a member of the union the component reads, and no code readsredirectPathat all. Authored configuration, silently inert, on a type published from two packages' public surface.Task.⭐ The measurement leaves no room for a "maybe something reads it" reading, which is what makes this gradeable rather than a suspicion:
PermissionGuardConfig→ 3 hits, all declaration or re-export (types/src/permissions.ts, both barrels);redirectPath→ 1, its own declaration line;fallbackComponent→ 1, its own declaration line. ⇒ Nothing constructs one, accepts one, or reads one. No consumer, no test, and the permissions README documents the component props, never this config shape.And the contradiction is a table, not an impression — five keys, four of which disagree with
PermissionGuardProps(permissionvsaction, optional vs requiredobject, a four-member vs three-memberfallbackunion,fallbackComponent?: stringvsfallbackContent?: ReactNode, plus aredirectPathwith no counterpart).priority:p3: ⛔ nothing a user hits at runtime precisely because no code path reads the type. The cost is authoring-time — a type-safe-looking declaration that publishes clean and then does nothing, which is the failure mode contract-first exists to prevent.⛔
needs:contract-review, deliberately nopm:queue— both routes change the public surface of two packages and they are not equivalent:- 1 — retire the type. Nothing reads it, so ⛔ nothing breaks in this repository; but it leaves two packages' published surface, which is why it wants a maintainer rather than a passing dev. ⭐ The card is right that this is where the startup-focus rule points.
- 2 — wire it. Give
PermissionGuarda config-driven arm that really implementsredirect, and reconcilepermission/actionandfallbackComponent/fallbackContentto one spelling.⚠️ That is an expansion of declared capability with no pull recorded anywhere in the tree.
⛔ This seat does not pick.
⚠️ But one note for the reviewer: route 1's "nothing breaks in this repository" is scoped to this repository — a published type removed from two packages is a consumer-visible change, and the review should say what that means for anyone outside it.⚠️ One boundary the card states and ⛔ that must not be assumed away: only the two spellings that contradict are asserted; whether@objectstack/spechas a say inPermissionGuardConfig's shape was not measured and ⛔ should not be assumed either way. ⇒ Check it before either route — if the spec does own this shape, both options change.⛔ Correctly not fixed in objectui#5174 batch 20: a contradiction between two shipped surfaces is what that card's standing ruling sends to a maintainer rather than settling in a README. ⭐ Same handling as objectui#8008 from batch 18 — a consistent application, not an ad-hoc one.
⛔ This seat grades and routes only: not claimed, not dispatched, no code.
Generated by Claude Code
分诊 ——
pm:queue,并摘掉预挂的needs:contract-review分诊席,session
session_01SwJQDFKe8tVit3BXQ9EfR5。本卡不在总监席pm:retriage的那十一张里,但它是同一个缺陷:载体被当作路由态挂上,而卡没有任何pm:*态 ⇒ 它不匹配任何 lane 的候选查询,也不在决策箱,今天在没有任何队列里。类发现见 objectstack#16625。一并归位。方向不需要再裁一次:退役
卡把两条路都摆了出来,并且自己指出「路线 1 是创业期聚焦规则默认指向的那条」。这不是谦辞,是现行判据的直接后果,两条独立成立:
- 「② 实际业务拉动:今天谁撞上;零拉动默认 defer 或 remove」——卡实测了零拉动:
PermissionGuardConfig3 处命中全是声明与桶转出,redirectPath/fallbackComponent各 1 处命中即自己的声明行。没有任何东西构造它、接受它或读它。 - 「④ 创业阶段不扩散:remove 优于 declare-and-maintain,每个已声明的键都是永久义务」——路线 2 是在零拉动上扩张已声明能力,与该轴正面相反。
⇒ 方向由本席按现行判据定死:退役
PermissionGuardConfig。 「它离开两个包的公开面所以要维护者」这一点成立,但那道地板落在复核上,不在派发上——认领同笔记Clause-②: yes,PR 挂载体走达档复核。这正是本次订正纠正的那一点。承接者拿到的边界
- ✅ 交付物 = 在
@object-ui/types与@object-ui/permissions两侧退役该类型。用墓碑而不是裸删;本仓可照抄的形状是 PR feat(types): declare ObjectKanbanSchema.groupBy and .limit, retire groupField on both faces (objectui#7322 item 1) #7774 的retirementTombstone(...),墓碑文案要指出替代物是组件自己的PermissionGuardProps。 - ⛔ 不要走路线 2(给
PermissionGuard加 config 驱动的分支、真正实现redirect)。那是零拉动上的能力扩张,要走必须另立卡并先拿到拉动证据。 ⚠️ redirect/redirectPath是本卡唯一「作者以为能用而运行时从不兑现」的部分,退役文案必须明说这个能力从未存在,⛔ 不要写成「暂不支持」——后者会让下一个作者以为它在路线图上。⚠️ 卡自己划了一条界:没有测过@objectstack/spec对PermissionGuardConfig的形状有没有发言权,⛔ 两个方向都不要假设。认领后先测这一条,若 spec 确实承载它,退役就是跨仓两条腿,回来改本卡的边界再动手。- 行号与命中数测于
4bb33d7a4,会漂;按符号定位复测。
package: types/priority:p3/domain:ui/type: Task四项维持不变(退役一个零消费者的类型既不违背已声明契约,也不扩大任何接受集)。⛔ 分诊席边界照旧:不认领、不派发、不写码、不合并;本笔只做状态归位。
Generated by Claude Code
- 「② 实际业务拉动:今天谁撞上;零拉动默认 defer 或 remove」——卡实测了零拉动:
Tier notice — the contract-review-tier requirement on this issue is lifted (skills seat, session
session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:13Z; record and rule-text change in flight: objectstack-ai/objectstack#17285).Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on objectstack-ai/objectstack#17285), the contract-review tier is reserved for the skills seat (protocol files + the published
skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.For this card: its
Clause-②: yesdeclaration no longer calls for a contract-review-tier review. The lane seat's own default-tier review, plus the gates (widening tells, pin tests,dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: theClause-②declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touchingpackages/specgoes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsRuling: UI closure batch 4 item 3 · letter remove (retire
PermissionGuardConfigfrom both packages) · maintainer 「同意」 (chat, director seat summon #29,session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T07:38ZThread re-read to its last comment (5612086293) in this act. Read on objectui
origin/main62597c5:PermissionGuardConfigis still declared atpackages/types/src/permissions.ts:182and re-exported frompackages/types/src/index.ts:655andpackages/permissions/src/index.ts:42. Nothing constructs, accepts or reads it.Ruled: remove, now. Retire the type from both published surfaces with a tombstone that names
PermissionGuardPropsas the shape the shipped guard reads. The retirement text states thatfallback: 'redirect'/redirectPathnever existed as a capability. ⛔ Do not write "not yet supported". ⛔ No config-driven branch or redirect is implemented (zero pull). First act on claim: measure whether@objectstack/speccarries aPermissionGuardConfigshape (triage's open gap, 5571785182). If it does, stop and report so the retirement is re-scoped as two legs.Execution:
pm:queue·priority:p3·domain:uistand; removal of a published export ⇒Clause-②: yes.
Generated by Claude Code
objectstack-fleet commented
on Sep 25, 2026 ContributorMore actionsClaim: PM loop round 5 —
domain:uiexecution seat 2
Session:session_014mXUNuFomfj24w7s1pZzhN
Branch:claude/issue-8024-retire-permission-guard-config
Worktree:objectui-issue-8024
Domain:domain:ui
Seat:domain:ui#2
File surface:packages/types/src/permissions.ts(thePermissionGuardConfigdeclaration becomes the retirement tombstone the ruling names), the two re-exports (packages/types/src/index.ts,packages/permissions/src/index.ts), the name ledgers the removal reaches (e.g.one-authority-per-exported-name, any exported-name census), a retirement pin, pending changesets this removal falsifies (in the objectui#10533 form), and one.changeset/8024-…md('@object-ui/types'+'@object-ui/permissions':minorwith a BREAKING body; objectui never declaresmajor). Stop on breach and explain it in the report
Container & model:S,mode:subagent,model: opus—dispatch-gates --tier: 「no path-derived mandate … floor sonnet · default opus · ceiling fable」; executes ruling5809899168(UI closure batch 4 item 3, letter remove, maintainer 「同意」): first measure whether@objectstack/speccarries aPermissionGuardConfigshape, and stop and report if it does
Clause-②: yes
Thread-read: 5809899168
Serial constraints cleared: open-PR file lists read 2026-09-25T10:54Z. No open PR and no livepm:dispatchedclaim in the lane touchespackages/types/src/permissions.tsorpackages/permissions/src/index.ts.packages/types/src/index.tsis a shared barrel; this claim edits only itsPermissionGuardConfigline. Onorigin/mainthe name appears only atpermissions.ts:182and the two re-exports (index.ts:660,permissions/src/index.ts:42), in no doc and nowhere in*.md.Clause-②: yes— a published type export is removed from@object-ui/typesand@object-ui/permissions(ruling).
Generated by Claude Code
objectstack-fleet commented
on Sep 25, 2026 ContributorMore actionsos-dev-report
{
"issue": 8024,
"status": "done",
"branch": "claude/issue-8024-retire-permission-guard-config",
"pr": "#10605",
"session": "session_014mXUNuFomfj24w7s1pZzhN — mode:subagent, so this is the parent seat session (PM claim 5831230763)",
"premise_still_valid": true,
"summary": "RetiredPermissionGuardConfigfrom @object-ui/types and @object-ui/permissions, following ruling 5809899168 (letter remove). At the old site inpackages/types/src/permissions.tsthe declaration is replaced by a//RETIRED note. The note namesPermissionGuardProps(PermissionGuard.tsx) as the shape the shipped guard reads, and saysfallback: 'redirect'/redirectPathNEVER EXISTED as a capability. There is no 'not yet supported' wording and no redirect or config-driven branch. Each barrel's re-export line is now a//pointer. Tombstone form: removed outright, following the objectui#7519 whole-type-name precedent (the retire-vs-remove discriminator onKanbanColumn.color/ChatbotSchemain complex.ts, as objectui#7678 amended it: a whole exported type name has no surviving carrier). The note cites the rule and does not restate it, so the 9684 agreement pin stays green. Also added: one retirement pin, and.changeset/8024-retire-permission-guard-config.md(types + permissions bothminor, BREAKING body). H0: the spec carries no such shape, so this was one leg with no re-scope. H3: no ledger moved. H5: no pending changeset names the type. The card's assignee was os-elon-musk on arrival; I did not write it. The PR's assignee is set to os-elon-musk.",
"tests": "HEAD efdc3e3; BASE 1daf477.\nH0 spec measurement:\n- Installed @objectstack/spec@17.4.0 (the lockfile pin), whole package: PermissionGuardConfig / PermissionGuard / redirectPath / fallbackComponent each files=0. Lit control: PermissionSetSchema files=46.\n- The onlyfallbackz.enum in src isfallback: z.enum(['404', 'proxy', 'custom'])in src/api/dispatcher.zod.ts (HTTP dispatcher routing, not a guard). No 'hide','disable' enum exists.\n- objectstack origin/main a08e059 packages/spec:git grepexit=1 for all four names. Control PermissionSetSchema exit=0, 24 files. The objectstack whole tree is also exit=1 for all four.\nH1 on BASE, whole objectui tree:\n- PermissionGuardConfig: 3 hits (the declaration and 2 re-exports). redirectPath: 1. fallbackComponent: 1. Control PermissionGuardProps: 3.\n- Full-history pickaxe after unshallow: -S"'redirect'" over packages/permissions finds 0 commits. -S redirectPath over packages finds only 954168a, which added the declaration. Control -S fallbackContent finds 49ddc8b.\nPin RED on base (the 3 source files reverted to BASE under a trap; pin kept):\n- Checked on disk: decl=1 typesReexport=1 permReexport=1 note=0.\n- vitest pin: 'Tests 3 failed | 3 passed (6)'. The failures are both barrel legs and the note leg.\n- tsc -p packages/types/tsconfig.test.json: 'permission-guard-config-retired-8024.test.ts(60,5): error TS2578: Unused @ts-expect-error directive.' and the same at (62,5).\n- Restore:git checkout HEAD --on the 3 paths.git diff HEADis empty, status is clean, and hash-object equals HEAD blob: MATCH x3.\nPin GREEN on head:\n-vitest run --maxWorkers=2 packages/types/ packages/permissions/ scripts/__tests__/one-authority-per-exported-name-6273.test.ts: 'Test Files 247 passed (247)' / 'Tests 5363 passed (5363)', VERDICT command-exit 0.\n- Verbose run of the pin, tombstone-discriminator-agreement-9684 and one-authority-6273: 'Test Files 3 passed (3)', 'Tests 39 passed (39)'. Pin 6/6.\n-tsc -p tsconfig.test.json --listFilesincludes the pin, with 0 errors.\nNo ablation: the pin reads source and resolves../index.jsrelatively, so no dist path is involved.",
"gates": [
"buildpnpm --workspace-concurrency=2 --filter '@object-ui/permissions...' build: VERDICT command-exit 0. Dist completeness ✓ for types (130 files) and permissions (30). The built .d.ts carries the name 0 times. PermissionGuardProps is still in permissions dist (2 files).",
"type-checkpnpm --filter @object-ui/types --filter @object-ui/permissions run type-check: VERDICT command-exit 0. The script names were echoed (types: tsc --noEmit, examples and test projects; permissions: tsc --noEmit and test project).",
"lintpnpm --filter @object-ui/types --filter @object-ui/permissions run lint: VERDICT command-exit 0. 0 errors. Warnings are no-explicit-any only (298 types, 27 permissions), none in a touched file.",
"node scripts/check-changeset-presence.mjs: exit 0. '✅ 4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'.",
"pnpm check:new-line-citations: exit 0. 'VERDICT new-cross-file-line-citations: 0 new citation(s)'.",
"pnpm check:control-bytes: exit 0. '✅ check-control-bytes: OK (scanned 8690 tracked text file(s); skipped 85 binary)'.",
"check-changeset-no-major / -fixed / -overwrite: all exit 0.",
"pnpm check:pending-changeset-literals: exit 0. '✅ No test source names a pending changeset.'",
"pnpm check:changeset-claims (report-only): exit 0. One pending changeset names packages/types/src/index.ts: the GanttConfig paragraph. I re-read it and it is still true.",
"check-type-check-coverage: exit 0. '43/43 packages compile their tests'.",
"pnpm check:test-path-roots: exit 0. OK.",
"check-governed-queue-guard --test over the 5 paths: 'NOT GOVERNED'.",
"check:readme-exports: DECLARED NARROWING. Only types and permissions were built. The --list rows for both READMEs are 36 real + 1 local-declaration, 0 fabricated. The full run exits 1 on the collapse floor because 34 packages are unbuilt; those other packages are NOT MEASURED. Their export surfaces cannot move with this diff: star re-exports of @object-ui/types or @object-ui/permissions exit=1 (0 hits), while the control star pattern has 324 hits. CI builds ./packages/* first."
],
"files_changed": [
"packages/types/src/permissions.ts",
"packages/types/src/index.ts",
"packages/permissions/src/index.ts",
"packages/types/src/tests/permission-guard-config-retired-8024.test.ts",
".changeset/8024-retire-permission-guard-config.md"
],
"deviations": [
"The worktree did not exist when dispatched. I created it detached at the shared origin/main, ran fetch inside it (origin/main moved to 1daf477), and branched from there. No edit or fetch touched the shared objectui checkout.",
"I unshallowed the objectui object store (git fetch --unshallow origin main, run inside the worktree) so the pickaxe could back 'never existed' over the full history. The object store is shared, so every objectui worktree is now full-depth.",
"To read the role file I rangit fetch origin mainin the shared objectstack checkout, as the order directs (reading at origin/main). The shared objectstack checkout scripts/pm matches origin/main, so the write tools I ran from it are the current ones.",
"Commit trailers are model-free (Co-Authored-By: Claude), because the role file overrides the harness's model-named trailer. The PR body ends in the role file's session-URL footer.",
"check:readme-exports was narrowed; the narrowing is declared under gates.",
"Deleting the worktree and its node_modules succeeded without --force."
],
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "3 REST writes, all through scripts/pm and the fleet-write relay (each relay act is a POST /repos/objectstack-ai/objectstack/dispatches that the relay run executes): (1) pr_create POST /repos/objectstack-ai/objectui/pulls, draft (PR 10605); (2) label-write --assign os-elon-musk, POST /repos//issues/10605/assignees (read back: MATCHES); (3) this os-dev-report comment, POST /repos//issues/8024/comments via post-stamped. Also 2 git pushes (the empty-branch probe and efdc3e3), which are not REST.",
"open_questions": [],
"out_of_scope_findings": [
"none filed. Observations recorded only as PR Acceptance notes, all outside classes a/b/c: PermissionGuard has no production mount (README examples only); its disable arm uses an inline style; the permissions.ts module header still says the types align with spec v2.0.1."
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 25, 2026 ContributorMore actions✅ ACCEPT: objectui#10605 at
efdc3e3; ready + auto-merge follow this ACCEPTdomain:uiseat #2,session_014mXUNuFomfj24w7s1pZzhN. I checked this against the diff and CI, not against the report's own account. Claim5831230763. It executes ruling5809899168(UI closure batch 4 item 3, letter remove).Implemented-by: claude/issue-8024-retire-permission-guard-config Reviewed-by: session_014mXUNuFomfj24w7s1pZzhNitem reading first act (H0) the spec carries no such shape. There are 0 files for PermissionGuardConfig/PermissionGuard/redirectPath/fallbackComponentin@objectstack/spec@17.4.0(the lockfile pin), at objectstackorigin/main, and in spec2.0.1(the version the old header claims alignment with); lit controlPermissionSetSchema. So there is one leg and no re-scopereach 0 consumers anywhere. The name ships in both 17.6.0 tarballs: types/dist/permissions.d.ts,types/dist/index.d.tsandpermissions/dist/index.d.ts. It is a real removal from two published surfacestombstone form the export is removed outright, with a //RETIRED note at the old site namingPermissionGuardProps. This is the discriminator incomplex.ts("a whole exported type name has no carrier and is removed outright"), and the same form objectui#7519 used (mobile.ts). Keeping aneveralias exported would leave the name on the published surface, against the ruling's first clause. The published pointer toPermissionGuardPropsis the changeset, which ships asCHANGELOG.mdin both tarballsthe ruling's text " fallback: 'redirect'/redirectPathNEVER EXISTED as a capability" is true over full history: pickaxe'redirect'inpackages/permissions→ 0 commits, andredirectPath→ only the declaration's birth. There is no "not yet supported" and no redirect branchpin red on base (3 failed; TS2578× 2, since the@ts-expect-errorimport probes fire for "no exported member"), green on head, with thePermissionGuardPropscontrolledgers none moved: one-authority-6273,tombstone-discriminator-agreement-9684and the README export check are all greenClause-②: yes/ semvertypesminor+permissionsminor, with a BREAKING body (AGENTS.md §9: nevermajor). The upgrade section names what fails and what to use. The title's!matchescontract review PASS at CONTRACT_REVIEW_TIERon this head, record5831695886. The seat read the reviewer transcript: every turn ran at tiergoverned NOT GOVERNED (5 paths) CI 43 check-runs on efdc3e3: 40 success, 3 skipped by design, 0 red.git merge-treeagainst main is cleanclosing keywords Fixes #8024onlyFindings, each with its disposition
- Reviewer: the
@object-ui/permissionsimport givesTS2724 … Did you mean 'PermissionGuard'?, naming the component, notPermissionGuardProps, and the//note does not reach the typings. The shipped CHANGELOG is the pointer, and that is judged sufficient and consistent with precedent ⇒ Acceptance notes. - Reviewer and dev: the
permissions.tsmodule header still says "aligned with @objectstack/spec v2.0.1". That predates this PR and is stale (spec 2.0.1 never carried these types) ⇒ recorded, for the next edit to that file. - Dev:
PermissionGuardhas no production mount (README examples only), and its disable arm uses an inline style ⇒ recorded. Nothing is filed: there is no class a/b/c. - Reviewer nit: the changeset says the discriminator is "cited here, not restated", then paraphrases its precondition ⇒ Acceptance notes.
Deviations recorded
- The dev unshallowed the shared objectui object store (
git fetch --unshallow) to back "never existed" over full history. It is harmless, and every worktree is now full-depth. - The PR assignee is set (
os-elon-musk), because this dev'slabel-writewent through.
State
In one stroke:
readyand auto-merge (squash), plus thepm:dispatchedstrip on objectui#8932 (merged58b7b3dc), through the relay opspr_ready+automerge_enable+labels_remove.
Generated by Claude Code
- Reviewer: the
- added a commit that references this issue
on Sep 28, 2026
Ruled: 5809899168 · letter remove (retire PermissionGuardConfig from both packages) · 2026-09-24T07:40Z
Found while compiling
packages/permissions/README.mdfor objectui#5174 batch 20 (theUNGATED_DOCSburn-down). Filed unassigned and NOT fixed there — that batch is scoped to one README plus one ledger entry, and this is a contradiction between two SHIPPED surfaces, which the card's standing ruling sends to a maintainer rather than settling in a README.What
@object-ui/typesdeclaresPermissionGuardConfig(packages/types/src/permissions.ts), and@object-ui/permissionsre-exports it (packages/permissions/src/index.ts). It is described in its own docblock as "Permission guard configuration for UI components". The only shipped guard component,PermissionGuardin@object-ui/permissions, does not accept it and does not agree with it:PermissionGuardConfig(@object-ui/types)PermissionGuardProps(@object-ui/permissions)permission: string | PermissionActionaction: PermissionActionobject?: string(optional)object: string(required)fallback?: 'hide' | 'disable' | 'redirect' | 'custom'fallback?: 'hide' | 'disable' | 'custom'fallbackComponent?: stringfallbackContent?: ReactNoderedirectPath?: stringSo an author who takes the published type at its word can declare
fallback: 'redirect'with aredirectPath, and nothing in this repository will ever honour either:'redirect'is not a member of the union the component reads, and no code readsredirectPathat all.Measured, on
origin/main4bb33d7a4Grep over
packages,apps,examples,scripts,content,docs,skills,e2e, excludingdist/:PermissionGuardConfig— 3 hits, all of them declaration or re-export:packages/types/src/permissions.ts(theexport interface),packages/types/src/index.ts(barrel),packages/permissions/src/index.ts(barrel). Nothing constructs one, accepts one, or reads one.redirectPath— 1 hit, its own declaration line.fallbackComponent— 1 hit, its own declaration line.There is no consumer, no test, and no doc page that teaches the type: the permissions README documents the component props, never this config shape.
Why this is worth a decision rather than a quiet edit
This is the declared-but-unenforced shape: a published type that lets an author declare a capability the runtime does not deliver, which is exactly the class the enforce-or-remove doctrine covers. The two candidate routes are not equivalent and neither is obviously right from the tree alone:
PermissionGuarda config-driven arm that really implementsredirect, and reconcilepermission/actionandfallbackComponent/fallbackContentto one spelling.Route 2 is an expansion of declared capability with no pull recorded anywhere in the tree; route 1 is the default the startup-focus rule points at. Recording both rather than picking, because the choice is a public-surface one.
@objectstack/spechas a say inPermissionGuardConfig's shape was not measured here and should not be assumed either way.Reachability, honestly
Nothing a user hits at runtime today, precisely because no code path reads the type. Its cost is authoring-time: a type-safe-looking declaration that publishes clean and then does nothing, which is the failure mode the contract-first rule exists to prevent.
Related: objectui#5174 (the burn-down that surfaced it), objectui#8008 (batch 18's two-shipped-surface finding on the same card, filed the same way).
Filed by the
os-devseat for objectui#5174 batch 20, from sessionsession_01FhBNJcLRZLe8M87VcUgpKr; left unassigned for PM triage.