Forge
csharp4405de34
1using System.Diagnostics.CodeAnalysis;
2using System.Globalization;
3
4namespace CascadeIDE.Models.AgentChat;
5
6/// <summary>Валидация доменной модели пакетов уточнений (ADR 0031) без UI.</summary>
7public static class ClarificationBatchValidation
8{
9 /// <summary>Проверяет пакет и опционально ответы; при успехе <paramref name="error"/> — null.</summary>
10 public static bool TryValidate(ClarificationBatch batch, ClarificationResponse? response, [NotNullWhen(false)] out string? error)
11 {
12 error = null;
13 if (batch.Items.Count == 0)
14 {
15 error = "Пакет уточнений не содержит ни одного пункта.";
16 return false;
17 }
18
19 var seen = new HashSet<string>(StringComparer.Ordinal);
20 for (var i = 0; i < batch.Items.Count; i++)
21 {
22 var item = batch.Items[i];
23 var id = (item.Id ?? "").Trim();
24 if (id.Length == 0)
25 {
26 error = string.Format(CultureInfo.InvariantCulture, "Пункт [{0}]: пустой идентификатор.", i);
27 return false;
28 }
29
30 if (!seen.Add(id))
31 {
32 error = string.Format(CultureInfo.InvariantCulture, "Дублируется идентификатор пункта «{0}».", id);
33 return false;
34 }
35
36 if (string.IsNullOrWhiteSpace(item.Prompt))
37 {
38 error = string.Format(CultureInfo.InvariantCulture, "Пункт «{0}»: пустой текст вопроса.", id);
39 return false;
40 }
41
42 if (item.AnswerStyle == ClarificationAnswerStyle.SingleChoice)
43 {
44 if (item.ChoiceOptions is null || item.ChoiceOptions.Count == 0)
45 {
46 error = string.Format(
47 CultureInfo.InvariantCulture,
48 "Пункт «{0}»: для SingleChoice нужен непустой список вариантов.",
49 id);
50 return false;
51 }
52 }
53 }
54
55 if (response is null)
56 {
57 return true;
58 }
59
60 if (response.BatchId != batch.Id)
61 {
62 error = "Идентификатор пакета в ответе не совпадает с пакетом.";
63 return false;
64 }
65
66 var answers = response.AnswersByItemId;
67 var trimmedKeyToOriginal = new Dictionary<string, string>(StringComparer.Ordinal);
68 foreach (var kv in answers)
69 {
70 var trimmed = (kv.Key ?? "").Trim();
71 if (trimmed.Length == 0)
72 {
73 error = "В ответе есть пункт с пустым ключом (после trim).";
74 return false;
75 }
76
77 if (!trimmedKeyToOriginal.TryAdd(trimmed, kv.Key ?? trimmed))
78 {
79 error = string.Format(
80 CultureInfo.InvariantCulture,
81 "В ответе дублируется ключ пункта после нормализации (trim): «{0}».",
82 trimmed);
83 return false;
84 }
85 }
86
87 foreach (var item in batch.Items)
88 {
89 var id = item.Id.Trim();
90 if (!trimmedKeyToOriginal.TryGetValue(id, out _))
91 {
92 error = string.Format(
93 CultureInfo.InvariantCulture,
94 "Нет непустого ответа для пункта «{0}».",
95 id);
96 return false;
97 }
98
99 var text = answers[trimmedKeyToOriginal[id]];
100 if (string.IsNullOrWhiteSpace(text))
101 {
102 error = string.Format(
103 CultureInfo.InvariantCulture,
104 "Нет непустого ответа для пункта «{0}».",
105 id);
106 return false;
107 }
108
109 if (item.AnswerStyle == ClarificationAnswerStyle.YesNo)
110 {
111 var t = text.Trim();
112 if (!IsYesNoToken(t))
113 {
114 error = string.Format(
115 CultureInfo.InvariantCulture,
116 "Пункт «{0}»: ожидался ответ да/нет (получено: «{1}»).",
117 id,
118 text);
119 return false;
120 }
121 }
122
123 if (item.AnswerStyle == ClarificationAnswerStyle.SingleChoice && item.ChoiceOptions is not null)
124 {
125 var t = text.Trim();
126 if (!item.ChoiceOptions.Contains(t, StringComparer.Ordinal))
127 {
128 error = string.Format(
129 CultureInfo.InvariantCulture,
130 "Пункт «{0}»: ответ не входит в список вариантов.",
131 id);
132 return false;
133 }
134 }
135 }
136
137 foreach (var trimmed in trimmedKeyToOriginal.Keys)
138 {
139 if (seen.Contains(trimmed))
140 continue;
141 error = string.Format(
142 CultureInfo.InvariantCulture,
143 "В ответе есть лишний ключ «{0}», отсутствующий в пакете.",
144 trimmed);
145 return false;
146 }
147
148 return true;
149 }
150
151 private static bool IsYesNoToken(string t)
152 {
153 if (t.Length == 0)
154 return false;
155 var lower = t.ToLowerInvariant();
156 return lower is "да" or "нет" or "yes" or "no" or "y" or "n";
157 }
158}
159
View only · write via MCP/CIDE