Skip to content

feat(cd): add reconciliation subject and events - #325

Open
sol-duara wants to merge 1 commit into
cdevents:mainfrom
Sol-Duara-Inc:feat/reconciliation-subject
Open

feat(cd): add reconciliation subject and events#325
sol-duara wants to merge 1 commit into
cdevents:mainfrom
Sol-Duara-Inc:feat/reconciliation-subject

Conversation

@sol-duara

Copy link
Copy Markdown

Changes

Adds a reconciliation subject to the Continuous Deployment vocabulary, with started and finished predicates.

A reconciliation is a convergence between a declared desired state and the observed state of a target. It is what a controller performs when it brings a target into line with a revision it has been told to converge on.

Why a new subject

This came from a DataOps request in which a database schema change reached production through a GitOps controller, and nothing in the event stream said the controller had done anything.

environment.modified reports that a mutation occurred. It carries none of the relation that defines a control loop — what desired state was being converged to, whether the target had drifted from it, and whether the convergence closed the gap. Without that, a consumer cannot distinguish a target that converged cleanly from one that was mutated by hand, and cannot tell which revision the running state corresponds to.

The occurrence-worthy moments are that a controller began converging toward a desired state, and that it finished.

Scope

The subject is defined over the declared-versus-observed relation generally, not over cluster convergence specifically. A controller reconciling a cluster to a commit and a process reconciling a data store to a declared contract are the same relation, and both are in scope.

This is the deliberate choice between the two readings. The narrower one — cluster and GitOps convergence only — describes work that an orchestration subject already partly covers, and would invite the question of why taskRun is not sufficient. The general reading describes a drift relation that no existing subject expresses, which is what justifies a distinct subject.

Design decisions

No queued predicate. Controllers converge continuously rather than accepting discrete jobs into a queue, so there is no queueing moment to report.

A cycle that finds nothing to do emits nothing. A reconciliation is emitted when a convergence occurs — when observed state differs from desired and the controller acts.

Defining an outcome value for the no-op case was considered and not taken. It would invite producers to emit on every poll, which turns an occurrence stream into a heartbeat, and a large estate would then produce continuous events reporting that nothing happened. A convergence that was expected and did not occur is found by querying for the event that is missing, not by emitting every non-event so that one can be noticed absent.

target carries a type alongside id and source, so consumers can route on the kind of target converged, and so target references are shaped consistently across subjects. type is a free string — cluster, namespace, environment — rather than a closed enum, so the subject is not coupled to a fixed list of convergeable things.

driftDetected and changed are separate. Drift is observed at the start of a convergence; whether the convergence applied any change is known only at the end. Collapsing them would lose the case where drift was detected but the convergence made no change.

No iteration or attempt identifier. Each convergence is its own occurrence with its own subject id. Numbering iterations asks the producer to carry correlation state it would have to compute. This is also not specific to reconciliation; if attempt identity is wanted, it belongs in the context for every subject.

Out of scope

  • Enforcement and admission control. Whether a convergence is permitted is control-plane behavior.
  • The controller's internal poll cycle. Checking is not an occurrence; converging is.

Notes on this change

Event versions start at 0.1.0-draft.

schemas/_defs/ entries and x-cdevents-semantics are not included here. The predicate definitions this subject would need (started, finished) overlap with those already being added by #319 and with further subjects we are proposing; adding them here would collide. We will follow up with them once #319 lands.

Submitter Checklist

As the author of this PR, please check off the items in this checklist:

  • Has the primer doc been updated if a design decision is involved — no new concept is introduced; this applies existing vocabulary patterns
  • Have the JSON schemas been updated if the specification changed
  • Has spec version and event versions been updated according to the versioning policynew events start at 0.1.0-draft; the spec version is unchanged
  • Meets the CDEvents contributor standards

A reconciliation is a convergence between a declared desired state and
the observed state of a target. It is what a controller performs when it
brings a target into line with a revision it has been told to converge
on.

This came from a DataOps request in which a schema change reached
production through a GitOps controller, and nothing in the event stream
said the controller had done anything. environment.modified reports that
a mutation occurred but carries none of the relation that defines a
control loop: which desired state was being converged to, whether the
target had drifted from it, and whether the convergence closed the gap.
Without that a consumer cannot distinguish a target that converged
cleanly from one that was changed by hand, nor tell which revision the
running state corresponds to.

The subject is defined over the declared-versus-observed relation
generally rather than over cluster convergence specifically. A
controller reconciling a cluster to a commit and a process reconciling a
data store to a declared contract are the same relation.

Two predicates: started and finished. There is no queued predicate,
since controllers converge continuously rather than accepting discrete
jobs into a queue. A cycle that finds nothing to do emits nothing, which
keeps the subject an occurrence model and avoids turning a continuously
polling controller into a heartbeat.

Event versions start at 0.1.0-draft.

Signed-off-by: Dadisi Sanyika <dadisi@solduara.com>
@sol-duara
sol-duara requested a review from a team as a code owner July 29, 2026 19:09
@xibz

xibz commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Why does reconciliation need to be its own subject with its own event types, rather than a property/object that decorates whatever event a producer already emits?

The scope section says this applies generally, cluster convergence and "a process reconciling a data store to a declared contract," both in scope. But a dedicated subject only fits producers whose occurrence naturally looks like this one. A reconciliation object (desiredState, driftDetected, changed) attached to whatever event a given producer already emits seems closer to "generally applicable" than a new subject every convergence-capable system has to adopt wholesale.

Counter case I'd want addressed: if it's a property instead of a subject, finding "every convergence that happened" requires already knowing which host event types might carry it, but the PR's own scope argument is that the set of host producers is open ended and host agnostic by design. Those two claims pull against each other: the more general the concept, the less a property-based design lets you query for it without enumerating every producer's native event shape in advance. A dedicated type doesn't have that problem. So: was a property based design considered, and if so, why was a new subject chosen over that?

@sol-duara

Copy link
Copy Markdown
Author

Thanks @xibz, that is the right question, and you've already worked out most of the answer yourself. Yes, a property-based design was considered; it was the first shape we looked at. Two reasons it lost, and your counter-case is the second of them.

A convergence has a lifecycle. started and finished bracket an interval: a controller began converging, and later stopped. An object decorating another producer's event can only report a fact at that event's instant; it has no predicates of its own, so there is nothing to open or close. The fields split across exactly that boundary: desiredState and driftDetected are known at the start, changed and outcome only at the end. A single decoration either collapses them into one moment or spreads itself across two host events with no defined relation between them. Convergence-in-flight stops being representable, and so does the difference between "still converging" and "never started." Predicates attach to subjects; something with a beginning and an end has to be one in order to say so. A span, not an attribute.

A third consequence turned up while we were working through it: a decoration fires when its host fires, not when a convergence happens. That takes the emit-on-change decision out of the producer's hands; a continuously polling controller would emit whenever its native event fires, which is the heartbeat problem arriving through a different door.

On your counter-case: you put it as a tension between two of the PR's own claims, which is sharper than the form we had. We had reasoned about reachability, but not noticed that it pulls directly against the scope argument, and that framing is the more useful one, because it makes the property design fail on generality, which is the ground it looked strongest on.

On why the description doesn't say any of this: it carried the shorter argument instead, that the narrow reading describes work an orchestration subject already partly covers. Both arguments are true and they land in the same place, and that one gets there in a sentence. You asked for the fuller version, which is what's above.

@xibz

xibz commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Grounding this against spec.md directly changes the read on it. "id (subject)" is a REQUIRED, MUST-level rule: "Subsequent events associated to the same subject MUST use the same subject id." That's not something I'm inferring about the spec, it's the actual mechanism that already ties build.started/build.finished, the pipelineRun triad, and the taskRun triad together into single occurrence lifecycles, independent of links entirely.

Checked against that, I think the argument here rests on a false dichotomy: a new subject vs. a bare decoration on some undisciplined host event. Every supporting point holds only for the second case, and the actual alternative on the table was the third one, fields on an existing job shaped subject's own started/finished pair, which already has the discipline being described.

  • "Spreads itself across two host events with no defined relation between them." If the two decorated events are a host subject's own started and finished, they share a subject id by the MUST-level rule above. The relation isn't undefined, it's required.
  • "Convergence-in-flight stops being representable... so does 'still converging' vs 'never started.'" With the relation defined, this is identical in both designs: a consumer looks for a started on a subject id with no matching finished yet. Nothing about that query depends on the subject being new.
  • Heartbeat point. This holds for a raw continuous status ping with no lifecycle discipline of its own. It doesn't hold for an already job-shaped subject, whose started/finished are by definition only emitted when a job begins or ends, decorating that introduces no heartbeat risk, because the emission is already exactly as controlled as a dedicated pair would be.
  • "Predicates attach to subjects; something with a beginning and end has to be one." This is the one point that survives, and it only proves the fields need to live on some subject with matched predicates, not that the subject has to be new. An existing one, already shaped this way and already correlated by the spec's own required subject-id mechanism, satisfies it too.

So once each supporting argument is checked against what's actually normative in the spec, three of the four don't hold up, and the one that does doesn't answer the question it's being used to answer. That leaves the same open question as before: what does a dedicated reconciliation pair get you that fields on an existing started/finished pair, correlated the way the spec already requires, doesn't?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants