Generic per-entry classification axis registry.
An axis is a named per-entry dimension with a declared value domain and a
default. Axis values are carried in the existing CommBus.Entry.metadata/0
bag — this module only owns the registry and the resolution/validation logic.
comm_bus ships the mechanism; consuming applications declare the
vocabularies (axes) they need.
What this is
Any consumer that needs to classify entries along a known dimension (for
example, distinguishing content the agent must keep to itself from content the
agent is instructed to surface) can declare that dimension as an axis and tag
entries via metadata. Resolution then reads the declared value or falls back
to the axis default, validating against the declared domain. The dimension name
and its value domain are entirely the consumer's choice; comm_bus attaches no
semantics to either.
What this is not
This module knows nothing about what an axis means. There is no security, confidentiality, sensitivity, or assurance concept here — only "a declared per-entry dimension with a value domain and a default." Consumers interpret resolved values; comm_bus never does.
Registry lifecycle
The registry is persistent_term-backed (mirroring
CommBus.Protocol.SectionRoles). Declarations persist for the VM lifetime
until deleted or reset. Tests should call reset/0 in setup to guarantee
isolation, since persistent_term is global.
Example
# A consumer declares two entirely independent axes. The vocabularies are
# the consumer's; comm_bus only stores and resolves them.
CommBus.Axis.declare(:visibility, values: [:internal, :disclosed], default: :internal)
CommBus.Axis.declare(:feasibility, values: [:proven, :speculative], default: :speculative)
# Values are read from entry metadata (string keys/values from yml are
# normalized to the declared atom domain).
entry = %CommBus.Entry{metadata: %{"visibility" => "disclosed"}}
{:ok, :disclosed} = CommBus.Axis.get(entry, :visibility)
{:ok, :speculative} = CommBus.Axis.get(entry, :feasibility) # default fallback
{:ok, :internal} = CommBus.Axis.get(%CommBus.Entry{}, :visibility)
CommBus.Axis.declared() #=> [:visibility, :feasibility]
CommBus.Axis.spec(:visibility) #=> %{values: [:internal, :disclosed], default: :internal}
Summary
Functions
Declares an axis with a value domain and a default.
Returns the names of all declared axes, sorted for determinism.
Removes an axis declaration.
Resolves the value of an axis for a given entry.
Resolves the value of an axis for a given entry, raising on error.
Resets the registry to empty (no axes declared).
Returns the declared spec for an axis.
Types
Functions
Declares an axis with a value domain and a default.
Idempotent: re-declaring an axis overwrites its previous spec. The default
must be a member of values. Both values entries and default may be given
as atoms or strings; they are normalized to atoms.
Options
:values- required, list of atoms/strings forming the value domain.:default- required, the fallback value; must be a member of:values.
Examples
iex> CommBus.Axis.reset()
iex> CommBus.Axis.declare(:visibility, values: [:internal, :disclosed], default: :internal)
:ok
iex> CommBus.Axis.declared()
[:visibility]
@spec declared() :: [atom()]
Returns the names of all declared axes, sorted for determinism.
Removes an axis declaration.
Removing an axis does not touch any entry's metadata; it only drops the
registry entry so subsequent get/2 calls for that axis return
{:error, {:unknown_axis, axis}}.
@spec get(CommBus.Entry.t(), axis()) :: {:ok, atom()} | {:error, term()}
Resolves the value of an axis for a given entry.
Resolution reads entry.metadata[axis], accepting either atom or string keys
(yml-loaded metadata uses string keys) and normalizing string values to atoms.
The resolved value is validated against the axis's declared domain.
Returns
{:ok, value}- the declared value for the entry, or the axis default when the entry's metadata carries no value for this axis.{:error, {:unknown_axis, axis}}- the axis has not been declared.{:error, {:invalid_value, axis, raw}}- the entry carries a value that is not a member of the axis's declared domain. Illegal stored values surface as an error rather than being silently coerced.
Examples
iex> CommBus.Axis.reset()
iex> CommBus.Axis.declare(:visibility, values: [:internal, :disclosed], default: :internal)
iex> entry = %CommBus.Entry{metadata: %{"visibility" => "disclosed"}}
iex> CommBus.Axis.get(entry, :visibility)
{:ok, :disclosed}
iex> CommBus.Axis.get(%CommBus.Entry{}, :visibility)
{:ok, :internal}
@spec get!(CommBus.Entry.t(), axis()) :: atom()
Resolves the value of an axis for a given entry, raising on error.
Like get/2 but returns the bare value on success and raises
ArgumentError on unknown axis or illegal value. Useful for call sites that
have already validated their inputs and prefer the ergonomic form.
@spec reset() :: :ok
Resets the registry to empty (no axes declared).
Use in test setup to guarantee isolation, since the backing persistent_term
store is shared VM-wide.
Returns the declared spec for an axis.
Returns {:error, {:unknown_axis, axis}} if the axis has not been declared.