khive_runtime/atomic_plan.rs
1//! ADR-099 (cross-op atomicity for bulk apply) — prepared write-plan types.
2//!
3//! Async prepare materializes a synchronous write plan outside any
4//! transaction; commit later applies DML or a typed GTD no-op read assertion under a per-op
5//! SAVEPOINT. This module defines the plan *shapes* only, one family per
6//! admissible verb group (`update`, `delete`, `link`, `merge`,
7//! `gtd.transition`, `gtd.complete`, the governance verbs) — not yet wired
8//! into a live handler or the dispatch path. Every plan is deliberately
9//! inert (plain data, no async, no embedding reference).
10//!
11//! Two validation-staleness invariants every plan must satisfy:
12//! 1. **Predicate-based plans** carry an "all rows matching a condition"
13//! effect as a statement evaluated inside the transaction
14//! (`PlanPredicate`), never as a prepare-time-enumerated row list.
15//! 2. **Affected-row guards** (`PlanStatement::guard`) are attached to the
16//! exact statement they validate, checked in-transaction; a mismatch
17//! fails the op and rolls back the whole unit.
18//!
19//! See `docs/atomic-plan.md` for why guards are per-statement rather than
20//! per-plan.
21
22use uuid::Uuid;
23
24use khive_storage::SqlStatement;
25
26/// One statement in a plan, paired with the guard (if any) that validates
27/// it. **Runner contract:** a present `guard` is checked against the
28/// affected-row count of applying `statement` alone (`SqlWriter::execute`'s
29/// return value), or the result-row count for a GTD no-op read assertion,
30/// not a batch total and not another statement's count.
31/// `guard: None` means prepare made no row-existence
32/// assumption about this particular statement (e.g. a cascade delete that
33/// may legitimately touch zero rows).
34#[derive(Debug, Clone)]
35pub struct PlanStatement {
36 /// The DML, or GTD no-op SELECT assertion, to apply inside the atomic unit.
37 pub statement: SqlStatement,
38 /// The expected-effect guard for `statement`, if prepare's validation
39 /// assumed a target row exists for it.
40 pub guard: Option<AffectedRowGuard>,
41}
42
43/// The predicate a prepare pass validated a plan's target against, replayed
44/// as a statement evaluated **inside** the transaction (ADR-099 D1, rule 1:
45/// "predicate-based plans wherever a write's scope depends on current
46/// state"). Carrying the predicate rather than a prepare-time-enumerated row
47/// list is what lets a later op in the same file (e.g. an intervening
48/// `link`) be visible to this plan's apply.
49#[derive(Debug, Clone)]
50pub struct PlanPredicate {
51 /// Human-readable description of the condition, for diagnostics (e.g.
52 /// `"source_id = :from"`).
53 pub description: String,
54 /// The in-transaction statement whose scope is evaluated against
55 /// current (committed-so-far) state, not prepare-time state.
56 pub statement: SqlStatement,
57}
58
59/// An affected-row guard (ADR-099 D1, rule 2): the row-count prepare assumed
60/// its target write would affect, re-verified in-transaction. A prepare-time
61/// validation is a plan *hypothesis*, never a commitment — if the guard does
62/// not hold at apply time, the op fails inside the atomic unit and the whole
63/// unit rolls back (ADR-099 acceptance criteria: "zero-row apply fails the
64/// unit").
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct AffectedRowGuard {
67 /// Minimum affected-row count for the guard to hold (inclusive).
68 pub expected_min: u64,
69 /// Maximum affected-row count for the guard to hold (inclusive), or
70 /// `None` for "no upper bound" (e.g. a predicate-based rewire that may
71 /// touch any number of rows).
72 pub expected_max: Option<u64>,
73}
74
75impl AffectedRowGuard {
76 /// A guard requiring exactly `n` affected rows (the common case for a
77 /// single-target `update`/`delete`/`link` statement).
78 pub fn exactly(n: u64) -> Self {
79 Self {
80 expected_min: n,
81 expected_max: Some(n),
82 }
83 }
84
85 /// A guard requiring at least one affected row and no upper bound (the
86 /// shape for a predicate-based rewire that may touch any number of rows,
87 /// e.g. `merge`'s edge rewire).
88 pub fn at_least_one() -> Self {
89 Self {
90 expected_min: 1,
91 expected_max: None,
92 }
93 }
94
95 /// Whether an observed affected-row count satisfies this guard.
96 pub fn holds_for(&self, affected: u64) -> bool {
97 affected >= self.expected_min
98 && self.expected_max.map(|max| affected <= max).unwrap_or(true)
99 }
100}
101
102/// A deferred side effect recorded during prepare and run once, after the
103/// atomic unit commits (ADR-099 D1, "post-commit pass"). v1's admissible set
104/// computes no embeddings during prepare (D3's `update`/`merge` caveat), so
105/// the only post-commit effects are reindex kicks computed from the
106/// **committed** row content, plus the GAP-5 addition under B3: the
107/// best-effort GTD lifecycle audit row.
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum PostCommitEffect {
110 /// No deferred side effect for this op.
111 None,
112 /// Re-embed and re-warm the given entity's vector row from its committed
113 /// content (ADR-099 D3 `update` caveat: entity name/description change).
114 ReindexEntity { entity_id: Uuid },
115 /// Re-embed and re-warm the given note's vector row from its committed
116 /// content (ADR-099 D3 `update` caveat: note name/content change).
117 ReindexNote { note_id: Uuid, version: i64 },
118 /// Invalidate consumers without recreating explicitly removed vectors.
119 NoteChanged { note_id: Uuid, kind: String },
120 /// Append one `gtd_lifecycle_audit` row for a committed `gtd.transition`
121 /// or `gtd.complete` (ADR-099 B3, GAP-5): canonical `handle_transition`/
122 /// `handle_complete` call `ensure_audit_schema` +
123 /// `write_audit_record_with_status`
124 /// (`khive-pack-gtd::handlers`) as a best-effort side write — a failed
125 /// audit insert must never roll back an already-committed transition.
126 /// Carries exactly the fields the lifecycle-audit helper needs. Applied
127 /// outside `khive-runtime` (crate-direction: `khive-pack-gtd` depends on
128 /// `khive-runtime`, not the other way around) — this crate's own
129 /// `apply_post_commit_effects` treats this variant as a no-op; the
130 /// `kkernel` caller that owns both crates applies it by calling the
131 /// canonical `ensure_audit_schema`/`write_audit_record_with_status`
132 /// functions
133 /// directly.
134 GtdAudit {
135 task_id: Uuid,
136 from_status: String,
137 to_status: String,
138 note: Option<String>,
139 namespace: String,
140 },
141 /// A committed note delete (soft or hard) — fire the pack-installed
142 /// note-mutation hook with the deleted note's kind (#750:
143 /// `DeletePlan` previously carried no `post_commit`
144 /// slot at all, so an atomic note delete never reached
145 /// `KhiveRuntime::fire_note_mutation_hook`, unlike `operations.rs`'s
146 /// `delete_note`, which fires it directly after a successful row
147 /// delete). Entity deletes have no equivalent — the hook system is
148 /// note-only (`khive-pack-memory`'s warm ANN cache is the only
149 /// installed consumer today).
150 NoteDeleted { note_id: Uuid, kind: String },
151}
152
153/// The natural key a committed symmetric edge update's surviving row must
154/// be looked up by (ADR-099 B3, second
155/// half). `khive-db`'s `edge_symmetric_absorb_or_update_inplace_statement`
156/// pair never trusts a prepare-time-computed target id (see that builder's
157/// doc comment); a caller rendering this op's result derives the actual
158/// surviving id by querying `graph_edges`'s own
159/// `UNIQUE(namespace, source_id, target_id, relation)` constraint via
160/// `KhiveRuntime::get_edge_by_natural_key_including_deleted`, which — unlike
161/// `list_edges` — includes soft-deleted rows: the ADR-039 DO NOTHING absorption
162/// arm can commit leaving the surviving canonical row tombstoned
163/// (khive#1213/#1214), and `list_edges` would report "not found" for exactly
164/// that row. Looked up strictly after commit.
165#[derive(Debug, Clone)]
166pub struct EdgeNaturalKey {
167 pub(crate) namespace: String,
168 pub(crate) canon_source_id: Uuid,
169 pub(crate) canon_target_id: Uuid,
170 pub(crate) relation: khive_storage::EdgeRelation,
171}
172
173impl EdgeNaturalKey {
174 /// The namespace containing the surviving edge.
175 pub fn namespace(&self) -> &str {
176 &self.namespace
177 }
178
179 /// The canonical source endpoint of the surviving edge.
180 pub fn canon_source_id(&self) -> Uuid {
181 self.canon_source_id
182 }
183
184 /// The canonical target endpoint of the surviving edge.
185 pub fn canon_target_id(&self) -> Uuid {
186 self.canon_target_id
187 }
188
189 /// The surviving edge's relation.
190 pub fn relation(&self) -> khive_storage::EdgeRelation {
191 self.relation
192 }
193}
194
195/// Write plan for an `update` op (entity or note shape — ADR-099 D3's
196/// `update` caveat covers both substrates the same way: row/FTS DML in the
197/// plan, any reindex deferred to `post_commit`).
198///
199/// Deferred effects are assigned by this crate's prepare pass and cannot be
200/// supplied by callers constructing a plan directly:
201///
202/// ```compile_fail
203/// use khive_runtime::{PostCommitEffect, UpdatePlan};
204/// use uuid::Uuid;
205///
206/// let id = Uuid::nil();
207/// let plan = UpdatePlan {
208/// target_id: id,
209/// statements: Vec::new(),
210/// post_commit: PostCommitEffect::ReindexEntity { entity_id: id },
211/// edge_natural_key: None,
212/// idempotent_noop: false,
213/// };
214/// ```
215///
216/// A plan returned by a prepare function cannot have its validated
217/// statements cleared or replaced before it reaches the runner:
218///
219/// ```compile_fail
220/// use khive_runtime::UpdatePlan;
221///
222/// fn clear_prepared_statements(mut prepared: UpdatePlan) {
223/// prepared.statements.clear();
224/// }
225/// ```
226#[derive(Debug, Clone)]
227pub struct UpdatePlan {
228 /// The id of the entity or note being updated. For a symmetric edge
229 /// update this is the CALLER's requested id — advisory only, never the
230 /// basis for post-commit result rendering (see [`EdgeNaturalKey`]).
231 pub(crate) target_id: Uuid,
232 /// Row + FTS DML statements to apply inside the atomic unit, in order.
233 /// The row-update statement carries the existence guard; any FTS-mirror
234 /// statement that follows it is unguarded (its target row's existence
235 /// was already asserted by the row-update statement's own guard).
236 pub(crate) statements: Vec<PlanStatement>,
237 /// Deferred reindex assigned by the prepare pass when the update changed
238 /// name, description, or content.
239 pub(crate) post_commit: PostCommitEffect,
240 /// `Some` only for a symmetric edge update — the natural key a caller
241 /// must use to derive the committed surviving row post-commit, rather
242 /// than trusting `target_id`. `None` for every other update shape
243 /// (entity, note, non-symmetric edge), where `target_id` alone is
244 /// already an exact, non-advisory identifier.
245 pub(crate) edge_natural_key: Option<EdgeNaturalKey>,
246 /// True for a note patch whose normalized values already equal the
247 /// snapshot. Such plans carry a guarded SELECT assertion and must not
248 /// execute DML that would advance the note revision.
249 pub(crate) idempotent_noop: bool,
250 pub(crate) entity_guard: Option<crate::entity_write::EntityWriteGuard>,
251 pub(crate) note_guard: Option<crate::note_write::NoteWriteGuard>,
252 pub(crate) note_vector_purge: Option<crate::note_write::NoteVectors>,
253 pub(crate) note_embedding_inheritance: Option<crate::note_write::NoteEmbeddingInheritance>,
254 /// Typed kind-owned graph operations, applied after the note statements in
255 /// the same savepoint. Assertions never execute as writes.
256 pub(crate) graph_effects: Vec<NoteUpdateStatement>,
257}
258
259/// Only runtime preparation can append these to an update. Packs return typed
260/// graph requests, not SQL or execution flags.
261#[derive(Debug, Clone)]
262pub(crate) enum NoteUpdateStatement {
263 Write(PlanStatement),
264 Assert(PlanStatement),
265}
266
267impl UpdatePlan {
268 /// The record id supplied to the prepare pass.
269 pub fn target_id(&self) -> Uuid {
270 self.target_id
271 }
272
273 /// The committed lookup key required for a symmetric edge update.
274 pub fn edge_natural_key(&self) -> Option<&EdgeNaturalKey> {
275 self.edge_natural_key.as_ref()
276 }
277
278 /// The potential deferred effect. The writer resolves inherited embedding
279 /// membership before issuing the committed-effects token.
280 pub fn post_commit(&self) -> &PostCommitEffect {
281 &self.post_commit
282 }
283
284 /// Whether this plan is a mutation-free note update assertion.
285 pub fn is_idempotent_noop(&self) -> bool {
286 self.idempotent_noop
287 }
288}
289
290/// Write plan for an `AddEntity` proposal change: a fresh entity row plus its
291/// FTS document in the same atomic unit. Vector indexing remains a deferred
292/// effect because embedding may suspend.
293#[derive(Debug, Clone)]
294pub struct AddEntityPlan {
295 /// The freshly generated id of the entity being created.
296 pub(crate) entity_id: Uuid,
297 /// Row + FTS insert statements to apply inside the atomic unit, in
298 /// order. The row-insert statement carries the existence guard; the
299 /// FTS-insert statement that follows it is unguarded (an ordinary
300 /// `INSERT` into a virtual table with no conflicting row).
301 pub(crate) statements: Vec<PlanStatement>,
302 /// Reindex the committed entity after the transaction closes, as
303 /// assigned by the prepare pass.
304 pub(crate) post_commit: PostCommitEffect,
305}
306
307impl AddEntityPlan {
308 /// The id generated for the prepared entity.
309 pub fn entity_id(&self) -> Uuid {
310 self.entity_id
311 }
312}
313
314/// Write plan for an `AddNote` proposal change: a fresh note row plus its FTS
315/// document in the same atomic unit.
316#[derive(Debug, Clone)]
317pub struct AddNotePlan {
318 /// The freshly generated id of the note being created.
319 pub(crate) note_id: Uuid,
320 pub(crate) note_guard: Option<crate::note_write::NoteWriteGuard>,
321 /// Row + FTS insert statements to apply inside the atomic unit, in
322 /// order, mirroring [`AddEntityPlan::statements`].
323 pub(crate) statements: Vec<PlanStatement>,
324 /// Reindex the committed note after the transaction closes, as assigned
325 /// by the prepare pass.
326 pub(crate) post_commit: PostCommitEffect,
327}
328
329impl AddNotePlan {
330 /// The id generated for the prepared note.
331 pub fn note_id(&self) -> Uuid {
332 self.note_id
333 }
334}
335
336/// Write plan for a `delete` op (soft or hard).
337///
338/// Deferred effects are assigned by this crate's prepare pass and cannot be
339/// attached to a statement-free plan by external callers:
340///
341/// ```compile_fail
342/// use khive_runtime::{DeletePlan, PostCommitEffect};
343/// use uuid::Uuid;
344///
345/// let id = Uuid::nil();
346/// let plan = DeletePlan {
347/// target_id: id,
348/// statements: Vec::new(),
349/// post_commit: PostCommitEffect::NoteDeleted {
350/// note_id: id,
351/// kind: "observation".to_owned(),
352/// },
353/// };
354/// ```
355#[derive(Debug, Clone)]
356pub struct DeletePlan {
357 /// The id of the entity or note being deleted.
358 pub(crate) target_id: Uuid,
359 /// Row DML (and, for a hard delete, incident-edge cascade DML) to apply
360 /// inside the atomic unit, in order. The target-row delete statement
361 /// carries the existence guard; a cascade edge-delete statement (hard
362 /// delete only) is unguarded — it may legitimately affect zero rows if
363 /// the target had no incident edges.
364 pub(crate) statements: Vec<PlanStatement>,
365 /// Deferred note-mutation-hook fire assigned by the prepare pass for a
366 /// note delete (#750 2). `PostCommitEffect::None` for entity and edge
367 /// deletes — the hook system is note-only.
368 pub(crate) post_commit: PostCommitEffect,
369}
370
371impl DeletePlan {
372 /// The record id supplied to the prepare pass.
373 pub fn target_id(&self) -> Uuid {
374 self.target_id
375 }
376}
377
378/// Write plan for a `link` op (create a typed directed edge). Endpoint
379/// existence is checked **structurally**, not via an unanchored plan-level
380/// guard: `statement` is a guarded `INSERT ... SELECT ... WHERE EXISTS`
381/// shape whose `SELECT` re-probes both endpoints inside the transaction, so
382/// the runner's affected-row check on this one statement *is* the
383/// in-transaction existence probe (ADR-099 acceptance criteria's
384/// dangling-edge case — `[delete(X, hard), link(A, X)]` — is closed by this
385/// guard failing once X is gone, regardless of statement ordering
386/// convention).
387#[derive(Debug, Clone)]
388pub struct LinkPlan {
389 pub(crate) source_id: Uuid,
390 pub(crate) target_id: Uuid,
391 /// Guarded edge mutation followed by its event-plane append statements.
392 /// The first statement's affected-row count is also the endpoint and
393 /// compare-and-swap probe; event statements are reached only after it
394 /// succeeds.
395 pub(crate) statements: Vec<PlanStatement>,
396 /// Prepare-time disposition, protected by the first statement's guard.
397 pub(crate) disposition: khive_storage::EdgeUpsertDisposition,
398}
399
400impl LinkPlan {
401 /// The canonical source endpoint used by the prepared statement.
402 pub fn source_id(&self) -> Uuid {
403 self.source_id
404 }
405
406 /// The canonical target endpoint used by the prepared statement.
407 pub fn target_id(&self) -> Uuid {
408 self.target_id
409 }
410
411 /// Whether this atomic link created, replaced, or explicitly resurrected
412 /// its natural-key row.
413 pub fn disposition(&self) -> khive_storage::EdgeUpsertDisposition {
414 self.disposition
415 }
416}
417
418/// Write plan for a `merge` op (deduplicate two entities). Rewires and
419/// lifecycle writes are split into separate fields precisely so a guard is
420/// never ambiguous between them: the edge rewire is **predicate-based**
421/// (ADR-099 D1 rule 1) and may touch zero or many rows depending on earlier
422/// in-file writes, so it is never guarded; the `from`/`into` entity
423/// lifecycle write assumes both rows exist, so it always is.
424#[derive(Debug, Clone)]
425pub struct MergePlan {
426 pub(crate) into_id: Uuid,
427 pub(crate) from_id: Uuid,
428 /// Predicate-based edge-rewire statement(s)
429 /// (`UPDATE graph_edges SET source_id = :into WHERE source_id = :from`-
430 /// shaped), evaluated inside the transaction so they structurally see
431 /// any earlier op's edge writes in the same file (ADR-099 acceptance
432 /// criteria: "merge rewires see earlier in-file writes"). Never
433 /// guarded — a rewire touching zero rows is a legitimate outcome.
434 pub(crate) rewires: Vec<PlanPredicate>,
435 /// The `from` entity's soft-delete/tombstone DML (and any other
436 /// lifecycle write prepare assumed a target row exists for). Always
437 /// guarded — prepare validated `into`/`from` both exist.
438 pub(crate) lifecycle: Vec<PlanStatement>,
439}
440
441impl MergePlan {
442 /// The entity retained by the prepared merge.
443 pub fn into_id(&self) -> Uuid {
444 self.into_id
445 }
446
447 /// The entity retired by the prepared merge.
448 pub fn from_id(&self) -> Uuid {
449 self.from_id
450 }
451}
452
453/// Write plan for a `gtd.transition` op (explicit task lifecycle change).
454#[derive(Debug, Clone)]
455pub struct GtdTransitionPlan {
456 pub(crate) task_id: Uuid,
457 /// Task-property DML to apply inside the atomic unit. Property-only status
458 /// mutation triggers no reindex (ADR-099 D3). The transition
459 /// statement carries the guard over the exact decision snapshot's note
460 /// revision, deletion marker, and semantic status (prepare validated the
461 /// current status and requested transition were legal). For an idempotent
462 /// no-op (`current == target` after `normalize_status`) this contains one
463 /// guarded SELECT assertion that revalidates the prepare snapshot
464 /// under the commit transaction. Atomic v1 and canonical dispatch both
465 /// persist no caller note.
466 pub(crate) statements: Vec<PlanStatement>,
467 /// Explicit result-shape and execution discriminator: true executes the
468 /// snapshot assertion through the writer's read API and guards its result
469 /// count; false executes DML and guards affected rows.
470 pub(crate) idempotent_noop: bool,
471 /// Deferred lifecycle audit row assigned by the prepare pass (GAP-5):
472 /// `PostCommitEffect::None` for the idempotent no-op case. This matches
473 /// canonical dispatch, which emits no audit row for a same-status request.
474 pub(crate) post_commit: PostCommitEffect,
475}
476
477impl GtdTransitionPlan {
478 /// Build a plan from its already-decided statements, no-op classification,
479 /// and audit effect.
480 /// Callers outside this crate (e.g. the atomic-apply layer wiring
481 /// `gtd.transition` into a multi-op unit) cannot construct the struct
482 /// literal directly since its fields are crate-private.
483 pub fn new(
484 task_id: Uuid,
485 statements: Vec<PlanStatement>,
486 idempotent_noop: bool,
487 post_commit: PostCommitEffect,
488 ) -> Self {
489 Self {
490 task_id,
491 statements,
492 idempotent_noop,
493 post_commit,
494 }
495 }
496
497 /// The task targeted by the prepared transition.
498 pub fn task_id(&self) -> Uuid {
499 self.task_id
500 }
501
502 /// Guarded task-property DML for this transition, or the guarded
503 /// mutation-free snapshot assertion for an idempotent no-op.
504 pub fn statements(&self) -> &[PlanStatement] {
505 &self.statements
506 }
507
508 /// Whether prepare classified this transition as already at its target.
509 pub fn is_idempotent_noop(&self) -> bool {
510 self.idempotent_noop
511 }
512
513 /// The deferred lifecycle audit effect assigned by the prepare pass.
514 pub fn post_commit(&self) -> &PostCommitEffect {
515 &self.post_commit
516 }
517}
518
519/// Write plan for a `gtd.complete` op (task lifecycle terminal transition).
520#[derive(Debug, Clone)]
521pub struct GtdCompletePlan {
522 pub(crate) task_id: Uuid,
523 /// Status + `completed_at` property DML to apply inside the atomic unit.
524 /// The statement guards the exact decision snapshot's note revision,
525 /// deletion marker, and semantic status after prepare validated that the
526 /// task was in a completable state.
527 pub(crate) statements: Vec<PlanStatement>,
528 /// Deferred lifecycle audit row assigned by the prepare pass (GAP-5):
529 /// mirrors `handle_complete`'s best-effort
530 /// `write_audit_record_with_status` call.
531 pub(crate) post_commit: PostCommitEffect,
532}
533
534impl GtdCompletePlan {
535 /// Build a plan from its already-decided statements and audit effect.
536 /// Callers outside this crate (e.g. the atomic-apply layer wiring
537 /// `gtd.complete` into a multi-op unit) cannot construct the struct
538 /// literal directly since its fields are crate-private.
539 pub fn new(
540 task_id: Uuid,
541 statements: Vec<PlanStatement>,
542 post_commit: PostCommitEffect,
543 ) -> Self {
544 Self {
545 task_id,
546 statements,
547 post_commit,
548 }
549 }
550
551 /// The task targeted by the prepared completion.
552 pub fn task_id(&self) -> Uuid {
553 self.task_id
554 }
555
556 /// Guarded status + `completed_at` property DML for this completion.
557 pub fn statements(&self) -> &[PlanStatement] {
558 &self.statements
559 }
560
561 /// The deferred lifecycle audit effect assigned by the prepare pass.
562 pub fn post_commit(&self) -> &PostCommitEffect {
563 &self.post_commit
564 }
565}
566
567/// Which governance verb (`propose` / `review` / `withdraw`) a
568/// [`GovernancePlan`] applies.
569#[derive(Debug, Clone, Copy, PartialEq, Eq)]
570pub enum GovernanceOp {
571 Propose,
572 Review,
573 Withdraw,
574}
575
576/// Write plan for a governance op (`propose`, `review`, or `withdraw` — the
577/// event-sourced change-proposal lifecycle, ADR-046).
578#[derive(Debug, Clone)]
579pub struct GovernancePlan {
580 pub(crate) op: GovernanceOp,
581 pub(crate) proposal_id: Uuid,
582 /// Event-log + status DML to apply inside the atomic unit. The
583 /// lifecycle-state-check statement carries the guard (prepare validated
584 /// the proposal was in a state admitting this transition).
585 pub(crate) statements: Vec<PlanStatement>,
586}
587
588impl GovernancePlan {
589 /// The governance operation represented by this plan.
590 pub fn op(&self) -> GovernanceOp {
591 self.op
592 }
593
594 /// The proposal targeted by this plan.
595 pub fn proposal_id(&self) -> Uuid {
596 self.proposal_id
597 }
598}
599
600#[cfg(test)]
601#[path = "atomic_plan_tests.rs"]
602mod tests;