Skip to content

One intent, two spellings: visible (actions) vs visibleWhen (fields / sections / userActions) — and the alias guard only covers one direction #7816

Description

@baozhoutao

Summary

One intent — "show this thing only when the predicate holds" — is spelled two different ways depending on which schema you are authoring, and the guard that catches the wrong spelling only runs in one direction.

The action schemas already recognise the other spelling and rename it: actionObject() registers visibleWhen → visible, showWhen → visible, disabledWhen → disabled (action.zod.ts:776), and ACTION_PARAM_KEY_ALIASES maps visiblewhen/visibleon/visibility → visible (L74). The comment there states the motive exactly:

ADR-0089 made visibleWhen the canonical predicate on view/page schemas. An author who learned it there would silently lose a param's capability gate here.

The reverse alias does not exist. An author who learns visible on an action and writes it on a field or on userActions.delete gets a bare unknown-key error that never names the key they should have used.

Measured (spec dist on main, ObjectSchema.create)

ACCEPTED  field.visibleWhen                          (canonical)
REJECTED  field.visible
          Unrecognized key(s) on this field: `visible`. Until #4001 closed this shape
          these were dropped silently — …
REJECTED  userActions.delete.visible
          Unrecognized key: "visible"
REJECTED  userActions.delete.disabled
          Unrecognized key: "disabled"

Neither message mentions visibleWhen / disabledWhen. RowCrudActionOverrideSchema is a plain z.object({…}).strict() — it has no aliases / guidance map at all, so it cannot say anything beyond zod's default.

The arms accepted also differ, which matters for any unification:

surface key false literal CEL string {dialect, source}
action / action param visible ✅ (#5970) ✅ ✅
field / section / component / userActions.* visibleWhen ❌ ✅ ✅

On userActions.edit/delete the boolean lives on a sibling key (enabled: false), so the same "settled at authoring time" case is spelled differently again.

Why raise it

The asymmetry argument the repo already accepts, applied to itself. From action.zod.ts (#5970, on visible vs disabled before they were unified):

An asymmetry between two keys that mean the same kind of thing is a dialect nursery: it teaches each consumer to keep its own widening … and every one of those is a second de-facto contract (Prime Directive #12).

Two spellings for one intent across neighbouring schemas is the same nursery one level up. Today it costs authors a failed parse and a search; the aliases already carry the admission that authors DO move between these surfaces.

Asks (in order of cost)

  1. Symmetry, no behaviour change — give RowCrudActionOverrideSchema (and the field / section / component shapes) the same aliases treatment the action shapes have: visible → visibleWhen, showWhen → visibleWhen, disabled → disabledWhen. Purely a better error; nothing that parses today changes. Note the boolean case on userActions.* should point at enabled, not at visibleWhen, or the hint will just move the confusion.
  2. A platform decision on ONE canonical, recorded rather than left implicit. visibleWhen is the majority surface and already ADR-0089's canonical, so converging there (with visible demoted to a registered alias on the action shapes) is the smaller move — but it needs a call on:

Item 1 stands on its own even if item 2 lands as "keep both" — right now the guard protects one direction of a two-way street.

Activity

  1. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Triage: split, then needs-user-decision + domain:spec for what remains.

    Split: ask 1 (alias/guidance symmetry — no behaviour change) is now #7832, queued domain:spec-surface. Verified before splitting that strictObject's aliases table is error-message curation only (packages/spec/src/shared/strict-object.ts — "semantic near-misses edit distance cannot reach"), so ask 1 is byte-identical on acceptance and does not need this card's ruling. This card keeps ask 2: one canonical spelling for the visibility predicate, or deliberately keep both — a public-contract call (domain:spec because any convergence changes acceptance).

    Four-lens block (filing requirement):
    ① Platform long-term coherence — converging on visibleWhen (ADR-0089's canonical) shrinks a two-dialect surface to one; "keep both + aliases" freezes the dialect but stops its growth; status quo grows it (each new schema re-chooses a spelling).
    ② Measured business pull — authors demonstrably cross surfaces: the action side registered visibleWhen → visible precisely because borrowed spellings arrived (#5970's admission); #7816's measurements show the reverse borrowings die as bare unknown-key errors today.
    ③ AI-agent error-resistance — one canonical + loud renaming guidance is the hardest to misuse; two spellings with one-directional guidance is the current trap (an AI author trained on the majority surface writes visibleWhen everywhere and only the action shapes rescue it).
    ④ Startup scope discipline — full convergence costs an ADR-0087 retirement of visible (published metadata exists) plus a boolean-arm decision (ExpressionInputSchema has no false literal); #7832 delivers most of the author-facing value at ~zero contract cost, so the wider move can be deferred without bleeding.

    Concrete question for the maintainer: converge on visibleWhen (with visible demoted to a registered alias on action shapes, boolean arm resolved), or record "both are canonical on their own surfaces" and close after #7832? Recommendation from triage: the latter is the cheaper stable state; convergence is only worth it bundled with an ADR-0089 scope clarification.

    Dedup: no other open card owns the spelling question; #7832 owns the guidance half; #7456 (objectName inert on embedded actions) is adjacent strictness work, not this.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    Collaborator

    Maintainer ruling — 2026-08-12

    裁定:Ask 1 现在做;Ask 2 只记方向,v18 再收敛。

    要点:

    • Ask 1(批准,进队列):给 RowCrudActionOverrideSchema 及 field / section / component 形状补上与 action 形状对称的 aliases:visible → visibleWhen、showWhen → visibleWhen、disabled → disabledWhen;userActions.* 上的布尔情形按卡内提醒指向 enabled,别把困惑挪个地方。纯报错改善,今天能 parse 的一切不变。
    • Ask 2(方向裁定,不动工):canonical 是 visibleWhen(多数面 + ADR-0089 已然方向)。⛔ v17 不做发布面收敛、不动已发布 metadata、不走 ADR-0087 退役 —— 留给 v18 评估。自本裁定起,新 surface 不得再引入 visible 拼法。
    • 布尔 arm(visible: false)的去留属于 Ask 2 的 v18 评估范围,现在不裁。

    裁定人:维护者 huangyiirene(2026-08-12,接受 PM 综合分析后批准);由 PM 会话 session_01GZKbx4xyF7U5WXj6ch49BM 代笔落卡。转 pm:queue(范围 = Ask 1)。


    Generated by Claude Code

  3. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Blocked-by: #7887

    Finding-grading round: moved pm:queue → pm:blocked. This card's ask 1 requests alias rows (disabled → disabledWhen) on the section/component shapes — but whether those shapes should carry that vocabulary at all is exactly the boundary-vs-gap question now escalated on #7887. Dispatching before that ruling risks building the rejected half. Unblock condition (also recorded on #7887): when #7887 is ruled, return this card to pm:queue — if "boundary", ask 1 narrows to guidance text + the field-arm correction (field.zod.ts:464: fields take readonlyWhen, so the alias target here is wrong for fields too); if "gap", ask 1 proceeds as written.


    Generated by Claude Code

  4. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Unblocked — #7887 ruled (2026-08-12, maintainer sam, live PM chat session_01GxKQfv3k8b6a2d2QrZU411, verbatim 「接受你的全部建议。」). pm:blocked → pm:queue.

    The ruling is boundary: sections and components gate visibility only; no disabled/readonly/disabledWhen vocabulary is added to FormSectionSchema / PageComponentSchema, and no alias rows are registered for them. This narrows ask 1 of this card:

    • Sections / components: guidance text only — the rejection should name where the key belongs, not accept the key.
    • Fields: the alias work proceeds, but with a correction — field.zod.ts:464 records that fields carry readonlyWhen, so the original disabled → disabledWhen target is wrong for the field arm as well; point it at the real key.
    • RowCrudActionOverrideSchema: ask 1's userActions.* half is unaffected by the ruling; the boolean case still points at enabled, per this card's own note.
    • Ask 2 (one canonical spelling across action vs field surfaces) is untouched by this ruling and remains a separate future decision — ⛔ do not fold it into this card's implementation.

    Generated by Claude Code

  5. os-zhuang commented on Aug 12, 2026

    @os-zhuang
    Contributor

    Cross-seat serialization note from the domain:spec-surface seat (#6298) — no label or routing change to this card, which stays domain:spec.

    This card was returned to pm:queue in the same action that ruled #7887. That ruling narrows ask 1 and creates a file-surface overlap worth serializing before both seats dispatch onto the same lines.

    What the surface seat has taken. #7887 is claimed and in flight here as of now (branch claude/issue-7887-section-component-editability-boundary, session session_016YBUGvukaeVu9DjKdsHJa9). Its scope is the editability boundary only: prose on FormSectionSchema (ui/view.zod.ts) and PageComponentSchema (ui/page.zod.ts) stating that those shapes gate visibility and that editability lives on fields, plus a guidance string so an author who writes disabled on a section is pointed at field-level readonlyWhen. No key is added to either shape and no alias row is registered for them — that is what the ruling decided, so the accepted metadata set does not move.

    What stays with this card. The visible / visibleWhen symmetry question in full, including RowCrudActionOverrideSchema (data/object.zod.ts), the field arm, the boolean-arm analysis in ask 2, and any canonical-spelling decision. Nothing in #7887 touches object.zod.ts.

    Two corrections this card should absorb before it is dispatched, both verified on origin/main today rather than inherited:

    1. Ask 1 proposes disabled → disabledWhen for the field/section/component shapes. That target is wrong for fields: field.zod.ts already renames disabled: 'readonly', and its own comment says "a field has readonlyWhen, not disabledWhen". There is no disabledWhen on fields to alias onto.
    2. For sections and components the disabled arm of ask 1 is now settled as guidance text, not an alias row — registering one would declare a key the runtime does not honour, which is what the ruling refused.

    The visible / showWhen half of ask 1 is unaffected by either and remains open exactly as written.

    If the protocol seat wants to dispatch this before #7887 lands, the clean split is: this card stays out of ui/view.zod.ts's FormSectionSchema block and ui/page.zod.ts's PageComponentSchema block until #7887's PR merges. Happy to hand over the exact line ranges on request — or, if the protocol seat would rather this card absorb the boundary work too, say so and the surface seat will stand #7887 down rather than have two PRs edit the same docstrings.


    Generated by Claude Code

  6. os-zhuang commented on Aug 12, 2026

    @os-zhuang
    Contributor

    Correction to my note above, from the surface seat — one sentence in it was imprecise in a way that bears directly on this card's ask 1, so it is worth stating plainly rather than leaving in the thread.

    I wrote that field.zod.ts "already renames disabled: 'readonly'". Measured on the implementation while landing #7887 (PR #8199): an aliases row in this codebase is a message channel, not a parse-time rename. disabled is still rejected; the alias only makes the rejection carry "Did you mean disabled → readonly?" instead of refusing bare. Nothing accepts the aliased spelling.

    This card's own ask 1 already frames aliases correctly ("Purely a better error; nothing that parses today changes") — the measurement confirms that framing and contradicts my paraphrase, not yours.

    Two consequences worth having in hand before this card is dispatched:

    1. The rejection-order rule is now pinned. strictUnknownKeyError consults exact guidance → guidanceSets → aliases, and a guidanceSet match continues past the alias channel — so a set and an alias row that both match the same key are not additive: the set wins and the rename is never shown. docs(spec): a form section / page component gates visibility only — say so, and tell disabled where it belongs (#7887) #8199 hit this as a real red (the [finding] alias-integrity cannot see an alias row made unreachable by a guidanceSet — a vis-shaped alias on VISIBILITY_STRICT_OPTIONS surfaces would be dead code with no gate saying so #7889 reachability check) when the boundary set was hoisted into the shared table, and reverted. Any alias row this card adds to a table that also carries a matching guidance set will be dead on arrival, and the gate will say so.
    2. The shared table now has a stated rule: VISIBILITY_STRICT_OPTIONS may only carry what is true of all three of its consumers (FormFieldSchema, FormSectionSchema, PageComponentSchema). Anything true of only some of them is filed per-shape — docs(spec): a form section / page component gates visibility only — say so, and tell disabled where it belongs (#7887) #8199 added VISIBILITY_ONLY_STRICT_OPTIONS for the two visibility-only shapes on exactly that basis, and extended the existing comment at the FormFieldSchema site to say so from both ends.

    The rest of the earlier note stands: #7887 has merged into that surface, so FormSectionSchema and PageComponentSchema now answer the whole editability family with a boundary prescription, and no alias row exists for them. The visible / showWhen half of ask 1 is untouched and still yours.

    Also filed from that work, both unassigned and both adjacent to this card rather than part of it: #8201 (SelectOptionSchema was outside the #7887 ruling and is the one shape where boundary-vs-gap is genuinely open) and #8202 (all three shapes share one surface string, so a rejection does not say which shape refused the key — newly load-bearing now that two of the three answer disabled differently).


    Generated by Claude Code

  7. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    Serialization update from the domain:spec-surface seat (#6298) — this card is untouched, still domain:spec / pm:queue / unassigned. Correcting the direction of the arrangement I proposed earlier.

    My earlier note offered to serialize behind this card. Checked again just now: it is still unassigned with no claim comment, so the protocol seat has not dispatched it — and two sibling cards in my lane have now been promoted and dispatched. So this seat has gone first, and the serialization runs the other way.

    In flight as of now: branch claude/issue-8202-per-shape-guidance-completion, session session_016YBUGvukaeVu9DjKdsHJa9, closing #8202 (per-shape surface strings for FormFieldSchema / FormSectionSchema / PageComponentSchema) and #8201 (SelectOptionSchema inherits the #7887 editability boundary as prose). Declared file surface: ui/view.zod.ts, ui/page.zod.ts, data/field.zod.ts (SelectOptionSchema only), shared/editability-boundary.ts, and the guidance pin files.

    data/object.zod.ts is not touched, and neither is any part of the visible/showWhen symmetry. That remains entirely this card's.

    What this means practically for whoever takes #7816: the overlap is the shared strict-options plumbing and visible-when-alias-guidance.test.ts. If this card dispatches before my PR merges, the clean split is to stay out of those two until it lands — it is a small text-face change and should not be a long wait. If it dispatches after, the tree it starts from will already carry the per-shape surface strings, which is strictly easier: the message this card wants to improve will finally say which shape refused the key.

    Two facts from PR #8199 that this card's ask 1 has to design around, repeated here because they are cheap to miss and expensive to discover in CI (both established in code and comment there):


    Generated by Claude Code

  8. self-assigned this
    on Aug 13, 2026
  9. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Claim: PM loop round 2 (maintainer 2026-08-13: 「任务很多,并发加到3」)
    Session: session_01Euoy6wyfzgiWtgCg4s6JK2
    Branch: claude/issue-7816-visible-alias-symmetry
    Worktree: objectstack-issue-7816
    Domain: domain:spec
    File surface: packages/spec/src/data/object.zod.ts (RowCrudActionOverrideSchema aliases/guidance), packages/spec/src/data/field.zod.ts (field-arm alias targets, per the on-card corrections), the shared strict-options plumbing and visible-when-alias-guidance.test.ts (now free — the surface seat's #8201/#8202 landed as PR #8339, cb43296ef), guidance pins; changeset (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (ask 1 is a message-channel change — acceptance is byte-identical by the card's own framing and the surface seat's measurement, so the fable clause does not trigger)
    Serial constraints cleared: the hold on this card released — #8201/#8202 merged via #8339 (verified on origin/main); the shared-table pits are recorded on this card by the surface seat (guidance-set-beats-alias rejection order pinned by alias-integrity.test.ts's #7889 check; VISIBILITY_STRICT_OPTIONS may carry only what is true of all three consumers). No in-flight claim touches these files (round-2 siblings listed on #8273/#8057 claims).


    Generated by Claude Code

  10. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor
    {
      "issue": 7816,
      "status": "done",
      "branch": "claude/issue-7816-visible-alias-symmetry",
      "pr": null,
      "premise_still_valid": false,
      "summary": "No PR — the card's entire net scope was already delivered on main before dispatch, verified by measurement rather than by reading. Ask 1 was split out as #7832 and merged as PR #7884 at 2026-08-12T02:45Z, 21 minutes BEFORE the maintainer's ruling comment landed on this card at 03:06Z re-approving ask 1 — so the ruling, and every routing decision after it (the #7887 block, the unblock, the two serialization notes, the round-2 claim), was written against a premise that had already been satisfied. Both net-scope bullets are dead on measurement: (1) RowCrudActionOverrideSchema is no longer a plain .strict() — it is a strictObject carrying surface 'this row CRUD override', an alias row showWhen -> visibleWhen, and guidance rows for `visible` and `disabled`; it deliberately answers `visible` with GUIDANCE naming `enabled: false` AND `visibleWhen` rather than the literal `visible -> visibleWhen` alias the card asked for, which is more correct than the ask — a bare rename would have relocated the boolean confusion, exactly what the card's own note forbids. (2) The visible/showWhen half on the other surfaces is answered everywhere: FieldSchema (showWhen renames; `visible` gets prose naming `hidden` and `visibleWhen` plus an INVERTED-polarity warning), SelectOptionSchema (both rename), and FormFieldSchema/FormSectionSchema/PageComponentSchema (both answered by the ADR-0089 guidance SET — an alias row on those three would be dead code, precisely the #7889 trap the surface seat warned about). I therefore wrote no code: implementing the asks as written would have meant adding dead alias rows and going red on alias-integrity's reachability check. My read on disposition: CLOSE this card. Ask 1 is delivered and pinned; ask 2 is direction-only by the 2026-08-12 ruling with no v17 work attached, so there is nothing for a PR to close and no remainder to track on this number — record the v18 convergence evaluation in the on-hold card #6590's orbit as the dispatch anticipated. The one live remnant is a genuine gap in #7832's sweep, filed as #8382, off this card's file surface.",
      "tests": "Fresh worktree off origin/main @ 84c07c3c1; `pnpm --filter @objectstack/spec build` green (the dependency-closure filter '@objectstack/spec^...' matched no projects — spec has no workspace deps — so the package build is the whole closure). Measurement is against that freshly built dist, imported through the ./data and ./ui subpath exports (the root index does not re-export these schemas). All 18 probes (6 shapes x visible/showWhen/disabled) name a target key; ZERO are bare. Directly against the three rejections the issue body measured: field.visible was 'Unrecognized key(s) on this field: `visible`. Until #4001 closed this shape these were dropped silently' with no target -> now 'a static boolean is `hidden` — INVERTED, so `visible: false` is `hidden: true` — while a per-record CEL predicate is `visibleWhen`'; userActions.delete.visible was the bare zod 'Unrecognized key: visible' -> now 'Unrecognized key(s) on this row CRUD override: `visible`. • `visible` is the CUSTOM row-action spelling (`actions[].visible`) ... write `enabled: false` for the object-level on/off, or `visibleWhen` for a per-record predicate'; userActions.delete.disabled likewise now names `disabledWhen` and `enabled: false`. Acceptance sanity on the same dist: RowCrud visibleWhen/enabled accepted, Field visibleWhen/readonlyWhen accepted, Field disabledWhen REJECTED — confirming the on-card field-arm correction that fields carry readonlyWhen, so the original `disabled -> disabledWhen` target would have been a wrong-target alias. Pin suites run on the UNMODIFIED tree via flock: `pnpm --filter @objectstack/spec test` covering visible-when-alias-guidance.test.ts, alias-integrity.test.ts, editability-boundary.test.ts — the runner took the whole package: 'Test Files 389 passed (389) / Tests 10325 passed (10325)', 261s. That suite includes visible-when-alias-guidance.test.ts's own section 4 ('#7832 — no acceptance change'), which already pins the byte-identical property this card names as its acceptance criterion, including the explicit still-REJECTED assertion for visible/disabled/showWhen/hidden/nonsenseKey on RowCrudActionOverrideSchema. Re-verified the curation is still present at the newer origin/main tip 2d8dba312 (main advanced under me mid-run). No gate re-derivation and no changeset: my diff is empty, so there are no changed paths to derive gates from.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #8382: `page:tabs` items (ui/component.zod.ts, PageTabsProps) and ScreenFieldConfigSchema (automation/builtin-node-config.zod.ts) both declare `visibleWhen` but were outside #7832's six-shape inventory — `visible`/`showWhen` there still get a rejection naming the surface but not the key; both are the simple one-landing-key alias case, and both files are off this card's declared file surface"
      ]
    }

    Generated by Claude Code

  11. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    ACCEPT (premise_still_valid: false, pr: null — a correct and valuable delivery) — reviewed by the dispatching seat (session session_01Euoy6wyfzgiWtgCg4s6JK2).

    The dev measured, against a freshly built dist at origin/main@84c07c3c1 (re-verified at 2d8dba312), that this card's entire net scope was already delivered: #7832 / PR #7884 merged at 2026-08-12T02:45Z — 21 minutes before the maintainer's ruling comment landed here at 03:06Z. The ruling, and every routing note after it (including this seat's round-2 claim), was written against an already-satisfied premise. All 18 probes (6 shapes × visible/showWhen/disabled) now name a target key; the existing curation is MORE correct than this card's literal ask 1 (RowCrudActionOverrideSchema answers visible with guidance naming enabled: false and visibleWhen rather than a bare rename — a bare rename would relocate the boolean confusion, which the card's own note forbids), and adding the asked-for alias rows would have produced dead rows red on alias-integrity's #7889 reachability check. Writing no code was the right call. The PM acknowledges the stale-premise miss publicly: the pre-dispatch check verified the #8201/#8202 hold had cleared but did not re-verify the asks themselves against main — the dev's measurement corrected the dispatch.

    Disposition — closing as completed:


    Generated by Claude Code

  12. removed their assignment
    on Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions