pub struct BasicCpmPlanner { /* private fields */ }Expand description
Open-source CPM planner with file-aware locking.
Implementations§
Source§impl BasicCpmPlanner
impl BasicCpmPlanner
Sourcepub fn new() -> Self
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).
Sourcepub fn with_audit(audit: Arc<dyn AuditSink>) -> Self
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.
Sourcepub async fn simulate_plan(
&self,
plan_id: &PlanId,
req: &SimulateRequest,
) -> Result<SimulationResult, PlannerError>
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.
Sourcepub fn with_ttl(self, ttl: Duration) -> Self
pub fn with_ttl(self, ttl: Duration) -> Self
Override the lock TTL. Useful for short-lived integration tests.
Sourcepub fn with_max_ttl(self, max_ttl: Duration) -> Self
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).
Sourcepub fn with_clock(self, clock: ClockFn) -> Self
pub fn with_clock(self, clock: ClockFn) -> Self
Override the clock. Intended for deterministic TTL tests; production code should not call this.
Sourcepub fn with_parts(
audit: Arc<dyn AuditSink>,
ttl: Duration,
clock: ClockFn,
) -> Self
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.
Sourcepub fn with_store(store: SqlitePlanStore, audit: Arc<dyn AuditSink>) -> Self
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.
Sourcepub fn with_store_parts(
store: SqlitePlanStore,
audit: Arc<dyn AuditSink>,
ttl: Duration,
clock: ClockFn,
) -> Self
pub fn with_store_parts( store: SqlitePlanStore, audit: Arc<dyn AuditSink>, ttl: Duration, clock: ClockFn, ) -> Self
Full-parts constructor over an explicit store.
Sourcepub fn with_project_root(self, root: ProjectRoot) -> Self
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.
Sourcepub fn project_root(&self) -> Option<&ProjectRoot>
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).
Sourcepub fn compare_inputs(
&self,
req: &ComparePlansRequest,
) -> Result<Vec<(PlanId, String, PlanGraph)>, PlannerError>
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.
Sourcepub async fn record_audit(&self, event: AuditEvent)
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
impl Default for BasicCpmPlanner
Source§impl Planner for BasicCpmPlanner
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,
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,
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,
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,
(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,
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,
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 moreSource§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,
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,
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,
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,
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,
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,
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,
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,
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 moreSource§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,
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,
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 moreSource§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,
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,
project, sorted by name, each with its variants
sorted by variant name. Archived lines and variants are omitted
unless include_archived.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,
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,
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,
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,
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,
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,
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,
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,
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,
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,
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<(), PlannerError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn mark_status<'life0, 'async_trait>(
&'life0 self,
req: MarkStatusRequest,
) -> Pin<Box<dyn Future<Output = Result<(), PlannerError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
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.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,
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,
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,
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,
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,
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,
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,
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,
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,
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,
reason.