vigil_redaction/lang_hint.rs
1//! v0.10 Sprint 2 — typed `LanguageHint` wrapper(Decision A-prime,Codex `019dfdab`)。
2//!
3//! **触发**:v0.9 Decision D=C 锁定 production firewall **不接** lang;但 SDK consumer
4//! 仍可能想 reproducible 走 lang-aware path。直接传 `Option<&str>` 不够 — 没有
5//! provenance / confidence 信息,caller 无法判 trust;启发式 detect 误判(短文本
6//! 17/45 EU sample,见 `feedback_lang_review_authoritative`)若混入 production 决策
7//! 路径会静默 threshold 误路由。
8//!
9//! **A-prime 设计**(本模块):typed `LanguageHint { lang, source, confidence }`,
10//! 强制 caller 表达**信息来源** + **可信度**;low-confidence 一律 fail-closed 退化
11//! baseline。
12//!
13//! **当前 v0.10 范围**:
14//! - typed wrapper + 工厂方法 + `into_lang_str()` 转 `Option<String>`(low-conf → None)
15//! - `scan_text_with_engine_with_hint(text, engine, hint)` 浅级 wrapper(SDK 友好)
16//! - SDK re-export(`vigil_sdk::{LanguageHint, LangHintSource}`)
17//! - **不**接 Firewall::evaluate(D=C 锁定;v0.11+ 视用户反馈)
18
19use crate::engine::RedactionEngine;
20use crate::scan::{scan_text_with_engine_with_lang, RedactionResult, ScanError};
21
22/// **v0.10 Sprint 2** — `LanguageHint` 信息来源 enum。
23///
24/// caller 必须表明 lang 字符串来自哪类信息 — 影响 audit trail + 可信度判定。
25/// `into_lang_str()` 决策时按 source × confidence 综合判 fail-closed 退化:
26/// - `CallerProvided`:caller 明确决策(如用户 locale 设置 / 业务上下文)— 高信任
27/// - `FixtureExperimental`:fixture / 测试 / release-gate 模式 — 中信任(仅非 production)
28/// - `Heuristic`:启发式 detect(unicode + 关键词)— 低信任,advisory only
29///
30/// **SemVer**:`#[non_exhaustive]` — 未来加 source(如 `UserAgent` / `MlClassifier`)
31/// 不破。
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33#[non_exhaustive]
34pub enum LangHintSource {
35 /// caller 显式传入(可信度最高;典型 user locale / 业务上下文)
36 CallerProvided,
37 /// fixture / 测试 / release-gate 场景(权威源,但仅非 production)
38 FixtureExperimental,
39 /// 启发式 lang detect(unicode 字符集 + 关键词;仅 advisory,不可作权威决策)
40 Heuristic,
41}
42
43impl LangHintSource {
44 /// 该 source 是否本质可信(进 production 决策)。
45 /// `Heuristic` 始终 false(`feedback_lang_review_authoritative` 约束)。
46 pub fn is_trusted(&self) -> bool {
47 // crate 内部穷举所有 variant(LangHintSource 定义在本 module);加新 variant
48 // 时 compiler 会 force update 本 match — 比 runtime `_ => false` fail-closed
49 // 兜底更早 catch。外部 SDK consumer 因 #[non_exhaustive] 必须写 `_` 兜底。
50 match self {
51 LangHintSource::CallerProvided => true,
52 LangHintSource::FixtureExperimental => true,
53 LangHintSource::Heuristic => false,
54 }
55 }
56}
57
58/// **v0.10 Sprint 2** — typed lang hint wrapper(Decision A-prime)。
59///
60/// **设计意图**:替代裸 `Option<&str>` 给 SDK consumer / 未来 firewall lang-aware
61/// 路径用;**强制 provenance + confidence**,low-conf 一律 fail-closed 退化。
62///
63/// **典型用法**(SDK consumer):
64/// ```rust
65/// use vigil_redaction::lang_hint::{LanguageHint, LangHintSource};
66///
67/// // caller 知道用户 locale → 高信任
68/// let hint = LanguageHint::caller_provided("de");
69///
70/// // 启发式 detect → advisory,low-conf 自动 fail-closed
71/// let hint = LanguageHint::heuristic("de", 0.4); // confidence 太低,into_lang_str → None
72/// ```
73///
74/// **SemVer**:`#[non_exhaustive]` — 未来加字段(如 `audit_id` / `provider_chain`)
75/// 不破;pub fields 可读,struct literal 构造仅 crate 内可用。
76#[derive(Debug, Clone)]
77#[non_exhaustive]
78pub struct LanguageHint {
79 /// ISO 639-1 lowercase(`"en"` / `"de"` / `"it"` / `"fr"` / ...);与 fixture
80 /// lang 字段对齐
81 pub lang: String,
82 /// 信息来源(用于 audit trail + 可信度决策)
83 pub source: LangHintSource,
84 /// 置信度 [0.0, 1.0];low-conf(< 0.5)`into_lang_str` 一律返 None(fail-closed
85 /// 退化 baseline path,避免误降 policy)
86 pub confidence: f32,
87}
88
89/// `into_lang_str` 用的 fail-closed 阈值。confidence < 此值 → None(走 baseline)。
90///
91/// **0.5 选择理由**:
92/// - 启发式 detect 在短文本无关键词时常返 0.3-0.4(`feedback_lang_review_authoritative`)
93/// - caller_provided / fixture 一般 1.0(用户/fixture 权威源)
94/// - 0.5 阈值切开"模糊 advisory" vs "可决策" 两类
95pub const LANG_HINT_TRUSTED_CONFIDENCE: f32 = 0.5;
96
97impl LanguageHint {
98 /// caller 显式传入(`CallerProvided` source,confidence = 1.0)。
99 /// 适用:用户 locale 设置 / 业务上下文 / 已校验输入。
100 pub fn caller_provided(lang: impl Into<String>) -> Self {
101 Self {
102 lang: lang.into(),
103 source: LangHintSource::CallerProvided,
104 confidence: 1.0,
105 }
106 }
107
108 /// fixture / 测试场景(`FixtureExperimental` source,confidence = 1.0)。
109 /// 仅非 production;典型 release-gate / spike-gate / fixture lang 字段透传。
110 pub fn fixture(lang: impl Into<String>) -> Self {
111 Self {
112 lang: lang.into(),
113 source: LangHintSource::FixtureExperimental,
114 confidence: 1.0,
115 }
116 }
117
118 /// 启发式 detect(`Heuristic` source,caller 给 confidence)。
119 /// **决策不可信**:即使 confidence = 1.0,`is_trusted` 返 false(由
120 /// `LangHintSource` 静态判定);仅 advisory(diagnose / suggestion UI)。
121 /// confidence < 0.5 时 `into_lang_str` 也返 None。
122 pub fn heuristic(lang: impl Into<String>, confidence: f32) -> Self {
123 Self {
124 lang: lang.into(),
125 source: LangHintSource::Heuristic,
126 confidence: confidence.clamp(0.0, 1.0),
127 }
128 }
129
130 /// **v0.10 Sprint 6** — 启发式 lang detect(advisory only)。
131 ///
132 /// 调用 [`detect_lang_heuristic`] 内部启发式(unicode 字符集 + 关键词),返
133 /// `LanguageHint` with `LangHintSource::Heuristic`。
134 /// **始终返 Heuristic** — `lang_str()` **永返 None**,无法触发 firewall 决策路径。
135 ///
136 /// 适用场景:
137 /// - fixture lang 字段标注辅助(P1.0 启发式同口径,但用 Rust 端版本)
138 /// - SDK consumer 诊断 UI / 建议性显示
139 /// - **永不**作 production 决策权威(`feedback_lang_review_authoritative` 约束)
140 pub fn detect(text: &str) -> Self {
141 detect_lang_heuristic(text)
142 }
143
144 /// **fail-closed 转换**:返 `Option<&str>`(走 `scan_text_with_engine_with_lang`)。
145 ///
146 /// 决策规则:
147 /// - confidence < `LANG_HINT_TRUSTED_CONFIDENCE`(0.5)→ `None`(退化 baseline)
148 /// - `LangHintSource::Heuristic` → `None`(无论 confidence,启发式不可信)
149 /// - 其他(`CallerProvided` / `FixtureExperimental` + confidence ≥ 0.5)→ `Some(&lang)`
150 ///
151 /// 这是 D=C 决议下的 SDK 边界 — caller 即使传 `Heuristic`,也**不会**触发
152 /// lang-conditional threshold(防 production 误降 policy)。
153 pub fn lang_str(&self) -> Option<&str> {
154 if !self.source.is_trusted() {
155 return None;
156 }
157 if self.confidence < LANG_HINT_TRUSTED_CONFIDENCE {
158 return None;
159 }
160 Some(self.lang.as_str())
161 }
162}
163
164/// **v0.10 Sprint 6** — 独立函数版启发式 lang detect(advisory only)。
165///
166/// **算法**(与 `scripts/spike-p3/analyze_fixture_distribution.py::detect_lang` 同口径):
167/// 1. unicode 字符集(明确特征,confidence 高)
168/// - 韩文 Hangul ≥ 2 字符 → `("ko", 0.9)`
169/// - 日文 Hiragana/Katakana ≥ 2 → `("ja", 0.9)`
170/// - 中文 CJK Han ≥ 2 → `("zh", 0.85)`(可能与 ja 共有汉字,故 confidence 略低)
171/// 2. 拉丁语系关键词(明确特征,confidence 中-高)
172/// - 德语关键词命中 → `("de", 0.7)`
173/// - 法语关键词命中 → `("fr", 0.7)`
174/// - 意大利语关键词命中 → `("it", 0.7)`
175/// - 西班牙语关键词命中 → `("es", 0.7)`
176/// 3. 重音字符 fallback(模糊,confidence 低)
177/// - `äöüß` 字符 → `("de", 0.5)`
178/// - 其他西欧重音字符 → `("fr", 0.4)`(默认归 fr,西欧最广)
179/// 4. 无特征 → `("en", 0.3)`(短文本无关键词时低 confidence,fail-closed 退化)
180///
181/// **fail-closed**:无论返哪个 lang,`source = Heuristic` 永远不可作 production 决策。
182/// caller 用 `into_lang_str()` / `lang_str()` 始终返 None(except low-conf <0.5 也返 None)。
183pub fn detect_lang_heuristic(text: &str) -> LanguageHint {
184 use LangHintSource::Heuristic;
185
186 // CJK 字符集(明确特征)
187 let mut cjk = 0;
188 let mut hira = 0;
189 let mut kata = 0;
190 let mut hangul = 0;
191 for ch in text.chars() {
192 let cp = ch as u32;
193 if (0x4E00..=0x9FFF).contains(&cp) {
194 cjk += 1;
195 } else if (0x3040..=0x309F).contains(&cp) {
196 hira += 1;
197 } else if (0x30A0..=0x30FF).contains(&cp) {
198 kata += 1;
199 } else if (0xAC00..=0xD7AF).contains(&cp) {
200 hangul += 1;
201 }
202 }
203 if hangul >= 2 {
204 return LanguageHint {
205 lang: "ko".to_string(),
206 source: Heuristic,
207 confidence: 0.9,
208 };
209 }
210 if hira + kata >= 2 {
211 return LanguageHint {
212 lang: "ja".to_string(),
213 source: Heuristic,
214 confidence: 0.9,
215 };
216 }
217 if cjk >= 2 {
218 return LanguageHint {
219 lang: "zh".to_string(),
220 source: Heuristic,
221 confidence: 0.85,
222 };
223 }
224
225 // 关键词命中(中-高 confidence)
226 let low = text.to_lowercase();
227 let de_hints = [
228 "herr ",
229 "frau ",
230 "straße",
231 "strasse",
232 "gmbh",
233 "münchen",
234 "berlin",
235 "hamburg",
236 "köln",
237 "müller",
238 "schmidt",
239 " und ",
240 "ich bin",
241 "guten tag",
242 "bitte",
243 "webseite",
244 "konto",
245 "verwendet",
246 "verfügbar",
247 "geboren am",
248 ];
249 let fr_hints = [
250 "monsieur",
251 "madame",
252 "bonjour",
253 "paris",
254 "lyon",
255 "marseille",
256 "merci",
257 "veuillez",
258 "envoyer",
259 "visitez",
260 "né le ",
261 "née le ",
262 "téléphone",
263 "adresse:",
264 " et ",
265 ];
266 let it_hints = [
267 "signor",
268 "signora",
269 "roma",
270 "milano",
271 "napoli",
272 "bologna",
273 "buongiorno",
274 "grazie",
275 "contatta",
276 "telefono",
277 "nato il",
278 "nata il",
279 "codice fiscale",
280 "visita ",
281 "indirizzo:",
282 " e ",
283 ];
284 let es_hints = [
285 "señor",
286 "señora",
287 "calle ",
288 "madrid",
289 "barcelona",
290 "valencia",
291 "sevilla",
292 "gracias",
293 "por favor",
294 "avenida",
295 ];
296 if de_hints.iter().any(|h| low.contains(h)) {
297 return LanguageHint {
298 lang: "de".to_string(),
299 source: Heuristic,
300 confidence: 0.7,
301 };
302 }
303 if fr_hints.iter().any(|h| low.contains(h)) {
304 return LanguageHint {
305 lang: "fr".to_string(),
306 source: Heuristic,
307 confidence: 0.7,
308 };
309 }
310 if it_hints.iter().any(|h| low.contains(h)) {
311 return LanguageHint {
312 lang: "it".to_string(),
313 source: Heuristic,
314 confidence: 0.7,
315 };
316 }
317 if es_hints.iter().any(|h| low.contains(h)) {
318 return LanguageHint {
319 lang: "es".to_string(),
320 source: Heuristic,
321 confidence: 0.7,
322 };
323 }
324
325 // 字符集 fallback(模糊;feedback_lang_review_authoritative 警告:短文本误判 17/45)
326 let has_de_chars = text
327 .chars()
328 .any(|c| matches!(c, 'ä' | 'ö' | 'ü' | 'ß' | 'Ä' | 'Ö' | 'Ü'));
329 if has_de_chars {
330 return LanguageHint {
331 lang: "de".to_string(),
332 source: Heuristic,
333 confidence: 0.45, // < TRUSTED 0.5(fail-closed 退化 baseline)
334 };
335 }
336 let has_western_accent = text.chars().any(|c| {
337 matches!(
338 c,
339 'à' | 'â'
340 | 'ç'
341 | 'é'
342 | 'è'
343 | 'ê'
344 | 'ë'
345 | 'î'
346 | 'ï'
347 | 'ô'
348 | 'û'
349 | 'ù'
350 | 'À'
351 | 'É'
352 | 'È'
353 | 'Ê'
354 | 'Ô'
355 )
356 });
357 if has_western_accent {
358 return LanguageHint {
359 lang: "fr".to_string(),
360 source: Heuristic,
361 confidence: 0.4,
362 };
363 }
364
365 // 无特征 → en,低 confidence(fail-closed:lang_str 返 None)
366 LanguageHint {
367 lang: "en".to_string(),
368 source: Heuristic,
369 confidence: 0.3,
370 }
371}
372
373/// **v0.10 Sprint 2** — `scan_text_with_engine_with_hint` 浅级 wrapper。
374///
375/// SDK consumer 友好版 `scan_text_with_engine_with_lang`:接 typed
376/// [`LanguageHint`](Option),内部按 fail-closed 规则转 `Option<&str>`。
377///
378/// 等价于:
379/// ```ignore
380/// let lang = hint.and_then(|h| h.lang_str());
381/// scan_text_with_engine_with_lang(input, engine, lang)
382/// ```
383///
384/// 但 typed wrapper 让 caller 必须表达 source / confidence,**强制可解释 + 可
385/// 进 audit**;裸 `Option<&str>` 无 provenance,SDK consumer 易混淆来源。
386///
387/// **SemVer**:新公共 API,SemVer 安全(legacy `scan_text_with_engine_with_lang`
388/// 保留,未改签名)。
389pub fn scan_text_with_engine_with_hint(
390 input: &str,
391 engine: &dyn RedactionEngine,
392 hint: Option<&LanguageHint>,
393) -> Result<RedactionResult, ScanError> {
394 let lang = hint.and_then(|h| h.lang_str());
395 scan_text_with_engine_with_lang(input, engine, lang)
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn caller_provided_high_trust_returns_lang() {
404 let h = LanguageHint::caller_provided("de");
405 assert_eq!(h.source, LangHintSource::CallerProvided);
406 assert!((h.confidence - 1.0).abs() < f32::EPSILON);
407 assert_eq!(h.lang_str(), Some("de"));
408 }
409
410 #[test]
411 fn fixture_experimental_returns_lang() {
412 let h = LanguageHint::fixture("it");
413 assert_eq!(h.source, LangHintSource::FixtureExperimental);
414 assert_eq!(h.lang_str(), Some("it"));
415 }
416
417 /// **关键**:Heuristic source 即使 confidence=1.0 也返 None(不可信任)。
418 /// 这是 D=C 决议下 SDK 边界;`feedback_lang_review_authoritative` 约束。
419 #[test]
420 fn heuristic_always_returns_none_even_max_confidence() {
421 let h = LanguageHint::heuristic("de", 1.0);
422 assert_eq!(h.source, LangHintSource::Heuristic);
423 assert_eq!(
424 h.lang_str(),
425 None,
426 "Heuristic source 即使 confidence=1.0 也必须返 None(决策不可信任)"
427 );
428 }
429
430 /// low confidence(< 0.5)即使 trusted source 也返 None(fail-closed)
431 #[test]
432 fn low_confidence_returns_none_even_caller_provided() {
433 let mut h = LanguageHint::caller_provided("de");
434 h.confidence = 0.4; // 模拟 caller 自降信任
435 assert_eq!(
436 h.lang_str(),
437 None,
438 "confidence < 0.5 必须 fail-closed 返 None"
439 );
440 }
441
442 /// confidence clamp [0.0, 1.0]
443 #[test]
444 fn heuristic_confidence_clamp() {
445 let h_neg = LanguageHint::heuristic("de", -0.5);
446 assert!(h_neg.confidence >= 0.0);
447 let h_over = LanguageHint::heuristic("de", 2.0);
448 assert!(h_over.confidence <= 1.0);
449 }
450
451 /// LangHintSource non_exhaustive — caller 必 _ 通配
452 #[test]
453 #[allow(unreachable_patterns)]
454 fn lang_hint_source_non_exhaustive_match_compiles() {
455 let s = LangHintSource::CallerProvided;
456 let trusted = match s {
457 LangHintSource::CallerProvided => true,
458 LangHintSource::FixtureExperimental => true,
459 LangHintSource::Heuristic => false,
460 _ => false, // non_exhaustive 强制 _
461 };
462 assert!(trusted);
463 }
464
465 /// `is_trusted` 与 `lang_str` 决策一致(Heuristic / 未知 source 不可信)
466 #[test]
467 fn is_trusted_consistent_with_lang_str_decision() {
468 assert!(LangHintSource::CallerProvided.is_trusted());
469 assert!(LangHintSource::FixtureExperimental.is_trusted());
470 assert!(!LangHintSource::Heuristic.is_trusted());
471 }
472
473 /// scan_text_with_engine_with_hint 等价于 hint.lang_str() + scan_with_lang
474 #[test]
475 fn scan_with_hint_empty_input_fail_closed() {
476 let h = LanguageHint::caller_provided("de");
477 let r = scan_text_with_engine_with_hint("", &crate::engine::NoopEngine, Some(&h));
478 assert!(matches!(r, Err(ScanError::EmptyInput)));
479 }
480
481 /// hint = None 等价于 scan_text_with_engine(legacy)
482 #[test]
483 fn scan_with_hint_none_equivalent_to_legacy() {
484 let r = scan_text_with_engine_with_hint("hello", &crate::engine::NoopEngine, None)
485 .expect("non-empty");
486 // NoopEngine 返空 model findings;Hard rules 在 "hello" 上不命中
487 assert!(r.findings.is_empty(), "NoopEngine + 'hello' 无 finding");
488 }
489
490 // ─── v0.10 Sprint 6 — advisory lang detect 守门 ───
491
492 /// CJK 字符明确特征(高 confidence)
493 #[test]
494 fn detect_lang_zh_chinese() {
495 let h = detect_lang_heuristic("请联系王小明处理订单");
496 assert_eq!(h.lang, "zh");
497 assert_eq!(h.source, LangHintSource::Heuristic);
498 assert!(h.confidence >= 0.8);
499 }
500
501 #[test]
502 fn detect_lang_ja_japanese() {
503 let h = detect_lang_heuristic("田中太郎さんが昨日来ました");
504 assert_eq!(h.lang, "ja");
505 assert!(h.confidence >= 0.85);
506 }
507
508 #[test]
509 fn detect_lang_ko_korean() {
510 let h = detect_lang_heuristic("김민수 씨에게 연락하세요");
511 assert_eq!(h.lang, "ko");
512 assert!(h.confidence >= 0.85);
513 }
514
515 /// 拉丁语系关键词命中(中 confidence 0.7)
516 #[test]
517 fn detect_lang_de_keyword() {
518 let h = detect_lang_heuristic("Herr Schmidt arbeitet hier.");
519 assert_eq!(h.lang, "de");
520 assert!((h.confidence - 0.7).abs() < 0.01);
521 }
522
523 #[test]
524 fn detect_lang_fr_keyword() {
525 let h = detect_lang_heuristic("Monsieur Dupont travaille ici.");
526 assert_eq!(h.lang, "fr");
527 }
528
529 #[test]
530 fn detect_lang_it_keyword() {
531 let h = detect_lang_heuristic("Il signor Rossi lavora qui.");
532 assert_eq!(h.lang, "it");
533 }
534
535 /// 短文本无关键词 → en 低 confidence(fail-closed)
536 #[test]
537 fn detect_lang_short_text_low_confidence() {
538 let h = detect_lang_heuristic("John Smith works here.");
539 assert_eq!(h.lang, "en");
540 assert!(
541 h.confidence < LANG_HINT_TRUSTED_CONFIDENCE,
542 "短英文文本 confidence 必须 < 0.5(fail-closed 退化 baseline)"
543 );
544 assert_eq!(
545 h.lang_str(),
546 None,
547 "无论 lang 是什么,Heuristic source 永返 None"
548 );
549 }
550
551 /// **关键不变量**:即使 detect 命中明确语言(高 confidence),lang_str 仍返 None
552 /// (Heuristic source 永不可信任 — D=C 锁定下的 SDK 边界)
553 #[test]
554 fn detect_lang_high_confidence_still_not_trusted() {
555 let h = detect_lang_heuristic("田中太郎さんが昨日来ました");
556 assert!(h.confidence >= 0.85, "ja 应高 confidence");
557 assert_eq!(
558 h.lang_str(),
559 None,
560 "Heuristic source 即使 high confidence 也必须返 None(feedback_lang_review_authoritative)"
561 );
562 }
563
564 /// `LanguageHint::detect(text)` 等价独立函数
565 #[test]
566 fn language_hint_detect_method_equivalent_to_function() {
567 let text = "Herr Schmidt arbeitet hier.";
568 let from_method = LanguageHint::detect(text);
569 let from_fn = detect_lang_heuristic(text);
570 assert_eq!(from_method.lang, from_fn.lang);
571 assert_eq!(from_method.source, from_fn.source);
572 assert_eq!(from_method.confidence, from_fn.confidence);
573 }
574
575 /// 重音字符 fallback(de ä/ö/ü/ß)— 模糊但比 en fallback 好
576 #[test]
577 fn detect_lang_de_accent_fallback() {
578 // 短文本无关键词,但有 ß 字符
579 let h = detect_lang_heuristic("Herr ß test");
580 // 实际"Herr "命中关键词,先返 de;此 case 走关键词路径
581 assert_eq!(h.lang, "de");
582
583 // 真 fallback case:无关键词但有 ä/ö/ü/ß
584 let h2 = detect_lang_heuristic("würde");
585 assert_eq!(h2.lang, "de");
586 assert!(
587 h2.confidence < LANG_HINT_TRUSTED_CONFIDENCE,
588 "fallback 模糊路径 confidence 必须 < 0.5"
589 );
590 }
591
592 /// 与 fixture lang 字段(P1.0+ 人工核对权威)对比 — 启发式应**部分**命中,
593 /// 但**绝不能**作权威源(本测试文档化:detect 仅 advisory)。
594 #[test]
595 fn detect_lang_documented_as_advisory_not_authoritative() {
596 // 短文本,fixture 真值是 en,但启发式无关键词
597 let h = detect_lang_heuristic("John Smith works here.");
598 // 即使启发式判 en,confidence 仍低 → lang_str None,不影响 production 决策
599 assert_eq!(h.source, LangHintSource::Heuristic);
600 assert!(!h.source.is_trusted(), "Heuristic source 永远不可信任");
601 }
602}