code_system_graph_model/lib.rs
1//! Stable domain contracts for `Code System Graph`'s federated boundary graph.
2
3use schemars::JsonSchema;
4use semver::Version;
5use serde::{Deserialize, Serialize};
6
7macro_rules! string_id {
8 ($name:ident, $description:literal) => {
9 #[doc = $description]
10 #[derive(
11 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
12 )]
13 #[serde(transparent)]
14 pub struct $name(pub String);
15
16 impl $name {
17 /// Creates an identifier from its canonical representation.
18 #[must_use]
19 pub fn new(value: impl Into<String>) -> Self {
20 Self(value.into())
21 }
22
23 /// Returns the canonical string representation.
24 #[must_use]
25 pub fn as_str(&self) -> &str {
26 &self.0
27 }
28 }
29 };
30}
31
32string_id!(RepoId, "Stable identifier for a registered repository.");
33string_id!(
34 CheckoutId,
35 "Stable identifier for one repository checkout or linked worktree."
36);
37string_id!(
38 WorkspaceId,
39 "Stable identifier for a `Code System Graph` workspace."
40);
41string_id!(NodeId, "Stable identifier for a federated graph node.");
42string_id!(EdgeId, "Stable identifier for a federated graph edge.");
43string_id!(EvidenceId, "Stable identifier for an evidence record.");
44string_id!(
45 CommunityId,
46 "Stable identifier for a detected graph community."
47);
48
49/// Lossless platform encoding used for a native filesystem path.
50#[derive(
51 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
52)]
53#[serde(rename_all = "snake_case")]
54pub enum NativePathEncoding {
55 /// Raw Unix `OsStr` bytes.
56 UnixBytes,
57 /// Little-endian Windows UTF-16 code units.
58 WindowsWide,
59 /// UTF-8 fallback for other targets.
60 Utf8,
61}
62
63/// Lossless native path plus a diagnostic-only display form.
64#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)]
65pub struct NativePath {
66 /// Platform-specific lossless encoding.
67 pub encoding: NativePathEncoding,
68 /// Encoded path bytes; these are not assumed to be UTF-8.
69 pub bytes: Vec<u8>,
70 /// Lossy display form intended only for diagnostics.
71 pub display: String,
72}
73
74/// Deterministic identity and checkout metadata for one registered repository.
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
76pub struct RepositoryRecord {
77 /// Stable repository identity shared by linked worktrees.
78 pub id: RepoId,
79 /// Stable identity of this concrete checkout.
80 pub checkout_id: CheckoutId,
81 /// Unique alias within the workspace.
82 pub alias: String,
83 /// Canonical native checkout path.
84 pub canonical_path: NativePath,
85 /// Canonical Git common directory when the checkout is a Git repository.
86 pub git_common_dir: Option<NativePath>,
87 /// Credential-free normalized remote identity when available.
88 pub normalized_remote: Option<String>,
89 /// Current Git commit when available.
90 pub head_commit: Option<String>,
91 /// Whether Git reports a linked worktree rather than the common checkout.
92 pub is_linked_worktree: bool,
93 /// Whether tracked or untracked working-tree changes were observed.
94 pub working_tree_dirty: bool,
95}
96
97/// Fully validated registry input for one workspace.
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
99pub struct WorkspaceRecord {
100 /// Stable workspace identity.
101 pub id: WorkspaceId,
102 /// User-facing workspace name.
103 pub name: String,
104 /// Fingerprint of the exact manifest content.
105 pub manifest_hash: String,
106 /// Canonical lossless workspace manifest path.
107 pub config_path: Option<NativePath>,
108 /// Repositories sorted by alias.
109 pub repositories: Vec<RepositoryRecord>,
110}
111
112/// Freshness state for one repository checkout.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
114#[serde(rename_all = "snake_case")]
115pub enum RepoFreshnessState {
116 /// Current inputs match the published snapshot.
117 Fresh,
118 /// The working tree changed after the snapshot.
119 WorkingTreeChanged,
120 /// Repository commits advanced after the snapshot.
121 CommitsBehind,
122 /// Workspace or repository configuration changed.
123 ConfigChanged,
124 /// An extractor version changed.
125 ExtractorChanged,
126 /// Local `CodeGraph` freshness is pending.
127 CodegraphPending,
128 /// Only part of the required inputs were scanned.
129 Partial,
130 /// Stored state failed an integrity check.
131 Corrupt,
132 /// Freshness could not be determined.
133 Unknown,
134 /// Repository checkout is unavailable.
135 Unavailable,
136}
137
138/// Snapshot freshness recorded for one repository checkout.
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
140pub struct RepoFreshness {
141 /// Stable repository identity.
142 pub repo_id: RepoId,
143 /// Concrete checkout identity.
144 pub checkout_id: CheckoutId,
145 /// Commit observed by the snapshot.
146 pub head_commit: Option<String>,
147 /// Manifest fingerprint used by the snapshot.
148 pub manifest_hash: String,
149 /// Current freshness classification.
150 pub state: RepoFreshnessState,
151 /// Optional bounded explanation.
152 pub reason: Option<String>,
153}
154
155/// Fingerprint of one extractor-relevant artifact.
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
157pub struct ArtifactFingerprint {
158 /// Repository containing the artifact.
159 pub repo_id: RepoId,
160 /// Concrete checkout containing the artifact.
161 pub checkout_id: CheckoutId,
162 /// Repository-relative native path.
163 pub path: NativePath,
164 /// Extractor that consumes this artifact.
165 pub extractor: String,
166 /// BLAKE3 content fingerprint.
167 pub content_hash: String,
168 /// Exact file size in bytes.
169 pub size_bytes: u64,
170}
171
172/// Incremental difference from the previous published artifact set.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
174#[serde(rename_all = "snake_case")]
175pub enum ArtifactChangeKind {
176 /// Artifact did not exist previously.
177 Added,
178 /// Artifact content changed.
179 Modified,
180 /// Artifact is absent from the current scan.
181 Deleted,
182 /// Artifact fingerprint is unchanged.
183 Unchanged,
184}
185
186/// Planned incremental action for one artifact identity.
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
188pub struct ArtifactChange {
189 /// Repository identity.
190 pub repo_id: RepoId,
191 /// Checkout identity.
192 pub checkout_id: CheckoutId,
193 /// Repository-relative path.
194 pub path: NativePath,
195 /// Extractor identity.
196 pub extractor: String,
197 /// Difference classification.
198 pub kind: ArtifactChangeKind,
199}
200
201/// Outcome of one extractor execution.
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
203#[serde(rename_all = "snake_case")]
204pub enum ExtractorRunStatus {
205 /// Extractor completed successfully.
206 Success,
207 /// Inputs were unchanged and reusable.
208 SkippedUnchanged,
209}
210
211/// Persisted metrics for one extractor execution or skip decision.
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
213pub struct ExtractorRun {
214 /// Stable run identity.
215 pub id: String,
216 /// Published snapshot identity.
217 pub snapshot_id: String,
218 /// Repository identity.
219 pub repo_id: RepoId,
220 /// Checkout identity.
221 pub checkout_id: CheckoutId,
222 /// Extractor identity.
223 pub extractor: String,
224 /// Extractor semantic version.
225 pub extractor_version: String,
226 /// Run outcome.
227 pub status: ExtractorRunStatus,
228 /// Number of discovered files.
229 pub discovered_files: u64,
230 /// Number of parsed files.
231 pub parsed_files: u64,
232 /// Number of skipped files.
233 pub skipped_files: u64,
234 /// Bounded execution time in milliseconds.
235 pub elapsed_ms: u64,
236}
237
238/// Opaque, versioned output owned by one extractor input.
239///
240/// The payload contains contract observations rather than source text. Its schema is owned by the
241/// extractor identified by [`Self::source`] and [`Self::extractor_version`].
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
243pub struct StoredExtractorBatch {
244 /// Fingerprint that identifies and invalidates the source-owned output.
245 pub source: ArtifactFingerprint,
246 /// Semantic version of the extractor payload schema.
247 pub extractor_version: String,
248 /// Canonical fingerprint of the effective extraction budgets.
249 pub budget_fingerprint: String,
250 /// Whether invalid UTF-8 input bytes were decoded lossily for text extraction.
251 pub source_was_lossy: bool,
252 /// Deterministic number of observations encoded in the payload.
253 pub output_count: u64,
254 /// UTF-8 JSON payload encoded as bytes for bounded, lossless persistence.
255 pub payload: Vec<u8>,
256}
257
258/// Produces a deterministic namespaced identifier from a canonical key.
259#[must_use]
260pub fn stable_id(namespace: &str, canonical_key: &str) -> String {
261 stable_id_bytes(namespace, canonical_key.as_bytes())
262}
263
264/// Produces a deterministic namespaced identifier from arbitrary canonical bytes.
265#[must_use]
266pub fn stable_id_bytes(namespace: &str, canonical_key: &[u8]) -> String {
267 let mut hasher = blake3::Hasher::new();
268 hasher.update(namespace.as_bytes());
269 hasher.update(&[0]);
270 hasher.update(canonical_key);
271 format!("{namespace}:{}", hasher.finalize().to_hex())
272}
273
274/// Reports whether untrusted metadata contains control or bidirectional formatting characters.
275///
276/// Source files may legitimately contain such characters, but identifiers, labels, paths, and
277/// diagnostic metadata must reject them before they reach durable state or terminal output.
278#[must_use]
279pub fn contains_unsafe_metadata_characters(value: &str) -> bool {
280 value.chars().any(|character| {
281 character.is_control()
282 || matches!(
283 character,
284 '\u{061c}'
285 | '\u{200e}'
286 | '\u{200f}'
287 | '\u{202a}'..='\u{202e}'
288 | '\u{2066}'..='\u{2069}'
289 | '\u{feff}'
290 )
291 })
292}
293
294/// Rejects repository-relative path display strings that are unsafe for metadata or diagnostics.
295///
296/// # Errors
297///
298/// Returns [`UnsafePathDisplayError`] when `value` contains control or bidirectional characters.
299pub fn validate_safe_path_display(value: &str) -> Result<(), UnsafePathDisplayError> {
300 if contains_unsafe_metadata_characters(value) {
301 Err(UnsafePathDisplayError)
302 } else {
303 Ok(())
304 }
305}
306
307/// Path display string contains unsafe metadata characters.
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub struct UnsafePathDisplayError;
310
311impl std::fmt::Display for UnsafePathDisplayError {
312 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313 formatter.write_str("path display contains unsafe control or bidirectional characters")
314 }
315}
316
317impl std::error::Error for UnsafePathDisplayError {}
318
319/// Kind of entity represented in the federated graph.
320#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
321#[serde(rename_all = "snake_case")]
322pub enum NodeKind {
323 /// Registered source repository.
324 Repository,
325 /// Deployable or logical service.
326 Service,
327 /// Package coordinate.
328 Package,
329 /// Boundary-defining artifact.
330 Artifact,
331 /// Lightweight reference to a repository-local symbol.
332 SymbolRef,
333 /// Executable test case in any supported language.
334 TestCase,
335 /// HTTP operation contract.
336 HttpOperation,
337 /// GraphQL operation contract.
338 GraphqlOperation,
339 /// Remote procedure call method.
340 RpcMethod,
341 /// Event topic, queue, or channel.
342 EventChannel,
343 /// Event payload schema.
344 EventSchema,
345 /// Database instance.
346 Database,
347 /// Database table.
348 DatabaseTable,
349 /// Database column.
350 DatabaseColumn,
351 /// Configuration key name.
352 ConfigKey,
353 /// Deployment unit.
354 Deployment,
355 /// Technical document.
356 Document,
357 /// Architecture decision record.
358 Adr,
359 /// Owning person or team.
360 Owner,
361 /// Local or remote change set.
362 ChangeSet,
363 /// Pull request.
364 PullRequest,
365 /// Detected graph community.
366 Community,
367}
368
369/// Kind of relationship represented in the federated graph.
370#[derive(
371 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
372)]
373#[serde(rename_all = "snake_case")]
374pub enum EdgeKind {
375 /// Parent contains child.
376 Contains,
377 /// Source provides target contract.
378 Provides,
379 /// Source consumes target contract.
380 Consumes,
381 /// Source performs a remote call to target.
382 CallsRemote,
383 /// Source publishes target event.
384 Publishes,
385 /// Source subscribes to target event.
386 Subscribes,
387 /// Event channel delivers messages to target subscriber.
388 DeliversTo,
389 /// Source depends on target package.
390 DependsOnPackage,
391 /// Source depends on target repository.
392 DependsOnRepository,
393 /// Source reads target table.
394 ReadsTable,
395 /// Source writes target table.
396 WritesTable,
397 /// Source deploys target.
398 Deploys,
399 /// Source configures target.
400 Configures,
401 /// Source documents target.
402 Documents,
403 /// Source is owned by target.
404 OwnedBy,
405 /// Source is implemented by target.
406 ImplementedBy,
407 /// Source test validates target contract or behavior.
408 Validates,
409 /// Source was changed in target.
410 ChangedIn,
411 /// Source affects target.
412 Affects,
413 /// Source migration is applied before target migration.
414 Precedes,
415 /// Source migration reverses target migration.
416 Reverts,
417 /// Source is compatible with target.
418 CompatibleWith,
419 /// Source is incompatible with target.
420 IncompatibleWith,
421 /// Source belongs to target community.
422 MemberOf,
423 /// User-declared relationship.
424 ManualLink,
425}
426
427/// Origin of a graph assertion.
428#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
429#[serde(rename_all = "snake_case")]
430pub enum Provenance {
431 /// Explicitly declared in a source artifact.
432 Declared,
433 /// Deterministically extracted from a source artifact.
434 Extracted,
435 /// Derived from incomplete or indirect signals.
436 Inferred,
437 /// Explicitly supplied by a user.
438 Manual,
439 /// Observed at runtime.
440 Runtime,
441 /// Returned through a public `CodeGraph` capability.
442 CodeGraph,
443}
444
445/// Confidence state attached to an assertion.
446#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
447#[serde(rename_all = "snake_case")]
448pub enum EpistemicStatus {
449 /// Supported by sufficient direct evidence.
450 Confirmed,
451 /// Derived from indirect evidence.
452 Inferred,
453 /// Multiple plausible interpretations remain.
454 Ambiguous,
455 /// Evidence no longer matches current inputs.
456 Stale,
457 /// Required inputs were unavailable.
458 Incomplete,
459}
460
461/// Auditable evidence supporting a node or edge.
462#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
463pub struct Evidence {
464 /// Stable evidence identifier.
465 pub id: EvidenceId,
466 /// Repository in which the evidence was observed.
467 pub repo_id: Option<RepoId>,
468 /// Repository-relative source path.
469 pub file_path: Option<String>,
470 /// Inclusive first source line.
471 pub start_line: Option<u32>,
472 /// Inclusive last source line.
473 pub end_line: Option<u32>,
474 /// Extractor identifier.
475 pub extractor: String,
476 /// Extractor semantic version.
477 pub extractor_version: String,
478 /// Origin of this evidence.
479 pub provenance: Provenance,
480 /// Normalized confidence in the inclusive range from zero to one.
481 pub confidence: f32,
482 /// Commit at which this evidence was observed.
483 pub observed_at_commit: Option<String>,
484 /// Hash of the relevant source content.
485 pub content_hash: Option<String>,
486 /// Bounded explanatory note without source or secrets.
487 pub note: Option<String>,
488}
489
490/// Entity in the federated graph.
491#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
492pub struct Node {
493 /// Stable node identifier.
494 pub id: NodeId,
495 /// Entity kind.
496 pub kind: NodeKind,
497 /// Owning repository when applicable.
498 pub repo_id: Option<RepoId>,
499 /// Versioned canonical identity key.
500 pub stable_key: String,
501 /// Human-readable label.
502 pub label: String,
503}
504
505/// Relationship in the federated graph.
506#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
507pub struct Edge {
508 /// Stable edge identifier.
509 pub id: EdgeId,
510 /// Source node.
511 pub source: NodeId,
512 /// Target node.
513 pub target: NodeId,
514 /// Relationship kind.
515 pub kind: EdgeKind,
516 /// Normalized confidence in the inclusive range from zero to one.
517 pub confidence: f32,
518 /// Epistemic state.
519 pub status: EpistemicStatus,
520 /// Evidence records supporting this relationship.
521 pub evidence: Vec<EvidenceId>,
522}
523
524/// Outcome of one auditable linker decision.
525#[derive(
526 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
527)]
528#[serde(rename_all = "snake_case")]
529pub enum LinkStatus {
530 /// The selected relationship is confirmed.
531 Confirmed,
532 /// An exact automatic relationship was removed by an explicit declaration.
533 Suppressed,
534 /// Multiple exact candidates prevented a selection.
535 Ambiguous,
536 /// The candidate was explicitly rejected.
537 Rejected,
538}
539
540/// Exact candidate considered but not selected by a linker matcher.
541#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
542#[serde(deny_unknown_fields)]
543pub struct RejectedAlternative {
544 /// Candidate node identity.
545 pub candidate: NodeId,
546 /// Normalized matcher score in the inclusive range from zero to one.
547 pub score: f32,
548 /// Deterministically ordered reasons the candidate was rejected.
549 pub reasons: Vec<String>,
550}
551
552/// Stable reference to evidence used by a linker decision.
553#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
554#[serde(deny_unknown_fields)]
555pub struct EvidenceRef {
556 /// Stable evidence identity.
557 pub id: EvidenceId,
558 /// Origin of the referenced evidence.
559 pub provenance: Provenance,
560}
561
562/// Versioned and explainable record of a linker match or suppression.
563#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
564#[serde(deny_unknown_fields)]
565pub struct LinkDecision {
566 /// Resolved source node.
567 pub source: NodeId,
568 /// Resolved target node.
569 pub target: NodeId,
570 /// Concrete relationship considered by the matcher.
571 pub relation: EdgeKind,
572 /// Stable matcher identifier.
573 pub matcher: String,
574 /// Semantic version of the matcher behavior.
575 #[schemars(with = "String")]
576 pub matcher_version: Version,
577 /// Normalized raw match score in the inclusive range from zero to one.
578 pub score: f32,
579 /// Normalized confidence in the inclusive range from zero to one.
580 pub confidence: f32,
581 /// Deterministically ordered explanations for the decision.
582 pub reasons: Vec<String>,
583 /// Deterministically ordered candidates that were not selected.
584 pub rejected_alternatives: Vec<RejectedAlternative>,
585 /// Deterministically ordered supporting evidence references.
586 pub evidence: Vec<EvidenceRef>,
587 /// Final decision state.
588 pub status: LinkStatus,
589}
590
591/// Overall freshness of data used by a result.
592#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
593#[serde(rename_all = "snake_case")]
594pub enum OverallFreshness {
595 /// All required inputs match their current fingerprints.
596 Fresh,
597 /// At least one relevant input is stale.
598 Stale,
599 /// Required inputs were only partially available.
600 Partial,
601 /// Freshness could not be established.
602 Unknown,
603}
604
605/// Aggregate freshness attached to a public result.
606#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
607pub struct FreshnessSummary {
608 /// Overall freshness classification.
609 pub overall: OverallFreshness,
610 /// Stable repository identifiers known to be stale.
611 pub stale_repositories: Vec<RepoId>,
612 /// Reasons freshness could not be fully established.
613 pub reasons: Vec<String>,
614}
615
616/// Status of a public tool result.
617#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
618#[serde(rename_all = "snake_case")]
619pub enum ToolStatus {
620 /// Complete result within requested bounds.
621 Ok,
622 /// Useful result with explicitly missing capabilities or inputs.
623 Degraded,
624 /// Request could not be completed.
625 Error,
626}
627
628/// One ordered segment in a federated trace.
629#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
630pub struct TraceSegment {
631 /// Source node.
632 pub source: Node,
633 /// Traversed relationship.
634 pub edge: Edge,
635 /// Target node.
636 pub target: Node,
637}
638
639/// Bounded and explainable federated trace.
640#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
641pub struct TraceReport {
642 /// Ordered trace segments.
643 pub segments: Vec<TraceSegment>,
644 /// Whether a configured bound truncated traversal.
645 pub truncated: bool,
646 /// Human-readable coverage gaps.
647 pub coverage_gaps: Vec<String>,
648}
649
650/// Community-detection algorithm selected for one reproducible analysis.
651#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
652#[serde(rename_all = "snake_case")]
653pub enum CommunityAlgorithm {
654 /// Maximal regions connected by eligible edges.
655 ConnectedComponents,
656 /// Deterministic weighted label propagation used as a conservative fallback.
657 WeightedClustering,
658 /// Deterministic seeded Louvain modularity optimization.
659 Louvain,
660}
661
662/// Scope over which communities are detected.
663#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
664#[serde(rename_all = "snake_case", tag = "kind", content = "value")]
665pub enum CommunityScope {
666 /// Entire current federated graph.
667 Federated,
668 /// Nodes owned by one repository.
669 Repository(RepoId),
670 /// Nodes associated with one exact service stable key.
671 Service(String),
672 /// Every node in the selected workspace.
673 Workspace,
674}
675
676/// Explicit weight assigned to one relationship kind during community detection.
677#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
678pub struct CommunityEdgeWeight {
679 /// Relationship kind.
680 pub kind: EdgeKind,
681 /// Finite non-negative weight.
682 pub weight: f64,
683}
684
685/// Versioned reproducibility inputs for one community analysis.
686#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
687pub struct CommunityConfig {
688 /// Selected algorithm.
689 pub algorithm: CommunityAlgorithm,
690 /// Analysis scope.
691 pub scope: CommunityScope,
692 /// Seed used to break otherwise equal deterministic choices.
693 pub seed: u64,
694 /// Positive Louvain resolution.
695 pub resolution: f64,
696 /// Minimum accepted edge confidence.
697 pub minimum_confidence: f32,
698 /// Explicit relationship weights; unspecified kinds use weight one.
699 pub edge_weights: Vec<CommunityEdgeWeight>,
700 /// Maximum optimization passes.
701 pub max_iterations: u32,
702}
703
704/// Quantitative properties of one detected community.
705#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
706pub struct CommunityMetrics {
707 /// Number of member nodes.
708 pub size: usize,
709 /// Internal edges divided by possible directed edges.
710 pub density: f64,
711 /// Internal accepted edge weight.
712 pub cohesion: f64,
713 /// Accepted edge weight crossing the community boundary.
714 pub coupling: f64,
715 /// Number of accepted cross-community edges.
716 pub cross_community_edges: usize,
717}
718
719/// Evidence explaining a deterministic community label.
720#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
721pub struct CommunityLabelEvidence {
722 /// Source node whose structure contributed a term.
723 pub node_id: NodeId,
724 /// Bounded normalized term.
725 pub term: String,
726}
727
728/// One versioned and explainable graph community.
729#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
730pub struct Community {
731 /// Stable content-derived community identity.
732 pub id: CommunityId,
733 /// Deterministic structural label.
734 pub label: String,
735 /// Sorted member node identifiers.
736 pub members: Vec<NodeId>,
737 /// Central nodes sorted by descending centrality and stable identity.
738 pub central_nodes: Vec<NodeId>,
739 /// Repository identities represented by members.
740 pub repositories: Vec<RepoId>,
741 /// Service node identities represented by members.
742 pub services: Vec<NodeId>,
743 /// Contract nodes receiving edges from outside the community.
744 pub inbound_contracts: Vec<NodeId>,
745 /// Contract nodes sending edges outside the community.
746 pub outbound_contracts: Vec<NodeId>,
747 /// Quantitative graph metrics.
748 pub metrics: CommunityMetrics,
749 /// Structural evidence used to derive the label.
750 pub label_evidence: Vec<CommunityLabelEvidence>,
751 /// Explicit limitations or incomplete inputs.
752 pub limitations: Vec<String>,
753}
754
755/// Complete community analysis tied to one immutable graph snapshot.
756#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
757pub struct CommunitySnapshot {
758 /// Graph snapshot analyzed.
759 pub snapshot_id: String,
760 /// Community engine semantic version.
761 pub engine_version: String,
762 /// Reproducibility configuration.
763 pub config: CommunityConfig,
764 /// Sorted detected communities.
765 pub communities: Vec<Community>,
766}
767
768/// Material relationship between communities in two snapshots.
769#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
770#[serde(rename_all = "snake_case")]
771pub enum CommunityChangeKind {
772 /// Community exists only in the newer snapshot.
773 Created,
774 /// Community exists only in the older snapshot.
775 Removed,
776 /// One older community materially maps to multiple newer communities.
777 Split,
778 /// Multiple older communities materially map to one newer community.
779 Merged,
780 /// Best matching community retained identity but changed materially.
781 MateriallyChanged,
782}
783
784/// Explainable community change between immutable snapshots.
785#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
786pub struct CommunityChange {
787 /// Classified change.
788 pub kind: CommunityChangeKind,
789 /// Related older community identities.
790 pub before: Vec<CommunityId>,
791 /// Related newer community identities.
792 pub after: Vec<CommunityId>,
793 /// Maximum member Jaccard overlap supporting the classification.
794 pub overlap: f64,
795 /// Bounded deterministic explanation.
796 pub explanation: String,
797}
798
799/// Community delta between two snapshots.
800#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
801pub struct CommunityDelta {
802 /// Older graph snapshot.
803 pub before_snapshot_id: String,
804 /// Newer graph snapshot.
805 pub after_snapshot_id: String,
806 /// Sorted material changes.
807 pub changes: Vec<CommunityChange>,
808}
809
810/// Versioned result envelope shared by CLI and MCP.
811#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
812pub struct ToolEnvelope<T> {
813 /// Public schema version.
814 pub schema_version: u32,
815 /// Result status.
816 pub status: ToolStatus,
817 /// Typed result data.
818 pub data: Option<T>,
819 /// Freshness of inputs used to produce the result.
820 pub freshness: FreshnessSummary,
821 /// Non-fatal warnings.
822 pub warnings: Vec<String>,
823}
824
825#[cfg(test)]
826mod tests {
827 use super::{contains_unsafe_metadata_characters, stable_id};
828
829 #[test]
830 fn stable_id_should_repeat_for_identical_input() {
831 let first = stable_id("repo", "ssh://example.test/team/api");
832 let second = stable_id("repo", "ssh://example.test/team/api");
833
834 assert_eq!(first, second);
835 }
836
837 #[test]
838 fn stable_id_should_separate_namespaces() {
839 let repository = stable_id("repo", "shared-key");
840 let service = stable_id("service", "shared-key");
841
842 assert_ne!(repository, service);
843 }
844
845 #[test]
846 fn unsafe_metadata_should_detect_terminal_and_bidi_controls() {
847 assert!(contains_unsafe_metadata_characters(
848 "trusted\u{202e}txt.exe"
849 ));
850 assert!(contains_unsafe_metadata_characters("line\nbreak"));
851 assert!(!contains_unsafe_metadata_characters(
852 "servicio-áccounts_日本"
853 ));
854 }
855}