dpm-engine 0.1.0

Command/query execution engine for DPM
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
use chrono::{DateTime, Utc};
use dpm_model::{
    ActorId, Artifact, DecisionId, DependencyId, ExternalIdentity, ExternalLinkRole,
    ExternalReferenceId, ExternalState, Key, OperationId, ValidationError, WorkItemId, WorkKind,
    WorkStatus,
};
use dpm_schedule::ScheduleError;
use serde::{Deserialize, Serialize};
use thiserror::Error;

/// A semantic state change; adapters must use the engine to apply it.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum Command {
    /// Apply a reviewed plan proposal without bypassing execution history; built from a full
    /// proposal by [`crate::plan_change`].
    ApplyChange {
        /// The canonical entity-level difference from the operation's base revision, in the
        /// order [`crate::propose_change`] reports it; every `before` must match that state.
        changes: Vec<crate::EntityChange>,
        /// Human-readable purpose of the accepted scope change.
        reason: String,
    },
    /// Undo a recorded plan change by applying its reviewed inverse, as a new operation that names
    /// the one it reverts; the history is never rewritten.
    RevertChange {
        /// The recorded plan change being undone.
        operation: OperationId,
        /// The inverse exactly as [`crate::propose_revert`] reports it for the current plan.
        changes: Vec<crate::EntityChange>,
        /// Human-readable purpose of the revert.
        reason: String,
    },
    /// Approve a proposed execution contract as a human or service.
    RatifyContract {
        /// Proposed task with a complete objective and acceptance criteria.
        work: WorkItemId,
    },
    /// Return submitted work to its owner with an independent review.
    Reject {
        /// Submitted task to return for rework.
        work: WorkItemId,
        /// Non-empty explanation of unmet acceptance.
        reason: String,
    },
    /// Reserve a ready task for the calling actor; a claim is not a start.
    Claim {
        /// Task to reserve.
        work: WorkItemId,
    },
    /// Give up the caller's own unstarted claim, returning the task to Planned without an owner.
    Release {
        /// Claimed task owned by the caller.
        work: WorkItemId,
        /// Non-empty explanation, such as a mistaken claim or an executor that cannot continue.
        reason: String,
    },
    /// Transfer claimed, started or blocked work to another owner, authorized by a human or
    /// service; every recorded execution fact stays with the work.
    Handoff {
        /// Task changing owners.
        work: WorkItemId,
        /// Owner the caller observed; the command is refused if the work has changed hands.
        from: ActorId,
        /// New owner, who continues from the recorded state.
        to: ActorId,
        /// Non-empty explanation of the change of executor.
        reason: String,
    },
    /// Begin executing a claimed task, recording its start event.
    Start {
        /// Claimed task owned by the caller.
        work: WorkItemId,
        /// When execution actually began, if before the commit; absent means the commit time.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        occurred_at: Option<DateTime<Utc>>,
    },
    /// Suspend a task while preserving any existing owner.
    Block {
        /// Task to suspend.
        work: WorkItemId,
        /// Concrete blocker.
        reason: String,
    },
    /// Resume blocked work, preserving its owner.
    Unblock {
        /// Task to resume.
        work: WorkItemId,
    },
    /// Report task execution progress without accepting its result.
    ReportProgress {
        /// Owned task receiving the report.
        work: WorkItemId,
        /// Execution percentage from 0 through 100.
        percent: u8,
        /// Optional explanation or correction context.
        note: Option<String>,
    },
    /// Request independent verification of owned work.
    Submit {
        /// Task whose result is submitted.
        work: WorkItemId,
        /// Optional evidence summary.
        note: Option<String>,
        /// When the result was actually submitted, if before the commit; absent means the
        /// commit time.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        occurred_at: Option<DateTime<Utc>>,
    },
    /// Accept a submitted result as a different actor.
    Verify {
        /// Submitted task to verify.
        work: WorkItemId,
        /// Optional verification evidence.
        note: Option<String>,
        /// When the result was actually verified, if before the commit; absent means the
        /// commit time.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        occurred_at: Option<DateTime<Utc>>,
    },
    /// Attach new evidence without replacing an existing artifact.
    AttachArtifact {
        /// Work receiving evidence.
        work: WorkItemId,
        /// New artifact attributed to the caller.
        artifact: Artifact,
    },
    /// Stop enforcing a soft constraint as a human or service, recording who, when and why.
    WaiveDependency {
        /// Soft constraint to set aside.
        dependency: DependencyId,
        /// Non-empty justification.
        reason: String,
    },
    /// Enforce a waived soft constraint again; the operation records who, when and why.
    RestoreDependency {
        /// Waived constraint to enforce again.
        dependency: DependencyId,
        /// Non-empty justification.
        reason: String,
    },
    /// Record, as an independent human or service, that started work built on a rejected
    /// predecessor attempt remains valid on the predecessor's current attempt.
    RevalidateBasis {
        /// Successor whose basis on the edge was invalidated.
        work: WorkItemId,
        /// Provisional edge carrying the invalidated basis.
        dependency: DependencyId,
        /// Predecessor attempt the reviewer checked; it must still be current and not rejected.
        attempt: u32,
        /// Non-empty justification of the review.
        reason: String,
    },
    /// Resolve an open gate.
    Decide {
        /// Gate to resolve.
        decision: DecisionId,
        /// Non-empty outcome.
        outcome: String,
    },
    /// Link work to an external tracker object without granting it local authority.
    LinkExternal(ExternalLinkRequest),
    /// Remove one work item's link to an external object; the last link removes the record.
    UnlinkExternal {
        /// Work whose link is removed.
        work: WorkItemId,
        /// Linked external reference.
        reference: ExternalReferenceId,
    },
}

/// Link request recording the resolved reference identifier so the operation log is explicit.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExternalLinkRequest {
    /// Work receiving the link.
    pub work: WorkItemId,
    /// The recorded reference for this identity, or a new identifier when none exists.
    pub reference: ExternalReferenceId,
    /// Canonical provider identity.
    pub identity: ExternalIdentity,
    /// Display label; replaces the recorded label without changing identity.
    pub label: String,
    /// Optional display URL; replaces the recorded URL without changing identity.
    #[serde(default)]
    pub url: Option<String>,
    /// Relationship of the work to the external object.
    pub role: ExternalLinkRole,
    /// Optional external state reported by the caller; recorded as an observation only.
    #[serde(default)]
    pub observed: Option<ExternalState>,
}

/// Audit envelope for one successfully applied command.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Operation {
    /// Unique identity of this operation.
    pub id: OperationId,
    /// Revision the caller observed before applying the command.
    pub base_revision: u64,
    /// Next wrapping revision committed by the command.
    pub resulting_revision: u64,
    /// Principal making the change.
    pub actor: ActorId,
    /// Caller-supplied UTC operation time.
    pub timestamp: DateTime<Utc>,
    /// Semantic change whose result is stored.
    pub command: Command,
}

/// A rejected command or failed execution projection.
#[derive(Debug, Error)]
pub enum EngineError {
    /// A plan proposal no longer refers to the current revision.
    #[error("revision conflict: expected {expected}, current {actual}")]
    RevisionConflict {
        /// Revision the caller observed.
        expected: u64,
        /// Current revision.
        actual: u64,
    },
    /// A recorded plan change names an entity whose state differs from the change's `before`.
    #[error(
        "plan change to {collection}{} was computed against a different state; diff the proposal again",
        .id.as_ref().map_or_else(String::new, |id| format!(" {id}"))
    )]
    StaleChange {
        /// Collection of the entity.
        collection: String,
        /// Entity identity, absent for workspace-wide values.
        id: Option<String>,
    },
    /// A semantic difference could not be serialized.
    #[error(transparent)]
    Json(#[from] serde_json::Error),
    /// Local principal policy refuses this action.
    #[error("actor {actor} is not permitted to {action}")]
    ActorNotAllowed {
        /// Principal requesting the operation.
        actor: ActorId,
        /// Refused action.
        action: &'static str,
    },
    /// Invalid authoritative input.
    #[error(transparent)]
    Validation(#[from] ValidationError),
    /// Invalid command field or target category.
    #[error("invalid command for {entity}: {reason}")]
    InvalidCommand {
        /// Identifier of the affected value.
        entity: String,
        /// Failed invariant.
        reason: String,
    },
    /// Referenced work does not exist.
    #[error("work item {0} does not exist")]
    MissingWorkItem(WorkItemId),
    /// Referenced temporal constraint does not exist.
    #[error("dependency {0} does not exist")]
    MissingDependency(DependencyId),
    /// Referenced gate does not exist.
    #[error("decision {0} does not exist")]
    MissingDecision(DecisionId),
    /// Execution commands target a grouping or condition rather than executable work.
    #[error("work item {work} is a {kind:?}, not a task; only tasks accept execution commands")]
    NotATask {
        /// Targeted work item.
        work: WorkItemId,
        /// Non-executable kind of the target.
        kind: WorkKind,
    },
    /// Lifecycle or relation gates do not permit this transition at the operation time.
    #[error("work item {work} is not ready to {transition}: {}", crate::gates::summary(.unmet))]
    NotReady {
        /// Affected work.
        work: WorkItemId,
        /// Transition that was refused.
        transition: crate::Transition,
        /// The same structured conditions the gate report returns for this transition.
        unmet: Vec<crate::UnmetGate>,
    },
    /// A supplied occurrence time lies after the time the operation commits.
    #[error(
        "work item {key} cannot record its {transition} at {occurred_at}: that is after the operation commits at {committed_at}"
    )]
    OccurrenceInFuture {
        /// Affected work.
        work: WorkItemId,
        /// Human key of the affected work.
        key: Key,
        /// Transition whose event time was supplied.
        transition: crate::Transition,
        /// Supplied occurrence time.
        occurred_at: DateTime<Utc>,
        /// Commit time of the operation.
        committed_at: DateTime<Utc>,
    },
    /// A supplied occurrence time precedes the latest time already recorded for the task.
    #[error(
        "work item {key} cannot record its {transition} at {occurred_at}: that is before its previous recorded event at {previous_at}"
    )]
    OccurrenceBeforePrevious {
        /// Affected work.
        work: WorkItemId,
        /// Human key of the affected work.
        key: Key,
        /// Transition whose event time was supplied.
        transition: crate::Transition,
        /// Supplied occurrence time.
        occurred_at: DateTime<Utc>,
        /// Latest time already recorded in the task's execution history.
        previous_at: DateTime<Utc>,
    },
    /// Progress and submission follow an explicit start; a claim only reserves work.
    #[error("work item {0} has not started; start it before reporting progress or submitting")]
    NotStarted(WorkItemId),
    /// Started work has execution facts a release would orphan; only a handoff moves it.
    #[error(
        "work item {0} has started; started work cannot be released, only handed off by a human or service"
    )]
    AlreadyStarted(WorkItemId),
    /// A handoff named an owner the work no longer has.
    #[error("work item {work} is owned by {actual}, not {expected}; reload before handing it off")]
    OwnerMismatch {
        /// Affected work.
        work: WorkItemId,
        /// Owner named by the request.
        expected: ActorId,
        /// Current owner.
        actual: ActorId,
    },
    /// A reported blocker prevents execution.
    #[error("work item {work} is blocked: {reason}")]
    Blocked {
        /// Affected work.
        work: WorkItemId,
        /// Recorded blocker.
        reason: String,
    },
    /// Another principal owns the task.
    #[error("work item {work} is owned by {owner}")]
    OwnedByAnother {
        /// Affected work.
        work: WorkItemId,
        /// Principal allowed to submit the result.
        owner: ActorId,
    },
    /// The lifecycle does not permit the requested transition.
    #[error("invalid state transition for {work} from {status:?}")]
    InvalidTransition {
        /// Affected work.
        work: WorkItemId,
        /// Current lifecycle.
        status: WorkStatus,
    },
    /// Verification requires a submitted result.
    #[error("cannot verify work {0} that has not been submitted")]
    NotSubmitted(WorkItemId),
    /// Verification must be independent of the owner.
    #[error("the submitting actor cannot verify its own work {0}")]
    SelfVerification(WorkItemId),
    /// A reviewed change would relax a gate on the applying actor's own work or on the work
    /// waiting for its result.
    #[error("actor {actor} owns {work} and cannot apply a change that relaxes {relaxed}")]
    OwnGateRelaxed {
        /// Principal applying the change.
        actor: ActorId,
        /// The actor's own work the constraint concerns.
        work: Key,
        /// The constraint the change would relax.
        relaxed: Box<crate::RelaxedConstraint>,
    },
    /// A revalidation names a predecessor attempt that is not its current pending or verified one.
    #[error(
        "cannot revalidate {work} on dependency {dependency} against attempt #{requested}: the predecessor's current unrejected attempt is {}",
        .current.map_or_else(|| "none".to_string(), |n| format!("#{n}"))
    )]
    StaleAttempt {
        /// Successor named by the request.
        work: WorkItemId,
        /// Provisional edge named by the request.
        dependency: DependencyId,
        /// Attempt the reviewer checked.
        requested: u32,
        /// Predecessor's latest attempt if it is pending or verified.
        current: Option<u32>,
    },
    /// A resolved gate cannot be resolved again.
    #[error("decision {0} is not open")]
    DecisionNotOpen(DecisionId),
    /// Referenced external object is not recorded in this workspace.
    #[error("external reference {0} does not exist")]
    MissingExternalReference(ExternalReferenceId),
    /// The work item has no link to the external reference.
    #[error("work item {work} has no link to external reference {reference}")]
    MissingExternalLink {
        /// Work named by the request.
        work: WorkItemId,
        /// External reference named by the request.
        reference: ExternalReferenceId,
    },
    /// The work item already links the external reference.
    #[error("work item {work} already links external reference {reference}; unlink it first")]
    DuplicateExternalLink {
        /// Work named by the request.
        work: WorkItemId,
        /// External reference already linked.
        reference: ExternalReferenceId,
    },
    /// Another work item already tracks the external object.
    #[error("{identity} is already tracked by {owner}; unlink it before transferring tracking")]
    TrackingOwned {
        /// External identity whose tracking is owned.
        identity: String,
        /// Key of the work that tracks it.
        owner: Key,
    },
    /// Schedule projection failed.
    #[error(transparent)]
    Schedule(#[from] ScheduleError),
}