Skip to content

refactor!: AccuracyTarget builders, AccuracyMetric naming, simpler Stage 3 interfaces and unread workload fields removed - #722

Merged
zzylol merged 5 commits into
mainfrom
refactor/accuracy-target-api
Oct 15, 2026
Merged

zzylol merged 5 commits into
mainfrom
refactor/accuracy-target-api

Conversation

@zzylol

@zzylol zzylol commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Why

Review of #690, #712 and #717 found several interfaces that are harder to read than they need to be, and several fields that no stage reads:

  • AccuracyTarget::epsilon_delta(ε, δ).with_metric(m) names its builders after Greek letters and a with_ prefix that the fields do not have. target_in and for_quantile are planner sizing helpers, but they sit on the public type, and every frontend calls them.
  • ErrorMetric and the metric / scope fields of a target do not say that they are about accuracy.
  • Budget splitting has two functions where one is enough (equal_shares, equal_share). whole_result_guarantee takes a fractional count of values. When resizing one candidate for its failure probability fails, the whole select_plan call fails.
  • TimeSelection (with QueryTimeScope) is set on every workload entry, but no stage reads it: the window comes from the query text. DataWorkload.ingestion_volume, DataWorkload.distribution and PropagationStats.data_distribution are also never read.

What

  • AccuracyTarget is built with exact() or max_error(ε), plus the optional builders .failure_probability(δ), .accuracy_metric(m) and .accuracy_scope(s). epsilon, epsilon_delta, with_metric, with_scope, target_in and for_quantile are removed.
  • ErrorMetric is renamed AccuracyMetric everywhere. On AccuracyTarget and AccuracySpec, the fields metric and scope become accuracy_metric and accuracy_scope; the JSON keys change with them.
  • The frontends store the query's target on each intent exactly as the user wrote it. Stage 1 restates it in the intent's own metric when it sizes a summary.
  • per_input_target replaces equal_shares and equal_share. The method AccuracyGuarantee::over_whole_result(values_per_result: Option<u64>) replaces whole_result_guarantee.
  • If a candidate's summaries cannot be rebuilt after Stage 3 resizes them for a failure probability, Stage 3 rejects that candidate with the reason. It no longer fails the whole selection, so SizingError and SelectionError::Sizing are gone. Selection.accepted holds one AcceptedAccuracy per query, with no Option.
  • Renames:
    • sized_roots → resize_for_failure_probability
    • summary_source → distinct_bound_source
    • hll::precision_for → hll::min_precision
  • Deleted because nothing outside tests reads them:
    • TimeSelection, QueryTimeScope, and the time_selection field of QueryWorkloadEntry, BatchEntry, RepeatingEntry and ComparisonScope
    • DataWorkload.ingestion_volume, DataWorkload.distribution and the DataDistribution type
    • PropagationStats.data_distribution
  • Stale comments from feat(planner)!: Stage 2 materialization, the latency check and simplified query requirements (Example 4) #717 are fixed: the Selection.accepted doc, the summary_sharing.rs module doc, "ImplicitExact", and "null without a target" in stages.js.

Out of scope: the Pass 2 *Candidate renames, the FamilyCapability changes and the cost model.

How

  • Targets. realization::accuracy_target(intent) now returns an owned target. It restates the intent's raw target in the intent's own metric through a private target_in helper:

    • an L1-scaled absolute error for Count and TopK;
    • a relative error for Cardinality and FrequencyL2;
    • an absolute error for FrequencyEntropy;
    • for Quantile, the set metric, or Rank when none is set.

    This is what the frontends used to stamp onto each intent. Every Stage 1 sizing and per-summary check already went through this accessor. Stage 3 still checks the query's own target at the root.

    An equality check that compares intents must restate both sides the same way. The legacy strictest_sibling_accuracy compared a restated target with a written one, and is fixed to compare both intents under the same target.

  • Budget split. per_input_target is the one-share case of the old equal_shares. Only the legacy core used shares > 1, for a nested sub-DAG that holds n − 1 of n layers, and that case cannot be written as a per-input split. It stays there as the private nested_layers_target.

  • Resize rejection. sized_for_failure_probability now returns the rejection reason as a String. select_plan records it as a Rejection { valid: false, .. } and goes on to the next candidate.

  • Deleted fields. For each deleted field, the only readers were tests, the ComparisonScope equality check, and the evidence copy into PropagationStats. The integration test that read time_selection.lookback now takes each window from the PATTERN_A / PATTERN_B_WINDOW_MS constants, which restate the query text. The empirical_cost distribution is a different type (DistributionDescriptor) and stays.

Key code interfaces added / modified

crates/types/src/types.rs

/// Which error quantity an accuracy statement bounds. The type stays; renamed.
pub enum AccuracyMetric {                            // was ErrorMetric
    /// `|estimate − truth| ≤ ε`, in value units or in multiples of a stream norm.
    Absolute { scale: Option<NormScale> },           // unchanged
    /// `|estimate − truth| ≤ ε · |truth|`.
    Relative,                                        // unchanged
    /// The returned value's rank is within `ε · n` of the requested rank.
    Rank,                                            // unchanged
}

/// The outer shape of a guarantee: `Pr[err > error] ≤ failure_probability` over
/// `accuracy_scope`. The type stays; two fields are renamed (JSON keys too).
pub struct AccuracySpec<E, P> {
    /// Which error quantity `error` bounds.
    pub accuracy_metric: AccuracyMetric,             // was metric
    /// ε, in `accuracy_metric`'s units.
    pub error: E,                                    // unchanged
    /// δ.
    pub failure_probability: P,                      // unchanged
    /// Which output values one statement covers at once.
    pub accuracy_scope: AccuracyScope,               // was scope
}

/// A query's accuracy requirement as the user states it. The type stays; two fields
/// are renamed (JSON keys `accuracy_metric`, `accuracy_scope`).
pub struct AccuracyTarget {
    /// The metric ε is stated in; None (default) accepts a guarantee in any metric.
    pub accuracy_metric: Option<AccuracyMetric>,     // was metric
    /// ε, the largest error allowed.
    pub error: f64,                                  // unchanged
    /// δ; None (default) imposes no failure-probability requirement.
    pub failure_probability: Option<f64>,            // unchanged
    /// Which values the target covers at once; default WholeResult.
    pub accuracy_scope: AccuracyScope,               // was scope
}

impl AccuracyTarget {
    /// No approximation permitted (ε = 0, δ = 0).
    pub const fn exact() -> Self;                                                // unchanged
    /// Error at most `error` over the whole result, no δ and no metric set.
    pub const fn max_error(error: f64) -> Self;                                  // new; was epsilon(ε)
    /// This target, met with probability at least `1 − failure_probability`.
    pub const fn failure_probability(self, failure_probability: f64) -> Self;   // new; epsilon_delta(ε, δ) is max_error(ε).failure_probability(δ)
    /// This target with ε stated in `accuracy_metric`.
    pub const fn accuracy_metric(self, accuracy_metric: AccuracyMetric) -> Self; // was with_metric
    /// This target over `accuracy_scope`.
    pub const fn accuracy_scope(self, accuracy_scope: AccuracyScope) -> Self;    // was with_scope
    /// Whether this target permits no approximation.
    pub fn is_exact(&self) -> bool;                                              // unchanged
    pub const fn epsilon(epsilon: f64) -> Self;                                  // removed
    pub const fn epsilon_delta(epsilon: f64, delta: f64) -> Self;                // removed
    pub fn target_in(&self, metric: ErrorMetric) -> Self;                        // removed: now private in realization.rs
    pub fn for_quantile(&self) -> Self;                                          // removed: folded into accuracy_target
}

crates/types/src/post_asap/guarantee.rs

/// The statistic an unbound number of values per result is left as; nothing binds it.
pub const VALUES_PER_RESULT: &str = "values_per_result";   // new; was accuracy::OUTPUT_VALUES = "output_values_per_evaluation"

impl AccuracyGuarantee {
    /// This guarantee over one evaluation's whole result of `values_per_result` values
    /// (`G`): δ becomes `G · δ` by the union bound; None leaves δ symbolic in
    /// VALUES_PER_RESULT. Stage 3 uses it to check a whole-result target.
    pub fn over_whole_result(&self, values_per_result: Option<u64>) -> Self;   // new; was accuracy::whole_result_guarantee(g, output_values: Option<f64>)
}

crates/asap-aware-mapping/src/accuracy/budget.rs

/// The target of one of `approximate_inputs` approximate inputs that compose into a
/// value with `target`, split equally in `metric`'s composition; None for an exact or
/// non-positive target. Stage 1 sizes a combination's summaries with it.
pub fn per_input_target(
    target: &AccuracyTarget,
    metric: AccuracyMetric,
    approximate_inputs: usize,
) -> Option<AccuracyTarget>;                                                   // new
pub fn equal_shares(target: &AccuracyTarget, metric: ErrorMetric, of: usize, shares: usize) -> Option<AccuracyTarget>; // removed: legacy core keeps a private nested_layers_target
pub fn equal_share(target: &AccuracyTarget, metric: ErrorMetric, approximate_inputs: usize) -> Option<AccuracyTarget>; // removed: now per_input_target

crates/asap-aware-mapping/src/realization.rs

/// The target an approximate-capable intent's summary is sized and checked against:
/// the query's target restated in the intent's own metric; None for other intents.
pub fn accuracy_target(intent: &AggIntent) -> Option<AccuracyTarget>;   // was -> Option<&AccuracyTarget>, the intent's stamped target

crates/asap-aware-mapping/src/stage3_plan_selection.rs

/// Stage 3's result. The type stays; `accepted` loses its Option.
pub struct Selection {
    /// The id of the cheapest valid candidate.
    pub selected: String,                                        // unchanged
    /// The selected candidate's roots with confirmed weights: the plan to run.
    pub plan: Vec<QueryRoot>,                                    // unchanged
    /// Per valid candidate, its cost per second.
    pub costs: BTreeMap<String, CandidateCost>,                  // unchanged
    /// Per valid candidate, per query, the accuracy Stage 3 accepted.
    pub accepted: BTreeMap<String, Vec<AcceptedAccuracy>>,       // was Vec<Option<AcceptedAccuracy>>
    /// Every candidate not selected, with why.
    pub rejected: Vec<Rejection>,                                // unchanged
}

/// What Stage 3 accepted for one query of a valid candidate. The type stays; its
/// metric fields change type with the AccuracyMetric rename.
pub struct AcceptedAccuracy {
    /// The target's metric; None when the user did not set one.
    pub target_metric: Option<AccuracyMetric>,       // was Option<ErrorMetric>
    /// The metric of the guarantee that met the target; None for an exact output.
    pub accepted_metric: Option<AccuracyMetric>,     // was Option<ErrorMetric>
    /// The sketch algorithms the query's output reads, in plan order; empty if exact.
    pub sketches: Vec<String>,                       // unchanged
}

/// Why select_plan failed. The type stays; one variant is removed.
pub enum SelectionError {
    /// Bad input: the number of workload entries differs from a candidate's roots.
    EntryCount { candidate: String, roots: usize, entries: usize },                // unchanged (now tested)
    /// Internal bug or bad calibration: a node of a candidate could not be priced.
    Cost { candidate: String, node: PhysicalASAPNodeId, error: AnalyticalCostError }, // unchanged
    /// Unsatisfiable request: every candidate was rejected.
    NoValidCandidate(Vec<Rejection>),                                              // unchanged
    Sizing { candidate: String, error: SizingError },                              // removed: the candidate is rejected instead
}

pub enum SizingError { Structure(SchemaDerivationError), Export(ExecutionDataStateError) }   // removed

/// `roots` with each summary enlarged for its queries' failure probabilities over
/// their values per result; the plan Stage 3 prices and runs.
pub fn resize_for_failure_probability(
    roots: &[QueryRoot],
    targets: &[AccuracyTarget],
    data: &DataWorkload,
) -> Result<Vec<QueryRoot>, SchemaDerivationError>;                            // was sized_roots

crates/asap-aware-mapping/src/accuracy/estimators/hll.rs, crates/asap-aware-mapping/src/per_computation_candidates.rs

/// The smallest classic-HLL precision, at least `at_least`, whose failure probability
/// at `relative_error` over at most `max_distinct` items is at most `failure_probability`.
pub(crate) fn min_precision(
    relative_error: f64,
    failure_probability: f64,
    max_distinct: u64,
    at_least: u8,
) -> u8;                                                                        // was precision_for(precision, relative_error, max_distinct, delta)
/// The one source whose distinct bound gives a summary state's failure probability.
pub(crate) fn distinct_bound_source(state: &OperatorNode) -> Option<String>;  // was summary_source

crates/types/src/workload.rs

/// One-shot query entry of a batch. The type stays; one field is removed.
pub struct BatchEntry {
    /// The query text; its window (range, offset) is in the text.
    pub query: Query,                          // unchanged
    /// Accuracy target and latency bound.
    pub requirements: QueryRequirements,       // unchanged
    /// How far ahead the query is known.
    pub predictability: Predictability,        // unchanged
    /// How many times the batch runs it.
    pub invocations: u64,                      // unchanged
    /// Known execution time, if any.
    pub execute_at: Option<TimestampMs>,       // unchanged
    pub time_selection: TimeSelection,         // removed
}

/// One query with repeated demand. The type stays; one field is removed.
pub struct RepeatingEntry {
    /// The query text; its window is in the text.
    pub query: Query,                          // unchanged
    /// How often it repeats.
    pub demand: RepeatedDemand,                // unchanged
    /// Accuracy target and latency bound.
    pub requirements: QueryRequirements,       // unchanged
    /// How far ahead the query is known.
    pub predictability: Predictability,        // unchanged
    pub time_selection: TimeSelection,         // removed
}

/// The planner's normalized form of either entry. The type stays; one field is removed.
pub struct QueryWorkloadEntry {
    /// The query text in the workload's language.
    pub query: Query,                          // unchanged
    /// Its accuracy target and latency bound (default: exact, no bound).
    pub requirements: QueryRequirements,       // unchanged
    /// How far ahead it is known (default: unknown).
    pub predictability: Predictability,        // unchanged
    /// How often it runs (default: unknown).
    pub recurrence: QueryRecurrence,           // unchanged
    pub time_selection: TimeSelection,         // removed
}

/// Facts about the data being queried. The type stays; two fields are removed.
pub struct DataWorkload {
    /// At rest, continuously ingesting, or both.
    pub arrival: DataArrival,                                            // unchanged
    /// Cadence at which each PromQL series supplies a sample.
    pub data_ingestion_interval: Evidence<DurationMs>,                   // unchanged
    /// Samples arriving per second across all series.
    pub ingestion_rate: Evidence<Rate>,                                  // unchanged
    /// Number of input series (or keys).
    pub input_cardinality: Evidence<u64>,                                // unchanged
    /// Per-source stream norms that scale Count-Min / Count Sketch errors.
    pub stream_norms: Evidence<BTreeMap<String, StreamNorms>>,           // unchanged
    /// Per-source bound on distinct items per evaluation (classic HLL δ).
    pub max_distinct_per_evaluation: Evidence<BTreeMap<String, u64>>,    // unchanged
    /// Declared Prometheus type per metric.
    pub metric_types: BTreeMap<String, MetricType>,                      // unchanged
    pub ingestion_volume: Evidence<u64>,                                 // removed: never read
    pub distribution: Evidence<DataDistribution>,                        // removed: only copied to an unread stat
}

pub struct TimeSelection { scope: QueryTimeScope, lookback: Option<DurationMs>, as_of: Option<TimestampMs> } // removed
pub enum QueryTimeScope { RealTime, Longitudinal, Mixed, Unknown }                                           // removed
pub enum DataDistribution { Zipf, Uniform, Bursty }                                                          // removed

crates/asap-aware-mapping/src/accuracy/evidence.rs

/// Statistics a propagation rule may consult; every field defaults to unknown.
/// The type stays; one field is removed.
pub struct PropagationStats {
    /// Certified ranges of a ratio's true numerator and denominator.
    pub division_operand_domains: Option<[OperandDomain; 2]>,          // unchanged
    /// Provenance for supplied evidence.
    pub evidence_provenance: Vec<GuaranteeSource>,                       // unchanged
    /// Whether every input value is known non-negative.
    pub values_non_negative: Option<bool>,                               // unchanged
    /// The statistic an unknown sign is named by.
    pub sign_statistic: Option<String>,                                  // unchanged
    /// Input rows an exact aggregation consumes.
    pub input_row_count: Option<u64>,                                    // unchanged
    /// The statistic an unknown row count is named by.
    pub row_count_statistic: Option<&'static str>,                       // unchanged
    /// Lower confidence bound of the kth selected TopK item.
    pub topk_selected_lower_bound: Option<f64>,                          // unchanged
    /// Greatest upper confidence bound among excluded TopK items.
    pub topk_excluded_upper_bound: Option<f64>,                          // unchanged
    /// Union-bound δ of the TopK margin intervals.
    pub topk_interval_failure_probability: Option<f64>,                  // unchanged
    /// Hydra shared-grid collision error.
    pub hydra_shared_grid_collision_bound: Option<f64>,                  // unchanged
    /// δ assigned to the Hydra shared-grid term.
    pub hydra_shared_grid_failure_probability: Option<f64>,              // unchanged
    pub data_distribution: Option<DataDistribution>,                     // removed: no rule read it
}

crates/asap-aware-mapping/src/physical_operator_statistics.rs

/// The boundary within which two resource estimates may be compared. The type stays;
/// one field is removed.
pub struct ComparisonScope {
    /// Whether the data is at rest or arriving.
    pub data_arrival: DataArrival,           // unchanged
    /// The planning instant.
    pub planning_time: TimestampMs,          // unchanged
    /// The finite horizon costs are summed over.
    pub horizon: DurationMs,                 // unchanged
    /// How often the query runs.
    pub recurrence: QueryRecurrence,         // unchanged
    /// The exact source selections covered.
    pub sources: Vec<ScanSelection>,         // unchanged
    pub time_selection: TimeSelection,       // removed (and its equality check)
}

The JSON and viewer formats change in the same way:

  • AccuracyTarget and AccuracySpec use the keys "accuracy_metric" and "accuracy_scope" (previously "metric" and "scope").
  • Workload entries no longer take "time_selection". DataWorkload no longer takes "ingestion_volume" or "distribution".
  • In the stage_pipeline output, stage3_plan_selection.accepted[id] is never null.
  • tools/dag-viewer/stages.js reads accuracy_metric.

Usage, end to end. One PromQL p99 panel with a rank-error target over its whole result, planned with run_planning_stages at this head. The numbers come from running this code as a test.

// 1. What the user writes: one dashboard panel, the p99 latency of every series over
//    the last 5 minutes, refreshed every 10 s. `requirements` is its target in the
//    workload JSON with the renamed keys: a rank error of at most 0.01, failing with
//    probability at most 0.01 over the whole returned result (the default
//    `accuracy_scope`). Stage 3 checks the query's output against it.
let requirements: QueryRequirements = serde_json::from_str(
    r#"{"accuracy": {"accuracy_metric": "rank", "error": 0.01, "failure_probability": 0.01}}"#,
)?;
// `target`: the same target through the builders, compared with the parsed one.
let target = AccuracyTarget::max_error(0.01)
    .failure_probability(0.01)
    .accuracy_metric(AccuracyMetric::Rank);
assert_eq!(requirements.accuracy, target);
assert_eq!(target.accuracy_scope, AccuracyScope::WholeResult);
// `panel`: the repeating entry. The 5-minute window is in the query text; there is no
// `time_selection` field any more.
let panel = RepeatingEntry {
    query: Query("quantile_over_time(0.99, latency_ms[5m])".into()),
    demand: RepeatedDemand::FixedInterval(RepetitionInterval(10_000)),
    requirements,
    predictability: Predictability::Predictable { known_at: None },
};
// 2. `data`: what the deployment declares. 10,000 series bound the values one
//    evaluation returns (G = 10,000); a sample per series every 15 s.
let data = DataWorkload {
    arrival: DataArrival::ContinuouslyIngesting,
    data_ingestion_interval: declared(DurationMs(15_000)),
    ingestion_rate: declared(Rate(10_000.0 / 15.0)),
    input_cardinality: declared(10_000),
    ..Default::default()
};
// `workload`: the planner input, the query workload plus the data facts.
let workload = PlanningWorkload {
    query_workload: QueryWorkload {
        language: QueryLanguage::PromQL,
        query_batch: None,
        repeating_queries: Some(vec![panel]),
    },
    data_workload: Some(data),
};
// 3. Stage 0 (query-language frontends): `roots` holds one root per entry. The
//    quantile intent keeps `target` as written; frontends no longer call target_in.
let roots = lower_promql_roots(&workload)?;
// 4. Stages 1-3. `run` holds every stage's candidates and the selection.
//    Stage 1 sizes the KLL candidate through `accuracy_target(intent)`, which restates
//    the target in the quantile's metric (Rank, set here): `Kll { k: 269 }`.
//    Stage 3 calls `resize_for_failure_probability`. The target covers the whole
//    result of G = 10,000 values, so each per-series KLL must fail with probability at
//    most 0.01 / 10,000 = 1e-6: `Kll { k: 521, failure_probability: Some(1e-6) }`.
//    It then checks `guarantee.over_whole_result(Some(10_000))`, whose δ is
//    10,000 · 1e-6 = 0.01 ≤ 0.01.
let run = run_planning_stages(&workload, roots, None)?;
// `selection`: Stage 3's result, read below.
let selection = run.stage3_plan_selection?;
// 5. What the user gets back.
//    The selected plan is P2, the KLL plan, at 0.126 cost/s; it runs the resized
//    `Kll { k: 521, failure_probability: Some(1e-6) }`.
assert_eq!(selection.selected, "P2");
//    `accepted` has one entry per query, with no Option:
//    P1 "metric rank; accepted exact", P2 "metric rank; accepted rank (Kll)".
assert_eq!(selection.accepted["P2"][0].to_string(), "metric rank; accepted rank (Kll)");
//    `rejected`: the exact plan P1 is valid but "costlier: 0.144 vs 0.126
//    cost_per_second". The DDSketch plans P4 and P5 are invalid: "the output
//    guarantees a Relative bound Some(0.01) ... which misses target AccuracyTarget
//    { accuracy_metric: Some(Rank), ... }", because a set metric accepts only that
//    metric. A candidate whose resized summaries did not rebuild would show up here
//    as "resizing for the failure probability failed: ..." instead of failing the call.

Before this PR

  • A caller wrote AccuracyTarget::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::Rank), and the JSON was {"metric": "rank", ...}.
  • Each frontend called target_in or for_quantile on every intent.
  • One candidate failing to rebuild after the failure-probability resize made select_plan return Err(SelectionError::Sizing), which hid every other candidate.
  • Every workload entry had to carry a time_selection that no stage read.

After this PR

  • A caller writes AccuracyTarget::max_error(0.01).failure_probability(0.01).accuracy_metric(AccuracyMetric::Rank), and the JSON is {"accuracy_metric": "rank", ...}.
  • Intents keep the user's target as written, and sizing restates it in one place.
  • A candidate that fails to rebuild after resizing is one rejected line, and selection still returns a plan.
  • Workload entries and data workloads carry only fields that some stage reads.

Verification

  • cargo nextest run --workspace after rebasing onto c127c62c: 1855 passed, 35 skipped.
  • cargo test --doc -p asap-types -p asap-aware-mapping: passed.
  • cargo fmt --all -- --check and cargo clippy --workspace --all-targets --all-features --locked -- -D warnings: clean.
  • tools/verify_metricsql_external_consumer.sh: passed.
  • python3 -m unittest test_viewer test_server in tools/dag-viewer: 50 tests OK. The unset-metric assertion in test_server.py now checks accuracy_metric, so it is no longer vacuous.
  • New tests:
    • a_candidate_that_fails_to_resize_is_rejected_alone. Written first, it failed on the previous commit with Sizing { candidate: "P2", error: Export(UntimedNode { operator: "Project" }) } and passes now.
    • an_entry_count_mismatch_is_an_error.
    • identical_count_and_distinct_siblings_share_the_strictest_target. This is a regression found in review: two count by (job) siblings (and two distinct-count siblings) at ε 0.05 and 0.01, with no metric set, must size the looser one for 0.01. Written first, it failed on the reviewed head (None instead of Some(max_error(0.01))) and passes after the fix. I found no other equality check that mixes a written target with a restated one: shared_quantile_target compares written targets on both sides, and reconciliation compares restated targets on both sides.
    • a_nested_sub_dag_gets_its_layers_share_of_the_target, which keeps the coverage of the 2-of-3 split that equal_shares had.
  • tools/dag-viewer/examples/planner-layering-example1.json was regenerated. Its diff is only the key renames, plus intents that no longer carry the frontend-stamped "accuracy_metric": "absolute_stream_l1".
  • The Usage numbers come from running that code as a temporary test at this head. The test is not committed.

Limitations and follow-ups

  • Behavior change. PromQL used to stamp an absolute metric on FrequencyL2 intents, while SQL and the root-metric table used a relative one. The single restating helper now uses relative for both. No test depended on the absolute version.
  • Docs.
    • The design doc planner-layering.md still uses lookback and as_of as columns in its example tables, and it still lists "volume" and "distribution" among the data-workload facts in its overview table and diagram. I only removed time_selection from the stage table, said that lookback and as_of are stated by the query text (its range and offset) rather than workload.rs fields, replaced as_of: None with "no offset", and dropped the ingestion_volume and distribution rows from the shared-data-workload table. A fuller rewrite belongs with the docs work in Update the docs after the G1–G11 consolidation groups land #677.
    • The historical proposal end-to-end-accuracy-guarantees.md shows a ResultGuarantee { metric: AccuracyMetric, .. }. Only the type name was renamed there.
  • Existing names this PR touches that still fall short:
    • AccuracyTarget.error would read better as max_error, to match its constructor.
    • Rejection.valid reads as a property of the rejection; passed_checks would say what it means.
    • realization::accuracy_target returns the restated target, so sizing_target would say that.
    • The stage_pipeline and viewer request flags --epsilon / --metric (and the "metric" key of the viewer's /api/plan request) still use the old words.
    • The PATTERN_A tuple (query, window, offset) has unnamed fields.
  • PropagationStats.topk_* are set only by tests and by deployment evidence providers, but a rule reads them, so they stay.
  • The rest of the DataWorkload reshape (a sources map, SourceFacts) is left for a later PR.

- AccuracyTarget is built with exact() or max_error(ε) and the optional
  builders failure_probability(δ), accuracy_metric(m) and accuracy_scope(s);
  epsilon, epsilon_delta, with_metric and with_scope are removed.
- ErrorMetric is renamed AccuracyMetric, and the metric and scope fields of
  AccuracyTarget and AccuracySpec are renamed accuracy_metric and
  accuracy_scope (JSON keys too).
- target_in and for_quantile leave the public type. Frontends keep the
  query's target on each intent as written; asap-aware-mapping's
  accuracy_target restates it in the intent's own metric for sizing.
…erfaces

- equal_shares and equal_share become one per_input_target(target,
  metric, approximate_inputs); the legacy core keeps its nested-layer
  split as a private helper.
- whole_result_guarantee(g, output_values: Option<f64>) becomes
  AccuracyGuarantee::over_whole_result(values_per_result: Option<u64>),
  and OUTPUT_VALUES becomes VALUES_PER_RESULT.
- A candidate whose summaries fail to rebuild after resizing for the
  failure probability is rejected with the reason instead of failing
  select_plan; SizingError and SelectionError::Sizing are removed.
- Selection.accepted holds one AcceptedAccuracy per query (no Option).
- Renames: sized_roots -> resize_for_failure_probability, summary_source
  -> distinct_bound_source, hll::precision_for -> hll::min_precision.
- Tests: the resize rejection (failing first) and the EntryCount error.
- TimeSelection, QueryTimeScope and the time_selection field of
  QueryWorkloadEntry, BatchEntry and RepeatingEntry: the window a query
  reads is in its text. ComparisonScope loses the copy it compared.
- DataWorkload.ingestion_volume and DataWorkload.distribution, the
  DataDistribution type, and PropagationStats.data_distribution, which
  only carried it to no reader.
strictest_sibling_accuracy compared a sibling's written target with a
restated one, so identical count and distinct-count siblings never
matched and the strictest-sibling candidate was never offered. Compare
both intents with the same target. Also fix a vacuous viewer assertion
and the stale lookback/as_of field claim in planner-layering.md.
@zzylol
zzylol force-pushed the refactor/accuracy-target-api branch from 1177e49 to 2af382c Compare October 15, 2026 22:59
@zzylol
zzylol merged commit f4a5b98 into main Oct 15, 2026
4 of 5 checks passed
@zzylol zzylol added accuracy Accuracy requirements, guarantees, and propagation workload Batch, repeating, or multi-query workload modeling labels Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

accuracy Accuracy requirements, guarantees, and propagation workload Batch, repeating, or multi-query workload modeling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant