feat(lint): move the views[] visibility-predicate family to the runtime publish gate (#7220) - #7479
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
…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
Fixes #7220
The four rules that judge a
views[]conditional-visibility predicate ran on the three CLI commands only. Aviewwritten 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_RUNTIMEwithruntimeTypes: ['view'], in one edit, on the maintainer's 2026-08-10 ruling and sequenced after #4717'sadvisorieschannel landed (PR #7435, verified closedcompletedat 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 entries —visibility-predicate-over-budget(#7217) landed after the card was filed:visibility-predicate-syntaxvalidateVisibilityPredicatesvisibility-predicate-over-budgetvalidateVisibilityPredicatesvisibility-bare-identifiervalidateVisibilityPredicatesvisibility-root-mislayeredvalidateVisibilityPredicatespredicate-path-unresolvedvalidatePredicatePathRefspredicate-path-unrootedvalidatePredicatePathRefsThe 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
viewrefused 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 prosesurfaceReasonthat 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_KEYalready mappedview → views, andruntime-authoring-gate.tspasses the written type straight through. Its two'flow'literals belong to the gate-local #6285 rule (findPlatformScheduleOrgGaps) and itsrulesRundisclosure — not a type allowlist. Declaring is sufficient, so this is not the cross-seat case: no file underpackages/metadata*is touched.runtimeGatedTypes()now returns['flow', 'view'].Neither rule needs a stack-wide snapshot. Both read
stack.viewsandstack.pagesand no other collection — neverobjects. The previoussurfaceReasononvalidateVisibilityPredicateswasRUNTIME_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. (pagesis simply absent on aviewwrite, so the page half contributes zero findings to both differential passes rather than inventing any.)The door and
os buildagree. 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
runRuntimeAuthoringRulesdispatch path,type: 'view':fires — each defect is now refused, with the located
{rule, path, where, message, hint}the 422 envelope carries verbatim aserr.issues:visibility-root-mislayerediswarningon every surface, so it does not 422 — it reaches the author through theadvisorieschannel 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
viewstill publishes witherrors: []andadvisories: [],rulesRunnon-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 unrelatedviewwrite); and the CLI reaches identical verdicts on all eight corpus inputs.Gates
pnpm --filter @objectstack/lint test(incl.authoring-rule-wiring.test.ts, the wiring pin — there is no separatecheck:script)pnpm --filter @objectstack/lint typecheckpnpm --filter @objectstack/metadata-protocol test(consumer)pnpm --filter @objectstack/objectql test src/save-meta-response-conformance.test.ts(consumer)Changeset
Ships a changeset, not
skip-changeset: this is a behaviour change on a public door — aviewpublish carrying one of these defects used to succeed and is now refused with422 invalid_metadata. Drafts are still never gated (only a publish runs the gate), the findings name the site and the fix, andOS_ALLOW_UNLINTED_METADATA_WRITES=1remains the migration hatch.Generated by Claude Code