ADR-0039: Namespaced event types¶
Status: Accepted Date: 2026-09-29 Deciders: Alex Nodeland
Context¶
An event type's name is its identity everywhere: on the wire as event.type, in stored envelopes and their event_type column, in rules' on filters, and in every type filter. Names are flat, and one process-wide registry holds them (_registry in core/events.py). Defining a second class under a name that is taken raises TypeError at import. Nothing checks a name's form.
Flat names have cost us once, and would cost us again (#45):
- An application and the test suite. oncall couldn't use
heartbeatordeploy.finished, because reflexr's test fixtures register them in the same pytest process (ADR-0031). - An application and a plugin, or two applications, can't use the same names in one process.
- A bridge and reflexr. stackr RFC-0002 adds relayr, which publishes artifactr's events into reflexr. artifactr's
feedback_givenandrun_startedare also names of reflexr's own facts. The RFC's decision D6 settles this issue before relayr's phase 1, so the bridged names are right the first time.
#72, which reserves a type to one publisher, may build on the same namespaces.
Where a name is used today:
| Where | What it does with the name |
|---|---|
An Event subclass (name=, or the snake-cased class name) |
Registers it, process-wide; event_types() and get_event_type() read the registry |
load_event and AnyEvent, for every frame, body and stored envelope |
Look the name up; an unregistered one becomes UnknownEvent, which round-trips |
Workspaces(events=, emitted=) |
Names the types clients may publish, and those only runs may. reflexr's facts are always forbidden |
on(...), the builder's field checks, Rule.check |
OnFilter.types holds names, checked against events= (or the registry) and reflexr's facts |
matches in core/evaluation.py |
envelope.event_type in types: string equality |
Rule.definition() |
Hashes the condition, names included. A new hash resets the rule |
publish, hello.types, REST's type=, MCP's publish_event and read_events, the agent's tools |
Carry names as strings |
| SQL storage | The envelope's JSON, and the event_type column, indexed on (tenant_id, workspace_id, event_type, seq) (migration 0003) |
| The JSON Schemas | OnFilter.types, Hello.types and event.type are plain strings, with no pattern |
| MCP | _RUN_FACTS picks run facts by their run_ prefix |
| Telemetry | reflexr.publish {type} spans, and reflexr.event.type on reflexr.events.published, which the workspace dashboard groups by |
reflexr's own facts have thirteen bare names: rule_fired, rule_errored, rule_reset, the eight run_* facts, feedback_given and tick. Feedback types have a registry of the same shape (core/feedback.py).
artifactr names events differently. Its own events are a closed union that only artifactr defines (artifactr ADR-0005). Applications' facts share one open type, app_event, told apart by a free-form name that nothing registers. pydantic-ai, whose CustomEvent naming reflexr follows, does namespace its capability events: class CheckpointStartEvent(CapabilityEvent, namespace="checkpoint"). Subclasses of an abstract=True base inherit its namespace, and a kind is the namespace and the name joined with a dot.
Decision¶
The maintainer decided the eight questions on 2026-09-29, as recorded on #45. Two differ from the draft's recommendations: namespaces live both on the wire and in a registry per Workspaces (1a), and every name is qualified, applications' included (2b). A simplicity review then reshaped the registry into a view over the one process-wide type table, which the maintainer chose (1a).
| # | Question | Decided |
|---|---|---|
| 1a | Where namespaces live | Both: qualified names on the wire, and a registry per Workspaces, as a view over the one type table |
| 1b | How an owner declares one | namespace= on an abstract base, declared once per process |
| 2a | Separator and grammar | :, exactly once; lowercase snake_case segments |
| 2b | Unqualified names | None: every name is qualified, applications' included |
| 2c | reflexr's facts | reflexr:rule_fired and so on, with migration 0004 |
| 3 | Compatibility | A clean break, no aliases, still reflexr.v1 |
| 4 | Reserved publishers (#72) | A policy on Workspaces, per namespace, with per-type exceptions |
| 5 | relayr's names | artifactr:message_posted and so on, and artifactr:turn_ended for run_ended |
Every name is qualified¶
A name is a namespace, a : and a local name, everywhere a name appears: oncall:alert.fired, reflexr:rule_fired, artifactr:message_posted. There are no bare names.
- A namespace is lowercase letters, digits and underscores, starting with a letter, like a Python package name.
- A local name is one or more such segments joined by dots, as names are written today.
- The pattern is
^[a-z][a-z0-9_]*:[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$.*and uppercase are outside it, so a namespace wildcard (artifactr:*) could come later without clashing with a name. - It is written once, as an
EventNameannotated type in core: the pattern, a validator that adds a hint, andWithJsonSchema, so the JSON Schemas carry the pattern. - Storage keeps the qualified name in the one
event_typecolumn. Stored envelopes aren't checked again as they are read.
An owner declares its namespace once, on an abstract base¶
Applications declare theirs the same way as libraries, plugins and bridges:
class OncallEvent(Event, abstract=True, namespace="oncall"):
"""Every oncall event."""
class AlertFired(OncallEvent, name="alert.fired"): # oncall:alert.fired
service: str
- Subclasses inherit the namespace.
name=is the local part, or the snake-cased class name as before, and a:in it is refused. - A concrete type with no namespace fails at definition.
- A namespace is unique per process, like a Python package name, and belongs to the base that declared it. Declaring it from another base fails at import, naming both. Importing the same base again is fine: its module and qualified name are compared, as
_registercompares types today. - reflexr declares
reflexrfor its facts, relayrartifactr, oncalloncall, and reflexr's testsapp.
Where names are checked, and the hint¶
Old names fail loudly, and point at the new one. Four checks cover every way in:
| Check | Covers | A bare name gets |
|---|---|---|
| Defining a type | Every class | TypeError: AlertFired has no namespace; subclass a base declared with namespace=..., such as class OncallEvent(Event, abstract=True, namespace="oncall") |
EventName, on OnFilter.types and Hello.types |
Rules, built or loaded from JSON, and hello |
Invalid: event type 'run_dead_lettered' has no namespace; did you mean 'reflexr:run_dead_lettered'? A bad hello closes the stream with 4400, as today |
Workspace.read |
REST's type=, MCP's types and the agent's tools |
validation_failed, with the same message |
_check_publishable |
Every publish, over any surface | validation_failed, with the same message |
The hint lists each type in the process with that local name, and otherwise shows the form a name takes. It searches the one type table, so it works whatever registry a Workspaces uses.
A registry per Workspaces: a view over the one type table¶
A registry's job is isolation: which types a Workspaces accepts. It is a view, not a second table. There is still one process-wide table of types, as today's _registry, so every name parses one way, with a table of namespaces beside it.
ONCALL = EventRegistry()
class OncallEvent(Event, abstract=True, namespace="oncall", registry=ONCALL):
"""Every oncall event."""
workspaces = Workspaces(storage, registry=ONCALL, events=EVENTS)
- An
EventRegistryis a set of namespaces.- A base's
registry=puts its namespace in that set. DEFAULT_REGISTRYholds every namespace whose base names no registry.registry.add(*bases)includes a library's namespace, by its base, not type by type.reflexris in every registry.
- A base's
- It is a read-only
Mapping[str, type[Event]]: the types in its namespaces, by name. SoRule.check(events=registry)works as it is, andevent_types()andget_event_type()fold into it:DEFAULT_REGISTRYis the mapping they returned, and a lookup isregistry[name]. Workspaces(registry=),DEFAULT_REGISTRYif omitted:events=Nonemeans the registry's typesevents=andemitted=must be in itRule.checkchecks against it- a publish of any other type is
not_found
- Nothing else changes. Storage, the surfaces, the builder, the reactor and the executor parse through the one type table, as today.
- What's given up: the same namespace defined as two sets of types in one process. No known need requires it. Two applications, or a test suite and an example, each have a namespace of their own, and each
Workspacesaccepts only its registry's. - It is additive. Without
registry=, every namespace is inDEFAULT_REGISTRYand everyWorkspacesuses it: the qualified-name design alone.
reflexr's facts, compatibility, reserved publishers and relayr¶
- reflexr's facts are
reflexr:*. Thereflexrnamespace is never published, which replaces the check onSYSTEM_EVENTS. - A clean break. There are no aliases, and the protocol stays
reflexr.v1, since nothing is released. Migration 0004 rewrites the thirteen facts' names inevent_typeand in the envelope'sevent.type, in Python batches to stay dialect-neutral (ADR-0030); its downgrade reverses it. An application's stored types are its to rename, in its own migration. Every rule gets a new name (RFC-0003), so every rule starts again as itsstartsays, at the head by default, losing its open windows (ADR-0026), and the executor cancels the unfinished runs of the old names, as it does for any rule no longer registered (ADR-0041). - Reserved publishers (#72) will be a policy on
Workspaces, besideevents=andemitted=, keyed by namespace, with per-type exceptions. A library exports its entry for the application to pass. #72 designs the values, and may foldemitted=into it. - relayr bridges artifactr's events as
artifactr:<type>, andrun_endedasartifactr:turn_ended. An application's projections ofapp_events and artifact kinds are its own types, in its own namespace.
Amendment (2026-09-29): what the implementation found¶
- The keyword is
event_namespace=. Pydantic's model metaclass takes the class body as a parameter namednamespace, so a class keyword of that name never reaches the class.event_namespace=matches theevent_typeandevent_namespaceclass attributes, and keeps event namespaces apart from rule namespaces:class OncallEvent(Event, abstract=True, event_namespace="oncall"). - The migration is 0005, not 0004, which is ADR-0040's. It pages by primary key in Python, not in SQL, because PostgreSQL's only setter for a JSON path is
jsonb_set, and JSONB reorders keys, which the stored envelopes avoid. - The builder checks fields against
DEFAULT_REGISTRY, as it checked the one registry, so a type in another registry has its fields checked byRule.checkwhen itsWorkspacesis built. AnyEvent's schema leavesevent.typea plain string. The same schema describes stored envelopes, and an application's envelopes stored before it renames its types keep their old names, which round-trip asUnknownEvent.
Options considered¶
The examples use three types: an application's alert.fired, relayr's bridge of artifactr's feedback_given (a name reflexr's own fact holds today), and reflexr's run_dead_lettered. Each shows a rule's filter, a publish command, and the stored envelope with its event_type column. ... stands for fields that don't change. The decided option is in bold.
1a. Where namespaces live¶
| Option | An application and a library in one Workspaces |
Wire | Cost |
|---|---|---|---|
| Convention: flat names with agreed prefixes | By discipline | Unchanged | Nothing to build, and nothing enforced |
A registry per Workspaces, the global one by default |
Not solved: one log still needs one name per type | Unchanged | A second parse path, for names no Workspaces reaches |
| Qualified names on the wire, in one registry | Solved | Names gain a namespace | The grammar, and a migration for reflexr's facts |
| Both (decided), with the registry as a view | Solved | Names gain a namespace | The grammar, the migration, and a set of namespaces per Workspaces |
Convention, as RFC-0002 wrote its placeholders. Nothing says artifactr. is a namespace or who owns it, so #72 has nothing to hang on:
rule {"kind": "on", "types": ["artifactr.feedback_given"]}
publish {"type": "publish", "event": {"type": "artifactr.feedback_given", "feedback_type": "rating", ...}}
envelope {"seq": 12, "actor": {"kind": "source", "name": "artifactr"}, ..., "event": {"type": "artifactr.feedback_given", ...}}
column event_type = 'artifactr.feedback_given'
A registry per Workspaces alone. Two applications in one process could each have a heartbeat. But relayr's types and reflexr's facts share one log, so relayr would still need a prefix by convention:
declare Workspaces(storage, registry=registry) a registry holding AlertFired and FeedbackGiven
rule {"kind": "on", "types": ["feedback_given"]} reflexr's fact, or artifactr's?
publish {"type": "publish", "event": {"type": "feedback_given", ...}}
envelope {"seq": 12, ..., "event": {"type": "feedback_given", ...}}
column event_type = 'feedback_given'
Qualified names alone. One mechanism covers all three collisions, as long as each owner picks a different namespace:
declare class ArtifactrEvent(Event, abstract=True, namespace="artifactr")
class FeedbackGiven(ArtifactrEvent) artifactr:feedback_given
rule {"kind": "on", "types": ["artifactr:feedback_given"]}
publish {"type": "publish", "event": {"type": "artifactr:feedback_given", "feedback_type": "rating", ...}}
envelope {"seq": 12, "actor": {"kind": "source", "name": "artifactr"}, ..., "event": {"type": "artifactr:feedback_given", ...}}
column event_type = 'artifactr:feedback_given'
Both (decided), with the registry as a view. The wire is the qualified names', and a registry scopes which types a Workspaces accepts. The draft recommended qualified names alone. The maintainer chose both.
As first drafted, a registry held types of its own, so one namespace could be two sets of types in one process. A simplicity review found that this needs a second parse path, and that it leaked:
- the reactor's evaluation and the executor's claims read storage without going through
Workspace, so they would miss the step that resolved types - SQL storage parses envelopes again where in-memory storage doesn't, so the two would behave differently
- one field error would get two response formats
- isolation wouldn't cover the default registry
So the registry is a view: a set of namespaces over the one process-wide type table. The maintainer chose that design. All it gives up is defining one namespace as two sets of types in one process:
declare ONCALL = EventRegistry()
class OncallEvent(Event, abstract=True, namespace="oncall", registry=ONCALL)
Workspaces(storage, registry=ONCALL, events=EVENTS)
rule {"kind": "on", "types": ["oncall:alert.fired"]}
publish {"type": "publish", "event": {"type": "oncall:alert.fired", "service": "api", ...}} not_found in a Workspaces whose registry lacks oncall
envelope {"seq": 7, ..., "event": {"type": "oncall:alert.fired", ...}}, event_type = 'oncall:alert.fired'
1b. How an owner declares its namespace¶
| Option | Declared | A typo | Precedent |
|---|---|---|---|
In each name: name="oncall:alert.fired" |
Per type | Quietly makes a new namespace for one type | None |
namespace= on an abstract base, declared once per process (decided) |
Once per owner | Is in the base, so every type shows it | pydantic-ai's CapabilityEvent |
Derived from the module: oncall.events gives oncall |
Implicitly | Can't happen: nothing is typed | None, and moving a module renames its types on the wire |
The wire is the same under all three:
in each name class AlertFired(Event, name="oncall:alert.fired")
on a base class OncallEvent(Event, abstract=True, namespace="oncall")
class AlertFired(OncallEvent, name="alert.fired")
from the module class AlertFired(Event, name="alert.fired") in oncall/events.py
rule {"kind": "on", "types": ["oncall:alert.fired"]}
publish {"type": "publish", "event": {"type": "oncall:alert.fired", "service": "api", ...}}
envelope {"seq": 7, ..., "event": {"type": "oncall:alert.fired", ...}}, event_type = 'oncall:alert.fired'
pydantic-ai lets any class use a namespace, and refuses only duplicate kinds. Declaring once makes the owner something the process knows, which #72 can rely on.
| Owner | Declares | Names |
|---|---|---|
| An application | Its own | oncall:alert.fired |
| reflexr | reflexr, in core |
reflexr:run_started |
| A library or plugin | Its own | acme_pager:page.sent |
| A bridge (relayr) | The source system's | artifactr:message_posted |
2a. The separator and the grammar¶
| Option | alert.fired in oncall |
Unambiguous | Notes |
|---|---|---|---|
: (decided) |
oncall:alert.fired |
Yes | The family already writes owners this way: RFC-0002's reflexr:<rule> client ids, and its thread: and chain: tags |
/ |
oncall/alert.fired |
Yes | Reads as a path and suggests nesting. It would need escaping if an endpoint ever took a type in its path |
. |
oncall.alert.fired |
No | pydantic-ai's join. But local names already use dots (service.error), so only the registry could say where a namespace ends |
A separate namespace field |
"namespace": "oncall" beside "type": "alert.fired" |
Yes | Every filter becomes a list of pairs, and namespace becomes a reserved field on every event |
":" rule {"kind": "on", "types": ["oncall:alert.fired"]}
publish {"type": "publish", "event": {"type": "oncall:alert.fired", "service": "api", ...}}
envelope {"seq": 7, ..., "event": {"type": "oncall:alert.fired", ...}}, event_type = 'oncall:alert.fired'
"/" rule {"kind": "on", "types": ["oncall/alert.fired"]}
publish {"type": "publish", "event": {"type": "oncall/alert.fired", "service": "api", ...}}
envelope {"seq": 7, ..., "event": {"type": "oncall/alert.fired", ...}}, event_type = 'oncall/alert.fired'
"." rule {"kind": "on", "types": ["oncall.alert.fired"]}
publish {"type": "publish", "event": {"type": "oncall.alert.fired", "service": "api", ...}}
envelope {"seq": 7, ..., "event": {"type": "oncall.alert.fired", ...}}, event_type = 'oncall.alert.fired'
field rule {"kind": "on", "types": [{"namespace": "oncall", "name": "alert.fired"}]}
publish {"type": "publish", "event": {"namespace": "oncall", "type": "alert.fired", "service": "api", ...}}
envelope {"seq": 7, ..., "event": {"namespace": "oncall", "type": "alert.fired", ...}}, plus an event_namespace column
2b. How unqualified names resolve¶
| Option | Application types | Spellings of one type |
|---|---|---|
| Every name qualified: applications declare a namespace too (decided) | All change | One |
| A bare name is the application's; nothing is filled in | Unchanged | One |
A default namespace per Workspaces fills in bare names |
Unchanged as sent, qualified as stored | Two |
every name qualified (decided)
rule {"kind": "on", "types": ["oncall:alert.fired"]}
publish {"type": "publish", "event": {"type": "oncall:alert.fired", ...}} a bare name fails, with a hint
envelope {"seq": 7, ..., "event": {"type": "oncall:alert.fired", ...}}, event_type = 'oncall:alert.fired'
a bare name is the application's
rule {"kind": "on", "types": ["alert.fired", "artifactr:feedback_given"]}
publish {"type": "publish", "event": {"type": "alert.fired", ...}}
envelope {"seq": 7, ..., "event": {"type": "alert.fired", ...}}, event_type = 'alert.fired'
a default namespace per Workspaces
declare Workspaces(storage, namespace="oncall") (hypothetical)
rule {"kind": "on", "types": ["alert.fired"]} checked as oncall:alert.fired
publish {"type": "publish", "event": {"type": "alert.fired", ...}}
envelope {"seq": 7, ..., "event": {"type": "oncall:alert.fired", ...}} not what was sent
The draft recommended bare names for the application. That leaves application types unchanged, but rests on a convention reflexr can't enforce: that libraries declare a namespace and applications needn't. With every name qualified, a name always says its owner, there is no exception to remember, and a client or an agent drafting a rule never has to guess. A default namespace gives one type two spellings, so every input would have to normalize names.
2c. reflexr's own facts¶
| Option | Names left to applications | Stored data | Refusing a publish |
|---|---|---|---|
reflexr: (decided) |
All | Migration 0004 rewrites thirteen names | The reflexr namespace is never published |
| Bare, as today | All but thirteen | Unchanged | isinstance(event, SYSTEM_EVENTS), as today |
reflexr: (decided)
rule {"kind": "on", "types": ["reflexr:run_dead_lettered"]}
publish {"type": "publish", "event": {"type": "reflexr:run_dead_lettered", ...}} forbidden, as today
envelope {"seq": 31, "actor": {"kind": "system", "name": "reflexr"}, ..., "event": {"type": "reflexr:run_dead_lettered", "run_id": "...", ...}}
column event_type = 'reflexr:run_dead_lettered'
bare
rule {"kind": "on", "types": ["run_dead_lettered"]}
publish {"type": "publish", "event": {"type": "run_dead_lettered", ...}} forbidden
envelope {"seq": 31, "actor": {"kind": "system", "name": "reflexr"}, ..., "event": {"type": "run_dead_lettered", ...}}
column event_type = 'run_dead_lettered'
Once every name is qualified (2b), bare facts would be the only bare names, so this follows from 2b. A combined log reads reflexr:feedback_given beside artifactr:feedback_given, and reflexr's facts are the first reserved namespace (4).
3. Compatibility¶
The libraries are pre-1.0 and pinned by GitHub revision. Nothing is released.
| Option | For | Against |
|---|---|---|
A clean break, no aliases, still reflexr.v1 (decided) |
One spelling from the first release | Every JSON rule, client and stored envelope changes once |
| Aliases for a release: old names accepted on input, then normalized | Old JSON rules and filters keep working | Normalizing at every input, as 2b's default namespace would, and code to remove later |
reflexr.v2, served beside v1 |
Follows the protocol's rule that v1 changes are additive | Two protocols before either has a user |
clean break (decided)
rule {"kind": "on", "types": ["run_dead_lettered"]}
invalid: event type 'run_dead_lettered' has no namespace; did you mean 'reflexr:run_dead_lettered'?
publish {"type": "publish", "event": {"type": "alert.fired", ...}} validation_failed, with the same hint
envelope {"seq": 31, ..., "event": {"type": "reflexr:run_dead_lettered", ...}}, rewritten by migration 0004
aliases
rule {"kind": "on", "types": ["run_dead_lettered"]} accepted as reflexr:run_dead_lettered
publish {"type": "publish", "event": {"type": "alert.fired", ...}} accepted as oncall:alert.fired
envelope {"seq": 31, ..., "event": {"type": "reflexr:run_dead_lettered", ...}}, always stored qualified
reflexr.v2 beside v1
rule {"kind": "on", "types": ["reflexr:run_dead_lettered"]} rules are code, not protocol
publish {"type": "publish", "event": {"type": "alert.fired", ...}} accepted over v1 only
envelope {"seq": 31, ..., "event": {"type": "reflexr:run_dead_lettered", ...}}, sent bare over v1
What changes:
| What | Change |
|---|---|
| Application types | Declare a namespace on a base, and are published, filtered and stored by their qualified names |
Workspaces(events=, emitted=) and EventContext(emit=) |
Nothing: they take classes |
Rules in Python, such as on(RunDeadLettered) |
No change to the source. Their definition hash changes, so they reset once and start afresh at the head |
| Rules as JSON | Use qualified names. Old names fail with the hint |
| The JSON Schemas | The pattern on names in both, regenerated. The $ids stay |
| Clients | Publish and filter by qualified names. Old names fail with the hint |
| Stored envelopes | Migration 0004 renames reflexr's facts. Stored application types with old names read back as UnknownEvent until the application renames them |
| MCP | _RUN_FACTS picks run facts by the reflexr:run_ prefix |
| Dashboards | Legends show qualified names. The queries name no types, so they don't change |
| oncall | Declares oncall, keeping ADR-0031's local names |
The protocol document says v1 changed before its first release, and the change is marked breaking.
4. Reserved publishers (#72)¶
A namespace can carry who may publish. It's the natural unit: a bridge owns a whole namespace, and reflexr owns its facts.
| Option | Declared by | Covers |
|---|---|---|
| On the namespace's declaration | The type's owner, in code | Whole namespaces only |
| Per type, as #72 is filed | The application | One type at a time |
Per namespace, with per-type exceptions, on Workspaces (decided) |
The application | Both |
Only the application resolves clients to actors, and reserving a type to SourceActor("artifactr") means nothing if a client can be resolved to it. So the policy belongs on Workspaces, beside events= and emitted=, and a library exports its entry for the application to pass. The reflexr namespace stays reserved in code. Per-type entries cover the exceptions: an application's projection of an artifactr app_event is in the application's namespace (5), but only relayr publishes it.
The cost: an application that forgets relayr's entry leaves forging open, so relayr's wiring helper should pass it. #72 designs the policy's values (actors, sources, runs). The declarations below illustrate the shape, not an API, and the wire is the same under all three:
declaration class ArtifactrEvent(Event, abstract=True, namespace="artifactr", publishers=[SourceActor(name="artifactr")])
per type Workspaces(storage, reserved={ProposalResolved: [SourceActor(name="artifactr")], ...})
per namespace Workspaces(storage, publishers={"artifactr": [SourceActor(name="artifactr")], "plans:plan_approved": [SourceActor(name="artifactr")]})
rule {"kind": "on", "types": ["artifactr:proposal_resolved"]}
publish {"type": "publish", "event": {"type": "artifactr:proposal_resolved", ...}} from a user: forbidden
envelope {"seq": 40, "actor": {"kind": "source", "name": "artifactr"}, ..., "event": {"type": "artifactr:proposal_resolved", ...}}
5. relayr's bridged names¶
| Option | message_posted becomes |
For | Against |
|---|---|---|---|
artifactr: (decided) |
artifactr:message_posted |
Names where the fact happened. A different bridge would keep the names, and rules with them | The namespace isn't named after relayr, which declares it |
relayr: |
relayr:message_posted |
Names the declarer | Names the plumbing. Rules would change if the bridge did |
| Flat prefixes, RFC-0002's placeholders | artifactr.message_posted |
Works today | 1a's convention: no owner, nothing for #72 |
| artifactr event | Bridged as |
|---|---|
message_posted |
artifactr:message_posted |
artifact_created, artifact_changed, artifact_archived |
artifactr:artifact_created and so on |
proposal_created, proposal_resolved |
artifactr:proposal_created, artifactr:proposal_resolved |
run_ended |
artifactr:turn_ended, keeping RFC-0002's rename, so "run" means one thing to rule authors |
feedback_given |
artifactr:feedback_given |
An application's app_events and artifact kinds |
The application's own types, in its namespace, reserved to relayr type by type (4) |
relayr declares artifactr once, on its base, so nothing else in the process can. RFC-0002's install-rule:
artifactr: (decided)
rule {"kind": "on", "types": ["artifactr:proposal_resolved"]}
publish {"type": "publish", "id": "<the artifactr envelope's id>", "event": {"type": "artifactr:proposal_resolved", "proposal_id": "prp_3", "decision": "accept", "author": {...}, ...}}
envelope {"seq": 40, "actor": {"kind": "source", "name": "artifactr"}, ..., "event": {"type": "artifactr:proposal_resolved", ...}}, event_type = 'artifactr:proposal_resolved'
relayr: rule {"kind": "on", "types": ["relayr:proposal_resolved"]}
publish {"type": "publish", "id": "...", "event": {"type": "relayr:proposal_resolved", ...}}
envelope {"seq": 40, ..., "event": {"type": "relayr:proposal_resolved", ...}}, event_type = 'relayr:proposal_resolved'
flat rule {"kind": "on", "types": ["artifactr.proposal_resolved"]}
publish {"type": "publish", "id": "...", "event": {"type": "artifactr.proposal_resolved", ...}}
envelope {"seq": 40, ..., "event": {"type": "artifactr.proposal_resolved", ...}}, event_type = 'artifactr.proposal_resolved'
Trade-off analysis¶
Qualified names solve all three collisions with one mechanism, and every name says who owns it. A registry per Workspaces scopes which types a Workspaces accepts, so two applications, or a test suite and an example, share a process without accepting each other's types. As a view over the one type table, it adds no parse path: storage, the surfaces, the builder, the reactor and the executor don't change. What it gives up, one namespace defined as two sets of types in one process, no known need requires. Qualifying every name costs every application a base class and a rename, and buys names with no exceptions. : is the one separator that can't be confused with the dots names already use, without adding a field. The protocol hasn't shipped, so this is the cheapest moment for a break.
What artifactr needs¶
No matching change. artifactr's own events are a closed union that nothing else registers, and relayr maps them to artifactr: names, so artifactr renames nothing. Its artifact kinds and feedback types, like reflexr's feedback types, sit in flat, process-wide registries that relayr's rule kind (RFC-0002's phase 5) could meet; the same grammar can apply there, through an issue in each library, but phase 1 doesn't need it.
Consequences¶
- Easier: libraries, plugins, bridges and applications define types without agreeing on names, and every name on the wire says who owns it.
- Easier: two applications, or a test suite and an example, share a process, each
Workspacesaccepting only its own registry's types. - Easier: relayr's names are final from phase 1, and #72 can reserve a namespace in one entry.
- Harder: every application declares a namespace, and every JSON rule, client and stored application envelope changes once.
- Harder: a namespace is unique per process, so one namespace can't be two sets of types, even in separate registries.
- To revisit:
- namespace wildcards in
on,hello.typesandtype= - feedback types (
feedback_given.feedback_type), whose registry has the same shape; relayr's phase 6 registers some -
72's policy on namespace keys¶
- namespace wildcards in
Action items¶
- Core:
EventNameand its hint;event_namespace=andregistry=on abstract bases; the namespace table;EventRegistryandDEFAULT_REGISTRY, in place ofevent_types()andget_event_type(); reflexr's facts asreflexr:*. - Workspace:
Workspaces(registry=), and the checks inWorkspace.readand_check_publishable; MCP's run facts by thereflexr:run_prefix. - SQL: migration 0005, tested on SQLite and PostgreSQL.
- Schemas: regenerate both, with
EventName's pattern. - oncall: the
oncallnamespace, keeping ADR-0031's local names. - Docs: the protocol, the architecture, the guides and the getting-started page.
- Then: #72 on namespace keys, relayr's phase 1 with
artifactr:names, and RFC-0002's tracking item in stackr.