Instance verb families (schema 3)
Status: accepted, 2026-10-13.
Context
Some Bus verbs are not one name. A bridge serves one service per instance it
runs, so a verb such as <instance>.status exists once for each running
instance, and the instances are known only at runtime. The verb registry
(docs/docs/spec/bus/verbs.conf.mix, checked by tools/verb_registry.mix) listed
only concrete names, so there was no place to record these verbs or to check
that a name could not be taken by an instance.
Decision
-
Schema 3 is additive. The
verbslist is unchanged from schema 2, and its entries stay concrete in this migration. A newfamilieslist follows it. A family does not stand in for a concrete verb, in the registry or in code. This decision does not settle whether a concrete verb may later be expressed as a family; that would take its own decision. -
Schema 2 takes no families. A schema 2 file with a
familieskey is refused, empty or not, so an unvalidated list cannot pass as legacy data. Schema 1 stays refused. Schema 3 requires afamilieslist, even an empty one. -
Family fields are exactly
pattern,owner,members,version,status,class, in that order, one entry per line, writtenpattern:"…"(nevername:"…"). The fields are validated like the verb fields: release versions, the statuses and classes below, and component names for the owner. -
Pattern. It is
<app>.<suffix>, where the literal<app>stands for the instance name and the suffix is one or more lower identifier segments (call,ui.tree). Catch-all patterns are refused, and each pattern is unique across all owners. -
Members.
membersis either"instances", meaning the members are runtime instances and are not listed, or a nonempty list of unique Bus service names.- Families of one owner may share a listed member, so one service can
implement several families (for example
<app>.calland<app>.status). - A listed member used by families of two owners is refused.
- A listed member may equal the first segment of concrete verbs only when
every such verb has the family's owner, and the expanded wire name
(
member.suffix) is not itself a concrete verb. An exact concrete duplicate is always refused. - Any other collision is refused, not resolved.
- Families of one owner may share a listed member, so one service can
implement several families (for example
-
Class semantics are those of
docs/docs/decisions/2026-10-11-verb-classes.md. A family records its class as written; the gate checks that it is one of the four classes and does not infer it from the handler. Thebridgedfamilies are classed:<app>.calland<app>.executeareintent.<app>.commands,<app>.inspectand<app>.statusarequery.<app>.screenshotisintent. A capture alone would be a query (a query may write the artefact it returns), but itsfocusoption can bring the window forward, which another caller sees. A family has one class, so the verb is intent whether or notfocusis set.
-
Family ownership. A family with
"instances"belongs to its owner component only. The source check accepts anon @.<suffix>declaration only from the component whose"instances"family has that suffix. Every such family must be declared at least once in its owner's runtime code. Another component declaring the same suffix is refused as a wrong owner. -
Declarations come from the native parser. The source check reads the handler metadata of the native
mix lint --json(lint schema 2). There is no fallback reader.
Checks and their limits
- Source check (
--source ROOT). The gate reads the.mixfiles underapps/,services/,cli/andlibs/. It skipstests/,test/,benches/,fuzz/, and*.conf.mix/*.spec.mixfiles. The declarations come from the native parser: each file is passed tomix lint --json FILE, and every handler whose command starts with@.is a declaration. The handler metadata is the parsed AST, so nested and duplicate declarations count, and text inside quotes, heredocs and comments does not. The source is never executed.- Feature detection. The lint document is schema 2, with a
handlerslist. A Mix that answers without it fails withnative Mix lacks parsed handler metadata; rebuild current Mix. There is no fallback parser. - Fail-closed. A file that does not lint (nonzero exit), output that is
truncated, a document that is not valid JSON, a wrong
tool,filesorsummary, or a handler with a badcommand,fileorlineeach make the check fail. An unreadable source root or nested source directory fails the check too. Diagnostics and source text are never printed. - Walk. The gate lists each directory with the native
lsand reads each entry withstat, recursing by hand. The nativeglobandwalkskip an unreadable directory without an error, so they are not used to prove an absence. Test, bench and fuzz directories are skipped before the recursion. The source root and each kind directory must be a plain directory (a symlink is refused). Trailing slashes are dropped before that check, so a symlink named with one is refused too. Only the last component of each such path is checked without following links; a symlink in a directory above it is followed, as any path is. A symlink anywhere else in a kind's tree is refused, and so is a directory that cannot be listed. - The check is structural. It proves that a handler for the suffix sits in the owner's code, not that it registers at runtime. The owner's Bus suite tests the runtime registration.
- Feature detection. The lint document is schema 2, with a
- Instance check (
--instances DIR). Each*.conf.mixfile stem in DIR is checked against the Bus name rule, the reserved names (themixos-prefix,nodedandnoded-*, the broker's session-name shape), the first segments of concrete verbs and the listed family members. A name that is a concrete prefix or a listed member is refused whatever its owner. DIR must be a plain directory (a trailing slash does not get a symlink past this), and an unreadable DIR fails the check. Any path may hold glob characters, sincelstakes a name, not a pattern. Configs are flat by design: the gate lists the top level only, dot files included. A*.conf.mixentry in a subdirectory, and any other file, is ignored; none of them is a runtime config, and the gate neither recurses into them nor reads their contents. Symlinks, directories and other non-regular files named*.conf.mixare refused, and so are configs when the registry has no instances family. Config contents and tokens are never read. - Offline. Neither check touches the network or a running broker. A name that passes here is a name the Bus will accept, as far as the Bus's own rules go. A runtime refusal (for example, a broker that has already registered the name) is still possible and is not this gate's concern.
- Mesh exposure is unchanged. Nothing in the gate or the registry changes what any mesh peer can reach. The families describe names; a service's own configuration decides what it serves.
Consequences
- Schema 2 files stay valid as long as they carry no families.
- The source check needs a Mix whose
mix lint --jsonreports handler metadata (lint schema 2). An older Mix fails the source check until it is rebuilt; the gate does not fall back to another reader. - A new instance family needs a family entry, and an
on @.<suffix>handler in its owner's code, in the same change. - Public examples use only the neutral fixture names
fakeapp,demodandotherd. No runtime instance name belongs in this repository's docs.