Skip to main content

relay_knowledge/domain/operations/
software.rs

1use serde::{Deserialize, Serialize};
2
3use super::{
4    DomainError, FreshnessPolicy, GraphVersion, RepositoryCodeRange, error::required_text,
5};
6
7/// Query kind for repository-scoped software global model facts.
8#[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    /// Stable CLI, API, and storage-facing representation.
24    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/// Repository-scoped software global model query.
40#[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    /// Validates the requested result bound while preserving repository scope.
50    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/// Projected dependency component from repository manifests and lockfiles.
72#[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    /// Creates a validated component identity derived from dependency evidence.
95    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/// Constructor input for `SoftwareComponent`.
145#[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/// Projected SDK or external API usage from unresolved import/include evidence.
164#[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    /// Creates a validated unresolved SDK/API usage candidate.
182    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/// Constructor input for `SoftwareSdkUsage`.
218#[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/// Import/include evidence that uses a declared dependency component.
233#[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    /// Creates a validated relationship between dependency metadata and import evidence.
254    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/// Constructor input for `SoftwareDependencyUsage`.
296#[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/// Projected repository file node for the software global model.
314#[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    /// Creates a stable file node identity for a repository snapshot.
328    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/// Constructor input for `SoftwareFile`.
346#[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/// Topic extracted from repository documentation or the repository knowledge map.
358#[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    /// Creates a stable topic identity tied to the source evidence location.
372    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/// Constructor input for `SoftwareTopic`.
402#[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/// Cross-domain relationship between projected software files, topics, components, and usages.
414#[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    /// Creates a validated projected relationship without upgrading unresolved targets.
436    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/// Constructor input for `SoftwareRelationship`.
475#[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/// Build target, script, profile, or generator entry projected from indexed repository evidence.
494#[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    /// Creates a validated build target projection row.
516    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/// Constructor input for `SoftwareBuildTarget`.
560#[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/// Infrastructure, deployment, or service-operation resource projected from indexed IaC evidence.
578#[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    /// Creates a validated IaC/deployment resource projection row.
601    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/// Constructor input for `SoftwareIacResource`.
647#[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/// Design, architecture, module, component, interface, or capability evidence.
666#[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    /// Creates a validated design projection row.
687    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/// Constructor input for `SoftwareDesignElement`.
728#[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/// Freshness and count summary for the software global projection.
745#[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/// Projected software global facts for one repository scope.
764#[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}