From 11f4eeb3348dc6199d594b042447e82983f1add3 Mon Sep 17 00:00:00 2001 From: zzylol Date: Wed, 30 Sep 2026 16:03:26 +0000 Subject: [PATCH] refactor: rename PlanSpace to CandidatePostASAPDAGs Rename the public candidate collection, signatures, re-exports, rustdoc, examples, and tests without changing candidate storage or selection behavior. Remove the old type name rather than keeping an alias. Co-Authored-By: Claude Opus 5.5 --- .../src/accuracy/reconciliation.rs | 2 +- crates/asap-aware-mapping/src/cost_model.rs | 12 +- .../src/exact_composition.rs | 4 +- crates/asap-aware-mapping/src/explanation.rs | 30 ++--- crates/asap-aware-mapping/src/lib.rs | 16 +-- crates/asap-aware-mapping/src/recurrence.rs | 18 +-- crates/asap-aware-mapping/src/replacement.rs | 118 +++++++++--------- .../src/summary_maintenance_lifecycle.rs | 8 +- ...te_post_asap_dags_series_identity_heap.rs} | 0 .../tests/precompute_candidates.rs | 2 +- crates/devtools/src/bin/dag_export.rs | 4 +- crates/integration-tests/tests/cse.rs | 4 +- .../tests/exact_composition.rs | 6 +- crates/types/src/cost.rs | 2 +- crates/types/src/dag_export.rs | 8 +- crates/types/src/pre_asap/cse.rs | 2 +- docs/design_docs/architecture/README.md | 12 +- .../architecture/asap-aware-mapping.md | 4 +- .../architecture/asap-aware-plan-search.md | 2 +- .../evidence-dependent-candidates.md | 16 +-- .../architecture/input-output-workflow.md | 46 +++---- .../architecture/planner-runtime-contract.md | 6 +- docs/design_docs/concepts/planner-pipeline.md | 4 +- docs/design_docs/decisions/cse-cost-model.md | 8 +- .../physical-planning-and-deployment.md | 4 +- .../analytical-resource-cost.md | 2 +- .../ddsketch-quantile-ratios.md | 2 +- .../workload-demand-and-summary-lifecycle.md | 2 +- .../design_docs/proposals/operator-sharing.md | 2 +- .../asap-aware-mapping-architecture.md | 12 +- .../asap-aware-mapping-contracts.md | 20 +-- .../end-to-end-accuracy-guarantees.md | 2 +- .../develop_docs/extend-asap-aware-mapping.md | 8 +- docs/develop_docs/library-api.md | 40 +++--- .../target-candidate-api-migration.md | 4 +- docs/user_guide_docs/run-a-query.md | 2 +- 36 files changed, 218 insertions(+), 216 deletions(-) rename crates/asap-physical-operators/tests/{planspace_series_identity_heap.rs => candidate_post_asap_dags_series_identity_heap.rs} (100%) diff --git a/crates/asap-aware-mapping/src/accuracy/reconciliation.rs b/crates/asap-aware-mapping/src/accuracy/reconciliation.rs index c5c09abf..5b2688e6 100644 --- a/crates/asap-aware-mapping/src/accuracy/reconciliation.rs +++ b/crates/asap-aware-mapping/src/accuracy/reconciliation.rs @@ -141,7 +141,7 @@ //! priced with, reflecting "one more reference into a structure that's //! already being maintained" rather than "build a whole new one." //! -//! `PlanSpace::global_selection` treats this rewrite as a cross-group edge: +//! `CandidatePostASAPDAGs::global_selection` treats this rewrite as a cross-group edge: //! selecting it increments `rc`'s own `effective_consumer_count`, then lets //! that sibling group propagate the uses through its selected implementation. //! Accuracy edges are directed strictly from looser to tighter budgets, so diff --git a/crates/asap-aware-mapping/src/cost_model.rs b/crates/asap-aware-mapping/src/cost_model.rs index b2c42953..d8e96553 100644 --- a/crates/asap-aware-mapping/src/cost_model.rs +++ b/crates/asap-aware-mapping/src/cost_model.rs @@ -42,7 +42,7 @@ //! `docs/design_docs/cse-cost-model-decision.md` for the full design discussion (why //! cost-based, why not a full plan-search engine, the layering constraint //! that forces detection to stay cost-agnostic). -//! [`PlanSpace::cost_sorted`](crate::replacement::PlanSpace::cost_sorted) +//! [`CandidatePostASAPDAGs::cost_sorted`](crate::replacement::CandidatePostASAPDAGs::cost_sorted) //! (via [`crate::replacement`]'s own `cse_preference`) and //! [`DefaultCostModel::estimate_cost`] are this crate's own callers. @@ -139,7 +139,7 @@ pub struct ExactCompositionCostRequest<'a> { /// formula charges. pub summary: &'a SummaryNode, /// How many times this site actually runs once ancestors' own choices - /// are accounted for (see `PlanSpace::global_selection`). + /// are accounted for (see `CandidatePostASAPDAGs::global_selection`). pub effective_consumer_count: usize, } @@ -268,7 +268,7 @@ fn finite_rate(units_per_second: f64) -> Option { /// A CSE-detected, legality-gated shared subtree with two or more consumers /// — the unit [`CostModel::cse_share_decision`] decides over. Built by -/// [`PlanSpace::cost_sorted`](crate::replacement::PlanSpace::cost_sorted) +/// [`CandidatePostASAPDAGs::cost_sorted`](crate::replacement::CandidatePostASAPDAGs::cost_sorted) /// (via [`crate::replacement`]'s own `cse_preference`) the first time it /// needs a representative bound node for a subtree that /// [`asap_types::pre_asap::cse::share_common_subtrees`] already collapsed @@ -735,7 +735,7 @@ pub trait CostModel { /// [`ReplacementSubDAG`] candidate at `target` — a real `f64`, not just a /// relative rank, meant for a caller that wants to *display* "candidate A /// costs ≈ X, candidate B costs ≈ Y" (e.g. a DAG-visualization view built - /// on [`PlanSpace::cost_sorted`](crate::replacement::PlanSpace::cost_sorted)), + /// on [`CandidatePostASAPDAGs::cost_sorted`](crate::replacement::CandidatePostASAPDAGs::cost_sorted)), /// not just order candidates against each other — that ordering job /// already belongs to [`rank_candidates`](Self::rank_candidates) (for a /// [`SketchAlgorithmStrategy`](crate::replacement::SketchAlgorithmStrategy) @@ -932,7 +932,7 @@ pub trait CostModel { /// /// Default: every input unknown ([`ExactCompositionCostInputs::unknown`]) /// — unknown is never zero, and with no rate derivable - /// `PlanSpace::global_selection` keeps the conservative `KeepPreAsap` + /// `CandidatePostASAPDAGs::global_selection` keeps the conservative `KeepPreAsap` /// behavior for the site. A deployment that wants defaults must supply /// them here explicitly. fn exact_composition_cost_inputs( @@ -1103,7 +1103,7 @@ impl CostModel for DefaultCostModel { } } // A composed candidate is costed in cost-units-per-second by - // `PlanSpace::global_selection` against the child decision it + // `CandidatePostASAPDAGs::global_selection` against the child decision it // is committed with — a different unit from this structural // estimate, and unknowable here without that child. `NaN` // keeps it from ever out-ranking a real estimate by accident. diff --git a/crates/asap-aware-mapping/src/exact_composition.rs b/crates/asap-aware-mapping/src/exact_composition.rs index 58c77e39..21e8be0f 100644 --- a/crates/asap-aware-mapping/src/exact_composition.rs +++ b/crates/asap-aware-mapping/src/exact_composition.rs @@ -19,8 +19,8 @@ //! `realize_child` takes the head of the child's own ranking): a //! [`Replacement::ExactComposition`] carries only the child *target* //! (`ExactComposition::child_target`, the same `Rc` whose -//! `TargetSubDAGCandidates` in `PlanSpace` already holds every candidate for it). It is -//! [`PlanSpace::global_selection`](crate::replacement::PlanSpace::global_selection) +//! `TargetSubDAGCandidates` in `CandidatePostASAPDAGs` already holds every candidate for it). It is +//! [`CandidatePostASAPDAGs::global_selection`](crate::replacement::CandidatePostASAPDAGs::global_selection) //! that commits the compatible parent/child pair — so the child's own //! cost-model ranking, workload-wide effective consumer count, and shared //! `Rc` identity (one inner summary serving two outer folds) all stay diff --git a/crates/asap-aware-mapping/src/explanation.rs b/crates/asap-aware-mapping/src/explanation.rs index 07951823..b2855d9d 100644 --- a/crates/asap-aware-mapping/src/explanation.rs +++ b/crates/asap-aware-mapping/src/explanation.rs @@ -22,7 +22,7 @@ //! (issue #252) now *already* computes, for every //! [`TargetSubDAG`](crate::replacement::TargetSubDAG) in the workload, every //! semantically valid [`crate::replacement::ReplacementSubDAG`] a registered -//! [`ReplacementStrategy`] can propose — a [`PlanSpace`] of [`TargetSubDAGCandidates`]s. A +//! [`ReplacementStrategy`] can propose — a [`CandidatePostASAPDAGs`] of [`TargetSubDAGCandidates`]s. A //! rule re-deriving the same yes/no fact from scratch would be answering a //! question the search already answered, via a second, independently //! maintained traversal that has to keep agreeing with the first one. @@ -39,7 +39,7 @@ //! genuine alternative to the status quo (share this already-shared subtree //! instead of recomputing it at every consumer), *is* an applicability //! finding — [`explain_replacements`] and -//! [`explain_replacements_with`] just translate [`PlanSpace`]'s +//! [`explain_replacements_with`] just translate [`CandidatePostASAPDAGs`]'s //! [`TargetSubDAGCandidates`]s into that shape: //! //! - [`ExplanationKind::SketchApproximation`] — the `TargetSubDAG`'s @@ -88,14 +88,14 @@ //! (`fn optimization(&self) -> ExplanationKind` + `fn evaluate(&self, roots) //! -> Vec`), the same shape [`crate::cost_model::CostModel`] //! and [`crate::replacement::Matcher`] use elsewhere in this crate. Once -//! findings are a *view* over [`PlanSpace`] rather than an independent +//! findings are a *view* over [`CandidatePostASAPDAGs`] rather than an independent //! computation, that trait would be a second extension point answering a //! question [`ReplacementStrategy`] (issue #251) already answers: "does this //! `TargetSubDAG` have an alternative worth reporting, and why". A caller who //! wants a new optimization represented as a finding needs a new //! `impl ReplacementStrategy` wired into //! [`crate::replacement::search_workload_with`]'s strategy set *regardless* -//! (that's the only way its candidates end up in the [`PlanSpace`] this +//! (that's the only way its candidates end up in the [`CandidatePostASAPDAGs`] this //! module reads) — adding an `ApplicabilityRule` too would mean maintaining //! two extension points for the same new capability, one of which (the rule) //! would just be re-describing candidates the other (the strategy) already @@ -123,7 +123,7 @@ //! [`crate::replacement`] now instead of here. //! 2. **A node reachable via more than one path is one finding, not one per //! path.** [`TargetSubDAGCandidates`]s are keyed by `Rc` pointer identity in -//! [`PlanSpace`]'s internal map — there is exactly one group per distinct +//! [`CandidatePostASAPDAGs`]'s internal map — there is exactly one group per distinct //! `Rc`, full stop, so a shared `Aggregate` reached via two different //! `BinaryOp` branches (or two different workload roots) is exactly one //! group, hence at most one [`ExplanationKind::SketchApproximation`] @@ -131,16 +131,16 @@ //! [`tests::a_shared_sketchable_aggregate_is_reported_only_once`] pins //! this directly. //! -//! ## One thing [`PlanSpace`] doesn't carry that this module still needs: +//! ## One thing [`CandidatePostASAPDAGs`] doesn't carry that this module still needs: //! human-readable `location` text //! -//! [`TargetSubDAGCandidates`]/[`PlanSpace`] deliberately track only `Rc` +//! [`TargetSubDAGCandidates`]/[`CandidatePostASAPDAGs`] deliberately track only `Rc` //! pointer identity — the currency the search itself needs — not //! caller-facing prose. [`ReplacementExplanation::location`] is prose (a //! breadcrumb like `root "dash_a" > lhs`), so this module keeps one small, //! self-contained walk of its own, [`collect_locations`], whose *only* job //! is turning "this `Rc`" into "the human-readable place(s) it occurs" for a -//! finding already decided by [`PlanSpace`]. This is not a reincarnation of +//! finding already decided by [`CandidatePostASAPDAGs`]. This is not a reincarnation of //! the deleted rule traversal: it makes no applicability decision (it runs //! the same regardless of what any strategy found), and duplicating this //! small, self-contained shape rather than threading location strings @@ -161,7 +161,7 @@ //! //! | Catalog entry | Status | Where a future `ExplanationKind` would come from | //! |---|---|---| -//! | Semantic-equivalent rewriting (e.g. `avg` → `sum`/`count`) | [`AvgToSumOverCountStrategy`](crate::rewrite::AvgToSumOverCountStrategy) exists and is wired into `default_strategies()` (issue #253) — but still no `ExplanationKind` of its own below, since this table is about *direct* findings for a catalog entry, and this strategy's whole point is indirect: its `Replacement::Rewrite` candidate exposes `sum`/`count` as independently bindable discovered targets, which can then earn `CommonSubexpressionReuse` findings when the workload actually reuses them | A dedicated variant would need `findings_from_plan_space` to recognize a `LogicalRewrite`-provenance candidate as a finding in its own right, not just rely on what it exposes downstream | +//! | Semantic-equivalent rewriting (e.g. `avg` → `sum`/`count`) | [`AvgToSumOverCountStrategy`](crate::rewrite::AvgToSumOverCountStrategy) exists and is wired into `default_strategies()` (issue #253) — but still no `ExplanationKind` of its own below, since this table is about *direct* findings for a catalog entry, and this strategy's whole point is indirect: its `Replacement::Rewrite` candidate exposes `sum`/`count` as independently bindable discovered targets, which can then earn `CommonSubexpressionReuse` findings when the workload actually reuses them | A dedicated variant would need `findings_from_candidate_dags` to recognize a `LogicalRewrite`-provenance candidate as a finding in its own right, not just rely on what it exposes downstream | //! | Roll-ups (fine-to-coarse group-by reuse) | [`RollupStrategy`](crate::rollup::RollupStrategy), derived from workload siblings after CSE/target discovery (issue #254) | Any `Replacement::Rewrite` candidate that rolls a coarse aggregate up from a compatible finer aggregate | //! | Wavelets/OMP | Params type exists (`WaveletKind`/`WaveletParams`), reachable only via a deployment `CostModel::realize_extension` (no core `AggIntent` dispatch picks it) | A `ReplacementStrategy` that inspects a deployment's own `CostModel`, once some intent shape actually maps to `Realization::Wavelet` | //! | Sampling | Same story as Wavelets: `SamplingKind`/`SamplingParams` exist, unreachable from core dispatch | Same hook as Wavelets, for `Realization::Sample` | @@ -179,7 +179,7 @@ //! [`Replacement::Rewrite`]: crate::replacement::Replacement::Rewrite //! [`SketchAlgorithmStrategy`]: crate::replacement::SketchAlgorithmStrategy //! [`SharedSubtreeStrategy`]: crate::replacement::SharedSubtreeStrategy -//! [`PlanSpace`]: crate::replacement::PlanSpace +//! [`CandidatePostASAPDAGs`]: crate::replacement::CandidatePostASAPDAGs //! [`TargetSubDAGCandidates`]: crate::replacement::TargetSubDAGCandidates use std::collections::HashMap; @@ -191,7 +191,7 @@ use asap_types::pre_asap::cse::{structural_hash, HashCache}; use asap_types::pre_asap::query_expr::QueryExpr; use crate::replacement::{ - self, PlanSpace, Replacement, ReplacementStrategy, TargetSubDAGCandidates, + self, CandidatePostASAPDAGs, Replacement, ReplacementStrategy, TargetSubDAGCandidates, }; /// Which kind of replacement a [`ReplacementExplanation`] is about. @@ -287,7 +287,7 @@ pub fn explain_replacements_with<'s, Id: Display>( .map(|(id, expr)| (id.to_string(), Rc::new(expr))) .collect(); let space = replacement::search_workload_with(ided, strategies); - findings_from_plan_space(&space) + findings_from_candidate_dags(&space) } /// Translate every discovered [`TargetSubDAGCandidates`] in `space` into zero, one, or two @@ -301,7 +301,9 @@ pub fn explain_replacements_with<'s, Id: Display>( /// so this function (and [`collect_locations`], which formats `id` with /// [`std::fmt::Debug`] for the breadcrumb text) doesn't need its own generic /// `Id` bound. -fn findings_from_plan_space(space: &PlanSpace) -> Vec { +fn findings_from_candidate_dags( + space: &CandidatePostASAPDAGs, +) -> Vec { let locations = collect_locations(&space.roots); // One cache for the whole pass, mirroring `dag_export::export`'s own // `HashCache` reuse — this is a bottom-up pass over every discovered @@ -425,7 +427,7 @@ fn is_sketch_realization(node: &SummaryNode) -> bool { // ── location breadcrumbs ───────────────────────────────────────────────── /// Build `location` text for every distinct `TargetSubDAG` reachable from -/// `roots` — see the module docs' "One thing `PlanSpace` doesn't carry" +/// `roots` — see the module docs' "One thing `CandidatePostASAPDAGs` doesn't carry" /// section for why this module needs its own small walk for this. Returns /// every breadcrumb path that reaches a given `Rc`, not just the first: a /// shared node referenced from two workload roots (or two branches of one diff --git a/crates/asap-aware-mapping/src/lib.rs b/crates/asap-aware-mapping/src/lib.rs index 921f504e..9c876a5a 100644 --- a/crates/asap-aware-mapping/src/lib.rs +++ b/crates/asap-aware-mapping/src/lib.rs @@ -29,7 +29,7 @@ //! //! ## Planning workflows //! -//! Candidate search returns [`PlanSpace`](replacement::PlanSpace), a compact +//! Candidate search returns [`CandidatePostASAPDAGs`](replacement::CandidatePostASAPDAGs), a compact //! logical choice space with one [`TargetSubDAGCandidates`] per target sub-DAG. //! [`ReplacementStrategy`] implementations propose local alternatives; search //! applies the applicable semantic and accuracy checks. Candidate presence does @@ -37,9 +37,9 @@ //! //! Integrators choose among these workflows: //! -//! - Inspect the candidate space, optionally using [`PlanSpace::cost_sorted`] +//! - Inspect the candidate space, optionally using [`CandidatePostASAPDAGs::cost_sorted`] //! to obtain ranked views, and perform selection downstream. -//! - Call [`PlanSpace::global_selection`] once for the workload, then +//! - Call [`CandidatePostASAPDAGs::global_selection`] once for the workload, then //! [`GlobalSelection::assemble_selected_dag`] for each query root. This //! coordinates logical choices and preserves shared nodes, but makes no //! summary-maintenance lifecycle decision. @@ -73,7 +73,7 @@ //! where, reusing the candidate's own rationale rather than inventing new //! prose), meant for the same downstream consumer (e.g. a //! DAG-visualization view) the crate doc's planning workflows section above -//! already names for [`replacement::PlanSpace`] itself. Superseded PR +//! already names for [`replacement::CandidatePostASAPDAGs`] itself. Superseded PR //! #247's own rule-based traversal, which re-walked the tree once per //! optimization before [`replacement::search_workload`] existed to read //! from instead — see that module's docs for the full reframing. @@ -105,7 +105,7 @@ //! pair under the same grouping, re-divided back by a wrapping `Project`, //! so those *are* ordinary mergeable accumulators sharing/sketching can //! reach. It only reshapes; [`replacement::search_workload`]'s cost-based -//! ranking (or a downstream consumer reading [`replacement::PlanSpace`]) +//! ranking (or a downstream consumer reading [`replacement::CandidatePostASAPDAGs`]) //! is what decides whether the reshaped form is actually worth picking, //! the same propose-don't-decide split every other strategy here keeps. //! @@ -208,9 +208,9 @@ pub use recurrence::{ }; pub use replacement::{ default_strategies, default_strategies_with, search_workload, search_workload_with, - search_workload_with_targets, summary_candidates, CompositionDecision, GlobalSelection, - Matcher, PlanSpace, Proposals, RankedTargetSubDAGCandidates, Realization, RealizationError, - RecurrenceProfileMap, RejectedCandidate, Replacement, ReplacementProvenance, + search_workload_with_targets, summary_candidates, CandidatePostASAPDAGs, CompositionDecision, + GlobalSelection, Matcher, Proposals, RankedTargetSubDAGCandidates, Realization, + RealizationError, RecurrenceProfileMap, RejectedCandidate, Replacement, ReplacementProvenance, ReplacementStrategy, ReplacementSubDAG, SharedSubtreeStrategy, SketchAlgorithmStrategy, TargetSubDAG, TargetSubDAGCandidates, TargetSubDAGSelection, MAX_SEARCH_ITERATIONS, }; diff --git a/crates/asap-aware-mapping/src/recurrence.rs b/crates/asap-aware-mapping/src/recurrence.rs index e01c10df..125906ef 100644 --- a/crates/asap-aware-mapping/src/recurrence.rs +++ b/crates/asap-aware-mapping/src/recurrence.rs @@ -75,7 +75,7 @@ //! //! - [`EvaluationRate`]: derived from [`asap_types::workload::RepeatingEntry::demand`] //! values of every repeating consumer reaching a target (via -//! [`evaluation_rate_of`], or [`crate::replacement::PlanSpace::recurrence_profiles`] +//! [`evaluation_rate_of`], or [`crate::replacement::CandidatePostASAPDAGs::recurrence_profiles`] //! for a whole workload). A one-shot ([`asap_types::workload::BatchEntry`]) //! consumer contributes to [`RecurrenceProfile::one_shot_consumers`] //! instead, never to this rate. @@ -216,18 +216,18 @@ pub enum RecurrenceError { CostRate with a one-shot Cost without distorting the comparison" )] InvalidHorizon(Horizon), - /// [`crate::replacement::PlanSpace::recurrence_profiles`] was called + /// [`crate::replacement::CandidatePostASAPDAGs::recurrence_profiles`] was called /// with a `root_recurrence` slice whose length doesn't match the - /// `PlanSpace`'s own root count — a caller error, but recoverable + /// `CandidatePostASAPDAGs`'s own root count — a caller error, but recoverable /// (this method's whole signature promises a `Result`, so this is /// reported the same way every other input-validation failure is, /// never a panic). #[error( "recurrence_profiles: root_recurrence must have one entry per root, in the same order \ - PlanSpace::roots is in (got {got} entries for {expected} roots)" + CandidatePostASAPDAGs::roots is in (got {got} entries for {expected} roots)" )] RootCountMismatch { - /// `PlanSpace::roots.len()`. + /// `CandidatePostASAPDAGs::roots.len()`. expected: usize, /// `root_recurrence.len()`. got: usize, @@ -239,7 +239,7 @@ pub enum RecurrenceError { /// applied at every point an `UpdateRate` enters a [`RecurrenceProfile`] /// ([`RecurrenceProfile::with_update_rate`], /// [`update_rate_from_data_workload`], -/// [`crate::replacement::PlanSpace::recurrence_profiles`]'s own parameter) +/// [`crate::replacement::CandidatePostASAPDAGs::recurrence_profiles`]'s own parameter) /// *and*, as a backstop that can't be bypassed by constructing a /// `RecurrenceProfile` via its public fields directly, inside [`decide`] /// itself before any comparison uses it. @@ -373,7 +373,7 @@ impl RecurrenceProfile { } /// How one workload root recurs — the opaque per-root tag -/// [`crate::replacement::PlanSpace::recurrence_profiles`] threads down to +/// [`crate::replacement::CandidatePostASAPDAGs::recurrence_profiles`] threads down to /// every target reachable from that root. Mirrors /// [`asap_types::workload::QueryWorkload`]'s own `query_batch` (one-shot) /// vs. `repeating_queries` (an interval each) split, but at the @@ -1126,7 +1126,7 @@ mod tests { } } - // ── multiple roots sharing a sub-DAG, via PlanSpace ────────────────── + // ── multiple roots sharing a sub-DAG, via CandidatePostASAPDAGs ────────────────── use crate::replacement::search_workload; use asap_types::pre_asap::agg_intent::AggIntent; @@ -1186,7 +1186,7 @@ mod tests { /// Three workload roots share one underlying `sum_agg()` sub-DAG: two /// repeating consumers with different intervals, one one-shot batch - /// consumer. `PlanSpace::recurrence_profiles` must aggregate all three + /// consumer. `CandidatePostASAPDAGs::recurrence_profiles` must aggregate all three /// onto the shared sub-DAG's own profile: `evaluation_rate = 1/t1 + /// 1/t2`, `one_shot_consumers = 1` — issue #287's "support a shared /// sub-DAG consumed by queries with different intervals" and "multiple diff --git a/crates/asap-aware-mapping/src/replacement.rs b/crates/asap-aware-mapping/src/replacement.rs index 9dee4ba8..57600188 100644 --- a/crates/asap-aware-mapping/src/replacement.rs +++ b/crates/asap-aware-mapping/src/replacement.rs @@ -54,7 +54,7 @@ //! //! A caller may inspect local replacements, but taking the first candidate //! does not establish a compatible workload plan or physical deployability. -//! For Planner-owned logical selection, call [`PlanSpace::global_selection`] +//! For Planner-owned logical selection, call [`CandidatePostASAPDAGs::global_selection`] //! once and [`GlobalSelection::assemble_selected_dag`] for each wanted query //! root. Alternatively, use the summary-maintenance-lifecycle-aware helpers //! when Planner should also compare maintenance against raw recomputation. @@ -107,7 +107,7 @@ //! `TargetSubDAG` discovery pass" — describing work deliberately left for a //! future Cascades/Volcano-style search engine (PR #263, //! `feat/cascades-search-252`, over the [`ReplacementStrategy`] extension -//! point above). That engine is [`PlanSpace`]/[`TargetSubDAGCandidates`]/ +//! point above). That engine is [`CandidatePostASAPDAGs`]/[`TargetSubDAGCandidates`]/ //! [`search_workload`]/[`search_workload_with`] below, merged into this //! module rather than kept as a separate `search` module — the same "one //! module, one step" reasoning the top of this file already uses for @@ -146,7 +146,7 @@ //! its own `Rc` pointer identity — the same currency //! [`asap_types::pre_asap::cse::share_common_subtrees`] already //! established across the workload) holding every -//! [`ReplacementSubDAG`] alternative discovered for it. [`PlanSpace`] is +//! [`ReplacementSubDAG`] alternative discovered for it. [`CandidatePostASAPDAGs`] is //! a collection of these groups, keyed by `TargetSubDAG` — a candidate //! "plan" is never materialized as a distinct top-level `Rc` //! at all; two logically-different overall choices at two different @@ -235,7 +235,7 @@ //! //! ### Cost-based final selection — reusing `CostModel`, not a second interface //! -//! [`PlanSpace::cost_sorted`] is the `sorted_by(cost_model)` step, and it +//! [`CandidatePostASAPDAGs::cost_sorted`] is the `sorted_by(cost_model)` step, and it //! reuses this crate's existing [`CostModel`] trait rather than inventing a //! second cost interface (`docs/design_docs/cse-cost-model-decision.md`, //! issue #237, explicitly reasoned about *why* a narrow, direct cost @@ -259,7 +259,7 @@ //! //! ## Whole-plan (cross-group) selection — issue #271 //! -//! [`PlanSpace::cost_sorted`] above ranks every group's candidates +//! [`CandidatePostASAPDAGs::cost_sorted`] above ranks every group's candidates //! independently: it never lets one group's choice influence how another //! group is costed. That's the right behavior when groups genuinely don't //! interact — which both shipped strategies' one-round convergence (see @@ -277,7 +277,7 @@ //! per-group ranking has no way to see this — it only ever looks at one //! group's own `candidates`, in isolation. //! -//! [`PlanSpace::global_selection`] is that missing step: a single +//! [`CandidatePostASAPDAGs::global_selection`] is that missing step: a single //! **top-down dynamic-programming pass** over the discovered sites, //! processed in the topological order [`topological_order`] computes over a //! small [`ReferenceGraph`] built for exactly this purpose (parent before @@ -305,9 +305,9 @@ //! subproblems (a site reachable through more than one parent path is //! solved once, memoized in `effective_uses`, and reused for every path //! into it) combined via a real recurrence — not just the MEMO-group -//! sharing [`PlanSpace`] itself already does for *storing* candidates. That +//! sharing [`CandidatePostASAPDAGs`] itself already does for *storing* candidates. That //! distinction is exactly what issue #271 raised: this module already looks -//! like a Cascades/Volcano MEMO, but [`PlanSpace::cost_sorted`] alone never +//! like a Cascades/Volcano MEMO, but [`CandidatePostASAPDAGs::cost_sorted`] alone never //! actually performed this composition step; `global_selection` is that //! step, added alongside `cost_sorted` rather than replacing it (both stay //! available — see [`RankedTargetSubDAGCandidates`] vs. [`TargetSubDAGSelection`]'s own docs for when @@ -483,7 +483,7 @@ pub enum Replacement { /// decision across an explicit update/readout boundary (issue #171): /// `ValueOperationAtQueryTime` over a child's summary readout, or /// `ValueOperationAtIngestionTime` feeding a maintained summary above. Carries only a - /// reference to the child target — [`PlanSpace::global_selection`] + /// reference to the child target — [`CandidatePostASAPDAGs::global_selection`] /// commits the compatible parent/child pair and /// [`GlobalSelection::assemble_selected_dag`] links it into one validated /// `SummaryNode`. See [`crate::exact_composition`]. @@ -1715,7 +1715,7 @@ pub(crate) fn describe_intent(intent: &AggIntent) -> String { /// every candidate via [`SketchAlgorithmStrategy::replacements`], keep the /// `cost_model`-preferred (first) one, and fall back to [`keep_pre_asap`] /// when there's no candidate at all — **not** a general single-answer API -/// for a whole workload. Use [`PlanSpace::global_selection`] and DAG assembly +/// for a whole workload. Use [`CandidatePostASAPDAGs::global_selection`] and DAG assembly /// for coordinated logical selection; physical deployment remains downstream. /// `root` must already be the caller's own /// `Rc`, never fabricated per call, so this never allocates beyond what the @@ -3754,7 +3754,7 @@ impl ReplacementStrategy for SharedSubtreeStrategy { } } -// ── Workload-wide search: TargetSubDAGCandidates / PlanSpace / search_workload ────────── +// ── Workload-wide search: TargetSubDAGCandidates / CandidatePostASAPDAGs / search_workload ────────── // // Merged in from the former `search.rs` (issue #252, part of #33) — see this // file's own top-level "Workload-wide search" doc section for the full @@ -3771,13 +3771,13 @@ pub const MAX_SEARCH_ITERATIONS: usize = 1_000; /// Candidates for one distinct [`TargetSubDAG`] (its /// own `target` `Rc`, keyed by pointer identity in -/// [`PlanSpace`]'s internal map — never re-derived by value) plus every +/// [`CandidatePostASAPDAGs`]'s internal map — never re-derived by value) plus every /// [`ReplacementSubDAG`] alternative any registered [`ReplacementStrategy`] /// proposed for it. /// /// `candidates` is deliberately *not* required to be non-empty — a /// `TargetSubDAG` no registered strategy has an opinion on still gets a -/// group (with an empty candidate list), so [`PlanSpace`] always has +/// group (with an empty candidate list), so [`CandidatePostASAPDAGs`] always has /// exactly one group per discovered `TargetSubDAG`, not "one group per /// `TargetSubDAG` something matched". #[derive(Debug, Clone)] @@ -3788,13 +3788,13 @@ pub struct TargetSubDAGCandidates { /// reference this exact `Rc` — see [`discover_targets`]. pub consumer_count: usize, /// Every distinct alternative discovered for `target`, in discovery - /// order (not ranked — see [`PlanSpace::cost_sorted`] for the ranked + /// order (not ranked — see [`CandidatePostASAPDAGs::cost_sorted`] for the ranked /// view). pub candidates: Vec, /// Every candidate a strategy considered for `target` but refused on /// accuracy-legality grounds (issue #172), plus any `candidates` entry /// the root-target check ([`search_workload_with_targets`]) moved here. - /// Never ranked — [`PlanSpace::cost_sorted`]/[`PlanSpace::global_selection`] + /// Never ranked — [`CandidatePostASAPDAGs::cost_sorted`]/[`CandidatePostASAPDAGs::global_selection`] /// read only `candidates`, so a [`CostModel`] cannot resurrect one. pub rejected: Vec, } @@ -3908,21 +3908,21 @@ fn is_duplicate_summary(_existing: &Rc, _candidate: &Rc` whose /// group holds its alternatives. -pub struct PlanSpace { +pub struct CandidatePostASAPDAGs { /// The workload's roots, after the one `share_common_subtrees` pass /// [`search_workload_with`] runs up front — the same post-CSE roots /// every `TargetSubDAG` in `groups` was discovered from. pub roots: Vec<(Id, Rc)>, groups: HashMap<*const QueryExpr, TargetSubDAGCandidates>, - /// Discovery order — stable iteration for [`PlanSpace::target_subdag_candidates`]/ - /// [`PlanSpace::cost_sorted`], since `HashMap` iteration order isn't. + /// Discovery order — stable iteration for [`CandidatePostASAPDAGs::target_subdag_candidates`]/ + /// [`CandidatePostASAPDAGs::cost_sorted`], since `HashMap` iteration order isn't. order: Vec<*const QueryExpr>, /// Composition proofs are computed with the search model, then retained /// through costing and DAG assembly so no later default can replace it. @@ -3936,7 +3936,7 @@ struct PreparedComposition { plan: Rc, } -impl PlanSpace { +impl CandidatePostASAPDAGs { fn prepare_compositions( &mut self, accuracy: &dyn AccuracyModel, @@ -4006,7 +4006,7 @@ pub struct CandidateDagInventory { type CandidateDagChoice<'a> = (Option<&'a ReplacementSubDAG>, Option>); -impl PlanSpace { +impl CandidatePostASAPDAGs { pub fn enumerate_candidate_dags( &self, expansion_limit: usize, @@ -4287,7 +4287,7 @@ impl CandidateCostOverrides { } } -impl PlanSpace { +impl CandidatePostASAPDAGs { /// One candidate set per discovered target sub-DAG, in discovery order. pub fn target_subdag_candidates(&self) -> impl Iterator { self.order.iter().map(move |ptr| &self.groups[ptr]) @@ -4425,16 +4425,16 @@ impl PlanSpace { // ── Recurrence-aware cost context (issue #287) ────────────────────────── /// One [`RecurrenceProfile`] per discovered [`TargetSubDAGCandidates`] target, built by -/// [`PlanSpace::recurrence_profiles`] — the "carry `RepeatingEntry.demand` +/// [`CandidatePostASAPDAGs::recurrence_profiles`] — the "carry `RepeatingEntry.demand` /// and relevant `DataWorkload` into ASAP-aware search/cost context" /// half of issue #287. Looked up by `Rc` pointer identity, the same -/// currency [`PlanSpace::candidates_for_target`]/[`GlobalSelection::for_target`] already +/// currency [`CandidatePostASAPDAGs::candidates_for_target`]/[`GlobalSelection::for_target`] already /// use. /// Holds an owned `Rc` clone alongside each profile (not just its /// raw pointer) so this map keeps every node it describes alive for as long /// as the map itself lives — a `RecurrenceProfileMap` is safe to outlive the -/// `PlanSpace` it was built from. Without this, a raw `*const QueryExpr` key -/// could, after the originating `PlanSpace` (the only other owner of those +/// `CandidatePostASAPDAGs` it was built from. Without this, a raw `*const QueryExpr` key +/// could, after the originating `CandidatePostASAPDAGs` (the only other owner of those /// `Rc`s) is dropped, collide with an unrelated, later allocation that /// happens to reuse the same freed address — silently returning a stale /// profile for the wrong node (issue #287 review, bug 4). @@ -4446,7 +4446,7 @@ pub struct RecurrenceProfileMap { impl RecurrenceProfileMap { /// The [`RecurrenceProfile`] for `target`, or /// [`RecurrenceProfile::EMPTY`] when `target` wasn't a discovered site - /// in the [`PlanSpace`] this map was built from (or carried no + /// in the [`CandidatePostASAPDAGs`] this map was built from (or carried no /// recurring/one-shot/update-rate metadata at all) — always a valid, /// "no metadata" answer, never a panic. pub fn for_target(&self, target: &Rc) -> RecurrenceProfile { @@ -4457,7 +4457,7 @@ impl RecurrenceProfileMap { } } -impl PlanSpace { +impl CandidatePostASAPDAGs { /// Build one [`RecurrenceProfile`] per discovered site, by walking every /// root's whole reachable sub-DAG (the same relational-skeleton /// traversal [`discover_targets`] itself used to discover those sites) @@ -4503,7 +4503,7 @@ impl PlanSpace { /// evaluated twice. This supplies recurrence-aware selection with the /// effective structural execution rate rather than mere reachability. /// - /// **Unreachable sites**: [`PlanSpace`] can contain a site no root's own + /// **Unreachable sites**: [`CandidatePostASAPDAGs`] can contain a site no root's own /// structural tree actually reaches — e.g. one only ever produced by a /// [`Replacement::Rewrite`] candidate a [`ReplacementStrategy`] invented /// (this walk only follows [`TargetSubDAGCandidates::target`]'s own structural @@ -4629,7 +4629,7 @@ impl PlanSpace { &self, workload: &QueryWorkload, data_workload: Option<&DataWorkload>, - // For each `PlanSpace::roots[i]`, the explicit index of its + // For each `CandidatePostASAPDAGs::roots[i]`, the explicit index of its // corresponding normalized workload entry. root_workload_entries: &[usize], now_ms: u64, @@ -4750,7 +4750,7 @@ impl PlanSpace { /// Record `times` occurrences of `recurrence` against `ptr` — `times > 1` /// when a single parent structurally references `ptr` more than once (see -/// [`PlanSpace::recurrence_profiles`]'s own doc on edge multiplicity). +/// [`CandidatePostASAPDAGs::recurrence_profiles`]'s own doc on edge multiplicity). /// A no-op for `times == 0` (an `Rc` returned as a `direct_child_counts` /// child always has `edge_count >= 1` in practice, but this keeps the /// helper correct regardless). @@ -4778,7 +4778,7 @@ fn contribute( } /// One [`TargetSubDAGCandidates`]'s candidates, ranked best-first by -/// [`PlanSpace::cost_sorted`]. +/// [`CandidatePostASAPDAGs::cost_sorted`]. #[derive(Debug)] pub struct RankedTargetSubDAGCandidates<'a> { pub target: &'a Rc, @@ -4982,12 +4982,12 @@ fn summary_grouping(node: &SummaryNode) -> Option<&GroupingStrategy> { // ── global_selection ───────────────────────────────────────────────────── /// One target sub-DAG's selected choice and usage information — the answer -/// [`PlanSpace::global_selection`] commits to for one site, after folding in +/// [`CandidatePostASAPDAGs::global_selection`] commits to for one site, after folding in /// every ancestor [`SharedSubtreeStrategy`] decision on the path from a /// workload root to this site. See the module docs' "Whole-plan /// (cross-group) selection" section for the full recurrence. /// -/// Contrast with [`RankedTargetSubDAGCandidates`] ([`PlanSpace::cost_sorted`]'s output): +/// Contrast with [`RankedTargetSubDAGCandidates`] ([`CandidatePostASAPDAGs::cost_sorted`]'s output): /// that ranks every candidate for one target in isolation and never commits /// to just one; this commits to exactly one (or none), and the count it /// ranks against — [`Self::effective_consumer_count`] — can differ from the @@ -5024,7 +5024,7 @@ pub struct TargetSubDAGSelection<'a> { pub composition: Option>, } -/// Why [`PlanSpace::global_selection`] committed an exact composition at a +/// Why [`CandidatePostASAPDAGs::global_selection`] committed an exact composition at a /// site: which child candidate it composes with, and the /// cost-units-per-second comparison against the raw fallback that it won. #[derive(Debug)] @@ -5047,9 +5047,9 @@ pub struct CompositionDecision<'a> { pub inputs: ExactCompositionCostInputs, } -/// [`PlanSpace::global_selection`]'s result: one [`TargetSubDAGSelection`] per -/// discovered site, in the same discovery order [`PlanSpace::target_subdag_candidates`]/ -/// [`PlanSpace::cost_sorted`] use. +/// [`CandidatePostASAPDAGs::global_selection`]'s result: one [`TargetSubDAGSelection`] per +/// discovered site, in the same discovery order [`CandidatePostASAPDAGs::target_subdag_candidates`]/ +/// [`CandidatePostASAPDAGs::cost_sorted`] use. #[derive(Debug)] pub struct GlobalSelection<'a> { order: Vec<*const QueryExpr>, @@ -5434,7 +5434,7 @@ fn is_composition_candidate(candidate: &ReplacementSubDAG) -> bool { matches!(candidate.replacement, Replacement::ExactComposition(_)) } -/// Everything [`PlanSpace::global_selection`] threads between sites for +/// Everything [`CandidatePostASAPDAGs::global_selection`] threads between sites for /// exact compositions (issue #171): child candidates already committed by /// an earlier parent, and the maintained summary above each site. #[derive(Default)] @@ -5456,7 +5456,7 @@ struct CompositionOption<'a> { /// Every [`Replacement::ExactComposition`] candidate of `group` whose /// composed-plan rate is *known* and beats the raw-recompute baseline — -/// costed against each compatible child candidate already in `PlanSpace` +/// costed against each compatible child candidate already in `CandidatePostASAPDAGs` /// (or the one an earlier parent committed). Unknown statistics yield no /// option at all: the conservative `KeepPreAsap` path stays. fn composition_options<'a>( @@ -5579,7 +5579,7 @@ fn composition_options<'a>( options } -impl PlanSpace { +impl CandidatePostASAPDAGs { /// The whole-plan (cross-group) selection step the module docs' /// "Whole-plan (cross-group) selection" section describes: one /// [`TargetSubDAGSelection`] per discovered site, each ranked against an @@ -5587,7 +5587,7 @@ impl PlanSpace { /// [`SharedSubtreeStrategy`] decision on the path to it — unlike /// [`Self::cost_sorted`], whose per-group ranking only ever sees a /// group's own raw [`TargetSubDAGCandidates::consumer_count`]. - /// Uncertified DDSketch ratios remain in [`PlanSpace`] for downstream + /// Uncertified DDSketch ratios remain in [`CandidatePostASAPDAGs`] for downstream /// inspection but are not chosen automatically by this selector. pub fn global_selection(&self, cost_model: &dyn CostModel) -> GlobalSelection<'_> { self.global_selection_impl(cost_model, None, None, None) @@ -5944,7 +5944,7 @@ fn is_automatically_selectable(candidate: &ReplacementSubDAG, cost_model: &dyn C /// /// Composing this recurrence transitively up the whole ancestor chain (not /// just the immediate parent) is exactly what makes -/// [`PlanSpace::global_selection`]'s `effective_consumer_count` differ from +/// [`CandidatePostASAPDAGs::global_selection`]'s `effective_consumer_count` differ from /// [`TargetSubDAGCandidates::consumer_count`] whenever a `RecomputeIndependently` /// ancestor sits anywhere on the path from a root to a site — see the /// module docs' "Whole-plan (cross-group) selection" section. @@ -6058,7 +6058,7 @@ fn pick_shared_subtree_candidate( // ── reference graph + topological order ───────────────────────────────── -/// The parent/child structure [`PlanSpace::global_selection`]'s DP walks — +/// The parent/child structure [`CandidatePostASAPDAGs::global_selection`]'s DP walks — /// built separately from [`discover_targets`]'s own `order`/`nodes`/`counts` /// maps (which only track *aggregate* reference counts, not per-parent /// breakdown or direction) rather than extending that already-reviewed, @@ -6079,7 +6079,7 @@ struct ReferenceGraph { /// a node's "external" use. Nothing inside the tree decides this (it /// isn't a reference from another discovered site), so it's never /// subject to any ancestor's Share/Recompute choice — it's the base - /// case [`PlanSpace::global_selection`]'s recurrence starts from. + /// case [`CandidatePostASAPDAGs::global_selection`]'s recurrence starts from. external_root_uses: HashMap<*const QueryExpr, usize>, } @@ -6090,7 +6090,7 @@ struct ReferenceGraph { /// their relational children as before. The /// graph is deliberately only used for topological ordering; effective-use /// counts are propagated through the one candidate actually selected. -fn reference_graph(space: &PlanSpace) -> ReferenceGraph { +fn reference_graph(space: &CandidatePostASAPDAGs) -> ReferenceGraph { let mut graph = ReferenceGraph { parents_of: HashMap::new(), children_of: HashMap::new(), @@ -6224,7 +6224,7 @@ fn direct_child_counts(node: &QueryExpr) -> Vec<(*const QueryExpr, usize)> { /// (first-seen-first), not a valid topological one: a node reached via two /// different root paths can have a parent that's discovered *after* it (see /// this function's own test for a worked diamond example), which is exactly -/// backwards for [`PlanSpace::global_selection`]'s recurrence. +/// backwards for [`CandidatePostASAPDAGs::global_selection`]'s recurrence. fn topological_order(order: &[*const QueryExpr], graph: &ReferenceGraph) -> Vec<*const QueryExpr> { let mut in_degree: HashMap<*const QueryExpr, usize> = HashMap::new(); for ptr in order { @@ -6347,14 +6347,14 @@ pub fn default_strategies_with_evidence<'a>( // ── search_workload ────────────────────────────────────────────────────── /// Search a whole workload's pre-ASAP roots for every candidate replacement -/// [`default_strategies`] can find, deduped into a [`PlanSpace`]. Candidate +/// [`default_strategies`] can find, deduped into a [`CandidatePostASAPDAGs`]. Candidate /// *generation* uses the built-in [`DefaultCostModel`] (via /// [`default_strategies`], the same way [`SketchAlgorithmStrategy::default_cost_model`] -/// does); call [`PlanSpace::cost_sorted`] on the result for the final +/// does); call [`CandidatePostASAPDAGs::cost_sorted`] on the result for the final /// `sorted_by(cost_model)` step. Use [`search_workload_with`] to plug in a /// custom strategy set (e.g. built via [`default_strategies_with`] for a /// deployment-specific [`CostModel`]). -pub fn search_workload(roots: Vec<(Id, Rc)>) -> PlanSpace { +pub fn search_workload(roots: Vec<(Id, Rc)>) -> CandidatePostASAPDAGs { search_workload_with(roots, &default_strategies()) } @@ -6374,12 +6374,12 @@ pub fn search_workload(roots: Vec<(Id, Rc)>) -> PlanSpace { /// section). Deduping candidate plans this way needs no /// [`CostModel`] at all — that only enters at two well-defined points: each /// [`ReplacementStrategy`] in `strategies` may already carry its own (e.g. -/// [`SketchAlgorithmStrategy::new`]'s), and [`PlanSpace::cost_sorted`]'s final +/// [`SketchAlgorithmStrategy::new`]'s), and [`CandidatePostASAPDAGs::cost_sorted`]'s final /// ranking step takes one explicitly. pub fn search_workload_with<'s, Id>( roots: Vec<(Id, Rc)>, strategies: &[Box], -) -> PlanSpace { +) -> CandidatePostASAPDAGs { let mut space = search_cse_workload_with(cse_workload(roots), strategies); space.prepare_compositions(&DefaultAccuracyModel, &HashMap::new()); space @@ -6392,7 +6392,7 @@ pub fn search_workload_with<'s, Id>( /// `accuracy_model`'s [`AccuracyModel::satisfies`]: a candidate whose /// guarantee is fully known and misses the target is moved from /// [`TargetSubDAGCandidates::candidates`] to [`TargetSubDAGCandidates::rejected`] *before* -/// [`PlanSpace::cost_sorted`]/[`PlanSpace::global_selection`] ever rank the +/// [`CandidatePostASAPDAGs::cost_sorted`]/[`CandidatePostASAPDAGs::global_selection`] ever rank the /// group. A constructible candidate with unknown accuracy remains visible for /// downstream review under an approximate target, but default whole-plan /// selection does not commit it. An exact target cannot accept an unknown @@ -6408,7 +6408,7 @@ pub fn search_workload_with_targets<'s, Id>( roots: Vec<(Id, Rc, Option)>, strategies: &[Box], accuracy_model: &dyn AccuracyModel, -) -> PlanSpace { +) -> CandidatePostASAPDAGs { let mut targets = Vec::with_capacity(roots.len()); let roots = roots .into_iter() @@ -6529,7 +6529,7 @@ fn cse_workload(roots: Vec<(Id, Rc)>) -> Vec<(Id, Rc)> fn search_cse_workload_with<'s, Id>( cse_roots: Vec<(Id, Rc)>, strategies: &[Box], -) -> PlanSpace { +) -> CandidatePostASAPDAGs { let mut order = Vec::new(); let mut nodes = HashMap::new(); let mut counts: HashMap<*const QueryExpr, usize> = HashMap::new(); @@ -6664,7 +6664,7 @@ fn search_cse_workload_with<'s, Id>( add_effective_count_cse_candidates(&order, &mut groups); - PlanSpace { + CandidatePostASAPDAGs { roots: cse_roots, groups, order, @@ -8289,7 +8289,7 @@ mod tests { assert_eq!(SharedSubtreeStrategy.replacements(&target).len(), 2); } - // ── search_workload / PlanSpace / TargetSubDAGCandidates (merged from search.rs) ── + // ── search_workload / CandidatePostASAPDAGs / TargetSubDAGCandidates (merged from search.rs) ── // // Reuses this test module's own `metric_scan`/`agg` fixture helpers // above (identical to `search.rs`'s own copies, which are dropped here @@ -9582,7 +9582,7 @@ mod tests { ) }) .collect(); - let space = PlanSpace { + let space = CandidatePostASAPDAGs { roots, groups, order: order.clone(), @@ -9688,7 +9688,7 @@ mod tests { // Moved from the former `bind.rs` (issue #251): `bind.rs`'s own // workload-wide orchestration (`implement_workload`/ // `implement_workload_with`) was deleted. Current whole-workload logical - // selection uses `PlanSpace::global_selection`; these tests exercise + // selection uses `CandidatePostASAPDAGs::global_selection`; these tests exercise // `construct_summary_agg`'s schema derivation end to end through // `realize_child` — production logic that still lives in this module — // so they move here rather than disappear. Unlike `bind.rs` (an diff --git a/crates/asap-aware-mapping/src/summary_maintenance_lifecycle.rs b/crates/asap-aware-mapping/src/summary_maintenance_lifecycle.rs index cb1dd9db..7cafcf46 100644 --- a/crates/asap-aware-mapping/src/summary_maintenance_lifecycle.rs +++ b/crates/asap-aware-mapping/src/summary_maintenance_lifecycle.rs @@ -43,7 +43,7 @@ use crate::recurrence::{ CostRate, EvaluationRate, Horizon, RecurrenceError, RecurrenceProfile, UpdateRate, }; use crate::replacement::{ - CandidateCostOverrides, GlobalSelection, PlanSpace, RealizationError, Replacement, + CandidateCostOverrides, CandidatePostASAPDAGs, GlobalSelection, RealizationError, Replacement, }; /// Summary-maintenance lifecycle shapes supported by the target runtime. @@ -623,7 +623,7 @@ pub fn enumerate_summary_maintenance_lifecycles<'a>( /// Internal candidate-costing form. The workload binding supplies temporal /// eligibility and data-arrival facts; `profile` supplies effective uses after -/// DAG path multiplicity has been propagated by `PlanSpace`. +/// DAG path multiplicity has been propagated by `CandidatePostASAPDAGs`. #[expect(clippy::too_many_arguments, reason = "internal bound planning context")] fn enumerate_with_profile<'a>( root: Rc, @@ -724,7 +724,7 @@ fn enumerate_with_profile<'a>( /// attached, so shared `Rc` identity and exact-composition commitments remain /// the responsibility of `GlobalSelection`. pub fn global_selection_with_summary_maintenance_lifecycles<'a, Id>( - space: &'a PlanSpace, + space: &'a CandidatePostASAPDAGs, demand: WorkloadDemand<'_>, now_ms: u64, horizon: Option, @@ -2701,7 +2701,7 @@ mod tests { } #[test] - fn normalized_workload_drives_plan_space_recurrence_profiles() { + fn normalized_workload_drives_candidate_dag_recurrence_profiles() { let root = query_root(); let space = crate::replacement::search_workload(vec![("dashboard", Rc::clone(&root))]); let workload = workload(vec![], vec![repeating()], continuous(1_000, 60_000)); diff --git a/crates/asap-physical-operators/tests/planspace_series_identity_heap.rs b/crates/asap-physical-operators/tests/candidate_post_asap_dags_series_identity_heap.rs similarity index 100% rename from crates/asap-physical-operators/tests/planspace_series_identity_heap.rs rename to crates/asap-physical-operators/tests/candidate_post_asap_dags_series_identity_heap.rs diff --git a/crates/asap-physical-operators/tests/precompute_candidates.rs b/crates/asap-physical-operators/tests/precompute_candidates.rs index c07c23f8..91bd5fdb 100644 --- a/crates/asap-physical-operators/tests/precompute_candidates.rs +++ b/crates/asap-physical-operators/tests/precompute_candidates.rs @@ -15,7 +15,7 @@ use futures::{executor::block_on, StreamExt}; use planner_types::{post_asap::*, pre_asap::DataType, types::AccuracyTarget, workload::*}; use std::{collections::BTreeMap, rc::Rc, sync::Arc}; -fn grouped_rate_space() -> asap_aware_mapping::PlanSpace<&'static str> { +fn grouped_rate_space() -> asap_aware_mapping::CandidatePostASAPDAGs<&'static str> { let workload = PlanningWorkload { query_workload: QueryWorkload { language: QueryLanguage::PromQL, diff --git a/crates/devtools/src/bin/dag_export.rs b/crates/devtools/src/bin/dag_export.rs index 4ec9fb1a..8e6d3eca 100644 --- a/crates/devtools/src/bin/dag_export.rs +++ b/crates/devtools/src/bin/dag_export.rs @@ -22,7 +22,7 @@ // additionally runs `asap_aware_mapping::replacement::search_workload` (this // binary took no strategies of its own — `default_strategies()` already // includes `AvgToSumOverCountStrategy` as of #282) over every lowered query -// and ranks each discovered `TargetSubDAGCandidates` via `PlanSpace::cost_sorted`. The +// and ranks each discovered `TargetSubDAGCandidates` via `CandidatePostASAPDAGs::cost_sorted`. The // best-ranked // candidate per group feeds two additive outputs: // @@ -1158,7 +1158,7 @@ fn assign_workload_node_ids(graphs: &mut [&mut DagGraph]) { /// `default_strategies()` — which includes `AvgToSumOverCountStrategy` as of /// #282 — is exactly the strategy set this binary wants; no custom list /// needed) over every lowered query, rank each discovered `TargetSubDAGCandidates` via -/// `PlanSpace::global_selection`, and build both `--post-asap` outputs from the +/// `CandidatePostASAPDAGs::global_selection`, and build both `--post-asap` outputs from the /// exact same set of winning candidates (see [`Winner`]), so the flat /// `replacements` list and the merged `post_graph` can never disagree about /// which candidate won for a given target. diff --git a/crates/integration-tests/tests/cse.rs b/crates/integration-tests/tests/cse.rs index 1a71cdae..c5c10578 100644 --- a/crates/integration-tests/tests/cse.rs +++ b/crates/integration-tests/tests/cse.rs @@ -6,7 +6,7 @@ //! `asap-types::pre_asap::cse`, run internally by `search_workload`) → //! `search_workload` (stage 2, `asap-aware-mapping`) — and asserts the //! sharing that stage 1 decides survives into stage 2's discovered -//! `PlanSpace` as one genuinely shared `TargetSubDAGCandidates`, not just one shared +//! `CandidatePostASAPDAGs` as one genuinely shared `TargetSubDAGCandidates`, not just one shared //! `Rc`. This is the "real caller" the issue's landing plan //! requires before `share_common_subtrees` is allowed to exist at all (its //! predecessor, `asap-plan::cse::dedupe_subtrees`, was deleted in #192 for @@ -16,7 +16,7 @@ //! workload (the former `implement_workload`/`implement_workload_with`, //! which this test file used to drive instead of `search_workload`) is out //! of `asap-aware-mapping`'s scope — see that crate's `lib.rs` `## Status` -//! section — so these tests assert on the discovered `PlanSpace` shape +//! section — so these tests assert on the discovered `CandidatePostASAPDAGs` shape //! directly, the same way `asap-aware-mapping::replacement`'s own //! `shared_aggregate_across_two_roots_gets_both_strategies_candidates` test //! does, just exercised through the crate's public API from this external diff --git a/crates/integration-tests/tests/exact_composition.rs b/crates/integration-tests/tests/exact_composition.rs index 40b593a1..3f87904d 100644 --- a/crates/integration-tests/tests/exact_composition.rs +++ b/crates/integration-tests/tests/exact_composition.rs @@ -1,6 +1,6 @@ //! Issue #171 — composing exact operators with summary plans across //! explicit update/readout boundaries, end to end through -//! `search_workload_with` → `PlanSpace::global_selection` → +//! `search_workload_with` → `CandidatePostASAPDAGs::global_selection` → //! `GlobalSelection::assemble_selected_dag` → `dag_export`. //! //! Covers the issue's integration matrix: both nesting directions, grouped @@ -274,7 +274,7 @@ fn unknown_runtime_capability_keeps_candidate_but_prevents_selection() { fn plan( roots: Vec<(&'static str, Rc)>, cost_model: &dyn CostModel, -) -> asap_aware_mapping::PlanSpace<&'static str> { +) -> asap_aware_mapping::CandidatePostASAPDAGs<&'static str> { search_workload_with(roots, &default_strategies_with(cost_model)) } @@ -723,7 +723,7 @@ fn a_runtime_without_mixed_execution_gets_no_composition_candidates() { } /// Without statistics (the built-in model) the composition is *proposed* -/// — visible in `PlanSpace` and explanations — but never *selected*: the +/// — visible in `CandidatePostASAPDAGs` and explanations — but never *selected*: the /// site keeps a non-composed alternative, and the inner summary stays /// independently selectable. #[test] diff --git a/crates/types/src/cost.rs b/crates/types/src/cost.rs index a19c6f7a..5a7a5a4d 100644 --- a/crates/types/src/cost.rs +++ b/crates/types/src/cost.rs @@ -73,7 +73,7 @@ pub enum BaselineRef { /// — "do nothing" (never apply ASAP-aware replacement at all). PreAsapRecomputation, /// The best-ranked *non-selected* legal candidate for the same target - /// (`rank` into that target's own `PlanSpace::cost_sorted` ordering, + /// (`rank` into that target's own `CandidatePostASAPDAGs::cost_sorted` ordering, /// `0` = best; a baseline referencing this variant is always `rank >= /// 1`, since `rank 0` is what got selected). HighestRankedNonSelectedCandidate { rank: usize }, diff --git a/crates/types/src/dag_export.rs b/crates/types/src/dag_export.rs index 70c04846..439e8f25 100644 --- a/crates/types/src/dag_export.rs +++ b/crates/types/src/dag_export.rs @@ -613,7 +613,7 @@ fn build_summary(node: &SummaryNode, nodes: &mut Vec) -> u32 { /// One replacement site a higher layer (the `dag_export` binary) found by /// running `asap_aware_mapping::replacement::search_workload_with` + -/// `PlanSpace::cost_sorted` and picking the best-ranked candidate for one +/// `CandidatePostASAPDAGs::cost_sorted` and picking the best-ranked candidate for one /// `TargetSubDAGCandidates` — `asap_types` never runs that search itself (same layering /// rule as [`DagNote`]: this crate defines the shape, a higher crate /// populates it). @@ -640,7 +640,7 @@ pub struct TargetReplacement { /// not re-derived here). pub rationale: String, /// This candidate's rank among its `TargetSubDAGCandidates`'s alternatives after - /// `PlanSpace::cost_sorted` (`0` = best). Exposed so a renderer can show + /// `CandidatePostASAPDAGs::cost_sorted` (`0` = best). Exposed so a renderer can show /// "this was the best of N candidates" without re-deriving the ranking. pub rank: usize, /// This candidate's own estimated cost, straight off @@ -712,7 +712,7 @@ pub fn export(expr: &QueryExpr) -> DagGraph { /// merged post-ASAP graph via [`export_post_asap`] — see that function's own /// doc for the full design. `asap_types` has no opinion on *how* this is /// decided (that's `asap_aware_mapping::replacement::search_workload_with` + -/// `PlanSpace::cost_sorted`'s job, a higher layer, exactly the layering rule +/// `CandidatePostASAPDAGs::cost_sorted`'s job, a higher layer, exactly the layering rule /// [`DagNode::notes`] already states); it only defines the shape a decision /// comes back in. #[derive(Debug, Clone)] @@ -750,7 +750,7 @@ pub enum PostAsapSubstitution { /// /// `find_winner` is the whole layering seam: `asap_types` never runs /// `asap_aware_mapping::replacement::search_workload_with` or -/// `PlanSpace::cost_sorted` itself, and has no idea what a `TargetSubDAGCandidates` or a +/// `CandidatePostASAPDAGs::cost_sorted` itself, and has no idea what a `TargetSubDAGCandidates` or a /// `ReplacementProvenance` is — it only asks, for one node at a time, "did a /// higher layer already decide something for you?" A caller (e.g. the /// `dag_export` devtools binary) builds this closure once per workload diff --git a/crates/types/src/pre_asap/cse.rs b/crates/types/src/pre_asap/cse.rs index 3f3a9cfe..cb6aa69a 100644 --- a/crates/types/src/pre_asap/cse.rs +++ b/crates/types/src/pre_asap/cse.rs @@ -94,7 +94,7 @@ //! `share_common_subtrees`-actual sharing — see `dag_export`'s module doc). //! Stage 4 (issue #237) is implemented in //! `asap_aware_mapping::cost_model::CostModel::cse_share_decision`, called -//! from `asap_aware_mapping::replacement::PlanSpace::cost_sorted` (via that +//! from `asap_aware_mapping::replacement::CandidatePostASAPDAGs::cost_sorted` (via that //! module's own `cse_preference`) — a real, Volcano/Cascades-style cost //! comparison over what this module detects, not a fixed rule. See //! `docs/design_docs/cost-model.md`. This module's own diff --git a/docs/design_docs/architecture/README.md b/docs/design_docs/architecture/README.md index 3099e847..9c32f623 100644 --- a/docs/design_docs/architecture/README.md +++ b/docs/design_docs/architecture/README.md @@ -7,7 +7,7 @@ as ASAPQuery-backend bind the candidates to physical alternatives, make the deployment-level decision, and run the selected contract. For the integration workflow, start with [ASAPPlanner input, output, and -workflows](input-output-workflow.md). It defines inputs, `PlanSpace`, selection +workflows](input-output-workflow.md). It defines inputs, `CandidatePostASAPDAGs`, selection and summary-maintenance lifecycle workflows, and future replanning support. ## Planner component flow @@ -19,7 +19,7 @@ flowchart TD E["Strategy, accuracy model, and applicable evidence"] PRE["Frontend lowering → canonical Pre-ASAP QueryExpr roots"] SEARCH["Whole-workload candidate search: sharing, legality, accuracy"] - SPACE["PlanSpace: compact logical candidate DAG space"] + SPACE["CandidatePostASAPDAGs: compact logical candidate DAG space"] RANK["Optional cost_sorted: ranked inspection view"] SELECT["Optional global_selection + assemble_selected_dag"] DAG["Selected logical Post-ASAP DAG"] @@ -39,7 +39,7 @@ flowchart TD LINPUT --> LIFE --> LMAT --> LPLAN --> BACKEND ``` -`PlanSpace` is the output of logical candidate search. Each target's candidate set holds +`CandidatePostASAPDAGs` is the output of logical candidate search. Each target's candidate set holds alternatives and rejection reasons, but no selected maintenance lifecycle. Choose among the three branches: inspect candidates (optionally ranked), select and assemble logical DAGs, or select and assemble with summary-maintenance @@ -49,7 +49,7 @@ call returns a `GlobalSelection`; the second returns a `SummaryMaintenanceLifecyclePlan` with an assembled DAG root and lifecycle decisions. No branch by itself deploys or executes a physical plan. Known-invalid evidence rejects a logical candidate. Missing accuracy evidence -leaves a constructible candidate visible in `PlanSpace` but uncertified; default +leaves a constructible candidate visible in `CandidatePostASAPDAGs` but uncertified; default selection does not commit it without the required guarantee. Cost evidence can rank eligible candidates, but it cannot establish a missing guarantee or turn an unsupported physical alternative into a deployable plan. @@ -72,7 +72,7 @@ requirements, the planning horizon, available materialized state, downstream capabilities, and complete cost evidence. Missing or stale evidence must remain explicit rather than being treated as zero. -The primary output is `PlanSpace`; `cost_sorted` derives an optional ranked +The primary output is `CandidatePostASAPDAGs`; `cost_sorted` derives an optional ranked view with index-aligned costs. Downstream may inspect compatible choices across targets rather than assuming the first candidate is a feasible physical workload plan. Candidates carry logical summary algorithms, @@ -87,7 +87,7 @@ serving, and operational feedback. Their physical planning can reorder candidates because it has evidence that the reusable Planner does not, but it must not silently change Planner-owned semantics. -`PlanSpace::global_selection` optionally coordinates structural choices across +`CandidatePostASAPDAGs::global_selection` optionally coordinates structural choices across targets; `GlobalSelection::assemble_selected_dag` constructs a selected semantic DAG. Those plain APIs do not establish physical feasibility or a maintenance-versus-recompute decision. The lifecycle-aware selection call uses diff --git a/docs/design_docs/architecture/asap-aware-mapping.md b/docs/design_docs/architecture/asap-aware-mapping.md index ae4ca865..bf88f9b4 100644 --- a/docs/design_docs/architecture/asap-aware-mapping.md +++ b/docs/design_docs/architecture/asap-aware-mapping.md @@ -7,7 +7,7 @@ ASAP-aware mapping decides **whether and how a query intent can be answered usin Given a logical query plan, the mapping layer explores alternative plans that may use sketches, exact summaries, shared computation, roll-ups, semantic rewrites, or combinations of these techniques. Candidate search takes canonical **Pre-ASAP query roots** and produces -`PlanSpace`, a compact set of **candidate Post-ASAP DAGs**. Ranking, selection, +`CandidatePostASAPDAGs`, a compact set of **candidate Post-ASAP DAGs**. Ranking, selection, and summary-maintenance lifecycle decisions are subsequent operations over it; see [input, output, and workflows](input-output-workflow.md). @@ -71,7 +71,7 @@ Check compatibility between replacement sub-DAGs Apply applicable accuracy and semantic checks | v -PlanSpace: compact candidate Post-ASAP DAGs +CandidatePostASAPDAGs: compact candidate Post-ASAP DAGs | +--> inspect / rank +--> select and assemble logical DAGs diff --git a/docs/design_docs/architecture/asap-aware-plan-search.md b/docs/design_docs/architecture/asap-aware-plan-search.md index 76663742..6f8a5bd7 100644 --- a/docs/design_docs/architecture/asap-aware-plan-search.md +++ b/docs/design_docs/architecture/asap-aware-plan-search.md @@ -80,7 +80,7 @@ sets. This avoids copying every full plan when most structure is shared. ## Current implementation boundary -`PlanSpace` stores per-target candidates rather than eagerly enumerating their +`CandidatePostASAPDAGs` stores per-target candidates rather than eagerly enumerating their Cartesian product. `cost_sorted` returns a `RankedTargetSubDAGCandidates` view for each target. `global_selection` coordinates supported sharing and composition choices; `assemble_selected_dag(root)` assembles one selected DAG diff --git a/docs/design_docs/architecture/evidence-dependent-candidates.md b/docs/design_docs/architecture/evidence-dependent-candidates.md index a897a630..1a53c0b7 100644 --- a/docs/design_docs/architecture/evidence-dependent-candidates.md +++ b/docs/design_docs/architecture/evidence-dependent-candidates.md @@ -2,7 +2,7 @@ Audience: ASAPPlanner library integrators, especially ASAPQuery-backend. -`PlanSpace` is a space of constructible logical alternatives, not a list of +`CandidatePostASAPDAGs` is a space of constructible logical alternatives, not a list of certified deployment choices. Missing external evidence must not erase a candidate whose semantics and Post-ASAP shape are already known. It also must not turn an unknown guarantee into a satisfied accuracy requirement. @@ -21,7 +21,7 @@ The exact `KeepPreAsap` path has an exact guarantee. |---|---|---| | Known guarantee | `ResultGuarantee` with evaluable bound and failure probability | Planner checks whether the guarantee satisfies the query's accuracy target. If this candidate is selected, the backend checks whether its implementation can realize the selected summary; it does not re-decide the accuracy target. | | Missing accuracy/domain evidence | Symbolic `BoundExpr::Unknown` or `ProbabilityExpr::Unknown`, or `guarantee: None` on a constructible summary | Inspect `ReplacementSubDAG::has_missing_accuracy_evidence()`, obtain applicable evidence or apply explicit policy; do not claim certification. | -| Missing cost | `CostModel::candidate_cost()` returns `None` for a `ReplacementSubDAG` (including a non-finite or negative legacy estimate) | Keep that logical summary/rewrite candidate in `PlanSpace` for inspection; provide a comparable cost before selecting it by cost. This does not make it a deployable physical plan. | +| Missing cost | `CostModel::candidate_cost()` returns `None` for a `ReplacementSubDAG` (including a non-finite or negative legacy estimate) | Keep that logical summary/rewrite candidate in `CandidatePostASAPDAGs` for inspection; provide a comparable cost before selecting it by cost. This does not make it a deployable physical plan. | | Unknown runtime support | `ReplacementSubDAG::runtime_support_evidence(model)` returns `None` | Candidate remains visible; bind a concrete implementation and confirm support before deployment. | | Known invalid evidence or impossible semantics | No candidate; where supported, a `RejectedCandidate` records the error | Do not deploy. | @@ -58,10 +58,10 @@ not emit a `RejectedCandidate` for that case. | HLL confidence | Symbolic failure probability | Reject a fully known unmet root target. | | Relative-value composition | Symbolic bound when input sign is unknown | Reject known signed input for this rule. | | Exact sum/average/extremum | Symbolic row-count probability term | Reject unsupported metric combinations. | -| Cost/rate/physical evidence | `None` cost or missing workload rate; candidate remains in `PlanSpace` | Physical/lifecycle evaluation reports unavailable or rejected evidence. | -| Mixed exact/summary operator | Unknown runtime support; candidate remains in `PlanSpace` | `Some(false)` prevents construction. | +| Cost/rate/physical evidence | `None` cost or missing workload rate; candidate remains in `CandidatePostASAPDAGs` | Physical/lifecycle evaluation reports unavailable or rejected evidence. | +| Mixed exact/summary operator | Unknown runtime support; candidate remains in `CandidatePostASAPDAGs` | `Some(false)` prevents construction. | -Lifecycle deployment choices are a separate output from `PlanSpace`; their +Lifecycle deployment choices are a separate output from `CandidatePostASAPDAGs`; their capability/cost rejections do not erase the logical summary candidate. The backend must still check ordinary summary family, window, and state-operation capabilities before deployment. @@ -87,7 +87,7 @@ capabilities before deployment. The default `global_selection()` skips summaries that `has_missing_accuracy_evidence()` identifies as uncertified. Its `GlobalSelection::assemble_selected_dag()` result is a selected logical plan, -not an instruction to deploy every candidate in `PlanSpace`. If no alternative +not an instruction to deploy every candidate in `CandidatePostASAPDAGs`. If no alternative is chosen at a site, DAG assembly retains the exact `KeepPreAsap` path. The backend can inspect alternatives, apply its own evidence and policy, then choose a physically supported one; it must not equate candidate presence with approval. @@ -108,7 +108,7 @@ backend. | PromQL input | Before this PR | After this PR | |---|---|---| -| `count by(job)(up)` with an ε/δ target | Hydra's shared CMS/CountSketch alternatives are absent: missing shared-grid bounds make the strategy decline the target. | Both Hydra alternatives remain in `PlanSpace` with symbolic unknown bound/probability terms. `has_missing_accuracy_evidence()` is true; default `global_selection()` does not choose either as a certified answer. | +| `count by(job)(up)` with an ε/δ target | Hydra's shared CMS/CountSketch alternatives are absent: missing shared-grid bounds make the strategy decline the target. | Both Hydra alternatives remain in `CandidatePostASAPDAGs` with symbolic unknown bound/probability terms. `has_missing_accuracy_evidence()` is true; default `global_selection()` does not choose either as a certified answer. | | `entropy_over_time(m[5m])` with an ε target | The uncalibrated frequency readout has no `SummaryEstimate` candidate. | Its `SummaryEstimate` remains inspectable with `guarantee: None`. Default selection still skips it, so candidate visibility is not an accuracy certificate. | | `quantile_over_time(0.9,data[5m]) / quantile_over_time(0.5,data[5m])` with an ε target | The uncertified direct DDSketch ratio is **already** visible because of #449. | Still visible with `guarantee: None`, and still skipped by default selection. This is a regression/control example, not a new candidate introduced by this PR. | @@ -139,7 +139,7 @@ The same loop applies to grouped Count with Hydra and to Count-ranked TopK: their symbolic guarantees keep them visible until shared-grid or interval evidence is available. Re-run planning with a provider when a certified Planner selection is needed; supplying evidence to the backend alone does -not retroactively change the guarantees stored in the existing `PlanSpace`. +not retroactively change the guarantees stored in the existing `CandidatePostASAPDAGs`. Backend integration work is tracked in [ASAPQuery-backend#752](https://github.com/ProjectASAP/ASAPQuery-backend/issues/752). diff --git a/docs/design_docs/architecture/input-output-workflow.md b/docs/design_docs/architecture/input-output-workflow.md index 32c0b093..f57b31f5 100644 --- a/docs/design_docs/architecture/input-output-workflow.md +++ b/docs/design_docs/architecture/input-output-workflow.md @@ -7,7 +7,7 @@ submitting queries through a backend. ASAPPlanner is a **logical planning library**. Its input is a planning workload plus the models, evidence, and deployment capabilities needed by the requested -planning workflow. Its canonical output is a `PlanSpace` containing the legal +planning workflow. Its canonical output is a `CandidatePostASAPDAGs` containing the legal Post-ASAP alternatives for the workload. ### Input fields at a glance @@ -28,13 +28,13 @@ fields and [frontend dependencies](#frontend-specific-dependencies). | Output | Fields or contents | Meaning | |---|---|---| -| `PlanSpace` | The legal candidate Post-ASAP DAGs for the workload, represented compactly as canonical roots, one candidate set per target sub-DAG, and cross-target composition information | The ASAPPlanner output | +| `CandidatePostASAPDAGs` | The legal candidate Post-ASAP DAGs for the workload, represented compactly as canonical roots, one candidate set per target sub-DAG, and cross-target composition information | The ASAPPlanner output | [Ranking](#ranked-view), [selection and DAG assembly](#selection-and-dag-assembly), and -[summary-maintenance lifecycle](#summary-maintenance-lifecycle-aware-helper) APIs operate on this `PlanSpace`. +[summary-maintenance lifecycle](#summary-maintenance-lifecycle-aware-helper) APIs operate on this `CandidatePostASAPDAGs`. These are alternative uses of the candidate space, not mandatory sequential -stages. `PlanSpace` itself has no selected summary-maintenance lifecycle, and +stages. `CandidatePostASAPDAGs` itself has no selected summary-maintenance lifecycle, and its candidates do not choose precompute versus query-time placement: a chosen lifecycle assignment sets each node's execution timing. @@ -49,7 +49,7 @@ PlanningWorkload + frontend dependencies + planning models/evidence ASAPPlanner | v - PlanSpace: candidate Post-ASAP DAGs + CandidatePostASAPDAGs: candidate Post-ASAP DAGs ``` --- @@ -68,7 +68,7 @@ flowchart TD F["PromQL lowering"] R["One canonical QueryExpr root"] S["Candidate search"] - P["PlanSpace: logical choices for this root"] + P["CandidatePostASAPDAGs: logical choices for this root"] I["cost_sorted: inspect choices"] G["global_selection + assemble_selected_dag(root)"] L["One selected Post-ASAP DAG; exact KeepPreAsap if no optimization is selected"] @@ -88,7 +88,7 @@ flowchart TD ``` “Predictable” says the query is known in advance; it is independent of its -one-minute recurrence. The `PlanSpace` may contain an exact count-summary +one-minute recurrence. The `CandidatePostASAPDAGs` may contain an exact count-summary realization, but it is not a deployed query. Without the extra lifecycle inputs, the caller can still inspect candidates or obtain a logical DAG; it cannot conclude that maintaining a summary is cheaper than recomputing raw @@ -103,7 +103,7 @@ flowchart LR C["SqlCatalog: resolves metrics and its columns"] F["SQL lowering"] R["One QueryExpr root"] - P["Candidate search → PlanSpace"] + P["Candidate search → CandidatePostASAPDAGs"] Q --> F C --> F F --> R --> P @@ -111,7 +111,7 @@ flowchart LR In this SQL example, `data_workload` can be `None` if the chosen lowering and search rules do not consume it. The lifecycle helper is not needed merely to -inspect the `PlanSpace`. +inspect the `CandidatePostASAPDAGs`. --- @@ -312,9 +312,9 @@ Additional inputs for a Planner-owned maintenance decision are listed with the ## Output -### `PlanSpace` +### `CandidatePostASAPDAGs` -`PlanSpace` is Planner's canonical output. It contains: +`CandidatePostASAPDAGs` is Planner's canonical output. It contains: * canonical workload roots; * one `TargetSubDAGCandidates` entry for each discovered target sub-DAG; @@ -322,7 +322,7 @@ Additional inputs for a Planner-owned maintenance decision are listed with the * rejected candidates and reasons; and * information needed to select compatible candidates across targets. -A `PlanSpace` represents a **space of logical DAG choices**, not a single plan. +A `CandidatePostASAPDAGs` represents a **space of logical DAG choices**, not a single plan. It is exposed to integrators because the backend may choose among candidates using implementation support, measured costs, and available resources that candidate search does not have. A summary that is cheap on one backend may be @@ -330,7 +330,7 @@ expensive or unsupported on another. Returning only one plan during search would discard those choices too early. Callers with suitable models can instead use the [selection workflows](#workflows) -below. A future higher-level API could hide `PlanSpace` behind those decisions; +below. A future higher-level API could hide `CandidatePostASAPDAGs` behind those decisions; the current interface lets an integrator own them. DAG assembly connects choices after selection and does not replace this candidate interface. @@ -340,7 +340,7 @@ For `count(up) + 1`, the addition is a root and `count(up)` can be an inner target. `TargetSubDAGCandidates` holds the alternatives for one such target. It represents that space compactly instead of eagerly copying every complete -DAG. `PlanSpace` stores the workload's canonical roots once, creates one +DAG. `CandidatePostASAPDAGs` stores the workload's canonical roots once, creates one `TargetSubDAGCandidates` entry for each distinct target sub-DAG, and stores that target's replacement alternatives once inside the entry. Candidate children refer back to canonical @@ -348,7 +348,7 @@ targets, so common subexpressions and shared alternatives are not duplicated across roots. For example, if one target has three alternatives and its child has two, -eager enumeration could create six complete DAGs. `PlanSpace` stores the three +eager enumeration could create six complete DAGs. `CandidatePostASAPDAGs` stores the three parent alternatives, the two child alternatives, and their relationship. Whole-plan selection chooses compatible alternatives across those targets; `assemble_selected_dag(root)` then recursively substitutes the selected alternatives to @@ -363,7 +363,7 @@ All paths start by lowering the workload and searching for candidates: PlanningWorkload + frontend dependencies + planning models/evidence -> frontend lowering: one QueryExpr root per normalized query entry -> search_workload_with_targets - -> PlanSpace + -> CandidatePostASAPDAGs ``` The integration associates each lowered root with a caller-owned `Id` and its @@ -380,7 +380,7 @@ Then choose the operation matching the caller's responsibility: ### Ranked view -`PlanSpace::cost_sorted` returns one `RankedTargetSubDAGCandidates` for each +`CandidatePostASAPDAGs::cost_sorted` returns one `RankedTargetSubDAGCandidates` for each `TargetSubDAGCandidates` entry. Conceptually, it is the same target's alternatives in cost-model preference order where the model defines one (otherwise discovery order), with one displayed cost per alternative. It is @@ -405,8 +405,8 @@ physical deployability. ### Selection and DAG assembly -The input is `PlanSpace` and a cost model. Call -`PlanSpace::global_selection(&cost_model)` once for the workload, then +The input is `CandidatePostASAPDAGs` and a cost model. Call +`CandidatePostASAPDAGs::global_selection(&cost_model)` once for the workload, then `GlobalSelection::assemble_selected_dag(root)` for each wanted query root. These are two public APIs, not one combined call: N roots require one selection and N assembly calls. Each successful assembly returns one DAG root; the caller @@ -421,7 +421,7 @@ the result for one query root. | Input → decisions → output (click a step for details) | |:---:| -| **Input:** [PlanSpace](asap-aware-plan-search.md) + cost model | +| **Input:** [CandidatePostASAPDAGs](asap-aware-plan-search.md) + cost model | | ↓ | | **Select:** [global_selection](../../develop_docs/library-api.md#what-does-global-selection-mean) chooses compatible alternatives | | ↓ | @@ -443,7 +443,7 @@ summary-maintenance lifecycle costs. Use it when ASAPPlanner owns the decision to maintain summaries versus recompute raw data. It is not needed for candidate inspection or when the downstream backend owns that decision. -Starting from an existing `PlanSpace`, call these two public helpers in order; +Starting from an existing `CandidatePostASAPDAGs`, call these two public helpers in order; there is no need to run the ordinary selection/assembly workflow first: 1. `global_selection_with_summary_maintenance_lifecycles` uses the workload @@ -472,8 +472,8 @@ Across the two calls, the caller supplies these parameters: | Helper parameter | Source | Required | |---|---|---:| -| `PlanSpace` | Canonical ASAPPlanner output; passed to selection | Yes | -| `GlobalSelection` and one root | Selection result and a root in that `PlanSpace`; passed to DAG assembly | Yes for each assembled root | +| `CandidatePostASAPDAGs` | Canonical ASAPPlanner output; passed to selection | Yes | +| `GlobalSelection` and one root | Selection result and a root in that `CandidatePostASAPDAGs`; passed to DAG assembly | Yes for each assembled root | | Workload binding | `QueryWorkload` plus the workload-entry indices associated with each root | Yes | | Planning time (`now_ms`) | Caller clock in Unix milliseconds | Yes | | Planning horizon | Caller policy | Conditional: required for finite totals over recurring demand | diff --git a/docs/design_docs/architecture/planner-runtime-contract.md b/docs/design_docs/architecture/planner-runtime-contract.md index c7d4f810..41a3e719 100644 --- a/docs/design_docs/architecture/planner-runtime-contract.md +++ b/docs/design_docs/architecture/planner-runtime-contract.md @@ -2,7 +2,7 @@ ## Purpose -ASAPPlanner produces `PlanSpace`, a compact logical candidate space. Integrators +ASAPPlanner produces `CandidatePostASAPDAGs`, a compact logical candidate space. Integrators may select candidates downstream or ask Planner's helpers to select and assemble DAGs. Summary-maintenance lifecycle decisions belong to Planner only when the integration uses its lifecycle-aware workflow; physical deployment and execution @@ -76,7 +76,7 @@ and rollback are not an end-to-end Planner protocol. source coverage, input/output edges, operation counts, update and bootstrap fanout, retained state, CPU, memory, I/O, and accuracy facts. 4. ASAPPlanner keeps constructible candidates with missing evidence visible - in `PlanSpace` but does not certify unknown accuracy. The + in `CandidatePostASAPDAGs` but does not certify unknown accuracy. The summary-maintenance-lifecycle-aware workflow compares supported alternatives over the same workload horizon. Missing or incomparable costs do not establish that maintaining a summary beats raw recomputation; structural scores and @@ -136,7 +136,7 @@ in one cost formula. execution. - A selected realization framework is a contract, not executor code. - Physical capabilities and evidence constrain deployment choices, not every - logical candidate's presence in `PlanSpace`. Known unsupported capabilities + logical candidate's presence in `CandidatePostASAPDAGs`. Known unsupported capabilities and unknown algorithms cannot become deployable alternatives. - Complete physical alternatives need identity and comparable evidence for cost-based deployment decisions. Stale evidence cannot certify or cost a diff --git a/docs/design_docs/concepts/planner-pipeline.md b/docs/design_docs/concepts/planner-pipeline.md index c85ec4e3..5e013e48 100644 --- a/docs/design_docs/concepts/planner-pipeline.md +++ b/docs/design_docs/concepts/planner-pipeline.md @@ -1,6 +1,6 @@ # Planner pipeline -ASAPPlanner accepts a planning workload and produces `PlanSpace`, a compact +ASAPPlanner accepts a planning workload and produces `CandidatePostASAPDAGs`, a compact representation of candidate Post-ASAP DAGs. Ranking and selection are operations over that output, not mandatory stages of candidate search. @@ -10,7 +10,7 @@ over that output, not mandatory stages of candidate search. Pre-ASAP IR: exact, language-independent query intent | enumerate legal summary-aware alternatives v - PlanSpace: candidate Post-ASAP DAGs + CandidatePostASAPDAGs: candidate Post-ASAP DAGs | +--> inspect candidates, optionally using cost_sorted +--> select and assemble logical DAGs diff --git a/docs/design_docs/decisions/cse-cost-model.md b/docs/design_docs/decisions/cse-cost-model.md index 3f974e06..6cad1730 100644 --- a/docs/design_docs/decisions/cse-cost-model.md +++ b/docs/design_docs/decisions/cse-cost-model.md @@ -49,10 +49,10 @@ carve-out — a cheap-to-recompute candidate naturally loses the comparison on its own. This decision does not need search infrastructure of its own. Issue #252's -MEMO-based search engine (`PlanSpace`/`TargetSubDAGCandidates` in `replacement.rs`) already +MEMO-based search engine (`CandidatePostASAPDAGs`/`TargetSubDAGCandidates` in `replacement.rs`) already enumerates and ranks the larger, workload-wide candidate space. The choice between sharing and recomputing one already-detected CSE candidate is binary, -so `PlanSpace::cost_sorted` reuses one direct +so `CandidatePostASAPDAGs::cost_sorted` reuses one direct `CostModel::cse_share_decision` comparison per group. This preserves the policy described here—compare costs rather than applying a fixed rule—inside the larger search engine. `search_workload_with`'s @@ -72,7 +72,7 @@ gate) and the cost-aware decision is applied downstream, in ## Where it hooks in -[`PlanSpace::cost_sorted`](../../../crates/asap-aware-mapping/src/replacement.rs) +[`CandidatePostASAPDAGs::cost_sorted`](../../../crates/asap-aware-mapping/src/replacement.rs) is where this hooks in today. `search_workload_with` computes each shared subtree's true `consumer_count` across the whole workload up front (the same role `implement_workload_with`'s pre-pass used to play, before that function @@ -110,7 +110,7 @@ either or both, same as `size_params` already lets a deployment override ## Scope -This decision, and `cse_share_decision`'s wiring into `PlanSpace::cost_sorted` +This decision, and `cse_share_decision`'s wiring into `CandidatePostASAPDAGs::cost_sorted` (originally into `implement_workload_with`, before `bind.rs` was retired — see above), close out #223's stage 4 and #212's original "add CSE" tracking issue. Stage 3 (`dag_export::structural_hash` unification) landed separately diff --git a/docs/design_docs/physical-planning-and-deployment.md b/docs/design_docs/physical-planning-and-deployment.md index 81afc40b..14af31a1 100644 --- a/docs/design_docs/physical-planning-and-deployment.md +++ b/docs/design_docs/physical-planning-and-deployment.md @@ -38,7 +38,7 @@ below states. ### Layer contract -1. **Logical Post-ASAP** (`PlanSpace`) decides what to compute: summary +1. **Logical Post-ASAP** (`CandidatePostASAPDAGs`) decides what to compute: summary families, readouts and sharing. It does not decide placement; timing that a realization strategy writes while building a candidate is provisional. 2. **Summary maintenance lifecycle** (Planner) lists the lifecycle choices for @@ -92,7 +92,7 @@ separate unsupported compilation, deployment infeasibility, missing evidence, and a feasible candidate that loses on cost. Absence is not a cost comparison. For `sum by(job)(rate(m[1m]))`, Rate remains per series before grouped Sum. -`PlanSpace` offers one such candidate, with a per-series Rate state and a grouped +`CandidatePostASAPDAGs` offers one such candidate, with a per-series Rate state and a grouped Sum state. Its lifecycle assignment places it: a retained Sum state finalizes Rate and builds Sum within a bounded precompute run; an `Ephemeral` Sum over a retained Rate state leaves the Rate readout and Sum in the query DAG. Storing a diff --git a/docs/design_docs/proposals/asap-aware-mapping/analytical-resource-cost.md b/docs/design_docs/proposals/asap-aware-mapping/analytical-resource-cost.md index 1e87193a..6633247e 100644 --- a/docs/design_docs/proposals/asap-aware-mapping/analytical-resource-cost.md +++ b/docs/design_docs/proposals/asap-aware-mapping/analytical-resource-cost.md @@ -1072,7 +1072,7 @@ The intended end-to-end selection pipeline is: The query lowerer and physical estimator cover the supported raw-query shapes listed above. `PhysicalPlanCostModel` executes this pipeline for every -candidate supplied to `PlanSpace::global_selection`. Logical rewrites are +candidate supplied to `CandidatePostASAPDAGs::global_selection`. Logical rewrites are lowered recursively. Summary candidates participate only after the deployment has bound their complete `SummaryExpr` DAG; there is no optimistic generic summary fallback. The streaming adapter connects raw recomputation and diff --git a/docs/design_docs/proposals/asap-aware-mapping/ddsketch-quantile-ratios.md b/docs/design_docs/proposals/asap-aware-mapping/ddsketch-quantile-ratios.md index 742fe798..620498c9 100644 --- a/docs/design_docs/proposals/asap-aware-mapping/ddsketch-quantile-ratios.md +++ b/docs/design_docs/proposals/asap-aware-mapping/ddsketch-quantile-ratios.md @@ -22,6 +22,6 @@ rule or remain exact. Callers that require a certified end-to-end accuracy target must use evidence or select another candidate. Planner's automatic whole-plan selection skips -uncertified ratios while retaining them in `PlanSpace`; a backend can inspect +uncertified ratios while retaining them in `CandidatePostASAPDAGs`; a backend can inspect the candidate and make its own evidence-based selection. Runtime or statically enforced domain contracts remain future work driven by observed v1 correctness needs. diff --git a/docs/design_docs/proposals/asap-aware-mapping/workload-demand-and-summary-lifecycle.md b/docs/design_docs/proposals/asap-aware-mapping/workload-demand-and-summary-lifecycle.md index 0a1f2892..64c3518e 100644 --- a/docs/design_docs/proposals/asap-aware-mapping/workload-demand-and-summary-lifecycle.md +++ b/docs/design_docs/proposals/asap-aware-mapping/workload-demand-and-summary-lifecycle.md @@ -66,7 +66,7 @@ For the broader lifecycle design, four categories of information matter distribution; 4. existing summaries and the lifecycle actions available to the deployment. -Candidate search outputs `PlanSpace`. The implemented lifecycle-aware workflow +Candidate search outputs `CandidatePostASAPDAGs`. The implemented lifecycle-aware workflow then returns a `SummaryMaintenanceLifecyclePlan` per query root, containing the Post-ASAP DAG and maintenance decisions. It can choose exact raw recomputation when summary maintenance does not beat raw cost or comparable costs are missing. A diff --git a/docs/design_docs/proposals/operator-sharing.md b/docs/design_docs/proposals/operator-sharing.md index 3d075105..99f5d5e2 100644 --- a/docs/design_docs/proposals/operator-sharing.md +++ b/docs/design_docs/proposals/operator-sharing.md @@ -361,7 +361,7 @@ returns the child's original subtree, and assembly keeps it as is. **Accuracy check during search**: binding sets no `guarantee` slot (§2.2), so the candidate filter in `search_workload_with_targets` and `prepare_compositions` run `derive_guarantees` on the candidate alone, with a fresh memo, then check its accuracy target. The derived -copy is only read, then dropped: PlanSpace keeps the original candidate, whose nodes are +copy is only read, then dropped: CandidatePostASAPDAGs keeps the original candidate, whose nodes are shared with other queries. **Assembly** — one rule replaces `assemble_residual`: diff --git a/docs/develop_docs/asap-aware-mapping-architecture.md b/docs/develop_docs/asap-aware-mapping-architecture.md index 65a94e88..06f47bfc 100644 --- a/docs/develop_docs/asap-aware-mapping-architecture.md +++ b/docs/develop_docs/asap-aware-mapping-architecture.md @@ -72,7 +72,7 @@ Terminology used in the diagram: is a compact data structure that trades exactness for bounded error. A query's **accuracy target** states the allowed error and failure probability. A candidate's **rationale** is its human-readable explanation. -- `PlanSpace` is a compact candidate space with one +- `CandidatePostASAPDAGs` is a compact candidate space with one `TargetSubDAGCandidates` per target instead of one full plan per combination of choices. A `node_hash` is a structural fingerprint used to narrow explanation lookup; exact structural equality is still checked afterward. @@ -101,12 +101,12 @@ flowchart TB end subgraph SEARCHSPACE[3. Store the workload-wide search space] - SPACE["PlanSpace
one TargetSubDAGCandidates per target; each candidate set keeps
all candidates, including dependent compositions"]:::store + SPACE["CandidatePostASAPDAGs
one TargetSubDAGCandidates per target; each candidate set keeps
all candidates, including dependent compositions"]:::store CAND -->|"deduplicate by target and candidate identity"| SPACE end subgraph RANKING[Optional ranked view] - SORT["PlanSpace::cost_sorted
use the CostModel to order each candidate set
and cost every candidate"]:::choose + SORT["CandidatePostASAPDAGs::cost_sorted
use the CostModel to order each candidate set
and cost every candidate"]:::choose RANKED["RankedTargetSubDAGCandidates
the same candidates in preferred order,
with costs aligned by index"]:::choose SPACE --> SORT -->|"reorder only; preserve every candidate"| RANKED end @@ -229,13 +229,13 @@ strategy-specific discovery logic. ### 3.4 Store and rank the complete search space -Workload search deduplicates candidates into a `PlanSpace`. Each distinct +Workload search deduplicates candidates into a `CandidatePostASAPDAGs`. Each distinct target has one `TargetSubDAGCandidates` containing retained alternatives and rejection reasons. This compact representation preserves independent choices without enumerating a flat list of `2^N` complete plans for `N` replaceable targets. -`PlanSpace::cost_sorted` ranks each target's existing candidates with the +`CandidatePostASAPDAGs::cost_sorted` ranks each target's existing candidates with the supplied `CostModel`. It returns the same candidates in preferred order, with costs aligned by index; ranking does not select or remove a candidate. @@ -253,7 +253,7 @@ execution policy. Constructing all candidates before taking the first costs more than constructing only the preferred candidate, but it keeps the strategy contract consistent and preserves the full choice set for other callers. -`PlanSpace::global_selection` optionally coordinates cross-target sharing and +`CandidatePostASAPDAGs::global_selection` optionally coordinates cross-target sharing and composition choices. `GlobalSelection::assemble_selected_dag` constructs the selected semantic DAG. These plain APIs do not establish lifecycle or physical deployment feasibility. Recurrence and lifecycle-aware variants require the corresponding diff --git a/docs/develop_docs/asap-aware-mapping-contracts.md b/docs/develop_docs/asap-aware-mapping-contracts.md index 37e3edff..3ea1d154 100644 --- a/docs/develop_docs/asap-aware-mapping-contracts.md +++ b/docs/develop_docs/asap-aware-mapping-contracts.md @@ -173,7 +173,7 @@ This guide uses the Cascades/Volcano terminology: operation. In this crate, that kind of candidate is represented by `Replacement::Rewrite`. - A **replacement candidate** packages either kind of result as a - `ReplacementSubDAG` for search. `PlanSpace` stores and ranks these candidates. + `ReplacementSubDAG` for search. `CandidatePostASAPDAGs` stores and ranks these candidates. - **Physical commitment and placement** happen downstream. An `Realization` therefore does not mean that the planner has committed the workload to that choice. @@ -184,7 +184,7 @@ The concrete flow is: AggIntent -> realizations_for_intent(): enumerate Realization values -> SketchAlgorithmStrategy: construct ReplacementSubDAG candidates - -> PlanSpace: store and rank candidates + -> CandidatePostASAPDAGs: store and rank candidates -> downstream deployment: select and place a final choice ``` @@ -273,7 +273,7 @@ bounds, but does not execute workloads or own deployment measurements. Most hook fn cse_share_decision(&self, candidate: &CseCandidate) -> ShareDecision; ``` -- **`estimate_cost`** — attach a comparable numeric cost to an already-constructed replacement. `PlanSpace::cost_sorted` calls it for every candidate and keeps the returned values aligned with the ranked candidates. The trait default returns `f64::NAN` deliberately; override it when a custom model's callers need displayable or otherwise consumable numeric costs. `DefaultCostModel` provides real values derived from its CSE cost hooks. +- **`estimate_cost`** — attach a comparable numeric cost to an already-constructed replacement. `CandidatePostASAPDAGs::cost_sorted` calls it for every candidate and keeps the returned values aligned with the ranked candidates. The trait default returns `f64::NAN` deliberately; override it when a custom model's callers need displayable or otherwise consumable numeric costs. `DefaultCostModel` provides real values derived from its CSE cost hooks. ```rust fn estimate_cost( @@ -287,9 +287,9 @@ A custom cost model does not necessarily need to override every hook. The curren --- -### `PlanSpace` / `TargetSubDAGCandidates` / `RankedTargetSubDAGCandidates` — the whole-workload view +### `CandidatePostASAPDAGs` / `TargetSubDAGCandidates` / `RankedTargetSubDAGCandidates` — the whole-workload view -`ReplacementStrategy` answers "what are the candidates for this one target?" `PlanSpace` answers the same question for every target in a whole workload at once, without enumerating `2^N` fully-copied plans for `N` independently-choosable sites. +`ReplacementStrategy` answers "what are the candidates for this one target?" `CandidatePostASAPDAGs` answers the same question for every target in a whole workload at once, without enumerating `2^N` fully-copied plans for `N` independently-choosable sites. ```rust // replacement.rs @@ -313,7 +313,7 @@ pub struct RankedTargetSubDAGCandidates<'a> { `search_workload(roots)` runs the shared-subtree pass once, discovers every target across every root's whole DAG (not just root-level sharing — a `SharedSubtreeStrategy` candidate three levels under an unshared `Filter` is exactly as real a site as a shared whole root), and asks every registered strategy to a fixpoint. Two logically different candidates at two different targets are never copied into two separate plans — they're two entries in two different `TargetSubDAGCandidates`s, sharing every other node in the workload by construction. -`PlanSpace::cost_sorted(cost_model)` is the one ranking step: for each candidate set, it dispatches by candidate shape — a same-shape `Rewrite` pair (a `SharedSubtreeStrategy` share/recompute choice) goes through `CostModel::cse_share_decision`; a same-shape run of `Summary` candidates realizing sketches (a `SketchAlgorithmStrategy` choice) goes through `CostModel::rank_candidates`; and a mixed candidate set is ordered by each candidate's `CostModel::estimate_cost`. Every candidate gets a numeric cost aligned index-for-index in `costs`. Count in, count out—nothing is dropped to produce a ranking. Legality checks +`CandidatePostASAPDAGs::cost_sorted(cost_model)` is the one ranking step: for each candidate set, it dispatches by candidate shape — a same-shape `Rewrite` pair (a `SharedSubtreeStrategy` share/recompute choice) goes through `CostModel::cse_share_decision`; a same-shape run of `Summary` candidates realizing sketches (a `SketchAlgorithmStrategy` choice) goes through `CostModel::rank_candidates`; and a mixed candidate set is ordered by each candidate's `CostModel::estimate_cost`. Every candidate gets a numeric cost aligned index-for-index in `costs`. Count in, count out—nothing is dropped to produce a ranking. Legality checks may already have removed proposals before this boundary. In particular, `search_workload_with_targets` checks explicit per-root targets, while retaining direct DDSketch ratios with missing domain evidence and no root guarantee for @@ -370,11 +370,11 @@ The crate provides no default `Matcher` implementation because the answer depend ## 2. Replacement explanations (`explanation.rs`) -`explanation::explain_replacements`/`explain_replacements_with` answer a different question than everything above: not "what could this target become" (`ReplacementStrategy::replacements`) but "why does the replacement already discovered for this target exist, and where." It is a **reporting view over `PlanSpace`**, not a second search or a second rule engine — this crate's *explanation of a replacement*, not an applicability classifier deciding admissibility from scratch. +`explanation::explain_replacements`/`explain_replacements_with` answer a different question than everything above: not "what could this target become" (`ReplacementStrategy::replacements`) but "why does the replacement already discovered for this target exist, and where." It is a **reporting view over `CandidatePostASAPDAGs`**, not a second search or a second rule engine — this crate's *explanation of a replacement*, not an applicability classifier deciding admissibility from scratch. ### The rule -> A `TargetSubDAG` is worth explaining exactly when its `PlanSpace` candidate list contains something beyond the trivial, no-op realization. +> A `TargetSubDAG` is worth explaining exactly when its `CandidatePostASAPDAGs` candidate list contains something beyond the trivial, no-op realization. Concretely, `explanation.rs` reports three candidate kinds from each `TargetSubDAGCandidates`: @@ -390,10 +390,10 @@ Each `ReplacementExplanation::reason` is copied verbatim from the matching candi ### Why there is no `ExplanationRule` trait -Explanations are derived from candidates already present in `PlanSpace`. A new candidate kind therefore requires an `impl ReplacementStrategy` wired into `default_strategies`/`default_strategies_with`; a second explanation-specific trait would duplicate registration and could drift from the actual search space. Custom callers supply strategies through `explain_replacements_with`, using the same extension point exposed by `search_workload_with`. +Explanations are derived from candidates already present in `CandidatePostASAPDAGs`. A new candidate kind therefore requires an `impl ReplacementStrategy` wired into `default_strategies`/`default_strategies_with`; a second explanation-specific trait would duplicate registration and could drift from the actual search space. Custom callers supply strategies through `explain_replacements_with`, using the same extension point exposed by `search_workload_with`. ### How it derives `location` text -`PlanSpace`/`TargetSubDAGCandidates` track `Rc` pointer identity, not human-readable breadcrumbs. `ReplacementExplanation::location` provides prose such as `root "dash_a" > lhs` so reporting consumers can identify the relevant part of the query without interpreting pointer identity. Location derivation does not make replacement or costing decisions. +`CandidatePostASAPDAGs`/`TargetSubDAGCandidates` track `Rc` pointer identity, not human-readable breadcrumbs. `ReplacementExplanation::location` provides prose such as `root "dash_a" > lhs` so reporting consumers can identify the relevant part of the query without interpreting pointer identity. Location derivation does not make replacement or costing decisions. --- diff --git a/docs/develop_docs/end-to-end-accuracy-guarantees.md b/docs/develop_docs/end-to-end-accuracy-guarantees.md index 3a4f6280..072c10c5 100644 --- a/docs/develop_docs/end-to-end-accuracy-guarantees.md +++ b/docs/develop_docs/end-to-end-accuracy-guarantees.md @@ -77,7 +77,7 @@ trait AccuracyModel { approximate layers. Every candidate must then be resized, propagated, and checked before being treated as satisfying the target. This is distinct from candidate visibility: direct DDSketch ratios lacking domain evidence remain in -`PlanSpace` with `guarantee: None` and can appear in `cost_sorted`, but automatic +`CandidatePostASAPDAGs` with `guarantee: None` and can appear in `cost_sorted`, but automatic `global_selection` skips them. Presence and cost are not accuracy certification. See the [workflow design](../design_docs/architecture/input-output-workflow.md#planning-evidence-inputs) for this boundary. diff --git a/docs/develop_docs/extend-asap-aware-mapping.md b/docs/develop_docs/extend-asap-aware-mapping.md index e97ac66f..fddf6091 100644 --- a/docs/develop_docs/extend-asap-aware-mapping.md +++ b/docs/develop_docs/extend-asap-aware-mapping.md @@ -327,7 +327,7 @@ Replacement::Rewrite( This strategy does **not** decide whether sharing is cheaper. That preference belongs to the cost model. -`PlanSpace::cost_sorted` calls `CostModel::cse_share_decision` when it ranks a +`CandidatePostASAPDAGs::cost_sorted` calls `CostModel::cse_share_decision` when it ranks a share-versus-recompute candidate pair. The strategy still returns both alternatives because enumeration and ranking are separate steps: @@ -711,7 +711,7 @@ fn estimate_cost( ) -> f64; ``` -The default returns `f64::NAN`, making the absence of a numeric model explicit. Override this hook when passing the model to `PlanSpace::cost_sorted` if downstream code displays or otherwise consumes the `costs` values. Prefer to derive the result from the same inputs used by `rank_candidates` and the CSE cost hooks so numeric costs do not disagree with relative ordering. +The default returns `f64::NAN`, making the absence of a numeric model explicit. Override this hook when passing the model to `CandidatePostASAPDAGs::cost_sorted` if downstream code displays or otherwise consumes the `costs` values. Prefer to derive the result from the same inputs used by `rank_candidates` and the CSE cost hooks so numeric costs do not disagree with relative ordering. --- @@ -983,9 +983,9 @@ Use this table to find the right place for a change. | Produce a normal (ranked-first) post-ASAP summary for one target | `SketchAlgorithmStrategy::replacements(...).into_iter().next()` | | Search a whole workload for supported legal candidates | `search_workload`/`search_workload_with` | | Enforce per-root result accuracy requirements | `search_workload_with_targets` | -| Coordinate compatible choices across groups | `PlanSpace::global_selection` | +| Coordinate compatible choices across groups | `CandidatePostASAPDAGs::global_selection` | | Assemble the selected logical DAG | `GlobalSelection::assemble_selected_dag` | -| Get every candidate ranked best-first, across a whole workload | `PlanSpace::cost_sorted` | +| Get every candidate ranked best-first, across a whole workload | `CandidatePostASAPDAGs::cost_sorted` | | Get a real numeric cost per candidate, not just a relative rank | `CostModel::estimate_cost` | | Enumerate valid sketch algorithms | `summary_candidates` | | Build a target with no workload context | `TargetSubDAG::new` | diff --git a/docs/develop_docs/library-api.md b/docs/develop_docs/library-api.md index a4119122..1bda9f5a 100644 --- a/docs/develop_docs/library-api.md +++ b/docs/develop_docs/library-api.md @@ -4,7 +4,7 @@ Audience: developers embedding ASAPPlanner or adding strategies/models. This is a compact reference for the public workflow APIs, not an exhaustive symbol reference. The [CLI guide](../user_guide_docs/run-a-query.md) covers command-line inspection; the [design overview](../design_docs/architecture/README.md) defines ownership. -ASAPPlanner's primary output is `PlanSpace`; ranking is a view over its candidates. +ASAPPlanner's primary output is `CandidatePostASAPDAGs`; ranking is a view over its candidates. Downstream owns physical binding and commitment. Selection/DAG assembly helpers do not deploy a plan, and a serializable DAG is not evidence of runtime readiness. @@ -144,7 +144,7 @@ example, see [the CLI frontend example](../../crates/devtools/src/bin/show_pre_a ### Target sub-DAG candidates `TargetSubDAGCandidates` collects alternatives for one query subexpression -discovered by search. `PlanSpace` contains these per-target candidate sets and +discovered by search. `CandidatePostASAPDAGs` contains these per-target candidate sets and the workload's query roots. A root is a whole query; an inner expression can also be a target. @@ -165,9 +165,9 @@ search_workload_with_targets<'s, Id>( roots: Vec<(Id, Rc, Option)>, strategies: &[Box], accuracy_model: &dyn AccuracyModel, -) -> PlanSpace +) -> CandidatePostASAPDAGs -PlanSpace::cost_sorted(&self, cost_model: &dyn CostModel) +CandidatePostASAPDAGs::cost_sorted(&self, cost_model: &dyn CostModel) -> Vec> ``` @@ -181,7 +181,7 @@ PlanSpace::cost_sorted(&self, cost_model: &dyn CostModel) `search_workload_with_targets` normally rejects candidates without a guarantee that satisfies the root target. One exception is a direct DDSketch quantile -ratio: without input-domain evidence, it remains in `PlanSpace` with +ratio: without input-domain evidence, it remains in `CandidatePostASAPDAGs` with `guarantee: None` so the downstream backend can decide whether to select it. Its presence does **not** mean it satisfies the target. `cost_sorted` still shows it, but `global_selection` skips it and DAG assembly uses the exact fallback @@ -254,11 +254,11 @@ fn main() -> Result<(), Box> { | API (`asap_aware_mapping`, unless qualified) | Inputs | Output and limits | | --- | --- | --- | -| `search_workload` | `(query_id, Rc)` roots | `PlanSpace` with built-in strategies/model; no explicit per-root target argument | -| `search_workload_with` | Roots, strategy slice | `PlanSpace`; callers choose context-free replacement strategies | +| `search_workload` | `(query_id, Rc)` roots | `CandidatePostASAPDAGs` with built-in strategies/model; no explicit per-root target argument | +| `search_workload_with` | Roots, strategy slice | `CandidatePostASAPDAGs`; callers choose context-free replacement strategies | | `search_workload_with_targets` | Roots with optional end-to-end targets, strategies, accuracy model | Candidate space with supplied root-target checks; `None` does not supply a root-level requirement; uncertified direct DDSketch ratios remain available for backend selection | -| `PlanSpace::cost_sorted` | Cost model | `Vec`; retains alternatives and pairs `candidates[i]` with `costs[i]` | -| `PlanSpace::cost_sorted_with_recurrence` | Cost model, recurrence profiles, optional horizon | Ranked per-target candidate sets or `RecurrenceError`; uses recurrence for applicable share/recompute comparisons | +| `CandidatePostASAPDAGs::cost_sorted` | Cost model | `Vec`; retains alternatives and pairs `candidates[i]` with `costs[i]` | +| `CandidatePostASAPDAGs::cost_sorted_with_recurrence` | Cost model, recurrence profiles, optional horizon | Ranked per-target candidate sets or `RecurrenceError`; uses recurrence for applicable share/recompute comparisons | | `SketchAlgorithmStrategy::replacements` through `ReplacementStrategy` | One `TargetSubDAG` | Alternatives at that target; not whole-workload search | `cost_sorted` is a ranking view, not a request to discard all but the first @@ -270,7 +270,7 @@ before physical selection; do not treat their presence as deployment permission. ### Enumerate candidate DAGs per root ```text -PlanSpace::enumerate_candidate_dags_for_root(&self, id: &Id, expansion_limit: usize) +CandidatePostASAPDAGs::enumerate_candidate_dags_for_root(&self, id: &Id, expansion_limit: usize) -> Result, RealizationError> ``` @@ -286,7 +286,7 @@ finalized, deduplicated, and marked `ReplacementProvenance::RootPhysicalRealizat Callers do not apply `with_series_identity` themselves. Compile each with `promql_rows::compile_current_series_readout`; other queries keep their previous inventory. `global_selection` never commits these candidates; the backend -compiles and prices them. PlanSpace lists no placement variants: node timing +compiles and prices them. CandidatePostASAPDAGs lists no placement variants: node timing comes from the summary maintenance lifecycle. ## Choose strategies and models @@ -536,7 +536,7 @@ hold. Workload legality and known cost evidence can further restrict alternative ```text global_selection_with_summary_maintenance_lifecycles<'a, Id>( - space: &'a PlanSpace, demand: WorkloadDemand<'_>, + space: &'a CandidatePostASAPDAGs, demand: WorkloadDemand<'_>, now_ms: u64, horizon: Option, capabilities: SummaryMaintenanceLifecycleCapabilities, cost_model: &dyn CostModel, ) -> Result, SummaryMaintenanceLifecycleSelectionError> @@ -578,14 +578,14 @@ entries, construct demand using all applicable indices. ```rust use asap_aware_mapping::{ global_selection_with_summary_maintenance_lifecycles, - assemble_selected_dag_with_summary_maintenance_lifecycles, CostModel, Horizon, PlanSpace, + assemble_selected_dag_with_summary_maintenance_lifecycles, CostModel, Horizon, CandidatePostASAPDAGs, SummaryMaintenanceLifecycleCapabilities, SummaryMaintenanceLifecyclePlan, WorkloadDemand, }; use asap_types::workload::PlanningWorkload; fn plan_batch_root( - space: &PlanSpace<&str>, + space: &CandidatePostASAPDAGs<&str>, workload: &PlanningWorkload, entry_index: usize, now_ms: u64, @@ -631,7 +631,7 @@ that prepared or retained shared state is supported. | Function | Inputs | Output / promise | | --- | --- | --- | | `plan_summary_maintenance_lifecycles` | Assembled logical DAG root, `WorkloadDemand`, `now_ms`, optional horizon, runtime capabilities, cost model | `Result` for that fixed root; does not revisit all semantic candidates | -| `global_selection_with_summary_maintenance_lifecycles` | `PlanSpace`, workload/root-entry associations, time, horizon, capabilities, cost model | Lifecycle-aware compatible selection/error, using eligible cost evidence | +| `global_selection_with_summary_maintenance_lifecycles` | `CandidatePostASAPDAGs`, workload/root-entry associations, time, horizon, capabilities, cost model | Lifecycle-aware compatible selection/error, using eligible cost evidence | | `assemble_selected_dag_with_summary_maintenance_lifecycles` | Selection, target root and lifecycle context | Optional lifecycle plan/error; attaches state deployment decisions | | `enumerate_summary_maintenance_lifecycles` | Same inputs as `plan_summary_maintenance_lifecycles` | `SummaryMaintenanceLifecycleCandidates`: per unique retained state, every alternative with its cost or rejection; nothing selected. `guarantee(&lifecycle)` gives the mode/schedule that alternative would carry | | `SummaryMaintenanceLifecycleCandidates::select(choices)` | One `(PostAsapNodeId, SummaryMaintenanceLifecycle)` per state, copied from `deployments()` | The same `SummaryMaintenanceLifecyclePlan` Planner selection would produce for that combination, or `SummaryMaintenanceLifecycleChoiceError` when a choice is unknown, missing, duplicated, rejected, schedule-incompatible, or not completely estimable | @@ -731,10 +731,10 @@ Plain `global_selection()` does not automatically perform lifecycle planning or establish physical deployment feasibility. Use the corresponding evidence-aware workflow for those decisions. Downstream still owns physical commitment. -| Method on `PlanSpace` / `GlobalSelection` | Behavior | +| Method on `CandidatePostASAPDAGs` / `GlobalSelection` | Behavior | | --- | --- | -| `PlanSpace::global_selection(&model)` | Compatible structural selection across targets; no recurrence or lifecycle planning implied | -| `PlanSpace::global_selection_with_recurrence(...)` | Compatible selection using supplied recurrence profiles/horizon; no lifecycle commitments implied | +| `CandidatePostASAPDAGs::global_selection(&model)` | Compatible structural selection across targets; no recurrence or lifecycle planning implied | +| `CandidatePostASAPDAGs::global_selection_with_recurrence(...)` | Compatible selection using supplied recurrence profiles/horizon; no lifecycle commitments implied | | `GlobalSelection::assemble_selected_dag(&target)` | `Result>, RealizationError>`; constructs semantic IR, not stored summary data | Use a target associated with the searched space; DAG assembly can return `None` @@ -746,7 +746,7 @@ for checking complete physical alternatives and deployment constraints. ### API definition and example ```text -PlanSpace::global_selection(&self, cost_model: &dyn CostModel) -> GlobalSelection<'_> +CandidatePostASAPDAGs::global_selection(&self, cost_model: &dyn CostModel) -> GlobalSelection<'_> GlobalSelection::assemble_selected_dag(&self, target: &Rc) -> Result>, RealizationError> ``` @@ -793,7 +793,7 @@ fn main() -> Result<(), Box> { let root = Rc::new(lower_promql_workload(&workload, 0)?.remove(0)); let space = search_workload(vec![("q1", root)]); let selection = space.global_selection(&DefaultCostModel); - // Search may canonicalize roots; use the root returned by PlanSpace. + // Search may canonicalize roots; use the root returned by CandidatePostASAPDAGs. if let Some(summary) = selection.assemble_selected_dag(&space.roots[0].1)? { let graph = asap_types::dag_export::export_summary(&summary); println!("{graph:#?}"); diff --git a/docs/develop_docs/target-candidate-api-migration.md b/docs/develop_docs/target-candidate-api-migration.md index b6c20584..e2b6ab82 100644 --- a/docs/develop_docs/target-candidate-api-migration.md +++ b/docs/develop_docs/target-candidate-api-migration.md @@ -6,8 +6,8 @@ are unchanged. #453 separately defines the integration API surface. | Previous name | New name | Meaning | |---|---|---| -| `PlanSpace::groups()` | `PlanSpace::target_subdag_candidates()` | Iterate candidate sets, one per target, in discovery order | -| `PlanSpace::group_for(target)` | `PlanSpace::candidates_for_target(target)` | Look up one target's candidate set | +| `CandidatePostASAPDAGs::groups()` | `CandidatePostASAPDAGs::target_subdag_candidates()` | Iterate candidate sets, one per target, in discovery order | +| `CandidatePostASAPDAGs::group_for(target)` | `CandidatePostASAPDAGs::candidates_for_target(target)` | Look up one target's candidate set | | `SelectedGroup` | `TargetSubDAGSelection` | Selected choice and usage information for one target; the choice may be absent | | `GlobalSelection::groups()` | `GlobalSelection::target_selections()` | Iterate decisions, not alternative sets | | `MaterializeSummaryMaintenanceLifecycleError` | `SummaryMaintenanceLifecycleAssemblyError` | Failure assembling a DAG or deriving maintenance decisions | diff --git a/docs/user_guide_docs/run-a-query.md b/docs/user_guide_docs/run-a-query.md index 3c163d26..89c47c42 100644 --- a/docs/user_guide_docs/run-a-query.md +++ b/docs/user_guide_docs/run-a-query.md @@ -104,7 +104,7 @@ and provide your own models. The default strategy generates DDSketch quantile-ratio candidates even when no input-domain evidence is available. Such candidates have `guarantee: None`: they do not claim a certified end-to-end accuracy bound. They also remain -visible in a target-aware `PlanSpace` so the downstream backend can decide +visible in a target-aware `CandidatePostASAPDAGs` so the downstream backend can decide whether to select them using its own evidence. Planner's automatic `global_selection` skips them; their presence alone does not show that they meet the requested target.