parseCommitteeClassifierResult function
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,
]);
}