Found while adding an object-bound twin of an object-less action in the duly app (objectstack-ai/duly#27), where "just declare it twice under one name" was the first shape considered. Measured before it was rejected.
@objectstack/spec 17.2.0.
What was measured
Two actions in one stack's actions collection may share a name. defineStack accepts both, emits nothing, and both survive into stack.actions:
// one global + one object-bound, both named `duly_dup_probe`
stack.actions = ["global:duly_dup_probe", "duly_catalog_item:duly_dup_probe"]
object.actions = ["duly_dup_probe/BOUND"] // the bound one merged in
// two GLOBALS, both named `duly_dup2`
outcome = accepted
stack.actions = ["duly_dup2/FIRST", "duly_dup2/SECOND"]
The cross-reference walk is awake for the adjacent case — a nav runAction naming an action that does not exist throws defineStack cross-reference validation failed (1 issue). It says nothing about a runAction whose name resolves to two actions.
Why the two-globals case is the sharp one
Handler dispatch is an exact-string Map lookup on <object>:<name> (executeAction), and registration is keyed the same way. Two globals sharing a name therefore collapse to one key: whichever handler registers second wins, and the other action is a live, declared, permission-gated button whose handler is unreachable. Nothing at author time, build time or boot says so — the declaration is legal, the registration succeeds, and the loser fails only when a user clicks it.
The global + object-bound pair is less lethal (the two occupy distinct keys, and resolveRouteActionDeclaration prefers the object's embedded array) but still ambiguous for every by-name consumer: nav runAction, validate-action-name-refs, and ql.registry.getItem('action', name), which is keyed by bare name and can only return one of them.
Shape
This is the ADR-0078 / ADR-0049 family arriving at the collection level rather than at a key: the metadata parses, renders and reads as wired, and one of the two declarations quietly does nothing. It is also exactly the shape an AI-authored app produces — copying a declaration to place it somewhere new and forgetting to rename it is the natural edit, and no gate catches it.
Suggested direction
Reject a duplicate <objectName ?? 'global'>:<name> within one stack's action set at defineStack time, with the offending pair named. If the global + object-bound pair is judged legitimate (one action, two placements), reject only the same-scope collision and leave that one alone — but then the by-name consumers above need a documented precedence rather than an implicit one.
Worked around in the application by giving the twin a distinct name (duly_catalog_apply_to_people), which is what we would want authors to do anyway — but that was a measurement result, not something the platform told us.
Filing unassigned for triage.
Generated by Claude Code
Found while adding an object-bound twin of an object-less action in the
dulyapp (objectstack-ai/duly#27), where "just declare it twice under one name" was the first shape considered. Measured before it was rejected.@objectstack/spec17.2.0.What was measured
Two actions in one stack's
actionscollection may share aname.defineStackaccepts both, emits nothing, and both survive intostack.actions:The cross-reference walk is awake for the adjacent case — a nav
runActionnaming an action that does not exist throwsdefineStack cross-reference validation failed (1 issue). It says nothing about arunActionwhose name resolves to two actions.Why the two-globals case is the sharp one
Handler dispatch is an exact-string
Maplookup on<object>:<name>(executeAction), and registration is keyed the same way. Two globals sharing a name therefore collapse to one key: whichever handler registers second wins, and the other action is a live, declared, permission-gated button whose handler is unreachable. Nothing at author time, build time or boot says so — the declaration is legal, the registration succeeds, and the loser fails only when a user clicks it.The global + object-bound pair is less lethal (the two occupy distinct keys, and
resolveRouteActionDeclarationprefers the object's embedded array) but still ambiguous for every by-name consumer: navrunAction,validate-action-name-refs, andql.registry.getItem('action', name), which is keyed by bare name and can only return one of them.Shape
This is the ADR-0078 / ADR-0049 family arriving at the collection level rather than at a key: the metadata parses, renders and reads as wired, and one of the two declarations quietly does nothing. It is also exactly the shape an AI-authored app produces — copying a declaration to place it somewhere new and forgetting to rename it is the natural edit, and no gate catches it.
Suggested direction
Reject a duplicate
<objectName ?? 'global'>:<name>within one stack's action set atdefineStacktime, with the offending pair named. If the global + object-bound pair is judged legitimate (one action, two placements), reject only the same-scope collision and leave that one alone — but then the by-name consumers above need a documented precedence rather than an implicit one.Worked around in the application by giving the twin a distinct name (
duly_catalog_apply_to_people), which is what we would want authors to do anyway — but that was a measurement result, not something the platform told us.Filing unassigned for triage.
Generated by Claude Code