1use unicode_normalization::UnicodeNormalization;
3
4use crate::{config::CommitConfig, types::ConventionalCommit, validation::is_past_tense_verb};
5
6pub fn normalize_unicode(text: &str) -> String {
9 let pre_normalized = text
12 .replace('≠', "!=") .replace('½', "1/2")
16 .replace('¼', "1/4")
17 .replace('¾', "3/4")
18 .replace('⅓', "1/3")
19 .replace('⅔', "2/3")
20 .replace('⅕', "1/5")
21 .replace('⅖', "2/5")
22 .replace('⅗', "3/5")
23 .replace('⅘', "4/5")
24 .replace('⅙', "1/6")
25 .replace('⅚', "5/6")
26 .replace('⅛', "1/8")
27 .replace('⅜', "3/8")
28 .replace('⅝', "5/8")
29 .replace('⅞', "7/8")
30 .replace('⁰', "^0")
32 .replace('¹', "^1")
33 .replace('²', "^2")
34 .replace('³', "^3")
35 .replace('⁴', "^4")
36 .replace('⁵', "^5")
37 .replace('⁶', "^6")
38 .replace('⁷', "^7")
39 .replace('⁸', "^8")
40 .replace('⁹', "^9")
41 .replace('₀', "_0")
43 .replace('₁', "_1")
44 .replace('₂', "_2")
45 .replace('₃', "_3")
46 .replace('₄', "_4")
47 .replace('₅', "_5")
48 .replace('₆', "_6")
49 .replace('₇', "_7")
50 .replace('₈', "_8")
51 .replace('₉', "_9");
52
53 let normalized: String = pre_normalized.nfkd().collect();
55
56 normalized
57 .replace(['\u{2018}', '\u{2019}'], "'") .replace(['\u{201C}', '\u{201D}'], "\"") .replace('\u{201A}', "'") .replace(['\u{201E}', '\u{00AB}', '\u{00BB}'], "\"") .replace(['\u{2039}', '\u{203A}'], "'") .replace(['\u{2010}', '\u{2011}', '\u{2012}'], "-") .replace(['\u{2013}', '\u{2014}', '\u{2015}'], "--") .replace('\u{2212}', "-") .replace('\u{2192}', "->") .replace('←', "<-") .replace('↔', "<->") .replace('⇒', "=>") .replace('⇐', "<=") .replace('⇔', "<=>") .replace('↑', "^") .replace('↓', "v") .replace('\u{2264}', "<=") .replace('≥', ">=") .replace('≈', "~=") .replace('≡', "==") .replace('\u{00D7}', "x") .replace('÷', "/") .replace(['\u{2026}', '⋯', '⋮'], "...") .replace(['•', '◦', '▪', '▫', '◆', '◇'], "-") .replace(['✓', '✔'], "v") .replace(['✗', '✘'], "x") .replace('λ', "lambda")
92 .replace('α', "alpha")
93 .replace('β', "beta")
94 .replace('γ', "gamma")
95 .replace('δ', "delta")
96 .replace('ε', "epsilon")
97 .replace('θ', "theta")
98 .replace('μ', "mu")
99 .replace('π', "pi")
100 .replace('σ', "sigma")
101 .replace('Σ', "Sigma")
102 .replace('Δ', "Delta")
103 .replace('Π', "Pi")
104 .replace(
106 [
107 '\u{00A0}', '\u{2000}', '\u{2001}', '\u{2002}', '\u{2003}', '\u{2004}', '\u{2005}',
108 '\u{2006}', '\u{2007}', '\u{2008}', '\u{2009}', '\u{200A}', '\u{202F}', '\u{205F}',
109 '\u{3000}',
110 ],
111 " ",
112 ) .replace(['\u{200B}', '\u{200C}', '\u{200D}', '\u{FEFF}'], "") }
116
117pub fn cap_details(details: &mut Vec<String>, max_count: usize) {
119 if details.len() <= max_count {
120 return;
121 }
122
123 let mut scored: Vec<(usize, i32, &String)> = details
125 .iter()
126 .enumerate()
127 .map(|(idx, detail)| {
128 let lower = detail.to_lowercase();
129 let mut score = 0;
130
131 if lower.contains("security")
133 || lower.contains("vulnerability")
134 || lower.contains("exploit")
135 || lower.contains("critical")
136 || (lower.contains("fix") && lower.contains("crash"))
137 {
138 score += 100;
139 }
140 if lower.contains("breaking") || lower.contains("incompatible") {
141 score += 90;
142 }
143 if lower.contains("performance")
144 || lower.contains("faster")
145 || lower.contains("optimization")
146 {
147 score += 80;
148 }
149 if lower.contains("fix") || lower.contains("bug") {
150 score += 70;
151 }
152
153 if lower.contains("api") || lower.contains("interface") || lower.contains("public") {
155 score += 50;
156 }
157 if lower.contains("user") || lower.contains("client") {
158 score += 40;
159 }
160 if lower.contains("deprecated") || lower.contains("removed") {
161 score += 35;
162 }
163
164 score += (detail.len() / 20).min(10) as i32;
166
167 (idx, score, detail)
168 })
169 .collect();
170
171 scored.sort_by(|a, b| b.1.cmp(&a.1));
173
174 let mut keep_indices: Vec<usize> = scored
176 .iter()
177 .take(max_count)
178 .map(|(idx, ..)| *idx)
179 .collect();
180 keep_indices.sort_unstable(); let kept: Vec<String> = keep_indices
184 .iter()
185 .filter_map(|&idx| details.get(idx).cloned())
186 .collect();
187 *details = kept;
188}
189
190pub fn normalize_summary_verb(summary: &mut String, commit_type: &str) {
193 if summary.trim().is_empty() {
194 return;
195 }
196
197 let mut parts_iter = summary.split_whitespace();
198 let first_word = match parts_iter.next() {
199 Some(word) => word.to_string(),
200 None => return,
201 };
202 let rest = parts_iter.collect::<Vec<_>>().join(" ");
203 let first_word_lower = first_word.to_lowercase();
204
205 if is_past_tense_verb(&first_word_lower) {
207 if commit_type == "refactor" && first_word_lower == "refactored" {
209 *summary = if rest.is_empty() {
210 "restructured".to_string()
211 } else {
212 format!("restructured {rest}")
213 };
214 }
215 return;
216 }
217
218 let converted = match first_word_lower.as_str() {
220 "add" | "adds" => Some("added"),
221 "fix" | "fixes" => Some("fixed"),
222 "update" | "updates" => Some("updated"),
223 "refactor" | "refactors" => Some(if commit_type == "refactor" {
224 "restructured"
225 } else {
226 "refactored"
227 }),
228 "remove" | "removes" => Some("removed"),
229 "replace" | "replaces" => Some("replaced"),
230 "improve" | "improves" => Some("improved"),
231 "implement" | "implements" => Some("implemented"),
232 "migrate" | "migrates" => Some("migrated"),
233 "rename" | "renames" => Some("renamed"),
234 "move" | "moves" => Some("moved"),
235 "merge" | "merges" => Some("merged"),
236 "split" | "splits" => Some("split"),
237 "extract" | "extracts" => Some("extracted"),
238 "restructure" | "restructures" => Some("restructured"),
239 "reorganize" | "reorganizes" => Some("reorganized"),
240 "consolidate" | "consolidates" => Some("consolidated"),
241 "simplify" | "simplifies" => Some("simplified"),
242 "optimize" | "optimizes" => Some("optimized"),
243 "document" | "documents" => Some("documented"),
244 "test" | "tests" => Some("tested"),
245 "change" | "changes" => Some("changed"),
246 "introduce" | "introduces" => Some("introduced"),
247 "deprecate" | "deprecates" => Some("deprecated"),
248 "delete" | "deletes" => Some("deleted"),
249 "correct" | "corrects" => Some("corrected"),
250 "enhance" | "enhances" => Some("enhanced"),
251 "revert" | "reverts" => Some("reverted"),
252 _ => None,
253 };
254
255 if let Some(past) = converted {
256 *summary = if rest.is_empty() {
257 past.to_string()
258 } else {
259 format!("{past} {rest}")
260 };
261 }
262}
263
264pub fn post_process_commit_message(msg: &mut ConventionalCommit, _config: &CommitConfig) {
266 let mut summary_str = normalize_unicode(msg.summary.as_str());
271
272 msg.body = msg.body.iter().map(|s| normalize_unicode(s)).collect();
274 msg.footers = msg.footers.iter().map(|s| normalize_unicode(s)).collect();
275
276 summary_str = summary_str
278 .replace(['\r', '\n'], " ")
279 .split_whitespace()
280 .collect::<Vec<_>>()
281 .join(" ")
282 .trim()
283 .trim_end_matches('.')
284 .trim_end_matches(';')
285 .trim_end_matches(':')
286 .to_string();
287
288 let is_first_token_all_caps = |s: &str| -> bool {
290 s.split_whitespace().next().is_some_and(|token| {
291 token
292 .chars()
293 .all(|c| !c.is_alphabetic() || c.is_uppercase())
294 })
295 };
296
297 if !is_first_token_all_caps(&summary_str)
299 && let Some(first_char) = summary_str.chars().next()
300 && first_char.is_uppercase()
301 {
302 let rest = &summary_str[first_char.len_utf8()..];
303 summary_str = format!("{}{}", first_char.to_lowercase(), rest);
304 }
305
306 normalize_summary_verb(&mut summary_str, msg.commit_type.as_str());
309 summary_str = summary_str.trim().to_string();
310
311 if !is_first_token_all_caps(&summary_str)
313 && let Some(first_char) = summary_str.chars().next()
314 && first_char.is_uppercase()
315 {
316 let rest = &summary_str[first_char.len_utf8()..];
317 summary_str = format!("{}{}", first_char.to_lowercase(), rest);
318 }
319
320 summary_str = summary_str.trim_end_matches('.').to_string();
323
324 msg.summary = crate::types::CommitSummary::new_unchecked(summary_str, 128)
327 .expect("post-processed summary should be valid");
328
329 for item in &mut msg.body {
331 let mut cleaned = item
332 .replace(['\r', '\n'], " ")
333 .trim()
334 .trim_start_matches('\u{2022}')
335 .trim_start_matches('-')
336 .trim_start_matches('*')
337 .trim_start_matches('+')
338 .trim()
339 .to_string();
340
341 cleaned = cleaned
342 .split_whitespace()
343 .collect::<Vec<_>>()
344 .join(" ")
345 .trim()
346 .trim_end_matches('.')
347 .trim_end_matches(';')
348 .trim_end_matches(',')
349 .to_string();
350
351 if cleaned.is_empty() {
352 *item = cleaned;
353 continue;
354 }
355
356 if let Some(first_char) = cleaned.chars().next()
358 && first_char.is_lowercase()
359 {
360 let rest = &cleaned[first_char.len_utf8()..];
361 cleaned = format!("{}{}", first_char.to_uppercase(), rest);
362 }
363
364 if !cleaned.ends_with('.') {
365 cleaned.push('.');
366 }
367
368 *item = cleaned;
369 }
370
371 msg.body.retain(|item| !item.trim().is_empty());
373
374 cap_details(&mut msg.body, 4);
376}
377
378pub fn format_commit_message(msg: &ConventionalCommit) -> String {
380 let scope_part = msg
382 .scope
383 .as_ref()
384 .map(|s| format!("({s})"))
385 .unwrap_or_default();
386 let first_line = format!("{}{}: {}", msg.commit_type, scope_part, msg.summary);
387
388 let body_formatted = if msg.body.is_empty() {
390 String::new()
391 } else {
392 msg.body
393 .iter()
394 .map(|item| format!("- {item}"))
395 .collect::<Vec<_>>()
396 .join("\n")
397 };
398
399 let footers_formatted = if msg.footers.is_empty() {
401 String::new()
402 } else {
403 msg.footers.join("\n")
404 };
405
406 let mut result = first_line;
408 if !body_formatted.is_empty() {
409 result.push_str("\n\n");
410 result.push_str(&body_formatted);
411 }
412 if !footers_formatted.is_empty() {
413 result.push_str("\n\n");
414 result.push_str(&footers_formatted);
415 }
416 result
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use crate::types::{CommitSummary, CommitType, ConventionalCommit, Scope};
423
424 #[test]
426 fn test_normalize_unicode_smart_quotes() {
427 assert_eq!(normalize_unicode("\u{2018}smart quotes\u{2019}"), "'smart quotes'");
428 assert_eq!(normalize_unicode("\u{201C}double quotes\u{201D}"), "\"double quotes\"");
429 assert_eq!(normalize_unicode("\u{201A}low quote\u{2019}"), "'low quote'");
430 assert_eq!(normalize_unicode("\u{201E}low double\u{201D}"), "\"low double\"");
431 }
432
433 #[test]
434 fn test_normalize_unicode_dashes() {
435 assert_eq!(normalize_unicode("en\u{2013}dash"), "en--dash");
436 assert_eq!(normalize_unicode("em\u{2014}dash"), "em--dash");
437 assert_eq!(normalize_unicode("fig\u{2012}dash"), "fig-dash");
438 assert_eq!(normalize_unicode("minus\u{2212}sign"), "minus-sign");
439 }
440
441 #[test]
442 fn test_normalize_unicode_arrows() {
443 assert_eq!(normalize_unicode("arrow\u{2192}right"), "arrow->right");
444 assert_eq!(normalize_unicode("arrow\u{2190}left"), "arrow<-left");
445 assert_eq!(normalize_unicode("arrow\u{2194}both"), "arrow<->both");
446 assert_eq!(normalize_unicode("double\u{21D2}arrow"), "double=>arrow");
447 assert_eq!(normalize_unicode("up\u{2191}arrow"), "up^arrow");
448 }
449
450 #[test]
451 fn test_normalize_unicode_math() {
452 assert_eq!(normalize_unicode("a\u{00D7}b"), "axb");
453 assert_eq!(normalize_unicode("a\u{00F7}b"), "a/b");
454 assert_eq!(normalize_unicode("x\u{2264}y"), "x<=y");
455 assert_eq!(normalize_unicode("x\u{2265}y"), "x>=y");
456 assert_eq!(normalize_unicode("x\u{2260}y"), "x!=y");
457 assert_eq!(normalize_unicode("x\u{2248}y"), "x~=y");
458 }
459
460 #[test]
461 fn test_normalize_unicode_greek() {
462 assert_eq!(normalize_unicode("\u{03BB} function"), "lambda function");
463 assert_eq!(normalize_unicode("\u{03B1} beta \u{03B3}"), "alpha beta gamma");
464 assert_eq!(normalize_unicode("\u{03BC} service"), "mu service");
465 assert_eq!(normalize_unicode("\u{03A3} total"), "Sigma total");
466 }
467
468 #[test]
469 fn test_normalize_unicode_fractions() {
470 assert_eq!(normalize_unicode("\u{00BD} cup"), "1/2 cup");
471 assert_eq!(normalize_unicode("\u{00BE} done"), "3/4 done");
472 assert_eq!(normalize_unicode("\u{2153} left"), "1/3 left");
473 }
474
475 #[test]
476 fn test_normalize_unicode_superscripts() {
477 assert_eq!(normalize_unicode("x\u{00B2}"), "x^2");
478 assert_eq!(normalize_unicode("10\u{00B3}"), "10^3");
479 }
480
481 #[test]
482 fn test_normalize_unicode_multiple_replacements() {
483 let input =
484 "\u{2018}smart\u{2019}\u{2192}straight \u{201C}quotes\u{201D}\u{00D7}math\u{2264}ops";
485 let expected = "'smart'->straight \"quotes\"xmath<=ops";
486 assert_eq!(normalize_unicode(input), expected);
487 }
488
489 #[test]
490 fn test_normalize_unicode_ellipsis() {
491 assert_eq!(normalize_unicode("wait\u{2026}"), "wait...");
492 assert_eq!(normalize_unicode("more\u{22EF}dots"), "more...dots");
493 }
494
495 #[test]
496 fn test_normalize_unicode_bullets() {
497 assert_eq!(normalize_unicode("\u{2022}item"), "-item");
498 assert_eq!(normalize_unicode("\u{25E6}item"), "-item");
499 }
500
501 #[test]
502 fn test_normalize_unicode_check_marks() {
503 assert_eq!(normalize_unicode("\u{2713}done"), "vdone");
504 assert_eq!(normalize_unicode("\u{2717}failed"), "xfailed");
505 }
506
507 #[test]
509 fn test_normalize_summary_verb_present_to_past() {
510 let mut s = "add new feature".to_string();
511 normalize_summary_verb(&mut s, "feat");
512 assert_eq!(s, "added new feature");
513
514 let mut s = "fix bug".to_string();
515 normalize_summary_verb(&mut s, "fix");
516 assert_eq!(s, "fixed bug");
517
518 let mut s = "update docs".to_string();
519 normalize_summary_verb(&mut s, "docs");
520 assert_eq!(s, "updated docs");
521 }
522
523 #[test]
524 fn test_normalize_summary_verb_already_past() {
525 let mut s = "added feature".to_string();
526 normalize_summary_verb(&mut s, "feat");
527 assert_eq!(s, "added feature");
528
529 let mut s = "fixed bug".to_string();
530 normalize_summary_verb(&mut s, "fix");
531 assert_eq!(s, "fixed bug");
532 }
533
534 #[test]
535 fn test_normalize_summary_verb_third_person() {
536 let mut s = "adds feature".to_string();
537 normalize_summary_verb(&mut s, "feat");
538 assert_eq!(s, "added feature");
539
540 let mut s = "fixes bug".to_string();
541 normalize_summary_verb(&mut s, "fix");
542 assert_eq!(s, "fixed bug");
543 }
544
545 #[test]
546 fn test_normalize_summary_verb_non_verb_start() {
547 let mut s = "123 files changed".to_string();
548 normalize_summary_verb(&mut s, "chore");
549 assert_eq!(s, "123 files changed");
550 }
551
552 #[test]
553 fn test_normalize_summary_verb_refactor_special_case() {
554 let mut s = "refactored code".to_string();
555 normalize_summary_verb(&mut s, "refactor");
556 assert_eq!(s, "restructured code");
557 }
558
559 #[test]
560 fn test_normalize_summary_verb_refactor_present() {
561 let mut s = "refactor code".to_string();
562 normalize_summary_verb(&mut s, "refactor");
563 assert_eq!(s, "restructured code");
564
565 let mut s = "refactor logic".to_string();
566 normalize_summary_verb(&mut s, "feat");
567 assert_eq!(s, "refactored logic");
568 }
569
570 #[test]
571 fn test_normalize_summary_verb_empty() {
572 let mut s = String::new();
573 normalize_summary_verb(&mut s, "feat");
574 assert_eq!(s, "");
575 }
576
577 #[test]
578 fn test_normalize_summary_verb_single_word() {
579 let mut s = "add".to_string();
580 normalize_summary_verb(&mut s, "feat");
581 assert_eq!(s, "added");
582 }
583
584 #[test]
586 fn test_cap_details_below_max() {
587 let mut details = vec!["first".to_string(), "second".to_string(), "third".to_string()];
588 cap_details(&mut details, 6);
589 assert_eq!(details.len(), 3);
590 }
591
592 #[test]
593 fn test_cap_details_at_max() {
594 let mut details = vec![
595 "one".to_string(),
596 "two".to_string(),
597 "three".to_string(),
598 "four".to_string(),
599 "five".to_string(),
600 "six".to_string(),
601 ];
602 cap_details(&mut details, 6);
603 assert_eq!(details.len(), 6);
604 }
605
606 #[test]
607 fn test_cap_details_security_priority() {
608 let mut details = vec![
609 "normal change".to_string(),
610 "security vulnerability fixed".to_string(),
611 "another change".to_string(),
612 "third change".to_string(),
613 "fourth change".to_string(),
614 "fifth change".to_string(),
615 "sixth change".to_string(),
616 ];
617 cap_details(&mut details, 4);
618 assert!(details.iter().any(|d| d.contains("security")));
619 }
620
621 #[test]
622 fn test_cap_details_performance_priority() {
623 let mut details = vec![
624 "normal change".to_string(),
625 "performance optimization added".to_string(),
626 "another change".to_string(),
627 "third change".to_string(),
628 "fourth change".to_string(),
629 "fifth change".to_string(),
630 ];
631 cap_details(&mut details, 3);
632 assert!(details.iter().any(|d| d.contains("performance")));
633 }
634
635 #[test]
636 fn test_cap_details_api_priority() {
637 let mut details = vec![
638 "normal change".to_string(),
639 "API interface updated".to_string(),
640 "internal change".to_string(),
641 "another internal change".to_string(),
642 "yet another change".to_string(),
643 ];
644 cap_details(&mut details, 3);
645 assert!(details.iter().any(|d| d.contains("API")));
646 }
647
648 #[test]
649 fn test_cap_details_preserves_order() {
650 let mut details = vec![
651 "first".to_string(),
652 "critical security fix".to_string(),
653 "third".to_string(),
654 "performance improvement".to_string(),
655 "fifth".to_string(),
656 ];
657 cap_details(&mut details, 3);
658 let security_idx = details.iter().position(|d| d.contains("security"));
660 let perf_idx = details.iter().position(|d| d.contains("performance"));
661 assert!(security_idx.unwrap() < perf_idx.unwrap());
662 }
663
664 #[test]
665 fn test_cap_details_empty_list() {
666 let mut details: Vec<String> = vec![];
667 cap_details(&mut details, 4);
668 assert_eq!(details.len(), 0);
669 }
670
671 #[test]
672 fn test_cap_details_breaking_priority() {
673 let mut details = vec![
674 "normal change".to_string(),
675 "breaking change introduced".to_string(),
676 "another change".to_string(),
677 "third change".to_string(),
678 "fourth change".to_string(),
679 ];
680 cap_details(&mut details, 3);
681 assert!(details.iter().any(|d| d.contains("breaking")));
682 }
683
684 #[test]
686 fn test_format_commit_message_type_summary_only() {
687 let commit = ConventionalCommit {
688 commit_type: CommitType::new("feat").unwrap(),
689 scope: None,
690 summary: CommitSummary::new_unchecked("added new feature", 128).unwrap(),
691 body: vec![],
692 footers: vec![],
693 };
694 assert_eq!(format_commit_message(&commit), "feat: added new feature");
695 }
696
697 #[test]
698 fn test_format_commit_message_with_scope() {
699 let commit = ConventionalCommit {
700 commit_type: CommitType::new("fix").unwrap(),
701 scope: Some(Scope::new("api").unwrap()),
702 summary: CommitSummary::new_unchecked("fixed bug", 128).unwrap(),
703 body: vec![],
704 footers: vec![],
705 };
706 assert_eq!(format_commit_message(&commit), "fix(api): fixed bug");
707 }
708
709 #[test]
710 fn test_format_commit_message_with_body() {
711 let commit = ConventionalCommit {
712 commit_type: CommitType::new("feat").unwrap(),
713 scope: None,
714 summary: CommitSummary::new_unchecked("added feature", 128).unwrap(),
715 body: vec!["First detail.".to_string(), "Second detail.".to_string()],
716 footers: vec![],
717 };
718 let expected = "feat: added feature\n\n- First detail.\n- Second detail.";
719 assert_eq!(format_commit_message(&commit), expected);
720 }
721
722 #[test]
723 fn test_format_commit_message_with_footers() {
724 let commit = ConventionalCommit {
725 commit_type: CommitType::new("fix").unwrap(),
726 scope: None,
727 summary: CommitSummary::new_unchecked("fixed bug", 128).unwrap(),
728 body: vec![],
729 footers: vec!["Closes: #123".to_string(), "Fixes: #456".to_string()],
730 };
731 let expected = "fix: fixed bug\n\nCloses: #123\nFixes: #456";
732 assert_eq!(format_commit_message(&commit), expected);
733 }
734
735 #[test]
736 fn test_format_commit_message_full() {
737 let commit = ConventionalCommit {
738 commit_type: CommitType::new("feat").unwrap(),
739 scope: Some(Scope::new("auth").unwrap()),
740 summary: CommitSummary::new_unchecked("added oauth support", 128).unwrap(),
741 body: vec![
742 "Implemented OAuth2 flow.".to_string(),
743 "Added token refresh.".to_string(),
744 ],
745 footers: vec!["Closes: #789".to_string()],
746 };
747 let expected = "feat(auth): added oauth support\n\n- Implemented OAuth2 flow.\n- Added \
748 token refresh.\n\nCloses: #789";
749 assert_eq!(format_commit_message(&commit), expected);
750 }
751
752 #[test]
753 fn test_format_commit_message_nested_scope() {
754 let commit = ConventionalCommit {
755 commit_type: CommitType::new("refactor").unwrap(),
756 scope: Some(Scope::new("api/client").unwrap()),
757 summary: CommitSummary::new_unchecked("restructured code", 128).unwrap(),
758 body: vec![],
759 footers: vec![],
760 };
761 assert_eq!(format_commit_message(&commit), "refactor(api/client): restructured code");
762 }
763}