Uh oh!
There was an error while loading. Please reload this page.
docs(objectui): repair the section-tier and action-tier visibility examples on the two protocol pages - #13162
Merged
Merged
Conversation
…es on the protocol pages
…n-visibility callout
os-elon
marked this pull request as ready for review
August 29, 2026 07:00
os-elon
enabled auto-merge
August 29, 2026 07:00
Contributor
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 29, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#13087
Two files, two different defects, repaired with separate reasoning. The card's own framing is the load-bearing part and this PR keeps it: four occurrences of one key name, at two tiers, with two mechanisms.
Tier A — form section: the key is refused by name
content/docs/protocol/objectui/layout-dsl.mdx, "Conditional Sections".FormSectionSchemaisstrictObjectand declaresvisibleWhenplus the@deprecatedaliasvisibleOn— nevervisible. Measured against the schema, not read off the docs:Example 1 (
account_type/premium) is translated, to the spelling this same page already carries 440 lines below at "Visibility Rule". That was the card's point: the page contradicted itself and the wrong half came first. The replacement is byte-for-byte the page's own canonical form, so the page now has one spelling of this predicate rather than three. It parses:Example 2 (
permission: admin) is removed, not translated — the documentation judgment the card reserved. Rewriting it as avisibleWhenposition test would teach the anti-pattern the schema's own description warns about, in both directions:/f/:slug) no host publishes a predicate scope, so the root is unbound, the predicate faults open, and the section it was meant to hide is shown to everyone.In its place the section gets a
Calloutthat records both facts and points at field-level security and row-level security — the same "removed rather than implemented" pattern this page already uses twice for other retired shapes, so the residue is preserved instead of silently vanishing. The intro sentence lost "or permissions" with it: that half of the sentence was itself the wrong claim.Tier B — action: the key is live, the VALUE is wrong
content/docs/protocol/objectui/concept.mdx, "2. Contextual Actions". A genuinely different defect, which is why it is reasoned separately.Action.visibleandAction.disabledare bothActionConditionInputSchema= boolean, or a CEL string, or a{ dialect, source }envelope (packages/spec/src/ui/action.zod.ts). The key is declared; a filter object is in none of the three arms:Note the discriminator against tier A:
invalid_unionon the value, versusunrecognized_keyson the key. Two mechanisms, measured.The polarity trap here is the opposite of the field/section one, and it changes the answer. The card asked whether the
has()guard that #12935 needed also applies at this tier. It does — for a different reason, with the opposite failure direction. An action predicate evaluates against whatever record the client already fetched, so onlist_iteman unprojected column is absent, not null, and CEL aborts at key resolution. That abort is fail-closed: the button is simply not offered, which looks exactly like the predicate having said no, and nothing logs it. (A form field or section predicate faults open.) So the replacement carries the guard the sibling page's own table prescribes for an equality comparison —has()alone:The approve action is not given a
visiblepredicate at all. Its intent was a permission gate, and the action tier has a real, server-enforced key for that — so translating it into a CEL predicate over somepermissionsmap would repeat tier A's anti-pattern one tier up. The schema's own doc comment onvisiblesays so:So it declares
requiredPermissionsinstead: one declaration, dual-surface under ADR-0066 D4 — 403 on the platform action route, and the UI hide derived from the same declaration. Verified:ActionSchema.safeParse({ ..., requiredPermissions: ['approve_invoice'] })parses. The page already documents this pairing atcontent/docs/automation/approvals.mdx, so the example now agrees with it.Bounded in-place fix, declared: the adjacent
disableddisabled: { has_children: true }sits two lines below in the same fence. The card named it ("very likely the same defect and was not separately verified") without measuring it. It is repaired here under the bounded exemption, and all four conditions are stated rather than assumed:ActionConditionInputSchema), same tier, same fence.invalid_unionon the current value, and the sibling actions page prescribes the exact guard for a truthiness test (rewrite to the equality form).Leaving it would have shipped a fence that still taught Mongo query syntax two lines under the correction.
What is NOT fixed here, and why
The same fence carries two further off-spec shapes that are a different class (an undeclared enum value; a key refused by name), so the fence still does not parse end to end after this PR. They are recorded rather than swept in, filed separately as #13159:
type: standard_edit/workflow_approve/standard_delete—invalid_value; the enum is script, url, modal, flow, api, form, and these three strings appear nowhere underpackages/. There is no mechanical rename target, so it needs a documentation decision.confirm: true—unrecognized_keys, with the schema itself suggestingconfirmText.Measurements against the PM's stated assumptions
visiblekey with the keys that exist #13094 did land and did edit both files, but every one of its insertions sits below the cited sites, solayout-dsl.mdx:363,369andconcept.mdx:592,595still resolved. All four were nevertheless re-located by content, and the anchors used for the edits were read verbatim off disk with acount == 1assertion — which caught one zero-hit anchor before it could become a silent no-op edit.visible:carrying a CEL string on a key that declares it) — the instrument matches text, so a correct use is a hit. Instrument dual: the same instrument returns 2 onactions.mdx(legal uses) and moved 4 to 1 here, so it is demonstrably able to return both non-zero and a changed reading.check:skill-examplesfilters candidate fences tolang === 'ts' || lang === 'typescript'and additionally requires an opt-in{/* os:check */}marker. Ayamlfence is never a candidate. These replacements are checked by the schema parses recorded above and by nothing else.Verification
Schema parses,
@objectstack/spec(vitest, source): 12 targeted cases — the four documented shapes refused, all replacements accepted, plus a positive control per tier proving the harness can return success. Run inside the full spec suite: 11728 passed, 1 skipped, 0 failed. The harness was a throwaway file, deleted before the first commit, so it is not in this diff.Gate family derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no stale-tree warning; derived at the final commit; the matched family list is identical to the derivation taken at the first commit). 24 of the 25 matched families re-run at final HEADd872a474e1— all green. Their own verdict lines, quoted:260 prose examples type-check across 3 surface(s)(check:skill-examples)231 generated files in sync with packages/spec(check:docs)check:doc-anchors,check:doc-authoring,check:doc-frontmatter,check:doc-route-spelling,check:docs-section-name,check:section-landing-index,check:docs-single-h1,check:docs-redirects,check:docs-audit-scope,check:role-word,check:nul-bytes,check:published-readme-links,check:react-page-adapter-contract,check:cross-package-test-inputs,check:doc-security-posture,check:doc-formula-expressions,check:empty-state,check:liveness,check:strictness-ledger,check:variant-docs,check-ci-filter-parity,check-shard-attestationThe 25th,
check-test-completeness.mjs, answers a standalone run with its usage text (exit 1) — a refusal, not a finding (#13110). Four other gates first answeredPREREQUISITE NOT MET; they were re-run after building the closure and are counted only from that measured run.check:role-wordcaught a real red on the first pass: the new callout used the ADR-0090 D3 reserved word three times. Rewritten to the position / permission-set vocabulary the page already uses, and green on re-run.Repo-wide
pnpm lintis provably a no-op for this diff, measured rather than skipped: ESLint's own answer for both changed paths isFile ignored because no matching configuration was supplied(population read from the tool, not assumed);--format jsonreports 2 files in, 0 errors, and those 2 notices as the only messages; andeslint.config.mjsscopes everyfilesglob to TypeScript/JavaScript extensions, so no untouched file's verdict can move.Changeset
None — docs-only, and derived rather than asserted. Re-measured on this tree: 69
package.jsonfiles declare afileswhitelist and zero of them namecontentordocs;content/matches nopnpm-workspace.yamlpackage glob (the globs arepackages/*,packages/apps/*,packages/drivers/*,packages/plugins/*,packages/qa/*,packages/triggers/*,packages/services/*,packages/adapters/*,packages/connectors/*,apps/*,examples/*). Nothing here is published, so this PR declares no release of its own and carriesskip-changeset.Generated by Claude Code
Generated by Claude Code