CommBus.Axis (CommBus v0.2.0)

Copy Markdown View Source

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

axis()

@type axis() :: atom() | String.t()

spec()

@type spec() :: %{values: [atom()], default: atom()}

value()

@type value() :: atom() | String.t()

Functions

declare(axis, opts)

@spec declare(
  axis(),
  keyword()
) :: :ok | {:error, term()}

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]

declared()

@spec declared() :: [atom()]

Returns the names of all declared axes, sorted for determinism.

delete(axis)

@spec delete(axis()) :: :ok | {:error, term()}

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}}.

get(entry, 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}

get!(entry, axis)

@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.

reset()

@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.

spec(axis)

@spec spec(axis()) :: {:ok, spec()} | {:error, term()}

Returns the declared spec for an axis.

Returns {:error, {:unknown_axis, axis}} if the axis has not been declared.