use std::{
collections::BTreeMap,
fmt::{Display, Formatter},
num::{NonZeroU32, NonZeroUsize},
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
use camino::{Utf8Path, Utf8PathBuf};
use globset::Glob;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::diagnostic::{Diagnostic, Severity};
const NAME_MAX_BYTES: usize = 128;
const OWNER_MAX_BYTES: usize = 64;
const TASK_MAX_BYTES: usize = 64;
const SCHEMA_MAX_BYTES: usize = 96;
const PATH_MAX_BYTES: usize = 512;
const COMMAND_PART_MAX_BYTES: usize = 256;
const COMMAND_ARG_LIMIT: usize = 64;
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum RepoLayout {
Functional,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct RepoName(String);
impl RepoName {
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
validate_ascii_identifier("repo name", &value, NAME_MAX_BYTES, false)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for RepoName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct ProjectName(String);
impl ProjectName {
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
validate_ascii_identifier("project name", &value, NAME_MAX_BYTES, true)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for ProjectName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct WorkspaceName(String);
impl WorkspaceName {
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
validate_ascii_identifier("workspace name", &value, NAME_MAX_BYTES, false)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for WorkspaceName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct OwnerHandle(String);
impl OwnerHandle {
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
if !value.starts_with('@') {
return Err(Diagnostic::error(
"manifest.owner.invalid",
"owner handle must start with @",
));
}
validate_ascii_identifier("owner handle", &value[1..], OWNER_MAX_BYTES - 1, false)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for OwnerHandle {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct TaskName(String);
impl TaskName {
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
validate_ascii_identifier("task name", &value, TASK_MAX_BYTES, false)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for TaskName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct SchemaId(String);
impl SchemaId {
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
if value.is_empty() || value.len() > SCHEMA_MAX_BYTES {
return Err(Diagnostic::error(
"manifest.schema.invalid",
"schema id must be non-empty and length-bounded",
));
}
if !value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'/' | b'-' | b'_'))
{
return Err(Diagnostic::error(
"manifest.schema.invalid",
"schema id contains unsupported characters",
));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for SchemaId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct ProtoPackageName(String);
impl ProtoPackageName {
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
validate_ascii_identifier("proto package", &value, NAME_MAX_BYTES, true)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for ProtoPackageName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct RepoRelativePath(String);
impl RepoRelativePath {
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
normalize_relative_path(value.into(), false).map(Self)
}
pub fn root() -> Self {
Self(".".to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn join_project(&self, child: &ProjectRelativePath) -> Result<Self, Diagnostic> {
if child.as_str() == "." {
return Ok(self.clone());
}
let joined = if self.0 == "." {
child.as_str().to_string()
} else {
format!("{}/{}", self.0, child.as_str())
};
Self::new(joined)
}
pub fn starts_with(&self, prefix: &RepoRelativePath) -> bool {
self.0 == prefix.0 || prefix.0 == "." || self.0.starts_with(&format!("{}/", prefix.0))
}
}
impl Display for RepoRelativePath {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl Default for RepoRelativePath {
fn default() -> Self {
Self::root()
}
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct ProjectRelativePath(String);
impl ProjectRelativePath {
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
normalize_relative_path(value.into(), true).map(Self)
}
pub fn root() -> Self {
Self(".".to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for ProjectRelativePath {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct RepoGlob(String);
impl RepoGlob {
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
validate_path_text(&value, true)?;
Glob::new(&value).map_err(|source| {
Diagnostic::error(
"manifest.glob.invalid",
format!("invalid glob pattern `{value}`: {source}"),
)
})?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for RepoGlob {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RepoRoot {
pub absolute: Utf8PathBuf,
}
impl RepoRoot {
pub fn new(path: Utf8PathBuf) -> Result<Self, Diagnostic> {
if !Utf8Path::new(path.as_str()).is_absolute() {
return Err(Diagnostic::error(
"repo.root.relative",
"repo root must be an absolute path",
));
}
Ok(Self { absolute: path })
}
pub fn join(&self, path: &RepoRelativePath) -> Utf8PathBuf {
if path.as_str() == "." {
self.absolute.clone()
} else {
self.absolute.join(path.as_str())
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProjectKind {
App,
Framework,
FoundationService,
ProtoRoot,
CoreInfra,
CoreInfraComponent,
Tool,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Toolchain {
Cargo,
Npm,
Pnpm,
Yarn,
Bun,
Uv,
Custom(String),
}
impl Toolchain {
pub fn parse(value: &str) -> Result<Self, Diagnostic> {
if value.is_empty() || value.len() > NAME_MAX_BYTES {
return Err(Diagnostic::error(
"manifest.workspace.toolchain",
"toolchain must be non-empty and length-bounded",
));
}
if !value
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
{
return Err(Diagnostic::error(
"manifest.workspace.toolchain",
"toolchain contains unsupported characters",
));
}
Ok(match value {
"cargo" => Self::Cargo,
"npm" => Self::Npm,
"pnpm" => Self::Pnpm,
"yarn" => Self::Yarn,
"bun" => Self::Bun,
"uv" => Self::Uv,
custom => Self::Custom(custom.to_string()),
})
}
pub fn as_str(&self) -> &str {
match self {
Self::Cargo => "cargo",
Self::Npm => "npm",
Self::Pnpm => "pnpm",
Self::Yarn => "yarn",
Self::Bun => "bun",
Self::Uv => "uv",
Self::Custom(value) => value.as_str(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum WorkspaceLanguage {
Rust,
#[serde(rename = "typescript")]
TypeScript,
Python,
Proto,
Iac,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Visibility {
#[default]
Internal,
Public,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum IacProvider {
Pulumi,
Terraform,
#[serde(rename = "opentofu")]
OpenTofu,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GeneratedCodePolicy {
#[default]
ConsumerLocal,
}
#[derive(
Clone, Copy, Debug, Default, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
)]
#[serde(rename_all = "kebab-case")]
pub enum CodeSizeScope {
#[default]
All,
Changed,
Affected,
}
#[derive(
Clone, Copy, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
)]
#[serde(rename_all = "kebab-case")]
pub enum CodeLanguage {
Rust,
#[serde(rename = "typescript")]
TypeScript,
Python,
}
#[derive(
Clone, Copy, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
)]
#[serde(rename_all = "kebab-case")]
pub enum CodeSizeRuleKind {
File,
Function,
Block,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum InspectionFailOn {
#[default]
Never,
Error,
Warning,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GeneratedCodeInspectionMode {
#[default]
Skip,
Inspect,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeSizeInspectionRequest {
pub repo: Option<PathBuf>,
pub scope: CodeSizeScope,
pub base: Option<String>,
pub head: Option<String>,
pub changed_files: Vec<RepoRelativePath>,
pub include_transitive: bool,
pub languages: Vec<CodeLanguage>,
pub rules: Vec<CodeSizeRuleKind>,
pub fail_on: InspectionFailOn,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeSizeInspectionReport {
pub scope: CodeSizeScope,
#[serde(skip_serializing_if = "Option::is_none")]
pub base: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub head: Option<String>,
pub summary: CodeSizeInspectionSummary,
pub config: CodeSizeResolvedConfigSummary,
pub findings: Vec<CodeSizeFinding>,
pub diagnostics: Vec<Diagnostic>,
pub skipped: Vec<CodeSizeSkippedReason>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeSizeInspectionSummary {
pub files_considered: u64,
pub files_scanned: u64,
pub files_skipped: u64,
pub files_errored: u64,
pub finding_count: u64,
pub files_with_findings: u64,
pub duration_millis: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeSizeResolvedConfigSummary {
pub enabled: bool,
pub generated_code: GeneratedCodeInspectionMode,
pub max_files: NonZeroUsize,
pub max_file_bytes: NonZeroUsize,
pub rules: CodeSizeRuleConfigSet,
}
impl Default for CodeSizeResolvedConfigSummary {
fn default() -> Self {
let config = CodeSizeConfig::default();
Self {
enabled: config.enabled,
generated_code: config.generated_code,
max_files: config.max_files,
max_file_bytes: config.max_file_bytes,
rules: config.rules,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeSizeFinding {
pub rule: CodeSizeRuleKind,
pub severity: Severity,
pub path: RepoRelativePath,
#[serde(skip_serializing_if = "Option::is_none")]
pub project: Option<ProjectName>,
pub language: CodeLanguage,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub node_kind: Option<String>,
pub start_line: NonZeroU32,
pub end_line: NonZeroU32,
pub measured_lines: NonZeroU32,
#[serde(skip_serializing_if = "Option::is_none")]
pub physical_lines: Option<NonZeroU32>,
pub limit: NonZeroU32,
pub message: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeSizeSkippedReason {
pub reason: String,
pub count: u64,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RepoInspectionConfig {
pub code_size: CodeSizeConfig,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeSizeConfig {
pub enabled: bool,
pub generated_code: GeneratedCodeInspectionMode,
pub max_files: NonZeroUsize,
pub max_file_bytes: NonZeroUsize,
pub rules: CodeSizeRuleConfigSet,
pub languages: BTreeMap<CodeLanguage, CodeLanguageConfig>,
pub excludes: Vec<RepoGlob>,
pub overrides: Vec<CodeSizeOverride>,
}
impl Default for CodeSizeConfig {
fn default() -> Self {
Self {
enabled: true,
generated_code: GeneratedCodeInspectionMode::Skip,
max_files: nonzero_usize(50_000),
max_file_bytes: nonzero_usize(2_000_000),
rules: CodeSizeRuleConfigSet::default(),
languages: default_code_size_languages(),
excludes: default_code_size_excludes(),
overrides: Vec::new(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeLanguageConfig {
pub enabled: bool,
}
impl Default for CodeLanguageConfig {
fn default() -> Self {
Self { enabled: true }
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeSizeRuleConfigSet {
pub file: CodeSizeRuleConfig,
pub function: CodeSizeRuleConfig,
pub block: CodeSizeRuleConfig,
}
impl Default for CodeSizeRuleConfigSet {
fn default() -> Self {
Self {
file: CodeSizeRuleConfig {
enabled: true,
max_lines: nonzero_u32(1_000),
severity: Severity::Warning,
include_tests: false,
},
function: CodeSizeRuleConfig {
enabled: true,
max_lines: nonzero_u32(250),
severity: Severity::Warning,
include_tests: true,
},
block: CodeSizeRuleConfig {
enabled: true,
max_lines: nonzero_u32(50),
severity: Severity::Warning,
include_tests: true,
},
}
}
}
impl CodeSizeRuleConfigSet {
pub fn get(&self, rule: CodeSizeRuleKind) -> &CodeSizeRuleConfig {
match rule {
CodeSizeRuleKind::File => &self.file,
CodeSizeRuleKind::Function => &self.function,
CodeSizeRuleKind::Block => &self.block,
}
}
pub fn get_mut(&mut self, rule: CodeSizeRuleKind) -> &mut CodeSizeRuleConfig {
match rule {
CodeSizeRuleKind::File => &mut self.file,
CodeSizeRuleKind::Function => &mut self.function,
CodeSizeRuleKind::Block => &mut self.block,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeSizeRuleConfig {
pub enabled: bool,
pub max_lines: NonZeroU32,
pub severity: Severity,
pub include_tests: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeSizeOverride {
pub paths: Vec<RepoGlob>,
pub rules: CodeSizeRuleConfigPatchSet,
pub reason: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeSizeRuleConfigPatchSet {
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<CodeSizeRuleConfigPatch>,
#[serde(skip_serializing_if = "Option::is_none")]
pub function: Option<CodeSizeRuleConfigPatch>,
#[serde(skip_serializing_if = "Option::is_none")]
pub block: Option<CodeSizeRuleConfigPatch>,
}
impl CodeSizeRuleConfigPatchSet {
pub fn get(&self, rule: CodeSizeRuleKind) -> Option<&CodeSizeRuleConfigPatch> {
match rule {
CodeSizeRuleKind::File => self.file.as_ref(),
CodeSizeRuleKind::Function => self.function.as_ref(),
CodeSizeRuleKind::Block => self.block.as_ref(),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeSizeRuleConfigPatch {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_lines: Option<NonZeroU32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub severity: Option<Severity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_tests: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RepoManifest {
pub schema: SchemaId,
pub name: RepoName,
pub layout: RepoLayout,
pub default_owner: Option<OwnerHandle>,
pub protos_root: RepoRelativePath,
pub core_infra_root: RepoRelativePath,
pub agent_skills_root: RepoRelativePath,
pub claude_skills_root: RepoRelativePath,
pub context_output: RepoRelativePath,
pub generated_code_policy: GeneratedCodePolicy,
pub policies: RepoPolicySet,
#[serde(default)]
pub inspection: RepoInspectionConfig,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RepoPolicySet {
pub cross_app_dependency: PolicyMode,
pub framework_internal_dependency: PolicyMode,
pub generated_code_direct_edit: PolicyMode,
pub prod_change_required_owners: Vec<OwnerHandle>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum PolicyMode {
#[default]
Deny,
Warn,
Allow,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceSpec {
pub name: WorkspaceName,
pub language: WorkspaceLanguage,
pub toolchain: Option<Toolchain>,
pub root: ProjectRelativePath,
pub manifest: ProjectRelativePath,
pub lockfile: Option<ProjectRelativePath>,
pub target_dir: Option<RepoRelativePath>,
pub cache_dir: Option<RepoRelativePath>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskCommand {
pub workspace: WorkspaceName,
pub command: CommandSpec,
pub depends_on: Vec<TaskDependency>,
}
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskDependency {
pub project: ProjectName,
pub workspace: WorkspaceName,
pub task: TaskName,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandSpec {
pub program: String,
pub args: Vec<String>,
}
impl CommandSpec {
pub fn parse(value: &str) -> Result<Self, Diagnostic> {
if value.trim().is_empty() || value.len() > COMMAND_PART_MAX_BYTES * COMMAND_ARG_LIMIT {
return Err(Diagnostic::error(
"manifest.command.invalid",
"command must be non-empty and length-bounded",
));
}
reject_shell_syntax(value)?;
let parts = split_command(value)?;
if parts.is_empty() {
return Err(Diagnostic::error(
"manifest.command.invalid",
"command must include a program",
));
}
if parts.len() > COMMAND_ARG_LIMIT {
return Err(Diagnostic::error(
"manifest.command.too_many_args",
"command has too many arguments",
));
}
for part in &parts {
if part.len() > COMMAND_PART_MAX_BYTES {
return Err(Diagnostic::error(
"manifest.command.part_too_long",
"command part exceeds byte limit",
));
}
}
let mut parts = parts.into_iter();
let program = parts.next().ok_or_else(|| {
Diagnostic::error("manifest.command.invalid", "command must include a program")
})?;
Ok(Self {
program,
args: parts.collect(),
})
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DependencySurface {
Unspecified,
FrameworkFacade,
FrameworkInternal,
FoundationPublicClient,
FoundationInternal,
CoreInfraPublicModule,
CoreInfraInternalModule,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum DependencyTarget {
Project(ProjectName),
ProtoPackage(ProtoPackageName),
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectDependency {
pub id: String,
pub target: DependencyTarget,
pub surface: DependencySurface,
}
impl ProjectDependency {
pub fn parse(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
if value.starts_with("protos.") {
return Ok(Self {
target: DependencyTarget::ProtoPackage(ProtoPackageName::new(value.clone())?),
id: value,
surface: DependencySurface::Unspecified,
});
}
let (target, surface) = if let Some(stripped) = value.strip_suffix(".client") {
(
ProjectName::new(stripped.to_string())?,
DependencySurface::FoundationPublicClient,
)
} else if let Some(stripped) = value.strip_suffix(".internal") {
let surface = if stripped.starts_with("frameworks.") {
DependencySurface::FrameworkInternal
} else if stripped.starts_with("foundations.") {
DependencySurface::FoundationInternal
} else {
DependencySurface::Unspecified
};
(ProjectName::new(stripped.to_string())?, surface)
} else if let Some(stripped) = value.strip_suffix(".facade") {
(
ProjectName::new(stripped.to_string())?,
DependencySurface::FrameworkFacade,
)
} else {
(
ProjectName::new(value.clone())?,
DependencySurface::Unspecified,
)
};
Ok(Self {
id: value,
target: DependencyTarget::Project(target),
surface,
})
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectProtoSpec {
pub owns: Vec<RepoGlob>,
pub consumes: Vec<RepoGlob>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IacSpec {
pub root: ProjectRelativePath,
pub provider: IacProvider,
pub stacks: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DeploySpec {
pub root: ProjectRelativePath,
pub environments: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectDnsSpec {
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
pub records: Vec<DnsRecordSpec>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DnsRecordSpec {
pub name: String,
pub record_type: String,
pub target: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub proxied: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<u32>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CdnSpec {
pub provider: String,
pub aliases: Vec<String>,
pub expected_response_headers: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectOpsSpec {
pub probes: Vec<ProbeSpec>,
pub runtime_dependencies: Vec<RuntimeDependencySpec>,
pub manual_state: Vec<ManualStateRecord>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProbeSpec {
pub name: String,
pub method: String,
pub url: String,
pub expect: ProbeExpectation,
#[serde(skip_serializing_if = "Option::is_none")]
pub classification: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProbeExpectation {
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<u16>,
pub headers: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body_contains: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeDependencySpec {
pub project: ProjectName,
pub endpoint: String,
pub purpose: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ManualStateRecord {
pub kind: String,
pub resource: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub managed_equivalent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cleanup_command: Option<ProcessCommand>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectAiSpec {
pub editable: Vec<RepoGlob>,
pub do_not_edit: Vec<RepoGlob>,
pub docs: Vec<ProjectRelativePath>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectAreas {
pub public_facades: BTreeMap<String, Vec<ProjectRelativePath>>,
pub public_clients: BTreeMap<String, Vec<ProjectRelativePath>>,
pub internal: BTreeMap<String, Vec<ProjectRelativePath>>,
pub public_modules: Vec<ProjectRelativePath>,
pub internal_modules: Vec<ProjectRelativePath>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectManifest {
pub schema: SchemaId,
pub name: ProjectName,
pub kind: ProjectKind,
pub path: RepoRelativePath,
pub owners: Vec<OwnerHandle>,
pub visibility: Visibility,
pub workspaces: Vec<WorkspaceSpec>,
pub depends_on: Vec<ProjectDependency>,
pub tasks: BTreeMap<TaskName, Vec<TaskCommand>>,
pub iac: Option<IacSpec>,
pub deploy: Option<DeploySpec>,
pub dns: ProjectDnsSpec,
pub cdn: Option<CdnSpec>,
pub ops: ProjectOpsSpec,
pub protos: ProjectProtoSpec,
pub ai: ProjectAiSpec,
pub areas: ProjectAreas,
pub policies: BTreeMap<String, serde_json::Value>,
pub source: RepoRelativePath,
}
impl ProjectManifest {
pub fn node_id(&self) -> String {
format!("project:{}", self.name)
}
pub fn contains_path(&self, path: &RepoRelativePath) -> bool {
path.starts_with(&self.path)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TemplateManifest {
pub schema: SchemaId,
pub name: String,
pub kind: String,
pub engine: String,
pub inputs: Vec<TemplateInput>,
pub files: Vec<TemplateFile>,
pub post_render_validate: Vec<CommandSpec>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum TemplateSource {
Builtin {
name: String,
},
Local {
root: RepoRelativePath,
},
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolvedTemplateSource {
pub root: RepoRelativePath,
pub manifest: TemplateManifest,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TemplateInput {
pub name: String,
pub input_type: String,
pub required: bool,
pub default: Option<serde_json::Value>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TemplateFile {
pub source: ProjectRelativePath,
pub target: String,
pub mode: String,
pub when: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GraphNodeKind {
Project,
Workspace,
ProtoPackage,
IacTarget,
Template,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GraphNode {
pub id: String,
pub kind: GraphNodeKind,
pub label: String,
pub project: Option<ProjectName>,
pub workspace: Option<WorkspaceName>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum EdgeKind {
DependsOnProject,
ContainsWorkspace,
ConsumesProto,
OwnsProto,
UsesFrameworkFacade,
UsesFrameworkInternal,
UsesFoundationClient,
UsesFoundationInternal,
UsesCoreInfraModule,
UsesCoreInfraInternalModule,
OwnsIac,
RunsTask,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GraphEdge {
pub from: String,
pub to: String,
pub kind: EdgeKind,
pub evidence: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RepoGraph {
pub nodes: Vec<GraphNode>,
pub edges: Vec<GraphEdge>,
}
impl RepoGraph {
pub fn add_node(&mut self, node: GraphNode) {
if !self.nodes.iter().any(|existing| existing.id == node.id) {
self.nodes.push(node);
}
}
pub fn add_edge(&mut self, edge: GraphEdge) {
if !self.edges.iter().any(|existing| existing == &edge) {
self.edges.push(edge);
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RepoSnapshot {
pub root: RepoRoot,
pub repo_manifest: RepoManifest,
pub projects: Vec<ProjectManifest>,
pub graph: RepoGraph,
pub generated_policy: GeneratedCodePolicy,
pub discovered_at_unix: u64,
}
impl RepoSnapshot {
pub fn new(
root: RepoRoot,
repo_manifest: RepoManifest,
projects: Vec<ProjectManifest>,
graph: RepoGraph,
) -> Self {
let discovered_at_unix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |duration| duration.as_secs());
let generated_policy = repo_manifest.generated_code_policy.clone();
Self {
root,
repo_manifest,
projects,
graph,
generated_policy,
discovered_at_unix,
}
}
pub fn project(&self, name: &ProjectName) -> Option<&ProjectManifest> {
self.projects.iter().find(|project| &project.name == name)
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoverRequest {
pub repo: Option<PathBuf>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GraphValidateRequest {
pub repo: Option<PathBuf>,
pub changed_files: Vec<RepoRelativePath>,
pub mode: ValidationMode,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ValidationMode {
#[default]
Structural,
Metadata,
Full,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GraphPrintRequest {
pub repo: Option<PathBuf>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GraphPrintReport {
pub snapshot: RepoSnapshot,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExplainRequest {
pub repo: Option<PathBuf>,
pub selector: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ExplainReport {
pub selector: String,
pub nodes: Vec<GraphNode>,
pub edges: Vec<GraphEdge>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BoundaryLintRequest {
pub repo: Option<PathBuf>,
pub changed_files: Vec<RepoRelativePath>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BoundaryLintReport {
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum InitProfile {
Startup,
Enterprise,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InitRequest {
pub repo_root: PathBuf,
pub name: RepoName,
pub profile: InitProfile,
pub layout: RepoLayout,
pub dry_run: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FileOperation {
pub path: RepoRelativePath,
pub operation: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InitPlan {
pub operations: Vec<FileOperation>,
pub warnings: Vec<Diagnostic>,
pub next_steps: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NewProjectRequest {
pub repo: Option<PathBuf>,
pub kind: ProjectKind,
pub path: RepoRelativePath,
pub stack: Vec<String>,
pub languages: Vec<String>,
pub clients: Vec<String>,
pub facade: bool,
pub iac: Option<IacProvider>,
pub proto: Option<ProtoPackageName>,
pub owner: Option<OwnerHandle>,
pub dry_run: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RenderPlan {
pub operations: Vec<FileOperation>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TemplateListRequest {
pub repo: Option<PathBuf>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TemplateSummary {
pub source: String,
pub name: String,
pub kind: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TemplateListReport {
pub templates: Vec<TemplateSummary>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TemplateRenderRequest {
pub repo: Option<PathBuf>,
pub source: TemplateSource,
pub inputs: serde_json::Value,
pub dry_run: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AffectedRequest {
pub repo: Option<PathBuf>,
pub base: Option<String>,
pub head: Option<String>,
pub changed_files: Vec<RepoRelativePath>,
pub tasks: Vec<TaskName>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AffectedReport {
pub directly_affected: Vec<ProjectName>,
pub transitively_affected: Vec<ProjectName>,
pub workspaces: Vec<String>,
pub tasks: Vec<String>,
pub risk_flags: Vec<String>,
pub reasons: Vec<AffectedReason>,
pub suggested_reviewers: Vec<OwnerHandle>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AffectedReason {
pub source: String,
pub target: String,
pub reason: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskRunRequest {
pub repo: Option<PathBuf>,
pub tasks: Vec<TaskName>,
pub projects: Vec<ProjectName>,
pub workspaces: Vec<String>,
pub affected: bool,
pub changed_files: Vec<RepoRelativePath>,
pub base: Option<String>,
pub head: Option<String>,
pub concurrency: Option<NonZeroU32>,
pub dry_run: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskRunPlan {
pub commands: Vec<ProcessCommand>,
pub concurrency: NonZeroUsize,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskRunReport {
pub commands: Vec<ProcessCommand>,
pub outputs: Vec<TaskCommandOutput>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskCommandOutput {
pub project: ProjectName,
pub workspace: WorkspaceName,
pub task: TaskName,
pub output: ProcessOutput,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessCommand {
#[serde(skip_serializing_if = "Option::is_none")]
pub project: Option<ProjectName>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace: Option<WorkspaceName>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task: Option<TaskName>,
pub cwd: RepoRelativePath,
#[serde(skip_serializing_if = "Option::is_none")]
pub absolute_cwd: Option<PathBuf>,
pub program: String,
pub args: Vec<String>,
pub env: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessOutput {
pub status: i32,
pub stdout: String,
pub stderr: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CiMatrixRequest {
pub repo: Option<PathBuf>,
pub tasks: Vec<TaskName>,
pub changed_files: Vec<RepoRelativePath>,
pub base: Option<String>,
pub head: Option<String>,
pub fallback: CiFallback,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum CiFallback {
All,
#[default]
None,
Error,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CiMatrixReport {
pub entries: Vec<serde_json::Value>,
pub github_actions: serde_json::Value,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum CiProvider {
#[default]
GitHubActions,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CiWorkflowRequest {
pub repo: Option<PathBuf>,
pub provider: CiProvider,
pub write: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CiWorkflowReport {
pub path: RepoRelativePath,
pub content: String,
pub operations: Vec<FileOperation>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HygieneCheckRequest {
pub repo: Option<PathBuf>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HygieneCleanRequest {
pub repo: Option<PathBuf>,
pub dry_run: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HygieneReport {
pub diagnostics: Vec<Diagnostic>,
pub operations: Vec<FileOperation>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DependencyRewriteMode {
#[default]
Auto,
Off,
ReportOnly,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum AdoptionCiMode {
#[default]
Update,
Off,
ReportOnly,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum AdoptionOutputFormat {
#[default]
Human,
Json,
GitHubActions,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AdoptionPlanRequest {
pub source: PathBuf,
pub dest: PathBuf,
pub include: Vec<String>,
pub exclude: Vec<String>,
pub map: BTreeMap<String, RepoRelativePath>,
pub kind: BTreeMap<String, ProjectKind>,
pub owner: BTreeMap<String, OwnerHandle>,
pub rewrite_deps: DependencyRewriteMode,
pub ci: AdoptionCiMode,
pub verification: ValidationMode,
pub format: AdoptionOutputFormat,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AdoptionApplyRequest {
pub plan: PathBuf,
pub refresh: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AdoptionVerifyRequest {
pub plan: PathBuf,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AdoptionPlan {
pub source_root: Utf8PathBuf,
pub dest_root: Utf8PathBuf,
pub sources: Vec<AdoptedSource>,
pub operations: Vec<AdoptionFileOperation>,
pub dependency_rewrites: Vec<DependencyRewrite>,
pub manifest_syntheses: Vec<ProjectManifestSynthesis>,
pub ci_operations: Vec<FileOperation>,
pub verification: VerificationPlan,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AdoptedSource {
pub name: String,
pub source_path: Utf8PathBuf,
pub destination_path: RepoRelativePath,
pub inferred_kind: ProjectKind,
pub confidence: f32,
pub reasons: Vec<String>,
pub inventory: SourceInventory,
pub skipped: bool,
pub override_applied: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SourceInventory {
pub name: String,
pub has_vcs: bool,
pub readme_summary: Option<String>,
pub manifests: Vec<String>,
pub top_level_dirs: Vec<String>,
pub dependency_references: Vec<String>,
pub generated_artifacts: Vec<String>,
pub required_tools: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AdoptionFileOperation {
pub id: String,
pub operation: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_path: Option<Utf8PathBuf>,
pub destination_path: RepoRelativePath,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DependencyRewrite {
pub file: RepoRelativePath,
pub package: String,
pub from: String,
pub to: String,
pub surface: DependencySurface,
pub owner_project: ProjectName,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectManifestSynthesis {
pub source: String,
pub project: ProjectName,
pub manifest_path: RepoRelativePath,
pub content: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VerificationPlan {
pub prerequisites: Vec<ToolPrerequisite>,
pub commands: Vec<ProcessCommand>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolPrerequisite {
pub tool: String,
pub reason: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PrSummaryRequest {
pub repo: Option<PathBuf>,
pub base: Option<String>,
pub head: Option<String>,
pub changed_files: Vec<RepoRelativePath>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PrSummary {
pub markdown: String,
pub impact: serde_json::Value,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AiContextRequest {
pub repo: Option<PathBuf>,
pub project: ProjectName,
pub audience: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AiContext {
pub payload: serde_json::Value,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodegenCheckRequest {
pub repo: Option<PathBuf>,
pub base: Option<String>,
pub head: Option<String>,
pub changed_files: Vec<RepoRelativePath>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodegenCheckReport {
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProtoFacadeRequest {
pub repo: Option<PathBuf>,
pub operation: ProtoOperation,
#[serde(skip_serializing_if = "Option::is_none")]
pub selector: Option<String>,
pub base: Option<String>,
pub head: Option<String>,
pub changed_files: Vec<RepoRelativePath>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProtoOperation {
#[default]
Check,
Owners,
Consumers,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProtoFacadeReport {
pub owners: Vec<ProjectName>,
pub consumers: Vec<ProjectName>,
pub commands: Vec<ProcessCommand>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IacFacadeRequest {
pub repo: Option<PathBuf>,
pub affected: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub project: Option<ProjectName>,
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<String>,
pub core: bool,
pub base: Option<String>,
pub head: Option<String>,
pub changed_files: Vec<RepoRelativePath>,
pub dry_run: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IacFacadeReport {
pub commands: Vec<ProcessCommand>,
pub risk_flags: Vec<String>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpsPlanRequest {
pub repo: Option<PathBuf>,
pub base: Option<String>,
pub head: Option<String>,
pub changed_files: Vec<RepoRelativePath>,
pub environments: Vec<String>,
pub tasks: Vec<TaskName>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<PathBuf>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpsPlan {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub repo_root: Option<RepoRoot>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub head: Option<String>,
pub environments: Vec<String>,
pub affected: AffectedReport,
pub task_plan: TaskRunReport,
pub iac: Vec<IacOperation>,
pub dns: Vec<DnsOperation>,
pub cdn: Vec<CdnCheck>,
pub provider_capabilities: Vec<ProviderCapabilityReport>,
pub probes: Vec<ProbeSpec>,
pub manual_reconciliation: Vec<ManualStateRecord>,
pub required_env: Vec<String>,
pub production_gaps: Vec<String>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IacOperation {
#[serde(skip_serializing_if = "Option::is_none")]
pub project: Option<ProjectName>,
pub workspace: String,
pub provider: IacProvider,
pub environment: String,
pub stack: String,
pub preview_command: ProcessCommand,
#[serde(skip_serializing_if = "Option::is_none")]
pub apply_command: Option<ProcessCommand>,
pub risk: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DnsOperation {
pub zone: String,
pub provider: String,
pub record: String,
pub expected_target: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub expected_proxied: Option<bool>,
pub verification: Vec<ProcessCommand>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CdnCheck {
pub provider: String,
pub alias: String,
pub expected_response_headers: Vec<String>,
pub verification: ProcessCommand,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpsVerifyRequest {
pub repo: Option<PathBuf>,
pub plan: PathBuf,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpsVerifyReport {
pub commands: Vec<ProcessCommand>,
pub skipped_mutating_commands: Vec<ProcessCommand>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpsReconcileRequest {
pub repo: Option<PathBuf>,
pub plan: PathBuf,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpsReconcileReport {
pub records: Vec<ManualStateRecord>,
pub cleanup_commands: Vec<ProcessCommand>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderCapabilityRequest {
pub repo: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace: Option<String>,
pub base: Option<String>,
pub head: Option<String>,
pub changed_files: Vec<RepoRelativePath>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderCapabilityReport {
pub workspace: String,
pub package: String,
pub version: String,
pub resource: String,
pub field: String,
pub status: String,
pub advice: String,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionJournal {
pub id: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub plan_id: Option<String>,
pub entries: Vec<SessionEntry>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionEntry {
pub kind: String,
pub timestamp: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exit_status: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plan_id: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpsJournalRequest {
pub repo: Option<PathBuf>,
pub action: OpsJournalAction,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum OpsJournalAction {
Start {
name: String,
plan_id: Option<String>,
},
AddCommand {
session: String,
command: String,
exit_status: Option<i32>,
},
AddNote {
session: String,
note_kind: String,
message: String,
},
Summary {
session: String,
},
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpsJournalReport {
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub journal: Option<SessionJournal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub markdown: Option<String>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillsFacadeRequest {
pub repo: Option<PathBuf>,
pub sync: bool,
pub dry_run: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillsFacadeReport {
pub diagnostics: Vec<Diagnostic>,
}
pub fn validate_project_convention(project: &ProjectManifest) -> Vec<Diagnostic> {
let expected_prefix = match project.kind {
ProjectKind::App => "apps/",
ProjectKind::Framework => "frameworks/",
ProjectKind::FoundationService => "foundations/",
ProjectKind::ProtoRoot => "protos",
ProjectKind::CoreInfra => "core-infra",
ProjectKind::CoreInfraComponent => "core-infra/",
ProjectKind::Tool => "tools/",
};
let valid = match project.kind {
ProjectKind::ProtoRoot | ProjectKind::CoreInfra => project.path.as_str() == expected_prefix,
ProjectKind::App
| ProjectKind::Framework
| ProjectKind::FoundationService
| ProjectKind::CoreInfraComponent
| ProjectKind::Tool => project.path.as_str().starts_with(expected_prefix),
};
if valid {
Vec::new()
} else {
vec![
Diagnostic::error(
"manifest.project.path_convention",
format!(
"project `{}` of kind {:?} must live under `{expected_prefix}`",
project.name, project.kind
),
)
.with_path(project.source.as_str()),
]
}
}
pub fn utf8_path_buf(path: PathBuf) -> Result<Utf8PathBuf, Diagnostic> {
Utf8PathBuf::from_path_buf(path).map_err(|path| {
Diagnostic::error(
"path.non_utf8",
format!("path is not valid UTF-8: {}", path.display()),
)
})
}
fn validate_ascii_identifier(
label: &str,
value: &str,
max_bytes: usize,
allow_dot: bool,
) -> Result<(), Diagnostic> {
if value.is_empty() || value.len() > max_bytes {
return Err(Diagnostic::error(
"manifest.identifier.invalid",
format!("{label} must be non-empty and length-bounded"),
));
}
let mut previous_dot = false;
for byte in value.bytes() {
let is_dot = byte == b'.';
let allowed = byte.is_ascii_lowercase()
|| byte.is_ascii_digit()
|| matches!(byte, b'-' | b'_')
|| (allow_dot && is_dot);
if !allowed {
return Err(Diagnostic::error(
"manifest.identifier.invalid",
format!("{label} contains unsupported characters"),
));
}
if allow_dot && is_dot && previous_dot {
return Err(Diagnostic::error(
"manifest.identifier.invalid",
format!("{label} cannot contain consecutive dots"),
));
}
previous_dot = is_dot;
}
if allow_dot && (value.starts_with('.') || value.ends_with('.')) {
return Err(Diagnostic::error(
"manifest.identifier.invalid",
format!("{label} cannot start or end with a dot"),
));
}
Ok(())
}
fn normalize_relative_path(value: String, allow_root: bool) -> Result<String, Diagnostic> {
validate_path_text(&value, false)?;
if value == "." {
return if allow_root {
Ok(value)
} else {
Err(Diagnostic::error(
"manifest.path.invalid",
"repo-relative path cannot be the root path",
))
};
}
let mut parts = Vec::new();
for part in value.split('/') {
if part.is_empty() || part == "." {
continue;
}
if part == ".." {
return Err(Diagnostic::error(
"manifest.path.traversal",
"relative path cannot contain ..",
));
}
parts.push(part);
}
if parts.is_empty() {
return if allow_root {
Ok(".".to_string())
} else {
Err(Diagnostic::error(
"manifest.path.invalid",
"relative path must not be empty",
))
};
}
Ok(parts.join("/"))
}
fn validate_path_text(value: &str, allow_glob: bool) -> Result<(), Diagnostic> {
if value.is_empty() || value.len() > PATH_MAX_BYTES {
return Err(Diagnostic::error(
"manifest.path.invalid",
"path must be non-empty and length-bounded",
));
}
if value.contains('\0') || value.contains('\\') {
return Err(Diagnostic::error(
"manifest.path.invalid",
"path must not contain NUL bytes or platform separators",
));
}
if value.starts_with('/') {
return Err(Diagnostic::error(
"manifest.path.absolute",
"path must be relative",
));
}
if value
.split('/')
.any(|part| part == ".." || (!allow_glob && part.contains('*')))
{
return Err(Diagnostic::error(
"manifest.path.traversal",
"path must not contain traversal or unsupported glob segments",
));
}
Ok(())
}
fn reject_shell_syntax(value: &str) -> Result<(), Diagnostic> {
const REJECTED: [&str; 13] = [
"&&", "||", "|", ";", ">", "<", "$(", "`", "\n", "\r", "*", "?", "[",
];
if let Some(token) = REJECTED.iter().find(|token| value.contains(**token)) {
return Err(Diagnostic::error(
"manifest.command.shell_syntax",
format!("command uses shell-only syntax `{token}`"),
));
}
Ok(())
}
fn split_command(value: &str) -> Result<Vec<String>, Diagnostic> {
let mut parts = Vec::new();
let mut current = String::new();
let mut quote: Option<char> = None;
let mut escaped = false;
for character in value.chars() {
if escaped {
current.push(character);
escaped = false;
continue;
}
if character == '\\' {
escaped = true;
continue;
}
match quote {
Some(active) if character == active => quote = None,
None if character == '\'' || character == '"' => quote = Some(character),
None if character.is_whitespace() => {
if !current.is_empty() {
parts.push(std::mem::take(&mut current));
}
}
Some(_) | None => current.push(character),
}
}
if escaped || quote.is_some() {
return Err(Diagnostic::error(
"manifest.command.invalid",
"command contains an unfinished escape or quote",
));
}
if !current.is_empty() {
parts.push(current);
}
Ok(parts)
}
fn nonzero_u32(value: u32) -> NonZeroU32 {
NonZeroU32::new(value).unwrap_or(NonZeroU32::MIN)
}
fn nonzero_usize(value: usize) -> NonZeroUsize {
NonZeroUsize::new(value).unwrap_or(NonZeroUsize::MIN)
}
fn default_code_size_languages() -> BTreeMap<CodeLanguage, CodeLanguageConfig> {
[
(CodeLanguage::Rust, CodeLanguageConfig::default()),
(CodeLanguage::TypeScript, CodeLanguageConfig::default()),
(CodeLanguage::Python, CodeLanguageConfig::default()),
]
.into_iter()
.collect()
}
fn default_code_size_excludes() -> Vec<RepoGlob> {
[
"**/target/**",
"**/node_modules/**",
"**/dist/**",
"**/.next/**",
]
.into_iter()
.filter_map(|pattern| RepoGlob::new(pattern).ok())
.collect()
}
#[cfg(test)]
mod tests {
use super::{
CommandSpec, OwnerHandle, ProjectDependency, ProjectRelativePath, RepoRelativePath,
};
#[test]
fn test_should_parse_argv_command() {
let command = CommandSpec::parse("cargo check --workspace").expect("command parses");
assert_eq!(command.program, "cargo");
assert_eq!(command.args, ["check", "--workspace"]);
}
#[test]
fn test_should_reject_shell_syntax_in_command() {
let error = CommandSpec::parse("cargo check && rm -rf target").expect_err("rejects shell");
assert_eq!(error.code.as_ref(), "manifest.command.shell_syntax");
}
#[test]
fn test_should_reject_invalid_owner() {
let error = OwnerHandle::new("platform").expect_err("owner requires @");
assert_eq!(error.code.as_ref(), "manifest.owner.invalid");
}
#[test]
fn test_should_reject_path_traversal() {
let error = RepoRelativePath::new("../outside").expect_err("rejects traversal");
assert_eq!(error.code.as_ref(), "manifest.path.traversal");
}
#[test]
fn test_should_allow_project_root_path() {
let path = ProjectRelativePath::new(".").expect("root is valid");
assert_eq!(path.as_str(), ".");
}
#[test]
fn test_should_parse_foundation_client_dependency() {
let dependency =
ProjectDependency::parse("foundations.identity.client").expect("dependency parses");
assert_eq!(dependency.id, "foundations.identity.client");
}
}