static generateAuditResult(audit, product) {
    if (product.score === undefined) {
      throw new Error('generateAuditResult requires a score');
    }

    // Default to binary scoring.
    let scoreDisplayMode = audit.meta.scoreDisplayMode || Audit.SCORING_MODES.BINARY;
    let score = product.score;

    // But override if product contents require it.
    if (product.errorMessage !== undefined) {
      // Error result.
      scoreDisplayMode = Audit.SCORING_MODES.ERROR;
    } else if (product.notApplicable) {
      // Audit was determined to not apply to the page.
      scoreDisplayMode = Audit.SCORING_MODES.NOT_APPLICABLE;
    } else if (product.scoreDisplayMode) {
      scoreDisplayMode = product.scoreDisplayMode;
    }

    const metricSavings = Audit._quantizeMetricSavings(product.metricSavings);
    const hasSomeSavings = Object.values(metricSavings || {}).some(v => v);

    if (scoreDisplayMode === Audit.SCORING_MODES.METRIC_SAVINGS) {
      if (score && score >= Util.PASS_THRESHOLD) {
        score = 1;
      } else if (hasSomeSavings) {
        score = 0;
      } else {
        score = 0.5;
      }
    }

    score = Audit._normalizeAuditScore(score, scoreDisplayMode, audit.meta.id);

    let auditTitle = audit.meta.title;
    if (audit.meta.failureTitle) {
      if (score !== null && score < Util.PASS_THRESHOLD) {
        auditTitle = audit.meta.failureTitle;
      }
    }

    // The Audit.Product type is bifurcated to enforce numericUnit accompanying numericValue;
    // the existence of `numericUnit` is our discriminant.
    // Make ts happy and enforce this contract programmatically by only pulling numericValue off of
    // a `NumericProduct` type.
    const numericProduct = 'numericUnit' in product ? product : undefined;

    return {
      id: audit.meta.id,
      title: auditTitle,
      description: audit.meta.description,

      score,
      scoreDisplayMode,
      numericValue: numericProduct?.numericValue,
      numericUnit: numericProduct?.numericUnit,

      displayValue: product.displayValue,
      explanation: product.explanation,
      errorMessage: product.errorMessage,
      errorStack: product.errorStack,
      warnings: product.warnings,
      scoringOptions: product.scoringOptions,
      metricSavings,

      details: product.details,
      guidanceLevel: audit.meta.guidanceLevel,
      replacesAudits: audit.meta.replacesAudits,
    };
  }
@param {typeof Audit} audit
@param {LH.Audit.Product} product
@return {LH.RawIcu<LH.Audit.Result>}