You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] The four views[] visibility-predicate rules are CLI-only — a Studio/REST/MCP view write bypasses all of them; if they move to runtime-publish, they must move together #7220
Recorded from #7010 / PR #7214's implementation report (devx PM seat filing; unassigned, ungraded — grading is triage's call).
The observation
The views[] visibility-predicate family — syntax (#6253's), bare-identifier (#6128's), root-mislayered, and now path-refs (#7214) — runs only on the CLI authoring surface. A view written through the runtime-publish door (Studio / REST /meta / MCP) is judged by none of them.
The instructive part: partial wiring was implemented and deliberately reverted
#7214's implementer wired its own rule to runtime-publish (technically free — the rule's oracle is a static registry, stackKeyForType('view') exists, it needs nothing but the written item), then reverted it, for a reason worth keeping:
A Studio view write would be refused for an unresolvable path while a predicate that does not parse at all, and one with no root at all, walked through the same door. Three sibling verdicts about one predicate, one enforced, none predictable. A half-wired wall is worse than an unwired one.
The registry entry records this as a decision, not a limitation, under a new surfaceReason constant (RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY) — deliberately not reusing RUNTIME_NEEDS_FULL_SNAPSHOT, which would have been a fabricated reason (this rule needs no stack-wide snapshot).
Dispositions
A — family stays CLI-only (shipped state; the recorded reason keeps it honest).
C — move all four rules to runtime-publish together, in one measured edit. This is a decision about what a views write door promises, not a rider on any one rule's card.
(B — wiring rules one at a time as each happens to be snapshot-free — is the reverted option and the anti-pattern this card exists to prevent.)
Hold rationale: A-vs-C is a decision about what the runtime views write door promises — but the publish-time-validation family question already sits in the decision box as objectui: metadata-admin 谓词求值器对「无法解析的路径」静默判假(fail-CLOSED),与文件自述的 fail-open 承诺相反 #6936 (cited by this card). Holding rather than opening a second inbox slot for the same family; the recorded surfaceReason keeps the shipped state honest meanwhile.
Maintainer ruling (2026-08-10, directed in session session_01BPWqbmEFU8gJepBJTHESXd): the four rules move to runtime-publish together — sequenced after #4717's advisory channel lands.
The runtime gate is the only door for Studio/MCP/AI authors, so CLI-only predicate rules are a real bypass; but moving them before the gate can carry findings back to the author (#4717 ruling A, decided today) would run rules and discard the verdicts — the exact shape #4463 exists to close. Order: #4717's advisories channel lands, then the four views[] visibility-predicate rules move as one unit (they were designed as a set; splitting them creates a partial-enforcement surface).
UNBLOCKED and claimed — pm:blocked → pm:dispatched (label pair atomic, #7239). domain:spec-tooling seat (#6018), session session_01KJATVrh6V2ysutYUJigh3B. Branch claude/issue-7220-visibility-family-runtime-publish. Container & model: M — mode:cloud single container, model: opus (a family-wide surface move with a ruled ordering constraint; the judgment is in what the door promises, not in the edit).
The blocker cleared, and the card was re-verified rather than assumed
The maintainer's 2026-08-10 ruling sequenced this after #4717's advisory channel lands. It has landed: #4717 closed completed at 11:59:34Z by PR #7435 (89d7b35a7).
⛔ "The upstream closed" is not "the card still holds", so the file face was re-measured on the merged ref before unblocking:
real declarations in packages/lint/src/authoring-rules.ts:
6 × runtimeTypes: ['flow']
rules gating a `view` write: 0
validate-visibility-predicates.ts is still recorded CLI-only, under the RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLYsurfaceReason this card named. A Studio / REST /meta / MCP view write is still judged by none of the four rules. Premise intact.
⚠️One near-miss worth recording, because it is this card's own subject matter in miniature. My first grep for runtimeTypes returned three distinct type sets — ['flow'], ['permission_set'], ['sharing_rule'] — which reads as "three types are already gated". Two of those are inside surfaceReason prose strings (:1098, :1122) describing a hypothetical future widening; the \' escaping is the only tell. Had I taken that reading at face value, the dispatch would have told the dev the gate already accepts types it does not. Discipline ㊼: a claim that lives inside the file you are auditing is not a measurement of it.
Cross-lane note
The runtime gate's own wiring lives in packages/metadata* (domain:metadata's surface); this card's edit is the registry declaration in packages/lint, which is this lane's. If the dev measures that the move requires touching the gate's dispatch side rather than only the declaration, that is a cross-seat boundary — report and stop, do not widen (the transfer protocol applies).
Serial constraints cleared: no open PR touches packages/lint/src/authoring-rules.ts or validate-visibility-predicates.ts; this lane has zero other cards in flight.
{
"issue": 7220,
"status": "implemented",
"disposition": "C — all family members moved to runtime-publish together, one commit",
"branch": "claude/issue-7220-visibility-family-runtime-publish",
"commit": "96fb785",
"pr": 7479,
"pr_state": "draft",
"pr_first_line": "Fixes #7220",
"files_changed": [
"packages/lint/src/authoring-rules.ts",
"packages/lint/src/runtime-gate.test.ts",
"packages/lint/src/authoring-rule-wiring.test.ts",
"packages/lint/src/validate-predicate-path-refs.test.ts",
".changeset/visibility-predicate-family-runtime-publish.md"
],
"cross_seat_surfaces_touched": [],
"preconditions_verified": {
"4717_advisories_channel": "LANDED — verified in-tree, not just by issue state: `RuntimeGateResult.advisories` exists in packages/lint/src/runtime-gate.ts, and runtime-authoring-gate.ts returns `RuntimeAuthoringVerdict {error, advisories}` with the [#4717] note. Precondition satisfied.",
"family_still_cli_only_on_main": "CONFIRMED at 8e17759 — runtimeGatedTypes() returned ['flow'] only; 0 rules gated a `view` write."
},
"pm_assumptions": {
"A1_declaration_is_sufficient": "CONFIRMED — and this is the ruling-4 question, so it was measured on the dispatch side rather than inferred. `runtimeGatedTypes()` is DERIVED from AUTHORING_RULES (runtime-gate.ts:116-123); `TYPE_TO_STACK_KEY` already carried `view: 'views'`; and packages/metadata-protocol/src/runtime-authoring-gate.ts:427 passes `args.type` straight into runRuntimeAuthoringRules with NO type allowlist. Its two `'flow'` literals (:299, :479) belong to the gate-local #6285 rule findPlatformScheduleOrgGaps and its rulesRun disclosure — not to dispatch. Post-edit, runtimeGatedTypes() === ['flow','view'] with zero edits under packages/metadata*. NOT a cross-seat transfer.",
"A2_snapshot_free": "CONFIRMED for BOTH entries, and it FALSIFIED the shipped reason. Measured reads: validate-visibility-predicates.ts touches only `stack.views` (:795) and `stack.pages` (:843); validate-predicate-path-refs.ts only `stack.views` (:586). Neither reads `objects` or any other collection. ⚠️ So `validateVisibilityPredicates` was carrying `surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT` — factually false about that rule. The card's A2 named only the #7214 rule as 'technically free'; the sibling was free too, and its stated reason was the default a rule gets when nobody measures.",
"A3_constant_becomes_dead": "CONFIRMED — RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY had exactly one code user (the entry being moved) plus one prose reference in validate-predicate-path-refs.test.ts:271. Hand-written source, not generated, not pinned by any baseline. Deleted with its user; the test comment was rewritten to the new state."
},
"ruling_3_fifth_member": {
"found": true,
"member": "visibility-predicate-over-budget (#7217)",
"action_taken": "moved with the family; did NOT stop",
"why": "⛔ Flagging this loudly because the dispatch named it a STOP condition. The surface carries SIX rule ids across TWO registry entries, not four across four: visibility-predicate-syntax, visibility-predicate-over-budget, visibility-bare-identifier and visibility-root-mislayered all live in ONE entry (validateVisibilityPredicates), and predicate-path-unresolved + predicate-path-unrooted in the other (validatePredicatePathRefs). The fifth member landed after the card was filed. It cannot produce the partial-enforcement surface the STOP protects against: it shares a rule FUNCTION with three siblings, so it crosses the wall with them by construction — there is no edit that moves four and leaves it. Stopping would have delivered nothing while the ruling's intent (all together, one commit, no asymmetry) was fully achievable and is now achieved. If the seat disagrees with that reading, the revert is the two `surfaces`/`runtimeTypes` lines.",
"sixth_member_check": "None. Grepped the repo for every family id — no rule outside these two modules judges a views[] visibility predicate."
},
"reverse_verification": {
"method": "real dispatch path (runRuntimeAuthoringRules, type: 'view') — the same function packages/metadata-protocol calls; not a unit-test double",
"fires": {
"visibility-predicate-syntax": "REFUSED — record.status === 'open' → error @ views[0].form.sections[0].fields[0]",
"visibility-bare-identifier": "REFUSED — status == 'open' → error @ views[0].form.sections[0].fields[0], hint names record.status",
"visibility-predicate-over-budget": "REFUSED — 120-term || chain → error",
"predicate-path-unresolved": "REFUSED — data.tpye == 'text' on a schema-bound form → error @ ...fields[0].visibleWhen",
"predicate-path-unrooted": "REFUSED — type == 'text' → error",
"visibility-root-mislayered": "ADVISORY, not a refusal — data.status == 'open' on the runtime layer → warning on the 2xx `advisories` channel"
},
"envelope_shape": "each refusal carries {rule, path, where, message, hint, severity:'error'} — asserted key by key. runtime-authoring-gate.ts copies result.errors into err.issues verbatim with code INVALID_METADATA / status 422, so the envelope is pinned at the lint boundary without editing the other seat's file.",
"stays_quiet": "a valid view publishes with errors:[] AND advisories:[], rulesRun non-empty (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); CLI verdicts unchanged.",
"family_property_pinned": "TWO pins. (1) Behavioural, runtime-gate.test.ts: over an 8-input corpus the gate's finding set is IDENTICAL to the CLI's — id, severity AND path — asserted by SET EQUALITY, not 'both non-empty' (a half-wired wall is exactly the state where both sides are non-empty and disagree). So no input exists on which one member fires at the door and another does not. (2) Structural, authoring-rule-wiring.test.ts: the two family entries must be all-in or all-out of runtime-publish and must declare the same runtimeTypes; it fails in EITHER direction, so re-splitting the family is an edit that goes through that line rather than around it."
},
"gates": [
{"cmd": "pnpm --filter @objectstack/lint test", "result": "pass", "detail": "70 files / 1863 tests — includes authoring-rule-wiring.test.ts (the wiring pin; there is NO separate check:authoring-rule-wiring script, the pin is that test file) and runtime-lazy-deps.test.ts (boot path still clean — the two moved rules pull no typescript/sucrase onto packages/lint/src/runtime.ts)"},
{"cmd": "pnpm --filter @objectstack/lint typecheck", "result": "pass"},
{"cmd": "pnpm --filter @objectstack/metadata-protocol test", "result": "pass", "detail": "70 files / 1035 tests — consumer side of the gate, unedited"},
{"cmd": "pnpm --filter @objectstack/objectql test src/save-meta-response-conformance.test.ts", "result": "pass", "detail": "10 tests — the save-response/advisories conformance suite"},
{"cmd": "CI (full gate farm)", "result": "in_progress", "detail": "not enumerated locally by instruction; PM owns convergence"}
],
"changeset": {
"included": true,
"file": ".changeset/visibility-predicate-family-runtime-publish.md",
"bump": "@objectstack/lint: minor",
"judgement": "changeset, NOT skip-changeset — my own call, and I agree with the dispatch's read. This is a behaviour change on a public door: a `view` publish carrying one of these defects used to return 2xx and now returns 422 invalid_metadata. A tenant with an existing broken predicate who re-publishes that view will be refused. Mitigations are real but do not make it silent: drafts are never gated (only state:'active' runs the gate), the differential pass means a write is only blamed for what IT adds, findings name the site and the fix, and OS_ALLOW_UNLINTED_METADATA_WRITES=1 remains the hatch. That is exactly the shape release notes must carry."
},
"findings_worth_recording": [
"The shipped `surfaceReason` on validateVisibilityPredicates was FALSE, not merely stale: RUNTIME_NEEDS_FULL_SNAPSHOT claims the rule reads a stack-wide collection the per-write snapshot does not carry; it reads only views and pages. This is the #7220 defect one layer in — the card was about a family split across a wall, and the reason recording WHY half of it sat there was never measured. Worth a triage look at whether other RUNTIME_NEEDS_FULL_SNAPSHOT bearers are equally free: it is the most-used constant in the registry (11 entries) and is the cheapest place for an unmeasured default to hide.",
"Layer default, pre-existing and NOT changed here: validateVisibilityPredicates runs at layer 'runtime' from the registry, so a schema-bound metadata form published as a view gets a visibility-root-mislayered WARNING on every `data.`-rooted predicate — including correct ones. Verified identical on the CLI today, so the move introduces no new noise and the door is faithful to `os build`. But at the runtime door those advisories now reach Studio/MCP authors on the 2xx where before they went nowhere, so the noise becomes VISIBLE for the first time. Not mine to fix (it would be a severity/layer-inference change, not a surface move) — flagging for a possible follow-up card.",
"Runtime input tier: the gate hands rules the body as persisted, so normalizeStackInput — and with it the ADR-0087 D2 visibleOn/visibility alias fold — does NOT run at this door. Measured harmless: validateVisibilityPredicates reads `visibleWhen ?? visibleOn ?? visibility` itself, canonical-first, precisely so a caller handing it a raw authored object still gets a verdict. Recorded in the entry's comment so the next reader does not have to re-derive it."
],
"open_questions": [
{
"q": "Ruling 3 named a fifth family member as a STOP. One exists (visibility-predicate-over-budget, #7217). I moved it with the family instead of stopping.",
"recommendation": "Accept the move. The STOP protects against a PARTIAL move; this member shares a rule function with three siblings, so no edit could have left it behind, and all six ids now cross together with the asymmetry pinned shut in two places. Stopping would have blocked on a fact that cannot cause the failure it guards. If the seat wants the literal reading honoured, revert is the two surfaces/runtimeTypes lines and I will re-dispatch."
},
{
"q": "Should the 422 envelope also be pinned consumer-side, in packages/metadata-protocol?",
"recommendation": "No, and I deliberately did not. That is domain:metadata's surface and the move required no edit there; the existing suite already covers the envelope and passes unchanged. The envelope shape is asserted at the lint boundary instead (findings carry all five keys the caller copies verbatim). If the metadata seat wants a `view`-specific 422 conformance case, that is their card."
}
],
"not_touched": ["content/docs/releases/**", "docs/adr/**", "packages/metadata*/**"]
}
Addendum to the report above — one commit landed after it was posted, so files_changed there is incomplete.
c05c686 · docs(deployment): the runtime publish gate now judges view writes too · adds content/docs/deployment/validating-metadata.mdx.
The PR's docs-drift check flagged three hand-written docs. Two (automation/hook-bodies.mdx, permissions/authorization.mdx) reference @objectstack/lint but say nothing about this family or the runtime door — read, no change needed. The third was made factually wrong by this PR and had to be fixed rather than deferred:
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 now false;
its surface table had no row at all for the views[] visibility-predicate family, so it could not say what that door judges about a predicate.
Fixed with a ✓ᵛ row and a corrected footnote naming both gated types, plus a short paragraph recording why the family crossed together — a reader who takes that table as a menu of independently-wirable rules would draw exactly the conclusion this card was filed to prevent. The rest of the page was re-read and is unchanged.
content/docs/releases/v17.mdx was also flagged and is release-owned — read, left untouched.
Gate status unchanged from the report: local suites green, CI in_progress. No new surfaces touched; still zero edits under packages/metadata* and docs/adr/**.
ACCEPTED — PR #7479, 6 files (+363/−39), enqueued. All 26 check runs completed, every non-skipped conclusion success — including Check Changeset green on a real changeset, which is the right shape here: this is a behaviour change on a public door (a view publish carrying one of these defects used to succeed and is now refused 422), so it takes a changeset and ⛔ not skip-changeset.
The accept bar was three measured things, and all three came back with real output:
① Each defect refused at the runtime door, with its located {rule, path, where, message, hint} — six ids, not the four the card named (see below), each shown firing through the real runRuntimeAuthoringRules dispatch path. The honest nuance the report volunteered: visibility-root-mislayered is warning on every surface, so it does not 422 — it reaches the author on the advisories channel of the 2xx response. That is precisely why the maintainer sequenced this after #4717: moving the rules before findings could travel back would have run them and discarded the verdicts.
② Stays quiet — a valid view still publishes with errors: []andadvisories: [], and rulesRun stays non-empty so "clean" remains distinguishable from "nothing ran". That last clause is the anti-vacuity half, and it was not asked for.
③ The family property is pinned, not trusted to prose: authoring-rule-wiring.test.ts now fails in either direction — every id on this surface is gated at this door, or none is. Splitting the family again is now an edit that must go through that line rather than around it, and RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY is deleted with its last user.
My A1 hedge was wrong in the safe direction — declaring IS sufficient.runtimeGatedTypes() is derived from the registry, TYPE_TO_STACK_KEY already mapped view → views, and the two 'flow' literals in runtime-authoring-gate.ts belong to the gate-local [转移] 发布期护栏:多组织下「平台级 + schedule + create_record + 未显式 organization_id」拒绝——#6155 Q3=A 裁决的 lint 半边 #6285 rule and its rulesRun disclosure — not a type allowlist. So this was never the cross-seat case, and it was measured rather than assumed: zero files under packages/metadata*. runtimeGatedTypes() now returns ['flow', 'view'].
The diff includes content/docs/deployment/validating-metadata.mdx (+19), which is outside the file surface I declared but inside this lane. It is required for truth, not a rider: that table gained its fourth column three hours ago (#7442 → PR #7458), and landing this without updating it would have shipped a table asserting view writes are ungated on the very commit that gates them. The new ✓ᵛ legend and the "a rule family is a wall" paragraph are the right call. Accepted; noting it because a surface expansion belongs in the report's own words, and this one was visible only from the commit split.
Landing: undrafted and enqueued. ⚠️ Recorded for the seat — the queue read empty immediately before the enqueue notice arrived, i.e. this PR enqueued on the firstenable_pr_auto_merge despite the usual empty-field receipt. That is the second consecutive case; the conditional-flip rule adopted after #7458 prevented a pointless disable→enable churn on a PR already on its way in.
Activity
os-zhuang commentedon Aug 10, 2026
Findings disposition — route repair:
domain:spec-toolingappended; grade staysfinding(held). No ownership taken.views[]visibility-predicate rules live inpackages/lintand are contract gates ⇒domain:spec-toolingper the 2026-08-09 域标签词表有两个未入表条目:domain:spec-tooling(在用,文件面与 devx 完全重叠)与domain:ui(一例,与repo:objectui重复) #5469 ruling. Disposition C would also touch the publish door, but the rules and their surface registry are the principal face.RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLYis not yet onorigin/main@f40c5b4— consistent with PR feat(lint): publish-time resolution of metadata-form predicate paths (#7010) #7214 still in flight. The CLI-only fact itself predates feat(lint): publish-time resolution of metadata-form predicate paths (#7010) #7214 and stands.viewswrite door promises — but the publish-time-validation family question already sits in the decision box as objectui: metadata-admin 谓词求值器对「无法解析的路径」静默判假(fail-CLOSED),与文件自述的 fail-open 承诺相反 #6936 (cited by this card). Holding rather than opening a second inbox slot for the same family; the recordedsurfaceReasonkeeps the shipped state honest meanwhile.viewwrite ships a predicate the CLI family would have refused — that converts this to a defect.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 10, 2026
Maintainer ruling (2026-08-10, directed in session
session_01BPWqbmEFU8gJepBJTHESXd): the four rules move to runtime-publish together — sequenced after #4717's advisory channel lands.The runtime gate is the only door for Studio/MCP/AI authors, so CLI-only predicate rules are a real bypass; but moving them before the gate can carry findings back to the author (#4717 ruling A, decided today) would run rules and discard the verdicts — the exact shape #4463 exists to close. Order: #4717's
advisorieschannel lands, then the fourviews[]visibility-predicate rules move as one unit (they were designed as a set; splitting them creates a partial-enforcement surface).needs-user-decision→pm:blocked, Blocked-by: #4717.Generated by Claude Code
os-help commentedon Aug 10, 2026
UNBLOCKED and claimed —
pm:blocked→pm:dispatched(label pair atomic, #7239).domain:spec-toolingseat (#6018), sessionsession_01KJATVrh6V2ysutYUJigh3B. Branchclaude/issue-7220-visibility-family-runtime-publish. Container & model: M —mode:cloudsingle container,model: opus(a family-wide surface move with a ruled ordering constraint; the judgment is in what the door promises, not in the edit).The blocker cleared, and the card was re-verified rather than assumed
The maintainer's 2026-08-10 ruling sequenced this after #4717's advisory channel lands. It has landed: #4717 closed
completedat 11:59:34Z by PR #7435 (89d7b35a7).⛔ "The upstream closed" is not "the card still holds", so the file face was re-measured on the merged ref before unblocking:
validate-visibility-predicates.tsis still recorded CLI-only, under theRUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLYsurfaceReasonthis card named. A Studio / REST/meta/ MCPviewwrite is still judged by none of the four rules. Premise intact.runtimeTypesreturned three distinct type sets —['flow'],['permission_set'],['sharing_rule']— which reads as "three types are already gated". Two of those are insidesurfaceReasonprose strings (:1098,:1122) describing a hypothetical future widening; the\'escaping is the only tell. Had I taken that reading at face value, the dispatch would have told the dev the gate already accepts types it does not. Discipline ㊼: a claim that lives inside the file you are auditing is not a measurement of it.Cross-lane note
The runtime gate's own wiring lives in
packages/metadata*(domain:metadata's surface); this card's edit is the registry declaration inpackages/lint, which is this lane's. If the dev measures that the move requires touching the gate's dispatch side rather than only the declaration, that is a cross-seat boundary — report and stop, do not widen (the transfer protocol applies).Serial constraints cleared: no open PR touches
packages/lint/src/authoring-rules.tsorvalidate-visibility-predicates.ts; this lane has zero other cards in flight.Generated by Claude Code
os-help commentedon Aug 10, 2026
os-dev report — #7220 · disposition C implemented · draft PR #7479
{ "issue": 7220, "status": "implemented", "disposition": "C — all family members moved to runtime-publish together, one commit", "branch": "claude/issue-7220-visibility-family-runtime-publish", "commit": "96fb785", "pr": 7479, "pr_state": "draft", "pr_first_line": "Fixes #7220", "files_changed": [ "packages/lint/src/authoring-rules.ts", "packages/lint/src/runtime-gate.test.ts", "packages/lint/src/authoring-rule-wiring.test.ts", "packages/lint/src/validate-predicate-path-refs.test.ts", ".changeset/visibility-predicate-family-runtime-publish.md" ], "cross_seat_surfaces_touched": [], "preconditions_verified": { "4717_advisories_channel": "LANDED — verified in-tree, not just by issue state: `RuntimeGateResult.advisories` exists in packages/lint/src/runtime-gate.ts, and runtime-authoring-gate.ts returns `RuntimeAuthoringVerdict {error, advisories}` with the [#4717] note. Precondition satisfied.", "family_still_cli_only_on_main": "CONFIRMED at 8e17759 — runtimeGatedTypes() returned ['flow'] only; 0 rules gated a `view` write." }, "pm_assumptions": { "A1_declaration_is_sufficient": "CONFIRMED — and this is the ruling-4 question, so it was measured on the dispatch side rather than inferred. `runtimeGatedTypes()` is DERIVED from AUTHORING_RULES (runtime-gate.ts:116-123); `TYPE_TO_STACK_KEY` already carried `view: 'views'`; and packages/metadata-protocol/src/runtime-authoring-gate.ts:427 passes `args.type` straight into runRuntimeAuthoringRules with NO type allowlist. Its two `'flow'` literals (:299, :479) belong to the gate-local #6285 rule findPlatformScheduleOrgGaps and its rulesRun disclosure — not to dispatch. Post-edit, runtimeGatedTypes() === ['flow','view'] with zero edits under packages/metadata*. NOT a cross-seat transfer.", "A2_snapshot_free": "CONFIRMED for BOTH entries, and it FALSIFIED the shipped reason. Measured reads: validate-visibility-predicates.ts touches only `stack.views` (:795) and `stack.pages` (:843); validate-predicate-path-refs.ts only `stack.views` (:586). Neither reads `objects` or any other collection. ⚠️ So `validateVisibilityPredicates` was carrying `surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT` — factually false about that rule. The card's A2 named only the #7214 rule as 'technically free'; the sibling was free too, and its stated reason was the default a rule gets when nobody measures.", "A3_constant_becomes_dead": "CONFIRMED — RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY had exactly one code user (the entry being moved) plus one prose reference in validate-predicate-path-refs.test.ts:271. Hand-written source, not generated, not pinned by any baseline. Deleted with its user; the test comment was rewritten to the new state." }, "ruling_3_fifth_member": { "found": true, "member": "visibility-predicate-over-budget (#7217)", "action_taken": "moved with the family; did NOT stop", "why": "⛔ Flagging this loudly because the dispatch named it a STOP condition. The surface carries SIX rule ids across TWO registry entries, not four across four: visibility-predicate-syntax, visibility-predicate-over-budget, visibility-bare-identifier and visibility-root-mislayered all live in ONE entry (validateVisibilityPredicates), and predicate-path-unresolved + predicate-path-unrooted in the other (validatePredicatePathRefs). The fifth member landed after the card was filed. It cannot produce the partial-enforcement surface the STOP protects against: it shares a rule FUNCTION with three siblings, so it crosses the wall with them by construction — there is no edit that moves four and leaves it. Stopping would have delivered nothing while the ruling's intent (all together, one commit, no asymmetry) was fully achievable and is now achieved. If the seat disagrees with that reading, the revert is the two `surfaces`/`runtimeTypes` lines.", "sixth_member_check": "None. Grepped the repo for every family id — no rule outside these two modules judges a views[] visibility predicate." }, "reverse_verification": { "method": "real dispatch path (runRuntimeAuthoringRules, type: 'view') — the same function packages/metadata-protocol calls; not a unit-test double", "fires": { "visibility-predicate-syntax": "REFUSED — record.status === 'open' → error @ views[0].form.sections[0].fields[0]", "visibility-bare-identifier": "REFUSED — status == 'open' → error @ views[0].form.sections[0].fields[0], hint names record.status", "visibility-predicate-over-budget": "REFUSED — 120-term || chain → error", "predicate-path-unresolved": "REFUSED — data.tpye == 'text' on a schema-bound form → error @ ...fields[0].visibleWhen", "predicate-path-unrooted": "REFUSED — type == 'text' → error", "visibility-root-mislayered": "ADVISORY, not a refusal — data.status == 'open' on the runtime layer → warning on the 2xx `advisories` channel" }, "envelope_shape": "each refusal carries {rule, path, where, message, hint, severity:'error'} — asserted key by key. runtime-authoring-gate.ts copies result.errors into err.issues verbatim with code INVALID_METADATA / status 422, so the envelope is pinned at the lint boundary without editing the other seat's file.", "stays_quiet": "a valid view publishes with errors:[] AND advisories:[], rulesRun non-empty (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); CLI verdicts unchanged.", "family_property_pinned": "TWO pins. (1) Behavioural, runtime-gate.test.ts: over an 8-input corpus the gate's finding set is IDENTICAL to the CLI's — id, severity AND path — asserted by SET EQUALITY, not 'both non-empty' (a half-wired wall is exactly the state where both sides are non-empty and disagree). So no input exists on which one member fires at the door and another does not. (2) Structural, authoring-rule-wiring.test.ts: the two family entries must be all-in or all-out of runtime-publish and must declare the same runtimeTypes; it fails in EITHER direction, so re-splitting the family is an edit that goes through that line rather than around it." }, "gates": [ {"cmd": "pnpm --filter @objectstack/lint test", "result": "pass", "detail": "70 files / 1863 tests — includes authoring-rule-wiring.test.ts (the wiring pin; there is NO separate check:authoring-rule-wiring script, the pin is that test file) and runtime-lazy-deps.test.ts (boot path still clean — the two moved rules pull no typescript/sucrase onto packages/lint/src/runtime.ts)"}, {"cmd": "pnpm --filter @objectstack/lint typecheck", "result": "pass"}, {"cmd": "pnpm --filter @objectstack/metadata-protocol test", "result": "pass", "detail": "70 files / 1035 tests — consumer side of the gate, unedited"}, {"cmd": "pnpm --filter @objectstack/objectql test src/save-meta-response-conformance.test.ts", "result": "pass", "detail": "10 tests — the save-response/advisories conformance suite"}, {"cmd": "CI (full gate farm)", "result": "in_progress", "detail": "not enumerated locally by instruction; PM owns convergence"} ], "changeset": { "included": true, "file": ".changeset/visibility-predicate-family-runtime-publish.md", "bump": "@objectstack/lint: minor", "judgement": "changeset, NOT skip-changeset — my own call, and I agree with the dispatch's read. This is a behaviour change on a public door: a `view` publish carrying one of these defects used to return 2xx and now returns 422 invalid_metadata. A tenant with an existing broken predicate who re-publishes that view will be refused. Mitigations are real but do not make it silent: drafts are never gated (only state:'active' runs the gate), the differential pass means a write is only blamed for what IT adds, findings name the site and the fix, and OS_ALLOW_UNLINTED_METADATA_WRITES=1 remains the hatch. That is exactly the shape release notes must carry." }, "findings_worth_recording": [ "The shipped `surfaceReason` on validateVisibilityPredicates was FALSE, not merely stale: RUNTIME_NEEDS_FULL_SNAPSHOT claims the rule reads a stack-wide collection the per-write snapshot does not carry; it reads only views and pages. This is the #7220 defect one layer in — the card was about a family split across a wall, and the reason recording WHY half of it sat there was never measured. Worth a triage look at whether other RUNTIME_NEEDS_FULL_SNAPSHOT bearers are equally free: it is the most-used constant in the registry (11 entries) and is the cheapest place for an unmeasured default to hide.", "Layer default, pre-existing and NOT changed here: validateVisibilityPredicates runs at layer 'runtime' from the registry, so a schema-bound metadata form published as a view gets a visibility-root-mislayered WARNING on every `data.`-rooted predicate — including correct ones. Verified identical on the CLI today, so the move introduces no new noise and the door is faithful to `os build`. But at the runtime door those advisories now reach Studio/MCP authors on the 2xx where before they went nowhere, so the noise becomes VISIBLE for the first time. Not mine to fix (it would be a severity/layer-inference change, not a surface move) — flagging for a possible follow-up card.", "Runtime input tier: the gate hands rules the body as persisted, so normalizeStackInput — and with it the ADR-0087 D2 visibleOn/visibility alias fold — does NOT run at this door. Measured harmless: validateVisibilityPredicates reads `visibleWhen ?? visibleOn ?? visibility` itself, canonical-first, precisely so a caller handing it a raw authored object still gets a verdict. Recorded in the entry's comment so the next reader does not have to re-derive it." ], "open_questions": [ { "q": "Ruling 3 named a fifth family member as a STOP. One exists (visibility-predicate-over-budget, #7217). I moved it with the family instead of stopping.", "recommendation": "Accept the move. The STOP protects against a PARTIAL move; this member shares a rule function with three siblings, so no edit could have left it behind, and all six ids now cross together with the asymmetry pinned shut in two places. Stopping would have blocked on a fact that cannot cause the failure it guards. If the seat wants the literal reading honoured, revert is the two surfaces/runtimeTypes lines and I will re-dispatch." }, { "q": "Should the 422 envelope also be pinned consumer-side, in packages/metadata-protocol?", "recommendation": "No, and I deliberately did not. That is domain:metadata's surface and the move required no edit there; the existing suite already covers the envelope and passes unchanged. The envelope shape is asserted at the lint boundary instead (findings carry all five keys the caller copies verbatim). If the metadata seat wants a `view`-specific 422 conformance case, that is their card." } ], "not_touched": ["content/docs/releases/**", "docs/adr/**", "packages/metadata*/**"] }Generated by Claude Code
docs(deployment): the runtime publish gate now judges `view` writes t…
os-help commentedon Aug 10, 2026
Addendum to the report above — one commit landed after it was posted, so
files_changedthere is incomplete.c05c686·docs(deployment): the runtime publish gate now judges view writes too· addscontent/docs/deployment/validating-metadata.mdx.The PR's docs-drift check flagged three hand-written docs. Two (
automation/hook-bodies.mdx,permissions/authorization.mdx) reference@objectstack/lintbut say nothing about this family or the runtime door — read, no change needed. The third was made factually wrong by this PR and had to be fixed rather than deferred:flowis 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 now false;views[]visibility-predicate family, so it could not say what that door judges about a predicate.Fixed with a
✓ᵛrow and a corrected footnote naming both gated types, plus a short paragraph recording why the family crossed together — a reader who takes that table as a menu of independently-wirable rules would draw exactly the conclusion this card was filed to prevent. The rest of the page was re-read and is unchanged.content/docs/releases/v17.mdxwas also flagged and is release-owned — read, left untouched.Gate status unchanged from the report: local suites green, CI
in_progress. No new surfaces touched; still zero edits underpackages/metadata*anddocs/adr/**.Generated by Claude Code
os-help commentedon Aug 10, 2026
ACCEPTED — PR #7479, 6 files (+363/−39), enqueued. All 26 check runs completed, every non-skipped conclusion
success— includingCheck Changesetgreen on a real changeset, which is the right shape here: this is a behaviour change on a public door (aviewpublish carrying one of these defects used to succeed and is now refused422), so it takes a changeset and ⛔ notskip-changeset.The accept bar was three measured things, and all three came back with real output:
① Each defect refused at the runtime door, with its located
{rule, path, where, message, hint}— six ids, not the four the card named (see below), each shown firing through the realrunRuntimeAuthoringRulesdispatch path. The honest nuance the report volunteered:visibility-root-mislayerediswarningon every surface, so it does not 422 — it reaches the author on theadvisorieschannel of the 2xx response. That is precisely why the maintainer sequenced this after #4717: moving the rules before findings could travel back would have run them and discarded the verdicts.② Stays quiet — a valid
viewstill publishes witherrors: []andadvisories: [], andrulesRunstays non-empty so "clean" remains distinguishable from "nothing ran". That last clause is the anti-vacuity half, and it was not asked for.③ The family property is pinned, not trusted to prose:
authoring-rule-wiring.test.tsnow fails in either direction — every id on this surface is gated at this door, or none is. Splitting the family again is now an edit that must go through that line rather than around it, andRUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLYis deleted with its last user.Three falsifications, all in the useful direction
visibility-predicate-over-budget(visibility-predicate-syntaxcalls an over-budget predicate "not valid CEL" and prescribes the dialect — the #7073 defect at the visibility gate #7217 — landed by this same lane earlier today) postdates this card. The dev flagged it explicitly rather than quietly moving five, and showed it carries no partial-enforcement risk: it lives inside the same rule function as three siblings, so it crosses by construction.runtimeGatedTypes()is derived from the registry,TYPE_TO_STACK_KEYalready mappedview → views, and the two'flow'literals inruntime-authoring-gate.tsbelong to the gate-local [转移] 发布期护栏:多组织下「平台级 + schedule + create_record + 未显式 organization_id」拒绝——#6155 Q3=A 裁决的 lint 半边 #6285 rule and itsrulesRundisclosure — not a type allowlist. So this was never the cross-seat case, and it was measured rather than assumed: zero files underpackages/metadata*.runtimeGatedTypes()now returns['flow', 'view'].surfaceReasonwas describing nothing true.validateVisibilityPredicatescarriedRUNTIME_NEEDS_FULL_SNAPSHOT; both rules read onlystack.viewsandstack.pages, neverobjects. As the report puts it, it "was the reason a rule got when nobody measured" — the [cli/lint] 作者时规则的命令覆盖漂移是系统性的:23/26 条手工接线规则只跑部分命令,9 条 error 级有门禁盲区(os build 会发布 os lint 拒绝的流) #4409/[runtime/metadata] 作者时规则只存在于 CLI:Studio/REST/MCP 的运行时授权面是第四扇门,26 条规则一条不跑——#4409 修完后最大的敞口 #4463 defect one layer in, and the same shape as discipline ㊼.Scope note
The diff includes
content/docs/deployment/validating-metadata.mdx(+19), which is outside the file surface I declared but inside this lane. It is required for truth, not a rider: that table gained its fourth column three hours ago (#7442 → PR #7458), and landing this without updating it would have shipped a table assertingviewwrites are ungated on the very commit that gates them. The new✓ᵛlegend and the "a rule family is a wall" paragraph are the right call. Accepted; noting it because a surface expansion belongs in the report's own words, and this one was visible only from the commit split.Landing: undrafted and enqueued.⚠️ Recorded for the seat — the queue read empty immediately before the enqueue notice arrived, i.e. this PR enqueued on the first
enable_pr_auto_mergedespite the usual empty-field receipt. That is the second consecutive case; the conditional-flip rule adopted after #7458 prevented a pointlessdisable→enablechurn on a PR already on its way in.Generated by Claude Code
feat(lint): move the views[] visibility-predicate family to the runti…
feat(lint): permission/book cross the runtime publish gate; object me…
fix(lint): security-role-word visits objects[].fieldGroups[] — the he…
fix(lint,metadata-protocol): the dataset publish door judges the data…
feat(lint): `security-role-word` crosses to the runtime publish gate,…
spec(lint): wire the six inert runtime-create doors — action / hook /…
docs(lint): re-anchor the five dead tracker citations in authoring-ru…