Skip to main content

BasicCpmPlanner

Struct BasicCpmPlanner 

Source
pub struct BasicCpmPlanner { /* private fields */ }
Expand description

Open-source CPM planner with file-aware locking.

Implementations§

Source§

impl BasicCpmPlanner

Source

pub fn new() -> Self

Construct a planner with a real-clock and the DEFAULT_TTL. Audit events are dropped on the floor (use Self::with_audit if you need them retained).

Source

pub fn with_audit(audit: Arc<dyn AuditSink>) -> Self

Construct a planner with the supplied audit sink and the default TTL. The real Utc::now is used as the clock.

Source

pub async fn simulate_plan( &self, plan_id: &PlanId, req: &SimulateRequest, ) -> Result<SimulationResult, PlannerError>

Simulate a stored plan’s graph read-only: nothing is written and no audit events are emitted. See crate::simulate::simulate.

Source

pub fn with_ttl(self, ttl: Duration) -> Self

Override the lock TTL. Useful for short-lived integration tests.

Source

pub fn with_max_ttl(self, max_ttl: Duration) -> Self

Override the maximum lease TTL a caller may request via ttl_seconds. Requested values above this are clamped down to it. The value is itself capped at MAX_TTL_CEILING (30 days).

Source

pub fn with_clock(self, clock: ClockFn) -> Self

Override the clock. Intended for deterministic TTL tests; production code should not call this.

Source

pub fn with_parts( audit: Arc<dyn AuditSink>, ttl: Duration, clock: ClockFn, ) -> Self

Full-parts constructor with an ephemeral in-memory store. Public for clients that want explicit control over every field at once.

Source

pub fn with_store(store: SqlitePlanStore, audit: Arc<dyn AuditSink>) -> Self

Construct a planner backed by the supplied (typically file-backed) store, with the default TTL and real clock. This is the durable, cross-process configuration used by the MCP server binary.

Source

pub fn with_store_parts( store: SqlitePlanStore, audit: Arc<dyn AuditSink>, ttl: Duration, clock: ClockFn, ) -> Self

Full-parts constructor over an explicit store.

Source

pub fn with_baseline_mode(self, mode: BaselineMode) -> Self

Set how execution treats a plan with no earned-value baseline yet: BaselineMode::Auto (the default) takes the first baseline automatically; BaselineMode::Refuse fails with BASELINE_REQUIRED until plan.baseline is called.

Source

pub fn with_project_root(self, root: ProjectRoot) -> Self

Give the planner a project root. Planner::status then reports definition_drift for that project’s file-backed variants, and Planner::fork_plan writes the new variant’s plan file there when the request names no root of its own.

Source

pub fn project_root(&self) -> Option<&ProjectRoot>

The project root given with Self::with_project_root, if any. The MCP server reads its root from here (one source of truth).

Source

pub fn compare_inputs( &self, req: &ComparePlansRequest, ) -> Result<Vec<(PlanId, String, PlanGraph)>, PlannerError>

The (plan id, label, head graph) inputs of a comparison, read in one store snapshot: plan_ids in the given order (an unnamed plan is labelled by its id) or every non-archived variant of the line plan (sorted by variant). Both or neither, duplicate ids, or a count outside 2..=crate::compare::MAX_COMPARE_VARIANTS is INVALID_GRAPH; an unknown plan or line is PLAN_NOT_FOUND. Pair it with the pure, CPU-bound crate::compare::compare.

Source

pub async fn record_audit(&self, event: AuditEvent)

Record one audit event emitted outside the planner’s own operations (e.g. plan.review). Sink failures are logged, never returned.

Trait Implementations§

Source§

impl Default for BasicCpmPlanner

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl Planner for BasicCpmPlanner

Source§

fn submit_plan<'life0, 'async_trait>( &'life0 self, graph: PlanGraph, ) -> Pin<Box<dyn Future<Output = Result<PlanId, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Submit a PlanGraph. Idempotent on (graph, caller_id); an identical resubmission returns the existing PlanId.
Source§

fn sync_plan<'life0, 'async_trait>( &'life0 self, req: SyncRequest, ) -> Pin<Box<dyn Future<Output = Result<SyncOutcome, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Register or update one variant of a named plan line. An unknown (project, name, variant) creates a plan (revision 1; the first variant of a line becomes its selected variant). A known variant whose graph is unchanged reports changed: false; a changed graph is revised in place exactly as Planner::revise_plan. Named plans dedup within their variant only, never against the global Planner::submit_plan dedup.
Source§

fn fork_plan<'life0, 'async_trait>( &'life0 self, req: ForkRequest, ) -> Pin<Box<dyn Future<Output = Result<SyncOutcome, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Fork the named variant req.plan_id: copy its head graph, apply req.edits in order (crate::edits::apply_edits) and register the result as the new draft (not selected) variant req.variant of the same line, exactly as a Planner::sync_plan that creates it. Read more
Source§

fn compare_plans<'life0, 'async_trait>( &'life0 self, req: ComparePlansRequest, ) -> Pin<Box<dyn Future<Output = Result<Comparison, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Compare plans on the scorecard (crate::compare::compare): either plan_ids (2..=16 distinct plans, in the given order; an unnamed plan is labelled by its id) or every non-archived variant of the line plan (sorted by variant). Giving both or neither, duplicate ids, or fewer than two or more than 16 variants is INVALID_GRAPH; an unknown plan or line is PLAN_NOT_FOUND. The Monte Carlo work budget is shared across variants. Read-only.
Source§

fn export_plan<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, plan_id: &'life1 PlanId, root: &'life2 ProjectRoot, path: Option<&'life3 str>, force: bool, ) -> Pin<Box<dyn Future<Output = Result<String, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait,

Write the head graph of plan_id under root and return the file’s root-relative path: to path when given (it must be of the form .cpm-planner/plans/<name>/<variant>.json), else to the named variant’s own file <name>/<variant>.json, which requires root to be the variant’s project. An unnamed plan requires path. An existing file is replaced, except (unless force) another variant’s tracked plan file, or this variant’s own file while it holds local edits never synced (drifted content no revision recorded), which are INVALID_PATH. The written file is NOT synced, except that exporting to the variant’s own tracked file records the written hash, so its drift is false afterwards.
Source§

fn baseline<'life0, 'async_trait>( &'life0 self, req: BaselineRequest, ) -> Pin<Box<dyn Future<Output = Result<BaselineOutcome, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Freeze the plan’s current CPM schedule and budgets as its next numbered earned-value baseline. Baselines belong to one plan (one variant): the first is number 1 and needs no reason; a re-baseline needs a non-blank reason (INVALID_GRAPH otherwise) and keeps actuals and snapshots. start defaults to now; calendar defaults to wall-clock hours on the first baseline and to the previous baseline’s calendar on a re-baseline. Gated like the execution methods. Audited as plan.ev.baselined.
Source§

fn ev<'life0, 'life1, 'async_trait>( &'life0 self, plan_id: &'life1 PlanId, as_of: Option<DateTime<Utc>>, ) -> Pin<Box<dyn Future<Output = Result<EvReport, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

The earned-value report of plan_id against its latest baseline as of as_of (default now). as_of is the PV status date; EV and AC reflect progress and actuals recorded up to the moment the call runs. AC includes the spend of removed and unbaselined deliverables; a removed baselined deliverable reports status removed and earns its frozen percent. Alerts compare the two latest stored non-backfilled snapshots (by as_of) of that baseline; the current reading is not one of them. NOT_BASELINED before the first baseline. Read-only. Read more
Source§

fn snapshot<'life0, 'async_trait>( &'life0 self, req: SnapshotRequest, ) -> Pin<Box<dyn Future<Output = Result<SnapshotOutcome, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Compute the earned-value report (as Planner::ev) and append it as a snapshot. Its alerts consider only readings up to its own (as_of, taken_at) position: this snapshot and the latest earlier one of the current baseline, skipping backfilled snapshots (as_of more than an hour before taken_at; flagged backfilled, and a backfilled snapshot raises no alerts itself, since its EV and AC are today’s). Returns the summary, the stored count and an export of the newest snapshots by as_of (an older backfill is counted but not listed). NOT_BASELINED before the first baseline. Gated like the execution methods. Read more
Source§

fn list_plans<'life0, 'life1, 'async_trait>( &'life0 self, project: &'life1 str, include_archived: bool, ) -> Pin<Box<dyn Future<Output = Result<Vec<PlanLineSummary>, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Every plan line of project, sorted by name, each with its variants sorted by variant name. Archived lines and variants are omitted unless include_archived.
Source§

fn resolve_plan<'life0, 'async_trait>( &'life0 self, plan: PlanRef, ) -> Pin<Box<dyn Future<Output = Result<PlanId, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Resolve a PlanRef to a stored plan’s id. PlanRef::Id is returned unchanged without a lookup; PlanRef::Line is the selected variant of the named line (an unknown line is PLAN_NOT_FOUND); PlanRef::Active is the project’s single active line, where active means unarchived with an unarchived selected variant whose plan is not all-complete. No active line is PlannerError::NoActivePlan; several are PlannerError::AmbiguousActivePlan naming every one.
Source§

fn list_active_plans<'life0, 'life1, 'async_trait>( &'life0 self, project: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<Vec<PlanLineSummary>, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

The project’s active plan lines: the rows Planner::list_plans returns with include_archived false, filtered to the names Planner::resolve_plan considers active, in the same order.
Source§

fn revision_graph<'life0, 'life1, 'async_trait>( &'life0 self, plan_id: &'life1 PlanId, revision: Option<u32>, ) -> Pin<Box<dyn Future<Output = Result<(u32, PlanGraph), PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

The graph of revision (the head when None) with its revision number. A plan never revised is at revision 1.
Source§

fn revise_plan<'life0, 'async_trait>( &'life0 self, req: ReviseRequest, ) -> Pin<Box<dyn Future<Output = Result<(u32, RevisionDiff), PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Replace a plan’s graph in place, carrying progress over (see crate::revise). Returns the new revision number and the diff. Works on named and unnamed plans alike; an archived variant or a variant of an archived line is ARCHIVE_REFUSED.
Source§

fn select_variant<'life0, 'life1, 'async_trait>( &'life0 self, plan_id: &'life1 PlanId, force: bool, ) -> Pin<Box<dyn Future<Output = Result<SelectOutcome, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Make the named variant owning plan_id its line’s selected (the only executable) variant. Progress carries over from the previously selected variant: every deliverable present in both with an identical canonical definition gets its Complete status and counters copied, unless one of its prerequisites in the new variant is not Complete after carrying (checked transitively), and then the new variant’s open deliverables are re-derived. Expired locks on the previous variant are reaped first; live ones refuse the selection with LOCK_HELD unless force, which releases them (audited). Selecting the selected variant is a no-op. An unnamed plan is PLAN_NOT_FOUND; an archived variant or line is ARCHIVE_REFUSED.
Source§

fn archive<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, project: &'life1 str, name: &'life2 str, variant: Option<&'life3 str>, archived: bool, force: bool, ) -> Pin<Box<dyn Future<Output = Result<(), PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait,

Archive (archived: true) or unarchive a whole plan line (variant: None) or one variant. The line flag and the variant flags are independent: a variant is effectively archived when its line or itself is, so unarchiving a line restores each variant as it was. Archived variants stay readable but are hidden from Planner::list_plans unless include_archived, and refuse sync and selection; every variant of an archived line refuses execution (ARCHIVE_REFUSED). Archiving the selected variant alone is ARCHIVE_REFUSED. Archiving a line whose selected variant holds live locks (expired ones are reaped first) is LOCK_HELD unless force, which releases them (audited). Unarchiving one variant of an archived line is ARCHIVE_REFUSED. Unknown line/variant is PLAN_NOT_FOUND.
Source§

fn acquire_cohort<'life0, 'async_trait>( &'life0 self, req: AcquireRequest, ) -> Pin<Box<dyn Future<Output = Result<Cohort, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Acquire up to max_count deliverables that are ready to run and have no conflicting owned_files claims (within the cohort and against all currently held locks; exclusive conflicts with anything, append/append may share). The returned Cohort carries one crate::plan::LockInfo per acquired deliverable, in the same order.
Source§

fn mark_status<'life0, 'async_trait>( &'life0 self, req: MarkStatusRequest, ) -> Pin<Box<dyn Future<Output = Result<Option<BaselineTaken>, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Update the lifecycle state of a deliverable. Setting Complete or Failed releases the lock; caller_id MUST be the lock holder or the call is rejected with PlannerError::LockNotHeld. Optional earned-value progress fields are validated as PlannerError::InvalidActuals (see MarkStatusRequest) and stored with the mark. A mark to InProgress or Complete is execution-starting: in the server’s auto baseline mode it takes the plan’s first baseline when none exists and returns it; in refuse mode it fails with PlannerError::BaselineRequired and changes nothing.
Source§

fn heartbeat<'life0, 'async_trait>( &'life0 self, req: HeartbeatRequest, ) -> Pin<Box<dyn Future<Output = Result<(), PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Refresh the TTL on a held lock. Rejected with PlannerError::LockNotHeld if caller_id is not the holder, or with PlannerError::LockExpired if the lock already lapsed.
Source§

fn get_plan<'life0, 'life1, 'async_trait>( &'life0 self, plan_id: &'life1 PlanId, ) -> Pin<Box<dyn Future<Output = Result<PlanDefinition, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Return the stored definition for plan_id — the PlanGraph exactly as submitted.
Source§

fn status<'life0, 'life1, 'async_trait>( &'life0 self, plan_id: &'life1 PlanId, ) -> Pin<Box<dyn Future<Output = Result<PlanStatus, PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Cheap read-only snapshot. Safe to poll on a timer. For a named variant it also reports the line name, variant, whether it is selected, and definition_drift (see PlanStatus::definition_drift).
Source§

fn accept<'life0, 'async_trait>( &'life0 self, req: AcceptRequest, ) -> Pin<Box<dyn Future<Output = Result<(), PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Manager/owner acceptance: mark a deliverable Complete without holding its lease. Requires all prerequisites Complete; a live lease held by another caller is refused (LOCK_HELD) unless override_lock is set. Always audited.
Source§

fn force_release<'life0, 'async_trait>( &'life0 self, req: ForceReleaseRequest, ) -> Pin<Box<dyn Future<Output = Result<(), PlannerError>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait,

Operator escape hatch: forcibly release a lock regardless of holder or TTL. Implementations MUST emit an audit event carrying reason.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WasmCompatSend for T
where T: Send,

Source§

impl<T> WasmCompatSync for T
where T: Sync,

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more