DeciderAnswer constructor

DeciderAnswer({
  1. required String? choice,
  2. required List<RankedOption> ranked,
  3. required ScoreKind scoreKind,
  4. required double margin,
  5. required DeciderProvenance provenance,
  6. double? confidence,
  7. AbstainReason? abstainReason,
  8. Map<String, double>? probabilities,
  9. DeciderQuestionType questionType = DeciderQuestionType.choice,
  10. Map<String, String>? legend,
})

For deciders implemented outside core (and test fakes). Lists are copied.

Throws ArgumentError for an answer that contradicts itself: exactly one of choice and abstainReason is set; a choice is the first of a non-empty ranked; ranked has each id once, no NaN score, and is sorted by score, highest first; margin is not NaN and not negative; confidence is in 0, 1 and only given with probabilities; probabilities are each in 0, 1, sum to 1 (within 1e-6) and cover every ranked option, whose RankedOption.probability is its entry there — and every RankedOption.probability is null without them; the provenance's abstain signal and its kind are both set or both null; an answer that abstains with AbstainReason.belowThreshold has its signal below the threshold (both set), and one that chooses with both set has it at or above; AbstainReason.noneOption comes with candidates in ranked and may have its signal on either side (the none option wins first); AbstainReason.noCandidates comes with nothing ranked, no signal, no probabilities and no confidence.

questionType is DeciderQuestionType.choice unless given; then legend is null. A DeciderQuestionType.noul or DeciderQuestionType.score answer never abstains, has no threshold, has probabilities, and chooses its most probable option. A Noul answer's probabilities are over no and yes exactly; a Score answer's over the levels 0 … K − 1 (K ≥ 2), and its legend has a non-empty text for each level and nothing else. legend is copied.

Implementation

DeciderAnswer({
  required this.choice,
  required List<RankedOption> ranked,
  required this.scoreKind,
  required this.margin,
  required this.provenance,
  this.confidence,
  this.abstainReason,
  Map<String, double>? probabilities,
  this.questionType = DeciderQuestionType.choice,
  Map<String, String>? legend,
}) : ranked = List.unmodifiable(ranked),
     probabilities = probabilities == null
         ? null
         : Map.unmodifiable(probabilities),
     legend = legend == null ? null : Map.unmodifiable(legend) {
  if ((choice == null) == (abstainReason == null)) {
    throw ArgumentError(
      'A DeciderAnswer has either a choice or an abstainReason, exactly '
      'one of them; got choice $choice, abstainReason $abstainReason.',
    );
  }
  if (choice != null && this.ranked.isNotEmpty) {
    if (this.ranked.first.id != choice) {
      throw ArgumentError.value(
        choice,
        'choice',
        'is not the best ranked option (${this.ranked.first.id})',
      );
    }
  }
  for (var i = 1; i < this.ranked.length; i++) {
    if (!(this.ranked[i - 1].score >= this.ranked[i].score)) {
      throw ArgumentError.value(
        this.ranked.map((r) => r.score).toList(),
        'ranked',
        'must be sorted by score, highest first',
      );
    }
  }
  final p = this.probabilities;
  if (confidence case final c?) {
    if (p == null) {
      throw ArgumentError.value(
        c,
        'confidence',
        'is only given with calibrated probabilities',
      );
    }
    if (!(c >= 0 && c <= 1)) {
      throw ArgumentError.value(c, 'confidence', 'must be in [0, 1]');
    }
  }
  _checkRanked();
  _checkProbabilities();
  _checkAbstention();
  _checkQuestionType();
}