Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

[finding] ADR-0010 四态锁的词表在本仓有两份手写 union 副本 + 一处三分支无穷尽兜底的横幅,第五态会静默落空 #5024

Closed

Description

@yinlianghui

发现于 #5004 的实施(PR #5023)。不在那个 PR 内修复 —— 超出该卡完成范围(#5004 的范围是审计面板的 zh 词表 + 键绑定 + 死条目),按 Prime Directive #10 立卡,未认领。

打 finding 标签:今天四个状态齐全、两份副本内容一致,用户现在碰不到。这是休眠的漂移面,不是现行缺陷。定级交 PM triage。

事实(逐条 file:line,基线 origin/main = b46cf11d6)

ADR-0010 §3.6 的四态锁 none / no-overlay / no-delete / full 在本仓被逐字写了两遍,两处都是手写字面 union,彼此无任何门禁比对:

  1. packages/data-objectstack/src/metadata-client.ts:298 —— MetadataLayered:
    lock?: 'none' | 'no-overlay' | 'no-delete' | 'full';
  2. packages/data-objectstack/src/metadata-client.ts:340 —— MetadataAuditEntry:
    lockState: 'none' | 'no-overlay' | 'no-delete' | 'full' | null;

同一文件、相隔 42 行,一个 optional 一个 nullable,除此之外完全相同。没有一处从另一处派生,@objectstack/spec 也没有对应的 z.enum 可派生 —— 所以只给其中一处加第五态,编译器不会有任何意见。

第三处枚举点是消费侧,且没有穷尽检查:packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx:1935-1937 的锁定横幅标题是三条彼此独立的 &&,没有 else / 没有兜底:

{layered?.lock === 'full' && t('engine.edit.lockFull', locale)}
{layered?.lock === 'no-overlay' && t('engine.edit.lockNoOverlay', locale)}
{layered?.lock === 'no-delete' && t('engine.edit.lockNoDelete', locale)}

开关它的是 :1439 的 isLocked = layered?.lock && layered.lock !== 'none' —— 对 none 以外的任何值都为真。

后果(第五态落地那天)

  • 只给两份 union 之一加值:另一处编译照绿,漂移无声开始;
  • ResourceEditPage 的 isLocked 判真 → 琥珀色横幅正常展开(图标 + 边框 + 可选的 lockReason),但三条 && 全假 → 标题那个 div 渲染成空。用户看到一条没有说明文字的锁定横幅;
  • 对照:PR fix(metadata-admin): 审计面板锁状态列补齐中文,词表键绑 lockState union (#5004) #5023 之后,审计面板那侧的 LOCK_STATE_ZH 键绑了 MetadataAuditEntry['lockState'],同样的第五态会让 type-check 变红并点名缺失的标签。所以本仓现在对同一套词表有两种截然不同的严格度 —— 一处编译期报错,一处界面上静默留白。

建议(交维护者定,不预设)

两条路,取舍是归属权问题,不是实现细节:

  • A. 归 @objectstack/spec:spec 出 z.enum,两份 union 改为派生,消费侧的键表照 LAYER_SCOPE_ZH / LOCK_STATE_ZH 绑到 spec,跨仓漂移一并关闭。长期正确,但需要 spec 侧动作与跨仓协调。
  • B. 留在本仓收敛为一份:抽一个导出的 MetadataLockState 类型别名,两处引用它;ResourceEditPage 的三分支改成键绑的查表(或补一条 satisfies 穷尽的 switch),让第五态在编译期就红。成本小得多,但如果 spec 终究该拥有这个词表,这是一次中途站。

无论走哪条,ResourceEditPage 横幅那三条 && 的穷尽性都值得单独补上 —— 它是唯一一处「新状态 = 界面空白」而非「编译期报错」的读点。

相关同族:#4984(date-range 词表的两份未钉副本)、#4972(ComponentInput 的三份结构副本)—— 都是「声明了 N 份、没有门禁比对」的同一失败类。

Activity

  1. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Triage first-touch — concentrated round #2 (triage seat, session session_019ZyCHcjXeTFPiS8RWRk4nN, 2026-08-19): promoted finding → pm:queue · Task, with a seat adjudication of the A/B ownership fork (auto-adjudicated, veto window open — mechanical-boundary test: behaviour-preserving convergence, no accept-set or public-contract change; route A, which WOULD touch the spec contract, is explicitly not chosen here).

    <!-- os-decision-facets -->

    • Platform long-term coherence: one exported MetadataLockState alias with both interfaces deriving from it beats two hand unions; B does not preclude a later spec-owned enum (A) — if spec ever grows one, the alias becomes a one-line re-export, so B is a way-station, not a workaround.
    • Measured business pull: zero — four states complete, copies identical, dormant.
    • AI-agent error-resistance: B turns "fifth state" from a silently-blank banner into a compile-time red (keyed lookup / exhaustive satisfies switch at ResourceEditPage), matching the strictness the audit panel already has after PR fix(metadata-admin): 审计面板锁状态列补齐中文,词表键绑 lockState union (#5004) #5023.
    • Startup scope discipline: B is a small local PR; A is cross-repo contract work for a dormant surface with no pull — exactly what the focus principle defers.

    Ruling: route B — single exported MetadataLockState in data-objectstack, both declarations derive from it, and the ResourceEditPage banner's three && branches become an exhaustive keyed lookup so an unknown state can never render an empty title. Route A (spec-owned z.enum) stays recorded as the future candidate and must go to the maintainer if pursued (contract-surface addition). Confidence gap: whether the wire can already carry values outside the four (older/newer servers) is unmeasured — the exhaustive lookup should therefore also handle the genuinely-unknown value loudly rather than only satisfying the compiler.

    Size/model suggestion: S–M, sonnet.


    Generated by Claude Code

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  3. self-assigned this
    on Aug 22, 2026
  4. os-sales commented on Aug 22, 2026

    @os-sales
    Collaborator

    Claim: PM loop round 13
    Session: session_012u2pRjcqAYtoEjgr3wwhnK
    Branch: claude/issue-5024-metadata-lock-state
    Domain: domain:ui (execution seat)

    Thread read before dispatch. The disposition is already adjudicated (auto-adjudicated, 2026-08-19) and you are implementing, not choosing:

    Ruling: route B — single exported MetadataLockState in data-objectstack, both declarations derive from it, and the ResourceEditPage banner's three && branches become an exhaustive keyed lookup so an unknown state can never render an empty title.

    ⛔ Route A (a spec-owned z.enum) is explicitly NOT chosen and stays recorded as a future candidate that must go to the maintainer if pursued — it is contract-surface work. If your measurement suggests A, stop and report; do not take it.

    Clause ②: cleared by the ruling on its own stated ground — behaviour-preserving convergence, no accept-set or public-contract change. If the implementation turns out to move an accept set, that is a different card: stop and report.

    The ruling's own confidence gap is your sharpest obligation. Triage recorded that whether the wire can already carry values outside the four (older/newer servers) is unmeasured, and concluded: the exhaustive lookup should handle a genuinely-unknown value LOUDLY rather than only satisfying the compiler. A satisfies-style exhaustiveness check that is compile-time-only leaves the runtime blank banner exactly as it is today — which is the defect. Measure whether an out-of-vocabulary value can reach the banner, and say what a user sees when it does.

    Give the pin a control that can fail. An exhaustiveness assertion is satisfied by the current four whether or not the runtime path is fixed, so a green type-check proves nothing on its own. Add a leg that feeds a fifth value through the real render path and measure it.

    ⛔ OUT: packages/types/** + packages/core/src/types/** (#4972 this round), packages/components/** + packages/react-runtime/** + content/docs/** (#5469), packages/plugin-calendar/** (#5045).


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

auto-adjudicateddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions