relay_knowledge/domain/code/workspace/mod.rs
1//! Defines monorepo workspace discovery contracts and validation.
2
3use serde::{Deserialize, Serialize};
4
5use super::{DomainError, error::required_text};
6
7/// Recognised monorepo workspace manifest formats.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum CodeMonorepoWorkspaceFormat {
11 /// pnpm workspace: `pnpm-workspace.yaml` or `package.json` with `workspaces` field.
12 Pnpm,
13 /// Go multi-module workspace: `go.work` or multiple `go.mod` files.
14 GoModules,
15 /// Rust workspace: `Cargo.toml` with a `[workspace]` section.
16 CargoWorkspace,
17}
18
19/// A detected monorepo workspace that groups multiple packages under a common root.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct CodeMonorepoWorkspace {
22 /// Format of the workspace manifest.
23 pub format: CodeMonorepoWorkspaceFormat,
24 /// Absolute path to the workspace root directory on the canonical host.
25 pub root_path: String,
26 /// Absolute path to the workspace definition file (e.g. `pnpm-workspace.yaml`, `go.work`).
27 pub workspace_file_path: String,
28 /// Packages discovered inside the workspace.
29 pub members: Vec<CodeWorkspaceMember>,
30}
31
32impl CodeMonorepoWorkspace {
33 /// Validates that the workspace contains at least two member packages
34 /// and that every required text field is non-empty after trimming.
35 pub fn validate(&self) -> Result<(), DomainError> {
36 let _ = required_text("root_path", &self.root_path)?;
37 let _ = required_text("workspace_file_path", &self.workspace_file_path)?;
38
39 if self.members.len() < 2 {
40 return Err(DomainError::invalid(
41 "members",
42 "monorepo workspace must contain at least 2 member packages",
43 ));
44 }
45
46 for member in &self.members {
47 let _ = required_text("member.package_name", &member.package_name)?;
48 let _ = required_text("member.relative_path", &member.relative_path)?;
49 }
50
51 Ok(())
52 }
53}
54
55/// A single package member inside a monorepo workspace.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct CodeWorkspaceMember {
58 /// Canonical package name as declared in the manifest (e.g. `@scope/pkg`, `gosdk`).
59 pub package_name: String,
60 /// Relative path from the workspace root to this package directory.
61 pub relative_path: String,
62}
63
64/// Maps a workspace member's package name to an indexed repository scope.
65///
66/// This is the bridge record that the cross-repo resolver uses to translate
67/// an unresolved import module into a candidate source scope and repository
68/// after workspace detection has grouped the packages.
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct CodeWorkspacePackageMapping {
71 /// Package name as discovered from the workspace manifest.
72 pub package_name: String,
73 /// Target ecosystem derived from the workspace format (e.g. `"go"`, `"rust"`, `"npm"`).
74 pub ecosystem: String,
75 /// Repository identifier the indexed scope belongs to.
76 pub repository_id: String,
77 /// Source scope the resolved target lives in (`git_snapshot:<hash>`).
78 pub source_scope: String,
79 /// Confidence in basis points (0–10 000) that this mapping is correct.
80 pub confidence_basis_points: u32,
81}
82
83/// Configuration controlling automated monorepo workspace detection.
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct CodeWorkspaceDetectionConfig {
86 /// Whether automatic workspace detection is active.
87 pub enabled: bool,
88 /// Workspace manifest formats that the detector should look for.
89 pub supported_formats: Vec<CodeMonorepoWorkspaceFormat>,
90}
91
92impl CodeWorkspaceDetectionConfig {
93 /// Returns a disabled configuration that still records the supported
94 /// formats to use when callers opt in.
95 pub fn disabled() -> Self {
96 Self {
97 enabled: false,
98 supported_formats: Self::all_supported_formats(),
99 }
100 }
101
102 /// Enables workspace detection for every supported manifest format.
103 pub fn enabled_all() -> Self {
104 Self {
105 enabled: true,
106 supported_formats: Self::all_supported_formats(),
107 }
108 }
109
110 fn all_supported_formats() -> Vec<CodeMonorepoWorkspaceFormat> {
111 vec![
112 CodeMonorepoWorkspaceFormat::Pnpm,
113 CodeMonorepoWorkspaceFormat::GoModules,
114 CodeMonorepoWorkspaceFormat::CargoWorkspace,
115 ]
116 }
117}
118
119impl Default for CodeWorkspaceDetectionConfig {
120 fn default() -> Self {
121 Self::disabled()
122 }
123}
124
125#[cfg(test)]
126mod mod_tests;