Skip to content

feat(lint): move the views[] visibility-predicate family to the runtime publish gate (#7220) - #7479

Merged
os-help merged 2 commits into
mainfrom
claude/issue-7220-visibility-family-runtime-publish
Aug 10, 2026
Merged

feat(lint): move the views[] visibility-predicate family to the runtime publish gate (#7220)#7479
os-help merged 2 commits into
mainfrom
claude/issue-7220-visibility-family-runtime-publish

Conversation

@os-help

@os-help os-help commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7220

The four rules that judge a views[] conditional-visibility predicate ran on the three CLI commands only. A view written through the runtime publish door — Studio's designer, REST /meta, MCP — was judged by none of them, and that door is the only one most tenants have and the one AI authors use. A predicate that cannot parse saved clean and then failed OPEN in the console: the element renders unconditionally, pixel-identical to one carrying no predicate at all (#5149).

Both registry entries move to surfaces: CLI_AND_RUNTIME with runtimeTypes: ['view'], in one edit, on the maintainer's 2026-08-10 ruling and sequenced after #4717's advisories channel landed (PR #7435, verified closed completed at 11:59:34Z).

The family is six rule ids, not four

The card names four defects. Measured on origin/main@8e17759, the surface carries six ids across two registry entriesvisibility-predicate-over-budget (#7217) landed after the card was filed:

id severity entry
visibility-predicate-syntax error validateVisibilityPredicates
visibility-predicate-over-budget error validateVisibilityPredicates
visibility-bare-identifier error validateVisibilityPredicates
visibility-root-mislayered warning validateVisibilityPredicates
predicate-path-unresolved error validatePredicatePathRefs
predicate-path-unrooted error validatePredicatePathRefs

The extra member does not create the partial-enforcement risk the card warns about: it lives inside the same rule function as three of its siblings, so it crosses the wall with them by construction. All six move; nothing is left behind. Flagging it explicitly because the dispatch asked to be told.

They move together on purpose

#7214's implementer wired the path rule alone, measured the result and reverted it: a view refused for an unresolvable predicate PATH while a predicate that does not parse at all walks through the same door is less predictable than refusing neither.

So the family property is now pinned in authoring-rule-wiring.test.ts — every id on this surface is gated at the runtime door, or none is — rather than trusted to a prose surfaceReason that goes stale silently. The pin fails in either direction, so splitting the family again is an edit that must go through that line rather than around it. RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY, the constant that recorded the CLI-only state as a decision, is deleted with its last user.

Three things measured rather than assumed

The gate needed no dispatch-side change. runtimeGatedTypes() is derived from this registry, TYPE_TO_STACK_KEY already mapped view → views, and runtime-authoring-gate.ts passes the written type straight through. Its two 'flow' literals belong to the gate-local #6285 rule (findPlatformScheduleOrgGaps) and its rulesRun disclosure — not a type allowlist. Declaring is sufficient, so this is not the cross-seat case: no file under packages/metadata* is touched. runtimeGatedTypes() now returns ['flow', 'view'].

Neither rule needs a stack-wide snapshot. Both read stack.views and stack.pages and no other collection — never objects. The previous surfaceReason on validateVisibilityPredicates was RUNTIME_NEEDS_FULL_SNAPSHOT, and it was describing nothing true about this rule; it was the reason a rule got when nobody measured, which is the #4409/#4463 defect one layer in. (pages is simply absent on a view write, so the page half contributes zero findings to both differential passes rather than inventing any.)

The door and os build agree. The new corpus test drives the real dispatch path over eight inputs and asserts the finding sets are identical — id, severity and path. The move changes where the rules run, never what they say.

Reverse verification

Driven through the real runRuntimeAuthoringRules dispatch path, type: 'view':

fires — each defect is now refused, with the located {rule, path, where, message, hint} the 422 envelope carries verbatim as err.issues:

── unparseable predicate   record.status === 'open'
   ERROR [visibility-predicate-syntax]        views[0].form.sections[0].fields[0]
── bare identifier         status == 'open'
   ERROR [visibility-bare-identifier]         views[0].form.sections[0].fields[0]
── over budget             120-term || chain
   ERROR [visibility-predicate-over-budget]   views[0].form.sections[0].fields[0]
── unresolvable path ref   data.tpye == 'text'
   ERROR [predicate-path-unresolved]          views[0].form.sections[0].fields[0].visibleWhen
── unrooted schema key     type == 'text'
   ERROR [predicate-path-unrooted]            views[0].form.sections[0].fields[0].visibleWhen
── mislayered root         data.status == 'open'  (runtime layer)
   advisory [visibility-root-mislayered]      views[0].form.sections[0].fields[0]  (warning)

visibility-root-mislayered is warning on every surface, so it does not 422 — it reaches the author through the advisories channel on the 2xx save response. That channel is exactly why the maintainer sequenced this move after #4717: moving the rules before findings could travel back would have run them and discarded the verdicts, the shape #4463 exists to close.

stays quiet — a valid view still publishes with errors: [] and advisories: [], rulesRun non-empty so "clean" stays distinguishable from "nothing ran"; the D4 subtraction still holds on the new type (a stored object with a broken predicate does not block an unrelated view write); and the CLI reaches identical verdicts on all eight corpus inputs.

Gates

gate result
pnpm --filter @objectstack/lint test (incl. authoring-rule-wiring.test.ts, the wiring pin — there is no separate check: script) ✅ 70 files, 1863 tests
pnpm --filter @objectstack/lint typecheck
pnpm --filter @objectstack/metadata-protocol test (consumer) ✅ 70 files, 1035 tests
pnpm --filter @objectstack/objectql test src/save-meta-response-conformance.test.ts (consumer) ✅ 10 tests

Changeset

Ships a changeset, not skip-changeset: this is a behaviour change on a public door — a view publish carrying one of these defects used to succeed and is now refused with 422 invalid_metadata. Drafts are still never gated (only a publish runs the gate), the findings name the site and the fix, and OS_ALLOW_UNLINTED_METADATA_WRITES=1 remains the migration hatch.


Generated by Claude Code

…me publish gate (#7220)

The four rules that judge a `views[]` conditional-visibility predicate ran on
the three CLI commands only. A `view` written through the runtime publish door
— Studio's designer, REST `/meta`, MCP — was judged by NONE of them, and that
door is the only one most tenants have and the one AI authors use. A predicate
that cannot parse saved clean and then failed OPEN in the console: the element
renders unconditionally, pixel-identical to one carrying no predicate (#5149).

Both registry entries move to `surfaces: CLI_AND_RUNTIME` with
`runtimeTypes: ['view']`, in ONE edit, on the maintainer's 2026-08-10 ruling
and sequenced after #4717's `advisories` channel landed (PR #7435). Six rule
ids cross together: `visibility-predicate-syntax`,
`visibility-predicate-over-budget`, `visibility-bare-identifier`,
`visibility-root-mislayered`, `predicate-path-unresolved` and
`predicate-path-unrooted`.

They move together on purpose. #7214's implementer wired the path rule alone,
measured the result and reverted it: a `view` refused for an unresolvable
predicate PATH while a predicate that does not parse at all walks through the
same door is less predictable than refusing neither. A half-wired wall is worse
than an unwired one, so the family property is now PINNED in
`authoring-rule-wiring.test.ts` — every id on this surface is gated at the
runtime door, or none is — rather than trusted to a prose `surfaceReason` that
goes stale silently. `RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY`, the constant that
recorded the CLI-only state as a decision, is deleted with its last user.

Three things were measured rather than assumed:

- The gate needed no dispatch-side change. `runtimeGatedTypes()` is derived
  from this registry, `TYPE_TO_STACK_KEY` already mapped `view -> views`, and
  `runtime-authoring-gate.ts` passes the written type straight through — its
  two `'flow'` literals belong to the gate-local #6285 rule, not to a type
  allowlist. Declaring is sufficient; `runtimeGatedTypes()` now returns
  `['flow', 'view']`.
- Neither rule needs a stack-wide snapshot. Both read `stack.views` and
  `stack.pages` and no other collection — never `objects` — so the previous
  `RUNTIME_NEEDS_FULL_SNAPSHOT` reason on `validateVisibilityPredicates` was
  describing nothing about this rule. It was the reason a rule got when nobody
  measured, which is the #4409/#4463 defect one layer in.
- The door and `os build` agree. `runtime-gate.test.ts` drives the real
  dispatch path over an eight-input corpus and asserts the finding sets are
  IDENTICAL — id, severity and path — so the move changes WHERE the rules run,
  never WHAT they say. Set equality, not "both non-empty": a half-wired wall is
  precisely the state where both sides are non-empty and disagree.

`visibility-root-mislayered` is `warning` on every surface, so it does not 422
— it reaches the author through the `advisories` channel on the 2xx save
response. That channel is why this move was allowed to happen at all: moving
the rules before findings could travel back would have run them and discarded
the verdicts, the exact shape #4463 exists to close.

Behaviour change on a public door (a `view` publish carrying one of these
defects is now refused), so it ships a changeset rather than skip-changeset.
Drafts are still never gated and `OS_ALLOW_UNLINTED_METADATA_WRITES=1` remains
the migration hatch.

Fixes #7220

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpSiahE9sb96neoWajbiQz
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 5:29pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/deployment/validating-metadata.mdx (via packages/lint)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/v17.mdx (via @objectstack/lint)

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.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…oo (#7220)

The surface table in `validating-metadata.mdx` was made wrong by the rule move
in this PR, in the two places a reader would actually rely on:

- it stated `flow` is the only metadata type any rule declares at the runtime
  publish door, and that "an object, view, page or dashboard save is checked by
  the schema parse and by nothing else". Both are now false for `view`;
- the `views[]` visibility-predicate family had no row at all, so the table
  could not say what that door does or does not judge about a predicate.

Adds the row with a `✓ᵛ` marker for `view` writes, corrects the footnote to name
both gated types, and records WHY the family crossed together — the property the
wiring guard now pins — since a reader who takes the table as a menu of
independently-wirable rules would draw exactly the conclusion #7220 was filed to
prevent.

The rest of the page was re-read and is unchanged: the "held to the CLI's
verdicts" paragraph, the 422 envelope description and the #6285 runtime-only
exception all remain accurate.

`content/docs/releases/v17.mdx` also references `@objectstack/lint` and is
release-owned — read, left untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpSiahE9sb96neoWajbiQz
@os-help
os-help marked this pull request as ready for review August 10, 2026 17:47
@os-help
os-help added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 333769d Aug 10, 2026
27 checks passed
@os-help
os-help deleted the claude/issue-7220-visibility-family-runtime-publish branch August 10, 2026 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants