# `GgenIgniter.Telemetry.OcelEmitter`
[🔗](https://github.com/seanchatmangpt/ggen_igniter/blob/v26.9.8/lib/ggen_igniter/telemetry/ocel_emitter.ex#L1)

CONSTRUCTION NOTE (2026-08-27): at the time this module was written, a
concurrent workflow was tasked with building `lib/ggen_igniter/receipt.ex`,
`lib/ggen_igniter/reactors/reconcile_reactor.ex`, and this file
(`lib/ggen_igniter/telemetry/ocel_emitter.ex`). This repo was polled for
their existence 6 times (roughly 90 seconds, per the concurrency protocol
given) and none of the three had appeared -- only a forward-referencing
comment in `mix.exs` (`{:reactor, "~> 1.0"}`'s dependency note) evidenced
that the other workflow had *started*. This module is therefore this
session's own from-scratch, best-effort construction, not a correction to
pre-existing code. If the concurrent workflow's real version lands later,
reconcile the two rather than silently preferring either.

An OCEL (object-centric event log, https://www.ocel-standard.org/)-SHAPED
event emitter for one reconciliation attempt's real lifecycle:

    ACTUATION_STARTED -> FILES_CHANGED -> VERIFICATION_FAILED ->
    COMPENSATION_STARTED -> FILES_RESTORED -> STANDING_SET

(or, on the happy path: ACTUATION_STARTED -> FILES_CHANGED ->
VERIFICATION_SUCCEEDED -> ADMITTED -> STANDING_SET.)

This is the exact stage sequence the user specified for
`GgenIgniter.Receipt`'s `standing: :compensated` case -- this module is
what actually PRODUCES that sequence as real, structured data (not prose):
each call to `emit/4` returns one OCEL-shaped event map, real enough to
serialize into a receipt's `events` list verbatim.

## Two real, independent effects per `emit/4` call

1. A real `:telemetry.execute/3` call under `[:ggen_igniter, :reconcile,
   :ocel]` -- so any process that wants to observe reconciliation activity
   (logging, metrics, a future dashboard) can attach a real `:telemetry`
   handler and receive these events live, without this module knowing or
   caring whether anyone is listening.
2. If a `sink` (a real, running `Agent` pid from `new_sink/0`) is given,
   the event is ALSO appended to that agent's real in-memory list -- this
   is how `GgenIgniter.Reactors.ReconcileReactor` accumulates one
   attempt's full event log across multiple Reactor steps (`:actuate`'s
   `run/3` AND its `undo/3`, `:verify`, `:admit`, ...) into a single
   ordered list it can embed in a `GgenIgniter.Receipt`, including on a
   FAILURE path where the reactor's own return value carries no such
   accumulated state.

Using a real, separately-started `Agent` process (rather than, say, the
process dictionary or an ETS table) keeps this fully Chicago-style
testable: a test starts a real sink, runs real code that emits into it,
and asserts on the real list `drain_sink/1` returns -- no interaction-based
mock of "was emit called" is needed anywhere.

## Relationship to autofde-lab's `ocp:` OCEL vocabulary (DECIDED, 2026-09-04)

`~/autofde-lab/ontology/ocel-production.ttl` defines a real, separate
`ocp:EventType` vocabulary (`ocp:RequirementDiscovered`,
`ocp:OntologyChanged`, `ocp:SparqlChanged`, `ocp:TypeManufactured`,
`ocp:ArtifactGenerated`, `ocp:TestFailed`, `ocp:RootCauseAnalyzed`,
`ocp:RepairApplied`, `ocp:PullRequestOpened`, `ocp:PullRequestReviewed`,
`ocp:CIRan`, `ocp:Merged`, `ocp:Released`, `ocp:ConsumerValidated`,
`ocp:InvariantIntroduced`, `ocp:InvariantViolated`) scoped to a
multi-repo manufacturing history: requirement discovery through ontology
change, code generation, test/repair cycles, PR review, CI, merge, and
release, spanning many consumer repos over the life of a capability.

This module's own stage constants (`ACTUATION_STARTED`, `FILES_CHANGED`,
`VERIFICATION_FAILED`, `VERIFICATION_SUCCEEDED`, `COMPENSATION_STARTED`,
`FILES_RESTORED`, `ADMITTED`, `STANDING_SET`) are a real but *narrower*
vocabulary, scoped to one single reconciliation attempt's transactional
lifecycle inside `GgenIgniter.Reactors.ReconcileReactor` (27 real call
sites there): write files, verify, admit-or-compensate, record standing --
seconds, not the days-to-months span an `ocp:` release lifecycle covers.

A real, exhaustive cross-check of all 8 of this module's stages against
all 16 `ocp:EventType` individuals found exactly **1 real match**:
`VERIFICATION_FAILED` ~ `ocp:TestFailed` (both mark a verification step
that did not pass). The other 7 stages have **no `ocp:` counterpart**,
because they name concepts `ocp:` has no object type for at all:

  * `ACTUATION_STARTED` / `FILES_CHANGED` -- `ocp:` has no "file write in
    progress" event; its nearest object types (`ocp:GeneratedArtifact`,
    `ocp:Commit`) are recorded as already-produced, not as write-in-flight.
  * `COMPENSATION_STARTED` / `FILES_RESTORED` -- rollback/undo of a
    transactional write attempt has no `ocp:` analogue; `ocp:`'s
    `ocp:failureLifecycle` runs `TestFailed -> RepairApplied` (repair
    forward), never restore-to-prior-state.
  * `ADMITTED` / `STANDING_SET` -- this repo's own admission/standing
    vocabulary (`.claude/rules/standing-law.md` in `autofde-lab`) has no
    `ocp:EventType` counterpart; `ocp:` records `ocp:ConsumerValidated`,
    a different, later-stage, cross-repo consumer-acceptance concept.

**Decision: cross-reference only -- do NOT unify.** This module keeps
emitting its own ad hoc string constants unchanged. Unifying onto `ocp:`
would either (a) force 7 of 8 real stages into an `ocp:EventType` that
does not actually mean what they mean, silently widening this module's
single-attempt scope into `ocp:`'s multi-repo manufacturing-history scope
it was never designed to carry, or (b) require `ocel-production.ttl` to
grow 7 new `EventType` individuals for a lifecycle that isn't the one it
models, diluting a vocabulary that is otherwise a clean multi-repo
manufacturing ontology. Both are worse than the 1-line real overlap this
cross-reference documents. If a future caller needs to correlate one
reconciliation attempt's events with `ocp:`'s manufacturing history (e.g.
to relate a `VERIFICATION_FAILED` event to an `ocp:TestFailed` object in a
wider OCEL graph), that correlation belongs in the caller that has both
contexts in view, keyed by explicit shared object identity per
`autofde-lab`'s `.claude/rules/no-dual-bookkeeping.md` (never inferred
from label similarity) -- not by renaming this module's own vocabulary.

# `event`

```elixir
@type event() :: %{required(String.t()) =&gt; String.t() | [object()] | map()}
```

One OCEL-shaped event.

# `object`

```elixir
@type object() :: %{required(String.t()) =&gt; String.t()}
```

An OCEL "object" reference -- `{type, id}` as a JSON-friendly map.

# `any?`

```elixir
@spec any?(list(), String.t()) :: boolean()
```

Whether any event in `events` has `"activity"` equal to `activity`.

# `drain_sink`

```elixir
@spec drain_sink(pid() | nil) :: [event()]
```

Drains `sink` -- returns every event appended so far, in the real order
they were emitted (oldest first) -- and stops the agent. Safe to call at
most once per sink (the agent is gone afterward); calling with `nil`
returns `[]` with no error, so callers that never had a sink can call this
unconditionally.

# `emit`

```elixir
@spec emit(pid() | nil, String.t(), [object()], map()) :: event()
```

Builds one OCEL-shaped event, executes it as a real `:telemetry` event,
appends it to `sink` (if not `nil`), and returns the built event map --
callers that need the event's own generated `"id"` (e.g. to cross-reference
it from a receipt) get it directly from the return value, not by
re-reading the sink.

  * `activity` -- the event's OCEL "activity" name, e.g.
    `"ACTUATION_STARTED"`. A plain string (not an atom) so it round-trips
    through `Jason.encode!/1` and a receipt's persisted JSONL unchanged.
  * `objects` -- the OCEL objects this event is about (build with
    `file_object/1` / `run_object/1`).
  * `attributes` -- a plain, JSON-encodable map of this event's own
    payload (e.g. `%{"paths" => [...], "pre_run_hash" => "sha256:..."}`).

# `file_object`

```elixir
@spec file_object(String.t() | nil) :: object()
```

Builds a `"file"`-typed OCEL object reference for `path`.

A `mode: eval` `PendingActuation` genuinely has no file path
(`PendingActuation.for_eval/3`'s `target` field is always `nil`) -- this is a
real, honest sentinel id for that case, not a crash. Fixes AR-9: previously
this function had only the `is_binary(path)` clause below, so ANY `:eval`
target raised `FunctionClauseError` inside the Reactor's `:render` step
before `:admit`/`:actuate` ever ran, independent of frontmatter.

# `find_last`

```elixir
@spec find_last(list(), String.t()) :: map() | nil
```

Finds the last event in `events` (oldest-first, as returned by
`drain_sink/1`/`peek_sink/1`) whose `"activity"` is `activity`, or `nil`.
A small real query helper -- both `GgenIgniter.Reactors.ReconcileReactor`
and its tests use this to pull a specific attribute (e.g. a recorded
`"pre_run_hash"`) back out of the real, already-emitted event log instead
of threading it through a second, parallel channel.

# `new_sink`

```elixir
@spec new_sink() :: pid()
```

Starts a fresh, real event sink (a plain `Agent` holding an ordered list,
most-recent-first internally). Returns its pid. Callers own the pid's
lifecycle -- pass `nil` anywhere a sink is accepted to opt out of
accumulation entirely (telemetry still fires either way).

# `peek_sink`

```elixir
@spec peek_sink(pid() | nil) :: [event()]
```

Peeks at `sink`'s current events (oldest first) WITHOUT stopping it --
unlike `drain_sink/1`, the sink stays alive and can receive more events
afterward. Used by callers (e.g. `GgenIgniter.Reactors.ReconcileReactor`'s
own `:actuate` step `undo/3` callback) that need to read back an
already-emitted attribute (like a recorded pre-run hash) mid-attempt.
`nil` returns `[]`.

# `run_object`

```elixir
@spec run_object(String.t()) :: object()
```

Builds a `"reconcile_run"`-typed OCEL object reference for `run_id`.

# `telemetry_event`

```elixir
@spec telemetry_event() :: [atom(), ...]
```

The `:telemetry` event name every `emit/4` call executes under.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
