Skip to main content

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}