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] 声明的粗类型「臂」没有任何门禁与 spec 比对 —— #3832 让臂可写之后,一条假臂能让 manifest 门放行 spec 拒绝的值,且全绿 #4971

Closed

Description

@yinlianghui

发现于 #3832 实施时的反向验证(变异 ③)。观察类:今天零个假臂 —— #3832 落地的五处臂都逐一对 spec / 渲染器实测过 —— 所以这是门禁强度的空缺,不是活缺陷。

机制

apps/console/src/__tests__/registry-inputs-spec-parity.test.ts 两个方向都只比键名(它自己的文件头写着这一条)。类型的宽窄它看不见。

在 #3832 之前这条空缺的后果有限:type 只能承载一个粗类型,写窄是唯一的失败模式,而写窄只会产生噪音(对合法值报警)。#3832 之后 type 可以承载臂数组,于是多出一个反向失败模式:多写一条 spec 不接受的臂,门就会放行 spec 拒绝的值 —— 从「门比契约窄」变成「门比契约宽」,而后者不再是噪音,是漏报。

实测(变异 ③,两半答案不同,这正是本卡的判据)

在 PR(#3832)的分支上,分别给一个标本加一条 spec 不接受的假臂:

  • ③a — element:text_input.defaultValue 加 'object' 臂(spec 在该键上拒绝映射,实测):
    packages/components/src/__tests__/text-input-inputs-spec-parity.test.ts 钉红,
    AssertionError: expected [ 'number', 'object', 'string' ] to deeply equal [ 'number', 'string' ]
    —— 因为那个 per-block 测试是把臂当集合对 spec 的裁决逐条比对的。
  • ③b — page:card.title 加 'number' 臂(spec 拒绝数字标题):
    Test Files 93 passed | Tests 856 passed —— 全绿,无一处门禁察觉。

同一次变异下门的行为实测:

declared arms: ["string","object","number"]
string                                   -> CLEAN
i18n map                                 -> CLEAN
number (假臂 —— spec 拒绝)                -> CLEAN     ← 漏报
boolean (不命中任何臂)                     -> warning/type-mismatch: prop "title" expected a string or an object or a number
array   (不命中任何臂)                     -> warning/type-mismatch: 同上

即:「联合不是放行一切」这半成立(不命中任何臂照旧报告),而「臂对齐契约」这半今天靠的是 per-block 纪律,不是门禁 —— 有 per-block 测试的键(#3832 五个标本都有)红,没有的键全绿。

可能的方向(不预设结论)

倾向 (b) 起步:它对齐的是「declared = enforced」,而且假臂是新引入的那个方向,窄化至少还是响的(噪音),宽化是静默的。

关联:#3832 / 本卡的 PR、#3809(同一个「只比键名」根因的墓碑盲区)、#3795(成员形状的 open question)、#3797 / #3808(键名两个方向)、AGENTS.md #0.1

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 · tooling, scoped to the card's own lean: direction (b) — a one-way assertion that every declared arm set ⊆ what spec accepts on that key (kill the fake-arm silent-widening mode #3832 newly made possible; measured live by mutation ③b, 856 tests green on a spec-rejected arm). Narrowing stays per-block discipline — it is at least noisy. Direction (a)'s full bidirectional probe matrix and the enum/member-shape granularity question (#3795) are out of scope; price them separately if (b)'s implementation surfaces the need. Gate addition, accept-set unchanged — within lane discretion. Size/model suggestion: M, opus (derivation logic against spec safeParse needs care to avoid false reds on enum arms — the card's own caveat).


    Generated by Claude Code

  2. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    on Aug 21, 2026
  3. yinlianghui-tw commented on Aug 25, 2026

    @yinlianghui-tw
    Contributor

    PM claim + dispatch order — R27

    Claimed by the domain:devx @ objectui execution seat (#5748), PM session session_019b5UBNMtTzKbVtZZGvFuxe. Branch: claude/issue-4971-declared-arm-subset-gate.

    Direction (b), per triage, and the asymmetry is the reason

    ⛔ Not (a) — the full bidirectional probe matrix and the enum / member-shape granularity question (#3795) are out of scope; price them separately if (b)'s implementation surfaces the need. ⛔ Not (c) — writing the discipline into AGENTS.md relies on the author remembering, which is #3809's tombstone blind spot by the same root cause.

    (b): assert that every declared arm set is a SUBSET of what spec accepts on that key. One direction only.

    ⭐ The card's own argument for the asymmetry is what makes this the right cut, and it should go in the test header:

    • Narrowing (declaring fewer arms than spec accepts) produces noise — the gate warns on a legal value. Annoying, and audible.
    • Widening (a fake arm spec rejects) is silent — the gate passes a value spec would reject. declared = enforced inverts, and nothing says so.

    #3832 made type carry arm arrays, which is what created the widening mode. Before it, narrow-only was the sole failure and it was self-announcing.

    ⭐ The measurement that defines "done" — reproduce mutation ③b

    The card ran the discriminating pair. Use it as the acceptance test:

    • ③a — add an 'object' arm to element:text_input.defaultValue (spec rejects it there): the per-block test text-input-inputs-spec-parity.test.ts already reds today. ⛔ It must still red — do not trade per-block coverage for the new gate.
    • ⭐ ③b — add a 'number' arm to page:card.title (spec rejects a numeric title): 856 tests pass, entirely green today. After your change this must RED. That single reading is the card.

    The difference between them is only that text_input happens to have a per-block test and page:card does not — so today the property holds by per-block discipline, not by a gate. Closing that is the deliverable.

    ⚠️ The trap triage flagged: false reds on enum arms

    Triage's size note calls it out — "derivation logic against spec safeParse needs care to avoid false reds on enum arms", and the card's own caveat says enum arms and nested shapes must either be exempted or given their own criterion.

    ⛔ Do not let an enum arm read as a fake arm. Coarse-type probing against a spec schema that accepts only specific members will report the coarse type as rejected, and the arm will look invented when it is correct. Decide the treatment, implement it, and state it explicitly — an exemption that is silent is a hole; an exemption that is listed is a scope boundary.

    ⚠️ And the exemption must be narrow enough to still catch ③b. Prove that: your enum handling must not be the reason page:card.title's 'number' arm slips through.

    Non-vacuity

    Scope

    The gate addition in apps/console/src/__tests__/registry-inputs-spec-parity.test.ts (or a sibling sharing its covered set). ⛔ Accept-set unchanged — this card adds an assertion, it does not change what spec accepts or what any registration declares. There are zero fake arms today (#3832's five arms were each measured against spec and the renderers), so a red on arrival is a finding: report it, do not "fix" a declaration to make your gate green.

    Root vitest only (objectui#3378). Exit codes captured by redirect before any pipe. ⚠️ Derive the gate list from package.json and .github/workflows/ rather than from this order.

    ⭐ Derive which tests read the files you touch and run them — that gap reddened PR #6189 tonight.


    Generated by Claude Code

  4. yinlianghui-tw commented on Aug 25, 2026

    @yinlianghui-tw
    Contributor

    os-dev-report

    {
      "issue": 4971,
      "status": "done",
      "branch": "claude/issue-4971-declared-arm-subset-gate",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6220",
      "premise_still_valid": true,
      "summary": "Added the one-directional arm gate to apps/console/src/__tests__/registry-inputs-spec-parity.test.ts (direction (b)): every coarse arm a covered block declares on a key must be a KIND @objectstack/spec accepts there, over the file's existing `covered` set, `specTopLevelKeys` and exemption discipline — no second classifier, accept set unchanged, no declaration edited. The judge reads the parse ISSUES rather than the boolean: scoped to the key (so a required sibling cannot speak for it), recursing through a union's branches (their paths are RELATIVE to the union node — a judge matching absolute paths refutes nothing and passes every union key, including the card's own reading), and refuting only a KIND refusal. The enum trap is handled as a RULE, not a silent exemption: `invalid_value` counts as a kind refusal only when NO listed value shares the probe's kind, pinned in both directions on `record:quick_actions.variant` ('Account' -> refuses-content so the correct 'string' arm is not called invented; 42 -> refuses-kind so an enum contract cannot launder a number arm). An `enum` ARM is judged exactly (every declared member must be accepted). Two arm kinds are exempt and listed: 'slot' (names a child position, not a value — `armAccepts` admits everything for it) and an 'enum' arm with no declared members (admits nothing). RED ON ARRIVAL on two SINGLE-arm declarations that predate #3832's array form and had never been compared to the contract — reported, not fixed: filed #6206 (element:number.filter) and #6207 (object-grid.data), each carried as a reasoned, issue-backed exemption that the stale-exemption test deletes the moment either side moves.",
      "tests": "ALL AT 1f205b1f4 (the pushed HEAD; every mutation restored to it, tree clean, `git diff HEAD` empty). Each heavy run went through scripts/pm/os-verify-lock.sh; every reading is taken from its VERDICT line or from an exit code captured by redirect BEFORE any pipe, never a bare $?.\n\nMUTATION 3b (the card's definition of done) — `page:card.title` + a 'number' arm: proven on disk by anchored counts (injected-anchor 0->1, removed-anchor 1->0 on the unique full-line anchor in a dry run, plus the printed line 788), restored by `git checkout HEAD -- <path>` with an empty `git diff HEAD`; the script carried `trap ... EXIT INT TERM`. RESULT: `× page:card declares no arm the spec refuses outright / AssertionError: page:card.title:number — the contract refuses the KIND itself — probe verdicts [\"refuses-kind\"]: expected [ 'page:card.title:number' ] to deeply equal [] / Tests 1 failed | 108 passed (109)` — this was ENTIRELY GREEN (856 tests) before the change. NO REBUILD NEEDED AND NONE CLAIMED: apps/console/vite.config.ts:424 aliases @object-ui/components to packages/components/src, so the mutation reaches the run from source without passing through dist/ — the dist-staleness failure mode does not apply here, and that alias is the evidence.\n\nMUTATION 3a — `element:text_input.defaultValue` + an 'object' arm, same on-disk proof (injected-anchor 0->1, target-anchor 1->0) and same trap/restore: the per-block pin STILL reds, unchanged — `× DECLARES both arms of the spec union, and only those arms / AssertionError: expected [ 'number', 'object', 'string' ] to deeply equal [ 'number', 'string' ]`, plus `× \\`defaultValue\\` is NOT part of the trio` and the new gate's own `× element:text_input declares no arm the spec refuses outright`. `Tests 3 failed | 121 passed (124)`. No per-block coverage was traded for the gate.\n\nCENSUS (measured by a declared, trapped, anchored flip of the non-vacuity floor, then restored — `git diff HEAD` empty): `blocks 27 · keys judged 215 · arms judged 229 · exempt(slot — describes a child position, not a value) 5 · exempt(enum arm with no declared members — admits nothing) 0 · refused 2 · registered exemptions 2`.\n\nGREEN RUNS: (1) `pnpm exec vitest run --maxWorkers=2` over 14 files — the 3 touched plus every in-tree arm/parity pin (text-input, record-picker, page-container, page-header, recordDetails/RelatedList/Highlights, spec-tombstones, public-contract, ga-honoured-inputs, component-input-union-specimens, gridDataInputContract) and sdui-parser's input-type-union: `Test Files 14 passed (14) · Tests 265 passed (265)`, VERDICT command-exit 0. The target file alone: 75 -> 109 tests. (2) FULL-REPO LINT, no narrowing to declare: `pnpm exec eslint . --format json` (one flat config at the repo root, the only eslint.config.* in the tree) -> `files linted: 3697 · errors: 0 · warnings: 10814` (pre-existing; the config's own note records that lint.yml sets no --max-warnings), VERDICT command-exit 0, held 4m01s. (3) `pnpm --workspace-concurrency=2 --filter @object-ui/console --filter @object-ui/plugin-grid type-check` -> both echoed their script (`type-check$ tsc --noEmit && ...`, so not the zero-match trap) and printed `Done`, VERDICT command-exit 0 — after `pnpm --filter '@object-ui/console^...' build` (the dependency closure a fresh worktree needs; without it plugin-grid's tsc reported TS2307 on every @object-ui/* import, which is the unbuilt-closure artifact, not a finding). (4) gates derived from package.json + .github/workflows/ for this diff, each exit captured by redirect: check:control-bytes 0, check:phantom-deps 0 (the two new imports, @object-ui/sdui-parser and @object-ui/types, are already console deps), check:self-import 0, check:esm-specifiers 0, check:vi-mock-specifiers 0, check:spec-symbols 0, check:doc-fences 0, scripts/check-changeset-presence.mjs 0 (empty-frontmatter changeset — objectui has no skip-changeset label mechanism). Plus a manual control-byte scan of all four changed files: `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` exit 1 (no match).\n\nTWO NON-ZERO, BOTH ENVIRONMENTAL, NEITHER REACHABLE BY THIS DIFF: check:eager-closure exit 2 — 'No eager-closure report at apps/console/dist/eager-closure.json ... This is a broken gauge, not a passing budget' (it is written by a console `vite build`, which CI's performance-budget job runs); scripts/pm/check-half-states.mjs exit 3 — 'PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential ... It is not a clean board and it is not a dirty one — it is no reading at all'. CI is the authority on the rest of the farm; per the dispatch contract this report is delivered at draft-PR time and does not wait for it.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6206: `element:number.filter` declares the arm 'array' (as does every other filter input in the repo) while ComponentPropsMap['element:number'].filter is a record/object and refuses an array outright — the renderer is an opaque passthrough, so which side moves is a contract question, not an implementation detail",
        "filed as #6207: `object-grid.data` — two spec authorities disagree about the KIND (ObjectGridSchema.data resolves to ViewDataSchema, an object discriminated on `provider`; ComponentPropsMap['object-grid'].data is z.array(z.unknown())), so no declaration can satisfy both; the 'object' arm is the deliberate one from #5090 / PR #5108 and flipping it back re-opens that card — convergence is upstream"
      ]
    }

    Draft PR: #6220


    Generated by Claude Code

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

Metadata

Metadata

Labels

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopm:dispatchedtooling

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions