Skip to main content

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>` with
78    /// an optional canonical `workspace-v1:<mask>` semantic suffix).
79    pub source_scope: String,
80    /// Confidence in basis points (0–10 000) that this mapping is correct.
81    pub confidence_basis_points: u32,
82}
83
84/// Configuration controlling automated monorepo workspace detection.
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct CodeWorkspaceDetectionConfig {
87    /// Whether automatic workspace detection is active.
88    pub enabled: bool,
89    /// Workspace manifest formats that the detector should look for.
90    pub supported_formats: Vec<CodeMonorepoWorkspaceFormat>,
91}
92
93impl CodeWorkspaceDetectionConfig {
94    /// Returns a disabled configuration that still records the supported
95    /// formats to use when callers opt in.
96    pub fn disabled() -> Self {
97        Self {
98            enabled: false,
99            supported_formats: Self::all_supported_formats(),
100        }
101    }
102
103    /// Enables workspace detection for every supported manifest format.
104    pub fn enabled_all() -> Self {
105        Self {
106            enabled: true,
107            supported_formats: Self::all_supported_formats(),
108        }
109    }
110
111    fn all_supported_formats() -> Vec<CodeMonorepoWorkspaceFormat> {
112        vec![
113            CodeMonorepoWorkspaceFormat::Pnpm,
114            CodeMonorepoWorkspaceFormat::GoModules,
115            CodeMonorepoWorkspaceFormat::CargoWorkspace,
116        ]
117    }
118}
119
120impl Default for CodeWorkspaceDetectionConfig {
121    fn default() -> Self {
122        Self::disabled()
123    }
124}
125
126#[cfg(test)]
127#[path = "mod_tests.rs"]
128mod mod_tests;