parseCommitteeClassifierResult function

CommitteeClassifierResult parseCommitteeClassifierResult(
  1. String? output, {
  2. required List<String> activeSemanticRubricIds,
})

Decodes ONE classifier answer STRICTLY.

The legal shape is a single JSON object with EXACTLY a rubricIds list of non-empty strings. Null/blank output maps to CommitteeClassifierResultKind.missing; a decode failure, a non-object, a non-list, an empty list or an extra key maps to CommitteeClassifierResultKind.malformed; any id outside kCommitteeClassifierAllowlist or outside activeSemanticRubricIds rejects the WHOLE answer as CommitteeClassifierResultKind.unknown. A valid answer is deduplicated and reordered by the active committee. No arm is a grade.

Implementation

CommitteeClassifierResult parseCommitteeClassifierResult(
  String? output, {
  required List<String> activeSemanticRubricIds,
}) {
  if (output == null || output.trim().isEmpty) {
    return const CommitteeClassifierResult.missing();
  }
  final Object? decoded;
  try {
    decoded = jsonDecode(output.trim());
  } on Object {
    return const CommitteeClassifierResult.malformed();
  }
  if (decoded is! Map ||
      decoded.keys.map((key) => '$key').toSet().difference({
        'rubricIds',
      }).isNotEmpty) {
    return const CommitteeClassifierResult.malformed();
  }
  final raw = decoded['rubricIds'];
  if (raw is! List ||
      raw.isEmpty ||
      raw.any((id) => id is! String || id.trim().isEmpty)) {
    return const CommitteeClassifierResult.malformed();
  }
  final requested = {for (final id in raw) (id as String).trim()};
  final active = activeSemanticRubricIds.toSet();
  if (!kCommitteeClassifierAllowlist.containsAll(requested) ||
      !active.containsAll(requested)) {
    return CommitteeClassifierResult.unknown(requested.toList()..sort());
  }
  return CommitteeClassifierResult.selected([
    for (final id in activeSemanticRubricIds)
      if (requested.contains(id)) id,
  ]);
}