1use serde::{Deserialize, Serialize};
2
3use super::{
4 DomainError, FreshnessPolicy, GraphVersion, RepositoryCodeRange, error::required_text,
5};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum SoftwareGlobalKind {
11 Dependencies,
12 Sdks,
13 Files,
14 Topics,
15 Relationships,
16 Build,
17 Iac,
18 Design,
19 All,
20}
21
22impl SoftwareGlobalKind {
23 pub const fn as_str(self) -> &'static str {
25 match self {
26 Self::Dependencies => "dependencies",
27 Self::Sdks => "sdks",
28 Self::Files => "files",
29 Self::Topics => "topics",
30 Self::Relationships => "relationships",
31 Self::Build => "build",
32 Self::Iac => "iac",
33 Self::Design => "design",
34 Self::All => "all",
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct SoftwareGlobalRequest {
42 pub repository: super::CodeRepositorySelector,
43 pub kind: SoftwareGlobalKind,
44 pub freshness_policy: FreshnessPolicy,
45 pub limit: usize,
46}
47
48impl SoftwareGlobalRequest {
49 pub fn new(
51 repository: super::CodeRepositorySelector,
52 kind: SoftwareGlobalKind,
53 freshness_policy: FreshnessPolicy,
54 limit: usize,
55 ) -> Result<Self, DomainError> {
56 let limit = match limit {
57 1..=500 => limit,
58 0 => return Err(DomainError::invalid("limit", "must be greater than zero")),
59 _ => return Err(DomainError::invalid("limit", "must be 500 or less")),
60 };
61
62 Ok(Self {
63 repository,
64 kind,
65 freshness_policy,
66 limit,
67 })
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct SoftwareComponent {
74 pub component_id: String,
75 pub repository_id: String,
76 pub source_scope: String,
77 pub ecosystem: String,
78 pub language_id: String,
79 pub name: String,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub requirement: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub resolved_version: Option<String>,
84 pub dependency_group: String,
85 pub source_kind: String,
86 pub relationship_state: String,
87 pub evidence_path: String,
88 pub evidence_line_range: RepositoryCodeRange,
89 pub confidence_basis_points: u16,
90 pub created_graph_version: GraphVersion,
91}
92
93impl SoftwareComponent {
94 pub fn new(input: SoftwareComponentInput) -> Result<Self, DomainError> {
96 let requirement = normalize_optional("requirement", input.requirement)?;
97 let resolved_version = normalize_optional("resolved_version", input.resolved_version)?;
98 let source_scope = required_text("source_scope", input.source_scope)?;
99 let ecosystem = required_text("ecosystem", input.ecosystem)?;
100 let name = required_text("component_name", input.name)?;
101 let dependency_group = required_text("dependency_group", input.dependency_group)?;
102 let source_kind = required_text("source_kind", input.source_kind)?;
103 let language_id = required_text("language_id", input.language_id)?;
104 let evidence_path = required_text("evidence_path", input.evidence_path)?;
105 let line_start = input.evidence_line_range.start.to_string();
106 let identity_version = resolved_version
107 .as_deref()
108 .or(requirement.as_deref())
109 .unwrap_or("unversioned");
110
111 Ok(Self {
112 component_id: stable_software_id(
113 "component",
114 [
115 source_scope.as_str(),
116 ecosystem.as_str(),
117 name.as_str(),
118 identity_version,
119 dependency_group.as_str(),
120 source_kind.as_str(),
121 language_id.as_str(),
122 evidence_path.as_str(),
123 line_start.as_str(),
124 ],
125 ),
126 repository_id: required_text("repository_id", input.repository_id)?,
127 source_scope,
128 ecosystem,
129 name,
130 requirement,
131 resolved_version,
132 dependency_group,
133 source_kind,
134 relationship_state: required_text("relationship_state", input.relationship_state)?,
135 language_id,
136 evidence_path,
137 evidence_line_range: input.evidence_line_range,
138 confidence_basis_points: validate_confidence(input.confidence_basis_points)?,
139 created_graph_version: input.created_graph_version,
140 })
141 }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct SoftwareComponentInput {
147 pub repository_id: String,
148 pub source_scope: String,
149 pub ecosystem: String,
150 pub language_id: String,
151 pub name: String,
152 pub requirement: Option<String>,
153 pub resolved_version: Option<String>,
154 pub dependency_group: String,
155 pub source_kind: String,
156 pub relationship_state: String,
157 pub evidence_path: String,
158 pub evidence_line_range: RepositoryCodeRange,
159 pub confidence_basis_points: u16,
160 pub created_graph_version: GraphVersion,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct SoftwareSdkUsage {
166 pub usage_id: String,
167 pub repository_id: String,
168 pub source_scope: String,
169 pub language_id: String,
170 pub module: String,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub target_hint: Option<String>,
173 pub resolution_state: String,
174 pub evidence_path: String,
175 pub evidence_line_range: RepositoryCodeRange,
176 pub confidence_basis_points: u16,
177 pub created_graph_version: GraphVersion,
178}
179
180impl SoftwareSdkUsage {
181 pub fn new(input: SoftwareSdkUsageInput) -> Result<Self, DomainError> {
183 let source_scope = required_text("source_scope", input.source_scope)?;
184 let language_id = required_text("language_id", input.language_id)?;
185 let module = required_text("module", input.module)?;
186 let target_hint = normalize_optional("target_hint", input.target_hint)?;
187 let resolution_state = required_text("resolution_state", input.resolution_state)?;
188 let evidence_path = required_text("evidence_path", input.evidence_path)?;
189 let line_start = input.evidence_line_range.start.to_string();
190
191 Ok(Self {
192 usage_id: stable_software_id(
193 "sdk_usage",
194 [
195 source_scope.as_str(),
196 language_id.as_str(),
197 evidence_path.as_str(),
198 module.as_str(),
199 resolution_state.as_str(),
200 line_start.as_str(),
201 ],
202 ),
203 repository_id: required_text("repository_id", input.repository_id)?,
204 source_scope,
205 language_id,
206 module,
207 target_hint,
208 resolution_state,
209 evidence_path,
210 evidence_line_range: input.evidence_line_range,
211 confidence_basis_points: validate_confidence(input.confidence_basis_points)?,
212 created_graph_version: input.created_graph_version,
213 })
214 }
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct SoftwareSdkUsageInput {
220 pub repository_id: String,
221 pub source_scope: String,
222 pub language_id: String,
223 pub module: String,
224 pub target_hint: Option<String>,
225 pub resolution_state: String,
226 pub evidence_path: String,
227 pub evidence_line_range: RepositoryCodeRange,
228 pub confidence_basis_points: u16,
229 pub created_graph_version: GraphVersion,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234pub struct SoftwareDependencyUsage {
235 pub usage_id: String,
236 pub component_id: String,
237 pub repository_id: String,
238 pub source_scope: String,
239 pub ecosystem: String,
240 pub package_name: String,
241 pub language_id: String,
242 pub module: String,
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub target_hint: Option<String>,
245 pub resolution_state: String,
246 pub evidence_path: String,
247 pub evidence_line_range: RepositoryCodeRange,
248 pub confidence_basis_points: u16,
249 pub created_graph_version: GraphVersion,
250}
251
252impl SoftwareDependencyUsage {
253 pub fn new(input: SoftwareDependencyUsageInput) -> Result<Self, DomainError> {
255 let component_id = required_text("component_id", input.component_id)?;
256 let source_scope = required_text("source_scope", input.source_scope)?;
257 let ecosystem = required_text("ecosystem", input.ecosystem)?;
258 let package_name = required_text("package_name", input.package_name)?;
259 let language_id = required_text("language_id", input.language_id)?;
260 let module = required_text("module", input.module)?;
261 let target_hint = normalize_optional("target_hint", input.target_hint)?;
262 let resolution_state = required_text("resolution_state", input.resolution_state)?;
263 let evidence_path = required_text("evidence_path", input.evidence_path)?;
264 let line_start = input.evidence_line_range.start.to_string();
265
266 Ok(Self {
267 usage_id: stable_software_id(
268 "dependency_usage",
269 [
270 source_scope.as_str(),
271 component_id.as_str(),
272 language_id.as_str(),
273 evidence_path.as_str(),
274 module.as_str(),
275 line_start.as_str(),
276 ],
277 ),
278 component_id,
279 repository_id: required_text("repository_id", input.repository_id)?,
280 source_scope,
281 ecosystem,
282 package_name,
283 language_id,
284 module,
285 target_hint,
286 resolution_state,
287 evidence_path,
288 evidence_line_range: input.evidence_line_range,
289 confidence_basis_points: validate_confidence(input.confidence_basis_points)?,
290 created_graph_version: input.created_graph_version,
291 })
292 }
293}
294
295#[derive(Debug, Clone, PartialEq, Eq)]
297pub struct SoftwareDependencyUsageInput {
298 pub component_id: String,
299 pub repository_id: String,
300 pub source_scope: String,
301 pub ecosystem: String,
302 pub package_name: String,
303 pub language_id: String,
304 pub module: String,
305 pub target_hint: Option<String>,
306 pub resolution_state: String,
307 pub evidence_path: String,
308 pub evidence_line_range: RepositoryCodeRange,
309 pub confidence_basis_points: u16,
310 pub created_graph_version: GraphVersion,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
315pub struct SoftwareFile {
316 pub software_file_id: String,
317 pub repository_id: String,
318 pub source_scope: String,
319 pub path: String,
320 pub language_id: String,
321 pub file_role: String,
322 pub parse_status: String,
323 pub created_graph_version: GraphVersion,
324}
325
326impl SoftwareFile {
327 pub fn new(input: SoftwareFileInput) -> Result<Self, DomainError> {
329 let source_scope = required_text("source_scope", input.source_scope)?;
330 let path = required_text("software_file_path", input.path)?;
331
332 Ok(Self {
333 software_file_id: stable_software_id("file", [source_scope.as_str(), path.as_str()]),
334 repository_id: required_text("repository_id", input.repository_id)?,
335 source_scope,
336 path,
337 language_id: required_text("language_id", input.language_id)?,
338 file_role: required_text("file_role", input.file_role)?,
339 parse_status: required_text("parse_status", input.parse_status)?,
340 created_graph_version: input.created_graph_version,
341 })
342 }
343}
344
345#[derive(Debug, Clone, PartialEq, Eq)]
347pub struct SoftwareFileInput {
348 pub repository_id: String,
349 pub source_scope: String,
350 pub path: String,
351 pub language_id: String,
352 pub file_role: String,
353 pub parse_status: String,
354 pub created_graph_version: GraphVersion,
355}
356
357#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
359pub struct SoftwareTopic {
360 pub topic_id: String,
361 pub repository_id: String,
362 pub source_scope: String,
363 pub name: String,
364 pub topic_kind: String,
365 pub source_path: String,
366 pub line_range: RepositoryCodeRange,
367 pub created_graph_version: GraphVersion,
368}
369
370impl SoftwareTopic {
371 pub fn new(input: SoftwareTopicInput) -> Result<Self, DomainError> {
373 let source_scope = required_text("source_scope", input.source_scope)?;
374 let name = required_text("topic_name", input.name)?;
375 let topic_kind = required_text("topic_kind", input.topic_kind)?;
376 let source_path = required_text("topic_source_path", input.source_path)?;
377 let line_start = input.line_range.start.to_string();
378
379 Ok(Self {
380 topic_id: stable_software_id(
381 "topic",
382 [
383 source_scope.as_str(),
384 topic_kind.as_str(),
385 source_path.as_str(),
386 name.as_str(),
387 line_start.as_str(),
388 ],
389 ),
390 repository_id: required_text("repository_id", input.repository_id)?,
391 source_scope,
392 name,
393 topic_kind,
394 source_path,
395 line_range: input.line_range,
396 created_graph_version: input.created_graph_version,
397 })
398 }
399}
400
401#[derive(Debug, Clone, PartialEq, Eq)]
403pub struct SoftwareTopicInput {
404 pub repository_id: String,
405 pub source_scope: String,
406 pub name: String,
407 pub topic_kind: String,
408 pub source_path: String,
409 pub line_range: RepositoryCodeRange,
410 pub created_graph_version: GraphVersion,
411}
412
413#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
415pub struct SoftwareRelationship {
416 pub relationship_id: String,
417 pub repository_id: String,
418 pub source_scope: String,
419 pub relationship_kind: String,
420 pub source_id: String,
421 pub source_kind: String,
422 pub target_id: String,
423 pub target_kind: String,
424 #[serde(skip_serializing_if = "Option::is_none")]
425 pub target_hint: Option<String>,
426 pub resolution_state: String,
427 pub confidence_basis_points: u16,
428 pub confidence_tier: String,
429 pub evidence_path: String,
430 pub evidence_line_range: RepositoryCodeRange,
431 pub created_graph_version: GraphVersion,
432}
433
434impl SoftwareRelationship {
435 pub fn new(input: SoftwareRelationshipInput) -> Result<Self, DomainError> {
437 let source_scope = required_text("source_scope", input.source_scope)?;
438 let relationship_kind = required_text("relationship_kind", input.relationship_kind)?;
439 let source_id = required_text("relationship_source_id", input.source_id)?;
440 let target_id = required_text("relationship_target_id", input.target_id)?;
441 let evidence_path = required_text("evidence_path", input.evidence_path)?;
442 let line_start = input.evidence_line_range.start.to_string();
443
444 Ok(Self {
445 relationship_id: stable_software_id(
446 "relationship",
447 [
448 source_scope.as_str(),
449 relationship_kind.as_str(),
450 source_id.as_str(),
451 target_id.as_str(),
452 evidence_path.as_str(),
453 line_start.as_str(),
454 ],
455 ),
456 repository_id: required_text("repository_id", input.repository_id)?,
457 source_scope,
458 relationship_kind,
459 source_id,
460 source_kind: required_text("relationship_source_kind", input.source_kind)?,
461 target_id,
462 target_kind: required_text("relationship_target_kind", input.target_kind)?,
463 target_hint: normalize_optional("target_hint", input.target_hint)?,
464 resolution_state: required_text("resolution_state", input.resolution_state)?,
465 confidence_basis_points: validate_confidence(input.confidence_basis_points)?,
466 confidence_tier: required_text("confidence_tier", input.confidence_tier)?,
467 evidence_path,
468 evidence_line_range: input.evidence_line_range,
469 created_graph_version: input.created_graph_version,
470 })
471 }
472}
473
474#[derive(Debug, Clone, PartialEq, Eq)]
476pub struct SoftwareRelationshipInput {
477 pub repository_id: String,
478 pub source_scope: String,
479 pub relationship_kind: String,
480 pub source_id: String,
481 pub source_kind: String,
482 pub target_id: String,
483 pub target_kind: String,
484 pub target_hint: Option<String>,
485 pub resolution_state: String,
486 pub confidence_basis_points: u16,
487 pub confidence_tier: String,
488 pub evidence_path: String,
489 pub evidence_line_range: RepositoryCodeRange,
490 pub created_graph_version: GraphVersion,
491}
492
493#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
495pub struct SoftwareBuildTarget {
496 pub target_id: String,
497 pub repository_id: String,
498 pub source_scope: String,
499 pub ecosystem: String,
500 pub language_id: String,
501 pub name: String,
502 pub kind: String,
503 #[serde(skip_serializing_if = "Option::is_none")]
504 pub command: Option<String>,
505 #[serde(skip_serializing_if = "Option::is_none")]
506 pub output_hint: Option<String>,
507 pub source_kind: String,
508 pub evidence_path: String,
509 pub evidence_line_range: RepositoryCodeRange,
510 pub confidence_basis_points: u16,
511 pub created_graph_version: GraphVersion,
512}
513
514impl SoftwareBuildTarget {
515 pub fn new(input: SoftwareBuildTargetInput) -> Result<Self, DomainError> {
517 let source_scope = required_text("source_scope", input.source_scope)?;
518 let ecosystem = required_text("ecosystem", input.ecosystem)?;
519 let language_id = required_text("language_id", input.language_id)?;
520 let name = required_text("build_target_name", input.name)?;
521 let kind = required_text("build_target_kind", input.kind)?;
522 let command = normalize_optional("command", input.command)?;
523 let output_hint = normalize_optional("output_hint", input.output_hint)?;
524 let source_kind = required_text("source_kind", input.source_kind)?;
525 let evidence_path = required_text("evidence_path", input.evidence_path)?;
526 let line_start = input.evidence_line_range.start.to_string();
527
528 Ok(Self {
529 target_id: stable_software_id(
530 "build_target",
531 [
532 source_scope.as_str(),
533 ecosystem.as_str(),
534 language_id.as_str(),
535 name.as_str(),
536 kind.as_str(),
537 source_kind.as_str(),
538 evidence_path.as_str(),
539 line_start.as_str(),
540 ],
541 ),
542 repository_id: required_text("repository_id", input.repository_id)?,
543 source_scope,
544 ecosystem,
545 language_id,
546 name,
547 kind,
548 command,
549 output_hint,
550 source_kind,
551 evidence_path,
552 evidence_line_range: input.evidence_line_range,
553 confidence_basis_points: validate_confidence(input.confidence_basis_points)?,
554 created_graph_version: input.created_graph_version,
555 })
556 }
557}
558
559#[derive(Debug, Clone, PartialEq, Eq)]
561pub struct SoftwareBuildTargetInput {
562 pub repository_id: String,
563 pub source_scope: String,
564 pub ecosystem: String,
565 pub language_id: String,
566 pub name: String,
567 pub kind: String,
568 pub command: Option<String>,
569 pub output_hint: Option<String>,
570 pub source_kind: String,
571 pub evidence_path: String,
572 pub evidence_line_range: RepositoryCodeRange,
573 pub confidence_basis_points: u16,
574 pub created_graph_version: GraphVersion,
575}
576
577#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
579pub struct SoftwareIacResource {
580 pub resource_id: String,
581 pub repository_id: String,
582 pub source_scope: String,
583 pub language_id: String,
584 pub provider: String,
585 pub resource_kind: String,
586 pub name: String,
587 #[serde(skip_serializing_if = "Option::is_none")]
588 pub scope_hint: Option<String>,
589 #[serde(skip_serializing_if = "Option::is_none")]
590 pub target_hint: Option<String>,
591 pub resolution_state: String,
592 pub source_kind: String,
593 pub evidence_path: String,
594 pub evidence_line_range: RepositoryCodeRange,
595 pub confidence_basis_points: u16,
596 pub created_graph_version: GraphVersion,
597}
598
599impl SoftwareIacResource {
600 pub fn new(input: SoftwareIacResourceInput) -> Result<Self, DomainError> {
602 let source_scope = required_text("source_scope", input.source_scope)?;
603 let language_id = required_text("language_id", input.language_id)?;
604 let provider = required_text("iac_provider", input.provider)?;
605 let resource_kind = required_text("iac_resource_kind", input.resource_kind)?;
606 let name = required_text("iac_resource_name", input.name)?;
607 let scope_hint = normalize_optional("scope_hint", input.scope_hint)?;
608 let target_hint = normalize_optional("target_hint", input.target_hint)?;
609 let resolution_state = required_text("resolution_state", input.resolution_state)?;
610 let source_kind = required_text("source_kind", input.source_kind)?;
611 let evidence_path = required_text("evidence_path", input.evidence_path)?;
612 let line_start = input.evidence_line_range.start.to_string();
613
614 Ok(Self {
615 resource_id: stable_software_id(
616 "iac_resource",
617 [
618 source_scope.as_str(),
619 language_id.as_str(),
620 provider.as_str(),
621 resource_kind.as_str(),
622 name.as_str(),
623 source_kind.as_str(),
624 evidence_path.as_str(),
625 line_start.as_str(),
626 ],
627 ),
628 repository_id: required_text("repository_id", input.repository_id)?,
629 source_scope,
630 language_id,
631 provider,
632 resource_kind,
633 name,
634 scope_hint,
635 target_hint,
636 resolution_state,
637 source_kind,
638 evidence_path,
639 evidence_line_range: input.evidence_line_range,
640 confidence_basis_points: validate_confidence(input.confidence_basis_points)?,
641 created_graph_version: input.created_graph_version,
642 })
643 }
644}
645
646#[derive(Debug, Clone, PartialEq, Eq)]
648pub struct SoftwareIacResourceInput {
649 pub repository_id: String,
650 pub source_scope: String,
651 pub language_id: String,
652 pub provider: String,
653 pub resource_kind: String,
654 pub name: String,
655 pub scope_hint: Option<String>,
656 pub target_hint: Option<String>,
657 pub resolution_state: String,
658 pub source_kind: String,
659 pub evidence_path: String,
660 pub evidence_line_range: RepositoryCodeRange,
661 pub confidence_basis_points: u16,
662 pub created_graph_version: GraphVersion,
663}
664
665#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
667pub struct SoftwareDesignElement {
668 pub element_id: String,
669 pub repository_id: String,
670 pub source_scope: String,
671 pub language_id: String,
672 pub element_kind: String,
673 pub name: String,
674 #[serde(skip_serializing_if = "Option::is_none")]
675 pub parent: Option<String>,
676 #[serde(skip_serializing_if = "Option::is_none")]
677 pub summary: Option<String>,
678 pub source_kind: String,
679 pub evidence_path: String,
680 pub evidence_line_range: RepositoryCodeRange,
681 pub confidence_basis_points: u16,
682 pub created_graph_version: GraphVersion,
683}
684
685impl SoftwareDesignElement {
686 pub fn new(input: SoftwareDesignElementInput) -> Result<Self, DomainError> {
688 let source_scope = required_text("source_scope", input.source_scope)?;
689 let language_id = required_text("language_id", input.language_id)?;
690 let element_kind = required_text("design_element_kind", input.element_kind)?;
691 let name = required_text("design_element_name", input.name)?;
692 let parent = normalize_optional("parent", input.parent)?;
693 let summary = normalize_optional("summary", input.summary)?;
694 let source_kind = required_text("source_kind", input.source_kind)?;
695 let evidence_path = required_text("evidence_path", input.evidence_path)?;
696 let line_start = input.evidence_line_range.start.to_string();
697
698 Ok(Self {
699 element_id: stable_software_id(
700 "design_element",
701 [
702 source_scope.as_str(),
703 language_id.as_str(),
704 element_kind.as_str(),
705 name.as_str(),
706 source_kind.as_str(),
707 evidence_path.as_str(),
708 line_start.as_str(),
709 ],
710 ),
711 repository_id: required_text("repository_id", input.repository_id)?,
712 source_scope,
713 language_id,
714 element_kind,
715 name,
716 parent,
717 summary,
718 source_kind,
719 evidence_path,
720 evidence_line_range: input.evidence_line_range,
721 confidence_basis_points: validate_confidence(input.confidence_basis_points)?,
722 created_graph_version: input.created_graph_version,
723 })
724 }
725}
726
727#[derive(Debug, Clone, PartialEq, Eq)]
729pub struct SoftwareDesignElementInput {
730 pub repository_id: String,
731 pub source_scope: String,
732 pub language_id: String,
733 pub element_kind: String,
734 pub name: String,
735 pub parent: Option<String>,
736 pub summary: Option<String>,
737 pub source_kind: String,
738 pub evidence_path: String,
739 pub evidence_line_range: RepositoryCodeRange,
740 pub confidence_basis_points: u16,
741 pub created_graph_version: GraphVersion,
742}
743
744#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
746pub struct SoftwareGlobalStatus {
747 pub repository_id: String,
748 pub source_scope: String,
749 pub projected_graph_version: GraphVersion,
750 pub stale: bool,
751 pub component_count: usize,
752 pub sdk_usage_count: usize,
753 pub file_count: usize,
754 pub topic_count: usize,
755 pub relationship_count: usize,
756 pub build_target_count: usize,
757 pub iac_resource_count: usize,
758 pub design_element_count: usize,
759 #[serde(skip_serializing_if = "Option::is_none")]
760 pub last_error: Option<String>,
761}
762
763#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
765pub struct SoftwareGlobalProjection {
766 pub status: SoftwareGlobalStatus,
767 pub components: Vec<SoftwareComponent>,
768 pub dependency_usages: Vec<SoftwareDependencyUsage>,
769 pub sdk_usages: Vec<SoftwareSdkUsage>,
770 pub files: Vec<SoftwareFile>,
771 pub topics: Vec<SoftwareTopic>,
772 pub relationships: Vec<SoftwareRelationship>,
773 pub build_targets: Vec<SoftwareBuildTarget>,
774 pub iac_resources: Vec<SoftwareIacResource>,
775 pub design_elements: Vec<SoftwareDesignElement>,
776}
777
778fn normalize_optional(
779 field: &'static str,
780 value: Option<String>,
781) -> Result<Option<String>, DomainError> {
782 value.map(|text| required_text(field, text)).transpose()
783}
784
785fn validate_confidence(value: u16) -> Result<u16, DomainError> {
786 if value > 10_000 {
787 return Err(DomainError::invalid(
788 "confidence",
789 "must be between 0 and 10000 basis points",
790 ));
791 }
792
793 Ok(value)
794}
795
796fn stable_software_id<'a>(prefix: &str, parts: impl IntoIterator<Item = &'a str>) -> String {
797 let mut hash = 0xcbf29ce484222325u64;
798 for part in parts {
799 for byte in part.as_bytes() {
800 hash ^= u64::from(*byte);
801 hash = hash.wrapping_mul(0x100000001b3);
802 }
803 hash ^= 0xff;
804 hash = hash.wrapping_mul(0x100000001b3);
805 }
806
807 format!("{prefix}:{hash:016x}")
808}
809
810#[cfg(test)]
811mod tests {
812 use super::*;
813
814 #[test]
815 fn component_identity_includes_scope_and_version() {
816 let base = component_input("scope-a", Some("1.0.0"));
817 let component = SoftwareComponent::new(base).expect("component should validate");
818 let changed = SoftwareComponent::new(component_input("scope-b", Some("1.0.0")))
819 .expect("component should validate");
820
821 assert_ne!(component.component_id, changed.component_id);
822 }
823
824 #[test]
825 fn component_identity_preserves_duplicate_evidence_rows() {
826 let first = SoftwareComponent::new(component_input("scope-a", Some("1.0.0")))
827 .expect("component should validate");
828 let mut second_input = component_input("scope-a", Some("1.0.0"));
829 second_input.evidence_path = "crates/core/Cargo.toml".to_owned();
830 second_input.evidence_line_range = RepositoryCodeRange { start: 9, end: 9 };
831 let second = SoftwareComponent::new(second_input).expect("component should validate");
832
833 assert_ne!(first.component_id, second.component_id);
834 }
835
836 #[test]
837 fn component_identity_includes_expanded_language_rows() {
838 let rust = SoftwareComponent::new(component_input("scope-a", Some("1.0.0")))
839 .expect("component should validate");
840 let mut tsx_input = component_input("scope-a", Some("1.0.0"));
841 tsx_input.language_id = "tsx".to_owned();
842 let tsx = SoftwareComponent::new(tsx_input).expect("component should validate");
843
844 assert_ne!(rust.component_id, tsx.component_id);
845 }
846
847 #[test]
848 fn component_rejects_empty_name_and_invalid_confidence() {
849 let mut input = component_input("scope-a", None);
850 input.name = " ".to_owned();
851 assert_eq!(
852 SoftwareComponent::new(input)
853 .expect_err("empty name should fail")
854 .field,
855 "component_name"
856 );
857
858 let mut input = component_input("scope-a", None);
859 input.confidence_basis_points = 10_001;
860 assert_eq!(
861 SoftwareComponent::new(input)
862 .expect_err("bad confidence should fail")
863 .field,
864 "confidence"
865 );
866 }
867
868 #[test]
869 fn sdk_usage_preserves_unresolved_target_hint() {
870 let usage = SoftwareSdkUsage::new(SoftwareSdkUsageInput {
871 repository_id: "repo".to_owned(),
872 source_scope: "scope".to_owned(),
873 language_id: "cpp".to_owned(),
874 module: "#include <securec.h>".to_owned(),
875 target_hint: Some("securec.h".to_owned()),
876 resolution_state: "unresolved".to_owned(),
877 evidence_path: "src/main.cc".to_owned(),
878 evidence_line_range: RepositoryCodeRange { start: 3, end: 3 },
879 confidence_basis_points: 2500,
880 created_graph_version: GraphVersion::new(7),
881 })
882 .expect("usage should validate");
883
884 assert_eq!(usage.target_hint.as_deref(), Some("securec.h"));
885 }
886
887 #[test]
888 fn sdk_usage_identity_preserves_repeated_evidence_rows() {
889 let first = SoftwareSdkUsage::new(sdk_usage_input(3)).expect("usage should validate");
890 let second = SoftwareSdkUsage::new(sdk_usage_input(9)).expect("usage should validate");
891
892 assert_ne!(first.usage_id, second.usage_id);
893 }
894
895 #[test]
896 fn dependency_usage_identity_binds_component_and_import_evidence() {
897 let first =
898 SoftwareDependencyUsage::new(dependency_usage_input("component:serde", "serde", 3))
899 .expect("usage should validate");
900 let second =
901 SoftwareDependencyUsage::new(dependency_usage_input("component:serde", "serde", 9))
902 .expect("usage should validate");
903 let other_component =
904 SoftwareDependencyUsage::new(dependency_usage_input("component:tokio", "serde", 3))
905 .expect("usage should validate");
906
907 assert_ne!(first.usage_id, second.usage_id);
908 assert_ne!(first.usage_id, other_component.usage_id);
909 }
910
911 fn component_input(scope: &str, version: Option<&str>) -> SoftwareComponentInput {
912 SoftwareComponentInput {
913 repository_id: "repo".to_owned(),
914 source_scope: scope.to_owned(),
915 ecosystem: "cargo".to_owned(),
916 name: "serde".to_owned(),
917 requirement: Some("1".to_owned()),
918 resolved_version: version.map(str::to_owned),
919 dependency_group: "normal".to_owned(),
920 source_kind: "manifest".to_owned(),
921 relationship_state: "declared".to_owned(),
922 language_id: "rust".to_owned(),
923 evidence_path: "Cargo.toml".to_owned(),
924 evidence_line_range: RepositoryCodeRange { start: 1, end: 1 },
925 confidence_basis_points: 10_000,
926 created_graph_version: GraphVersion::new(1),
927 }
928 }
929
930 fn sdk_usage_input(line: u32) -> SoftwareSdkUsageInput {
931 SoftwareSdkUsageInput {
932 repository_id: "repo".to_owned(),
933 source_scope: "scope".to_owned(),
934 language_id: "cpp".to_owned(),
935 module: "#include <securec.h>".to_owned(),
936 target_hint: Some("securec.h".to_owned()),
937 resolution_state: "unresolved".to_owned(),
938 evidence_path: "src/main.cc".to_owned(),
939 evidence_line_range: RepositoryCodeRange {
940 start: line,
941 end: line,
942 },
943 confidence_basis_points: 2500,
944 created_graph_version: GraphVersion::new(7),
945 }
946 }
947
948 fn dependency_usage_input(
949 component_id: &str,
950 module: &str,
951 line: u32,
952 ) -> SoftwareDependencyUsageInput {
953 SoftwareDependencyUsageInput {
954 component_id: component_id.to_owned(),
955 repository_id: "repo".to_owned(),
956 source_scope: "scope".to_owned(),
957 ecosystem: "cargo".to_owned(),
958 package_name: "serde".to_owned(),
959 language_id: "rust".to_owned(),
960 module: module.to_owned(),
961 target_hint: Some(module.to_owned()),
962 resolution_state: "unresolved".to_owned(),
963 evidence_path: "src/lib.rs".to_owned(),
964 evidence_line_range: RepositoryCodeRange {
965 start: line,
966 end: line,
967 },
968 confidence_basis_points: 9000,
969 created_graph_version: GraphVersion::new(7),
970 }
971 }
972}