1use crate::agent::config;
2use crate::agent::inference::InferenceEvent;
3use serde_json::Value;
4use tokio::sync::mpsc;
5
6const BUILD_TIMEOUT_SECS: u64 = 120;
7
8pub async fn execute_streaming(
11 args: &Value,
12 tx: mpsc::Sender<InferenceEvent>,
13) -> Result<String, String> {
14 let cwd =
15 std::env::current_dir().map_err(|e| format!("Cannot determine working directory: {e}"))?;
16 let action = args
17 .get("action")
18 .and_then(|v| v.as_str())
19 .unwrap_or("build");
20 let explicit_profile = args.get("profile").and_then(|v| v.as_str());
21 let timeout_override = args.get("timeout_secs").and_then(|v| v.as_u64());
22
23 let config = config::load_config();
24 if let Some(profile_name) = explicit_profile {
25 let profile = config.verify.profiles.get(profile_name).ok_or_else(|| {
26 format!(
27 "Unknown verify profile `{}`. Define it in `.hematite/settings.json` or omit the profile argument.",
28 profile_name
29 )
30 })?;
31 if let Some(command) = profile_command(profile, action) {
32 let timeout_secs = timeout_override
33 .or(profile.timeout_secs)
34 .unwrap_or(BUILD_TIMEOUT_SECS);
35 return run_profile_command_streaming(profile_name, action, command, timeout_secs, tx)
36 .await;
37 }
38
39 return Err(format!(
40 "VERIFY PROFILE MISSING [{profile_name}] action `{action}`.\n\
41 Configure `.hematite/settings.json` with a `{action}` command for this profile, \
42 or call `verify_build` with a different action/profile."
43 ));
44 }
45
46 if let Some(default_profile) = config.verify.default_profile.as_deref() {
47 let profile = config.verify.profiles.get(default_profile).ok_or_else(|| {
48 format!(
49 "Configured default verify profile `{}` was not found in `.hematite/settings.json`.",
50 default_profile
51 )
52 })?;
53 if let Some(command) = profile_command(profile, action) {
54 let timeout_secs = timeout_override
55 .or(profile.timeout_secs)
56 .unwrap_or(BUILD_TIMEOUT_SECS);
57 return run_profile_command_streaming(
58 default_profile,
59 action,
60 command,
61 timeout_secs,
62 tx,
63 )
64 .await;
65 }
66
67 return Err(format!(
68 "VERIFY PROFILE MISSING [{default_profile}] action `{action}`.\n\
69 Configure `.hematite/settings.json` with a `{action}` command for the default profile, \
70 or call `verify_build` with an explicit profile."
71 ));
72 }
73
74 let (label, command, timeout_secs) = autodetect_command(&cwd, action, timeout_override)?;
75 run_profile_command_streaming(label, action, &command, timeout_secs, tx).await
76}
77
78pub async fn execute(args: &Value) -> Result<String, String> {
79 let cwd =
80 std::env::current_dir().map_err(|e| format!("Cannot determine working directory: {e}"))?;
81 let action = args
82 .get("action")
83 .and_then(|v| v.as_str())
84 .unwrap_or("build");
85 let explicit_profile = args.get("profile").and_then(|v| v.as_str());
86 let timeout_override = args.get("timeout_secs").and_then(|v| v.as_u64());
87
88 let config = config::load_config();
89 if let Some(profile_name) = explicit_profile {
90 let profile = config.verify.profiles.get(profile_name).ok_or_else(|| {
91 format!(
92 "Unknown verify profile `{}`. Define it in `.hematite/settings.json` or omit the profile argument.",
93 profile_name
94 )
95 })?;
96 if let Some(command) = profile_command(profile, action) {
97 let timeout_secs = timeout_override
98 .or(profile.timeout_secs)
99 .unwrap_or(BUILD_TIMEOUT_SECS);
100 return run_profile_command(profile_name, action, command, timeout_secs).await;
101 }
102
103 return Err(format!(
104 "VERIFY PROFILE MISSING [{profile_name}] action `{action}`.\n\
105 Configure `.hematite/settings.json` with a `{action}` command for this profile, \
106 or call `verify_build` with a different action/profile."
107 ));
108 }
109
110 if let Some(default_profile) = config.verify.default_profile.as_deref() {
111 let profile = config.verify.profiles.get(default_profile).ok_or_else(|| {
112 format!(
113 "Configured default verify profile `{}` was not found in `.hematite/settings.json`.",
114 default_profile
115 )
116 })?;
117 if let Some(command) = profile_command(profile, action) {
118 let timeout_secs = timeout_override
119 .or(profile.timeout_secs)
120 .unwrap_or(BUILD_TIMEOUT_SECS);
121 return run_profile_command(default_profile, action, command, timeout_secs).await;
122 }
123
124 return Err(format!(
125 "VERIFY PROFILE MISSING [{default_profile}] action `{action}`.\n\
126 Configure `.hematite/settings.json` with a `{action}` command for the default profile, \
127 or call `verify_build` with an explicit profile."
128 ));
129 }
130
131 let (label, command, timeout_secs) = autodetect_command(&cwd, action, timeout_override)?;
132 run_profile_command(label, action, &command, timeout_secs).await
133}
134
135fn profile_command<'a>(profile: &'a config::VerifyProfile, action: &str) -> Option<&'a str> {
136 match action {
137 "build" => profile.build.as_deref(),
138 "test" => profile.test.as_deref(),
139 "lint" => profile.lint.as_deref(),
140 "fix" => profile.fix.as_deref(),
141 _ => None,
142 }
143}
144
145fn autodetect_command(
146 cwd: &std::path::Path,
147 action: &str,
148 timeout_override: Option<u64>,
149) -> Result<(&'static str, String, u64), String> {
150 let timeout_secs = timeout_override.unwrap_or(BUILD_TIMEOUT_SECS);
151 let command = if cwd.join("Cargo.toml").exists() {
152 match action {
153 "build" => ("Rust/Cargo", "cargo build --color never".to_string()),
154 "test" => ("Rust/Cargo", "cargo test --color never".to_string()),
155 "lint" => (
156 "Rust/Cargo",
157 "cargo clippy --all-targets --all-features -- -D warnings".to_string(),
158 ),
159 "fix" => ("Rust/Cargo", "cargo fmt".to_string()),
160 _ => return Err(unknown_action(action)),
161 }
162 } else if cwd.join("go.mod").exists() {
163 match action {
164 "build" => ("Go", "go build ./...".to_string()),
165 "test" => ("Go", "go test ./...".to_string()),
166 "lint" => ("Go", "go vet ./...".to_string()),
167 "fix" => ("Go", "gofmt -w .".to_string()),
168 _ => return Err(unknown_action(action)),
169 }
170 } else if cwd.join("CMakeLists.txt").exists() {
171 let build_dir = if cwd.join("build").exists() {
173 "build"
174 } else {
175 "build"
176 };
177 match action {
178 "build" => (
179 "C++/CMake",
180 format!("cmake -B {build_dir} -DCMAKE_BUILD_TYPE=Release && cmake --build {build_dir} --parallel"),
181 ),
182 "test" => (
183 "C++/CMake",
184 format!("ctest --test-dir {build_dir} --output-on-failure"),
185 ),
186 "lint" => return Err(missing_profile_msg("C++/CMake", action)),
187 "fix" => return Err(missing_profile_msg("C++/CMake", action)),
188 _ => return Err(unknown_action(action)),
189 }
190 } else if cwd.join("package.json").exists() {
191 let pm = if cwd.join("pnpm-lock.yaml").exists()
193 || cwd.join(".npmrc").exists() && {
194 let rc = std::fs::read_to_string(cwd.join(".npmrc")).unwrap_or_default();
195 rc.contains("pnpm")
196 } {
197 "pnpm"
198 } else if cwd.join("yarn.lock").exists() {
199 "yarn"
200 } else if cwd.join("bun.lockb").exists() {
201 "bun"
202 } else {
203 "npm"
204 };
205 let label: &'static str = if cwd.join("tsconfig.json").exists() {
207 match pm {
208 "pnpm" => "TypeScript/pnpm",
209 "yarn" => "TypeScript/yarn",
210 "bun" => "TypeScript/bun",
211 _ => "TypeScript/npm",
212 }
213 } else {
214 match pm {
215 "pnpm" => "Node/pnpm",
216 "yarn" => "Node/yarn",
217 "bun" => "Node/bun",
218 _ => "Node/npm",
219 }
220 };
221 match action {
222 "build" => (label, format!("{pm} run build")),
223 "test" => (label, format!("{pm} test")),
224 "lint" => (label, format!("{pm} run lint")),
225 "fix" => (label, format!("{pm} run format")),
226 _ => return Err(unknown_action(action)),
227 }
228 } else if cwd.join("pyproject.toml").exists()
229 || cwd.join("setup.py").exists()
230 || cwd.join("requirements.txt").exists()
231 || cwd.join(".venv").is_dir()
232 || cwd.join("venv").is_dir()
233 || cwd.join("env").is_dir()
234 {
235 let py = resolve_python_cmd(cwd);
238 match action {
239 "build" => ("Python", format!("{py} -m compileall -q .")),
240 "test" => ("Python", format!("{py} -m pytest -q")),
241 "lint" => (
242 "Python",
243 format!("{py} -m ruff check . || {py} -m flake8 ."),
244 ),
245 "fix" => (
246 "Python",
247 format!("{py} -m ruff format . || {py} -m black ."),
248 ),
249 _ => return Err(unknown_action(action)),
250 }
251 } else if cwd.join("tsconfig.json").exists() {
252 match action {
254 "build" => ("TypeScript/tsc", "tsc --noEmit".to_string()),
255 "test" => return Err(missing_profile_msg("TypeScript/tsc", action)),
256 "lint" => return Err(missing_profile_msg("TypeScript/tsc", action)),
257 "fix" => return Err(missing_profile_msg("TypeScript/tsc", action)),
258 _ => return Err(unknown_action(action)),
259 }
260 } else if cwd.join("index.html").exists() {
261 match action {
262 "build" => ("Static Web", "echo \"BUILD OK (Static assets ready)\"".to_string()),
263 "test" => (
264 "Static Web",
265 "echo \"TEST OK (No test runner found; manual visual check and link verification suggested)\"".to_string(),
266 ),
267 "lint" => ("Static Web", "echo \"LINT OK (Basic structure verified)\"".to_string()),
268 "fix" => ("Static Web", "echo \"FIX OK (No auto-formatter found for static assets)\"".to_string()),
269 _ => return Err(unknown_action(action)),
270 }
271 } else {
272 return Err(format!(
273 "No recognized project root (Cargo.toml, package.json, go.mod, CMakeLists.txt, pyproject.toml, etc.) \
274 found in {}.\nUse an explicit profile or configure a default verify profile in `.hematite/settings.json`.",
275 cwd.display()
276 ));
277 };
278
279 Ok((command.0, command.1, timeout_secs))
280}
281
282fn resolve_python_cmd(cwd: &std::path::Path) -> String {
283 if cwd.join("poetry.lock").exists() {
285 return "poetry run python".to_string();
286 }
287 if cwd.join("Pipfile.lock").exists() || cwd.join("Pipfile").exists() {
289 return "pipenv run python".to_string();
290 }
291 let venv_folders = [".venv", "venv", "env"];
293 for folder in venv_folders {
294 if cwd.join(folder).is_dir() {
295 let rel_path = if cfg!(windows) {
296 format!("{}\\Scripts\\python.exe", folder)
297 } else {
298 format!("{}/bin/python", folder)
299 };
300 if cwd.join(&rel_path).exists() {
301 return format!(".{}{}", if cfg!(windows) { "\\" } else { "/" }, rel_path);
302 }
303 }
304 }
305
306 "python".to_string()
307}
308
309fn missing_profile_msg(stack: &str, action: &str) -> String {
310 format!(
311 "No auto-detected `{action}` command for [{stack}].\n\
312 Add a verify profile in `.hematite/settings.json` if you want Hematite to run `{action}` for this project."
313 )
314}
315
316fn unknown_action(action: &str) -> String {
317 format!(
318 "Unknown verify_build action `{}`. Use one of: build, test, lint, fix.",
319 action
320 )
321}
322
323async fn run_profile_command(
324 profile_name: &str,
325 action: &str,
326 command: &str,
327 timeout_secs: u64,
328) -> Result<String, String> {
329 let output = crate::tools::shell::execute(
330 &serde_json::json!({
331 "command": command,
332 "timeout_secs": timeout_secs,
333 "reason": format!("verify_build:{}:{}", profile_name, action),
334 }),
335 16384,
336 )
337 .await?;
338
339 if output.contains("[exit code: 0]") || !output.contains("[exit code:") {
340 Ok(format!(
341 "BUILD OK [{}:{}]\ncommand: {}\n{}",
342 profile_name,
343 action,
344 command,
345 output.trim()
346 ))
347 } else if should_fallback_to_cargo_check(action, command, &output) {
348 run_windows_self_hosted_check_fallback(profile_name, action, command, timeout_secs, &output)
349 .await
350 } else {
351 Err(format!(
352 "BUILD FAILED [{}:{}]\ncommand: {}\n{}",
353 profile_name,
354 action,
355 command,
356 output.trim()
357 ))
358 }
359}
360
361async fn run_profile_command_streaming(
362 profile_name: &str,
363 action: &str,
364 command: &str,
365 timeout_secs: u64,
366 tx: mpsc::Sender<InferenceEvent>,
367) -> Result<String, String> {
368 let output = crate::tools::shell::execute_streaming(
369 &serde_json::json!({
370 "command": command,
371 "timeout_secs": timeout_secs,
372 "reason": format!("verify_build:{}:{}", profile_name, action),
373 }),
374 tx.clone(),
375 16384,
376 )
377 .await?;
378
379 if output.contains("[exit code: 0]") || !output.contains("[exit code:") {
380 Ok(format!(
381 "BUILD OK [{}:{}]\ncommand: {}\n{}",
382 profile_name,
383 action,
384 command,
385 output.trim()
386 ))
387 } else if should_fallback_to_cargo_check(action, command, &output) {
388 run_windows_self_hosted_check_fallback_streaming(
389 profile_name,
390 action,
391 command,
392 timeout_secs,
393 &output,
394 tx,
395 )
396 .await
397 } else {
398 Err(format!(
399 "BUILD FAILED [{}:{}]\ncommand: {}\n{}",
400 profile_name,
401 action,
402 command,
403 output.trim()
404 ))
405 }
406}
407
408async fn run_windows_self_hosted_check_fallback_streaming(
409 profile_name: &str,
410 action: &str,
411 original_command: &str,
412 timeout_secs: u64,
413 original_output: &str,
414 tx: mpsc::Sender<InferenceEvent>,
415) -> Result<String, String> {
416 let fallback_command = "cargo check --color never";
417 let fallback_output = crate::tools::shell::execute_streaming(
418 &serde_json::json!({
419 "command": fallback_command,
420 "timeout_secs": timeout_secs,
421 "reason": format!("verify_build:{}:{}:self_hosted_windows_fallback", profile_name, action),
422 }),
423 tx,
424 16384,
425 )
426 .await?;
427
428 if fallback_output.contains("[exit code: 0]") || !fallback_output.contains("[exit code:") {
429 Ok(format!(
430 "BUILD OK [{}:{}]\ncommand: {}\n\
431 Windows self-hosted note: `cargo build` could not replace the running `target\\\\debug\\\\hematite.exe`, so Hematite fell back to `cargo check` to verify code health without deleting the live binary.\n\
432 original build output:\n{}\n\
433 fallback command: {}\n{}",
434 profile_name,
435 action,
436 original_command,
437 original_output.trim(),
438 fallback_command,
439 fallback_output.trim()
440 ))
441 } else {
442 Err(format!(
443 "BUILD FAILED [{}:{}]\ncommand: {}\n\
444 Windows self-hosted note: `cargo build` could not replace the running `target\\\\debug\\\\hematite.exe`, and the fallback `cargo check` also failed.\n\
445 original build output:\n{}\n\
446 fallback command: {}\n{}",
447 profile_name,
448 action,
449 original_command,
450 original_output.trim(),
451 fallback_command,
452 fallback_output.trim()
453 ))
454 }
455}
456
457fn should_fallback_to_cargo_check(action: &str, command: &str, output: &str) -> bool {
458 if action != "build" || command.trim() != "cargo build --color never" {
459 return false;
460 }
461
462 if cfg!(windows) {
463 looks_like_windows_self_hosted_build_lock(output)
464 } else {
465 false
466 }
467}
468
469fn looks_like_windows_self_hosted_build_lock(output: &str) -> bool {
470 let lower = output.to_ascii_lowercase();
471 lower.contains("failed to remove file")
472 && lower.contains("target\\debug\\hematite.exe")
473 && (lower.contains("access is denied")
474 || lower.contains("being used by another process")
475 || lower.contains("permission denied"))
476}
477
478async fn run_windows_self_hosted_check_fallback(
479 profile_name: &str,
480 action: &str,
481 original_command: &str,
482 timeout_secs: u64,
483 original_output: &str,
484) -> Result<String, String> {
485 let fallback_command = "cargo check --color never";
486 let fallback_output = crate::tools::shell::execute(&serde_json::json!({
487 "command": fallback_command,
488 "timeout_secs": timeout_secs,
489 "reason": format!("verify_build:{}:{}:self_hosted_windows_fallback", profile_name, action),
490 }), 16384)
491 .await?;
492
493 if fallback_output.contains("[exit code: 0]") || !fallback_output.contains("[exit code:") {
494 Ok(format!(
495 "BUILD OK [{}:{}]\ncommand: {}\n\
496 Windows self-hosted note: `cargo build` could not replace the running `target\\\\debug\\\\hematite.exe`, so Hematite fell back to `cargo check` to verify code health without deleting the live binary.\n\
497 original build output:\n{}\n\
498 fallback command: {}\n{}",
499 profile_name,
500 action,
501 original_command,
502 original_output.trim(),
503 fallback_command,
504 fallback_output.trim()
505 ))
506 } else {
507 Err(format!(
508 "BUILD FAILED [{}:{}]\ncommand: {}\n\
509 Windows self-hosted note: `cargo build` could not replace the running `target\\\\debug\\\\hematite.exe`, and the fallback `cargo check` also failed.\n\
510 original build output:\n{}\n\
511 fallback command: {}\n{}",
512 profile_name,
513 action,
514 original_command,
515 original_output.trim(),
516 fallback_command,
517 fallback_output.trim()
518 ))
519 }
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525
526 #[test]
527 fn detects_windows_self_hosted_build_lock_pattern() {
528 let sample = "[stderr] error: failed to remove file `C:\\Users\\ocean\\AntigravityProjects\\Hematite-CLI\\target\\debug\\hematite.exe`\r\nAccess is denied. (os error 5)";
529 assert!(looks_like_windows_self_hosted_build_lock(sample));
530 }
531
532 #[test]
533 fn ignores_unrelated_build_failures() {
534 let sample = "[stderr] error[E0425]: cannot find value `foo` in this scope";
535 assert!(!looks_like_windows_self_hosted_build_lock(sample));
536 assert!(!should_fallback_to_cargo_check(
537 "build",
538 "cargo build --color never",
539 sample
540 ));
541 }
542
543 #[test]
544 fn autodetect_rust_stack() {
545 let dir = tempfile::tempdir().unwrap();
546 std::fs::write(dir.path().join("Cargo.toml"), "").unwrap();
547 let (label, cmd, _) = autodetect_command(dir.path(), "build", None).unwrap();
548 assert_eq!(label, "Rust/Cargo");
549 assert!(cmd.contains("cargo build"));
550 let (_, test_cmd, _) = autodetect_command(dir.path(), "test", None).unwrap();
551 assert!(test_cmd.contains("cargo test"));
552 let (_, lint_cmd, _) = autodetect_command(dir.path(), "lint", None).unwrap();
553 assert!(lint_cmd.contains("clippy"));
554 }
555
556 #[test]
557 fn autodetect_go_stack() {
558 let dir = tempfile::tempdir().unwrap();
559 std::fs::write(
560 dir.path().join("go.mod"),
561 "module example.com/foo\ngo 1.21\n",
562 )
563 .unwrap();
564 let (label, cmd, _) = autodetect_command(dir.path(), "build", None).unwrap();
565 assert_eq!(label, "Go");
566 assert!(cmd.contains("go build"));
567 let (_, test_cmd, _) = autodetect_command(dir.path(), "test", None).unwrap();
568 assert!(test_cmd.contains("go test"));
569 let (_, lint_cmd, _) = autodetect_command(dir.path(), "lint", None).unwrap();
570 assert!(lint_cmd.contains("go vet"));
571 }
572
573 #[test]
574 fn autodetect_cmake_stack() {
575 let dir = tempfile::tempdir().unwrap();
576 std::fs::write(
577 dir.path().join("CMakeLists.txt"),
578 "cmake_minimum_required(VERSION 3.20)\n",
579 )
580 .unwrap();
581 let (label, cmd, _) = autodetect_command(dir.path(), "build", None).unwrap();
582 assert_eq!(label, "C++/CMake");
583 assert!(cmd.contains("cmake"));
584 assert!(cmd.contains("--build"));
585 }
586
587 #[test]
588 fn autodetect_node_npm_stack() {
589 let dir = tempfile::tempdir().unwrap();
590 std::fs::write(dir.path().join("package.json"), "{}").unwrap();
591 let (label, cmd, _) = autodetect_command(dir.path(), "build", None).unwrap();
592 assert!(label.contains("Node") || label.contains("TypeScript"));
593 assert!(cmd.contains("npm run build"));
594 }
595
596 #[test]
597 fn autodetect_node_yarn_stack() {
598 let dir = tempfile::tempdir().unwrap();
599 std::fs::write(dir.path().join("package.json"), "{}").unwrap();
600 std::fs::write(dir.path().join("yarn.lock"), "").unwrap();
601 let (label, cmd, _) = autodetect_command(dir.path(), "build", None).unwrap();
602 assert!(label.contains("yarn"));
603 assert!(cmd.contains("yarn run build"));
604 }
605
606 #[test]
607 fn autodetect_node_pnpm_stack() {
608 let dir = tempfile::tempdir().unwrap();
609 std::fs::write(dir.path().join("package.json"), "{}").unwrap();
610 std::fs::write(dir.path().join("pnpm-lock.yaml"), "").unwrap();
611 let (label, cmd, _) = autodetect_command(dir.path(), "build", None).unwrap();
612 assert!(label.contains("pnpm"));
613 assert!(cmd.contains("pnpm run build"));
614 }
615
616 #[test]
617 fn autodetect_python_stack_pyproject() {
618 let dir = tempfile::tempdir().unwrap();
619 std::fs::write(dir.path().join("pyproject.toml"), "[build-system]\n").unwrap();
620 let (label, cmd, _) = autodetect_command(dir.path(), "build", None).unwrap();
621 assert_eq!(label, "Python");
622 assert!(cmd.contains("compileall"));
623 let (_, test_cmd, _) = autodetect_command(dir.path(), "test", None).unwrap();
624 assert!(test_cmd.contains("pytest"));
625 }
626
627 #[test]
628 fn autodetect_python_stack_requirements() {
629 let dir = tempfile::tempdir().unwrap();
630 std::fs::write(dir.path().join("requirements.txt"), "fastapi\n").unwrap();
631 let (label, _, _) = autodetect_command(dir.path(), "build", None).unwrap();
632 assert_eq!(label, "Python");
633 }
634
635 #[test]
636 fn resolves_local_venv_python() {
637 let dir = tempfile::tempdir().unwrap();
638 let venv = dir.path().join(".venv");
639 std::fs::create_dir(&venv).unwrap();
640
641 let bin_sub = if cfg!(windows) { "Scripts" } else { "bin" };
643 let exe_name = if cfg!(windows) {
644 "python.exe"
645 } else {
646 "python"
647 };
648 let bin_dir = venv.join(bin_sub);
649 std::fs::create_dir(&bin_dir).unwrap();
650 std::fs::write(bin_dir.join(exe_name), "").unwrap();
651
652 let cmd = resolve_python_cmd(dir.path());
653 assert!(cmd.contains(".venv"));
654 assert!(cmd.contains(bin_sub));
655 }
656
657 #[test]
658 fn resolves_poetry_run() {
659 let dir = tempfile::tempdir().unwrap();
660 std::fs::write(dir.path().join("poetry.lock"), "").unwrap();
661 let cmd = resolve_python_cmd(dir.path());
662 assert_eq!(cmd, "poetry run python");
663 }
664
665 #[test]
666 fn autodetect_no_project_returns_err() {
667 let dir = tempfile::tempdir().unwrap();
668 let result = autodetect_command(dir.path(), "build", None);
669 assert!(result.is_err());
670 let msg = result.unwrap_err();
671 assert!(msg.contains("No recognized project root"));
672 assert!(msg.contains("Cargo.toml"));
673 assert!(msg.contains("CMakeLists.txt"));
674 }
675}