Skip to content

docs(spec): visibleWhen's describe states the bound roots per ADR-0068 (was three of nine) - #11600

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11256-visiblewhen-describe-roots
Aug 24, 2026
Merged

docs(spec): visibleWhen's describe states the bound roots per ADR-0068 (was three of nine)#11600
os-steve merged 1 commit into
mainfrom
claude/issue-11256-visiblewhen-describe-roots

Conversation

@claude

@claudeclaudeBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes#11256

PageComponentSchema.visibleWhen's describe named three roots while the shipping renderer binds nine. The card's ⚠️ was explicit that the wrong move is to paste the renderer's list because it happens to be what the code does, so this settles the ADR question first and lets the answer decide the wording.

Draft on purpose — path-limb clause-② (packages/spec/src/**), held for the PM's review. Substantively clause-② is no: describe/prose only, no accept/reject change, no shape change, no new keys.

1. The ADR-0068 reading — it does reach this surface, but only for the user object

Two operative quotes settle the user aliases. ADR-0068 D1:

Canonical variable current_user in all three surfaces, with user and ctx.user as aliases pointing at the same object. A predicate current_user.roles.exists(r, r == 'org_admin') (or 'org_admin' in current_user.roles) evaluates identically in a formula, an RLS policy, and a client visible gate.

A page-component visibleWhenis a client visible gate, so the ADR reaches this surface by its own words. And @objectstack/spec already says so itself — packages/spec/src/identity/eval-user.zod.ts, the ADR-0068 contract home:

The signed-in user exposed to every predicate surface (server formula, server RLS, client UI gates) under the canonical variable name current_user (aliases user, ctx.user) with an identical shape.

So documenting user / ctx.user here is transcribing a ruling this package already makes elsewhere, not code-worship. That is the load-bearing finding: page.zod.ts was under-stating a contract, not merely lagging an implementation. It also explains the card's point 1 — the sys_user alert gate's ctx.user is contract-legal, and the describe was what was wrong.

The same ADR fences everything else off. Non-goals:

Deeper formula/RLS unification beyond the current_user object (e.g. record/org/env namespaces) — only the user object is in scope here.

ADR-0058 was checked as the other candidate and rules dialects, backends and fail-policy — not the UI root set. So nothing rules app, features, os.user or data on this surface.

os.user deserves its own line, because it is the one that looks ruled and is not. ADR-0068 mentions it only as "the legacy os.userformula namespace" and among aliases "kept … during migration", and both places where the ADR states objectui's conformance (D1 and migration checklist item 5) list exactly three: "ExpressionProvider exposes current_user/user/ctx.user". os.user is a real spec spelling on other CEL surfaces (seed-loader identity, plugin-security-advanced conditions) but not a ruled root for a UI predicate — so it sits with the ambient set, not the contract set.

Neither branch 1 nor branch 2 alone, and that is the finding. Branch 1 (align to a sanctioned alias set) is right for the user object; branch 2 (honest middle) is right for the rest. Renderer-narrowing (branch 3) was not concluded — no objectui change here, no spec behaviour change.

2. The root list, as it now reads

RootVerdictBasis
recordcontractalready declared; the renderer cites this very describe as its authority
current_usercontractADR-0068 D1
user, ctx.usercontractADR-0068 D1 + eval-user.zod.ts, "client UI gates" by name
page state under a page. prefixcontractalready declared (PageSchema.variables)
os.userrenderer, not guaranteedADR-0068 names it as the legacy formula namespace; objectui's conformance clause lists three aliases, not four
app, featuresrenderer, not guaranteedADR-0068 Non-goals — user object only
datarenderer, not guaranteed, and surface-dependentsee below

The describe states the first group as binding and the second as measured-but-not-promised. The widening question — should the contract guarantee all nine? — is left open and is NOT decided here (reported to the PM).

3. The data double-meaning — measured, not inherited from the card

Verified read-only in /home/user/objectui at the pin this repo builds against (.objectui-sha = 190fbd01d061), and confirmed byte-identical at objectui origin/main (f2732afe), so the finding is not an artifact of a stale pin:

  • Component nodepackages/react/src/SchemaRenderer.tsx:463-471 composes { ...predicateScope, current_user, record?, data: dataSource, page }. data is the data-source adapter (what ${data.total} interpolates against).
  • page:tabs itempackages/components/src/renderers/layout/containers.tsx:450-457 builds its own evaluator: { ...recordData, ...predicateScope, current_user, record: recordData, data: recordData, page }. data is the record row.

Same key, two bindings. The describe cannot say that briefly enough to be safe, so it does two things: names the adapter meaning where it applies, and points at the tabs key rather than implying it carries over. The tabs key then states its own environment precisely — following the house precedent already set by view.zod.ts:1959, which documents data's surface-dependence the same way ("Inside a repeater data is the ROW, but it is still spelled data").

The tabs evaluator also spreads the row's bare fields, a third undeclared breadth; it is now named, with the precedence noted (ambient scope is spread after the row, so an ambient root wins over a same-named record field).

4. Sibling sweep

Swept packages/spec/src for the same three-root sentence.

  • One identical-staleness sibling: component.zod.ts:676page:tabs item visibleWhen, carrying the verbatim phrase "Binds record, current_user, page.<var>". Fixed identically. Its JSDoc additionally claimed it "Binds the same environment as page-component visibleWhen" — a claim the measurement above disproves on two points, so that sentence was the most wrong text in the sweep and is corrected rather than copied.
  • Left alone, wording differs materially (reported, not touched):
    • action.zod.ts:323 — per-option visibleWhen, "Same env as the field-level per-option visibleWhen (record + current_user)"; different surface, different sentence.
    • action.zod.ts:377 — action-param visible JSDoc already names its ambient scope as current_user/app/data/features; a different surface (action buttons) that does not under-state the same way.
    • view.zod.ts:1959 / :2119 — field/section visibleWhen, which deliberately say current_user is NOT bound. Opposite claim, correctly stated; untouched.

5. Regen audit

pnpm --filter @objectstack/spec gen:docs produced exactly one line of change — the visibleWhen row in content/docs/references/ui/page.mdx. No collateral.

The tabs describe produced no docs diff, and that is expected rather than a miss: component.mdx renders that item shape as a collapsed nested signature (visibleWhen?: string \| object) and emits no nested describe text, so the tabs wording reaches authors through the schema/TS surface, not the MDX table.

authorable-surface.base.json did not move either — it anchors keys, not prose, which is independent structural confirmation that this change adds no keys.

6. Verification

Gate union re-run on the final commit, a880a63fd6 (tree clean at the time of the run), quoting each gate's own verdict line:

  • check:generated✓ All 14 generated artifacts are up to date.
  • check:docs✅ 229 generated files in sync with packages/spec
  • check:objectui-pin-citations✅ 8 asserting objectui pin citation(s) match .objectui-sha (190fbd01d), 6 historical citation(s) recorded and not checked, across 991 spec source(s). The two new citations use the historical spelling (`.objectui-sha` pin `<sha>`) — a dated measurement record, which is what they are; the asserting form would have claimed something the gate re-checks on every pin bump.
  • check:nul-bytescheck-nul-bytes: OK (scanned 6490 text file(s) … no raw ASCII control bytes).
  • pnpm --filter @objectstack/spec testTest Files 420 passed (420) / Tests 11213 passed (11213)
  • pnpm --filter @objectstack/spec typecheckcheck:test-typecheck: OK
  • pnpm --filter @objectstack/spec build — exit 0

Gate list derived mechanically with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (27 families for these paths, + 6 changeset families once the changeset existed). All ran green except one, declared below. Also green: check:cross-package-test-inputs, check:doc-anchors, check:doc-authoring, check:docs-audit-scope, check:docs-redirects, check:merge-driver, check:published-files, check:quick-reference-counts, check:role-word, check:slot-lookup, check:spec-parsed-alias, check:test-source-alias, check:type-source-resolution, check:empty-state, check:liveness, check:strictness-ledger, check:variant-docs, check-ci-filter-parity, check-doc-frontmatter, check-plugin-teardown-shape, check-section-landing-index, check-affected-docs, check:changeset-gate-self-tests, check:objectui-changeset, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, release-rehearsal-clone --self-test.

Declared, not runnable here: node scripts/check-dev-prereqs.mjs (exit 1). It asserts a precondition, not a verdict on this diff: "66 of 67 workspace packages declare an entry point under dist/ that is not on disk". 67 total minus 66 unmet leaves exactly one built — @objectstack/spec, the package this card touches and the one built here. Clearing it needs a full pnpm build of the workspace, a repo-scale run CI performs before its gates.

Lint was narrowed, and the narrowing is measured rather than assumed:

  1. Population from eslint's own config: of the four changed paths, eslint reports the .mdx and .md as File ignored because no matching configuration was supplied — two files are in population.
  2. Count from --format json: 2 files linted, 0 errors / 0 warnings.
  3. Invariance for untouched files: eslint.config.mjs states it "never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file", so this diff cannot move the verdict of a file it did not touch.

7. Scope

Describe/prose only. No accept/reject change, no shape change, no new keys, no objectui change, no spec behaviour change. Changeset: patch for @objectstack/spec.


Generated by Claude Code

…ontract from renderer
`PageComponentSchema.visibleWhen` named three roots (`record`, `current_user`,
`page.<var>`) while the shipping renderer binds nine. Rather than pasting the
renderer's list, the describe now separates two different claims:
- Contract-bound: `record`, `current_user` with ADR-0068's `user` / `ctx.user`
aliases, and `page.<var>`. ADR-0068 D1 rules that a predicate "evaluates
identically in a formula, an RLS policy, and a client `visible` gate", and
`EvalUser`'s docblock already states the same alias set for "client UI
gates" — so these are transcribed from a ruling, not observed from code.
- Renderer-provided, NOT guaranteed: `app`, `features`, `os.user`, `data`.
ADR-0068's Non-goals fence its ruling to the user object, so nothing rules
these on this surface; they are recorded as measured behaviour.
- `data` is surface-dependent: the data-source adapter on a component node,
the record row on a `page:tabs` item-level `visibleWhen`.
The `page:tabs` item `visibleWhen` carried the identical three-root sentence
plus a "binds the same environment as page-component `visibleWhen`" claim that
measurement disproves; corrected the same way, naming its two real divergences.
Describe/prose only — no accept/reject change, no shape change, no new keys.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9cDbY2NBiVJWYx3BpWfH2
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/releases/v15.mdx(via PageComponentSchema (symbol), PageTabsProps (symbol))

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
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 126 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 6d3550961f57d2b5efcbb5de5dd45c0090363d03packageMentionDocs.

Which tree this was computed on

This run read content/docs from e9fda9a112bfdbe680ce3c879d2db03d0bee16af — the merge of head a880a63fd66bbf44c6e74e2f029d67415224e899 into base 6d3550961f57d2b5efcbb5de5dd45c0090363d03, 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 e9fda9a112bfdbe680ce3c879d2db03d0bee16af && git checkout e9fda9a112bfdbe680ce3c879d2db03d0bee16af
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6d3550961f57d2b5efcbb5de5dd45c0090363d03 a880a63fd66bbf44c6e74e2f029d67415224e899 && git checkout -B drift-repro 6d3550961f57d2b5efcbb5de5dd45c0090363d03 && git merge --no-ff a880a63fd66bbf44c6e74e2f029d67415224e899
node scripts/docs-audit/affected-docs.mjs --json 6d3550961f57d2b5efcbb5de5dd45c0090363d03

⚠️ 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 6d3550961f57d2b5efcbb5de5dd45c0090363d03 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:ui tooling labels Aug 24, 2026
@os-steve
os-steve marked this pull request as ready for review August 24, 2026 07:51
@os-steve
os-steve added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit 644ad50Aug 24, 2026
33 checks passed
@os-steve
os-steve deleted the claude/issue-11256-visiblewhen-describe-roots branch August 24, 2026 08:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:uisize/stooling

Projects

None yet

2 participants

@os-steve@claude