…metadata type
The generic `ConditionWidget` mount is polymorphic over every metadata type, so it
took the default subject vocabulary — `record.id` plus `user.*` — for all of them.
`CONDITION_SCOPE_BY_METADATA_TYPE` cannot separate them: it rules `action`, `hook`
and `validation` all `record`, which is a claim about how a predicate is LINTED and
not about what its host BINDS. A hook's condition and a validation rule's guard are
evaluated on the server against `record` and `previous` alone; an action's `visible`
is evaluated in the browser, where `user` is bound.
Measured: `hook` has a registered default inspector but no registered preview, so
`ResourceEditPage` renders its plain whole-draft form and the curated
`HookDefaultInspector` — which hides `condition` from its own fallback — never runs.
So a server-evaluated condition does reach the generic mount today.
`conditionSubjectsForMetadataType`, the sibling of `conditionScopeForMetadataType`,
derives the vocabulary from the type on screen over a table of which host evaluates
each type's condition. Server-evaluated types get the list the two curated
inspectors already declare; the client-evaluated tier keeps the default, because
narrowing it would take a working subject away. A type with no measured host
declares nothing and is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
Fixes #9953
What this changes
CONDITION_SCOPE_BY_METADATA_TYPErulesaction,hookandvalidationall'record', and eachverdict is right — every one of those evaluators binds the row as the
recordroot. But that tableanswers how a predicate is linted, not what its host binds, and on that second question the
three disagree. The generic
ConditionWidgetmount is polymorphic over all of them, so it took thedefault subject vocabulary —
record.idplususer.id/user.email/user.role/user.isAdmin— for every type.conditionSubjectsForMetadataType, the sibling ofconditionScopeForMetadataType, now derives thatvocabulary from the type on screen, over a table of which host evaluates each type's condition.
Server-evaluated types get the narrowed list the two curated inspectors already declare
(
RECORD_CONDITION_SUBJECTS); the client-evaluated tier keeps the default; a type with no measuredhost declares nothing and is byte-for-byte unchanged. ⛔ Not a third copy of the one-line declaration
objectui#9949 gave the curated mounts.
⭐ Reachability — the card's first deliverable, measured
The card and its triage both left open whether a server-evaluated condition reaches this generic
mount today. It does.
hookhas a registered default inspector (HookDefaultInspector, whichhides
conditionfrom its own fallback form) but no registered preview — and inResourceEditPagethe inspector panel that would host it exists only inside thePreviewComponent ?branch. With no preview the page takes its plain branch: the whole-draftSchemaFormcarrying the derivedWidgetContext, whereconditionroutes toConditionWidgetbyname convention with
conditionScope: 'record'and the defaultuser.*dropdown.The measurement is not recorded in prose — the first case in the new file mounts the real host and
re-takes it on every run, so it reddens if that route ever changes.
Binding sets, re-derived at source (read-only) in
objectstackhookrecord,previouswrapDeclarativeHook's pre-compiled condition:ExpressionEngine.evaluate(expr, { record: record ?? {}, previous }), throwing when the result is notokvalidationrecord,previouscheckPredicate(a rule'scondition) andcheckConditional(itswhen), both evaluating against{ record, previous }and returningunevaluableRuleErrorotherwise — the log line's own words are "rejected, not skipped"actionuserbuildExpressionScope(ExpressionProvider) returning a bag carryinguser,current_user,ctx,osandfeatures; the row arrives viausePredicateRecordContext⇒
user.*is a working subject on the client tier and an unbound one on the server tiers. That iswhy this is a per-type derivation and not one declaration.
Tests
New:
ConditionWidget.conditionSubjects.test.tsx— the reachability case at the real host, thenarrowing gate (derived from
RECORD_CONDITION_ROOTS, never from a copy of the narrowed list), themust-not-break client-tier case, and three pins on the derivation including one that requires a
measured host for every type the scope table rules
'record'.Control reading (ablation, on the committed fix, restore proven)
Deleting the single forwarding line in
ConditionWidgetand re-running:Two readings in that one run: the new gate reddens against today's behaviour and names the exact
subjects an author is offered at a hook mount — and
ConditionWidget.conditionScope.test.tsxstaysgreen through the ablation, which is the reading that says the existing pins were blind to this
door.
No build was needed for the ablation: every import under test is an intra-package relative specifier
resolved from source by vitest, so nothing here reads a package
exportsentry or adist/artifact.Acceptance notes
objectstackwas changed. That repository was read only, to derive the binding setsabove.
EmbeddedItemEditorrenders itsSchemaFormwith no
widgetContextat all, and the__object_validationanchor'seditAs: 'validation'is what opens it — so an object's embedded validation rule's
conditionis authored with no lintscope (falling through to
celAuthoring'shint.scope ?? 'flattened'default, which lints theretired bare shorthand clean) and with the full
user.*dropdown, against a server rule validatorthat binds
record/previousand is fail-closed.conditionScope.tsnames that repair as thistable's successor in its own words. It is a separate card: giving that editor a
WidgetContextmoves every embedded child —
fieldandindexas well asvalidation— and neither of thosetiers has a reading behind it.
flattened-scoped generic mount still receivesConditionBuilder's defaultfieldPrefixof'record.', so its row builder compilesrecord.FIELDsubjects at a tier whosescope claims bare field names. ⛔ Not measured whether any such type reaches the generic form with
a condition-named field today, so it is not filed as a defect. Carrier: the next change to the
flattened tiers' generic mount.
validationrow of the scope table still has no legitimate live host throughResourceEditPage— but if one ever arrives it now lands on the derivation rather than on thedefault vocabulary.
Generated by Claude Code