Skip to content

feat(types): one AccuracyTarget with an optional, derived metric; any metric accepted when unset - #718

Merged
zzylol merged 1 commit into
mainfrom
feat/unify-accuracy-target
Oct 15, 2026
Merged

zzylol merged 1 commit into
mainfrom
feat/unify-accuracy-target

Conversation

@zzylol

@zzylol zzylol commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Why

The user asked for one accuracy-target type whose metric is optional (#690 revisions, 2026-10-15). Before this PR a workload stated a RequestedAccuracy, the planner resolved it per query root to an AccuracyTarget with a concrete metric from a per-intent default table, and Stage 3 accepted only guarantees in that metric. A quantile without a metric was an input error, though the user may not care whether its error is a rank error (KLL) or a relative one (DDSketch).

What

  • RequestedAccuracy and AccuracyTarget are merged into one plain struct, AccuracyTarget, whose metric is optional.
  • New acceptance rule. A target that sets no metric accepts a guarantee in any metric (absolute at any scale, relative, rank) that meets its ε, δ and scope, each compared in the guarantee's own metric. A target that sets a metric keeps today's rule: only guarantees in that metric, or convertible to it, qualify.
  • A quantile without a metric is no longer an input error, so KLL and DDSketch compete on cost. AccuracyRequestError::MissingQuantileMetric and SelectionError::AccuracyRequest are removed.
  • Reporting. Stage 3 reports, for each valid candidate and each query, the metric it accepted and whether the target set one, e.g. metric not set; accepted rank (Kll). stage_pipeline writes this to its document and the dag-viewer shows it.
  • The per-intent default-metric table stays, but only for sizing and display (default_metric_for_root). It no longer rejects candidates.
  • Breaking JSON change. AccuracyTarget reads and writes as {"metric"?, "error", "failure_probability"?, "scope"?}. The old forms "Exact", {"Epsilon": ε} and {"EpsilonDelta": {...}} are no longer accepted. This PR has no compatibility shim.

How

  • types.rs: AccuracyTarget is now a struct with serde defaults: metric: None, failure_probability: None and scope: whole_result. It has no wire or untagged helper types. AccuracySpec<E, P> is unchanged and remains the guarantee's outer shape. root_metric became default_metric_for_root, which returns None for a quantile instead of an error.
  • DefaultAccuracyModel::satisfies: target.metric.is_none_or(|m| metric_meets(g.metric, m)). The ε, δ and scope checks are unchanged.
  • Stage 3. accuracy_violation became accuracy_check. It returns, per query, the AcceptedAccuracy of the guarantee that met the target: the theoretical guarantee or the deployment's evidence, plus the sketches the root reads. select_plan keeps these in Selection::accepted. run_stage2_and_stage3 passes the workload targets through unchanged, and resolve_targets is gone.
  • Fail closed. When a summary's sizing split has neither an error model nor a metric on its intent's target, it no longer guesses a metric. It sizes for the whole target, with debug_assert!, and Stage 3 still checks the composed output.
  • Mechanical rename. RequestedAccuracy → AccuracyTarget across about 90 files, including tools/verify_metricsql_external_consumer.sh.
  • stage_pipeline and dag-viewer.
    • requirements.accuracy is now the target's own JSON.
    • An approximate target without a metric also gets requirements.default_metric, for display only.
    • stage3_plan_selection.accepted holds {target_metric, accepted_metric, sketches, summary} for each shown valid candidate and query.
    • The viewer prints ε=…, δ=… (metric | metric not set; default …) for each query, and the accepted metric for each valid candidate and for the plan in lane 2.

Key code interfaces added / modified

crates/types/src/types.rs

/// An accuracy requirement as a workload, caller or frontend states it; the
/// target Stage 3 checks at a query root and summaries are sized against.
#[derive(Serialize, Deserialize)] #[serde(deny_unknown_fields)]
pub struct AccuracyTarget {                       // was a type alias AccuracySpec<f64, Option<f64>>
    /// The error metric; None (default) = not set by the user: any metric meeting ε, δ, scope is accepted.
    pub metric: Option<ErrorMetric>,              // was ErrorMetric
    /// ε, in the metric's units (required in JSON).
    pub error: f64,                               // unchanged
    /// δ; None (default) = no δ requirement (sizing uses the planner's default δ).
    pub failure_probability: Option<f64>,         // unchanged
    /// The values one (ε, δ) statement covers; default WholeResult.
    pub scope: AccuracyScope,                     // unchanged
}

impl AccuracyTarget {
    /// No approximation: ε = 0, δ = 0, no metric.
    pub const fn exact() -> Self;                 // metric was ABSOLUTE
    /// Error ≤ ε, no δ requirement, no metric set.
    pub const fn epsilon(epsilon: f64) -> Self;   // metric was ABSOLUTE
    /// Error ≤ ε with probability ≥ 1 − δ, no metric set.
    pub const fn epsilon_delta(epsilon: f64, delta: f64) -> Self;
    /// This target with its metric set (restricts accepted guarantees to it).
    pub fn with_metric(self, metric: ErrorMetric) -> Self;
    /// This target over `scope`.
    pub fn with_scope(self, scope: AccuracyScope) -> Self;
    /// Whether ε = 0 and δ = 0.
    pub fn is_exact(&self) -> bool;
    /// ε, δ, scope stated in `metric`: the target an intent's summary is sized against (frontends).
    pub fn target_in(&self, metric: ErrorMetric) -> Self;      // moved from RequestedAccuracy
    /// A quantile intent's sizing target: the set metric, else Rank.
    pub fn for_quantile(&self) -> Self;                         // moved from RequestedAccuracy
}

/// The metric a root's result is naturally stated in (count/top-k Absolute@StreamL1,
/// distinct/L2 Relative, else Absolute; None for a quantile). Display only.
pub fn default_metric_for_root(root: &QueryRoot) -> Option<ErrorMetric>;   // new (was private root_metric)

/// Removed: the separate input type, its resolution and its input error.
pub struct RequestedAccuracy {                                              // removed
    /// The error metric, if the user set one.
    pub metric: Option<ErrorMetric>,                                        // removed (now AccuracyTarget::metric)
    /// ε.
    pub error: f64,                                                         // removed (now AccuracyTarget::error)
    /// δ; `None` when the target states none.
    pub failure_probability: Option<f64>,                                   // removed (now AccuracyTarget::failure_probability)
    /// Per value or whole result.
    pub scope: AccuracyScope,                                               // removed (now AccuracyTarget::scope)
}
impl RequestedAccuracy { pub fn for_root(&self, root: &QueryRoot)
    -> Result<AccuracyTarget, AccuracyRequestError>; }                     // removed
pub enum AccuracyRequestError { MissingQuantileMetric { intent: &'static str } }  // removed

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

/// The deployment-extensible accuracy algebra. The trait stays; only `satisfies` changes.
pub trait AccuracyModel {
    /// The registered rule for applying an exact operation to an approximate input;
    /// `None` (default) means the operation is exact only over exact inputs.
    fn exact_operation_rule(&self, operation: &ExactOperation) -> Option<CompositionOperator>;   // unchanged
    /// The guarantee of reading `query` from a summary of `family` built over an exact input;
    /// `None` when the model has no error model for the family.
    fn local_guarantee(&self, family: &FieldDataType, query: &SketchStatistic)
        -> Option<AccuracyGuarantee>;   // unchanged
    /// Composes the inputs' guarantees with the parent's own under `op`;
    /// `Err` is the fail-closed answer (no rule, or a missing input guarantee).
    fn propagate(&self, op: &CompositionOperator, inputs: &[AccuracyGuarantee],
        local: Option<&AccuracyGuarantee>, stats: &PropagationStats)
        -> Result<AccuracyGuarantee, AccuracyError>;   // unchanged
    /// Whether `guarantee` meets `target`'s ε (and δ, scope). With target.metric None, a bound in
    /// any metric qualifies, compared in its own metric; with Some(m), only m (or a sound conversion).
    fn satisfies(&self, guarantee: &AccuracyGuarantee, target: &AccuracyTarget) -> bool;  // changed: the rule for None is new
}

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

/// What Stage 3 accepted for one query of a valid candidate; produced by select_plan.
pub struct AcceptedAccuracy {                     // new
    /// The target's metric; None when the user did not set one.
    pub target_metric: Option<ErrorMetric>,
    /// The metric of the guarantee that met the target; None for an exact output.
    pub accepted_metric: Option<ErrorMetric>,
    /// Sketch algorithms the query's output reads, in plan order (empty for exact).
    pub sketches: Vec<String>,
}
/// One line, e.g. "metric not set; accepted rank (Kll)" or "metric rank; accepted exact".
impl std::fmt::Display for AcceptedAccuracy;      // new

/// Stage 3's result. The type stays; `accepted` is new.
pub struct Selection {
    /// The selected physical candidate's id.
    pub selected: String,                         // unchanged
    /// The selected candidate's query roots as Stage 3 sized and confirmed them: the plan to run.
    pub plan: Vec<QueryRoot>,                     // unchanged
    /// Each valid candidate's cost, by id.
    pub costs: BTreeMap<String, CandidateCost>,   // unchanged
    /// Per valid candidate, per query: what was accepted (None for a query without a target).
    pub accepted: BTreeMap<String, Vec<Option<AcceptedAccuracy>>>,   // new
    /// Every other candidate, with whether it was valid and why it lost.
    pub rejected: Vec<Rejection>,                 // unchanged
}

/// Why Stage 3 could not select a plan. The enum stays; only `AccuracyRequest` is removed.
pub enum SelectionError {
    /// A candidate's root count differs from the number of targets or recurrences given (caller bug).
    TargetCount { candidate: String, roots: usize, targets: usize, recurrences: usize },   // unchanged
    /// The cost model failed on a node (internal bug).
    Cost { candidate: String, node: PhysicalASAPNodeId, error: AnalyticalCostError },     // unchanged
    /// Every candidate was rejected (the request cannot be satisfied); lists each rejection.
    NoValidCandidate(Vec<Rejection>),                                                     // unchanged
    /// Resizing a candidate's summaries for its failure probability failed (internal bug).
    Sizing { candidate: String, error: SizingError },                                     // unchanged
    /// Was produced for a quantile target without a metric (bad input); no longer an error.
    AccuracyRequest { query: usize, error: AccuracyRequestError },                        // removed
}

crates/types/src/workload.rs

/// Whether the query gave a target. Stays in this PR; #717 removes it: `QueryRequirements.accuracy` becomes a plain `AccuracyTarget` (default exact).
pub enum AccuracyRequirement {
    /// The caller's target, metric optional.
    Explicit(AccuracyTarget),                     // was Explicit(RequestedAccuracy)
    /// No target given: exact.
    ImplicitExact,                                // unchanged
}
impl AccuracyRequirement {
    /// The target Stage 3 checks for this query (exact for ImplicitExact).
    pub fn requested(&self) -> AccuracyTarget;    // was -> RequestedAccuracy
}

JSON formats. These are the AccuracyTarget wire form and the stage_pipeline document (asap-stage-pipeline/v1), see crates/devtools/src/bin/stage_pipeline.rs and tools/dag-viewer/README.md:

// AccuracyTarget: every field but "error" optional; old "Exact"/{"Epsilon"}/{"EpsilonDelta"} rejected
{"metric": "rank", "error": 0.01, "failure_probability": 0.001, "scope": "whole_result"}
{"error": 0.0, "failure_probability": 0.0}                       // exact
// stage_pipeline document
"requirements": {"accuracy": {"error": 0.01, "scope": "whole_result"},    // was {"epsilon": …, "delta": …} or "exact"
                 "default_metric": null},                                  // new: display only, unset approximate targets
"stage3_plan_selection": {"accepted": {"P2": [{"target_metric": null, "accepted_metric": "rank",
                          "sketches": ["Kll"], "summary": "metric not set; accepted rank (Kll)"}]}, …}  // new

Usage: an AccuracyTarget is bound to one query through that query's workload entry. Each query in query_batch or repeating_queries carries its own requirements.accuracy; the planner passes one target per query root to Stage 3 (stage1_to_stage3.rs: entry.requirements.accuracy.requested()), so two queries in one workload can ask for different ε, δ and metrics.

use asap_types::types::{AccuracyTarget, ErrorMetric};
use asap_types::workload::{AccuracyRequirement, BatchEntry, Query, QueryRequirements};

// Q1: p99 latency within ε = 0.01, δ = 0.01, metric not set.
// Stage 3 accepts any candidate whose own guarantee meets ε/δ in its own
// metric (a KLL rank error or a DDSketch relative error) and reports which.
let q1 = BatchEntry {
    query: Query("quantile_over_time(0.99, latency_ms[5m])".into()),
    requirements: QueryRequirements {
        accuracy: AccuracyRequirement::Explicit(AccuracyTarget::epsilon_delta(0.01, 0.01)),
        ..Default::default()
    },
    predictability: Default::default(),
    invocations: 1,
    execute_at: None,
    time_selection: Default::default(),
};

// Q2: the same quantile, but only a rank error is acceptable, so
// relative-error candidates (DDSketch) are rejected for this query.
let q2 = BatchEntry {
    requirements: QueryRequirements {
        accuracy: AccuracyRequirement::Explicit(
            AccuracyTarget::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::Rank),
        ),
        ..Default::default()
    },
    ..q1.clone()
};

// Q3: no accuracy given: `QueryRequirements::default()` is exact.
let q3 = BatchEntry { requirements: QueryRequirements::default(), ..q1.clone() };

The same entries in the workload JSON:

{"query": "quantile_over_time(0.99, latency_ms[5m])",
 "requirements": {"accuracy": {"explicit": {"error": 0.01, "failure_probability": 0.01, "scope": "whole_result"}},
                  "response_latency": "unspecified"}}
{"query": "...", "requirements": {"accuracy": {"explicit": {"metric": "rank", "error": 0.01, "failure_probability": 0.01, "scope": "whole_result"}}, "response_latency": "unspecified"}}
{"query": "...", "requirements": {"accuracy": "implicit_exact", "response_latency": "unspecified"}}

After planning, selection.accepted says which metric each query was accepted under. Continuing the example (checked at f0b07992 with a scratch test):

// `data` holds the deployment's facts. PromQL needs the ingestion interval; the input
// cardinality (1,000 series) is G, which states each per-series δ over the whole result
// (the targets above are whole-result). Stage 3 reads it when it checks the targets.
let data = DataWorkload {
    data_ingestion_interval: declared(DurationMs(15_000)),
    input_cardinality: declared(1_000),
    ..Default::default()
};
// `workload` is the planner input: Q1–Q3 as one batch, plus `data`.
let workload = PlanningWorkload {
    query_workload: QueryWorkload {
        language: QueryLanguage::PromQL,
        query_batch: Some(vec![q1, q2, q3]),
        repeating_queries: None,
    },
    data_workload: Some(data.clone()),
};
// `roots` holds the PromQL frontend's lowered queries, one per entry, in workload order.
let roots = lower_promql_roots(&workload)?;
// `targets[i]` is query i's AccuracyTarget, read from its entry
// (`ImplicitExact` becomes the exact target); Stage 1 sizes for it, Stage 3 checks against it.
let targets: Vec<Option<AccuracyTarget>> = workload.query_workload.entries()
    .map(|entry| Some(entry.requirements.accuracy.requested()))
    .collect();
// `recurrence[i]` is how often query i runs (here once); Stage 3 prices each candidate by it.
let recurrence: Vec<QueryRecurrence> = workload.query_workload.entries()
    .map(|entry| entry.recurrence)
    .collect();
// `run` holds every stage's output: Stage 1 and 2 give 48 physical candidates; Stage 3
// (select_plan, with the built-in cost and accuracy models) checks each against `targets`.
let run = run_stage1_to_stage3(
    roots.into_iter().enumerate().collect(), &targets, &recurrence, &data, PlanningModels::builtin(),
)?;
// `selection` is Stage 3's answer: P16, 0.00163 cost_per_second; 18 candidates are
// invalid, e.g. P10, where Q2's DDSketch states a relative bound against a rank target.
let selection = run.stage3_plan_selection?;
// `accepted` holds, per query of the selected plan, the metric its target set and the one it was accepted under.
let accepted = &selection.accepted[&selection.selected];
println!("{}", accepted[0].as_ref().unwrap());   // Q1: "metric not set; accepted rank (Kll)"
println!("{}", accepted[1].as_ref().unwrap());   // Q2: "metric rank; accepted rank (Kll)"
println!("{}", accepted[2].as_ref().unwrap());   // Q3: "metric not set; accepted exact"

The AccuracyRequirement::Explicit / ImplicitExact wrapper shown here is what this PR merged with. #717 removes it: there QueryRequirements is { accuracy: AccuracyTarget, max_latency_ms: Option<f64> }, with exact as the default, so Q1 becomes accuracy: AccuracyTarget::epsilon_delta(0.01, 0.01).

Before this PR

  • Two types: RequestedAccuracy (input, metric optional) and AccuracyTarget (resolved, metric required).
  • Stage 3 rejected every guarantee whose metric differed from the resolved one.
  • A quantile_over_time target without --metric failed Stage 3 with "must name its metric".
  • Workload JSON accepted "Exact", {"Epsilon"} and {"EpsilonDelta"}.

After this PR

  • One AccuracyTarget. Leaving its metric unset lets every candidate whose guarantee meets ε, δ and scope compete on cost, and the output says which metric each candidate was accepted in.
  • Example: stage_pipeline --promql 'quantile_over_time(0.99, m[1m])' --epsilon 0.01 now selects a plan. The KLL candidates report metric not set; accepted rank (Kll) and the DDSketch candidates metric not set; accepted relative (DDSketch). With --metric rank, the valid candidates are KLL (metric rank; accepted rank (Kll)) and exact ones (metric rank; accepted exact).

#509 examples. Selection is unchanged for all committed examples. Examples 3a and 3b set rank, and Examples 1 and 2 have no quantile. Example 1 still selects the same plan and Example 2 still selects P1 at 6.08 with 12 valid plans. Only the document's accuracy and accepted fields change.

I also ran Examples 3a and 3b with the metric unset. These are local runs, not committed.

Example Selected Effect of leaving the metric unset
3b P2 (KLL) at 2.100 DDSketch (P4) becomes valid and ties KLL at 2.100. KLL wins on enumeration order. The DDSketch panes (P5) are valid at 2.183. All 5 plans are valid, where 2 were invalid before.
3a P485 (KLL ×5) at 39828.803 The DDSketch mixes are valid, as before.

Verification

  • Failing tests first.
    • accuracy::tests::a_target_without_a_metric_accepts_any_metric did not compile against main's concrete metric.
    • The stage_pipeline test a_quantile_target_without_a_metric_accepts_rank_or_relative failed before the reporting was added.
  • Touched crates. cargo nextest run on asap-types, asap-aware-mapping, asap-physical-operators, asap-frontend-promql, asap-frontend-metricsql, asap-frontend-sql, asap-planner, asap-devtools and asap-integration-tests: 1788 of 1789 passed. The one failure was the committed Example 1 fixture check. I regenerated the fixture, and a rerun of asap-devtools passed 28 of 28.
  • Doctests. cargo test --doc passes on asap-types, asap-aware-mapping, the frontends, asap-planner and asap-devtools.
  • Workspace checks. cargo fmt --all -- --check and cargo clippy --workspace --all-targets --all-features --locked -- -D warnings are clean.
  • External consumer. tools/verify_metricsql_external_consumer.sh passes.
  • Viewer. python3 -m unittest test_viewer test_server in tools/dag-viewer: 50 tests pass. This includes a fix to the stale "9 candidates" assertion, which already failed on main because the fixture has had 27 candidates since feat(planner): tumbling-pane window composition as a Pass 2 rule (Example 3) #713.
  • I did not run the full-workspace nextest; this follows the review-only gate.

Limitations and follow-ups

… metric accepted when unset

Merge RequestedAccuracy into AccuracyTarget, a plain struct whose metric
is optional. A target that sets no metric accepts a guarantee in any
metric (absolute at any scale, relative, rank) that meets its epsilon,
delta and scope; a target that sets one keeps today's rule. A quantile
without a metric is no longer an input error: KLL and DDSketch compete on
cost.

- AccuracyTarget serializes as {"metric"?, "error", "failure_probability"?,
  "scope"?}; the old "Exact"/{"Epsilon"}/{"EpsilonDelta"} forms are gone.
- AccuracySpec<E, P> stays the guarantee's outer shape.
- RequestedAccuracy, AccuracyRequestError, for_root and
  SelectionError::AccuracyRequest are removed; default_metric_for_root
  keeps the per-intent default table for display.
- Stage 3 reports, per valid candidate and query, the accepted metric
  (Selection::accepted, AcceptedAccuracy); stage_pipeline writes it and
  the target JSON, and the dag-viewer shows both.
- Sizing a share without a metric fails closed (no split) with a debug
  assertion.
@zzylol
zzylol merged commit f0b0799 into main Oct 15, 2026
4 checks passed
@zzylol

zzylol commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Usage:

let target = AccuracyTarget::epsilon_delta(0.01, 0.01); // metric not set
let selection = select_plan(&candidates, &[Some(target)], &recurrence, &data, models)?;
println!("{}", selection.accepted[&selection.selected][0].as_ref().unwrap());
// "metric not set; accepted rank (Kll)"
let rank_only = AccuracyTarget::epsilon_delta(0.01, 0.01).with_metric(ErrorMetric::Rank);

this should give an example of how accuracyTarget binds to a query?

@zzylol

zzylol commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Updated the PR body's Usage: it now shows the target bound to a query through its workload entry (BatchEntry.requirements.accuracy), with three queries side by side (metric not set, rank only, exact by default). It also shows the same entries as workload JSON (checked by serializing them at the merge commit), how Stage 3 gets one target per query root (entry.requirements.accuracy.requested() in stage1_to_stage3.rs), and what selection.accepted reports. It notes that #717 drops the Explicit/ImplicitExact wrapper, so there the field is a plain AccuracyTarget.

@zzylol

zzylol commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

impl AccuracyTarget {
/// No approximation: ε = 0, δ = 0, no metric.
pub const fn exact() -> Self; // metric was ABSOLUTE
/// Error ≤ ε, no δ requirement, no metric set.
pub const fn epsilon(epsilon: f64) -> Self; // metric was ABSOLUTE
/// Error ≤ ε with probability ≥ 1 − δ, no metric set.
pub const fn epsilon_delta(epsilon: f64, delta: f64) -> Self;
/// This target with its metric set (restricts accepted guarantees to it).
pub fn with_metric(self, metric: ErrorMetric) -> Self;
/// This target over scope.
pub fn with_scope(self, scope: AccuracyScope) -> Self;
/// Whether ε = 0 and δ = 0.
pub fn is_exact(&self) -> bool;
/// ε, δ, scope stated in metric: the target an intent's summary is sized against (frontends).
pub fn target_in(&self, metric: ErrorMetric) -> Self; // moved from RequestedAccuracy
/// A quantile intent's sizing target: the set metric, else Rank.
pub fn for_quantile(&self) -> Self; // moved from RequestedAccuracy
}

this should be simplifed, and each interface should be human understandable without comments.

@zzylol

zzylol commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

is the entire pub enum SelectionError {
/* TargetCount, Cost, NoValidCandidate, Sizing unchanged */
/// Was produced for a quantile target without a metric (bad input); no longer an error.
AccuracyRequest { query: usize, error: AccuracyRequestError }, // removed
} removed then?

@zzylol

zzylol commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Agreed. Planned public API, where each method is named after the field it sets:

AccuracyTarget::exact()
AccuracyTarget::max_error(0.01)
    .failure_probability(0.01)      // optional
    .metric(ErrorMetric::Rank)      // optional; unset = any metric is accepted
    .scope(AccuracyScope::PerValue) // optional; default WholeResult
target.is_exact()

epsilon, epsilon_delta, with_metric and with_scope go away. target_in and for_quantile are planner-internal sizing helpers, so they move out of the public type into private functions in asap-aware-mapping. This lands as its own mechanical PR right after #717 merges, because #717 also changes QueryRequirements.

@zzylol

zzylol commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

No, SelectionError stays. Only its AccuracyRequest variant was removed, because a quantile target without a metric is no longer an error. TargetCount, Cost, NoValidCandidate and Sizing are unchanged. The PR body now lists the whole enum with each variant marked unchanged or removed, so this is no longer ambiguous.

@zzylol

zzylol commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Revised plan for the same follow-up PR: ErrorMetric is renamed to AccuracyMetric (matching AccuracyScope), and metric/scope get the accuracy_ prefix everywhere.

AccuracyTarget::exact()
AccuracyTarget::max_error(0.01)
    .failure_probability(0.01)
    .accuracy_metric(AccuracyMetric::Rank)
    .accuracy_scope(AccuracyScope::PerValue)
target.is_exact()

The fields follow the method names (accuracy_metric, accuracy_scope on both AccuracyTarget and the guarantee-side AccuracySpec), so the JSON keys become "accuracy_metric" / "accuracy_scope".

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant