@lamentis/naome 1.3.12 → 1.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/Cargo.lock +2 -2
  2. package/crates/naome-cli/Cargo.toml +1 -1
  3. package/crates/naome-core/Cargo.toml +1 -1
  4. package/crates/naome-core/src/architecture/rules/external.rs +59 -0
  5. package/crates/naome-core/src/architecture/scan/graph_builder.rs +130 -30
  6. package/crates/naome-core/src/architecture/scan/imports/extractors/swift.rs +48 -0
  7. package/crates/naome-core/src/architecture/scan/imports/extractors.rs +3 -0
  8. package/crates/naome-core/src/architecture/scan/imports/resolver.rs +41 -1
  9. package/crates/naome-core/src/architecture/scan/imports.rs +1 -0
  10. package/crates/naome-core/src/architecture/scan/manifest/common.rs +102 -0
  11. package/crates/naome-core/src/architecture/scan/manifest/parsers/json.rs +46 -0
  12. package/crates/naome-core/src/architecture/scan/manifest/parsers/other.rs +280 -0
  13. package/crates/naome-core/src/architecture/scan/manifest/parsers/toml.rs +184 -0
  14. package/crates/naome-core/src/architecture/scan/manifest/parsers.rs +3 -0
  15. package/crates/naome-core/src/architecture/scan/manifest.rs +33 -0
  16. package/crates/naome-core/src/architecture/scan.rs +27 -1
  17. package/crates/naome-core/tests/architecture_manifests.rs +289 -0
  18. package/crates/naome-core/tests/architecture_support/mod.rs +2 -0
  19. package/crates/naome-core/tests/architecture_swift.rs +111 -0
  20. package/native/darwin-arm64/naome +0 -0
  21. package/native/linux-x64/naome +0 -0
  22. package/package.json +1 -1
  23. package/templates/naome-root/.naome/manifest.json +2 -2
  24. package/templates/naome-root/docs/naome/architecture-fitness.md +17 -7
package/Cargo.lock CHANGED
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
76
76
 
77
77
  [[package]]
78
78
  name = "naome-cli"
79
- version = "1.3.12"
79
+ version = "1.3.13"
80
80
  dependencies = [
81
81
  "naome-core",
82
82
  "serde_json",
@@ -84,7 +84,7 @@ dependencies = [
84
84
 
85
85
  [[package]]
86
86
  name = "naome-core"
87
- version = "1.3.12"
87
+ version = "1.3.13"
88
88
  dependencies = [
89
89
  "serde",
90
90
  "serde_json",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-cli"
3
- version = "1.3.12"
3
+ version = "1.3.13"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-core"
3
- version = "1.3.12"
3
+ version = "1.3.13"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -73,6 +73,7 @@ fn is_standard_library_dependency(language: Option<&str>, package: &str) -> bool
73
73
  Some("javascript") | Some("typescript") => NODE_BUILTINS.contains(&root),
74
74
  Some("python") => PYTHON_STDLIB.contains(&root),
75
75
  Some("rust") => matches!(root, "std" | "core" | "alloc" | "proc_macro" | "test"),
76
+ Some("swift") => SWIFT_APPLE_FRAMEWORKS.contains(&root),
76
77
  _ => false,
77
78
  }
78
79
  }
@@ -183,3 +184,61 @@ const PYTHON_STDLIB: &[&str] = &[
183
184
  "xml",
184
185
  "zipfile",
185
186
  ];
187
+
188
+ const SWIFT_APPLE_FRAMEWORKS: &[&str] = &[
189
+ "Accelerate",
190
+ "AppIntents",
191
+ "AppKit",
192
+ "ARKit",
193
+ "AVFoundation",
194
+ "CloudKit",
195
+ "Combine",
196
+ "CoreData",
197
+ "CoreFoundation",
198
+ "CoreBluetooth",
199
+ "CoreGraphics",
200
+ "CoreImage",
201
+ "CoreLocation",
202
+ "CoreML",
203
+ "CoreMotion",
204
+ "CoreNFC",
205
+ "CoreSpotlight",
206
+ "CoreTelephony",
207
+ "CoreText",
208
+ "CryptoKit",
209
+ "Dispatch",
210
+ "EventKit",
211
+ "Foundation",
212
+ "GameKit",
213
+ "HealthKit",
214
+ "LocalAuthentication",
215
+ "MapKit",
216
+ "MessageUI",
217
+ "Metal",
218
+ "MetalKit",
219
+ "NaturalLanguage",
220
+ "Network",
221
+ "Observation",
222
+ "PassKit",
223
+ "PencilKit",
224
+ "Photos",
225
+ "QuartzCore",
226
+ "RealityKit",
227
+ "SafariServices",
228
+ "SceneKit",
229
+ "Security",
230
+ "SpriteKit",
231
+ "StoreKit",
232
+ "Swift",
233
+ "SwiftData",
234
+ "SwiftUI",
235
+ "ThreadNetwork",
236
+ "UIKit",
237
+ "UniformTypeIdentifiers",
238
+ "UserNotifications",
239
+ "Vision",
240
+ "WatchKit",
241
+ "WebKit",
242
+ "WidgetKit",
243
+ "XCTest",
244
+ ];
@@ -5,7 +5,7 @@ use std::path::Path;
5
5
  use serde_json::json;
6
6
 
7
7
  use super::imports;
8
- use super::FileFact;
8
+ use super::{FileFact, ManifestFact};
9
9
  use crate::architecture::config::ArchitectureConfig;
10
10
  use crate::architecture::model::{ArchitectureEdgeKind, ArchitectureGraph, ArchitectureNodeKind};
11
11
 
@@ -16,13 +16,16 @@ pub(super) fn build_path_graph(
16
16
  root: &Path,
17
17
  files: Vec<String>,
18
18
  config: &ArchitectureConfig,
19
+ manifests: &[ManifestFact],
19
20
  ) -> (ArchitectureGraph, BTreeMap<String, FileFact>) {
20
21
  let mut graph = ArchitectureGraph::default();
21
22
  let mut file_facts = BTreeMap::new();
23
+ let mut emitted_external_nodes = BTreeSet::new();
22
24
  let file_set = files.iter().cloned().collect::<BTreeSet<_>>();
23
25
 
24
26
  push_repository_and_policy_nodes(&mut graph, config);
25
27
  push_directories(&mut graph, &files);
28
+ push_manifests(&mut graph, manifests, &mut emitted_external_nodes);
26
29
 
27
30
  for path in files {
28
31
  let content = fs::read_to_string(root.join(&path)).unwrap_or_default();
@@ -32,7 +35,7 @@ pub(super) fn build_path_graph(
32
35
  file_facts.insert(path, fact);
33
36
  }
34
37
 
35
- push_imports(&mut graph, &file_facts);
38
+ push_imports(&mut graph, &file_facts, &mut emitted_external_nodes);
36
39
  (graph, file_facts)
37
40
  }
38
41
 
@@ -121,44 +124,117 @@ fn push_file(graph: &mut ArchitectureGraph, fact: &FileFact) {
121
124
  push_membership_edges(graph, path, "context", &fact.contexts);
122
125
  }
123
126
 
124
- fn push_imports(graph: &mut ArchitectureGraph, file_facts: &BTreeMap<String, FileFact>) {
125
- let mut emitted_target_nodes = BTreeSet::new();
127
+ fn push_manifests(
128
+ graph: &mut ArchitectureGraph,
129
+ manifests: &[ManifestFact],
130
+ emitted_external_nodes: &mut BTreeSet<String>,
131
+ ) {
132
+ for manifest in manifests {
133
+ let package_id = manifest_package_id(manifest);
134
+ emit::push_node_with_metadata(
135
+ graph,
136
+ &package_id,
137
+ ArchitectureNodeKind::Package,
138
+ &manifest.package_name,
139
+ Some(manifest.path.clone()),
140
+ None,
141
+ manifest.confidence,
142
+ &manifest.extractor,
143
+ json!({
144
+ "path": manifest.path,
145
+ "ecosystem": manifest.ecosystem,
146
+ "packageName": manifest.package_name
147
+ }),
148
+ );
149
+ emit::push_edge(
150
+ graph,
151
+ facts::parent_node_id(&manifest.path)
152
+ .as_deref()
153
+ .unwrap_or("repository:."),
154
+ &package_id,
155
+ ArchitectureEdgeKind::Contains,
156
+ "contains",
157
+ Some(manifest.path.clone()),
158
+ );
159
+ for dependency in &manifest.dependencies {
160
+ let external_id = format!("external:{}", dependency.name);
161
+ push_external_node(
162
+ graph,
163
+ emitted_external_nodes,
164
+ &external_id,
165
+ &dependency.name,
166
+ dependency.confidence,
167
+ &manifest.extractor,
168
+ json!({
169
+ "manifestPath": manifest.path,
170
+ "ecosystem": manifest.ecosystem,
171
+ "dependencyKind": dependency.dependency_kind,
172
+ "version": dependency.version
173
+ }),
174
+ );
175
+ emit::push_edge_with_metadata(
176
+ graph,
177
+ &package_id,
178
+ &external_id,
179
+ ArchitectureEdgeKind::DependsOn,
180
+ "depends_on",
181
+ Some(manifest.path.clone()),
182
+ None,
183
+ None,
184
+ dependency.confidence,
185
+ &manifest.extractor,
186
+ json!({
187
+ "specifier": dependency.name,
188
+ "manifestPath": manifest.path,
189
+ "dependencyKind": dependency.dependency_kind,
190
+ "version": dependency.version
191
+ }),
192
+ );
193
+ }
194
+ }
195
+ }
196
+
197
+ fn manifest_package_id(manifest: &ManifestFact) -> String {
198
+ format!(
199
+ "package:{}:{}",
200
+ stable_fragment(&manifest.ecosystem),
201
+ stable_fragment(&manifest.path)
202
+ )
203
+ }
204
+
205
+ fn push_imports(
206
+ graph: &mut ArchitectureGraph,
207
+ file_facts: &BTreeMap<String, FileFact>,
208
+ emitted_external_nodes: &mut BTreeSet<String>,
209
+ ) {
126
210
  for fact in file_facts.values() {
127
211
  for import in &fact.imports {
128
212
  let target_id = match &import.target {
129
213
  super::ImportTarget::File(path) => format!("file:{path}"),
130
214
  super::ImportTarget::ExternalDependency(name) => {
131
215
  let id = format!("external:{name}");
132
- if emitted_target_nodes.insert(id.clone()) {
133
- emit::push_node_with_metadata(
134
- graph,
135
- &id,
136
- ArchitectureNodeKind::ExternalDependency,
137
- name,
138
- None,
139
- None,
140
- import.confidence,
141
- &import.extractor,
142
- json!({ "specifier": import.specifier, "resolvedAs": "external" }),
143
- );
144
- }
216
+ push_external_node(
217
+ graph,
218
+ emitted_external_nodes,
219
+ &id,
220
+ name,
221
+ import.confidence,
222
+ &import.extractor,
223
+ json!({ "specifier": import.specifier, "resolvedAs": "external" }),
224
+ );
145
225
  id
146
226
  }
147
227
  super::ImportTarget::Unknown(specifier) => {
148
228
  let id = format!("unknown-import:{}", stable_fragment(specifier));
149
- if emitted_target_nodes.insert(id.clone()) {
150
- emit::push_node_with_metadata(
151
- graph,
152
- &id,
153
- ArchitectureNodeKind::ExternalDependency,
154
- specifier,
155
- None,
156
- None,
157
- import.confidence,
158
- &import.extractor,
159
- json!({ "specifier": specifier, "resolvedAs": "unknown" }),
160
- );
161
- }
229
+ push_external_node(
230
+ graph,
231
+ emitted_external_nodes,
232
+ &id,
233
+ specifier,
234
+ import.confidence,
235
+ &import.extractor,
236
+ json!({ "specifier": specifier, "resolvedAs": "unknown" }),
237
+ );
162
238
  id
163
239
  }
164
240
  };
@@ -179,6 +255,30 @@ fn push_imports(graph: &mut ArchitectureGraph, file_facts: &BTreeMap<String, Fil
179
255
  }
180
256
  }
181
257
 
258
+ fn push_external_node(
259
+ graph: &mut ArchitectureGraph,
260
+ emitted_external_nodes: &mut BTreeSet<String>,
261
+ id: &str,
262
+ label: &str,
263
+ confidence: f32,
264
+ extractor: &str,
265
+ raw_origin: serde_json::Value,
266
+ ) {
267
+ if emitted_external_nodes.insert(id.to_string()) {
268
+ emit::push_node_with_metadata(
269
+ graph,
270
+ id,
271
+ ArchitectureNodeKind::ExternalDependency,
272
+ label,
273
+ None,
274
+ None,
275
+ confidence,
276
+ extractor,
277
+ raw_origin,
278
+ );
279
+ }
280
+ }
281
+
182
282
  fn push_membership_edges(
183
283
  graph: &mut ArchitectureGraph,
184
284
  path: &str,
@@ -0,0 +1,48 @@
1
+ use super::{is_comment_only, raw, RawImport};
2
+
3
+ pub(super) fn extract(content: &str) -> Vec<RawImport> {
4
+ let mut imports = Vec::new();
5
+ for (index, line) in content.lines().enumerate() {
6
+ let trimmed = line.trim();
7
+ if is_comment_only(trimmed) {
8
+ continue;
9
+ }
10
+ let Some(import_value) = swift_import_value(trimmed) else {
11
+ continue;
12
+ };
13
+ if let Some(specifier) = swift_import_specifier(import_value) {
14
+ imports.push(raw(specifier, index, line, 0.88, "import:swift"));
15
+ }
16
+ }
17
+ imports
18
+ }
19
+
20
+ fn swift_import_value(line: &str) -> Option<&str> {
21
+ let mut remaining = line.trim();
22
+ while remaining.starts_with('@') {
23
+ let Some((_, after_attribute)) = remaining.split_once(' ') else {
24
+ return None;
25
+ };
26
+ remaining = after_attribute.trim_start();
27
+ }
28
+ remaining.strip_prefix("import ")
29
+ }
30
+
31
+ fn swift_import_specifier(value: &str) -> Option<String> {
32
+ let mut parts = value.split_whitespace();
33
+ let first = parts.next()?;
34
+ let module = if matches!(
35
+ first,
36
+ "class" | "struct" | "enum" | "protocol" | "func" | "var" | "typealias"
37
+ ) {
38
+ parts.next()?
39
+ } else {
40
+ first
41
+ };
42
+ module
43
+ .split('.')
44
+ .next()
45
+ .map(str::trim)
46
+ .filter(|value| !value.is_empty())
47
+ .map(ToString::to_string)
48
+ }
@@ -1,6 +1,8 @@
1
1
  use super::language_for_path;
2
2
  use crate::architecture::model::SourceRange;
3
3
 
4
+ mod swift;
5
+
4
6
  #[derive(Debug, Clone)]
5
7
  pub(super) struct RawImport {
6
8
  pub specifier: String,
@@ -15,6 +17,7 @@ pub(super) fn extract_raw_imports(path: &str, content: &str) -> Vec<RawImport> {
15
17
  Some("rust") => extract_rust(content),
16
18
  Some("python") => extract_python(content),
17
19
  Some("go") => extract_go(content),
20
+ Some("swift") => swift::extract(content),
18
21
  _ => Vec::new(),
19
22
  }
20
23
  }
@@ -35,6 +35,11 @@ pub(super) fn resolve_import(
35
35
  return ImportTarget::File(path);
36
36
  }
37
37
  }
38
+ if from_path.ends_with(".swift") {
39
+ if let Some(path) = resolve_swift_module_import(from_path, specifier, repository_files) {
40
+ return ImportTarget::File(path);
41
+ }
42
+ }
38
43
  external_target(specifier)
39
44
  }
40
45
 
@@ -225,6 +230,40 @@ fn go_mod_module(root: &Path, go_mod_path: &str) -> Option<String> {
225
230
  })
226
231
  }
227
232
 
233
+ fn resolve_swift_module_import(
234
+ from_path: &str,
235
+ specifier: &str,
236
+ repository_files: &BTreeSet<String>,
237
+ ) -> Option<String> {
238
+ if specifier.contains('/') || specifier.contains('.') {
239
+ return None;
240
+ }
241
+ let sources_root = swift_sources_root(from_path)?;
242
+ let target_prefix = format!("{sources_root}{specifier}/");
243
+ let conventional_file = format!("{target_prefix}{specifier}.swift");
244
+ if repository_files.contains(&conventional_file) {
245
+ return Some(conventional_file);
246
+ }
247
+ repository_files
248
+ .iter()
249
+ .find(|path| path.starts_with(&target_prefix) && path.ends_with(".swift"))
250
+ .cloned()
251
+ }
252
+
253
+ fn swift_sources_root(from_path: &str) -> Option<String> {
254
+ if from_path.starts_with("Sources/") {
255
+ return Some("Sources/".to_string());
256
+ }
257
+ if from_path.starts_with("Tests/") {
258
+ return Some("Sources/".to_string());
259
+ }
260
+ if let Some((prefix, _)) = from_path.split_once("/Sources/") {
261
+ return Some(format!("{prefix}/Sources/"));
262
+ }
263
+ let (prefix, _) = from_path.split_once("/Tests/")?;
264
+ Some(format!("{prefix}/Sources/"))
265
+ }
266
+
228
267
  fn resolve_candidates(
229
268
  candidate: &str,
230
269
  repository_files: &BTreeSet<String>,
@@ -286,10 +325,11 @@ fn extensions_for_language(language: Option<&str>) -> &'static [&'static str] {
286
325
  Some("rust") => &["", ".rs"],
287
326
  Some("python") => &["", ".py"],
288
327
  Some("go") => &["", ".go"],
328
+ Some("swift") => &["", ".swift"],
289
329
  Some("typescript") => &["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
290
330
  Some("javascript") => &["", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"],
291
331
  _ => &[
292
- "", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs", ".py", ".go",
332
+ "", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs", ".py", ".go", ".swift",
293
333
  ],
294
334
  }
295
335
  }
@@ -70,6 +70,7 @@ pub(super) fn language_for_path(path: &str) -> Option<&'static str> {
70
70
  "rs" => Some("rust"),
71
71
  "py" => Some("python"),
72
72
  "go" => Some("go"),
73
+ "swift" => Some("swift"),
73
74
  _ => None,
74
75
  }
75
76
  }
@@ -0,0 +1,102 @@
1
+ use std::path::Path;
2
+
3
+ use crate::architecture::scan::{ManifestDependency, ManifestFact};
4
+
5
+ pub(super) enum ManifestKind {
6
+ PackageJson,
7
+ CargoToml,
8
+ PyprojectToml,
9
+ GoMod,
10
+ PomXml,
11
+ BuildGradle,
12
+ PackageSwift,
13
+ XcodeProject,
14
+ }
15
+
16
+ pub(super) fn manifest_kind(path: &str) -> Option<ManifestKind> {
17
+ match path.rsplit('/').next()? {
18
+ "package.json" => Some(ManifestKind::PackageJson),
19
+ "Cargo.toml" => Some(ManifestKind::CargoToml),
20
+ "pyproject.toml" => Some(ManifestKind::PyprojectToml),
21
+ "go.mod" => Some(ManifestKind::GoMod),
22
+ "pom.xml" => Some(ManifestKind::PomXml),
23
+ "build.gradle" | "build.gradle.kts" => Some(ManifestKind::BuildGradle),
24
+ "Package.swift" => Some(ManifestKind::PackageSwift),
25
+ "project.pbxproj" => {
26
+ if path.contains(".xcodeproj/") {
27
+ Some(ManifestKind::XcodeProject)
28
+ } else {
29
+ None
30
+ }
31
+ }
32
+ _ => None,
33
+ }
34
+ }
35
+
36
+ pub(super) fn fact(
37
+ path: &str,
38
+ ecosystem: &str,
39
+ package_name: String,
40
+ mut dependencies: Vec<ManifestDependency>,
41
+ confidence: f32,
42
+ ) -> ManifestFact {
43
+ dependencies.sort_by(|left, right| {
44
+ left.name
45
+ .cmp(&right.name)
46
+ .then(left.dependency_kind.cmp(&right.dependency_kind))
47
+ });
48
+ dependencies.dedup_by(|left, right| {
49
+ left.name == right.name && left.dependency_kind == right.dependency_kind
50
+ });
51
+ ManifestFact {
52
+ path: path.to_string(),
53
+ ecosystem: ecosystem.to_string(),
54
+ package_name,
55
+ dependencies,
56
+ confidence,
57
+ extractor: format!("manifest:{}", manifest_kind_label(path)),
58
+ }
59
+ }
60
+
61
+ pub(super) fn dependency(
62
+ name: impl Into<String>,
63
+ dependency_kind: impl Into<String>,
64
+ version: Option<String>,
65
+ confidence: f32,
66
+ ) -> ManifestDependency {
67
+ ManifestDependency {
68
+ name: name.into(),
69
+ dependency_kind: dependency_kind.into(),
70
+ version,
71
+ confidence,
72
+ }
73
+ }
74
+
75
+ pub(super) fn fallback_package_name(path: &str, ecosystem: &str) -> String {
76
+ let parent = Path::new(path)
77
+ .parent()
78
+ .map(|path| path.to_string_lossy().replace('\\', "/"))
79
+ .filter(|path| !path.is_empty())
80
+ .unwrap_or_else(|| path.to_string());
81
+ format!("{ecosystem}:{parent}")
82
+ }
83
+
84
+ pub(super) fn quoted_value(value: &str) -> Option<String> {
85
+ let trimmed = value.trim();
86
+ trimmed
87
+ .strip_prefix('"')
88
+ .and_then(|value| value.split_once('"').map(|(value, _)| value.to_string()))
89
+ .or_else(|| {
90
+ trimmed
91
+ .strip_prefix('\'')
92
+ .and_then(|value| value.split_once('\'').map(|(value, _)| value.to_string()))
93
+ })
94
+ }
95
+
96
+ pub(super) fn clean_toml_line(line: &str) -> &str {
97
+ line.split('#').next().unwrap_or_default().trim()
98
+ }
99
+
100
+ fn manifest_kind_label(path: &str) -> &str {
101
+ path.rsplit('/').next().unwrap_or(path)
102
+ }
@@ -0,0 +1,46 @@
1
+ use serde_json::Value;
2
+
3
+ use crate::architecture::scan::{ManifestDependency, ManifestFact};
4
+
5
+ use super::super::common::{dependency, fact, fallback_package_name};
6
+
7
+ pub(in crate::architecture::scan::manifest) fn package_json(
8
+ path: &str,
9
+ content: &str,
10
+ ) -> Option<ManifestFact> {
11
+ let value = serde_json::from_str::<Value>(content).ok()?;
12
+ let package_name = value
13
+ .get("name")
14
+ .and_then(Value::as_str)
15
+ .map(str::to_string)
16
+ .unwrap_or_else(|| fallback_package_name(path, "npm"));
17
+ let mut dependencies = Vec::new();
18
+ push_dependency_object(&mut dependencies, &value, "dependencies", "runtime");
19
+ push_dependency_object(&mut dependencies, &value, "devDependencies", "development");
20
+ push_dependency_object(&mut dependencies, &value, "peerDependencies", "peer");
21
+ push_dependency_object(
22
+ &mut dependencies,
23
+ &value,
24
+ "optionalDependencies",
25
+ "optional",
26
+ );
27
+ Some(fact(path, "npm", package_name, dependencies, 0.98))
28
+ }
29
+
30
+ fn push_dependency_object(
31
+ dependencies: &mut Vec<ManifestDependency>,
32
+ value: &Value,
33
+ field: &str,
34
+ kind: &str,
35
+ ) {
36
+ if let Some(object) = value.get(field).and_then(Value::as_object) {
37
+ dependencies.extend(object.iter().map(|(name, version)| {
38
+ dependency(
39
+ name.clone(),
40
+ kind,
41
+ version.as_str().map(str::to_string),
42
+ 0.98,
43
+ )
44
+ }));
45
+ }
46
+ }