Skip to main content

repoctl_core/
domain.rs

1//! Validated domain model and facade request/response DTOs.
2
3use std::{
4    collections::BTreeMap,
5    fmt::{Display, Formatter},
6    num::{NonZeroU32, NonZeroUsize},
7    path::PathBuf,
8    time::{SystemTime, UNIX_EPOCH},
9};
10
11use camino::{Utf8Path, Utf8PathBuf};
12use globset::Glob;
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15
16use crate::diagnostic::Diagnostic;
17
18const NAME_MAX_BYTES: usize = 128;
19const OWNER_MAX_BYTES: usize = 64;
20const TASK_MAX_BYTES: usize = 64;
21const SCHEMA_MAX_BYTES: usize = 96;
22const PATH_MAX_BYTES: usize = 512;
23const COMMAND_PART_MAX_BYTES: usize = 256;
24const COMMAND_ARG_LIMIT: usize = 64;
25
26/// Repository layout supported by repoctl.
27#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
28#[serde(rename_all = "kebab-case")]
29pub enum RepoLayout {
30    /// Functional top-level layout required for v0.2.
31    Functional,
32}
33
34/// Name of a repo.
35#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
36#[serde(transparent)]
37pub struct RepoName(String);
38
39impl RepoName {
40    /// Validates and creates a repo name.
41    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
42        let value = value.into();
43        validate_ascii_identifier("repo name", &value, NAME_MAX_BYTES, false)?;
44        Ok(Self(value))
45    }
46
47    /// Returns the repo name as a string slice.
48    pub fn as_str(&self) -> &str {
49        &self.0
50    }
51}
52
53impl Display for RepoName {
54    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
55        f.write_str(&self.0)
56    }
57}
58
59/// Globally unique project identifier.
60#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
61#[serde(transparent)]
62pub struct ProjectName(String);
63
64impl ProjectName {
65    /// Validates and creates a project name.
66    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
67        let value = value.into();
68        validate_ascii_identifier("project name", &value, NAME_MAX_BYTES, true)?;
69        Ok(Self(value))
70    }
71
72    /// Returns the project name as a string slice.
73    pub fn as_str(&self) -> &str {
74        &self.0
75    }
76}
77
78impl Display for ProjectName {
79    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
80        f.write_str(&self.0)
81    }
82}
83
84/// Workspace identifier unique within a project.
85#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
86#[serde(transparent)]
87pub struct WorkspaceName(String);
88
89impl WorkspaceName {
90    /// Validates and creates a workspace name.
91    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
92        let value = value.into();
93        validate_ascii_identifier("workspace name", &value, NAME_MAX_BYTES, false)?;
94        Ok(Self(value))
95    }
96
97    /// Returns the workspace name as a string slice.
98    pub fn as_str(&self) -> &str {
99        &self.0
100    }
101}
102
103impl Display for WorkspaceName {
104    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
105        f.write_str(&self.0)
106    }
107}
108
109/// Owner handle such as `@platform`.
110#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
111#[serde(transparent)]
112pub struct OwnerHandle(String);
113
114impl OwnerHandle {
115    /// Validates and creates an owner handle.
116    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
117        let value = value.into();
118        if !value.starts_with('@') {
119            return Err(Diagnostic::error(
120                "manifest.owner.invalid",
121                "owner handle must start with @",
122            ));
123        }
124        validate_ascii_identifier("owner handle", &value[1..], OWNER_MAX_BYTES - 1, false)?;
125        Ok(Self(value))
126    }
127
128    /// Returns the owner handle as a string slice.
129    pub fn as_str(&self) -> &str {
130        &self.0
131    }
132}
133
134impl Display for OwnerHandle {
135    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
136        f.write_str(&self.0)
137    }
138}
139
140/// Task identifier unique within a project manifest.
141#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
142#[serde(transparent)]
143pub struct TaskName(String);
144
145impl TaskName {
146    /// Validates and creates a task name.
147    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
148        let value = value.into();
149        validate_ascii_identifier("task name", &value, TASK_MAX_BYTES, false)?;
150        Ok(Self(value))
151    }
152
153    /// Returns the task name as a string slice.
154    pub fn as_str(&self) -> &str {
155        &self.0
156    }
157}
158
159impl Display for TaskName {
160    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
161        f.write_str(&self.0)
162    }
163}
164
165/// Manifest schema identifier.
166#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
167#[serde(transparent)]
168pub struct SchemaId(String);
169
170impl SchemaId {
171    /// Validates and creates a schema identifier.
172    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
173        let value = value.into();
174        if value.is_empty() || value.len() > SCHEMA_MAX_BYTES {
175            return Err(Diagnostic::error(
176                "manifest.schema.invalid",
177                "schema id must be non-empty and length-bounded",
178            ));
179        }
180        if !value
181            .bytes()
182            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'/' | b'-' | b'_'))
183        {
184            return Err(Diagnostic::error(
185                "manifest.schema.invalid",
186                "schema id contains unsupported characters",
187            ));
188        }
189        Ok(Self(value))
190    }
191
192    /// Returns the schema identifier as a string slice.
193    pub fn as_str(&self) -> &str {
194        &self.0
195    }
196}
197
198impl Display for SchemaId {
199    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
200        f.write_str(&self.0)
201    }
202}
203
204/// Proto package identifier.
205#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
206#[serde(transparent)]
207pub struct ProtoPackageName(String);
208
209impl ProtoPackageName {
210    /// Validates and creates a proto package name.
211    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
212        let value = value.into();
213        validate_ascii_identifier("proto package", &value, NAME_MAX_BYTES, true)?;
214        Ok(Self(value))
215    }
216
217    /// Returns the proto package name as a string slice.
218    pub fn as_str(&self) -> &str {
219        &self.0
220    }
221}
222
223impl Display for ProtoPackageName {
224    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
225        f.write_str(&self.0)
226    }
227}
228
229/// Repo-relative path that cannot escape the repo root.
230#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
231#[serde(transparent)]
232pub struct RepoRelativePath(String);
233
234impl RepoRelativePath {
235    /// Validates and normalizes a repo-relative path.
236    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
237        normalize_relative_path(value.into(), false).map(Self)
238    }
239
240    /// Creates the root path `.`.
241    pub fn root() -> Self {
242        Self(".".to_string())
243    }
244
245    /// Returns the path as a string slice.
246    pub fn as_str(&self) -> &str {
247        &self.0
248    }
249
250    /// Joins a project-relative path below this repo-relative path.
251    pub fn join_project(&self, child: &ProjectRelativePath) -> Result<Self, Diagnostic> {
252        if child.as_str() == "." {
253            return Ok(self.clone());
254        }
255        let joined = if self.0 == "." {
256            child.as_str().to_string()
257        } else {
258            format!("{}/{}", self.0, child.as_str())
259        };
260        Self::new(joined)
261    }
262
263    /// Returns true if this path begins with the given repo-relative prefix.
264    pub fn starts_with(&self, prefix: &RepoRelativePath) -> bool {
265        self.0 == prefix.0 || prefix.0 == "." || self.0.starts_with(&format!("{}/", prefix.0))
266    }
267}
268
269impl Display for RepoRelativePath {
270    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
271        f.write_str(&self.0)
272    }
273}
274
275impl Default for RepoRelativePath {
276    fn default() -> Self {
277        Self::root()
278    }
279}
280
281/// Project-relative path that cannot escape its project root.
282#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
283#[serde(transparent)]
284pub struct ProjectRelativePath(String);
285
286impl ProjectRelativePath {
287    /// Validates and normalizes a project-relative path.
288    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
289        normalize_relative_path(value.into(), true).map(Self)
290    }
291
292    /// Creates the root path `.`.
293    pub fn root() -> Self {
294        Self(".".to_string())
295    }
296
297    /// Returns the path as a string slice.
298    pub fn as_str(&self) -> &str {
299        &self.0
300    }
301}
302
303impl Display for ProjectRelativePath {
304    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
305        f.write_str(&self.0)
306    }
307}
308
309/// Glob pattern scoped to repo-relative or project-relative paths.
310#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
311#[serde(transparent)]
312pub struct RepoGlob(String);
313
314impl RepoGlob {
315    /// Validates a glob pattern and rejects path traversal.
316    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
317        let value = value.into();
318        validate_path_text(&value, true)?;
319        Glob::new(&value).map_err(|source| {
320            Diagnostic::error(
321                "manifest.glob.invalid",
322                format!("invalid glob pattern `{value}`: {source}"),
323            )
324        })?;
325        Ok(Self(value))
326    }
327
328    /// Returns the glob as a string slice.
329    pub fn as_str(&self) -> &str {
330        &self.0
331    }
332}
333
334impl Display for RepoGlob {
335    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
336        f.write_str(&self.0)
337    }
338}
339
340/// Absolute repository root.
341#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
342#[serde(rename_all = "camelCase")]
343pub struct RepoRoot {
344    /// Absolute UTF-8 path to the repository root.
345    pub absolute: Utf8PathBuf,
346}
347
348impl RepoRoot {
349    /// Creates a repository root from an absolute UTF-8 path.
350    pub fn new(path: Utf8PathBuf) -> Result<Self, Diagnostic> {
351        if !Utf8Path::new(path.as_str()).is_absolute() {
352            return Err(Diagnostic::error(
353                "repo.root.relative",
354                "repo root must be an absolute path",
355            ));
356        }
357        Ok(Self { absolute: path })
358    }
359
360    /// Joins a repo-relative path against the absolute root.
361    pub fn join(&self, path: &RepoRelativePath) -> Utf8PathBuf {
362        if path.as_str() == "." {
363            self.absolute.clone()
364        } else {
365            self.absolute.join(path.as_str())
366        }
367    }
368}
369
370/// Project kind.
371#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
372#[serde(rename_all = "kebab-case")]
373pub enum ProjectKind {
374    /// Product or service application boundary.
375    App,
376    /// Shared capability extracted behind public facades.
377    Framework,
378    /// Company-level foundation service.
379    FoundationService,
380    /// Central proto source ownership project.
381    ProtoRoot,
382    /// Shared infrastructure baseline.
383    CoreInfra,
384    /// Independently owned component under the core infrastructure lane.
385    CoreInfraComponent,
386    /// Developer, agent, or repository automation tooling.
387    Tool,
388}
389
390/// Workspace execution toolchain.
391#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
392#[serde(rename_all = "kebab-case")]
393pub enum Toolchain {
394    /// Cargo toolchain.
395    Cargo,
396    /// npm toolchain.
397    Npm,
398    /// pnpm toolchain.
399    Pnpm,
400    /// Yarn toolchain.
401    Yarn,
402    /// Bun toolchain.
403    Bun,
404    /// uv toolchain.
405    Uv,
406    /// Custom toolchain name.
407    Custom(String),
408}
409
410impl Toolchain {
411    /// Parses a manifest toolchain value.
412    pub fn parse(value: &str) -> Result<Self, Diagnostic> {
413        if value.is_empty() || value.len() > NAME_MAX_BYTES {
414            return Err(Diagnostic::error(
415                "manifest.workspace.toolchain",
416                "toolchain must be non-empty and length-bounded",
417            ));
418        }
419        if !value
420            .bytes()
421            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
422        {
423            return Err(Diagnostic::error(
424                "manifest.workspace.toolchain",
425                "toolchain contains unsupported characters",
426            ));
427        }
428        Ok(match value {
429            "cargo" => Self::Cargo,
430            "npm" => Self::Npm,
431            "pnpm" => Self::Pnpm,
432            "yarn" => Self::Yarn,
433            "bun" => Self::Bun,
434            "uv" => Self::Uv,
435            custom => Self::Custom(custom.to_string()),
436        })
437    }
438
439    /// Returns the manifest representation.
440    pub fn as_str(&self) -> &str {
441        match self {
442            Self::Cargo => "cargo",
443            Self::Npm => "npm",
444            Self::Pnpm => "pnpm",
445            Self::Yarn => "yarn",
446            Self::Bun => "bun",
447            Self::Uv => "uv",
448            Self::Custom(value) => value.as_str(),
449        }
450    }
451}
452
453/// Workspace language.
454#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
455#[serde(rename_all = "kebab-case")]
456pub enum WorkspaceLanguage {
457    /// Rust workspace.
458    Rust,
459    /// TypeScript or JavaScript workspace.
460    #[serde(rename = "typescript")]
461    TypeScript,
462    /// Python workspace.
463    Python,
464    /// Protocol buffer workspace.
465    Proto,
466    /// Infrastructure workspace.
467    Iac,
468}
469
470/// Project visibility.
471#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
472#[serde(rename_all = "kebab-case")]
473pub enum Visibility {
474    /// Internal project visibility.
475    #[default]
476    Internal,
477    /// Public project visibility.
478    Public,
479}
480
481/// `IaC` provider.
482#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
483#[serde(rename_all = "kebab-case")]
484pub enum IacProvider {
485    /// Pulumi provider.
486    Pulumi,
487    /// Terraform provider.
488    Terraform,
489    /// `OpenTofu` provider.
490    #[serde(rename = "opentofu")]
491    OpenTofu,
492}
493
494/// Generated-code placement policy.
495#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
496#[serde(rename_all = "kebab-case")]
497pub enum GeneratedCodePolicy {
498    /// Generated code lives under each consumer project.
499    #[default]
500    ConsumerLocal,
501}
502
503/// Validated repo-level manifest.
504#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
505#[serde(rename_all = "camelCase")]
506pub struct RepoManifest {
507    /// Manifest schema id.
508    pub schema: SchemaId,
509    /// Repository name.
510    pub name: RepoName,
511    /// Required repository layout.
512    pub layout: RepoLayout,
513    /// Default owner used when project manifests omit owners.
514    pub default_owner: Option<OwnerHandle>,
515    /// Proto source root.
516    pub protos_root: RepoRelativePath,
517    /// Core infrastructure root.
518    pub core_infra_root: RepoRelativePath,
519    /// Agent skills root.
520    pub agent_skills_root: RepoRelativePath,
521    /// Claude skills root.
522    pub claude_skills_root: RepoRelativePath,
523    /// Context output root.
524    pub context_output: RepoRelativePath,
525    /// Generated-code policy.
526    pub generated_code_policy: GeneratedCodePolicy,
527    /// Global policy settings.
528    pub policies: RepoPolicySet,
529}
530
531/// Repo-level policy configuration.
532#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
533#[serde(rename_all = "camelCase")]
534pub struct RepoPolicySet {
535    /// Cross-app dependency policy mode.
536    pub cross_app_dependency: PolicyMode,
537    /// Framework internal dependency policy mode.
538    pub framework_internal_dependency: PolicyMode,
539    /// Generated-code direct-edit policy mode.
540    pub generated_code_direct_edit: PolicyMode,
541    /// Required owners for production changes.
542    pub prod_change_required_owners: Vec<OwnerHandle>,
543}
544
545/// Policy enforcement mode.
546#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
547#[serde(rename_all = "kebab-case")]
548pub enum PolicyMode {
549    /// Deny the matched condition.
550    #[default]
551    Deny,
552    /// Warn on the matched condition.
553    Warn,
554    /// Allow the matched condition.
555    Allow,
556}
557
558/// Validated workspace specification.
559#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
560#[serde(rename_all = "camelCase")]
561pub struct WorkspaceSpec {
562    /// Workspace id.
563    pub name: WorkspaceName,
564    /// Workspace language.
565    pub language: WorkspaceLanguage,
566    /// Optional execution toolchain such as `npm`, `bun`, or `uv`.
567    pub toolchain: Option<Toolchain>,
568    /// Workspace root relative to the project root.
569    pub root: ProjectRelativePath,
570    /// Workspace manifest path relative to the project root.
571    pub manifest: ProjectRelativePath,
572    /// Optional lockfile path relative to the project root.
573    pub lockfile: Option<ProjectRelativePath>,
574    /// Optional repo-level target directory.
575    pub target_dir: Option<RepoRelativePath>,
576    /// Optional repo-level cache directory.
577    pub cache_dir: Option<RepoRelativePath>,
578}
579
580/// Task command declared for a workspace.
581#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
582#[serde(rename_all = "camelCase")]
583pub struct TaskCommand {
584    /// Workspace that should execute the command.
585    pub workspace: WorkspaceName,
586    /// Command in argv form.
587    pub command: CommandSpec,
588    /// Commands that must run before this command.
589    pub depends_on: Vec<TaskDependency>,
590}
591
592/// Task prerequisite edge.
593#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
594#[serde(rename_all = "camelCase")]
595pub struct TaskDependency {
596    /// Project containing the prerequisite task.
597    pub project: ProjectName,
598    /// Workspace containing the prerequisite task.
599    pub workspace: WorkspaceName,
600    /// Task to run first.
601    pub task: TaskName,
602}
603
604/// Task run plan command in argv form.
605#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
606#[serde(rename_all = "camelCase")]
607pub struct CommandSpec {
608    /// Executable program.
609    pub program: String,
610    /// Command arguments.
611    pub args: Vec<String>,
612}
613
614impl CommandSpec {
615    /// Parses a PRD-compatible command string into argv form.
616    pub fn parse(value: &str) -> Result<Self, Diagnostic> {
617        if value.trim().is_empty() || value.len() > COMMAND_PART_MAX_BYTES * COMMAND_ARG_LIMIT {
618            return Err(Diagnostic::error(
619                "manifest.command.invalid",
620                "command must be non-empty and length-bounded",
621            ));
622        }
623        reject_shell_syntax(value)?;
624        let parts = split_command(value)?;
625        if parts.is_empty() {
626            return Err(Diagnostic::error(
627                "manifest.command.invalid",
628                "command must include a program",
629            ));
630        }
631        if parts.len() > COMMAND_ARG_LIMIT {
632            return Err(Diagnostic::error(
633                "manifest.command.too_many_args",
634                "command has too many arguments",
635            ));
636        }
637        for part in &parts {
638            if part.len() > COMMAND_PART_MAX_BYTES {
639                return Err(Diagnostic::error(
640                    "manifest.command.part_too_long",
641                    "command part exceeds byte limit",
642                ));
643            }
644        }
645        let mut parts = parts.into_iter();
646        let program = parts.next().ok_or_else(|| {
647            Diagnostic::error("manifest.command.invalid", "command must include a program")
648        })?;
649        Ok(Self {
650            program,
651            args: parts.collect(),
652        })
653    }
654}
655
656/// Dependency target surface requested by a manifest or discovered adapter.
657#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
658#[serde(rename_all = "kebab-case")]
659pub enum DependencySurface {
660    /// No specific surface was requested.
661    Unspecified,
662    /// Public facade of a framework.
663    FrameworkFacade,
664    /// Internal area of a framework.
665    FrameworkInternal,
666    /// Public client of a foundation service.
667    FoundationPublicClient,
668    /// Internal area of a foundation service.
669    FoundationInternal,
670    /// Public module of core infrastructure.
671    CoreInfraPublicModule,
672    /// Internal module of core infrastructure.
673    CoreInfraInternalModule,
674}
675
676/// Dependency target.
677#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
678#[serde(rename_all = "camelCase")]
679pub enum DependencyTarget {
680    /// Dependency points to a project.
681    Project(ProjectName),
682    /// Dependency points to a proto package.
683    ProtoPackage(ProtoPackageName),
684}
685
686/// Validated dependency declaration.
687#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
688#[serde(rename_all = "camelCase")]
689pub struct ProjectDependency {
690    /// Raw dependency id as written in the manifest.
691    pub id: String,
692    /// Resolved target.
693    pub target: DependencyTarget,
694    /// Requested dependency surface.
695    pub surface: DependencySurface,
696}
697
698impl ProjectDependency {
699    /// Parses a dependency declaration.
700    pub fn parse(value: impl Into<String>) -> Result<Self, Diagnostic> {
701        let value = value.into();
702        if value.starts_with("protos.") {
703            return Ok(Self {
704                target: DependencyTarget::ProtoPackage(ProtoPackageName::new(value.clone())?),
705                id: value,
706                surface: DependencySurface::Unspecified,
707            });
708        }
709        let (target, surface) = if let Some(stripped) = value.strip_suffix(".client") {
710            (
711                ProjectName::new(stripped.to_string())?,
712                DependencySurface::FoundationPublicClient,
713            )
714        } else if let Some(stripped) = value.strip_suffix(".internal") {
715            let surface = if stripped.starts_with("frameworks.") {
716                DependencySurface::FrameworkInternal
717            } else if stripped.starts_with("foundations.") {
718                DependencySurface::FoundationInternal
719            } else {
720                DependencySurface::Unspecified
721            };
722            (ProjectName::new(stripped.to_string())?, surface)
723        } else if let Some(stripped) = value.strip_suffix(".facade") {
724            (
725                ProjectName::new(stripped.to_string())?,
726                DependencySurface::FrameworkFacade,
727            )
728        } else {
729            (
730                ProjectName::new(value.clone())?,
731                DependencySurface::Unspecified,
732            )
733        };
734        Ok(Self {
735            id: value,
736            target: DependencyTarget::Project(target),
737            surface,
738        })
739    }
740}
741
742/// Project proto ownership and consumption specification.
743#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
744#[serde(rename_all = "camelCase")]
745pub struct ProjectProtoSpec {
746    /// Proto glob patterns owned by the project.
747    pub owns: Vec<RepoGlob>,
748    /// Proto glob patterns consumed by the project.
749    pub consumes: Vec<RepoGlob>,
750}
751
752/// Project `IaC` specification.
753#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
754#[serde(rename_all = "camelCase")]
755pub struct IacSpec {
756    /// `IaC` root relative to the project root.
757    pub root: ProjectRelativePath,
758    /// `IaC` provider.
759    pub provider: IacProvider,
760    /// Declared stack names.
761    pub stacks: Vec<String>,
762}
763
764/// Project deployment specification.
765#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
766#[serde(rename_all = "camelCase")]
767pub struct DeploySpec {
768    /// Deploy root relative to the project root.
769    pub root: ProjectRelativePath,
770    /// Environment names.
771    pub environments: Vec<String>,
772}
773
774/// DNS intent declared by a project manifest.
775#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
776#[serde(rename_all = "camelCase")]
777pub struct ProjectDnsSpec {
778    /// DNS provider name when known.
779    #[serde(skip_serializing_if = "Option::is_none")]
780    pub provider: Option<String>,
781    /// DNS records owned or verified by the project.
782    pub records: Vec<DnsRecordSpec>,
783}
784
785/// DNS record intent.
786#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
787#[serde(rename_all = "camelCase")]
788pub struct DnsRecordSpec {
789    /// DNS record name.
790    pub name: String,
791    /// DNS record type.
792    pub record_type: String,
793    /// Expected target value or output selector.
794    pub target: String,
795    /// Expected Cloudflare proxy setting when known.
796    #[serde(skip_serializing_if = "Option::is_none")]
797    pub proxied: Option<bool>,
798    /// Expected TTL in seconds when declared.
799    #[serde(skip_serializing_if = "Option::is_none")]
800    pub ttl: Option<u32>,
801}
802
803/// CDN intent declared by a project manifest.
804#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
805#[serde(rename_all = "camelCase")]
806pub struct CdnSpec {
807    /// CDN provider name.
808    pub provider: String,
809    /// Host aliases expected to be served by the CDN.
810    pub aliases: Vec<String>,
811    /// Expected response header patterns such as `via: *CloudFront*`.
812    pub expected_response_headers: Vec<String>,
813}
814
815/// Operations metadata declared by a project manifest.
816#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
817#[serde(rename_all = "camelCase")]
818pub struct ProjectOpsSpec {
819    /// Non-secret smoke probes.
820    pub probes: Vec<ProbeSpec>,
821    /// Runtime dependencies that should be verified with this project.
822    pub runtime_dependencies: Vec<RuntimeDependencySpec>,
823    /// Known manual state records to reconcile.
824    pub manual_state: Vec<ManualStateRecord>,
825}
826
827/// Operational smoke probe.
828#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
829#[serde(rename_all = "camelCase")]
830pub struct ProbeSpec {
831    /// Probe name.
832    pub name: String,
833    /// HTTP method.
834    pub method: String,
835    /// URL to probe.
836    pub url: String,
837    /// Expected response properties.
838    pub expect: ProbeExpectation,
839    /// Failure class assigned by verification.
840    #[serde(skip_serializing_if = "Option::is_none")]
841    pub classification: Option<String>,
842}
843
844/// Expected probe result.
845#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
846#[serde(rename_all = "camelCase")]
847pub struct ProbeExpectation {
848    /// Expected status code.
849    #[serde(skip_serializing_if = "Option::is_none")]
850    pub status: Option<u16>,
851    /// Header match patterns keyed by header name.
852    pub headers: BTreeMap<String, String>,
853    /// Body substring expected in a non-secret response.
854    #[serde(skip_serializing_if = "Option::is_none")]
855    pub body_contains: Option<String>,
856}
857
858/// Runtime dependency that should be healthy before declaring an operation complete.
859#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
860#[serde(rename_all = "camelCase")]
861pub struct RuntimeDependencySpec {
862    /// Depended-on project id.
863    pub project: ProjectName,
864    /// Dependency endpoint.
865    pub endpoint: String,
866    /// Human-readable purpose.
867    pub purpose: String,
868}
869
870/// Manual cloud or infrastructure state that must be reconciled back into `IaC`.
871#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
872#[serde(rename_all = "camelCase")]
873pub struct ManualStateRecord {
874    /// Stable manual action kind.
875    pub kind: String,
876    /// Resource identifier.
877    pub resource: String,
878    /// Current reconciliation status.
879    pub status: String,
880    /// Managed equivalent when `IaC` now owns the state.
881    #[serde(skip_serializing_if = "Option::is_none")]
882    pub managed_equivalent: Option<String>,
883    /// Cleanup command in argv form when available.
884    #[serde(skip_serializing_if = "Option::is_none")]
885    pub cleanup_command: Option<ProcessCommand>,
886}
887
888/// Project AI edit policy.
889#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
890#[serde(rename_all = "camelCase")]
891pub struct ProjectAiSpec {
892    /// Editable path globs.
893    pub editable: Vec<RepoGlob>,
894    /// Do-not-edit path globs.
895    pub do_not_edit: Vec<RepoGlob>,
896    /// Documentation paths.
897    pub docs: Vec<ProjectRelativePath>,
898}
899
900/// Public and internal project areas used for boundary classification.
901#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
902#[serde(rename_all = "camelCase")]
903pub struct ProjectAreas {
904    /// Public facade paths keyed by language.
905    pub public_facades: BTreeMap<String, Vec<ProjectRelativePath>>,
906    /// Public client paths keyed by language.
907    pub public_clients: BTreeMap<String, Vec<ProjectRelativePath>>,
908    /// Internal paths keyed by language.
909    pub internal: BTreeMap<String, Vec<ProjectRelativePath>>,
910    /// Public core-infra modules.
911    pub public_modules: Vec<ProjectRelativePath>,
912    /// Internal core-infra modules.
913    pub internal_modules: Vec<ProjectRelativePath>,
914}
915
916/// Validated project manifest.
917#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
918#[serde(rename_all = "camelCase")]
919pub struct ProjectManifest {
920    /// Manifest schema id.
921    pub schema: SchemaId,
922    /// Project id.
923    pub name: ProjectName,
924    /// Project kind.
925    pub kind: ProjectKind,
926    /// Project root path in the repository.
927    pub path: RepoRelativePath,
928    /// Project owners.
929    pub owners: Vec<OwnerHandle>,
930    /// Project visibility.
931    pub visibility: Visibility,
932    /// Workspaces declared by the project.
933    pub workspaces: Vec<WorkspaceSpec>,
934    /// Project dependencies declared in the manifest.
935    pub depends_on: Vec<ProjectDependency>,
936    /// Task definitions keyed by task name.
937    pub tasks: BTreeMap<TaskName, Vec<TaskCommand>>,
938    /// Optional `IaC` specification.
939    pub iac: Option<IacSpec>,
940    /// Optional deployment specification.
941    pub deploy: Option<DeploySpec>,
942    /// Optional DNS intent.
943    pub dns: ProjectDnsSpec,
944    /// Optional CDN intent.
945    pub cdn: Option<CdnSpec>,
946    /// Optional operations metadata.
947    pub ops: ProjectOpsSpec,
948    /// Optional proto specification.
949    pub protos: ProjectProtoSpec,
950    /// Optional AI policy specification.
951    pub ai: ProjectAiSpec,
952    /// Public and internal areas.
953    pub areas: ProjectAreas,
954    /// Project-local policy flags.
955    pub policies: BTreeMap<String, serde_json::Value>,
956    /// Source manifest path.
957    pub source: RepoRelativePath,
958}
959
960impl ProjectManifest {
961    /// Returns the project node id.
962    pub fn node_id(&self) -> String {
963        format!("project:{}", self.name)
964    }
965
966    /// Returns true when the project owns the given repo-relative path.
967    pub fn contains_path(&self, path: &RepoRelativePath) -> bool {
968        path.starts_with(&self.path)
969    }
970}
971
972/// Template manifest.
973#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
974#[serde(rename_all = "camelCase")]
975pub struct TemplateManifest {
976    /// Manifest schema id.
977    pub schema: SchemaId,
978    /// Template name.
979    pub name: String,
980    /// Template kind.
981    pub kind: String,
982    /// Template engine.
983    pub engine: String,
984    /// Template inputs.
985    pub inputs: Vec<TemplateInput>,
986    /// Template file mappings.
987    pub files: Vec<TemplateFile>,
988    /// Post-render validation commands.
989    pub post_render_validate: Vec<CommandSpec>,
990}
991
992/// Template source selector.
993#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
994#[serde(rename_all = "camelCase", tag = "kind")]
995pub enum TemplateSource {
996    /// Built-in template by stable name.
997    Builtin {
998        /// Built-in template name.
999        name: String,
1000    },
1001    /// Repository-local template root.
1002    Local {
1003        /// Repo-relative template root.
1004        root: RepoRelativePath,
1005    },
1006}
1007
1008/// Resolved template source and manifest.
1009#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1010#[serde(rename_all = "camelCase")]
1011pub struct ResolvedTemplateSource {
1012    /// Source root used to resolve template files.
1013    pub root: RepoRelativePath,
1014    /// Parsed template manifest.
1015    pub manifest: TemplateManifest,
1016}
1017
1018/// Template input declaration.
1019#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1020#[serde(rename_all = "camelCase")]
1021pub struct TemplateInput {
1022    /// Input name.
1023    pub name: String,
1024    /// Input type.
1025    pub input_type: String,
1026    /// Whether the input is required.
1027    pub required: bool,
1028    /// Optional JSON default value.
1029    pub default: Option<serde_json::Value>,
1030}
1031
1032/// Template file declaration.
1033#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1034#[serde(rename_all = "camelCase")]
1035pub struct TemplateFile {
1036    /// Template source path.
1037    pub source: ProjectRelativePath,
1038    /// Render target expression.
1039    pub target: String,
1040    /// Render mode.
1041    pub mode: String,
1042    /// Optional condition expression.
1043    pub when: Option<String>,
1044}
1045
1046/// Graph node kind.
1047#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1048#[serde(rename_all = "kebab-case")]
1049pub enum GraphNodeKind {
1050    /// Project node.
1051    Project,
1052    /// Workspace node.
1053    Workspace,
1054    /// Proto package or glob node.
1055    ProtoPackage,
1056    /// Infrastructure target node.
1057    IacTarget,
1058    /// Template node.
1059    Template,
1060}
1061
1062/// Graph node.
1063#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1064#[serde(rename_all = "camelCase")]
1065pub struct GraphNode {
1066    /// Stable graph node id.
1067    pub id: String,
1068    /// Node kind.
1069    pub kind: GraphNodeKind,
1070    /// Human-readable label.
1071    pub label: String,
1072    /// Owning project when applicable.
1073    pub project: Option<ProjectName>,
1074    /// Owning workspace when applicable.
1075    pub workspace: Option<WorkspaceName>,
1076}
1077
1078/// Graph edge kind.
1079#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1080#[serde(rename_all = "kebab-case")]
1081pub enum EdgeKind {
1082    /// Project dependency edge.
1083    DependsOnProject,
1084    /// Project contains workspace edge.
1085    ContainsWorkspace,
1086    /// Project consumes proto edge.
1087    ConsumesProto,
1088    /// Project owns proto edge.
1089    OwnsProto,
1090    /// Project uses framework facade edge.
1091    UsesFrameworkFacade,
1092    /// Project uses framework internal edge.
1093    UsesFrameworkInternal,
1094    /// Project uses foundation public client edge.
1095    UsesFoundationClient,
1096    /// Project uses foundation internal edge.
1097    UsesFoundationInternal,
1098    /// Project uses public core infrastructure module edge.
1099    UsesCoreInfraModule,
1100    /// Project uses internal core infrastructure module edge.
1101    UsesCoreInfraInternalModule,
1102    /// Project owns `IaC` target edge.
1103    OwnsIac,
1104    /// Project runs task edge.
1105    RunsTask,
1106}
1107
1108/// Graph edge.
1109#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1110#[serde(rename_all = "camelCase")]
1111pub struct GraphEdge {
1112    /// Source node id.
1113    pub from: String,
1114    /// Target node id.
1115    pub to: String,
1116    /// Edge kind.
1117    pub kind: EdgeKind,
1118    /// Evidence path or manifest value.
1119    pub evidence: Option<String>,
1120}
1121
1122/// Repository graph.
1123#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1124#[serde(rename_all = "camelCase")]
1125pub struct RepoGraph {
1126    /// Graph nodes.
1127    pub nodes: Vec<GraphNode>,
1128    /// Graph edges.
1129    pub edges: Vec<GraphEdge>,
1130}
1131
1132impl RepoGraph {
1133    /// Adds a graph node if it is not already present.
1134    pub fn add_node(&mut self, node: GraphNode) {
1135        if !self.nodes.iter().any(|existing| existing.id == node.id) {
1136            self.nodes.push(node);
1137        }
1138    }
1139
1140    /// Adds a graph edge if it is not already present.
1141    pub fn add_edge(&mut self, edge: GraphEdge) {
1142        if !self.edges.iter().any(|existing| existing == &edge) {
1143            self.edges.push(edge);
1144        }
1145    }
1146}
1147
1148/// Snapshot of the discovered repository.
1149#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1150#[serde(rename_all = "camelCase")]
1151pub struct RepoSnapshot {
1152    /// Absolute repository root.
1153    pub root: RepoRoot,
1154    /// Repo-level manifest.
1155    pub repo_manifest: RepoManifest,
1156    /// Discovered project manifests.
1157    pub projects: Vec<ProjectManifest>,
1158    /// Built repository graph.
1159    pub graph: RepoGraph,
1160    /// Generated-code policy.
1161    pub generated_policy: GeneratedCodePolicy,
1162    /// Unix timestamp in seconds when discovery completed.
1163    pub discovered_at_unix: u64,
1164}
1165
1166impl RepoSnapshot {
1167    /// Creates a repository snapshot.
1168    pub fn new(
1169        root: RepoRoot,
1170        repo_manifest: RepoManifest,
1171        projects: Vec<ProjectManifest>,
1172        graph: RepoGraph,
1173    ) -> Self {
1174        let discovered_at_unix = SystemTime::now()
1175            .duration_since(UNIX_EPOCH)
1176            .map_or(0, |duration| duration.as_secs());
1177        let generated_policy = repo_manifest.generated_code_policy.clone();
1178        Self {
1179            root,
1180            repo_manifest,
1181            projects,
1182            graph,
1183            generated_policy,
1184            discovered_at_unix,
1185        }
1186    }
1187
1188    /// Finds a project by name.
1189    pub fn project(&self, name: &ProjectName) -> Option<&ProjectManifest> {
1190        self.projects.iter().find(|project| &project.name == name)
1191    }
1192}
1193
1194/// Request to discover a repository.
1195#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1196#[serde(rename_all = "camelCase")]
1197pub struct DiscoverRequest {
1198    /// Optional starting path or explicit repo root.
1199    pub repo: Option<PathBuf>,
1200}
1201
1202/// Request to validate a graph and boundary policies.
1203#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1204#[serde(rename_all = "camelCase")]
1205pub struct GraphValidateRequest {
1206    /// Optional starting path or explicit repo root.
1207    pub repo: Option<PathBuf>,
1208    /// Changed files used by path-based policy rules.
1209    pub changed_files: Vec<RepoRelativePath>,
1210    /// Validation depth.
1211    pub mode: ValidationMode,
1212}
1213
1214/// Graph validation mode.
1215#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
1216#[serde(rename_all = "kebab-case")]
1217pub enum ValidationMode {
1218    /// Structural manifest and policy checks only.
1219    #[default]
1220    Structural,
1221    /// Structural checks plus offline metadata checks when available.
1222    Metadata,
1223    /// Metadata checks plus full task-planning/environment checks.
1224    Full,
1225}
1226
1227/// Request to print a graph.
1228#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1229#[serde(rename_all = "camelCase")]
1230pub struct GraphPrintRequest {
1231    /// Optional starting path or explicit repo root.
1232    pub repo: Option<PathBuf>,
1233}
1234
1235/// Report returned for graph printing.
1236#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1237#[serde(rename_all = "camelCase")]
1238pub struct GraphPrintReport {
1239    /// Repository snapshot.
1240    pub snapshot: RepoSnapshot,
1241}
1242
1243/// Request to explain a selector.
1244#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1245#[serde(rename_all = "camelCase")]
1246pub struct ExplainRequest {
1247    /// Optional starting path or explicit repo root.
1248    pub repo: Option<PathBuf>,
1249    /// Project name or graph node id.
1250    pub selector: String,
1251}
1252
1253/// Explanation report for a graph selector.
1254#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1255#[serde(rename_all = "camelCase")]
1256pub struct ExplainReport {
1257    /// Requested selector.
1258    pub selector: String,
1259    /// Matching graph nodes.
1260    pub nodes: Vec<GraphNode>,
1261    /// Edges touching the matching nodes.
1262    pub edges: Vec<GraphEdge>,
1263    /// Diagnostics produced while explaining.
1264    pub diagnostics: Vec<Diagnostic>,
1265}
1266
1267/// Request to lint boundaries.
1268#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1269#[serde(rename_all = "camelCase")]
1270pub struct BoundaryLintRequest {
1271    /// Optional starting path or explicit repo root.
1272    pub repo: Option<PathBuf>,
1273    /// Changed files used by path-based policy rules.
1274    pub changed_files: Vec<RepoRelativePath>,
1275}
1276
1277/// Boundary lint report.
1278#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1279#[serde(rename_all = "camelCase")]
1280pub struct BoundaryLintReport {
1281    /// Boundary diagnostics.
1282    pub diagnostics: Vec<Diagnostic>,
1283}
1284
1285/// Init profile.
1286#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1287#[serde(rename_all = "kebab-case")]
1288pub enum InitProfile {
1289    /// Startup profile.
1290    Startup,
1291    /// Enterprise profile.
1292    Enterprise,
1293}
1294
1295/// Request for repository initialization.
1296#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1297#[serde(rename_all = "camelCase")]
1298pub struct InitRequest {
1299    /// Target repo root.
1300    pub repo_root: PathBuf,
1301    /// Repository name.
1302    pub name: RepoName,
1303    /// Initialization profile.
1304    pub profile: InitProfile,
1305    /// Repository layout.
1306    pub layout: RepoLayout,
1307    /// Whether to only plan writes.
1308    pub dry_run: bool,
1309}
1310
1311/// Planned file operation.
1312#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1313#[serde(rename_all = "camelCase")]
1314pub struct FileOperation {
1315    /// Target path.
1316    pub path: RepoRelativePath,
1317    /// Operation kind.
1318    pub operation: String,
1319    /// UTF-8 content to write for file operations.
1320    #[serde(skip_serializing_if = "Option::is_none")]
1321    pub content: Option<String>,
1322}
1323
1324/// Repository initialization plan.
1325#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1326#[serde(rename_all = "camelCase")]
1327pub struct InitPlan {
1328    /// Planned operations.
1329    pub operations: Vec<FileOperation>,
1330    /// Warnings.
1331    pub warnings: Vec<Diagnostic>,
1332    /// Recommended next steps.
1333    pub next_steps: Vec<String>,
1334}
1335
1336/// Request to create a new project.
1337#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1338#[serde(rename_all = "camelCase")]
1339pub struct NewProjectRequest {
1340    /// Optional starting path or explicit repo root.
1341    pub repo: Option<PathBuf>,
1342    /// Project kind.
1343    pub kind: ProjectKind,
1344    /// Project path.
1345    pub path: RepoRelativePath,
1346    /// Requested stack entries such as `rust-api`, `bun-web`, and `uv-jobs`.
1347    pub stack: Vec<String>,
1348    /// Requested languages for framework/foundation generation.
1349    pub languages: Vec<String>,
1350    /// Requested public clients for foundation generation.
1351    pub clients: Vec<String>,
1352    /// Whether framework generation should include facade/internal areas.
1353    pub facade: bool,
1354    /// Optional `IaC` provider.
1355    pub iac: Option<IacProvider>,
1356    /// Optional proto package.
1357    pub proto: Option<ProtoPackageName>,
1358    /// Optional owner handle.
1359    pub owner: Option<OwnerHandle>,
1360    /// Whether to only plan writes.
1361    pub dry_run: bool,
1362}
1363
1364/// Render plan returned by project and template operations.
1365#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1366#[serde(rename_all = "camelCase")]
1367pub struct RenderPlan {
1368    /// Planned file operations.
1369    pub operations: Vec<FileOperation>,
1370    /// Diagnostics.
1371    pub diagnostics: Vec<Diagnostic>,
1372}
1373
1374/// Template listing request.
1375#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1376#[serde(rename_all = "camelCase")]
1377pub struct TemplateListRequest {
1378    /// Optional starting path or explicit repo root.
1379    pub repo: Option<PathBuf>,
1380}
1381
1382/// Template summary.
1383#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1384#[serde(rename_all = "camelCase")]
1385pub struct TemplateSummary {
1386    /// Template source label.
1387    pub source: String,
1388    /// Template name.
1389    pub name: String,
1390    /// Template kind.
1391    pub kind: String,
1392}
1393
1394/// Template listing report.
1395#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1396#[serde(rename_all = "camelCase")]
1397pub struct TemplateListReport {
1398    /// Available templates.
1399    pub templates: Vec<TemplateSummary>,
1400    /// Diagnostics.
1401    pub diagnostics: Vec<Diagnostic>,
1402}
1403
1404/// Template render request.
1405#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1406#[serde(rename_all = "camelCase")]
1407pub struct TemplateRenderRequest {
1408    /// Optional starting path or explicit repo root.
1409    pub repo: Option<PathBuf>,
1410    /// Source to render.
1411    pub source: TemplateSource,
1412    /// JSON input values.
1413    pub inputs: serde_json::Value,
1414    /// Whether to only plan writes.
1415    pub dry_run: bool,
1416}
1417
1418/// Request for affected analysis.
1419#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1420#[serde(rename_all = "camelCase")]
1421pub struct AffectedRequest {
1422    /// Optional starting path or explicit repo root.
1423    pub repo: Option<PathBuf>,
1424    /// Optional base git ref.
1425    pub base: Option<String>,
1426    /// Optional head git ref.
1427    pub head: Option<String>,
1428    /// Changed files.
1429    pub changed_files: Vec<RepoRelativePath>,
1430    /// Requested task names for affected reports.
1431    pub tasks: Vec<TaskName>,
1432}
1433
1434/// Affected analysis report.
1435#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1436#[serde(rename_all = "camelCase")]
1437pub struct AffectedReport {
1438    /// Directly affected project ids.
1439    pub directly_affected: Vec<ProjectName>,
1440    /// Transitively affected project ids.
1441    pub transitively_affected: Vec<ProjectName>,
1442    /// Affected workspaces in `project:workspace` form.
1443    pub workspaces: Vec<String>,
1444    /// Affected task ids in `project:workspace:task` form.
1445    pub tasks: Vec<String>,
1446    /// Risk flags emitted by affected analysis.
1447    pub risk_flags: Vec<String>,
1448    /// Explainable reasons.
1449    pub reasons: Vec<AffectedReason>,
1450    /// Suggested reviewers.
1451    pub suggested_reviewers: Vec<OwnerHandle>,
1452    /// Diagnostics.
1453    pub diagnostics: Vec<Diagnostic>,
1454}
1455
1456/// Reason attached to an affected result.
1457#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1458#[serde(rename_all = "camelCase")]
1459pub struct AffectedReason {
1460    /// Changed or propagated source.
1461    pub source: String,
1462    /// Affected target.
1463    pub target: String,
1464    /// Human-readable explanation.
1465    pub reason: String,
1466}
1467
1468/// Request to run repo tasks.
1469#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1470#[serde(rename_all = "camelCase")]
1471pub struct TaskRunRequest {
1472    /// Optional starting path or explicit repo root.
1473    pub repo: Option<PathBuf>,
1474    /// Requested task names.
1475    pub tasks: Vec<TaskName>,
1476    /// Optional project filter.
1477    pub projects: Vec<ProjectName>,
1478    /// Optional workspace filter in `project:workspace` form.
1479    pub workspaces: Vec<String>,
1480    /// Run only affected tasks.
1481    pub affected: bool,
1482    /// Changed files used when `affected` is true.
1483    pub changed_files: Vec<RepoRelativePath>,
1484    /// Optional base git ref used when `affected` is true.
1485    pub base: Option<String>,
1486    /// Optional head git ref used when `affected` is true.
1487    pub head: Option<String>,
1488    /// Maximum task concurrency.
1489    pub concurrency: Option<NonZeroU32>,
1490    /// Plan tasks without executing them.
1491    pub dry_run: bool,
1492}
1493
1494/// Task run plan.
1495#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1496#[serde(rename_all = "camelCase")]
1497pub struct TaskRunPlan {
1498    /// Planned process commands.
1499    pub commands: Vec<ProcessCommand>,
1500    /// Maximum concurrency.
1501    pub concurrency: NonZeroUsize,
1502}
1503
1504/// Task run report.
1505#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1506#[serde(rename_all = "camelCase")]
1507pub struct TaskRunReport {
1508    /// Planned task commands.
1509    pub commands: Vec<ProcessCommand>,
1510    /// Command outputs.
1511    pub outputs: Vec<TaskCommandOutput>,
1512    /// Diagnostics.
1513    pub diagnostics: Vec<Diagnostic>,
1514}
1515
1516/// Output from one task command.
1517#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1518#[serde(rename_all = "camelCase")]
1519pub struct TaskCommandOutput {
1520    /// Project id.
1521    pub project: ProjectName,
1522    /// Workspace id.
1523    pub workspace: WorkspaceName,
1524    /// Task id.
1525    pub task: TaskName,
1526    /// Process output.
1527    pub output: ProcessOutput,
1528}
1529
1530/// Process command passed to a runner.
1531#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1532#[serde(rename_all = "camelCase")]
1533pub struct ProcessCommand {
1534    /// Project id.
1535    #[serde(skip_serializing_if = "Option::is_none")]
1536    pub project: Option<ProjectName>,
1537    /// Workspace id.
1538    #[serde(skip_serializing_if = "Option::is_none")]
1539    pub workspace: Option<WorkspaceName>,
1540    /// Task id.
1541    #[serde(skip_serializing_if = "Option::is_none")]
1542    pub task: Option<TaskName>,
1543    /// Working directory.
1544    pub cwd: RepoRelativePath,
1545    /// Absolute working directory for local execution.
1546    #[serde(skip_serializing_if = "Option::is_none")]
1547    pub absolute_cwd: Option<PathBuf>,
1548    /// Program.
1549    pub program: String,
1550    /// Arguments.
1551    pub args: Vec<String>,
1552    /// Environment overlay.
1553    pub env: BTreeMap<String, String>,
1554}
1555
1556/// Process output from a runner.
1557#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1558#[serde(rename_all = "camelCase")]
1559pub struct ProcessOutput {
1560    /// Exit status code.
1561    pub status: i32,
1562    /// Captured stdout.
1563    pub stdout: String,
1564    /// Captured stderr.
1565    pub stderr: String,
1566}
1567
1568/// Request for CI matrix generation.
1569#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1570#[serde(rename_all = "camelCase")]
1571pub struct CiMatrixRequest {
1572    /// Optional starting path or explicit repo root.
1573    pub repo: Option<PathBuf>,
1574    /// Requested task names.
1575    pub tasks: Vec<TaskName>,
1576    /// Changed files used for affected CI matrix generation.
1577    pub changed_files: Vec<RepoRelativePath>,
1578    /// Optional base git ref.
1579    pub base: Option<String>,
1580    /// Optional head git ref.
1581    pub head: Option<String>,
1582    /// Behavior when affected-file detection cannot select entries.
1583    pub fallback: CiFallback,
1584}
1585
1586/// CI fallback behavior.
1587#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
1588#[serde(rename_all = "kebab-case")]
1589pub enum CiFallback {
1590    /// Include all projects with requested tasks.
1591    All,
1592    /// Include no projects.
1593    #[default]
1594    None,
1595    /// Fail when no changed-file signal is available.
1596    Error,
1597}
1598
1599/// CI matrix report.
1600#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1601#[serde(rename_all = "camelCase")]
1602pub struct CiMatrixReport {
1603    /// JSON matrix entries.
1604    pub entries: Vec<serde_json::Value>,
1605    /// GitHub Actions-safe matrix object.
1606    pub github_actions: serde_json::Value,
1607}
1608
1609/// Supported CI workflow provider.
1610#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
1611#[serde(rename_all = "kebab-case")]
1612pub enum CiProvider {
1613    /// GitHub Actions.
1614    #[default]
1615    GitHubActions,
1616}
1617
1618/// Request to render a CI workflow.
1619#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1620#[serde(rename_all = "camelCase")]
1621pub struct CiWorkflowRequest {
1622    /// Optional starting path or explicit repo root.
1623    pub repo: Option<PathBuf>,
1624    /// Workflow provider.
1625    pub provider: CiProvider,
1626    /// Write the generated workflow file.
1627    pub write: bool,
1628}
1629
1630/// Rendered CI workflow.
1631#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1632#[serde(rename_all = "camelCase")]
1633pub struct CiWorkflowReport {
1634    /// Workflow file path.
1635    pub path: RepoRelativePath,
1636    /// Workflow content.
1637    pub content: String,
1638    /// Planned or applied operations.
1639    pub operations: Vec<FileOperation>,
1640    /// Diagnostics.
1641    pub diagnostics: Vec<Diagnostic>,
1642}
1643
1644/// Hygiene request.
1645#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1646#[serde(rename_all = "camelCase")]
1647pub struct HygieneCheckRequest {
1648    /// Optional starting path or explicit repo root.
1649    pub repo: Option<PathBuf>,
1650}
1651
1652/// Hygiene clean request.
1653#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1654#[serde(rename_all = "camelCase")]
1655pub struct HygieneCleanRequest {
1656    /// Optional starting path or explicit repo root.
1657    pub repo: Option<PathBuf>,
1658    /// Plan without deleting files.
1659    pub dry_run: bool,
1660}
1661
1662/// Hygiene report.
1663#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1664#[serde(rename_all = "camelCase")]
1665pub struct HygieneReport {
1666    /// Hygiene diagnostics.
1667    pub diagnostics: Vec<Diagnostic>,
1668    /// Cleanable generated-artifact operations.
1669    pub operations: Vec<FileOperation>,
1670}
1671
1672/// Dependency rewrite mode for adoption planning.
1673#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
1674#[serde(rename_all = "kebab-case")]
1675pub enum DependencyRewriteMode {
1676    /// Rewrite supported dependencies automatically.
1677    #[default]
1678    Auto,
1679    /// Do not inspect or rewrite dependencies.
1680    Off,
1681    /// Report rewrite candidates without changing copied files.
1682    ReportOnly,
1683}
1684
1685/// CI behavior for adoption planning.
1686#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
1687#[serde(rename_all = "kebab-case")]
1688pub enum AdoptionCiMode {
1689    /// Update generated CI files.
1690    #[default]
1691    Update,
1692    /// Do not generate CI changes.
1693    Off,
1694    /// Report CI changes without applying them.
1695    ReportOnly,
1696}
1697
1698/// Adoption output format.
1699#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
1700#[serde(rename_all = "kebab-case")]
1701pub enum AdoptionOutputFormat {
1702    /// Human-readable report.
1703    #[default]
1704    Human,
1705    /// JSON report.
1706    Json,
1707    /// GitHub Actions format.
1708    GitHubActions,
1709}
1710
1711/// Request to create an adoption plan.
1712#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1713#[serde(rename_all = "camelCase")]
1714pub struct AdoptionPlanRequest {
1715    /// Directory containing source repositories or a single source repository.
1716    pub source: PathBuf,
1717    /// Destination initialized monorepo.
1718    pub dest: PathBuf,
1719    /// Source names to include.
1720    pub include: Vec<String>,
1721    /// Source names to exclude.
1722    pub exclude: Vec<String>,
1723    /// Placement overrides keyed by source name.
1724    pub map: BTreeMap<String, RepoRelativePath>,
1725    /// Project-kind overrides keyed by source name.
1726    pub kind: BTreeMap<String, ProjectKind>,
1727    /// Owner overrides keyed by source name.
1728    pub owner: BTreeMap<String, OwnerHandle>,
1729    /// Dependency rewrite mode.
1730    pub rewrite_deps: DependencyRewriteMode,
1731    /// CI generation behavior.
1732    pub ci: AdoptionCiMode,
1733    /// Verification mode.
1734    pub verification: ValidationMode,
1735    /// Requested output format.
1736    pub format: AdoptionOutputFormat,
1737}
1738
1739/// Request to apply a reviewed adoption plan.
1740#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1741#[serde(rename_all = "camelCase")]
1742pub struct AdoptionApplyRequest {
1743    /// Plan JSON file.
1744    pub plan: PathBuf,
1745    /// Recompute inference before applying.
1746    pub refresh: bool,
1747}
1748
1749/// Request to verify a reviewed adoption plan.
1750#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1751#[serde(rename_all = "camelCase")]
1752pub struct AdoptionVerifyRequest {
1753    /// Plan JSON file.
1754    pub plan: PathBuf,
1755}
1756
1757/// Adoption plan.
1758#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
1759#[serde(rename_all = "camelCase")]
1760pub struct AdoptionPlan {
1761    /// Source root.
1762    pub source_root: Utf8PathBuf,
1763    /// Destination repo root.
1764    pub dest_root: Utf8PathBuf,
1765    /// Source repository decisions.
1766    pub sources: Vec<AdoptedSource>,
1767    /// Copy operations.
1768    pub operations: Vec<AdoptionFileOperation>,
1769    /// Dependency rewrite operations.
1770    pub dependency_rewrites: Vec<DependencyRewrite>,
1771    /// Synthesized manifests.
1772    pub manifest_syntheses: Vec<ProjectManifestSynthesis>,
1773    /// CI file operations.
1774    pub ci_operations: Vec<FileOperation>,
1775    /// Verification plan.
1776    pub verification: VerificationPlan,
1777    /// Diagnostics.
1778    pub diagnostics: Vec<Diagnostic>,
1779}
1780
1781/// Source repository selected for adoption.
1782#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1783#[serde(rename_all = "camelCase")]
1784pub struct AdoptedSource {
1785    /// Source repository name.
1786    pub name: String,
1787    /// Source path.
1788    pub source_path: Utf8PathBuf,
1789    /// Destination path.
1790    pub destination_path: RepoRelativePath,
1791    /// Inferred kind.
1792    pub inferred_kind: ProjectKind,
1793    /// Confidence score from 0.0 to 1.0.
1794    pub confidence: f32,
1795    /// Inference reasons.
1796    pub reasons: Vec<String>,
1797    /// Inventory snapshot.
1798    pub inventory: SourceInventory,
1799    /// Whether this source is skipped.
1800    pub skipped: bool,
1801    /// Whether placement was explicitly overridden.
1802    pub override_applied: bool,
1803}
1804
1805/// Source inventory collected before planning.
1806#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1807#[serde(rename_all = "camelCase")]
1808pub struct SourceInventory {
1809    /// Repository name.
1810    pub name: String,
1811    /// Whether a VCS marker exists.
1812    pub has_vcs: bool,
1813    /// README first heading or summary.
1814    pub readme_summary: Option<String>,
1815    /// Primary manifest paths.
1816    pub manifests: Vec<String>,
1817    /// Top-level directories.
1818    pub top_level_dirs: Vec<String>,
1819    /// Dependency package names referenced by source manifests.
1820    pub dependency_references: Vec<String>,
1821    /// Generated artifact paths found in the source.
1822    pub generated_artifacts: Vec<String>,
1823    /// Required local tools inferred from manifests and build scripts.
1824    pub required_tools: Vec<String>,
1825}
1826
1827/// Adoption file operation.
1828#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1829#[serde(rename_all = "camelCase")]
1830pub struct AdoptionFileOperation {
1831    /// Operation id.
1832    pub id: String,
1833    /// Operation kind.
1834    pub operation: String,
1835    /// Optional source file path.
1836    #[serde(skip_serializing_if = "Option::is_none")]
1837    pub source_path: Option<Utf8PathBuf>,
1838    /// Destination path relative to the destination repo root.
1839    pub destination_path: RepoRelativePath,
1840    /// Optional UTF-8 replacement content.
1841    #[serde(skip_serializing_if = "Option::is_none")]
1842    pub content: Option<String>,
1843    /// File checksum when known.
1844    #[serde(skip_serializing_if = "Option::is_none")]
1845    pub checksum: Option<String>,
1846}
1847
1848/// Dependency rewrite record.
1849#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1850#[serde(rename_all = "camelCase")]
1851pub struct DependencyRewrite {
1852    /// File being rewritten.
1853    pub file: RepoRelativePath,
1854    /// Package or crate dependency name.
1855    pub package: String,
1856    /// Original dependency value.
1857    pub from: String,
1858    /// Replacement dependency value.
1859    pub to: String,
1860    /// Dependency surface.
1861    pub surface: DependencySurface,
1862    /// Owning project.
1863    pub owner_project: ProjectName,
1864}
1865
1866/// Synthesized project manifest record.
1867#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1868#[serde(rename_all = "camelCase")]
1869pub struct ProjectManifestSynthesis {
1870    /// Source repository name.
1871    pub source: String,
1872    /// Project name.
1873    pub project: ProjectName,
1874    /// Manifest path.
1875    pub manifest_path: RepoRelativePath,
1876    /// Synthesized YAML.
1877    pub content: String,
1878}
1879
1880/// Verification plan generated for adoption.
1881#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1882#[serde(rename_all = "camelCase")]
1883pub struct VerificationPlan {
1884    /// Tool prerequisites.
1885    pub prerequisites: Vec<ToolPrerequisite>,
1886    /// Ordered commands.
1887    pub commands: Vec<ProcessCommand>,
1888}
1889
1890/// Required local tool.
1891#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1892#[serde(rename_all = "camelCase")]
1893pub struct ToolPrerequisite {
1894    /// Tool name.
1895    pub tool: String,
1896    /// Why it is needed.
1897    pub reason: String,
1898}
1899
1900/// Request for PR summary.
1901#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1902#[serde(rename_all = "camelCase")]
1903pub struct PrSummaryRequest {
1904    /// Optional starting path or explicit repo root.
1905    pub repo: Option<PathBuf>,
1906    /// Optional base git ref.
1907    pub base: Option<String>,
1908    /// Optional head git ref.
1909    pub head: Option<String>,
1910    /// Explicit changed files.
1911    pub changed_files: Vec<RepoRelativePath>,
1912}
1913
1914/// PR summary report.
1915#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1916#[serde(rename_all = "camelCase")]
1917pub struct PrSummary {
1918    /// Markdown body.
1919    pub markdown: String,
1920    /// Machine-readable impact payload.
1921    pub impact: serde_json::Value,
1922}
1923
1924/// Request for AI context.
1925#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1926#[serde(rename_all = "camelCase")]
1927pub struct AiContextRequest {
1928    /// Optional starting path or explicit repo root.
1929    pub repo: Option<PathBuf>,
1930    /// Project to build context for.
1931    pub project: ProjectName,
1932    /// Context audience.
1933    pub audience: String,
1934}
1935
1936/// AI context report.
1937#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1938#[serde(rename_all = "camelCase")]
1939pub struct AiContext {
1940    /// JSON context payload.
1941    pub payload: serde_json::Value,
1942}
1943
1944/// Generated-code check request.
1945#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1946#[serde(rename_all = "camelCase")]
1947pub struct CodegenCheckRequest {
1948    /// Optional starting path or explicit repo root.
1949    pub repo: Option<PathBuf>,
1950    /// Optional base git ref.
1951    pub base: Option<String>,
1952    /// Optional head git ref.
1953    pub head: Option<String>,
1954    /// Explicit changed files.
1955    pub changed_files: Vec<RepoRelativePath>,
1956}
1957
1958/// Generated-code check report.
1959#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1960#[serde(rename_all = "camelCase")]
1961pub struct CodegenCheckReport {
1962    /// Diagnostics.
1963    pub diagnostics: Vec<Diagnostic>,
1964}
1965
1966/// Proto facade request.
1967#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1968#[serde(rename_all = "camelCase")]
1969pub struct ProtoFacadeRequest {
1970    /// Optional starting path or explicit repo root.
1971    pub repo: Option<PathBuf>,
1972    /// Proto operation.
1973    pub operation: ProtoOperation,
1974    /// Proto path or package selector.
1975    #[serde(skip_serializing_if = "Option::is_none")]
1976    pub selector: Option<String>,
1977    /// Optional base git ref for changed-file checks.
1978    pub base: Option<String>,
1979    /// Optional head git ref for changed-file checks.
1980    pub head: Option<String>,
1981    /// Explicit changed files for generated-code policy checks.
1982    pub changed_files: Vec<RepoRelativePath>,
1983}
1984
1985/// Proto operation.
1986#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1987#[serde(rename_all = "kebab-case")]
1988pub enum ProtoOperation {
1989    /// Check proto toolchain and generated-code policy.
1990    #[default]
1991    Check,
1992    /// Find owners for a proto path.
1993    Owners,
1994    /// Find consumers for a proto path or package.
1995    Consumers,
1996}
1997
1998/// Proto facade report.
1999#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2000#[serde(rename_all = "camelCase")]
2001pub struct ProtoFacadeReport {
2002    /// Matching owner projects.
2003    pub owners: Vec<ProjectName>,
2004    /// Matching consumer projects.
2005    pub consumers: Vec<ProjectName>,
2006    /// Commands planned or executed by proto toolchains.
2007    pub commands: Vec<ProcessCommand>,
2008    /// Diagnostics.
2009    pub diagnostics: Vec<Diagnostic>,
2010}
2011
2012/// `IaC` facade request.
2013#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2014#[serde(rename_all = "camelCase")]
2015pub struct IacFacadeRequest {
2016    /// Optional starting path or explicit repo root.
2017    pub repo: Option<PathBuf>,
2018    /// Plan only affected `IaC` targets.
2019    pub affected: bool,
2020    /// Explicit project selector.
2021    #[serde(skip_serializing_if = "Option::is_none")]
2022    pub project: Option<ProjectName>,
2023    /// Environment or stack name.
2024    #[serde(skip_serializing_if = "Option::is_none")]
2025    pub env: Option<String>,
2026    /// Plan core infrastructure.
2027    pub core: bool,
2028    /// Optional base git ref for affected selection.
2029    pub base: Option<String>,
2030    /// Optional head git ref for affected selection.
2031    pub head: Option<String>,
2032    /// Explicit changed files for affected selection and risk classification.
2033    pub changed_files: Vec<RepoRelativePath>,
2034    /// Plan without executing provider commands.
2035    pub dry_run: bool,
2036}
2037
2038/// `IaC` facade report.
2039#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2040#[serde(rename_all = "camelCase")]
2041pub struct IacFacadeReport {
2042    /// Provider plan commands.
2043    pub commands: Vec<ProcessCommand>,
2044    /// Risk flags.
2045    pub risk_flags: Vec<String>,
2046    /// Diagnostics.
2047    pub diagnostics: Vec<Diagnostic>,
2048}
2049
2050/// Request for an operations plan.
2051#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2052#[serde(rename_all = "camelCase")]
2053pub struct OpsPlanRequest {
2054    /// Optional starting path or explicit repo root.
2055    pub repo: Option<PathBuf>,
2056    /// Optional base git ref.
2057    pub base: Option<String>,
2058    /// Optional head git ref.
2059    pub head: Option<String>,
2060    /// Explicit changed files.
2061    pub changed_files: Vec<RepoRelativePath>,
2062    /// Target environment names.
2063    pub environments: Vec<String>,
2064    /// Requested task names.
2065    pub tasks: Vec<TaskName>,
2066    /// Optional plan output path.
2067    #[serde(skip_serializing_if = "Option::is_none")]
2068    pub output: Option<PathBuf>,
2069}
2070
2071/// Operations plan for day-2 infrastructure work.
2072#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2073#[serde(rename_all = "camelCase")]
2074pub struct OpsPlan {
2075    /// Stable plan id.
2076    pub id: String,
2077    /// Repository root.
2078    #[serde(skip_serializing_if = "Option::is_none")]
2079    pub repo_root: Option<RepoRoot>,
2080    /// Optional base git ref.
2081    #[serde(skip_serializing_if = "Option::is_none")]
2082    pub base: Option<String>,
2083    /// Optional head git ref.
2084    #[serde(skip_serializing_if = "Option::is_none")]
2085    pub head: Option<String>,
2086    /// Planned environments.
2087    pub environments: Vec<String>,
2088    /// Affected report used to build the plan.
2089    pub affected: AffectedReport,
2090    /// Deduplicated task dry-run report.
2091    pub task_plan: TaskRunReport,
2092    /// Ordered `IaC` operations.
2093    pub iac: Vec<IacOperation>,
2094    /// DNS verification operations.
2095    pub dns: Vec<DnsOperation>,
2096    /// CDN checks.
2097    pub cdn: Vec<CdnCheck>,
2098    /// Provider capability diagnostics.
2099    pub provider_capabilities: Vec<ProviderCapabilityReport>,
2100    /// Runtime smoke probes.
2101    pub probes: Vec<ProbeSpec>,
2102    /// Manual-state reconciliation records.
2103    pub manual_reconciliation: Vec<ManualStateRecord>,
2104    /// Required environment variable names.
2105    pub required_env: Vec<String>,
2106    /// Unresolved production gaps.
2107    pub production_gaps: Vec<String>,
2108    /// Diagnostics.
2109    pub diagnostics: Vec<Diagnostic>,
2110}
2111
2112/// One `IaC` operation in an operations plan.
2113#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2114#[serde(rename_all = "camelCase")]
2115pub struct IacOperation {
2116    /// Project id when the operation belongs to a project.
2117    #[serde(skip_serializing_if = "Option::is_none")]
2118    pub project: Option<ProjectName>,
2119    /// `IaC` workspace or root.
2120    pub workspace: String,
2121    /// `IaC` provider.
2122    pub provider: IacProvider,
2123    /// Environment or stack name.
2124    pub environment: String,
2125    /// Stack name.
2126    pub stack: String,
2127    /// Non-mutating preview command.
2128    pub preview_command: ProcessCommand,
2129    /// Mutating apply command, gated and not run by default.
2130    #[serde(skip_serializing_if = "Option::is_none")]
2131    pub apply_command: Option<ProcessCommand>,
2132    /// Risk flags.
2133    pub risk: Vec<String>,
2134}
2135
2136/// DNS verification operation.
2137#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2138#[serde(rename_all = "camelCase")]
2139pub struct DnsOperation {
2140    /// Zone name inferred from the record.
2141    pub zone: String,
2142    /// DNS provider.
2143    pub provider: String,
2144    /// Record name.
2145    pub record: String,
2146    /// Expected target.
2147    pub expected_target: String,
2148    /// Expected proxied setting.
2149    #[serde(skip_serializing_if = "Option::is_none")]
2150    pub expected_proxied: Option<bool>,
2151    /// Verification commands.
2152    pub verification: Vec<ProcessCommand>,
2153}
2154
2155/// CDN serving-layer check.
2156#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2157#[serde(rename_all = "camelCase")]
2158pub struct CdnCheck {
2159    /// CDN provider.
2160    pub provider: String,
2161    /// Alias checked.
2162    pub alias: String,
2163    /// Header patterns that should prove serving layer.
2164    pub expected_response_headers: Vec<String>,
2165    /// Verification command.
2166    pub verification: ProcessCommand,
2167}
2168
2169/// Request for non-mutating operations verification.
2170#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2171#[serde(rename_all = "camelCase")]
2172pub struct OpsVerifyRequest {
2173    /// Optional starting path or explicit repo root.
2174    pub repo: Option<PathBuf>,
2175    /// Plan JSON path.
2176    pub plan: PathBuf,
2177}
2178
2179/// Operations verification report.
2180#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2181#[serde(rename_all = "camelCase")]
2182pub struct OpsVerifyReport {
2183    /// Commands that are safe to run for verification.
2184    pub commands: Vec<ProcessCommand>,
2185    /// Mutating commands intentionally skipped.
2186    pub skipped_mutating_commands: Vec<ProcessCommand>,
2187    /// Diagnostics.
2188    pub diagnostics: Vec<Diagnostic>,
2189}
2190
2191/// Request for manual-state reconciliation.
2192#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2193#[serde(rename_all = "camelCase")]
2194pub struct OpsReconcileRequest {
2195    /// Optional starting path or explicit repo root.
2196    pub repo: Option<PathBuf>,
2197    /// Plan JSON path.
2198    pub plan: PathBuf,
2199}
2200
2201/// Manual-state reconciliation report.
2202#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2203#[serde(rename_all = "camelCase")]
2204pub struct OpsReconcileReport {
2205    /// Manual state records from the plan.
2206    pub records: Vec<ManualStateRecord>,
2207    /// Cleanup commands for unreconciled temporary state.
2208    pub cleanup_commands: Vec<ProcessCommand>,
2209    /// Diagnostics.
2210    pub diagnostics: Vec<Diagnostic>,
2211}
2212
2213/// Request for provider capability diagnostics.
2214#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2215#[serde(rename_all = "camelCase")]
2216pub struct ProviderCapabilityRequest {
2217    /// Optional starting path or explicit repo root.
2218    pub repo: Option<PathBuf>,
2219    /// Workspace selector in `project:workspace` form or repo-relative path form.
2220    #[serde(skip_serializing_if = "Option::is_none")]
2221    pub workspace: Option<String>,
2222    /// Optional base git ref.
2223    pub base: Option<String>,
2224    /// Optional head git ref.
2225    pub head: Option<String>,
2226    /// Explicit changed files.
2227    pub changed_files: Vec<RepoRelativePath>,
2228}
2229
2230/// Provider capability diagnostic.
2231#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2232#[serde(rename_all = "camelCase")]
2233pub struct ProviderCapabilityReport {
2234    /// Workspace selector or path.
2235    pub workspace: String,
2236    /// Provider package name.
2237    pub package: String,
2238    /// Installed or declared package version.
2239    pub version: String,
2240    /// Resource with a known capability gap.
2241    pub resource: String,
2242    /// Field with a known capability gap.
2243    pub field: String,
2244    /// Diagnostic status.
2245    pub status: String,
2246    /// Operator advice.
2247    pub advice: String,
2248    /// Diagnostics.
2249    pub diagnostics: Vec<Diagnostic>,
2250}
2251
2252/// Local operations session journal.
2253#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2254#[serde(rename_all = "camelCase")]
2255pub struct SessionJournal {
2256    /// Session id.
2257    pub id: String,
2258    /// Human-readable session name.
2259    pub name: String,
2260    /// Associated operations plan id.
2261    #[serde(skip_serializing_if = "Option::is_none")]
2262    pub plan_id: Option<String>,
2263    /// Journal entries.
2264    pub entries: Vec<SessionEntry>,
2265}
2266
2267/// One operations session entry.
2268#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2269#[serde(rename_all = "camelCase")]
2270pub struct SessionEntry {
2271    /// Entry kind.
2272    pub kind: String,
2273    /// Unix timestamp in seconds.
2274    pub timestamp: u64,
2275    /// Redacted command text when applicable.
2276    #[serde(skip_serializing_if = "Option::is_none")]
2277    pub command: Option<String>,
2278    /// Exit status when applicable.
2279    #[serde(skip_serializing_if = "Option::is_none")]
2280    pub exit_status: Option<i32>,
2281    /// Redacted message or selected evidence.
2282    #[serde(skip_serializing_if = "Option::is_none")]
2283    pub message: Option<String>,
2284    /// Associated plan id.
2285    #[serde(skip_serializing_if = "Option::is_none")]
2286    pub plan_id: Option<String>,
2287}
2288
2289/// Journal command request.
2290#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2291#[serde(rename_all = "camelCase")]
2292pub struct OpsJournalRequest {
2293    /// Optional starting path or explicit repo root.
2294    pub repo: Option<PathBuf>,
2295    /// Journal action.
2296    pub action: OpsJournalAction,
2297}
2298
2299/// Journal action.
2300#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2301#[serde(rename_all = "camelCase", tag = "kind")]
2302pub enum OpsJournalAction {
2303    /// Start a journal.
2304    Start {
2305        /// Session name.
2306        name: String,
2307        /// Optional plan id.
2308        plan_id: Option<String>,
2309    },
2310    /// Add a command entry.
2311    AddCommand {
2312        /// Session id or name.
2313        session: String,
2314        /// Command text.
2315        command: String,
2316        /// Optional exit status.
2317        exit_status: Option<i32>,
2318    },
2319    /// Add a note entry.
2320    AddNote {
2321        /// Session id or name.
2322        session: String,
2323        /// Note kind.
2324        note_kind: String,
2325        /// Note message.
2326        message: String,
2327    },
2328    /// Render a summary.
2329    Summary {
2330        /// Session id or name.
2331        session: String,
2332    },
2333}
2334
2335/// Journal command report.
2336#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2337#[serde(rename_all = "camelCase")]
2338pub struct OpsJournalReport {
2339    /// Journal path.
2340    #[serde(skip_serializing_if = "Option::is_none")]
2341    pub path: Option<PathBuf>,
2342    /// Current journal.
2343    #[serde(skip_serializing_if = "Option::is_none")]
2344    pub journal: Option<SessionJournal>,
2345    /// Markdown summary when requested.
2346    #[serde(skip_serializing_if = "Option::is_none")]
2347    pub markdown: Option<String>,
2348    /// Diagnostics.
2349    pub diagnostics: Vec<Diagnostic>,
2350}
2351
2352/// Skills facade request.
2353#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2354#[serde(rename_all = "camelCase")]
2355pub struct SkillsFacadeRequest {
2356    /// Optional starting path or explicit repo root.
2357    pub repo: Option<PathBuf>,
2358    /// Whether to write missing or stale skills.
2359    pub sync: bool,
2360    /// Whether to only plan writes.
2361    pub dry_run: bool,
2362}
2363
2364/// Skills facade report.
2365#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2366#[serde(rename_all = "camelCase")]
2367pub struct SkillsFacadeReport {
2368    /// Diagnostics.
2369    pub diagnostics: Vec<Diagnostic>,
2370}
2371
2372/// Validates project path conventions for the supported kinds.
2373pub fn validate_project_convention(project: &ProjectManifest) -> Vec<Diagnostic> {
2374    let expected_prefix = match project.kind {
2375        ProjectKind::App => "apps/",
2376        ProjectKind::Framework => "frameworks/",
2377        ProjectKind::FoundationService => "foundations/",
2378        ProjectKind::ProtoRoot => "protos",
2379        ProjectKind::CoreInfra => "core-infra",
2380        ProjectKind::CoreInfraComponent => "core-infra/",
2381        ProjectKind::Tool => "tools/",
2382    };
2383    let valid = match project.kind {
2384        ProjectKind::ProtoRoot | ProjectKind::CoreInfra => project.path.as_str() == expected_prefix,
2385        ProjectKind::App
2386        | ProjectKind::Framework
2387        | ProjectKind::FoundationService
2388        | ProjectKind::CoreInfraComponent
2389        | ProjectKind::Tool => project.path.as_str().starts_with(expected_prefix),
2390    };
2391    if valid {
2392        Vec::new()
2393    } else {
2394        vec![
2395            Diagnostic::error(
2396                "manifest.project.path_convention",
2397                format!(
2398                    "project `{}` of kind {:?} must live under `{expected_prefix}`",
2399                    project.name, project.kind
2400                ),
2401            )
2402            .with_path(project.source.as_str()),
2403        ]
2404    }
2405}
2406
2407/// Converts a repo path to a UTF-8 path.
2408pub fn utf8_path_buf(path: PathBuf) -> Result<Utf8PathBuf, Diagnostic> {
2409    Utf8PathBuf::from_path_buf(path).map_err(|path| {
2410        Diagnostic::error(
2411            "path.non_utf8",
2412            format!("path is not valid UTF-8: {}", path.display()),
2413        )
2414    })
2415}
2416
2417fn validate_ascii_identifier(
2418    label: &str,
2419    value: &str,
2420    max_bytes: usize,
2421    allow_dot: bool,
2422) -> Result<(), Diagnostic> {
2423    if value.is_empty() || value.len() > max_bytes {
2424        return Err(Diagnostic::error(
2425            "manifest.identifier.invalid",
2426            format!("{label} must be non-empty and length-bounded"),
2427        ));
2428    }
2429    let mut previous_dot = false;
2430    for byte in value.bytes() {
2431        let is_dot = byte == b'.';
2432        let allowed = byte.is_ascii_lowercase()
2433            || byte.is_ascii_digit()
2434            || matches!(byte, b'-' | b'_')
2435            || (allow_dot && is_dot);
2436        if !allowed {
2437            return Err(Diagnostic::error(
2438                "manifest.identifier.invalid",
2439                format!("{label} contains unsupported characters"),
2440            ));
2441        }
2442        if allow_dot && is_dot && previous_dot {
2443            return Err(Diagnostic::error(
2444                "manifest.identifier.invalid",
2445                format!("{label} cannot contain consecutive dots"),
2446            ));
2447        }
2448        previous_dot = is_dot;
2449    }
2450    if allow_dot && (value.starts_with('.') || value.ends_with('.')) {
2451        return Err(Diagnostic::error(
2452            "manifest.identifier.invalid",
2453            format!("{label} cannot start or end with a dot"),
2454        ));
2455    }
2456    Ok(())
2457}
2458
2459fn normalize_relative_path(value: String, allow_root: bool) -> Result<String, Diagnostic> {
2460    validate_path_text(&value, false)?;
2461    if value == "." {
2462        return if allow_root {
2463            Ok(value)
2464        } else {
2465            Err(Diagnostic::error(
2466                "manifest.path.invalid",
2467                "repo-relative path cannot be the root path",
2468            ))
2469        };
2470    }
2471    let mut parts = Vec::new();
2472    for part in value.split('/') {
2473        if part.is_empty() || part == "." {
2474            continue;
2475        }
2476        if part == ".." {
2477            return Err(Diagnostic::error(
2478                "manifest.path.traversal",
2479                "relative path cannot contain ..",
2480            ));
2481        }
2482        parts.push(part);
2483    }
2484    if parts.is_empty() {
2485        return if allow_root {
2486            Ok(".".to_string())
2487        } else {
2488            Err(Diagnostic::error(
2489                "manifest.path.invalid",
2490                "relative path must not be empty",
2491            ))
2492        };
2493    }
2494    Ok(parts.join("/"))
2495}
2496
2497fn validate_path_text(value: &str, allow_glob: bool) -> Result<(), Diagnostic> {
2498    if value.is_empty() || value.len() > PATH_MAX_BYTES {
2499        return Err(Diagnostic::error(
2500            "manifest.path.invalid",
2501            "path must be non-empty and length-bounded",
2502        ));
2503    }
2504    if value.contains('\0') || value.contains('\\') {
2505        return Err(Diagnostic::error(
2506            "manifest.path.invalid",
2507            "path must not contain NUL bytes or platform separators",
2508        ));
2509    }
2510    if value.starts_with('/') {
2511        return Err(Diagnostic::error(
2512            "manifest.path.absolute",
2513            "path must be relative",
2514        ));
2515    }
2516    if value
2517        .split('/')
2518        .any(|part| part == ".." || (!allow_glob && part.contains('*')))
2519    {
2520        return Err(Diagnostic::error(
2521            "manifest.path.traversal",
2522            "path must not contain traversal or unsupported glob segments",
2523        ));
2524    }
2525    Ok(())
2526}
2527
2528fn reject_shell_syntax(value: &str) -> Result<(), Diagnostic> {
2529    const REJECTED: [&str; 13] = [
2530        "&&", "||", "|", ";", ">", "<", "$(", "`", "\n", "\r", "*", "?", "[",
2531    ];
2532    if let Some(token) = REJECTED.iter().find(|token| value.contains(**token)) {
2533        return Err(Diagnostic::error(
2534            "manifest.command.shell_syntax",
2535            format!("command uses shell-only syntax `{token}`"),
2536        ));
2537    }
2538    Ok(())
2539}
2540
2541fn split_command(value: &str) -> Result<Vec<String>, Diagnostic> {
2542    let mut parts = Vec::new();
2543    let mut current = String::new();
2544    let mut quote: Option<char> = None;
2545    let mut escaped = false;
2546    for character in value.chars() {
2547        if escaped {
2548            current.push(character);
2549            escaped = false;
2550            continue;
2551        }
2552        if character == '\\' {
2553            escaped = true;
2554            continue;
2555        }
2556        match quote {
2557            Some(active) if character == active => quote = None,
2558            None if character == '\'' || character == '"' => quote = Some(character),
2559            None if character.is_whitespace() => {
2560                if !current.is_empty() {
2561                    parts.push(std::mem::take(&mut current));
2562                }
2563            }
2564            Some(_) | None => current.push(character),
2565        }
2566    }
2567    if escaped || quote.is_some() {
2568        return Err(Diagnostic::error(
2569            "manifest.command.invalid",
2570            "command contains an unfinished escape or quote",
2571        ));
2572    }
2573    if !current.is_empty() {
2574        parts.push(current);
2575    }
2576    Ok(parts)
2577}
2578
2579#[cfg(test)]
2580mod tests {
2581    use super::{
2582        CommandSpec, OwnerHandle, ProjectDependency, ProjectRelativePath, RepoRelativePath,
2583    };
2584
2585    #[test]
2586    fn test_should_parse_argv_command() {
2587        let command = CommandSpec::parse("cargo check --workspace").expect("command parses");
2588        assert_eq!(command.program, "cargo");
2589        assert_eq!(command.args, ["check", "--workspace"]);
2590    }
2591
2592    #[test]
2593    fn test_should_reject_shell_syntax_in_command() {
2594        let error = CommandSpec::parse("cargo check && rm -rf target").expect_err("rejects shell");
2595        assert_eq!(error.code.as_ref(), "manifest.command.shell_syntax");
2596    }
2597
2598    #[test]
2599    fn test_should_reject_invalid_owner() {
2600        let error = OwnerHandle::new("platform").expect_err("owner requires @");
2601        assert_eq!(error.code.as_ref(), "manifest.owner.invalid");
2602    }
2603
2604    #[test]
2605    fn test_should_reject_path_traversal() {
2606        let error = RepoRelativePath::new("../outside").expect_err("rejects traversal");
2607        assert_eq!(error.code.as_ref(), "manifest.path.traversal");
2608    }
2609
2610    #[test]
2611    fn test_should_allow_project_root_path() {
2612        let path = ProjectRelativePath::new(".").expect("root is valid");
2613        assert_eq!(path.as_str(), ".");
2614    }
2615
2616    #[test]
2617    fn test_should_parse_foundation_client_dependency() {
2618        let dependency =
2619            ProjectDependency::parse("foundations.identity.client").expect("dependency parses");
2620        assert_eq!(dependency.id, "foundations.identity.client");
2621    }
2622}